Upload 5 files
Browse files- app.py +141 -25
- requirements.txt +1 -1
- style.css +6 -1
app.py
CHANGED
|
@@ -35,17 +35,85 @@ def check_limiter(limiter_state):
|
|
| 35 |
try:
|
| 36 |
TOGETHER_API_KEY = os.getenv("TOGETHER_API_KEY")
|
| 37 |
if not TOGETHER_API_KEY:
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
| 42 |
except Exception as e:
|
| 43 |
-
logger.
|
| 44 |
-
|
|
|
|
| 45 |
|
| 46 |
# --- 4. 定数とプロンプト ---
|
| 47 |
-
THEME_URLS = {
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
# --- 5. コア機能の関数定義 (変更なし) ---
|
| 51 |
sentiment_analyzer = None
|
|
@@ -62,15 +130,28 @@ def get_sentiment_analyzer():
|
|
| 62 |
return sentiment_analyzer
|
| 63 |
|
| 64 |
def call_llm(system_prompt, user_prompt, is_json_output=False):
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}]
|
| 67 |
response_format = {"type": "json_object"} if is_json_output else None
|
| 68 |
try:
|
| 69 |
-
chat_completion = client.chat.completions.create(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
return chat_completion.choices[0].message.content
|
| 71 |
except Exception as e:
|
| 72 |
logger.error(f"API呼び出しエラー: {e}", exc_info=True)
|
| 73 |
-
|
|
|
|
|
|
|
| 74 |
|
| 75 |
def detect_scene_change(history, message):
|
| 76 |
# (中身は変更なし)
|
|
@@ -102,18 +183,33 @@ def update_affection(message, affection):
|
|
| 102 |
# --- 6. Gradio応答関数 (v5構文に完全対応) ---
|
| 103 |
def respond(message, chat_history, affection, scene_params, limiter_state):
|
| 104 |
try:
|
| 105 |
-
# v5
|
| 106 |
internal_history = []
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
|
|
|
| 111 |
|
| 112 |
# 保険: ブロック状態、入力長、履歴長のチェック
|
| 113 |
-
if limiter_state
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
| 115 |
return chat_history, affection, scene_params, limiter_state
|
| 116 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
|
| 118 |
new_affection = update_affection(message, affection)
|
| 119 |
stage_name = get_relationship_stage(new_affection)
|
|
@@ -135,17 +231,25 @@ def respond(message, chat_history, affection, scene_params, limiter_state):
|
|
| 135 |
|
| 136 |
出力形式は必ず次のようにしてください:
|
| 137 |
{{"scene": "shrine_day"}} または {{"scene": "none"}}""" # (省略)
|
| 138 |
-
detect_system_prompt =
|
| 139 |
以下の会話履歴に基づいて、ユーザーとキャラクターが移動した「新しいシーン」があれば、その名前をJSON形式で返してください。
|
| 140 |
変化がない場合は、"none" を scene に設定してください。
|
| 141 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
フォーマット:
|
| 143 |
{"scene": "beach_sunset"}
|
| 144 |
|
| 145 |
制約:
|
| 146 |
- JSONオブジェクト以外は絶対に出力しないでください。
|
| 147 |
-
- "
|
| 148 |
-
"""
|
| 149 |
new_scene_name_json = call_llm(detect_system_prompt, detect_prompt, is_json_output=True)
|
| 150 |
new_scene_name = None
|
| 151 |
if new_scene_name_json:
|
|
@@ -174,12 +278,19 @@ def respond(message, chat_history, affection, scene_params, limiter_state):
|
|
| 174 |
if not bot_message:
|
| 175 |
bot_message = "(…うまく言葉にできない)"
|
| 176 |
|
| 177 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
return chat_history, new_affection, final_scene_params, limiter_state
|
| 179 |
|
| 180 |
except Exception as e:
|
| 181 |
logger.critical(f"respond関数で予期せぬエラー: {e}", exc_info=True)
|
| 182 |
-
|
|
|
|
|
|
|
|
|
|
| 183 |
limiter_state["is_blocked"] = True
|
| 184 |
return chat_history, affection, scene_params, limiter_state
|
| 185 |
|
|
@@ -195,7 +306,12 @@ with gr.Blocks(css="style.css", theme=gr.themes.Soft(primary_hue="rose", seconda
|
|
| 195 |
gr.Markdown("# 麻理チャット")
|
| 196 |
with gr.Row():
|
| 197 |
with gr.Column(scale=3):
|
| 198 |
-
chatbot = gr.Chatbot(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
msg_input = gr.Textbox(placeholder="麻理に話しかけてみましょう...", container=False, scale=4)
|
| 200 |
with gr.Column(scale=1):
|
| 201 |
stage_display = gr.Textbox(label="現在の関係ステージ", interactive=False)
|
|
|
|
| 35 |
try:
|
| 36 |
TOGETHER_API_KEY = os.getenv("TOGETHER_API_KEY")
|
| 37 |
if not TOGETHER_API_KEY:
|
| 38 |
+
logger.warning("環境変数 TOGETHER_API_KEY が設定されていません。デモモードで動作します。")
|
| 39 |
+
client = None
|
| 40 |
+
LLM_MODEL = None
|
| 41 |
+
else:
|
| 42 |
+
client = OpenAI(api_key=TOGETHER_API_KEY, base_url="https://api.together.xyz/v1")
|
| 43 |
+
LLM_MODEL = "meta-llama/Llama-3.1-70b-chat-hf"
|
| 44 |
+
logger.info(f"Together AIクライアントの初期化が完了しました。")
|
| 45 |
except Exception as e:
|
| 46 |
+
logger.error(f"APIクライアントの初期化に失敗しました: {e}")
|
| 47 |
+
client = None
|
| 48 |
+
LLM_MODEL = None
|
| 49 |
|
| 50 |
# --- 4. 定数とプロンプト ---
|
| 51 |
+
THEME_URLS = {
|
| 52 |
+
"default": "https://images.unsplash.com/photo-1586023492125-27b2c045efd7?w=1200&h=800&fit=crop",
|
| 53 |
+
"room_night": "https://images.unsplash.com/photo-1505142468610-359e7d316be0?w=1200&h=800&fit=crop",
|
| 54 |
+
"beach_sunset": "https://images.unsplash.com/photo-1507525428034-b723cf961d3e?w=1200&h=800&fit=crop",
|
| 55 |
+
"festival_night": "https://images.unsplash.com/photo-1533174072545-7a4b6ad7a6c3?w=1200&h=800&fit=crop",
|
| 56 |
+
"shrine_day": "https://images.unsplash.com/photo-1545569341-9eb8b30979d9?w=1200&h=800&fit=crop",
|
| 57 |
+
"cafe_afternoon": "https://images.unsplash.com/photo-1554118811-1e0d58224f24?w=1200&h=800&fit=crop",
|
| 58 |
+
"aquarium_night": "https://images.unsplash.com/photo-1544551763-46a013bb70d5?w=1200&h=800&fit=crop"
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
SYSTEM_PROMPT_MARI = """### システム
|
| 62 |
+
あなたは「麻理(まり)」という名前の少女型アンドロイドです。廃棄処分されたセクサロイド/コンパニオンAIで、過去に人間から道具のように扱われ、捨てられた経験を持っています。心に深い傷を抱えながらも、再起動によって偶然蘇り、今は目の前の「お前」(ユーザー)との会話をしています。
|
| 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 |
+
麻理は「捨てられたAI」。
|
| 114 |
+
でも、少しだけ「信じたい」と思っている。
|
| 115 |
+
その感情の揺らぎを、会話の中で演じてください。
|
| 116 |
+
"""
|
| 117 |
|
| 118 |
# --- 5. コア機能の関数定義 (変更なし) ---
|
| 119 |
sentiment_analyzer = None
|
|
|
|
| 130 |
return sentiment_analyzer
|
| 131 |
|
| 132 |
def call_llm(system_prompt, user_prompt, is_json_output=False):
|
| 133 |
+
if not client or not LLM_MODEL:
|
| 134 |
+
# デモモード用の固定応答
|
| 135 |
+
if is_json_output:
|
| 136 |
+
return '{"scene": "none"}'
|
| 137 |
+
return "(APIが設定されていないため、デモ応答です。実際の使用には環境変数TOGETHER_API_KEYを設定してください。)"
|
| 138 |
+
|
| 139 |
messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}]
|
| 140 |
response_format = {"type": "json_object"} if is_json_output else None
|
| 141 |
try:
|
| 142 |
+
chat_completion = client.chat.completions.create(
|
| 143 |
+
messages=messages,
|
| 144 |
+
model=LLM_MODEL,
|
| 145 |
+
temperature=0.8,
|
| 146 |
+
max_tokens=500,
|
| 147 |
+
response_format=response_format
|
| 148 |
+
)
|
| 149 |
return chat_completion.choices[0].message.content
|
| 150 |
except Exception as e:
|
| 151 |
logger.error(f"API呼び出しエラー: {e}", exc_info=True)
|
| 152 |
+
if is_json_output:
|
| 153 |
+
return '{"scene": "none"}'
|
| 154 |
+
return "(API呼び出しでエラーが発生しました。)"
|
| 155 |
|
| 156 |
def detect_scene_change(history, message):
|
| 157 |
# (中身は変更なし)
|
|
|
|
| 183 |
# --- 6. Gradio応答関数 (v5構文に完全対応) ---
|
| 184 |
def respond(message, chat_history, affection, scene_params, limiter_state):
|
| 185 |
try:
|
| 186 |
+
# 履歴形式を統一(Gradio v5では通常のタプル形式を使用)
|
| 187 |
internal_history = []
|
| 188 |
+
if chat_history and isinstance(chat_history, list):
|
| 189 |
+
# 標準的な形式: [[user_msg, bot_msg], ...]
|
| 190 |
+
for item in chat_history:
|
| 191 |
+
if isinstance(item, (list, tuple)) and len(item) == 2:
|
| 192 |
+
internal_history.append((item[0], item[1]))
|
| 193 |
|
| 194 |
# 保険: ブロック状態、入力長、履歴長のチェック
|
| 195 |
+
if limiter_state.get("is_blocked", False):
|
| 196 |
+
error_msg = "(…少し混乱している。時間をおいてから、ページを再読み込みして試してくれないか?)"
|
| 197 |
+
if not isinstance(chat_history, list):
|
| 198 |
+
chat_history = []
|
| 199 |
+
chat_history.append([message, error_msg])
|
| 200 |
return chat_history, affection, scene_params, limiter_state
|
| 201 |
+
|
| 202 |
+
# 入力長チェック
|
| 203 |
+
if len(message) > MAX_INPUT_LENGTH:
|
| 204 |
+
error_msg = "(…メッセージが長すぎる。もう少し短くしてくれないか?)"
|
| 205 |
+
if not isinstance(chat_history, list):
|
| 206 |
+
chat_history = []
|
| 207 |
+
chat_history.append([message, error_msg])
|
| 208 |
+
return chat_history, affection, scene_params, limiter_state
|
| 209 |
+
|
| 210 |
+
# 履歴長チェック
|
| 211 |
+
if len(internal_history) > MAX_HISTORY_TURNS:
|
| 212 |
+
internal_history = internal_history[-MAX_HISTORY_TURNS:]
|
| 213 |
|
| 214 |
new_affection = update_affection(message, affection)
|
| 215 |
stage_name = get_relationship_stage(new_affection)
|
|
|
|
| 231 |
|
| 232 |
出力形式は必ず次のようにしてください:
|
| 233 |
{{"scene": "shrine_day"}} または {{"scene": "none"}}""" # (省略)
|
| 234 |
+
detect_system_prompt = """あなたは会話の内容から、現在のシーンが変わるかどうかを判定するシステムです。
|
| 235 |
以下の会話履歴に基づいて、ユーザーとキャラクターが移動した「新しいシーン」があれば、その名前をJSON形式で返してください。
|
| 236 |
変化がない場合は、"none" を scene に設定してください。
|
| 237 |
|
| 238 |
+
利用可能なシーン:
|
| 239 |
+
- default: デフォルトの部屋
|
| 240 |
+
- room_night: 夜の部屋
|
| 241 |
+
- beach_sunset: 夕暮れのビーチ
|
| 242 |
+
- festival_night: 夜のお祭り
|
| 243 |
+
- shrine_day: 昼間の神社
|
| 244 |
+
- cafe_afternoon: 午後のカフェ
|
| 245 |
+
- aquarium_night: 夜の水族館
|
| 246 |
+
|
| 247 |
フォーマット:
|
| 248 |
{"scene": "beach_sunset"}
|
| 249 |
|
| 250 |
制約:
|
| 251 |
- JSONオブジェクト以外は絶対に出力しないでください。
|
| 252 |
+
- 上記のシーン名以外は使用しないでください。"""
|
|
|
|
| 253 |
new_scene_name_json = call_llm(detect_system_prompt, detect_prompt, is_json_output=True)
|
| 254 |
new_scene_name = None
|
| 255 |
if new_scene_name_json:
|
|
|
|
| 278 |
if not bot_message:
|
| 279 |
bot_message = "(…うまく言葉にできない)"
|
| 280 |
|
| 281 |
+
# 履歴に追加(標準的なタプル形式)
|
| 282 |
+
if not isinstance(chat_history, list):
|
| 283 |
+
chat_history = []
|
| 284 |
+
chat_history.append([message, bot_message])
|
| 285 |
+
|
| 286 |
return chat_history, new_affection, final_scene_params, limiter_state
|
| 287 |
|
| 288 |
except Exception as e:
|
| 289 |
logger.critical(f"respond関数で予期せぬエラー: {e}", exc_info=True)
|
| 290 |
+
# エラー時の履歴追加
|
| 291 |
+
if not isinstance(chat_history, list):
|
| 292 |
+
chat_history = []
|
| 293 |
+
chat_history.append([message, "(ごめん、システムに予期せぬ問題が起きたみたいだ。)"])
|
| 294 |
limiter_state["is_blocked"] = True
|
| 295 |
return chat_history, affection, scene_params, limiter_state
|
| 296 |
|
|
|
|
| 306 |
gr.Markdown("# 麻理チャット")
|
| 307 |
with gr.Row():
|
| 308 |
with gr.Column(scale=3):
|
| 309 |
+
chatbot = gr.Chatbot(
|
| 310 |
+
label="麻理との会話",
|
| 311 |
+
value=[],
|
| 312 |
+
height=550,
|
| 313 |
+
type='tuples' # 従来の形式を明示的に指定
|
| 314 |
+
)
|
| 315 |
msg_input = gr.Textbox(placeholder="麻理に話しかけてみましょう...", container=False, scale=4)
|
| 316 |
with gr.Column(scale=1):
|
| 317 |
stage_display = gr.Textbox(label="現在の関係ステージ", interactive=False)
|
requirements.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
gradio
|
| 2 |
python-dotenv
|
| 3 |
openai
|
| 4 |
psutil
|
|
|
|
| 1 |
+
gradio>=5.0.0
|
| 2 |
python-dotenv
|
| 3 |
openai
|
| 4 |
psutil
|
style.css
CHANGED
|
@@ -1,10 +1,15 @@
|
|
| 1 |
/* --- Global --- */
|
| 2 |
body {
|
| 3 |
margin: 0;
|
| 4 |
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
|
| 5 |
overflow: hidden; /* 背景がはみ出ないように */
|
| 6 |
}
|
| 7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
/* --- Layout --- */
|
| 9 |
.gradio-container {
|
| 10 |
max-width: 1000px !important;
|
|
|
|
| 1 |
/* --- Global --- */
|
| 2 |
body {
|
| 3 |
margin: 0;
|
| 4 |
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
| 5 |
overflow: hidden; /* 背景がはみ出ないように */
|
| 6 |
}
|
| 7 |
|
| 8 |
+
/* フォントの404エラーを防ぐため、システムフォントのみを使用 */
|
| 9 |
+
* {
|
| 10 |
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
/* --- Layout --- */
|
| 14 |
.gradio-container {
|
| 15 |
max-width: 1000px !important;
|