commit a657979bfdff1d3fcf96844b503caa2c14213b37 Author: Dani Date: Tue Sep 30 18:42:54 2025 -0400 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 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0ada22f --- /dev/null +++ b/.env.example @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f75d15c --- /dev/null +++ b/.gitignore @@ -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 diff --git a/CURRENT_STATUS.md b/CURRENT_STATUS.md new file mode 100644 index 0000000..5e8cc35 --- /dev/null +++ b/CURRENT_STATUS.md @@ -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 diff --git a/PROJECT_PLAN.md b/PROJECT_PLAN.md new file mode 100644 index 0000000..b9137a9 --- /dev/null +++ b/PROJECT_PLAN.md @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..7845a36 --- /dev/null +++ b/README.md @@ -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 diff --git a/RESEARCH_FINDINGS.md b/RESEARCH_FINDINGS.md new file mode 100644 index 0000000..035ba27 --- /dev/null +++ b/RESEARCH_FINDINGS.md @@ -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) diff --git a/assets/sounds/.gitkeep b/assets/sounds/.gitkeep new file mode 100644 index 0000000..40f3767 --- /dev/null +++ b/assets/sounds/.gitkeep @@ -0,0 +1,2 @@ +# Place sound effect files here +# Example: squeak.wav, click.wav, etc. diff --git a/main.py b/main.py new file mode 100644 index 0000000..49c004a --- /dev/null +++ b/main.py @@ -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() diff --git a/models/.gitkeep b/models/.gitkeep new file mode 100644 index 0000000..545c5e9 --- /dev/null +++ b/models/.gitkeep @@ -0,0 +1,2 @@ +# Place your .vrm model file here +# Example: waifu.vrm diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..496ba43 --- /dev/null +++ b/requirements.txt @@ -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 diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..c03ec59 --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,2 @@ +"""Desktop Waifu - Source Package""" +__version__ = "0.1.0" diff --git a/src/audio/__init__.py b/src/audio/__init__.py new file mode 100644 index 0000000..88e8a49 --- /dev/null +++ b/src/audio/__init__.py @@ -0,0 +1 @@ +"""Audio system for Desktop Waifu (TTS, STT, sound effects)""" diff --git a/src/core/__init__.py b/src/core/__init__.py new file mode 100644 index 0000000..d2b9c92 --- /dev/null +++ b/src/core/__init__.py @@ -0,0 +1 @@ +"""Core functionality for Desktop Waifu""" diff --git a/src/core/state_manager.py b/src/core/state_manager.py new file mode 100644 index 0000000..9ef226a --- /dev/null +++ b/src/core/state_manager.py @@ -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:] diff --git a/src/discord_bot/__init__.py b/src/discord_bot/__init__.py new file mode 100644 index 0000000..d305c95 --- /dev/null +++ b/src/discord_bot/__init__.py @@ -0,0 +1 @@ +"""Discord bot integration for Desktop Waifu""" diff --git a/src/discord_bot/bot.py b/src/discord_bot/bot.py new file mode 100644 index 0000000..81d2837 --- /dev/null +++ b/src/discord_bot/bot.py @@ -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}") diff --git a/src/llm/__init__.py b/src/llm/__init__.py new file mode 100644 index 0000000..e6fbe86 --- /dev/null +++ b/src/llm/__init__.py @@ -0,0 +1 @@ +"""LLM integration for Desktop Waifu""" diff --git a/src/ui/__init__.py b/src/ui/__init__.py new file mode 100644 index 0000000..dbf0c1d --- /dev/null +++ b/src/ui/__init__.py @@ -0,0 +1 @@ +"""UI components for Desktop Waifu""" diff --git a/src/ui/vrm_widget.py b/src/ui/vrm_widget.py new file mode 100644 index 0000000..d13c075 --- /dev/null +++ b/src/ui/vrm_widget.py @@ -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 diff --git a/src/ui/waifu_window.py b/src/ui/waifu_window.py new file mode 100644 index 0000000..5c97ea3 --- /dev/null +++ b/src/ui/waifu_window.py @@ -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()