Compare commits

...

2 Commits

4 changed files with 241 additions and 57 deletions

View File

@@ -358,6 +358,45 @@ async def cleanup_webhooks(client):
return cleaned_count 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 # DISPLAY NAME HELPERS
# ============================================================================ # ============================================================================

View File

@@ -40,13 +40,72 @@ async def is_miku_addressed(message) -> bool:
except Exception as e: except Exception as e:
logger.warning(f"Could not fetch referenced message: {e}") logger.warning(f"Could not fetch referenced message: {e}")
cleaned = message.content.strip() cleaned = message.content.strip().lower()
return bool(re.search( # Base names for Miku in different scripts
r'(?<![\w\(])(?:[^\w\s]{0,2}\s*)?miku(?:\s*[^\w\s]{0,2})?(?=,|\s*,|[!\.?\s]*$)', base_names = [
cleaned, 'miku', 'мику', 'みく', 'ミク', '未来'
re.IGNORECASE ]
))
# Japanese honorifics - all scripts combined for simpler matching
honorifics_all_scripts = [
# Latin
'chan', 'san', 'kun', 'nyan', 'hime', 'tan', 'chin', 'heika',
'denka', 'kakka', 'shi', 'chama', 'kyun', 'dono', 'sensei', 'senpai', 'jou',
# Hiragana
'ちゃん', 'さん', 'くん', 'にゃん', 'ひめ', 'たん', 'ちん', 'へいか',
'でんか', 'かっか', '', 'ちゃま', 'きゅん', 'どの', 'せんせい', 'せんぱい', 'じょう',
# Katakana
'チャン', 'サン', 'クン', 'ニャン', 'ヒメ', 'タン', 'チン', 'ヘイカ',
'デンカ', 'カッカ', '', 'チャマ', 'キュン', 'ドノ', 'センセイ', 'センパイ', 'ジョウ',
# Cyrillic
'чан', 'сан', 'кун', 'ньян', 'химе', 'тан', 'чин', 'хэйка',
'дэнка', 'какка', 'си', 'чама', 'кюн', 'доно', 'сэнсэй', 'сэнпай', 'жо'
]
# Optional o- prefix in different scripts
o_prefixes = ['o-', 'о-', '', '']
# Strategy: Just check if any base name appears (case insensitive for latin/cyrillic)
# Then allow any honorific to optionally follow
for base in base_names:
base_lower = base.lower()
# Check for just the base name
if re.search(r'(?<![a-zа-яa-я\w])' + re.escape(base_lower) + r'(?![a-zа-яa-я\w])', cleaned):
return True
# Check with optional o- prefix
for prefix in o_prefixes:
prefix_pattern = prefix.lower() if prefix != '' and prefix != '' else prefix
pattern = r'(?<![a-zа-яa-я\w])' + re.escape(prefix_pattern) + r'\s*' + re.escape(base_lower) + r'(?![a-zа-яa-я\w])'
if re.search(pattern, cleaned):
return True
# Check base name followed by any honorific (no spacing requirement to catch mixed script)
for honorific in honorifics_all_scripts:
honorific_lower = honorific.lower()
# Allow optional dash, space, or no separator between name and honorific
pattern = (r'(?<![a-zа-яa-я\w])' + re.escape(base_lower) +
r'[-\s]*' + re.escape(honorific_lower) +
r'(?![a-zа-яa-я\w])')
if re.search(pattern, cleaned):
return True
# Check with o- prefix + base + honorific
for prefix in o_prefixes:
prefix_lower = prefix.lower() if prefix != '' and prefix != '' else prefix
for honorific in honorifics_all_scripts:
honorific_lower = honorific.lower()
pattern = (r'(?<![a-zа-яa-я\w])' + re.escape(prefix_lower) +
r'[-\s]*' + re.escape(base_lower) +
r'[-\s]*' + re.escape(honorific_lower) +
r'(?![a-zа-яa-я\w])')
if re.search(pattern, cleaned):
return True
return False
# Vectorstore functionality disabled - not needed with current structured context approach # Vectorstore functionality disabled - not needed with current structured context approach
# If you need embeddings in the future, you can use a different embedding provider # If you need embeddings in the future, you can use a different embedding provider

View File

