|
|
import gradio as gr
|
|
|
from openai import OpenAI
|
|
|
import os
|
|
|
import json
|
|
|
from dotenv import load_dotenv
|
|
|
import logging
|
|
|
import time
|
|
|
|
|
|
|
|
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
|
logger = logging.getLogger(__name__)
|
|
|
load_dotenv()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
MAX_INPUT_LENGTH = 1000
|
|
|
MAX_HISTORY_TURNS = 100
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
|
|
|
|
|
|
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))) |