Spaces:
Sleeping
Sleeping
Off-Brand UI: bespoke courtroom panel (reference / post-freeze)
#1
by Farseen0 - opened
app.py
CHANGED
|
@@ -6,9 +6,17 @@ confidence) steers him; surface three contradictions and his voice cracks.
|
|
| 6 |
Boots anywhere: with WITNESSBOX_BACKEND unset it runs the offline mock end to
|
| 7 |
end (type your questions). Set WITNESSBOX_BACKEND=modal + Modal Space secrets
|
| 8 |
for live Whisper ASR / MiniCPM4.1-8B / VoxCPM2 and push-to-talk.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
"""
|
| 10 |
from __future__ import annotations
|
| 11 |
|
|
|
|
|
|
|
|
|
|
| 12 |
import os
|
| 13 |
|
| 14 |
import numpy as np
|
|
@@ -19,10 +27,70 @@ from witnessbox.backends import get_backends
|
|
| 19 |
from witnessbox.engine import WitnessBoxEngine
|
| 20 |
from witnessbox.witness import WITNESS_NAME, WITNESS_ROLE
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
CSS = """
|
| 23 |
-
/* ===== WitnessBox — a courtroom
|
| 24 |
-
|
| 25 |
-
|
|
|
|
|
|
|
| 26 |
.gradio-container, .gradio-container.dark {
|
| 27 |
--wb-ink:#2b2014; --wb-ink-soft:#6d5a3a;
|
| 28 |
--wb-card:#f5edd9; --wb-card-2:#efe4cb;
|
|
@@ -30,7 +98,7 @@ CSS = """
|
|
| 30 |
--wb-oxblood:#7c1f25; --wb-oxblood-hi:#94333a;
|
| 31 |
--wb-brass:#9c7a3c; --wb-walnut:#241a10;
|
| 32 |
|
| 33 |
-
--body-background-fill: radial-gradient(125% 95% at 50% -12%, #
|
| 34 |
--body-text-color: var(--wb-ink);
|
| 35 |
--body-text-color-subdued: var(--wb-ink-soft);
|
| 36 |
--background-fill-primary: var(--wb-card);
|
|
@@ -58,26 +126,104 @@ CSS = """
|
|
| 58 |
--color-accent-soft:#ecdfbc;
|
| 59 |
--link-text-color: var(--wb-oxblood);
|
| 60 |
--link-text-color-hover: var(--wb-oxblood-hi);
|
| 61 |
-
--block-shadow: 0 2px
|
| 62 |
--block-radius:12px;
|
| 63 |
font-family:'Iowan Old Style','Palatino Linotype','Palatino',Georgia,'Times New Roman',serif;
|
| 64 |
}
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
#wb-title .wb-sub b {color:var(--wb-oxblood); font-weight:700;}
|
| 74 |
-
#wb-title .wb-rule {width:
|
| 75 |
-
|
| 76 |
|
| 77 |
/* ---- small-caps block labels everywhere ---- */
|
| 78 |
.gradio-container .block-label, .gradio-container label > span:first-child {
|
| 79 |
font-variant:small-caps; letter-spacing:.6px; font-weight:700;}
|
| 80 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
/* ---- chatbot = a deposition transcript on aged paper ---- */
|
| 82 |
#wb-chat {background:var(--wb-card)!important; border:1px solid var(--wb-border)!important;}
|
| 83 |
#wb-chat *, #wb-chat .message, #wb-chat .prose {color:var(--wb-ink)!important;}
|
|
@@ -86,68 +232,142 @@ CSS = """
|
|
| 86 |
#wb-chat .bot, #wb-chat [data-role="assistant"] {background:#fbf6e8!important; border-left:3px solid var(--wb-oxblood)!important;}
|
| 87 |
#wb-chat .placeholder, #wb-chat .placeholder * {color:var(--wb-ink-soft)!important; opacity:.7;}
|
| 88 |
|
| 89 |
-
/* ---- banner ---- */
|
| 90 |
-
.wb-banner {text-align:center; font-size:1.
|
| 91 |
-
padding:12px; border-radius:10px; border:1px solid var(--wb-border);}
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
.wb-
|
| 96 |
-
|
| 97 |
-
.wb-disclaimer {font-size:11px; color:var(--wb-ink-soft); font-style:italic;}
|
| 98 |
-
.wb-tier {font-variant:small-caps; font-weight:700; color:var(--wb-oxblood); letter-spacing:.5px;}
|
| 99 |
-
|
| 100 |
-
/* ---- portrait framing ---- */
|
| 101 |
-
#wb-portrait img {border:1px solid var(--wb-border-2); border-radius:10px;}
|
| 102 |
|
| 103 |
/* ---- evidence: a walnut 'exhibit' readout ---- */
|
| 104 |
-
#wb-evidence textarea {font-family:ui-monospace,Menlo,
|
| 105 |
background:var(--wb-walnut)!important; color:#ecdfae!important;
|
| 106 |
border:1px solid var(--wb-brass)!important; border-radius:8px;}
|
| 107 |
|
| 108 |
-
/* ----
|
| 109 |
.gradio-container .block-label, .gradio-container .block-label * {
|
| 110 |
background:var(--wb-oxblood)!important; color:#f4e8c9!important; opacity:1!important;
|
| 111 |
border-color:var(--wb-border)!important;}
|
| 112 |
.gradio-container .block-label svg, .gradio-container .block-label path {fill:#f4e8c9!important;}
|
| 113 |
-
|
| 114 |
-
/* ---- accordion header: dark, legible, small-caps ---- */
|
| 115 |
.gradio-container .label-wrap, .gradio-container .label-wrap > span {
|
| 116 |
color:var(--wb-ink)!important; font-weight:700; font-variant:small-caps; letter-spacing:.5px;}
|
| 117 |
.gradio-container .label-wrap .icon, .gradio-container .label-wrap svg {color:var(--wb-oxblood)!important; opacity:1;}
|
| 118 |
|
| 119 |
-
/*
|
| 120 |
-
|
| 121 |
"""
|
|
|
|
| 122 |
|
| 123 |
|
| 124 |
# --------------------------------------------------------------------------- #
|
| 125 |
-
# render helpers
|
| 126 |
# --------------------------------------------------------------------------- #
|
| 127 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
pct = max(0, min(100, int(round(pct))))
|
| 129 |
return (
|
| 130 |
-
|
| 131 |
-
f"<div
|
| 132 |
-
f"<
|
| 133 |
-
|
| 134 |
-
f"{f'<div class=wb-disclaimer>{sub}</div>' if sub else ''}</div>"
|
| 135 |
)
|
| 136 |
|
| 137 |
|
| 138 |
-
def
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
|
| 144 |
|
| 145 |
def _counters_html(status: dict) -> str:
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
|
| 152 |
|
| 153 |
def _parse_mic(mic):
|
|
@@ -173,12 +393,6 @@ def _concat(a, b, sr):
|
|
| 173 |
return np.concatenate([a.astype(np.float32), gap, b.astype(np.float32)])
|
| 174 |
|
| 175 |
|
| 176 |
-
def _banner(kind: str, text: str) -> str:
|
| 177 |
-
colors = {"win": "#2f7d3b;color:#fff", "lose": "#7a2f2f;color:#fff", "info": "#e9dfc3;color:#5a4220"}
|
| 178 |
-
bg = colors.get(kind, colors["info"])
|
| 179 |
-
return f"<div class='wb-banner' style='background:{bg}'>{text}</div>"
|
| 180 |
-
|
| 181 |
-
|
| 182 |
# --------------------------------------------------------------------------- #
|
| 183 |
# callbacks
|
| 184 |
# --------------------------------------------------------------------------- #
|
|
@@ -198,6 +412,7 @@ def on_start(engine):
|
|
| 198 |
gr.update(value=opening_audio),
|
| 199 |
_stance_html(_neutral("awaiting your first question")),
|
| 200 |
_counters_html(intro["status"]),
|
|
|
|
| 201 |
gr.update(value="", visible=False),
|
| 202 |
_banner("info", "Examination open. Mind how you say it — he listens for doubt."),
|
| 203 |
footer,
|
|
@@ -210,7 +425,7 @@ def on_start(engine):
|
|
| 210 |
|
| 211 |
def on_ask(engine, mic, typed):
|
| 212 |
if engine is None:
|
| 213 |
-
return (engine, gr.skip(), gr.skip(), gr.skip(), gr.skip(), gr.skip(),
|
| 214 |
_banner("info", "Press “Call the witness” to begin."), gr.skip())
|
| 215 |
|
| 216 |
try:
|
|
@@ -219,7 +434,7 @@ def on_ask(engine, mic, typed):
|
|
| 219 |
except Exception as exc: # surface the real cause in the UI, don't silently toast
|
| 220 |
import traceback
|
| 221 |
traceback.print_exc()
|
| 222 |
-
return (engine, gr.skip(), gr.skip(), gr.skip(), gr.skip(), gr.skip(),
|
| 223 |
_banner("lose", f"Turn failed — {type(exc).__name__}: {exc}"), gr.skip())
|
| 224 |
|
| 225 |
# Rebuild the chat from the transcript (engine keeps it consistent with what
|
|
@@ -248,6 +463,8 @@ def on_ask(engine, mic, typed):
|
|
| 248 |
banner = _banner("win", "🩻 He breaks. Three contradictions on the record — you win.")
|
| 249 |
elif result.events.lost:
|
| 250 |
banner = _banner("lose", "The bench excuses the witness. You’ve lost the room.")
|
|
|
|
|
|
|
| 251 |
elif result.events.near_miss:
|
| 252 |
banner = _banner("info", "He flinched. You’re circling something — name the specific fact.")
|
| 253 |
else:
|
|
@@ -263,6 +480,7 @@ def on_ask(engine, mic, typed):
|
|
| 263 |
gr.update(value=audio_val),
|
| 264 |
_stance_html(result.stance),
|
| 265 |
_counters_html(result.status),
|
|
|
|
| 266 |
evidence_update,
|
| 267 |
banner,
|
| 268 |
gr.update(value=""), # clear typed box
|
|
@@ -278,11 +496,12 @@ def build() -> gr.Blocks:
|
|
| 278 |
secondary_hue=gr.themes.colors.amber, # brass
|
| 279 |
neutral_hue=gr.themes.colors.stone, # warm paper greys, never blue-greys
|
| 280 |
)
|
| 281 |
-
with gr.Blocks(css=CSS, title="WitnessBox", theme=theme) as demo:
|
| 282 |
engine_state = gr.State(None)
|
| 283 |
gr.HTML(
|
| 284 |
"<div id='wb-title'>"
|
| 285 |
"<div class='wb-crest'>⚖️</div>"
|
|
|
|
| 286 |
"<h1>WitnessBox</h1>"
|
| 287 |
f"<div class='wb-sub'>Cross-examine <b>{WITNESS_NAME}</b>, {WITNESS_ROLE}. "
|
| 288 |
"Your <b>voice</b> is the weapon.</div>"
|
|
@@ -293,19 +512,14 @@ def build() -> gr.Blocks:
|
|
| 293 |
|
| 294 |
with gr.Row():
|
| 295 |
with gr.Column(scale=2):
|
| 296 |
-
|
| 297 |
-
gr.
|
| 298 |
-
value=_portrait if os.path.exists(_portrait) else None,
|
| 299 |
-
show_label=False, height=300, elem_id="wb-portrait",
|
| 300 |
-
show_download_button=False, container=True,
|
| 301 |
-
)
|
| 302 |
-
stance_html = gr.HTML(label="Delivery")
|
| 303 |
with gr.Column(scale=4):
|
| 304 |
chat = gr.Chatbot(type="messages", height=420, label="The Stand",
|
| 305 |
elem_id="wb-chat")
|
| 306 |
witness_audio = gr.Audio(label="Witness", autoplay=True, interactive=False)
|
| 307 |
with gr.Column(scale=2):
|
| 308 |
-
counters_html = gr.HTML()
|
| 309 |
|
| 310 |
with gr.Accordion("🔎 Contradiction Engine (live verdict)", open=True):
|
| 311 |
evidence = gr.Textbox(
|
|
@@ -331,17 +545,31 @@ def build() -> gr.Blocks:
|
|
| 331 |
footer = gr.Markdown("")
|
| 332 |
|
| 333 |
outs_start = [engine_state, chat, witness_audio, stance_html, counters_html,
|
| 334 |
-
evidence, banner, footer, ask_btn, begin_btn, mic, typed]
|
| 335 |
begin_btn.click(on_start, [engine_state], outs_start)
|
| 336 |
|
| 337 |
outs_ask = [engine_state, chat, witness_audio, stance_html, counters_html,
|
| 338 |
-
evidence, banner, typed]
|
| 339 |
ask_btn.click(on_ask, [engine_state, mic, typed], outs_ask)
|
| 340 |
typed.submit(on_ask, [engine_state, mic, typed], outs_ask)
|
| 341 |
|
| 342 |
return demo
|
| 343 |
|
| 344 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
demo = build()
|
| 346 |
|
| 347 |
if __name__ == "__main__":
|
|
|
|
| 6 |
Boots anywhere: with WITNESSBOX_BACKEND unset it runs the offline mock end to
|
| 7 |
end (type your questions). Set WITNESSBOX_BACKEND=modal + Modal Space secrets
|
| 8 |
for live Whisper ASR / MiniCPM4.1-8B / VoxCPM2 and push-to-talk.
|
| 9 |
+
|
| 10 |
+
The UI is a bespoke courtroom instrument panel — a custom SVG delivery gauge,
|
| 11 |
+
three wax case-file seals that stamp as contradictions land, and a witness
|
| 12 |
+
"dock" whose portrait visibly cracks as Reid breaks — built from custom
|
| 13 |
+
HTML/CSS over Gradio's event layer (no stock-component look).
|
| 14 |
"""
|
| 15 |
from __future__ import annotations
|
| 16 |
|
| 17 |
+
import base64
|
| 18 |
+
import io
|
| 19 |
+
import math
|
| 20 |
import os
|
| 21 |
|
| 22 |
import numpy as np
|
|
|
|
| 27 |
from witnessbox.engine import WitnessBoxEngine
|
| 28 |
from witnessbox.witness import WITNESS_NAME, WITNESS_ROLE
|
| 29 |
|
| 30 |
+
|
| 31 |
+
# --------------------------------------------------------------------------- #
|
| 32 |
+
# baked-in assets (data URIs) — computed once at import, so app.py stays clean
|
| 33 |
+
# --------------------------------------------------------------------------- #
|
| 34 |
+
def _portrait_uri() -> str:
|
| 35 |
+
"""The witness portrait, downscaled to a light JPEG data URI for the dock."""
|
| 36 |
+
p = "assets/marcus_reid.png"
|
| 37 |
+
if not os.path.exists(p):
|
| 38 |
+
return ""
|
| 39 |
+
try:
|
| 40 |
+
from PIL import Image
|
| 41 |
+
im = Image.open(p).convert("RGB")
|
| 42 |
+
im.thumbnail((560, 560))
|
| 43 |
+
buf = io.BytesIO()
|
| 44 |
+
im.save(buf, "JPEG", quality=82)
|
| 45 |
+
return "data:image/jpeg;base64," + base64.b64encode(buf.getvalue()).decode()
|
| 46 |
+
except Exception:
|
| 47 |
+
try:
|
| 48 |
+
return "data:image/png;base64," + base64.b64encode(open(p, "rb").read()).decode()
|
| 49 |
+
except Exception:
|
| 50 |
+
return ""
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
PORTRAIT_DATA_URI = _portrait_uri()
|
| 54 |
+
|
| 55 |
+
# A fracture overlay for the dock — dark cracks with a faint glass highlight.
|
| 56 |
+
_CRACK_SVG = (
|
| 57 |
+
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'>"
|
| 58 |
+
"<g fill='none' stroke='#190d07' stroke-width='0.7' opacity='0.9' stroke-linejoin='round'>"
|
| 59 |
+
"<path d='M53 -3 L49 17 L59 32 L46 49 L56 69 L45 103'/>"
|
| 60 |
+
"<path d='M49 17 L30 23 M49 17 L70 10'/>"
|
| 61 |
+
"<path d='M59 32 L82 38 M46 49 L24 55 M56 69 L80 75 M56 69 L61 90'/>"
|
| 62 |
+
"<path d='M30 23 L16 18 M70 10 L84 4 M24 55 L9 61'/>"
|
| 63 |
+
"</g>"
|
| 64 |
+
"<g fill='none' stroke='#f3e6c6' stroke-width='0.32' opacity='0.45'>"
|
| 65 |
+
"<path d='M53.7 -3 L49.7 17 L59.7 32 L46.7 49 L56.7 69 L45.7 103'/>"
|
| 66 |
+
"</g></svg>"
|
| 67 |
+
)
|
| 68 |
+
WB_CRACK_URI = "data:image/svg+xml;base64," + base64.b64encode(_CRACK_SVG.encode()).decode()
|
| 69 |
+
|
| 70 |
+
# Subtle paper grain (fractal noise) laid over the parchment.
|
| 71 |
+
_NOISE_SVG = (
|
| 72 |
+
"<svg xmlns='http://www.w3.org/2000/svg' width='140' height='140'>"
|
| 73 |
+
"<filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/></filter>"
|
| 74 |
+
"<rect width='100%' height='100%' filter='url(#n)'/></svg>"
|
| 75 |
+
)
|
| 76 |
+
WB_NOISE_URI = "data:image/svg+xml;base64," + base64.b64encode(_NOISE_SVG.encode()).decode()
|
| 77 |
+
|
| 78 |
+
# Fonts: a dramatic display serif (masthead/plates) + a typewriter mono (case-file
|
| 79 |
+
# captions, seals, evidence). Falls back to a serif stack if the CDN is blocked.
|
| 80 |
+
HEAD = (
|
| 81 |
+
"<link rel='preconnect' href='https://fonts.googleapis.com'>"
|
| 82 |
+
"<link rel='preconnect' href='https://fonts.gstatic.com' crossorigin>"
|
| 83 |
+
"<link rel='stylesheet' href='https://fonts.googleapis.com/css2?"
|
| 84 |
+
"family=Playfair+Display:wght@600;700;800&family=Cutive+Mono&display=swap'>"
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
CSS = """
|
| 89 |
+
/* ===== WitnessBox — a bespoke courtroom panel ============================
|
| 90 |
+
Palette: parchment · ink · oxblood · brass. Gradio's CSS variables are
|
| 91 |
+
overridden in BOTH light and dark mode (no default indigo anywhere), and
|
| 92 |
+
the signature pieces — delivery gauge, case-file seals, witness dock — are
|
| 93 |
+
hand-built HTML/SVG, not stock components. */
|
| 94 |
.gradio-container, .gradio-container.dark {
|
| 95 |
--wb-ink:#2b2014; --wb-ink-soft:#6d5a3a;
|
| 96 |
--wb-card:#f5edd9; --wb-card-2:#efe4cb;
|
|
|
|
| 98 |
--wb-oxblood:#7c1f25; --wb-oxblood-hi:#94333a;
|
| 99 |
--wb-brass:#9c7a3c; --wb-walnut:#241a10;
|
| 100 |
|
| 101 |
+
--body-background-fill: radial-gradient(125% 95% at 50% -12%, #f3e9d2 0%, #e7d7af 72%);
|
| 102 |
--body-text-color: var(--wb-ink);
|
| 103 |
--body-text-color-subdued: var(--wb-ink-soft);
|
| 104 |
--background-fill-primary: var(--wb-card);
|
|
|
|
| 126 |
--color-accent-soft:#ecdfbc;
|
| 127 |
--link-text-color: var(--wb-oxblood);
|
| 128 |
--link-text-color-hover: var(--wb-oxblood-hi);
|
| 129 |
+
--block-shadow: 0 2px 9px rgba(60,40,15,.12);
|
| 130 |
--block-radius:12px;
|
| 131 |
font-family:'Iowan Old Style','Palatino Linotype','Palatino',Georgia,'Times New Roman',serif;
|
| 132 |
}
|
| 133 |
+
|
| 134 |
+
/* Frame the whole app like a document on a desk: parchment everywhere (no dark
|
| 135 |
+
gutters), a centred column with inked edges, grain + a soft vignette. */
|
| 136 |
+
html, body, gradio-app {background:#e0cfa6 !important;}
|
| 137 |
+
.gradio-container {
|
| 138 |
+
background: var(--body-background-fill);
|
| 139 |
+
max-width:1180px; margin:0 auto;
|
| 140 |
+
border-left:1px solid var(--wb-border-2); border-right:1px solid var(--wb-border-2);
|
| 141 |
+
box-shadow:0 0 70px rgba(40,25,8,.14);
|
| 142 |
+
}
|
| 143 |
+
.gradio-container::after {content:""; position:fixed; inset:0; pointer-events:none; z-index:2;
|
| 144 |
+
background-image:url("%%NOISE%%"); background-size:140px; opacity:.045; mix-blend-mode:multiply;}
|
| 145 |
+
.gradio-container::before {content:""; position:fixed; inset:0; pointer-events:none; z-index:2;
|
| 146 |
+
background:radial-gradient(115% 85% at 50% 42%, transparent 56%, rgba(40,25,8,.22));}
|
| 147 |
+
|
| 148 |
+
/* ---- masthead ---- */
|
| 149 |
+
#wb-title {text-align:center; padding:12px 0 4px; position:relative; z-index:3;}
|
| 150 |
+
#wb-title .wb-crest {font-size:1.45rem; line-height:1; opacity:.82;}
|
| 151 |
+
#wb-title h1 {font-family:'Playfair Display',Georgia,serif; color:var(--wb-ink)!important;
|
| 152 |
+
font-weight:800; font-size:3rem; letter-spacing:1px; margin:.02em 0 .02em; line-height:1;}
|
| 153 |
+
#wb-title .wb-case {font-family:'Cutive Mono',ui-monospace,monospace; text-transform:uppercase;
|
| 154 |
+
letter-spacing:2.5px; font-size:.7rem; color:var(--wb-oxblood); margin-bottom:3px;}
|
| 155 |
+
#wb-title .wb-sub {color:var(--wb-ink-soft)!important; font-size:1.04rem; letter-spacing:.2px;}
|
| 156 |
#wb-title .wb-sub b {color:var(--wb-oxblood); font-weight:700;}
|
| 157 |
+
#wb-title .wb-rule {width:240px; height:3px; margin:11px auto 0;
|
| 158 |
+
border-top:1px solid var(--wb-brass); border-bottom:1px solid var(--wb-brass); opacity:.55;}
|
| 159 |
|
| 160 |
/* ---- small-caps block labels everywhere ---- */
|
| 161 |
.gradio-container .block-label, .gradio-container label > span:first-child {
|
| 162 |
font-variant:small-caps; letter-spacing:.6px; font-weight:700;}
|
| 163 |
|
| 164 |
+
/* ---- witness dock: a framed mugshot with a brass plate that reacts to tier ---- */
|
| 165 |
+
.wb-dock {position:relative; z-index:3;}
|
| 166 |
+
.wb-dock-frame {position:relative; border:2px solid var(--wb-border-2); border-radius:10px;
|
| 167 |
+
overflow:hidden; background:#241a10;
|
| 168 |
+
box-shadow:0 7px 20px rgba(40,25,8,.30), inset 0 0 0 4px rgba(0,0,0,.08);}
|
| 169 |
+
.wb-mug {display:block; width:100%; height:auto; filter:saturate(.97) contrast(1.02);}
|
| 170 |
+
.wb-dock-empty {aspect-ratio:1/1; display:flex; align-items:center; justify-content:center;
|
| 171 |
+
color:#caa552; font-size:3rem; background:radial-gradient(circle at 50% 35%,#3a2c1a,#241a10);}
|
| 172 |
+
.wb-wash {position:absolute; inset:0; pointer-events:none; opacity:0; transition:opacity .6s ease;
|
| 173 |
+
mix-blend-mode:multiply;}
|
| 174 |
+
.wb-crack {position:absolute; inset:0; pointer-events:none; opacity:0; transition:opacity .6s ease;
|
| 175 |
+
background:url("%%CRACK%%") center/100% 100% no-repeat;}
|
| 176 |
+
.wb-state-tag {position:absolute; left:8px; bottom:8px; font-family:'Cutive Mono',monospace;
|
| 177 |
+
font-size:.7rem; letter-spacing:1.5px; text-transform:uppercase; color:#f4e8c9;
|
| 178 |
+
background:rgba(36,18,10,.72); padding:2px 9px; border:1px solid var(--wb-brass); border-radius:4px;}
|
| 179 |
+
.wb-tier-rattled .wb-wash {opacity:.30; background:radial-gradient(135% 115% at 50% 122%, rgba(150,42,30,.55), transparent 60%);}
|
| 180 |
+
.wb-tier-cornered .wb-wash {opacity:.52; background:radial-gradient(140% 120% at 50% 122%, rgba(152,38,28,.72), transparent 62%);}
|
| 181 |
+
.wb-tier-breaking .wb-wash {opacity:.78; background:radial-gradient(150% 130% at 50% 122%, rgba(172,30,24,.88), transparent 66%);}
|
| 182 |
+
.wb-tier-cornered .wb-crack {opacity:.40;}
|
| 183 |
+
.wb-tier-breaking .wb-crack {opacity:.94;}
|
| 184 |
+
.wb-tier-breaking .wb-dock-frame {animation:wb-shake 1.7s ease-in-out infinite;}
|
| 185 |
+
@keyframes wb-shake {0%,100%{transform:translate(0,0)} 14%{transform:translate(-1px,1px)}
|
| 186 |
+
28%{transform:translate(1px,-1px)} 42%{transform:translate(-1px,0)} 56%{transform:translate(1px,1px)} 70%{transform:translate(-1px,-1px)}}
|
| 187 |
+
.wb-plate {margin-top:9px; text-align:center; border:1px solid #7d5f2c; border-radius:6px;
|
| 188 |
+
padding:5px 8px; background:linear-gradient(180deg,#bd963f,#9c7a3c);
|
| 189 |
+
box-shadow:inset 0 1px 0 #ffe7a8, 0 1px 3px rgba(40,25,8,.25);}
|
| 190 |
+
.wb-plate b {display:block; font-family:'Playfair Display',serif; color:#1c1409;
|
| 191 |
+
letter-spacing:2.5px; font-size:1.05rem;}
|
| 192 |
+
.wb-plate i {font-style:normal; font-family:'Cutive Mono',monospace; color:#33260f;
|
| 193 |
+
font-size:.68rem; letter-spacing:1.2px; opacity:.9;}
|
| 194 |
+
|
| 195 |
+
/* ---- delivery gauge (custom SVG instrument) ---- */
|
| 196 |
+
.wb-instrument {background:var(--wb-card); border:1px solid var(--wb-border); border-radius:10px;
|
| 197 |
+
padding:9px 12px 11px; text-align:center; box-shadow:var(--block-shadow); position:relative; z-index:3;}
|
| 198 |
+
.wb-instrument-cap, .wb-casefile-cap {font-family:'Cutive Mono',ui-monospace,monospace; font-size:.68rem;
|
| 199 |
+
letter-spacing:2.5px; text-transform:uppercase; color:var(--wb-oxblood); margin-bottom:2px;}
|
| 200 |
+
.wb-instrument svg {width:100%; max-width:240px; height:auto;}
|
| 201 |
+
.wb-gauge-read {display:flex; align-items:baseline; justify-content:center; gap:12px; margin-top:-8px;}
|
| 202 |
+
.wb-gauge-tier {font-variant:small-caps; font-weight:800; letter-spacing:1px; font-size:1.3rem;}
|
| 203 |
+
.wb-gauge-tier.wb-confident {color:#2f7d3b;} .wb-gauge-tier.wb-neutral {color:#9a7500;} .wb-gauge-tier.wb-hesitant {color:#9c3b2f;}
|
| 204 |
+
.wb-gauge-num {font-family:'Cutive Mono',monospace; font-size:1.05rem; color:var(--wb-ink);}
|
| 205 |
+
.wb-gauge-num i {font-style:normal; font-size:.66rem; opacity:.6;}
|
| 206 |
+
.wb-disclaimer {font-size:10.5px; color:var(--wb-ink-soft); font-style:italic; margin-top:3px; line-height:1.3;}
|
| 207 |
+
|
| 208 |
+
/* ---- case file: three wax seals + composure/standing meters ---- */
|
| 209 |
+
.wb-casefile {background:var(--wb-card); border:1px solid var(--wb-border); border-radius:10px;
|
| 210 |
+
padding:12px 13px 13px; box-shadow:var(--block-shadow); position:relative; z-index:3;}
|
| 211 |
+
.wb-seals {display:flex; gap:10px; justify-content:center; margin:7px 0 5px;}
|
| 212 |
+
.wb-seal {width:50px; height:50px; border-radius:50%; display:flex; align-items:center; justify-content:center;
|
| 213 |
+
font-family:'Playfair Display',serif; font-size:1.15rem; border:2px dashed var(--wb-border-2);
|
| 214 |
+
color:var(--wb-border-2); background:#efe3c6;}
|
| 215 |
+
.wb-seal.stamped {border:2px solid #5b141a; color:#f6e7c8; transform:rotate(-8deg);
|
| 216 |
+
background:radial-gradient(circle at 38% 30%, #ad3f45, #7c1f25 58%, #5c1217);
|
| 217 |
+
box-shadow:0 2px 6px rgba(60,10,10,.45), inset 0 1px 2px rgba(255,205,175,.55);}
|
| 218 |
+
.wb-seals-cap {text-align:center; font-family:'Cutive Mono',monospace; font-size:.7rem;
|
| 219 |
+
letter-spacing:1px; color:var(--wb-ink-soft); margin-bottom:4px;}
|
| 220 |
+
.wb-meter {margin-top:11px;}
|
| 221 |
+
.wb-meter-top {display:flex; justify-content:space-between; font-size:.8rem; font-variant:small-caps;
|
| 222 |
+
letter-spacing:.5px; color:var(--wb-ink); margin-bottom:3px;}
|
| 223 |
+
.wb-meter-top i {font-style:normal; font-family:'Cutive Mono',monospace; opacity:.7; font-size:.74rem;}
|
| 224 |
+
.wb-meter-track {height:9px; background:#e0d3af; border:1px solid var(--wb-border); border-radius:6px; overflow:hidden;}
|
| 225 |
+
.wb-meter-fill {height:100%; transition:width .5s ease;}
|
| 226 |
+
|
| 227 |
/* ---- chatbot = a deposition transcript on aged paper ---- */
|
| 228 |
#wb-chat {background:var(--wb-card)!important; border:1px solid var(--wb-border)!important;}
|
| 229 |
#wb-chat *, #wb-chat .message, #wb-chat .prose {color:var(--wb-ink)!important;}
|
|
|
|
| 232 |
#wb-chat .bot, #wb-chat [data-role="assistant"] {background:#fbf6e8!important; border-left:3px solid var(--wb-oxblood)!important;}
|
| 233 |
#wb-chat .placeholder, #wb-chat .placeholder * {color:var(--wb-ink-soft)!important; opacity:.7;}
|
| 234 |
|
| 235 |
+
/* ---- verdict banner (wax-stamp feel on win/lose) ---- */
|
| 236 |
+
.wb-banner {text-align:center; font-size:1.14rem; font-variant:small-caps; letter-spacing:.6px;
|
| 237 |
+
padding:12px; border-radius:10px; border:1px solid var(--wb-border); position:relative; z-index:3;}
|
| 238 |
+
.wb-banner-info {background:linear-gradient(180deg,#efe4c7,#e7d9b6); color:#5a4220;}
|
| 239 |
+
.wb-banner-win {background:radial-gradient(circle at 50% -25%, #3c6c33, #234020); color:#eef7e6;
|
| 240 |
+
border:2px solid #2f7d3b; font-weight:700; text-shadow:0 1px 0 rgba(0,0,0,.3);}
|
| 241 |
+
.wb-banner-lose {background:radial-gradient(circle at 50% -25%, #80312f, #4f1d1d); color:#f6e6e6;
|
| 242 |
+
border:2px solid #7c1f25; font-weight:700;}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
|
| 244 |
/* ---- evidence: a walnut 'exhibit' readout ---- */
|
| 245 |
+
#wb-evidence textarea {font-family:'Cutive Mono',ui-monospace,Menlo,monospace!important;
|
| 246 |
background:var(--wb-walnut)!important; color:#ecdfae!important;
|
| 247 |
border:1px solid var(--wb-brass)!important; border-radius:8px;}
|
| 248 |
|
| 249 |
+
/* ---- component labels = the same oxblood plate (no stray pink/purple) ---- */
|
| 250 |
.gradio-container .block-label, .gradio-container .block-label * {
|
| 251 |
background:var(--wb-oxblood)!important; color:#f4e8c9!important; opacity:1!important;
|
| 252 |
border-color:var(--wb-border)!important;}
|
| 253 |
.gradio-container .block-label svg, .gradio-container .block-label path {fill:#f4e8c9!important;}
|
|
|
|
|
|
|
| 254 |
.gradio-container .label-wrap, .gradio-container .label-wrap > span {
|
| 255 |
color:var(--wb-ink)!important; font-weight:700; font-variant:small-caps; letter-spacing:.5px;}
|
| 256 |
.gradio-container .label-wrap .icon, .gradio-container .label-wrap svg {color:var(--wb-oxblood)!important; opacity:1;}
|
| 257 |
|
| 258 |
+
/* primary action reads like a gavel strike */
|
| 259 |
+
.gradio-container button.primary, .gradio-container .primary {font-variant:small-caps; letter-spacing:1px; font-weight:700;}
|
| 260 |
"""
|
| 261 |
+
CSS = CSS.replace("%%CRACK%%", WB_CRACK_URI).replace("%%NOISE%%", WB_NOISE_URI)
|
| 262 |
|
| 263 |
|
| 264 |
# --------------------------------------------------------------------------- #
|
| 265 |
+
# render helpers — hand-built HTML/SVG instruments
|
| 266 |
# --------------------------------------------------------------------------- #
|
| 267 |
+
def _arc_points(cx: float, cy: float, r: float, a0: float, a1: float, n: int = 16) -> str:
|
| 268 |
+
pts = []
|
| 269 |
+
for i in range(n + 1):
|
| 270 |
+
a = a0 + (a1 - a0) * i / n
|
| 271 |
+
pts.append(f"{cx + r * math.cos(a):.1f},{cy - r * math.sin(a):.1f}")
|
| 272 |
+
return " ".join(pts)
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
def _gauge_svg(confidence: float, tier: str) -> str:
|
| 276 |
+
"""A semicircular needle gauge: red (hesitant) · amber (neutral) · green (confident)."""
|
| 277 |
+
cx, cy, R = 100.0, 98.0, 80.0
|
| 278 |
+
zones = [
|
| 279 |
+
("hesitant", math.pi, 2 * math.pi / 3, "#9c3b2f"),
|
| 280 |
+
("neutral", 2 * math.pi / 3, math.pi / 3, "#b08900"),
|
| 281 |
+
("confident", math.pi / 3, 0.0, "#2f7d3b"),
|
| 282 |
+
]
|
| 283 |
+
active = (tier or "").lower()
|
| 284 |
+
seg = ""
|
| 285 |
+
for name, a0, a1, col in zones:
|
| 286 |
+
op = "1" if name == active else "0.22"
|
| 287 |
+
seg += (f"<polyline points='{_arc_points(cx, cy, R, a0, a1)}' fill='none' "
|
| 288 |
+
f"stroke='{col}' stroke-width='13' opacity='{op}'/>")
|
| 289 |
+
conf = max(0.0, min(100.0, float(confidence)))
|
| 290 |
+
a = math.pi - (conf / 100.0) * math.pi
|
| 291 |
+
nx, ny = cx + 66 * math.cos(a), cy - 66 * math.sin(a)
|
| 292 |
+
needle = (f"<line x1='{cx}' y1='{cy}' x2='{nx:.1f}' y2='{ny:.1f}' "
|
| 293 |
+
f"stroke='#241a10' stroke-width='2.6' stroke-linecap='round'/>")
|
| 294 |
+
hub = (f"<circle cx='{cx}' cy='{cy}' r='6' fill='#241a10'/>"
|
| 295 |
+
f"<circle cx='{cx}' cy='{cy}' r='2.4' fill='#caa552'/>")
|
| 296 |
+
return f"<svg viewBox='0 0 200 110' xmlns='http://www.w3.org/2000/svg'>{seg}{needle}{hub}</svg>"
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
def _stance_html(stance) -> str:
|
| 300 |
+
tier = stance.tier
|
| 301 |
+
return (
|
| 302 |
+
"<div class='wb-instrument'>"
|
| 303 |
+
"<div class='wb-instrument-cap'>Delivery Read</div>"
|
| 304 |
+
f"{_gauge_svg(stance.confidence, tier)}"
|
| 305 |
+
"<div class='wb-gauge-read'>"
|
| 306 |
+
f"<span class='wb-gauge-tier wb-{tier.lower()}'>{tier.title()}</span>"
|
| 307 |
+
f"<span class='wb-gauge-num'>{int(round(stance.confidence))}<i>%</i></span>"
|
| 308 |
+
"</div>"
|
| 309 |
+
"<div class='wb-disclaimer'>Perceived delivery — not a lie detector. "
|
| 310 |
+
"Reads pace & pauses, not whether anything is true.</div>"
|
| 311 |
+
"</div>"
|
| 312 |
+
)
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
def _meter_html(label: str, pct: float, color: str) -> str:
|
| 316 |
pct = max(0, min(100, int(round(pct))))
|
| 317 |
return (
|
| 318 |
+
"<div class='wb-meter'>"
|
| 319 |
+
f"<div class='wb-meter-top'><span>{label}</span><i>{pct}</i></div>"
|
| 320 |
+
f"<div class='wb-meter-track'><div class='wb-meter-fill' style='width:{pct}%;background:{color}'></div></div>"
|
| 321 |
+
"</div>"
|
|
|
|
| 322 |
)
|
| 323 |
|
| 324 |
|
| 325 |
+
def _seals_html(catches: int, need: int) -> str:
|
| 326 |
+
roman = ["Ⅰ", "Ⅱ", "Ⅲ", "Ⅳ", "Ⅴ"]
|
| 327 |
+
seals = ""
|
| 328 |
+
for i in range(need):
|
| 329 |
+
if i < catches:
|
| 330 |
+
seals += "<div class='wb-seal stamped'><span>⚖</span></div>"
|
| 331 |
+
else:
|
| 332 |
+
seals += f"<div class='wb-seal'><span>{roman[i] if i < len(roman) else i + 1}</span></div>"
|
| 333 |
+
return (
|
| 334 |
+
f"<div class='wb-seals'>{seals}</div>"
|
| 335 |
+
f"<div class='wb-seals-cap'>Contradictions on the record · {catches} / {need}</div>"
|
| 336 |
+
)
|
| 337 |
|
| 338 |
|
| 339 |
def _counters_html(status: dict) -> str:
|
| 340 |
+
return (
|
| 341 |
+
"<div class='wb-casefile'>"
|
| 342 |
+
"<div class='wb-casefile-cap'>Case File</div>"
|
| 343 |
+
f"{_seals_html(status['catches'], status['catches_to_win'])}"
|
| 344 |
+
f"{_meter_html('Witness composure', status['composure'], '#7a4a2f')}"
|
| 345 |
+
f"{_meter_html('Your standing with the bench', status['credibility'], '#43607f')}"
|
| 346 |
+
"</div>"
|
| 347 |
+
)
|
| 348 |
+
|
| 349 |
+
|
| 350 |
+
def _dock_html(tier: str) -> str:
|
| 351 |
+
"""The witness in the dock; `tier` (composed→rattled→cornered→breaking) drives the visual break."""
|
| 352 |
+
if PORTRAIT_DATA_URI:
|
| 353 |
+
face = f"<img class='wb-mug' src='{PORTRAIT_DATA_URI}' alt='Marcus Reid'/>"
|
| 354 |
+
else:
|
| 355 |
+
face = "<div class='wb-dock-empty'>⚖️</div>"
|
| 356 |
+
return (
|
| 357 |
+
f"<div class='wb-dock wb-tier-{tier}'>"
|
| 358 |
+
"<div class='wb-dock-frame'>"
|
| 359 |
+
f"{face}"
|
| 360 |
+
"<div class='wb-wash'></div><div class='wb-crack'></div>"
|
| 361 |
+
f"<div class='wb-state-tag'>{tier}</div>"
|
| 362 |
+
"</div>"
|
| 363 |
+
"<div class='wb-plate'><b>MARCUS REID</b><i>CFO · Halcyon Dynamics</i></div>"
|
| 364 |
+
"</div>"
|
| 365 |
+
)
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
def _banner(kind: str, text: str) -> str:
|
| 369 |
+
kind = kind if kind in ("win", "lose", "info") else "info"
|
| 370 |
+
return f"<div class='wb-banner wb-banner-{kind}'>{text}</div>"
|
| 371 |
|
| 372 |
|
| 373 |
def _parse_mic(mic):
|
|
|
|
| 393 |
return np.concatenate([a.astype(np.float32), gap, b.astype(np.float32)])
|
| 394 |
|
| 395 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 396 |
# --------------------------------------------------------------------------- #
|
| 397 |
# callbacks
|
| 398 |
# --------------------------------------------------------------------------- #
|
|
|
|
| 412 |
gr.update(value=opening_audio),
|
| 413 |
_stance_html(_neutral("awaiting your first question")),
|
| 414 |
_counters_html(intro["status"]),
|
| 415 |
+
_dock_html("composed"),
|
| 416 |
gr.update(value="", visible=False),
|
| 417 |
_banner("info", "Examination open. Mind how you say it — he listens for doubt."),
|
| 418 |
footer,
|
|
|
|
| 425 |
|
| 426 |
def on_ask(engine, mic, typed):
|
| 427 |
if engine is None:
|
| 428 |
+
return (engine, gr.skip(), gr.skip(), gr.skip(), gr.skip(), gr.skip(), gr.skip(),
|
| 429 |
_banner("info", "Press “Call the witness” to begin."), gr.skip())
|
| 430 |
|
| 431 |
try:
|
|
|
|
| 434 |
except Exception as exc: # surface the real cause in the UI, don't silently toast
|
| 435 |
import traceback
|
| 436 |
traceback.print_exc()
|
| 437 |
+
return (engine, gr.skip(), gr.skip(), gr.skip(), gr.skip(), gr.skip(), gr.skip(),
|
| 438 |
_banner("lose", f"Turn failed — {type(exc).__name__}: {exc}"), gr.skip())
|
| 439 |
|
| 440 |
# Rebuild the chat from the transcript (engine keeps it consistent with what
|
|
|
|
| 463 |
banner = _banner("win", "🩻 He breaks. Three contradictions on the record — you win.")
|
| 464 |
elif result.events.lost:
|
| 465 |
banner = _banner("lose", "The bench excuses the witness. You’ve lost the room.")
|
| 466 |
+
elif result.evidence: # a contradiction just landed (non-winning) — the gotcha moment
|
| 467 |
+
banner = _banner("win", f"⚖ Contradiction admitted — {result.status['catches']} of {result.status['catches_to_win']} on the record.")
|
| 468 |
elif result.events.near_miss:
|
| 469 |
banner = _banner("info", "He flinched. You’re circling something — name the specific fact.")
|
| 470 |
else:
|
|
|
|
| 480 |
gr.update(value=audio_val),
|
| 481 |
_stance_html(result.stance),
|
| 482 |
_counters_html(result.status),
|
| 483 |
+
_dock_html(result.status["witness_tier"]),
|
| 484 |
evidence_update,
|
| 485 |
banner,
|
| 486 |
gr.update(value=""), # clear typed box
|
|
|
|
| 496 |
secondary_hue=gr.themes.colors.amber, # brass
|
| 497 |
neutral_hue=gr.themes.colors.stone, # warm paper greys, never blue-greys
|
| 498 |
)
|
| 499 |
+
with gr.Blocks(css=CSS, head=HEAD, title="WitnessBox", theme=theme) as demo:
|
| 500 |
engine_state = gr.State(None)
|
| 501 |
gr.HTML(
|
| 502 |
"<div id='wb-title'>"
|
| 503 |
"<div class='wb-crest'>⚖️</div>"
|
| 504 |
+
"<div class='wb-case'>Cross-Examination · The People v. Halcyon Dynamics</div>"
|
| 505 |
"<h1>WitnessBox</h1>"
|
| 506 |
f"<div class='wb-sub'>Cross-examine <b>{WITNESS_NAME}</b>, {WITNESS_ROLE}. "
|
| 507 |
"Your <b>voice</b> is the weapon.</div>"
|
|
|
|
| 512 |
|
| 513 |
with gr.Row():
|
| 514 |
with gr.Column(scale=2):
|
| 515 |
+
dock_html = gr.HTML(_dock_html("composed"))
|
| 516 |
+
stance_html = gr.HTML(_stance_html(_neutral_seed()))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 517 |
with gr.Column(scale=4):
|
| 518 |
chat = gr.Chatbot(type="messages", height=420, label="The Stand",
|
| 519 |
elem_id="wb-chat")
|
| 520 |
witness_audio = gr.Audio(label="Witness", autoplay=True, interactive=False)
|
| 521 |
with gr.Column(scale=2):
|
| 522 |
+
counters_html = gr.HTML(_counters_seed())
|
| 523 |
|
| 524 |
with gr.Accordion("🔎 Contradiction Engine (live verdict)", open=True):
|
| 525 |
evidence = gr.Textbox(
|
|
|
|
| 545 |
footer = gr.Markdown("")
|
| 546 |
|
| 547 |
outs_start = [engine_state, chat, witness_audio, stance_html, counters_html,
|
| 548 |
+
dock_html, evidence, banner, footer, ask_btn, begin_btn, mic, typed]
|
| 549 |
begin_btn.click(on_start, [engine_state], outs_start)
|
| 550 |
|
| 551 |
outs_ask = [engine_state, chat, witness_audio, stance_html, counters_html,
|
| 552 |
+
dock_html, evidence, banner, typed]
|
| 553 |
ask_btn.click(on_ask, [engine_state, mic, typed], outs_ask)
|
| 554 |
typed.submit(on_ask, [engine_state, mic, typed], outs_ask)
|
| 555 |
|
| 556 |
return demo
|
| 557 |
|
| 558 |
|
| 559 |
+
def _neutral_seed():
|
| 560 |
+
from witnessbox.stance import _neutral
|
| 561 |
+
return _neutral("awaiting examination")
|
| 562 |
+
|
| 563 |
+
|
| 564 |
+
def _counters_seed():
|
| 565 |
+
return _counters_html({
|
| 566 |
+
"catches": 0, "catches_to_win": getattr(config, "CATCHES_TO_WIN", 3),
|
| 567 |
+
"composure": getattr(config, "COMPOSURE_START", 100),
|
| 568 |
+
"credibility": getattr(config, "CREDIBILITY_START", 100),
|
| 569 |
+
"witness_tier": "composed",
|
| 570 |
+
})
|
| 571 |
+
|
| 572 |
+
|
| 573 |
demo = build()
|
| 574 |
|
| 575 |
if __name__ == "__main__":
|