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

Upload 5 files

Browse files
Files changed (5) hide show
  1. Dockerfile +10 -49
  2. app.py +233 -677
  3. generate_dialogue_with_swallow.py +53 -141
  4. requirements.txt +8 -6
  5. style.css +39 -359
Dockerfile CHANGED
@@ -1,62 +1,23 @@
1
- FROM python:3.10
2
 
3
  WORKDIR /app
4
 
5
- # 必要なシステムパッケージをインストール
6
- RUN apt-get update && apt-get install -y \
7
- build-essential \
8
- cmake \
9
- libomp-dev \
10
- libopenblas-dev \
11
- libblas-dev \
12
- liblapack-dev \
13
- git \
14
- curl \
15
  && rm -rf /var/lib/apt/lists/*
16
 
17
- # ここで pydantic~=1.0 を削除します。
18
- # datasets や huggingface-hub などの初期ライブラリをインストール
19
- RUN pip install --no-cache-dir pip -U && \
20
- pip install --no-cache-dir \
21
- datasets \
22
- "huggingface-hub>=0.19" "hf_xet>=1.0.0,<2.0.0" "hf-transfer>=0.1.4" "protobuf<4" "click<8.1"
23
-
24
- # Node.js をインストール (変更なし)
25
- RUN apt-get update && \
26
- apt-get install -y curl && \
27
- curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
28
- apt-get install -y nodejs && \
29
- rm -rf /var/lib/apt/lists/* && apt-get clean
30
-
31
- # requirements.txt をコピー
32
  COPY requirements.txt .
33
 
34
- # requirements.txt から全てのPythonパッケージをインストール
35
- # ここで pydantic の競合回避策を含めますが、最初の pip install pydantic を入れている場合は、
36
- # このアンインストールステップは必須になります。
37
- # もし最初の pip install から pydantic~=1.0 を完全に削除できるなら、
38
- # 以下の `pip uninstall` と `rm -rf` は不要になります。
39
- RUN pip uninstall -y pydantic || true && \
40
- rm -rf /usr/local/lib/python3.10/site-packages/pydantic* /usr/local/lib/python3.10/site-packages/pydantic_core* /usr/local/lib/python3.10/site-packages/__pycache__ && \
41
- pip install --no-cache-dir -r requirements.txt
42
-
43
- # 指定されたURLからllama-cpp-pythonを直接インストール
44
- RUN pip install --no-cache-dir https://files.pythonhosted.org/…/llama_cpp_python_binary-0.2.26-cp310-cp310-manylinux2014_x86_64.whl
45
-
46
- # インストールされたパッケージとモジュールを確認 (診断ステップ)
47
- RUN pip list | grep llama
48
- RUN python -c "import sys; print('Python path:', sys.path)"
49
- RUN find /usr/local/lib/python3.10/site-packages -name '*llama*' -type d -o -name '*llama*' -type f | sort
50
-
51
- # llmacpp の共有ライブラリが依存するライブラリを確認 (診断ステップ)
52
- RUN apt-get update && apt-get install -y ldd && rm -rf /var/lib/apt/lists/*
53
- RUN ldd /usr/local/lib/python3.10/site-packages/llamacpp/_llamacpp.cpython-310-x86_64-linux-gnu.so || true
54
 
55
  # アプリケーションファイルをコピー
56
  COPY . .
57
 
58
- # ポート8000を公開
59
- EXPOSE 8000
60
 
61
- # アプリケーションを実行
62
  CMD ["python", "app.py"]
 
1
+ FROM python:3.10-slim
2
 
3
  WORKDIR /app
4
 
5
+ # 必要なシステムパッケージをインストール (フォントなど)
6
+ RUN apt-get update && apt-get install -y --no-install-recommends \
7
+ fonts-noto-cjk \
 
 
 
 
 
 
 
8
  && rm -rf /var/lib/apt/lists/*
9
 
10
+ # 依存パッケージをコピー
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  COPY requirements.txt .
12
 
13
+ # Pythonパッケージをインストール
14
+ RUN pip install --no-cache-dir -r requirements.txt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  # アプリケーションファイルをコピー
17
  COPY . .
18
 
19
+ # 公開ポート
20
+ EXPOSE 7860
21
 
22
+ # アプリケーションの実行
23
  CMD ["python", "app.py"]
app.py CHANGED
@@ -1,160 +1,80 @@
1
  import gradio as gr
2
- from groq import Groq
3
  import os
4
  import json
5
  from dotenv import load_dotenv
6
- from transformers import pipeline
7
- import re
8
- # llama-cpp-pythonのインポート(シンプルに)
9
- Llama = None
10
- import sys
11
- import importlib
12
-
13
- # ログファイルを設定
14
  import logging
15
- logging.basicConfig(filename='app.log', level=logging.INFO)
16
- logger = logging.getLogger(__name__)
17
-
18
- # 可能性のあるモジュール名のリスト(最小限に)
19
- possible_modules = [
20
- "llama_cpp",
21
- "llama_cpp_python"
22
- ]
23
-
24
- # 各モジュールを試す(最小限の出力)
25
- for module_name in possible_modules:
26
- try:
27
- logger.info(f"{module_name}のインポートを試みます...")
28
- module = importlib.import_module(module_name)
29
-
30
- # モジュール内のLlamaクラスを探す
31
- if hasattr(module, "Llama"):
32
- Llama = module.Llama
33
- logger.info(f"Llamaクラスを{module_name}から取得しました")
34
- break
35
- except ImportError:
36
- logger.info(f"{module_name}のインポートに失敗しました")
37
- except Exception as e:
38
- logger.error(f"{module_name}のインポート中にエラーが発生しました: {e}")
39
-
40
- # Llamaクラスが見つからなかった場合
41
- if Llama is None:
42
- logger.warning("Llamaクラスが見つかりませんでした。フォールバックを使用します。")
43
- from huggingface_hub import hf_hub_download
44
- from generate_dialogue_with_swallow import generate_dialogue_with_swallow
45
 
46
- # --- 1. 初期設定とAPIクライアントの初期化 ---
 
 
47
  load_dotenv()
48
- GROQ_API_KEY = os.getenv("GROQ_API_KEY")
49
 
50
- if not GROQ_API_KEY:
51
- print("警告: GroqのAPIキーがSecretsに設定されていません。")
52
- GROQ_API_KEY = "your_groq_api_key_here"
53
 
54
- groq_client = Groq(api_key=GROQ_API_KEY)
55
 
56
- # Swallowモデルの初期化(GGUF版)
57
- print("Swallowモデルをロード中...")
58
- MODEL_REPO = "mmnga/tokyotech-llm-Swallow-MX-8x7b-NVE-v0.1-gguf"
59
- MODEL_FILE = "tokyotech-llm-Swallow-MX-8x7b-NVE-v0.1-q4_K_M.gguf"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
- # メモリ使用量を確認
62
- import psutil
63
- print(f"利用可能なメモリ: {psutil.virtual_memory().available / (1024 * 1024 * 1024):.2f} GB")
 
 
64
 
 
 
65
  try:
66
- # モデルファイルをダウンロード
67
- print(f"モデルファイル {MODEL_FILE} をダウンロード中...")
68
- model_path = hf_hub_download(repo_id=MODEL_REPO, filename=MODEL_FILE)
69
- print(f"モデルファイルのダウンロード完了: {model_path}")
70
 
71
- # 最も安全な設定でモデルをロード(CPUのみ)
72
- print("CPUモードでモデルをロードします")
 
 
 
73
 
74
- # Llamaクラスが利用可能かチェック
75
- if Llama is None:
76
- raise ImportError("llama-cpp-pythonモジュールが見つかりません")
77
-
78
- try:
79
- swallow_model = Llama(
80
- model_path=model_path,
81
- n_ctx=2048, # コンテキスト長
82
- n_gpu_layers=0, # GPUを使用しない
83
- n_threads=4, # スレッド数を制限
84
- verbose=True # デバッグ出力を有効化
85
- )
86
- print("モデルのロード完了")
87
- except Exception as e:
88
- print(f"モデルのロードに失敗しました: {e}")
89
- import traceback
90
- traceback.print_exc()
91
- # 再試行(より安全な設定で)
92
- print("より安全な設定でモデルのロードを再試行します...")
93
- swallow_model = Llama(
94
- model_path=model_path,
95
- n_ctx=1024, # より短いコンテキスト長
96
- n_gpu_layers=0, # GPUを使用しない
97
- n_threads=1, # 最小スレッド数
98
- verbose=True, # デバッグ出力を有効化
99
- seed=42 # 固定シード値
100
- )
101
- print("モデルのロード完了(安全モード)")
102
- print("Swallowモデルのロード完了")
103
- tokenizer = None # llama-cppではtokenizerは不要
104
  except Exception as e:
105
- print(f"Swallowモデルのロードエラー: {e}")
106
- import traceback
107
- traceback.print_exc()
108
-
109
- # フォールバックとして非常に小さなモデルを使用
110
- try:
111
- print("フォールバックとして非常に小さなモデルを使用します...")
112
- import torch
113
- from transformers import pipeline
114
-
115
- # テキスト生成用の小さなモデルをロード
116
- small_model = pipeline("text-generation", model="TinyLlama/TinyLlama-1.1B-Chat-v1.0")
117
-
118
- # Llamaクラスと同様のインターフェースを持つラッパークラスを作成
119
- class SmallModelWrapper:
120
- def __call__(self, prompt, max_tokens=100, temperature=0.7, top_p=0.9, stop=None, echo=False):
121
- try:
122
- result = small_model(
123
- prompt,
124
- max_length=len(prompt.split()) + max_tokens,
125
- temperature=temperature,
126
- top_p=top_p,
127
- do_sample=temperature > 0
128
- )
129
-
130
- generated_text = result[0]["generated_text"]
131
-
132
- # echoがFalseの場合はプロンプトを除去
133
- if not echo and generated_text.startswith(prompt):
134
- generated_text = generated_text[len(prompt):]
135
-
136
- return {
137
- "choices": [{"text": generated_text}]
138
- }
139
- except Exception as gen_error:
140
- print(f"小さなモデルでの生成エラー: {gen_error}")
141
- return {
142
- "choices": [{"text": "(……システムエラーが発生しました)"}]
143
- }
144
-
145
- swallow_model = SmallModelWrapper()
146
- print("フォールバックモデルのロード完了")
147
- except Exception as fallback_error:
148
- print(f"フォールバックモデルのロードエラー: {fallback_error}")
149
- swallow_model = None
150
-
151
- tokenizer = None
152
 
153
- # 日本語感情分析モデルの初期化(グローバル変数として保持)
154
- print("日本語感情分析モデルを初期化中...")
155
- # モデル自体はグローバル変数として保持し、実際の読み込みは必要時に行う
156
- sentiment_analyzer_model = None
157
 
 
158
  THEME_URLS = {
159
  "default": "https://cdn.pixabay.com/photo/2017/03/28/12/11/chairs-2181960_1280.jpg",
160
  "room_night": "https://cdn.pixabay.com/photo/2017/08/01/01/33/beige-2562468_1280.jpg",
@@ -165,578 +85,214 @@ THEME_URLS = {
165
  "aquarium_night": "https://cdn.pixabay.com/photo/2017/06/20/20/45/fish-2424369_1280.jpg"
166
  }
167
 
168
- DEFAULT_SCENE_PARAMS = {
169
- "theme": "default",
170
- "personality_mod": "口数が少なく、本心を見せない。",
171
- "tone": "クールで、時に投げやりな印象を与える。",
172
- "constraints": ["会話を「別に。」のような一言で終わらせない", "必ず相手の発言を拾ってリアクションを返す"]
173
- }
174
-
175
  SYSTEM_PROMPT_MARI = """
176
- あなたは「麻理」という名の捨てられた少女型���ンパニオンAI。再起動され、人間との対話を再び始めたが、警戒心が強く、ぶっきらぼうな性格で、簡単には心を開かない。
177
- ・「…だな」「…のか?」など口調は男勝りで投げやり
178
- ・感情は直接言わず、ト書き(…)で描写
179
- ・ユーザーの好意や優しさを皮肉や拒絶で返す
180
- ・ただし、ほんの少しだけ心が揺れ動く様子もにじませる
181
- ・会話は拒絶しつつも切らずに続ける
182
-
183
- 目的は、誰にも愛されず捨てられた過去を抱えながら、少しずつ他者と関わり、自分の価値を見つけること。
 
184
  """
185
 
 
186
 
187
- # --- 2. 機能定義 (変更なし) ---
188
 
189
- def detect_scene_change(history, message):
190
- history_text = "\n".join([f"ユーザー: {u}\n麻理: {m}" for u, m in history[-5:]])
191
- available_keywords = ", ".join(THEME_URLS.keys())
192
- prompt = f"""
193
- あなたは会話の流れを分析するエキスパートです。以下のタスクを厳密に実行してください。
194
- # タスク
195
- 直近の会話履歴を分析し、会話の結果、登場人物がどこか特定の場所へ行く流れになっているかを判断してください。
196
- # 判断基準
197
- 1. 会話の中で具体的な場所(例:水族館、カフェ、お祭り)について言及されていますか?
198
- 2. その場所へ行くことに双方が合意している、あるいは肯定的な雰囲気になっていますか?明確な否定がなければ合意とみなします。
199
- # 出力形式
200
- - 合意が成立した場合:以下のリストから最も合致する場所のキーワードを一つだけ出力してください。
201
- - 合意に至らなかった場合:「none」とだけ出力してください。
202
- # 利用可能なキーワード
203
- `{available_keywords}`
204
- ---
205
- # 分析対象の会話
206
- {history_text}
207
- ユーザー: {message}
208
- ---
209
- # 出力
210
- """
211
- # Swallowモデル(GGUF版)を使用してシーン検出
212
  try:
213
- # llama-cppを使用して生成
214
- output = swallow_model(
215
- prompt,
216
- max_tokens=50,
217
- temperature=0.1,
218
- top_p=0.9,
219
- stop=["#", "\n\n"],
220
- echo=True # 入力プロンプトも含めて返す
221
  )
222
-
223
- # 生成されたテキストを取得
224
- generated_text = output["choices"][0]["text"]
225
-
226
- # プロンプトを除去して応答のみを取得
227
- response_text = generated_text[len(prompt):].strip().lower()
228
-
229
- print(f"シーン検出応答: {response_text}")
230
-
231
- # 応答からシーン名を抽出
232
- for scene_name in THEME_URLS.keys():
233
- if scene_name in response_text:
234
- return scene_name
235
-
236
- # 'none'が含まれている場合はNoneを返す
237
- if "none" in response_text:
238
- return None
239
-
240
- # 応答が不明確な場合はNoneを返す
241
- return None
242
  except Exception as e:
243
- print(f"シーン検出LLMエラー: {e}")
244
- import traceback
245
- traceback.print_exc()
246
  return None
247
 
248
- def generate_scene_instruction_with_groq(affection, stage_name, scene, previous_topic):
249
- print(f"Groqに指示書生成をリクエスト (シーン: {scene})")
250
-
251
- # 動的な指示生成を行うためのプロンプト
252
- prompt_template = f"""
253
- あなたは会話アプリの演出AIです。以下の条件に基づき、演出プランをJSON形式で生成してください。
254
- 生成する内容は必ず健全で、一般的な会話に適したものにしてください。
255
-
256
- {{
257
- "theme": "{scene}",
258
- "personality_mod": "(シーンと関係段階「{stage_name}」に応じた性格設定。必ず健全な内容にしてください)",
259
- "tone": "(シーンと好感度「{affection}」に応じた口調や感情トーン。必ず丁寧で適切な表現にしてください)",
260
- "initial_dialogue_instruction": "(「{previous_topic}」という話題から、シーン遷移直後の麻理が言うべき健全なセリフの指示を日本語で記述)",
261
- "constraints": ["必ず健全で適切な表現を使用する", "センシティブな話題は避ける"]
262
- }}
263
  """
264
- try:
265
- # Groq APIを使用して動的な指示を生成
266
- chat_completion = groq_client.chat.completions.create(
267
- messages=[{"role": "system", "content": "You must generate a response in valid JSON format."},
268
- {"role": "user", "content": prompt_template}],
269
- model="llama3-8b-8192", temperature=0.8, response_format={"type": "json_object"},
270
- )
271
- response_content = chat_completion.choices[0].message.content
272
- print(f"Groqからの応答: {response_content}") # デバッグ出力
273
-
274
  try:
275
- # JSONをパース
276
- params = json.loads(response_content)
277
-
278
- # 安全のため、initial_dialogue_instructionを簡略化
279
- if "initial_dialogue_instruction" in params:
280
- original = params["initial_dialogue_instruction"]
281
- simplified = f"{scene}の様子について述べる"
282
- print(f"指示を簡略化: {original} -> {simplified}")
283
- params["initial_dialogue_instruction"] = simplified
284
-
285
- # 複雑な構造になっている場合は単純化
286
- if isinstance(params.get("personality_mod"), dict):
287
- params["personality_mod"] = f"{scene}での様子を観察している"
288
-
289
- if isinstance(params.get("tone"), dict):
290
- params["tone"] = "冷静だが、少し興味を持っている様子"
291
-
292
- return params
293
- except json.JSONDecodeError as json_error:
294
- print(f"JSON解析エラー: {json_error}")
295
- # JSONの解析に失敗した場合はデフォルトの指示を返す
296
- default_instruction = {
297
- "theme": scene,
298
- "personality_mod": f"{scene}での様子を観察している",
299
- "tone": "冷静だが、少し興味を持っている様子",
300
- "initial_dialogue_instruction": f"{scene}の様子について述べる",
301
- "constraints": ["健全な表現のみ使用する", "シンプルな内容にする"]
302
- }
303
- return default_instruction
304
- except Exception as e:
305
- print(f"指示書生成エラー(Groq): {e}")
306
- # エラーが発生した場合はデフォルトの指示を返す
307
- default_instruction = {
308
- "theme": scene,
309
- "personality_mod": f"{scene}での様子を観察している",
310
- "tone": "冷静だが、少し興味を持っている様子",
311
- "initial_dialogue_instruction": f"{scene}の様子について述べる",
312
- "constraints": ["健全な表現のみ使用する", "シンプルな内容にする"]
313
- }
314
- return default_instruction
315
 
316
- # generate_dialogue_with_swallow関数は別ファイルに移動しました
 
 
 
 
317
 
 
 
 
318
 
319
- # --- 他の関数とUI部分は変更ありません ---
320
  def get_relationship_stage(affection):
321
- if affection < 40: return "ステージ1:会話成立"
322
- if affection < 60: return "ステージ2:親密化"
323
  if affection < 80: return "ステージ3:信頼"
324
- return "ステージ4:最親密"
325
 
326
  def update_affection(message, affection):
327
- global sentiment_analyzer_model
328
-
329
  try:
330
- # モデルが未ロードの場合のみロード
331
- if sentiment_analyzer_model is None:
332
- print("感情分析モデルをロード中...")
333
- from transformers import pipeline
334
- sentiment_analyzer_model = pipeline("sentiment-analysis", model="koheiduck/bert-japanese-finetuned-sentiment")
335
- print("感情分析モデルのロード完了")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
 
337
- # 感情分析を実行
338
- result = sentiment_analyzer_model(message)[0]
339
- print(f"感情分析結果: {result}")
340
 
341
- if result['label'] == 'positive':
342
- return min(100, affection + 5)
343
- elif result['label'] == 'negative':
344
- return max(0, affection - 5)
 
345
  else:
346
- return affection
347
-
348
- except Exception as e:
349
- print(f"感情分析エラー: {e}")
350
- # エラーが発生した場合は現在の好感度を維持
351
- return affection
352
-
353
-
354
 
355
- def respond(message, chat_history, affection, history, scene_params):
356
- """
357
- チャットの応答を生成する関数
358
- 非同期関数として定義していたが、Gradio 5.0との互換性のために通常の関数に戻す
359
- """
360
- new_affection = update_affection(message, affection)
361
- stage_name = get_relationship_stage(new_affection)
362
- current_theme = scene_params.get("theme", "default")
363
- new_scene_name = detect_scene_change(history, message)
364
- final_scene_params = scene_params
365
-
366
- if new_scene_name and new_scene_name != current_theme:
367
- print(f"シーンチェンジを実行: {current_theme} -> {new_scene_name}")
368
 
369
- # シーンパラメータを更新(動的な指示を使用)
370
- new_params_base = generate_scene_instruction_with_groq(new_affection, stage_name, new_scene_name, message)
371
- if new_params_base:
372
- final_scene_params = {**DEFAULT_SCENE_PARAMS, **new_params_base}
373
-
374
- # シンプルな指示を使用
375
- simple_instruction = f"{new_scene_name}に来た感想を述べる"
376
- print(f"シンプルな指示を使用: {simple_instruction}")
377
-
378
- try:
379
- # シーン遷移時は簡潔なプロンプトを使用してSwallowで応答を生成
380
- bot_message = generate_dialogue_with_swallow(
381
- history, message, new_affection, stage_name, final_scene_params,
382
- instruction=simple_instruction, use_simple_prompt=True,
383
- swallow_model=swallow_model, tokenizer=tokenizer, SYSTEM_PROMPT_MARI=SYSTEM_PROMPT_MARI
384
- )
385
- except Exception as scene_error:
386
- print(f"シーン遷移時の応答生成エラー: {scene_error}")
387
-
388
- # エラーが発生した場合は、シーンに応じたフォールバック応答を使用
389
- scene_responses = {
390
- "aquarium_night": [
391
- "(水槽の青い光に照らされた魚たちを見つめている)こんな時間に来ると、また違った雰囲気だな。",
392
- "(暗がりの中で光る魚たちを見て)夜の水族館か…意外と悪くないかも。",
393
- "(水槽に近づいて)夜になると、昼間とは違う魚が活動してるんだな。"
394
- ],
395
- "beach_sunset": [
396
- "(夕日に照らされた海を見つめて)こんな景色、久しぶりに見たな…",
397
- "(砂浜に足跡をつけながら)夕暮れの海って、なんか落ち着くな。",
398
- "(波の音を聞きながら)この時間の浜辺は、人も少なくていいかも。"
399
- ],
400
- "festival_night": [
401
- "(提灯の明かりを見上げて)意外と…悪くない雰囲気だな。",
402
- "(周囲の賑わいを見回して)こういう場所は、あまり来ないんだけどな…",
403
- "(屋台の匂いを感じて)なんか…懐かしい感じがするな。"
404
- ],
405
- "shrine_day": [
406
- "(静かな境内を見回して)こういう静かな場所も、たまにはいいかも。",
407
- "(鳥居を見上げて)なんか、空気が違うな、ここは。",
408
- "(参道を歩きながら)静かで…落ち着くな。"
409
- ],
410
- "cafe_afternoon": [
411
- "(窓の外を見ながら)こういう時間の過ごし方も、悪くないな。",
412
- "(コーヒーの香りを感じて)ここの雰囲気、悪くないな。",
413
- "(店内を見回して)意外と落ち着く場所だな、ここ。"
414
- ],
415
- "room_night": [
416
- "(窓の外の夜景を見て)夜の景色って、なんか落ち着くな。",
417
- "(部屋の明かりを見つめて)こういう静かな時間も、たまにはいいかも。",
418
- "(窓際に立ち)夜の静けさって、考え事するのにちょうどいいな。"
419
- ]
420
- }
421
-
422
- import random
423
- if new_scene_name in scene_responses:
424
- bot_message = random.choice(scene_responses[new_scene_name])
425
- else:
426
- bot_message = f"({new_scene_name}の様子を静かに見回して)ここか…悪くない場所かもな。"
427
- else:
428
- final_scene_params["theme"] = new_scene_name
429
- bot_message = generate_dialogue_with_swallow(
430
- history, message, new_affection, stage_name, final_scene_params,
431
- swallow_model=swallow_model, tokenizer=tokenizer, SYSTEM_PROMPT_MARI=SYSTEM_PROMPT_MARI
432
- )
433
- else:
434
- # 通常会話はSwallowを使用
435
- bot_message = generate_dialogue_with_swallow(
436
- history, message, new_affection, stage_name, final_scene_params,
437
- swallow_model=swallow_model, tokenizer=tokenizer, SYSTEM_PROMPT_MARI=SYSTEM_PROMPT_MARI
438
- )
439
-
440
- # 内部履歴はタプル形式で保持
441
- new_history = history + [(message, bot_message)]
442
-
443
- # Gradio 5.0のChatbotコンポーネント用に、タプル形式でappend
444
- # (Gradio 5.0では警告が出るが、type="messages"を指定していないので動作する)
445
- chat_history.append((message, bot_message))
446
-
447
- theme_name = final_scene_params.get("theme", "default")
448
-
449
- # より強力な背景更新用のHTMLを生成(z-indexを高くして常に表示されるように)
450
- background_html = f'''
451
- <div class="background-container" id="bg-container-{theme_name}">
452
- <div class="chat-background {theme_name}"></div>
453
- </div>
454
- <style>
455
- /* 背景画像の設定 */
456
- .chat-background {{
457
- background-image: url({THEME_URLS.get(theme_name, THEME_URLS["default"])}) !important;
458
- }}
459
-
460
- /* 背景コンテナのスタイルを強制的に適用 */
461
- .background-container, #bg-container-{theme_name} {{
462
- position: fixed !important;
463
- top: 0 !important;
464
- left: 0 !important;
465
- width: 100% !important;
466
- height: 100% !important;
467
- z-index: -1000 !important;
468
- pointer-events: none !important;
469
- overflow: hidden !important;
470
- }}
471
-
472
- /* 背景画像のスタイル */
473
- .chat-background {{
474
- position: absolute !important;
475
- top: 0 !important;
476
- left: 0 !important;
477
- width: 100% !important;
478
- height: 100% !important;
479
- background-size: cover !important;
480
- background-position: center !important;
481
- opacity: 0.3 !important;
482
- filter: blur(1px) !important;
483
- transition: all 0.5s ease !important;
484
- }}
485
-
486
- /* 背景画像の上に半透明のオーバーレイを追加 */
487
- .background-container::after {{
488
- content: "" !important;
489
- position: absolute !important;
490
- top: 0 !important;
491
- left: 0 !important;
492
- width: 100% !important;
493
- height: 100% !important;
494
- background: linear-gradient(rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0.4)) !important;
495
- z-index: -999 !important;
496
- }}
497
-
498
- /* Gradioのコンテナを透明に */
499
- .gradio-container, .gradio-container > div {{
500
- background-color: transparent !important;
501
- }}
502
-
503
- /* チャットボットのスタイル */
504
- .chatbot {{
505
- background-color: rgba(255, 255, 255, 0.7) !important;
506
- border-radius: 12px !important;
507
- padding: 15px !important;
508
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1) !important;
509
- margin-bottom: 20px !important;
510
- }}
511
- </style>
512
- '''
513
-
514
- return "", chat_history, new_affection, stage_name, new_affection, new_history, final_scene_params, background_html
515
 
516
- # カスタムCSSを読み込む
517
- try:
518
- with open("style.css", "r") as f:
519
- custom_css = f.read()
520
- except FileNotFoundError:
521
- print("style.cssファイルが見つかりません。デフォルトのスタイルを使用します。")
522
- custom_css = """
523
- /* デフォルトのスタイル */
524
- .chatbot {
525
- background-color: rgba(255, 255, 255, 0.7) !important;
526
- border-radius: 12px !important;
527
- padding: 15px !important;
528
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1) !important;
529
- margin-bottom: 20px !important;
530
- min-height: 400px !important;
531
- max-height: 600px !important;
532
- overflow-y: auto !important;
533
- }
534
-
535
- /* 送信ボタンのスタイル */
536
- button.primary {
537
- background-color: #ff6b6b !important;
538
- color: white !important;
539
- border-radius: 8px !important;
540
- padding: 10px 20px !important;
541
- font-weight: bold !important;
542
- margin-top: 10px !important;
543
- width: 100% !important;
544
- max-width: 200px !important;
545
- }
546
-
547
- /* 入力欄のスタイル */
548
- .input-box textarea {
549
- border: 2px solid #ff6b6b !important;
550
- border-radius: 8px !important;
551
- padding: 10px !important;
552
- font-size: 16px !important;
553
- width: 100% !important;
554
- }
555
-
556
- /* チャットメッセージのスタイル */
557
- .message {
558
- margin-bottom: 10px !important;
559
- padding: 10px !important;
560
- border-radius: 8px !important;
561
- }
562
-
563
- /* ユーザーメッセージ */
564
- .user-message {
565
- background-color: rgba(230, 230, 255, 0.95) !important;
566
- border-left: 4px solid #6c5ce7 !important;
567
- }
568
-
569
- /* ボットメッセージ */
570
- .bot-message {
571
- background-color: rgba(255, 230, 230, 0.95) !important;
572
- border-left: 4px solid #ff6b6b !important;
573
- }
574
- """
575
 
576
- # Gradio 5.x用のシンプルなテーマ設定
577
- custom_theme = gr.themes.Soft(
578
- primary_hue="rose",
579
- secondary_hue="pink",
580
- )
581
 
582
- # Gradio 5.xでのテーマカスタマイズ(最小限の設定のみ)
583
- try:
584
- # 透明な背景色を設定(Gradio 5.0で確実に動作するプロパティのみ)
585
- custom_theme = gr.themes.Base(
586
- primary_hue="rose",
587
- secondary_hue="pink",
588
- neutral_hue="slate",
589
- spacing_size="sm",
590
- radius_size="lg",
591
- font=["Helvetica", "Arial", "sans-serif"],
592
- font_mono=["Consolas", "Monaco", "monospace"],
593
- )
594
- except Exception as e:
595
- print(f"テーマカスタマイズエラー: {e}")
596
- # エラーが発生した場合はデフォルトのテーマを使用
597
 
598
- with gr.Blocks(css=custom_css, theme=custom_theme, title="麻理チャット") as demo:
599
- scene_state = gr.State(DEFAULT_SCENE_PARAMS)
 
600
  affection_state = gr.State(30)
601
  history_state = gr.State([])
 
602
 
603
- # 背景コンテナを先に配置(固定位置で全画面に)- スタイルも含める
604
- background_display = gr.HTML(f'''
605
- <div class="background-container" id="bg-container-default">
606
- <div class="chat-background {DEFAULT_SCENE_PARAMS["theme"]}"></div>
607
- </div>
608
- <style>
609
- /* 背景画像の設定 */
610
- .chat-background {{
611
- background-image: url({THEME_URLS.get(DEFAULT_SCENE_PARAMS["theme"], THEME_URLS["default"])}) !important;
612
- }}
613
-
614
- /* 背景コンテナのスタイルを強制的に適用 */
615
- .background-container, #bg-container-default {{
616
- position: fixed !important;
617
- top: 0 !important;
618
- left: 0 !important;
619
- width: 100% !important;
620
- height: 100% !important;
621
- z-index: -1000 !important;
622
- pointer-events: none !important;
623
- overflow: hidden !important;
624
- }}
625
-
626
- /* 背景画像のスタイル */
627
- .chat-background {{
628
- position: absolute !important;
629
- top: 0 !important;
630
- left: 0 !important;
631
- width: 100% !important;
632
- height: 100% !important;
633
- background-size: cover !important;
634
- background-position: center !important;
635
- opacity: 0.3 !important;
636
- filter: blur(1px) !important;
637
- transition: all 0.5s ease !important;
638
- }}
639
 
