diff --git a/bot/api.py b/bot/api.py
index f495987..d336ffb 100644
--- a/bot/api.py
+++ b/bot/api.py
@@ -50,8 +50,25 @@ from utils.figurine_notifier import (
send_figurine_dm_to_single_user
)
from utils.dm_logger import dm_logger
+from utils.logger import get_logger, list_components, get_component_stats
+from utils.log_config import (
+ load_config as load_log_config,
+ save_config as save_log_config,
+ update_component,
+ update_global_level,
+ update_timestamp_format,
+ update_api_filters,
+ reset_to_defaults,
+ reload_all_loggers
+)
+import time
+from fnmatch import fnmatch
nest_asyncio.apply()
+# Initialize API logger
+logger = get_logger('api')
+api_requests_logger = get_logger('api.requests')
+
# ========== GPU Selection Helper ==========
def get_current_gpu_url():
"""Get the URL for the currently selected GPU"""
@@ -70,6 +87,58 @@ def get_current_gpu_url():
app = FastAPI()
+# ========== Logging Middleware ==========
+@app.middleware("http")
+async def log_requests(request: Request, call_next):
+ """Middleware to log HTTP requests based on configuration."""
+ start_time = time.time()
+
+ # Get logging config
+ log_config = load_log_config()
+ api_config = log_config.get('components', {}).get('api.requests', {})
+
+ # Check if API request logging is enabled
+ if not api_config.get('enabled', False):
+ return await call_next(request)
+
+ # Get filters
+ filters = api_config.get('filters', {})
+ exclude_paths = filters.get('exclude_paths', [])
+ exclude_status = filters.get('exclude_status', [])
+ include_slow_requests = filters.get('include_slow_requests', True)
+ slow_threshold_ms = filters.get('slow_threshold_ms', 1000)
+
+ # Process request
+ response = await call_next(request)
+
+ # Calculate duration
+ duration_ms = (time.time() - start_time) * 1000
+
+ # Check if path should be excluded
+ path = request.url.path
+ for pattern in exclude_paths:
+ if fnmatch(path, pattern):
+ return response
+
+ # Check if status should be excluded (unless it's a slow request)
+ is_slow = duration_ms >= slow_threshold_ms
+ if response.status_code in exclude_status and not (include_slow_requests and is_slow):
+ return response
+
+ # Log the request
+ log_msg = f"{request.method} {path} - {response.status_code} ({duration_ms:.2f}ms)"
+
+ if is_slow:
+ api_requests_logger.warning(f"SLOW REQUEST: {log_msg}")
+ elif response.status_code >= 500:
+ api_requests_logger.error(log_msg)
+ elif response.status_code >= 400:
+ api_requests_logger.warning(log_msg)
+ else:
+ api_requests_logger.api(log_msg)
+
+ return response
+
# Serve static folder
app.mount("/static", StaticFiles(directory="static"), name="static")
@@ -354,7 +423,7 @@ def trigger_argument(data: BipolarTriggerRequest):
channel_id, message_id, globals.client, data.context
)
if not success:
- print(f"⚠️ Failed to trigger argument from message: {error}")
+ logger.error(f"Failed to trigger argument from message: {error}")
globals.client.loop.create_task(trigger_from_message())
@@ -419,17 +488,17 @@ def trigger_dialogue(data: dict):
continue
if not message:
- print(f"⚠️ Message {message_id} not found")
+ logger.error(f"Message {message_id} not found")
return
# Check if there's already an argument or dialogue in progress
dialogue_manager = get_dialogue_manager()
if dialogue_manager.is_dialogue_active(message.channel.id):
- print(f"⚠️ Dialogue already active in channel {message.channel.id}")
+ logger.error(f"Dialogue already active in channel {message.channel.id}")
return
if is_argument_in_progress(message.channel.id):
- print(f"⚠️ Argument already in progress in channel {message.channel.id}")
+ logger.error(f"Argument already in progress in channel {message.channel.id}")
return
# Determine current persona from the message author
@@ -441,12 +510,12 @@ def trigger_dialogue(data: dict):
current_persona = "evil" if globals.EVIL_MODE else "miku"
else:
# User message - can't trigger dialogue from user messages
- print(f"⚠️ Cannot trigger dialogue from user message")
+ logger.error(f"Cannot trigger dialogue from user message")
return
opposite_persona = "evil" if current_persona == "miku" else "miku"
- print(f"🎭 [Manual Trigger] Forcing {opposite_persona} to start dialogue on message {message_id}")
+ logger.info(f"[Manual Trigger] Forcing {opposite_persona} to start dialogue on message {message_id}")
# Force start the dialogue (bypass interjection check)
dialogue_manager.start_dialogue(message.channel.id)
@@ -459,7 +528,7 @@ def trigger_dialogue(data: dict):
)
except Exception as e:
- print(f"⚠️ Error triggering dialogue: {e}")
+ logger.error(f"Error triggering dialogue: {e}")
import traceback
traceback.print_exc()
@@ -514,8 +583,6 @@ def get_gpu_status():
@app.post("/gpu-select")
async def select_gpu(request: Request):
"""Select which GPU to use for inference"""
- from utils.gpu_preload import preload_amd_models
-
data = await request.json()
gpu = data.get("gpu", "nvidia").lower()
@@ -532,16 +599,10 @@ async def select_gpu(request: Request):
with open(gpu_state_file, "w") as f:
json.dump(state, f, indent=2)
- print(f"🎮 GPU Selection: Switched to {gpu.upper()} GPU")
-
- # Preload models on AMD GPU (16GB VRAM - can hold both text + vision)
- if gpu == "amd":
- asyncio.create_task(preload_amd_models())
- print("🔧 Preloading text and vision models on AMD GPU...")
-
+ logger.info(f"GPU Selection: Switched to {gpu.upper()} GPU")
return {"status": "ok", "message": f"Switched to {gpu.upper()} GPU", "gpu": gpu}
except Exception as e:
- print(f"🎮 GPU Selection Error: {e}")
+ logger.error(f"GPU Selection Error: {e}")
return {"status": "error", "message": str(e)}
@app.get("/bipolar-mode/arguments")
@@ -574,17 +635,17 @@ async def set_server_mood_endpoint(guild_id: int, data: MoodSetRequest):
# Check if server exists
if guild_id not in server_manager.servers:
- print(f"🎭 API: Server {guild_id} not found in server_manager.servers")
+ logger.warning(f"Server {guild_id} not found in server_manager.servers")
return {"status": "error", "message": "Server not found"}
# Check if mood is valid
from utils.moods import MOOD_EMOJIS
if data.mood not in MOOD_EMOJIS:
- print(f"🎭 API: Mood '{data.mood}' not found in MOOD_EMOJIS. Available moods: {list(MOOD_EMOJIS.keys())}")
+ logger.warning(f"Mood '{data.mood}' not found in MOOD_EMOJIS. Available moods: {list(MOOD_EMOJIS.keys())}")
return {"status": "error", "message": f"Mood '{data.mood}' not recognized. Available moods: {', '.join(MOOD_EMOJIS.keys())}"}
success = server_manager.set_server_mood(guild_id, data.mood)
- print(f"🎭 API: Server mood set result: {success}")
+ logger.debug(f"Server mood set result: {success}")
if success:
# V2: Notify autonomous engine of mood change
@@ -592,30 +653,30 @@ async def set_server_mood_endpoint(guild_id: int, data: MoodSetRequest):
from utils.autonomous import on_mood_change
on_mood_change(guild_id, data.mood)
except Exception as e:
- print(f"⚠️ API: Failed to notify autonomous engine of mood change: {e}")
+ logger.error(f"Failed to notify autonomous engine of mood change: {e}")
# Update the nickname for this server
from utils.moods import update_server_nickname
- print(f"🎭 API: Updating nickname for server {guild_id}")
+ logger.debug(f"Updating nickname for server {guild_id}")
globals.client.loop.create_task(update_server_nickname(guild_id))
return {"status": "ok", "new_mood": data.mood, "guild_id": guild_id}
- print(f"🎭 API: set_server_mood returned False for unknown reason")
+ logger.warning(f"set_server_mood returned False for unknown reason")
return {"status": "error", "message": "Failed to set server mood"}
@app.post("/servers/{guild_id}/mood/reset")
async def reset_server_mood_endpoint(guild_id: int):
"""Reset mood to neutral for a specific server"""
- print(f"🎭 API: Resetting mood for server {guild_id} to neutral")
+ logger.debug(f"Resetting mood for server {guild_id} to neutral")
# Check if server exists
if guild_id not in server_manager.servers:
- print(f"🎭 API: Server {guild_id} not found in server_manager.servers")
+ logger.warning(f"Server {guild_id} not found in server_manager.servers")
return {"status": "error", "message": "Server not found"}
- print(f"🎭 API: Server validation passed, calling set_server_mood")
+ logger.debug(f"Server validation passed, calling set_server_mood")
success = server_manager.set_server_mood(guild_id, "neutral")
- print(f"🎭 API: Server mood reset result: {success}")
+ logger.debug(f"Server mood reset result: {success}")
if success:
# V2: Notify autonomous engine of mood change
@@ -623,15 +684,15 @@ async def reset_server_mood_endpoint(guild_id: int):
from utils.autonomous import on_mood_change
on_mood_change(guild_id, "neutral")
except Exception as e:
- print(f"⚠️ API: Failed to notify autonomous engine of mood reset: {e}")
+ logger.error(f"Failed to notify autonomous engine of mood reset: {e}")
# Update the nickname for this server
from utils.moods import update_server_nickname
- print(f"🎭 API: Updating nickname for server {guild_id}")
+ logger.debug(f"Updating nickname for server {guild_id}")
globals.client.loop.create_task(update_server_nickname(guild_id))
return {"status": "ok", "new_mood": "neutral", "guild_id": guild_id}
- print(f"🎭 API: set_server_mood returned False for unknown reason")
+ logger.warning(f"set_server_mood returned False for unknown reason")
return {"status": "error", "message": "Failed to reset server mood"}
@app.get("/servers/{guild_id}/mood/state")
@@ -788,22 +849,22 @@ async def trigger_autonomous_reaction(guild_id: int = None):
async def trigger_detect_and_join_conversation(guild_id: int = None):
# If guild_id is provided, detect and join conversation only for that server
# If no guild_id, trigger for all servers
- print(f"🔍 [API] Join conversation endpoint called with guild_id={guild_id}")
+ logger.debug(f"Join conversation endpoint called with guild_id={guild_id}")
if globals.client and globals.client.loop and globals.client.loop.is_running():
if guild_id is not None:
# Trigger for specific server only (force=True to bypass checks when manually triggered)
- print(f"🔍 [API] Importing and calling miku_detect_and_join_conversation_for_server({guild_id}, force=True)")
+ logger.debug(f"Importing and calling miku_detect_and_join_conversation_for_server({guild_id}, force=True)")
from utils.autonomous import miku_detect_and_join_conversation_for_server
globals.client.loop.create_task(miku_detect_and_join_conversation_for_server(guild_id, force=True))
return {"status": "ok", "message": f"Detect and join conversation queued for server {guild_id}"}
else:
# Trigger for all servers (force=True to bypass checks when manually triggered)
- print(f"🔍 [API] Importing and calling miku_detect_and_join_conversation() for all servers")
+ logger.debug(f"Importing and calling miku_detect_and_join_conversation() for all servers")
from utils.autonomous import miku_detect_and_join_conversation
globals.client.loop.create_task(miku_detect_and_join_conversation(force=True))
return {"status": "ok", "message": "Detect and join conversation queued for all servers"}
else:
- print(f"⚠️ [API] Bot not ready: client={globals.client}, loop={globals.client.loop if globals.client else None}")
+ logger.error(f"Bot not ready: client={globals.client}, loop={globals.client.loop if globals.client else None}")
return {"status": "error", "message": "Bot not ready"}
@app.post("/profile-picture/change")
@@ -834,7 +895,7 @@ async def trigger_profile_picture_change(
custom_image_bytes = None
if file:
custom_image_bytes = await file.read()
- print(f"🖼️ Received custom image upload ({len(custom_image_bytes)} bytes)")
+ logger.info(f"Received custom image upload ({len(custom_image_bytes)} bytes)")
# Change profile picture
result = await profile_picture_manager.change_profile_picture(
@@ -858,7 +919,7 @@ async def trigger_profile_picture_change(
}
except Exception as e:
- print(f"⚠️ Error in profile picture API: {e}")
+ logger.error(f"Error in profile picture API: {e}")
import traceback
traceback.print_exc()
return {"status": "error", "message": f"Unexpected error: {str(e)}"}
@@ -955,7 +1016,7 @@ async def manual_send(
'content': file_content
})
except Exception as e:
- print(f"❌ Failed to read file {file.filename}: {e}")
+ logger.error(f"Failed to read file {file.filename}: {e}")
return {"status": "error", "message": f"Failed to read file {file.filename}: {e}"}
# Use create_task to avoid timeout context manager error
@@ -967,28 +1028,28 @@ async def manual_send(
try:
reference_message = await channel.fetch_message(int(reply_to_message_id))
except Exception as e:
- print(f"⚠️ Could not fetch message {reply_to_message_id} for reply: {e}")
+ logger.error(f"Could not fetch message {reply_to_message_id} for reply: {e}")
return
# Send the main message
if message.strip():
if reference_message:
await channel.send(message, reference=reference_message, mention_author=mention_author)
- print(f"✅ Manual message sent as reply to #{channel.name}")
+ logger.info(f"Manual message sent as reply to #{channel.name}")
else:
await channel.send(message)
- print(f"✅ Manual message sent to #{channel.name}")
+ logger.info(f"Manual message sent to #{channel.name}")
# Send files if any
for file_info in file_data:
try:
await channel.send(file=discord.File(io.BytesIO(file_info['content']), filename=file_info['filename']))
- print(f"✅ File {file_info['filename']} sent to #{channel.name}")
+ logger.info(f"File {file_info['filename']} sent to #{channel.name}")
except Exception as e:
- print(f"❌ Failed to send file {file_info['filename']}: {e}")
+ logger.error(f"Failed to send file {file_info['filename']}: {e}")
except Exception as e:
- print(f"❌ Failed to send message: {e}")
+ logger.error(f"Failed to send message: {e}")
globals.client.loop.create_task(send_message_and_files())
return {"status": "ok", "message": "Message and files queued for sending"}
@@ -1028,7 +1089,7 @@ async def manual_send_webhook(
'content': file_content
})
except Exception as e:
- print(f"❌ Failed to read file {file.filename}: {e}")
+ logger.error(f"Failed to read file {file.filename}: {e}")
return {"status": "error", "message": f"Failed to read file {file.filename}: {e}"}
# Use create_task to avoid timeout context manager error
@@ -1037,7 +1098,7 @@ async def manual_send_webhook(
# Get or create webhooks for this channel (inside the task)
webhooks = await get_or_create_webhooks_for_channel(channel)
if not webhooks:
- print(f"❌ Failed to create webhooks for channel #{channel.name}")
+ logger.error(f"Failed to create webhooks for channel #{channel.name}")
return
# Select the appropriate webhook
@@ -1065,10 +1126,10 @@ async def manual_send_webhook(
)
persona_name = "Evil Miku" if persona == "evil" else "Hatsune Miku"
- print(f"✅ Manual webhook message sent as {persona_name} to #{channel.name}")
+ logger.info(f"Manual webhook message sent as {persona_name} to #{channel.name}")
except Exception as e:
- print(f"❌ Failed to send webhook message: {e}")
+ logger.error(f"Failed to send webhook message: {e}")
import traceback
traceback.print_exc()
@@ -1196,7 +1257,7 @@ async def delete_figurine_subscriber(user_id: str):
async def figurines_send_now(tweet_url: str = Form(None)):
"""Trigger immediate figurine DM send to all subscribers, optionally with specific tweet URL"""
if globals.client and globals.client.loop and globals.client.loop.is_running():
- print(f"🚀 API: Sending figurine DMs to all subscribers, tweet_url: {tweet_url}")
+ logger.info(f"Sending figurine DMs to all subscribers, tweet_url: {tweet_url}")
globals.client.loop.create_task(send_figurine_dm_to_all_subscribers(globals.client, tweet_url=tweet_url))
return {"status": "ok", "message": "Figurine DMs queued"}
return {"status": "error", "message": "Bot not ready"}
@@ -1205,24 +1266,24 @@ async def figurines_send_now(tweet_url: str = Form(None)):
@app.post("/figurines/send_to_user")
async def figurines_send_to_user(user_id: str = Form(...), tweet_url: str = Form(None)):
"""Send figurine DM to a specific user, optionally with specific tweet URL"""
- print(f"🎯 API: Received figurine send request - user_id: '{user_id}', tweet_url: '{tweet_url}'")
+ logger.debug(f"Received figurine send request - user_id: '{user_id}', tweet_url: '{tweet_url}'")
if not globals.client or not globals.client.loop or not globals.client.loop.is_running():
- print("❌ API: Bot not ready")
+ logger.error("Bot not ready")
return {"status": "error", "message": "Bot not ready"}
try:
user_id_int = int(user_id)
- print(f"✅ API: Parsed user_id as {user_id_int}")
+ logger.debug(f"Parsed user_id as {user_id_int}")
except ValueError:
- print(f"❌ API: Invalid user ID: '{user_id}'")
+ logger.error(f"Invalid user ID: '{user_id}'")
return {"status": "error", "message": "Invalid user ID"}
# Clean up tweet URL if it's empty string
if tweet_url == "":
tweet_url = None
- print(f"🎯 API: Sending figurine DM to user {user_id_int}, tweet_url: {tweet_url}")
+ logger.info(f"Sending figurine DM to user {user_id_int}, tweet_url: {tweet_url}")
# Queue the DM send task in the bot's event loop
globals.client.loop.create_task(send_figurine_dm_to_single_user(globals.client, user_id_int, tweet_url=tweet_url))
@@ -1233,22 +1294,22 @@ async def figurines_send_to_user(user_id: str = Form(...), tweet_url: str = Form
@app.get("/servers")
def get_servers():
"""Get all configured servers"""
- print(f"🎭 API: /servers endpoint called")
- print(f"🎭 API: server_manager.servers keys: {list(server_manager.servers.keys())}")
- print(f"🎭 API: server_manager.servers count: {len(server_manager.servers)}")
+ logger.debug("/servers endpoint called")
+ logger.debug(f"server_manager.servers keys: {list(server_manager.servers.keys())}")
+ logger.debug(f"server_manager.servers count: {len(server_manager.servers)}")
# Debug: Check config file directly
config_file = server_manager.config_file
- print(f"🎭 API: Config file path: {config_file}")
+ logger.debug(f"Config file path: {config_file}")
if os.path.exists(config_file):
try:
with open(config_file, "r", encoding="utf-8") as f:
config_data = json.load(f)
- print(f"🎭 API: Config file contains: {list(config_data.keys())}")
+ logger.debug(f"Config file contains: {list(config_data.keys())}")
except Exception as e:
- print(f"🎭 API: Failed to read config file: {e}")
+ logger.error(f"Failed to read config file: {e}")
else:
- print(f"🎭 API: Config file does not exist")
+ logger.warning("Config file does not exist")
servers = []
for server in server_manager.get_all_servers():
@@ -1260,10 +1321,10 @@ def get_servers():
server_data['guild_id'] = str(server_data['guild_id'])
servers.append(server_data)
- print(f"🎭 API: Adding server to response: {server_data['guild_id']} - {server_data['guild_name']}")
- print(f"🎭 API: Server data type check - guild_id: {type(server_data['guild_id'])}, value: {server_data['guild_id']}")
+ logger.debug(f"Adding server to response: {server_data['guild_id']} - {server_data['guild_name']}")
+ logger.debug(f"Server data type check - guild_id: {type(server_data['guild_id'])}, value: {server_data['guild_id']}")
- print(f"🎭 API: Returning {len(servers)} servers")
+ logger.debug(f"Returning {len(servers)} servers")
# Debug: Show exact JSON being sent
import json
@@ -1316,7 +1377,7 @@ def update_server(guild_id: int, data: dict):
@app.post("/servers/{guild_id}/bedtime-range")
def update_server_bedtime_range(guild_id: int, data: dict):
"""Update server bedtime range configuration"""
- print(f"⏰ API: Updating bedtime range for server {guild_id}: {data}")
+ logger.debug(f"Updating bedtime range for server {guild_id}: {data}")
# Validate the data
required_fields = ['bedtime_hour', 'bedtime_minute', 'bedtime_hour_end', 'bedtime_minute_end']
@@ -1346,7 +1407,7 @@ def update_server_bedtime_range(guild_id: int, data: dict):
# Update just the bedtime job for this server (avoid restarting all schedulers)
job_success = server_manager.update_server_bedtime_job(guild_id, globals.client)
if job_success:
- print(f"✅ API: Bedtime range updated for server {guild_id}")
+ logger.info(f"Bedtime range updated for server {guild_id}")
return {
"status": "ok",
"message": f"Bedtime range updated: {bedtime_hour:02d}:{bedtime_minute:02d} - {bedtime_hour_end:02d}:{bedtime_minute_end:02d}"
@@ -1413,14 +1474,14 @@ async def send_custom_prompt_dm(user_id: str, req: CustomPromptRequest):
try:
response = await query_llama(req.prompt, user_id=user_id, guild_id=None, response_type="dm_response")
await user.send(response)
- print(f"✅ Custom DM prompt sent to user {user_id}: {req.prompt[:50]}...")
+ logger.info(f"Custom DM prompt sent to user {user_id}: {req.prompt[:50]}...")
# Log to DM history
from utils.dm_logger import dm_logger
dm_logger.log_conversation(user_id, req.prompt, response)
except Exception as e:
- print(f"❌ Failed to send custom DM prompt to user {user_id}: {e}")
+ logger.error(f"Failed to send custom DM prompt to user {user_id}: {e}")
# Use create_task to avoid timeout context manager error
globals.client.loop.create_task(send_dm_custom_prompt())
@@ -1456,7 +1517,7 @@ async def send_manual_message_dm(
'content': file_content
})
except Exception as e:
- print(f"❌ Failed to read file {file.filename}: {e}")
+ logger.error(f"Failed to read file {file.filename}: {e}")
return {"status": "error", "message": f"Failed to read file {file.filename}: {e}"}
async def send_dm_message_and_files():
@@ -1468,32 +1529,32 @@ async def send_manual_message_dm(
dm_channel = user.dm_channel or await user.create_dm()
reference_message = await dm_channel.fetch_message(int(reply_to_message_id))
except Exception as e:
- print(f"⚠️ Could not fetch DM message {reply_to_message_id} for reply: {e}")
+ logger.error(f"Could not fetch DM message {reply_to_message_id} for reply: {e}")
return
# Send the main message
if message.strip():
if reference_message:
await user.send(message, reference=reference_message, mention_author=mention_author)
- print(f"✅ Manual DM reply message sent to user {user_id}")
+ logger.info(f"Manual DM reply message sent to user {user_id}")
else:
await user.send(message)
- print(f"✅ Manual DM message sent to user {user_id}")
+ logger.info(f"Manual DM message sent to user {user_id}")
# Send files if any
for file_info in file_data:
try:
await user.send(file=discord.File(io.BytesIO(file_info['content']), filename=file_info['filename']))
- print(f"✅ File {file_info['filename']} sent via DM to user {user_id}")
+ logger.info(f"File {file_info['filename']} sent via DM to user {user_id}")
except Exception as e:
- print(f"❌ Failed to send file {file_info['filename']} via DM: {e}")
+ logger.error(f"Failed to send file {file_info['filename']} via DM: {e}")
# Log to DM history (user message = manual override trigger, miku response = the message sent)
from utils.dm_logger import dm_logger
dm_logger.log_conversation(user_id, "[Manual Override Trigger]", message, attachments=[f['filename'] for f in file_data])
except Exception as e:
- print(f"❌ Failed to send manual DM to user {user_id}: {e}")
+ logger.error(f"Failed to send manual DM to user {user_id}: {e}")
# Use create_task to avoid timeout context manager error
globals.client.loop.create_task(send_dm_message_and_files())
@@ -1559,7 +1620,7 @@ async def test_image_detection(req: dict):
async def view_generated_image(filename: str):
"""Serve generated images from ComfyUI output directory"""
try:
- print(f"🖼️ Image view request for: {filename}")
+ logger.debug(f"Image view request for: {filename}")
# Try multiple possible paths for ComfyUI output
possible_paths = [
@@ -1572,13 +1633,13 @@ async def view_generated_image(filename: str):
for path in possible_paths:
if os.path.exists(path):
image_path = path
- print(f"✅ Found image at: {path}")
+ logger.debug(f"Found image at: {path}")
break
else:
- print(f"❌ Not found at: {path}")
+ logger.debug(f"Not found at: {path}")
if not image_path:
- print(f"❌ Image not found anywhere: {filename}")
+ logger.warning(f"Image not found anywhere: {filename}")
return {"status": "error", "message": f"Image not found: {filename}"}
# Determine content type based on file extension
@@ -1591,11 +1652,11 @@ async def view_generated_image(filename: str):
elif ext == "webp":
content_type = "image/webp"
- print(f"📤 Serving image: {image_path} as {content_type}")
+ logger.info(f"Serving image: {image_path} as {content_type}")
return FileResponse(image_path, media_type=content_type)
except Exception as e:
- print(f"❌ Error serving image: {e}")
+ logger.error(f"Error serving image: {e}")
return {"status": "error", "message": f"Error serving image: {e}"}
@app.post("/servers/{guild_id}/autonomous/tweet")
@@ -1638,36 +1699,36 @@ def get_available_moods():
@app.post("/test/mood/{guild_id}")
async def test_mood_change(guild_id: int, data: MoodSetRequest):
"""Test endpoint for debugging mood changes"""
- print(f"🧪 TEST: Testing mood change for server {guild_id} to {data.mood}")
+ logger.debug(f"TEST: Testing mood change for server {guild_id} to {data.mood}")
# Check if server exists
if guild_id not in server_manager.servers:
return {"status": "error", "message": f"Server {guild_id} not found"}
server_config = server_manager.get_server_config(guild_id)
- print(f"🧪 TEST: Server config found: {server_config.guild_name if server_config else 'None'}")
+ logger.debug(f"TEST: Server config found: {server_config.guild_name if server_config else 'None'}")
# Try to set mood
success = server_manager.set_server_mood(guild_id, data.mood)
- print(f"🧪 TEST: Mood set result: {success}")
+ logger.debug(f"TEST: Mood set result: {success}")
if success:
# V2: Notify autonomous engine of mood change
try:
from utils.autonomous import on_mood_change
on_mood_change(guild_id, data.mood)
- print(f"🧪 TEST: Notified autonomous engine of mood change")
+ logger.debug(f"TEST: Notified autonomous engine of mood change")
except Exception as e:
- print(f"⚠️ TEST: Failed to notify autonomous engine: {e}")
+ logger.error(f"TEST: Failed to notify autonomous engine: {e}")
# Try to update nickname
from utils.moods import update_server_nickname
- print(f"🧪 TEST: Attempting nickname update...")
+ logger.debug(f"TEST: Attempting nickname update...")
try:
await update_server_nickname(guild_id)
- print(f"🧪 TEST: Nickname update completed")
+ logger.debug(f"TEST: Nickname update completed")
except Exception as e:
- print(f"🧪 TEST: Nickname update failed: {e}")
+ logger.error(f"TEST: Nickname update failed: {e}")
import traceback
traceback.print_exc()
@@ -1707,10 +1768,10 @@ def get_dm_conversations(user_id: str, limit: int = 50):
from utils.dm_logger import dm_logger
# Convert string user_id to int for internal processing
user_id_int = int(user_id)
- print(f"🔍 API: Loading conversations for user {user_id_int}, limit: {limit}")
+ logger.debug(f"Loading conversations for user {user_id_int}, limit: {limit}")
logs = dm_logger._load_user_logs(user_id_int)
- print(f"🔍 API: Loaded logs for user {user_id_int}: {len(logs.get('conversations', []))} conversations")
+ logger.debug(f"Loaded logs for user {user_id_int}: {len(logs.get('conversations', []))} conversations")
conversations = logs["conversations"][-limit:] if limit > 0 else logs["conversations"]
@@ -1719,20 +1780,20 @@ def get_dm_conversations(user_id: str, limit: int = 50):
if "message_id" in conv:
conv["message_id"] = str(conv["message_id"])
- print(f"🔍 API: Returning {len(conversations)} conversations")
+ logger.debug(f"Returning {len(conversations)} conversations")
# Debug: Show message IDs being returned
for i, conv in enumerate(conversations):
msg_id = conv.get("message_id", "")
is_bot = conv.get("is_bot_message", False)
content_preview = conv.get("content", "")[:30] + "..." if conv.get("content", "") else "[No content]"
- print(f"🔍 API: Conv {i}: id={msg_id} (type: {type(msg_id)}), is_bot={is_bot}, content='{content_preview}'")
+ logger.debug(f"Conv {i}: id={msg_id} (type: {type(msg_id)}), is_bot={is_bot}, content='{content_preview}'")
return {"status": "ok", "conversations": conversations}
except ValueError:
return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
except Exception as e:
- print(f"❌ API: Failed to get conversations for user {user_id}: {e}")
+ logger.error(f"Failed to get conversations for user {user_id}: {e}")
return {"status": "error", "message": f"Failed to get conversations: {e}"}
@app.get("/dms/users/{user_id}/search")
@@ -1792,7 +1853,7 @@ def get_blocked_users():
blocked_users = dm_logger.get_blocked_users()
return {"status": "ok", "blocked_users": blocked_users}
except Exception as e:
- print(f"❌ API: Failed to get blocked users: {e}")
+ logger.error(f"Failed to get blocked users: {e}")
return {"status": "error", "message": f"Failed to get blocked users: {e}"}
@app.post("/dms/users/{user_id}/block")
@@ -1808,7 +1869,7 @@ def block_user(user_id: str):
success = dm_logger.block_user(user_id_int, username)
if success:
- print(f"🚫 API: User {user_id} ({username}) blocked")
+ logger.info(f"User {user_id} ({username}) blocked")
return {"status": "ok", "message": f"User {username} has been blocked"}
else:
return {"status": "error", "message": f"User {username} is already blocked"}
@@ -1816,7 +1877,7 @@ def block_user(user_id: str):
except ValueError:
return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
except Exception as e:
- print(f"❌ API: Failed to block user {user_id}: {e}")
+ logger.error(f"Failed to block user {user_id}: {e}")
return {"status": "error", "message": f"Failed to block user: {e}"}
@app.post("/dms/users/{user_id}/unblock")
@@ -1827,7 +1888,7 @@ def unblock_user(user_id: str):
success = dm_logger.unblock_user(user_id_int)
if success:
- print(f"✅ API: User {user_id} unblocked")
+ logger.info(f"User {user_id} unblocked")
return {"status": "ok", "message": f"User has been unblocked"}
else:
return {"status": "error", "message": f"User is not blocked"}
@@ -1835,7 +1896,7 @@ def unblock_user(user_id: str):
except ValueError:
return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
except Exception as e:
- print(f"❌ API: Failed to unblock user {user_id}: {e}")
+ logger.error(f"Failed to unblock user {user_id}: {e}")
return {"status": "error", "message": f"Failed to unblock user: {e}"}
@app.post("/dms/users/{user_id}/conversations/{conversation_id}/delete")
@@ -1853,13 +1914,13 @@ def delete_conversation(user_id: str, conversation_id: str):
# For now, return success immediately since we can't await in FastAPI sync endpoint
# The actual deletion happens asynchronously
- print(f"🗑️ API: Queued deletion of conversation {conversation_id} for user {user_id}")
+ logger.info(f"Queued deletion of conversation {conversation_id} for user {user_id}")
return {"status": "ok", "message": "Message deletion queued (will delete from both Discord and logs)"}
except ValueError:
return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
except Exception as e:
- print(f"❌ API: Failed to queue conversation deletion {conversation_id}: {e}")
+ logger.error(f"Failed to queue conversation deletion {conversation_id}: {e}")
return {"status": "error", "message": f"Failed to delete conversation: {e}"}
@app.post("/dms/users/{user_id}/conversations/delete-all")
@@ -1876,13 +1937,13 @@ def delete_all_conversations(user_id: str):
success = globals.client.loop.create_task(do_delete_all())
# Return success immediately since we can't await in FastAPI sync endpoint
- print(f"🗑️ API: Queued bulk deletion of all conversations for user {user_id}")
+ logger.info(f"Queued bulk deletion of all conversations for user {user_id}")
return {"status": "ok", "message": "Bulk deletion queued (will delete all Miku messages from Discord and clear logs)"}
except ValueError:
return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
except Exception as e:
- print(f"❌ API: Failed to queue bulk conversation deletion for user {user_id}: {e}")
+ logger.error(f"Failed to queue bulk conversation deletion for user {user_id}: {e}")
return {"status": "error", "message": f"Failed to delete conversations: {e}"}
@app.post("/dms/users/{user_id}/delete-completely")
@@ -1893,7 +1954,7 @@ def delete_user_completely(user_id: str):
success = dm_logger.delete_user_completely(user_id_int)
if success:
- print(f"🗑️ API: Completely deleted user {user_id}")
+ logger.info(f"Completely deleted user {user_id}")
return {"status": "ok", "message": "User data deleted completely"}
else:
return {"status": "error", "message": "No user data found"}
@@ -1901,7 +1962,7 @@ def delete_user_completely(user_id: str):
except ValueError:
return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
except Exception as e:
- print(f"❌ API: Failed to completely delete user {user_id}: {e}")
+ logger.error(f"Failed to completely delete user {user_id}: {e}")
return {"status": "error", "message": f"Failed to delete user: {e}"}
# ========== DM Interaction Analysis Endpoints ==========
@@ -1923,7 +1984,7 @@ def run_dm_analysis():
return {"status": "ok", "message": "DM analysis started"}
except Exception as e:
- print(f"❌ API: Failed to run DM analysis: {e}")
+ logger.error(f"Failed to run DM analysis: {e}")
return {"status": "error", "message": f"Failed to run DM analysis: {e}"}
@app.post("/dms/users/{user_id}/analyze")
@@ -1949,7 +2010,7 @@ def analyze_user_interaction(user_id: str):
except ValueError:
return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
except Exception as e:
- print(f"❌ API: Failed to analyze user {user_id}: {e}")
+ logger.error(f"Failed to analyze user {user_id}: {e}")
return {"status": "error", "message": f"Failed to analyze user: {e}"}
@app.get("/dms/analysis/reports")
@@ -1974,11 +2035,11 @@ def get_analysis_reports(limit: int = 20):
report['filename'] = filename
reports.append(report)
except Exception as e:
- print(f"⚠️ Failed to load report {filename}: {e}")
+ logger.warning(f"Failed to load report {filename}: {e}")
return {"status": "ok", "reports": reports}
except Exception as e:
- print(f"❌ API: Failed to get reports: {e}")
+ logger.error(f"Failed to get reports: {e}")
return {"status": "error", "message": f"Failed to get reports: {e}"}
@app.get("/dms/analysis/reports/{user_id}")
@@ -2005,13 +2066,13 @@ def get_user_reports(user_id: str, limit: int = 10):
report['filename'] = filename
reports.append(report)
except Exception as e:
- print(f"⚠️ Failed to load report {filename}: {e}")
+ logger.warning(f"Failed to load report {filename}: {e}")
return {"status": "ok", "reports": reports}
except ValueError:
return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
except Exception as e:
- print(f"❌ API: Failed to get user reports: {e}")
+ logger.error(f"Failed to get user reports: {e}")
return {"status": "error", "message": f"Failed to get user reports: {e}"}
# ========== Message Reaction Endpoint ==========
@@ -2043,15 +2104,15 @@ async def add_reaction_to_message(
try:
message = await channel.fetch_message(msg_id)
await message.add_reaction(emoji)
- print(f"✅ Added reaction {emoji} to message {msg_id} in channel #{channel.name}")
+ logger.info(f"Added reaction {emoji} to message {msg_id} in channel #{channel.name}")
except discord.NotFound:
- print(f"❌ Message {msg_id} not found in channel #{channel.name}")
+ logger.error(f"Message {msg_id} not found in channel #{channel.name}")
except discord.Forbidden:
- print(f"❌ Bot doesn't have permission to add reactions in channel #{channel.name}")
+ logger.error(f"Bot doesn't have permission to add reactions in channel #{channel.name}")
except discord.HTTPException as e:
- print(f"❌ Failed to add reaction: {e}")
+ logger.error(f"Failed to add reaction: {e}")
except Exception as e:
- print(f"❌ Unexpected error adding reaction: {e}")
+ logger.error(f"Unexpected error adding reaction: {e}")
globals.client.loop.create_task(add_reaction_task())
@@ -2061,7 +2122,7 @@ async def add_reaction_to_message(
}
except Exception as e:
- print(f"❌ API: Failed to add reaction: {e}")
+ logger.error(f"Failed to add reaction: {e}")
return {"status": "error", "message": f"Failed to add reaction: {e}"}
# ========== Autonomous V2 Endpoints ==========
@@ -2290,7 +2351,7 @@ Be detailed but conversational. React to what you see with Miku's cheerful, play
except Exception as e:
error_msg = f"Error in chat stream: {str(e)}"
- print(f"❌ {error_msg}")
+ logger.error(error_msg)
yield f"data: {json.dumps({'error': error_msg})}\n\n"
return StreamingResponse(
@@ -2303,6 +2364,168 @@ Be detailed but conversational. React to what you see with Miku's cheerful, play
}
)
+# ========== Log Management API ==========
+class LogConfigUpdateRequest(BaseModel):
+ component: Optional[str] = None
+ enabled: Optional[bool] = None
+ enabled_levels: Optional[List[str]] = None
+
+class LogFilterUpdateRequest(BaseModel):
+ exclude_paths: Optional[List[str]] = None
+ exclude_status: Optional[List[int]] = None
+ include_slow_requests: Optional[bool] = None
+ slow_threshold_ms: Optional[int] = None
+
+@app.get("/api/log/config")
+async def get_log_config():
+ """Get current logging configuration."""
+ try:
+ config = load_log_config()
+ logger.debug("Log config requested")
+ return {"success": True, "config": config}
+ except Exception as e:
+ logger.error(f"Failed to get log config: {e}")
+ return {"success": False, "error": str(e)}
+
+@app.post("/api/log/config")
+async def update_log_config(request: LogConfigUpdateRequest):
+ """Update logging configuration."""
+ try:
+ if request.component:
+ success = update_component(
+ request.component,
+ enabled=request.enabled,
+ enabled_levels=request.enabled_levels
+ )
+ if not success:
+ return {"success": False, "error": f"Failed to update component {request.component}"}
+
+ logger.info(f"Log config updated: component={request.component}, enabled_levels={request.enabled_levels}, enabled={request.enabled}")
+ return {"success": True, "message": "Configuration updated"}
+ except Exception as e:
+ logger.error(f"Failed to update log config: {e}")
+ return {"success": False, "error": str(e)}
+
+@app.get("/api/log/components")
+async def get_log_components():
+ """Get list of all logging components with their descriptions."""
+ try:
+ components = list_components()
+ stats = get_component_stats()
+ logger.debug("Log components list requested")
+ return {
+ "success": True,
+ "components": components,
+ "stats": stats
+ }
+ except Exception as e:
+ logger.error(f"Failed to get log components: {e}")
+ return {"success": False, "error": str(e)}
+
+@app.post("/api/log/reload")
+async def reload_log_config():
+ """Reload logging configuration from file."""
+ try:
+ success = reload_all_loggers()
+ if success:
+ logger.info("Log configuration reloaded")
+ return {"success": True, "message": "Configuration reloaded"}
+ else:
+ return {"success": False, "error": "Failed to reload configuration"}
+ except Exception as e:
+ logger.error(f"Failed to reload log config: {e}")
+ return {"success": False, "error": str(e)}
+
+@app.post("/api/log/filters")
+async def update_log_filters(request: LogFilterUpdateRequest):
+ """Update API request filtering configuration."""
+ try:
+ success = update_api_filters(
+ exclude_paths=request.exclude_paths,
+ exclude_status=request.exclude_status,
+ include_slow_requests=request.include_slow_requests,
+ slow_threshold_ms=request.slow_threshold_ms
+ )
+
+ if success:
+ logger.info(f"API filters updated: {request.dict(exclude_none=True)}")
+ return {"success": True, "message": "Filters updated"}
+ else:
+ return {"success": False, "error": "Failed to update filters"}
+ except Exception as e:
+ logger.error(f"Failed to update filters: {e}")
+ return {"success": False, "error": str(e)}
+
+@app.post("/api/log/reset")
+async def reset_log_config():
+ """Reset logging configuration to defaults."""
+ try:
+ success = reset_to_defaults()
+ if success:
+ logger.info("Log configuration reset to defaults")
+ return {"success": True, "message": "Configuration reset to defaults"}
+ else:
+ return {"success": False, "error": "Failed to reset configuration"}
+ except Exception as e:
+ logger.error(f"Failed to reset log config: {e}")
+ return {"success": False, "error": str(e)}
+
+@app.post("/api/log/global-level")
+async def update_global_level_endpoint(level: str, enabled: bool):
+ """Enable or disable a specific log level across all components."""
+ try:
+ from utils.log_config import update_global_level
+ success = update_global_level(level, enabled)
+ if success:
+ action = "enabled" if enabled else "disabled"
+ logger.info(f"Global level {level} {action} across all components")
+ return {"success": True, "message": f"Level {level} {action} globally"}
+ else:
+ return {"success": False, "error": f"Failed to update global level {level}"}
+ except Exception as e:
+ logger.error(f"Failed to update global level: {e}")
+ return {"success": False, "error": str(e)}
+
+@app.post("/api/log/timestamp-format")
+async def update_timestamp_format_endpoint(format_type: str):
+ """Update timestamp format for all log outputs."""
+ try:
+ success = update_timestamp_format(format_type)
+ if success:
+ logger.info(f"Timestamp format updated to: {format_type}")
+ return {"success": True, "message": f"Timestamp format set to: {format_type}"}
+ else:
+ return {"success": False, "error": f"Invalid timestamp format: {format_type}"}
+ except Exception as e:
+ logger.error(f"Failed to update timestamp format: {e}")
+ return {"success": False, "error": str(e)}
+
+@app.get("/api/log/files/{component}")
+async def get_log_file(component: str, lines: int = 100):
+ """Get last N lines from a component's log file."""
+ try:
+ from pathlib import Path
+ log_dir = Path('/app/memory/logs')
+ log_file = log_dir / f'{component.replace(".", "_")}.log'
+
+ if not log_file.exists():
+ return {"success": False, "error": "Log file not found"}
+
+ with open(log_file, 'r', encoding='utf-8') as f:
+ all_lines = f.readlines()
+ last_lines = all_lines[-lines:] if len(all_lines) >= lines else all_lines
+
+ logger.debug(f"Log file requested: {component} ({lines} lines)")
+ return {
+ "success": True,
+ "component": component,
+ "lines": last_lines,
+ "total_lines": len(all_lines)
+ }
+ except Exception as e:
+ logger.error(f"Failed to read log file for {component}: {e}")
+ return {"success": False, "error": str(e)}
+
def start_api():
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=3939)
diff --git a/bot/bot.py b/bot/bot.py
index 5ded028..8d604ef 100644
--- a/bot/bot.py
+++ b/bot/bot.py
@@ -46,9 +46,13 @@ from utils.autonomous import (
)
from utils.dm_logger import dm_logger
from utils.dm_interaction_analyzer import init_dm_analyzer
+from utils.logger import get_logger
import globals
+# Initialize bot logger
+logger = get_logger('bot')
+
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s: %(message)s",
@@ -61,10 +65,14 @@ logging.basicConfig(
@globals.client.event
async def on_ready():
- print(f'🎤 MikuBot connected as {globals.client.user}')
- print(f'💬 DM support enabled - users can message Miku directly!')
+ logger.info(f'🎤 MikuBot connected as {globals.client.user}')
+ logger.info(f'💬 DM support enabled - users can message Miku directly!')
globals.BOT_USER = globals.client.user
+
+ # Intercept external library loggers (APScheduler, etc.)
+ from utils.logger import intercept_external_loggers
+ intercept_external_loggers()
# Restore evil mode state from previous session (if any)
from utils.evil_mode import restore_evil_mode_on_startup
@@ -77,7 +85,7 @@ async def on_ready():
# Initialize DM interaction analyzer
if globals.OWNER_USER_ID and globals.OWNER_USER_ID != 0:
init_dm_analyzer(globals.OWNER_USER_ID)
- print(f"📊 DM Interaction Analyzer initialized for owner ID: {globals.OWNER_USER_ID}")
+ logger.info(f"📊 DM Interaction Analyzer initialized for owner ID: {globals.OWNER_USER_ID}")
# Schedule daily DM analysis (runs at 2 AM every day)
from utils.scheduled import run_daily_dm_analysis
@@ -88,9 +96,9 @@ async def on_ready():
minute=0,
id='daily_dm_analysis'
)
- print("⏰ Scheduled daily DM analysis at 2:00 AM")
+ logger.info("⏰ Scheduled daily DM analysis at 2:00 AM")
else:
- print("⚠️ OWNER_USER_ID not set, DM analysis feature disabled")
+ logger.warning("OWNER_USER_ID not set, DM analysis feature disabled")
# Setup autonomous speaking (now handled by server manager)
setup_autonomous_speaking()
@@ -146,7 +154,7 @@ async def on_message(message):
await replied_msg.reply(file=discord.File(output_video))
except Exception as e:
- print(f"⚠️ Error processing video: {e}")
+ logger.error(f"Error processing video: {e}")
await message.channel.send("Sorry, something went wrong while generating the video.")
return
@@ -159,11 +167,11 @@ async def on_message(message):
miku_addressed = await is_miku_addressed(message)
if is_dm:
- print(f"💌 DM from {message.author.display_name}: {message.content[:50]}{'...' if len(message.content) > 50 else ''}")
+ logger.info(f"💌 DM from {message.author.display_name}: {message.content[:50]}{'...' if len(message.content) > 50 else ''}")
# Check if user is blocked
if dm_logger.is_user_blocked(message.author.id):
- print(f"🚫 Blocked user {message.author.display_name} ({message.author.id}) tried to send DM - ignoring")
+ logger.info(f"🚫 Blocked user {message.author.display_name} ({message.author.id}) tried to send DM - ignoring")
return
# Log the user's DM message
@@ -185,7 +193,7 @@ async def on_message(message):
# Add reply context marker to the prompt
prompt = f'[Replying to your message: "{replied_content}"] {prompt}'
except Exception as e:
- print(f"⚠️ Failed to fetch replied message for context: {e}")
+ logger.error(f"Failed to fetch replied message for context: {e}")
async with message.channel.typing():
# If message has an image, video, or GIF attachment
@@ -212,9 +220,9 @@ async def on_message(message):
)
if is_dm:
- print(f"💌 DM image response to {message.author.display_name} (using DM mood: {globals.DM_MOOD})")
+ logger.info(f"💌 DM image response to {message.author.display_name} (using DM mood: {globals.DM_MOOD})")
else:
- print(f"💬 Server image response to {message.author.display_name} in {message.guild.name} (using server mood)")
+ logger.info(f"💬 Server image response to {message.author.display_name} in {message.guild.name} (using server mood)")
response_message = await message.channel.send(miku_reply)
@@ -229,7 +237,7 @@ async def on_message(message):
current_persona = "evil" if globals.EVIL_MODE else "miku"
asyncio.create_task(check_for_interjection(response_message, current_persona))
except Exception as e:
- print(f"⚠️ Error checking for persona interjection: {e}")
+ logger.error(f"Error checking for persona interjection: {e}")
return
@@ -239,7 +247,7 @@ async def on_message(message):
is_gif = attachment.filename.lower().endswith('.gif')
media_type = "gif" if is_gif else "video"
- print(f"🎬 Processing {media_type}: {attachment.filename}")
+ logger.debug(f"🎬 Processing {media_type}: {attachment.filename}")
# Download the media
media_bytes_b64 = await download_and_encode_media(attachment.url)
@@ -253,13 +261,13 @@ async def on_message(message):
# If it's a GIF, convert to MP4 for better processing
if is_gif:
- print(f"🔄 Converting GIF to MP4 for processing...")
+ logger.debug(f"🔄 Converting GIF to MP4 for processing...")
mp4_bytes = await convert_gif_to_mp4(media_bytes)
if mp4_bytes:
media_bytes = mp4_bytes
- print(f"✅ GIF converted to MP4")
+ logger.info(f"✅ GIF converted to MP4")
else:
- print(f"⚠️ GIF conversion failed, trying direct processing")
+ logger.warning(f"GIF conversion failed, trying direct processing")
# Extract frames
frames = await extract_video_frames(media_bytes, num_frames=6)
@@ -268,7 +276,7 @@ async def on_message(message):
await message.channel.send(f"I couldn't extract frames from that {media_type}, sorry!")
return
- print(f"📹 Extracted {len(frames)} frames from {attachment.filename}")
+ logger.debug(f"📹 Extracted {len(frames)} frames from {attachment.filename}")
# Analyze the video/GIF with appropriate media type
video_description = await analyze_video_with_vision(frames, media_type=media_type)
@@ -284,9 +292,9 @@ async def on_message(message):
)
if is_dm:
- print(f"💌 DM {media_type} response to {message.author.display_name} (using DM mood: {globals.DM_MOOD})")
+ logger.info(f"💌 DM {media_type} response to {message.author.display_name} (using DM mood: {globals.DM_MOOD})")
else:
- print(f"💬 Server video response to {message.author.display_name} in {message.guild.name} (using server mood)")
+ logger.info(f"💬 Server video response to {message.author.display_name} in {message.guild.name} (using server mood)")
response_message = await message.channel.send(miku_reply)
@@ -301,7 +309,7 @@ async def on_message(message):
current_persona = "evil" if globals.EVIL_MODE else "miku"
asyncio.create_task(check_for_interjection(response_message, current_persona))
except Exception as e:
- print(f"⚠️ Error checking for persona interjection: {e}")
+ logger.error(f"Error checking for persona interjection: {e}")
return
@@ -310,7 +318,7 @@ async def on_message(message):
for embed in message.embeds:
# Handle Tenor GIF embeds specially (Discord uses these for /gif command)
if embed.type == 'gifv' and embed.url and 'tenor.com' in embed.url:
- print(f"🎭 Processing Tenor GIF from embed: {embed.url}")
+ logger.info(f"🎭 Processing Tenor GIF from embed: {embed.url}")
# Extract the actual GIF URL from Tenor
gif_url = await extract_tenor_gif_url(embed.url)
@@ -322,7 +330,7 @@ async def on_message(message):
gif_url = embed.thumbnail.url
if not gif_url:
- print(f"⚠️ Could not extract GIF URL from Tenor embed")
+ logger.warning(f"Could not extract GIF URL from Tenor embed")
continue
# Download the GIF
@@ -336,13 +344,13 @@ async def on_message(message):
media_bytes = base64.b64decode(media_bytes_b64)
# Convert GIF to MP4
- print(f"🔄 Converting Tenor GIF to MP4 for processing...")
+ logger.debug(f"Converting Tenor GIF to MP4 for processing...")
mp4_bytes = await convert_gif_to_mp4(media_bytes)
if not mp4_bytes:
- print(f"⚠️ GIF conversion failed, trying direct frame extraction")
+ logger.warning(f"GIF conversion failed, trying direct frame extraction")
mp4_bytes = media_bytes
else:
- print(f"✅ Tenor GIF converted to MP4")
+ logger.debug(f"Tenor GIF converted to MP4")
# Extract frames
frames = await extract_video_frames(mp4_bytes, num_frames=6)
@@ -351,7 +359,7 @@ async def on_message(message):
await message.channel.send("I couldn't extract frames from that GIF, sorry!")
return
- print(f"📹 Extracted {len(frames)} frames from Tenor GIF")
+ logger.info(f"📹 Extracted {len(frames)} frames from Tenor GIF")
# Analyze the GIF with tenor_gif media type
video_description = await analyze_video_with_vision(frames, media_type="tenor_gif")
@@ -366,9 +374,9 @@ async def on_message(message):
)
if is_dm:
- print(f"💌 DM Tenor GIF response to {message.author.display_name} (using DM mood: {globals.DM_MOOD})")
+ logger.info(f"💌 DM Tenor GIF response to {message.author.display_name} (using DM mood: {globals.DM_MOOD})")
else:
- print(f"💬 Server Tenor GIF response to {message.author.display_name} in {message.guild.name} (using server mood)")
+ logger.info(f"💬 Server Tenor GIF response to {message.author.display_name} in {message.guild.name} (using server mood)")
response_message = await message.channel.send(miku_reply)
@@ -383,19 +391,19 @@ async def on_message(message):
current_persona = "evil" if globals.EVIL_MODE else "miku"
asyncio.create_task(check_for_interjection(response_message, current_persona))
except Exception as e:
- print(f"⚠️ Error checking for persona interjection: {e}")
+ logger.error(f"Error checking for persona interjection: {e}")
return
# Handle other types of embeds (rich, article, image, video, link)
elif embed.type in ['rich', 'article', 'image', 'video', 'link']:
- print(f"📰 Processing {embed.type} embed")
+ logger.error(f"Processing {embed.type} embed")
# Extract content from embed
embed_content = await extract_embed_content(embed)
if not embed_content['has_content']:
- print(f"⚠️ Embed has no extractable content, skipping")
+ logger.warning(f"Embed has no extractable content, skipping")
continue
# Build context string with embed text
@@ -406,28 +414,28 @@ async def on_message(message):
# Process images from embed
if embed_content['images']:
for img_url in embed_content['images']:
- print(f"🖼️ Processing image from embed: {img_url}")
+ logger.error(f"Processing image from embed: {img_url}")
try:
base64_img = await download_and_encode_image(img_url)
if base64_img:
- print(f"✅ Image downloaded, analyzing with vision model...")
+ logger.info(f"Image downloaded, analyzing with vision model...")
# Analyze image
qwen_description = await analyze_image_with_qwen(base64_img)
truncated = (qwen_description[:50] + "...") if len(qwen_description) > 50 else qwen_description
- print(f"📝 Vision analysis result: {truncated}")
+ logger.error(f"Vision analysis result: {truncated}")
if qwen_description and qwen_description.strip():
embed_context_parts.append(f"[Embedded image shows: {qwen_description}]")
else:
- print(f"❌ Failed to download image from embed")
+ logger.error(f"Failed to download image from embed")
except Exception as e:
- print(f"⚠️ Error processing embedded image: {e}")
+ logger.error(f"Error processing embedded image: {e}")
import traceback
traceback.print_exc()
# Process videos from embed
if embed_content['videos']:
for video_url in embed_content['videos']:
- print(f"🎬 Processing video from embed: {video_url}")
+ logger.info(f"🎬 Processing video from embed: {video_url}")
try:
media_bytes_b64 = await download_and_encode_media(video_url)
if media_bytes_b64:
@@ -435,17 +443,17 @@ async def on_message(message):
media_bytes = base64.b64decode(media_bytes_b64)
frames = await extract_video_frames(media_bytes, num_frames=6)
if frames:
- print(f"📹 Extracted {len(frames)} frames, analyzing with vision model...")
+ logger.info(f"📹 Extracted {len(frames)} frames, analyzing with vision model...")
video_description = await analyze_video_with_vision(frames, media_type="video")
- print(f"📝 Video analysis result: {video_description[:100]}...")
+ logger.info(f"Video analysis result: {video_description[:100]}...")
if video_description and video_description.strip():
embed_context_parts.append(f"[Embedded video shows: {video_description}]")
else:
- print(f"❌ Failed to extract frames from video")
+ logger.error(f"Failed to extract frames from video")
else:
- print(f"❌ Failed to download video from embed")
+ logger.error(f"Failed to download video from embed")
except Exception as e:
- print(f"⚠️ Error processing embedded video: {e}")
+ logger.error(f"Error processing embedded video: {e}")
import traceback
traceback.print_exc()
@@ -468,9 +476,9 @@ async def on_message(message):
)
if is_dm:
- print(f"💌 DM embed response to {message.author.display_name} (using DM mood: {globals.DM_MOOD})")
+ logger.info(f"💌 DM embed response to {message.author.display_name} (using DM mood: {globals.DM_MOOD})")
else:
- print(f"💬 Server embed response to {message.author.display_name} in {message.guild.name}")
+ logger.info(f"💬 Server embed response to {message.author.display_name} in {message.guild.name}")
response_message = await message.channel.send(response)
@@ -485,7 +493,7 @@ async def on_message(message):
current_persona = "evil" if globals.EVIL_MODE else "miku"
asyncio.create_task(check_for_interjection(response_message, current_persona))
except Exception as e:
- print(f"⚠️ Error checking for persona interjection: {e}")
+ logger.error(f"Error checking for persona interjection: {e}")
return
@@ -494,7 +502,7 @@ async def on_message(message):
is_image_request, image_prompt = await detect_image_request(prompt)
if is_image_request and image_prompt:
- print(f"🎨 Image generation request detected: '{image_prompt}' from {message.author.display_name}")
+ logger.info(f"🎨 Image generation request detected: '{image_prompt}' from {message.author.display_name}")
# Handle the image generation workflow
success = await handle_image_generation_request(message, image_prompt)
@@ -502,7 +510,7 @@ async def on_message(message):
return # Image generation completed successfully
# If image generation failed, fall back to normal response
- print(f"⚠️ Image generation failed, falling back to normal response")
+ logger.warning(f"Image generation failed, falling back to normal response")
# If message is just a prompt, no image
# For DMs, pass None as guild_id to use DM mood
@@ -518,9 +526,9 @@ async def on_message(message):
)
if is_dm:
- print(f"💌 DM response to {message.author.display_name} (using DM mood: {globals.DM_MOOD})")
+ logger.info(f"💌 DM response to {message.author.display_name} (using DM mood: {globals.DM_MOOD})")
else:
- print(f"💬 Server response to {message.author.display_name} in {message.guild.name} (using server mood)")
+ logger.info(f"💬 Server response to {message.author.display_name} in {message.guild.name} (using server mood)")
response_message = await message.channel.send(response)
@@ -530,15 +538,15 @@ async def on_message(message):
# For server messages, check if opposite persona should interject (persona dialogue system)
if not is_dm and globals.BIPOLAR_MODE:
- print(f"🔧 [DEBUG] Attempting to check for interjection (is_dm={is_dm}, BIPOLAR_MODE={globals.BIPOLAR_MODE})")
+ logger.debug(f"Attempting to check for interjection (is_dm={is_dm}, BIPOLAR_MODE={globals.BIPOLAR_MODE})")
try:
from utils.persona_dialogue import check_for_interjection
current_persona = "evil" if globals.EVIL_MODE else "miku"
- print(f"🔧 [DEBUG] Creating interjection check task for persona: {current_persona}")
+ logger.debug(f"Creating interjection check task for persona: {current_persona}")
# Pass the bot's response message for analysis
asyncio.create_task(check_for_interjection(response_message, current_persona))
except Exception as e:
- print(f"⚠️ Error checking for persona interjection: {e}")
+ logger.error(f"Error checking for persona interjection: {e}")
import traceback
traceback.print_exc()
@@ -557,11 +565,11 @@ async def on_message(message):
detected = detect_mood_shift(response, server_context)
if detected and detected != server_config.current_mood_name:
- print(f"🔄 Auto mood detection for server {message.guild.name}: {server_config.current_mood_name} -> {detected}")
+ logger.info(f"🔄 Auto mood detection for server {message.guild.name}: {server_config.current_mood_name} -> {detected}")
# Block direct transitions to asleep unless from sleepy
if detected == "asleep" and server_config.current_mood_name != "sleepy":
- print("❌ Ignoring asleep mood; server wasn't sleepy before.")
+ logger.warning("Ignoring asleep mood; server wasn't sleepy before.")
else:
# Update server mood
server_manager.set_server_mood(message.guild.id, detected)
@@ -570,7 +578,7 @@ async def on_message(message):
from utils.moods import update_server_nickname
globals.client.loop.create_task(update_server_nickname(message.guild.id))
- print(f"🔄 Server mood auto-updated to: {detected}")
+ logger.info(f"🔄 Server mood auto-updated to: {detected}")
if detected == "asleep":
server_manager.set_server_sleep_state(message.guild.id, True)
@@ -580,15 +588,15 @@ async def on_message(message):
server_manager.set_server_sleep_state(message.guild.id, False)
server_manager.set_server_mood(message.guild.id, "neutral")
await update_server_nickname(message.guild.id)
- print(f"🌅 Server {message.guild.name} woke up from auto-sleep")
+ logger.info(f"🌅 Server {message.guild.name} woke up from auto-sleep")
globals.client.loop.create_task(delayed_wakeup())
else:
- print(f"⚠️ No server config found for guild {message.guild.id}, skipping mood detection")
+ logger.error(f"No server config found for guild {message.guild.id}, skipping mood detection")
except Exception as e:
- print(f"⚠️ Error in server mood detection: {e}")
+ logger.error(f"Error in server mood detection: {e}")
elif is_dm:
- print("💌 DM message - no mood detection (DM mood only changes via auto-rotation)")
+ logger.debug("DM message - no mood detection (DM mood only changes via auto-rotation)")
# V2: Track message for autonomous engine (non-blocking, no LLM calls)
# IMPORTANT: Only call this if the message was NOT addressed to Miku
@@ -645,7 +653,7 @@ async def on_raw_reaction_add(payload):
)
reactor_type = "🤖 Miku" if is_bot_reactor else f"👤 {user.display_name}"
- print(f"➕ DM reaction added: {emoji_str} by {reactor_type} on message {payload.message_id}")
+ logger.debug(f"DM reaction added: {emoji_str} by {reactor_type} on message {payload.message_id}")
@globals.client.event
async def on_raw_reaction_remove(payload):
@@ -683,7 +691,7 @@ async def on_raw_reaction_remove(payload):
)
reactor_type = "🤖 Miku" if user.id == globals.client.user.id else f"👤 {user.display_name}"
- print(f"➖ DM reaction removed: {emoji_str} by {reactor_type} from message {payload.message_id}")
+ logger.debug(f"DM reaction removed: {emoji_str} by {reactor_type} from message {payload.message_id}")
@globals.client.event
async def on_presence_update(before, after):
@@ -698,16 +706,18 @@ async def on_member_join(member):
autonomous_member_join(member)
def start_api():
- uvicorn.run(app, host="0.0.0.0", port=3939, log_level="info")
+ # Set log_level to "critical" to silence uvicorn's access logs
+ # Our custom api.requests middleware handles HTTP logging with better formatting and filtering
+ uvicorn.run(app, host="0.0.0.0", port=3939, log_level="critical")
def save_autonomous_state():
"""Save autonomous context on shutdown"""
try:
from utils.autonomous import autonomous_engine
autonomous_engine.save_context()
- print("💾 Saved autonomous context on shutdown")
+ logger.info("💾 Saved autonomous context on shutdown")
except Exception as e:
- print(f"⚠️ Failed to save autonomous context on shutdown: {e}")
+ logger.error(f"Failed to save autonomous context on shutdown: {e}")
# Register shutdown handlers
atexit.register(save_autonomous_state)
diff --git a/bot/commands/actions.py b/bot/commands/actions.py
index be0e8ea..4c01b9d 100644
--- a/bot/commands/actions.py
+++ b/bot/commands/actions.py
@@ -4,17 +4,20 @@ import asyncio
import globals
from utils.moods import load_mood_description
from utils.scheduled import send_bedtime_reminder
+from utils.logger import get_logger
+
+logger = get_logger('commands')
def set_mood(new_mood: str) -> bool:
"""Set mood (legacy function - now handled per-server or DM)"""
- print("⚠️ set_mood called - this function is deprecated. Use server-specific mood endpoints instead.")
+ logger.warning("set_mood called - this function is deprecated. Use server-specific mood endpoints instead.")
return False
def reset_mood() -> str:
"""Reset mood to neutral (legacy function - now handled per-server or DM)"""
- print("⚠️ reset_mood called - this function is deprecated. Use server-specific mood endpoints instead.")
+ logger.warning("reset_mood called - this function is deprecated. Use server-specific mood endpoints instead.")
return "neutral"
@@ -24,7 +27,7 @@ def check_mood():
def calm_miku() -> str:
"""Calm Miku down (legacy function - now handled per-server or DM)"""
- print("⚠️ calm_miku called - this function is deprecated. Use server-specific mood endpoints instead.")
+ logger.warning("calm_miku called - this function is deprecated. Use server-specific mood endpoints instead.")
return "neutral"
@@ -34,14 +37,14 @@ def reset_conversation(user_id):
async def force_sleep() -> str:
"""Force Miku to sleep (legacy function - now handled per-server or DM)"""
- print("⚠️ force_sleep called - this function is deprecated. Use server-specific mood endpoints instead.")
+ logger.warning("force_sleep called - this function is deprecated. Use server-specific mood endpoints instead.")
return "asleep"
async def wake_up(set_sleep_state=None):
reset_mood()
# Note: DMs don't have sleep states, so this is deprecated
- print("⚠️ wake_up called - this function is deprecated. Use server-specific mood endpoints instead.")
+ logger.warning("wake_up called - this function is deprecated. Use server-specific mood endpoints instead.")
if set_sleep_state:
await set_sleep_state(False)
@@ -59,5 +62,5 @@ async def update_profile_picture(mood: str = "neutral"):
success = await update_profile_picture(globals.client, mood=mood)
return success
except Exception as e:
- print(f"⚠️ Error updating profile picture: {e}")
+ logger.error(f"Error updating profile picture: {e}")
return False
diff --git a/bot/server_manager.py b/bot/server_manager.py
index 3b94058..eb9c479 100644
--- a/bot/server_manager.py
+++ b/bot/server_manager.py
@@ -13,6 +13,9 @@ from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.date import DateTrigger
import random
from datetime import datetime, timedelta
+from utils.logger import get_logger
+
+logger = get_logger('server')
@dataclass
class ServerConfig:
@@ -58,7 +61,7 @@ class ServerConfig:
features_list = [f.strip().strip("'\"") for f in features_str.split(',') if f.strip()]
data['enabled_features'] = set(features_list)
except Exception as e:
- print(f"⚠️ Failed to parse enabled_features string '{data['enabled_features']}': {e}")
+ logger.warning(f"Failed to parse enabled_features string '{data['enabled_features']}': {e}")
# Fallback to default features
data['enabled_features'] = {"autonomous", "bedtime", "monday_video"}
return cls(**data)
@@ -83,12 +86,12 @@ class ServerManager:
guild_id = int(guild_id_str)
self.servers[guild_id] = ServerConfig.from_dict(server_data)
self.server_memories[guild_id] = {}
- print(f"📋 Loaded config for server: {server_data['guild_name']} (ID: {guild_id})")
+ logger.info(f"Loaded config for server: {server_data['guild_name']} (ID: {guild_id})")
# After loading, check if we need to repair the config
self.repair_config()
except Exception as e:
- print(f"⚠️ Failed to load server config: {e}")
+ logger.error(f"Failed to load server config: {e}")
self._create_default_config()
else:
self._create_default_config()
@@ -101,21 +104,21 @@ class ServerManager:
# Check if enabled_features is a string (corrupted)
if isinstance(server.enabled_features, str):
needs_repair = True
- print(f"🔧 Repairing corrupted enabled_features for server: {server.guild_name}")
+ logger.info(f"Repairing corrupted enabled_features for server: {server.guild_name}")
# Re-parse the features
try:
features_str = server.enabled_features.strip('{}')
features_list = [f.strip().strip("'\"") for f in features_str.split(',') if f.strip()]
server.enabled_features = set(features_list)
except Exception as e:
- print(f"⚠️ Failed to repair enabled_features for {server.guild_name}: {e}")
+ logger.warning(f"Failed to repair enabled_features for {server.guild_name}: {e}")
server.enabled_features = {"autonomous", "bedtime", "monday_video"}
if needs_repair:
- print("🔧 Saving repaired configuration...")
+ logger.info("Saving repaired configuration...")
self.save_config()
except Exception as e:
- print(f"⚠️ Failed to repair config: {e}")
+ logger.error(f"Failed to repair config: {e}")
def _create_default_config(self):
"""Create default configuration for backward compatibility"""
@@ -132,7 +135,7 @@ class ServerManager:
self.servers[default_server.guild_id] = default_server
self.server_memories[default_server.guild_id] = {}
self.save_config()
- print("📋 Created default server configuration")
+ logger.info("Created default server configuration")
def save_config(self):
"""Save server configurations to file"""
@@ -150,14 +153,14 @@ class ServerManager:
with open(self.config_file, "w", encoding="utf-8") as f:
json.dump(config_data, f, indent=2)
except Exception as e:
- print(f"⚠️ Failed to save server config: {e}")
+ logger.error(f"Failed to save server config: {e}")
def add_server(self, guild_id: int, guild_name: str, autonomous_channel_id: int,
autonomous_channel_name: str, bedtime_channel_ids: List[int] = None,
enabled_features: Set[str] = None) -> bool:
"""Add a new server configuration"""
if guild_id in self.servers:
- print(f"⚠️ Server {guild_id} already exists")
+ logger.info(f"Server {guild_id} already exists")
return False
if bedtime_channel_ids is None:
@@ -178,7 +181,7 @@ class ServerManager:
self.servers[guild_id] = server
self.server_memories[guild_id] = {}
self.save_config()
- print(f"✅ Added new server: {guild_name} (ID: {guild_id})")
+ logger.info(f"Added new server: {guild_name} (ID: {guild_id})")
return True
def remove_server(self, guild_id: int) -> bool:
@@ -199,7 +202,7 @@ class ServerManager:
del self.server_memories[guild_id]
self.save_config()
- print(f"🗑️ Removed server: {server_name} (ID: {guild_id})")
+ logger.info(f"Removed server: {server_name} (ID: {guild_id})")
return True
def get_server_config(self, guild_id: int) -> Optional[ServerConfig]:
@@ -221,7 +224,7 @@ class ServerManager:
setattr(server, key, value)
self.save_config()
- print(f"✅ Updated config for server: {server.guild_name}")
+ logger.info(f"Updated config for server: {server.guild_name}")
return True
def get_server_memory(self, guild_id: int, key: str = None):
@@ -267,12 +270,12 @@ class ServerManager:
from utils.moods import load_mood_description
server.current_mood_description = load_mood_description(mood_name)
except Exception as e:
- print(f"⚠️ Failed to load mood description for {mood_name}: {e}")
+ logger.error(f"Failed to load mood description for {mood_name}: {e}")
server.current_mood_description = f"I'm feeling {mood_name} today."
self.save_config()
- print(f"😊 Server {server.guild_name} mood changed to: {mood_name}")
- print(f"😊 Mood description: {server.current_mood_description[:100]}{'...' if len(server.current_mood_description) > 100 else ''}")
+ logger.info(f"Server {server.guild_name} mood changed to: {mood_name}")
+ logger.debug(f"Mood description: {server.current_mood_description[:100]}{'...' if len(server.current_mood_description) > 100 else ''}")
return True
def get_server_sleep_state(self, guild_id: int) -> bool:
@@ -323,7 +326,7 @@ class ServerManager:
def setup_server_scheduler(self, guild_id: int, client: discord.Client):
"""Setup independent scheduler for a specific server"""
if guild_id not in self.servers:
- print(f"⚠️ Cannot setup scheduler for unknown server: {guild_id}")
+ logger.warning(f"Cannot setup scheduler for unknown server: {guild_id}")
return
server_config = self.servers[guild_id]
@@ -363,8 +366,8 @@ class ServerManager:
# Add bedtime reminder job
if "bedtime" in server_config.enabled_features:
- print(f"⏰ Setting up bedtime scheduler for server {server_config.guild_name}")
- print(f" Random time range: {server_config.bedtime_hour:02d}:{server_config.bedtime_minute:02d} - {server_config.bedtime_hour_end:02d}:{server_config.bedtime_minute_end:02d}")
+ logger.info(f"Setting up bedtime scheduler for server {server_config.guild_name}")
+ logger.debug(f" Random time range: {server_config.bedtime_hour:02d}:{server_config.bedtime_minute:02d} - {server_config.bedtime_hour_end:02d}:{server_config.bedtime_minute_end:02d}")
scheduler.add_job(
self._schedule_random_bedtime_for_server,
CronTrigger(hour=server_config.bedtime_hour, minute=server_config.bedtime_minute),
@@ -382,11 +385,11 @@ class ServerManager:
self.schedulers[guild_id] = scheduler
scheduler.start()
- print(f"⏰ Started scheduler for server: {server_config.guild_name}")
+ logger.info(f"Started scheduler for server: {server_config.guild_name}")
def start_all_schedulers(self, client: discord.Client):
"""Start schedulers for all servers"""
- print("🚀 Starting all server schedulers...")
+ logger.info("Starting all server schedulers...")
for guild_id in self.servers:
self.setup_server_scheduler(guild_id, client)
@@ -396,42 +399,42 @@ class ServerManager:
# Start Figurine DM scheduler
self.setup_figurine_updates_scheduler(client)
- print(f"✅ Started {len(self.servers)} server schedulers + DM mood scheduler")
+ logger.info(f"Started {len(self.servers)} server schedulers + DM mood scheduler")
def update_server_bedtime_job(self, guild_id: int, client: discord.Client):
"""Update just the bedtime job for a specific server without restarting all schedulers"""
server_config = self.servers.get(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 False
scheduler = self.schedulers.get(guild_id)
if not scheduler:
- print(f"⚠️ No scheduler found for guild {guild_id}")
+ logger.warning(f"No scheduler found for guild {guild_id}")
return False
# Remove existing bedtime job if it exists
bedtime_job_id = f"bedtime_schedule_{guild_id}"
try:
scheduler.remove_job(bedtime_job_id)
- print(f"🗑️ Removed old bedtime job for server {guild_id}")
+ logger.info(f"Removed old bedtime job for server {guild_id}")
except Exception as e:
- print(f"ℹ️ No existing bedtime job to remove for server {guild_id}: {e}")
+ logger.debug(f"No existing bedtime job to remove for server {guild_id}: {e}")
# Add new bedtime job with updated configuration
if "bedtime" in server_config.enabled_features:
- print(f"⏰ Updating bedtime scheduler for server {server_config.guild_name}")
- print(f" New random time range: {server_config.bedtime_hour:02d}:{server_config.bedtime_minute:02d} - {server_config.bedtime_hour_end:02d}:{server_config.bedtime_minute_end:02d}")
+ logger.info(f"Updating bedtime scheduler for server {server_config.guild_name}")
+ logger.debug(f" New random time range: {server_config.bedtime_hour:02d}:{server_config.bedtime_minute:02d} - {server_config.bedtime_hour_end:02d}:{server_config.bedtime_minute_end:02d}")
scheduler.add_job(
self._schedule_random_bedtime_for_server,
CronTrigger(hour=server_config.bedtime_hour, minute=server_config.bedtime_minute),
args=[guild_id, client],
id=bedtime_job_id
)
- print(f"✅ Updated bedtime job for server {server_config.guild_name}")
+ logger.info(f"Updated bedtime job for server {server_config.guild_name}")
return True
else:
- print(f"ℹ️ Bedtime feature not enabled for server {guild_id}")
+ logger.info(f"Bedtime feature not enabled for server {guild_id}")
return True
def setup_dm_mood_scheduler(self, client: discord.Client):
@@ -449,10 +452,10 @@ class ServerManager:
scheduler.start()
self.schedulers["dm_mood"] = scheduler
- print("🔄 DM mood rotation scheduler started (every 2 hours)")
+ logger.info("DM mood rotation scheduler started (every 2 hours)")
except Exception as e:
- print(f"❌ Failed to setup DM mood scheduler: {e}")
+ logger.error(f"Failed to setup DM mood scheduler: {e}")
def _enqueue_figurine_send(self, client: discord.Client):
"""Enqueue the figurine DM send task in the client's loop."""
@@ -460,11 +463,11 @@ class ServerManager:
from utils.figurine_notifier import send_figurine_dm_to_all_subscribers
if client.loop and client.loop.is_running():
client.loop.create_task(send_figurine_dm_to_all_subscribers(client))
- print("✅ Figurine DM send task queued")
+ logger.debug("Figurine DM send task queued")
else:
- print("⚠️ Client loop not available for figurine DM send")
+ logger.warning("Client loop not available for figurine DM send")
except Exception as e:
- print(f"⚠️ Error enqueuing figurine DM: {e}")
+ logger.error(f"Error enqueuing figurine DM: {e}")
def _schedule_one_figurine_send_today(self, scheduler: AsyncIOScheduler, client: discord.Client):
"""Schedule one figurine DM send at a random non-evening time today (or tomorrow if time passed)."""
@@ -475,7 +478,7 @@ class ServerManager:
target_time = now.replace(hour=random_hour, minute=random_minute, second=0, microsecond=0)
if target_time <= now:
target_time = target_time + timedelta(days=1)
- print(f"🗓️ Scheduling figurine DM at {target_time.strftime('%Y-%m-%d %H:%M')} (random non-evening)")
+ logger.info(f"Scheduling figurine DM at {target_time.strftime('%Y-%m-%d %H:%M')} (random non-evening)")
scheduler.add_job(
self._enqueue_figurine_send,
DateTrigger(run_date=target_time),
@@ -499,22 +502,22 @@ class ServerManager:
self._schedule_one_figurine_send_today(scheduler, client)
scheduler.start()
self.schedulers["figurine_dm"] = scheduler
- print("🗓️ Figurine updates scheduler started")
+ logger.info("Figurine updates scheduler started")
except Exception as e:
- print(f"❌ Failed to setup figurine updates scheduler: {e}")
+ logger.error(f"Failed to setup figurine updates scheduler: {e}")
def stop_all_schedulers(self):
"""Stop all schedulers"""
- print("🛑 Stopping all schedulers...")
+ logger.info("Stopping all schedulers...")
for scheduler in self.schedulers.values():
try:
scheduler.shutdown()
except Exception as e:
- print(f"⚠️ Error stopping scheduler: {e}")
+ logger.warning(f"Error stopping scheduler: {e}")
self.schedulers.clear()
- print("✅ All schedulers stopped")
+ logger.info("All schedulers stopped")
# Implementation of autonomous functions - these integrate with the autonomous system
def _run_autonomous_for_server(self, guild_id: int, client: discord.Client):
@@ -525,11 +528,11 @@ class ServerManager:
# Create an async task in the client's event loop
if client.loop and client.loop.is_running():
client.loop.create_task(autonomous_tick(guild_id))
- print(f"✅ [V2] Autonomous tick queued for server {guild_id}")
+ logger.debug(f"[V2] Autonomous tick queued for server {guild_id}")
else:
- print(f"⚠️ Client loop not available for autonomous tick in server {guild_id}")
+ logger.warning(f"Client loop not available for autonomous tick in server {guild_id}")
except Exception as e:
- print(f"⚠️ Error in autonomous tick for server {guild_id}: {e}")
+ logger.error(f"Error in autonomous tick for server {guild_id}: {e}")
def _run_autonomous_reaction_for_server(self, guild_id: int, client: discord.Client):
"""Run autonomous reaction for a specific server - called by APScheduler"""
@@ -539,11 +542,11 @@ class ServerManager:
# Create an async task in the client's event loop
if client.loop and client.loop.is_running():
client.loop.create_task(autonomous_reaction_tick(guild_id))
- print(f"✅ [V2] Autonomous reaction queued for server {guild_id}")
+ logger.debug(f"[V2] Autonomous reaction queued for server {guild_id}")
else:
- print(f"⚠️ Client loop not available for autonomous reaction in server {guild_id}")
+ logger.warning(f"Client loop not available for autonomous reaction in server {guild_id}")
except Exception as e:
- print(f"⚠️ Error in autonomous reaction for server {guild_id}: {e}")
+ logger.error(f"Error in autonomous reaction for server {guild_id}: {e}")
def _run_conversation_detection_for_server(self, guild_id: int, client: discord.Client):
"""Run conversation detection for a specific server - called by APScheduler"""
@@ -552,11 +555,11 @@ class ServerManager:
# Create an async task in the client's event loop
if client.loop and client.loop.is_running():
client.loop.create_task(miku_detect_and_join_conversation_for_server(guild_id))
- print(f"✅ Conversation detection queued for server {guild_id}")
+ logger.debug(f"Conversation detection queued for server {guild_id}")
else:
- print(f"⚠️ Client loop not available for conversation detection in server {guild_id}")
+ logger.warning(f"Client loop not available for conversation detection in server {guild_id}")
except Exception as e:
- print(f"⚠️ Error in conversation detection for server {guild_id}: {e}")
+ logger.error(f"Error in conversation detection for server {guild_id}: {e}")
def _send_monday_video_for_server(self, guild_id: int, client: discord.Client):
"""Send Monday video for a specific server - called by APScheduler"""
@@ -565,35 +568,35 @@ class ServerManager:
# Create an async task in the client's event loop
if client.loop and client.loop.is_running():
client.loop.create_task(send_monday_video_for_server(guild_id))
- print(f"✅ Monday video queued for server {guild_id}")
+ logger.debug(f"Monday video queued for server {guild_id}")
else:
- print(f"⚠️ Client loop not available for Monday video in server {guild_id}")
+ logger.warning(f"Client loop not available for Monday video in server {guild_id}")
except Exception as e:
- print(f"⚠️ Error in Monday video for server {guild_id}: {e}")
+ logger.error(f"Error in Monday video for server {guild_id}: {e}")
def _schedule_random_bedtime_for_server(self, guild_id: int, client: discord.Client):
"""Schedule bedtime reminder for a specific server at a random time within the configured range"""
- print(f"⏰ Bedtime scheduler triggered for server {guild_id} at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
+ logger.info(f"Bedtime scheduler triggered for server {guild_id} at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
# Get server config to determine the random time range
server_config = self.servers.get(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
# Calculate random time within the bedtime range
start_minutes = server_config.bedtime_hour * 60 + server_config.bedtime_minute
end_minutes = server_config.bedtime_hour_end * 60 + server_config.bedtime_minute_end
- print(f"🕐 Bedtime range calculation: {server_config.bedtime_hour:02d}:{server_config.bedtime_minute:02d} ({start_minutes} min) to {server_config.bedtime_hour_end:02d}:{server_config.bedtime_minute_end:02d} ({end_minutes} min)")
+ logger.debug(f"Bedtime range calculation: {server_config.bedtime_hour:02d}:{server_config.bedtime_minute:02d} ({start_minutes} min) to {server_config.bedtime_hour_end:02d}:{server_config.bedtime_minute_end:02d} ({end_minutes} min)")
# Handle case where end time is next day (e.g., 23:30 to 00:30)
if end_minutes <= start_minutes:
end_minutes += 24 * 60 # Add 24 hours
- print(f"🌙 Cross-midnight range detected, adjusted end to {end_minutes} minutes")
+ logger.debug(f"Cross-midnight range detected, adjusted end to {end_minutes} minutes")
random_minutes = random.randint(start_minutes, end_minutes)
- print(f"🎲 Random time selected: {random_minutes} minutes from midnight")
+ logger.debug(f"Random time selected: {random_minutes} minutes from midnight")
# Convert back to hours and minutes
random_hour = (random_minutes // 60) % 24
@@ -609,7 +612,7 @@ class ServerManager:
delay_seconds = (target_time - now).total_seconds()
- print(f"🎲 Random bedtime for server {server_config.guild_name}: {random_hour:02d}:{random_minute:02d} (in {delay_seconds/60:.1f} minutes)")
+ logger.info(f"Random bedtime for server {server_config.guild_name}: {random_hour:02d}:{random_minute:02d} (in {delay_seconds/60:.1f} minutes)")
# Schedule the actual bedtime reminder
try:
@@ -618,9 +621,9 @@ class ServerManager:
def send_bedtime_delayed():
if client.loop and client.loop.is_running():
client.loop.create_task(send_bedtime_reminder_for_server(guild_id, client))
- print(f"✅ Random bedtime reminder sent for server {guild_id}")
+ logger.info(f"Random bedtime reminder sent for server {guild_id}")
else:
- print(f"⚠️ Client loop not available for bedtime reminder in server {guild_id}")
+ logger.warning(f"Client loop not available for bedtime reminder in server {guild_id}")
# Use the scheduler to schedule the delayed bedtime reminder
scheduler = self.schedulers.get(guild_id)
@@ -630,12 +633,12 @@ class ServerManager:
DateTrigger(run_date=target_time),
id=f"bedtime_reminder_{guild_id}_{int(target_time.timestamp())}"
)
- print(f"✅ Bedtime reminder scheduled for server {guild_id} at {target_time.strftime('%Y-%m-%d %H:%M:%S')}")
+ logger.info(f"Bedtime reminder scheduled for server {guild_id} at {target_time.strftime('%Y-%m-%d %H:%M:%S')}")
else:
- print(f"⚠️ No scheduler found for server {guild_id}")
+ logger.warning(f"No scheduler found for server {guild_id}")
except Exception as e:
- print(f"⚠️ Error scheduling bedtime reminder for server {guild_id}: {e}")
+ logger.error(f"Error scheduling bedtime reminder for server {guild_id}: {e}")
def _rotate_server_mood(self, guild_id: int, client: discord.Client):
"""Rotate mood for a specific server - called by APScheduler"""
@@ -644,11 +647,11 @@ class ServerManager:
# Create an async task in the client's event loop
if client.loop and client.loop.is_running():
client.loop.create_task(rotate_server_mood(guild_id))
- print(f"✅ Mood rotation queued for server {guild_id}")
+ logger.debug(f"Mood rotation queued for server {guild_id}")
else:
- print(f"⚠️ Client loop not available for mood rotation in server {guild_id}")
+ logger.warning(f"Client loop not available for mood rotation in server {guild_id}")
except Exception as e:
- print(f"⚠️ Error in mood rotation for server {guild_id}: {e}")
+ logger.error(f"Error in mood rotation for server {guild_id}: {e}")
# Global instance
server_manager = ServerManager()
diff --git a/bot/static/index.html b/bot/static/index.html
index 62d355d..b1d0bce 100644
--- a/bot/static/index.html
+++ b/bot/static/index.html
@@ -658,12 +658,13 @@
diff --git a/bot/static/system-logic.js b/bot/static/system-logic.js
new file mode 100644
index 0000000..b3ed5a7
--- /dev/null
+++ b/bot/static/system-logic.js
@@ -0,0 +1,415 @@
+let currentConfig = null;
+let componentsData = null;
+
+// Load configuration on page load
+window.addEventListener('DOMContentLoaded', () => {
+ loadConfiguration();
+ loadComponents();
+});
+
+async function loadConfiguration() {
+ try {
+ const response = await fetch('/api/log/config');
+ const data = await response.json();
+
+ if (data.success) {
+ currentConfig = data.config;
+
+ // Load timestamp format setting
+ const timestampFormat = data.config.formatting?.timestamp_format || 'datetime';
+ const timestampSelect = document.getElementById('timestampFormat');
+ if (timestampSelect) {
+ timestampSelect.value = timestampFormat;
+ }
+ } else {
+ showNotification('Failed to load configuration', 'error');
+ }
+ } catch (error) {
+ showNotification('Error loading configuration: ' + error.message, 'error');
+ }
+}
+
+async function loadComponents() {
+ try {
+ const response = await fetch('/api/log/components');
+ const data = await response.json();
+
+ if (data.success) {
+ componentsData = data;
+ renderComponentsTable();
+ populatePreviewSelect();
+ } else {
+ showNotification('Failed to load components', 'error');
+ }
+ } catch (error) {
+ showNotification('Error loading components: ' + error.message, 'error');
+ }
+}
+
+function renderComponentsTable() {
+ const tbody = document.getElementById('componentsTable');
+ tbody.innerHTML = '';
+
+ for (const [name, description] of Object.entries(componentsData.components)) {
+ const stats = componentsData.stats[name] || {};
+ const enabled = stats.enabled !== undefined ? stats.enabled : true;
+ const enabledLevels = stats.enabled_levels || ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'];
+
+ const allLevels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'];
+ if (name === 'api.requests') {
+ allLevels.push('API');
+ }
+
+ const levelCheckboxes = allLevels.map(level => {
+ const emoji = {'DEBUG': '🔍', 'INFO': 'ℹ️', 'WARNING': '⚠️', 'ERROR': '❌', 'CRITICAL': '🔥', 'API': '🌐'}[level];
+ const checked = enabledLevels.includes(level) ? 'checked' : '';
+ return `
+
+
+
+
+ `;
+ }).join('');
+
+ const row = document.createElement('tr');
+ row.innerHTML = `
+
+ ${name}
+ ${description}
+ |
+
+
+ |
+
+
+ ${levelCheckboxes}
+
+ |
+
+
+ ${enabled ? 'Active' : 'Inactive'}
+ |
+ `;
+ tbody.appendChild(row);
+
+ if (name === 'api.requests') {
+ document.getElementById('enabled_' + name).addEventListener('change', (e) => {
+ document.getElementById('apiFilters').style.display = e.target.checked ? 'block' : 'none';
+ });
+
+ if (enabled) {
+ document.getElementById('apiFilters').style.display = 'block';
+ loadApiFilters();
+ }
+ }
+ }
+
+ // Update global level checkboxes based on current state
+ updateGlobalLevelCheckboxes();
+}
+
+function updateGlobalLevelCheckboxes() {
+ const allLevels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL', 'API'];
+
+ for (const level of allLevels) {
+ let allComponentsHaveLevel = true;
+
+ // Check if ALL components have this level enabled
+ for (const [name, description] of Object.entries(componentsData.components)) {
+ const stats = componentsData.stats[name] || {};
+ const enabledLevels = stats.enabled_levels || [];
+
+ // Skip API level for non-api.requests components
+ if (level === 'API' && name !== 'api.requests') {
+ continue;
+ }
+
+ if (!enabledLevels.includes(level)) {
+ allComponentsHaveLevel = false;
+ break;
+ }
+ }
+
+ const checkbox = document.getElementById('global_' + level);
+ if (checkbox) {
+ checkbox.checked = allComponentsHaveLevel;
+ }
+ }
+}
+
+function populatePreviewSelect() {
+ const select = document.getElementById('previewComponent');
+ select.innerHTML = '';
+
+ for (const name of Object.keys(componentsData.components)) {
+ const option = document.createElement('option');
+ option.value = name;
+ option.textContent = name;
+ select.appendChild(option);
+ }
+
+ loadLogPreview();
+}
+
+async function updateComponentEnabled(component) {
+ const enabled = document.getElementById('enabled_' + component).checked;
+
+ try {
+ const response = await fetch('/api/log/config', {
+ method: 'POST',
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({
+ component: component,
+ enabled: enabled
+ })
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ showNotification(`${enabled ? 'Enabled' : 'Disabled'} ${component}`, 'success');
+
+ const row = document.getElementById('enabled_' + component).closest('tr');
+ const statusCell = row.querySelector('td:last-child');
+ statusCell.innerHTML = `
+
+ ${enabled ? 'Active' : 'Inactive'}
+ `;
+ } else {
+ showNotification('Failed to update ' + component + ': ' + data.error, 'error');
+ }
+ } catch (error) {
+ showNotification('Error updating component: ' + error.message, 'error');
+ }
+}
+
+async function updateGlobalLevel(level, enabled) {
+ try {
+ const response = await fetch(`/api/log/global-level?level=${level}&enabled=${enabled}`, {
+ method: 'POST',
+ headers: {'Content-Type': 'application/json'}
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ const action = enabled ? 'enabled' : 'disabled';
+ showNotification(`${level} ${action} globally across all components`, 'success');
+
+ // Reload components to reflect changes
+ await loadComponents();
+ } else {
+ showNotification('Failed to update global level: ' + data.error, 'error');
+ }
+ } catch (error) {
+ showNotification('Error updating global level: ' + error.message, 'error');
+ }
+}
+
+async function updateTimestampFormat(format) {
+ try {
+ const response = await fetch(`/api/log/timestamp-format?format_type=${format}`, {
+ method: 'POST',
+ headers: {'Content-Type': 'application/json'}
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ showNotification(`Timestamp format updated: ${format}`, 'success');
+
+ // Reload all loggers to apply the change
+ await fetch('/api/log/reload', { method: 'POST' });
+ } else {
+ showNotification('Failed to update timestamp format: ' + data.error, 'error');
+ }
+ } catch (error) {
+ showNotification('Error updating timestamp format: ' + error.message, 'error');
+ }
+}
+
+async function updateComponentLevels(component) {
+ const allLevels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'];
+ if (component === 'api.requests') {
+ allLevels.push('API');
+ }
+
+ const enabledLevels = allLevels.filter(level => {
+ const checkbox = document.getElementById(`level_${component}_${level}`);
+ return checkbox && checkbox.checked;
+ });
+
+ try {
+ const response = await fetch('/api/log/config', {
+ method: 'POST',
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({
+ component: component,
+ enabled_levels: enabledLevels
+ })
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ showNotification(`Updated levels for ${component}: ${enabledLevels.join(', ')}`, 'success');
+
+ // Update global level checkboxes to reflect current state
+ updateGlobalLevelCheckboxes();
+ } else {
+ showNotification('Failed to update ' + component + ': ' + data.error, 'error');
+ }
+ } catch (error) {
+ showNotification('Error updating component: ' + error.message, 'error');
+ }
+}
+
+async function loadApiFilters() {
+ if (!currentConfig || !currentConfig.components['api.requests']) return;
+
+ const filters = currentConfig.components['api.requests'].filters || {};
+ document.getElementById('excludePaths').value = (filters.exclude_paths || []).join(', ');
+ document.getElementById('excludeStatus').value = (filters.exclude_status || []).join(', ');
+ document.getElementById('includeSlowRequests').checked = filters.include_slow_requests !== false;
+ document.getElementById('slowThreshold').value = filters.slow_threshold_ms || 1000;
+}
+
+async function saveApiFilters() {
+ const excludePaths = document.getElementById('excludePaths').value
+ .split(',')
+ .map(s => s.trim())
+ .filter(s => s.length > 0);
+
+ const excludeStatus = document.getElementById('excludeStatus').value
+ .split(',')
+ .map(s => parseInt(s.trim()))
+ .filter(n => !isNaN(n));
+
+ const includeSlowRequests = document.getElementById('includeSlowRequests').checked;
+ const slowThreshold = parseInt(document.getElementById('slowThreshold').value);
+
+ try {
+ const response = await fetch('/api/log/filters', {
+ method: 'POST',
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({
+ exclude_paths: excludePaths,
+ exclude_status: excludeStatus,
+ include_slow_requests: includeSlowRequests,
+ slow_threshold_ms: slowThreshold
+ })
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ showNotification('API filters saved', 'success');
+ } else {
+ showNotification('Failed to save filters: ' + data.error, 'error');
+ }
+ } catch (error) {
+ showNotification('Error saving filters: ' + error.message, 'error');
+ }
+}
+
+async function saveAllSettings() {
+ try {
+ const response = await fetch('/api/log/reload', {
+ method: 'POST'
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ showNotification('All settings saved and reloaded', 'success');
+ await loadConfiguration();
+ await loadComponents();
+ } else {
+ showNotification('Failed to reload settings: ' + data.error, 'error');
+ }
+ } catch (error) {
+ showNotification('Error saving settings: ' + error.message, 'error');
+ }
+}
+
+async function resetToDefaults() {
+ if (!confirm('Are you sure you want to reset all logging settings to defaults?')) {
+ return;
+ }
+
+ try {
+ const response = await fetch('/api/log/reset', {
+ method: 'POST'
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ showNotification('Settings reset to defaults', 'success');
+ await loadConfiguration();
+ await loadComponents();
+ } else {
+ showNotification('Failed to reset settings: ' + data.error, 'error');
+ }
+ } catch (error) {
+ showNotification('Error resetting settings: ' + error.message, 'error');
+ }
+}
+
+async function loadLogPreview() {
+ const component = document.getElementById('previewComponent').value;
+ const preview = document.getElementById('logPreview');
+
+ preview.innerHTML = '
Loading logs...
';
+
+ try {
+ const response = await fetch(`/api/log/files/${component}?lines=50`);
+ const data = await response.json();
+
+ if (data.success) {
+ if (data.lines.length === 0) {
+ preview.innerHTML = '
No logs yet for this component
';
+ } else {
+ preview.innerHTML = data.lines.map(line =>
+ `
${escapeHtml(line)}
`
+ ).join('');
+
+ preview.scrollTop = preview.scrollHeight;
+ }
+ } else {
+ preview.innerHTML = `
Error: ${data.error}
`;
+ }
+ } catch (error) {
+ preview.innerHTML = `
Error loading logs: ${error.message}
`;
+ }
+}
+
+function escapeHtml(text) {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+}
+
+function showNotification(message, type) {
+ const notification = document.createElement('div');
+ notification.className = `notification notification-${type}`;
+ notification.textContent = message;
+ document.body.appendChild(notification);
+
+ setTimeout(() => {
+ notification.remove();
+ }, 3000);
+}
+
+// Auto-refresh log preview every 5 seconds
+setInterval(() => {
+ if (document.getElementById('previewComponent').value) {
+ loadLogPreview();
+ }
+}, 5000);
diff --git a/bot/static/system.html b/bot/static/system.html
new file mode 100644
index 0000000..2256c2e
--- /dev/null
+++ b/bot/static/system.html
@@ -0,0 +1,408 @@
+
+
+
+
+
+
🎛️ System Settings - Logging Configuration
+
+
+
+
+
+
+
+
📊 Logging Components
+
+ Enable or disable specific log levels for each component. You can toggle any combination of levels.
+
+
+
+
🌍 Global Level Controls
+
+ Quickly enable/disable a log level across all components
+
+
+
+
+
+
🕐 Timestamp Format
+
+ Control how timestamps appear in logs
+
+
+
+
+
+
+
+
+
+
+ | Component |
+ Enabled |
+ Log Levels |
+ Status |
+
+
+
+ | Loading components... |
+
+
+
+
+
+
📜 Live Log Preview
+
+
+
Select a component to view logs...
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/bot/utils/autonomous.py b/bot/utils/autonomous.py
index 4cca2bf..ff7f403 100644
--- a/bot/utils/autonomous.py
+++ b/bot/utils/autonomous.py
@@ -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 ==========
diff --git a/bot/utils/autonomous_engine.py b/bot/utils/autonomous_engine.py
index d346d22..c4789ba 100644
--- a/bot/utils/autonomous_engine.py
+++ b/bot/utils/autonomous_engine.py
@@ -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
diff --git a/bot/utils/autonomous_persistence.py b/bot/utils/autonomous_persistence.py
index eb35dc5..71ed736 100644
--- a/bot/utils/autonomous_persistence.py
+++ b/bot/utils/autonomous_persistence.py
@@ -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 {}, {}
diff --git a/bot/utils/autonomous_v1_legacy.py b/bot/utils/autonomous_v1_legacy.py
index d8f1ad7..1170964 100644
--- a/bot/utils/autonomous_v1_legacy.py
+++ b/bot/utils/autonomous_v1_legacy.py
@@ -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}")
diff --git a/bot/utils/bipolar_mode.py b/bot/utils/bipolar_mode.py
index 1b075eb..bc1108b 100644
--- a/bot/utils/bipolar_mode.py
+++ b/bot/utils/bipolar_mode.py
@@ -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))
diff --git a/bot/utils/context_manager.py b/bot/utils/context_manager.py
index 9be044b..a461d0b 100644
--- a/bot/utils/context_manager.py
+++ b/bot/utils/context_manager.py
@@ -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]"
diff --git a/bot/utils/core.py b/bot/utils/core.py
index 5af8a5c..647b8f0 100644
--- a/bot/utils/core.py
+++ b/bot/utils/core.py
@@ -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()
diff --git a/bot/utils/danbooru_client.py b/bot/utils/danbooru_client.py
index 0c8b271..480ff37 100644
--- a/bot/utils/danbooru_client.py
+++ b/bot/utils/danbooru_client.py
@@ -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
diff --git a/bot/utils/dm_interaction_analyzer.py b/bot/utils/dm_interaction_analyzer.py
index 15a4b88..5ee27ba 100644
--- a/bot/utils/dm_interaction_analyzer.py
+++ b/bot/utils/dm_interaction_analyzer.py
@@ -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)
diff --git a/bot/utils/dm_logger.py b/bot/utils/dm_logger.py
index b3d09ce..bfb5686 100644
--- a/bot/utils/dm_logger.py
+++ b/bot/utils/dm_logger.py
@@ -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
diff --git a/bot/utils/evil_mode.py b/bot/utils/evil_mode.py
index 3cd9a34..8fd6518 100644
--- a/bot/utils/evil_mode.py
+++ b/bot/utils/evil_mode.py
@@ -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}")
diff --git a/bot/utils/face_detector_manager.py b/bot/utils/face_detector_manager.py
index 4fe3c0e..e16f2b9 100644
--- a/bot/utils/face_detector_manager.py
+++ b/bot/utils/face_detector_manager.py
@@ -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
diff --git a/bot/utils/figurine_notifier.py b/bot/utils/figurine_notifier.py
index 68cba06..d1db068 100644
--- a/bot/utils/figurine_notifier.py
+++ b/bot/utils/figurine_notifier.py
@@ -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
diff --git a/bot/utils/image_generation.py b/bot/utils/image_generation.py
index 27e5e43..2c68390 100644
--- a/bot/utils/image_generation.py
+++ b/bot/utils/image_generation.py
@@ -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:
diff --git a/bot/utils/image_handling.py b/bot/utils/image_handling.py
index eb982b6..040cf0f 100644
--- a/bot/utils/image_handling.py
+++ b/bot/utils/image_handling.py
@@ -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)}"
diff --git a/bot/utils/kindness.py b/bot/utils/kindness.py
index 731e946..612f2f8 100644
--- a/bot/utils/kindness.py
+++ b/bot/utils/kindness.py
@@ -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}")
diff --git a/bot/utils/llm.py b/bot/utils/llm.py
index 6bc9a85..cf193be 100644
--- a/bot/utils/llm.py
+++ b/bot/utils/llm.py
@@ -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
diff --git a/bot/utils/log_config.py b/bot/utils/log_config.py
new file mode 100644
index 0000000..ee2189b
--- /dev/null
+++ b/bot/utils/log_config.py
@@ -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
diff --git a/bot/utils/logger.py b/bot/utils/logger.py
new file mode 100644
index 0000000..c56b3df
--- /dev/null
+++ b/bot/utils/logger.py
@@ -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()
diff --git a/bot/utils/media.py b/bot/utils/media.py
index 35b23bd..af5d3c6 100644
--- a/bot/utils/media.py
+++ b/bot/utils/media.py
@@ -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}")
diff --git a/bot/utils/moods.py b/bot/utils/moods.py
index 16541e6..a28cc8d 100644
--- a/bot/utils/moods.py
+++ b/bot/utils/moods.py
@@ -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
diff --git a/bot/utils/persona_dialogue.py b/bot/utils/persona_dialogue.py
index 23eff17..d802a88 100644
--- a/bot/utils/persona_dialogue.py
+++ b/bot/utils/persona_dialogue.py
@@ -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)
diff --git a/bot/utils/profile_picture_manager.py b/bot/utils/profile_picture_manager.py
index 891ad1a..01f32c7 100644
--- a/bot/utils/profile_picture_manager.py
+++ b/bot/utils/profile_picture_manager.py
@@ -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
diff --git a/bot/utils/scheduled.py b/bot/utils/scheduled.py
index 30f9043..0f9525d 100644
--- a/bot/utils/scheduled.py
+++ b/bot/utils/scheduled.py
@@ -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()
diff --git a/bot/utils/sentiment_analysis.py b/bot/utils/sentiment_analysis.py
index b58e42e..dba3948 100644
--- a/bot/utils/sentiment_analysis.py
+++ b/bot/utils/sentiment_analysis.py
@@ -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
\ No newline at end of file
diff --git a/bot/utils/twitter_fetcher.py b/bot/utils/twitter_fetcher.py
index 79235b7..00c635e 100644
--- a/bot/utils/twitter_fetcher.py
+++ b/bot/utils/twitter_fetcher.py
@@ -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
diff --git a/bot/utils/twscrape_fix.py b/bot/utils/twscrape_fix.py
index 5664c4e..53dd57a 100644
--- a/bot/utils/twscrape_fix.py
+++ b/bot/utils/twscrape_fix.py
@@ -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}")