Compare commits
2 Commits
c0aaab0c3a
...
ecd14cf704
| Author | SHA1 | Date | |
|---|---|---|---|
| ecd14cf704 | |||
| 641a5b83e8 |
@@ -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
|
||||
# ============================================================================
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
# ============================================================================
|
||||
|
||||
117
bot/utils/llm.py
117
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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user