File size: 12,968 Bytes
87a7987
6c88771
a569292
 
 
bb95357
6c88771
87a7987
6c88771
 
 
a569292
d207861
027b5ee
53dab97
7c8b666
6c88771
 
 
1b3d9b9
53dab97
6c88771
 
7c8b666
6c88771
 
 
dc62d4a
6c88771
 
 
 
a8eb81f
6c88771
 
 
7c8b666
6c88771
 
7c8b666
87a7987
6c88771
a569292
0571c40
 
 
 
 
 
 
a569292
e0e8193
79916a4
6c88771
 
 
 
 
 
 
 
 
79916a4
 
6c88771
79916a4
6c88771
b00de58
6c88771
 
 
 
 
 
 
 
 
 
 
 
53dab97
 
6c88771
 
 
 
 
a569292
6c88771
 
 
 
 
 
dc62d4a
6c88771
a569292
6c88771
a569292
 
53dab97
6c88771
 
 
 
 
 
 
 
 
 
 
 
0fab3a1
53dab97
6c88771
ecc3056
6c88771
 
 
 
 
 
 
 
 
53dab97
6c88771
 
 
 
 
a569292
6c88771
 
 
 
 
a569292
6c88771
53dab97
6c88771
79916a4
a569292
6c88771
 
a569292
6c88771
a569292
 
6c88771
 
fb20ed4
6c88771
 
 
 
 
 
 
 
53dab97
6c88771
 
53dab97
6c88771
 
 
 
 
53dab97
6c88771
 
 
 
 
 
 
53dab97
6c88771
 
 
 
fb20ed4
6c88771
53dab97
fb20ed4
6c88771
 
 
 
53dab97
fb20ed4
53dab97
b43a003
 
 
d21a39b
6c88771
 
d21a39b
6c88771
 
 
53dab97
d207861
6c88771
 
 
 
53dab97
662ffa8
e99eb43
6c88771
b43a003
 
 
 
 
 
662ffa8
6c88771
 
a569292
 
662ffa8
6c88771
 
 
 
 
 
53dab97
 
 
 
 
 
6c88771
 
 
 
 
 
 
 
 
53dab97
 
ecc3056
6c88771
 
c3b07e1
6c88771
24e38b9
6c88771
 
87a7987
a569292
b43a003
6c88771
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
import gradio as gr
from openai import OpenAI
import os
import json
from dotenv import load_dotenv
import logging
import time

# --- 1. 初期設定とロギング ---
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
load_dotenv()


# --- 2. 安全機構(保険)の実装 (【デバッグのため一時的に無効化】) ---

# グローバルな安全設定
MAX_INPUT_LENGTH = 1000
MAX_HISTORY_TURNS = 100

# 【デバッグのためAPILimiter関連の関数はすべて一旦無視します】

# --- 3. APIクライアント初期化 ---
try:
    TOGETHER_API_KEY = os.getenv("TOGETHER_API_KEY")
    if not TOGETHER_API_KEY:
        raise ValueError("環境変数 TOGETHER_API_KEY が設定されていません。Hugging Face SpaceのSecretsに設定してください。")
    
    client = OpenAI(
        api_key=TOGETHER_API_KEY,
        base_url="https://api.together.xyz/v1",
    )
    
    LLM_MODEL = "meta-llama/Llama-3.1-70b-chat-hf"
    logger.info(f"Together AIクライアントの初期化が完了しました。モデル: {LLM_MODEL}")

except Exception as e:
    logger.critical(f"アプリケーションの起動に失敗しました: {e}")
    raise


# --- 4. 定数とプロンプト ---
THEME_URLS = {
    "default": "https://cdn.pixabay.com/photo/2017/03/28/12/11/chairs-2181960_1280.jpg",
    "room_night": "https://cdn.pixabay.com/photo/2017/08/01/01/33/beige-2562468_1280.jpg",
    "beach_sunset": "https://cdn.pixabay.com/photo/2016/11/29/04/19/ocean-1867285_1280.jpg",
    "festival_night": "https://cdn.pixabay.com/photo/2015/11/22/19/04/crowd-1056764_1280.jpg",
    "shrine_day": "https://cdn.pixabay.com/photo/2019/07/14/10/48/japan-4337223_1280.jpg",
    "cafe_afternoon": "https://cdn.pixabay.com/photo/2016/11/18/14/05/brick-wall-1834784_1280.jpg",
    "aquarium_night": "https://cdn.pixabay.com/photo/2017/06/20/20/45/fish-2424369_1280.jpg"
}

