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:
@@ -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 ==========
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 {}, {}
|
||||
|
||||
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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]"
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)}"
|
||||
|
||||
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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
286
bot/utils/log_config.py
Normal 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
395
bot/utils/logger.py
Normal 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()
|
||||
@@ -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}")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user