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"{_escape(model)}" "Input" f"

{_escape(input_preview)}

" "Output" f"

{_escape(output_preview)}

" "Click to open full thread" "
" ) def _thread_modal(name: str, role: str, model: str, turns: list[dict[str, str]]) -> str: body = ( "".join( "
" f"
{_escape(turn['phase'])} / {_escape(turn['role'])} / confidence {turn['confidence']}
" "Input" f"
{_escape(turn['input'])}
" "Output" f"
{_escape(turn['output'])}
" "
" for turn in turns ) or "
Waiting for this model thread to appear.
" ) return ( f"" ) 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 {"Advocate Auric", "Counsel Sable"} and agent in active_agents else "" small = " small" if agent in {"Clerk Meridian", "Auditor Prism"} else "" turns = _thread_for_character(events, agent) bubble = "" if agent in active_agents: speech = _latest_turn_text(latest, agent) if speech: bubble = f"{_escape(speech)}" portrait = "" if meta.get("image"): portrait = ( f"" ) 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"{_escape(name)} bust" 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"
" "Open docket book" "Closed docket book" "
" ) 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"" ) return f"
{''.join(sheets)}
" def _foreground_props() -> str: fence = "/gradio_api/file=assets/foreground/foregroundFence.png" judge_table = "/gradio_api/file=assets/foreground/JudgeTable.png" return ( "" ) 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) -> str: latest = events[-1] if events else None phase = latest.phase if latest else "pretrial" title, subtitle = _latest_packet_title(events) active_agents = _active_agents_for(latest) active_speaker = _active_speaker_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", "Advocate Auric", "Counsel Sable", "Auditor Prism"] ) 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"
" "
" "" "" "
" "
Judge-GPT Virtual Courtroom
" f"

{_escape(title)}

" f"

{_escape(subtitle)}

" f"
Step {len(events) if events else 0}: {caption_title}
Hover characters for agent and model details
" f"{_book(book_open)}" f"
{_escape(JUDGE_NAME)}
" "
Claimant Table
" "
Respondent Table
" "
" "
Evidence Stand
" "
Jury Box
" f"{left_jurors}
" "
Jury Box
" f"{right_jurors}
" f"{puppets}" f"{evidence_props}" f"{_foreground_props()}" "" "
" f"
Live Trial Feed / {_escape(caption_phase)}
" f"
{_escape(caption_title)}
" f"
{_escape(caption_body)}
" "
" f"{thread_modals}
" ) def render_evidence(events: list[TrialEvent]) -> str: evidence = next((event.evidence for event in reversed(events) if event.evidence), []) if not evidence: return "
The exhibit drawer is closed until the clerk opens the docket.
" return ( "
" + "".join( "
" f"
{_escape(item.id)} / {item.reliability:.2f}
" f"

{_escape(item.title)}

" f"

{_escape(item.excerpt)}

" f"

Direction: {_escape(item.supports)}

" f"

{_escape(item.note)}

" for item in evidence ) + "
" ) 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 ( "
Jury Box
" f"
{sleepers}
" "

The jurors are seated and silent.

" ) return ( "
" + "".join( "
" f"
{_escape(vote.juror)}
" f"

Persona: {_escape(vote.persona)}

" f"

{_escape(vote.vote.replace('_', ' '))}

" f"

{_escape(vote.reason)}

" f"

Evidence: {_escape(', '.join(vote.evidence_ids))}

" for vote in votes ) + "
" ) def render_mind(events: list[TrialEvent], enabled: bool) -> str: if not enabled: return "
Mind Layer hidden.
" if not events: return "
Awaiting trace.
" 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"
{_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("
DATA TRIAL:
") 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_court([]), label="Live courtroom") search = gr.State("") speed = gr.State("swift") mind = gr.State(True) with gr.Accordion("Advanced trial options", open=False, elem_classes=["trial-options"]): with gr.Row(): hypo = gr.Textbox(label="Hypothetical sidebar", lines=1) 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) start.click( run_ui, inputs=[case, search, hypo, 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(), )