Upload 5 files
Browse files- Dockerfile +10 -49
- app.py +233 -677
- generate_dialogue_with_swallow.py +53 -141
- requirements.txt +8 -6
- 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 |
-
|
| 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 |
-
#
|
| 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 |
-
#
|
| 35 |
-
|
| 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 |
-
#
|
| 59 |
-
EXPOSE
|
| 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
|
| 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 |
-
|
| 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.
|
|
|
|
|
|
|
| 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 |
-
|
| 55 |
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
-
#
|
| 62 |
-
|
| 63 |
-
|
|
|
|
|
|
|
| 64 |
|
|
|
|
|
|
|
| 65 |
try:
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
print(f"モデルファイルのダウンロード完了: {model_path}")
|
| 70 |
|
| 71 |
-
#
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 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 |
-
|
| 106 |
-
|
| 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 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
|
|
|
| 184 |
"""
|
| 185 |
|
|
|
|
| 186 |
|
| 187 |
-
# ---
|
| 188 |
|
| 189 |
-
def
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
"""
|
| 211 |
-
# Swallowモデル(GGUF版)を使用してシーン検出
|
| 212 |
try:
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 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 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
return None
|
| 247 |
|
| 248 |
-
def
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
"constraints": ["必ず健全で適切な表現を使用する", "センシティブな話題は避ける"]
|
| 262 |
-
}}
|
| 263 |
"""
|
| 264 |
-
|
| 265 |
-
|
| 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 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 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 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 328 |
-
|
| 329 |
try:
|
| 330 |
-
|
| 331 |
-
if
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
print(f"感情分析結果: {result}")
|
| 340 |
|
| 341 |
-
if
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
|
|
|
| 345 |
else:
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
except Exception as e:
|
| 349 |
-
print(f"感情分析エラー: {e}")
|
| 350 |
-
# エラーが発生した場合は現在の好感度を維持
|
| 351 |
-
return affection
|
| 352 |
-
|
| 353 |
-
|
| 354 |
|
| 355 |
-
|
| 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 |
-
|
| 371 |
-
|
| 372 |
-
|
| 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 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 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
|
| 583 |
-
|
| 584 |
-
|
| 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=
|
| 599 |
-
|
|
|
|
| 600 |
affection_state = gr.State(30)
|
| 601 |
history_state = gr.State([])
|
|
|
|
| 602 |
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 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 |
-
.
|
| 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 |
-
|
| 718 |
|
| 719 |
if __name__ == "__main__":
|
| 720 |
-
|
| 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 |
-
|
|
|
|
|
|
|
| 6 |
"""
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 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 |
-
|
| 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 |
-
-
|
| 58 |
-
-
|
| 59 |
-
-
|
| 60 |
-
-
|
| 61 |
|
| 62 |
# 会話履歴
|
| 63 |
{history_text}
|
| 64 |
---
|
| 65 |
-
#
|
| 66 |
-
{
|
| 67 |
-
麻理:
|
| 68 |
-
"""
|
| 69 |
-
|
| 70 |
-
print(f"Swallowモデルに応答生成をリクエストします (モード: {'シーン遷移' if instruction else '通常会話'}, 簡潔プロンプト: {use_simple_prompt})")
|
| 71 |
-
print(f"プロンプト長: {len(system_prompt)}")
|
| 72 |
|
| 73 |
try:
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 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 |
-
|
| 106 |
-
|
| 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
|
| 2 |
-
groq
|
| 3 |
python-dotenv
|
| 4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
fugashi
|
| 6 |
unidic_lite
|
| 7 |
-
|
| 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 |
-
|
| 7 |
-
|
| 8 |
}
|
| 9 |
|
| 10 |
-
/*
|
| 11 |
.gradio-container {
|
| 12 |
-
max-width:
|
| 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 |
-
|
| 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
|
| 33 |
-
top: 0
|
| 34 |
-
left: 0
|
| 35 |
-
width:
|
| 36 |
-
height:
|
| 37 |
-
z-index: -
|
| 38 |
-
|
| 39 |
-
|
| 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 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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.
|
| 350 |
border-radius: 12px !important;
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
max-height: 600px !important;
|
| 356 |
-
overflow-y: auto !important;
|
| 357 |
}
|
| 358 |
|
| 359 |
/* チャットメッセージのスタイル */
|
| 360 |
-
.chatbot >
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 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 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|