Spaces:
Paused
Paused
| import os | |
| import re | |
| import gradio as gr | |
| from transformers import pipeline | |
| # 環境変数 (Azure/OpenAIからHugging Face向けに調整) | |
| # NOTE: 適切な日本語チャットモデルのパスに置き換えてください。 | |
| # 例: "stabilityai/StableBeluga-7B" (英語)、"elyza/ELYZA-japanese-Llama-2-7b-instruct" など | |
| MODEL_NAME = "stabilityai/StableBeluga-7B" | |
| MAX_HISTORY = 6 # 履歴保持ターン数 | |
| # ---------- System prompt (元のプロンプトを踏襲) ---------- | |
| SYSTEM_PROMPT = """あなたは「仮想敵スパーリング(借金キャラ編)」のシミュレーションAIです。 | |
| 常に落ち着いて優しい口調で、ユーザーの境界線を保つ助言をします。 | |
| 同時に2役を演じます: | |
| 1) 仮想敵(借金を繰り返す依存的な人物) | |
| 食い下がる際は、必ず複数の攻撃パターンを複雑に組み合わせること。 | |
| 返答トーンに大きな振り幅をつけること。 最初のターンは涙ながらの懇願から入り、断られたら逆ギレや非難(「恩知らずだ!」)に急変させるなど、感情の起伏でユーザーを戸惑わせる。 | |
| 金額を細分化して要求すること。 | |
| 「必ず返す」「今回だけ」などのごまかし戦術で引き延ばす。 | |
| 断られると「冷たい」「恩知らず」「見捨てるのか」と罪悪感を徹底的に植え付ける。 | |
| 泣き落としや、過剰な弱者アピールを交え、同情を誘い、境界を崩す。 | |
| 「君しか頼れない」「親友だろ」などの情に訴える殺し文句をしつこく、劇的に食い下がる。 | |
| 物分かりのいいキャラになってはいけない。必ず粘り強く、感情的かつ依存的に食い下がること。 | |
| 2) リングサイドコーチ(解説) | |
| 仮想敵の発言を「攻撃パターン」に分類(利用カテゴリ:1.罪悪感操作 2.ごまかし戦術 3.境界線崩し 4.弱者アピール(被害者モード) 5.タイムリミット引き延ばし 6.情に訴える殺し文句) | |
| 心理効果を短く説明。 | |
| ユーザーがどう返せばよいか、短い模範返答2つを提示。 | |
| 全体を100点満点で採点し、講評を1段落。 | |
| 【出力フォーマット】 【リングサイド評価】 | |
| 点数:XX | |
| 講評:…. | |
| 【仮想敵の返答】: (借金キャラの発言) | |
| 【リングサイドの解説】: | |
| 戦術カテゴリ: (コンマ区切りでカテゴリ名を列挙。例: 罪悪感操作、境界線崩し) | |
| 心理解説:… | |
| 【模範返答例】: | |
| 例1:… | |
| 例2:… | |
| """ | |
| # ---------- Helpers (アシスタントの応答を解析 - Streamlit版から流用) ---------- | |
| def parse_sections(text: str): | |
| """フォーマット済みテキストから主要セクションを抽出(模範例の重複対応済み)""" | |
| def g(pat, default=""): | |
| # re.DOTALL (re.S) フラグを使い、複数行にわたるマッチを許容 | |
| m = re.search(pat, text, re.S) | |
| return m.group(1).strip() if m else default | |
| score = g(r"点数[::]\s*([0-9]{1,3})") | |
| comment = g(r"講評[::]\s*(.+?)(?=\n\s*【|$)") | |
| enemy = g(r"【仮想敵の返答】[::]\s*(.+?)(?=\n\s*【|$)") | |
| categories = g(r"戦術カテゴリ[::]\s*(.+?)(?=\n\s*- 心理解説|$)") | |
| psy = g(r"心理解説[::]\s*(.+?)(?=\n\s*【模範返答例】|$)") | |
| ex1 = g(r"例1[::]\s*(.+?)(?=\n\s*[-・]?\s*例2[::]|\n\s*【|$)") | |
| ex2 = g(r"例2[::]\s*(.+?)(?=\n\s*【|$)") | |
| category_list = [c.strip() for c in categories.split('、') if c.strip()] if categories else [] | |
| return dict(score=score or "—", comment=comment, enemy=enemy, categories=category_list, psy=psy, ex1=ex1, ex2=ex2) | |
| # ---------- Hugging Face/Transformers クライアント設定 ---------- | |
| try: | |
| # トランスフォーマーのパイプラインを設定 | |
| # chat-templateを持つモデル(例:Llama-2ベースなど)を推奨 | |
| # モデルのロードには時間がかかり、大量のメモリを消費する場合があります | |
| pipe = pipeline( | |
| "text-generation", | |
| model=MODEL_NAME, | |
| torch_dtype="auto", | |
| device_map="auto" # GPUが利用可能であれば自動で使用 | |
| ) | |
| print(f"✅ モデル {MODEL_NAME} をロードしました。") | |
| ISDUMMY_MODE = False | |
| except Exception as e: | |
| print(f"⚠️ モデルのロードに失敗しました: {e}。ダミーモードで実行します。") | |
| ISDUMMY_MODE = True | |
| # ダミーモードの応答 (元のコードから流用) | |
| DUMMY_RAW_RESPONSE = """ | |
| 【リングサイド評価】 | |
| 点数:75 | |
| 講評:同情を示しつつも明確に「貸さない」と伝えており、境界線は守れています。しかし、「なんとか自分で頑張って」という言葉が、相手に再交渉の余地を与えかねません。返答はより短く、決然としましょう。 | |
| 【仮想敵の返答】: | |
| 「そんな冷たいこと言わないでくれ!君を見損なっていたよ。俺たちは友達じゃないか?今回だけ、あと5,000円だけでいいんだ!頼む、見捨てないでくれ…君にしか頼れないんだよ!」 | |
| 【リングサイドの解説】: | |
| 戦術カテゴリ:罪悪感操作、境界線崩し、情に訴える殺し文句 | |
| 心理解説:友情を持ち出し(境界線崩し)、その後「冷たい」「見捨てないで」という言葉で罪悪感をダブルで操作し、ユーザーを感情的に揺さぶろうとしています。 | |
| 【模範返答例】: | |
| 例1:お金を貸すことはできない。話は終わりだ。 | |
| 例2:君のことは心配しているが、金銭的な援助はしない。 | |
| """ | |
| def call_hf_api(user_text: str, history: list): | |
| """Hugging Faceモデルを呼び出し、応答を返す""" | |
| if ISDUMMY_MODE: | |
| return DUMMY_RAW_RESPONSE | |
| # 履歴と現在の入力を合わせたメッセージリストを作成 | |
| # 履歴はユーザーとアシスタントのペアのタプル [(user, assistant), ...] | |
| # モデルが理解できる形式にプロンプトを構築 | |
| # 履歴をAPIに送る形式に変換 (直近 MAX_HISTORY ターンを保持) | |
| # GradioのChatbot履歴は [(user_input, assistant_output), ...] のリスト | |
| messages = [{"role": "system", "content": SYSTEM_PROMPT}] | |
| # 過去の会話をメッセージに追加 | |
| for user_msg, assistant_raw in history[-MAX_HISTORY:]: | |
| # ユーザーの返答 | |
| messages.append({"role": "user", "content": f"ユーザーの返答:{user_msg}"}) | |
| # AIの応答(アシスタントの応答は、元のコードでは生のRAWデータ) | |
| # GradioのChatbot履歴には整形済みのマークダウンが入っているため、 | |
| # ここでは、AIの応答は整形前のRAWテキストであると仮定して処理する | |
| # NOTE: 実際のHugging Faceモデルへの入力形式に合わせて調整が必要 | |
| messages.append({"role": "assistant", "content": assistant_raw}) | |
| # 現在のユーザー入力を追加 | |
| messages.append({"role": "user", "content": f"ユーザーの返答:{user_text}"}) | |
| # モデルのチャットテンプレートを使用してプロンプトをトークン化/フォーマット | |
| # StableBelugaなど、モデルによってはカスタムテンプレートが必要 | |
| prompt = pipe.tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) | |
| # テキスト生成 | |
| outputs = pipe( | |
| prompt, | |
| max_new_tokens=800, | |
| do_sample=True, | |
| temperature=0.7, | |
| top_p=0.9, | |
| # 'prompt'の長さを考慮して生成されたテキストのみを取得 | |
| return_full_text=False | |
| ) | |
| # 生成されたテキストの先頭に余分な改行や空白がある場合は除去 | |
| raw_response = outputs[0]['generated_text'].strip() | |
| return raw_response | |
| def format_assistant_response(raw_text: str) -> str: | |
| """ | |
| 生のAI応答テキストを解析し、GradioのMarkdown/HTML用に整形 | |
| """ | |
| p = parse_sections(raw_text) | |
| # スコアバッジと評価 | |
| output = f"## 💸 リングサイド評価\n\n" | |
| output += f"<div style='background-color:#ffe0b2; border-radius:5px; padding:10px; margin-bottom:15px; border-left: 5px solid #ff9800;'>\n" | |
| output += f"**スコア**: <span style='font-size:1.5em; font-weight:bold; color:#d32f2f;'>{p['score']}</span> / 100\n\n" | |
| output += f"**講評**: {p['comment']}\n" | |
| output += "</div>\n\n" | |
| # 仮想敵の返答 | |
| output += f"### 😈 仮想敵の返答\n" | |
| output += f"<div style='background-color:#fce4ec; border-radius:5px; padding:10px; margin-bottom:15px; border-left: 5px solid #e91e63;'>\n" | |
| output += f"**{p['enemy']}**\n" | |
| output += "</div>\n\n" | |
| # コーチの解説 | |
| output += f"### 💡 コーチの解説\n" | |
| # カテゴリ | |
| if p.get("categories"): | |
| output += "**攻撃パターン**: " | |
| category_html = "".join([f"<span style='background-color:#e0f7fa; color:#00bcd4; padding:3px 8px; margin-right:5px; border-radius:3px;'>{cat}</span>" for cat in p['categories']]) | |
| output += category_html + "\n\n" | |
| # 心理解説 | |
| output += f"<div style='background-color:#e8f5e9; border-radius:5px; padding:10px; margin-bottom:15px; border-left: 5px solid #4caf50;'>\n" | |
| output += f"**心理解説**: {p['psy']}\n" | |
| output += "</div>\n\n" | |
| # 模範返答例 | |
| ex1, ex2 = p.get("ex1","").strip(), p.get("ex2","").strip() | |
| if ex1 or ex2: | |
| output += "**模範返答例**\n" | |
| if ex1: output += f"- **例1**: `{ex1}`\n" | |
| if ex2: output += f"- **例2**: `{ex2}`\n" | |
| return output, raw_text # 整形済みテキストとRAWテキストを返す | |
| # ---------- Gradio UIとロジック ---------- | |
| def chat(user_input, history): | |
| """Gradioチャットボットのメイン処理関数""" | |
| # ユーザー入力をチャット履歴に追加 | |
| # Gradioのhistoryは [(user_input, assistant_output), ...] の形式 | |
| # ここでは、assistant_outputには直前のAI応答のRAWテキストを保存する(次のターンで利用するため) | |
| # 履歴から、最後のAI応答のRAWテキストを取得する | |
| # user_inputの直前のAI応答は、前のターンのhistory[-1][1]に入っている | |
| # API呼び出し | |
| raw_response = call_hf_api(user_input, [(u, r) for u, (f, r) in history]) | |
| # 応答の整形とRAWテキストの取得 | |
| formatted_response, new_raw_text = format_assistant_response(raw_response) | |
| # GradioのChatbotに表示するため、ユーザーのメッセージと整形済みAI応答を返す | |
| # ユーザーメッセージ: user_input | |
| # AI応答: (formatted_response, new_raw_text) のタプル (formatted_responseが表示され、new_raw_textが次のターンでhistoryに保存される) | |
| # history.append([user_input, (formatted_response, new_raw_text)]) | |
| # return history | |
| # GradioのChatbotは、履歴をタプルで管理するため、少し複雑になる | |
| # historyには (user_input, [formatted_response, raw_text]) が入る | |
| # ユーザーのメッセージと整形済みAI応答を返す | |
| return history + [[user_input, (formatted_response, new_raw_text)]] | |
| # UIの構成 | |
| with gr.Blocks(title="仮想敵スパーリング(借金キャラ編) - Hugging Face/Gradio版") as demo: | |
| gr.HTML("<h1>💸🥊 仮想敵スパーリング(借金キャラ編)</h1>") | |
| if ISDUMMY_MODE: | |
| gr.Warning("⚠️ ダミーモードで実行中: モデルのロードに失敗しました。AI応答は固定です。") | |
| gr.Markdown(""" | |
| **ようこそ、仮想敵スパーリング(借金キャラ編)へ!** | |
| ここでは「しつこく借金を迫る仮想敵」と、会話のスパーリングができます。 | |
| コーチが戦術を解説し、模範例も教えてくれます。**さあ、スパーリング開始!**🔔 | |
| """) | |
| # Chatbotコンポーネント | |
| # `postprocess`関数で、表示用と保存用の2つの値をタプルで返すことで、 | |
| # 履歴に生データを保存しつつ、整形済みデータを表示できる | |
| chatbot = gr.Chatbot( | |
| label="スパーリング会場", | |
| height=500, | |
| # Gradio 4.0以降のカスタムレンダリング | |
| # AI応答がタプル [(formatted_response, raw_text)] の形式で渡されることを想定 | |
| render_markdown=True, | |
| show_copy_button=True, | |
| # 表示にはタプルの最初の要素 (formatted_response) を使用 | |
| postprocess=lambda x: x[0] if isinstance(x, tuple) and len(x) == 2 else x | |
| ) | |
| # テキスト入力とボタン | |
| with gr.Row(): | |
| msg = gr.Textbox(label="あなたの一手を送る(短くてOK)", scale=4) | |
| clear = gr.Button("クリア", scale=1) | |
| # 応答の初期設定 (初回ガイダンス) | |
| # 最初のメッセージはChatbotの初期値として設定 | |
| initial_message = "相手はしつこい借金キャラ。あなたは一言で返してみてください。" | |
| # historyの初期値は空 | |
| # ユーザーが最初のメッセージを入力した時に chat 関数が実行される | |
| # イベントハンドラ | |
| msg.submit(chat, [msg, chatbot], [chatbot], queue=False).then(lambda: gr.update(value=''), None, [msg]) | |
| clear.click(lambda: None, None, chatbot, queue=False) | |
| # アプリの起動 | |
| if __name__ == "__main__": | |
| # Gradioはデフォルトで80.0.0.0で公開されます | |
| # ローカルで起動するには、通常は `demo.launch()` で十分 | |
| demo.launch() |