feat: Implement comprehensive non-hierarchical logging system

- 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.
This commit is contained in:
2026-01-10 20:46:19 +02:00
parent ce00f9bd95
commit 32c2a7b930
34 changed files with 2766 additions and 936 deletions

View File

@@ -9,6 +9,9 @@ import time
from utils.autonomous_engine import autonomous_engine
from server_manager import server_manager
import globals
from utils.logger import get_logger
logger = get_logger('autonomous')
# Rate limiting: Track last action time per server to prevent rapid-fire
_last_action_execution = {} # guild_id -> timestamp
@@ -25,7 +28,7 @@ async def autonomous_tick_v2(guild_id: int):
if guild_id in _last_action_execution:
time_since_last = now - _last_action_execution[guild_id]
if time_since_last < _MIN_ACTION_INTERVAL:
print(f"⏱️ [V2] Rate limit: Only {time_since_last:.0f}s since last action (need {_MIN_ACTION_INTERVAL}s)")
logger.debug(f"[V2] Rate limit: Only {time_since_last:.0f}s since last action (need {_MIN_ACTION_INTERVAL}s)")
return
# Ask the engine if Miku should act (with optional debug logging)
@@ -35,7 +38,7 @@ async def autonomous_tick_v2(guild_id: int):
# Engine decided not to act
return
print(f"🤖 [V2] Autonomous engine decided to: {action_type} for server {guild_id}")
logger.info(f"[V2] Autonomous engine decided to: {action_type} for server {guild_id}")
# Execute the action using legacy functions
from utils.autonomous_v1_legacy import (
@@ -58,12 +61,12 @@ async def autonomous_tick_v2(guild_id: int):
elif action_type == "change_profile_picture":
# Get current mood for this server
mood, _ = server_manager.get_server_mood(guild_id)
print(f"🎨 [V2] Changing profile picture (mood: {mood})")
logger.info(f"[V2] Changing profile picture (mood: {mood})")
result = await profile_picture_manager.change_profile_picture(mood=mood, debug=True)
if result["success"]:
print(f"Profile picture changed successfully!")
logger.info(f"Profile picture changed successfully!")
else:
print(f"⚠️ Profile picture change failed: {result.get('error')}")
logger.warning(f"Profile picture change failed: {result.get('error')}")
# Record that action was taken
autonomous_engine.record_action(guild_id)
@@ -84,10 +87,10 @@ async def autonomous_tick_v2(guild_id: int):
if channel:
await maybe_trigger_argument(channel, globals.client, "Triggered after an autonomous action")
except Exception as bipolar_err:
print(f"⚠️ Bipolar check error: {bipolar_err}")
logger.warning(f"Bipolar check error: {bipolar_err}")
except Exception as e:
print(f"⚠️ Error executing autonomous action: {e}")
logger.error(f"Error executing autonomous action: {e}")
async def autonomous_reaction_tick_v2(guild_id: int):
@@ -101,7 +104,7 @@ async def autonomous_reaction_tick_v2(guild_id: int):
if not should_react:
return
print(f"🤖 [V2] Scheduled reaction check triggered for server {guild_id}")
logger.debug(f"[V2] Scheduled reaction check triggered for server {guild_id}")
try:
from utils.autonomous_v1_legacy import miku_autonomous_reaction_for_server
@@ -112,7 +115,7 @@ async def autonomous_reaction_tick_v2(guild_id: int):
autonomous_engine.record_action(guild_id)
except Exception as e:
print(f"⚠️ Error executing scheduled reaction: {e}")
logger.error(f"Error executing scheduled reaction: {e}")
def on_message_event(message):
@@ -160,7 +163,7 @@ async def _check_and_react(guild_id: int, message):
should_react = autonomous_engine.should_react_to_message(guild_id, message_age)
if should_react:
print(f"🎯 [V2] Real-time reaction triggered for message from {message.author.display_name}")
logger.info(f"[V2] Real-time reaction triggered for message from {message.author.display_name}")
from utils.autonomous_v1_legacy import miku_autonomous_reaction_for_server
await miku_autonomous_reaction_for_server(guild_id, force_message=message)
@@ -186,7 +189,7 @@ async def _check_and_act(guild_id: int):
action_type = autonomous_engine.should_take_action(guild_id, triggered_by_message=True)
if action_type:
print(f"🎯 [V2] Message triggered autonomous action: {action_type}")
logger.info(f"[V2] Message triggered autonomous action: {action_type}")
# Execute the action directly (don't call autonomous_tick_v2 which would check again)
from utils.autonomous_v1_legacy import (
@@ -209,12 +212,12 @@ async def _check_and_act(guild_id: int):
elif action_type == "change_profile_picture":
# Get current mood for this server
mood, _ = server_manager.get_server_mood(guild_id)
print(f"🎨 [V2] Changing profile picture (mood: {mood})")
logger.info(f"[V2] Changing profile picture (mood: {mood})")
result = await profile_picture_manager.change_profile_picture(mood=mood, debug=True)
if result["success"]:
print(f"Profile picture changed successfully!")
logger.info(f"Profile picture changed successfully!")
else:
print(f"⚠️ Profile picture change failed: {result.get('error')}")
logger.warning(f"Profile picture change failed: {result.get('error')}")
# Record that action was taken
autonomous_engine.record_action(guild_id)
@@ -232,10 +235,10 @@ async def _check_and_act(guild_id: int):
if channel:
await maybe_trigger_argument(channel, globals.client, "Triggered after message-based action")
except Exception as bipolar_err:
print(f"⚠️ Bipolar check error: {bipolar_err}")
logger.warning(f"Bipolar check error: {bipolar_err}")
except Exception as e:
print(f"⚠️ Error executing message-triggered action: {e}")
logger.error(f"Error executing message-triggered action: {e}")
def on_presence_update(member, before, after):
@@ -256,7 +259,7 @@ def on_presence_update(member, before, after):
# Track status changes
if before.status != after.status:
autonomous_engine.track_user_event(guild_id, "status_changed")
print(f"👤 [V2] {member.display_name} status changed: {before.status}{after.status}")
logger.debug(f"[V2] {member.display_name} status changed: {before.status}{after.status}")
# Track activity changes
if before.activities != after.activities:
@@ -272,7 +275,7 @@ def on_presence_update(member, before, after):
"activity_started",
{"activity_name": activity_name}
)
print(f"🎮 [V2] {member.display_name} started activity: {activity_name}")
logger.debug(f"[V2] {member.display_name} started activity: {activity_name}")
def on_member_join(member):
@@ -310,17 +313,17 @@ async def periodic_decay_task():
try:
autonomous_engine.decay_events(guild_id)
except Exception as e:
print(f"⚠️ Error decaying events for guild {guild_id}: {e}")
logger.warning(f"Error decaying events for guild {guild_id}: {e}")
# Save context to disk periodically
try:
autonomous_engine.save_context()
except Exception as e:
print(f"⚠️ Error saving autonomous context: {e}")
logger.error(f"Error saving autonomous context: {e}")
uptime_hours = (time.time() - task_start_time) / 3600
print(f"🧹 [V2] Decay task completed (iteration #{iteration_count}, uptime: {uptime_hours:.1f}h)")
print(f" └─ Processed {len(guild_ids)} servers")
logger.debug(f"[V2] Decay task completed (iteration #{iteration_count}, uptime: {uptime_hours:.1f}h)")
logger.debug(f" └─ Processed {len(guild_ids)} servers")
def initialize_v2_system(client):
@@ -328,7 +331,7 @@ def initialize_v2_system(client):
Initialize the V2 autonomous system.
Call this from bot.py on startup.
"""
print("🚀 Initializing Autonomous V2 System...")
logger.debug("Initializing Autonomous V2 System...")
# Initialize mood states for all servers
for guild_id, server_config in server_manager.servers.items():
@@ -337,7 +340,7 @@ def initialize_v2_system(client):
# Start decay task
client.loop.create_task(periodic_decay_task())
print("Autonomous V2 System initialized")
logger.info("Autonomous V2 System initialized")
# ========== Legacy Function Wrappers ==========

View File

@@ -12,6 +12,9 @@ from typing import Dict, List, Optional
from collections import deque
import discord
from .autonomous_persistence import save_autonomous_context, load_autonomous_context, apply_context_to_signals
from utils.logger import get_logger
logger = get_logger('autonomous')
@dataclass
class ContextSignals:
@@ -238,13 +241,13 @@ class AutonomousEngine:
time_since_startup = time.time() - self.bot_startup_time
if time_since_startup < 120: # 2 minutes
if debug:
print(f"[V2 Debug] Startup cooldown active ({time_since_startup:.0f}s / 120s)")
logger.debug(f"[V2 Debug] Startup cooldown active ({time_since_startup:.0f}s / 120s)")
return None
# Never act when asleep
if ctx.current_mood == "asleep":
if debug:
print(f"💤 [V2 Debug] Mood is 'asleep' - no action taken")
logger.debug(f"[V2 Debug] Mood is 'asleep' - no action taken")
return None
# Get mood personality
@@ -254,14 +257,14 @@ class AutonomousEngine:
self._update_activity_metrics(guild_id)
if debug:
print(f"\n🔍 [V2 Debug] Decision Check for Guild {guild_id}")
print(f" Triggered by message: {triggered_by_message}")
print(f" Mood: {ctx.current_mood} (energy={profile['energy']:.2f}, sociability={profile['sociability']:.2f}, impulsiveness={profile['impulsiveness']:.2f})")
print(f" Momentum: {ctx.conversation_momentum:.2f}")
print(f" Messages (5min/1hr): {ctx.messages_last_5min}/{ctx.messages_last_hour}")
print(f" Messages since appearance: {ctx.messages_since_last_appearance}")
print(f" Time since last action: {ctx.time_since_last_action:.0f}s")
print(f" Active activities: {len(ctx.users_started_activity)}")
logger.debug(f"\n[V2 Debug] Decision Check for Guild {guild_id}")
logger.debug(f" Triggered by message: {triggered_by_message}")
logger.debug(f" Mood: {ctx.current_mood} (energy={profile['energy']:.2f}, sociability={profile['sociability']:.2f}, impulsiveness={profile['impulsiveness']:.2f})")
logger.debug(f" Momentum: {ctx.conversation_momentum:.2f}")
logger.debug(f" Messages (5min/1hr): {ctx.messages_last_5min}/{ctx.messages_last_hour}")
logger.debug(f" Messages since appearance: {ctx.messages_since_last_appearance}")
logger.debug(f" Time since last action: {ctx.time_since_last_action:.0f}s")
logger.debug(f" Active activities: {len(ctx.users_started_activity)}")
# --- Decision Logic ---
@@ -272,7 +275,7 @@ class AutonomousEngine:
# 1. CONVERSATION JOIN (high priority when momentum is high)
if self._should_join_conversation(ctx, profile, debug):
if debug:
print(f"[V2 Debug] DECISION: join_conversation")
logger.debug(f"[V2 Debug] DECISION: join_conversation")
return "join_conversation"
# 2. USER ENGAGEMENT (someone interesting appeared)
@@ -280,17 +283,17 @@ class AutonomousEngine:
if triggered_by_message:
# Convert to join_conversation when message-triggered
if debug:
print(f"[V2 Debug] DECISION: join_conversation (engage_user converted due to message trigger)")
logger.debug(f"[V2 Debug] DECISION: join_conversation (engage_user converted due to message trigger)")
return "join_conversation"
if debug:
print(f"[V2 Debug] DECISION: engage_user")
logger.debug(f"[V2 Debug] DECISION: engage_user")
return "engage_user"
# 3. FOMO RESPONSE (lots of activity without her)
# When FOMO triggers, join the conversation instead of saying something random
if self._should_respond_to_fomo(ctx, profile, debug):
if debug:
print(f"[V2 Debug] DECISION: join_conversation (FOMO)")
logger.debug(f"[V2 Debug] DECISION: join_conversation (FOMO)")
return "join_conversation" # Jump in and respond to what's being said
# 4. BORED/LONELY (quiet for too long, depending on mood)
@@ -299,29 +302,29 @@ class AutonomousEngine:
if self._should_break_silence(ctx, profile, debug):
if triggered_by_message:
if debug:
print(f"[V2 Debug] DECISION: join_conversation (break silence, but message just sent)")
logger.debug(f"[V2 Debug] DECISION: join_conversation (break silence, but message just sent)")
return "join_conversation" # Respond to the message instead of random general statement
else:
if debug:
print(f"[V2 Debug] DECISION: general (break silence)")
logger.debug(f"[V2 Debug] DECISION: general (break silence)")
return "general"
# 5. SHARE TWEET (low activity, wants to share something)
# Skip this entirely when triggered by message - would be inappropriate to ignore user's message
if not triggered_by_message and self._should_share_content(ctx, profile, debug):
if debug:
print(f"[V2 Debug] DECISION: share_tweet")
logger.debug(f"[V2 Debug] DECISION: share_tweet")
return "share_tweet"
# 6. CHANGE PROFILE PICTURE (very rare, once per day)
# Skip this entirely when triggered by message
if not triggered_by_message and self._should_change_profile_picture(ctx, profile, debug):
if debug:
print(f"[V2 Debug] DECISION: change_profile_picture")
logger.debug(f"[V2 Debug] DECISION: change_profile_picture")
return "change_profile_picture"
if debug:
print(f"[V2 Debug] DECISION: None (no conditions met)")
logger.debug(f"[V2 Debug] DECISION: None (no conditions met)")
return None
@@ -341,10 +344,10 @@ class AutonomousEngine:
result = all(conditions.values())
if debug:
print(f" [Join Conv] momentum={ctx.conversation_momentum:.2f} > {mood_adjusted:.2f}? {conditions['momentum_check']}")
print(f" [Join Conv] messages={ctx.messages_since_last_appearance} >= 5? {conditions['messages_check']}")
print(f" [Join Conv] cooldown={ctx.time_since_last_action:.0f}s > 300s? {conditions['cooldown_check']}")
print(f" [Join Conv] impulsive roll? {conditions['impulsiveness_roll']} | Result: {result}")
logger.debug(f" [Join Conv] momentum={ctx.conversation_momentum:.2f} > {mood_adjusted:.2f}? {conditions['momentum_check']}")
logger.debug(f" [Join Conv] messages={ctx.messages_since_last_appearance} >= 5? {conditions['messages_check']}")
logger.debug(f" [Join Conv] cooldown={ctx.time_since_last_action:.0f}s > 300s? {conditions['cooldown_check']}")
logger.debug(f" [Join Conv] impulsive roll? {conditions['impulsiveness_roll']} | Result: {result}")
return result
@@ -361,8 +364,8 @@ class AutonomousEngine:
if debug and has_activities:
activities = [name for name, ts in ctx.users_started_activity]
print(f" [Engage] activities={activities}, cooldown={ctx.time_since_last_action:.0f}s > 1800s? {cooldown_ok}")
print(f" [Engage] roll={roll:.2f} < {threshold:.2f}? {roll_ok} | Result: {result}")
logger.debug(f" [Engage] activities={activities}, cooldown={ctx.time_since_last_action:.0f}s > 1800s? {cooldown_ok}")
logger.debug(f" [Engage] roll={roll:.2f} < {threshold:.2f}? {roll_ok} | Result: {result}")
return result
@@ -378,9 +381,9 @@ class AutonomousEngine:
result = msgs_check and momentum_check and cooldown_check
if debug:
print(f" [FOMO] messages={ctx.messages_since_last_appearance} > {fomo_threshold:.0f}? {msgs_check}")
print(f" [FOMO] momentum={ctx.conversation_momentum:.2f} > 0.3? {momentum_check}")
print(f" [FOMO] cooldown={ctx.time_since_last_action:.0f}s > 900s? {cooldown_check} | Result: {result}")
logger.debug(f" [FOMO] messages={ctx.messages_since_last_appearance} > {fomo_threshold:.0f}? {msgs_check}")
logger.debug(f" [FOMO] momentum={ctx.conversation_momentum:.2f} > 0.3? {momentum_check}")
logger.debug(f" [FOMO] cooldown={ctx.time_since_last_action:.0f}s > 900s? {cooldown_check} | Result: {result}")
return result
@@ -397,9 +400,9 @@ class AutonomousEngine:
result = quiet_check and silence_check and energy_ok
if debug:
print(f" [Silence] msgs_last_hour={ctx.messages_last_hour} < 5? {quiet_check}")
print(f" [Silence] time={ctx.time_since_last_action:.0f}s > {min_silence:.0f}s? {silence_check}")
print(f" [Silence] energy roll={energy_roll:.2f} < {profile['energy']:.2f}? {energy_ok} | Result: {result}")
logger.debug(f" [Silence] msgs_last_hour={ctx.messages_last_hour} < 5? {quiet_check}")
logger.debug(f" [Silence] time={ctx.time_since_last_action:.0f}s > {min_silence:.0f}s? {silence_check}")
logger.debug(f" [Silence] energy roll={energy_roll:.2f} < {profile['energy']:.2f}? {energy_ok} | Result: {result}")
return result
@@ -416,10 +419,10 @@ class AutonomousEngine:
result = quiet_check and cooldown_check and energy_ok and mood_ok
if debug:
print(f" [Share] msgs_last_hour={ctx.messages_last_hour} < 10? {quiet_check}")
print(f" [Share] cooldown={ctx.time_since_last_action:.0f}s > 3600s? {cooldown_check}")
print(f" [Share] energy roll={energy_roll:.2f} < {energy_threshold:.2f}? {energy_ok}")
print(f" [Share] mood '{ctx.current_mood}' appropriate? {mood_ok} | Result: {result}")
logger.debug(f" [Share] msgs_last_hour={ctx.messages_last_hour} < 10? {quiet_check}")
logger.debug(f" [Share] cooldown={ctx.time_since_last_action:.0f}s > 3600s? {cooldown_check}")
logger.debug(f" [Share] energy roll={energy_roll:.2f} < {energy_threshold:.2f}? {energy_ok}")
logger.debug(f" [Share] mood '{ctx.current_mood}' appropriate? {mood_ok} | Result: {result}")
return result
@@ -447,11 +450,11 @@ class AutonomousEngine:
if hours_since_change < 20: # At least 20 hours between changes
if debug:
print(f" [PFP] Last change {hours_since_change:.1f}h ago, waiting...")
logger.debug(f" [PFP] Last change {hours_since_change:.1f}h ago, waiting...")
return False
except Exception as e:
if debug:
print(f" [PFP] Error checking last change: {e}")
logger.debug(f" [PFP] Error checking last change: {e}")
# Only consider changing during certain hours (10 AM - 10 PM)
hour = ctx.hour_of_day
@@ -472,11 +475,11 @@ class AutonomousEngine:
result = time_check and quiet_check and cooldown_check and roll_ok
if debug:
print(f" [PFP] hour={hour}, time_ok={time_check}")
print(f" [PFP] msgs_last_hour={ctx.messages_last_hour} < 5? {quiet_check}")
print(f" [PFP] cooldown={ctx.time_since_last_action:.0f}s > 5400s? {cooldown_check}")
print(f" [PFP] mood_boost={mood_boost}, roll={roll:.4f} < {base_chance:.4f}? {roll_ok}")
print(f" [PFP] Result: {result}")
logger.debug(f" [PFP] hour={hour}, time_ok={time_check}")
logger.debug(f" [PFP] msgs_last_hour={ctx.messages_last_hour} < 5? {quiet_check}")
logger.debug(f" [PFP] cooldown={ctx.time_since_last_action:.0f}s > 5400s? {cooldown_check}")
logger.debug(f" [PFP] mood_boost={mood_boost}, roll={roll:.4f} < {base_chance:.4f}? {roll_ok}")
logger.debug(f" [PFP] Result: {result}")
return result

View File

@@ -8,6 +8,9 @@ import time
from pathlib import Path
from typing import Dict, Optional
from datetime import datetime, timezone
from utils.logger import get_logger
logger = get_logger('autonomous')
CONTEXT_FILE = Path("memory/autonomous_context.json")
@@ -48,9 +51,9 @@ def save_autonomous_context(server_contexts: dict, server_last_action: dict):
CONTEXT_FILE.parent.mkdir(parents=True, exist_ok=True)
with open(CONTEXT_FILE, 'w') as f:
json.dump(data, f, indent=2)
print(f"💾 [V2] Saved autonomous context for {len(server_contexts)} servers")
logger.info(f"[V2] Saved autonomous context for {len(server_contexts)} servers")
except Exception as e:
print(f"⚠️ [V2] Failed to save autonomous context: {e}")
logger.error(f"[V2] Failed to save autonomous context: {e}")
def load_autonomous_context() -> tuple[Dict[int, dict], Dict[int, float]]:
@@ -63,7 +66,7 @@ def load_autonomous_context() -> tuple[Dict[int, dict], Dict[int, float]]:
- Timestamps are adjusted for elapsed time
"""
if not CONTEXT_FILE.exists():
print(" [V2] No saved context found, starting fresh")
logger.info("[V2] No saved context found, starting fresh")
return {}, {}
try:
@@ -74,7 +77,7 @@ def load_autonomous_context() -> tuple[Dict[int, dict], Dict[int, float]]:
downtime = time.time() - saved_at
downtime_minutes = downtime / 60
print(f"📂 [V2] Loading context from {downtime_minutes:.1f} minutes ago")
logger.info(f"[V2] Loading context from {downtime_minutes:.1f} minutes ago")
context_data = {}
last_action = {}
@@ -106,13 +109,13 @@ def load_autonomous_context() -> tuple[Dict[int, dict], Dict[int, float]]:
if last_action_timestamp > 0:
last_action[guild_id] = last_action_timestamp
print(f"[V2] Restored context for {len(context_data)} servers")
print(f" └─ Momentum decay factor: {decay_factor:.3f} (from {downtime_minutes:.1f}min downtime)")
logger.info(f"[V2] Restored context for {len(context_data)} servers")
logger.debug(f" └─ Momentum decay factor: {decay_factor:.3f} (from {downtime_minutes:.1f}min downtime)")
return context_data, last_action
except Exception as e:
print(f"⚠️ [V2] Failed to load autonomous context: {e}")
logger.error(f"[V2] Failed to load autonomous context: {e}")
return {}, {}

View File

@@ -23,6 +23,9 @@ from utils.image_handling import (
convert_gif_to_mp4
)
from utils.sleep_responses import SLEEP_RESPONSES
from utils.logger import get_logger
logger = get_logger('autonomous')
# Server-specific memory storage
_server_autonomous_messages = {} # guild_id -> rotating buffer of last general messages
@@ -48,7 +51,7 @@ def save_autonomous_config(config):
def setup_autonomous_speaking():
"""Setup autonomous speaking for all configured servers"""
# This is now handled by the server manager
print("🤖 Autonomous Miku setup delegated to server manager!")
logger.debug("Autonomous Miku setup delegated to server manager!")
async def miku_autonomous_tick_for_server(guild_id: int, action_type="general", force=False, force_action=None):
"""Run autonomous behavior for a specific server"""
@@ -71,12 +74,12 @@ async def miku_say_something_general_for_server(guild_id: int):
"""Miku says something general in a specific server"""
server_config = server_manager.get_server_config(guild_id)
if not server_config:
print(f"⚠️ No config found for server {guild_id}")
logger.warning(f"No config found for server {guild_id}")
return
channel = globals.client.get_channel(server_config.autonomous_channel_id)
if not channel:
print(f"⚠️ Autonomous channel not found for server {guild_id}")
logger.warning(f"Autonomous channel not found for server {guild_id}")
return
# Check if evil mode is active
@@ -123,7 +126,7 @@ async def miku_say_something_general_for_server(guild_id: int):
message = await query_llama(prompt, user_id=f"miku-autonomous-{guild_id}", guild_id=guild_id, response_type="autonomous_general")
if not is_too_similar(message, _server_autonomous_messages[guild_id]):
break
print("🔁 Response was too similar to past messages, retrying...")
logger.debug("Response was too similar to past messages, retrying...")
try:
await channel.send(message)
@@ -131,9 +134,9 @@ async def miku_say_something_general_for_server(guild_id: int):
if len(_server_autonomous_messages[guild_id]) > MAX_HISTORY:
_server_autonomous_messages[guild_id].pop(0)
character_name = "Evil Miku" if evil_mode else "Miku"
print(f"💬 {character_name} said something general in #{channel.name} (Server: {server_config.guild_name})")
logger.info(f"{character_name} said something general in #{channel.name} (Server: {server_config.guild_name})")
except Exception as e:
print(f"⚠️ Failed to send autonomous message: {e}")
logger.error(f"Failed to send autonomous message: {e}")
async def miku_engage_random_user_for_server(guild_id: int, user_id: str = None, engagement_type: str = None):
"""Miku engages a random user in a specific server
@@ -145,17 +148,17 @@ async def miku_engage_random_user_for_server(guild_id: int, user_id: str = None,
"""
server_config = server_manager.get_server_config(guild_id)
if not server_config:
print(f"⚠️ No config found for server {guild_id}")
logger.warning(f"No config found for server {guild_id}")
return
guild = globals.client.get_guild(guild_id)
if not guild:
print(f"⚠️ Guild {guild_id} not found.")
logger.warning(f"Guild {guild_id} not found.")
return
channel = globals.client.get_channel(server_config.autonomous_channel_id)
if not channel:
print(f"⚠️ Autonomous channel not found for server {guild_id}")
logger.warning(f"Autonomous channel not found for server {guild_id}")
return
# Get target user
@@ -164,14 +167,14 @@ async def miku_engage_random_user_for_server(guild_id: int, user_id: str = None,
try:
target = guild.get_member(int(user_id))
if not target:
print(f"⚠️ User {user_id} not found in server {guild_id}")
logger.warning(f"User {user_id} not found in server {guild_id}")
return
if target.bot:
print(f"⚠️ Cannot engage bot user {user_id}")
logger.warning(f"Cannot engage bot user {user_id}")
return
print(f"🎯 Targeting specific user: {target.display_name} (ID: {user_id})")
logger.info(f"Targeting specific user: {target.display_name} (ID: {user_id})")
except ValueError:
print(f"⚠️ Invalid user ID: {user_id}")
logger.warning(f"Invalid user ID: {user_id}")
return
else:
# Pick random user
@@ -181,11 +184,11 @@ async def miku_engage_random_user_for_server(guild_id: int, user_id: str = None,
]
if not members:
print(f"😴 No available members to talk to in server {guild_id}.")
logger.warning(f"No available members to talk to in server {guild_id}.")
return
target = random.choice(members)
print(f"🎲 Randomly selected user: {target.display_name}")
logger.info(f"Randomly selected user: {target.display_name}")
time_of_day = get_time_of_day()
@@ -196,7 +199,7 @@ async def miku_engage_random_user_for_server(guild_id: int, user_id: str = None,
now = time.time()
last_time = _server_user_engagements[guild_id].get(target.id, 0)
if now - last_time < 43200: # 12 hours in seconds
print(f"⏱️ Recently engaged {target.display_name} in server {guild_id}, switching to general message.")
logger.info(f"Recently engaged {target.display_name} in server {guild_id}, switching to general message.")
await miku_say_something_general_for_server(guild_id)
return
@@ -286,7 +289,7 @@ async def miku_engage_random_user_for_server(guild_id: int, user_id: str = None,
)
if engagement_type:
print(f"💬 Engagement type: {engagement_type}")
logger.debug(f"Engagement type: {engagement_type}")
try:
# Use consistent user_id for engaging users to enable conversation history
@@ -294,9 +297,9 @@ async def miku_engage_random_user_for_server(guild_id: int, user_id: str = None,
await channel.send(f"{target.mention} {message}")
_server_user_engagements[guild_id][target.id] = time.time()
character_name = "Evil Miku" if evil_mode else "Miku"
print(f"👤 {character_name} engaged {display_name} in server {server_config.guild_name}")
logger.info(f"{character_name} engaged {display_name} in server {server_config.guild_name}")
except Exception as e:
print(f"⚠️ Failed to engage user: {e}")
logger.error(f"Failed to engage user: {e}")
async def miku_detect_and_join_conversation_for_server(guild_id: int, force: bool = False):
"""Miku detects and joins conversations in a specific server
@@ -305,30 +308,30 @@ async def miku_detect_and_join_conversation_for_server(guild_id: int, force: boo
guild_id: The server ID
force: If True, bypass activity checks and random chance (for manual triggers)
"""
print(f"🔍 [Join Conv] Called for server {guild_id} (force={force})")
logger.debug(f"[Join Conv] Called for server {guild_id} (force={force})")
server_config = server_manager.get_server_config(guild_id)
if not server_config:
print(f"⚠️ No config found for server {guild_id}")
logger.warning(f"No config found for server {guild_id}")
return
channel = globals.client.get_channel(server_config.autonomous_channel_id)
if not isinstance(channel, TextChannel):
print(f"⚠️ Autonomous channel is invalid or not found for server {guild_id}")
logger.warning(f"Autonomous channel is invalid or not found for server {guild_id}")
return
# Fetch last 20 messages (for filtering)
try:
messages = [msg async for msg in channel.history(limit=20)]
print(f"📜 [Join Conv] Fetched {len(messages)} messages from history")
logger.debug(f"[Join Conv] Fetched {len(messages)} messages from history")
except Exception as e:
print(f"⚠️ Failed to fetch channel history for server {guild_id}: {e}")
logger.error(f"Failed to fetch channel history for server {guild_id}: {e}")
return
# Filter messages based on force mode
if force:
# When forced, use messages from real users (no time limit) - but limit to last 10
recent_msgs = [msg for msg in messages if not msg.author.bot][:10]
print(f"📊 [Join Conv] Force mode: Using last {len(recent_msgs)} messages from users (no time limit)")
logger.debug(f"[Join Conv] Force mode: Using last {len(recent_msgs)} messages from users (no time limit)")
else:
# Normal mode: Filter to messages in last 10 minutes from real users (not bots)
recent_msgs = [
@@ -336,23 +339,23 @@ async def miku_detect_and_join_conversation_for_server(guild_id: int, force: boo
if not msg.author.bot
and (datetime.now(msg.created_at.tzinfo) - msg.created_at).total_seconds() < 600
]
print(f"📊 [Join Conv] Found {len(recent_msgs)} recent messages from users (last 10 min)")
logger.debug(f"[Join Conv] Found {len(recent_msgs)} recent messages from users (last 10 min)")
user_ids = set(msg.author.id for msg in recent_msgs)
if not force:
if len(recent_msgs) < 5 or len(user_ids) < 2:
# Not enough activity
print(f"⚠️ [Join Conv] Not enough activity: {len(recent_msgs)} messages, {len(user_ids)} users (need 5+ messages, 2+ users)")
logger.debug(f"[Join Conv] Not enough activity: {len(recent_msgs)} messages, {len(user_ids)} users (need 5+ messages, 2+ users)")
return
if random.random() > 0.5:
print(f"🎲 [Join Conv] Random chance failed (50% chance)")
logger.debug(f"[Join Conv] Random chance failed (50% chance)")
return # 50% chance to engage
else:
print(f"[Join Conv] Force mode - bypassing activity checks")
logger.debug(f"[Join Conv] Force mode - bypassing activity checks")
if len(recent_msgs) < 1:
print(f"⚠️ [Join Conv] No messages found in channel history")
logger.warning(f"[Join Conv] No messages found in channel history")
return
# Use last 10 messages for context (oldest to newest)
@@ -386,27 +389,27 @@ async def miku_detect_and_join_conversation_for_server(guild_id: int, force: boo
reply = await query_llama(prompt, user_id=f"miku-conversation-{guild_id}", guild_id=guild_id, response_type="conversation_join")
await channel.send(reply)
character_name = "Evil Miku" if evil_mode else "Miku"
print(f"💬 {character_name} joined an ongoing conversation in server {server_config.guild_name}")
logger.info(f"{character_name} joined an ongoing conversation in server {server_config.guild_name}")
except Exception as e:
print(f"⚠️ Failed to interject in conversation: {e}")
logger.error(f"Failed to interject in conversation: {e}")
async def share_miku_tweet_for_server(guild_id: int):
"""Share a Miku tweet in a specific server"""
server_config = server_manager.get_server_config(guild_id)
if not server_config:
print(f"⚠️ No config found for server {guild_id}")
logger.warning(f"No config found for server {guild_id}")
return
channel = globals.client.get_channel(server_config.autonomous_channel_id)
tweets = await fetch_miku_tweets(limit=5)
if not tweets:
print(f"📭 No good tweets found for server {guild_id}")
logger.warning(f"No good tweets found for server {guild_id}")
return
fresh_tweets = [t for t in tweets if t["url"] not in LAST_SENT_TWEETS]
if not fresh_tweets:
print(f"⚠️ All fetched tweets were recently sent in server {guild_id}. Reusing tweets.")
logger.warning(f"All fetched tweets were recently sent in server {guild_id}. Reusing tweets.")
fresh_tweets = tweets
tweet = random.choice(fresh_tweets)
@@ -454,12 +457,12 @@ async def handle_custom_prompt_for_server(guild_id: int, user_prompt: str):
"""Handle custom prompt for a specific server"""
server_config = server_manager.get_server_config(guild_id)
if not server_config:
print(f"⚠️ No config found for server {guild_id}")
logger.warning(f"No config found for server {guild_id}")
return False
channel = globals.client.get_channel(server_config.autonomous_channel_id)
if not channel:
print(f"⚠️ Autonomous channel not found for server {guild_id}")
logger.warning(f"Autonomous channel not found for server {guild_id}")
return False
mood = server_config.current_mood_name
@@ -478,7 +481,7 @@ async def handle_custom_prompt_for_server(guild_id: int, user_prompt: str):
# Use consistent user_id for manual prompts to enable conversation history
message = await query_llama(prompt, user_id=f"miku-manual-{guild_id}", guild_id=guild_id, response_type="autonomous_general")
await channel.send(message)
print(f"🎤 Miku responded to custom prompt in server {server_config.guild_name}")
logger.info(f"Miku responded to custom prompt in server {server_config.guild_name}")
# Add to server-specific message history
if guild_id not in _server_autonomous_messages:
@@ -489,7 +492,7 @@ async def handle_custom_prompt_for_server(guild_id: int, user_prompt: str):
return True
except Exception as e:
print(f"Failed to send custom autonomous message: {e}")
logger.error(f"Failed to send custom autonomous message: {e}")
return False
# Legacy functions for backward compatibility - these now delegate to server-specific versions
@@ -542,7 +545,7 @@ def load_last_sent_tweets():
with open(LAST_SENT_TWEETS_FILE, "r", encoding="utf-8") as f:
LAST_SENT_TWEETS = json.load(f)
except Exception as e:
print(f"⚠️ Failed to load last sent tweets: {e}")
logger.error(f"Failed to load last sent tweets: {e}")
LAST_SENT_TWEETS = []
else:
LAST_SENT_TWEETS = []
@@ -552,7 +555,7 @@ def save_last_sent_tweets():
with open(LAST_SENT_TWEETS_FILE, "w", encoding="utf-8") as f:
json.dump(LAST_SENT_TWEETS, f)
except Exception as e:
print(f"⚠️ Failed to save last sent tweets: {e}")
logger.error(f"Failed to save last sent tweets: {e}")
def get_time_of_day():
hour = datetime.now().hour + 3
@@ -602,7 +605,7 @@ async def _analyze_message_media(message):
try:
# Handle images
if any(attachment.filename.lower().endswith(ext) for ext in [".jpg", ".jpeg", ".png", ".webp"]):
print(f" 📸 Analyzing image for reaction: {attachment.filename}")
logger.debug(f" Analyzing image for reaction: {attachment.filename}")
base64_img = await download_and_encode_image(attachment.url)
if base64_img:
description = await analyze_image_with_qwen(base64_img)
@@ -612,7 +615,7 @@ async def _analyze_message_media(message):
elif any(attachment.filename.lower().endswith(ext) for ext in [".gif", ".mp4", ".webm", ".mov"]):
is_gif = attachment.filename.lower().endswith('.gif')
media_type = "GIF" if is_gif else "video"
print(f" 🎬 Analyzing {media_type} for reaction: {attachment.filename}")
logger.debug(f" Analyzing {media_type} for reaction: {attachment.filename}")
# Download media
media_bytes_b64 = await download_and_encode_media(attachment.url)
@@ -635,7 +638,7 @@ async def _analyze_message_media(message):
return f"[{media_type}: {description}]"
except Exception as e:
print(f" ⚠️ Error analyzing media for reaction: {e}")
logger.warning(f" Error analyzing media for reaction: {e}")
continue
return None
@@ -650,25 +653,25 @@ async def miku_autonomous_reaction_for_server(guild_id: int, force_message=None,
"""
# 50% chance to proceed (unless forced or with a specific message)
if not force and force_message is None and random.random() > 0.5:
print(f"🎲 Autonomous reaction skipped for server {guild_id} (50% chance)")
logger.debug(f"Autonomous reaction skipped for server {guild_id} (50% chance)")
return
server_config = server_manager.get_server_config(guild_id)
if not server_config:
print(f"⚠️ No config found for server {guild_id}")
logger.warning(f"No config found for server {guild_id}")
return
server_name = server_config.guild_name
# Don't react if asleep
if server_config.current_mood_name == "asleep" or server_config.is_sleeping:
print(f"💤 [{server_name}] Miku is asleep, skipping autonomous reaction")
logger.info(f"[{server_name}] Miku is asleep, skipping autonomous reaction")
return
# Get the autonomous channel
channel = globals.client.get_channel(server_config.autonomous_channel_id)
if not channel:
print(f"⚠️ [{server_name}] Autonomous channel not found")
logger.warning(f"[{server_name}] Autonomous channel not found")
return
try:
@@ -677,9 +680,9 @@ async def miku_autonomous_reaction_for_server(guild_id: int, force_message=None,
target_message = force_message
# Check if we've already reacted to this message
if target_message.id in _reacted_message_ids:
print(f"⏭️ [{server_name}] Already reacted to message {target_message.id}, skipping")
logger.debug(f"[{server_name}] Already reacted to message {target_message.id}, skipping")
return
print(f"🎯 [{server_name}] Reacting to new message from {target_message.author.display_name}")
logger.info(f"[{server_name}] Reacting to new message from {target_message.author.display_name}")
else:
# Fetch recent messages (last 50 messages to get more candidates)
messages = []
@@ -697,14 +700,14 @@ async def miku_autonomous_reaction_for_server(guild_id: int, force_message=None,
messages.append(message)
if not messages:
print(f"📭 [{server_name}] No recent unreacted messages to react to")
logger.debug(f"[{server_name}] No recent unreacted messages to react to")
return
# Pick a random message from the recent ones
target_message = random.choice(messages)
# Analyze any media in the message
print(f"🔍 [{server_name}] Analyzing message for reaction from {target_message.author.display_name}")
logger.debug(f"[{server_name}] Analyzing message for reaction from {target_message.author.display_name}")
media_description = await _analyze_message_media(target_message)
# Build message content with media description if present
@@ -764,7 +767,7 @@ async def miku_autonomous_reaction_for_server(guild_id: int, force_message=None,
emoji = emojis[0]
else:
# No emoji found in response, use fallback
print(f"⚠️ [{server_name}] LLM response contained no emoji: '{original_response[:50]}' - using fallback")
logger.warning(f"[{server_name}] LLM response contained no emoji: '{original_response[:50]}' - using fallback")
emoji = "💙"
# Final validation: try adding the reaction
@@ -772,7 +775,7 @@ async def miku_autonomous_reaction_for_server(guild_id: int, force_message=None,
await target_message.add_reaction(emoji)
except discord.HTTPException as e:
if "Unknown Emoji" in str(e):
print(f"[{server_name}] Invalid emoji from LLM: '{original_response[:50]}' - using fallback")
logger.warning(f"[{server_name}] Invalid emoji from LLM: '{original_response[:50]}' - using fallback")
emoji = "💙"
await target_message.add_reaction(emoji)
else:
@@ -789,14 +792,14 @@ async def miku_autonomous_reaction_for_server(guild_id: int, force_message=None,
for msg_id in ids_to_remove:
_reacted_message_ids.discard(msg_id)
print(f"[{server_name}] Autonomous reaction: Added {emoji} to message from {target_message.author.display_name}")
logger.info(f"[{server_name}] Autonomous reaction: Added {emoji} to message from {target_message.author.display_name}")
except discord.Forbidden:
print(f"[{server_name}] Missing permissions to add reactions")
logger.error(f"[{server_name}] Missing permissions to add reactions")
except discord.HTTPException as e:
print(f"[{server_name}] Failed to add reaction: {e}")
logger.error(f"[{server_name}] Failed to add reaction: {e}")
except Exception as e:
print(f"⚠️ [{server_name}] Error in autonomous reaction: {e}")
logger.error(f"[{server_name}] Error in autonomous reaction: {e}")
async def miku_autonomous_reaction(force=False):
"""Legacy function - run autonomous reactions for all servers
@@ -816,14 +819,14 @@ async def miku_autonomous_reaction_for_dm(user_id: int, force_message=None):
"""
# 50% chance to proceed (unless forced with a specific message)
if force_message is None and random.random() > 0.5:
print(f"🎲 DM reaction skipped for user {user_id} (50% chance)")
logger.debug(f"DM reaction skipped for user {user_id} (50% chance)")
return
# Get the user object
try:
user = await globals.client.fetch_user(user_id)
if not user:
print(f"⚠️ Could not find user {user_id}")
logger.warning(f"Could not find user {user_id}")
return
dm_channel = user.dm_channel
@@ -833,7 +836,7 @@ async def miku_autonomous_reaction_for_dm(user_id: int, force_message=None):
username = user.display_name
except Exception as e:
print(f"⚠️ Error fetching DM channel for user {user_id}: {e}")
logger.error(f"Error fetching DM channel for user {user_id}: {e}")
return
try:
@@ -842,9 +845,9 @@ async def miku_autonomous_reaction_for_dm(user_id: int, force_message=None):
target_message = force_message
# Check if we've already reacted to this message
if target_message.id in _reacted_message_ids:
print(f"⏭️ [DM: {username}] Already reacted to message {target_message.id}, skipping")
logger.debug(f"[DM: {username}] Already reacted to message {target_message.id}, skipping")
return
print(f"🎯 [DM: {username}] Reacting to new message")
logger.info(f"[DM: {username}] Reacting to new message")
else:
# Fetch recent messages from DM (last 50 messages)
messages = []
@@ -862,14 +865,14 @@ async def miku_autonomous_reaction_for_dm(user_id: int, force_message=None):
messages.append(message)
if not messages:
print(f"📭 [DM: {username}] No recent unreacted messages to react to")
logger.debug(f"[DM: {username}] No recent unreacted messages to react to")
return
# Pick a random message from the recent ones
target_message = random.choice(messages)
# Analyze any media in the message
print(f"🔍 [DM: {username}] Analyzing message for reaction")
logger.debug(f"[DM: {username}] Analyzing message for reaction")
media_description = await _analyze_message_media(target_message)
# Build message content with media description if present
@@ -929,7 +932,7 @@ async def miku_autonomous_reaction_for_dm(user_id: int, force_message=None):
emoji = emojis[0]
else:
# No emoji found in response, use fallback
print(f"⚠️ [DM: {username}] LLM response contained no emoji: '{original_response[:50]}' - using fallback")
logger.warning(f"[DM: {username}] LLM response contained no emoji: '{original_response[:50]}' - using fallback")
emoji = "💙"
# Final validation: try adding the reaction
@@ -937,7 +940,7 @@ async def miku_autonomous_reaction_for_dm(user_id: int, force_message=None):
await target_message.add_reaction(emoji)
except discord.HTTPException as e:
if "Unknown Emoji" in str(e):
print(f"[DM: {username}] Invalid emoji from LLM: '{original_response[:50]}' - using fallback")
logger.warning(f"[DM: {username}] Invalid emoji from LLM: '{original_response[:50]}' - using fallback")
emoji = "💙"
await target_message.add_reaction(emoji)
else:
@@ -954,14 +957,14 @@ async def miku_autonomous_reaction_for_dm(user_id: int, force_message=None):
for msg_id in ids_to_remove:
_reacted_message_ids.discard(msg_id)
print(f"[DM: {username}] Autonomous reaction: Added {emoji} to message")
logger.info(f"[DM: {username}] Autonomous reaction: Added {emoji} to message")
except discord.Forbidden:
print(f"[DM: {username}] Missing permissions to add reactions")
logger.error(f"[DM: {username}] Missing permissions to add reactions")
except discord.HTTPException as e:
print(f"[DM: {username}] Failed to add reaction: {e}")
logger.error(f"[DM: {username}] Failed to add reaction: {e}")
except Exception as e:
print(f"⚠️ [DM: {username}] Error in autonomous reaction: {e}")
logger.error(f"[DM: {username}] Error in autonomous reaction: {e}")
async def miku_update_profile_picture_for_server(guild_id: int):
@@ -973,18 +976,18 @@ async def miku_update_profile_picture_for_server(guild_id: int):
# Check if enough time has passed
if not should_update_profile_picture():
print(f"📸 [Server: {guild_id}] Profile picture not ready for update yet")
logger.debug(f"[Server: {guild_id}] Profile picture not ready for update yet")
return
# Get server config to use current mood
server_config = server_manager.get_server_config(guild_id)
if not server_config:
print(f"⚠️ No config found for server {guild_id}")
logger.warning(f"No config found for server {guild_id}")
return
mood = server_config.current_mood_name
print(f"📸 [Server: {guild_id}] Attempting profile picture update (mood: {mood})")
logger.info(f"[Server: {guild_id}] Attempting profile picture update (mood: {mood})")
try:
success = await update_profile_picture(globals.client, mood=mood)
@@ -1001,9 +1004,9 @@ async def miku_update_profile_picture_for_server(guild_id: int):
"*updates avatar* Time for a fresh look! ✨"
]
await channel.send(random.choice(messages))
print(f"[Server: {guild_id}] Profile picture updated and announced!")
logger.info(f"[Server: {guild_id}] Profile picture updated and announced!")
else:
print(f"⚠️ [Server: {guild_id}] Profile picture update failed")
logger.warning(f"[Server: {guild_id}] Profile picture update failed")
except Exception as e:
print(f"⚠️ [Server: {guild_id}] Error updating profile picture: {e}")
logger.error(f"[Server: {guild_id}] Error updating profile picture: {e}")

View File

@@ -11,6 +11,9 @@ import random
import asyncio
import discord
import globals
from utils.logger import get_logger
logger = get_logger('persona')
# ============================================================================
# CONSTANTS
@@ -38,26 +41,26 @@ def save_bipolar_state():
}
with open(BIPOLAR_STATE_FILE, "w", encoding="utf-8") as f:
json.dump(state, f, indent=2)
print(f"💾 Saved bipolar mode state: enabled={globals.BIPOLAR_MODE}")
logger.info(f"Saved bipolar mode state: enabled={globals.BIPOLAR_MODE}")
except Exception as e:
print(f"⚠️ Failed to save bipolar mode state: {e}")
logger.error(f"Failed to save bipolar mode state: {e}")
def load_bipolar_state():
"""Load bipolar mode state from JSON file"""
try:
if not os.path.exists(BIPOLAR_STATE_FILE):
print(" No bipolar mode state file found, using defaults")
logger.info("No bipolar mode state file found, using defaults")
return False
with open(BIPOLAR_STATE_FILE, "r", encoding="utf-8") as f:
state = json.load(f)
bipolar_mode = state.get("bipolar_mode_enabled", False)
print(f"📂 Loaded bipolar mode state: enabled={bipolar_mode}")
logger.info(f"Loaded bipolar mode state: enabled={bipolar_mode}")
return bipolar_mode
except Exception as e:
print(f"⚠️ Failed to load bipolar mode state: {e}")
logger.error(f"Failed to load bipolar mode state: {e}")
return False
@@ -71,16 +74,16 @@ def save_webhooks():
with open(BIPOLAR_WEBHOOKS_FILE, "w", encoding="utf-8") as f:
json.dump(webhooks_data, f, indent=2)
print(f"💾 Saved bipolar webhooks for {len(webhooks_data)} server(s)")
logger.info(f"Saved bipolar webhooks for {len(webhooks_data)} server(s)")
except Exception as e:
print(f"⚠️ Failed to save bipolar webhooks: {e}")
logger.error(f"Failed to save bipolar webhooks: {e}")
def load_webhooks():
"""Load webhook URLs from JSON file"""
try:
if not os.path.exists(BIPOLAR_WEBHOOKS_FILE):
print(" No bipolar webhooks file found")
logger.info("No bipolar webhooks file found")
return {}
with open(BIPOLAR_WEBHOOKS_FILE, "r", encoding="utf-8") as f:
@@ -91,10 +94,10 @@ def load_webhooks():
for guild_id_str, webhook_data in webhooks_data.items():
webhooks[int(guild_id_str)] = webhook_data
print(f"📂 Loaded bipolar webhooks for {len(webhooks)} server(s)")
logger.info(f"Loaded bipolar webhooks for {len(webhooks)} server(s)")
return webhooks
except Exception as e:
print(f"⚠️ Failed to load bipolar webhooks: {e}")
logger.error(f"Failed to load bipolar webhooks: {e}")
return {}
@@ -105,8 +108,8 @@ def restore_bipolar_mode_on_startup():
globals.BIPOLAR_WEBHOOKS = load_webhooks()
if bipolar_mode:
print("🔄 Bipolar mode restored from previous session")
print("💬 Persona dialogue system enabled (natural conversations + arguments)")
logger.info("Bipolar mode restored from previous session")
logger.info("Persona dialogue system enabled (natural conversations + arguments)")
return bipolar_mode
@@ -124,7 +127,7 @@ def load_scoreboard() -> dict:
with open(BIPOLAR_SCOREBOARD_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
print(f"⚠️ Failed to load scoreboard: {e}")
logger.error(f"Failed to load scoreboard: {e}")
return {"miku": 0, "evil": 0, "history": []}
@@ -134,9 +137,9 @@ def save_scoreboard(scoreboard: dict):
os.makedirs(os.path.dirname(BIPOLAR_SCOREBOARD_FILE), exist_ok=True)
with open(BIPOLAR_SCOREBOARD_FILE, "w", encoding="utf-8") as f:
json.dump(scoreboard, f, indent=2)
print(f"💾 Saved scoreboard: Miku {scoreboard['miku']} - {scoreboard['evil']} Evil Miku")
logger.info(f"Saved scoreboard: Miku {scoreboard['miku']} - {scoreboard['evil']} Evil Miku")
except Exception as e:
print(f"⚠️ Failed to save scoreboard: {e}")
logger.error(f"Failed to save scoreboard: {e}")
def record_argument_result(winner: str, exchanges: int, reasoning: str = ""):
@@ -205,7 +208,7 @@ def enable_bipolar_mode():
"""Enable bipolar mode"""
globals.BIPOLAR_MODE = True
save_bipolar_state()
print("🔄 Bipolar mode enabled!")
logger.info("Bipolar mode enabled!")
def disable_bipolar_mode():
@@ -214,7 +217,7 @@ def disable_bipolar_mode():
# Clear any ongoing arguments
globals.BIPOLAR_ARGUMENT_IN_PROGRESS.clear()
save_bipolar_state()
print("🔄 Bipolar mode disabled!")
logger.info("Bipolar mode disabled!")
def toggle_bipolar_mode() -> bool:
@@ -256,11 +259,11 @@ async def get_or_create_webhooks_for_channel(channel: discord.TextChannel) -> di
if miku_webhook and evil_webhook:
return {"miku": miku_webhook, "evil_miku": evil_webhook}
except Exception as e:
print(f"⚠️ Failed to retrieve cached webhooks: {e}")
logger.warning(f"Failed to retrieve cached webhooks: {e}")
# Create new webhooks
try:
print(f"🔧 Creating bipolar webhooks for channel #{channel.name}")
logger.info(f"Creating bipolar webhooks for channel #{channel.name}")
# Load avatar images
miku_avatar = None
@@ -300,14 +303,14 @@ async def get_or_create_webhooks_for_channel(channel: discord.TextChannel) -> di
}
save_webhooks()
print(f"Created bipolar webhooks for #{channel.name}")
logger.info(f"Created bipolar webhooks for #{channel.name}")
return {"miku": miku_webhook, "evil_miku": evil_webhook}
except discord.Forbidden:
print(f"Missing permissions to create webhooks in #{channel.name}")
logger.error(f"Missing permissions to create webhooks in #{channel.name}")
return None
except Exception as e:
print(f"Failed to create webhooks: {e}")
logger.error(f"Failed to create webhooks: {e}")
return None
@@ -322,11 +325,11 @@ async def cleanup_webhooks(client):
await webhook.delete(reason="Bipolar mode cleanup")
cleaned_count += 1
except Exception as e:
print(f"⚠️ Failed to cleanup webhooks in {guild.name}: {e}")
logger.warning(f"Failed to cleanup webhooks in {guild.name}: {e}")
globals.BIPOLAR_WEBHOOKS.clear()
save_webhooks()
print(f"🧹 Cleaned up {cleaned_count} bipolar webhook(s)")
logger.info(f"Cleaned up {cleaned_count} bipolar webhook(s)")
return cleaned_count
@@ -602,7 +605,7 @@ async def judge_argument_winner(conversation_log: list, guild_id: int) -> tuple[
)
if not judgment or judgment.startswith("Error"):
print("⚠️ Arbiter failed to make judgment, defaulting to draw")
logger.warning("Arbiter failed to make judgment, defaulting to draw")
return "draw", "The arbiter could not make a decision."
# Parse the judgment - look at the first line/sentence for the decision
@@ -610,37 +613,37 @@ async def judge_argument_winner(conversation_log: list, guild_id: int) -> tuple[
first_line = judgment_lines[0].strip().strip('"').strip()
first_line_lower = first_line.lower()
print(f"🔍 Parsing arbiter first line: '{first_line}'")
logger.debug(f"Parsing arbiter first line: '{first_line}'")
# Check the first line for the decision - be very specific
# The arbiter should respond with ONLY the name on the first line
if first_line_lower == "evil miku":
winner = "evil"
print("Detected Evil Miku win from first line exact match")
logger.debug("Detected Evil Miku win from first line exact match")
elif first_line_lower == "hatsune miku":
winner = "miku"
print("Detected Hatsune Miku win from first line exact match")
logger.debug("Detected Hatsune Miku win from first line exact match")
elif first_line_lower == "draw":
winner = "draw"
print("Detected Draw from first line exact match")
logger.debug("Detected Draw from first line exact match")
elif "evil miku" in first_line_lower and "hatsune" not in first_line_lower:
# First line mentions Evil Miku but not Hatsune Miku
winner = "evil"
print("Detected Evil Miku win from first line (contains 'evil miku' only)")
logger.debug("Detected Evil Miku win from first line (contains 'evil miku' only)")
elif "hatsune miku" in first_line_lower and "evil" not in first_line_lower:
# First line mentions Hatsune Miku but not Evil Miku
winner = "miku"
print("Detected Hatsune Miku win from first line (contains 'hatsune miku' only)")
logger.debug("Detected Hatsune Miku win from first line (contains 'hatsune miku' only)")
else:
# Fallback: check the whole judgment
print(f"⚠️ First line ambiguous, using fallback counting method")
logger.debug(f"First line ambiguous, using fallback counting method")
judgment_lower = judgment.lower()
# Count mentions to break ties
evil_count = judgment_lower.count("evil miku")
miku_count = judgment_lower.count("hatsune miku")
draw_count = judgment_lower.count("draw")
print(f"📊 Counts - Evil: {evil_count}, Miku: {miku_count}, Draw: {draw_count}")
logger.debug(f"Counts - Evil: {evil_count}, Miku: {miku_count}, Draw: {draw_count}")
if draw_count > 0 and draw_count >= evil_count and draw_count >= miku_count:
winner = "draw"
@@ -654,7 +657,7 @@ async def judge_argument_winner(conversation_log: list, guild_id: int) -> tuple[
return winner, judgment
except Exception as e:
print(f"⚠️ Error in arbiter judgment: {e}")
logger.error(f"Error in arbiter judgment: {e}")
return "draw", "An error occurred during judgment."
@@ -756,13 +759,13 @@ async def run_argument(channel: discord.TextChannel, client, trigger_context: st
guild_id = channel.guild.id
if is_argument_in_progress(channel_id):
print(f"⚠️ Argument already in progress in #{channel.name}")
logger.warning(f"Argument already in progress in #{channel.name}")
return
# Get webhooks for this channel
webhooks = await get_or_create_webhooks_for_channel(channel)
if not webhooks:
print(f"Could not create webhooks for argument in #{channel.name}")
logger.error(f"Could not create webhooks for argument in #{channel.name}")
return
# Determine who initiates based on starting_message or inactive persona
@@ -773,12 +776,12 @@ async def run_argument(channel: discord.TextChannel, client, trigger_context: st
is_evil_message = globals.EVIL_MODE or (starting_message.webhook_id is not None and "Evil" in (starting_message.author.name or ""))
initiator = "miku" if is_evil_message else "evil" # Opposite persona responds
last_message = starting_message.content
print(f"🔄 Starting argument from message, responder: {initiator}")
logger.info(f"Starting argument from message, responder: {initiator}")
else:
# The inactive persona breaks through
initiator = get_inactive_persona()
last_message = None
print(f"🔄 Starting bipolar argument in #{channel.name}, initiated by {initiator}")
logger.info(f"Starting bipolar argument in #{channel.name}, initiated by {initiator}")
start_argument(channel_id, initiator)
@@ -812,7 +815,7 @@ async def run_argument(channel: discord.TextChannel, client, trigger_context: st
globals.EVIL_MODE = original_evil_mode
if not initial_message or initial_message.startswith("Error") or initial_message.startswith("Sorry"):
print("Failed to generate initial argument message")
logger.error("Failed to generate initial argument message")
end_argument(channel_id)
return
@@ -877,22 +880,22 @@ async def run_argument(channel: discord.TextChannel, client, trigger_context: st
if should_end:
exchange_count = globals.BIPOLAR_ARGUMENT_IN_PROGRESS.get(channel_id, {}).get("exchange_count", 0)
print(f"⚖️ Argument complete with {exchange_count} exchanges. Calling arbiter...")
logger.info(f"Argument complete with {exchange_count} exchanges. Calling arbiter...")
# Use arbiter to judge the winner
winner, judgment = await judge_argument_winner(conversation_log, guild_id)
print(f"⚖️ Arbiter decision: {winner}")
print(f"📝 Judgment: {judgment}")
logger.info(f"Arbiter decision: {winner}")
logger.info(f"Judgment: {judgment}")
# If it's a draw, continue the argument instead of ending
if winner == "draw":
print("🤝 Arbiter ruled it's still a draw - argument continues...")
logger.info("Arbiter ruled it's still a draw - argument continues...")
# Reduce the end chance by 5% (but don't go below 5%)
current_end_chance = globals.BIPOLAR_ARGUMENT_IN_PROGRESS[channel_id].get("end_chance", 0.1)
new_end_chance = max(0.05, current_end_chance - 0.05)
globals.BIPOLAR_ARGUMENT_IN_PROGRESS[channel_id]["end_chance"] = new_end_chance
print(f"📉 Reduced end chance to {new_end_chance*100:.0f}% - argument continues...")
logger.info(f"Reduced end chance to {new_end_chance*100:.0f}% - argument continues...")
# Don't end, just continue to the next exchange
else:
# Clear winner - generate final triumphant message
@@ -938,10 +941,10 @@ async def run_argument(channel: discord.TextChannel, client, trigger_context: st
# Switch to winner's mode (including role color)
from utils.evil_mode import apply_evil_mode_changes, revert_evil_mode_changes
if winner == "evil":
print("👿 Evil Miku won! Switching to Evil Mode...")
logger.info("Evil Miku won! Switching to Evil Mode...")
await apply_evil_mode_changes(client, change_username=True, change_pfp=True, change_nicknames=True, change_role_color=True)
else:
print("💙 Hatsune Miku won! Switching to Normal Mode...")
logger.info("Hatsune Miku won! Switching to Normal Mode...")
await revert_evil_mode_changes(client, change_username=True, change_pfp=True, change_nicknames=True, change_role_color=True)
# Clean up argument conversation history
@@ -951,7 +954,7 @@ async def run_argument(channel: discord.TextChannel, client, trigger_context: st
pass # History cleanup is not critical
end_argument(channel_id)
print(f"Argument ended in #{channel.name}, winner: {winner}")
logger.info(f"Argument ended in #{channel.name}, winner: {winner}")
return
# Get current speaker
@@ -982,7 +985,7 @@ async def run_argument(channel: discord.TextChannel, client, trigger_context: st
globals.EVIL_MODE = original_evil_mode
if not response or response.startswith("Error") or response.startswith("Sorry"):
print(f"Failed to generate argument response")
logger.error(f"Failed to generate argument response")
end_argument(channel_id)
return
@@ -1021,7 +1024,7 @@ async def run_argument(channel: discord.TextChannel, client, trigger_context: st
is_first_response = False
except Exception as e:
print(f"Argument error: {e}")
logger.error(f"Argument error: {e}")
import traceback
traceback.print_exc()
end_argument(channel_id)
@@ -1057,11 +1060,11 @@ async def force_trigger_argument(channel: discord.TextChannel, client, context:
starting_message: Optional message to use as the first message in the argument
"""
if not globals.BIPOLAR_MODE:
print("⚠️ Cannot trigger argument - bipolar mode is not enabled")
logger.warning("Cannot trigger argument - bipolar mode is not enabled")
return False
if is_argument_in_progress(channel.id):
print("⚠️ Argument already in progress in this channel")
logger.warning("Argument already in progress in this channel")
return False
asyncio.create_task(run_argument(channel, client, context, starting_message))

View File

@@ -5,13 +5,18 @@ Replaces the vector search system with organized, complete context.
Preserves original content files in their entirety.
"""
from utils.logger import get_logger
logger = get_logger('core')
def get_original_miku_lore() -> str:
"""Load the complete, unmodified miku_lore.txt file"""
try:
with open("miku_lore.txt", "r", encoding="utf-8") as f:
return f.read()
except Exception as e:
print(f"⚠️ Failed to load miku_lore.txt: {e}")
logger.error(f"Failed to load miku_lore.txt: {e}")
return "## MIKU LORE\n[File could not be loaded]"
@@ -21,7 +26,7 @@ def get_original_miku_prompt() -> str:
with open("miku_prompt.txt", "r", encoding="utf-8") as f:
return f.read()
except Exception as e:
print(f"⚠️ Failed to load miku_prompt.txt: {e}")
logger.error(f"Failed to load miku_prompt.txt: {e}")
return "## MIKU PROMPT\n[File could not be loaded]"
@@ -31,7 +36,7 @@ def get_original_miku_lyrics() -> str:
with open("miku_lyrics.txt", "r", encoding="utf-8") as f:
return f.read()
except Exception as e:
print(f"⚠️ Failed to load miku_lyrics.txt: {e}")
logger.error(f"Failed to load miku_lyrics.txt: {e}")
return "## MIKU LYRICS\n[File could not be loaded]"

View File

@@ -8,6 +8,9 @@ import globals
from langchain_community.vectorstores import FAISS
from langchain_text_splitters import CharacterTextSplitter, RecursiveCharacterTextSplitter
from langchain_core.documents import Document
from utils.logger import get_logger
logger = get_logger('core')
# switch_model() removed - llama-swap handles model switching automatically
@@ -21,7 +24,7 @@ async def is_miku_addressed(message) -> bool:
# Safety check: ensure guild and guild.me exist
if not message.guild or not message.guild.me:
print(f"⚠️ Warning: Invalid guild or guild.me in message from {message.author}")
logger.warning(f"Invalid guild or guild.me in message from {message.author}")
return False
# If message contains a ping for Miku, return true
@@ -35,7 +38,7 @@ async def is_miku_addressed(message) -> bool:
if referenced_msg.author == message.guild.me:
return True
except Exception as e:
print(f"⚠️ Could not fetch referenced message: {e}")
logger.warning(f"Could not fetch referenced message: {e}")
cleaned = message.content.strip()

View File

@@ -7,6 +7,10 @@ import aiohttp
import random
from typing import Optional, List, Dict
import asyncio
from utils.logger import get_logger
logger = get_logger('media')
class DanbooruClient:
"""Client for interacting with Danbooru API"""
@@ -74,23 +78,23 @@ class DanbooruClient:
try:
url = f"{self.BASE_URL}/posts.json"
print(f"🎨 Danbooru request: {url} with params: {params}")
logger.debug(f"Danbooru request: {url} with params: {params}")
async with self.session.get(url, params=params, timeout=10) as response:
if response.status == 200:
posts = await response.json()
print(f"🎨 Danbooru: Found {len(posts)} posts (page {page})")
logger.debug(f"Danbooru: Found {len(posts)} posts (page {page})")
return posts
else:
error_text = await response.text()
print(f"⚠️ Danbooru API error: {response.status}")
print(f"⚠️ Request URL: {response.url}")
print(f"⚠️ Error details: {error_text[:500]}")
logger.error(f"Danbooru API error: {response.status}")
logger.error(f"Request URL: {response.url}")
logger.error(f"Error details: {error_text[:500]}")
return []
except asyncio.TimeoutError:
print(f"⚠️ Danbooru API timeout")
logger.error(f"Danbooru API timeout")
return []
except Exception as e:
print(f"⚠️ Danbooru API error: {e}")
logger.error(f"Danbooru API error: {e}")
return []
async def get_random_miku_image(
@@ -128,7 +132,7 @@ class DanbooruClient:
)
if not posts:
print("⚠️ No posts found, trying without mood tags")
logger.warning("No posts found, trying without mood tags")
# Fallback: try without mood tags
posts = await self.search_miku_images(
rating=["g", "s"],
@@ -146,13 +150,13 @@ class DanbooruClient:
]
if not valid_posts:
print("⚠️ No valid posts with sufficient resolution")
logger.warning("No valid posts with sufficient resolution")
return None
# Pick a random one
selected = random.choice(valid_posts)
print(f"🎨 Selected Danbooru post #{selected.get('id')} - {selected.get('tag_string_character', 'unknown character')}")
logger.info(f"Selected Danbooru post #{selected.get('id')} - {selected.get('tag_string_character', 'unknown character')}")
return selected

View File

@@ -11,6 +11,9 @@ import discord
import globals
from utils.llm import query_llama
from utils.dm_logger import dm_logger
from utils.logger import get_logger
logger = get_logger('dm')
# Directories
REPORTS_DIR = "memory/dm_reports"
@@ -26,7 +29,7 @@ class DMInteractionAnalyzer:
"""
self.owner_user_id = owner_user_id
os.makedirs(REPORTS_DIR, exist_ok=True)
print(f"📊 DM Interaction Analyzer initialized for owner: {owner_user_id}")
logger.info(f"DM Interaction Analyzer initialized for owner: {owner_user_id}")
def _load_reported_today(self) -> Dict[str, str]:
"""Load the list of users reported today with their dates"""
@@ -35,7 +38,7 @@ class DMInteractionAnalyzer:
with open(REPORTED_TODAY_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"⚠️ Failed to load reported_today.json: {e}")
logger.error(f"Failed to load reported_today.json: {e}")
return {}
return {}
@@ -45,7 +48,7 @@ class DMInteractionAnalyzer:
with open(REPORTED_TODAY_FILE, 'w', encoding='utf-8') as f:
json.dump(reported, f, indent=2)
except Exception as e:
print(f"⚠️ Failed to save reported_today.json: {e}")
logger.error(f"Failed to save reported_today.json: {e}")
def _clean_old_reports(self, reported: Dict[str, str]) -> Dict[str, str]:
"""Remove entries from reported_today that are older than 24 hours"""
@@ -58,7 +61,7 @@ class DMInteractionAnalyzer:
if now - report_date < timedelta(hours=24):
cleaned[user_id] = date_str
except Exception as e:
print(f"⚠️ Failed to parse date for user {user_id}: {e}")
logger.error(f"Failed to parse date for user {user_id}: {e}")
return cleaned
@@ -91,7 +94,7 @@ class DMInteractionAnalyzer:
if msg_time >= cutoff_time:
recent_messages.append(msg)
except Exception as e:
print(f"⚠️ Failed to parse message timestamp: {e}")
logger.error(f"Failed to parse message timestamp: {e}")
return recent_messages
@@ -126,14 +129,14 @@ class DMInteractionAnalyzer:
recent_messages = self._get_recent_messages(user_id, hours=24)
if not recent_messages:
print(f"📊 No recent messages from user {username} ({user_id})")
logger.debug(f"No recent messages from user {username} ({user_id})")
return None
# Count user messages only (not bot responses)
user_messages = [msg for msg in recent_messages if not msg.get("is_bot_message", False)]
if len(user_messages) < 3: # Minimum threshold for analysis
print(f"📊 Not enough messages from user {username} ({user_id}) for analysis")
logger.info(f"Not enough messages from user {username} ({user_id}) for analysis")
return None
# Format messages for analysis
@@ -174,7 +177,7 @@ Respond ONLY with the JSON object, no other text."""
response_type="dm_analysis"
)
print(f"📊 Raw LLM response for {username}:\n{response}\n")
logger.debug(f"Raw LLM response for {username}:\n{response}\n")
# Parse JSON response
# Remove markdown code blocks if present
@@ -192,7 +195,7 @@ Respond ONLY with the JSON object, no other text."""
if start_idx != -1 and end_idx != -1:
cleaned_response = cleaned_response[start_idx:end_idx+1]
print(f"📊 Cleaned JSON for {username}:\n{cleaned_response}\n")
logger.debug(f"Cleaned JSON for {username}:\n{cleaned_response}\n")
analysis = json.loads(cleaned_response)
@@ -205,11 +208,11 @@ Respond ONLY with the JSON object, no other text."""
return analysis
except json.JSONDecodeError as e:
print(f"⚠️ JSON parse error for user {username}: {e}")
print(f"⚠️ Failed response: {response}")
logger.error(f"JSON parse error for user {username}: {e}")
logger.error(f"Failed response: {response}")
return None
except Exception as e:
print(f"⚠️ Failed to analyze interaction for user {username}: {e}")
logger.error(f"Failed to analyze interaction for user {username}: {e}")
return None
def _save_report(self, user_id: int, analysis: Dict) -> str:
@@ -221,10 +224,10 @@ Respond ONLY with the JSON object, no other text."""
try:
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(analysis, f, indent=2, ensure_ascii=False)
print(f"💾 Saved report: {filepath}")
logger.info(f"Saved report: {filepath}")
return filepath
except Exception as e:
print(f"⚠️ Failed to save report: {e}")
logger.error(f"Failed to save report: {e}")
return ""
async def _send_report_to_owner(self, analysis: Dict):
@@ -232,7 +235,7 @@ Respond ONLY with the JSON object, no other text."""
try:
# Ensure we're using the Discord client's event loop
if not globals.client or not globals.client.is_ready():
print(f"⚠️ Discord client not ready, cannot send report")
logger.warning(f"Discord client not ready, cannot send report")
return
owner = await globals.client.fetch_user(self.owner_user_id)
@@ -294,10 +297,10 @@ Respond ONLY with the JSON object, no other text."""
)
await owner.send(embed=embed)
print(f"📤 Report sent to owner for user {username}")
logger.info(f"Report sent to owner for user {username}")
except Exception as e:
print(f"⚠️ Failed to send report to owner: {e}")
logger.error(f"Failed to send report to owner: {e}")
async def analyze_and_report(self, user_id: int) -> bool:
"""
@@ -306,12 +309,11 @@ Respond ONLY with the JSON object, no other text."""
Returns:
True if analysis was performed and reported, False otherwise
"""
# Check if already reported today
if self.has_been_reported_today(user_id):
print(f"📊 User {user_id} already reported today, skipping")
return False
# Analyze interaction
logger.debug(f"User {user_id} already reported today, skipping")
return False # Analyze interaction
analysis = await self.analyze_user_interaction(user_id)
if not analysis:
@@ -331,13 +333,13 @@ Respond ONLY with the JSON object, no other text."""
async def run_daily_analysis(self):
"""Run analysis on all DM users and report significant interactions"""
print("📊 Starting daily DM interaction analysis...")
logger.info("Starting daily DM interaction analysis...")
# Get all DM users
all_users = dm_logger.get_all_dm_users()
if not all_users:
print("📊 No DM users to analyze")
logger.info("No DM users to analyze")
return
reported_count = 0
@@ -363,9 +365,9 @@ Respond ONLY with the JSON object, no other text."""
analyzed_count += 1
except Exception as e:
print(f"⚠️ Failed to process user {user_summary.get('username', 'Unknown')}: {e}")
logger.error(f"Failed to process user {user_summary.get('username', 'Unknown')}: {e}")
print(f"📊 Daily analysis complete: Analyzed {analyzed_count} users, reported {reported_count}")
logger.info(f"Daily analysis complete: Analyzed {analyzed_count} users, reported {reported_count}")
# Global instance (will be initialized with owner ID)

View File

@@ -9,6 +9,9 @@ import discord
from datetime import datetime
from typing import List, Optional
import globals
from utils.logger import get_logger
logger = get_logger('dm')
# Directory for storing DM logs
DM_LOG_DIR = "memory/dms"
@@ -19,7 +22,7 @@ class DMLogger:
"""Initialize the DM logger and ensure directory exists"""
os.makedirs(DM_LOG_DIR, exist_ok=True)
os.makedirs("memory", exist_ok=True)
print(f"📁 DM Logger initialized: {DM_LOG_DIR}")
logger.info(f"DM Logger initialized: {DM_LOG_DIR}")
def _get_user_log_file(self, user_id: int) -> str:
"""Get the log file path for a specific user"""
@@ -28,19 +31,19 @@ class DMLogger:
def _load_user_logs(self, user_id: int) -> dict:
"""Load existing logs for a user, create new if doesn't exist"""
log_file = self._get_user_log_file(user_id)
print(f"📁 DM Logger: Loading logs from {log_file}")
logger.debug(f"DM Logger: Loading logs from {log_file}")
if os.path.exists(log_file):
try:
with open(log_file, 'r', encoding='utf-8') as f:
logs = json.load(f)
print(f"📁 DM Logger: Successfully loaded logs for user {user_id}: {len(logs.get('conversations', []))} conversations")
logger.debug(f"DM Logger: Successfully loaded logs for user {user_id}: {len(logs.get('conversations', []))} conversations")
return logs
except Exception as e:
print(f"⚠️ DM Logger: Failed to load DM logs for user {user_id}: {e}")
logger.error(f"DM Logger: Failed to load DM logs for user {user_id}: {e}")
return {"user_id": user_id, "username": "Unknown", "conversations": []}
else:
print(f"📁 DM Logger: No log file found for user {user_id}, creating new")
logger.debug(f"DM Logger: No log file found for user {user_id}, creating new")
return {"user_id": user_id, "username": "Unknown", "conversations": []}
def _save_user_logs(self, user_id: int, logs: dict):
@@ -50,7 +53,7 @@ class DMLogger:
with open(log_file, 'w', encoding='utf-8') as f:
json.dump(logs, f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"⚠️ Failed to save DM logs for user {user_id}: {e}")
logger.error(f"Failed to save DM logs for user {user_id}: {e}")
def log_user_message(self, user: discord.User, message: discord.Message, is_bot_message: bool = False):
"""Log a user message in DMs"""
@@ -92,15 +95,15 @@ class DMLogger:
# Keep only last 1000 messages to prevent files from getting too large
if len(logs["conversations"]) > 1000:
logs["conversations"] = logs["conversations"][-1000:]
print(f"📝 DM logs for user {username} trimmed to last 1000 messages")
logger.info(f"DM logs for user {username} trimmed to last 1000 messages")
# Save logs
self._save_user_logs(user_id, logs)
if is_bot_message:
print(f"🤖 DM logged: Bot -> {username} ({len(message_entry['attachments'])} attachments)")
logger.debug(f"DM logged: Bot -> {username} ({len(message_entry['attachments'])} attachments)")
else:
print(f"💬 DM logged: {username} -> Bot ({len(message_entry['attachments'])} attachments)")
logger.debug(f"DM logged: {username} -> Bot ({len(message_entry['attachments'])} attachments)")
def get_user_conversation_summary(self, user_id: int) -> dict:
"""Get a summary of conversations with a user"""
@@ -211,10 +214,10 @@ class DMLogger:
bot_msg = MockMessage(bot_response, attachments=bot_attachments)
self.log_user_message(user, bot_msg, is_bot_message=True)
print(f"📝 Conversation logged for user {user_id}: user='{user_message[:50]}...', bot='{bot_response[:50]}...'")
logger.debug(f"Conversation logged for user {user_id}: user='{user_message[:50]}...', bot='{bot_response[:50]}...'")
except Exception as e:
print(f"⚠️ Failed to log conversation for user {user_id}: {e}")
logger.error(f"Failed to log conversation for user {user_id}: {e}")
def export_user_conversation(self, user_id: int, format: str = "json") -> str:
"""Export all conversations with a user in specified format"""
@@ -254,7 +257,7 @@ class DMLogger:
with open(BLOCKED_USERS_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"⚠️ Failed to load blocked users: {e}")
logger.error(f"Failed to load blocked users: {e}")
return {"blocked_users": []}
return {"blocked_users": []}
@@ -262,9 +265,9 @@ class DMLogger:
"""Save the blocked users list"""
try:
with open(BLOCKED_USERS_FILE, 'w', encoding='utf-8') as f:
json.dump(blocked_data, f, indent=2, ensure_ascii=False)
json.dump(blocked_data, f, indent=2)
except Exception as e:
print(f"⚠️ Failed to save blocked users: {e}")
logger.error(f"Failed to save blocked users: {e}")
def is_user_blocked(self, user_id: int) -> bool:
"""Check if a user is blocked"""
@@ -289,13 +292,13 @@ class DMLogger:
}
self._save_blocked_users(blocked_data)
print(f"🚫 User {user_id} ({username}) has been blocked")
logger.info(f"User {user_id} ({username}) has been blocked")
return True
else:
print(f"⚠️ User {user_id} is already blocked")
logger.warning(f"User {user_id} is already blocked")
return False
except Exception as e:
print(f"Failed to block user {user_id}: {e}")
logger.error(f"Failed to block user {user_id}: {e}")
return False
def unblock_user(self, user_id: int) -> bool:
@@ -313,13 +316,13 @@ class DMLogger:
username = "Unknown"
self._save_blocked_users(blocked_data)
print(f"User {user_id} ({username}) has been unblocked")
logger.info(f"User {user_id} ({username}) has been unblocked")
return True
else:
print(f"⚠️ User {user_id} is not blocked")
logger.warning(f"User {user_id} is not blocked")
return False
except Exception as e:
print(f"Failed to unblock user {user_id}: {e}")
logger.error(f"Failed to unblock user {user_id}: {e}")
return False
def get_blocked_users(self) -> List[dict]:
@@ -368,17 +371,17 @@ class DMLogger:
self._save_user_logs(user_id, logs)
reactor_type = "🤖 Miku" if is_bot_reactor else f"👤 {reactor_name}"
print(f" Reaction logged: {emoji} by {reactor_type} on message {message_id}")
logger.debug(f"Reaction logged: {emoji} by {reactor_type} on message {message_id}")
return True
else:
print(f"⚠️ Reaction {emoji} by {reactor_name} already exists on message {message_id}")
logger.debug(f"Reaction {emoji} by {reactor_name} already exists on message {message_id}")
return False
print(f"⚠️ Message {message_id} not found in user {user_id}'s logs")
logger.warning(f"Message {message_id} not found in user {user_id}'s logs")
return False
except Exception as e:
print(f"Failed to log reaction add for user {user_id}, message {message_id}: {e}")
logger.error(f"Failed to log reaction add for user {user_id}, message {message_id}: {e}")
return False
async def log_reaction_remove(self, user_id: int, message_id: int, emoji: str, reactor_id: int):
@@ -399,20 +402,20 @@ class DMLogger:
if len(message["reactions"]) < original_count:
self._save_user_logs(user_id, logs)
print(f" Reaction removed: {emoji} by user/bot {reactor_id} from message {message_id}")
logger.debug(f"Reaction removed: {emoji} by user/bot {reactor_id} from message {message_id}")
return True
else:
print(f"⚠️ Reaction {emoji} by {reactor_id} not found on message {message_id}")
logger.debug(f"Reaction {emoji} by {reactor_id} not found on message {message_id}")
return False
else:
print(f"⚠️ No reactions on message {message_id}")
logger.debug(f"No reactions on message {message_id}")
return False
print(f"⚠️ Message {message_id} not found in user {user_id}'s logs")
logger.warning(f"Message {message_id} not found in user {user_id}'s logs")
return False
except Exception as e:
print(f"Failed to log reaction remove for user {user_id}, message {message_id}: {e}")
logger.error(f"Failed to log reaction remove for user {user_id}, message {message_id}: {e}")
return False
async def delete_conversation(self, user_id: int, conversation_id: str) -> bool:
@@ -420,8 +423,8 @@ class DMLogger:
try:
logs = self._load_user_logs(user_id)
print(f"🔍 DM Logger: Looking for bot message ID '{conversation_id}' for user {user_id}")
print(f"🔍 DM Logger: Searching through {len(logs['conversations'])} conversations")
logger.debug(f"DM Logger: Looking for bot message ID '{conversation_id}' for user {user_id}")
logger.debug(f"DM Logger: Searching through {len(logs['conversations'])} conversations")
# Convert conversation_id to int for comparison if it looks like a Discord message ID
conv_id_as_int = None
@@ -441,7 +444,7 @@ class DMLogger:
break
if not message_to_delete:
print(f"⚠️ No bot message found with ID {conversation_id} for user {user_id}")
logger.warning(f"No bot message found with ID {conversation_id} for user {user_id}")
return False
# Try to delete from Discord first
@@ -463,13 +466,13 @@ class DMLogger:
discord_message = await dm_channel.fetch_message(int(message_id))
await discord_message.delete()
discord_deleted = True
print(f"Deleted Discord message {message_id} from DM with user {user_id}")
logger.info(f"Deleted Discord message {message_id} from DM with user {user_id}")
except Exception as e:
print(f"⚠️ Could not delete Discord message {message_id}: {e}")
logger.warning(f"Could not delete Discord message {message_id}: {e}")
# Continue anyway to delete from logs
except Exception as e:
print(f"⚠️ Discord deletion failed: {e}")
logger.warning(f"Discord deletion failed: {e}")
# Continue anyway to delete from logs
# Remove from logs regardless of Discord deletion success
@@ -488,16 +491,16 @@ class DMLogger:
if deleted_count > 0:
self._save_user_logs(user_id, logs)
if discord_deleted:
print(f"🗑️ Deleted bot message from both Discord and logs for user {user_id}")
logger.info(f"Deleted bot message from both Discord and logs for user {user_id}")
else:
print(f"🗑️ Deleted bot message from logs only (Discord deletion failed) for user {user_id}")
logger.info(f"Deleted bot message from logs only (Discord deletion failed) for user {user_id}")
return True
else:
print(f"⚠️ No bot message found in logs with ID {conversation_id} for user {user_id}")
logger.warning(f"No bot message found in logs with ID {conversation_id} for user {user_id}")
return False
except Exception as e:
print(f"Failed to delete conversation {conversation_id} for user {user_id}: {e}")
logger.error(f"Failed to delete conversation {conversation_id} for user {user_id}: {e}")
return False
async def delete_all_conversations(self, user_id: int) -> bool:
@@ -507,12 +510,12 @@ class DMLogger:
conversation_count = len(logs["conversations"])
if conversation_count == 0:
print(f"⚠️ No conversations found for user {user_id}")
logger.warning(f"No conversations found for user {user_id}")
return False
# Find all bot messages to delete from Discord
bot_messages = [conv for conv in logs["conversations"] if conv.get("is_bot_message", False)]
print(f"🔍 Found {len(bot_messages)} bot messages to delete from Discord for user {user_id}")
logger.debug(f"Found {len(bot_messages)} bot messages to delete from Discord for user {user_id}")
# Try to delete all bot messages from Discord
discord_deleted_count = 0
@@ -534,13 +537,13 @@ class DMLogger:
discord_message = await dm_channel.fetch_message(int(message_id))
await discord_message.delete()
discord_deleted_count += 1
print(f"Deleted Discord message {message_id} from DM with user {user_id}")
logger.info(f"Deleted Discord message {message_id} from DM with user {user_id}")
except Exception as e:
print(f"⚠️ Could not delete Discord message {message_id}: {e}")
logger.error(f"Could not delete Discord message {message_id}: {e}")
# Continue with other messages
except Exception as e:
print(f"⚠️ Discord bulk deletion failed: {e}")
logger.warning(f"Discord bulk deletion failed: {e}")
# Continue anyway to delete from logs
# Delete all conversations from logs regardless of Discord deletion success
@@ -548,14 +551,14 @@ class DMLogger:
self._save_user_logs(user_id, logs)
if discord_deleted_count > 0:
print(f"🗑️ Deleted {discord_deleted_count} bot messages from Discord and all {conversation_count} conversations from logs for user {user_id}")
logger.info(f"Deleted {discord_deleted_count} bot messages from Discord and all {conversation_count} conversations from logs for user {user_id}")
else:
print(f"🗑️ Deleted all {conversation_count} conversations from logs only (Discord deletion failed) for user {user_id}")
logger.info(f"Deleted all {conversation_count} conversations from logs only (Discord deletion failed) for user {user_id}")
return True
except Exception as e:
print(f"Failed to delete all conversations for user {user_id}: {e}")
logger.error(f"Failed to delete all conversations for user {user_id}: {e}")
return False
def delete_user_completely(self, user_id: int) -> bool:
@@ -564,13 +567,13 @@ class DMLogger:
log_file = self._get_user_log_file(user_id)
if os.path.exists(log_file):
os.remove(log_file)
print(f"🗑️ Completely deleted log file for user {user_id}")
logger.info(f"Completely deleted log file for user {user_id}")
return True
else:
print(f"⚠️ No log file found for user {user_id}")
logger.warning(f"No log file found for user {user_id}")
return False
except Exception as e:
print(f"Failed to delete user log file {user_id}: {e}")
logger.error(f"Failed to delete user log file {user_id}: {e}")
return False
# Global instance

View File

@@ -9,6 +9,9 @@ import os
import random
import json
import globals
from utils.logger import get_logger
logger = get_logger('persona')
# ============================================================================
# EVIL MODE PERSISTENCE
@@ -40,16 +43,16 @@ def save_evil_mode_state(saved_role_color=None):
}
with open(EVIL_MODE_STATE_FILE, "w", encoding="utf-8") as f:
json.dump(state, f, indent=2)
print(f"💾 Saved evil mode state: {state}")
logger.debug(f"Saved evil mode state: {state}")
except Exception as e:
print(f"⚠️ Failed to save evil mode state: {e}")
logger.error(f"Failed to save evil mode state: {e}")
def load_evil_mode_state():
"""Load evil mode state from JSON file"""
try:
if not os.path.exists(EVIL_MODE_STATE_FILE):
print(f" No evil mode state file found, using defaults")
logger.info(f"No evil mode state file found, using defaults")
return False, "evil_neutral", None
with open(EVIL_MODE_STATE_FILE, "r", encoding="utf-8") as f:
@@ -58,10 +61,10 @@ def load_evil_mode_state():
evil_mode = state.get("evil_mode_enabled", False)
evil_mood = state.get("evil_mood", "evil_neutral")
saved_role_color = state.get("saved_role_color")
print(f"📂 Loaded evil mode state: evil_mode={evil_mode}, mood={evil_mood}, saved_color={saved_role_color}")
logger.debug(f"Loaded evil mode state: evil_mode={evil_mode}, mood={evil_mood}, saved_color={saved_role_color}")
return evil_mode, evil_mood, saved_role_color
except Exception as e:
print(f"⚠️ Failed to load evil mode state: {e}")
logger.error(f"Failed to load evil mode state: {e}")
return False, "evil_neutral", None
@@ -70,13 +73,13 @@ def restore_evil_mode_on_startup():
evil_mode, evil_mood, saved_role_color = load_evil_mode_state()
if evil_mode:
print("😈 Restoring evil mode from previous session...")
logger.debug("Restoring evil mode from previous session...")
globals.EVIL_MODE = True
globals.EVIL_DM_MOOD = evil_mood
globals.EVIL_DM_MOOD_DESCRIPTION = load_evil_mood_description(evil_mood)
print(f"😈 Evil mode restored: {evil_mood}")
logger.info(f"Evil mode restored: {evil_mood}")
else:
print("🎤 Normal mode active")
logger.info("Normal mode active")
return evil_mode
@@ -90,7 +93,7 @@ def get_evil_miku_lore() -> str:
with open("evil_miku_lore.txt", "r", encoding="utf-8") as f:
return f.read()
except Exception as e:
print(f"⚠️ Failed to load evil_miku_lore.txt: {e}")
logger.error(f"Failed to load evil_miku_lore.txt: {e}")
return "## EVIL MIKU LORE\n[File could not be loaded]"
@@ -100,7 +103,7 @@ def get_evil_miku_prompt() -> str:
with open("evil_miku_prompt.txt", "r", encoding="utf-8") as f:
return f.read()
except Exception as e:
print(f"⚠️ Failed to load evil_miku_prompt.txt: {e}")
logger.error(f"Failed to load evil_miku_prompt.txt: {e}")
return "## EVIL MIKU PROMPT\n[File could not be loaded]"
@@ -110,7 +113,7 @@ def get_evil_miku_lyrics() -> str:
with open("evil_miku_lyrics.txt", "r", encoding="utf-8") as f:
return f.read()
except Exception as e:
print(f"⚠️ Failed to load evil_miku_lyrics.txt: {e}")
logger.error(f"Failed to load evil_miku_lyrics.txt: {e}")
return "## EVIL MIKU LYRICS\n[File could not be loaded]"
@@ -178,7 +181,7 @@ def load_evil_mood_description(mood_name: str) -> str:
with open(path, "r", encoding="utf-8") as f:
return f.read().strip()
except FileNotFoundError:
print(f"⚠️ Evil mood file '{mood_name}' not found. Falling back to evil_neutral.")
logger.warning(f"Evil mood file '{mood_name}' not found. Falling back to evil_neutral.")
try:
with open(os.path.join("moods", "evil", "evil_neutral.txt"), "r", encoding="utf-8") as f:
return f.read().strip()
@@ -338,13 +341,13 @@ async def get_current_role_color(client) -> str:
if role.name.lower() in ["miku color", "miku colour", "miku-color"]:
# Convert discord.Color to hex
hex_color = f"#{role.color.value:06x}"
print(f"🎨 Current role color: {hex_color}")
logger.debug(f"Current role color: {hex_color}")
return hex_color
print("⚠️ No 'Miku Color' role found in any server")
logger.warning("No 'Miku Color' role found in any server")
return None
except Exception as e:
print(f"⚠️ Failed to get current role color: {e}")
logger.warning(f"Failed to get current role color: {e}")
return None
@@ -377,14 +380,14 @@ async def set_role_color(client, hex_color: str):
if color_role:
await color_role.edit(color=discord_color, reason="Evil mode color change")
updated_count += 1
print(f" 🎨 Updated role color in {guild.name}: #{hex_color}")
logger.debug(f"Updated role color in {guild.name}: #{hex_color}")
except Exception as e:
print(f" ⚠️ Failed to update role color in {guild.name}: {e}")
logger.warning(f"Failed to update role color in {guild.name}: {e}")
print(f"🎨 Updated role color in {updated_count} server(s) to #{hex_color}")
logger.info(f"Updated role color in {updated_count} server(s) to #{hex_color}")
return updated_count > 0
except Exception as e:
print(f"⚠️ Failed to set role color: {e}")
logger.error(f"Failed to set role color: {e}")
return False
@@ -398,7 +401,7 @@ async def apply_evil_mode_changes(client, change_username=True, change_pfp=True,
change_nicknames: Whether to change server nicknames (default True, but skip on startup restore)
change_role_color: Whether to change role color (default True, but skip on startup restore)
"""
print("😈 Enabling Evil Mode...")
logger.info("Enabling Evil Mode...")
# Save current role color before changing (if we're actually changing it)
if change_role_color:
@@ -412,9 +415,9 @@ async def apply_evil_mode_changes(client, change_username=True, change_pfp=True,
if change_username:
try:
await client.user.edit(username="Evil Miku")
print("Changed bot username to 'Evil Miku'")
logger.debug("Changed bot username to 'Evil Miku'")
except Exception as e:
print(f"⚠️ Could not change bot username: {e}")
logger.error(f"Could not change bot username: {e}")
# Update nicknames in all servers
if change_nicknames:
@@ -431,7 +434,7 @@ async def apply_evil_mode_changes(client, change_username=True, change_pfp=True,
# Save state to file
save_evil_mode_state()
print("😈 Evil Mode enabled!")
logger.info("Evil Mode enabled!")
async def revert_evil_mode_changes(client, change_username=True, change_pfp=True, change_nicknames=True, change_role_color=True):
@@ -444,16 +447,16 @@ async def revert_evil_mode_changes(client, change_username=True, change_pfp=True
change_nicknames: Whether to change server nicknames (default True, but skip on startup restore)
change_role_color: Whether to restore role color (default True, but skip on startup restore)
"""
print("🎤 Disabling Evil Mode...")
logger.info("Disabling Evil Mode...")
globals.EVIL_MODE = False
# Change bot username back
if change_username:
try:
await client.user.edit(username="Hatsune Miku")
print("Changed bot username back to 'Hatsune Miku'")
logger.debug("Changed bot username back to 'Hatsune Miku'")
except Exception as e:
print(f"⚠️ Could not change bot username: {e}")
logger.error(f"Could not change bot username: {e}")
# Update nicknames in all servers back to normal
if change_nicknames:
@@ -469,16 +472,16 @@ async def revert_evil_mode_changes(client, change_username=True, change_pfp=True
_, _, saved_color = load_evil_mode_state()
if saved_color:
await set_role_color(client, saved_color)
print(f"🎨 Restored role color to {saved_color}")
logger.debug(f"Restored role color to {saved_color}")
else:
print("⚠️ No saved role color found, skipping color restoration")
logger.warning("No saved role color found, skipping color restoration")
except Exception as e:
print(f"⚠️ Failed to restore role color: {e}")
logger.error(f"Failed to restore role color: {e}")
# Save state to file (this will clear saved_role_color since we're back to normal)
save_evil_mode_state(saved_role_color=None)
print("🎤 Evil Mode disabled!")
logger.info("Evil Mode disabled!")
async def update_all_evil_nicknames(client):
@@ -505,9 +508,9 @@ async def update_evil_server_nickname(client, guild_id: int):
me = guild.get_member(client.user.id)
if me:
await me.edit(nick=nickname)
print(f"😈 Changed nickname to '{nickname}' in server {guild.name}")
logger.debug(f"Changed nickname to '{nickname}' in server {guild.name}")
except Exception as e:
print(f"⚠️ Failed to update evil nickname in guild {guild_id}: {e}")
logger.error(f"Failed to update evil nickname in guild {guild_id}: {e}")
async def revert_all_nicknames(client):
@@ -524,7 +527,7 @@ async def set_evil_profile_picture(client):
evil_pfp_path = "memory/profile_pictures/evil_pfp.png"
if not os.path.exists(evil_pfp_path):
print(f"⚠️ Evil profile picture not found at {evil_pfp_path}")
logger.error(f"Evil profile picture not found at {evil_pfp_path}")
return False
try:
@@ -532,10 +535,10 @@ async def set_evil_profile_picture(client):
avatar_bytes = f.read()
await client.user.edit(avatar=avatar_bytes)
print("😈 Set evil profile picture")
logger.debug("Set evil profile picture")
return True
except Exception as e:
print(f"⚠️ Failed to set evil profile picture: {e}")
logger.error(f"Failed to set evil profile picture: {e}")
return False
@@ -554,12 +557,12 @@ async def restore_normal_profile_picture(client):
avatar_bytes = f.read()
await client.user.edit(avatar=avatar_bytes)
print(f"🎤 Restored normal profile picture from {path}")
logger.debug(f"Restored normal profile picture from {path}")
return True
except Exception as e:
print(f"⚠️ Failed to restore from {path}: {e}")
logger.error(f"Failed to restore from {path}: {e}")
print("⚠️ Could not restore normal profile picture - no backup found")
logger.error("Could not restore normal profile picture - no backup found")
return False
@@ -602,4 +605,4 @@ async def rotate_evil_mood():
globals.EVIL_DM_MOOD_DESCRIPTION = load_evil_mood_description(new_mood)
save_evil_mode_state() # Save state when mood rotates
print(f"😈 Evil mood rotated from {old_mood} to {new_mood}")
logger.info(f"Evil mood rotated from {old_mood} to {new_mood}")

View File

@@ -1,4 +1,4 @@
# face_detector_manager.py
Y# face_detector_manager.py
"""
Manages on-demand starting/stopping of anime-face-detector container
to free up VRAM when not needed.
@@ -9,6 +9,9 @@ import aiohttp
import subprocess
import time
from typing import Optional, Dict
from utils.logger import get_logger
logger = get_logger('gpu')
class FaceDetectorManager:
@@ -31,7 +34,7 @@ class FaceDetectorManager:
"""
try:
if debug:
print("🚀 Starting anime-face-detector container...")
logger.debug("Starting anime-face-detector container...")
# Start container using docker compose
result = subprocess.run(
@@ -44,7 +47,7 @@ class FaceDetectorManager:
if result.returncode != 0:
if debug:
print(f"⚠️ Failed to start container: {result.stderr}")
logger.error(f"Failed to start container: {result.stderr}")
return False
# Wait for API to be ready
@@ -53,17 +56,17 @@ class FaceDetectorManager:
if await self._check_health():
self.is_running = True
if debug:
print(f"Face detector container started and ready")
logger.info(f"Face detector container started and ready")
return True
await asyncio.sleep(1)
if debug:
print(f"⚠️ Container started but API not ready after {self.STARTUP_TIMEOUT}s")
logger.warning(f"Container started but API not ready after {self.STARTUP_TIMEOUT}s")
return False
except Exception as e:
if debug:
print(f"⚠️ Error starting face detector container: {e}")
logger.error(f"Error starting face detector container: {e}")
return False
async def stop_container(self, debug: bool = False) -> bool:
@@ -75,7 +78,7 @@ class FaceDetectorManager:
"""
try:
if debug:
print("🛑 Stopping anime-face-detector container...")
logger.debug("Stopping anime-face-detector container...")
result = subprocess.run(
["docker", "compose", "stop", self.CONTAINER_NAME],
@@ -88,16 +91,16 @@ class FaceDetectorManager:
if result.returncode == 0:
self.is_running = False
if debug:
print("Face detector container stopped")
logger.info("Face detector container stopped")
return True
else:
if debug:
print(f"⚠️ Failed to stop container: {result.stderr}")
logger.error(f"Failed to stop container: {result.stderr}")
return False
except Exception as e:
if debug:
print(f"⚠️ Error stopping face detector container: {e}")
logger.error(f"Error stopping face detector container: {e}")
return False
async def _check_health(self) -> bool:
@@ -137,7 +140,7 @@ class FaceDetectorManager:
# Step 1: Unload vision model if callback provided
if unload_vision_model:
if debug:
print("📤 Unloading vision model to free VRAM...")
logger.debug("Unloading vision model to free VRAM...")
await unload_vision_model()
await asyncio.sleep(2) # Give time for VRAM to clear
@@ -145,7 +148,7 @@ class FaceDetectorManager:
if not self.is_running:
if not await self.start_container(debug=debug):
if debug:
print("⚠️ Could not start face detector container")
logger.error("Could not start face detector container")
return None
container_was_started = True
@@ -161,7 +164,7 @@ class FaceDetectorManager:
if reload_vision_model:
if debug:
print("📥 Reloading vision model...")
logger.debug("Reloading vision model...")
await reload_vision_model()
async def _detect_face_api(self, image_bytes: bytes, debug: bool = False) -> Optional[Dict]:
@@ -178,14 +181,14 @@ class FaceDetectorManager:
) as response:
if response.status != 200:
if debug:
print(f"⚠️ Face detection API returned status {response.status}")
logger.warning(f"Face detection API returned status {response.status}")
return None
result = await response.json()
if result.get('count', 0) == 0:
if debug:
print("👤 No faces detected by API")
logger.debug("No faces detected by API")
return None
detections = result.get('detections', [])
@@ -205,9 +208,9 @@ class FaceDetectorManager:
if debug:
width = int(x2 - x1)
height = int(y2 - y1)
print(f"👤 Detected {len(detections)} face(s) via API, using best at ({center_x}, {center_y}) [confidence: {confidence:.2%}]")
print(f" Bounding box: x={int(x1)}, y={int(y1)}, w={width}, h={height}")
print(f" Keypoints: {len(keypoints)} facial landmarks detected")
logger.debug(f"Detected {len(detections)} face(s) via API, using best at ({center_x}, {center_y}) [confidence: {confidence:.2%}]")
logger.debug(f" Bounding box: x={int(x1)}, y={int(y1)}, w={width}, h={height}")
logger.debug(f" Keypoints: {len(keypoints)} facial landmarks detected")
return {
'center': (center_x, center_y),
@@ -219,7 +222,7 @@ class FaceDetectorManager:
except Exception as e:
if debug:
print(f"⚠️ Error calling face detection API: {e}")
logger.error(f"Error calling face detection API: {e}")
return None

View File

@@ -10,7 +10,9 @@ import globals
from utils.twitter_fetcher import fetch_figurine_tweets_latest
from utils.image_handling import analyze_image_with_qwen, download_and_encode_image
from utils.llm import query_llama
from utils.logger import get_logger
logger = get_logger('bot')
from utils.dm_logger import dm_logger
@@ -37,14 +39,14 @@ def _ensure_dir(path: str) -> None:
def load_subscribers() -> List[int]:
try:
if os.path.exists(SUBSCRIBERS_FILE):
print(f"📁 Figurines: Loading subscribers from {SUBSCRIBERS_FILE}")
logger.debug(f"Loading subscribers from {SUBSCRIBERS_FILE}")
with open(SUBSCRIBERS_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
subs = [int(uid) for uid in data.get("subscribers", [])]
print(f"📋 Figurines: Loaded {len(subs)} subscribers")
logger.debug(f"Loaded {len(subs)} subscribers")
return subs
except Exception as e:
print(f"⚠️ Failed to load figurine subscribers: {e}")
logger.error(f"Failed to load figurine subscribers: {e}")
return []
@@ -53,85 +55,85 @@ def save_subscribers(user_ids: List[int]) -> None:
_ensure_dir(SUBSCRIBERS_FILE)
# Save as strings to be JS-safe in the API layer if needed
payload = {"subscribers": [str(uid) for uid in user_ids]}
print(f"💾 Figurines: Saving {len(user_ids)} subscribers to {SUBSCRIBERS_FILE}")
logger.debug(f"Saving {len(user_ids)} subscribers to {SUBSCRIBERS_FILE}")
with open(SUBSCRIBERS_FILE, "w", encoding="utf-8") as f:
json.dump(payload, f, indent=2)
except Exception as e:
print(f"⚠️ Failed to save figurine subscribers: {e}")
logger.error(f"Failed to save figurine subscribers: {e}")
def add_subscriber(user_id: int) -> bool:
print(f" Figurines: Adding subscriber {user_id}")
logger.info(f"Adding subscriber {user_id}")
subscribers = load_subscribers()
if user_id in subscribers:
print(f" Figurines: Subscriber {user_id} already present")
logger.info(f"Subscriber {user_id} already present")
return False
subscribers.append(user_id)
save_subscribers(subscribers)
print(f"✅ Figurines: Subscriber {user_id} added")
logger.info(f"Subscriber {user_id} added")
return True
def remove_subscriber(user_id: int) -> bool:
print(f"🗑️ Figurines: Removing subscriber {user_id}")
logger.info(f"Removing subscriber {user_id}")
subscribers = load_subscribers()
if user_id not in subscribers:
print(f" Figurines: Subscriber {user_id} was not present")
logger.info(f"Subscriber {user_id} was not present")
return False
subscribers = [uid for uid in subscribers if uid != user_id]
save_subscribers(subscribers)
print(f"✅ Figurines: Subscriber {user_id} removed")
logger.info(f"Subscriber {user_id} removed")
return True
def load_sent_tweets() -> List[str]:
try:
if os.path.exists(SENT_TWEETS_FILE):
print(f"📁 Figurines: Loading sent tweets from {SENT_TWEETS_FILE}")
logger.debug(f"Loading sent tweets from {SENT_TWEETS_FILE}")
with open(SENT_TWEETS_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
urls = data.get("urls", [])
print(f"📋 Figurines: Loaded {len(urls)} sent tweet URLs")
logger.debug(f"Loaded {len(urls)} sent tweet URLs")
return urls
except Exception as e:
print(f"⚠️ Failed to load figurine sent tweets: {e}")
logger.error(f"Failed to load figurine sent tweets: {e}")
return []
def save_sent_tweets(urls: List[str]) -> None:
try:
_ensure_dir(SENT_TWEETS_FILE)
print(f"💾 Figurines: Saving {len(urls)} sent tweet URLs to {SENT_TWEETS_FILE}")
logger.debug(f"Saving {len(urls)} sent tweet URLs to {SENT_TWEETS_FILE}")
with open(SENT_TWEETS_FILE, "w", encoding="utf-8") as f:
json.dump({"urls": urls}, f, indent=2)
except Exception as e:
print(f"⚠️ Failed to save figurine sent tweets: {e}")
logger.error(f"Failed to save figurine sent tweets: {e}")
async def choose_random_figurine_tweet() -> Dict[str, Any] | None:
"""Fetch figurine tweets from multiple sources, filter out sent, and pick one randomly."""
print("🔎 Figurines: Fetching figurine tweets by Latest across sources")
logger.info("Fetching figurine tweets by Latest across sources")
tweets = await fetch_figurine_tweets_latest(limit_per_source=10)
if not tweets:
print("📭 No figurine tweets found across sources")
logger.warning("No figurine tweets found across sources")
return None
sent_urls = set(load_sent_tweets())
fresh = [t for t in tweets if t.get("url") not in sent_urls]
print(f"🧮 Figurines: {len(tweets)} total, {len(fresh)} fresh after filtering sent")
logger.debug(f"{len(tweets)} total, {len(fresh)} fresh after filtering sent")
if not fresh:
print(" All figurine tweets have been sent before; allowing reuse")
logger.warning("All figurine tweets have been sent before; allowing reuse")
fresh = tweets
chosen = random.choice(fresh)
print(f"🎯 Chosen figurine tweet: {chosen.get('url')}")
logger.info(f"Chosen figurine tweet: {chosen.get('url')}")
return chosen
async def send_figurine_dm_to_user(client: discord.Client, user_id: int, tweet: Dict[str, Any]) -> Tuple[bool, str]:
"""Send the figurine tweet to a single subscriber via DM, with analysis and LLM commentary."""
try:
print(f"✉️ Figurines: Preparing DM to user {user_id}")
logger.debug(f"Preparing DM to user {user_id}")
user = client.get_user(user_id)
if user is None:
# Try fetching
@@ -169,7 +171,7 @@ async def send_figurine_dm_to_user(client: discord.Client, user_id: int, tweet:
img_desc = await analyze_image_with_qwen(base64_img)
base_prompt += f"\n\nImage looks like: {img_desc}"
except Exception as e:
print(f"⚠️ Image analysis failed: {e}")
logger.warning(f"Image analysis failed: {e}")
# Include tweet text too
tweet_text = tweet.get("text", "").strip()
@@ -190,14 +192,14 @@ async def send_figurine_dm_to_user(client: discord.Client, user_id: int, tweet:
# Send the tweet URL first (convert to fxtwitter for better embeds)
fx_tweet_url = convert_to_fxtwitter(tweet_url)
tweet_message = await dm.send(fx_tweet_url)
print(f"✅ Figurines: Tweet URL sent to {user_id}: {fx_tweet_url}")
logger.info(f"Tweet URL sent to {user_id}: {fx_tweet_url}")
# Log the tweet URL message
dm_logger.log_user_message(user, tweet_message, is_bot_message=True)
# Send Miku's comment
comment_message = await dm.send(miku_comment)
print(f"✅ Figurines: Miku comment sent to {user_id}")
logger.info(f"Miku comment sent to {user_id}")
# Log the comment message
dm_logger.log_user_message(user, comment_message, is_bot_message=True)
@@ -212,27 +214,27 @@ async def send_figurine_dm_to_user(client: discord.Client, user_id: int, tweet:
# Use empty user prompt since this was initiated by Miku
globals.conversation_history.setdefault(user_id_str, []).append((tweet_context, miku_comment))
print(f"📝 Figurines: Messages logged to both DM history and conversation context for user {user_id}")
logger.debug(f"Messages logged to both DM history and conversation context for user {user_id}")
return True, "ok"
except Exception as e:
print(f"❌ Figurines: Failed DM to {user_id}: {e}")
logger.error(f"Failed DM to {user_id}: {e}")
return False, f"{e}"
async def send_figurine_dm_to_single_user(client: discord.Client, user_id: int, tweet_url: str = None) -> Dict[str, Any]:
"""Send a figurine tweet to a single user, either from search or specific URL."""
print(f"🎯 Figurines: Sending DM to single user {user_id}")
logger.info(f"Sending DM to single user {user_id}")
if tweet_url:
# Use specific tweet URL
print(f"📎 Figurines: Using specific tweet URL: {tweet_url}")
logger.info(f"Using specific tweet URL: {tweet_url}")
tweet = await fetch_specific_tweet_by_url(tweet_url)
if not tweet:
return {"status": "error", "message": "Failed to fetch specified tweet"}
else:
# Search for a random tweet
print("🔎 Figurines: Searching for random figurine tweet")
logger.info("Searching for random figurine tweet")
tweet = await choose_random_figurine_tweet()
if not tweet:
return {"status": "error", "message": "No figurine tweets found"}
@@ -256,7 +258,7 @@ async def send_figurine_dm_to_single_user(client: discord.Client, user_id: int,
"failed": [],
"tweet": {"url": tweet.get("url", ""), "username": tweet.get("username", "")}
}
print(f"✅ Figurines: Single user DM sent successfully → {result}")
logger.info(f"Single user DM sent successfully → {result}")
return result
else:
result = {
@@ -265,27 +267,27 @@ async def send_figurine_dm_to_single_user(client: discord.Client, user_id: int,
"failed": [{"user_id": str(user_id), "error": msg}],
"message": f"Failed to send DM: {msg}"
}
print(f"❌ Figurines: Single user DM failed → {result}")
logger.error(f"Single user DM failed → {result}")
return result
async def fetch_specific_tweet_by_url(tweet_url: str) -> Dict[str, Any] | None:
"""Fetch a specific tweet by URL for manual figurine notifications."""
try:
print(f"🔗 Figurines: Fetching specific tweet from URL: {tweet_url}")
logger.debug(f"Fetching specific tweet from URL: {tweet_url}")
# Extract tweet ID from URL
tweet_id = None
if "/status/" in tweet_url:
try:
tweet_id = tweet_url.split("/status/")[1].split("?")[0].split("/")[0]
print(f"📋 Figurines: Extracted tweet ID: {tweet_id}")
logger.debug(f"Extracted tweet ID: {tweet_id}")
except Exception as e:
print(f"❌ Figurines: Failed to extract tweet ID from URL: {e}")
logger.error(f"Failed to extract tweet ID from URL: {e}")
return None
if not tweet_id:
print("❌ Figurines: Could not extract tweet ID from URL")
logger.error("Could not extract tweet ID from URL")
return None
# Set up twscrape API (same pattern as existing functions)
@@ -313,15 +315,15 @@ async def fetch_specific_tweet_by_url(tweet_url: str) -> Dict[str, Any] | None:
# Try to fetch the tweet using search instead of tweet_details
# Search for the specific tweet ID should return it if accessible
print(f"🔍 Figurines: Searching for tweet with ID {tweet_id}")
logger.debug(f"Searching for tweet with ID {tweet_id}")
search_results = []
try:
# Search using the tweet ID - this should find the specific tweet
from twscrape import gather
search_results = await gather(api.search(f"{tweet_id}", limit=1))
print(f"🔍 Figurines: Search returned {len(search_results)} results")
logger.debug(f"Search returned {len(search_results)} results")
except Exception as search_error:
print(f"⚠️ Figurines: Search failed: {search_error}")
logger.warning(f"Search failed: {search_error}")
return None
# Check if we found the tweet
@@ -329,21 +331,21 @@ async def fetch_specific_tweet_by_url(tweet_url: str) -> Dict[str, Any] | None:
for tweet in search_results:
if str(tweet.id) == str(tweet_id):
tweet_data = tweet
print(f"✅ Figurines: Found matching tweet with ID {tweet.id}")
logger.debug(f"Found matching tweet with ID {tweet.id}")
break
if not tweet_data and search_results:
# If no exact match but we have results, use the first one
tweet_data = search_results[0]
print(f"🔍 Figurines: Using first search result with ID {tweet_data.id}")
logger.debug(f"Using first search result with ID {tweet_data.id}")
if tweet_data:
# Extract data using the same pattern as the working search code
username = tweet_data.user.username if hasattr(tweet_data, 'user') and tweet_data.user else "unknown"
text_content = tweet_data.rawContent if hasattr(tweet_data, 'rawContent') else ""
print(f"🔍 Figurines: Found tweet from @{username}")
print(f"🔍 Figurines: Tweet text: {text_content[:100]}...")
logger.debug(f"Found tweet from @{username}")
logger.debug(f"Tweet text: {text_content[:100]}...")
# For media, we'll need to extract it from the tweet_url using the same method as other functions
# But for now, let's see if we can get basic tweet data working first
@@ -354,37 +356,37 @@ async def fetch_specific_tweet_by_url(tweet_url: str) -> Dict[str, Any] | None:
"media": [] # We'll add media extraction later
}
print(f"✅ Figurines: Successfully fetched tweet from @{result['username']}")
logger.info(f"Successfully fetched tweet from @{result['username']}")
return result
else:
print("❌ Figurines: No tweet found with the specified ID")
logger.error("No tweet found with the specified ID")
return None
except Exception as e:
print(f"❌ Figurines: Error fetching tweet by URL: {e}")
logger.error(f"Error fetching tweet by URL: {e}")
return None
async def send_figurine_dm_to_all_subscribers(client: discord.Client, tweet_url: str = None) -> Dict[str, Any]:
"""Pick a figurine tweet and DM it to all subscribers, recording the sent URL."""
print("🚀 Figurines: Sending figurine DM to all subscribers")
logger.info("Sending figurine DM to all subscribers")
subscribers = load_subscribers()
if not subscribers:
print(" Figurines: No subscribers configured")
logger.warning("No subscribers configured")
return {"status": "no_subscribers"}
if tweet_url:
# Use specific tweet URL
print(f"📎 Figurines: Using specific tweet URL for all subscribers: {tweet_url}")
logger.info(f"Using specific tweet URL for all subscribers: {tweet_url}")
tweet = await fetch_specific_tweet_by_url(tweet_url)
if not tweet:
print(" Figurines: Failed to fetch specified tweet")
logger.warning("Failed to fetch specified tweet")
return {"status": "no_tweet", "message": "Failed to fetch specified tweet"}
else:
# Search for random tweet
tweet = await choose_random_figurine_tweet()
if tweet is None:
print(" Figurines: No tweet to send")
logger.warning("No tweet to send")
return {"status": "no_tweet"}
results = {"sent": [], "failed": []}
@@ -393,7 +395,7 @@ async def send_figurine_dm_to_all_subscribers(client: discord.Client, tweet_url:
if ok:
results["sent"].append(str(uid))
else:
print(f"⚠️ Failed to DM user {uid}: {msg}")
logger.warning(f"Failed to DM user {uid}: {msg}")
results["failed"].append({"user_id": str(uid), "error": msg})
# Record as sent if at least one success to avoid repeats
@@ -407,7 +409,7 @@ async def send_figurine_dm_to_all_subscribers(client: discord.Client, tweet_url:
save_sent_tweets(sent_urls)
summary = {"status": "ok", **results, "tweet": {"url": tweet.get("url", ""), "username": tweet.get("username", "")}}
print(f"📦 Figurines: DM send complete → {summary}")
logger.info(f"DM send complete → {summary}")
return summary

View File

@@ -14,6 +14,9 @@ import time
from typing import Optional, Tuple
import globals
from utils.llm import query_llama
from utils.logger import get_logger
logger = get_logger('media')
# Image generation detection patterns
IMAGE_REQUEST_PATTERNS = [
@@ -133,11 +136,11 @@ def find_latest_generated_image(prompt_id: str, expected_filename: str = None) -
recent_threshold = time.time() - 600 # 10 minutes
for file_path in all_files:
if os.path.getmtime(file_path) > recent_threshold:
print(f"🎨 Found recent image: {file_path}")
logger.debug(f"Found recent image: {file_path}")
return file_path
except Exception as e:
print(f"⚠️ Error searching in {output_dir}: {e}")
logger.error(f"Error searching in {output_dir}: {e}")
continue
return None
@@ -156,7 +159,7 @@ async def generate_image_with_comfyui(prompt: str) -> Optional[str]:
# Load the workflow template
workflow_path = "Miku_BasicWorkflow.json"
if not os.path.exists(workflow_path):
print(f"Workflow template not found: {workflow_path}")
logger.error(f"Workflow template not found: {workflow_path}")
return None
with open(workflow_path, 'r') as f:
@@ -186,29 +189,29 @@ async def generate_image_with_comfyui(prompt: str) -> Optional[str]:
async with test_session.get(f"{url}/system_stats", timeout=timeout) as test_response:
if test_response.status == 200:
comfyui_url = url
print(f"ComfyUI found at: {url}")
logger.debug(f"ComfyUI found at: {url}")
break
except:
continue
if not comfyui_url:
print(f"ComfyUI not reachable at any of: {comfyui_urls}")
logger.error(f"ComfyUI not reachable at any of: {comfyui_urls}")
return None
async with aiohttp.ClientSession() as session:
# Submit the generation request
async with session.post(f"{comfyui_url}/prompt", json=payload) as response:
if response.status != 200:
print(f"ComfyUI request failed: {response.status}")
logger.error(f"ComfyUI request failed: {response.status}")
return None
result = await response.json()
prompt_id = result.get("prompt_id")
if not prompt_id:
print("No prompt_id received from ComfyUI")
logger.error("No prompt_id received from ComfyUI")
return None
print(f"🎨 ComfyUI generation started with prompt_id: {prompt_id}")
logger.info(f"ComfyUI generation started with prompt_id: {prompt_id}")
# Poll for completion (timeout after 5 minutes)
timeout = 300 # 5 minutes
@@ -242,20 +245,20 @@ async def generate_image_with_comfyui(prompt: str) -> Optional[str]:
# Verify the file exists before returning
if os.path.exists(image_path):
print(f"Image generated successfully: {image_path}")
logger.info(f"Image generated successfully: {image_path}")
return image_path
else:
# Try alternative paths in case of different mounting
alt_path = os.path.join("/app/ComfyUI/output", filename)
if os.path.exists(alt_path):
print(f"Image generated successfully: {alt_path}")
logger.info(f"Image generated successfully: {alt_path}")
return alt_path
else:
print(f"⚠️ Generated image not found at expected paths: {image_path} or {alt_path}")
logger.warning(f"Generated image not found at expected paths: {image_path} or {alt_path}")
continue
# If we couldn't find the image via API, try the fallback method
print("🔍 Image not found via API, trying fallback method...")
logger.debug("Image not found via API, trying fallback method...")
fallback_image = find_latest_generated_image(prompt_id)
if fallback_image:
return fallback_image
@@ -263,19 +266,19 @@ async def generate_image_with_comfyui(prompt: str) -> Optional[str]:
# Wait before polling again
await asyncio.sleep(2)
print("ComfyUI generation timed out")
logger.error("ComfyUI generation timed out")
# Final fallback: look for the most recent image
print("🔍 Trying final fallback: most recent image...")
logger.debug("Trying final fallback: most recent image...")
fallback_image = find_latest_generated_image(prompt_id)
if fallback_image:
print(f"Found image via fallback method: {fallback_image}")
logger.info(f"Found image via fallback method: {fallback_image}")
return fallback_image
return None
except Exception as e:
print(f"Error in generate_image_with_comfyui: {e}")
logger.error(f"Error in generate_image_with_comfyui: {e}")
return None
async def handle_image_generation_request(message, prompt: str) -> bool:
@@ -307,7 +310,7 @@ async def handle_image_generation_request(message, prompt: str) -> bool:
# Start typing to show we're working
async with message.channel.typing():
# Generate the image
print(f"🎨 Starting image generation for prompt: {prompt}")
logger.info(f"Starting image generation for prompt: {prompt}")
image_path = await generate_image_with_comfyui(prompt)
if image_path and os.path.exists(image_path):
@@ -322,7 +325,7 @@ async def handle_image_generation_request(message, prompt: str) -> bool:
await message.channel.send(completion_response, file=file)
print(f"Image sent successfully to {message.author.display_name}")
logger.info(f"Image sent successfully to {message.author.display_name}")
# Log to DM history if it's a DM
if is_dm:
@@ -336,11 +339,11 @@ async def handle_image_generation_request(message, prompt: str) -> bool:
error_response = await query_llama(error_prompt, user_id=user_id, guild_id=guild_id, response_type=response_type)
await message.channel.send(error_response)
print(f"Image generation failed for prompt: {prompt}")
logger.error(f"Image generation failed for prompt: {prompt}")
return False
except Exception as e:
print(f"Error in handle_image_generation_request: {e}")
logger.error(f"Error in handle_image_generation_request: {e}")
# Send error message
try:

View File

@@ -10,6 +10,10 @@ from PIL import Image
import re
import globals
from utils.logger import get_logger
logger = get_logger('vision')
# No need for switch_model anymore - llama-swap handles this automatically
@@ -47,7 +51,7 @@ async def extract_tenor_gif_url(tenor_url):
match = re.search(r'tenor\.com/(\d+)\.gif', tenor_url)
if not match:
print(f"⚠️ Could not extract Tenor GIF ID from: {tenor_url}")
logger.warning(f"Could not extract Tenor GIF ID from: {tenor_url}")
return None
gif_id = match.group(1)
@@ -60,7 +64,7 @@ async def extract_tenor_gif_url(tenor_url):
async with aiohttp.ClientSession() as session:
async with session.head(media_url) as resp:
if resp.status == 200:
print(f"Found Tenor GIF: {media_url}")
logger.debug(f"Found Tenor GIF: {media_url}")
return media_url
# If that didn't work, try alternative formats
@@ -69,14 +73,14 @@ async def extract_tenor_gif_url(tenor_url):
async with aiohttp.ClientSession() as session:
async with session.head(alt_url) as resp:
if resp.status == 200:
print(f"Found Tenor GIF (alternative): {alt_url}")
logger.debug(f"Found Tenor GIF (alternative): {alt_url}")
return alt_url
print(f"⚠️ Could not find working Tenor media URL for ID: {gif_id}")
logger.warning(f"Could not find working Tenor media URL for ID: {gif_id}")
return None
except Exception as e:
print(f"⚠️ Error extracting Tenor GIF URL: {e}")
logger.error(f"Error extracting Tenor GIF URL: {e}")
return None
@@ -114,7 +118,7 @@ async def convert_gif_to_mp4(gif_bytes):
with open(temp_mp4_path, 'rb') as f:
mp4_bytes = f.read()
print(f"Converted GIF to MP4 ({len(gif_bytes)} bytes → {len(mp4_bytes)} bytes)")
logger.info(f"Converted GIF to MP4 ({len(gif_bytes)} bytes → {len(mp4_bytes)} bytes)")
return mp4_bytes
finally:
@@ -125,10 +129,10 @@ async def convert_gif_to_mp4(gif_bytes):
os.remove(temp_mp4_path)
except subprocess.CalledProcessError as e:
print(f"⚠️ ffmpeg error converting GIF to MP4: {e.stderr.decode()}")
logger.error(f"ffmpeg error converting GIF to MP4: {e.stderr.decode()}")
return None
except Exception as e:
print(f"⚠️ Error converting GIF to MP4: {e}")
logger.error(f"Error converting GIF to MP4: {e}")
import traceback
traceback.print_exc()
return None
@@ -165,7 +169,7 @@ async def extract_video_frames(video_bytes, num_frames=4):
if frames:
return frames
except Exception as e:
print(f"Not a GIF, trying video extraction: {e}")
logger.debug(f"Not a GIF, trying video extraction: {e}")
# For video files (MP4, WebM, etc.), use ffmpeg
import subprocess
@@ -222,7 +226,7 @@ async def extract_video_frames(video_bytes, num_frames=4):
os.remove(temp_video_path)
except Exception as e:
print(f"⚠️ Error extracting frames: {e}")
logger.error(f"Error extracting frames: {e}")
import traceback
traceback.print_exc()
@@ -271,10 +275,10 @@ async def analyze_image_with_vision(base64_img):
return data.get("choices", [{}])[0].get("message", {}).get("content", "No description.")
else:
error_text = await response.text()
print(f"Vision API error: {response.status} - {error_text}")
logger.error(f"Vision API error: {response.status} - {error_text}")
return f"Error analyzing image: {response.status}"
except Exception as e:
print(f"⚠️ Error in analyze_image_with_vision: {e}")
logger.error(f"Error in analyze_image_with_vision: {e}")
return f"Error analyzing image: {str(e)}"
@@ -333,10 +337,10 @@ async def analyze_video_with_vision(video_frames, media_type="video"):
return data.get("choices", [{}])[0].get("message", {}).get("content", "No description.")
else:
error_text = await response.text()
print(f"Vision API error: {response.status} - {error_text}")
logger.error(f"Vision API error: {response.status} - {error_text}")
return f"Error analyzing video: {response.status}"
except Exception as e:
print(f"⚠️ Error in analyze_video_with_vision: {e}")
logger.error(f"Error in analyze_video_with_vision: {e}")
return f"Error analyzing video: {str(e)}"

View File

@@ -3,6 +3,9 @@
import random
import globals
from utils.llm import query_llama # Adjust path as needed
from utils.logger import get_logger
logger = get_logger('bot')
async def detect_and_react_to_kindness(message, after_reply=False, server_context=None):
@@ -19,14 +22,14 @@ async def detect_and_react_to_kindness(message, after_reply=False, server_contex
await message.add_reaction(emoji)
globals.kindness_reacted_messages.add(message.id)
message.kindness_reacted = True # Mark as done
print("Kindness detected via keywords. Reacted immediately.")
logger.info("Kindness detected via keywords. Reacted immediately.")
except Exception as e:
print(f"⚠️ Error adding reaction: {e}")
logger.error(f"Error adding reaction: {e}")
return
# 2. If not after_reply, defer model-based check
if not after_reply:
print("🗝️ No kindness via keywords. Deferring...")
logger.debug("No kindness via keywords. Deferring...")
return
# 3. Model-based detection
@@ -42,8 +45,8 @@ async def detect_and_react_to_kindness(message, after_reply=False, server_contex
if result.strip().lower().startswith("yes"):
await message.add_reaction(emoji)
globals.kindness_reacted_messages.add(message.id)
print("Kindness detected via model. Reacted.")
logger.info("Kindness detected via model. Reacted.")
else:
print("🧊 No kindness detected.")
logger.debug("No kindness detected.")
except Exception as e:
print(f"⚠️ Error during kindness analysis: {e}")
logger.error(f"Error during kindness analysis: {e}")

View File

@@ -10,6 +10,10 @@ import os
from utils.context_manager import get_context_for_response_type, get_complete_context
from utils.moods import load_mood_description
from utils.conversation_history import conversation_history
from utils.logger import get_logger
logger = get_logger('llm')
def get_current_gpu_url():
"""Get the URL for the currently selected GPU for text models"""
@@ -23,7 +27,7 @@ def get_current_gpu_url():
else:
return globals.LLAMA_URL
except Exception as e:
print(f"⚠️ GPU state read error: {e}, defaulting to NVIDIA")
logger.warning(f"GPU state read error: {e}, defaulting to NVIDIA")
# Default to NVIDIA if state file doesn't exist
return globals.LLAMA_URL
@@ -102,7 +106,7 @@ async def query_llama(user_prompt, user_id, guild_id=None, response_type="dm_res
if model is None:
if evil_mode:
model = globals.EVIL_TEXT_MODEL # Use DarkIdol uncensored model
print(f"😈 Using evil model: {model}")
logger.info(f"Using evil model: {model}")
else:
model = globals.TEXT_MODEL
@@ -155,7 +159,7 @@ You ARE Miku. Act like it."""
is_sleeping = False
forced_angry_until = None
just_woken_up = False
print(f"😈 Using Evil mode with mood: {current_mood_name}")
logger.info(f"Using Evil mode with mood: {current_mood_name}")
else:
current_mood = globals.DM_MOOD_DESCRIPTION # Default to DM mood
current_mood_name = globals.DM_MOOD # Default to DM mood name
@@ -175,14 +179,14 @@ You ARE Miku. Act like it."""
is_sleeping = server_config.is_sleeping
forced_angry_until = server_config.forced_angry_until
just_woken_up = server_config.just_woken_up
print(f"🎭 Using server mood: {current_mood_name} for guild {guild_id}")
logger.debug(f"Using server mood: {current_mood_name} for guild {guild_id}")
else:
print(f"⚠️ No server config found for guild {guild_id}, using DM mood")
logger.warning(f"No server config found for guild {guild_id}, using DM mood")
except Exception as e:
print(f"⚠️ Failed to get server mood for guild {guild_id}, falling back to DM mood: {e}")
logger.error(f"Failed to get server mood for guild {guild_id}, falling back to DM mood: {e}")
# Fall back to DM mood if server mood fails
elif not evil_mode:
print(f"🌍 Using DM mood: {globals.DM_MOOD}")
logger.debug(f"Using DM mood: {globals.DM_MOOD}")
# Append angry wake-up note if JUST_WOKEN_UP flag is set (only in non-evil mode)
if just_woken_up and not evil_mode:
@@ -262,7 +266,7 @@ Please respond in a way that reflects this emotional tone.{pfp_context}"""
try:
# Get current GPU URL based on user selection
llama_url = get_current_gpu_url()
print(f"🎮 Using GPU endpoint: {llama_url}")
logger.debug(f"Using GPU endpoint: {llama_url}")
# Add timeout to prevent hanging indefinitely
timeout = aiohttp.ClientTimeout(total=300) # 300 second timeout
@@ -301,13 +305,13 @@ Please respond in a way that reflects this emotional tone.{pfp_context}"""
return reply
else:
error_text = await response.text()
print(f"Error from llama-swap: {response.status} - {error_text}")
logger.error(f"Error from llama-swap: {response.status} - {error_text}")
# Don't save error responses to conversation history
return f"Error: {response.status}"
except asyncio.TimeoutError:
return "Sorry, the response took too long. Please try again."
except Exception as e:
print(f"⚠️ Error in query_llama: {e}")
logger.error(f"Error in query_llama: {e}")
return f"Sorry, there was an error: {str(e)}"
# Backward compatibility alias for existing code

286
bot/utils/log_config.py Normal file
View File

@@ -0,0 +1,286 @@
"""
Log Configuration Manager
Handles runtime configuration updates for the logging system.
Provides API for the web UI to update log settings without restarting the bot.
"""
from pathlib import Path
from typing import Dict, List, Optional
import json
try:
from utils.logger import get_logger
logger = get_logger('core')
except Exception:
logger = None
CONFIG_FILE = Path('/app/memory/log_settings.json')
def load_config() -> Dict:
"""Load log configuration from file."""
from utils.logger import get_log_config
return get_log_config()
def save_config(config: Dict) -> bool:
"""
Save log configuration to file.
Args:
config: Configuration dictionary
Returns:
True if successful, False otherwise
"""
try:
from utils.logger import save_config
save_config(config)
return True
except Exception as e:
if logger:
logger.error(f"Failed to save log config: {e}")
print(f"Failed to save log config: {e}")
return False
def update_component(component: str, enabled: bool = None, enabled_levels: List[str] = None) -> bool:
"""
Update a single component's configuration.
Args:
component: Component name
enabled: Enable/disable the component
enabled_levels: List of log levels to enable (DEBUG, INFO, WARNING, ERROR, CRITICAL, API)
Returns:
True if successful, False otherwise
"""
try:
config = load_config()
if component not in config['components']:
return False
if enabled is not None:
config['components'][component]['enabled'] = enabled
if enabled_levels is not None:
valid_levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL', 'API']
# Validate all levels
for level in enabled_levels:
if level.upper() not in valid_levels:
return False
config['components'][component]['enabled_levels'] = [l.upper() for l in enabled_levels]
return save_config(config)
except Exception as e:
if logger:
logger.error(f"Failed to update component {component}: {e}")
print(f"Failed to update component {component}: {e}")
return False
def update_global_level(level: str, enabled: bool) -> bool:
"""
Enable or disable a specific log level across all components.
Args:
level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL, API)
enabled: True to enable, False to disable
Returns:
True if successful, False otherwise
"""
try:
level = level.upper()
valid_levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL', 'API']
if level not in valid_levels:
return False
config = load_config()
# Update all components
for component_name in config['components'].keys():
current_levels = config['components'][component_name].get('enabled_levels', [])
if enabled:
# Add level if not present
if level not in current_levels:
current_levels.append(level)
else:
# Remove level if present
if level in current_levels:
current_levels.remove(level)
config['components'][component_name]['enabled_levels'] = current_levels
return save_config(config)
except Exception as e:
if logger:
logger.error(f"Failed to update global level {level}: {e}")
print(f"Failed to update global level {level}: {e}")
return False
def update_timestamp_format(format_type: str) -> bool:
"""
Update timestamp format for all log outputs.
Args:
format_type: Format type - 'off', 'time', 'date', or 'datetime'
Returns:
True if successful, False otherwise
"""
try:
valid_formats = ['off', 'time', 'date', 'datetime']
if format_type not in valid_formats:
return False
config = load_config()
if 'formatting' not in config:
config['formatting'] = {}
config['formatting']['timestamp_format'] = format_type
return save_config(config)
except Exception as e:
if logger:
logger.error(f"Failed to update timestamp format: {e}")
print(f"Failed to update timestamp format: {e}")
return False
def update_api_filters(
exclude_paths: List[str] = None,
exclude_status: List[int] = None,
include_slow_requests: bool = None,
slow_threshold_ms: int = None
) -> bool:
"""
Update API request filtering configuration.
Args:
exclude_paths: List of path patterns to exclude (e.g., ['/health', '/static/*'])
exclude_status: List of HTTP status codes to exclude (e.g., [200, 304])
include_slow_requests: Whether to log slow requests
slow_threshold_ms: Threshold for slow requests in milliseconds
Returns:
True if successful, False otherwise
"""
try:
config = load_config()
if 'api.requests' not in config['components']:
return False
filters = config['components']['api.requests'].get('filters', {})
if exclude_paths is not None:
filters['exclude_paths'] = exclude_paths
if exclude_status is not None:
filters['exclude_status'] = exclude_status
if include_slow_requests is not None:
filters['include_slow_requests'] = include_slow_requests
if slow_threshold_ms is not None:
filters['slow_threshold_ms'] = slow_threshold_ms
config['components']['api.requests']['filters'] = filters
return save_config(config)
except Exception as e:
if logger:
logger.error(f"Failed to update API filters: {e}")
print(f"Failed to update API filters: {e}")
return False
def reset_to_defaults() -> bool:
"""
Reset configuration to defaults.
Returns:
True if successful, False otherwise
"""
try:
from utils.logger import get_default_config, save_config
default_config = get_default_config()
save_config(default_config)
return True
except Exception as e:
if logger:
logger.error(f"Failed to reset config: {e}")
print(f"Failed to reset config: {e}")
return False
def get_component_config(component: str) -> Optional[Dict]:
"""
Get configuration for a specific component.
Args:
component: Component name
Returns:
Component configuration dictionary or None
"""
try:
config = load_config()
return config['components'].get(component)
except Exception:
return None
def is_component_enabled(component: str) -> bool:
"""
Check if a component is enabled.
Args:
component: Component name
Returns:
True if enabled, False otherwise
"""
component_config = get_component_config(component)
if component_config is None:
return True # Default to enabled
return component_config.get('enabled', True)
def get_component_level(component: str) -> str:
"""
Get log level for a component.
Args:
component: Component name
Returns:
Log level string (e.g., 'INFO', 'DEBUG')
"""
component_config = get_component_config(component)
if component_config is None:
return 'INFO' # Default level
return component_config.get('level', 'INFO')
def reload_all_loggers():
"""Reload all logger configurations."""
try:
from utils.logger import reload_config
reload_config()
return True
except Exception as e:
if logger:
logger.error(f"Failed to reload loggers: {e}")
print(f"Failed to reload loggers: {e}")
return False

395
bot/utils/logger.py Normal file
View File

@@ -0,0 +1,395 @@
"""
Centralized Logging System for Miku Discord Bot
This module provides a robust, component-based logging system with:
- Configurable log levels per component
- Emoji-based log formatting
- Multiple output handlers (console, separate log files per component)
- Runtime configuration updates
- API request filtering
- Docker-compatible output
Usage:
from utils.logger import get_logger
logger = get_logger('bot')
logger.info("Bot started successfully")
logger.error("Failed to connect", exc_info=True)
"""
import logging
import sys
import os
from pathlib import Path
from typing import Optional, Dict
from logging.handlers import RotatingFileHandler
import json
# Log level emojis
LEVEL_EMOJIS = {
'DEBUG': '🔍',
'INFO': '',
'WARNING': '⚠️',
'ERROR': '',
'CRITICAL': '🔥',
'API': '🌐',
}
# Custom API log level (between INFO and WARNING)
API_LEVEL = 25
logging.addLevelName(API_LEVEL, 'API')
# Component definitions
COMPONENTS = {
'bot': 'Main bot lifecycle and events',
'api': 'FastAPI endpoints (non-HTTP)',
'api.requests': 'HTTP request/response logs',
'autonomous': 'Autonomous messaging system',
'persona': 'Bipolar/persona dialogue system',
'vision': 'Image and video processing',
'llm': 'LLM API calls and interactions',
'conversation': 'Conversation history management',
'mood': 'Mood system and state changes',
'dm': 'Direct message handling',
'scheduled': 'Scheduled tasks and cron jobs',
'gpu': 'GPU routing and model management',
'media': 'Media processing (audio, video, images)',
'server': 'Server management and configuration',
'commands': 'Command handling and routing',
'sentiment': 'Sentiment analysis',
'core': 'Core utilities and helpers',
'apscheduler': 'Job scheduler logs (APScheduler)',
}
# Global configuration
_log_config: Optional[Dict] = None
_loggers: Dict[str, logging.Logger] = {}
_handlers_initialized = False
# Log directory (in mounted volume so logs persist)
LOG_DIR = Path(os.getenv('LOG_DIR', '/app/memory/logs'))
class EmojiFormatter(logging.Formatter):
"""Custom formatter that adds emojis and colors to log messages."""
def __init__(self, use_emojis=True, use_colors=False, timestamp_format='datetime', *args, **kwargs):
super().__init__(*args, **kwargs)
self.use_emojis = use_emojis
self.use_colors = use_colors
self.timestamp_format = timestamp_format
def format(self, record):
# Add emoji prefix
if self.use_emojis:
emoji = LEVEL_EMOJIS.get(record.levelname, '')
record.levelname_emoji = f"{emoji} {record.levelname}"
else:
record.levelname_emoji = record.levelname
# Format timestamp based on settings
if self.timestamp_format == 'off':
record.timestamp_formatted = ''
elif self.timestamp_format == 'time':
record.timestamp_formatted = self.formatTime(record, '%H:%M:%S') + ' '
elif self.timestamp_format == 'date':
record.timestamp_formatted = self.formatTime(record, '%Y-%m-%d') + ' '
elif self.timestamp_format == 'datetime':
record.timestamp_formatted = self.formatTime(record, '%Y-%m-%d %H:%M:%S') + ' '
else:
# Default to datetime if invalid option
record.timestamp_formatted = self.formatTime(record, '%Y-%m-%d %H:%M:%S') + ' '
# Format the message
return super().format(record)
class ComponentFilter(logging.Filter):
"""Filter logs based on component configuration with individual level toggles."""
def __init__(self, component_name: str):
super().__init__()
self.component_name = component_name
def filter(self, record):
"""Check if this log should be output based on enabled levels."""
config = get_log_config()
if not config:
return True
component_config = config.get('components', {}).get(self.component_name, {})
# Check if component is enabled
if not component_config.get('enabled', True):
return False
# Check if specific log level is enabled
enabled_levels = component_config.get('enabled_levels', ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL', 'API'])
# Get the level name for this record
level_name = logging.getLevelName(record.levelno)
return level_name in enabled_levels
def get_log_config() -> Optional[Dict]:
"""Get current log configuration."""
global _log_config
if _log_config is None:
# Try to load from file
config_path = Path('/app/memory/log_settings.json')
if config_path.exists():
try:
with open(config_path, 'r') as f:
_log_config = json.load(f)
except Exception:
_log_config = get_default_config()
else:
_log_config = get_default_config()
return _log_config
def get_default_config() -> Dict:
"""Get default logging configuration."""
# Read from environment variables
# Enable api.requests by default (now that uvicorn access logs are disabled)
enable_api_requests = os.getenv('LOG_ENABLE_API_REQUESTS', 'true').lower() == 'true'
use_emojis = os.getenv('LOG_USE_EMOJIS', 'true').lower() == 'true'
config = {
'version': '1.0',
'formatting': {
'use_emojis': use_emojis,
'use_colors': False,
'timestamp_format': 'datetime' # Options: 'off', 'time', 'date', 'datetime'
},
'components': {}
}
# Set defaults for each component
for component in COMPONENTS.keys():
if component == 'api.requests':
# API requests component defaults to only ERROR and CRITICAL
default_levels = ['ERROR', 'CRITICAL'] if not enable_api_requests else ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL', 'API']
config['components'][component] = {
'enabled': enable_api_requests,
'enabled_levels': default_levels,
'filters': {
'exclude_paths': ['/health', '/static/*'],
'exclude_status': [200, 304] if not enable_api_requests else [],
'include_slow_requests': True,
'slow_threshold_ms': 1000
}
}
elif component == 'apscheduler':
# APScheduler defaults to WARNING and above (lots of INFO noise)
config['components'][component] = {
'enabled': True,
'enabled_levels': ['WARNING', 'ERROR', 'CRITICAL']
}
else:
# All other components default to all levels enabled
config['components'][component] = {
'enabled': True,
'enabled_levels': ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
}
return config
def reload_config():
"""Reload configuration from file."""
global _log_config
_log_config = None
get_log_config()
# Update all existing loggers
for component_name, logger in _loggers.items():
_configure_logger(logger, component_name)
def save_config(config: Dict):
"""Save configuration to file."""
global _log_config
_log_config = config
config_path = Path('/app/memory/log_settings.json')
config_path.parent.mkdir(parents=True, exist_ok=True)
with open(config_path, 'w') as f:
json.dump(config, f, indent=2)
# Reload all loggers
reload_config()
def _setup_handlers():
"""Set up log handlers (console and file)."""
global _handlers_initialized
if _handlers_initialized:
return
# Create log directory
LOG_DIR.mkdir(parents=True, exist_ok=True)
_handlers_initialized = True
def _configure_logger(logger: logging.Logger, component_name: str):
"""Configure a logger with handlers and filters."""
config = get_log_config()
formatting = config.get('formatting', {})
# Clear existing handlers
logger.handlers.clear()
# Set logger level to DEBUG so handlers can filter
logger.setLevel(logging.DEBUG)
logger.propagate = False
# Create formatter
timestamp_format = formatting.get('timestamp_format', 'datetime') # 'off', 'time', 'date', or 'datetime'
use_emojis = formatting.get('use_emojis', True)
use_colors = formatting.get('use_colors', False)
# Console handler - goes to Docker logs
console_handler = logging.StreamHandler(sys.stdout)
console_formatter = EmojiFormatter(
fmt='%(timestamp_formatted)s[%(levelname_emoji)s] [%(name)s] %(message)s',
use_emojis=use_emojis,
use_colors=use_colors,
timestamp_format=timestamp_format
)
console_handler.setFormatter(console_formatter)
console_handler.addFilter(ComponentFilter(component_name))
logger.addHandler(console_handler)
# File handler - separate file per component
log_file = LOG_DIR / f'{component_name.replace(".", "_")}.log'
file_handler = RotatingFileHandler(
log_file,
maxBytes=10 * 1024 * 1024, # 10MB
backupCount=5,
encoding='utf-8'
)
file_formatter = EmojiFormatter(
fmt='%(timestamp_formatted)s[%(levelname)s] [%(name)s] %(message)s',
use_emojis=False, # No emojis in file logs
use_colors=False,
timestamp_format=timestamp_format
)
file_handler.setFormatter(file_formatter)
file_handler.addFilter(ComponentFilter(component_name))
logger.addHandler(file_handler)
def get_logger(component: str) -> logging.Logger:
"""
Get a logger for a specific component.
Args:
component: Component name (e.g., 'bot', 'api', 'autonomous')
Returns:
Configured logger instance
Example:
logger = get_logger('bot')
logger.info("Bot started")
logger.error("Connection failed", exc_info=True)
"""
if component not in COMPONENTS:
raise ValueError(
f"Unknown component '{component}'. "
f"Available: {', '.join(COMPONENTS.keys())}"
)
if component in _loggers:
return _loggers[component]
# Setup handlers if not done
_setup_handlers()
# Create logger
logger = logging.Logger(component)
# Add custom API level method
def api(self, message, *args, **kwargs):
if self.isEnabledFor(API_LEVEL):
self._log(API_LEVEL, message, args, **kwargs)
logger.api = lambda msg, *args, **kwargs: api(logger, msg, *args, **kwargs)
# Configure logger
_configure_logger(logger, component)
# Cache it
_loggers[component] = logger
return logger
def list_components() -> Dict[str, str]:
"""Get list of all available components with descriptions."""
return COMPONENTS.copy()
def get_component_stats() -> Dict[str, Dict]:
"""Get statistics about each component's logging."""
stats = {}
for component in COMPONENTS.keys():
log_file = LOG_DIR / f'{component.replace(".", "_")}.log'
stats[component] = {
'enabled': True, # Will be updated from config
'log_file': str(log_file),
'file_exists': log_file.exists(),
'file_size': log_file.stat().st_size if log_file.exists() else 0,
}
# Update from config
config = get_log_config()
component_config = config.get('components', {}).get(component, {})
stats[component]['enabled'] = component_config.get('enabled', True)
stats[component]['level'] = component_config.get('level', 'INFO')
stats[component]['enabled_levels'] = component_config.get('enabled_levels', ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'])
return stats
def intercept_external_loggers():
"""
Intercept logs from external libraries (APScheduler, etc.) and route them through our system.
Call this after initializing your application.
"""
# Intercept APScheduler loggers
apscheduler_loggers = [
'apscheduler',
'apscheduler.scheduler',
'apscheduler.executors',
'apscheduler.jobstores',
]
our_logger = get_logger('apscheduler')
for logger_name in apscheduler_loggers:
ext_logger = logging.getLogger(logger_name)
# Remove existing handlers
ext_logger.handlers.clear()
ext_logger.propagate = False
# Add our handlers
for handler in our_logger.handlers:
ext_logger.addHandler(handler)
# Set level
ext_logger.setLevel(logging.DEBUG)
# Initialize on import
_setup_handlers()

View File

@@ -1,6 +1,9 @@
# utils/media.py
import subprocess
from utils.logger import get_logger
logger = get_logger('media')
async def overlay_username_with_ffmpeg(base_video_path, output_path, username):
font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
@@ -65,6 +68,6 @@ async def overlay_username_with_ffmpeg(base_video_path, output_path, username):
try:
subprocess.run(ffmpeg_command, check=True)
print("Video processed successfully with username overlays.")
logger.info("Video processed successfully with username overlays.")
except subprocess.CalledProcessError as e:
print(f"⚠️ FFmpeg error: {e}")
logger.error(f"FFmpeg error: {e}")

View File

@@ -7,6 +7,9 @@ import asyncio
from discord.ext import tasks
import globals
import datetime
from utils.logger import get_logger
logger = get_logger('mood')
MOOD_EMOJIS = {
"asleep": "💤",
@@ -47,7 +50,7 @@ def load_mood_description(mood_name: str) -> str:
with open(path, "r", encoding="utf-8") as f:
return f.read().strip()
except FileNotFoundError:
print(f"⚠️ Mood file '{mood_name}' not found. Falling back to default.")
logger.warning(f"Mood file '{mood_name}' not found. Falling back to default.")
# Return a default mood description instead of recursive call
return "I'm feeling neutral and balanced today."
@@ -120,17 +123,17 @@ def detect_mood_shift(response_text, server_context=None):
# For server context, check against server's current mood
current_mood = server_context.get('current_mood_name', 'neutral')
if current_mood != "sleepy":
print(f"Mood 'asleep' skipped - server mood isn't 'sleepy', it's '{current_mood}'")
logger.debug(f"Mood 'asleep' skipped - server mood isn't 'sleepy', it's '{current_mood}'")
continue
else:
# For DM context, check against DM mood
if globals.DM_MOOD != "sleepy":
print(f"Mood 'asleep' skipped - DM mood isn't 'sleepy', it's '{globals.DM_MOOD}'")
logger.debug(f"Mood 'asleep' skipped - DM mood isn't 'sleepy', it's '{globals.DM_MOOD}'")
continue
for phrase in phrases:
if phrase.lower() in response_text.lower():
print(f"*️⃣ Mood keyword triggered: {phrase}")
logger.info(f"Mood keyword triggered: {phrase}")
return mood
return None
@@ -155,13 +158,13 @@ async def rotate_dm_mood():
globals.DM_MOOD = new_mood
globals.DM_MOOD_DESCRIPTION = load_mood_description(new_mood)
print(f"🔄 DM mood rotated from {old_mood} to {new_mood}")
logger.info(f"DM mood rotated from {old_mood} to {new_mood}")
# Note: We don't update server nicknames here because servers have their own independent moods.
# DM mood only affects direct messages to users.
except Exception as e:
print(f"Exception in rotate_dm_mood: {e}")
logger.error(f"Exception in rotate_dm_mood: {e}")
async def update_all_server_nicknames():
"""
@@ -171,8 +174,8 @@ async def update_all_server_nicknames():
This function incorrectly used DM mood to update all server nicknames,
breaking the independent per-server mood system.
"""
print("⚠️ WARNING: update_all_server_nicknames() is deprecated and should not be called!")
print("⚠️ Use update_server_nickname(guild_id) for per-server nickname updates instead.")
logger.warning("WARNING: update_all_server_nicknames() is deprecated and should not be called!")
logger.warning("Use update_server_nickname(guild_id) for per-server nickname updates instead.")
# Do nothing - this function should not modify nicknames
async def nickname_mood_emoji(guild_id: int):
@@ -182,11 +185,11 @@ async def nickname_mood_emoji(guild_id: int):
async def update_server_nickname(guild_id: int):
"""Update nickname for a specific server based on its mood"""
try:
print(f"🎭 Starting nickname update for server {guild_id}")
logger.debug(f"Starting nickname update for server {guild_id}")
# Check if bot is ready
if not globals.client.is_ready():
print(f"⚠️ Bot not ready yet, deferring nickname update for server {guild_id}")
logger.warning(f"Bot not ready yet, deferring nickname update for server {guild_id}")
return
# Check if evil mode is active
@@ -196,7 +199,7 @@ async def update_server_nickname(guild_id: int):
from server_manager import server_manager
server_config = server_manager.get_server_config(guild_id)
if not server_config:
print(f"⚠️ No server config found for guild {guild_id}")
logger.warning(f"No server config found for guild {guild_id}")
return
if evil_mode:
@@ -209,29 +212,29 @@ async def update_server_nickname(guild_id: int):
emoji = MOOD_EMOJIS.get(mood, "")
base_name = "Hatsune Miku"
print(f"🔍 Server {guild_id} mood is: {mood} (evil_mode={evil_mode})")
print(f"🔍 Using emoji: {emoji}")
logger.debug(f"Server {guild_id} mood is: {mood} (evil_mode={evil_mode})")
logger.debug(f"Using emoji: {emoji}")
nickname = f"{base_name}{emoji}"
print(f"🔍 New nickname will be: {nickname}")
logger.debug(f"New nickname will be: {nickname}")
guild = globals.client.get_guild(guild_id)
if guild:
print(f"🔍 Found guild: {guild.name}")
logger.debug(f"Found guild: {guild.name}")
me = guild.get_member(globals.BOT_USER.id)
if me is not None:
print(f"🔍 Found bot member: {me.display_name}")
logger.debug(f"Found bot member: {me.display_name}")
try:
await me.edit(nick=nickname)
print(f"💱 Changed nickname to {nickname} in server {guild.name}")
logger.info(f"Changed nickname to {nickname} in server {guild.name}")
except Exception as e:
print(f"⚠️ Failed to update nickname in server {guild.name}: {e}")
logger.warning(f"Failed to update nickname in server {guild.name}: {e}")
else:
print(f"⚠️ Could not find bot member in server {guild.name}")
logger.warning(f"Could not find bot member in server {guild.name}")
else:
print(f"⚠️ Could not find guild {guild_id}")
logger.warning(f"Could not find guild {guild_id}")
except Exception as e:
print(f"⚠️ Error updating server nickname for guild {guild_id}: {e}")
logger.error(f"Error updating server nickname for guild {guild_id}: {e}")
import traceback
traceback.print_exc()
@@ -268,7 +271,7 @@ async def rotate_server_mood(guild_id: int):
# Block transition to asleep unless coming from sleepy
if new_mood_name == "asleep" and old_mood_name != "sleepy":
print(f"Cannot rotate to asleep from {old_mood_name}, must be sleepy first")
logger.warning(f"Cannot rotate to asleep from {old_mood_name}, must be sleepy first")
# Try to get a different mood
attempts = 0
while (new_mood_name == "asleep" or new_mood_name == old_mood_name) and attempts < 5:
@@ -282,7 +285,7 @@ async def rotate_server_mood(guild_id: int):
from utils.autonomous import on_mood_change
on_mood_change(guild_id, new_mood_name)
except Exception as mood_notify_error:
print(f"⚠️ Failed to notify autonomous engine of mood change: {mood_notify_error}")
logger.error(f"Failed to notify autonomous engine of mood change: {mood_notify_error}")
# If transitioning to asleep, set up auto-wake
if new_mood_name == "asleep":
@@ -298,22 +301,22 @@ async def rotate_server_mood(guild_id: int):
from utils.autonomous import on_mood_change
on_mood_change(guild_id, "neutral")
except Exception as mood_notify_error:
print(f"⚠️ Failed to notify autonomous engine of wake-up mood change: {mood_notify_error}")
logger.error(f"Failed to notify autonomous engine of wake-up mood change: {mood_notify_error}")
await update_server_nickname(guild_id)
print(f"🌅 Server {guild_id} woke up from auto-sleep (mood rotation)")
logger.info(f"Server {guild_id} woke up from auto-sleep (mood rotation)")
globals.client.loop.create_task(delayed_wakeup())
print(f"Scheduled auto-wake for server {guild_id} in 1 hour")
logger.info(f"Scheduled auto-wake for server {guild_id} in 1 hour")
# Update nickname for this specific server
await update_server_nickname(guild_id)
print(f"🔄 Rotated mood for server {guild_id} from {old_mood_name} to {new_mood_name}")
logger.info(f"Rotated mood for server {guild_id} from {old_mood_name} to {new_mood_name}")
except Exception as e:
print(f"Exception in rotate_server_mood for server {guild_id}: {e}")
logger.error(f"Exception in rotate_server_mood for server {guild_id}: {e}")
async def clear_angry_mood_after_delay():
"""Clear angry mood after delay (legacy function - now handled per-server)"""
print("⚠️ clear_angry_mood_after_delay called - this function is deprecated")
logger.warning("clear_angry_mood_after_delay called - this function is deprecated")
pass

View File

@@ -15,6 +15,15 @@ This system is designed to be lightweight on LLM calls:
- Only escalates to argument system when tension threshold is reached
"""
import discord
import asyncio
import time
import globals
from utils.logger import get_logger
logger = get_logger('persona')
"""
import os
import json
import time
@@ -38,7 +47,7 @@ ARGUMENT_TENSION_THRESHOLD = 0.75 # Tension level that triggers argument escal
# Initial trigger settings
INTERJECTION_COOLDOWN_HARD = 180 # 3 minutes hard block
INTERJECTION_COOLDOWN_SOFT = 900 # 15 minutes for full recovery
INTERJECTION_THRESHOLD = 0.75 # Score needed to trigger interjection (lowered to account for mood multipliers)
INTERJECTION_THRESHOLD = 0.5 # Score needed to trigger interjection
# ============================================================================
# INTERJECTION SCORER (Initial Trigger Decision)
@@ -62,15 +71,15 @@ class InterjectionScorer:
def sentiment_analyzer(self):
"""Lazy load sentiment analyzer"""
if self._sentiment_analyzer is None:
print("🔄 Loading sentiment analyzer for persona dialogue...")
logger.debug("Loading sentiment analyzer for persona dialogue...")
try:
self._sentiment_analyzer = pipeline(
"sentiment-analysis",
model="distilbert-base-uncased-finetuned-sst-2-english"
)
print("Sentiment analyzer loaded")
logger.info("Sentiment analyzer loaded")
except Exception as e:
print(f"⚠️ Failed to load sentiment analyzer: {e}")
logger.error(f"Failed to load sentiment analyzer: {e}")
self._sentiment_analyzer = None
return self._sentiment_analyzer
@@ -97,8 +106,8 @@ class InterjectionScorer:
opposite_persona = "evil" if current_persona == "miku" else "miku"
print(f"🔍 [Interjection] Analyzing content: '{message.content[:100]}...'")
print(f"🔍 [Interjection] Current persona: {current_persona}, Opposite: {opposite_persona}")
logger.debug(f"[Interjection] Analyzing content: '{message.content[:100]}...'")
logger.debug(f"[Interjection] Current persona: {current_persona}, Opposite: {opposite_persona}")
# Calculate score from various factors
score = 0.0
@@ -106,7 +115,7 @@ class InterjectionScorer:
# Factor 1: Direct addressing (automatic trigger)
if self._mentions_opposite(message.content, opposite_persona):
print(f"[Interjection] Direct mention of {opposite_persona} detected!")
logger.info(f"[Interjection] Direct mention of {opposite_persona} detected!")
return True, "directly_addressed", 1.0
# Factor 2: Topic relevance
@@ -147,8 +156,8 @@ class InterjectionScorer:
reason_str = " | ".join(reasons) if reasons else "no_triggers"
if should_interject:
print(f"{opposite_persona.upper()} WILL INTERJECT (score: {score:.2f})")
print(f" Reasons: {reason_str}")
logger.info(f"{opposite_persona.upper()} WILL INTERJECT (score: {score:.2f})")
logger.info(f" Reasons: {reason_str}")
return should_interject, reason_str, score
@@ -156,12 +165,12 @@ class InterjectionScorer:
"""Fast rejection criteria"""
# System messages
if message.type != discord.MessageType.default:
print(f"[Basic Filter] System message type: {message.type}")
logger.debug(f"[Basic Filter] System message type: {message.type}")
return False
# Bipolar mode must be enabled
if not globals.BIPOLAR_MODE:
print(f"[Basic Filter] Bipolar mode not enabled")
logger.debug(f"[Basic Filter] Bipolar mode not enabled")
return False
# Allow bot's own messages (we're checking them for interjections!)
@@ -170,10 +179,10 @@ class InterjectionScorer:
if message.author.bot and not message.webhook_id:
# Check if it's our own bot
if message.author.id != globals.client.user.id:
print(f"[Basic Filter] Other bot message (not our bot)")
logger.debug(f"[Basic Filter] Other bot message (not our bot)")
return False
print(f"[Basic Filter] Passed (bot={message.author.bot}, webhook={message.webhook_id}, our_bot={message.author.id == globals.client.user.id if message.author.bot else 'N/A'})")
logger.debug(f"[Basic Filter] Passed (bot={message.author.bot}, webhook={message.webhook_id}, our_bot={message.author.id == globals.client.user.id if message.author.bot else 'N/A'})")
return True
def _mentions_opposite(self, content: str, opposite_persona: str) -> bool:
@@ -233,7 +242,7 @@ class InterjectionScorer:
return min(confidence * 0.6 + intensity_markers, 1.0)
except Exception as e:
print(f"⚠️ Sentiment analysis error: {e}")
logger.error(f"Sentiment analysis error: {e}")
return 0.5
def _detect_personality_clash(self, content: str, opposite_persona: str) -> float:
@@ -364,15 +373,15 @@ class PersonaDialogue:
}
self.active_dialogues[channel_id] = state
globals.LAST_PERSONA_DIALOGUE_TIME = time.time()
print(f"💬 Started persona dialogue in channel {channel_id}")
logger.info(f"Started persona dialogue in channel {channel_id}")
return state
def end_dialogue(self, channel_id: int):
"""End a dialogue in a channel"""
if channel_id in self.active_dialogues:
state = self.active_dialogues[channel_id]
print(f"🏁 Ended persona dialogue in channel {channel_id}")
print(f" Turns: {state['turn_count']}, Final tension: {state['tension']:.2f}")
logger.info(f"Ended persona dialogue in channel {channel_id}")
logger.info(f" Turns: {state['turn_count']}, Final tension: {state['tension']:.2f}")
del self.active_dialogues[channel_id]
# ========================================================================
@@ -400,7 +409,7 @@ class PersonaDialogue:
else:
base_delta = -sentiment_score * 0.05
except Exception as e:
print(f"⚠️ Sentiment analysis error in tension calc: {e}")
logger.error(f"Sentiment analysis error in tension calc: {e}")
text_lower = response_text.lower()
@@ -557,7 +566,7 @@ On a new line after your response, write:
# Override: If the response contains a question mark, always continue
if '?' in response_text:
print(f"⚠️ [Parse Override] Question detected, forcing continue=YES")
logger.debug(f"[Parse Override] Question detected, forcing continue=YES")
should_continue = True
if confidence == "LOW":
confidence = "MEDIUM"
@@ -605,12 +614,12 @@ You can use emojis naturally! ✨💙"""
# Safety limits
if state["turn_count"] >= MAX_TURNS:
print(f"🛑 Dialogue reached {MAX_TURNS} turns, ending")
logger.info(f"Dialogue reached {MAX_TURNS} turns, ending")
self.end_dialogue(channel_id)
return
if time.time() - state["started_at"] > DIALOGUE_TIMEOUT:
print(f"🛑 Dialogue timeout (15 min), ending")
logger.info(f"Dialogue timeout (15 min), ending")
self.end_dialogue(channel_id)
return
@@ -625,7 +634,7 @@ You can use emojis naturally! ✨💙"""
)
if not response_text:
print(f"⚠️ Failed to generate response for {responding_persona}")
logger.error(f"Failed to generate response for {responding_persona}")
self.end_dialogue(channel_id)
return
@@ -639,11 +648,11 @@ You can use emojis naturally! ✨💙"""
"total": state["tension"],
})
print(f"🌡️ Tension: {state['tension']:.2f} (delta: {tension_delta:+.2f})")
logger.debug(f"Tension: {state['tension']:.2f} (delta: {tension_delta:+.2f})")
# Check if we should escalate to argument
if state["tension"] >= ARGUMENT_TENSION_THRESHOLD:
print(f"🔥 TENSION THRESHOLD REACHED ({state['tension']:.2f}) - ESCALATING TO ARGUMENT")
logger.info(f"TENSION THRESHOLD REACHED ({state['tension']:.2f}) - ESCALATING TO ARGUMENT")
# Send the response that pushed us over
await self._send_as_persona(channel, responding_persona, response_text)
@@ -659,7 +668,7 @@ You can use emojis naturally! ✨💙"""
state["turn_count"] += 1
state["last_speaker"] = responding_persona
print(f"🗣️ Turn {state['turn_count']}: {responding_persona} | Continue: {should_continue} ({confidence}) | Tension: {state['tension']:.2f}")
logger.debug(f"Turn {state['turn_count']}: {responding_persona} | Continue: {should_continue} ({confidence}) | Tension: {state['tension']:.2f}")
# Decide what happens next
opposite = "evil" if responding_persona == "miku" else "miku"
@@ -677,14 +686,14 @@ You can use emojis naturally! ✨💙"""
)
else:
# Clear signal to end
print(f"🏁 Dialogue ended naturally after {state['turn_count']} turns (tension: {state['tension']:.2f})")
logger.info(f"Dialogue ended naturally after {state['turn_count']} turns (tension: {state['tension']:.2f})")
self.end_dialogue(channel_id)
async def _next_turn(self, channel: discord.TextChannel, persona: str):
"""Queue the next turn"""
# Check if dialogue was interrupted
if await self._was_interrupted(channel):
print(f"💬 Dialogue interrupted by other activity")
logger.info(f"Dialogue interrupted by other activity")
self.end_dialogue(channel.id)
return
@@ -741,7 +750,7 @@ Don't force a response if you have nothing meaningful to contribute."""
return
if "[DONE]" in response.upper():
print(f"🏁 {persona} chose not to respond, dialogue ended (tension: {state['tension']:.2f})")
logger.info(f"{persona} chose not to respond, dialogue ended (tension: {state['tension']:.2f})")
self.end_dialogue(channel_id)
else:
clean_response = response.replace("[DONE]", "").strip()
@@ -750,11 +759,11 @@ Don't force a response if you have nothing meaningful to contribute."""
tension_delta = self.calculate_tension_delta(clean_response, state["tension"])
state["tension"] = max(0.0, min(1.0, state["tension"] + tension_delta))
print(f"🌡️ Last word tension: {state['tension']:.2f} (delta: {tension_delta:+.2f})")
logger.debug(f"Last word tension: {state['tension']:.2f} (delta: {tension_delta:+.2f})")
# Check for argument escalation
if state["tension"] >= ARGUMENT_TENSION_THRESHOLD:
print(f"🔥 TENSION THRESHOLD REACHED on last word - ESCALATING TO ARGUMENT")
logger.info(f"TENSION THRESHOLD REACHED on last word - ESCALATING TO ARGUMENT")
await self._send_as_persona(channel, persona, clean_response)
await self._escalate_to_argument(channel, persona, clean_response)
return
@@ -782,7 +791,7 @@ Don't force a response if you have nothing meaningful to contribute."""
]
if all(closing_indicators):
print(f"🏁 Dialogue ended after last word, {state['turn_count']} turns total")
logger.info(f"Dialogue ended after last word, {state['turn_count']} turns total")
self.end_dialogue(channel.id)
else:
asyncio.create_task(self._next_turn(channel, opposite))
@@ -802,7 +811,7 @@ Don't force a response if you have nothing meaningful to contribute."""
# Don't start if an argument is already going
if is_argument_in_progress(channel.id):
print(f"⚠️ Argument already in progress, skipping escalation")
logger.warning(f"Argument already in progress, skipping escalation")
return
# Build context for the argument
@@ -811,7 +820,7 @@ The last thing said was: "{triggering_message}"
This pushed things over the edge into a full argument."""
print(f"⚔️ Escalating to argument in #{channel.name}")
logger.info(f"Escalating to argument in #{channel.name}")
# Use the existing argument system
# Pass the triggering message so the opposite persona responds to it
@@ -839,7 +848,7 @@ This pushed things over the edge into a full argument."""
if msg.author.id != globals.client.user.id:
return True
except Exception as e:
print(f"⚠️ Error checking for interruption: {e}")
logger.warning(f"Error checking for interruption: {e}")
return False
@@ -853,7 +862,7 @@ This pushed things over the edge into a full argument."""
messages.reverse()
except Exception as e:
print(f"⚠️ Error building conversation context: {e}")
logger.warning(f"Error building conversation context: {e}")
return '\n'.join(messages)
@@ -881,7 +890,7 @@ This pushed things over the edge into a full argument."""
webhooks = await get_or_create_webhooks_for_channel(channel)
if not webhooks:
print(f"⚠️ Could not get webhooks for #{channel.name}")
logger.warning(f"Could not get webhooks for #{channel.name}")
return
webhook = webhooks["evil_miku"] if persona == "evil" else webhooks["miku"]
@@ -890,7 +899,7 @@ This pushed things over the edge into a full argument."""
try:
await webhook.send(content=content, username=display_name)
except Exception as e:
print(f"⚠️ Error sending as {persona}: {e}")
logger.error(f"Error sending as {persona}: {e}")
# ============================================================================
@@ -929,24 +938,24 @@ async def check_for_interjection(message: discord.Message, current_persona: str)
Returns:
True if an interjection was triggered, False otherwise
"""
print(f"🔍 [Persona Dialogue] Checking interjection for message from {current_persona}")
logger.debug(f"[Persona Dialogue] Checking interjection for message from {current_persona}")
scorer = get_interjection_scorer()
dialogue_manager = get_dialogue_manager()
# Don't trigger if dialogue already active
if dialogue_manager.is_dialogue_active(message.channel.id):
print(f"⏸️ [Persona Dialogue] Dialogue already active in channel {message.channel.id}")
logger.debug(f"[Persona Dialogue] Dialogue already active in channel {message.channel.id}")
return False
# Check if we should interject
should_interject, reason, score = await scorer.should_interject(message, current_persona)
print(f"📊 [Persona Dialogue] Interjection check: should_interject={should_interject}, reason={reason}, score={score:.2f}")
logger.debug(f"[Persona Dialogue] Interjection check: should_interject={should_interject}, reason={reason}, score={score:.2f}")
if should_interject:
opposite_persona = "evil" if current_persona == "miku" else "miku"
print(f"🎭 Triggering {opposite_persona} interjection (reason: {reason}, score: {score:.2f})")
logger.info(f"Triggering {opposite_persona} interjection (reason: {reason}, score: {score:.2f})")
# Start dialogue with the opposite persona responding first
dialogue_manager.start_dialogue(message.channel.id)

View File

@@ -25,8 +25,11 @@ import discord
import globals
from .danbooru_client import danbooru_client
from .logger import get_logger
import globals
logger = get_logger('vision')
class ProfilePictureManager:
"""Manages Miku's profile picture with intelligent cropping and face detection"""
@@ -55,10 +58,10 @@ class ProfilePictureManager:
async with aiohttp.ClientSession() as session:
async with session.get("http://anime-face-detector:6078/health", timeout=aiohttp.ClientTimeout(total=5)) as response:
if response.status == 200:
print("Anime face detector API connected (pre-loaded)")
logger.info("Anime face detector API connected (pre-loaded)")
return True
except Exception as e:
print(f" Face detector not pre-loaded (container not running)")
logger.info(f"Face detector not pre-loaded (container not running)")
return False
async def _ensure_vram_available(self, debug: bool = False):
@@ -68,7 +71,7 @@ class ProfilePictureManager:
"""
try:
if debug:
print("💾 Swapping to text model to free VRAM for face detection...")
logger.info("Swapping to text model to free VRAM for face detection...")
# Make a simple request to text model to trigger swap
async with aiohttp.ClientSession() as session:
@@ -86,13 +89,13 @@ class ProfilePictureManager:
) as response:
if response.status == 200:
if debug:
print("Vision model unloaded, VRAM available")
logger.debug("Vision model unloaded, VRAM available")
# Give system time to fully release VRAM
await asyncio.sleep(3)
return True
except Exception as e:
if debug:
print(f"⚠️ Could not swap models: {e}")
logger.error(f"Could not swap models: {e}")
return False
@@ -100,7 +103,7 @@ class ProfilePictureManager:
"""Start the face detector container using Docker socket API"""
try:
if debug:
print("🚀 Starting face detector container...")
logger.info("Starting face detector container...")
# Use Docker socket API to start container
import aiofiles
@@ -112,7 +115,7 @@ class ProfilePictureManager:
# Check if socket exists
if not os.path.exists(socket_path):
if debug:
print("⚠️ Docker socket not available")
logger.error("Docker socket not available")
return False
# Use aiohttp UnixConnector to communicate with Docker socket
@@ -127,7 +130,7 @@ class ProfilePictureManager:
if response.status not in [204, 304]: # 204=started, 304=already running
if debug:
error_text = await response.text()
print(f"⚠️ Failed to start container: {response.status} - {error_text}")
logger.error(f"Failed to start container: {response.status} - {error_text}")
return False
# Wait for API to be ready
@@ -140,32 +143,32 @@ class ProfilePictureManager:
) as response:
if response.status == 200:
if debug:
print(f"Face detector ready (took {i+1}s)")
logger.info(f"Face detector ready (took {i+1}s)")
return True
except:
pass
await asyncio.sleep(1)
if debug:
print("⚠️ Face detector didn't become ready in time")
logger.warning("Face detector didn't become ready in time")
return False
except Exception as e:
if debug:
print(f"⚠️ Error starting face detector: {e}")
logger.error(f"Error starting face detector: {e}")
return False
async def _stop_face_detector(self, debug: bool = False):
"""Stop the face detector container using Docker socket API"""
try:
if debug:
print("🛑 Stopping face detector to free VRAM...")
logger.info("Stopping face detector to free VRAM...")
socket_path = "/var/run/docker.sock"
if not os.path.exists(socket_path):
if debug:
print("⚠️ Docker socket not available")
logger.error("Docker socket not available")
return
from aiohttp import UnixConnector
@@ -178,26 +181,26 @@ class ProfilePictureManager:
async with session.post(url, params={"t": 10}) as response: # 10 second timeout
if response.status in [204, 304]: # 204=stopped, 304=already stopped
if debug:
print("Face detector stopped")
logger.info("Face detector stopped")
else:
if debug:
error_text = await response.text()
print(f"⚠️ Failed to stop container: {response.status} - {error_text}")
logger.warning(f"Failed to stop container: {response.status} - {error_text}")
except Exception as e:
if debug:
print(f"⚠️ Error stopping face detector: {e}")
logger.error(f"Error stopping face detector: {e}")
async def save_current_avatar_as_fallback(self):
"""Save the bot's current avatar as fallback (only if fallback doesn't exist)"""
try:
# Only save if fallback doesn't already exist
if os.path.exists(self.FALLBACK_PATH):
print("Fallback avatar already exists, skipping save")
logger.info("Fallback avatar already exists, skipping save")
return True
if not globals.client or not globals.client.user:
print("⚠️ Bot client not ready")
logger.warning("Bot client not ready")
return False
avatar_asset = globals.client.user.avatar or globals.client.user.default_avatar
@@ -209,11 +212,11 @@ class ProfilePictureManager:
with open(self.FALLBACK_PATH, 'wb') as f:
f.write(avatar_bytes)
print(f"Saved current avatar as fallback ({len(avatar_bytes)} bytes)")
logger.info(f"Saved current avatar as fallback ({len(avatar_bytes)} bytes)")
return True
except Exception as e:
print(f"⚠️ Error saving fallback avatar: {e}")
logger.error(f"Error saving fallback avatar: {e}")
return False
async def change_profile_picture(
@@ -251,7 +254,7 @@ class ProfilePictureManager:
if custom_image_bytes:
# Custom upload - no retry needed
if debug:
print("🖼️ Using provided custom image")
logger.info("Using provided custom image")
image_bytes = custom_image_bytes
result["source"] = "custom_upload"
@@ -259,7 +262,7 @@ class ProfilePictureManager:
try:
image = Image.open(io.BytesIO(image_bytes))
if debug:
print(f"📐 Original image size: {image.size}")
logger.debug(f"Original image size: {image.size}")
# Check if it's an animated GIF
if image.format == 'GIF':
@@ -269,11 +272,11 @@ class ProfilePictureManager:
is_animated_gif = True
image.seek(0) # Reset to first frame
if debug:
print("🎬 Detected animated GIF - will preserve animation")
logger.debug("Detected animated GIF - will preserve animation")
except EOFError:
# Only one frame, treat as static image
if debug:
print("🖼️ Single-frame GIF - will process as static image")
logger.debug("Single-frame GIF - will process as static image")
except Exception as e:
result["error"] = f"Failed to open image: {e}"
@@ -282,11 +285,11 @@ class ProfilePictureManager:
else:
# Danbooru - retry until we find a valid Miku image
if debug:
print(f"🎨 Searching Danbooru for Miku image (mood: {mood})")
logger.info(f"Searching Danbooru for Miku image (mood: {mood})")
for attempt in range(max_retries):
if attempt > 0 and debug:
print(f"🔄 Retry attempt {attempt + 1}/{max_retries}")
logger.info(f"Retry attempt {attempt + 1}/{max_retries}")
post = await danbooru_client.get_random_miku_image(mood=mood)
if not post:
@@ -302,23 +305,23 @@ class ProfilePictureManager:
continue
if debug:
print(f"Downloaded image from Danbooru (post #{danbooru_client.get_post_metadata(post).get('id')})")
logger.info(f"Downloaded image from Danbooru (post #{danbooru_client.get_post_metadata(post).get('id')})")
# Load image with PIL
try:
temp_image = Image.open(io.BytesIO(temp_image_bytes))
if debug:
print(f"📐 Original image size: {temp_image.size}")
logger.debug(f"Original image size: {temp_image.size}")
except Exception as e:
if debug:
print(f"⚠️ Failed to open image: {e}")
logger.warning(f"Failed to open image: {e}")
continue
# Verify it's Miku
miku_verification = await self._verify_and_locate_miku(temp_image_bytes, debug=debug)
if not miku_verification["is_miku"]:
if debug:
print(f"Image verification failed: not Miku, trying another...")
logger.warning(f"Image verification failed: not Miku, trying another...")
continue
# Success! This image is valid
@@ -330,7 +333,7 @@ class ProfilePictureManager:
# If multiple characters detected, use LLM's suggested crop region
if miku_verification.get("crop_region"):
if debug:
print(f"🎯 Using LLM-suggested crop region for Miku")
logger.debug(f"Using LLM-suggested crop region for Miku")
image = self._apply_crop_region(image, miku_verification["crop_region"])
break
@@ -344,11 +347,11 @@ class ProfilePictureManager:
# If this is an animated GIF, skip most processing and use raw bytes
if is_animated_gif:
if debug:
print("🎬 Using GIF fast path - skipping face detection and cropping")
logger.info("Using GIF fast path - skipping face detection and cropping")
# Generate description of the animated GIF
if debug:
print("📝 Generating GIF description using video analysis pipeline...")
logger.info("Generating GIF description using video analysis pipeline...")
description = await self._generate_gif_description(image_bytes, debug=debug)
if description:
# Save description to file
@@ -358,12 +361,12 @@ class ProfilePictureManager:
f.write(description)
result["metadata"]["description"] = description
if debug:
print(f"📝 Saved GIF description ({len(description)} chars)")
logger.info(f"Saved GIF description ({len(description)} chars)")
except Exception as e:
print(f"⚠️ Failed to save description file: {e}")
logger.error(f"Failed to save description file: {e}")
else:
if debug:
print("⚠️ GIF description generation returned None")
logger.error("GIF description generation returned None")
# Extract dominant color from first frame
dominant_color = self._extract_dominant_color(image, debug=debug)
@@ -373,14 +376,14 @@ class ProfilePictureManager:
"hex": "#{:02x}{:02x}{:02x}".format(*dominant_color)
}
if debug:
print(f"🎨 Dominant color from first frame: RGB{dominant_color} (#{result['metadata']['dominant_color']['hex'][1:]})")
logger.debug(f"Dominant color from first frame: RGB{dominant_color} (#{result['metadata']['dominant_color']['hex'][1:]})")
# Save the original GIF bytes
with open(self.CURRENT_PATH, 'wb') as f:
f.write(image_bytes)
if debug:
print(f"💾 Saved animated GIF ({len(image_bytes)} bytes)")
logger.info(f"Saved animated GIF ({len(image_bytes)} bytes)")
# Update Discord avatar with original GIF
if globals.client and globals.client.user:
@@ -401,7 +404,7 @@ class ProfilePictureManager:
# Save metadata
self._save_metadata(result["metadata"])
print(f"Animated profile picture updated successfully!")
logger.info(f"Animated profile picture updated successfully!")
# Update role colors if we have a dominant color
if dominant_color:
@@ -411,12 +414,13 @@ class ProfilePictureManager:
except discord.HTTPException as e:
result["error"] = f"Discord API error: {e}"
print(f"⚠️ Failed to update Discord avatar with GIF: {e}")
print(f" Note: Animated avatars require Discord Nitro")
logger.warning(f"Failed to update Discord avatar with GIF: {e}")
if debug:
logger.debug("Note: Animated avatars require Discord Nitro")
return result
except Exception as e:
result["error"] = f"Unexpected error updating avatar: {e}"
print(f"⚠️ Unexpected error: {e}")
logger.error(f"Unexpected error: {e}")
return result
else:
result["error"] = "Bot client not ready"
@@ -425,7 +429,7 @@ class ProfilePictureManager:
# === NORMAL STATIC IMAGE PATH ===
# Step 2: Generate description of the validated image
if debug:
print("📝 Generating image description...")
logger.info("Generating image description...")
description = await self._generate_image_description(image_bytes, debug=debug)
if description:
# Save description to file
@@ -435,12 +439,12 @@ class ProfilePictureManager:
f.write(description)
result["metadata"]["description"] = description
if debug:
print(f"📝 Saved image description ({len(description)} chars)")
logger.info(f"Saved image description ({len(description)} chars)")
except Exception as e:
print(f"⚠️ Failed to save description file: {e}")
logger.warning(f"Failed to save description file: {e}")
else:
if debug:
print("⚠️ Description generation returned None")
logger.warning("Description generation returned None")
# Step 3: Detect face and crop intelligently
cropped_image = await self._intelligent_crop(image, image_bytes, target_size=512, debug=debug)
@@ -459,7 +463,7 @@ class ProfilePictureManager:
f.write(cropped_bytes)
if debug:
print(f"💾 Saved cropped image ({len(cropped_bytes)} bytes)")
logger.info(f"Saved cropped image ({len(cropped_bytes)} bytes)")
# Step 5: Extract dominant color from saved current.png
saved_image = Image.open(self.CURRENT_PATH)
@@ -470,7 +474,7 @@ class ProfilePictureManager:
"hex": "#{:02x}{:02x}{:02x}".format(*dominant_color)
}
if debug:
print(f"🎨 Dominant color: RGB{dominant_color} (#{result['metadata']['dominant_color']['hex'][1:]})")
logger.debug(f"Dominant color: RGB{dominant_color} (#{result['metadata']['dominant_color']['hex'][1:]})")
# Step 6: Update Discord avatar
if globals.client and globals.client.user:
@@ -495,7 +499,7 @@ class ProfilePictureManager:
# Save metadata
self._save_metadata(result["metadata"])
print(f"Profile picture updated successfully!")
logger.info(f"Profile picture updated successfully!")
# Step 7: Update role colors across all servers
if dominant_color:
@@ -503,16 +507,16 @@ class ProfilePictureManager:
except discord.HTTPException as e:
result["error"] = f"Discord API error: {e}"
print(f"⚠️ Failed to update Discord avatar: {e}")
logger.warning(f"Failed to update Discord avatar: {e}")
except Exception as e:
result["error"] = f"Unexpected error updating avatar: {e}"
print(f"⚠️ Unexpected error: {e}")
logger.error(f"Unexpected error: {e}")
else:
result["error"] = "Bot client not ready"
except Exception as e:
result["error"] = f"Unexpected error: {e}"
print(f"⚠️ Error in change_profile_picture: {e}")
logger.error(f"Error in change_profile_picture: {e}")
return result
@@ -524,7 +528,7 @@ class ProfilePictureManager:
if response.status == 200:
return await response.read()
except Exception as e:
print(f"⚠️ Error downloading image: {e}")
logger.error(f"Error downloading image: {e}")
return None
async def _generate_image_description(self, image_bytes: bytes, debug: bool = False) -> Optional[str]:
@@ -544,7 +548,7 @@ class ProfilePictureManager:
image_b64 = base64.b64encode(image_bytes).decode('utf-8')
if debug:
print(f"📸 Encoded image: {len(image_b64)} chars, calling vision model...")
logger.debug(f"Encoded image: {len(image_b64)} chars, calling vision model...")
prompt = """This is an image of Hatsune Miku that will be used as a profile picture.
Please describe this image in detail, including:
@@ -583,7 +587,7 @@ Keep the description conversational and in second-person (referring to Miku as "
headers = {"Content-Type": "application/json"}
if debug:
print(f"🌐 Calling {globals.LLAMA_URL}/v1/chat/completions with model {globals.VISION_MODEL}")
logger.debug(f"Calling {globals.LLAMA_URL}/v1/chat/completions with model {globals.VISION_MODEL}")
async with aiohttp.ClientSession() as session:
async with session.post(f"{globals.LLAMA_URL}/v1/chat/completions", json=payload, headers=headers, timeout=aiohttp.ClientTimeout(total=60)) as resp:
@@ -591,8 +595,8 @@ Keep the description conversational and in second-person (referring to Miku as "
data = await resp.json()
if debug:
print(f"📦 API Response keys: {data.keys()}")
print(f"📦 Choices: {data.get('choices', [])}")
logger.debug(f"API Response keys: {data.keys()}")
logger.debug(f"Choices: {data.get('choices', [])}")
# Try to get content from the response
choice = data.get("choices", [{}])[0]
@@ -607,21 +611,21 @@ Keep the description conversational and in second-person (referring to Miku as "
if description and description.strip():
if debug:
print(f"Generated description: {description[:100]}...")
logger.info(f"Generated description: {description[:100]}...")
return description.strip()
else:
if debug:
print(f"⚠️ Description is empty or None")
print(f" Full response: {data}")
logger.warning(f"Description is empty or None")
logger.warning(f" Full response: {data}")
else:
print(f"⚠️ Description is empty or None")
logger.warning(f"Description is empty or None")
return None
else:
error_text = await resp.text()
print(f"Vision API error generating description: {resp.status} - {error_text}")
logger.error(f"Vision API error generating description: {resp.status} - {error_text}")
except Exception as e:
print(f"⚠️ Error generating image description: {e}")
logger.error(f"Error generating image description: {e}")
import traceback
traceback.print_exc()
@@ -642,19 +646,19 @@ Keep the description conversational and in second-person (referring to Miku as "
from utils.image_handling import extract_video_frames, analyze_video_with_vision
if debug:
print("🎬 Extracting frames from GIF...")
logger.info("Extracting frames from GIF...")
# Extract frames from the GIF (6 frames for good analysis)
frames = await extract_video_frames(gif_bytes, num_frames=6)
if not frames:
if debug:
print("⚠️ Failed to extract frames from GIF")
logger.warning("Failed to extract frames from GIF")
return None
if debug:
print(f"Extracted {len(frames)} frames from GIF")
print(f"🌐 Analyzing GIF with vision model...")
logger.info(f"Extracted {len(frames)} frames from GIF")
logger.info(f"Analyzing GIF with vision model...")
# Use the existing analyze_video_with_vision function (no timeout issues)
# Note: This uses a generic prompt, but it works reliably
@@ -662,15 +666,15 @@ Keep the description conversational and in second-person (referring to Miku as "
if description and description.strip() and not description.startswith("Error"):
if debug:
print(f"Generated GIF description: {description[:100]}...")
logger.info(f"Generated GIF description: {description[:100]}...")
return description.strip()
else:
if debug:
print(f"⚠️ GIF description failed or empty: {description}")
logger.warning(f"GIF description failed or empty: {description}")
return None
except Exception as e:
print(f"⚠️ Error generating GIF description: {e}")
logger.error(f"Error generating GIF description: {e}")
import traceback
traceback.print_exc()
@@ -740,11 +744,11 @@ Respond in JSON format:
response = data.get("choices", [{}])[0].get("message", {}).get("content", "")
else:
error_text = await resp.text()
print(f"Vision API error: {resp.status} - {error_text}")
logger.error(f"Vision API error: {resp.status} - {error_text}")
return result
if debug:
print(f"🤖 Vision model response: {response}")
logger.debug(f"Vision model response: {response}")
# Parse JSON response
import re
@@ -766,7 +770,7 @@ Respond in JSON format:
result["is_miku"] = "yes" in response_lower or "miku" in response_lower
except Exception as e:
print(f"⚠️ Error in vision verification: {e}")
logger.warning(f"Error in vision verification: {e}")
# Assume it's Miku on error (trust Danbooru tags)
result["is_miku"] = True
@@ -793,7 +797,7 @@ Respond in JSON format:
region["vertical"] = "bottom"
if debug:
print(f"📍 Parsed location '{location}' -> {region}")
logger.debug(f"Parsed location '{location}' -> {region}")
return region
@@ -856,11 +860,11 @@ Respond in JSON format:
if face_detection and face_detection.get('center'):
if debug:
print(f"😊 Face detected at {face_detection['center']}")
logger.debug(f"Face detected at {face_detection['center']}")
crop_center = face_detection['center']
else:
if debug:
print("🎯 No face detected, using saliency detection")
logger.debug("No face detected, using saliency detection")
# Fallback to saliency detection
cv_image = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
crop_center = self._detect_saliency(cv_image, debug=debug)
@@ -895,12 +899,12 @@ Respond in JSON format:
top = 0
# Adjust crop_center for logging
if debug:
print(f"⚠️ Face too close to top edge, shifted crop to y=0")
logger.debug(f"Face too close to top edge, shifted crop to y=0")
elif top + crop_size > height:
# Face is too close to bottom edge
top = height - crop_size
if debug:
print(f"⚠️ Face too close to bottom edge, shifted crop to y={top}")
logger.debug(f"Face too close to bottom edge, shifted crop to y={top}")
# Crop
cropped = image.crop((left, top, left + crop_size, top + crop_size))
@@ -909,7 +913,7 @@ Respond in JSON format:
cropped = cropped.resize((target_size, target_size), Image.Resampling.LANCZOS)
if debug:
print(f"✂️ Cropped to {target_size}x{target_size} centered at {crop_center}")
logger.debug(f"Cropped to {target_size}x{target_size} centered at {crop_center}")
return cropped
@@ -933,7 +937,7 @@ Respond in JSON format:
# Step 2: Start face detector container
if not await self._start_face_detector(debug=debug):
if debug:
print("⚠️ Could not start face detector")
logger.error("Could not start face detector")
return None
face_detector_started = True
@@ -951,14 +955,14 @@ Respond in JSON format:
) as response:
if response.status != 200:
if debug:
print(f"⚠️ Face detection API returned status {response.status}")
logger.error(f"Face detection API returned status {response.status}")
return None
result = await response.json()
if result.get('count', 0) == 0:
if debug:
print("👤 No faces detected by API")
logger.debug("No faces detected by API")
return None
# Get detections and pick the one with highest confidence
@@ -981,9 +985,9 @@ Respond in JSON format:
if debug:
width = int(x2 - x1)
height = int(y2 - y1)
print(f"👤 Detected {len(detections)} face(s) via API, using best at ({center_x}, {center_y}) [confidence: {confidence:.2%}]")
print(f" Bounding box: x={int(x1)}, y={int(y1)}, w={width}, h={height}")
print(f" Keypoints: {len(keypoints)} facial landmarks detected")
logger.debug(f"Detected {len(detections)} face(s) via API, using best at ({center_x}, {center_y}) [confidence: {confidence:.2%}]")
logger.debug(f" Bounding box: x={int(x1)}, y={int(y1)}, w={width}, h={height}")
logger.debug(f" Keypoints: {len(keypoints)} facial landmarks detected")
return {
'center': (center_x, center_y),
@@ -995,10 +999,10 @@ Respond in JSON format:
except asyncio.TimeoutError:
if debug:
print("⚠️ Face detection API timeout")
logger.warning("Face detection API timeout")
except Exception as e:
if debug:
print(f"⚠️ Error calling face detection API: {e}")
logger.error(f"Error calling face detection API: {e}")
finally:
# Always stop face detector to free VRAM
if face_detector_started:
@@ -1027,12 +1031,12 @@ Respond in JSON format:
_, max_val, _, max_loc = cv2.minMaxLoc(saliency_map)
if debug:
print(f"🎯 Saliency peak at {max_loc}")
logger.debug(f"Saliency peak at {max_loc}")
return max_loc
except Exception as e:
if debug:
print(f"⚠️ Saliency detection failed: {e}")
logger.error(f"Saliency detection failed: {e}")
# Ultimate fallback: center of image
height, width = cv_image.shape[:2]
@@ -1070,7 +1074,7 @@ Respond in JSON format:
if len(pixels) == 0:
if debug:
print("⚠️ No valid pixels after filtering, using fallback")
logger.warning("No valid pixels after filtering, using fallback")
return (200, 200, 200) # Neutral gray fallback
# Use k-means to find dominant colors
@@ -1085,11 +1089,11 @@ Respond in JSON format:
counts = np.bincount(labels)
if debug:
print(f"🎨 Found {n_colors} color clusters:")
logger.debug(f"Found {n_colors} color clusters:")
for i, (color, count) in enumerate(zip(colors, counts)):
pct = (count / len(labels)) * 100
r, g, b = color.astype(int)
print(f" {i+1}. RGB({r}, {g}, {b}) = #{r:02x}{g:02x}{b:02x} ({pct:.1f}%)")
logger.debug(f" {i+1}. RGB({r}, {g}, {b}) = #{r:02x}{g:02x}{b:02x} ({pct:.1f}%)")
# Sort by frequency
sorted_indices = np.argsort(-counts)
@@ -1108,7 +1112,7 @@ Respond in JSON format:
saturation = (max_c - min_c) / max_c if max_c > 0 else 0
if debug:
print(f" Color RGB({r}, {g}, {b}) saturation: {saturation:.2f}")
logger.debug(f" Color RGB({r}, {g}, {b}) saturation: {saturation:.2f}")
# Prefer more saturated colors
if saturation > best_saturation:
@@ -1118,7 +1122,7 @@ Respond in JSON format:
if best_color:
if debug:
print(f"🎨 Selected color: RGB{best_color} (saturation: {best_saturation:.2f})")
logger.debug(f"Selected color: RGB{best_color} (saturation: {best_saturation:.2f})")
return best_color
# Fallback to most common color
@@ -1126,12 +1130,12 @@ Respond in JSON format:
# Convert to native Python ints
result = (int(dominant_color[0]), int(dominant_color[1]), int(dominant_color[2]))
if debug:
print(f"🎨 Using most common color: RGB{result}")
logger.debug(f"Using most common color: RGB{result}")
return result
except Exception as e:
if debug:
print(f"⚠️ Error extracting dominant color: {e}")
logger.error(f"Error extracting dominant color: {e}")
return None
async def _update_role_colors(self, color: Tuple[int, int, int], debug: bool = False):
@@ -1143,15 +1147,15 @@ Respond in JSON format:
debug: Enable debug output
"""
if debug:
print(f"🎨 Starting role color update with RGB{color}")
logger.debug(f"Starting role color update with RGB{color}")
if not globals.client:
if debug:
print("⚠️ No client available for role updates")
logger.error("No client available for role updates")
return
if debug:
print(f"🌐 Found {len(globals.client.guilds)} guild(s)")
logger.debug(f"Found {len(globals.client.guilds)} guild(s)")
# Convert RGB to Discord color (integer)
discord_color = discord.Color.from_rgb(*color)
@@ -1162,20 +1166,20 @@ Respond in JSON format:
for guild in globals.client.guilds:
try:
if debug:
print(f"🔍 Checking guild: {guild.name}")
logger.debug(f"Checking guild: {guild.name}")
# Find the bot's top role (usually colored role)
member = guild.get_member(globals.client.user.id)
if not member:
if debug:
print(f" ⚠️ Bot not found as member in {guild.name}")
logger.warning(f" Bot not found as member in {guild.name}")
continue
# Get the highest role that the bot has (excluding @everyone)
roles = [r for r in member.roles if r.name != "@everyone"]
if not roles:
if debug:
print(f" ⚠️ No roles found in {guild.name}")
logger.warning(f" No roles found in {guild.name}")
continue
# Look for a dedicated color role first (e.g., "Miku Color")
@@ -1191,19 +1195,19 @@ Respond in JSON format:
# Use dedicated color role if found, otherwise use top role
if color_role:
if debug:
print(f" 🎨 Found dedicated color role: {color_role.name} (position {color_role.position})")
logger.debug(f" Found dedicated color role: {color_role.name} (position {color_role.position})")
target_role = color_role
else:
if debug:
print(f" 📝 No 'Miku Color' role found, using top role: {bot_top_role.name} (position {bot_top_role.position})")
logger.debug(f" No 'Miku Color' role found, using top role: {bot_top_role.name} (position {bot_top_role.position})")
target_role = bot_top_role
# Check permissions
can_manage = guild.me.guild_permissions.manage_roles
if debug:
print(f" 🔑 Manage roles permission: {can_manage}")
print(f" 📊 Bot top role: {bot_top_role.name} (pos {bot_top_role.position}), Target: {target_role.name} (pos {target_role.position})")
logger.debug(f" Manage roles permission: {can_manage}")
logger.debug(f" Bot top role: {bot_top_role.name} (pos {bot_top_role.position}), Target: {target_role.name} (pos {target_role.position})")
# Only update if we have permission and it's not a special role
if can_manage:
@@ -1219,28 +1223,28 @@ Respond in JSON format:
updated_count += 1
if debug:
print(f" Updated role color in {guild.name}: {target_role.name}")
logger.info(f" Updated role color in {guild.name}: {target_role.name}")
else:
if debug:
print(f" ⚠️ No manage_roles permission in {guild.name}")
logger.warning(f" No manage_roles permission in {guild.name}")
except discord.Forbidden:
failed_count += 1
if debug:
print(f" Forbidden: No permission to update role in {guild.name}")
logger.error(f" Forbidden: No permission to update role in {guild.name}")
except Exception as e:
failed_count += 1
if debug:
print(f" Error updating role in {guild.name}: {e}")
logger.error(f" Error updating role in {guild.name}: {e}")
import traceback
traceback.print_exc()
if updated_count > 0:
print(f"🎨 Updated role colors in {updated_count} server(s)")
logger.info(f"Updated role colors in {updated_count} server(s)")
else:
print(f"⚠️ No roles were updated (failed: {failed_count})")
logger.warning(f"No roles were updated (failed: {failed_count})")
if failed_count > 0 and debug:
print(f"⚠️ Failed to update {failed_count} server(s)")
logger.error(f"Failed to update {failed_count} server(s)")
async def set_custom_role_color(self, hex_color: str, debug: bool = False) -> Dict:
"""
@@ -1267,7 +1271,7 @@ Respond in JSON format:
}
if debug:
print(f"🎨 Setting custom role color: #{hex_color} RGB{color}")
logger.debug(f"Setting custom role color: #{hex_color} RGB{color}")
await self._update_role_colors(color, debug=debug)
@@ -1290,7 +1294,7 @@ Respond in JSON format:
Dict with success status
"""
if debug:
print(f"🎨 Resetting to fallback color: RGB{self.FALLBACK_ROLE_COLOR}")
logger.debug(f"Resetting to fallback color: RGB{self.FALLBACK_ROLE_COLOR}")
await self._update_role_colors(self.FALLBACK_ROLE_COLOR, debug=debug)
@@ -1308,7 +1312,7 @@ Respond in JSON format:
with open(self.METADATA_PATH, 'w') as f:
json.dump(metadata, f, indent=2)
except Exception as e:
print(f"⚠️ Error saving metadata: {e}")
logger.error(f"Error saving metadata: {e}")
def load_metadata(self) -> Optional[Dict]:
"""Load metadata about current profile picture"""
@@ -1317,14 +1321,14 @@ Respond in JSON format:
with open(self.METADATA_PATH, 'r') as f:
return json.load(f)
except Exception as e:
print(f"⚠️ Error loading metadata: {e}")
logger.error(f"Error loading metadata: {e}")
return None
async def restore_fallback(self) -> bool:
"""Restore the fallback profile picture"""
try:
if not os.path.exists(self.FALLBACK_PATH):
print("⚠️ No fallback avatar found")
logger.warning("No fallback avatar found")
return False
with open(self.FALLBACK_PATH, 'rb') as f:
@@ -1341,11 +1345,11 @@ Respond in JSON format:
else:
await globals.client.user.edit(avatar=avatar_bytes)
print("Restored fallback avatar")
logger.info("Restored fallback avatar")
return True
except Exception as e:
print(f"⚠️ Error restoring fallback: {e}")
logger.error(f"Error restoring fallback: {e}")
return False
@@ -1362,7 +1366,7 @@ Respond in JSON format:
with open(description_path, 'r', encoding='utf-8') as f:
return f.read().strip()
except Exception as e:
print(f"⚠️ Error reading description: {e}")
logger.error(f"Error reading description: {e}")
return None

View File

@@ -13,6 +13,9 @@ import globals
from server_manager import server_manager
from utils.llm import query_llama
from utils.dm_interaction_analyzer import dm_analyzer
from utils.logger import get_logger
logger = get_logger('scheduled')
BEDTIME_TRACKING_FILE = "last_bedtime_targets.json"
@@ -20,7 +23,7 @@ async def send_monday_video_for_server(guild_id: int):
"""Send Monday video for a specific server"""
server_config = server_manager.get_server_config(guild_id)
if not server_config:
print(f"⚠️ No config found for server {guild_id}")
logger.warning(f"No config found for server {guild_id}")
return
# No need to switch model - llama-swap handles this automatically
@@ -37,7 +40,7 @@ async def send_monday_video_for_server(guild_id: int):
for channel_id in target_channel_ids:
channel = globals.client.get_channel(channel_id)
if channel is None:
print(f"Could not find channel with ID {channel_id} in server {server_config.guild_name}")
logger.error(f"Could not find channel with ID {channel_id} in server {server_config.guild_name}")
continue
try:
@@ -45,9 +48,9 @@ async def send_monday_video_for_server(guild_id: int):
# Send video link
await channel.send(f"[Happy Miku Monday!]({video_url})")
print(f"Sent Monday video to channel ID {channel_id} in server {server_config.guild_name}")
logger.info(f"Sent Monday video to channel ID {channel_id} in server {server_config.guild_name}")
except Exception as e:
print(f"⚠️ Failed to send video to channel ID {channel_id} in server {server_config.guild_name}: {e}")
logger.error(f"Failed to send video to channel ID {channel_id} in server {server_config.guild_name}: {e}")
async def send_monday_video():
"""Legacy function - now sends to all servers"""
@@ -61,7 +64,7 @@ def load_last_bedtime_targets():
with open(BEDTIME_TRACKING_FILE, "r") as f:
return json.load(f)
except Exception as e:
print(f"⚠️ Failed to load bedtime tracking file: {e}")
logger.error(f"Failed to load bedtime tracking file: {e}")
return {}
_last_bedtime_targets = load_last_bedtime_targets()
@@ -71,13 +74,13 @@ def save_last_bedtime_targets(data):
with open(BEDTIME_TRACKING_FILE, "w") as f:
json.dump(data, f)
except Exception as e:
print(f"⚠️ Failed to save bedtime tracking file: {e}")
logger.error(f"Failed to save bedtime tracking file: {e}")
async def send_bedtime_reminder_for_server(guild_id: int, client=None):
"""Send bedtime reminder for a specific server"""
server_config = server_manager.get_server_config(guild_id)
if not server_config:
print(f"⚠️ No config found for server {guild_id}")
logger.warning(f"No config found for server {guild_id}")
return
# Use provided client or fall back to globals.client
@@ -85,7 +88,7 @@ async def send_bedtime_reminder_for_server(guild_id: int, client=None):
client = globals.client
if client is None:
print(f"⚠️ No Discord client available for bedtime reminder in server {guild_id}")
logger.error(f"No Discord client available for bedtime reminder in server {guild_id}")
return
# No need to switch model - llama-swap handles this automatically
@@ -94,7 +97,7 @@ async def send_bedtime_reminder_for_server(guild_id: int, client=None):
for channel_id in server_config.bedtime_channel_ids:
channel = client.get_channel(channel_id)
if not channel:
print(f"⚠️ Channel ID {channel_id} not found in server {server_config.guild_name}")
logger.warning(f"Channel ID {channel_id} not found in server {server_config.guild_name}")
continue
guild = channel.guild
@@ -112,7 +115,8 @@ async def send_bedtime_reminder_for_server(guild_id: int, client=None):
online_members.append(specific_user)
if not online_members:
print(f"😴 No online members to ping in {guild.name}")
# TODO: Handle this in a different way in the future
logger.debug(f"No online members to ping in {guild.name}")
continue
# Avoid repeating the same person unless they're the only one
@@ -162,9 +166,9 @@ async def send_bedtime_reminder_for_server(guild_id: int, client=None):
try:
await channel.send(f"{chosen_one.mention} {bedtime_message}")
print(f"🌙 Sent bedtime reminder to {chosen_one.display_name} in server {server_config.guild_name}")
logger.info(f"Sent bedtime reminder to {chosen_one.display_name} in server {server_config.guild_name}")
except Exception as e:
print(f"⚠️ Failed to send bedtime reminder in server {server_config.guild_name}: {e}")
logger.error(f"Failed to send bedtime reminder in server {server_config.guild_name}: {e}")
async def send_bedtime_reminder():
"""Legacy function - now sends to all servers"""
@@ -176,7 +180,7 @@ def schedule_random_bedtime():
for guild_id in server_manager.servers:
# Schedule bedtime for each server using the async function
# This will be called from the server manager's event loop
print(f"Scheduling bedtime for server {guild_id}")
logger.info(f"Scheduling bedtime for server {guild_id}")
# Note: This function is now called from the server manager's context
# which properly handles the async operations
@@ -188,8 +192,8 @@ async def send_bedtime_now():
async def run_daily_dm_analysis():
"""Run daily DM interaction analysis - reports one user per day"""
if dm_analyzer is None:
print("⚠️ DM Analyzer not initialized, skipping daily analysis")
logger.warning("DM Analyzer not initialized, skipping daily analysis")
return
print("📊 Running daily DM interaction analysis...")
logger.info("Running daily DM interaction analysis...")
await dm_analyzer.run_daily_analysis()

View File

@@ -1,4 +1,7 @@
from utils.llm import query_llama
from utils.logger import get_logger
logger = get_logger('sentiment')
async def analyze_sentiment(messages: list) -> tuple[str, float]:
"""
@@ -40,5 +43,5 @@ Response:"""
return summary, score
except Exception as e:
print(f"Error in sentiment analysis: {e}")
logger.error(f"Error in sentiment analysis: {e}")
return "Error analyzing sentiment", 0.5

View File

@@ -11,11 +11,14 @@ apply_twscrape_fix()
from twscrape import API, gather, Account
from playwright.async_api import async_playwright
from pathlib import Path
from utils.logger import get_logger
logger = get_logger('media')
COOKIE_PATH = Path(__file__).parent / "x.com.cookies.json"
async def extract_media_urls(page, tweet_url):
print(f"🔍 Visiting tweet page: {tweet_url}")
logger.debug(f"Visiting tweet page: {tweet_url}")
try:
await page.goto(tweet_url, timeout=15000)
await page.wait_for_timeout(1000)
@@ -29,11 +32,11 @@ async def extract_media_urls(page, tweet_url):
cleaned = src.split("&name=")[0] + "&name=large"
urls.add(cleaned)
print(f"🖼️ Found {len(urls)} media URLs on tweet: {tweet_url}")
logger.debug(f"Found {len(urls)} media URLs on tweet: {tweet_url}")
return list(urls)
except Exception as e:
print(f"Playwright error on {tweet_url}: {e}")
logger.error(f"Playwright error on {tweet_url}: {e}")
return []
async def fetch_miku_tweets(limit=5):
@@ -53,11 +56,11 @@ async def fetch_miku_tweets(limit=5):
)
await api.pool.login_all()
print(f"🔎 Searching for Miku tweets (limit={limit})...")
logger.info(f"Searching for Miku tweets (limit={limit})...")
query = 'Hatsune Miku OR 初音ミク has:images after:2025'
tweets = await gather(api.search(query, limit=limit, kv={"product": "Top"}))
print(f"📄 Found {len(tweets)} tweets, launching browser...")
logger.info(f"Found {len(tweets)} tweets, launching browser...")
async with async_playwright() as p:
browser = await p.firefox.launch(headless=True)
@@ -78,7 +81,7 @@ async def fetch_miku_tweets(limit=5):
for i, tweet in enumerate(tweets, 1):
username = tweet.user.username
tweet_url = f"https://twitter.com/{username}/status/{tweet.id}"
print(f"🧵 Processing tweet {i}/{len(tweets)} from @{username}")
logger.debug(f"Processing tweet {i}/{len(tweets)} from @{username}")
media_urls = await extract_media_urls(page, tweet_url)
if media_urls:
@@ -90,7 +93,7 @@ async def fetch_miku_tweets(limit=5):
})
await browser.close()
print(f"Finished! Returning {len(results)} tweet(s) with media.")
logger.info(f"Finished! Returning {len(results)} tweet(s) with media.")
return results
@@ -99,7 +102,7 @@ async def _search_latest(api: API, query: str, limit: int) -> list:
try:
return await gather(api.search(query, limit=limit, kv={"product": "Latest"}))
except Exception as e:
print(f"⚠️ Latest search failed for '{query}': {e}")
logger.error(f"Latest search failed for '{query}': {e}")
return []
@@ -131,13 +134,13 @@ async def fetch_figurine_tweets_latest(limit_per_source: int = 10) -> list:
"miku from:OtakuOwletMerch",
]
print("🔎 Searching figurine tweets by Latest across sources...")
logger.info("Searching figurine tweets by Latest across sources...")
all_tweets = []
for q in queries:
tweets = await _search_latest(api, q, limit_per_source)
all_tweets.extend(tweets)
print(f"📄 Found {len(all_tweets)} candidate tweets, launching browser to extract media...")
logger.info(f"Found {len(all_tweets)} candidate tweets, launching browser to extract media...")
async with async_playwright() as p:
browser = await p.firefox.launch(headless=True)
@@ -157,7 +160,7 @@ async def fetch_figurine_tweets_latest(limit_per_source: int = 10) -> list:
try:
username = tweet.user.username
tweet_url = f"https://twitter.com/{username}/status/{tweet.id}"
print(f"🧵 Processing tweet {i}/{len(all_tweets)} from @{username}")
logger.debug(f"Processing tweet {i}/{len(all_tweets)} from @{username}")
media_urls = await extract_media_urls(page, tweet_url)
if media_urls:
results.append({
@@ -167,10 +170,10 @@ async def fetch_figurine_tweets_latest(limit_per_source: int = 10) -> list:
"media": media_urls
})
except Exception as e:
print(f"⚠️ Error processing tweet: {e}")
logger.error(f"Error processing tweet: {e}")
await browser.close()
print(f"Figurine fetch finished. Returning {len(results)} tweet(s) with media.")
logger.info(f"Figurine fetch finished. Returning {len(results)} tweet(s) with media.")
return results

View File

@@ -7,6 +7,9 @@ See: https://github.com/vladkens/twscrape/issues/284
import json
import re
from utils.logger import get_logger
logger = get_logger('core')
def script_url(k: str, v: str):
@@ -36,6 +39,6 @@ def apply_twscrape_fix():
try:
from twscrape import xclid
xclid.get_scripts_list = patched_get_scripts_list
print("Applied twscrape monkey patch for 'Failed to parse scripts' fix")
logger.info("Applied twscrape monkey patch for 'Failed to parse scripts' fix")
except Exception as e:
print(f"⚠️ Failed to apply twscrape monkey patch: {e}")
logger.error(f"Failed to apply twscrape monkey patch: {e}")