sirochild commited on
Commit
b43a003
·
verified ·
1 Parent(s): 6c88771

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +62 -58
app.py CHANGED
@@ -12,40 +12,7 @@ logger = logging.getLogger(__name__)
12
  load_dotenv()
13
 
14
 
15
- # --- 2. 安全機構(保険)の実装 ---
16
-
17
- class APILimiter:
18
- """
19
- APIの呼び出し回数を制限し、クレジットを守るためのクラス。
20
- GradioのStateでセッションごとに管理される。
21
- """
22
- def __init__(self, max_requests, in_seconds):
23
- self.max_requests = max_requests
24
- self.in_seconds = in_seconds
25
- self.timestamps = []
26
- self.is_blocked = False # 一度ブロックされたら、このセッションではAPIを呼ばせない
27
-
28
- def check_and_record(self):
29
- """
30
- API呼び出しを試みる。制限内であればTrueを返し、呼び出しを記録する。
31
- 制限を超えていればFalseを返し、以降の呼び出しをブロックする。
32
- """
33
- if self.is_blocked:
34
- logger.warning("API呼び出しは永続的にブロックされています。")
35
- return False
36
-
37
- now = time.time()
38
- # 制限時間外の古いタイムスタンプを削除
39
- self.timestamps = [t for t in self.timestamps if now - t < self.in_seconds]
40
-
41
- if len(self.timestamps) >= self.max_requests:
42
- logger.error(f"レートリミット超過! {self.in_seconds}秒以内に{self.max_requests}回を超えました。このセッションのAPI呼び出しを永久にブロックします。")
43
- self.is_blocked = True
44
- return False
45
-
46
- # 呼び出しを許可し、今回の呼び出し時刻を記録する
47
- self.timestamps.append(now)
48
- return True
49
 
50
  # グローバルな安全設定
51
  RATE_LIMIT_MAX_REQUESTS = 15
@@ -53,6 +20,33 @@ RATE_LIMIT_IN_SECONDS = 60
53
  MAX_INPUT_LENGTH = 1000
54
  MAX_HISTORY_TURNS = 100
55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
  # --- 3. APIクライアント初期化 ---
58
  try:
@@ -113,9 +107,9 @@ def get_sentiment_analyzer():
113
  logger.error(f"感情分析モデルのロードに失敗: {e}")
114
  return sentiment_analyzer
115
 
116
- def call_llm(system_prompt, user_prompt, api_limiter, is_json_output=False):
117
  """Together AIを呼び出す共通関数。必ずリミッターを通過させる。"""
118
- if not api_limiter.check_and_record():
119
  return None
120
 
