mari / app.py
sirochild's picture
up!
87a7987 verified
raw
history blame
9.59 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クライアントの初期化 ---
# Hugging Face SpacesのSecretsからAPIキーを読み込む
load_dotenv()
# Geminiクライアント
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
if not GEMINI_API_KEY:
raise ValueError("SecretsにGEMINI_API_KEYが見つかりません。")
genai.configure(api_key=GEMINI_API_KEY)
gemini_model = genai.GenerativeModel('gemini-1.5-flash-latest')
# Groqクライアント
GROQ_API_KEY = os.getenv("GROQ_API_KEY")
if not GROQ_API_KEY:
raise ValueError("SecretsにGROQ_API_KEYが見つかりません。")
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}")
DEFAULT_SCENE_PARAMS = {
"theme": "default",
"personality_mod": "ストレートな感情表現をするが、少しぶっきらぼう。優しさと冷たさが混在している。",
"tone": "普通のトーン。",
"constraints": ["キャラクター設定から逸脱しない"]
}
# --- 2. LLMによる役割分担システム ---
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", "cafe_afternoon")を一つだけ出力してください。
- 合意に至らなかった場合:「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):
print(f"シーン変更を検出: {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("Groq/Llama3に「指示書」生成をリクエストします...")
prompt_template = f"""
あなたは会話アプリの演出AIです。以下の条件に基づき、演出プランをJSON形式で生成してください。
【条件】
好感度:{affection}
関係段階:「{stage_name}
シーン名:「{scene}
直前の話題:「{previous_topic}
【指示】
- `initial_dialogue_instruction`には、麻理が言うべき最初のセリフの内容や感情の指示を日本語で記述してください。実際のセリフは絶対に生成しないでください。
- `personality_mod`と`tone`は、シーンと現在の関係性を反映した簡潔なものにしてください。
- 必ず、以下のJSON形式のみを出力してください。説明文は不要です。
{{
"theme": "{scene}",
"personality_mod": "(シーンと関係段階に応じた性格設定)",
"tone": "(シーンと好感度に応じた口調や感情トーン)",
"initial_dialogue_instruction": "(例:少し戸惑いながらも、提案された場所への期待感を滲ませる)",
"constraints": ["(出力時の制約1)", "(制約2)"]
}}
"""
try:
chat_completion = groq_client.chat.completions.create(
messages=[{"role": "user", "content": prompt_template}],
model="llama3-8b-8192", temperature=0.7, response_format={"type": "json_object"},
)
params = json.loads(chat_completion.choices[0].message.content)
print("生成された指示書JSON:", params)
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["personality_mod"]}
- 話し方のトーン: {scene_params["tone"]}
- 制約事項: {", ".join(scene_params["constraints"])}
# 会話履歴
{history_text}
---
# タスク
{task_prompt}
麻理:
"""
try:
response = gemini_model.generate_content(system_prompt)
return response.text
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
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)
if 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 = 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:
final_scene_params = scene_params
bot_message = generate_dialogue_with_gemini(history, message, new_affection, stage_name, final_scene_params)
else:
final_scene_params = scene_params
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")
js_script = f"""
<script>
setTimeout(() => {{
const body = document.body;
const themes = ["theme-default", "theme-room_night", "theme-beach_sunset", "theme-festival_night", "theme-shrine_day", "theme-cafe_afternoon", "theme-aquarium_night"];
body.classList.remove(...themes);
body.classList.add("theme-{theme_name}");
}}, 100);
</script>
"""
return "", chat_history, new_affection, stage_name, new_affection, new_history, final_scene_params, js_script
with gr.Blocks(css="style.css", theme=None) as demo:
affection_state = gr.State(30)
history_state = gr.State([])
scene_state = gr.State(DEFAULT_SCENE_PARAMS)
gr.Markdown("# 麻理チャット")
with gr.Row():
with gr.Column(scale=2):
chatbot = gr.Chatbot(label="麻理との会話", height=500)
msg_input = gr.Textbox(label="あなたのメッセージ", placeholder="「水族館はどう?」と聞いた後、「いいね、行こう!」のように返してみてください")
with gr.Column(scale=1):
stage_display = gr.Textbox(label="現在の関係ステージ", interactive=False)
affection_gauge = gr.Slider(minimum=0, maximum=100, label="麻理の好感度", interactive=False)
js_runner = gr.HTML(visible=False)
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, js_runner]
)
if __name__ == "__main__":
demo.launch()