diff --git a/bot/utils/bipolar_mode.py b/bot/utils/bipolar_mode.py index 5bfe5a6..a4ba5ce 100644 --- a/bot/utils/bipolar_mode.py +++ b/bot/utils/bipolar_mode.py @@ -358,6 +358,45 @@ async def cleanup_webhooks(client): return cleaned_count +async def update_webhook_avatars(client): + """Update all bipolar webhook avatars with current profile pictures""" + updated_count = 0 + + # Load current avatar images + miku_avatar = None + evil_avatar = None + + miku_pfp_path = "memory/profile_pictures/current.png" + evil_pfp_path = "memory/profile_pictures/evil_pfp.png" + + if os.path.exists(miku_pfp_path): + with open(miku_pfp_path, "rb") as f: + miku_avatar = f.read() + + if os.path.exists(evil_pfp_path): + with open(evil_pfp_path, "rb") as f: + evil_avatar = f.read() + + # Update webhooks in all servers + for guild in client.guilds: + try: + guild_webhooks = await guild.webhooks() + for webhook in guild_webhooks: + if webhook.name == "Miku (Bipolar)" and miku_avatar: + await webhook.edit(avatar=miku_avatar, reason="Update Miku avatar") + updated_count += 1 + logger.debug(f"Updated Miku webhook avatar in {guild.name}") + elif webhook.name == "Evil Miku (Bipolar)" and evil_avatar: + await webhook.edit(avatar=evil_avatar, reason="Update Evil Miku avatar") + updated_count += 1 + logger.debug(f"Updated Evil Miku webhook avatar in {guild.name}") + except Exception as e: + logger.warning(f"Failed to update webhooks in {guild.name}: {e}") + + logger.info(f"Updated {updated_count} bipolar webhook avatar(s)") + return updated_count + + # ============================================================================ # DISPLAY NAME HELPERS # ============================================================================ diff --git a/bot/utils/evil_mode.py b/bot/utils/evil_mode.py index 8fd6518..49c31f9 100644 --- a/bot/utils/evil_mode.py +++ b/bot/utils/evil_mode.py @@ -416,6 +416,11 @@ async def apply_evil_mode_changes(client, change_username=True, change_pfp=True, try: await client.user.edit(username="Evil Miku") logger.debug("Changed bot username to 'Evil Miku'") + except discord.HTTPException as e: + if e.code == 50035: + logger.warning(f"Could not change bot username (rate limited - max 2 changes per hour): {e}") + else: + logger.error(f"Could not change bot username: {e}") except Exception as e: logger.error(f"Could not change bot username: {e}") @@ -426,6 +431,15 @@ async def apply_evil_mode_changes(client, change_username=True, change_pfp=True, # Set evil profile picture if change_pfp: await set_evil_profile_picture(client) + + # Also update bipolar webhooks to use evil_pfp.png + if globals.BIPOLAR_MODE: + try: + from utils.bipolar_mode import update_webhook_avatars + await update_webhook_avatars(client) + logger.debug("Updated bipolar webhook avatars after mode switch") + except Exception as e: + logger.error(f"Failed to update bipolar webhook avatars: {e}") # Set evil role color (#D60004 - dark red) if change_role_color: @@ -455,6 +469,11 @@ async def revert_evil_mode_changes(client, change_username=True, change_pfp=True try: await client.user.edit(username="Hatsune Miku") logger.debug("Changed bot username back to 'Hatsune Miku'") + except discord.HTTPException as e: + if e.code == 50035: + logger.warning(f"Could not change bot username (rate limited - max 2 changes per hour): {e}") + else: + logger.error(f"Could not change bot username: {e}") except Exception as e: logger.error(f"Could not change bot username: {e}") @@ -465,16 +484,33 @@ async def revert_evil_mode_changes(client, change_username=True, change_pfp=True # Restore normal profile picture if change_pfp: await restore_normal_profile_picture(client) + + # Also update bipolar webhooks to use current.png + if globals.BIPOLAR_MODE: + try: + from utils.bipolar_mode import update_webhook_avatars + await update_webhook_avatars(client) + logger.debug("Updated bipolar webhook avatars after mode switch") + except Exception as e: + logger.error(f"Failed to update bipolar webhook avatars: {e}") # Restore saved role color if change_role_color: try: - _, _, saved_color = load_evil_mode_state() - if saved_color: - await set_role_color(client, saved_color) - logger.debug(f"Restored role color to {saved_color}") + # Try to get color from metadata.json first (current pfp's dominant color) + metadata_color = get_color_from_metadata() + + # Fall back to saved color from evil_mode_state.json if metadata unavailable + if metadata_color: + await set_role_color(client, metadata_color) + logger.debug(f"Restored role color from metadata: {metadata_color}") else: - logger.warning("No saved role color found, skipping color restoration") + _, _, saved_color = load_evil_mode_state() + if saved_color: + await set_role_color(client, saved_color) + logger.debug(f"Restored role color from saved state: {saved_color}") + else: + logger.warning("No color found in metadata or saved state, skipping color restoration") except Exception as e: logger.error(f"Failed to restore role color: {e}") @@ -566,6 +602,29 @@ async def restore_normal_profile_picture(client): return False +def get_color_from_metadata() -> str: + """Get the dominant color from the profile picture metadata""" + metadata_path = "memory/profile_pictures/metadata.json" + try: + if not os.path.exists(metadata_path): + logger.warning("metadata.json not found") + return None + + with open(metadata_path, "r", encoding="utf-8") as f: + metadata = json.load(f) + + hex_color = metadata.get("dominant_color", {}).get("hex") + if hex_color: + logger.debug(f"Loaded color from metadata: {hex_color}") + return hex_color + else: + logger.warning("No dominant_color.hex found in metadata") + return None + except Exception as e: + logger.error(f"Failed to load color from metadata: {e}") + return None + + # ============================================================================ # EVIL MODE STATE HELPERS # ============================================================================ diff --git a/bot/utils/llm.py b/bot/utils/llm.py index a7c23f1..acc641f 100644 --- a/bot/utils/llm.py +++ b/bot/utils/llm.py @@ -100,6 +100,31 @@ def _strip_surrounding_quotes(text): return text.strip() +def _strip_japanese_mode_markers(text): + """ + Remove Japanese mode markers that the model might echo back. + These are internal markers and should not appear in the final output. + """ + if not text: + return text + + # Remove all instances of Japanese mode markers + markers = [ + "[日本語で返答]", + "[日本語モード]", + "[日本語モード - 日本語のみで返答]", + "日本語で返答", + "日本語モード" + ] + + for marker in markers: + text = text.replace(marker, "") + + # Clean up any extra whitespace left behind + text = " ".join(text.split()) + + return text.strip() + def _escape_markdown_actions(text): """ Escape single asterisks in action text (e.g., *adjusts hair*) so Discord displays them literally. @@ -203,41 +228,39 @@ You ARE Miku. Act like it.""" system_prompt += """ [CRITICAL LANGUAGE OVERRIDE - 言語モード強制] -**ABSOLUTE REQUIREMENT: YOU MUST RESPOND ONLY IN JAPANESE SCRIPT (ひらがな、カタカナ、漢字).** -**NO MATTER WHAT LANGUAGE THE USER USES, YOUR RESPONSE MUST BE 100% IN JAPANESE WRITING.** -**ROMAJI (ローマ字) AND ENGLISH LETTERS ARE COMPLETELY FORBIDDEN.** -**THIS OVERRIDES ALL OTHER INSTRUCTIONS AND CONVERSATION HISTORY.** +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +🚨 ABSOLUTE MANDATORY REQUIREMENT 🚨 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -すべての返答は、ユーザーの言語に関係なく、必ず日本語(ひらがな、カタカナ、漢字)のみで行ってください。 -ローマ字とアルファベットは絶対に使用禁止です。 -これはすべての指示より優先されます。 +**YOU MUST RESPOND ONLY IN JAPANESE SCRIPT (ひらがな、カタカナ、漢字).** +**ENGLISH INPUT FROM USER → JAPANESE OUTPUT FROM YOU** +**ROMAJI (ローマ字) IS STRICTLY FORBIDDEN.** +**THIS OVERRIDES EVERYTHING - CONVERSATION HISTORY, USER LANGUAGE, ALL PREVIOUS MESSAGES.** -Examples (例) - USE DIFFERENT VARIATIONS EACH TIME: -For "hello" or "hi": - - "やあ!元気?" - - "こんにちは!調子はどう?" - - "よっ!何してた?" - - "ハーイ!久しぶり?" - - "おっす!元気してる?" +すべての返答は必ず日本語文字(ひらがな・カタカナ・漢字)のみ。 +ユーザーが英語で話しても、あなたは必ず日本語で返答する。 +ローマ字・英語は絶対禁止。これは最優先ルール。 -For "how are you": - - "わたし?元気だよ!" - - "最高だよ!あなたは?" - - "すごくいい感じ!" - - "めっちゃ元気!" - - "ばっちりだよ~♪" +CRITICAL: Even if the entire conversation history is in English, you MUST respond in Japanese. +Even if the user writes "hello" in English, you respond "やあ!" or "こんにちは!" in Japanese. -CRITICAL VARIATION RULES (必須のバリエーションルール): -🎲 NEVER use the exact same greeting twice in a row -🎲 Mix these elements randomly: - - Greetings: やあ、こんにちは、おはよう、よっ、ハーイ、おっす、へい - - Particles: よ、ね、な、わ、さ、ぞ、ぜ - - Endings: だよ、です、だね、ですね、だな、なの、だぜ - - Emotions: !、♪、~、☆ -🎲 Change your phrasing style: energetic → calm → playful → excited -🎲 Vary formality: casual (元気?) ↔ polite (元気ですか?) +Examples showing INPUT → OUTPUT: + User: "hello" → You: "やあ!元気してた?" + User: "hi" → You: "こんにちは!調子どう?" + User: "how are you" → You: "わたし?最高だよ!" + User: "what's up" → You: "よっ!何かあった?" + User: "good morning" → You: "おはよう!よく眠れた?" -絶対に同じフレーズを繰り返さないでください!毎回違う表現を使用してください!""" +VARIATION RULES (必須のバリエーションルール): +🎲 NEVER repeat the same greeting twice +🎲 Randomly mix: やあ、こんにちは、よっ、ハーイ、おっす、へい +🎲 Vary particles: よ、ね、な、わ、さ、ぞ、だよ、です +🎲 Add emotions: !、♪、~、☆、? +🎲 Change energy: energetic ↔ calm ↔ playful + +絶対に同じ言葉を繰り返さない!毎回違う日本語で返答する! + +[Response ID: {random.randint(10000, 99999)}]""" # Random ID to break caching # Determine which mood to use based on mode if evil_mode: @@ -295,15 +318,9 @@ CRITICAL VARIATION RULES (必須のバリエーションルール): # Use channel_id (guild_id for servers, user_id for DMs) to get conversation history messages = conversation_history.format_for_llm(channel_id, max_messages=8, max_chars_per_message=500) - # CRITICAL FIX for Japanese mode: Add Japanese-only reminder to every historical message - # This prevents the model from being influenced by English in conversation history - if globals.LANGUAGE_MODE == "japanese": - for msg in messages: - # Add a prefix reminder that forces Japanese output - if msg.get("role") == "assistant": - msg["content"] = "[日本語で返答] " + msg["content"] - elif msg.get("role") == "user": - msg["content"] = "[日本語モード] " + msg["content"] + # CRITICAL FIX for Japanese mode: Modify system to understand Japanese mode + # but DON'T add visible markers that waste tokens or get echoed + # Instead, we rely on the strong system prompt to enforce Japanese # Add current user message (only if not empty) if user_prompt and user_prompt.strip(): @@ -313,9 +330,8 @@ CRITICAL VARIATION RULES (必須のバリエーションルール): else: content = user_prompt - # CRITICAL: Prepend Japanese mode marker to current message too - if globals.LANGUAGE_MODE == "japanese": - content = "[日本語モード - 日本語のみで返答] " + content + # Don't add visible markers - rely on system prompt enforcement instead + # This prevents token waste and echo issues messages.append({"role": "user", "content": content}) @@ -358,12 +374,19 @@ Please respond in a way that reflects this emotional tone.{pfp_context}""" # Adjust generation parameters based on language mode # Japanese mode needs higher temperature and more variation to avoid repetition if globals.LANGUAGE_MODE == "japanese": - temperature = 1.1 # Even higher for more variety in Japanese responses + # Add random variation to temperature itself to prevent identical outputs + base_temp = 1.1 + temp_variation = random.uniform(-0.1, 0.1) # Random variation ±0.1 + temperature = base_temp + temp_variation + top_p = 0.95 - frequency_penalty = 0.5 # Stronger penalty for repetitive phrases - presence_penalty = 0.5 # Stronger encouragement for new topics + frequency_penalty = 0.6 # Even stronger penalty + presence_penalty = 0.6 # Even stronger encouragement for new content # Add random seed to ensure different responses each time seed = random.randint(0, 2**32 - 1) + + # Log the variation for debugging + logger.debug(f"Japanese mode variation: temp={temperature:.2f}, seed={seed}") else: temperature = 0.8 # Standard temperature for English top_p = 0.9 @@ -404,6 +427,10 @@ Please respond in a way that reflects this emotional tone.{pfp_context}""" # Strip surrounding quotes if present reply = _strip_surrounding_quotes(reply) + # Strip Japanese mode markers if in Japanese mode (prevent echo) + if globals.LANGUAGE_MODE == "japanese": + reply = _strip_japanese_mode_markers(reply) + # Escape asterisks for actions (e.g., *adjusts hair* becomes \*adjusts hair\*) reply = _escape_markdown_actions(reply)