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"
\n" output += f"**スコア**: {p['score']} / 100\n\n" output += f"**講評**: {p['comment']}\n" output += "
\n\n" # 仮想敵の返答 output += f"### 😈 仮想敵の返答\n" output += f"
\n" output += f"**{p['enemy']}**\n" output += "
\n\n" # コーチの解説 output += f"### 💡 コーチの解説\n" # カテゴリ if p.get("categories"): output += "**攻撃パターン**: " category_html = "".join([f"{cat}" for cat in p['categories']]) output += category_html + "\n\n" # 心理解説 output += f"
\n" output += f"**心理解説**: {p['psy']}\n" output += "
\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("

💸🥊 仮想敵スパーリング(借金キャラ編)

") 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()