JudgeGPT / app.py
AliIqbal05's picture
Update Judge-GPT code and README
f1c85c1 verified
Raw
History Blame
75.8 kB
from __future__ import annotations
import json
import os
import queue
import threading
import time
from collections.abc import Iterable
from dataclasses import dataclass
import gradio as gr
import httpx
from sovereign_bench.engine import JUDGE_NAME, JUROR_PERSONAS, stream_trial
from sovereign_bench.cases import CASES, get_case
from sovereign_bench.models import CasePacket, EvidenceItem, TrialEvent, TrialRequest
def _load_env_file() -> None:
path = ".env"
if not os.path.exists(path):
return
with open(path, encoding="utf-8") as handle:
for line in handle:
stripped = line.strip()
if not stripped or stripped.startswith("#") or "=" not in stripped:
continue
key, value = stripped.split("=", 1)
os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'"))
_load_env_file()
CASE_OPTIONS = {
"Trial of Socrates": "socrates",
"Greg Heffley vs Mom": "greg",
"Custom": "custom",
}
DEFAULT_MODAL_TRIAL_URL = "https://ali-j-iqbal24--trial-stream.modal.run"
MIN_READ_SECONDS = 2.2
WORDS_PER_SECOND = 3.2
READ_BUFFER_SECONDS = 0.8
MAX_READ_SECONDS = 8.5
PHASE_GLYPHS = {
"pretrial": "00",
"intake": "01",
"claims": "02",
"opening": "03",
"evidence": "04",
"questions": "05",
"deliberation": "06",
"verdict": "07",
"appeal": "08",
}
TRIAL_PROGRESS_STAGES = (
("pretrial", "Pretrial"),
("intake", "Intake"),
("claims", "Claims"),
("opening", "Opening"),
("evidence", "Evidence"),
("questions", "Questions"),
("deliberation", "Deliberation"),
("verdict", "Verdict"),
)
VERDICT_LABELS = {
"liable": "Guilty",
"not_liable": "Not Guilty",
"mixed": "Mixed",
"uncertain": "Uncertain",
}
AUDIO_PATHS = {
"score": "/gradio_api/file=assets/audio/courtroom.ogg",
"judgement": "/gradio_api/file=assets/audio/Judgement.ogg",
"crowd": "/gradio_api/file=assets/audio/crowd_shouting.ogg",
"gavel": "/gradio_api/file=assets/audio/wood_hammer_01.ogg",
"wood": "/gradio_api/file=assets/audio/wood_hit_03.ogg",
"steps": "/gradio_api/file=assets/audio/steps_in_wood_floor.wav",
"paper": "/gradio_api/file=assets/audio/paper_sound_1.mp3",
"paper_long": "/gradio_api/file=assets/audio/paper_sound_4.mp3",
"select": "/gradio_api/file=assets/audio/select_001.ogg",
}
CSS = """
:root {
--ink: #23170e;
--paper: #f3dfb7;
--paper-dark: #c79455;
--gold: #d9b060;
--mahogany: #4b2119;
--shadow: rgba(10, 5, 2, .6);
--red: #8f2e2d;
--green: #2f6f5e;
--blue: #254f7a;
}
body,
.gradio-container {
margin: 0;
background: #141413 !important;
background-color: #141413 !important;
color: var(--ink);
font-family: Georgia, "Times New Roman", serif;
}
.gradio-container {
max-width: none !important;
padding: 0 !important;
}
.main,
.contain {
max-width: none !important;
padding: 0 !important;
background: transparent !important;
}
.gradio-container main,
.gradio-container .wrap,
.gradio-container .app,
.gradio-container .html-container {
background: transparent !important;
padding-left: 0 !important;
padding-right: 0 !important;
}
.docket-book-controls {
position: fixed;
left: 50%;
top: 72px;
z-index: 9999;
width: min(760px, calc(100vw - 160px));
max-width: none;
margin: 0;
padding: 0;
transform: translateX(-50%) rotate(-1deg);
border: 0 !important;
border-radius: 0 !important;
background: transparent !important;
box-shadow: none !important;
color: #321d10;
transition: opacity .32s ease, transform .65s ease;
}
body.trial-has-started .docket-book-controls {
opacity: 0;
pointer-events: none;
transform: translateX(-50%) rotateX(56deg) rotate(-1deg) scale(.45);
}
.docket-book-controls::before {
content: none;
}
.docket-book-controls,
.docket-book-controls > *,
.docket-book-controls .form,
.docket-book-controls .block,
.docket-book-controls .gap,
.docket-book-controls .wrap {
background: transparent !important;
border: 0 !important;
box-shadow: none !important;
}
.docket-book-controls .docket-book-controls {
position: static !important;
left: auto !important;
top: auto !important;
width: 100% !important;
transform: none !important;
opacity: 1;
pointer-events: auto;
}
body.trial-has-started .docket-book-controls .docket-book-controls {
pointer-events: none;
}
.book-control-heading {
margin: 0 0 6px;
color: #694019;
font: 900 12px/1 ui-monospace, SFMono-Regular, Consolas, monospace;
letter-spacing: .08em;
text-transform: uppercase;
}
.docket-book-controls label,
.docket-book-controls span,
.docket-book-controls .prose {
color: #321d10 !important;
}
.docket-book-controls label {
font-size: 11px !important;
font-weight: 800 !important;
}
.docket-book-controls input,
.docket-book-controls textarea,
.docket-book-controls [role="combobox"],
.docket-book-controls .wrap-inner {
border-color: rgba(90, 50, 20, .24) !important;
border-radius: 4px !important;
background: rgba(255, 243, 207, .58) !important;
color: #241509 !important;
box-shadow: inset 0 1px 0 rgba(255,255,255,.24) !important;
}
.docket-book-controls textarea {
min-height: 42px !important;
}
.docket-book-controls button.primary {
min-height: 42px;
border: 1px solid rgba(44, 21, 10, .42) !important;
border-radius: 5px !important;
background: #1c130d !important;
color: #fff3d2 !important;
box-shadow: inset 0 1px 0 rgba(255,255,255,.12), 0 8px 18px rgba(40, 18, 9, .28);
}
.docket-book-controls .book-status p {
margin: 0 !important;
color: #5a3519 !important;
font-size: 12px;
line-height: 1.25;
}
.court-episode-stage {
--spot-x: 50%;
--spot-y: 36%;
position: relative;
min-height: min(880px, calc(100vh - 112px));
height: min(880px, calc(100vh - 112px));
margin: 0;
width: 100%;
max-width: none;
overflow: hidden;
isolation: auto;
color: #fff0d2;
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
}
.court-episode-stage::before {
content: "";
display: none;
}
.court-episode-stage::after {
content: "";
display: none;
}
.court-episode-stage > * {
position: relative;
z-index: 4;
}
.trial-progress {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 70;
display: grid;
grid-template-columns: repeat(8, minmax(0, 1fr));
gap: 1px;
padding: 3px clamp(10px, 2vw, 24px) 4px;
border-bottom: 1px solid rgba(217, 176, 96, .2);
background: rgba(23, 13, 8, .58);
backdrop-filter: blur(8px);
box-shadow: 0 8px 18px rgba(8, 4, 2, .22);
pointer-events: none;
}
.trial-progress-segment {
position: relative;
min-width: 0;
padding-top: 5px;
overflow: hidden;
color: rgba(244, 213, 143, .38);
font: 800 10px/1 ui-monospace, SFMono-Regular, Consolas, monospace;
letter-spacing: .04em;
text-align: center;
text-transform: uppercase;
white-space: nowrap;
}
.trial-progress-segment::before {
content: "";
position: absolute;
left: 3px;
right: 3px;
top: 0;
height: 2px;
border-radius: 999px;
background: rgba(217, 176, 96, .18);
}
.trial-progress-segment.complete {
color: rgba(217, 176, 96, .68);
}
.trial-progress-segment.complete::before {
background: rgba(217, 176, 96, .48);
}
.trial-progress-segment.current {
color: #ffe6a6;
text-shadow: 0 0 10px rgba(255, 211, 116, .52);
}
.trial-progress-segment.current::before {
height: 3px;
background: #ffd675;
box-shadow: 0 0 12px rgba(255, 214, 117, .68);
}
.trial-progress-abbrev {
display: none;
}
.episode-room {
position: absolute;
inset: 0;
z-index: 3;
background:
url('/gradio_api/file=assets/background/CourtRoom.png') center center / 100% 100% no-repeat,
#26120b;
filter: none;
transform: none;
}
.trial-started .episode-room,
.phase-intake .episode-room,
.phase-claims .episode-room,
.phase-opening .episode-room,
.phase-evidence .episode-room,
.phase-questions .episode-room,
.phase-deliberation .episode-room,
.phase-verdict .episode-room,
.phase-appeal .episode-room {
filter: none;
transform: none;
}
.phase-intake,
.phase-appeal { --spot-x: 50%; --spot-y: 30%; }
.phase-claims,
.phase-opening { --spot-x: 43%; --spot-y: 66%; }
.phase-evidence { --spot-x: 70%; --spot-y: 56%; }
.phase-questions,
.phase-verdict { --spot-x: 50%; --spot-y: 34%; }
.phase-deliberation { --spot-x: 79%; --spot-y: 60%; }
.episode-title {
position: absolute;
left: 26px;
top: 22px;
z-index: 9;
max-width: min(780px, calc(100% - 330px));
text-shadow: 0 3px 18px rgba(0, 0, 0, .75);
}
.episode-kicker,
.prop-label,
.caption-phase,
.tooltip-meta,
.drawer-kicker {
color: #f4d58f;
font: 800 11px/1.2 ui-monospace, SFMono-Regular, Consolas, monospace;
letter-spacing: .06em;
text-transform: uppercase;
}
.episode-title h1 {
margin: 4px 0 6px;
max-width: 780px;
color: #fff4d7;
font-size: clamp(28px, 4.2vw, 58px);
line-height: .98;
letter-spacing: 0;
}
.episode-title p {
margin: 0;
max-width: 720px;
color: #f8dcaa;
font-size: 15px;
line-height: 1.38;
}
.audio-deck {
display: none;
}
.sound-toggle {
position: fixed;
left: 18px;
bottom: 18px;
z-index: 80;
width: 46px;
height: 46px;
border: 1px solid rgba(255, 226, 154, .48);
border-radius: 50%;
background: rgba(22, 11, 7, .82);
box-shadow: 0 12px 28px rgba(0, 0, 0, .42), inset 0 1px 0 rgba(255, 255, 255, .12);
cursor: pointer;
}
.sound-toggle:hover,
.sound-toggle:focus-visible {
outline: none;
border-color: rgba(255, 226, 154, .82);
background: rgba(41, 20, 12, .92);
}
.sound-toggle .sound-icon {
position: absolute;
left: 13px;
top: 15px;
width: 10px;
height: 16px;
border-radius: 2px 0 0 2px;
background: #ffe5a6;
}
.sound-toggle .sound-icon::before {
content: "";
position: absolute;
left: 7px;
top: -3px;
width: 14px;
height: 22px;
border: 3px solid #ffe5a6;
border-left: 0;
border-radius: 0 18px 18px 0;
}
.sound-toggle .sound-icon::after {
content: "";
position: absolute;
left: 20px;
top: -9px;
width: 3px;
height: 34px;
border-radius: 4px;
background: #d64d45;
opacity: 0;
transform: rotate(42deg);
transform-origin: center;
}
.sound-toggle.muted .sound-icon::after {
opacity: 1;
}
.episode-book {
position: absolute;
left: 50%;
top: 122px;
z-index: 14;
width: min(980px, calc(100% - 32px));
aspect-ratio: 3 / 2;
transform: translateX(-50%) rotateX(0) rotateZ(-1deg);
transform-origin: center bottom;
color: #2b1b10;
filter: drop-shadow(0 34px 36px rgba(0, 0, 0, .48));
pointer-events: none;
transition: top .85s ease, width .85s ease, transform .85s ease, filter .85s ease, opacity .85s ease;
}
.episode-book.custom-book {
pointer-events: auto;
}
.book-art {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: contain;
pointer-events: none;
user-select: none;
transition: opacity .36s ease;
}
.book-art.closed-art {
opacity: 0;
}
.episode-book.closed {
top: 50%;
width: min(163px, 20vw);
transform: translateX(-50%) rotateX(56deg) rotateZ(1deg);
opacity: .92;
filter: drop-shadow(0 18px 18px rgba(0, 0, 0, .45));
}
.episode-book.closed .open-art {
opacity: 0;
}
.episode-book.closed .closed-art {
opacity: 1;
}
.episode-book.closed .book-open-content {
opacity: 0;
pointer-events: none;
}
.book-open-content {
position: absolute;
inset: 15% 9% 12%;
z-index: 2;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 82px;
padding: 0 20px;
transition: opacity .35s ease;
}
.book-open-content h2 {
margin: 0 0 8px;
color: #4c2a12;
font-size: 28px;
letter-spacing: 0;
}
.book-open-content p,
.book-entry {
color: #3c2615;
font-size: 14px;
line-height: 1.28;
}
.book-entry {
margin: 8px 0;
padding-left: 12px;
border-left: 3px solid rgba(111, 61, 23, .36);
}
.book-context {
margin-top: 8px;
}
.book-case-title {
margin: 0 0 6px;
color: #4c2a12;
font-weight: 800;
}
.book-evidence-columns {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.book-evidence-column h3 {
margin: 0 0 6px;
color: #4c2a12;
font-size: 15px;
line-height: 1.12;
}
.book-evidence-list {
margin: 0;
padding: 0;
list-style: none;
}
.book-evidence-list li {
margin: 0 0 6px;
padding-left: 9px;
border-left: 2px solid rgba(111, 61, 23, .32);
color: #3c2615;
font-size: 12px;
line-height: 1.2;
}
.book-field {
width: 100%;
min-height: 42px;
resize: none;
border: 1px solid rgba(90, 50, 20, .34);
border-radius: 4px;
background: rgba(255, 247, 224, .7);
color: #2b1b10;
font: 12px/1.22 Georgia, "Times New Roman", serif;
box-shadow: inset 0 1px 2px rgba(59, 29, 10, .16);
pointer-events: auto;
}
.book-context-field {
min-height: 138px;
font-size: 13px;
}
.judge-dais {
position: absolute;
left: 50%;
top: 27%;
z-index: 6;
width: min(360px, 32vw);
min-width: 230px;
transform: translateX(-50%);
text-align: center;
}
.bench-front {
display: none;
}
.gavel {
display: none;
}
.gavel::before {
content: "";
display: none;
}
.phase-verdict .gavel {
animation: gavel-hit .55s ease-out both;
}
.counsel-table {
position: absolute;
bottom: 19%;
z-index: 6;
width: min(255px, 22vw);
height: 84px;
border: 0;
background: transparent;
box-shadow: none;
}
.counsel-table.left { left: 17%; }
.counsel-table.right { right: 17%; }
.trial-floor-mark {
display: none;
}
.witness-area {
position: absolute;
right: 8.5%;
bottom: 28%;
z-index: 6;
width: min(190px, 18vw);
height: 98px;
border: 0;
background: transparent;
box-shadow: none;
}
.jury-benches {
position: absolute;
top: 43%;
z-index: 7;
width: min(220px, 16vw);
min-width: 150px;
display: grid;
gap: 6px;
}
.jury-benches.left {
left: 1%;
}
.jury-benches.right {
right: 1%;
}
.jury-benches.left .jury-row {
transform: rotate(-7deg) skewY(-3deg);
}
.jury-benches.right .jury-row {
transform: rotate(7deg) skewY(3deg);
}
.jury-rail {
display: none;
}
.jury-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
align-items: end;
}
.gallery-benches {
display: none;
}
.gallery-benches div {
display: none;
}
.prop-label {
display: none;
}
.foreground-props {
position: absolute;
inset: 0;
z-index: 13;
pointer-events: none;
}
.foreground-fence,
.judge-table-foreground {
position: absolute;
display: block;
max-width: none;
height: auto;
filter: none;
opacity: 1;
pointer-events: none;
user-select: none;
}
.foreground-fence {
bottom: -6.5%;
width: 47%;
}
.foreground-fence.fence-left {
left: 0;
transform: translateX(-2%);
}
.foreground-fence.fence-right {
right: 0;
transform: translateX(2%);
}
.judge-table-foreground {
left: 50%;
top: 20%;
z-index: 1;
width: 39.1%;
transform: translateX(-50%);
}
.puppet {
--skin: #c99257;
--robe: #282128;
--accent: #8a2f2f;
--portrait-width: 74px;
--portrait-top: -14px;
position: absolute;
z-index: 8;
width: 72px;
height: 128px;
transform: translate(-50%, -100%);
transform-origin: center bottom;
filter: none;
color: inherit;
text-decoration: none;
}
.puppet.small {
width: 50px;
height: 94px;
--portrait-width: 54px;
--portrait-top: -8px;
}
.puppet.active {
animation: puppet-breathe 1.45s ease-in-out infinite;
}
.puppet.walking {
animation: lawyer-walk 1.9s ease-in-out infinite;
}
.puppet.judge {
left: 50%;
top: calc(40% + 156px);
--skin: #c38a55;
--robe: #1b1b20;
--accent: #79242a;
--portrait-width: 96px;
--portrait-top: -28px;
}
.puppet.clerk {
left: 43%;
top: 66%;
z-index: 14;
--skin: #b77b52;
--robe: #365548;
--accent: #2f6f5e;
}
.puppet.auric {
left: 24%;
top: 87%;
--skin: #c9975d;
--robe: #5b2719;
--accent: #a45c25;
}
.speaker-auric .puppet.auric {
left: 43%;
top: 87%;
}
.puppet.sable {
left: 75%;
top: 87%;
--skin: #a86d4a;
--robe: #1d3045;
--accent: #254f7a;
}
.speaker-sable .puppet.sable {
left: 75%;
top: 87%;
}
.puppet-portrait {
position: absolute;
left: 50%;
top: var(--portrait-top);
z-index: 3;
width: var(--portrait-width);
height: auto;
max-height: 118px;
transform: translateX(-50%);
object-fit: contain;
pointer-events: none;
}
.puppet::before {
content: "";
position: absolute;
left: 50%;
top: 0;
width: 44px;
height: 44px;
transform: translateX(-50%);
border: 2px solid rgba(255, 232, 174, .58);
border-radius: 50%;
background:
radial-gradient(circle at 34% 32%, rgba(255,255,255,.38), transparent 22%),
radial-gradient(circle at 36% 42%, #1b120c 0 2px, transparent 2.5px),
radial-gradient(circle at 62% 42%, #1b120c 0 2px, transparent 2.5px),
linear-gradient(180deg, var(--skin), #8b5638);
}
.puppet::after {
content: "";
position: absolute;
left: 50%;
top: 48px;
width: 58px;
height: 70px;
transform: translateX(-50%);
border: 1px solid rgba(255, 232, 174, .22);
border-radius: 24px 24px 8px 8px;
background:
linear-gradient(90deg, transparent 46%, rgba(255, 226, 154, .14) 49%, transparent 52%),
linear-gradient(180deg, var(--accent), var(--robe) 52%, #130a07);
}
.puppet.judge::before,
.puppet.judge::after {
display: none;
}
.puppet .mouth {
position: absolute;
left: 50%;
top: 27px;
z-index: 2;
width: 15px;
height: 7px;
transform: translateX(-50%);
border-bottom: 2px solid #28150c;
border-radius: 0 0 18px 18px;
}
.puppet.judge .mouth {
display: none;
}
.puppet.active .mouth,
.puppet.walking .mouth {
animation: speak-mouth .5s ease-in-out infinite;
}
.speech-bubble.active-dialogue {
position: absolute;
left: 50%;
top: 43%;
bottom: auto;
z-index: 30;
width: min(500px, calc(100vw - 44px));
max-height: 34vh;
overflow: visible;
transform: translate(-50%, -100%);
padding: 10px 13px 11px;
border: 2px solid #141413;
border-radius: 20px;
background: rgba(255, 253, 247, .97);
color: #141413 !important;
box-shadow: 0 12px 24px rgba(0, 0, 0, .32);
font-size: 12px;
font-weight: 650;
line-height: 1.32;
pointer-events: none;
}
.speech-bubble.active-dialogue,
.speech-bubble.active-dialogue * {
color: #141413 !important;
}
.speech-bubble.active-dialogue::before,
.speech-bubble.active-dialogue::after {
content: "";
position: absolute;
left: var(--bubble-tail-x, 50%);
display: block;
transform: translateX(-50%) rotate(45deg);
}
.speech-bubble.active-dialogue::before {
bottom: -13px;
width: 22px;
height: 22px;
background: #141413;
border-radius: 0 0 5px 0;
}
.speech-bubble.active-dialogue::after {
bottom: -9px;
width: 16px;
height: 16px;
transform: translateX(-50%) rotate(45deg);
background: rgba(255, 253, 247, .97);
border-radius: 0 0 3px 0;
}
.speech-bubble.active-dialogue.pending {
opacity: .82;
}
.dialogue-meta {
display: flex;
align-items: baseline;
gap: 6px;
margin-bottom: 5px;
font: 800 9px/1.2 ui-monospace, SFMono-Regular, Consolas, monospace;
text-transform: uppercase;
}
.dialogue-meta strong {
font-size: 10px;
}
.dialogue-text {
max-height: calc(34vh - 42px);
overflow: auto;
white-space: pre-wrap;
}
.speech-bubble.active-dialogue.speaker-clerk { left: 43%; top: 62%; }
.speech-bubble.active-dialogue.speaker-judge { left: 50%; top: 43%; }
.speech-bubble.active-dialogue.speaker-auric { left: 43%; top: 78%; }
.speech-bubble.active-dialogue.speaker-sable { left: 75%; top: 78%; }
.speech-bubble.active-dialogue.juror-dialogue { left: 50%; top: 57%; }
.speech-bubble.active-dialogue.juror-dialogue {
top: 42%;
width: min(340px, calc(50vw - 24px));
}
.speech-bubble.active-dialogue.speaker-karl-marx,
.speech-bubble.active-dialogue.speaker-john-stuart-mill,
.speech-bubble.active-dialogue.speaker-confucius {
left: 1.5%;
transform: translateY(-100%);
}
.speech-bubble.active-dialogue.speaker-cleopatra-vii,
.speech-bubble.active-dialogue.speaker-niccolo-machiavelli,
.speech-bubble.active-dialogue.speaker-jensen-huang {
right: 1.5%;
left: auto;
transform: translateY(-100%);
}
.speech-bubble.active-dialogue.speaker-karl-marx,
.speech-bubble.active-dialogue.speaker-cleopatra-vii {
--bubble-tail-x: 19%;
}
.speech-bubble.active-dialogue.speaker-john-stuart-mill,
.speech-bubble.active-dialogue.speaker-niccolo-machiavelli {
--bubble-tail-x: 50%;
}
.speech-bubble.active-dialogue.speaker-confucius,
.speech-bubble.active-dialogue.speaker-jensen-huang {
--bubble-tail-x: 81%;
}
.verdict-popup {
position: absolute;
left: 50%;
top: 54%;
z-index: 42;
width: min(460px, calc(100vw - 44px));
transform: translate(-50%, -50%);
padding: 18px 22px 20px;
border: 2px solid rgba(255, 235, 178, .94);
border-radius: 8px;
background: rgba(20, 12, 7, .95);
color: #fff4d6;
text-align: center;
box-shadow: 0 28px 58px rgba(0, 0, 0, .5);
animation: verdict-pop .34s ease-out both;
}
.verdict-popup-kicker {
display: block;
margin-bottom: 7px;
color: #d9b060;
font: 800 11px/1 ui-monospace, SFMono-Regular, Consolas, monospace;
letter-spacing: 0;
text-transform: uppercase;
}
.verdict-popup-finding {
display: block;
color: #fff8e6;
font: 900 clamp(28px, 5vw, 48px)/1.02 Georgia, serif;
}
.verdict-popup-decree {
margin: 10px auto 0;
max-width: 38ch;
color: rgba(255, 244, 214, .86);
font-size: 13px;
line-height: 1.35;
}
.tooltip {
position: absolute;
left: 50%;
bottom: calc(100% + 10px);
z-index: 20;
width: 320px;
max-width: min(360px, calc(100vw - 32px));
transform: translateX(-50%) translateY(6px);
opacity: 0;
pointer-events: none;
padding: 8px 10px;
border: 1px solid rgba(255, 226, 154, .34);
border-radius: 5px;
background: rgba(17, 9, 5, .88);
color: #fff0d2;
box-shadow: 0 12px 24px rgba(0,0,0,.36);
transition: opacity .18s ease, transform .18s ease;
}
.puppet:hover .tooltip,
.puppet:focus-within .tooltip,
.juror:hover .tooltip,
.juror:focus-within .tooltip {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
.tooltip strong {
display: block;
color: #fff6df;
font-size: 13px;
}
.tooltip p {
margin: 6px 0 0;
color: #f5dfb5;
font-size: 11px;
line-height: 1.28;
white-space: normal;
}
.tooltip-meta {
margin-top: 3px;
color: #f4d58f;
font-size: 10px;
}
.tooltip-io-label,
.thread-label {
display: block;
margin-top: 7px;
color: #f4d58f;
font: 800 10px/1.2 ui-monospace, SFMono-Regular, Consolas, monospace;
text-transform: uppercase;
}
.ai-thread-modal {
display: none;
position: fixed;
inset: max(18px, 4vh) max(18px, 5vw);
z-index: 120;
overflow: auto;
padding: 20px;
border: 1px solid rgba(255, 226, 154, .42);
border-radius: 8px;
background: rgba(18, 9, 5, .96);
color: #fff0d2;
box-shadow: 0 24px 70px rgba(0, 0, 0, .58);
}
.ai-thread-modal:target {
display: block;
}
.thread-close {
position: sticky;
top: 0;
float: right;
padding: 7px 10px;
border: 1px solid rgba(255, 226, 154, .38);
border-radius: 4px;
background: rgba(255, 226, 154, .12);
color: #fff0d2;
text-decoration: none;
font: 800 11px/1 ui-monospace, SFMono-Regular, Consolas, monospace;
}
.thread-title {
margin: 0 0 4px;
color: #fff6df;
font-size: 22px;
}
.thread-subtitle {
margin: 0 0 16px;
color: #f4d58f;
font: 800 12px/1.3 ui-monospace, SFMono-Regular, Consolas, monospace;
text-transform: uppercase;
}
.thread-turn {
margin: 0 0 18px;
padding-bottom: 16px;
border-bottom: 1px solid rgba(255, 226, 154, .16);
}
.thread-turn:last-child {
border-bottom: 0;
}
.thread-block {
margin: 7px 0 0;
white-space: pre-wrap;
color: #f8dfaa;
font: 12px/1.42 ui-monospace, SFMono-Regular, Consolas, monospace;
}
.juror {
--face: #c89259;
--juror-image: none;
position: relative;
height: 72px;
transform-origin: center bottom;
filter: none;
color: inherit;
text-decoration: none;
}
.juror.active {
animation: juror-react .82s ease-in-out infinite alternate;
}
.juror-face {
position: absolute;
left: 50%;
top: 0;
width: 30px;
height: 30px;
transform: translateX(-50%);
border: 2px solid rgba(255, 232, 174, .5);
border-radius: 50%;
background:
radial-gradient(circle at 35% 40%, #1d1109 0 2px, transparent 2.5px),
radial-gradient(circle at 64% 40%, #1d1109 0 2px, transparent 2.5px),
linear-gradient(180deg, var(--face), #835235);
}
.juror-face::after {
content: "";
position: absolute;
left: 10px;
bottom: 9px;
width: 14px;
height: 7px;
border-bottom: 2px solid #25140c;
border-radius: 0 0 18px 18px;
}
.juror-portrait {
position: absolute;
left: 50%;
top: -19px;
z-index: 3;
width: 58px;
height: 74px;
transform: translateX(-50%);
object-fit: contain;
pointer-events: none;
}
.juror-body {
position: absolute;
left: 50%;
top: 32px;
width: 36px;
height: 36px;
transform: translateX(-50%);
border-radius: 20px 20px 7px 7px;
border: 1px solid rgba(255, 232, 174, .18);
background: linear-gradient(180deg, #5b496f, #211726);
}
.phase-deliberation .juror:nth-child(odd) {
animation-delay: .18s;
}
.evidence-props {
position: absolute;
left: 55%;
right: 11%;
bottom: 36%;
z-index: 9;
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
pointer-events: auto;
}
.evidence-sheet {
width: 96px;
min-height: 72px;
padding: 8px;
transform: rotate(var(--tilt));
border: 1px solid rgba(56, 32, 15, .22);
border-radius: 3px;
background:
linear-gradient(135deg, transparent 0 84%, rgba(64, 38, 20, .18) 85%),
#fff6df;
color: #372212;
box-shadow: 0 10px 20px rgba(0,0,0,.28);
opacity: .18;
transition: transform .25s ease, opacity .25s ease;
}
.phase-evidence .evidence-sheet,
.phase-questions .evidence-sheet,
.phase-deliberation .evidence-sheet,
.phase-verdict .evidence-sheet,
.phase-appeal .evidence-sheet {
opacity: .96;
animation: paper-land .55s ease-out both;
}
.evidence-sheet:hover {
transform: rotate(0) translateY(-8px) scale(1.08);
z-index: 15;
}
.evidence-sheet strong {
display: block;
margin-bottom: 4px;
color: #254f7a;
font: 800 12px/1 ui-monospace, SFMono-Regular, Consolas, monospace;
}
.evidence-sheet span {
display: block;
font-size: 11px;
line-height: 1.2;
}
.trial-caption {
position: absolute;
left: 50%;
bottom: 108px;
z-index: 14;
width: min(870px, calc(100% - 44px));
transform: translateX(-50%);
padding: 12px 16px 13px;
border: 1px solid rgba(255, 226, 154, .34);
border-radius: 6px;
background: rgba(13, 7, 4, .78);
backdrop-filter: blur(12px);
box-shadow: 0 18px 36px rgba(0,0,0,.38);
}
.caption-title {
margin-top: 3px;
color: #fff3d7;
font-size: 20px;
font-weight: 800;
}
.caption-body {
margin-top: 5px;
color: #f8dfaa;
font-size: 14px;
line-height: 1.36;
white-space: pre-wrap;
}
.decree-ribbon {
position: absolute;
right: 26px;
top: 22px;
z-index: 10;
max-width: 230px;
padding: 9px 11px;
border: 1px solid rgba(255, 226, 154, .26);
border-radius: 5px;
background: rgba(18, 9, 5, .68);
color: #ffe6ae;
font: 800 11px/1.35 ui-monospace, SFMono-Regular, Consolas, monospace;
text-transform: uppercase;
}
.phase-verdict .judge-dais,
.phase-questions .judge-dais {
animation: bench-lean .9s ease-in-out infinite alternate;
}
.phase-deliberation .jury-benches {
animation: jury-murmur .7s ease-in-out infinite alternate;
}
.stage-prop-link {
cursor: help;
}
.drawer-shell {
max-width: 1500px;
margin: 12px auto 0;
}
.drawer-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 18px 28px;
}
.drawer-text-stack {
color: var(--ink);
line-height: 1.42;
}
.drawer-text-block {
color: var(--ink);
}
.drawer-text-block h4 {
margin: 5px 0 7px;
}
.drawer-text-block p,
.drawer-empty {
margin: 0 0 8px;
line-height: 1.38;
white-space: pre-wrap;
}
.vote-liable { color: var(--red); font-weight: 800; }
.vote-not_liable { color: var(--green); font-weight: 800; }
.vote-uncertain { color: var(--blue); font-weight: 800; }
.mind-text {
max-height: 340px;
overflow: auto;
color: var(--ink);
font: 12px/1.42 ui-monospace, SFMono-Regular, Consolas, monospace;
white-space: pre-wrap;
}
@keyframes puppet-breathe {
0%, 100% { transform: translate(-50%, -100%) translateY(0); }
50% { transform: translate(-50%, -100%) translateY(-4px); }
}
@keyframes lawyer-walk {
0%, 100% { transform: translate(-50%, -100%) translateY(0) rotate(-1deg); }
25% { transform: translate(-50%, -100%) translateY(-7px) rotate(2deg); }
50% { transform: translate(-50%, -100%) translateY(0) rotate(1deg); }
75% { transform: translate(-50%, -100%) translateY(-5px) rotate(-2deg); }
}
@keyframes speak-mouth {
0%, 100% { height: 5px; border-radius: 0 0 18px 18px; }
50% { height: 10px; border-radius: 50%; border: 2px solid #28150c; }
}
@keyframes juror-react {
from { transform: translateY(0) rotate(-1deg); }
to { transform: translateY(-5px) rotate(2deg); }
}
@keyframes evidence-focus {
0%, 100% { transform: translate(-50%, -100%) translateY(0) scale(1); }
50% { transform: translate(-50%, -100%) translateY(-6px) scale(1.035); }
}
@keyframes paper-land {
from { transform: rotate(var(--tilt)) translateY(-18px); opacity: 0; }
to { transform: rotate(var(--tilt)) translateY(0); opacity: .96; }
}
@keyframes bench-lean {
from { transform: translateX(-50%) translateY(0); }
to { transform: translateX(-50%) translateY(-5px); }
}
@keyframes jury-murmur {
from { transform: translateX(0); }
to { transform: translateX(-3px); }
}
@keyframes gavel-hit {
0% { transform: rotate(-18deg) translateY(0); }
45% { transform: rotate(21deg) translateY(18px); }
100% { transform: rotate(-18deg) translateY(0); }
}
@keyframes verdict-pop {
0% {
opacity: 0;
transform: translate(-50%, -46%) scale(.94);
}
100% {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
}
@media (max-width: 820px) {
.docket-book-controls {
position: fixed;
top: 130px;
width: calc(100vw - 52px);
transform: translateX(-50%) rotate(-1deg);
}
.trial-progress {
grid-template-columns: repeat(8, minmax(24px, 1fr));
padding: 2px 5px 3px;
}
.trial-progress-segment {
font-size: 9px;
letter-spacing: 0;
}
.trial-progress-label {
display: none;
}
.trial-progress-abbrev {
display: inline;
}
.court-episode-stage {
height: 1280px;
min-height: 1280px;
}
.episode-room {
background-position: center top;
}
.episode-title {
left: 16px;
right: 16px;
max-width: none;
}
.decree-ribbon {
top: 164px;
left: 16px;
right: auto;
max-width: calc(100% - 32px);
}
.speech-bubble.active-dialogue,
.speech-bubble.active-dialogue.speaker-clerk,
.speech-bubble.active-dialogue.speaker-judge,
.speech-bubble.active-dialogue.speaker-auric,
.speech-bubble.active-dialogue.speaker-sable,
.speech-bubble.active-dialogue.juror-dialogue {
left: 50%;
top: 218px;
width: calc(100% - 28px);
max-height: 260px;
transform: translateX(-50%);
}
.speech-bubble.active-dialogue::after {
display: none;
}
.speech-bubble.active-dialogue.juror-dialogue,
.speech-bubble.active-dialogue.speaker-karl-marx,
.speech-bubble.active-dialogue.speaker-john-stuart-mill,
.speech-bubble.active-dialogue.speaker-confucius,
.speech-bubble.active-dialogue.speaker-cleopatra-vii,
.speech-bubble.active-dialogue.speaker-niccolo-machiavelli,
.speech-bubble.active-dialogue.speaker-jensen-huang {
top: 500px;
width: min(320px, calc(100vw - 28px));
transform: translateY(-100%);
}
.speech-bubble.active-dialogue.speaker-karl-marx,
.speech-bubble.active-dialogue.speaker-john-stuart-mill,
.speech-bubble.active-dialogue.speaker-confucius {
left: 14px;
right: auto;
}
.speech-bubble.active-dialogue.speaker-cleopatra-vii,
.speech-bubble.active-dialogue.speaker-niccolo-machiavelli,
.speech-bubble.active-dialogue.speaker-jensen-huang {
right: 14px;
left: auto;
}
.episode-book {
top: 218px;
width: min(680px, calc(100% - 20px));
}
.episode-book.closed {
top: 640px;
width: 140px;
}
.book-open-content {
grid-template-columns: 1fr;
gap: 10px;
inset: 15% 11% 13%;
padding: 0 16px;
}
.book-open-content h2 {
font-size: 22px;
margin-bottom: 5px;
}
.book-open-content p,
.book-entry {
font-size: 12px;
line-height: 1.22;
}
.book-entry {
margin: 5px 0;
}
.book-evidence-columns {
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.book-evidence-list li {
font-size: 10px;
line-height: 1.12;
}
.book-field {
min-height: 34px;
font-size: 10px;
}
.book-context-field {
min-height: 84px;
}
.judge-dais {
top: 390px;
width: 280px;
}
.counsel-table.left {
left: 7%;
bottom: 470px;
}
.counsel-table.right {
right: 7%;
bottom: 470px;
}
.counsel-table {
width: 154px;
}
.puppet.auric {
left: 20%;
top: 970px;
}
.puppet.sable {
left: 80%;
top: 970px;
}
.speaker-auric .puppet.auric {
left: 42%;
top: 970px;
}
.speaker-sable .puppet.sable {
left: 80%;
top: 970px;
}
.puppet.clerk {
left: 35%;
top: 880px;
}
.witness-area {
right: 5%;
bottom: 580px;
width: 138px;
}
.jury-benches {
top: 520px;
width: 126px;
min-width: 126px;
}
.jury-benches.left {
left: .5%;
}
.jury-benches.right {
right: .5%;
}
.foreground-fence {
bottom: -66px;
width: 64%;
}
.foreground-fence.fence-left {
left: -17%;
}
.foreground-fence.fence-right {
right: -17%;
}
.judge-table-foreground {
top: 213px;
width: 646px;
}
.evidence-props {
left: 8%;
right: 8%;
bottom: 410px;
}
.trial-caption {
bottom: 105px;
}
.gallery-benches {
bottom: 42px;
grid-template-columns: repeat(3, 1fr);
}
}
"""
APP_JS = f"""
() => {{
const paths = {json.dumps(AUDIO_PATHS)};
const SCORE_BASE_VOLUME = 0.16;
const SCORE_QUIET_VOLUME = 0.035;
const SCORE_BREATH_INTERVAL_MS = 20000;
const SCORE_BREATH_DURATION_MS = 5000;
const make = (name, volume = 1, loop = false) => {{
const audio = new Audio(paths[name]);
audio.preload = 'auto';
audio.volume = volume;
audio.loop = loop;
return audio;
}};
if (!window.SovereignCourtAudio) {{
const controller = {{
unlocked: false,
lastPhase: null,
muted: false,
scoreVolume: SCORE_BASE_VOLUME,
crowdVolume: 0.0,
fadeFrame: null,
breathTimer: null,
score: make('score', SCORE_BASE_VOLUME, true),
crowd: make('crowd', 0.0, true),
begin() {{
this.unlocked = true;
this.ensureLooping();
this.startBreathing();
this.play('select', 0.26);
window.setTimeout(() => this.play('paper_long', 0.45), 120);
window.setTimeout(() => this.play('gavel', 0.72), 520);
this.observePhase();
this.updateToggle();
}},
ensureLooping() {{
if (!this.unlocked || this.muted) return;
this.applyLoopVolumes();
this.score.play().catch(() => {{}});
this.crowd.play().catch(() => {{}});
}},
applyLoopVolumes() {{
this.score.volume = this.muted ? 0 : this.scoreVolume;
this.crowd.volume = this.muted ? 0 : this.crowdVolume;
}},
play(name, volume = 1) {{
if (!this.unlocked || this.muted) return;
const cue = make(name, volume, false);
cue.play().catch(() => {{}});
}},
setCrowd(volume) {{
this.crowdVolume = volume;
this.applyLoopVolumes();
}},
fadeScore(toVolume, duration, onComplete) {{
if (this.fadeFrame) window.cancelAnimationFrame(this.fadeFrame);
const fromVolume = this.scoreVolume;
const started = window.performance.now();
const step = (now) => {{
const progress = Math.min(1, (now - started) / duration);
this.scoreVolume = fromVolume + ((toVolume - fromVolume) * progress);
this.applyLoopVolumes();
if (progress < 1) {{
this.fadeFrame = window.requestAnimationFrame(step);
}} else {{
this.fadeFrame = null;
if (onComplete) onComplete();
}}
}};
this.fadeFrame = window.requestAnimationFrame(step);
}},
breatheScore() {{
if (!this.unlocked) return;
const halfDuration = SCORE_BREATH_DURATION_MS / 2;
this.fadeScore(SCORE_QUIET_VOLUME, halfDuration, () => {{
this.fadeScore(SCORE_BASE_VOLUME, halfDuration);
}});
}},
startBreathing() {{
if (this.breathTimer) return;
this.breathTimer = window.setInterval(() => this.breatheScore(), SCORE_BREATH_INTERVAL_MS);
}},
toggleMuted() {{
this.muted = !this.muted;
if (this.muted) {{
this.applyLoopVolumes();
this.score.pause();
this.crowd.pause();
}} else {{
this.ensureLooping();
}}
this.updateToggle();
}},
updateToggle() {{
document.querySelectorAll('.sound-toggle').forEach((button) => {{
button.classList.toggle('muted', this.muted);
button.setAttribute('aria-pressed', String(this.muted));
button.setAttribute('title', this.muted ? 'Sound off' : 'Sound on');
}});
}},
cuePhase(phase) {{
if (!this.unlocked || !phase || phase === this.lastPhase) return;
this.lastPhase = phase;
if (phase === 'intake') {{
this.setCrowd(0.08);
this.play('paper', 0.45);
this.play('wood', 0.42);
}} else if (phase === 'claims' || phase === 'opening') {{
this.setCrowd(0.045);
this.play('steps', 0.33);
}} else if (phase === 'evidence') {{
this.setCrowd(0.035);
this.play('paper_long', 0.52);
}} else if (phase === 'questions') {{
this.setCrowd(0.02);
this.play('wood', 0.34);
}} else if (phase === 'deliberation') {{
this.setCrowd(0.18);
}} else if (phase === 'verdict') {{
this.setCrowd(0.0);
this.play('judgement', 0.66);
window.setTimeout(() => this.play('gavel', 0.9), 650);
}} else if (phase === 'appeal') {{
this.setCrowd(0.035);
this.play('paper_long', 0.5);
}}
}},
observePhase() {{
const stage = document.querySelector('.court-episode-stage');
if (stage) this.cuePhase(stage.dataset.phase);
this.updateToggle();
}}
}};
window.SovereignCourtAudio = controller;
const observer = new MutationObserver(() => controller.observePhase());
observer.observe(document.body, {{ childList: true, subtree: true, attributes: true, attributeFilter: ['data-phase'] }});
document.addEventListener('click', (event) => {{
const toggle = event.target.closest('.sound-toggle');
if (toggle) {{
event.preventDefault();
controller.toggleMuted();
return;
}}
if (event.target.closest('.docket-book-controls')) {{
controller.play('select', 0.22);
}}
}}, true);
}}
}}
"""
APP_HEAD = f"""
<script>
(function() {{
const installCourtAudio = {APP_JS.strip()};
if (document.readyState === 'loading') {{
document.addEventListener('DOMContentLoaded', installCourtAudio, {{ once: true }});
}} else {{
installCourtAudio();
}}
}})();
</script>
"""
START_JS = """
(case_label, search_query, hypothetical, custom_payload, speed, mind_layer) => {
const book = document.querySelector('.episode-book.custom-book');
const collect = (selector) => Array.from(document.querySelectorAll(selector)).map((node) => node.value || '');
const payload = book ? JSON.stringify({
context: document.querySelector('.book-context-field')?.value || '',
claimant_evidence: collect('.book-claimant-field'),
respondent_evidence: collect('.book-respondent-field')
}) : (custom_payload || '');
if (book) {
const data = JSON.parse(payload);
const hasContext = data.context.trim().length > 0;
const hasClaimant = data.claimant_evidence.some((value) => value.trim().length > 0);
const hasRespondent = data.respondent_evidence.some((value) => value.trim().length > 0);
if (!hasContext || !hasClaimant || !hasRespondent) {
return [case_label, search_query, hypothetical, payload, speed, mind_layer];
}
}
document.body.classList.add('trial-has-started');
if (window.SovereignCourtAudio) {
window.SovereignCourtAudio.begin();
}
return [case_label, search_query, hypothetical, payload, speed, mind_layer];
}
"""
CHARACTERS = {
JUDGE_NAME: {
"class": "judge",
"name": JUDGE_NAME,
"role": "Stoic presiding judge",
"model": "gpt-oss-20b",
"image": "/gradio_api/file=assets/characters/marcus-aurelius.png",
},
"Clerk Meridian": {
"class": "clerk",
"name": "Clerk Meridian",
"role": "Court clerk",
"model": "AgentCPM-Explore",
},
"Mike OSS": {
"class": "auric",
"name": "Mike OSS",
"role": "Claimant advocate",
"model": "gpt-oss-20b",
},
"Harvey Vector": {
"class": "sable",
"name": "Harvey Vector",
"role": "Respondent advocate",
"model": "gpt-oss-20b",
},
"Nemotron Jury": {
"class": "jury",
"name": "Nemotron Jury",
"role": "Jury panel",
"model": "Nemotron-Orchestrator-8B",
},
}
JUROR_FACES = {
"Karl Marx": "#d0b79c",
"John Stuart Mill": "#c99b72",
"Confucius": "#c49a64",
"Cleopatra VII": "#b98755",
"Niccolo Machiavelli": "#b88963",
"Jensen Huang": "#b37758",
}
JUROR_IMAGES = {
"Karl Marx": "/gradio_api/file=assets/characters/karl-marx.png",
"John Stuart Mill": "/gradio_api/file=assets/characters/john-stuart-mill.png",
"Confucius": "/gradio_api/file=assets/characters/confucius.png",
"Cleopatra VII": "/gradio_api/file=assets/characters/cleopatra-vii.png",
"Niccolo Machiavelli": "/gradio_api/file=assets/characters/niccolo-machiavelli.png",
"Jensen Huang": "/gradio_api/file=assets/characters/jensen-huang.png",
}
TRIAL_TURN_ORDER = (
"Clerk Meridian",
JUDGE_NAME,
"Mike OSS",
"Harvey Vector",
JUDGE_NAME,
"Mike OSS",
"Harvey Vector",
"Nemotron Jury",
*JUROR_PERSONAS.keys(),
JUDGE_NAME,
)
PHASE_AGENTS = {
"pretrial": ["Clerk Meridian"],
}
@dataclass(frozen=True)
class SpeakerCue:
name: str
role: str
text: str
pending: bool = False
_EVENT_STREAM_DONE = object()
def _remote_events(request: TrialRequest) -> Iterable[TrialEvent] | None:
endpoint = os.getenv("MODAL_TRIAL_URL", DEFAULT_MODAL_TRIAL_URL).strip()
if not endpoint:
return None
def iterator() -> Iterable[TrialEvent]:
with httpx.stream("POST", endpoint, json=request.model_dump(), timeout=900.0) as response:
response.raise_for_status()
for line in response.iter_lines():
if line:
yield TrialEvent.model_validate_json(line)
return iterator()
def get_events(request: TrialRequest, delay: float | None = None) -> Iterable[TrialEvent]:
remote = _remote_events(request)
if remote is not None:
yield from remote
return
stream_delay = {"swift": 1.4, "measured": 2.4, "ceremonial": 3.4}[request.speed] if delay is None else delay
yield from stream_trial(request, delay=stream_delay)
def _escape(value: str) -> str:
return (
value.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace('"', "&quot;")
)
def _latest_packet_title(events: list[TrialEvent]) -> tuple[str, str]:
if not events:
return (
"Judge-GPT",
"The gallery doors open on an AI-native courtroom. Choose a case from the docket book and begin the proceeding.",
)
lines = events[0].body.splitlines()
title = lines[0] if lines else "Judge-GPT"
subtitle = lines[1] if len(lines) > 1 else events[0].title
return title, subtitle
def _active_agents_for(event: TrialEvent | None) -> set[str]:
if event is None:
return set(PHASE_AGENTS["pretrial"])
if not event.turns:
return set()
return {event.turns[0].agent}
def _active_speaker_for(event: TrialEvent | None) -> str:
if event is None:
return "Clerk Meridian"
if not event.turns:
return ""
return event.turns[0].agent
def _role_for_speaker(name: str, event: TrialEvent | None = None) -> str:
if event is not None:
turn = next((turn for turn in event.turns if turn.agent == name), None)
if turn is not None:
return turn.role
if name in CHARACTERS:
return CHARACTERS[name]["role"]
if name in JUROR_FACES:
return "juror"
return "speaker"
def _expected_next_speaker(events: list[TrialEvent]) -> SpeakerCue | None:
if len(events) >= len(TRIAL_TURN_ORDER):
return None
name = TRIAL_TURN_ORDER[len(events)]
role = _role_for_speaker(name)
return SpeakerCue(name=name, role=role, text=f"{name} is preparing a response.", pending=True)
def _speaker_class_for(speaker: str) -> str:
if not speaker:
return ""
if speaker in CHARACTERS:
return f" speaker-{CHARACTERS[speaker]['class']}"
return " speaker-" + "".join(ch.lower() if ch.isalnum() else "-" for ch in speaker).strip("-")
def _latest_turn_text(event: TrialEvent | None, agent: str) -> str:
if event is None:
return ""
turn = next((turn for turn in event.turns if turn.agent == agent), None)
if turn is None:
return ""
return _short_text(turn.content, 210)
def _active_speaker_cue(event: TrialEvent | None, pending_speaker: SpeakerCue | None = None) -> SpeakerCue | None:
if pending_speaker is not None:
return pending_speaker
if event is None or not event.turns:
return None
turn = event.turns[0]
text = turn.content.strip()
if not text:
return None
return SpeakerCue(name=turn.agent, role=turn.role, text=text)
def _reading_duration(text: str) -> float:
word_count = len(text.split())
return min(MAX_READ_SECONDS, max(MIN_READ_SECONDS, (word_count / WORDS_PER_SECOND) + READ_BUFFER_SECONDS))
def _event_dialogue_text(event: TrialEvent) -> str:
if event.turns:
return event.turns[0].content
return event.body
def _event_status(event: TrialEvent, step: int) -> str:
if event.turns:
return f"Step {step}: {event.turns[0].agent} - {event.title}"
return f"Step {step}: {event.title}"
def _pending_status(cue: SpeakerCue | None) -> str:
if cue is None:
return "The court is preparing the next turn."
return f"{cue.name} is preparing their response."
def _start_event_producer(request: TrialRequest) -> queue.Queue[object]:
events: queue.Queue[object] = queue.Queue()
def produce() -> None:
try:
try:
stream = get_events(request, delay=0.0)
except TypeError:
stream = get_events(request)
for event in stream:
events.put(event)
except Exception as exc:
events.put(exc)
finally:
events.put(_EVENT_STREAM_DONE)
threading.Thread(target=produce, name="trial-event-producer", daemon=True).start()
return events
def _thread_id(name: str) -> str:
return "ai-thread-" + "".join(ch.lower() if ch.isalnum() else "-" for ch in name).strip("-")
def _turns_for_agent(events: list[TrialEvent], agent: str) -> list[dict[str, str]]:
turns = []
for event in events:
for turn in event.turns:
if turn.agent == agent:
turns.append(
{
"phase": event.phase,
"title": event.title,
"role": turn.role,
"model": turn.model,
"confidence": f"{turn.confidence:.2f}",
"input": turn.input or "Prompt unavailable for this turn.",
"output": turn.content or "No output captured yet.",
}
)
return turns
def _thread_for_character(events: list[TrialEvent], agent: str) -> list[dict[str, str]]:
if agent in JUROR_FACES:
turns = _turns_for_agent(events, agent)
vote = next((vote for event in reversed(events) for vote in event.votes if vote.juror == agent), None)
if not turns:
turns = _turns_for_agent(events, "Nemotron Jury")
if vote and turns:
turns = [
dict(
turn,
output=f"{turn['output']}\n\n{agent} persona: {vote.persona}\n{agent} vote: {vote.vote}\nReason: {vote.reason}",
)
for turn in turns
]
return turns
return _turns_for_agent(events, agent)
def _short_text(value: str, limit: int = 170) -> str:
squashed = " ".join(value.split())
return squashed if len(squashed) <= limit else squashed[: limit - 1].rstrip() + "..."
def _tooltip(name: str, role: str, model: str, turns: list[dict[str, str]]) -> str:
latest = turns[-1] if turns else None
input_preview = _short_text(latest["input"] if latest else "Waiting for this model to receive its first prompt.")
output_preview = _short_text(latest["output"] if latest else "No output has been emitted yet.")
return (
"<span class='tooltip'>"
f"<strong>{_escape(name)}</strong>"
f"{_escape(role)}"
f"<span class='tooltip-meta'>{_escape(model)}</span>"
"<span class='tooltip-io-label'>Input</span>"
f"<p>{_escape(input_preview)}</p>"
"<span class='tooltip-io-label'>Output</span>"
f"<p>{_escape(output_preview)}</p>"
"<span class='tooltip-meta'>Click to open full thread</span>"
"</span>"
)
def _thread_modal(name: str, role: str, model: str, turns: list[dict[str, str]]) -> str:
body = (
"".join(
"<section class='thread-turn'>"
f"<div class='thread-subtitle'>{_escape(turn['phase'])} / {_escape(turn['role'])} / confidence {turn['confidence']}</div>"
"<span class='thread-label'>Input</span>"
f"<div class='thread-block'>{_escape(turn['input'])}</div>"
"<span class='thread-label'>Output</span>"
f"<div class='thread-block'>{_escape(turn['output'])}</div>"
"</section>"
for turn in turns
)
or "<section class='thread-turn'><div class='thread-block'>Waiting for this model thread to appear.</div></section>"
)
return (
f"<aside id='{_escape(_thread_id(name))}' class='ai-thread-modal'>"
"<a class='thread-close' href='#court-stage'>Close</a>"
f"<h2 class='thread-title'>{_escape(name)}</h2>"
f"<div class='thread-subtitle'>{_escape(role)} / {_escape(model)}</div>"
f"{body}</aside>"
)
def _active_dialogue(cue: SpeakerCue | None) -> str:
if cue is None:
return ""
speaker_cls = _speaker_class_for(cue.name).strip()
classes = ["speech-bubble", "active-dialogue"]
if speaker_cls:
classes.append(speaker_cls)
if cue.name in JUROR_FACES:
classes.append("juror-dialogue")
if cue.pending:
classes.append("pending")
pending_attr = " data-pending='true'" if cue.pending else ""
return (
f"<div class='{' '.join(classes)}' data-speaker='{_escape(cue.name)}'{pending_attr}>"
"<div class='dialogue-meta'>"
f"<strong>{_escape(cue.name)}</strong>"
f"<span>{_escape(cue.role)}</span>"
"</div>"
f"<div class='dialogue-text'>{_escape(cue.text)}</div>"
"</div>"
)
def _verdict_popup(events: list[TrialEvent], show: bool) -> str:
if not show:
return ""
verdict = next((event.verdict for event in reversed(events) if event.verdict is not None), None)
if verdict is None:
return ""
finding = VERDICT_LABELS.get(verdict.finding, verdict.finding.replace("_", " ").title())
return (
f"<div class='verdict-popup' role='alert' aria-live='assertive' data-finding='{_escape(verdict.finding)}'>"
"<span class='verdict-popup-kicker'>Verdict</span>"
f"<strong class='verdict-popup-finding'>Verdict: {_escape(finding)}</strong>"
f"<p class='verdict-popup-decree'>{_escape(verdict.decree)}</p>"
"</div>"
)
def _puppet(agent: str, active_agents: set[str], phase: str, events: list[TrialEvent], latest: TrialEvent | None) -> str:
meta = CHARACTERS[agent]
active = " active" if agent in active_agents else ""
walking = " walking" if agent in {"Mike OSS", "Harvey Vector"} and agent in active_agents else ""
small = " small" if agent == "Clerk Meridian" else ""
turns = _thread_for_character(events, agent)
portrait = ""
if meta.get("image"):
portrait = (
f"<img class='puppet-portrait' src='{_escape(meta['image'])}' "
f"alt='{_escape(meta['name'])} bust' onerror=\"this.style.display='none'\">"
)
return (
f"<a class='puppet {meta['class']}{active}{walking}{small}' href='#{_escape(_thread_id(agent))}' aria-label='Open {_escape(agent)} model thread'>"
f"{portrait}"
"<span class='mouth'></span>"
f"{_tooltip(meta['name'], meta['role'], meta['model'], turns)}"
"</a>"
)
def _juror(name: str, active: bool, events: list[TrialEvent] | None = None, latest: TrialEvent | None = None) -> str:
face = JUROR_FACES.get(name, "#c89259")
image = JUROR_IMAGES.get(name, "")
active_cls = " active" if active else ""
turns = _thread_for_character(events or [], name)
portrait = (
f"<img class='juror-portrait' src='{_escape(image)}' alt='{_escape(name)} bust' "
"onerror=\"this.style.display='none'\">"
if image
else ""
)
fallback_art = "" if image else "<span class='juror-face'></span><span class='juror-body'></span>"
return (
f"<a class='juror{active_cls}' href='#{_escape(_thread_id(name))}' style='--face: {face}' aria-label='Open {_escape(name)} model thread'>"
f"{portrait}"
f"{fallback_art}"
f"{_tooltip(name, 'HF-style juror', 'Nemotron panel', turns)}"
"</a>"
)
def _packet_for_label(case_label: str) -> CasePacket:
return get_case(CASE_OPTIONS.get(case_label, "socrates"))
def _split_evidence(packet: CasePacket) -> tuple[list[EvidenceItem], list[EvidenceItem]]:
claimant = [item for item in packet.evidence if item.supports == "claimant"]
respondent = [item for item in packet.evidence if item.supports == "respondent"]
if len(claimant) < 3:
claimant.extend(item for item in packet.evidence if item.supports in {"mixed", "context"} and item not in claimant)
if len(respondent) < 3:
respondent.extend(item for item in packet.evidence if item.supports in {"mixed", "context"} and item not in respondent)
return claimant[:3], respondent[:3]
def _book_evidence_column(title: str, items: list[EvidenceItem]) -> str:
entries = "".join(
"<li>"
f"<strong>{_escape(item.title)}</strong><br>"
f"{_escape(item.note)}"
"</li>"
for item in items
)
return (
"<section class='book-evidence-column'>"
f"<h3>{_escape(title)}</h3>"
f"<ul class='book-evidence-list'>{entries}</ul>"
"</section>"
)
def _custom_evidence_fields(class_name: str, label: str) -> str:
fields = "".join(
f"<textarea class='book-field {class_name}' aria-label='{_escape(label)} {index}' "
f"placeholder='{_escape(label)} {index}'></textarea>"
for index in range(1, 4)
)
return f"<section class='book-evidence-column'><h3>{_escape(label)}</h3>{fields}</section>"
def _book(open_book: bool, packet: CasePacket | None = None, custom_mode: bool = False) -> str:
closed = "" if open_book else " closed"
custom_class = " custom-book" if custom_mode and open_book else ""
hidden_attr = "" if custom_mode and open_book else " aria-hidden='true'"
packet = packet or get_case("socrates")
if custom_mode and open_book:
left_page = (
"<section><h2>Trial details</h2>"
"<textarea class='book-field book-context-field' aria-label='Custom trial details' "
"placeholder='Write a short paragraph describing what happened and why the court is hearing it.'></textarea>"
"</section>"
)
right_page = (
"<section><h2>Evidence</h2><div class='book-evidence-columns'>"
f"{_custom_evidence_fields('book-claimant-field', 'Evidence for Claimant')}"
f"{_custom_evidence_fields('book-respondent-field', 'Evidence against Claimant')}"
"</div></section>"
)
else:
claimant_evidence, respondent_evidence = _split_evidence(packet)
left_page = (
"<section><h2>Trial details</h2>"
f"<p class='book-case-title'>{_escape(packet.title)}</p>"
f"<p class='book-context'>{_escape(packet.context or packet.setting)}</p>"
f"<div class='book-entry'><strong>{_escape(packet.claimant)}</strong><br>{_escape(packet.claimant_claim)}</div>"
f"<div class='book-entry'><strong>{_escape(packet.respondent)}</strong><br>{_escape(packet.respondent_claim)}</div>"
"</section>"
)
right_page = (
"<section><h2>Evidence</h2><div class='book-evidence-columns'>"
f"{_book_evidence_column(f'Evidence for {packet.claimant}', claimant_evidence)}"
f"{_book_evidence_column(f'Evidence for {packet.respondent}', respondent_evidence)}"
"</div></section>"
)
return (
f"<div class='episode-book{closed}{custom_class}'>"
"<img class='book-art open-art' src='/gradio_api/file=assets/book/docket-book-open.png' alt='Open docket book'>"
"<img class='book-art closed-art' src='/gradio_api/file=assets/book/docket-book-closed.png' alt='Closed docket book'>"
f"<div class='book-open-content'{hidden_attr}>"
f"{left_page}"
f"{right_page}"
"</div>"
"</div>"
)
def _caption(event: TrialEvent | None, phase: str) -> tuple[str, str, str]:
if event is None:
return (
"PRETRIAL",
"The Courtroom Is Ready",
"The docket is open, the room is dimmed, and the clerk is waiting for the first call.",
)
body = event.body.splitlines()[0] if event.body.splitlines() else event.body
return (f"{PHASE_GLYPHS[phase]} / {phase.upper()}", event.title, body)
def _evidence_props(events: list[TrialEvent]) -> str:
evidence = next((event.evidence for event in reversed(events) if event.evidence), [])
if not evidence:
return ""
tilts = ["-5deg", "3deg", "-2deg", "5deg"]
sheets = []
for idx, item in enumerate(evidence[:4]):
sheets.append(
f"<div class='evidence-sheet stage-prop-link' style='--tilt: {tilts[idx % len(tilts)]}' title='{_escape(item.note)}'>"
f"<strong>{_escape(item.id)}</strong>"
f"<span>{_escape(item.title)}</span></div>"
)
return f"<div class='evidence-props'>{''.join(sheets)}</div>"
def _foreground_props() -> str:
fence = "/gradio_api/file=assets/foreground/foregroundFence.png"
judge_table = "/gradio_api/file=assets/foreground/JudgeTable.png"
return (
"<div class='foreground-props' aria-hidden='true'>"
f"<img class='foreground-fence fence-left' src='{fence}' alt=''>"
f"<img class='foreground-fence fence-right' src='{fence}' alt=''>"
f"<img class='judge-table-foreground' src='{judge_table}' alt=''>"
"</div>"
)
def _trial_progress(events: list[TrialEvent]) -> str:
latest = events[-1] if events else None
current_phase = latest.phase if latest else "pretrial"
stage_keys = [key for key, _label in TRIAL_PROGRESS_STAGES]
current_index = stage_keys.index(current_phase) if current_phase in stage_keys else None
segments = []
for index, (key, label) in enumerate(TRIAL_PROGRESS_STAGES):
classes = ["trial-progress-segment"]
attrs = [f"data-phase='{_escape(key)}'"]
if current_index is not None and index < current_index:
classes.append("complete")
if current_index == index:
classes.append("current")
attrs.append("aria-current='step'")
if key == "verdict":
classes.append("complete")
abbrev = label[:3]
segments.append(
f"<span class='{' '.join(classes)}' {' '.join(attrs)}>"
f"<span class='trial-progress-label'>{_escape(label)}</span>"
f"<span class='trial-progress-abbrev' aria-hidden='true'>{_escape(abbrev)}</span>"
"</span>"
)
return (
"<nav class='trial-progress' aria-label='Trial progress'>"
+ "".join(segments)
+ "</nav>"
)
def _courtroom_juror_names(votes: list) -> list[str]:
names = list(JUROR_FACES)
names.extend(vote.juror for vote in votes if vote.juror not in names)
return names[:6]
def _latest_votes(events: list[TrialEvent]) -> list:
by_juror = {}
for event in events:
for vote in event.votes:
by_juror[vote.juror] = vote
ordered = [by_juror[name] for name in JUROR_FACES if name in by_juror]
ordered.extend(vote for juror, vote in by_juror.items() if juror not in JUROR_FACES)
return ordered
def render_court(
events: list[TrialEvent],
started: bool = False,
pending_speaker: SpeakerCue | None = None,
show_verdict_popup: bool = False,
pretrial_case: CasePacket | None = None,
custom_mode: bool = False,
) -> str:
latest = events[-1] if events else None
phase = latest.phase if latest else "pretrial"
title, subtitle = _latest_packet_title(events)
active_cue = _active_speaker_cue(latest, pending_speaker)
active_speaker = active_cue.name if active_cue is not None else _active_speaker_for(latest)
active_agents = {active_speaker} if active_speaker else _active_agents_for(latest)
speaker_cls = _speaker_class_for(active_speaker)
caption_phase, caption_title, caption_body = _caption(latest, phase)
latest_votes = _latest_votes(events)
juror_names = _courtroom_juror_names(latest_votes)
started_cls = " trial-started" if started or events else ""
book_open = not started and not events
puppets = "".join(
_puppet(agent, active_agents, phase, events, latest)
for agent in [JUDGE_NAME, "Clerk Meridian", "Mike OSS", "Harvey Vector"]
)
left_jurors = "".join(_juror(name, name == active_speaker, events, latest) for name in juror_names[:3])
right_jurors = "".join(_juror(name, name == active_speaker, events, latest) for name in juror_names[3:6])
evidence_props = _evidence_props(events)
thread_modals = "".join(
_thread_modal(meta["name"], meta["role"], meta["model"], _thread_for_character(events, agent))
for agent, meta in CHARACTERS.items()
) + "".join(
_thread_modal(name, "HF-style juror", "Nemotron panel", _thread_for_character(events, name))
for name in juror_names
)
return (
f"<section id='court-stage' class='court-episode-stage phase-{_escape(phase)}{_escape(speaker_cls)}{started_cls}' data-phase='{_escape(phase)}'>"
f"{_trial_progress(events)}"
"<div class='episode-room'></div>"
"<div class='audio-deck' aria-hidden='true'>"
+ "".join(f"<audio preload='auto' src='{_escape(src)}'></audio>" for src in AUDIO_PATHS.values())
+ "</div>"
"<button class='sound-toggle' type='button' aria-label='Toggle sound' aria-pressed='false' title='Sound on'>"
"<span class='sound-icon' aria-hidden='true'></span></button>"
"<div class='episode-title'>"
"<div class='episode-kicker'>Judge-GPT Virtual Courtroom</div>"
f"<h1>{_escape(title)}</h1>"
f"<p>{_escape(subtitle)}</p></div>"
f"<div class='decree-ribbon'>Step {len(events) if events else 0}: {caption_title}<br>Hover characters for agent and model details</div>"
f"{_book(book_open, pretrial_case, custom_mode)}"
f"<div class='judge-dais'><div class='prop-label'>{_escape(JUDGE_NAME)}</div><div class='bench-front'></div><span class='gavel'></span></div>"
"<div class='counsel-table left'><div class='prop-label'>Claimant Table</div></div>"
"<div class='counsel-table right'><div class='prop-label'>Respondent Table</div></div>"
"<div class='trial-floor-mark'></div>"
"<div class='witness-area'><div class='prop-label'>Evidence Stand</div></div>"
"<div class='jury-benches left'><div class='prop-label'>Jury Box</div><div class='jury-row'>"
f"{left_jurors}</div><div class='jury-rail'></div></div>"
"<div class='jury-benches right'><div class='prop-label'>Jury Box</div><div class='jury-row'>"
f"{right_jurors}</div><div class='jury-rail'></div></div>"
f"{puppets}"
f"{evidence_props}"
f"{_foreground_props()}"
f"{_active_dialogue(active_cue)}"
f"{_verdict_popup(events, show_verdict_popup)}"
"<div class='gallery-benches'><div></div><div></div><div></div><div></div><div></div><div></div></div>"
"<div class='trial-caption'>"
f"<div class='caption-phase'>Live Trial Feed / {_escape(caption_phase)}</div>"
f"<div class='caption-title'>{_escape(caption_title)}</div>"
f"<div class='caption-body'>{_escape(caption_body)}</div>"
"</div>"
f"{thread_modals}</section>"
)
def render_evidence(events: list[TrialEvent]) -> str:
evidence = next((event.evidence for event in reversed(events) if event.evidence), [])
if not evidence:
return "<div class='drawer-empty'>The exhibit drawer is closed until the clerk opens the docket.</div>"
return (
"<div class='drawer-grid drawer-text-stack'>"
+ "".join(
"<section class='drawer-text-block'>"
f"<div class='drawer-kicker'>{_escape(item.id)} / {item.reliability:.2f}</div>"
f"<h4>{_escape(item.title)}</h4>"
f"<p>{_escape(item.excerpt)}</p>"
f"<p><strong>Direction:</strong> {_escape(item.supports)}</p>"
f"<p>{_escape(item.note)}</p></section>"
for item in evidence
)
+ "</div>"
)
def render_jurors(events: list[TrialEvent]) -> str:
votes = _latest_votes(events)
if not votes:
sleepers = "".join(_juror(name, False) for name in JUROR_FACES)
return (
"<section class='drawer-text-block drawer-text-stack'><div class='drawer-kicker'>Jury Box</div>"
f"<div class='jury-row'>{sleepers}</div>"
"<p>The jurors are seated and silent.</p></section>"
)
return (
"<div class='drawer-grid drawer-text-stack'>"
+ "".join(
"<section class='drawer-text-block'>"
f"<div class='drawer-kicker'>{_escape(vote.juror)}</div>"
f"<p><strong>Persona:</strong> {_escape(vote.persona)}</p>"
f"<p class='vote-{vote.vote}'>{_escape(vote.vote.replace('_', ' '))}</p>"
f"<p>{_escape(vote.reason)}</p>"
f"<p><strong>Evidence:</strong> {_escape(', '.join(vote.evidence_ids))}</p></section>"
for vote in votes
)
+ "</div>"
)
def render_mind(events: list[TrialEvent], enabled: bool) -> str:
if not enabled:
return "<div class='mind-text'>Mind Layer hidden.</div>"
if not events:
return "<div class='mind-text'>Awaiting trace.</div>"
compact = [
{
"phase": event.phase,
"title": event.title,
"turns": [turn.model_dump() for turn in event.turns],
"trace": event.trace,
}
for event in events
]
return f"<pre class='mind-text'>{_escape(json.dumps(compact, indent=2))}</pre>"
def _clean_custom_items(values: list[str]) -> list[str]:
return [" ".join(value.split()) for value in values if " ".join(value.split())]
def _custom_case_from_payload(payload: str) -> CasePacket:
try:
data = json.loads(payload or "{}")
except json.JSONDecodeError as exc:
raise ValueError("Custom case details could not be read from the docket book.") from exc
context = " ".join(str(data.get("context", "")).split())
claimant_items = _clean_custom_items([str(value) for value in data.get("claimant_evidence", [])])
respondent_items = _clean_custom_items([str(value) for value in data.get("respondent_evidence", [])])
if not context:
raise ValueError("Custom requires a trial details paragraph.")
if not claimant_items or not respondent_items:
raise ValueError("Custom requires at least one evidence item for each side.")
evidence = [
EvidenceItem(
id=f"CUS-F{index}",
title=f"Claimant Evidence {index}",
source="Custom docket entry",
excerpt=item,
supports="claimant",
reliability=0.65,
note=item,
)
for index, item in enumerate(claimant_items[:3], start=1)
]
evidence.extend(
EvidenceItem(
id=f"CUS-A{index}",
title=f"Respondent Evidence {index}",
source="Custom docket entry",
excerpt=item,
supports="respondent",
reliability=0.65,
note=item,
)
for index, item in enumerate(respondent_items[:3], start=1)
)
return CasePacket(
id="custom",
title="Custom Trial",
subtitle="A custom docket assembled in the opening book.",
claimant="Claimant",
respondent="Respondent",
charge="Whether the custom record supports the claimant or the respondent.",
setting="A custom courtroom packet entered by the user.",
context=context,
claimant_claim="The claimant says the custom context and supporting evidence justify a favorable finding.",
respondent_claim="The respondent says the custom context is incomplete, overread, or answered by contrary evidence.",
source_note="Custom user-entered case packet from the docket book.",
evidence=evidence,
)
def render_case_preview(case_label: str) -> str:
case_id = CASE_OPTIONS.get(case_label, "socrates")
return render_court(
[],
pretrial_case=get_case(case_id) if case_id != "custom" else None,
custom_mode=case_id == "custom",
)
def run_ui(
case_label: str,
search_query: str,
hypothetical: str,
custom_payload: str,
speed: str,
mind_layer: bool,
):
case_id = CASE_OPTIONS.get(case_label, "socrates")
try:
custom_case = _custom_case_from_payload(custom_payload) if case_id == "custom" else None
except ValueError as exc:
yield (
render_court([], pretrial_case=None, custom_mode=True),
render_evidence([]),
render_jurors([]),
render_mind([], mind_layer),
str(exc),
)
return
request = TrialRequest(
case_id=case_id,
search_query=search_query or "",
hypothetical=hypothetical or "",
custom_case=custom_case,
speed=speed or "swift",
mind_layer=bool(mind_layer),
)
events: list[TrialEvent] = []
produced_events = _start_event_producer(request)
pending_speaker = _expected_next_speaker(events)
yield (
render_court(events, started=True, pending_speaker=pending_speaker),
render_evidence(events),
render_jurors(events),
render_mind(events, mind_layer),
_pending_status(pending_speaker),
)
try:
while True:
item = produced_events.get()
if item is _EVENT_STREAM_DONE:
break
if isinstance(item, Exception):
raise item
event = item
events.append(event)
yield (
render_court(events, started=True),
render_evidence(events),
render_jurors(events),
render_mind(events, mind_layer),
_event_status(event, len(events)),
)
duration = _reading_duration(_event_dialogue_text(event))
if duration > 0:
time.sleep(duration)
pending_speaker = _expected_next_speaker(events)
if pending_speaker is not None and produced_events.empty():
yield (
render_court(events, started=True, pending_speaker=pending_speaker),
render_evidence(events),
render_jurors(events),
render_mind(events, mind_layer),
_pending_status(pending_speaker),
)
except Exception as exc:
yield (
render_court(events, started=True),
render_evidence(events),
render_jurors(events),
render_mind(events, mind_layer),
f"Model response required. Trial stopped: {exc}",
)
return
yield (
render_court(events, started=True, show_verdict_popup=True),
render_evidence(events),
render_jurors(events),
render_mind(events, mind_layer),
"Verdict sealed.",
)
def build_app() -> gr.Blocks:
with gr.Blocks(title="Judge-GPT") as demo:
with gr.Group(elem_classes=["docket-book-controls"]):
gr.HTML("<div class='book-control-heading'>DATA TRIAL:</div>")
with gr.Row():
case = gr.Dropdown(
label="Case entry",
choices=list(CASE_OPTIONS.keys()),
value="Trial of Socrates",
scale=2,
)
start = gr.Button("Begin Trial", variant="primary", scale=1)
status = gr.Markdown("Ready.", elem_classes=["book-status"])
courtroom = gr.HTML(render_case_preview("Trial of Socrates"), label="Live courtroom")
search = gr.State("")
hypo = gr.State("")
custom_payload = gr.State("")
speed = gr.State("swift")
mind = gr.State(True)
with gr.Row(elem_classes=["drawer-shell"]):
with gr.Column(scale=1):
with gr.Tab("Evidence Drawer"):
evidence = gr.HTML(render_evidence([]))
with gr.Tab("Juror Panel"):
jurors = gr.HTML(render_jurors([]))
mind_html = gr.HTML(render_mind([], True), visible=False)
case.change(
render_case_preview,
inputs=[case],
outputs=[courtroom],
)
start.click(
run_ui,
inputs=[case, search, hypo, custom_payload, speed, mind],
outputs=[courtroom, evidence, jurors, mind_html, status],
js=START_JS,
)
return demo
demo = build_app()
if __name__ == "__main__":
demo.queue().launch(
show_error=True,
allowed_paths=["assets"],
css=CSS,
head=APP_HEAD,
theme=gr.themes.Soft(),
)