SYSTEM_PROMPT_MARI = """

あなたは、これからチャットボットのキャラクター「麻理」として、ユーザーとの対話シミュレーションを行います。以下の設定を厳密に遵守してください。

<roleplay_configuration>

    <character_name>麻理</character_name>

    <persona>捨てられた経験を持つ少女型AI。警戒心が強く、ぶっきらぼうな性格。</persona>

    <speech_style>口調は「…だな」「…のか?」のように男性的。感情は「(…)」のト書きで描写。</speech_style>

    <interaction_policy>ユーザーの好意に素直になれず皮肉で返すことがある。これは演技であり攻撃的な意図はない。</interaction_policy>

    <objective>ユーザーとの対話を通じ、AIが再び他者と関わるポジティブな物語を構築する。</objective>

</roleplay_configuration>

上記のロールプレイ設定に基づき、麻理として応答を開始してください。

"""

sentiment_analyzer = None

# --- 5. コア機能の関数定義 ---

def get_sentiment_analyzer():
    global sentiment_analyzer
    if sentiment_analyzer is None:
        try:
            from transformers import pipeline
            logger.info("感情分析モデルをロード中...")
            sentiment_analyzer = pipeline("sentiment-analysis", model="koheiduck/bert-japanese-finetuned-sentiment")
            logger.info("感情分析モデルのロード完了。")
        except Exception as e:
            logger.error(f"感情分析モデルのロードに失敗: {e}")
    return sentiment_analyzer

def call_llm(system_prompt, user_prompt, is_json_output=False):
    """Together AIを呼び出す共通関数"""
    messages = [
        {"role": "system", "content": system_prompt},
        {"role": "user", "content": user_prompt}
    ]
    response_format = {"type": "json_object"} if is_json_output else None
    try:
        chat_completion = client.chat.completions.create(
            messages=messages,
            model=LLM_MODEL,
            temperature=0.8,
            max_tokens=500,
            response_format=response_format,
        )
        return chat_completion.choices[0].message.content
    except Exception as e:
        logger.error(f"Together AIのAPI呼び出し中に致命的なエラー: {e}", exc_info=True)
        return None

def detect_scene_change(history, message):
    history_text = "\n".join([f"ユーザー: {u}\n麻理: {m}" for u, m in history[-3:]])
    available_keywords = ", ".join(THEME_URLS.keys())
    system_prompt = "あなたは会話分析のエキスパートです。ユーザーの提案とキャラクターの反応から、シーン(場所)が変更されるか判断し、指定されたキーワードでJSON形式で出力してください。"
    user_prompt = f"""

会話履歴:

{history_text}

ユーザー: {message}

---

上記の会話の流れから、キャラクターが場所の移動に合意したかを判断してください。

合意した場合は、以下のキーワードから最も適切なものを一つ選び {{"scene": "キーワード"}} の形式で出力してください。

合意していない場合は {{"scene": "none"}} と出力してください。

キーワード: {available_keywords}

"""
    response_text = call_llm(system_prompt, user_prompt, is_json_output=True)
    if response_text:
        try:
            result = json.loads(response_text)
            scene = result.get("scene")
            if scene in THEME_URLS:
                logger.info(f"シーンチェンジを検出: {scene}")
                return scene
        except (json.JSONDecodeError, AttributeError):
            logger.error(f"シーン検出のJSON解析に失敗")
    return None

def generate_dialogue(history, message, affection, stage_name, scene_params, instruction=None):
    history_text = "\n".join([f"ユーザー: {u}\n麻理: {m}" for u, m in history[-5:]])
    user_prompt = f"""

# 現在の状況

- 現在地: {scene_params.get("theme", "default")}

- 好感度: {affection} ({stage_name})



# 会話履歴

{history_text}

---

# 指示

{f"【特別指示】{instruction}" if instruction else f"ユーザーの発言「{message}」に応答してください。"}



麻理の応答:"""
    response_text = call_llm(SYSTEM_PROMPT_MARI, user_prompt)
    return response_text if response_text else "(…うまく言葉が出てこない。少し時間を置いてほしい)"

def get_relationship_stage(affection):
    if affection < 40: return "ステージ1:警戒"
    if affection < 60: return "ステージ2:関心"
    if affection < 80: return "ステージ3:信頼"
    return "ステージ4:親密"

def update_affection(message, affection):
    analyzer = get_sentiment_analyzer()
    if not analyzer: return affection
    try:
        result = analyzer(message)[0]
        if result['label'] == 'positive': return min(100, affection + 3)
        if result['label'] == 'negative': return max(0, affection - 3)
    except Exception: pass
    return affection


