NPCAlign-Demo / app.py
HermitQ's picture
Upload app.py
066de9d verified
Raw
History Blame Contribute Delete
45 kB
"""
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())