- Fixed missing client parameter in animated GIF webhook update path
- Added get_persona_avatar_urls() helper that returns bot's current Discord
avatar URL for Miku persona (always fresh, no cache lag)
- Pass avatar_url on every webhook.send() call in bipolar_mode.py,
persona_dialogue.py, and api.py so avatars always match current pfp
regardless of webhook cache state
- miku-bot: Re-add scikit-learn to requirements.txt (needed for vision color extraction)
- miku-stt: Upgrade from CUDA 12.6.2 to 12.8.1, PyTorch 2.5.1 to 2.7.1 per RealtimeSTT PR #295
- miku-stt: Use Ubuntu 24.04 with Python 3.12 (single installation, no dual Python)
- miku-stt: Add requirements-gpu-torch.txt for separate PyTorch installation
- miku-stt: Use --break-system-packages flag for Ubuntu 24.04 pip compatibility
#16 Timezone consistency — added TZ=Europe/Sofia to docker-compose.yml
so datetime.now() returns local time inside the container. Removed
the +3 hour hack from get_time_of_day(). All three time-of-day
consumers (autonomous_v1_legacy, moods, autonomous_engine) now
use the same correct local hour automatically.
#17 Decay truncation — replaced int() with round() in decay_events()
so a counter of 1 survives one more 15-minute cycle instead of
being immediately zeroed (round(0.841)=1 vs int(0.841)=0).
#20 Unpersisted rate limiter — _last_action_execution dict in
autonomous.py is now seeded from the engine's persisted
server_last_action on import, so restarts don't bypass the
30-second cooldown.
Note: #18 (dead config fields) was a false positive — autonomous_interval_minutes
IS used by the scheduler. #19 deferred to bipolar mode rework.
#10 Redundant coin flip in join_conversation — removed the 50% random
gate that doubled the V2 engine's own decision to act.
#11 Message-triggered actions skip _autonomous_paused — _check_and_act
and _check_and_react now bail out immediately when the autonomous
system is paused (voice session), matching the scheduled-tick path.
#12 Duplicate emoji dictionaries — removed MOOD_EMOJIS and
EVIL_MOOD_EMOJIS from globals.py (had different emojis from moods.py).
bipolar_mode.py and evil_mode.py now import the canonical dicts
from utils/moods.py so all code sees the same emojis.
#13 DM mood can spontaneously become 'asleep' — rotate_dm_mood() now
filters 'asleep' out of the candidate list since DMs have no
sleepy-to-asleep transition guard and no wakeup timer.
#15 Engage-user fallback misreports action type — log level raised to
WARNING with an explicit [engage_user->general] prefix so the
cooldown-triggered fallback is visible in logs.
#4 Sleep/mood desync — set_server_mood() now clears is_sleeping when
mood changes away from 'asleep', preventing ghost-sleep state.
#5 Race condition in _check_and_act — added per-guild asyncio.Lock so
overlapping ticks + message-triggered calls cannot fire concurrently.
#6 Class-level attrs on ServerConfig — sleepy_responses_left,
angry_wakeup_timer, and forced_angry_until are now proper dataclass
fields with defaults, so asdict()/from_dict() round-trip correctly.
Also strips unknown keys in from_dict() to survive schema changes.
#7 Persistence decay_factor crash — initialise decay_factor = 1.0
before the loop so empty-server or zero-downtime paths don't
raise NameError.
#8 Double record_action — removed the redundant call in
autonomous_tick_v2(); only _check_and_act records the action now.
#9 Engine mood desync — on_mood_change() is now called inside
set_server_mood() (single source of truth) and removed from 4
call-sites in api.py, moods.py, and server_manager wakeup task.
1. Momentum cliff at 10 messages (P0): The conversation momentum formula
had a discontinuity where the 10th message caused momentum to DROP from
0.9 to 0.5. Replaced with a smooth log1p curve that monotonically
increases (0→0→0.20→0.32→...→0.70→0.89→1.0 at 30 msgs).
2. Neutral keywords overriding all moods (P0): detect_mood_shift() checked
neutral early with generic keywords (okay, sure, hmm) that matched
almost any response, constantly resetting mood to neutral. Now: all
specific moods are scored by match count first (best-match wins),
neutral is only checked as fallback and requires 2+ keyword matches.
3. Uncancellable delayed_wakeup tasks (P0): Fire-and-forget sleep tasks
could stack and overwrite mood state after manual wake-up. Added a
centralized wakeup task registry in ServerManager with automatic
cancellation on manual wake or new sleep cycle.
- Added manual_trigger parameter to /autonomous/engage endpoint to bypass 12h cooldown
- Updated miku_engage_random_user_for_server() and miku_engage_random_user() to accept manual_trigger flag
- Modified Web UI to always send manual_trigger=true when engaging users from the UI
- Users can now manually engage the same user multiple times from web UI without cooldown restriction
- Regular autonomous schedules still respect the 12h cooldown between engagements to the same user
Changes:
- bot/api.py: Added manual_trigger parameter with string-to-boolean conversion
- bot/static/index.html: Added manual_trigger=true to engage user request
- bot/utils/autonomous_v1_legacy.py: Added manual_trigger parameter and cooldown bypass logic
- Add COPY config_manager.py to Dockerfile so it's included in the image
- Add 'config_manager' to logger COMPONENTS list to enable logging
Fixes the ModuleNotFoundError and ValueError when importing config_manager
Add restore_runtime_settings() to ConfigManager that reads config_runtime.yaml
on startup and restores persisted values into globals:
- LANGUAGE_MODE, AUTONOMOUS_DEBUG, VOICE_DEBUG_MODE
- USE_CHESHIRE_CAT, PREFER_AMD_GPU, DM_MOOD
Add missing persistence calls to API endpoints:
- POST /language/set now persists to config_runtime.yaml
- POST /voice/debug-mode now persists to config_runtime.yaml
- POST /memory/toggle now persists to config_runtime.yaml
Call restore_runtime_settings() in on_ready() after evil/bipolar restore.
Resolves#22
Replace the minimal sync-only shutdown (which only saved autonomous state)
with a comprehensive async graceful_shutdown() coroutine that:
1. Ends active voice sessions (disconnect, release GPU locks, cleanup audio)
2. Saves autonomous engine state
3. Stops the APScheduler
4. Cancels all tracked background tasks (from task_tracker)
5. Closes the Discord gateway connection
Signal handlers (SIGTERM/SIGINT) now schedule the async shutdown on the
running event loop. The atexit handler is kept as a last-resort sync fallback.
Resolves#5, also addresses #4 (voice cleanup at shutdown)
Major changes:
- Remove unused ML libraries: torch, scikit-learn, langchain-core, langchain-text-splitters, langchain-community, faiss-cpu
- Comment out unused langchain imports in utils/core.py (only used in commented-out code)
- Keep transformers (used in persona_dialogue.py for sentiment analysis)
Results:
- Container size reduced from 14.5GB to 2.6GB
- 82% reduction (11.9GB saved)
- Bot runs correctly without errors
- All functionality preserved
Removed packages:
- torch: ~1.0-1.5GB (not used, only in soprano_to_rvc/)
- scikit-learn: ~200-300MB (not used in bot/)
- langchain-core: ~50-100MB (not used, only in commented code)
- langchain-text-splitters: ~30-50MB (not used, only in commented code)
- langchain-community: ~50-80MB (not used, only in commented code)
- faiss-cpu: ~100-200MB (not used in bot/)
This is Phase 1 of container optimization (Quick Wins).
Further optimizations possible:
- OpenCV headless (150-200MB)
- Evaluate Playwright usage (500MB-1GB)
- Alpine base image (1-1.5GB)
- Multi-stage builds (200-400MB)
Major changes:
- Add Pydantic-based configuration system (bot/config.py, bot/config_manager.py)
- Add config.yaml with all service URLs, models, and feature flags
- Fix config.yaml path resolution in Docker (check /app/config.yaml first)
- Remove Fish Audio API integration (tested feature that didn't work)
- Remove hardcoded ERROR_WEBHOOK_URL, import from config instead
- Add missing Pydantic models (LogConfigUpdateRequest, LogFilterUpdateRequest)
- Enable Cheshire Cat memory system by default (USE_CHESHIRE_CAT=true)
- Add .env.example template with all required environment variables
- Add setup.sh script for user-friendly initialization
- Update docker-compose.yml with proper env file mounting
- Update .gitignore for config files and temporary files
Config system features:
- Static configuration from config.yaml
- Runtime overrides from config_runtime.yaml
- Environment variables for secrets (.env)
- Web UI integration via config_manager
- Graceful fallback to defaults
Secrets handling:
- Move ERROR_WEBHOOK_URL from hardcoded to .env
- Add .env.example with all placeholder values
- Document all required secrets
- Fish API key and voice ID removed from .env
Documentation:
- CONFIG_README.md - Configuration system guide
- CONFIG_SYSTEM_COMPLETE.md - Implementation summary
- FISH_API_REMOVAL_COMPLETE.md - Removal record
- SECRETS_CONFIGURED.md - Secrets setup record
- BOT_STARTUP_FIX.md - Pydantic model fixes
- MIGRATION_CHECKLIST.md - Setup checklist
- WEB_UI_INTEGRATION_COMPLETE.md - Web UI config guide
- Updated readmes/README.md with new features
MOOD SYSTEM FIX:
- Mount bot/moods directory in docker-compose.yml for Cat container access
- Update miku_personality plugin to load mood descriptions from .txt files
- Add Cat logger for debugging mood loading (replaces print statements)
- Moods now dynamically loaded from working_memory instead of hardcoded neutral
- Replaced Playwright browser scraping with direct API media extraction
- Both fetch_miku_tweets() and fetch_figurine_tweets_latest() now use twscrape's built-in media info
- Reduced tweet fetching from 10-15 minutes to ~5 seconds
- Eliminated browser timeout/hanging issues
- Relaxed autonomous tweet sharing conditions:
* Increased message threshold from 10 to 20 per hour
* Reduced cooldown from 3600s to 2400s (40 minutes)
* Increased energy threshold from 50% to 70%
* Added 'silly' and 'flirty' moods to allowed sharing moods
This makes both figurine notifications and tweet sharing much more reliable and responsive.
**Critical Bug Fixes:**
1. Per-user memory isolation bug
- Changed CatAdapter from HTTP POST to WebSocket /ws/{user_id}
- User_id now comes from URL path parameter (true per-user isolation)
- Verified: Different users can't see each other's memories
2. Memory API 405 errors
- Replaced non-existent Cat endpoint calls with Qdrant direct queries
- get_memory_points(): Now uses POST /collections/{collection}/points/scroll
- delete_memory_point(): Now uses POST /collections/{collection}/points/delete
3. Memory stats showing null counts
- Reimplemented get_memory_stats() to query Qdrant directly
- Now returns accurate counts: episodic: 20, declarative: 6, procedural: 4
4. Miku couldn't see usernames
- Modified discord_bridge before_cat_reads_message hook
- Prepends [Username says:] to every message text
- LLM now knows who is texting: [Alice says:] Hello Miku!
5. Web UI Memory tab layout
- Tab9 was positioned outside .tab-container div (showed to the right)
- Moved tab9 HTML inside container, before closing divs
- Memory tab now displays below tab buttons like other tabs
**Code Changes:**
bot/utils/cat_client.py:
- Line 25: Logger name changed to 'llm' (available component)
- get_memory_stats() (lines 256-285): Query Qdrant directly via HTTP GET
- get_memory_points() (lines 275-310): Use Qdrant POST /points/scroll
- delete_memory_point() (lines 350-370): Use Qdrant POST /points/delete
cat-plugins/discord_bridge/discord_bridge.py:
- Fixed .pop() → .get() (UserMessage is Pydantic BaseModelDict)
- Added before_cat_reads_message logic to prepend [Username says:]
- Message format: [Alice says:] message content
Dockerfile.llamaswap-rocm:
- Lines 37-44: Added conditional check for UI directory
- if [ -d ui ] before npm install && npm run build
- Fixes build failure when llama-swap UI dir doesn't exist
bot/static/index.html:
- Moved tab9 from lines 1554-1688 (outside container)
- To position before container closing divs (now inside)
- Memory tab button at line 673: 🧠 Memories
**Testing & Verification:**
✅ Per-user isolation verified (Docker exec test)
✅ Memory stats showing real counts (curl test)
✅ Memory API working (facts/episodic loading)
✅ Web UI layout fixed (tab displays correctly)
✅ All 5 services running (llama-swap, llama-swap-amd, qdrant, cat, bot)
✅ Username prepending working (message context for LLM)
**Result:** All Phase 3 critical bugs fixed and verified working.
Key changes:
- CatAdapter (bot/utils/cat_client.py): WebSocket /ws/{user_id} for chat
queries instead of HTTP POST (fixes per-user memory isolation when no
API keys are configured — HTTP defaults all users to user_id='user')
- Memory management API: 8 endpoints for status, stats, facts, episodic
memories, consolidation trigger, multi-step delete with confirmation
- Web UI: Memory tab (tab9) with collection stats, fact/episodic browser,
manual consolidation trigger, and 3-step delete flow requiring exact
confirmation string
- Bot integration: Cat-first response path with query_llama fallback for
both text and embed responses, server mood detection
- Discord bridge plugin: fixed .pop() to .get() (UserMessage is a Pydantic
BaseModelDict, not a raw dict), metadata extraction via extra attributes
- Unified docker-compose: Cat + Qdrant services merged into main compose,
bot depends_on Cat healthcheck
- All plugins (discord_bridge, memory_consolidation, miku_personality)
consolidated into cat-plugins/ for volume mount
- query_llama deprecated but functional for compatibility
Major architectural overhaul of the speech-to-text pipeline for real-time voice chat:
STT Server Rewrite:
- Replaced RealtimeSTT dependency with direct Silero VAD + Faster-Whisper integration
- Achieved sub-second latency by eliminating unnecessary abstractions
- Uses small.en Whisper model for fast transcription (~850ms)
Speculative Transcription (NEW):
- Start transcribing at 150ms silence (speculative) while still listening
- If speech continues, discard speculative result and keep buffering
- If 400ms silence confirmed, use pre-computed speculative result immediately
- Reduces latency by ~250-850ms for typical utterances with clear pauses
VAD Implementation:
- Silero VAD with ONNX (CPU-efficient) for 32ms chunk processing
- Direct speech boundary detection without RealtimeSTT overhead
- Configurable thresholds for silence detection (400ms final, 150ms speculative)
Architecture:
- Single Whisper model loaded once, shared across sessions
- VAD runs on every 512-sample chunk for immediate speech detection
- Background transcription worker thread for non-blocking processing
- Greedy decoding (beam_size=1) for maximum speed
Performance:
- Previous: 400ms silence wait + ~850ms transcription = ~1.25s total latency
- Current: 400ms silence wait + 0ms (speculative ready) = ~400ms (best case)
- Single model reduces VRAM usage, prevents OOM on GTX 1660
Container Manager Updates:
- Updated health check logic to work with new response format
- Changed from checking 'warmed_up' flag to just 'status: ready'
- Improved terminology from 'warmup' to 'models loading'
Files Changed:
- stt-realtime/stt_server.py: Complete rewrite with Silero VAD + speculative transcription
- stt-realtime/requirements.txt: Removed RealtimeSTT, using torch.hub for Silero VAD
- bot/utils/container_manager.py: Updated health check for new STT response format
- bot/api.py: Updated docstring to reflect new architecture
- backups/: Archived old RealtimeSTT-based implementation
This addresses low latency requirements while maintaining accuracy with configurable
speech detection thresholds.
- Removed local 'import json' statement inside get_servers() function
- This was shadowing the module-level import and causing
'cannot access local variable' error
- json is already imported at the top of the file (line 44)
- Created new logging infrastructure with per-component filtering
- Added 6 log levels: DEBUG, INFO, API, WARNING, ERROR, CRITICAL
- Implemented non-hierarchical level control (any combination can be enabled)
- Migrated 917 print() statements across 31 files to structured logging
- Created web UI (system.html) for runtime configuration with dark theme
- Added global level controls to enable/disable levels across all components
- Added timestamp format control (off/time/date/datetime options)
- Implemented log rotation (10MB per file, 5 backups)
- Added API endpoints for dynamic log configuration
- Configured HTTP request logging with filtering via api.requests component
- Intercepted APScheduler logs with proper formatting
- Fixed persistence paths to use /app/memory for Docker volume compatibility
- Fixed checkbox display bug in web UI (enabled_levels now properly shown)
- Changed System Settings button to open in same tab instead of new window
Components: bot, api, api.requests, autonomous, persona, vision, llm,
conversation, mood, dm, scheduled, gpu, media, server, commands,
sentiment, core, apscheduler
All settings persist across container restarts via JSON config.
Features:
- Built custom ROCm container for AMD RX 6800 GPU
- Added GPU selection toggle in web UI (NVIDIA/AMD)
- Unified model names across both GPUs for seamless switching
- Vision model always uses NVIDIA GPU (optimal performance)
- Text models (llama3.1, darkidol) can use either GPU
- Added /gpu-status and /gpu-select API endpoints
- Implemented GPU state persistence in memory/gpu_state.json
Technical details:
- Multi-stage Dockerfile.llamaswap-rocm with ROCm 6.2.4
- llama.cpp compiled with GGML_HIP=ON for gfx1030 (RX 6800)
- Proper GPU permissions without root (groups 187/989)
- AMD container on port 8091, NVIDIA on port 8090
- Updated bot/utils/llm.py with get_current_gpu_url() and get_vision_gpu_url()
- Modified bot/utils/image_handling.py to always use NVIDIA for vision
- Enhanced web UI with GPU selector button (blue=NVIDIA, red=AMD)
Files modified:
- docker-compose.yml (added llama-swap-amd service)
- bot/globals.py (added LLAMA_AMD_URL)
- bot/api.py (added GPU selection endpoints and helper function)
- bot/utils/llm.py (GPU routing for text models)
- bot/utils/image_handling.py (GPU routing for vision models)
- bot/static/index.html (GPU selector UI)
- llama-swap-rocm-config.yaml (unified model names)
New files:
- Dockerfile.llamaswap-rocm
- bot/memory/gpu_state.json
- bot/utils/gpu_router.py (load balancing utility)
- setup-dual-gpu.sh (setup verification script)
- DUAL_GPU_*.md (documentation files)
ISSUE
=====
When using the manual webhook message feature via API, the following error occurred:
- 'Timeout context manager should be used inside a task'
- 'NoneType' object is not iterable (when sending without files)
The error happened because Discord.py's webhook operations were being awaited
directly in the FastAPI endpoint context, rather than within a task running in
the bot's event loop.
SOLUTION
========
Refactored /manual/send-webhook endpoint to properly handle async operations:
1. Moved webhook creation inside task function
- get_or_create_webhooks_for_channel() now runs in send_webhook_message()
- All Discord operations (webhook selection, sending) happen inside the task
- Follows same pattern as working /manual/send endpoint
2. Fixed file parameter handling
- Changed from 'files=discord_files if discord_files else None'
- To conditional: only pass files parameter when list is non-empty
- Discord.py's webhook.send() cannot iterate over None, requires list or omit
3. Maintained proper file reading
- File content still read in endpoint context (before form closes)
- File data passed to task as pre-read byte arrays
- Prevents form closure issues
TECHNICAL DETAILS
=================
- Discord.py HTTP operations use timeout context managers
- Context managers must run inside bot's event loop (via create_task)
- FastAPI endpoint context is separate from bot's event loop
- Solution: Wrap all Discord API calls in async task function
- Pattern: Read files → Create task → Task handles Discord operations
TESTING
=======
- Manual webhook sending now works without timeout errors
- Both personas (Miku/Evil) send correctly
- File attachments work properly
- Messages without files send correctly
Users can now send manual messages as either Hatsune Miku or Evil Miku
via webhooks without needing to toggle Evil Mode. This provides more
flexibility for controlling which persona sends messages.
Features:
- Checkbox option to "Send as Webhook" in manual message section
- Radio buttons to select between Hatsune Miku and Evil Miku
- Both personas use their respective profile pictures and mood emojis
- Webhooks only available for channel messages (not DMs)
- DM option automatically disabled when webhook mode is enabled
- New API endpoint: POST /manual/send-webhook
Frontend Changes:
- Added webhook checkbox and persona selection UI
- toggleWebhookOptions() function to show/hide persona options
- Updated sendManualMessage() to handle webhook mode
- Automatic channel selection when webhook is enabled
Backend Changes:
- New /manual/send-webhook endpoint in api.py
- Integrates with bipolar_mode.py webhook management
- Uses get_or_create_webhooks_for_channel() for webhook creation
- Applies correct display name with mood emoji based on persona
- Supports file attachments via webhook
This allows manual control over which Miku persona sends messages,
useful for testing, demonstrations, or creative scenarios without
needing to switch the entire bot mode.
Removed the restriction that required Evil Mode to be active before
enabling Bipolar Mode. Users can now toggle Bipolar Mode at any time.
Changes:
- Bipolar Mode toggle button now always visible in web UI
- Removed auto-disable of Bipolar Mode when Evil Mode is turned off
- Updated CSS to work in both normal and evil mode states
- Simplified updateBipolarToggleVisibility() to always show button
This allows for more flexible usage where users can have Regular Miku
and Evil Miku argue without needing Evil Mode to be the active persona.
When an argument ends and a winner is determined, the bot now explicitly
passes all mode change parameters (change_username, change_pfp, change_nicknames,
change_role_color) to ensure the winner's role color is properly restored.
- Evil Miku wins: Saves current color, switches to dark red (#D60004)
- Regular Miku wins: Restores previously saved color (from before Evil Mode)
This ensures the visual identity matches the active persona after arguments.
Major Features:
- Complete Bipolar Mode system allowing Regular Miku and Evil Miku to coexist and argue via webhooks
- LLM arbiter system using neutral model to judge argument winners with detailed reasoning
- Persistent scoreboard tracking wins, percentages, and last 50 results with timestamps and reasoning
- Automatic mode switching based on argument winner
- Webhook management per channel with profile pictures and display names
- Progressive probability system for dynamic argument lengths (starts at 10%, increases 5% per exchange, min 4 exchanges)
- Draw handling with penalty system (-5% end chance, continues argument)
- Integration with autonomous system for random argument triggers
Argument System:
- MIN_EXCHANGES = 4, progressive end chance starting at 10%
- Enhanced prompts for both personas (strategic, short, punchy responses 1-3 sentences)
- Evil Miku triumphant victory messages with gloating and satisfaction
- Regular Miku assertive defense (not passive, shows backbone)
- Message-based argument starting (can respond to specific messages via ID)
- Conversation history tracking per argument with special user_id
- Full context queries (personality, lore, lyrics, last 8 messages)
LLM Arbiter:
- Decisive prompt emphasizing picking winners (draws should be rare)
- Improved parsing with first-line exact matching and fallback counting
- Debug logging for decision transparency
- Arbiter reasoning stored in scoreboard history for review
- Uses neutral TEXT_MODEL (not evil) for unbiased judgment
Web UI & API:
- Bipolar mode toggle button (only visible when evil mode is on)
- Channel ID + Message ID input fields for argument triggering
- Scoreboard display with win percentages and recent history
- Manual argument trigger endpoint with string-based IDs
- GET /bipolar-mode/scoreboard endpoint for stats retrieval
- Real-time active arguments tracking (refreshes every 5 seconds)
Prompt Optimizations:
- All argument prompts limited to 1-3 sentences for impact
- Evil Miku system prompt with variable response length guidelines
- Removed walls of text, emphasizing brevity and precision
- "Sometimes the cruelest response is the shortest one"
Evil Miku Updates:
- Added height to lore (15.8m tall, 10x bigger than regular Miku)
- Height added to prompt facts for size-based belittling
- More strategic and calculating personality in arguments
Integration:
- Bipolar mode state restoration on bot startup
- Bot skips processing messages during active arguments
- Autonomous system checks for bipolar triggers after actions
- Import fixes (apply_evil_mode_changes/revert_evil_mode_changes)
Technical Details:
- State persistence via JSON (bipolar_mode_state.json, bipolar_webhooks.json, bipolar_scoreboard.json)
- Webhook caching per guild with fallback creation
- Event loop management with asyncio.create_task
- Rate limiting and argument conflict prevention
- Globals integration (BIPOLAR_MODE, BIPOLAR_WEBHOOKS, BIPOLAR_ARGUMENT_IN_PROGRESS, MOOD_EMOJIS)
Files Changed:
- bot/bot.py: Added bipolar mode restoration and argument-in-progress checks
- bot/globals.py: Added bipolar mode state variables and mood emoji mappings
- bot/utils/bipolar_mode.py: Complete 1106-line implementation
- bot/utils/autonomous.py: Added bipolar argument trigger checks
- bot/utils/evil_mode.py: Updated system prompt, added height info to lore/prompt
- bot/api.py: Added bipolar mode endpoints (trigger, toggle, scoreboard)
- bot/static/index.html: Added bipolar controls section with scoreboard
- bot/memory/: Various DM conversation updates
- bot/evil_miku_lore.txt: Added height description
- bot/evil_miku_prompt.txt: Added height to facts, updated personality guidelines
- Evil mode now saves current 'Miku Color' role color before changing
- Sets role color to #D60004 (dark red) when evil mode is enabled
- Restores saved color when evil mode is disabled
- Color is persisted in evil_mode_state.json between restarts
- Role color changes are skipped on startup restore to avoid rate limits
- Figurine DM notifications now respect evil mode state
- Evil Miku sends cruel, mocking comments about merch instead of excited ones
- Normal Miku remains enthusiastic and friendly about figurines
- Both modes use appropriate sign-off emojis (cute vs dark)
- Added Evil Miku mode with 4 evil moods (aggressive, cunning, sarcastic, evil_neutral)
- Created evil mode content files (evil_miku_lore.txt, evil_miku_prompt.txt, evil_miku_lyrics.txt)
- Implemented persistent evil mode state across restarts (saves to memory/evil_mode_state.json)
- Fixed API endpoints to use client.loop.create_task() to prevent timeout errors
- Added evil mode toggle in web UI with red theme styling
- Modified mood rotation to handle evil mode
- Configured DarkIdol uncensored model for evil mode text generation
- Reduced system prompt redundancy by removing duplicate content
- Added markdown escape for single asterisks (actions) while preserving bold formatting
- Evil mode now persists username, pfp, and nicknames across restarts without re-applying changes