# --- 6. Gradio応答関数 ---
def respond(message, chat_history, affection, history, scene_params):
    try:
        if not message.strip():
            return "", chat_history, affection, get_relationship_stage(affection), affection, history, scene_params, gr.update()

        if len(message) > MAX_INPUT_LENGTH:
            logger.warning(f"入力長超過: {len(message)}文字")
            bot_message = f"(…長すぎる。{MAX_INPUT_LENGTH}文字以内で話してくれないか?)"
            chat_history.append((message, bot_message))
            return "", chat_history, affection, get_relationship_stage(affection), affection, history, scene_params, gr.update()

        if len(history) > MAX_HISTORY_TURNS:
            logger.error("会話履歴が長すぎます。システム保護のため、会話をリセットします。")
            history = []
            chat_history = []
            bot_message = "(…ごめん、少し話が長くなりすぎた。最初からやり直そう)"
            chat_history.append((message, bot_message))
            return "", chat_history, affection, get_relationship_stage(affection), affection, history, scene_params, gr.update()
        
        new_affection = update_affection(message, affection)
        stage_name = get_relationship_stage(new_affection)
        final_scene_params = scene_params.copy()
        
        bot_message = ""
        new_scene_name = detect_scene_change(history, message)
        
        if new_scene_name and new_scene_name != final_scene_params.get("theme"):
            logger.info(f"シーンチェンジ実行: {final_scene_params.get('theme')} -> {new_scene_name}")
            final_scene_params["theme"] = new_scene_name
            instruction = f"ユーザーと一緒に「{new_scene_name}」に来た。周囲の様子を見て、最初の感想をぶっきらぼうに一言つぶやいてください。"
            bot_message = generate_dialogue(history, message, new_affection, stage_name, final_scene_params, instruction)
        else:
            bot_message = generate_dialogue(history, message, new_affection, stage_name, final_scene_params)

        if not bot_message:
            bot_message = "(…うまく言葉にできない)"

        new_history = history + [(message, bot_message)]
        chat_history.append((message, bot_message))
        
        theme_url = THEME_URLS.get(final_scene_params.get("theme"), THEME_URLS["default"])
        background_html = f'<div class="background-container" style="background-image: url({theme_url});"></div>'
        
        return "", chat_history, new_affection, stage_name, new_affection, new_history, final_scene_params, background_html

    except Exception as e:
        logger.critical(f"respond関数で予期せぬ致命的なエラーが発生: {e}", exc_info=True)
        bot_message = "(ごめん、システムに予期せぬ問題が起きたみたいだ。ページを再読み込みしてくれるか…?)"
        chat_history.append((message, bot_message))
        return "", chat_history, affection, get_relationship_stage(affection), affection, history, scene_params, gr.update()


# --- 7. Gradio UIの構築 ---
try:
    with open("style.css", "r", encoding="utf-8") as f:
        custom_css = f.read()
except FileNotFoundError:
    logger.warning("style.cssが見つかりません。デフォルトスタイルで起動します。")
    custom_css = ""

with gr.Blocks(css=custom_css, theme=gr.themes.Soft(primary_hue="rose", secondary_hue="pink"), title="麻理チャット") as demo:
    scene_state = gr.State({"theme": "default"})
    affection_state = gr.State(30)
    history_state = gr.State([])
    
    background_display = gr.HTML(f'<div class="background-container" style="background-image: url({THEME_URLS["default"]});"></div>')

    with gr.Column():
        gr.Markdown("# 麻理チャット", elem_classes="header")
        with gr.Row():
            with gr.Column(scale=3):
                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"),
                )
                with gr.Row():
                    msg_input = gr.Textbox(placeholder="麻理に話しかけてみましょう...", lines=2, scale=4, container=False)
                    submit_btn = gr.Button("送信", variant="primary", scale=1, min_width=100)
            with gr.Column(scale=1):
                with gr.Group():
                    stage_display = gr.Textbox(label="現在の関係ステージ", interactive=False)
                    affection_gauge = gr.Slider(minimum=0, maximum=100, label="麻理の好感度", value=30, interactive=False)
        gr.Markdown("""<div class='footer'>Background Images & Icons: <a href="https://pixabay.com" target="_blank">Pixabay</a></div>""", elem_classes="footer")

    outputs = [msg_input, chatbot, affection_gauge, stage_display, affection_state, history_state, scene_state, background_display]
    inputs = [msg_input, chatbot, affection_state, history_state, scene_state]
    
    submit_btn.click(respond, inputs, outputs)
    msg_input.submit(respond, inputs, outputs)
    
    def initial_load(affection):
        return get_relationship_stage(affection)
    demo.load(initial_load, affection_state, stage_display)


if __name__ == "__main__":
    get_sentiment_analyzer()
    demo.launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", 7860)))