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""" """ 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("&", "&") .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 _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 ( "" 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 _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"
" "
" f"{_escape(cue.name)}" f"{_escape(cue.role)}" "
" f"
{_escape(cue.text)}
" "
" ) 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"" ) 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"" ) return ( f"" f"{portrait}" "" 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) portrait = ( f"{_escape(name)} bust" if image else "" ) fallback_art = "" if image else "" return ( f"" f"{portrait}" f"{fallback_art}" f"{_tooltip(name, 'HF-style juror', 'Nemotron panel', turns)}" "" ) 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( "
  • " f"{_escape(item.title)}
    " f"{_escape(item.note)}" "
  • " for item in items ) return ( "
    " f"

    {_escape(title)}

    " f"" "
    " ) def _custom_evidence_fields(class_name: str, label: str) -> str: fields = "".join( f"" for index in range(1, 4) ) return f"

    {_escape(label)}

    {fields}
    " 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 = ( "

    Trial details

    " "" "
    " ) right_page = ( "

    Evidence

    " f"{_custom_evidence_fields('book-claimant-field', 'Evidence for Claimant')}" f"{_custom_evidence_fields('book-respondent-field', 'Evidence against Claimant')}" "
    " ) else: claimant_evidence, respondent_evidence = _split_evidence(packet) left_page = ( "

    Trial details

    " f"

    {_escape(packet.title)}

    " f"

    {_escape(packet.context or packet.setting)}

    " f"
    {_escape(packet.claimant)}
    {_escape(packet.claimant_claim)}
    " f"
    {_escape(packet.respondent)}
    {_escape(packet.respondent_claim)}
    " "
    " ) right_page = ( "

    Evidence

    " f"{_book_evidence_column(f'Evidence for {packet.claimant}', claimant_evidence)}" f"{_book_evidence_column(f'Evidence for {packet.respondent}', respondent_evidence)}" "
    " ) return ( f"
    " "Open docket book" "Closed docket book" f"
    " f"{left_page}" f"{right_page}" "
    " "
    " ) 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 _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"" f"{_escape(label)}" f"" "" ) 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, 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"
    " f"{_trial_progress(events)}" "
    " "" "" "
    " "
    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, pretrial_case, custom_mode)}" 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"{_active_dialogue(active_cue)}" f"{_verdict_popup(events, show_verdict_popup)}" "" "
    " 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 _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("
    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_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(), )