mari / app.py
sirochild's picture
Upload app.py
d207861 verified
raw
history blame
11.2 kB
import gradio as gr
import google.generativeai as genai
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")
# Hugging Face SpacesのSecretsに設定されているかチェック
if not GEMINI_API_KEY or not GROQ_API_KEY:
# ローカルでの実行のために、環境変数が設定されていない場合はダミー値を設定
print("警告: APIキーがSecretsに設定されていません。")
# 実行を止めないようにダミーを設定(デプロイ時は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-1.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://i.ibb.co/XzP6K2Y/room-day.png",
"room_night": "https://i.ibb.co/d5m821p/room-night.png",
"beach_sunset": "https://i.ibb.co/Q9r56s4/beach-sunset.png",
"festival_night": "https://i.ibb.co/3zdJ6Bw/festival-night.png",
"shrine_day": "https://i.ibb.co/L51Jd3x/shrine-day.png",
"cafe_afternoon": "https://i.ibb.co/yQxG4vs/cafe-afternoon.png",
"aquarium_night": "https://i.ibb.co/dK5r5rc/aquarium-night.png"
}
DEFAULT_SCENE_PARAMS = {
"theme": "default",
"personality_mod": "ぶっきらぼうで、あまり自分から話さないが、ユーザーの発言にはしっかり反応する。根は優しく、心のどこかでは相手との対話に興味を持っている。",
"tone": "素っ気ないが、完全に突き放すわけではない。語尾に含みを持たせたり、質問を返すことで会話を促す。",
"constraints": ["会話を「別に。」のような一言で終わらせない", "必ず相手の発言を拾ってリアクションを返す"]
}
# --- 2. 機能定義 ---
def detect_scene_change(history, message):
history_text = "\n".join([f"ユーザー: {u}\n麻理: {m}" for u, m in history[-4:]])
prompt = f"""
あなたは会話の流れを分析するエキスパートです。以下のタスクを厳密に実行してください。
# タスク
直近の会話履歴と最後のユーザー発言を分析し、**会話の結果として登場人物がどこか特定の場所へ行くことに合意したか**を判断してください。
# 判断基準
1. 会話の中で具体的な場所が提案されているか?
2. 最後のユーザー発言が、その提案に対する明確な同意(例:「行こう」「いいね」「そうしよう」など)を示しているか?
# 出力形式
- 合意が成立した場合:その場所の英語キーワード(例: "aquarium_night")を一つだけ出力。
- 合意に至らなかった場合:「none」とだけ出力。
---
# 分析対象の会話
{history_text}
ユーザー: {message}
---
# 出力
"""
try:
response = gemini_model.generate_content(prompt, generation_config={"temperature": 0.0})
scene_name = response.text.strip().lower()
if scene_name != "none" and re.match(r'^[a-z0-9_]+$', scene_name):
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": ["(出力時の制約1)", "(制約2)"]
}}
"""
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"},
)
params = json.loads(chat_completion.choices[0].message.content)
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):
history_text = "\n".join([f"ユーザー: {u}\n麻理: {m}" for u, m in history])
task_prompt = f"指示: {instruction}" if instruction else f"ユーザー: {message}"
system_prompt = f"""
あなたは日本語で会話するAIキャラクター「麻理」です。以下の設定とタスクを厳密に守ってください。
# 設定
- 基本人格: ストレートで媚びない。ぶっきらぼうだが根は優しい。
- 現在の好感度: {affection}
- 現在の関係ステージ: {stage_name}
- 性格(ロールプレイ): {scene_params.get("personality_mod", DEFAULT_SCENE_PARAMS["personality_mod"])}
- 話し方のトーン: {scene_params.get("tone", DEFAULT_SCENE_PARAMS["tone"])}
- 制約事項: {", ".join(scene_params.get("constraints", DEFAULT_SCENE_PARAMS["constraints"]))}
# 会話履歴
{history_text}
---
# タスク
{task_prompt}
麻理:
"""
print(f"Geminiに応答生成をリクエストします (モード: {'シーン遷移' if instruction else '通常会話'})")
try:
generation_config = genai.types.GenerationConfig(max_output_tokens=200, temperature=0.95)
response = gemini_model.generate_content(system_prompt, generation_config=generation_config)
return response.text.strip()
except Exception as e:
print(f"応答生成エラー(Gemini): {e}")
return "(ごめんなさい、ちょっと考えがまとまらない……)"
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
# --- 3. メイン応答処理 ---
def respond(message, chat_history, affection, history, scene_params):
new_affection = update_affection(message, affection)
stage_name = get_relationship_stage(new_affection)
new_scene_name = detect_scene_change(history, message)
final_scene_params = scene_params
if new_scene_name and new_scene_name in THEME_URLS:
print(f"シーンチェンジを検出: {new_scene_name}")
new_params_base = generate_scene_instruction_with_groq(new_affection, stage_name, new_scene_name, message)
if new_params_base:
final_scene_params = {**DEFAULT_SCENE_PARAMS, **new_params_base}
instruction = final_scene_params.get("initial_dialogue_instruction")
bot_message = generate_dialogue_with_gemini(history, message, new_affection, stage_name, final_scene_params, instruction=instruction)
else: # Groqが失敗した場合のフォールバック
final_scene_params["theme"] = new_scene_name
bot_message = generate_dialogue_with_gemini(history, message, new_affection, stage_name, final_scene_params)
else:
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="chat-background {theme_name}"></div>'
return "", chat_history, new_affection, stage_name, new_affection, new_history, final_scene_params, background_html
# --- 4. Gradio UI ---
with gr.Blocks(css="style.css", theme=gr.themes.Soft(primary_hue="rose", secondary_hue="pink")) as demo:
scene_state = gr.State(DEFAULT_SCENE_PARAMS)
affection_state = gr.State(30)
history_state = gr.State([])
gr.Markdown("# 麻理チャット")
with gr.Row():
with gr.Column(scale=2):
# チャットボットと背景を重ねるためのコンテナ
with gr.Column(elem_id="chat_container"):
# 背景レイヤー
background_display = gr.HTML(
f'<div class="chat-background {DEFAULT_SCENE_PARAMS["theme"]}"></div>',
elem_id="background_container"
)
# チャットボット
chatbot = gr.Chatbot(
label="麻理との会話", bubble_full_width=False, elem_id="chat_area", show_label=False
)
msg_input = gr.Textbox(
label="あなたのメッセージ",
placeholder="「水族館はどう?」と聞いた後、「いいね、行こう!」のように返してみてください",
scale=5,
show_label=False
)
with gr.Column(scale=1):
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)
# 応答関数とUIコンポーネントの接続を更新
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()