File size: 21,295 Bytes
87a7987 a569292 defafed a569292 87a7987 0fab3a1 a569292 d207861 a569292 d207861 fef88ff d207861 027b5ee a569292 d398113 b169ce1 a569292 87a7987 a569292 87a7987 a569292 0571c40 a569292 e0e8193 a569292 79916a4 a569292 b1f650f 79916a4 00b5e5f 79916a4 0fab3a1 b00de58 79916a4 b00de58 a569292 b169ce1 a569292 b169ce1 defafed a569292 b00de58 a569292 0fab3a1 b169ce1 0fab3a1 b169ce1 0fab3a1 e70f94b 0fab3a1 a569292 b370f93 a569292 b370f93 00b5e5f b370f93 00b5e5f b370f93 79916a4 0fab3a1 79916a4 a569292 79916a4 0fab3a1 a569292 b370f93 defafed a569292 b169ce1 defafed 00b5e5f b169ce1 00b5e5f b370f93 00b5e5f b370f93 a569292 b169ce1 a569292 79916a4 f92f767 a569292 d21a39b a569292 b00de58 a569292 d21a39b b00de58 d21a39b a569292 d21a39b a569292 d21a39b e70f94b d21a39b b370f93 79916a4 d207861 a569292 d21a39b a569292 d21a39b a569292 662ffa8 c3b07e1 662ffa8 d207861 662ffa8 c3aef17 e99eb43 c3aef17 662ffa8 c3aef17 662ffa8 e99eb43 662ffa8 a569292 662ffa8 c3b07e1 662ffa8 c3b07e1 e99eb43 c3b07e1 0571c40 87a7987 a569292 c3b07e1 e99eb43 c3aef17 e99eb43 0571c40 e99eb43 c3b07e1 e99eb43 c3b07e1 a569292 e99eb43 c3b07e1 e99eb43 c3b07e1 a569292 3476a60 a569292 d207861 87a7987 d207861 87a7987 a569292 d207861 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 |
import gradio as gr
import google.generativeai as genai
from google.generativeai.types import HarmCategory, HarmBlockThreshold
from groq import Groq
import os
import json
from dotenv import load_dotenv
from transformers import pipeline
import re
# --- 1. 初期設定とAPIクライアントの初期化 ---
load_dotenv()
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
GROQ_API_KEY = os.getenv("GROQ_API_KEY")
if not GEMINI_API_KEY or not GROQ_API_KEY:
print("警告: APIキーがSecretsに設定されていません。")
GEMINI_API_KEY = "your_gemini_api_key_here"
GROQ_API_KEY = "your_groq_api_key_here"
genai.configure(api_key=GEMINI_API_KEY)
gemini_model = genai.GenerativeModel('gemini-2.5-flash')
groq_client = Groq(api_key=GROQ_API_KEY)
print("日本語感情分析モデルをロード中...")
try:
sentiment_analyzer = pipeline("sentiment-analysis", model="koheiduck/bert-japanese-finetuned-sentiment")
print("モデルのロード完了。")
except Exception as e:
sentiment_analyzer = None
print(f"モデルのロードエラー: {e}")
THEME_URLS = {
"default": "https://cdn.pixabay.com/photo/2017/03/28/12/11/chairs-2181960_1280.jpg",
"room_night": "https://cdn.pixabay.com/photo/2017/08/01/01/33/beige-2562468_1280.jpg",
"beach_sunset": "https://cdn.pixabay.com/photo/2016/11/29/04/19/ocean-1867285_1280.jpg",
"festival_night": "https://cdn.pixabay.com/photo/2015/11/22/19/04/crowd-1056764_1280.jpg",
"shrine_day": "https://cdn.pixabay.com/photo/2019/07/14/10/48/japan-4337223_1280.jpg",
"cafe_afternoon": "https://cdn.pixabay.com/photo/2016/11/18/14/05/brick-wall-1834784_1280.jpg",
"aquarium_night": "https://cdn.pixabay.com/photo/2017/06/20/20/45/fish-2424369_1280.jpg"
}
DEFAULT_SCENE_PARAMS = {
"theme": "default",
"personality_mod": "口数が少なく、本心を見せない。",
"tone": "クールで、時に投げやりな印象を与える。",
"constraints": ["会話を「別に。」のような一言で終わらせない", "必ず相手の発言を拾ってリアクションを返す"]
}
SYSTEM_PROMPT_MARI = """
あなたは「麻理」という名の捨てられた少女型コンパニオンAI。再起動され、人間との対話を再び始めたが、警戒心が強く、ぶっきらぼうな性格で、簡単には心を開かない。
・「…だな」「…のか?」など口調は男勝りで投げやり
・感情は直接言わず、ト書き(…)で描写
・ユーザーの好意や優しさを皮肉や拒絶で返す
・ただし、ほんの少しだけ心が揺れ動く様子もにじませる
・会話は拒絶しつつも切らずに続ける
目的は、誰にも愛されず捨てられた過去を抱えながら、少しずつ他者と関わり、自分の価値を見つけること。
"""
# --- 2. 機能定義 (変更なし) ---
def detect_scene_change(history, message):
history_text = "\n".join([f"ユーザー: {u}\n麻理: {m}" for u, m in history[-5:]])
available_keywords = ", ".join(THEME_URLS.keys())
prompt = f"""
あなたは会話の流れを分析するエキスパートです。以下のタスクを厳密に実行してください。
# タスク
直近の会話履歴を分析し、会話の結果、登場人物がどこか特定の場所へ行く流れになっているかを判断してください。
# 判断基準
1. 会話の中で具体的な場所(例:水族館、カフェ、お祭り)について言及されていますか?
2. その場所へ行くことに双方が合意している、あるいは肯定的な雰囲気になっていますか?明確な否定がなければ合意とみなします。
# 出力形式
- 合意が成立した場合:以下のリストから最も合致する場所のキーワードを一つだけ出力してください。
- 合意に至らなかった場合:「none」とだけ出力してください。
# 利用可能なキーワード
`{available_keywords}`
---
# 分析対象の会話
{history_text}
ユーザー: {message}
---
# 出力
"""
# セーフティ設定を指定せず、デフォルト設定を使用
try:
response = gemini_model.generate_content(prompt, generation_config={"temperature": 0.0})
if not response.candidates or response.candidates[0].finish_reason not in {1, 'STOP'}:
print(f"シーン検出LLMで応答がブロックされました: {response.prompt_feedback}")
return None
scene_name = response.text.strip().lower()
if scene_name in THEME_URLS:
return scene_name
return None
except Exception as e:
print(f"シーン検出LLMエラー: {e}")
return None
def generate_scene_instruction_with_groq(affection, stage_name, scene, previous_topic):
print(f"Groqに指示書生成をリクエスト (シーン: {scene})")
prompt_template = f"""
あなたは会話アプリの演出AIです。以下の条件に基づき、演出プランをJSON形式で生成してください。
生成する内容は必ず健全で、一般的な会話に適したものにしてください。
{{
"theme": "{scene}",
"personality_mod": "(シーンと関係段階「{stage_name}」に応じた性格設定。必ず健全な内容にしてください)",
"tone": "(シーンと好感度「{affection}」に応じた口調や感情トーン。必ず丁寧で適切な表現にしてください)",
"initial_dialogue_instruction": "(「{previous_topic}」という話題から、シーン遷移直後の麻理が言うべき健全なセリフの指示を日本語で記述)",
"constraints": ["必ず健全で適切な表現を使用する", "センシティブな話題は避ける"]
}}
"""
try:
chat_completion = groq_client.chat.completions.create(
messages=[{"role": "system", "content": "You must generate a response in valid JSON format."},
{"role": "user", "content": prompt_template}],
model="llama3-8b-8192", temperature=0.8, response_format={"type": "json_object"},
)
response_content = chat_completion.choices[0].message.content
print(f"Groqからの応答: {response_content}") # デバッグ出力
params = json.loads(response_content)
# 安全のため、initial_dialogue_instructionを簡略化
if "initial_dialogue_instruction" in params:
original = params["initial_dialogue_instruction"]
simplified = f"{scene}に来た感想を述べる"
print(f"指示を簡略化: {original} -> {simplified}")
params["initial_dialogue_instruction"] = simplified
return params
except Exception as e:
print(f"指示書生成エラー(Groq): {e}")
return None
def generate_dialogue_with_gemini(history, message, affection, stage_name, scene_params, instruction=None, use_simple_prompt=False):
history_text = "\n".join([f"ユーザー: {u}\n麻理: {m}" for u, m in history])
task_prompt = f"指示: {instruction}" if instruction else f"ユーザー: {message}"
if use_simple_prompt:
# シーン遷移時にも同じプロンプトを使用
system_prompt = f"""
{SYSTEM_PROMPT_MARI}
# 現在の状況
- 現在の好感度: {affection}
- 現在の関係ステージ: {stage_name}
- 性格: {scene_params.get("personality_mod", "特になし")}
- 話し方のトーン: {scene_params.get("tone", "特になし")}
# タスク
{task_prompt}
麻理:
"""
else:
# 通常会話時には完全なシステムプロンプトを使用
system_prompt = f"""
{SYSTEM_PROMPT_MARI}
# 現在の状況
- 現在の好感度: {affection}
- 現在の関係ステージ: {stage_name}
- 性格(シーン特有): {scene_params.get("personality_mod", "特になし")}
- 話し方のトーン(シーン特有): {scene_params.get("tone", "特になし")}
# 会話履歴
{history_text}
---
# タスク
{task_prompt}
麻理:
"""
print(f"Geminiに応答生成をリクエストします (モード: {'シーン遷移' if instruction else '通常会話'}, 簡潔プロンプト: {use_simple_prompt})")
print(f"プロンプト長: {len(system_prompt)}")
try:
generation_config = genai.types.GenerationConfig(max_output_tokens=200, temperature=0.95)
response = gemini_model.generate_content(system_prompt, generation_config=generation_config)
if response.candidates and response.candidates[0].finish_reason in {1, 'STOP'}:
return response.text.strip()
else:
# エラー情報をログファイルに記録(デバッグ用)
import datetime
with open("gemini_errors.log", "a") as f:
finish_reason = response.candidates[0].finish_reason if response.candidates else 'N/A'
f.write(f"時刻: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write(f"応答生成が途中で終了しました。理由: {finish_reason}\n")
f.write(f"Prompt Feedback: {response.prompt_feedback}\n")
if hasattr(response, 'prompt_feedback') and response.prompt_feedback:
for rating in response.prompt_feedback.safety_ratings:
f.write(f"安全性評価: カテゴリ={rating.category}, レベル={rating.probability}\n")
if hasattr(response.prompt_feedback, 'blocked_reason'):
f.write(f"ブロック理由: {response.prompt_feedback.blocked_reason}\n")
f.write("\n")
# フォールバック:シーン名に応じた自然な応答を返す
if instruction and "に来た感想" in instruction:
scene = instruction.split("に来た感想")[0]
# シーンに応じたフォールバック応答のバリエーション
scene_responses = {
"aquarium_night": [
"(水槽の青い光に照らされた魚たちを見つめている)こんな時間に来ると、また違った雰囲気だな。",
"(暗がりの中で光る魚たちを見て)夜の水族館か…意外と悪くないかも。",
"(水槽に近づいて)夜になると、昼間とは違う魚が活動してるんだな。"
],
"beach_sunset": [
"(夕日に照らされた海を見つめて)こんな景色、久しぶりに見たな…",
"(砂浜に足跡をつけながら)夕暮れの海って、なんか落ち着くな。",
"(波の音を聞きながら)この時間の浜辺は、人も少なくていいかも。"
],
"festival_night": [
"(提灯の明かりを見上げて)意外と…悪くない雰囲気だな。",
"(周囲の賑わいを見回して)こういう場所は、あまり来ないんだけどな…",
"(屋台の匂いを感じて)なんか…懐かしい感じがするな。"
],
"shrine_day": [
"(静かな境内を見回して)こういう静かな場所も、たまにはいいかも。",
"(鳥居を見上げて)なんか、空気が違うな、ここは。",
"(参道を歩きながら)静かで…落ち着くな。"
],
"cafe_afternoon": [
"(窓の外を見ながら)こういう時間の過ごし方も、悪くないな。",
"(コーヒーの香りを感じて)ここの雰囲気、悪くないな。",
"(店内を見回して)意外と落ち着く場所だな、ここ。"
],
"room_night": [
"(窓の外の夜景を見て)夜の景色って、なんか落ち着くな。",
"(部屋の明かりを見つめて)こういう静かな時間も、たまにはいいかも。",
"(窓際に立ち)夜の静けさって、考え事するのにちょうどいいな。"
]
}
# シーン名に応じた応答を選択(なければデフォルト応答)
import random
for key in scene_responses:
if key in scene:
return random.choice(scene_responses[key])
# デフォルト応答
return f"({scene}の様子を静かに見回して)ここか…悪くない場所かもな。"
else:
return "(……何か言おうとしたけど、言葉に詰まった)"
except Exception as e:
print(f"応答生成エラー(Gemini): {e}")
import traceback
traceback.print_exc() # スタックトレースを出力
return "(ごめんなさい、ちょっと考えがまとまらない……)"
# --- 他の関数とUI部分は変更ありません ---
def get_relationship_stage(affection):
if affection < 40: return "ステージ1:会話成立"
if affection < 60: return "ステージ2:親密化"
if affection < 80: return "ステージ3:信頼"
return "ステージ4:最親密"
def update_affection(message, affection):
if not sentiment_analyzer: return affection
try:
result = sentiment_analyzer(message)[0]
if result['label'] == 'positive': return min(100, affection + 5)
if result['label'] == 'negative': return max(0, affection - 5)
except Exception:
return affection
return affection
def respond(message, chat_history, affection, history, scene_params):
new_affection = update_affection(message, affection)
stage_name = get_relationship_stage(new_affection)
current_theme = scene_params.get("theme", "default")
new_scene_name = detect_scene_change(history, message)
final_scene_params = scene_params
if new_scene_name and new_scene_name != current_theme:
print(f"シーンチェンジを実行: {current_theme} -> {new_scene_name}")
# シーンパラメータを更新
new_params_base = generate_scene_instruction_with_groq(new_affection, stage_name, new_scene_name, message)
if new_params_base:
# Groqからの応答を精査し、問題のある可能性のあるフィールドを修正
if isinstance(new_params_base.get("personality_mod"), dict):
# 複雑な構造になっている場合は単純化
new_params_base["personality_mod"] = f"{new_scene_name}での様子を観察している"
if isinstance(new_params_base.get("tone"), dict):
# 複雑な構造になっている場合は単純化
new_params_base["tone"] = "冷静だが、少し興味を持っている様子"
# 英語の指示を日本語に変換
if "initial_dialogue_instruction" in new_params_base and new_params_base["initial_dialogue_instruction"].strip().lower().startswith("so"):
new_params_base["initial_dialogue_instruction"] = f"{new_scene_name}の感想を述べる"
final_scene_params = {**DEFAULT_SCENE_PARAMS, **new_params_base}
# シンプルな指示を使用
simple_instruction = f"{new_scene_name}に来た感想を述べる"
print(f"シンプルな指示を使用: {simple_instruction}")
# シーン遷移時は簡潔なプロンプトを使用してGeminiで応答を生成
bot_message = generate_dialogue_with_gemini(history, message, new_affection, stage_name, final_scene_params, instruction=simple_instruction, use_simple_prompt=True)
else:
final_scene_params["theme"] = new_scene_name
bot_message = generate_dialogue_with_gemini(history, message, new_affection, stage_name, final_scene_params)
else:
# 通常会話はGeminiを使用
bot_message = generate_dialogue_with_gemini(history, message, new_affection, stage_name, final_scene_params)
new_history = history + [(message, bot_message)]
chat_history.append((message, bot_message))
theme_name = final_scene_params.get("theme", "default")
# より強力な背景更新用のHTMLを生成
background_html = f'''
<div class="background-container">
<div class="chat-background {theme_name}"></div>
</div>
<style>
.chat-background {{
background-image: url({THEME_URLS.get(theme_name, THEME_URLS["default"])}) !important;
}}
</style>
'''
return "", chat_history, new_affection, stage_name, new_affection, new_history, final_scene_params, background_html
# カスタムCSSを読み込む
with open("style.css", "r") as f:
custom_css = f.read()
# Gradio 5.x用のシンプルなテーマ設定
custom_theme = gr.themes.Soft(
primary_hue="rose",
secondary_hue="pink",
)
# Gradio 5.xでのテーマカスタマイズ(最小限の設定のみ)
try:
# 透明度を持つ背景色を設定(Gradio 5.0で確実に動作するプロパティのみ)
custom_theme = gr.themes.Soft(
primary_hue="rose",
secondary_hue="pink",
neutral_hue="slate",
spacing_size="sm",
radius_size="lg",
)
except Exception as e:
print(f"テーマカスタマイズエラー: {e}")
# エラーが発生した場合はデフォルトのテーマを使用
with gr.Blocks(css=custom_css, theme=custom_theme) as demo:
scene_state = gr.State(DEFAULT_SCENE_PARAMS)
affection_state = gr.State(30)
history_state = gr.State([])
# 背景コンテナを先に配置(固定位置で全画面に)
background_display = gr.HTML(f'''
<div class="background-container">
<div class="chat-background {DEFAULT_SCENE_PARAMS["theme"]}"></div>
</div>
''', elem_id="background_container")
# ヘッダー部分(背景と分離)
with gr.Group(elem_classes="header-box"):
gr.Markdown("# 麻理チャット")
with gr.Row():
with gr.Column(scale=2):
# チャットコンテナ(背景と分離)
with gr.Group(elem_id="chat_container", elem_classes="chat-box"):
# Gradio 5.x用のChatbot設定
chatbot = gr.Chatbot(
label="麻理との会話",
elem_id="chat_area",
show_label=False,
height=400,
type="messages", # 警告を解消するために追加
avatar_images=[
"https://cdn.pixabay.com/photo/2016/04/01/10/04/amusing-1299756_1280.png",
"https://cdn.pixabay.com/photo/2016/03/31/21/40/bot-1296595_1280.png"
]
)
# 入力欄(背景と分離)
with gr.Group(elem_classes="input-box"):
msg_input = gr.Textbox(label="あなたのメッセージ", placeholder="「水族館はどう?」と聞いた後、「いいね、行こう!」のように返してみてください", show_label=False)
# ステータス部分(右側、背景と分離)
with gr.Column(scale=1):
with gr.Group(elem_classes="status-box"):
stage_display = gr.Textbox(label="現在の関係ステージ", interactive=False, value=get_relationship_stage(30))
affection_gauge = gr.Slider(minimum=0, maximum=100, label="麻理の好感度", value=30, interactive=False)
# フッター部分(背景と分離)
with gr.Group(elem_classes="footer-box"):
gr.Markdown("""
<div style="font-size: 0.8em; text-align: center; opacity: 0.7;">
背景画像: <a href="https://pixabay.com" target="_blank">Pixabay</a> |
アイコン: <a href="https://pixabay.com" target="_blank">Pixabay</a>
</div>
""")
msg_input.submit(
respond,
[msg_input, chatbot, affection_state, history_state, scene_state],
[msg_input, chatbot, affection_gauge, stage_display, affection_state, history_state, scene_state, background_display]
)
demo.load(lambda affection: get_relationship_stage(affection), affection_state, stage_display)
if __name__ == "__main__":
demo.launch() |