from __future__ import annotations
import json
import os
from collections.abc import Iterable
import gradio as gr
import httpx
from sovereign_bench.engine import JUDGE_NAME, JUROR_PERSONAS, stream_trial
from sovereign_bench.models import 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",
"The People v. Barnaby Buttons": "barnaby",
"Live Search Tribunal": "live",
}
PHASE_GLYPHS = {
"pretrial": "00",
"intake": "01",
"claims": "02",
"opening": "03",
"evidence": "04",
"questions": "05",
"deliberation": "06",
"verdict": "07",
"appeal": "08",
}
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: clamp(172px, 21vh, 212px);
z-index: 9999;
width: min(620px, 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;
}
.trial-options {
max-width: 1120px;
margin: 0 auto 14px;
border: 1px solid rgba(255, 226, 154, .18);
border-radius: 6px;
background: rgba(18, 9, 5, .78);
color: #f5dfb5;
}
.trial-options label,
.trial-options span,
.trial-options .prose {
color: #f5dfb5 !important;
}
.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;
}
.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: 12%;
z-index: 12;
width: min(760px, 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;
}
.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: 36%;
width: min(245px, 30vw);
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: 17% 10% 13%;
z-index: 2;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 72px;
padding: 0 28px;
transition: opacity .35s ease;
}
.book-open-content h2 {
margin: 0 0 10px;
color: #4c2a12;
font-size: 30px;
letter-spacing: 0;
}
.book-open-content p,
.book-entry {
color: #3c2615;
font-size: 15px;
line-height: 1.34;
}
.book-entry {
margin: 11px 0;
padding-left: 12px;
border-left: 3px solid rgba(111, 61, 23, .36);
}
.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: 4.5%;
}
.jury-benches.right {
right: 4.5%;
}
.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: -1.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: 35%;
z-index: 1;
width: 46%;
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: 31%;
--skin: #c38a55;
--robe: #1b1b20;
--accent: #79242a;
--portrait-width: 96px;
--portrait-top: -28px;
}
.puppet.clerk {
left: 43%;
top: 41%;
--skin: #b77b52;
--robe: #365548;
--accent: #2f6f5e;
}
.puppet.auric {
left: 24%;
top: 62%;
--skin: #c9975d;
--robe: #5b2719;
--accent: #a45c25;
}
.speaker-auric .puppet.auric {
left: 43%;
top: 66%;
}
.puppet.sable {
left: 75%;
top: 62%;
--skin: #a86d4a;
--robe: #1d3045;
--accent: #254f7a;
}
.speaker-sable .puppet.sable {
left: 57%;
top: 66%;
}
.puppet.auditor {
left: 71%;
top: 55%;
--skin: #c6a65b;
--robe: #4b3d1b;
--accent: #8d6b1f;
}
.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;
}
.phase-evidence .puppet.auditor {
animation: evidence-focus 1.35s ease-in-out infinite;
}
.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 .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.active .mouth,
.puppet.walking .mouth {
animation: speak-mouth .5s ease-in-out infinite;
}
.speech-bubble {
position: absolute;
left: 50%;
bottom: calc(100% + 12px);
z-index: 18;
width: 260px;
max-width: min(320px, calc(100vw - 32px));
transform: translateX(-50%);
padding: 10px 12px;
border: 1px solid rgba(255, 226, 154, .48);
border-radius: 6px;
background: rgba(255, 244, 215, .94);
color: #2d1b0d;
box-shadow: 0 14px 30px rgba(0, 0, 0, .34);
font-size: 12px;
font-weight: 700;
line-height: 1.3;
pointer-events: none;
}
.speech-bubble::after {
content: "";
position: absolute;
left: 50%;
bottom: -8px;
width: 14px;
height: 14px;
transform: translateX(-50%) rotate(45deg);
border-right: 1px solid rgba(255, 226, 154, .48);
border-bottom: 1px solid rgba(255, 226, 154, .48);
background: rgba(255, 244, 215, .94);
}
.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 .speech-bubble {
bottom: calc(100% + 6px);
width: 230px;
}
.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); }
}
@media (max-width: 820px) {
.docket-book-controls {
position: fixed;
top: 262px;
width: calc(100vw - 52px);
transform: translateX(-50%) rotate(-1deg);
}
.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);
}
.episode-book {
top: 220px;
width: min(680px, calc(100% - 20px));
}
.episode-book.closed {
top: 430px;
width: 210px;
}
.book-open-content {
grid-template-columns: 1fr;
gap: 10px;
inset: 17% 12% 14%;
padding: 0 18px;
}
.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;
}
.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: 650px;
}
.puppet.sable {
left: 80%;
top: 650px;
}
.speaker-auric .puppet.auric {
left: 42%;
top: 730px;
}
.speaker-sable .puppet.sable {
left: 58%;
top: 730px;
}
.puppet.clerk {
left: 35%;
top: 560px;
}
.puppet.auditor {
left: 78%;
top: 540px;
}
.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: -2px;
width: 64%;
}
.foreground-fence.fence-left {
left: -17%;
}
.foreground-fence.fence-right {
right: -17%;
}
.judge-table-foreground {
top: 405px;
width: 760px;
}
.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"""
"""
START_JS = """
(case_label, search_query, hypothetical, speed, mind_layer) => {
document.body.classList.add('trial-has-started');
if (window.SovereignCourtAudio) {
window.SovereignCourtAudio.begin();
}
return [case_label, search_query, hypothetical, 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",
},
"Advocate Auric": {
"class": "auric",
"name": "Advocate Auric",
"role": "Claimant advocate",
"model": "gpt-oss-20b",
},
"Counsel Sable": {
"class": "sable",
"name": "Counsel Sable",
"role": "Respondent advocate",
"model": "gpt-oss-20b",
},
"Auditor Prism": {
"class": "auditor",
"name": "Auditor Prism",
"role": "Evidence auditor",
"model": "Nemotron-Orchestrator-8B",
},
"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",
}
PHASE_AGENTS = {
"pretrial": ["Clerk Meridian"],
}
def _remote_events(request: TrialRequest) -> Iterable[TrialEvent] | None:
endpoint = os.getenv("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) -> Iterable[TrialEvent]:
remote = _remote_events(request)
if remote is not None:
yield from remote
return
delay = {"swift": 1.4, "measured": 2.4, "ceremonial": 3.4}[request.speed]
yield from stream_trial(request, delay=delay)
def _escape(value: str) -> str:
return (
value.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace('"', """)
)
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 _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 _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 (
""
f"{_escape(name)}"
f"{_escape(role)}"
f""
"Input"
f" {_escape(input_preview)} {_escape(output_preview)}"
)
return (
f""
f"{portrait}"
""
f"{bubble}"
f"{_tooltip(meta['name'], meta['role'], meta['model'], turns)}"
""
)
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)
bubble = ""
if active:
vote = next((vote for vote in (latest.votes if latest else []) if vote.juror == name), None)
speech = _latest_turn_text(latest, name)
if vote:
speech = f"{vote.vote.replace('_', ' ').title()}. {vote.reason}"
if speech:
bubble = f"{_escape(_short_text(speech, 190))}"
portrait = (
f"
"
if image
else ""
)
return (
f""
f"{portrait}"
""
f"{bubble}"
f"{_tooltip(name, 'HF-style juror', 'Nemotron panel', turns)}"
""
)
def _book(open_book: bool) -> str:
closed = "" if open_book else " closed"
return (
f"
"
"
"
"{_escape(subtitle)}
{_escape(item.excerpt)}
" f"Direction: {_escape(item.supports)}
" f"{_escape(item.note)}
The jurors are seated and silent.
Persona: {_escape(vote.persona)}
" f"{_escape(vote.vote.replace('_', ' '))}
" f"{_escape(vote.reason)}
" f"Evidence: {_escape(', '.join(vote.evidence_ids))}
{_escape(json.dumps(compact, indent=2))}"
def run_ui(case_label: str, search_query: str, hypothetical: str, speed: str, mind_layer: bool):
request = TrialRequest(
case_id=CASE_OPTIONS.get(case_label, "socrates"),
search_query=search_query or "",
hypothetical=hypothetical or "",
speed=speed or "swift",
mind_layer=bool(mind_layer),
)
events: list[TrialEvent] = []
yield (
render_court(events, started=True),
render_evidence(events),
render_jurors(events),
render_mind(events, mind_layer),
"The docket closes and the bailiff calls the room to order.",
)
try:
for event in get_events(request):
events.append(event)
status = f"Step {len(events)}: {event.title}"
yield (
render_court(events, started=True),
render_evidence(events),
render_jurors(events),
render_mind(events, mind_layer),
status,
)
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),
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("