@@ -416,6 +416,11 @@ async def apply_evil_mode_changes(client, change_username=True, change_pfp=True,
try: try:
await client.user.edit(username="Evil Miku") await client.user.edit(username="Evil Miku")
logger.debug("Changed bot username to '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: except Exception as e:
logger.error(f"Could not change bot username: {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 # Set evil profile picture
if change_pfp: if change_pfp:
await set_evil_profile_picture(client) 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) # Set evil role color (#D60004 - dark red)
if change_role_color: if change_role_color:
@@ -455,6 +469,11 @@ async def revert_evil_mode_changes(client, change_username=True, change_pfp=True
try: try:
await client.user.edit(username="Hatsune Miku") await client.user.edit(username="Hatsune Miku")
logger.debug("Changed bot username back to '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: except Exception as e:
logger.error(f"Could not change bot username: {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 # Restore normal profile picture
if change_pfp: if change_pfp:
await restore_normal_profile_picture(client) 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 # Restore saved role color
if change_role_color: if change_role_color:
try: try:
_, _, saved_color = load_evil_mode_state() # Try to get color from metadata.json first (current pfp's dominant color)
if saved_color: metadata_color = get_color_from_metadata()
await set_role_color(client, saved_color)
logger.debug(f"Restored role color to {saved_color}") # 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: 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: except Exception as e:
logger.error(f"Failed to restore role color: {e}") logger.error(f"Failed to restore role color: {e}")
@@ -566,6 +602,29 @@ async def restore_normal_profile_picture(client):
return False 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 # EVIL MODE STATE HELPERS
# ============================================================================ # ============================================================================

View File

@@ -100,6 +100,31 @@ def _strip_surrounding_quotes(text):
return text.strip() 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): def _escape_markdown_actions(text):
""" """
Escape single asterisks in action text (e.g., *adjusts hair*) so Discord displays them literally. 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 += """ system_prompt += """
[CRITICAL LANGUAGE OVERRIDE - 言語モード強制] [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.** 🚨 ABSOLUTE MANDATORY REQUIREMENT 🚨
**ROMAJI (ローマ字) AND ENGLISH LETTERS ARE COMPLETELY FORBIDDEN.** ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
**THIS OVERRIDES ALL OTHER INSTRUCTIONS AND CONVERSATION HISTORY.**
すべての返答は、ユーザーの言語に関係なく、必ず日本語(ひらがな、カタカナ、漢字)のみで行ってください。 **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 (必須のバリエーションルール): Examples showing INPUT → OUTPUT:
🎲 NEVER use the exact same greeting twice in a row User: "hello" → You: "やあ!元気してた?"
🎲 Mix these elements randomly: User: "hi" → You: "こんにちは!調子どう?"
- Greetings: やあ、こんにちは、おはよう、よっ、ハーイ、おっす、へい User: "how are you" → You: "わたし?最高だよ!"
- Particles: よ、ね、な、わ、さ、ぞ、ぜ User: "what's up" → You: "よっ!何かあった?"
- Endings: だよ、です、だね、ですね、だな、なの、だぜ User: "good morning" → You: "おはよう!よく眠れた?"
- Emotions: !、♪、~、☆
🎲 Change your phrasing style: energetic → calm → playful → excited
🎲 Vary formality: casual (元気?) ↔ polite (元気ですか?)
絶対に同じフレーズを繰り返さないでください!毎回違う表現を使用してください!""" 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 # Determine which mood to use based on mode
if evil_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 # 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) 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 # CRITICAL FIX for Japanese mode: Modify system to understand Japanese mode
# This prevents the model from being influenced by English in conversation history # but DON'T add visible markers that waste tokens or get echoed
if globals.LANGUAGE_MODE == "japanese": # Instead, we rely on the strong system prompt to enforce 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"]
# Add current user message (only if not empty) # Add current user message (only if not empty)
if user_prompt and user_prompt.strip(): if user_prompt and user_prompt.strip():
@@ -313,9 +330,8 @@ CRITICAL VARIATION RULES (必須のバリエーションルール):
else: else:
content = user_prompt content = user_prompt
# CRITICAL: Prepend Japanese mode marker to current message too # Don't add visible markers - rely on system prompt enforcement instead
if globals.LANGUAGE_MODE == "japanese": # This prevents token waste and echo issues
content = "[日本語モード - 日本語のみで返答] " + content
messages.append({"role": "user", "content": content}) 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 # Adjust generation parameters based on language mode
# Japanese mode needs higher temperature and more variation to avoid repetition # Japanese mode needs higher temperature and more variation to avoid repetition
if globals.LANGUAGE_MODE == "japanese": 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 top_p = 0.95
frequency_penalty = 0.5 # Stronger penalty for repetitive phrases frequency_penalty = 0.6 # Even stronger penalty
presence_penalty = 0.5 # Stronger encouragement for new topics presence_penalty = 0.6 # Even stronger encouragement for new content
# Add random seed to ensure different responses each time # Add random seed to ensure different responses each time
seed = random.randint(0, 2**32 - 1) seed = random.randint(0, 2**32 - 1)
# Log the variation for debugging
logger.debug(f"Japanese mode variation: temp={temperature:.2f}, seed={seed}")
else: else:
temperature = 0.8 # Standard temperature for English temperature = 0.8 # Standard temperature for English
top_p = 0.9 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 # Strip surrounding quotes if present
reply = _strip_surrounding_quotes(reply) 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\*) # Escape asterisks for actions (e.g., *adjusts hair* becomes \*adjusts hair\*)
reply = _escape_markdown_actions(reply) reply = _escape_markdown_actions(reply)