sirochild commited on
Commit
5dc10e2
·
verified ·
1 Parent(s): 9abd0f3

Upload 5 files

Browse files
Files changed (3) hide show
  1. app.py +141 -25
  2. requirements.txt +1 -1
  3. 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
- raise ValueError("環境変数 TOGETHER_API_KEY が設定されていません。")
39
- client = OpenAI(api_key=TOGETHER_API_KEY, base_url="https://api.together.xyz/v1")
40
- LLM_MODEL = "meta-llama/Llama-3.1-70b-chat-hf"
41
- logger.info(f"Together AIクライアントの初期化が完了しました。")
 
 
 
42
  except Exception as e:
43
- logger.critical(f"アプリケーションの起動に失敗しました: {e}")
44
- raise
 
45
 
46
  # --- 4. 定数とプロンプト ---
47
- THEME_URLS = { "default": "...", "room_night": "...", "beach_sunset": "...", "festival_night": "...", "shrine_day": "...", "cafe_afternoon": "...", "aquarium_night": "..."} # URLを省略
48
- SYSTEM_PROMPT_MARI = """...""" # プロンプトを省略
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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(messages=messages, model=LLM_MODEL, temperature=0.8, max_tokens=500, response_format=response_format)
 
 
 
 
 
 
70
  return chat_completion.choices[0].message.content
71
  except Exception as e:
72
  logger.error(f"API呼び出しエラー: {e}", exc_info=True)
73
- return None
 
 
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の履歴形式 `[{"role": "user", "content": "..."}, ...]` から内部形式 `[(user, bot), ...]` へ変換
106
  internal_history = []
107
- user_msgs = [msg["content"] for msg in chat_history if msg["role"] == "user"]
108
- assistant_msgs = [msg["content"] for msg in chat_history if msg["role"] == "assistant"]
109
- for i in range(len(assistant_msgs)):
110
- internal_history.append((user_msgs[i], assistant_msgs[i]))
 
111
 
112
  # 保険: ブロック状態、入力長、履歴長のチェック
113
- if limiter_state["is_blocked"]:
114
- chat_history.append((message, "(…少し混乱している。時間をおいてから、ページを再読み込みして試してくれないか?)"))
 
 
 
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 = f"""あなたは会話の内容から、現在のシーンが変わるかどう���を判定するシステムです。
139
  以下の会話履歴に基づいて、ユーザーとキャラクターが移動した「新しいシーン」があれば、その名前をJSON形式で返してください。
140
  変化がない場合は、"none" を scene に設定してください。
141
 
 
 
 
 
 
 
 
 
 
142
  フォーマット:
143
  {"scene": "beach_sunset"}
144
 
145
  制約:
146
  - JSONオブジェクト以外は絶対に出力しないでください。
147
- - "true", "false", "yes", "no" など単語だけを出力しないでください。' # (省略)
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
- chat_history.append((message, bot_message))
 
 
 
 
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
- chat_history.append((message, "(ごめん、システムに予期せぬ問題が起きたみたいだ。)"))
 
 
 
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(label="麻理との会話", value=[], height=550, avatar_images=(None, "https://...bot.png"))
 
 
 
 
 
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;