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:
2025-09-30 18:42:54 -04:00
commit a657979bfd
20 changed files with 1178 additions and 0 deletions

19
.env.example Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,2 @@
# Place sound effect files here
# Example: squeak.wav, click.wav, etc.

41
main.py Normal file
View 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
View File

@@ -0,0 +1,2 @@
# Place your .vrm model file here
# Example: waifu.vrm

25
requirements.txt Normal file
View 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
View File

@@ -0,0 +1,2 @@
"""Desktop Waifu - Source Package"""
__version__ = "0.1.0"

1
src/audio/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Audio system for Desktop Waifu (TTS, STT, sound effects)"""

1
src/core/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Core functionality for Desktop Waifu"""

99
src/core/state_manager.py Normal file
View 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:]

View File

@@ -0,0 +1 @@
"""Discord bot integration for Desktop Waifu"""

79
src/discord_bot/bot.py Normal file
View 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
View File

@@ -0,0 +1 @@
"""LLM integration for Desktop Waifu"""

1
src/ui/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""UI components for Desktop Waifu"""

151
src/ui/vrm_widget.py Normal file
View 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
View 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()