File size: 12,968 Bytes
87a7987 6c88771 a569292 bb95357 6c88771 87a7987 6c88771 a569292 d207861 027b5ee 53dab97 7c8b666 6c88771 1b3d9b9 53dab97 6c88771 7c8b666 6c88771 dc62d4a 6c88771 a8eb81f 6c88771 7c8b666 6c88771 7c8b666 87a7987 6c88771 a569292 0571c40 a569292 e0e8193 79916a4 6c88771 79916a4 6c88771 79916a4 6c88771 b00de58 6c88771 53dab97 6c88771 a569292 6c88771 dc62d4a 6c88771 a569292 6c88771 a569292 53dab97 6c88771 0fab3a1 53dab97 6c88771 ecc3056 6c88771 53dab97 6c88771 a569292 6c88771 a569292 6c88771 53dab97 6c88771 79916a4 a569292 6c88771 a569292 6c88771 a569292 6c88771 fb20ed4 6c88771 53dab97 6c88771 53dab97 6c88771 53dab97 6c88771 53dab97 6c88771 fb20ed4 6c88771 53dab97 fb20ed4 6c88771 53dab97 fb20ed4 53dab97 b43a003 d21a39b 6c88771 d21a39b 6c88771 53dab97 d207861 6c88771 53dab97 662ffa8 e99eb43 6c88771 b43a003 662ffa8 6c88771 a569292 662ffa8 6c88771 53dab97 6c88771 53dab97 ecc3056 6c88771 c3b07e1 6c88771 24e38b9 6c88771 87a7987 a569292 b43a003 6c88771 |
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 |
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))) |