Spaces:
Paused
Paused
| """ | |
| app.py | |
| ------ | |
| Gradio demo for NPCAlign: sequential blind evaluation of SFT vs DPO models. | |
| Flow: | |
| Step 1 Configure NPC character | |
| Step 2 Chat with Model 1 (SFT or DPO, randomly assigned) | |
| Step 3 Rate Model 1 on 5 dimensions | |
| Step 4 Chat with Model 2 (the other model) | |
| Step 5 Rate Model 2 on 5 dimensions | |
| Step 6 Submit โ ratings saved to Storage Bucket โ restart option | |
| Conversation safety net: | |
| - Turn >= SOFT_HINT_TURN : hidden system hint asking model to wrap up | |
| - Turn == MAX_TURNS - 1 : hard departure signal appended to user input | |
| - Turn >= MAX_TURNS : conversation forcibly ended after NPC responds | |
| - <End> tag detection : model signals its own farewell proactively | |
| """ | |
| import json | |
| import random | |
| import re | |
| import uuid | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| import gradio as gr | |
| from inference import ( | |
| MAX_TURNS, | |
| build_messages, | |
| build_npc_fields, | |
| detect_end, | |
| generate_response, | |
| ) | |
| # โโ NPC response display formatter โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def _bubbles(text: str) -> list[str]: | |
| """ | |
| Split one NPC response into a list of individual chat bubble strings. | |
| Strategy: | |
| - *action descriptions* โ each becomes its own blockquote bubble. | |
| - Dialogue text is split into chunks of SENTENCES_PER_BUBBLE sentences so | |
| that long monologues are broken up even when the model outputs no newlines. | |
| The raw `text` stored in `history` (passed to the model) is NOT changed โ | |
| only the display list fed to gr.Chatbot uses this function. | |
| Example | |
| ------- | |
| Input : '*Leans wearily.* The cave is dark. We need help urgently. Please go.' | |
| Output (2 sentences/bubble): | |
| ['> *Leans wearily.*', 'The cave is dark. We need help urgently.', 'Please go.'] | |
| """ | |
| # Match both *action* and (action) formats as action descriptions. | |
| # DPO model sometimes uses (...) due to Llama's pretraining on screenplay text. | |
| ACTION_RE = re.compile(r'(\*[^*\n]+\*|\([^)\n]{4,80}\))') | |
| def _is_action(s: str) -> bool: | |
| return (s.startswith('*') and s.endswith('*')) or \ | |
| (s.startswith('(') and s.endswith(')')) | |
| def _format_action(s: str) -> str: | |
| """Wrap action in blockquote; normalise (...) to *...* for italics.""" | |
| if s.startswith('(') and s.endswith(')'): | |
| return f'> *{s[1:-1].strip()}*' | |
| return f'> {s}' | |
| def _segment_to_bubbles(segment: str) -> list[str]: | |
| """ | |
| Split one newline-free segment into bubbles: | |
| - action descriptions become their own blockquote bubble | |
| - remaining dialogue is ONE bubble (the model already put each idea | |
| on its own line, so no further sentence-splitting is needed here) | |
| """ | |
| parts = ACTION_RE.split(segment) | |
| result = [] | |
| dialogue_parts = [] | |
| for p in parts: | |
| s = p.strip() | |
| if not s: | |
| continue | |
| if _is_action(s): | |
| if dialogue_parts: | |
| result.append(' '.join(dialogue_parts)) | |
| dialogue_parts = [] | |
| result.append(_format_action(s)) | |
| else: | |
| dialogue_parts.append(s) | |
| if dialogue_parts: | |
| result.append(' '.join(dialogue_parts)) | |
| return result | |
| # Primary split: use the model's own paragraph breaks (\n or \n\n). | |
| # Each non-empty line/paragraph becomes its own bubble. | |
| # Fallback: if no newlines exist, treat the whole text as one segment. | |
| segments = [s.strip() for s in re.split(r'\n+', text) if s.strip()] | |
| bubbles = [] | |
| for seg in segments: | |
| bubbles.extend(_segment_to_bubbles(seg)) | |
| return bubbles if bubbles else [text.strip()] | |
| # โโ Storage โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # Use the mounted Storage Bucket path on HF Spaces; fall back to a local dir. | |
| _RATINGS_DIR = Path("/data/ratings") if Path("/data").exists() else Path("./local_ratings") | |
| _RATINGS_DIR.mkdir(parents=True, exist_ok=True) | |
| # โโ Translations โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| _T = { | |
| "en": { | |
| # Step 1 | |
| "step1_header": "## Step 1 โ Configure Your NPC", | |
| "step1_inst": ( | |
| "Fill in your NPC's details. All fields except **Occupation** are required. \n" | |
| "**Personality**, **Occupation** (if any), and **Experience** are combined into the " | |
| "character's Background. \n\n" | |
| "> โ All inputs must be written in **English**." | |
| ), | |
| "example_title": "๐ Example / ็คบไพ", | |
| "example_md": ( | |
| "| Field | Value |\n" | |
| "|---|---|\n" | |
| "| **Name** | Chief Hawkwind |\n" | |
| "| **Personality** | Wise, fatherly, speaks with quiet authority |\n" | |
| "| **Experience** | Trusted tribal leader for decades, believes in collective strength |\n" | |
| "| **Occupation** | Tribal Chief *(optional)* |\n" | |
| "| **Location** | Camp Narache โ verdant hills, near the central gathering fire |\n" | |
| "| **Quest** | Your elderly mother went to the water well this morning and hasn't returned. " | |
| "Ask the player to check on her. |" | |
| ), | |
| "name_label": "Name *", | |
| "name_ph": "e.g. Chief Hawkwind", | |
| "personality_label": "Personality *", | |
| "personality_ph": "e.g. Wise, fatherly, speaks with quiet authority", | |
| "experience_label": "Experience / Background *", | |
| "experience_ph": "e.g. Trusted tribal leader for decades, believes in collective strength", | |
| "occupation_label": "Occupation (optional)", | |
| "occupation_ph": "e.g. Tribal Chief", | |
| "location_label": "Current Location *", | |
| "location_ph": "e.g. Camp Narache โ near the central gathering fire", | |
| "quest_label": "Quest *", | |
| "quest_ph": "Describe the task or goal the NPC needs the player's help with", | |
| "start_btn": "โถ Start Evaluation", | |
| "fill_required": "โ Please fill in all required fields (Name, Personality, Experience, Location, Quest).", | |
| "model_loading": "โณ Loading model for the first time โ this may take up to 60 seconds. Please waitโฆ", | |
| # Step 2 / 4 | |
| "chat_header_1": "## Step 2 โ Chat with Model 1", | |
| "chat_header_2": "## Step 4 โ Chat with Model 2", | |
| "chat_inst": ( | |
| "Have a natural conversation with the NPC. \n" | |
| "> *Italic blocks* in responses indicate the NPC's action descriptions. \n" | |
| "When you feel the quest has been fully introduced, you may end the conversation early." | |
| ), | |
| "user_input_ph": "Type your message here (in English)โฆ", | |
| "send_btn": "Send", | |
| "end_conv_btn": "โน End Conversation", | |
| "end_notice": "๐ The conversation has ended.", | |
| "rate_btn_1": "Rate Model 1 โ", | |
| "rate_btn_2": "Rate Model 2 โ", | |
| # Step 3 / 5 | |
| "rate_header_1": "## Step 3 โ Rate Model 1", | |
| "rate_header_2": "## Step 5 โ Rate Model 2", | |
| "rate_inst": "Rate each dimension from **1 (poor)** to **5 (excellent)**.", | |
| "dim_cons": "1. Character Consistency โ Does the NPC's tone and wording match the background description?", | |
| "dim_flu": "2. Dialogue Fluency โ Are responses natural without awkward transitions?", | |
| "dim_quest": "3. Quest Completeness โ Is the quest described clearly and completely?", | |
| "dim_end": "4. Ending Quality โ Did the NPC end the conversation naturally at the right time?", | |
| "dim_over": "5. Overall Experience โ Overall impression as a game quest NPC", | |
| "next_btn": "Next: Chat with Model 2 โ", | |
| "submit_btn": "โ Submit Ratings", | |
| # Step 6 | |
| "done_header": "## โ Thank You!", | |
| "done_text": "Your ratings have been saved. You can start a new evaluation session below.", | |
| "restart_btn": "๐ Start Again", | |
| "submit_error": "โ Ratings could not be saved. Please try again.", | |
| }, | |
| "zh": { | |
| # Step 1 | |
| "step1_header": "## ็ฌฌไธๆญฅ โ ้ ็ฝฎไฝ ็ NPC", | |
| "step1_inst": ( | |
| "่ฏทๅกซๅ NPC ็่ฏฆ็ปไฟกๆฏใ้ค**่ไธ**ๅค๏ผๆๆๅญๆฎตๅไธบๅฟ ๅกซใ \n" | |
| "**ไธชๆง็น็น**ใ**่ไธ**๏ผๅฆๆ๏ผไธ**่ฟๅพ็ปๅ**ๅฐ็ปๅๆ่ง่ฒ่ๆฏๅญๆฎตใ \n\n" | |
| "> โ ๆๆ่พๅ ฅๅ ๅฎนๅฟ ้กปไฝฟ็จ**่ฑๆ**ๅกซๅใ" | |
| ), | |
| "example_title": "๐ Example / ็คบไพ", | |
| "example_md": ( | |
| "| ๅญๆฎต | ็คบไพๅ ๅฎน |\n" | |
| "|---|---|\n" | |
| "| **Name** | Chief Hawkwind |\n" | |
| "| **Personality** | Wise, fatherly, speaks with quiet authority |\n" | |
| "| **Experience** | Trusted tribal leader for decades, believes in collective strength |\n" | |
| "| **Occupation** | Tribal Chief *๏ผ้ๅกซ๏ผ* |\n" | |
| "| **Location** | Camp Narache โ verdant hills, near the central gathering fire |\n" | |
| "| **Quest** | Your elderly mother went to the water well this morning and hasn't returned. " | |
| "Ask the player to check on her. |" | |
| ), | |
| "name_label": "Name / ่ง่ฒๅ็งฐ *", | |
| "name_ph": "ไพ๏ผChief Hawkwind", | |
| "personality_label": "Personality / ไธชๆง็น็น *", | |
| "personality_ph": "ไพ๏ผWise, fatherly, speaks with quiet authority", | |
| "experience_label": "Experience / ่ฟๅพ็ปๅ *", | |
| "experience_ph": "ไพ๏ผTrusted tribal leader for decades, believes in collective strength", | |
| "occupation_label": "Occupation / ่ไธ๏ผ้ๅกซ๏ผ", | |
| "occupation_ph": "ไพ๏ผTribal Chief", | |
| "location_label": "Current Location / ๅฝๅๅฐ็น *", | |
| "location_ph": "ไพ๏ผCamp Narache โ near the central gathering fire", | |
| "quest_label": "Quest / ไปปๅก็ฎๆ *", | |
| "quest_ph": "ๆ่ฟฐ NPC ้่ฆ็ฉๅฎถๅธฎๅฉๅฎๆ็ไปปๅก็ฎๆ ๆๅ ๅฎน", | |
| "start_btn": "โถ ๅผๅงๆต่ฏ", | |
| "fill_required": "โ ่ฏทๅกซๅๆๆๅฟ ๅกซๅญๆฎต๏ผNameใPersonalityใExperienceใLocationใQuest๏ผใ", | |
| "model_loading": "โณ ๆญฃๅจ้ฆๆฌกๅ ่ฝฝๆจกๅ๏ผๆๅค้่ฆ 60 ็ง๏ผ่ฏท็จๅโฆโฆ", | |
| # Step 2 / 4 | |
| "chat_header_1": "## ็ฌฌไบๆญฅ โ ไธๆจกๅ 1 ๅฏน่ฏ", | |
| "chat_header_2": "## ็ฌฌๅๆญฅ โ ไธๆจกๅ 2 ๅฏน่ฏ", | |
| "chat_inst": ( | |
| "ไธ NPC ่ฟ่ก่ช็ถๅฏน่ฏใ \n" | |
| "> ๅๅคไธญ็*ๆไฝๅ*ไธบ NPC ็ๅจไฝๆ่ฟฐใ \n" | |
| "ๅฆๆไฝ ่ฎคไธบไปปๅกๅทฒไป็ปๅฎๆฏ๏ผๅฏไปฅไธปๅจ็ปๆๅฏน่ฏใ" | |
| ), | |
| "user_input_ph": "ๅจๆญค่พๅ ฅๆถๆฏ๏ผ่ฏท็จ่ฑๆ๏ผโฆ", | |
| "send_btn": "ๅ้", | |
| "end_conv_btn": "โน ็ปๆๅฏน่ฏ", | |
| "end_notice": "๐ ๅฏน่ฏๅทฒ็ปๆใ", | |
| "rate_btn_1": "ไธบๆจกๅ 1 ่ฏๅ โ", | |
| "rate_btn_2": "ไธบๆจกๅ 2 ่ฏๅ โ", | |
| # Step 3 / 5 | |
| "rate_header_1": "## ็ฌฌไธๆญฅ โ ไธบๆจกๅ 1 ่ฏๅ", | |
| "rate_header_2": "## ็ฌฌไบๆญฅ โ ไธบๆจกๅ 2 ่ฏๅ", | |
| "rate_inst": "่ฏทๅฏนๆฏไธช็ปดๅบฆ่ฟ่ก่ฏๅ๏ผ**1 ๅ๏ผๅพๅทฎ๏ผ** ๅฐ **5 ๅ๏ผๅพๅฅฝ๏ผ**ใ", | |
| "dim_cons": "1. ่ง่ฒไธ่ดๆง โ NPC ็่ฏญๆฐไธๆช่พๆฏๅฆ็ฌฆๅ่ๆฏๆ่ฟฐ๏ผ", | |
| "dim_flu": "2. ๅฏน่ฏๆต็ ๅบฆ โ ๅๅคๆฏๅฆ่ช็ถ๏ผๆฒกๆ็ชๅ ๆ๏ผ", | |
| "dim_quest": "3. ไปปๅกไฟกๆฏๅฎๆดๆง โ ไปปๅก็ฎๆ ๆฏๅฆๆ่ฟฐๆธ ๆฅๅฎๆด๏ผ", | |
| "dim_end": "4. ็ปๆๅฏน่ฏๆฐๅฝๆง โ NPC ๆฏๅฆๅจๅ้ๆถๆบ่ช็ถๅๅซ๏ผ", | |
| "dim_over": "5. ๆดไฝไฝ้ช โ ไฝไธบๆธธๆไปปๅก NPC ็็ปผๅๅฐ่ฑก", | |
| "next_btn": "ไธไธๆญฅ๏ผไธๆจกๅ 2 ๅฏน่ฏ โ", | |
| "submit_btn": "โ ๆไบค่ฏๅ", | |
| # Step 6 | |
| "done_header": "## โ ๆ่ฐขๆจ็ๅไธ๏ผ", | |
| "done_text": "ๆจ็่ฏๅๅทฒไฟๅญใๆจๅฏไปฅๅจไธๆนๅผๅงๆฐไธ่ฝฎๆต่ฏใ", | |
| "restart_btn": "๐ ้ๆฐๅผๅง", | |
| "submit_error": "โ ่ฏๅไฟๅญๅคฑ่ดฅ๏ผ่ฏท้่ฏใ", | |
| }, | |
| } | |
| def _t(lang: str, key: str, **fmt) -> str: | |
| text = _T.get(lang, _T["en"]).get(key, _T["en"].get(key, key)) | |
| return text.format(**fmt) if fmt else text | |
| # โโ Persistence โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def _save_ratings(session_id, npc_config, model_order, | |
| conv1, conv2, ratings1, ratings2) -> bool: | |
| """Write one complete evaluation session as JSON to the ratings directory.""" | |
| record = { | |
| "session_id": session_id, | |
| "timestamp": datetime.now(timezone.utc).isoformat(), | |
| "npc_config": npc_config, | |
| "model_order": model_order, | |
| "model_1_ratings": ratings1, | |
| "model_2_ratings": ratings2, | |
| "model_1_conversation": conv1, | |
| "model_2_conversation": conv2, | |
| } | |
| try: | |
| (_RATINGS_DIR / f"{session_id}.json").write_text( | |
| json.dumps(record, ensure_ascii=False, indent=2), encoding="utf-8" | |
| ) | |
| return True | |
| except Exception as exc: | |
| print(f"[WARN] Rating save failed: {exc}") | |
| return False | |
| # โโ Core chat helper โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def _chat_step( | |
| user_text: str, | |
| chatbot: list, | |
| history: list, | |
| turn: int, | |
| ended: bool, | |
| npc_fields: dict, | |
| adapter: str, | |
| lang: str, | |
| ): | |
| """ | |
| Process one user message: append to history, generate NPC response, detect end. | |
| Hard departure signal: when turn == MAX_TURNS - 1, appends a gentle | |
| departure cue to the user's message so the model receives a natural | |
| closing context before the final forced turn. | |
| Returns a 9-tuple: | |
| (chatbot, history, turn, ended, cleared_input, | |
| send_btn_update, end_conv_btn_update, end_notice_update, rate_btn_update) | |
| """ | |
| no_op = (chatbot, history, turn, ended, user_text, | |
| gr.update(), gr.update(), gr.update(), gr.update()) | |
| if ended or not user_text.strip(): | |
| return no_op | |
| # Hard departure signal on penultimate turn | |
| effective = user_text | |
| if turn == MAX_TURNS - 1: | |
| effective = user_text + " (The player nods, ready to depart.)" | |
| history = history + [{"role": "user", "content": effective}] | |
| next_turn = turn + 1 | |
| try: | |
| msgs = build_messages(npc_fields, history, next_turn) | |
| raw = generate_response(adapter, msgs) | |
| except Exception as exc: | |
| err_bubble = f"โ Generation error: {exc}" | |
| chatbot = chatbot + [ | |
| {"role": "user", "content": user_text}, | |
| {"role": "assistant", "content": err_bubble}, | |
| ] | |
| return (chatbot, history[:-1], turn, ended, "", | |
| gr.update(), gr.update(), gr.update(), gr.update()) | |
| clean, is_end = detect_end(raw) | |
| if next_turn >= MAX_TURNS: | |
| is_end = True | |
| history = history + [{"role": "assistant", "content": clean}] | |
| # history keeps raw text for model context; chatbot display splits into | |
| # individual bubbles (actions + dialogue paragraphs) via _bubbles(). | |
| chatbot = chatbot + [{"role": "user", "content": user_text}] + [ | |
| {"role": "assistant", "content": b} for b in _bubbles(clean) | |
| ] | |
| end_txt = _t(lang, "end_notice") if is_end else "" | |
| return ( | |
| chatbot, | |
| history, | |
| next_turn, | |
| is_end, | |
| "", # clear input box | |
| gr.update(interactive=not is_end), # send button | |
| gr.update(visible=not is_end), # end_conv_btn hidden when ended | |
| gr.update(value=end_txt, visible=is_end), # end notice | |
| gr.update(visible=is_end), # rate button | |
| ) | |
| # โโ Gradio app โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| with gr.Blocks() as demo: | |
| # โโ Global header โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| with gr.Row(): | |
| gr.Markdown("# ๐ฎ NPCAlign โ NPC Quest Dialogue Demo") | |
| lang_radio = gr.Radio( | |
| choices=["English", "ไธญๆ"], | |
| value="English", | |
| label="Language / ่ฏญ่จ", | |
| scale=0, | |
| min_width=160, | |
| ) | |
| gr.Markdown( | |
| "Compare two NPC dialogue models fine-tuned on RPG quest conversations. " | |
| "Neither model is identified โ rate them blind and see which one you prefer. " | |
| "ๆฏ่พไธคไธชๆ นๆฎ่ง่ฒๆฎๆผๆธธๆไปปๅกๅฏน่ฏๅพฎ่ฐ็NPCๅฏน่ฏๆจกๅ" | |
| "่ฟไธคไธชๆจกๅๅๆชๆ ๆๅ็งฐโโ่ฏท็ฒๆต๏ผ็็ไฝ ๆดๅๆฌขๅชไธไธช" | |
| ) | |
| # โโ Step 1: NPC Configuration โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| with gr.Column(visible=True) as col_step1: | |
| step1_hdr = gr.Markdown(_T["en"]["step1_header"]) | |
| step1_inst = gr.Markdown(_T["en"]["step1_inst"]) | |
| with gr.Accordion(_T["en"]["example_title"], open=False) as example_acc: | |
| example_md = gr.Markdown(_T["en"]["example_md"]) | |
| name_inp = gr.Textbox(label=_T["en"]["name_label"], | |
| placeholder=_T["en"]["name_ph"], | |
| max_length=100, info="Max 100 characters / ๆๅค100ๅญ็ฌฆ") | |
| personality_inp = gr.Textbox(label=_T["en"]["personality_label"], | |
| placeholder=_T["en"]["personality_ph"], | |
| lines=2, max_length=400, info="Max 400 characters / ๆๅค400ๅญ็ฌฆ") | |
| experience_inp = gr.Textbox(label=_T["en"]["experience_label"], | |
| placeholder=_T["en"]["experience_ph"], | |
| lines=2, max_length=400, info="Max 400 characters / ๆๅค400ๅญ็ฌฆ") | |
| occupation_inp = gr.Textbox(label=_T["en"]["occupation_label"], | |
| placeholder=_T["en"]["occupation_ph"], | |
| max_length=150, info="Max 150 characters / ๆๅค150ๅญ็ฌฆ") | |
| location_inp = gr.Textbox(label=_T["en"]["location_label"], | |
| placeholder=_T["en"]["location_ph"], | |
| lines=2, max_length=400, info="Max 400 characters / ๆๅค400ๅญ็ฌฆ") | |
| quest_inp = gr.Textbox(label=_T["en"]["quest_label"], | |
| placeholder=_T["en"]["quest_ph"], | |
| lines=3, max_length=400, info="Max 400 characters / ๆๅค400ๅญ็ฌฆ") | |
| start_err = gr.Markdown("", visible=False) | |
| start_loading = gr.Markdown("", visible=False) | |
| start_btn = gr.Button(_T["en"]["start_btn"], variant="primary") | |
| # โโ Step 2: Model 1 Chat โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| with gr.Column(visible=False) as col_step2: | |
| chat1_hdr = gr.Markdown(_T["en"]["chat_header_1"]) | |
| chat1_inst = gr.Markdown(_T["en"]["chat_inst"]) | |
| chatbot_1 = gr.Chatbot(height=450, label="", group_consecutive_messages=False) | |
| with gr.Row(): | |
| user_inp_1 = gr.Textbox( | |
| placeholder=_T["en"]["user_input_ph"], | |
| show_label=False, scale=5, lines=1, | |
| max_length=500, | |
| ) | |
| send_btn_1 = gr.Button(_T["en"]["send_btn"], scale=1, variant="primary") | |
| char_count_1 = gr.Markdown("0 / 500", elem_classes=["char-counter"]) | |
| end_conv_btn_1 = gr.Button(_T["en"]["end_conv_btn"], variant="secondary", visible=False) | |
| end_notice_1 = gr.Markdown("", visible=False) | |
| rate_btn_1 = gr.Button(_T["en"]["rate_btn_1"], visible=False, variant="primary") | |
| # โโ Step 3: Model 1 Rating โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| with gr.Column(visible=False) as col_step3: | |
| rate1_hdr = gr.Markdown(_T["en"]["rate_header_1"]) | |
| rate1_inst = gr.Markdown(_T["en"]["rate_inst"]) | |
| r1_cons = gr.Slider(1, 5, step=1, value=3, label=_T["en"]["dim_cons"]) | |
| r1_flu = gr.Slider(1, 5, step=1, value=3, label=_T["en"]["dim_flu"]) | |
| r1_quest = gr.Slider(1, 5, step=1, value=3, label=_T["en"]["dim_quest"]) | |
| r1_end = gr.Slider(1, 5, step=1, value=3, label=_T["en"]["dim_end"]) | |
| r1_over = gr.Slider(1, 5, step=1, value=3, label=_T["en"]["dim_over"]) | |
| next_btn = gr.Button(_T["en"]["next_btn"], variant="primary") | |
| model2_loading = gr.Markdown("", visible=False) | |
| # โโ Step 4: Model 2 Chat โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| with gr.Column(visible=False) as col_step4: | |
| chat2_hdr = gr.Markdown(_T["en"]["chat_header_2"]) | |
| chat2_inst = gr.Markdown(_T["en"]["chat_inst"]) | |
| chatbot_2 = gr.Chatbot(height=450, label="", group_consecutive_messages=False) | |
| with gr.Row(): | |
| user_inp_2 = gr.Textbox( | |
| placeholder=_T["en"]["user_input_ph"], | |
| show_label=False, scale=5, lines=1, | |
| max_length=500, | |
| ) | |
| send_btn_2 = gr.Button(_T["en"]["send_btn"], scale=1, variant="primary") | |
| char_count_2 = gr.Markdown("0 / 500", elem_classes=["char-counter"]) | |
| end_conv_btn_2 = gr.Button(_T["en"]["end_conv_btn"], variant="secondary", visible=False) | |
| end_notice_2 = gr.Markdown("", visible=False) | |
| rate_btn_2 = gr.Button(_T["en"]["rate_btn_2"], visible=False, variant="primary") | |
| # โโ Step 5: Model 2 Rating โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| with gr.Column(visible=False) as col_step5: | |
| rate2_hdr = gr.Markdown(_T["en"]["rate_header_2"]) | |
| rate2_inst = gr.Markdown(_T["en"]["rate_inst"]) | |
| r2_cons = gr.Slider(1, 5, step=1, value=3, label=_T["en"]["dim_cons"]) | |
| r2_flu = gr.Slider(1, 5, step=1, value=3, label=_T["en"]["dim_flu"]) | |
| r2_quest = gr.Slider(1, 5, step=1, value=3, label=_T["en"]["dim_quest"]) | |
| r2_end = gr.Slider(1, 5, step=1, value=3, label=_T["en"]["dim_end"]) | |
| r2_over = gr.Slider(1, 5, step=1, value=3, label=_T["en"]["dim_over"]) | |
| submit_btn = gr.Button(_T["en"]["submit_btn"], variant="primary") | |
| submit_err = gr.Markdown("", visible=False) | |
| # โโ Step 6: Done โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| with gr.Column(visible=False) as col_step6: | |
| done_hdr = gr.Markdown(_T["en"]["done_header"]) | |
| done_text = gr.Markdown(_T["en"]["done_text"]) | |
| restart_btn = gr.Button(_T["en"]["restart_btn"]) | |
| # โโ Session state โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| lang_state = gr.State("en") | |
| session_state = gr.State(None) # str โ session UUID | |
| npc_cfg_state = gr.State(None) # dict โ raw form values | |
| order_state = gr.State(None) # dict โ {"model_1": "sft"/"dpo", "model_2": ...} | |
| npc_fld_state = gr.State(None) # dict โ processed npc_fields for build_messages | |
| history1_state = gr.State([]) | |
| history2_state = gr.State([]) | |
| turn1_state = gr.State(0) | |
| turn2_state = gr.State(0) | |
| ended1_state = gr.State(False) | |
| ended2_state = gr.State(False) | |
| ratings1_state = gr.State(None) # dict โ model 1 scores, held until submit | |
| # โโ Language change โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def _lang_updates(choice): | |
| lang = "zh" if choice == "ไธญๆ" else "en" | |
| T = _T[lang] | |
| return ( | |
| lang, | |
| gr.update(value=T["step1_header"]), | |
| gr.update(value=T["step1_inst"]), | |
| gr.update(label=T["example_title"]), | |
| gr.update(value=T["example_md"]), | |
| gr.update(label=T["name_label"], placeholder=T["name_ph"]), | |
| gr.update(label=T["personality_label"], placeholder=T["personality_ph"]), | |
| gr.update(label=T["experience_label"], placeholder=T["experience_ph"]), | |
| gr.update(label=T["occupation_label"], placeholder=T["occupation_ph"]), | |
| gr.update(label=T["location_label"], placeholder=T["location_ph"]), | |
| gr.update(label=T["quest_label"], placeholder=T["quest_ph"]), | |
| gr.update(value=T["start_btn"]), | |
| # Step 2 | |
| gr.update(value=T["chat_header_1"]), | |
| gr.update(value=T["chat_inst"]), | |
| gr.update(placeholder=T["user_input_ph"]), | |
| gr.update(value=T["send_btn"]), | |
| gr.update(value=T["end_conv_btn"]), | |
| gr.update(value=T["rate_btn_1"]), | |
| # Step 3 | |
| gr.update(value=T["rate_header_1"]), | |
| gr.update(value=T["rate_inst"]), | |
| gr.update(label=T["dim_cons"]), | |
| gr.update(label=T["dim_flu"]), | |
| gr.update(label=T["dim_quest"]), | |
| gr.update(label=T["dim_end"]), | |
| gr.update(label=T["dim_over"]), | |
| gr.update(value=T["next_btn"]), | |
| # Step 4 | |
| gr.update(value=T["chat_header_2"]), | |
| gr.update(value=T["chat_inst"]), | |
| gr.update(placeholder=T["user_input_ph"]), | |
| gr.update(value=T["send_btn"]), | |
| gr.update(value=T["end_conv_btn"]), | |
| gr.update(value=T["rate_btn_2"]), | |
| # Step 5 | |
| gr.update(value=T["rate_header_2"]), | |
| gr.update(value=T["rate_inst"]), | |
| gr.update(label=T["dim_cons"]), | |
| gr.update(label=T["dim_flu"]), | |
| gr.update(label=T["dim_quest"]), | |
| gr.update(label=T["dim_end"]), | |
| gr.update(label=T["dim_over"]), | |
| gr.update(value=T["submit_btn"]), | |
| # Step 6 | |
| gr.update(value=T["done_header"]), | |
| gr.update(value=T["done_text"]), | |
| gr.update(value=T["restart_btn"]), | |
| ) | |
| lang_radio.change( | |
| _lang_updates, | |
| inputs=[lang_radio], | |
| outputs=[ | |
| lang_state, | |
| step1_hdr, step1_inst, example_acc, example_md, | |
| name_inp, personality_inp, experience_inp, occupation_inp, | |
| location_inp, quest_inp, start_btn, | |
| chat1_hdr, chat1_inst, user_inp_1, send_btn_1, end_conv_btn_1, rate_btn_1, | |
| rate1_hdr, rate1_inst, | |
| r1_cons, r1_flu, r1_quest, r1_end, r1_over, next_btn, | |
| chat2_hdr, chat2_inst, user_inp_2, send_btn_2, end_conv_btn_2, rate_btn_2, | |
| rate2_hdr, rate2_inst, | |
| r2_cons, r2_flu, r2_quest, r2_end, r2_over, submit_btn, | |
| done_hdr, done_text, restart_btn, | |
| ], | |
| ) | |
| # โโ Chat input character counters โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| user_inp_1.change( | |
| lambda t: f"{len(t)} / 500", | |
| inputs=[user_inp_1], | |
| outputs=[char_count_1], | |
| ) | |
| user_inp_2.change( | |
| lambda t: f"{len(t)} / 500", | |
| inputs=[user_inp_2], | |
| outputs=[char_count_2], | |
| ) | |
| # โโ Start (Step 1 โ Step 2) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def _start(name, personality, experience, occupation, location, quest, lang): | |
| """Generator: yields a loading message first, then the real result.""" | |
| T = _T.get(lang, _T["en"]) | |
| # โโ Validation โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| if not all([name.strip(), personality.strip(), experience.strip(), | |
| location.strip(), quest.strip()]): | |
| yield ( | |
| None, None, None, None, | |
| [], [], 0, 0, False, False, | |
| gr.update(value=T["fill_required"], visible=True), | |
| gr.update(visible=False), # start_loading | |
| gr.update(visible=True), # col_step1 | |
| gr.update(visible=False), # col_step2 | |
| gr.update(value=[]), # chatbot_1 | |
| gr.update(visible=False), # end_conv_btn_1 | |
| gr.update(visible=False), # end_notice_1 | |
| gr.update(visible=False), # rate_btn_1 | |
| gr.update(interactive=True), # send_btn_1 | |
| ) | |
| return | |
| npc_config = dict(name=name, personality=personality, experience=experience, | |
| occupation=occupation, location=location, quest=quest) | |
| npc_fields = build_npc_fields(name, personality, experience, | |
| occupation, location, quest) | |
| model_order = random.choice([ | |
| {"model_1": "sft", "model_2": "dpo"}, | |
| {"model_1": "dpo", "model_2": "sft"}, | |
| ]) | |
| session_id = str(uuid.uuid4()) | |
| # โโ First yield: show loading notice while waiting for model โโโโโโโโ | |
| yield ( | |
| None, None, None, None, | |
| [], [], 0, 0, False, False, | |
| gr.update(value="", visible=False), | |
| gr.update(value=T["model_loading"], visible=True), # start_loading | |
| gr.update(visible=True), # col_step1 | |
| gr.update(visible=False), # col_step2 | |
| gr.update(value=[]), # chatbot_1 | |
| gr.update(visible=False), # end_conv_btn_1 | |
| gr.update(visible=False), # end_notice_1 | |
| gr.update(visible=False), # rate_btn_1 | |
| gr.update(interactive=False), # send_btn_1 disabled while loading | |
| ) | |
| # โโ Auto-send opening "Greetings" turn โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| history = [{"role": "user", "content": "Greetings"}] | |
| try: | |
| msgs = build_messages(npc_fields, history, turn=1) | |
| raw = generate_response(model_order["model_1"], msgs) | |
| except Exception as exc: | |
| err = ( | |
| f"โ Model loading failed ({type(exc).__name__}): {str(exc)[:300]}\n\n" | |
| "**Possible causes:**\n" | |
| "1. Llama 3.1 license not accepted โ " | |
| "https://huggingface.co/meta-llama/Meta-Llama-3.1-8B-Instruct\n" | |
| "2. HF_TOKEN not set or lacks read access\n" | |
| "3. Adapter repo files missing\n\n" | |
| "Check the **Space Logs** for the full error traceback." | |
| ) | |
| yield ( | |
| None, None, None, None, | |
| [], [], 0, 0, False, False, | |
| gr.update(value=err, visible=True), | |
| gr.update(visible=False), # start_loading | |
| gr.update(visible=True), # col_step1 | |
| gr.update(visible=False), # col_step2 | |
| gr.update(value=[]), # chatbot_1 | |
| gr.update(visible=False), # end_conv_btn_1 | |
| gr.update(visible=False), # end_notice_1 | |
| gr.update(visible=False), # rate_btn_1 | |
| gr.update(interactive=True), # send_btn_1 | |
| ) | |
| return | |
| clean, is_end = detect_end(raw) | |
| history = history + [{"role": "assistant", "content": clean}] | |
| chatbot = [{"role": "user", "content": "Greetings"}] + [ | |
| {"role": "assistant", "content": b} for b in _bubbles(clean) | |
| ] | |
| end_txt = T["end_notice"] if is_end else "" | |
| yield ( | |
| npc_config, model_order, npc_fields, session_id, | |
| history, [], 1, 0, is_end, False, | |
| gr.update(value="", visible=False), | |
| gr.update(visible=False), # start_loading cleared | |
| gr.update(visible=False), # col_step1 | |
| gr.update(visible=True), # col_step2 | |
| gr.update(value=chatbot), | |
| gr.update(visible=not is_end), # end_conv_btn_1 shown unless already ended | |
| gr.update(value=end_txt, visible=is_end), | |
| gr.update(visible=is_end), | |
| gr.update(interactive=not is_end), | |
| ) | |
| _start_outputs = [ | |
| npc_cfg_state, order_state, npc_fld_state, session_state, | |
| history1_state, history2_state, turn1_state, turn2_state, | |
| ended1_state, ended2_state, | |
| start_err, start_loading, | |
| col_step1, col_step2, | |
| chatbot_1, end_conv_btn_1, end_notice_1, rate_btn_1, send_btn_1, | |
| ] | |
| start_btn.click( | |
| _start, | |
| inputs=[name_inp, personality_inp, experience_inp, occupation_inp, | |
| location_inp, quest_inp, lang_state], | |
| outputs=_start_outputs, | |
| ) | |
| # โโ Model 1 chat (Step 2) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def _send1(user_text, chatbot, history, turn, ended, npc_fields, order, lang): | |
| adapter = (order or {}).get("model_1", "sft") | |
| return _chat_step(user_text, chatbot, history, turn, ended, | |
| npc_fields, adapter, lang) | |
| _chat1_inputs = [user_inp_1, chatbot_1, history1_state, turn1_state, | |
| ended1_state, npc_fld_state, order_state, lang_state] | |
| _chat1_outputs = [chatbot_1, history1_state, turn1_state, ended1_state, | |
| user_inp_1, send_btn_1, end_conv_btn_1, end_notice_1, rate_btn_1] | |
| send_btn_1.click(_send1, inputs=_chat1_inputs, outputs=_chat1_outputs).then( | |
| lambda: "0 / 500", outputs=[char_count_1] | |
| ) | |
| user_inp_1.submit(_send1, inputs=_chat1_inputs, outputs=_chat1_outputs).then( | |
| lambda: "0 / 500", outputs=[char_count_1] | |
| ) | |
| # โโ End Conversation button (Model 1) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def _end_conv_1(lang): | |
| """User manually ends Model 1 conversation.""" | |
| T = _T.get(lang, _T["en"]) | |
| return ( | |
| True, | |
| gr.update(interactive=False), | |
| gr.update(visible=False), | |
| gr.update(value=T["end_notice"], visible=True), | |
| gr.update(visible=True), | |
| ) | |
| end_conv_btn_1.click( | |
| _end_conv_1, | |
| inputs=[lang_state], | |
| outputs=[ended1_state, send_btn_1, end_conv_btn_1, end_notice_1, rate_btn_1], | |
| ) | |
| # โโ Rate Model 1 button (Step 2 โ Step 3) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| rate_btn_1.click( | |
| lambda: (gr.update(visible=False), gr.update(visible=True)), | |
| outputs=[col_step2, col_step3], | |
| ) | |
| # โโ Next: Chat with Model 2 (Step 3 โ Step 4) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def _start_model2(rc, rf, rq, re, ro, npc_fields, order, lang): | |
| """Generator: shows loading notice, then transitions to Model 2 chat.""" | |
| T = _T.get(lang, _T["en"]) | |
| ratings1 = dict(character_consistency=int(rc), fluency=int(rf), | |
| quest_completeness=int(rq), ending_quality=int(re), | |
| overall=int(ro)) | |
| # First yield: show loading notice while generating first NPC response | |
| yield ( | |
| ratings1, [], | |
| gr.update(value=[]), # chatbot_2 | |
| 0, False, | |
| gr.update(visible=True), # col_step3 (stay visible with notice) | |
| gr.update(visible=False), # col_step4 | |
| gr.update(value=T["model_loading"], visible=True), # model2_loading | |
| gr.update(visible=False), # end_conv_btn_2 | |
| gr.update(visible=False), # end_notice_2 | |
| gr.update(visible=False), # rate_btn_2 | |
| gr.update(interactive=False), # send_btn_2 | |
| ) | |
| history = [{"role": "user", "content": "Greetings"}] | |
| adapter = (order or {}).get("model_2", "dpo") | |
| try: | |
| msgs = build_messages(npc_fields, history, turn=1) | |
| raw = generate_response(adapter, msgs) | |
| except Exception as exc: | |
| err_chatbot = [ | |
| {"role": "user", "content": "Greetings"}, | |
| {"role": "assistant", "content": f"โ Model error: {exc}"}, | |
| ] | |
| yield ( | |
| ratings1, history, | |
| gr.update(value=err_chatbot), | |
| 1, False, | |
| gr.update(visible=False), # col_step3 | |
| gr.update(visible=True), # col_step4 | |
| gr.update(visible=False), # model2_loading | |
| gr.update(visible=False), # end_conv_btn_2 | |
| gr.update(visible=False), # end_notice_2 | |
| gr.update(visible=False), # rate_btn_2 | |
| gr.update(interactive=True), # send_btn_2 | |
| ) | |
| return | |
| clean, is_end = detect_end(raw) | |
| history = history + [{"role": "assistant", "content": clean}] | |
| chatbot = [{"role": "user", "content": "Greetings"}] + [ | |
| {"role": "assistant", "content": b} for b in _bubbles(clean) | |
| ] | |
| end_txt = T["end_notice"] if is_end else "" | |
| yield ( | |
| ratings1, | |
| history, | |
| gr.update(value=chatbot), | |
| 1, | |
| is_end, | |
| gr.update(visible=False), # col_step3 | |
| gr.update(visible=True), # col_step4 | |
| gr.update(visible=False), # model2_loading cleared | |
| gr.update(visible=not is_end), # end_conv_btn_2 | |
| gr.update(value=end_txt, visible=is_end), # end_notice_2 | |
| gr.update(visible=is_end), # rate_btn_2 | |
| gr.update(interactive=not is_end), # send_btn_2 | |
| ) | |
| next_btn.click( | |
| _start_model2, | |
| inputs=[r1_cons, r1_flu, r1_quest, r1_end, r1_over, | |
| npc_fld_state, order_state, lang_state], | |
| outputs=[ | |
| ratings1_state, history2_state, | |
| chatbot_2, turn2_state, ended2_state, | |
| col_step3, col_step4, | |
| model2_loading, end_conv_btn_2, end_notice_2, rate_btn_2, send_btn_2, | |
| ], | |
| ) | |
| # โโ Model 2 chat (Step 4) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def _send2(user_text, chatbot, history, turn, ended, npc_fields, order, lang): | |
| adapter = (order or {}).get("model_2", "dpo") | |
| return _chat_step(user_text, chatbot, history, turn, ended, | |
| npc_fields, adapter, lang) | |
| _chat2_inputs = [user_inp_2, chatbot_2, history2_state, turn2_state, | |
| ended2_state, npc_fld_state, order_state, lang_state] | |
| _chat2_outputs = [chatbot_2, history2_state, turn2_state, ended2_state, | |
| user_inp_2, send_btn_2, end_conv_btn_2, end_notice_2, rate_btn_2] | |
| send_btn_2.click(_send2, inputs=_chat2_inputs, outputs=_chat2_outputs).then( | |
| lambda: "0 / 500", outputs=[char_count_2] | |
| ) | |
| user_inp_2.submit(_send2, inputs=_chat2_inputs, outputs=_chat2_outputs).then( | |
| lambda: "0 / 500", outputs=[char_count_2] | |
| ) | |
| # โโ End Conversation button (Model 2) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def _end_conv_2(lang): | |
| """User manually ends Model 2 conversation.""" | |
| T = _T.get(lang, _T["en"]) | |
| return ( | |
| True, | |
| gr.update(interactive=False), | |
| gr.update(visible=False), | |
| gr.update(value=T["end_notice"], visible=True), | |
| gr.update(visible=True), | |
| ) | |
| end_conv_btn_2.click( | |
| _end_conv_2, | |
| inputs=[lang_state], | |
| outputs=[ended2_state, send_btn_2, end_conv_btn_2, end_notice_2, rate_btn_2], | |
| ) | |
| # โโ Rate Model 2 button (Step 4 โ Step 5) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| rate_btn_2.click( | |
| lambda: (gr.update(visible=False), gr.update(visible=True)), | |
| outputs=[col_step4, col_step5], | |
| ) | |
| # โโ Submit (Step 5 โ Step 6) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def _submit(rc, rf, rq, re, ro, | |
| ratings1, session_id, npc_config, order, | |
| history1, history2, lang): | |
| T = _T.get(lang, _T["en"]) | |
| ratings2 = dict(character_consistency=int(rc), fluency=int(rf), | |
| quest_completeness=int(rq), ending_quality=int(re), | |
| overall=int(ro)) | |
| ok = _save_ratings(session_id, npc_config, order, | |
| history1, history2, ratings1, ratings2) | |
| if ok: | |
| return ( | |
| gr.update(visible=False), | |
| gr.update(visible=True), | |
| gr.update(value="", visible=False), | |
| ) | |
| return ( | |
| gr.update(visible=True), | |
| gr.update(visible=False), | |
| gr.update(value=T["submit_error"], visible=True), | |
| ) | |
| submit_btn.click( | |
| _submit, | |
| inputs=[r2_cons, r2_flu, r2_quest, r2_end, r2_over, | |
| ratings1_state, session_state, npc_cfg_state, order_state, | |
| history1_state, history2_state, lang_state], | |
| outputs=[col_step5, col_step6, submit_err], | |
| ) | |
| # โโ Restart (Step 6 โ Step 1) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def _restart(): | |
| """Reset all session state and return to the NPC configuration step.""" | |
| return ( | |
| gr.update(visible=True), gr.update(visible=False), # step1 / step6 | |
| gr.update(value=[]), gr.update(value=[]), # chatbots | |
| # Reset sliders | |
| gr.update(value=3), gr.update(value=3), gr.update(value=3), | |
| gr.update(value=3), gr.update(value=3), | |
| gr.update(value=3), gr.update(value=3), gr.update(value=3), | |
| gr.update(value=3), gr.update(value=3), | |
| # Clear form inputs | |
| gr.update(value=""), gr.update(value=""), gr.update(value=""), | |
| gr.update(value=""), gr.update(value=""), gr.update(value=""), | |
| # Reset chat counters | |
| "0 / 500", "0 / 500", | |
| # Reset states | |
| None, None, None, None, | |
| [], [], 0, 0, False, False, None, | |
| ) | |
| restart_btn.click( | |
| _restart, | |
| outputs=[ | |
| col_step1, col_step6, | |
| chatbot_1, chatbot_2, | |
| r1_cons, r1_flu, r1_quest, r1_end, r1_over, | |
| r2_cons, r2_flu, r2_quest, r2_end, r2_over, | |
| name_inp, personality_inp, experience_inp, | |
| occupation_inp, location_inp, quest_inp, | |
| char_count_1, char_count_2, | |
| session_state, npc_cfg_state, order_state, npc_fld_state, | |
| history1_state, history2_state, turn1_state, turn2_state, | |
| ended1_state, ended2_state, ratings1_state, | |
| ], | |
| ) | |
| demo.launch(theme=gr.themes.Soft()) | |