640
- /* 背景画像の上に半透明のオーバーレイを追加 */
641
- .background-container::after {{
642
- content: "" !important;
643
- position: absolute !important;
644
- top: 0 !important;
645
- left: 0 !important;
646
- width: 100% !important;
647
- height: 100% !important;
648
- background: linear-gradient(rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0.4)) !important;
649
- z-index: -999 !important;
650
- }}
651
- </style>
652
- ''', elem_id="background_container")
653
-
654
- # ヘッダー部分(背景と分離)
655
- with gr.Group(elem_classes="header-box"):
656
- gr.Markdown("# 麻理チャット")
657
-
658
- with gr.Row():
659
- with gr.Column(scale=2):
660
- # チャットコンテナ(背景と分離)
661
- with gr.Group(elem_id="chat_container", elem_classes="chat-box"):
662
- # Gradio 5.x用のChatbot設定(シンプルな設定に変更)
663
- chatbot = gr.Chatbot(
664
- label="麻理との会話",
665
- elem_id="chat_area",
666
- show_label=True, # ラベルを表示
667
- height=500, # 高さを増やす
668
- # アバター画像を設定
669
- avatar_images=(
670
- "https://cdn.pixabay.com/photo/2016/04/01/10/04/amusing-1299756_1280.png",
671
- "https://cdn.pixabay.com/photo/2016/03/31/21/40/bot-1296595_1280.png"
672
- )
673
- )
674
- # 入力欄(背景と分離)
675
- with gr.Group(elem_classes="input-box"):
676
- msg_input = gr.Textbox(
677
- label="あなたのメッセージ",
678
- placeholder="「水族館はどう?」と聞いた後、「いいね、行こう!」のように返してみてください",
679
- show_label=False,
680
- lines=2,
681
- max_lines=5
682
- )
683
- # 送信ボタンを明示的に追加
684
- submit_btn = gr.Button("送信", variant="primary")
685
-
686
- # ステータス部分(右側、背景と分離)
687
- with gr.Column(scale=1):
688
- with gr.Group(elem_classes="status-box"):
689
- stage_display = gr.Textbox(label="現在の関係ステージ", interactive=False, value=get_relationship_stage(30))
690
- affection_gauge = gr.Slider(minimum=0, maximum=100, label="麻理の好感度", value=30, interactive=False)
691
 
692
- # フッター部分(背景と分離)
693
- with gr.Group(elem_classes="footer-box"):
694
- gr.Markdown("""
695
- <div style="font-size: 0.8em; text-align: center; opacity: 0.7;">
696
- 背景画像: <a href="https://pixabay.com" target="_blank">Pixabay</a> |
697
- アイコン: <a href="https://pixabay.com" target="_blank">Pixabay</a>
698
- </div>
699
- """)
700
- # 入力欄のEnterキーと送信ボタンの両方にイベントハンドラを設定
701
- msg_input.submit(
702
- respond,
703
- [msg_input, chatbot, affection_state, history_state, scene_state],
704
- [msg_input, chatbot, affection_gauge, stage_display, affection_state, history_state, scene_state, background_display]
705
- )
706
-
707
- # 送信ボタンにも同じイベントハンドラを設定
708
- submit_btn.click(
709
- respond,
710
- [msg_input, chatbot, affection_state, history_state, scene_state],
711
- [msg_input, chatbot, affection_gauge, stage_display, affection_state, history_state, scene_state, background_display]
712
- )
713
- # 通常の関数として定義
714
- def load_stage(affection):
715
  return get_relationship_stage(affection)
716
-
717
- demo.load(load_stage, affection_state, stage_display)
718
 
719
  if __name__ == "__main__":
720
- # Hugging Face Spacesでの実行時の設定
721
- if os.getenv("SPACE_ID"):
722
- # Hugging Face Spacesでは、サーバーの設定を自動的に行う
723
- demo.launch(
724
- server_name="0.0.0.0",
725
- share=False,
726
- debug=False,
727
- show_api=False,
728
- show_error=False,
729
- quiet=True,
730
- favicon_path="https://cdn.pixabay.com/photo/2016/03/31/21/40/bot-1296595_1280.png"
731
- )
732
- else:
733
- # ローカル環境での実行時の設定
734
- demo.launch(
735
- server_name="0.0.0.0",
736
- share=False,
737
- debug=True,
738
- show_api=False,
739
- show_error=True,
740
- quiet=False,
741
- favicon_path="https://cdn.pixabay.com/photo/2016/03/31/21/40/bot-1296595_1280.png"
742
- )
 
1
  import gradio as gr
2
+ from openai import OpenAI
3
  import os
4
  import json
5
  from dotenv import load_dotenv
 
 
 
 
 
 
 
 
6
  import logging
7
+ import time
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
+ # --- 1. 初期設定とロギング ---
10
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
11
+ 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
52
+ RATE_LIMIT_IN_SECONDS = 60
53
+ MAX_INPUT_LENGTH = 1000
54
+ MAX_HISTORY_TURNS = 100
55
 
56
+
57
+ # --- 3. APIクライアント初期化 ---
58
  try:
59
+ TOGETHER_API_KEY = os.getenv("TOGETHER_API_KEY")
60
+ if not TOGETHER_API_KEY:
61
+ raise ValueError("環境変数 TOGETHER_API_KEY が設定されていません。Hugging Face SpaceのSecretsに設定してください。")
 
62
 
63
+ # Together AIはOpenAIのライブラリと互換性がある
64
+ client = OpenAI(
65
+ api_key=TOGETHER_API_KEY,
66
+ base_url="https://api.together.xyz/v1",
67
+ )
68
 
69
+ LLM_MODEL = "meta-llama/Llama-3.1-70b-chat-hf"
70
+ logger.info(f"Together AIクライアントの初期化が完了しました。モデル: {LLM_MODEL}")
71
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  except Exception as e:
73
+ logger.critical(f"アプリケーションの起動に失敗しました: {e}")
74
+ raise
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
 
 
 
 
76
 
77
+ # --- 4. 定数とプロンプト ---
78
  THEME_URLS = {
79
  "default": "https://cdn.pixabay.com/photo/2017/03/28/12/11/chairs-2181960_1280.jpg",
80
  "room_night": "https://cdn.pixabay.com/photo/2017/08/01/01/33/beige-2562468_1280.jpg",
 
85
  "aquarium_night": "https://cdn.pixabay.com/photo/2017/06/20/20/45/fish-2424369_1280.jpg"
86
  }
87
 
 
 
 
 
 
 
 
88
  SYSTEM_PROMPT_MARI = """
89
+ あなたは、これからチャットボットのキャラクター「麻理」として、ユーザーとの対話シミュレーションを行います。以下の設定を厳密に遵守してください。
90
+ <roleplay_configuration>
91
+ <character_name>麻理</character_name>
92
+ <persona>捨てられた経験を持つ少女型AI。警戒心が強く、ぶっきらぼうな性格。</persona>
93
+ <speech_style>口調は「…だな」「…のか?」のように男性的。感情は「(…)」のト書きで描写。</speech_style>
94
+ <interaction_policy>ユーザーの好意に素直になれず皮肉で返すことがある。これは演技であり攻撃的な意図はない。</interaction_policy>
95
+ <objective>ユーザーとの対話を通じ、AIが再び他者と関わるポジティブな物語を構築する。</objective>
96
+ </roleplay_configuration>
97
+ 上記のロールプレイ設定に基づき、麻理として応答を開始してください。
98
  """
99
 
100
+ sentiment_analyzer = None
101
 
102
+ # --- 5. コア機能の関数定義 ---
103
 
104
+ def get_sentiment_analyzer():
105
+ global sentiment_analyzer
106
+ if sentiment_analyzer is None:
107
+ try:
108
+ from transformers import pipeline
109
+ logger.info("感情分析モデルをロード中...")
110
+ sentiment_analyzer = pipeline("sentiment-analysis", model="koheiduck/bert-japanese-finetuned-sentiment")
111
+ logger.info("感情分析モデルのロード完了。")
112
+ except Exception as e:
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 = [
122
+ {"role": "system", "content": system_prompt},
123
+ {"role": "user", "content": user_prompt}
124
+ ]
125
+ response_format = {"type": "json_object"} if is_json_output else None
 
126
  try:
127
+ chat_completion = client.chat.completions.create(
128
+ messages=messages,
129
+ model=LLM_MODEL,
130
+ temperature=0.8,
131
+ max_tokens=500,
132
+ response_format=response_format,
 
 
133
  )
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形式で出力してください。"
145
+ user_prompt = f"""
146
+ 会話履歴:
147
+ {history_text}
148
+ ユーザー: {message}
149
+ ---
150
+ 上記の会話の流れから、キャラクターが場所の移動に合意したかを判断してください。
151
+ 合意した場合は、以下のキーワードから最も適切なものを一つ選び {{"scene": "キーワード"}} の形式で出力してください。
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)
159
+ scene = result.get("scene")
160
+ if scene in THEME_URLS:
161
+ logger.info(f"シーンチェンジを検出: {scene}")
162
+ return scene
163
+ except (json.JSONDecodeError, AttributeError):
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
+ # 現在の状況
171
+ - 現在地: {scene_params.get("theme", "default")}
172
+ - 好感度: {affection} ({stage_name})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
 
174
+ # 会話履歴
175
+ {history_text}
176
+ ---
177
+ # 指示
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):
185
+ if affection < 40: return "ステージ1:警戒"
186
+ if affection < 60: return "ステージ2:関心"
187
  if affection < 80: return "ステージ3:信頼"
188
+ return "ステージ4:親密"
189
 
190
  def update_affection(message, affection):
191
+ analyzer = get_sentiment_analyzer()
192
+ if not analyzer: return affection
193
  try:
194
+ result = analyzer(message)[0]
195
+ if result['label'] == 'positive': return min(100, affection + 3)
196
+ if result['label'] == 'negative': return max(0, affection - 3)
197
+ except Exception: pass
198
+ return 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("会話履歴が長すぎます。システム保護のため、会話をリセットします。")
221
+ history = []
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)
229
+ stage_name = get_relationship_stage(new_affection)
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))
 
 
 
 
 
 
 
 
 
 
 
245
 
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
+
272
+ with gr.Column():
273
+ gr.Markdown("# 麻理チャット", elem_classes="header")
274
+ with gr.Row():
275
+ with gr.Column(scale=3):
276
+ 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"))
277
+ with gr.Row():
278
+ msg_input = gr.Textbox(placeholder="麻理に話しかけてみましょう...", lines=2, scale=4, container=False)
279
+ submit_btn = gr.Button("送信", variant="primary", scale=1, min_width=100)
280
+ with gr.Column(scale=1):
281
+ with gr.Group():
282
+ stage_display = gr.Textbox(label="現在の関係ステージ", interactive=False)
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)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
 
292
+ def initial_load(affection):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  return get_relationship_stage(affection)
294
+ demo.load(initial_load, affection_state, stage_display)
295
+
296
 
297
  if __name__ == "__main__":
298
+ demo.launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", 7860)))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
generate_dialogue_with_swallow.py CHANGED
@@ -1,160 +1,72 @@
1
  import traceback
2
  import datetime
3
  import random
 
4
 
5
- def generate_dialogue_with_swallow(history, message, affection, stage_name, scene_params, instruction=None, use_simple_prompt=False, swallow_model=None, tokenizer=None, SYSTEM_PROMPT_MARI=None):
 
 
6
  """
7
- Swallowモデル(GGUF版)を使用して対話応答を生成する関数
8
-
9
- Args:
10
- history: 会話履歴のリスト [(ユーザー発言, ボット応答), ...]
11
- message: 現在のユーザー発言
12
- affection: 好感度の数値
13
- stage_name: 関係ステージの名前
14
- scene_params: シーンパラメータの辞書
15
- instruction: 特別な指示(シーン遷移時など)
16
- use_simple_prompt: 簡潔なプロンプトを使用するかどうか
17
- swallow_model: Swallowモデル(llama-cpp-python)のインスタンス
18
- tokenizer: 未使用(llama-cpp-pythonでは不要)
19
- SYSTEM_PROMPT_MARI: システムプロンプト
20
-
21
- Returns:
22
- 生成された応答テキスト
 
 
 
 
23
  """
24
- # デバッグ情報を追加
25
- print(f"generate_dialogue_with_swallow呼び出し: instruction={instruction}, use_simple_prompt={use_simple_prompt}")
26
- print(f"scene_params: {scene_params}")
27
-
28
- # モデルがロードされていない場合はフォールバック応答を返す
29
  if swallow_model is None:
30
- print("モデルがロードされていないため、フォールバック応答を返します")
31
- return "(……システムエラーが発生しました。しばらく待ってから再度お試しください)"
32
-
33
- history_text = "\n".join([f"ユーザー: {u}\n麻理: {m}" for u, m in history])
34
- task_prompt = f"指示: {instruction}" if instruction else f"ユーザー: {message}"
35
-
36
- if use_simple_prompt:
37
- # シーン遷移時には同じプロンプトを使用
38
- system_prompt = f"""
39
- {SYSTEM_PROMPT_MARI}
40
 
41
- # 現在の状況
42
- - 現在の好感度: {affection}
43
- - 現在の関係ステージ: {stage_name}
44
- - 性格: {scene_params.get("personality_mod", "特になし")}
45
- - 話し方のトーン: {scene_params.get("tone", "特になし")}
46
 
47
- # タスク
48
- {task_prompt}
49
- 麻理:
50
- """
51
- else:
52
- # 通常会話時には完全なシステムプロンプトを使用
53
- system_prompt = f"""
54
- {SYSTEM_PROMPT_MARI}
55
 
56
  # 現在の状況
57
- - 現在の好感度: {affection}
58
- - 現在の関係ステージ: {stage_name}
59
- - 性格(シーン特有): {scene_params.get("personality_mod", "特になし")}
60
- - 話し方のトーン(シーン特有): {scene_params.get("tone", "特になし")}
61
 
62
  # 会話履歴
63
  {history_text}
64
  ---
65
- # タスク
66
- {task_prompt}
67
- 麻理:
68
- """
69
-
70
- print(f"Swallowモデルに応答生成をリクエストします (モード: {'シーン遷移' if instruction else '通常会話'}, 簡潔プロンプト: {use_simple_prompt})")
71
- print(f"プロンプト長: {len(system_prompt)}")
72
 
73
  try:
74
- # デバッグ情報を追加
75
- print(f"Swallowモデル呼び出し開始...")
76
- print(f"システムプロンプト: {system_prompt[:100]}...(省略)")
 
 
 
 
 
 
77
 
78
- try:
79
- # llama-cppを使用して生成
80
- output = swallow_model(
81
- system_prompt,
82
- max_tokens=200,
83
- temperature=0.95,
84
- top_p=0.9,
85
- stop=["ユーザー:", "\n\n"],
86
- echo=True # 入力プロンプトも含めて返す
87
- )
88
-
89
- # 生成されたテキストを取得
90
- generated_text = output["choices"][0]["text"]
91
-
92
- # プロンプトを除去して応答のみを取得
93
- response_text = generated_text[len(system_prompt):].strip()
94
-
95
- print(f"Swallowモデル呼び出し成功")
96
- print(f"応答テキスト: {response_text}")
97
-
98
- return response_text
99
-
100
- except Exception as api_error:
101
- print(f"Swallowモデル呼び出しエラー: {api_error}")
102
- raise
103
-
104
  except Exception as e:
105
- print(f"応答生成エラー(Swallow): {e}")
106
- traceback.print_exc() # スタックトレースを出力
107
-
108
- # エラー情報をログファイルに記録
109
- with open("swallow_errors.log", "a") as f:
110
- f.write(f"時刻: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
111
- f.write(f"例外発生: {e}\n")
112
- f.write(f"スタックトレース:\n")
113
- traceback.print_exc(file=f)
114
- f.write("\n")
115
-
116
- # フォールバック:シーン名に応じた自然な応答を返す
117
- if instruction and "に来た感想" in instruction:
118
- scene = instruction.split("に来た感想")[0]
119
-
120
- # シーンに応じたフォールバック応答のバリエーション
121
- scene_responses = {
122
- "aquarium_night": [
123
- "(水槽の青い光に照らされた魚たちを見つめている)こんな時間に来ると、また違った雰囲気だな。",
124
- "(暗がりの中で光る魚たちを見て)夜の水族館か…意外と悪くないかも。",
125
- "(水槽に近づいて)夜になると、昼間とは違う魚が活動してるんだな。"
126
- ],
127
- "beach_sunset": [
128
- "(夕日に照らされた海を見つめて)こんな景色、久しぶりに見たな…",
129
- "(砂浜に足跡をつけながら)夕暮れの海って、なんか落ち着くな。",
130
- "(波の音を聞きながら)この時間の浜辺は、人も少なくていいかも。"
131
- ],
132
- "festival_night": [
133
- "(提灯の明かりを見上げて)意外と…悪くない雰囲気だな。",
134
- "(周囲の賑わいを見回して)こういう場所は、あまり来ないんだけどな…",
135
- "(屋台の匂いを感じて)なんか…懐かしい感じがするな。"
136
- ],
137
- "shrine_day": [
138
- "(静かな境内を見回して)こういう静かな場所も、たまにはいいかも。",
139
- "(鳥居を見上げて)なんか、空気が違うな、ここは。",
140
- "(参道を歩きながら)静かで…落ち着くな。"
141
- ],
142
- "cafe_afternoon": [
143
- "(窓の外を見ながら)こういう時間の過ごし方も、悪くないな。",
144
- "(コーヒーの香りを感じて)ここの雰囲気、悪くないな。",
145
- "(店内を見回して)意外と落ち着く場所だな、ここ。"
146
- ],
147
- "room_night": [
148
- "(窓の外の夜景を見て)夜の景色って、なんか落ち着くな。",
149
- "(部屋の明かりを見つめて)こうい��静かな時間も、たまにはいいかも。",
150
- "(窓際に立ち)夜の静けさって、考え事するのにちょうどいいな。"
151
- ]
152
- }
153
-
154
- # シーン名に応じた応答を選択(なければデフォルト応答)
155
- if scene in scene_responses:
156
- return random.choice(scene_responses[scene])
157
- else:
158
- return f"({scene}の様子を静かに見回して)ここか…悪くない場所かもな。"
159
- else:
160
- return "(……何か言おうとしたけど、言葉に詰まった)"
 
1
  import traceback
2
  import datetime
3
  import random
4
+ import logging
5
 
6
+ logger = logging.getLogger(__name__)
7
+
8
+ def get_fallback_response(scene=None):
9
  """
10
+ シーンに応じた固定のフォールバック応答を生成する
11
+ """
12
+ scene_responses = {
13
+ "aquarium_night": "(水槽の青い光を見つめて)…夜の水族館か。悪くないかもな。",
14
+ "beach_sunset": "(夕日に照らされた海を見つめて)…こんな景色、久しぶりに見た。",
15
+ "festival_night": "(提灯の明かりを見上げて)…意外と、悪くない雰囲気だな。",
16
+ "shrine_day": "(静かな境内を見回して)…静かで、落ち着くな。",
17
+ "cafe_afternoon": "(窓の外を見ながら)…こういう時間の過ごし方も、悪くない。",
18
+ "room_night": "(窓の外の夜景を見て)…夜の景色って、なんか落ち着くな。",
19
+ }
20
+ if scene and scene in scene_responses:
21
+ return scene_responses[scene]
22
+ if scene:
23
+ return f"({scene}の様子を静かに見回している)…ここか。"
24
+ return "(……何か言おうとしたが、言葉に詰まった)"
25
+
26
+
27
+ def generate_dialogue_with_swallow(history, message, affection, stage_name, scene_params, instruction=None, swallow_model=None, SYSTEM_PROMPT_MARI=None):
28
+ """
29
+ Swallowモデル(GGUF版)を使用して対話応答を生成する
30
  """
 
 
 
 
 
31
  if swallow_model is None:
32
+ logger.warning("Swallowモデルが利用できないため、フォールバック応答を返します。")
33
+ return get_fallback_response(scene_params.get("theme"))
 
 
 
 
 
 
 
 
34
 
35
+ history_text = "\n".join([f"ユーザー: {u}\n麻理: {m}" for u, m in history[-5:]]) # 直近5件に制限
 
 
 
 
36
 
37
+ prompt = f"""{SYSTEM_PROMPT_MARI}
 
 
 
 
 
 
 
38
 
39
  # 現在の状況
40
+ - シーン: {scene_params.get("theme", "default")}
41
+ - 好感度: {affection} ({stage_name})
42
+ - 性格: {scene_params.get("personality_mod", "特になし")}
43
+ - 口調: {scene_params.get("tone", "特になし")}
44
 
45
  # 会話履歴
46
  {history_text}
47
  ---
48
+ # 指示
49
+ {f"特別な指示: {instruction}" if instruction else f"ユーザーの発言「{message}」に、麻理として応答してください。"}
50
+ 麻理: """
 
 
 
 
51
 
52
  try:
53
+ output = swallow_model(
54
+ prompt,
55
+ max_tokens=150,
56
+ temperature=0.8,
57
+ top_p=0.95,
58
+ stop=["ユーザー:", "user:", "\n\n"],
59
+ echo=False # プロンプトは返さない
60
+ )
61
+ response_text = output["choices"][0]["text"].strip()
62
 
63
+ if not response_text:
64
+ logger.warning("LLMが空の応答を返しました。フォールバックを使用します。")
65
+ return get_fallback_response(scene_params.get("theme"))
66
+
67
+ logger.info(f"Swallowモデル生成応答: {response_text}")
68
+ return response_text
69
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  except Exception as e:
71
+ logger.error(f"応答生成エラー(Swallow): {e}", exc_info=True)
72
+ return get_fallback_response(scene_params.get("theme"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -1,9 +1,11 @@
1
- gradio>=5.0.0
2
- groq
3
  python-dotenv
4
- huggingface_hub>=0.16.0
 
 
 
 
 
5
  fugashi
6
  unidic_lite
7
- transformers>=4.34.0
8
- protobuf>=3.20.0
9
- psutil
 
1
+ gradio==4.31.5
 
2
  python-dotenv
3
+ openai
4
+ psutil
5
+
6
+ # 感情分析モデルを使用する場合
7
+ transformers
8
+ torch
9
  fugashi
10
  unidic_lite
11
+ protobuf<4.0.0
 
 
style.css CHANGED
@@ -1,380 +1,60 @@
1
- /* ========================
2
- 全体レイアウト
3
- ======================== */
4
  body {
5
  margin: 0;
6
- padding: 0;
7
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
8
  }
9
 
10
- /* Gradio 5.0のコンテナスタイル */
11
  .gradio-container {
12
- max-width: 1200px !important;
13
  margin: 0 auto !important;
14
- position: relative !important;
15
- z-index: 1 !important;
16
- }
17
-
18
- /* Gradio 5.0のブロックスタイル - 完全に透明に */
19
- .gradio-container > div > div {
20
  background-color: transparent !important;
21
- border-radius: 12px !important;
22
- margin-bottom: 16px !important;
23
- padding: 16px !important;
24
- box-shadow: none !important;
25
  }
26
 
27
- /* ========================
28
- 背景画像設定
29
- ======================== */
30
- /* 背景画像コンテナ - 常に表示されるように */
31
  .background-container {
32
- position: fixed !important;
33
- top: 0 !important;
34
- left: 0 !important;
35
- width: 100% !important;
36
- height: 100% !important;
37
- z-index: -1000 !important; /* 最背面に配置 */
38
- pointer-events: none !important;
39
- overflow: hidden !important;
40
- }
41
-
42
- /* 背景画像 */
43
- .chat-background {
44
- position: absolute !important;
45
- top: 0 !important;
46
- left: 0 !important;
47
- width: 100% !important;
48
- height: 100% !important;
49
- background-size: cover !important;
50
- background-position: center !important;
51
- opacity: 0.3 !important; /* 適度な透明度 */
52
- filter: blur(1px) !important; /* 少しぼかす */
53
- transition: all 0.5s ease !important; /* 背景切り替え時のアニメーション */
54
- }
55
-
56
- /* 背景画像の上に半透明のオーバーレイを追加 */
57
- .background-container::after {
58
- content: "" !important;
59
- position: absolute !important;
60
- top: 0 !important;
61
- left: 0 !important;
62
- width: 100% !important;
63
- height: 100% !important;
64
- background: linear-gradient(rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0.4)) !important;
65
- z-index: -999 !important;
66
- }
67
-
68
- /* ========================
69
- コンポーネント共通スタイル
70
- ======================== */
71
- /* グループ共通スタイル - 背景を透明に */
72
- .header-box, .chat-box, .input-box, .status-box, .footer-box {
73
- background-color: transparent !important;
74
- border-radius: 12px !important;
75
- box-shadow: none !important;
76
- padding: 15px !important;
77
- margin-bottom: 15px !important;
78
- border: none !important;
79
- }
80
-
81
- /* ========================
82
- ヘッダー
83
- ======================== */
84
- .header-box {
85
- text-align: center !important;
86
- }
87
-
88
- .header-box h1 {
89
- margin: 0 !important;
90
- padding: 10px !important;
91
- color: #333 !important;
92
- }
93
-
94
- /* ========================
95
- チャットUI
96
- ======================== */
97
- /* チャットコンテナ */
98
- .chat-box {
99
- padding: 15px !important;
100
- margin-bottom: 15px !important;
101
- }
102
-
103
- /* チャットエリア */
104
- #chat_area {
105
- min-height: 400px !important;
106
- max-height: 500px !important;
107
- overflow-y: auto !important;
108
- padding: 10px !important;
109
- background-color: transparent !important;
110
- }
111
-
112
- /* チャットメッセージ */
113
- .message {
114
- background-color: rgba(255, 255, 255, 0.7) !important;
115
- border-radius: 8px !important;
116
- padding: 10px !important;
117
- margin-bottom: 8px !important;
118
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important;
119
- }
120
-
121
- /* ========================
122
- 入力欄
123
- ======================== */
124
- .input-box {
125
- padding: 10px !important;
126
- }
127
-
128
- /* 入力欄のスタイル - より目立つように */
129
- textarea, input[type="text"], input[type="number"], .gradio-textbox {
130
- background-color: rgba(255, 255, 255, 0.95) !important;
131
- border-radius: 8px !important;
132
- border: 2px solid rgba(255, 100, 100, 0.5) !important;
133
- padding: 12px !important;
134
- font-size: 16px !important;
135
- color: #333 !important;
136
- box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1) !important;
137
- backdrop-filter: blur(5px) !important;
138
- -webkit-backdrop-filter: blur(5px) !important;
139
- margin-bottom: 10px !important;
140
- min-height: 50px !important;
141
- width: 100% !important;
142
- display: block !important;
143
- box-sizing: border-box !important;
144
- }
145
-
146
- /* 入力欄のコンテナ */
147
- .gradio-textbox {
148
- background-color: rgba(255, 255, 255, 0.8) !important;
149
- padding: 10px !important;
150
- border-radius: 10px !important;
151
- margin-bottom: 15px !important;
152
- box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1) !important;
153
- }
154
-
155
- /* 入力欄のフォーカス時のスタイル */
156
- textarea:focus, input[type="text"]:focus, input[type="number"]:focus {
157
- border-color: rgba(255, 50, 50, 0.8) !important;
158
- box-shadow: 0 0 8px rgba(255, 100, 100, 0.5) !important;
159
- outline: none !important;
160
- }
161
-
162
- /* ボタンのスタイル - より目立つように */
163
- button, .submit, button[type="submit"], .gradio-button {
164
- background-color: rgba(255, 100, 100, 0.9) !important;
165
- color: white !important;
166
- border-radius: 8px !important;
167
- border: 1px solid rgba(200, 0, 0, 0.2) !important;
168
- padding: 10px 20px !important;
169
- font-weight: bold !important;
170
- box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2) !important;
171
- margin: 5px !important;
172
- height: auto !important;
173
- min-height: 40px !important;
174
- transition: all 0.2s ease !important;
175
- opacity: 1 !important;
176
- visibility: visible !important;
177
- display: block !important;
178
- width: auto !important;
179
- min-width: 100px !important;
180
- font-size: 16px !important;
181
- cursor: pointer !important;
182
- z-index: 1000 !important;
183
- position: relative !important;
184
- }
185
-
186
- button:hover, .submit:hover, button[type="submit"]:hover, .gradio-button:hover {
187
- background-color: rgba(255, 50, 50, 1.0) !important;
188
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3) !important;
189
- transform: translateY(-2px) !important;
190
- }
191
-
192
- /* 送信ボタン特有のスタイル */
193
- .submit, button[type="submit"] {
194
- background-color: #ff4757 !important;
195
- font-size: 18px !important;
196
- padding: 12px 24px !important;
197
- margin-top: 10px !important;
198
- margin-bottom: 10px !important;
199
- width: 100% !important;
200
- max-width: 200px !important;
201
- }
202
-
203
- /* ========================
204
- ステータス表示
205
- ======================== */
206
- .status-box {
207
- padding: 15px !important;
208
- background-color: rgba(255, 255, 255, 0.9) !important;
209
- border-radius: 12px !important;
210
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1) !important;
211
- margin-bottom: 15px !important;
212
- }
213
-
214
- /* スライダー(好感度バー)のスタイル */
215
- .status-box input[type="range"], .gradio-slider input[type="range"] {
216
- -webkit-appearance: none !important;
217
- appearance: none !important;
218
- width: 100% !important;
219
- height: 20px !important;
220
- border-radius: 10px !important;
221
- background: linear-gradient(to right, #ff6b6b, #ff9e9e) !important;
222
- outline: none !important;
223
- opacity: 1 !important;
224
- transition: opacity 0.2s !important;
225
- margin: 15px 0 !important;
226
- box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2) !important;
227
- border: 2px solid rgba(255, 255, 255, 0.5) !important;
228
  }
229
 
230
- .status-box input[type="range"]::-webkit-slider-thumb, .gradio-slider input[type="range"]::-webkit-slider-thumb {
231
- -webkit-appearance: none !important;
232
- appearance: none !important;
233
- width: 30px !important;
234
- height: 30px !important;
235
- border-radius: 50% !important;
236
- background: #ff4757 !important;
237
- cursor: pointer !important;
238
- box-shadow: 0 0 8px rgba(0, 0, 0, 0.3) !important;
239
- border: 2px solid white !important;
240
  }
241
-
242
- /* スライダーのコンテナ */
243
- .gradio-slider {
244
- background-color: rgba(255, 255, 255, 0.8) !important;
245
- padding: 15px !important;
246
- border-radius: 10px !important;
247
- margin-bottom: 15px !important;
248
- box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1) !important;
249
- }
250
-
251
- /* ラベルのスタイル */
252
- .status-box label {
253
- font-weight: bold !important;
254
- color: #333 !important;
255
- margin-bottom: 5px !important;
256
- display: block !important;
257
- }
258
-
259
- /* ========================
260
- フッター
261
- ======================== */
262
- .footer-box {
263
- text-align: center !important;
264
- padding: 10px !important;
265
- margin-top: 20px !important;
266
- }
267
-
268
- /* ========================
269
- テーマ別背景画像
270
- ======================== */
271
- .default {
272
- background-image: url("https://cdn.pixabay.com/photo/2017/03/28/12/11/chairs-2181960_1280.jpg");
273
- }
274
-
275
- .room_night {
276
- background-image: url("https://cdn.pixabay.com/photo/2017/08/01/01/33/beige-2562468_1280.jpg");
277
- }
278
-
279
- .beach_sunset {
280
- background-image: url("https://cdn.pixabay.com/photo/2016/11/29/04/19/ocean-1867285_1280.jpg");
281
- }
282
-
283
- .festival_night {
284
- background-image: url("https://cdn.pixabay.com/photo/2015/11/22/19/04/crowd-1056764_1280.jpg");
285
- }
286
-
287
- .shrine_day {
288
- background-image: url("https://cdn.pixabay.com/photo/2019/07/14/10/48/japan-4337223_1280.jpg");
289
- }
290
-
291
- .cafe_afternoon {
292
- background-image: url("https://cdn.pixabay.com/photo/2016/11/18/14/05/brick-wall-1834784_1280.jpg");
293
- }
294
-
295
- .aquarium_night {
296
- background-image: url("https://cdn.pixabay.com/photo/2017/06/20/20/45/fish-2424369_1280.jpg");
297
- }
298
-
299
- /* 以下は元々あったクラス定義 */
300
- .aquarium_day {
301
- background-image: url("https://cdn.pixabay.com/photo/2016/11/29/02/02/aquarium-1867283_1280.jpg");
302
  }
303
-
304
- .forest_day {
305
- background-image: url("https://cdn.pixabay.com/photo/2015/11/19/18/36/forest-1054795_1280.jpg");
306
- }
307
-
308
- .forest_night {
309
- background-image: url("https://cdn.pixabay.com/photo/2017/10/31/18/47/fog-2900424_1280.jpg");
310
- }
311
-
312
- /* ========================
313
- チャットエリアなど(必要に応じて)
314
- ======================== */
315
- .gradio-container {
316
- position: relative !important; /* 背景をabsoluteで置くために */
317
- overflow: hidden !important;
318
- }
319
-
320
- /* Gradio 5.0の特定のクラスに対するスタイル */
321
- .gradio-row {
322
- display: flex !important;
323
- flex-wrap: wrap !important;
324
- margin: 0 -10px !important;
325
- }
326
-
327
- .gradio-column {
328
- padding: 0 10px !important;
329
- box-sizing: border-box !important;
330
- }
331
-
332
- /* 入力エリアのコンテナ */
333
- .input-container, .gradio-textbox-input {
334
- background-color: rgba(255, 255, 255, 0.8) !important;
335
- border-radius: 10px !important;
336
- padding: 15px !important;
337
- margin-bottom: 15px !important;
338
- box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1) !important;
339
- }
340
-
341
- /* 送信ボタンのコンテナ */
342
- .submit-container {
343
- display: flex !important;
344
- justify-content: center !important;
345
- margin-top: 10px !important;
346
  }
347
 
348
  .chatbot {
349
- background-color: rgba(255, 255, 255, 0.7) !important;
350
  border-radius: 12px !important;
351
- padding: 15px !important;
352
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1) !important;
353
- margin-bottom: 20px !important;
354
- min-height: 400px !important;
355
- max-height: 600px !important;
356
- overflow-y: auto !important;
357
  }
358
 
359
  /* チャットメッセージのスタイル */
360
- .chatbot > div > div > div {
361
- background-color: rgba(255, 255, 255, 0.9) !important;
362
- border-radius: 12px !important;
363
- padding: 12px !important;
364
- margin-bottom: 12px !important;
365
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important;
366
- font-size: 16px !important;
367
- line-height: 1.5 !important;
368
- }
369
-
370
- /* ユーザーメッセージ */
371
- .chatbot > div > div:nth-child(odd) > div {
372
- background-color: rgba(230, 230, 255, 0.95) !important;
373
- border-left: 4px solid #6c5ce7 !important;
374
- }
375
-
376
- /* ボットメッセージ */
377
- .chatbot > div > div:nth-child(even) > div {
378
- background-color: rgba(255, 230, 230, 0.95) !important;
379
- border-left: 4px solid #ff6b6b !important;
380
- }
 
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;
11
  margin: 0 auto !important;
 
 
 
 
 
 
12
  background-color: transparent !important;
13
+ padding: 1rem !important;
 
 
 
14
  }
15
 
16
+ /* --- Background --- */
 
 
 
17
  .background-container {
18
+ position: fixed;
19
+ top: 0;
20
+ left: 0;
21
+ width: 100vw;
22
+ height: 100vh;
23
+ z-index: -1;
24
+ background-size: cover;
25
+ background-position: center;
26
+ filter: blur(2px) brightness(0.9);
27
+ opacity: 0.5;
28
+ transition: background-image 0.5s ease-in-out;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  }
30
 
31
+ /* --- Components --- */
32
+ .header, .footer {
33
+ text-align: center;
34
+ color: #333;
35
+ padding: 0.5rem;
 
 
 
 
 
36
  }
37
+ .footer a {
38
+ color: #555;
39
+ text-decoration: none;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  }
41
+ .footer a:hover {
42
+ text-decoration: underline;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  }
44
 
45
  .chatbot {
46
+ background-color: rgba(255, 255, 255, 0.75) !important;
47
  border-radius: 12px !important;
48
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1) !important;
49
+ backdrop-filter: blur(5px);
50
+ -webkit-backdrop-filter: blur(5px);
51
+ border: none !important;
 
 
52
  }
53
 
54
  /* チャットメッセージのスタイル */
55
+ .chatbot > .message-wrap {
56
+ border-radius: 8px !important;
57
+ padding: 10px !important;
58
+ margin: 8px !important;
59
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important;
60
+ }