121
  messages = [
@@ -134,11 +128,11 @@ def call_llm(system_prompt, user_prompt, api_limiter, is_json_output=False):
134
  return chat_completion.choices[0].message.content
135
  except Exception as e:
136
  logger.error(f"Together AIのAPI呼び出し中に致命的なエラー: {e}", exc_info=True)
137
- api_limiter.is_blocked = True
138
  logger.error("APIエラーのため、このセッションのAPI呼び出しをブロックします。")
139
  return None
140
 
141
- def detect_scene_change(history, message, api_limiter):
142
  history_text = "\n".join([f"ユーザー: {u}\n麻理: {m}" for u, m in history[-3:]])
143
  available_keywords = ", ".join(THEME_URLS.keys())
144
  system_prompt = "あなたは会話分析のエキスパートです。ユーザーの提案とキャラクターの反応から、シーン(場所)が変更されるか判断し、指定されたキーワードでJSON形式で出力してください。"
@@ -152,7 +146,7 @@ def detect_scene_change(history, message, api_limiter):
152
  合意していない場合は {{"scene": "none"}} と出力してください。
153
  キーワード: {available_keywords}
154
  """
155
- response_text = call_llm(system_prompt, user_prompt, api_limiter, is_json_output=True)
156
  if response_text:
157
  try:
158
  result = json.loads(response_text)
@@ -164,7 +158,7 @@ def detect_scene_change(history, message, api_limiter):
164
  logger.error(f"シーン検出のJSON解析に失敗")
165
  return None
166
 
167
- def generate_dialogue(history, message, affection, stage_name, scene_params, api_limiter, instruction=None):
168
  history_text = "\n".join([f"ユーザー: {u}\n麻理: {m}" for u, m in history[-5:]])
169
  user_prompt = f"""
170
  # 現在の状況
@@ -178,7 +172,7 @@ def generate_dialogue(history, message, affection, stage_name, scene_params, api
178
  {f"【特別指示】{instruction}" if instruction else f"ユーザーの発言「{message}」に応答してください。"}
179
 
180
  麻理の応答:"""
181
- response_text = call_llm(SYSTEM_PROMPT_MARI, user_prompt, api_limiter)
182
  return response_text if response_text else "(…うまく言葉が出てこない。少し時間を置いてほしい)"
183
 
184
  def get_relationship_stage(affection):
@@ -199,22 +193,22 @@ def update_affection(message, affection):
199
 
200
 
201
  # --- 6. Gradio応答関数 ---
202
- def respond(message, chat_history, affection, history, scene_params, api_limiter):
203
  try:
204
  # 保険: ブロック状態、入力長、履歴長のチェック
205
- if api_limiter.is_blocked:
206
  bot_message = "(…少し混乱している。時間をおいてから、ページを再読み込みして試してくれないか?)"
207
  chat_history.append((message, bot_message))
208
- return "", chat_history, affection, get_relationship_stage(affection), affection, history, scene_params, api_limiter, gr.update()
209
 
210
  if not message.strip():
211
- return "", chat_history, affection, get_relationship_stage(affection), affection, history, scene_params, api_limiter, gr.update()
212
 
213
  if len(message) > MAX_INPUT_LENGTH:
214
  logger.warning(f"入力長超過: {len(message)}文字")
215
  bot_message = f"(…長すぎる。{MAX_INPUT_LENGTH}文字以内で話してくれないか?)"
216
  chat_history.append((message, bot_message))
217
- return "", chat_history, affection, get_relationship_stage(affection), affection, history, scene_params, api_limiter, gr.update()
218
 
219
  if len(history) > MAX_HISTORY_TURNS:
220
  logger.error("会話履歴が長すぎます。システム保護のため、会話をリセットします。")
@@ -222,7 +216,7 @@ def respond(message, chat_history, affection, history, scene_params, api_limiter
222
  chat_history = []
223
  bot_message = "(…ごめん、少し話が長くなりすぎた。最初からやり直そう)"
224
  chat_history.append((message, bot_message))
225
- return "", chat_history, affection, get_relationship_stage(affection), affection, history, scene_params, api_limiter, gr.update()
226
 
227
  # 通常処理
228
  new_affection = update_affection(message, affection)
@@ -230,15 +224,19 @@ def respond(message, chat_history, affection, history, scene_params, api_limiter
230
  final_scene_params = scene_params.copy()
231
 
232
  bot_message = ""
233
- new_scene_name = detect_scene_change(history, message, api_limiter)
234
 
235
  if new_scene_name and new_scene_name != final_scene_params.get("theme"):
236
  logger.info(f"シーンチェンジ実行: {final_scene_params.get('theme')} -> {new_scene_name}")
237
  final_scene_params["theme"] = new_scene_name
238
  instruction = f"ユーザーと一緒に「{new_scene_name}」に来た。周囲の様子を見て、最初の感想をぶっきらぼうに一言つぶやいてください。"
239
- bot_message = generate_dialogue(history, message, new_affection, stage_name, final_scene_params, api_limiter, instruction)
240
  else:
241
- bot_message = generate_dialogue(history, message, new_affection, stage_name, final_scene_params, api_limiter)
 
 
 
 
242
 
243
  new_history = history + [(message, bot_message)]
244
  chat_history.append((message, bot_message))
@@ -246,26 +244,30 @@ def respond(message, chat_history, affection, history, scene_params, api_limiter
246
  theme_url = THEME_URLS.get(final_scene_params.get("theme"), THEME_URLS["default"])
247
  background_html = f'<div class="background-container" style="background-image: url({theme_url});"></div>'
248
 
249
- return "", chat_history, new_affection, stage_name, new_affection, new_history, final_scene_params, api_limiter, background_html
250
 
251
  except Exception as e:
252
  logger.critical(f"respond関数で予期せぬ致命的なエラーが発生: {e}", exc_info=True)
253
  bot_message = "(ごめん、システムに予期せぬ問題が起きたみたいだ。ページを再読み込みしてくれるか…?)"
254
  chat_history.append((message, bot_message))
255
- api_limiter.is_blocked = True
256
- return "", chat_history, affection, get_relationship_stage(affection), affection, history, scene_params, api_limiter, gr.update()
257
 
258
 
259
  # --- 7. Gradio UIの構築 ---
260
- with open("style.css", "r", encoding="utf-8") as f:
261
- custom_css = f.read()
 
 
 
 
262
 
263
  with gr.Blocks(css=custom_css, theme=gr.themes.Soft(primary_hue="rose", secondary_hue="pink"), title="麻理チャット") as demo:
264
  # 内部状態管理用
265
  scene_state = gr.State({"theme": "default"})
266
  affection_state = gr.State(30)
267
  history_state = gr.State([])
268
- api_limiter_state = gr.State(APILimiter(max_requests=RATE_LIMIT_MAX_REQUESTS, in_seconds=RATE_LIMIT_IN_SECONDS))
269
 
270
  background_display = gr.HTML(f'<div class="background-container" style="background-image: url({THEME_URLS["default"]});"></div>')
271
 
@@ -283,8 +285,8 @@ with gr.Blocks(css=custom_css, theme=gr.themes.Soft(primary_hue="rose", secondar
283
  affection_gauge = gr.Slider(minimum=0, maximum=100, label="麻理の好感度", value=30, interactive=False)
284
  gr.Markdown("""<div class='footer'>Background Images & Icons: <a href="https://pixabay.com" target="_blank">Pixabay</a></div>""", elem_classes="footer")
285
 
286
- outputs = [msg_input, chatbot, affection_gauge, stage_display, affection_state, history_state, scene_state, api_limiter_state, background_display]
287
- inputs = [msg_input, chatbot, affection_state, history_state, scene_state, api_limiter_state]
288
 
289
  submit_btn.click(respond, inputs, outputs)
290
  msg_input.submit(respond, inputs, outputs)
@@ -295,4 +297,6 @@ with gr.Blocks(css=custom_css, theme=gr.themes.Soft(primary_hue="rose", secondar
295
 
296
 
297
  if __name__ == "__main__":
 
 
298
  demo.launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", 7860)))
 
12
  load_dotenv()
13
 
14
 
15
+ # --- 2. 安全機構(保険)の実装 (辞書と関数ベース) ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
  # グローバルな安全設定
18
  RATE_LIMIT_MAX_REQUESTS = 15
 
20
  MAX_INPUT_LENGTH = 1000
21
  MAX_HISTORY_TURNS = 100
22
 
23
+ def create_limiter_state():
24
+ """レートリミッターの初期状態(辞書)を作成する関数"""
25
+ return {
26
+ "timestamps": [],
27
+ "is_blocked": False
28
+ }
29
+
30
+ def check_limiter(limiter_state):
31
+ """
32
+ リミッターの状態をチェックし、呼び出し可否を判断して状態を更新する関数。
33
+ 呼び出しが許可されればTrueを、拒否されればFalseを返す。
34
+ """
35
+ if limiter_state["is_blocked"]:
36
+ logger.warning("API呼び出しは永続的にブロックされています。")
37
+ return False
38
+
39
+ now = time.time()
40
+ limiter_state["timestamps"] = [t for t in limiter_state["timestamps"] if now - t < RATE_LIMIT_IN_SECONDS]
41
+
42
+ if len(limiter_state["timestamps"]) >= RATE_LIMIT_MAX_REQUESTS:
43
+ logger.error(f"レートリミット超過! {RATE_LIMIT_IN_SECONDS}秒以内に{RATE_LIMIT_MAX_REQUESTS}回を超えました。このセッションのAPI呼び出しを永久にブロックします。")
44
+ limiter_state["is_blocked"] = True
45
+ return False
46
+
47
+ limiter_state["timestamps"].append(now)
48
+ return True
49
+
50
 
51
  # --- 3. APIクライアント初期化 ---
52
  try:
 
107
  logger.error(f"感情分析モデルのロードに失敗: {e}")
108
  return sentiment_analyzer
109
 
110
+ def call_llm(system_prompt, user_prompt, limiter_state, is_json_output=False):
111
  """Together AIを呼び出す共通関数。必ずリミッターを通過させる。"""
112
+ if not check_limiter(limiter_state):
113
  return None
114
 
115
  messages = [
 
128
  return chat_completion.choices[0].message.content
129
  except Exception as e:
130
  logger.error(f"Together AIのAPI呼び出し中に致命的なエラー: {e}", exc_info=True)
131
+ limiter_state["is_blocked"] = True
132
  logger.error("APIエラーのため、このセッションのAPI呼び出しをブロックします。")
133
  return None
134
 
135
+ def detect_scene_change(history, message, limiter_state):
136
  history_text = "\n".join([f"ユーザー: {u}\n麻理: {m}" for u, m in history[-3:]])
137
  available_keywords = ", ".join(THEME_URLS.keys())
138
  system_prompt = "あなたは会話分析のエキスパートです。ユーザーの提案とキャラクターの反応から、シーン(場所)が変更されるか判断し、指定されたキーワードでJSON形式で出力してください。"
 
146
  合意していない場合は {{"scene": "none"}} と出力してください。
147
  キーワード: {available_keywords}
148
  """
149
+ response_text = call_llm(system_prompt, user_prompt, limiter_state, is_json_output=True)
150
  if response_text:
151
  try:
152
  result = json.loads(response_text)
 
158
  logger.error(f"シーン検出のJSON解析に失敗")
159
  return None
160
 
161
+ def generate_dialogue(history, message, affection, stage_name, scene_params, limiter_state, instruction=None):
162
  history_text = "\n".join([f"ユーザー: {u}\n麻理: {m}" for u, m in history[-5:]])
163
  user_prompt = f"""
164
  # 現在の状況
 
172
  {f"【特別指示】{instruction}" if instruction else f"ユーザーの発言「{message}」に応答してください。"}
173
 
174
  麻理の応答:"""
175
+ response_text = call_llm(SYSTEM_PROMPT_MARI, user_prompt, limiter_state)
176
  return response_text if response_text else "(…うまく言葉が出てこない。少し時間を置いてほしい)"
177
 
178
  def get_relationship_stage(affection):
 
193
 
194
 
195
  # --- 6. Gradio応答関数 ---
196
+ def respond(message, chat_history, affection, history, scene_params, limiter_state):
197
  try:
198
  # 保険: ブロック状態、入力長、履歴長のチェック
199
+ if limiter_state["is_blocked"]:
200
  bot_message = "(…少し混乱している。時間をおいてから、ページを再読み込みして試してくれないか?)"
201
  chat_history.append((message, bot_message))
202
+ return "", chat_history, affection, get_relationship_stage(affection), affection, history, scene_params, limiter_state, gr.update()
203
 
204
  if not message.strip():
205
+ return "", chat_history, affection, get_relationship_stage(affection), affection, history, scene_params, limiter_state, gr.update()
206
 
207
  if len(message) > MAX_INPUT_LENGTH:
208
  logger.warning(f"入力長超過: {len(message)}文字")
209
  bot_message = f"(…長すぎる。{MAX_INPUT_LENGTH}文字以内で話してくれないか?)"
210
  chat_history.append((message, bot_message))
211
+ return "", chat_history, affection, get_relationship_stage(affection), affection, history, scene_params, limiter_state, gr.update()
212
 
213
  if len(history) > MAX_HISTORY_TURNS:
214
  logger.error("会話履歴が長すぎます。システム保護のため、会話をリセットします。")
 
216
  chat_history = []
217
  bot_message = "(…ごめん、少し話が長くなりすぎた。最初からやり直そう)"
218
  chat_history.append((message, bot_message))
219
+ return "", chat_history, affection, get_relationship_stage(affection), affection, history, scene_params, limiter_state, gr.update()
220
 
221
  # 通常処理
222
  new_affection = update_affection(message, affection)
 
224
  final_scene_params = scene_params.copy()
225
 
226
  bot_message = ""
227
+ new_scene_name = detect_scene_change(history, message, limiter_state)
228
 
229
  if new_scene_name and new_scene_name != final_scene_params.get("theme"):
230
  logger.info(f"シーンチェンジ実行: {final_scene_params.get('theme')} -> {new_scene_name}")
231
  final_scene_params["theme"] = new_scene_name
232
  instruction = f"ユーザーと一緒に「{new_scene_name}」に来た。周囲の様子を見て、最初の感想をぶっきらぼうに一言つぶやいてください。"
233
+ bot_message = generate_dialogue(history, message, new_affection, stage_name, final_scene_params, limiter_state, instruction)
234
  else:
235
+ bot_message = generate_dialogue(history, message, new_affection, stage_name, final_scene_params, limiter_state)
236
+
237
+ # bot_messageがNoneや空の場合のフォールバック
238
+ if not bot_message:
239
+ bot_message = "(…うまく言葉にできない)"
240
 
241
  new_history = history + [(message, bot_message)]
242
  chat_history.append((message, bot_message))
 
244
  theme_url = THEME_URLS.get(final_scene_params.get("theme"), THEME_URLS["default"])
245
  background_html = f'<div class="background-container" style="background-image: url({theme_url});"></div>'
246
 
247
+ return "", chat_history, new_affection, stage_name, new_affection, new_history, final_scene_params, limiter_state, background_html
248
 
249
  except Exception as e:
250
  logger.critical(f"respond関数で予期せぬ致命的なエラーが発生: {e}", exc_info=True)
251
  bot_message = "(ごめん、システムに予期せぬ問題が起きたみたいだ。ページを再読み込みしてくれるか…?)"
252
  chat_history.append((message, bot_message))
253
+ limiter_state["is_blocked"] = True
254
+ return "", chat_history, affection, get_relationship_stage(affection), affection, history, scene_params, limiter_state, gr.update()
255
 
256
 
257
  # --- 7. Gradio UIの構築 ---
258
+ try:
259
+ with open("style.css", "r", encoding="utf-8") as f:
260
+ custom_css = f.read()
261
+ except FileNotFoundError:
262
+ logger.warning("style.cssが見つかりません。デフォルトスタイルで起動します。")
263
+ custom_css = ""
264
 
265
  with gr.Blocks(css=custom_css, theme=gr.themes.Soft(primary_hue="rose", secondary_hue="pink"), title="麻理チャット") as demo:
266
  # 内部状態管理用
267
  scene_state = gr.State({"theme": "default"})
268
  affection_state = gr.State(30)
269
  history_state = gr.State([])
270
+ limiter_state = gr.State(create_limiter_state())
271
 
272
  background_display = gr.HTML(f'<div class="background-container" style="background-image: url({THEME_URLS["default"]});"></div>')
273
 
 
285
  affection_gauge = gr.Slider(minimum=0, maximum=100, label="麻理の好感度", value=30, interactive=False)
286
  gr.Markdown("""<div class='footer'>Background Images & Icons: <a href="https://pixabay.com" target="_blank">Pixabay</a></div>""", elem_classes="footer")
287
 
288
+ outputs = [msg_input, chatbot, affection_gauge, stage_display, affection_state, history_state, scene_state, limiter_state, background_display]
289
+ inputs = [msg_input, chatbot, affection_state, history_state, scene_state, limiter_state]
290
 
291
  submit_btn.click(respond, inputs, outputs)
292
  msg_input.submit(respond, inputs, outputs)
 
297
 
298
 
299
  if __name__ == "__main__":
300
+ # 感情分析モデルのプレロード(任意)
301
+ get_sentiment_analyzer()
302
  demo.launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", 7860)))