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
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
# ============================================================================

View File

@@ -40,13 +40,72 @@ async def is_miku_addressed(message) -> bool:
except Exception as e:
logger.warning(f"Could not fetch referenced message: {e}")
cleaned = message.content.strip()
cleaned = message.content.strip().lower()
return bool(re.search(
r'(?<![\w\(])(?:[^\w\s]{0,2}\s*)?miku(?:\s*[^\w\s]{0,2})?(?=,|\s*,|[!\.?\s]*$)',
cleaned,
re.IGNORECASE
))
# Base names for Miku in different scripts
base_names = [
'miku', 'мику', 'みく', 'ミク', '未来'
]
# 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
# 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:
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}")
@@ -427,6 +432,15 @@ async def apply_evil_mode_changes(client, change_username=True, change_pfp=True,
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:
await set_role_color(client, "#D60004")
@@ -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}")
@@ -466,15 +485,32 @@ async def revert_evil_mode_changes(client, change_username=True, change_pfp=True
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:
# 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:
_, _, 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}")
logger.debug(f"Restored role color from saved state: {saved_color}")
else:
logger.warning("No saved role color found, skipping color restoration")
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
# ============================================================================

View File

@@ -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)