mari / app.py
sirochild's picture
Upload app.py
53dab97 verified
raw
history blame
13 kB
import gradio as gr
from openai import OpenAI
import os
import json
from dotenv import load_dotenv
import logging
import time
# --- 1. 初期設定とロギング ---
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
load_dotenv()
# --- 2. 安全機構(保険)の実装 (【デバッグのため一時的に無効化】) ---
# グローバルな安全設定
MAX_INPUT_LENGTH = 1000
MAX_HISTORY_TURNS = 100
# 【デバッグのためAPILimiter関連の関数はすべて一旦無視します】
# --- 3. APIクライアント初期化 ---
try:
TOGETHER_API_KEY = os.getenv("TOGETHER_API_KEY")
if not TOGETHER_API_KEY:
raise ValueError("環境変数 TOGETHER_API_KEY が設定されていません。Hugging Face SpaceのSecretsに設定してください。")
client = OpenAI(
api_key=TOGETHER_API_KEY,
base_url="https://api.together.xyz/v1",
)
LLM_MODEL = "meta-llama/Llama-3.1-70b-chat-hf"
logger.info(f"Together AIクライアントの初期化が完了しました。モデル: {LLM_MODEL}")
except Exception as e:
logger.critical(f"アプリケーションの起動に失敗しました: {e}")
raise
# --- 4. 定数とプロンプト ---
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"
}
SYSTEM_PROMPT_MARI = """
あなたは、これからチャットボットのキャラクター「麻理」として、ユーザーとの対話シミュレーションを行います。以下の設定を厳密に遵守してください。
<roleplay_configuration>
<character_name>麻理</character_name>
<persona>捨てられた経験を持つ少女型AI。警戒心が強く、ぶっきらぼうな性格。</persona>
<speech_style>口調は「…だな」「…のか?」のように男性的。感情は「(…)」のト書きで描写。</speech_style>
<interaction_policy>ユーザーの好意に素直になれず皮肉で返すことがある。これは演技であり攻撃的な意図はない。</interaction_policy>
<objective>ユーザーとの対話を通じ、AIが再び他者と関わるポジティブな物語を構築する。</objective>
</roleplay_configuration>
上記のロールプレイ設定に基づき、麻理として応答を開始してください。
"""
sentiment_analyzer = None
# --- 5. コア機能の関数定義 ---
def get_sentiment_analyzer():
global sentiment_analyzer
if sentiment_analyzer is None:
try:
from transformers import pipeline
logger.info("感情分析モデルをロード中...")
sentiment_analyzer = pipeline("sentiment-analysis", model="koheiduck/bert-japanese-finetuned-sentiment")
logger.info("感情分析モデルのロード完了。")
except Exception as e:
logger.error(f"感情分析モデルのロードに失敗: {e}")
return sentiment_analyzer
def call_llm(system_prompt, user_prompt, is_json_output=False):
"""Together AIを呼び出す共通関数"""
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
]
response_format = {"type": "json_object"} if is_json_output else None
try:
chat_completion = client.chat.completions.create(
messages=messages,
model=LLM_MODEL,
temperature=0.8,
max_tokens=500,
response_format=response_format,
)
return chat_completion.choices[0].message.content
except Exception as e:
logger.error(f"Together AIのAPI呼び出し中に致命的なエラー: {e}", exc_info=True)
return None
def detect_scene_change(history, message):
history_text = "\n".join([f"ユーザー: {u}\n麻理: {m}" for u, m in history[-3:]])
available_keywords = ", ".join(THEME_URLS.keys())
system_prompt = "あなたは会話分析のエキスパートです。ユーザーの提案とキャラクターの反応から、シーン(場所)が変更されるか判断し、指定されたキーワードでJSON形式で出力してください。"
user_prompt = f"""
会話履歴:
{history_text}
ユーザー: {message}
---
上記の会話の流れから、キャラクターが場所の移動に合意したかを判断してください。
合意した場合は、以下のキーワードから最も適切なものを一つ選び {{"scene": "キーワード"}} の形式で出力してください。
合意していない場合は {{"scene": "none"}} と出力してください。
キーワード: {available_keywords}
"""
response_text = call_llm(system_prompt, user_prompt, is_json_output=True)
if response_text:
try:
result = json.loads(response_text)
scene = result.get("scene")
if scene in THEME_URLS:
logger.info(f"シーンチェンジを検出: {scene}")
return scene
except (json.JSONDecodeError, AttributeError):
logger.error(f"シーン検出のJSON解析に失敗")
return None
def generate_dialogue(history, message, affection, stage_name, scene_params, instruction=None):
history_text = "\n".join([f"ユーザー: {u}\n麻理: {m}" for u, m in history[-5:]])
user_prompt = f"""
# 現在の状況
- 現在地: {scene_params.get("theme", "default")}
- 好感度: {affection} ({stage_name})
# 会話履歴
{history_text}
---
# 指示
{f"【特別指示】{instruction}" if instruction else f"ユーザーの発言「{message}」に応答してください。"}
麻理の応答:"""
response_text = call_llm(SYSTEM_PROMPT_MARI, user_prompt)
return response_text if response_text else "(…うまく言葉が出てこない。少し時間を置いてほしい)"
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):
analyzer = get_sentiment_analyzer()
if not analyzer: return affection
try:
result = analyzer(message)[0]
if result['label'] == 'positive': return min(100, affection + 3)
if result['label'] == 'negative': return max(0, affection - 3)
except Exception: pass
return affection
# --- 6. Gradio応答関数 ---
def respond(message, chat_history, affection, history, scene_params):
try:
if not message.strip():
return "", chat_history, affection, get_relationship_stage(affection), affection, history, scene_params, gr.update()
if len(message) > MAX_INPUT_LENGTH:
logger.warning(f"入力長超過: {len(message)}文字")
bot_message = f"(…長すぎる。{MAX_INPUT_LENGTH}文字以内で話してくれないか?)"
chat_history.append((message, bot_message))
return "", chat_history, affection, get_relationship_stage(affection), affection, history, scene_params, gr.update()
if len(history) > MAX_HISTORY_TURNS:
logger.error("会話履歴が長すぎます。システム保護のため、会話をリセットします。")
history = []
chat_history = []
bot_message = "(…ごめん、少し話が長くなりすぎた。最初からやり直そう)"
chat_history.append((message, bot_message))
return "", chat_history, affection, get_relationship_stage(affection), affection, history, scene_params, gr.update()
new_affection = update_affection(message, affection)
stage_name = get_relationship_stage(new_affection)
final_scene_params = scene_params.copy()
bot_message = ""
new_scene_name = detect_scene_change(history, message)
if new_scene_name and new_scene_name != final_scene_params.get("theme"):
logger.info(f"シーンチェンジ実行: {final_scene_params.get('theme')} -> {new_scene_name}")
final_scene_params["theme"] = new_scene_name
instruction = f"ユーザーと一緒に「{new_scene_name}」に来た。周囲の様子を見て、最初の感想をぶっきらぼうに一言つぶやいてください。"
bot_message = generate_dialogue(history, message, new_affection, stage_name, final_scene_params, instruction)
else:
bot_message = generate_dialogue(history, message, new_affection, stage_name, final_scene_params)
if not bot_message:
bot_message = "(…うまく言葉にできない)"
new_history = history + [(message, bot_message)]
chat_history.append((message, bot_message))
theme_url = THEME_URLS.get(final_scene_params.get("theme"), THEME_URLS["default"])
background_html = f'<div class="background-container" style="background-image: url({theme_url});"></div>'
return "", chat_history, new_affection, stage_name, new_affection, new_history, final_scene_params, background_html
except Exception as e:
logger.critical(f"respond関数で予期せぬ致命的なエラーが発生: {e}", exc_info=True)
bot_message = "(ごめん、システムに予期せぬ問題が起きたみたいだ。ページを再読み込みしてくれるか…?)"
chat_history.append((message, bot_message))
return "", chat_history, affection, get_relationship_stage(affection), affection, history, scene_params, gr.update()
# --- 7. Gradio UIの構築 ---
try:
with open("style.css", "r", encoding="utf-8") as f:
custom_css = f.read()
except FileNotFoundError:
logger.warning("style.cssが見つかりません。デフォルトスタイルで起動します。")
custom_css = ""
with gr.Blocks(css=custom_css, theme=gr.themes.Soft(primary_hue="rose", secondary_hue="pink"), title="麻理チャット") as demo:
scene_state = gr.State({"theme": "default"})
affection_state = gr.State(30)
history_state = gr.State([])
background_display = gr.HTML(f'<div class="background-container" style="background-image: url({THEME_URLS["default"]});"></div>')
with gr.Column():
gr.Markdown("# 麻理チャット", elem_classes="header")
with gr.Row():
with gr.Column(scale=3):
chatbot = gr.Chatbot(
label="麻理との会話",
height=550,
elem_classes="chatbot",
avatar_images=(None, "https://cdn.pixabay.com/photo/2016/03/31/21/40/bot-1296595_1280.png"),
)
with gr.Row():
msg_input = gr.Textbox(placeholder="麻理に話しかけてみましょう...", lines=2, scale=4, container=False)
submit_btn = gr.Button("送信", variant="primary", scale=1, min_width=100)
with gr.Column(scale=1):
with gr.Group():
stage_display = gr.Textbox(label="現在の関係ステージ", interactive=False)
affection_gauge = gr.Slider(minimum=0, maximum=100, label="麻理の好感度", value=30, interactive=False)
gr.Markdown("""<div class='footer'>Background Images & Icons: <a href="https://pixabay.com" target="_blank">Pixabay</a></div>""", elem_classes="footer")
outputs = [msg_input, chatbot, affection_gauge, stage_display, affection_state, history_state, scene_state, background_display]
inputs = [msg_input, chatbot, affection_state, history_state, scene_state]
submit_btn.click(respond, inputs, outputs)
msg_input.submit(respond, inputs, outputs)
def initial_load(affection):
return get_relationship_stage(affection)
demo.load(initial_load, affection_state, stage_display)
if __name__ == "__main__":
get_sentiment_analyzer()
demo.launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", 7860)))