Initial commit: Desktop Waifu MVP foundation
- Project structure with modular architecture - State management system (emotion states, conversation history) - Transparent, draggable PyQt6 window - OpenGL rendering widget with placeholder cube - Discord bot framework (commands, event handlers) - Complete documentation (README, project plan, research findings) - Environment configuration template - Dependencies defined in requirements.txt MVP features working: - Transparent window appears at bottom-right - Window can be dragged around - Placeholder 3D cube renders and rotates - Emotion state changes on interaction - Event-driven state management Next steps: VRM model loading and rendering
This commit is contained in:
19
.env.example
Normal file
19
.env.example
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Discord Bot Configuration
|
||||||
|
DISCORD_BOT_TOKEN=your_bot_token_here
|
||||||
|
|
||||||
|
# LLM Configuration
|
||||||
|
LLM_API_URL=http://localhost:11434 # Ollama default
|
||||||
|
LLM_MODEL_NAME=llama2
|
||||||
|
|
||||||
|
# Waifu Configuration
|
||||||
|
WAIFU_NAME=Waifu
|
||||||
|
VRM_MODEL_PATH=./models/waifu.vrm
|
||||||
|
|
||||||
|
# Audio Configuration
|
||||||
|
TTS_VOICE_RATE=150
|
||||||
|
TTS_VOICE_VOLUME=0.9
|
||||||
|
|
||||||
|
# System Configuration
|
||||||
|
ENABLE_VOICE_INPUT=true
|
||||||
|
ENABLE_VOICE_OUTPUT=true
|
||||||
|
ENABLE_SYSTEM_ACCESS=false # Requires elevated permissions
|
54
.gitignore
vendored
Normal file
54
.gitignore
vendored
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Virtual Environment
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
.venv
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
.claude/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Models and Assets (optional - keep if you want to commit them)
|
||||||
|
models/*.vrm
|
||||||
|
assets/sounds/*.wav
|
||||||
|
assets/sounds/*.mp3
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
*.exe
|
||||||
|
*.spec
|
174
CURRENT_STATUS.md
Normal file
174
CURRENT_STATUS.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
# Current Development Status
|
||||||
|
**Last Updated:** 2025-09-30
|
||||||
|
|
||||||
|
## ✅ Completed
|
||||||
|
|
||||||
|
### Project Setup
|
||||||
|
- Project structure created with modular architecture
|
||||||
|
- Dependencies defined in `requirements.txt`
|
||||||
|
- Environment configuration (`.env.example`)
|
||||||
|
- Documentation (`README.md`, `PROJECT_PLAN.md`, `RESEARCH_FINDINGS.md`)
|
||||||
|
|
||||||
|
### Core Systems
|
||||||
|
- **State Manager** (`src/core/state_manager.py`): Event-driven state synchronization
|
||||||
|
- Emotion states
|
||||||
|
- Conversation history
|
||||||
|
- Event listeners for state changes
|
||||||
|
|
||||||
|
### UI Framework
|
||||||
|
- **Waifu Window** (`src/ui/waifu_window.py`): Main transparent window
|
||||||
|
- Frameless, transparent, always-on-top
|
||||||
|
- Drag functionality with mouse events
|
||||||
|
- Window positioning (bottom-right by default)
|
||||||
|
- Integration with state manager
|
||||||
|
|
||||||
|
- **VRM Widget** (`src/ui/vrm_widget.py`): OpenGL rendering widget
|
||||||
|
- Basic OpenGL setup with transparency support
|
||||||
|
- Placeholder rendering (rotating cube)
|
||||||
|
- Animation timer (60 FPS)
|
||||||
|
- Emotion state integration
|
||||||
|
- Ready for VRM model loading
|
||||||
|
|
||||||
|
### Discord Integration (Stub)
|
||||||
|
- **Bot Framework** (`src/discord_bot/bot.py`): Basic bot structure
|
||||||
|
- Command system (hello, status)
|
||||||
|
- Event handlers (on_ready, on_message)
|
||||||
|
- State manager integration
|
||||||
|
- Mention/DM detection
|
||||||
|
|
||||||
|
## 🔄 Next Steps (In Priority Order)
|
||||||
|
|
||||||
|
### 1. Test Basic Functionality
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Run the app (will show placeholder cube)
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected result:
|
||||||
|
- Transparent window appears at bottom-right
|
||||||
|
- Rotating colored cube visible
|
||||||
|
- Window can be dragged
|
||||||
|
- Emotion changes when grabbed (check console)
|
||||||
|
|
||||||
|
### 2. Add Your VRM Model
|
||||||
|
- Place `.vrm` file in `models/` folder
|
||||||
|
- Update `.env` with path: `VRM_MODEL_PATH=./models/your_model.vrm`
|
||||||
|
|
||||||
|
### 3. Implement VRM Loading
|
||||||
|
**File:** `src/ui/vrm_widget.py`
|
||||||
|
|
||||||
|
Tasks:
|
||||||
|
- [ ] Use `pygltflib` to parse VRM file
|
||||||
|
- [ ] Extract mesh data (vertices, normals, UVs, indices)
|
||||||
|
- [ ] Extract material data (textures, colors)
|
||||||
|
- [ ] Extract blend shape data (facial expressions)
|
||||||
|
- [ ] Load textures into OpenGL
|
||||||
|
- [ ] Create VBO/VAO for mesh rendering
|
||||||
|
|
||||||
|
**Reference:** `RESEARCH_FINDINGS.md` for VRM structure
|
||||||
|
|
||||||
|
### 4. Implement MToon Shader
|
||||||
|
**New files:** `src/rendering/shaders/`
|
||||||
|
|
||||||
|
Tasks:
|
||||||
|
- [ ] Create vertex shader (GLSL)
|
||||||
|
- [ ] Create MToon fragment shader (GLSL)
|
||||||
|
- [ ] Shader compilation and linking
|
||||||
|
- [ ] Pass material parameters to shader
|
||||||
|
- [ ] Render VRM mesh with shader
|
||||||
|
|
||||||
|
**Reference:** https://github.com/Santarh/MToon
|
||||||
|
|
||||||
|
### 5. Add Sound Effects
|
||||||
|
**New file:** `src/audio/sound_manager.py`
|
||||||
|
|
||||||
|
Tasks:
|
||||||
|
- [ ] Initialize pygame mixer
|
||||||
|
- [ ] Load sound files from `assets/sounds/`
|
||||||
|
- [ ] Play squeak on grab/drag
|
||||||
|
- [ ] Play click on double-click
|
||||||
|
- [ ] Integrate with state manager events
|
||||||
|
|
||||||
|
### 6. Create Chat Interface
|
||||||
|
**New file:** `src/ui/chat_window.py`
|
||||||
|
|
||||||
|
Tasks:
|
||||||
|
- [ ] Create chat window (QDialog or QWidget)
|
||||||
|
- [ ] Text input field
|
||||||
|
- [ ] Message history display
|
||||||
|
- [ ] Show/hide on double-click
|
||||||
|
- [ ] Integration with LLM backend
|
||||||
|
|
||||||
|
### 7. Integrate LLM
|
||||||
|
**New file:** `src/llm/llm_backend.py`
|
||||||
|
|
||||||
|
Tasks:
|
||||||
|
- [ ] Abstract LLM interface
|
||||||
|
- [ ] Ollama implementation
|
||||||
|
- [ ] llama.cpp implementation
|
||||||
|
- [ ] Message processing
|
||||||
|
- [ ] Emotion detection from responses
|
||||||
|
- [ ] Update state based on LLM output
|
||||||
|
|
||||||
|
### 8. Complete Discord Bot
|
||||||
|
**File:** `src/discord_bot/bot.py`
|
||||||
|
|
||||||
|
Tasks:
|
||||||
|
- [ ] Connect Discord bot to LLM backend
|
||||||
|
- [ ] Implement proper message responses
|
||||||
|
- [ ] Add more commands
|
||||||
|
- [ ] Sync desktop emotion with Discord messages
|
||||||
|
- [ ] Handle async communication with desktop app
|
||||||
|
|
||||||
|
### 9. Package as .exe
|
||||||
|
Tasks:
|
||||||
|
- [ ] Test with PyInstaller
|
||||||
|
- [ ] Create spec file
|
||||||
|
- [ ] Bundle assets (models, sounds)
|
||||||
|
- [ ] Test standalone .exe
|
||||||
|
|
||||||
|
## 🐛 Known Issues
|
||||||
|
None yet - basic structure is in place
|
||||||
|
|
||||||
|
## 📝 Notes
|
||||||
|
|
||||||
|
### VRM Model Requirements
|
||||||
|
- Format: `.vrm` (based on glTF 2.0)
|
||||||
|
- Should contain blend shapes for expressions
|
||||||
|
- MToon material support needed
|
||||||
|
|
||||||
|
### LLM Integration Options
|
||||||
|
1. **Ollama** (easiest): `ollama pull llama2` then use API
|
||||||
|
2. **llama.cpp**: Faster, more control
|
||||||
|
3. **Custom**: Your own model server
|
||||||
|
|
||||||
|
### Discord Bot Token
|
||||||
|
- Get from: https://discord.com/developers/applications
|
||||||
|
- Enable Message Content Intent
|
||||||
|
- Add to `.env`: `DISCORD_BOT_TOKEN=your_token_here`
|
||||||
|
|
||||||
|
## 🔗 Important Files to Review
|
||||||
|
|
||||||
|
- `PROJECT_PLAN.md`: Overall project plan and architecture
|
||||||
|
- `RESEARCH_FINDINGS.md`: Technical research on VRM rendering
|
||||||
|
- `README.md`: Setup and usage instructions
|
||||||
|
- `main.py`: Application entry point
|
||||||
|
- `src/core/state_manager.py`: Central state system
|
||||||
|
- `src/ui/vrm_widget.py`: Where VRM rendering happens
|
||||||
|
|
||||||
|
## 🎯 Testing Checklist
|
||||||
|
|
||||||
|
- [ ] Window appears and is transparent
|
||||||
|
- [ ] Window can be dragged
|
||||||
|
- [ ] Placeholder renders correctly
|
||||||
|
- [ ] Console shows emotion changes
|
||||||
|
- [ ] Install dependencies works
|
||||||
|
- [ ] .env configuration works
|
||||||
|
- [ ] (After VRM) Model loads without errors
|
||||||
|
- [ ] (After VRM) Model renders correctly
|
||||||
|
- [ ] (After sound) Sound plays on interaction
|
||||||
|
- [ ] (After LLM) Chat works
|
||||||
|
- [ ] (After Discord) Bot connects and responds
|
130
PROJECT_PLAN.md
Normal file
130
PROJECT_PLAN.md
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
# Desktop Waifu Project
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Desktop companion application controlled by LLM that can interact both on desktop and Discord.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
- **Language**: Python
|
||||||
|
- **Character Model**: VRM format
|
||||||
|
- **LLM**: Local (TBD which model)
|
||||||
|
- **Distribution**: .exe packaging
|
||||||
|
- **Platforms**: Desktop app + Discord bot
|
||||||
|
|
||||||
|
## Core Features
|
||||||
|
|
||||||
|
### Desktop Visuals
|
||||||
|
- VRM model rendering in transparent window
|
||||||
|
- Draggable character
|
||||||
|
- Sound effects on interaction (squeaks, touch sounds)
|
||||||
|
- Multiple poses/expressions controlled by LLM
|
||||||
|
- Always on top unless asked to hide
|
||||||
|
- VRM animations (TBD - see notes below)
|
||||||
|
|
||||||
|
### AI & Interaction
|
||||||
|
- Local LLM (custom/TBD)
|
||||||
|
- Memory/context persistence
|
||||||
|
- AI-chosen personality
|
||||||
|
- Always-on chat interface
|
||||||
|
- Voice input (STT)
|
||||||
|
- Voice output (TTS - local)
|
||||||
|
|
||||||
|
### Discord Integration
|
||||||
|
- Respond in servers/channels and DMs
|
||||||
|
- Proactive messaging
|
||||||
|
- Desktop-Discord state sync
|
||||||
|
|
||||||
|
### System Integration
|
||||||
|
- System access (notifications, apps, searches, etc.)
|
||||||
|
- Designed for cross-platform deployment
|
||||||
|
- Future: OS-level integration
|
||||||
|
|
||||||
|
## VRM Animation Notes
|
||||||
|
VRM models support:
|
||||||
|
- **Blend shapes** (facial expressions): smile, blink, surprised, angry, sad, etc.
|
||||||
|
- **Bone animations**: waving, pointing, head tilts, body movements
|
||||||
|
- **Presets**: if your VRM has animation clips embedded
|
||||||
|
- **IK (Inverse Kinematics)**: dynamic movements like looking at cursor
|
||||||
|
|
||||||
|
We can trigger these based on:
|
||||||
|
- LLM emotional state (happy → smile + wave)
|
||||||
|
- User interaction (grabbed → surprised expression + squeak)
|
||||||
|
- Idle states (occasional blinks, breathing animation)
|
||||||
|
- Context (thinking → hand on chin pose)
|
||||||
|
|
||||||
|
## MVP (Phase 1)
|
||||||
|
1. VRM model renders on screen
|
||||||
|
2. Transparent, draggable window
|
||||||
|
3. Basic sound on interaction
|
||||||
|
4. Simple chat interface (text only)
|
||||||
|
5. Basic LLM connection (local)
|
||||||
|
6. Simple expression changes (happy/neutral/sad)
|
||||||
|
7. **Discord bot integration** (respond in servers/DMs, basic sync with desktop)
|
||||||
|
|
||||||
|
## Post-MVP Features
|
||||||
|
- Voice I/O (STT/TTS)
|
||||||
|
- Advanced animations
|
||||||
|
- Full system integration (notifications, app control, etc.)
|
||||||
|
- Memory persistence (database)
|
||||||
|
- Proactive messaging
|
||||||
|
- .exe packaging
|
||||||
|
- Cross-platform support
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Components
|
||||||
|
1. **VRM Renderer** (PyOpenGL + VRM loader)
|
||||||
|
2. **LLM Backend** (local inference)
|
||||||
|
3. **Audio System** (TTS, STT, sound effects)
|
||||||
|
4. **Discord Client** (discord.py)
|
||||||
|
5. **State Manager** (sync between desktop/Discord)
|
||||||
|
6. **System Interface** (OS interactions)
|
||||||
|
7. **GUI Framework** (PyQt/tkinter with transparency)
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
```
|
||||||
|
User Input (voice/text/click)
|
||||||
|
↓
|
||||||
|
State Manager
|
||||||
|
↓
|
||||||
|
LLM Processing
|
||||||
|
↓
|
||||||
|
Output (animation + voice + text + actions)
|
||||||
|
↓
|
||||||
|
Desktop Display + Discord Bot
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tech Stack Candidates
|
||||||
|
|
||||||
|
### VRM Rendering
|
||||||
|
- PyVRM (if available)
|
||||||
|
- PyOpenGL + custom VRM parser
|
||||||
|
- Unity Python wrapper (heavy)
|
||||||
|
- Godot Python binding (alternative)
|
||||||
|
|
||||||
|
### LLM
|
||||||
|
- llama.cpp Python bindings
|
||||||
|
- Ollama API
|
||||||
|
- Custom model server
|
||||||
|
- Transformers library
|
||||||
|
|
||||||
|
### Voice
|
||||||
|
- **TTS**: pyttsx3, Coqui TTS, XTTS
|
||||||
|
- **STT**: Whisper (local), Vosk
|
||||||
|
|
||||||
|
### Discord
|
||||||
|
- discord.py
|
||||||
|
|
||||||
|
### Packaging
|
||||||
|
- PyInstaller
|
||||||
|
- Nuitka (better performance)
|
||||||
|
|
||||||
|
## Current Status
|
||||||
|
- Phase: Planning
|
||||||
|
- Last Updated: 2025-09-30
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
1. Research VRM rendering in Python
|
||||||
|
2. Create basic window with transparency
|
||||||
|
3. Load and display VRM model
|
||||||
|
4. Implement dragging
|
||||||
|
5. Add sound effects
|
182
README.md
Normal file
182
README.md
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
# Desktop Waifu 🎀
|
||||||
|
|
||||||
|
An AI-powered desktop companion with VRM model rendering and Discord integration.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Current (MVP)
|
||||||
|
- ✅ Transparent desktop widget
|
||||||
|
- ✅ Draggable VRM character
|
||||||
|
- ✅ Always-on-top window
|
||||||
|
- ✅ Basic state management
|
||||||
|
- ⏳ VRM model rendering (in progress)
|
||||||
|
- ⏳ Sound effects on interaction
|
||||||
|
- ⏳ Text chat interface
|
||||||
|
- ⏳ Local LLM integration
|
||||||
|
- ⏳ Expression changes based on emotion
|
||||||
|
- ⏳ Discord bot integration
|
||||||
|
|
||||||
|
### Planned
|
||||||
|
- Voice input/output (STT/TTS)
|
||||||
|
- Advanced animations
|
||||||
|
- System integration (notifications, app control)
|
||||||
|
- Memory persistence
|
||||||
|
- Proactive messaging
|
||||||
|
- Cross-platform support
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Python 3.10+
|
||||||
|
- VRM model file (`.vrm`)
|
||||||
|
- Local LLM (Ollama, llama.cpp, etc.) - optional for testing
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
1. **Clone/Download the project**
|
||||||
|
|
||||||
|
2. **Install dependencies:**
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Configure environment:**
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env with your settings
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Add your VRM model:**
|
||||||
|
- Place your `.vrm` file in the `models/` folder
|
||||||
|
- Update `VRM_MODEL_PATH` in `.env`
|
||||||
|
|
||||||
|
5. **Add sound effects (optional):**
|
||||||
|
- Place `.wav` files in `assets/sounds/`
|
||||||
|
- Examples: `squeak.wav`, `click.wav`
|
||||||
|
|
||||||
|
### Discord Setup (Optional)
|
||||||
|
|
||||||
|
1. Create a Discord bot at https://discord.com/developers/applications
|
||||||
|
2. Enable these intents:
|
||||||
|
- Message Content Intent
|
||||||
|
- Server Members Intent
|
||||||
|
3. Copy bot token to `DISCORD_BOT_TOKEN` in `.env`
|
||||||
|
4. Invite bot to your server with permissions:
|
||||||
|
- Send Messages
|
||||||
|
- Read Message History
|
||||||
|
- Use Slash Commands
|
||||||
|
|
||||||
|
### LLM Setup (Optional)
|
||||||
|
|
||||||
|
#### Using Ollama:
|
||||||
|
```bash
|
||||||
|
# Install Ollama from https://ollama.ai
|
||||||
|
ollama pull llama2
|
||||||
|
# Update .env: LLM_API_URL=http://localhost:11434
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Using llama.cpp:
|
||||||
|
```bash
|
||||||
|
pip install llama-cpp-python
|
||||||
|
# Configure model path in code
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Desktop Interactions
|
||||||
|
- **Click and drag**: Move the waifu around your screen
|
||||||
|
- **Double-click**: Open chat interface (coming soon)
|
||||||
|
- **Right-click**: Context menu (coming soon)
|
||||||
|
|
||||||
|
### Discord Commands
|
||||||
|
- `!hello`: Greet the waifu
|
||||||
|
- `!status`: Check current mood/status
|
||||||
|
- `@mention`: Talk to the waifu in any channel
|
||||||
|
- **DM**: Send direct messages
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
Waifu/
|
||||||
|
├── main.py # Entry point
|
||||||
|
├── requirements.txt # Dependencies
|
||||||
|
├── .env # Configuration (create from .env.example)
|
||||||
|
├── PROJECT_PLAN.md # Development plan
|
||||||
|
├── RESEARCH_FINDINGS.md # Technical research
|
||||||
|
├── models/ # VRM model files
|
||||||
|
│ └── .gitkeep
|
||||||
|
├── assets/
|
||||||
|
│ └── sounds/ # Sound effects
|
||||||
|
│ └── .gitkeep
|
||||||
|
└── src/
|
||||||
|
├── core/
|
||||||
|
│ └── state_manager.py # State synchronization
|
||||||
|
├── ui/
|
||||||
|
│ ├── waifu_window.py # Main window
|
||||||
|
│ └── vrm_widget.py # VRM renderer
|
||||||
|
├── discord_bot/
|
||||||
|
│ └── bot.py # Discord integration
|
||||||
|
├── llm/ # LLM integration (TODO)
|
||||||
|
└── audio/ # Audio system (TODO)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Status
|
||||||
|
|
||||||
|
### Phase 1: MVP (In Progress)
|
||||||
|
- [x] Project structure
|
||||||
|
- [x] State management system
|
||||||
|
- [x] Transparent window framework
|
||||||
|
- [x] Drag functionality
|
||||||
|
- [x] Basic OpenGL setup
|
||||||
|
- [ ] VRM model loading
|
||||||
|
- [ ] VRM rendering with MToon shader
|
||||||
|
- [ ] Sound effects
|
||||||
|
- [ ] Chat interface
|
||||||
|
- [ ] LLM integration
|
||||||
|
- [ ] Discord bot
|
||||||
|
- [ ] Expression control
|
||||||
|
|
||||||
|
### Phase 2: Enhancement (Planned)
|
||||||
|
- [ ] Voice I/O
|
||||||
|
- [ ] Advanced animations
|
||||||
|
- [ ] System integration
|
||||||
|
- [ ] Memory/database
|
||||||
|
- [ ] .exe packaging
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
- **UI**: PyQt6 with transparent windows
|
||||||
|
- **Rendering**: OpenGL via QOpenGLWidget
|
||||||
|
- **VRM Parsing**: pygltflib
|
||||||
|
- **State Management**: Custom event-driven system
|
||||||
|
- **Discord**: discord.py
|
||||||
|
- **LLM**: Flexible (Ollama, llama.cpp, custom)
|
||||||
|
|
||||||
|
### VRM Rendering
|
||||||
|
- VRM models are glTF 2.0 with extensions
|
||||||
|
- Custom MToon shader implementation needed
|
||||||
|
- Blend shapes for facial expressions
|
||||||
|
- Bone animations for gestures
|
||||||
|
|
||||||
|
See `RESEARCH_FINDINGS.md` for detailed technical research.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
This is a personal project, but suggestions and feedback are welcome!
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
TBD
|
||||||
|
|
||||||
|
## Acknowledgments
|
||||||
|
|
||||||
|
- VRM Consortium for the VRM specification
|
||||||
|
- PyQt project for the excellent GUI framework
|
||||||
|
- discord.py for Discord integration
|
105
RESEARCH_FINDINGS.md
Normal file
105
RESEARCH_FINDINGS.md
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
# VRM Rendering Research Findings
|
||||||
|
|
||||||
|
## Date: 2025-09-30
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
Need to render VRM 3D model in a transparent desktop window using Python.
|
||||||
|
|
||||||
|
## VRM Format
|
||||||
|
- VRM is based on glTF 2.0 with VRM-specific extensions
|
||||||
|
- Uses MToon shader for toon/cel-shaded rendering
|
||||||
|
- Contains blend shapes for facial expressions
|
||||||
|
- Contains bone data for animations
|
||||||
|
|
||||||
|
## Python VRM/glTF Libraries
|
||||||
|
|
||||||
|
### pygltflib (RECOMMENDED)
|
||||||
|
- **Pros**: Can parse VRM files, read extension data, well-maintained
|
||||||
|
- **Cons**: Parsing only, doesn't handle rendering
|
||||||
|
- **Use**: Load VRM data, extract meshes/materials/animations
|
||||||
|
|
||||||
|
### Alternatives
|
||||||
|
- `gltflib`: Fork of pygltflib with divergent features
|
||||||
|
- `pyrender`: Can render glTF but **doesn't support MToon shader properly** (produces holes)
|
||||||
|
|
||||||
|
## Rendering Solutions
|
||||||
|
|
||||||
|
### ✅ CHOSEN: PyQt6 + QOpenGLWidget
|
||||||
|
**Best fit for desktop widget**
|
||||||
|
|
||||||
|
**Pros:**
|
||||||
|
- Native transparent window support (`WA_TranslucentBackground`)
|
||||||
|
- Always-on-top windows (`WindowStaysOnTopHint`)
|
||||||
|
- Frameless windows (`FramelessWindowHint`)
|
||||||
|
- Direct OpenGL rendering via QOpenGLWidget
|
||||||
|
- Excellent for desktop widgets/mascots
|
||||||
|
- Can implement custom MToon shader in GLSL
|
||||||
|
|
||||||
|
**Implementation:**
|
||||||
|
```python
|
||||||
|
class VRMWidget(QOpenGLWidget):
|
||||||
|
def initializeGL(self):
|
||||||
|
# Setup OpenGL state, load shaders
|
||||||
|
def resizeGL(self, w, h):
|
||||||
|
# Handle window resize
|
||||||
|
def paintGL(self):
|
||||||
|
# Render VRM model
|
||||||
|
```
|
||||||
|
|
||||||
|
**Window Setup:**
|
||||||
|
```python
|
||||||
|
window.setAttribute(Qt.WA_TranslucentBackground)
|
||||||
|
window.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Alternative: Panda3D
|
||||||
|
**Not chosen due to:**
|
||||||
|
- Limited transparent window support
|
||||||
|
- Requires embedding in another GUI framework anyway
|
||||||
|
- More complexity for our use case
|
||||||
|
|
||||||
|
### Alternative: Blender Python API
|
||||||
|
**Not chosen due to:**
|
||||||
|
- Too heavyweight for a desktop widget
|
||||||
|
- Not designed for real-time desktop applications
|
||||||
|
|
||||||
|
## MToon Shader Implementation
|
||||||
|
Since Python libraries don't support MToon, we need to:
|
||||||
|
1. Parse VRM using pygltflib to get material properties
|
||||||
|
2. Implement custom GLSL MToon shader in OpenGL
|
||||||
|
3. Apply shader during rendering
|
||||||
|
|
||||||
|
MToon shader resources:
|
||||||
|
- Reference: https://github.com/Santarh/MToon
|
||||||
|
- Can port GLSL version to PyQt's OpenGL context
|
||||||
|
|
||||||
|
## Recommended Tech Stack
|
||||||
|
|
||||||
|
### Core
|
||||||
|
- **PyQt6**: Window management, transparency, always-on-top
|
||||||
|
- **QOpenGLWidget**: OpenGL rendering context
|
||||||
|
- **pygltflib**: VRM/glTF parsing
|
||||||
|
- **PyOpenGL**: OpenGL bindings for Python
|
||||||
|
|
||||||
|
### Audio
|
||||||
|
- **pygame**: Sound effects (squeaks, etc.)
|
||||||
|
- **pyttsx3** or **Coqui TTS**: Text-to-speech
|
||||||
|
- **whisper.cpp Python bindings**: Speech-to-text
|
||||||
|
|
||||||
|
### LLM
|
||||||
|
- **llama-cpp-python**: Local LLM inference
|
||||||
|
- **ollama-python**: Alternative if using Ollama
|
||||||
|
|
||||||
|
### Discord
|
||||||
|
- **discord.py**: Discord bot
|
||||||
|
|
||||||
|
### Packaging
|
||||||
|
- **PyInstaller** or **Nuitka**: .exe compilation
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
1. Install PyQt6, PyOpenGL, pygltflib
|
||||||
|
2. Create basic transparent window
|
||||||
|
3. Load VRM using pygltflib
|
||||||
|
4. Implement basic OpenGL renderer
|
||||||
|
5. Port MToon shader to GLSL
|
||||||
|
6. Add interactivity (drag, click)
|
2
assets/sounds/.gitkeep
Normal file
2
assets/sounds/.gitkeep
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Place sound effect files here
|
||||||
|
# Example: squeak.wav, click.wav, etc.
|
41
main.py
Normal file
41
main.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
"""
|
||||||
|
Desktop Waifu - Main Entry Point
|
||||||
|
A VRM-based AI desktop companion with Discord integration
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import asyncio
|
||||||
|
from PyQt6.QtWidgets import QApplication
|
||||||
|
from PyQt6.QtCore import Qt
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Import application modules
|
||||||
|
from src.ui.waifu_window import WaifuWindow
|
||||||
|
from src.discord_bot.bot import WaifuBot
|
||||||
|
from src.core.state_manager import StateManager
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main application entry point"""
|
||||||
|
# Create Qt Application
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
app.setApplicationName("Desktop Waifu")
|
||||||
|
|
||||||
|
# Initialize state manager (shared between desktop and Discord)
|
||||||
|
state_manager = StateManager()
|
||||||
|
|
||||||
|
# Create main window
|
||||||
|
window = WaifuWindow(state_manager)
|
||||||
|
window.show()
|
||||||
|
|
||||||
|
# Start Discord bot in background (if configured)
|
||||||
|
# TODO: Implement Discord bot integration
|
||||||
|
# discord_bot = WaifuBot(state_manager)
|
||||||
|
# asyncio.create_task(discord_bot.start())
|
||||||
|
|
||||||
|
# Run application
|
||||||
|
sys.exit(app.exec())
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
2
models/.gitkeep
Normal file
2
models/.gitkeep
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Place your .vrm model file here
|
||||||
|
# Example: waifu.vrm
|
25
requirements.txt
Normal file
25
requirements.txt
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Core GUI and Rendering
|
||||||
|
PyQt6>=6.6.0
|
||||||
|
PyOpenGL>=3.1.7
|
||||||
|
PyOpenGL-accelerate>=3.1.7
|
||||||
|
|
||||||
|
# VRM/glTF Loading
|
||||||
|
pygltflib>=1.16.1
|
||||||
|
numpy>=1.24.0
|
||||||
|
pillow>=10.0.0
|
||||||
|
|
||||||
|
# Audio
|
||||||
|
pygame>=2.5.0
|
||||||
|
pyttsx3>=2.90
|
||||||
|
|
||||||
|
# LLM (choose one or install as needed)
|
||||||
|
# llama-cpp-python>=0.2.0
|
||||||
|
# ollama>=0.1.0
|
||||||
|
|
||||||
|
# Discord
|
||||||
|
discord.py>=2.3.0
|
||||||
|
python-dotenv>=1.0.0
|
||||||
|
|
||||||
|
# Utilities
|
||||||
|
aiohttp>=3.9.0
|
||||||
|
asyncio-mqtt>=0.16.0
|
2
src/__init__.py
Normal file
2
src/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
"""Desktop Waifu - Source Package"""
|
||||||
|
__version__ = "0.1.0"
|
1
src/audio/__init__.py
Normal file
1
src/audio/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Audio system for Desktop Waifu (TTS, STT, sound effects)"""
|
1
src/core/__init__.py
Normal file
1
src/core/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Core functionality for Desktop Waifu"""
|
99
src/core/state_manager.py
Normal file
99
src/core/state_manager.py
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
"""
|
||||||
|
State Manager - Synchronizes state between desktop and Discord
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
from typing import Optional, Callable, Dict, Any
|
||||||
|
from enum import Enum
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class EmotionState(Enum):
|
||||||
|
"""Waifu emotional states"""
|
||||||
|
NEUTRAL = "neutral"
|
||||||
|
HAPPY = "happy"
|
||||||
|
SAD = "sad"
|
||||||
|
SURPRISED = "surprised"
|
||||||
|
THINKING = "thinking"
|
||||||
|
EXCITED = "excited"
|
||||||
|
ANNOYED = "annoyed"
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WaifuState:
|
||||||
|
"""Current state of the waifu"""
|
||||||
|
emotion: EmotionState = EmotionState.NEUTRAL
|
||||||
|
is_speaking: bool = False
|
||||||
|
is_listening: bool = False
|
||||||
|
current_animation: Optional[str] = None
|
||||||
|
last_interaction: Optional[datetime] = None
|
||||||
|
conversation_context: list = None
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
if self.conversation_context is None:
|
||||||
|
self.conversation_context = []
|
||||||
|
|
||||||
|
class StateManager:
|
||||||
|
"""Manages and synchronizes waifu state across components"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.state = WaifuState()
|
||||||
|
self._listeners: Dict[str, list[Callable]] = {
|
||||||
|
'emotion_change': [],
|
||||||
|
'speech_start': [],
|
||||||
|
'speech_end': [],
|
||||||
|
'interaction': [],
|
||||||
|
}
|
||||||
|
|
||||||
|
def register_listener(self, event: str, callback: Callable):
|
||||||
|
"""Register a callback for state changes"""
|
||||||
|
if event in self._listeners:
|
||||||
|
self._listeners[event].append(callback)
|
||||||
|
|
||||||
|
def unregister_listener(self, event: str, callback: Callable):
|
||||||
|
"""Unregister a callback"""
|
||||||
|
if event in self._listeners and callback in self._listeners[event]:
|
||||||
|
self._listeners[event].remove(callback)
|
||||||
|
|
||||||
|
def _notify_listeners(self, event: str, *args, **kwargs):
|
||||||
|
"""Notify all listeners of an event"""
|
||||||
|
for callback in self._listeners.get(event, []):
|
||||||
|
try:
|
||||||
|
callback(*args, **kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error in listener callback: {e}")
|
||||||
|
|
||||||
|
def set_emotion(self, emotion: EmotionState):
|
||||||
|
"""Change waifu's emotional state"""
|
||||||
|
if self.state.emotion != emotion:
|
||||||
|
old_emotion = self.state.emotion
|
||||||
|
self.state.emotion = emotion
|
||||||
|
self._notify_listeners('emotion_change', old_emotion, emotion)
|
||||||
|
|
||||||
|
def start_speaking(self):
|
||||||
|
"""Mark waifu as speaking"""
|
||||||
|
self.state.is_speaking = True
|
||||||
|
self._notify_listeners('speech_start')
|
||||||
|
|
||||||
|
def stop_speaking(self):
|
||||||
|
"""Mark waifu as not speaking"""
|
||||||
|
self.state.is_speaking = False
|
||||||
|
self._notify_listeners('speech_end')
|
||||||
|
|
||||||
|
def add_to_conversation(self, role: str, message: str):
|
||||||
|
"""Add message to conversation history"""
|
||||||
|
self.state.conversation_context.append({
|
||||||
|
'role': role,
|
||||||
|
'message': message,
|
||||||
|
'timestamp': datetime.now()
|
||||||
|
})
|
||||||
|
# Keep only last 20 messages
|
||||||
|
if len(self.state.conversation_context) > 20:
|
||||||
|
self.state.conversation_context = self.state.conversation_context[-20:]
|
||||||
|
|
||||||
|
def record_interaction(self):
|
||||||
|
"""Record that an interaction occurred"""
|
||||||
|
self.state.last_interaction = datetime.now()
|
||||||
|
self._notify_listeners('interaction')
|
||||||
|
|
||||||
|
def get_conversation_history(self, limit: int = 10) -> list:
|
||||||
|
"""Get recent conversation history"""
|
||||||
|
return self.state.conversation_context[-limit:]
|
1
src/discord_bot/__init__.py
Normal file
1
src/discord_bot/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Discord bot integration for Desktop Waifu"""
|
79
src/discord_bot/bot.py
Normal file
79
src/discord_bot/bot.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
"""
|
||||||
|
Discord Bot - Waifu Discord integration
|
||||||
|
"""
|
||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
import os
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from src.core.state_manager import StateManager, EmotionState
|
||||||
|
|
||||||
|
class WaifuBot(commands.Bot):
|
||||||
|
"""Discord bot for waifu interactions"""
|
||||||
|
|
||||||
|
def __init__(self, state_manager: StateManager):
|
||||||
|
intents = discord.Intents.default()
|
||||||
|
intents.message_content = True
|
||||||
|
intents.guilds = True
|
||||||
|
intents.dm_messages = True
|
||||||
|
|
||||||
|
super().__init__(command_prefix='!', intents=intents)
|
||||||
|
|
||||||
|
self.state_manager = state_manager
|
||||||
|
self.setup_commands()
|
||||||
|
self.setup_event_handlers()
|
||||||
|
|
||||||
|
def setup_commands(self):
|
||||||
|
"""Setup bot commands"""
|
||||||
|
|
||||||
|
@self.command(name='hello')
|
||||||
|
async def hello(ctx):
|
||||||
|
"""Say hello to the waifu"""
|
||||||
|
self.state_manager.set_emotion(EmotionState.HAPPY)
|
||||||
|
self.state_manager.record_interaction()
|
||||||
|
await ctx.send("Hello! 👋 I'm here!")
|
||||||
|
|
||||||
|
@self.command(name='status')
|
||||||
|
async def status(ctx):
|
||||||
|
"""Check waifu status"""
|
||||||
|
emotion = self.state_manager.state.emotion.value
|
||||||
|
await ctx.send(f"Current mood: {emotion}")
|
||||||
|
|
||||||
|
def setup_event_handlers(self):
|
||||||
|
"""Setup Discord event handlers"""
|
||||||
|
|
||||||
|
@self.event
|
||||||
|
async def on_ready():
|
||||||
|
print(f'{self.user} has connected to Discord!')
|
||||||
|
print(f'Bot is in {len(self.guilds)} guilds')
|
||||||
|
|
||||||
|
@self.event
|
||||||
|
async def on_message(message):
|
||||||
|
# Don't respond to self
|
||||||
|
if message.author == self.user:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if bot is mentioned or in DM
|
||||||
|
if self.user.mentioned_in(message) or isinstance(message.channel, discord.DMChannel):
|
||||||
|
# TODO: Process message with LLM
|
||||||
|
self.state_manager.add_to_conversation('user', message.content)
|
||||||
|
self.state_manager.record_interaction()
|
||||||
|
|
||||||
|
# Placeholder response
|
||||||
|
response = "I heard you! (LLM integration coming soon)"
|
||||||
|
await message.channel.send(response)
|
||||||
|
|
||||||
|
# Process commands
|
||||||
|
await self.process_commands(message)
|
||||||
|
|
||||||
|
async def start_bot(self):
|
||||||
|
"""Start the Discord bot"""
|
||||||
|
token = os.getenv('DISCORD_BOT_TOKEN')
|
||||||
|
if not token:
|
||||||
|
print("Warning: DISCORD_BOT_TOKEN not set in .env file")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self.start(token)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error starting Discord bot: {e}")
|
1
src/llm/__init__.py
Normal file
1
src/llm/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""LLM integration for Desktop Waifu"""
|
1
src/ui/__init__.py
Normal file
1
src/ui/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""UI components for Desktop Waifu"""
|
151
src/ui/vrm_widget.py
Normal file
151
src/ui/vrm_widget.py
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
"""
|
||||||
|
VRM Widget - OpenGL widget for rendering VRM models
|
||||||
|
"""
|
||||||
|
from PyQt6.QtOpenGLWidgets import QOpenGLWidget
|
||||||
|
from PyQt6.QtCore import Qt, QTimer
|
||||||
|
from PyQt6.QtGui import QSurfaceFormat
|
||||||
|
from OpenGL.GL import *
|
||||||
|
from OpenGL.GLU import *
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from src.core.state_manager import StateManager, EmotionState
|
||||||
|
|
||||||
|
class VRMWidget(QOpenGLWidget):
|
||||||
|
"""OpenGL widget for rendering VRM character model"""
|
||||||
|
|
||||||
|
def __init__(self, state_manager: StateManager, parent=None):
|
||||||
|
# Configure OpenGL format with alpha channel for transparency
|
||||||
|
fmt = QSurfaceFormat()
|
||||||
|
fmt.setVersion(2, 1) # Use OpenGL 2.1 for compatibility with fixed-function pipeline
|
||||||
|
fmt.setAlphaBufferSize(8)
|
||||||
|
QSurfaceFormat.setDefaultFormat(fmt)
|
||||||
|
|
||||||
|
super().__init__(parent)
|
||||||
|
self.state_manager = state_manager
|
||||||
|
self.vrm_model = None
|
||||||
|
self.current_emotion = EmotionState.NEUTRAL
|
||||||
|
|
||||||
|
# Setup state change listener
|
||||||
|
self.state_manager.register_listener('emotion_change', self.on_emotion_change)
|
||||||
|
|
||||||
|
# Animation timer
|
||||||
|
self.animation_timer = QTimer(self)
|
||||||
|
self.animation_timer.timeout.connect(self.update_animation)
|
||||||
|
self.animation_timer.start(16) # ~60 FPS
|
||||||
|
|
||||||
|
def initializeGL(self):
|
||||||
|
"""Initialize OpenGL state"""
|
||||||
|
# Enable depth testing
|
||||||
|
glEnable(GL_DEPTH_TEST)
|
||||||
|
|
||||||
|
# Enable blending for transparency
|
||||||
|
glEnable(GL_BLEND)
|
||||||
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
|
||||||
|
|
||||||
|
# Set clear color (transparent)
|
||||||
|
glClearColor(0.0, 0.0, 0.0, 0.0)
|
||||||
|
|
||||||
|
# TODO: Load VRM model
|
||||||
|
# self.load_vrm_model()
|
||||||
|
|
||||||
|
# TODO: Setup shaders (MToon shader)
|
||||||
|
# self.setup_shaders()
|
||||||
|
|
||||||
|
print("OpenGL initialized")
|
||||||
|
|
||||||
|
def resizeGL(self, w: int, h: int):
|
||||||
|
"""Handle window resize"""
|
||||||
|
glViewport(0, 0, w, h)
|
||||||
|
|
||||||
|
# Setup projection matrix
|
||||||
|
glMatrixMode(GL_PROJECTION)
|
||||||
|
glLoadIdentity()
|
||||||
|
aspect = w / h if h != 0 else 1
|
||||||
|
gluPerspective(45, aspect, 0.1, 100.0)
|
||||||
|
|
||||||
|
glMatrixMode(GL_MODELVIEW)
|
||||||
|
|
||||||
|
def paintGL(self):
|
||||||
|
"""Render the VRM model"""
|
||||||
|
# Clear buffers
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
|
||||||
|
|
||||||
|
# Reset modelview matrix
|
||||||
|
glLoadIdentity()
|
||||||
|
|
||||||
|
# Move camera back
|
||||||
|
glTranslatef(0.0, -0.5, -3.0)
|
||||||
|
|
||||||
|
# TODO: Render VRM model
|
||||||
|
# For now, render a placeholder
|
||||||
|
self.render_placeholder()
|
||||||
|
|
||||||
|
def render_placeholder(self):
|
||||||
|
"""Render a placeholder (cube) until VRM is loaded"""
|
||||||
|
# Rotate for animation
|
||||||
|
import time
|
||||||
|
angle = (time.time() % 4) * 90
|
||||||
|
glRotatef(angle, 0, 1, 0)
|
||||||
|
|
||||||
|
# Draw a simple cube as placeholder
|
||||||
|
glBegin(GL_QUADS)
|
||||||
|
|
||||||
|
# Front face (red)
|
||||||
|
glColor4f(1.0, 0.0, 0.0, 0.8)
|
||||||
|
glVertex3f(-0.5, -0.5, 0.5)
|
||||||
|
glVertex3f( 0.5, -0.5, 0.5)
|
||||||
|
glVertex3f( 0.5, 0.5, 0.5)
|
||||||
|
glVertex3f(-0.5, 0.5, 0.5)
|
||||||
|
|
||||||
|
# Back face (green)
|
||||||
|
glColor4f(0.0, 1.0, 0.0, 0.8)
|
||||||
|
glVertex3f(-0.5, -0.5, -0.5)
|
||||||
|
glVertex3f(-0.5, 0.5, -0.5)
|
||||||
|
glVertex3f( 0.5, 0.5, -0.5)
|
||||||
|
glVertex3f( 0.5, -0.5, -0.5)
|
||||||
|
|
||||||
|
# Other faces (blue)
|
||||||
|
glColor4f(0.0, 0.0, 1.0, 0.8)
|
||||||
|
# Top
|
||||||
|
glVertex3f(-0.5, 0.5, -0.5)
|
||||||
|
glVertex3f(-0.5, 0.5, 0.5)
|
||||||
|
glVertex3f( 0.5, 0.5, 0.5)
|
||||||
|
glVertex3f( 0.5, 0.5, -0.5)
|
||||||
|
# Bottom
|
||||||
|
glVertex3f(-0.5, -0.5, -0.5)
|
||||||
|
glVertex3f( 0.5, -0.5, -0.5)
|
||||||
|
glVertex3f( 0.5, -0.5, 0.5)
|
||||||
|
glVertex3f(-0.5, -0.5, 0.5)
|
||||||
|
|
||||||
|
# Right face (yellow)
|
||||||
|
glColor4f(1.0, 1.0, 0.0, 0.8)
|
||||||
|
glVertex3f( 0.5, -0.5, -0.5)
|
||||||
|
glVertex3f( 0.5, 0.5, -0.5)
|
||||||
|
glVertex3f( 0.5, 0.5, 0.5)
|
||||||
|
glVertex3f( 0.5, -0.5, 0.5)
|
||||||
|
|
||||||
|
# Left face (magenta)
|
||||||
|
glColor4f(1.0, 0.0, 1.0, 0.8)
|
||||||
|
glVertex3f(-0.5, -0.5, -0.5)
|
||||||
|
glVertex3f(-0.5, -0.5, 0.5)
|
||||||
|
glVertex3f(-0.5, 0.5, 0.5)
|
||||||
|
glVertex3f(-0.5, 0.5, -0.5)
|
||||||
|
|
||||||
|
glEnd()
|
||||||
|
|
||||||
|
def update_animation(self):
|
||||||
|
"""Update animation frame"""
|
||||||
|
# Trigger repaint
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def on_emotion_change(self, old_emotion: EmotionState, new_emotion: EmotionState):
|
||||||
|
"""Handle emotion state changes"""
|
||||||
|
self.current_emotion = new_emotion
|
||||||
|
# TODO: Update blend shapes/expressions based on emotion
|
||||||
|
print(f"VRM Widget: Emotion changed to {new_emotion.value}")
|
||||||
|
|
||||||
|
def load_vrm_model(self, model_path: str):
|
||||||
|
"""Load VRM model from file"""
|
||||||
|
# TODO: Implement VRM loading using pygltflib
|
||||||
|
print(f"Loading VRM model from {model_path}")
|
||||||
|
pass
|
108
src/ui/waifu_window.py
Normal file
108
src/ui/waifu_window.py
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
"""
|
||||||
|
Waifu Window - Main transparent desktop widget
|
||||||
|
"""
|
||||||
|
from PyQt6.QtWidgets import QMainWindow, QWidget, QVBoxLayout
|
||||||
|
from PyQt6.QtCore import Qt, QPoint
|
||||||
|
from PyQt6.QtGui import QPalette, QColor
|
||||||
|
|
||||||
|
from src.core.state_manager import StateManager, EmotionState
|
||||||
|
from src.ui.vrm_widget import VRMWidget
|
||||||
|
|
||||||
|
class WaifuWindow(QMainWindow):
|
||||||
|
"""Main window for desktop waifu"""
|
||||||
|
|
||||||
|
def __init__(self, state_manager: StateManager):
|
||||||
|
super().__init__()
|
||||||
|
self.state_manager = state_manager
|
||||||
|
self.dragging = False
|
||||||
|
self.drag_position = QPoint()
|
||||||
|
|
||||||
|
self.init_ui()
|
||||||
|
self.setup_event_listeners()
|
||||||
|
|
||||||
|
def init_ui(self):
|
||||||
|
"""Initialize user interface"""
|
||||||
|
# Window properties
|
||||||
|
self.setWindowTitle("Desktop Waifu")
|
||||||
|
self.setFixedSize(400, 600) # Adjust based on VRM model size
|
||||||
|
|
||||||
|
# Make window frameless and transparent
|
||||||
|
self.setWindowFlags(
|
||||||
|
Qt.WindowType.FramelessWindowHint |
|
||||||
|
Qt.WindowType.WindowStaysOnTopHint |
|
||||||
|
Qt.WindowType.Tool # Prevents taskbar icon
|
||||||
|
)
|
||||||
|
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||||
|
|
||||||
|
# Create central widget
|
||||||
|
central_widget = QWidget()
|
||||||
|
self.setCentralWidget(central_widget)
|
||||||
|
|
||||||
|
# Layout
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
central_widget.setLayout(layout)
|
||||||
|
|
||||||
|
# VRM rendering widget
|
||||||
|
self.vrm_widget = VRMWidget(self.state_manager)
|
||||||
|
layout.addWidget(self.vrm_widget)
|
||||||
|
|
||||||
|
# Position window (bottom-right corner by default)
|
||||||
|
self.position_window()
|
||||||
|
|
||||||
|
def position_window(self):
|
||||||
|
"""Position window at bottom-right of screen"""
|
||||||
|
from PyQt6.QtGui import QScreen
|
||||||
|
screen: QScreen = self.screen()
|
||||||
|
screen_geometry = screen.availableGeometry()
|
||||||
|
|
||||||
|
x = screen_geometry.width() - self.width() - 50
|
||||||
|
y = screen_geometry.height() - self.height() - 50
|
||||||
|
self.move(x, y)
|
||||||
|
|
||||||
|
def setup_event_listeners(self):
|
||||||
|
"""Setup state manager event listeners"""
|
||||||
|
self.state_manager.register_listener('emotion_change', self.on_emotion_change)
|
||||||
|
|
||||||
|
def on_emotion_change(self, old_emotion: EmotionState, new_emotion: EmotionState):
|
||||||
|
"""Handle emotion state changes"""
|
||||||
|
print(f"Emotion changed: {old_emotion.value} -> {new_emotion.value}")
|
||||||
|
# VRM widget will handle animation changes
|
||||||
|
|
||||||
|
def mousePressEvent(self, event):
|
||||||
|
"""Handle mouse press - start dragging"""
|
||||||
|
if event.button() == Qt.MouseButton.LeftButton:
|
||||||
|
self.dragging = True
|
||||||
|
self.drag_position = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
|
||||||
|
|
||||||
|
# Play squeak sound
|
||||||
|
# TODO: Implement sound effects
|
||||||
|
self.state_manager.record_interaction()
|
||||||
|
self.state_manager.set_emotion(EmotionState.SURPRISED)
|
||||||
|
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
def mouseMoveEvent(self, event):
|
||||||
|
"""Handle mouse move - dragging"""
|
||||||
|
if self.dragging:
|
||||||
|
self.move(event.globalPosition().toPoint() - self.drag_position)
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
def mouseReleaseEvent(self, event):
|
||||||
|
"""Handle mouse release - stop dragging"""
|
||||||
|
if event.button() == Qt.MouseButton.LeftButton:
|
||||||
|
self.dragging = False
|
||||||
|
|
||||||
|
# Return to neutral emotion after a moment
|
||||||
|
# TODO: Add delay before returning to neutral
|
||||||
|
self.state_manager.set_emotion(EmotionState.NEUTRAL)
|
||||||
|
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
def mouseDoubleClickEvent(self, event):
|
||||||
|
"""Handle double-click - open chat"""
|
||||||
|
if event.button() == Qt.MouseButton.LeftButton:
|
||||||
|
# TODO: Open chat interface
|
||||||
|
print("Double-click detected - open chat")
|
||||||
|
self.state_manager.record_interaction()
|
||||||
|
event.accept()
|
Reference in New Issue
Block a user