cvpfus Codex commited on
Commit
dfd3402
·
1 Parent(s): fdde5de

Simplify sidebar with model stack

Browse files

Co-authored-by: Codex <codex@openai.com>

Files changed (10) hide show
  1. FIELD_NOTES.md +8 -8
  2. README.md +10 -10
  3. SUBMISSION.md +5 -5
  4. app.py +1 -6
  5. scripts/verify.py +16 -45
  6. static/app.css +0 -45
  7. static/app.js +35 -31
  8. static/evidence.html +0 -95
  9. static/evidence.js +0 -341
  10. static/index.html +5 -6
FIELD_NOTES.md CHANGED
@@ -24,17 +24,17 @@ Tiny Narrator is a small-model accessibility article reader. It is not a generic
24
  | Speech | `hexgrad/Kokoro-82M` | 82M | Fast screen-reader voice with a tiny footprint. |
25
  | Image generation | `black-forest-labs/FLUX.2-klein-4B` | 4B | Generates the article illustrations while staying Tiny Titan-safe. |
26
 
27
- The app exposes the same budget through `/api/model-budget`, including numeric `params_billion` values, per-model `within_limit` values, and a boolean `all_models_within_limit` check. The `/evidence` page renders each model's parameter count and Tiny Titan pass state so the claim is visible during the demo.
28
 
29
- Runtime setup is also data-backed. `/api/runtime-setup` lists the app command, llama.cpp launch command, model-specific runtime paths, environment values, and fallbacks. The `/evidence` page renders the runtime labels, setup commands, copy controls, and fallbacks so the demo can distinguish real model wiring from deterministic safety nets.
30
 
31
- The judge runbook lives at `/api/demo-script` and is rendered on `/evidence` with its API evidence checks. It keeps the live presentation repeatable by pairing visible actions with API checks for health, model budget, runtime setup, runtime status, image descriptions, reader narration, and speech. Each API check includes copyable curl and PowerShell-friendly `curl.exe` commands for quick reproduction. Deployed demos can set `PUBLIC_BASE_URL` so those commands point at the public Space instead of localhost.
32
 
33
  Submission readiness lives at `/api/submission-readiness`. It aggregates the demo-critical checks into one payload: model budget, award evidence, custom frontend assets, runtime setup, runtime status, accessibility audit, image receipts, executable demo API checks, and command base URL checks. POST checks must include sample bodies before readiness passes.
34
 
35
- The copyable judge evidence bundle lives at `/api/evidence-bundle`. The `/evidence` page's Copy Evidence button uses that endpoint to put the core receipts, including schema version, UTC generation time, `PUBLIC_BASE_URL`, runtime status, and readiness, on the clipboard as formatted JSON.
36
 
37
- Image provenance lives in `/api/image-descriptions`. Each article illustration carries the planned FLUX.2 klein model id, prompt, seed, asset URL, and fallback status, and `/evidence` renders those receipts so the generated-image claim is inspectable.
38
 
39
  Accessibility evidence lives at `/api/accessibility-audit`. It records the reader-mode choices that matter most for this prototype: semantic reading order, keyboard navigation, reader cursor state, shortcut safety, live narration, image alt text, transcript review, user-controlled playback, and fallback resilience.
40
 
@@ -81,8 +81,8 @@ Reader navigation interrupts active playback before requesting the next narratio
81
 
82
  Every model-facing backend path reports `elapsed_ms`. The frontend surfaces the latest end-to-end reader latency and stores per-item timing in the transcript, giving the build report concrete evidence for the small-model responsiveness claim.
83
 
84
- The app exposes `/api/award-evidence` and renders that data on `/evidence`, so the live demo can point directly to the hackathon bonus targets it is designed to satisfy.
85
 
86
- The app also renders `/api/submission-readiness`, so the Field Notes story has a single rollup of which submission claims are currently backed by app evidence.
87
 
88
- `/api/runtime-status` keeps the demo honest: if llama.cpp or Kokoro is unavailable, the UI labels the fallback state instead of silently pretending that every model path is online. The `/evidence` page also lists the live or fallback state for reader brain, vision, speech, and image generation.
 
24
  | Speech | `hexgrad/Kokoro-82M` | 82M | Fast screen-reader voice with a tiny footprint. |
25
  | Image generation | `black-forest-labs/FLUX.2-klein-4B` | 4B | Generates the article illustrations while staying Tiny Titan-safe. |
26
 
27
+ The app exposes the same budget through `/api/model-budget`, including numeric `params_billion` values, per-model `within_limit` values, and a boolean `all_models_within_limit` check. The sidebar model stack renders each model id, runtime, parameter count, and Tiny Titan pass state so the claim is visible during the demo.
28
 
29
+ Runtime setup is also data-backed. `/api/runtime-setup` lists the app command, llama.cpp launch command, model-specific runtime paths, environment values, and fallbacks so the demo can distinguish real model wiring from deterministic safety nets.
30
 
31
+ The judge runbook lives at `/api/demo-script`. It keeps the live presentation repeatable by pairing visible actions with API checks for health, model budget, runtime setup, runtime status, image descriptions, reader narration, and speech. Each API check includes copyable curl and PowerShell-friendly `curl.exe` commands for quick reproduction. Deployed demos can set `PUBLIC_BASE_URL` so those commands point at the public Space instead of localhost.
32
 
33
  Submission readiness lives at `/api/submission-readiness`. It aggregates the demo-critical checks into one payload: model budget, award evidence, custom frontend assets, runtime setup, runtime status, accessibility audit, image receipts, executable demo API checks, and command base URL checks. POST checks must include sample bodies before readiness passes.
34
 
35
+ The judge evidence bundle lives at `/api/evidence-bundle`. That endpoint returns the core receipts, including schema version, UTC generation time, `PUBLIC_BASE_URL`, runtime status, and readiness, as formatted JSON.
36
 
37
+ Image provenance lives in `/api/image-descriptions`. Each article illustration carries the planned FLUX.2 klein model id, prompt, seed, asset URL, and fallback status so the generated-image claim is inspectable.
38
 
39
  Accessibility evidence lives at `/api/accessibility-audit`. It records the reader-mode choices that matter most for this prototype: semantic reading order, keyboard navigation, reader cursor state, shortcut safety, live narration, image alt text, transcript review, user-controlled playback, and fallback resilience.
40
 
 
81
 
82
  Every model-facing backend path reports `elapsed_ms`. The frontend surfaces the latest end-to-end reader latency and stores per-item timing in the transcript, giving the build report concrete evidence for the small-model responsiveness claim.
83
 
84
+ The app exposes `/api/award-evidence`, so the live demo can point directly to the hackathon bonus targets it is designed to satisfy.
85
 
86
+ The app also exposes `/api/submission-readiness`, so the Field Notes story has a single rollup of which submission claims are currently backed by app evidence.
87
 
88
+ `/api/runtime-status` keeps the demo honest: if llama.cpp or Kokoro is unavailable, the API labels the fallback state instead of silently pretending that every model path is online.
README.md CHANGED
@@ -52,7 +52,7 @@ Start the app:
52
  python app.py
53
  ```
54
 
55
- Open the local URL printed by Gradio. The article page calls `/api/reader-brain`, `/api/image-descriptions`, `/api/describe-image`, and `/api/speak`. The `/evidence` page calls `/api/model-budget`, `/api/runtime-setup`, `/api/runtime-status`, `/api/demo-script`, `/api/accessibility-audit`, `/api/submission-readiness`, and `/api/evidence-bundle`; `/api/generate-image` remains available as an API placeholder.
56
 
57
  Useful environment variables:
58
 
@@ -76,7 +76,7 @@ The verifier checks syntax, static assets, Space metadata consistency, determini
76
 
77
  `/api/runtime-setup` exposes the commands, environment values, and fallback paths used for the model stack so the demo can be reproduced from the same data the UI displays.
78
 
79
- `/api/demo-script` exposes a compact judge runbook with the visible actions, API checks, sample bodies, curl commands, and PowerShell-friendly `curl.exe` commands that prove the submission claims. The commands use `PUBLIC_BASE_URL`, and `/evidence` renders those actions and API evidence checks as the Judge Runbook with per-command copy buttons.
80
 
81
  `/api/accessibility-audit` exposes structured evidence for semantic reading order, keyboard navigation, reader cursor state, shortcut safety, live narration, image alt text, transcript review, user-controlled playback, and fallback resilience.
82
 
@@ -86,7 +86,7 @@ The verifier checks syntax, static assets, Space metadata consistency, determini
86
 
87
  The readiness rollup only passes the demo API check when POST entries include executable sample bodies.
88
 
89
- `/api/evidence-bundle` returns a copyable JSON bundle of the main judging receipts, including schema version, UTC generation time, `PUBLIC_BASE_URL`, runtime status, and the submission readiness rollup. The `/evidence` page includes a Copy Evidence button that writes this bundle to the clipboard.
90
 
91
  ## Screen Reader Mode
92
 
@@ -103,7 +103,7 @@ The frontend builds a reading queue from semantic article nodes. When screen-rea
103
 
104
  Each readable node is sent to the reader brain for concise narration, then Kokoro generates speech. If a model is unavailable, the app uses deterministic fallbacks so the demo remains navigable.
105
 
106
- The session panel stays reader-first and renders the same semantic reader queue used by playback, including the active item marker, total item count, and click-to-read controls for individual queue items.
107
 
108
  When screen-reader mode turns on, it selects the focused or most visible article item, assigns stable reader-node ids, and marks the active item with `aria-current`. Clicking a readable article item while the mode is on reads that item directly.
109
 
@@ -115,7 +115,7 @@ The session panel keeps a transcript of recent narration with reader position, r
115
 
116
  Images start with meaningful fallback `alt` text in the HTML. Image descriptions are then preloaded into a local cache and written into the page's real `img alt` attributes. When the planned MiniCPM-V runtime is unavailable, deterministic alt-text fallbacks keep the screen-reader path usable.
117
 
118
- The `/evidence` page renders image receipts from `/api/image-descriptions`, so judges can inspect the prompt, seed, planned <=4B image model, and fallback status behind each bundled article illustration without lengthening the reader sidebar.
119
 
120
  Kokoro remains the planned tiny-model TTS path. During local demos, if the server-side Kokoro call falls back, the browser speech engine can read the same transcript so screen-reader mode still produces audible feedback.
121
 
@@ -129,12 +129,12 @@ Navigation commands interrupt current speech before starting the next request, m
129
 
130
  Reader-brain, image-description, speech, and image-generation responses include `elapsed_ms`. The session panel and transcript show recent latency so the Field Notes can discuss responsiveness with concrete numbers.
131
 
132
- The `/evidence` page renders the judge runbook and API evidence checks from `/api/demo-script`, plus manifest-backed evidence for Tiny Titan, Llama Champion, Off-Brand, and Field Notes. A model-budget panel reads `/api/model-budget` so judges can see each role's parameter count and Tiny Titan pass status in the live app.
133
 
134
- The `/evidence` page reads `/api/submission-readiness`, giving judges one compact rollup of the claims the live app can prove.
135
 
136
- The Copy Evidence button on `/evidence` reads `/api/evidence-bundle` and copies the core judge receipts as formatted JSON for quick review or submission notes.
137
 
138
- A runtime-plan panel on `/evidence` reads `/api/runtime-setup` and summarizes each model path's runtime, setup command, and fallback, keeping the live demo honest about what is online and what is deterministic. Runtime setup commands have copy controls with the same clipboard fallback messaging as judge API commands.
139
 
140
- `/api/runtime-status` performs a short readiness check for llama.cpp and local speech dependencies, then reports which fallback paths are ready for a live demo. The `/evidence` page renders live status details for reader brain, vision, speech, and image generation.
 
52
  python app.py
53
  ```
54
 
55
+ Open the local URL printed by Gradio. The article page calls `/api/reader-brain`, `/api/image-descriptions`, `/api/describe-image`, `/api/speak`, and `/api/model-budget`; the remaining evidence endpoints stay available for docs and submission checks.
56
 
57
  Useful environment variables:
58
 
 
76
 
77
  `/api/runtime-setup` exposes the commands, environment values, and fallback paths used for the model stack so the demo can be reproduced from the same data the UI displays.
78
 
79
+ `/api/demo-script` exposes a compact judge runbook with the visible actions, API checks, sample bodies, curl commands, and PowerShell-friendly `curl.exe` commands that prove the submission claims. The commands use `PUBLIC_BASE_URL` so they can be copied from the API response or submission notes.
80
 
81
  `/api/accessibility-audit` exposes structured evidence for semantic reading order, keyboard navigation, reader cursor state, shortcut safety, live narration, image alt text, transcript review, user-controlled playback, and fallback resilience.
82
 
 
86
 
87
  The readiness rollup only passes the demo API check when POST entries include executable sample bodies.
88
 
89
+ `/api/evidence-bundle` returns a JSON bundle of the main judging receipts, including schema version, UTC generation time, `PUBLIC_BASE_URL`, runtime status, and the submission readiness rollup.
90
 
91
  ## Screen Reader Mode
92
 
 
103
 
104
  Each readable node is sent to the reader brain for concise narration, then Kokoro generates speech. If a model is unavailable, the app uses deterministic fallbacks so the demo remains navigable.
105
 
106
+ The session panel stays reader-first: it shows current reader state, live narration, transcript controls, latency, and the tiny-model stack. The semantic reader queue remains internal to playback and keyboard navigation.
107
 
108
  When screen-reader mode turns on, it selects the focused or most visible article item, assigns stable reader-node ids, and marks the active item with `aria-current`. Clicking a readable article item while the mode is on reads that item directly.
109
 
 
115
 
116
  Images start with meaningful fallback `alt` text in the HTML. Image descriptions are then preloaded into a local cache and written into the page's real `img alt` attributes. When the planned MiniCPM-V runtime is unavailable, deterministic alt-text fallbacks keep the screen-reader path usable.
117
 
118
+ `/api/image-descriptions` exposes image receipts so judges can inspect the prompt, seed, planned <=4B image model, and fallback status behind each bundled article illustration without lengthening the reader sidebar.
119
 
120
  Kokoro remains the planned tiny-model TTS path. During local demos, if the server-side Kokoro call falls back, the browser speech engine can read the same transcript so screen-reader mode still produces audible feedback.
121
 
 
129
 
130
  Reader-brain, image-description, speech, and image-generation responses include `elapsed_ms`. The session panel and transcript show recent latency so the Field Notes can discuss responsiveness with concrete numbers.
131
 
132
+ The sidebar model stack reads `/api/model-budget` so judges can see each role's model id, runtime, parameter count, and Tiny Titan pass status in the live app.
133
 
134
+ `/api/submission-readiness` gives judges one compact rollup of the claims the live app can prove.
135
 
136
+ `/api/evidence-bundle` returns the core judge receipts as formatted JSON for quick review or submission notes.
137
 
138
+ `/api/runtime-setup` summarizes each model path's runtime, setup command, and fallback, keeping the live demo honest about what is online and what is deterministic.
139
 
140
+ `/api/runtime-status` performs a short readiness check for llama.cpp and local speech dependencies, then reports which fallback paths are ready for a live demo.
SUBMISSION.md CHANGED
@@ -6,7 +6,7 @@ Tiny Narrator turns an article into a guided screen-reader experience using smal
6
 
7
  ## Short description
8
 
9
- Tiny Narrator is a custom Gradio Server app that looks like an article/blog reader until screen-reader mode is switched on. In reader mode, the app builds and displays a semantic reading queue with click-to-read controls, narrates headings and paragraphs, describes generated images, summarizes the current section, speaks with Kokoro, and keeps a visible transcript of the spoken path with reader position, runtime, and latency.
10
 
11
  The prototype is designed for a live hackathon demo: every model-facing path has a deterministic fallback, runtime readiness is labeled in the UI, and the repo exposes machine-readable evidence for model size, setup, demo flow, and accessibility behavior.
12
 
@@ -32,15 +32,15 @@ The prototype is designed for a live hackathon demo: every model-facing path has
32
  2. Turn on screen-reader mode and press `Space` or `Next` to narrate the first semantic node.
33
  3. Use `Heading`, `Image`, and `Summary` to navigate by article meaning instead of by raw page order.
34
  4. Show the reader-first session panel: current item, live narration, reader queue, transcript, and latency.
35
- 5. Open `/evidence` to inspect runtime readiness, judge runbook, award evidence, model budget, image receipts, and runtime plan.
36
- 6. Mention that `/api/demo-script` exposes the same judge runbook and API evidence checks as structured data.
37
 
38
  ## Evidence endpoints
39
 
40
  - `/api/health`: app identity, custom frontend marker, llama.cpp base URL, and model manifest.
41
  - `/api/model-budget`: Tiny Titan parameter proof with numeric `params_billion` and per-model pass values.
42
  - `/api/runtime-setup`: app command, copyable model runtime commands rendered in the UI, environment values, and fallback paths.
43
- - `/api/runtime-status`: online or fallback-ready status for each model path, rendered on `/evidence`.
44
  - `/api/accessibility-audit`: semantic queue, keyboard navigation, reader cursor state, shortcut safety, live narration, alt text, transcript, user control, and fallback evidence.
45
  - `/api/demo-script`: repeatable judge runbook and API checks.
46
  - `/api/image-descriptions`: generated article image descriptions plus prompt, seed, model, asset URL, and fallback status receipts.
@@ -55,7 +55,7 @@ The image receipts keep the generated-asset claim inspectable: each article illu
55
 
56
  The submission-readiness panel and API give judges a compact checklist for the whole build, so the live demo can move from individual receipts to an overall readiness view.
57
 
58
- The `/evidence` page's Copy Evidence button pulls `/api/evidence-bundle` and writes the formatted JSON bundle to the clipboard for quick judging notes. Transcript, evidence, and command copy actions label clipboard-unavailable fallback states in visible live regions.
59
 
60
  ## Reliability notes
61
 
 
6
 
7
  ## Short description
8
 
9
+ Tiny Narrator is a custom Gradio Server app that looks like an article/blog reader until screen-reader mode is switched on. In reader mode, the app builds an internal semantic reading queue, narrates headings and paragraphs, describes generated images, summarizes the current section, speaks with Kokoro, and keeps a visible transcript of the spoken path with reader position, runtime, and latency.
10
 
11
  The prototype is designed for a live hackathon demo: every model-facing path has a deterministic fallback, runtime readiness is labeled in the UI, and the repo exposes machine-readable evidence for model size, setup, demo flow, and accessibility behavior.
12
 
 
32
  2. Turn on screen-reader mode and press `Space` or `Next` to narrate the first semantic node.
33
  3. Use `Heading`, `Image`, and `Summary` to navigate by article meaning instead of by raw page order.
34
  4. Show the reader-first session panel: current item, live narration, reader queue, transcript, and latency.
35
+ 5. Point to the model stack panel for model id, runtime, parameter count, and Tiny Titan pass status.
36
+ 6. Mention that `/api/demo-script` exposes the judge runbook and API evidence checks as structured data.
37
 
38
  ## Evidence endpoints
39
 
40
  - `/api/health`: app identity, custom frontend marker, llama.cpp base URL, and model manifest.
41
  - `/api/model-budget`: Tiny Titan parameter proof with numeric `params_billion` and per-model pass values.
42
  - `/api/runtime-setup`: app command, copyable model runtime commands rendered in the UI, environment values, and fallback paths.
43
+ - `/api/runtime-status`: online or fallback-ready status for each model path.
44
  - `/api/accessibility-audit`: semantic queue, keyboard navigation, reader cursor state, shortcut safety, live narration, alt text, transcript, user control, and fallback evidence.
45
  - `/api/demo-script`: repeatable judge runbook and API checks.
46
  - `/api/image-descriptions`: generated article image descriptions plus prompt, seed, model, asset URL, and fallback status receipts.
 
55
 
56
  The submission-readiness panel and API give judges a compact checklist for the whole build, so the live demo can move from individual receipts to an overall readiness view.
57
 
58
+ `/api/evidence-bundle` returns formatted JSON for quick judging notes. Transcript copy actions label clipboard-unavailable fallback states in the visible live narration region.
59
 
60
  ## Reliability notes
61
 
app.py CHANGED
@@ -432,7 +432,7 @@ def accessibility_audit_core() -> dict[str, Any]:
432
  "id": "semantic_queue",
433
  "label": "Semantic reading queue",
434
  "status": "pass",
435
- "evidence": "Readable article nodes declare data-reader-type values and the session panel renders their ordered queue with click-to-read controls.",
436
  },
437
  {
438
  "id": "keyboard_navigation",
@@ -816,11 +816,6 @@ async def home() -> str:
816
  return (STATIC_DIR / "index.html").read_text(encoding="utf-8")
817
 
818
 
819
- @app.get("/evidence", response_class=HTMLResponse)
820
- async def evidence_page() -> str:
821
- return (STATIC_DIR / "evidence.html").read_text(encoding="utf-8")
822
-
823
-
824
  @app.get("/api/health")
825
  async def health() -> JSONResponse:
826
  return _json(
 
432
  "id": "semantic_queue",
433
  "label": "Semantic reading queue",
434
  "status": "pass",
435
+ "evidence": "Readable article nodes declare data-reader-type values and the reader controls navigate the ordered semantic node list.",
436
  },
437
  {
438
  "id": "keyboard_navigation",
 
816
  return (STATIC_DIR / "index.html").read_text(encoding="utf-8")
817
 
818
 
 
 
 
 
 
819
  @app.get("/api/health")
820
  async def health() -> JSONResponse:
821
  return _json(
scripts/verify.py CHANGED
@@ -23,10 +23,8 @@ def verify_static_assets() -> None:
23
  ROOT / "LICENSE",
24
  ROOT / "SUBMISSION.md",
25
  ROOT / "static" / "index.html",
26
- ROOT / "static" / "evidence.html",
27
  ROOT / "static" / "app.css",
28
  ROOT / "static" / "app.js",
29
- ROOT / "static" / "evidence.js",
30
  ROOT / "static" / "generated" / "desk-reader.svg",
31
  ROOT / "static" / "generated" / "model-map.svg",
32
  ROOT / "static" / "generated" / "field-notes.svg",
@@ -35,9 +33,7 @@ def verify_static_assets() -> None:
35
  assert_true(path.exists(), f"Missing required asset: {path}")
36
 
37
  app_js = (ROOT / "static" / "app.js").read_text(encoding="utf-8")
38
- evidence_js = (ROOT / "static" / "evidence.js").read_text(encoding="utf-8")
39
  index_html = (ROOT / "static" / "index.html").read_text(encoding="utf-8")
40
- evidence_html = (ROOT / "static" / "evidence.html").read_text(encoding="utf-8")
41
  assert_true('data-reader-type="heading"' in index_html, "Article should mark heading reader nodes")
42
  assert_true('data-reader-type="image"' in index_html, "Article should mark image reader nodes")
43
  assert_true('alt=""' not in index_html, "Article images should not start with empty alt text")
@@ -49,10 +45,12 @@ def verify_static_assets() -> None:
49
  for shortcut in ['aria-keyshortcuts="Space"', 'aria-keyshortcuts="N"', 'aria-keyshortcuts="R"', 'aria-keyshortcuts="S"']:
50
  assert_true(shortcut in index_html, f"Reader controls should expose {shortcut}")
51
  assert_true("transcriptLog" in index_html, "Article should expose a visible transcript log")
52
- assert_true("readerQueueList" in index_html, "Article should expose the semantic reader queue")
53
  assert_true("repeatButton" in index_html, "Reader controls should expose a visible repeat command")
54
  assert_true("stopButton" in index_html, "Reader controls should expose a visible stop command")
55
- for moved_id in [
 
 
56
  "demoScriptList",
57
  "demoApiCheckList",
58
  "imageReceiptList",
@@ -62,23 +60,14 @@ def verify_static_assets() -> None:
62
  "modelBudgetList",
63
  "runtimeSetupList",
64
  ]:
65
- assert_true(moved_id not in index_html, f"Article sidebar should not include moved evidence panel: {moved_id}")
66
- assert_true(moved_id in evidence_html, f"Evidence page should include moved evidence panel: {moved_id}")
67
  assert_true("loadDemoScript" not in app_js, "Article frontend should not render the structured demo script")
68
- assert_true("loadDemoScript" in evidence_js, "Evidence frontend should render the structured demo script")
69
- assert_true("runtimeStatusList.innerHTML" in evidence_js, "Evidence frontend should render live runtime status details")
70
- assert_true("runtime-command" in evidence_js, "Evidence frontend should render runtime setup commands")
71
- assert_true("runtime-command-copy" in evidence_js, "Evidence frontend should expose copy buttons for runtime setup commands")
72
- assert_true("runtimeSetupList.addEventListener" in evidence_js, "Runtime setup command copy actions should be delegated")
73
- assert_true("payload.api_checks" in evidence_js, "Evidence frontend should render structured demo API checks")
74
- assert_true("demo-api-command" in evidence_js, "Evidence frontend should render copyable demo commands")
75
- assert_true("demo-command-copy" in evidence_js, "Evidence frontend should expose copy buttons for demo commands")
76
- assert_true("item.powershell" in evidence_js, "Evidence frontend should render PowerShell-friendly demo commands")
77
- assert_true("Tiny Titan pass" in evidence_js, "Evidence frontend should render per-model Tiny Titan pass labels")
78
- assert_true("/api/evidence-bundle" in evidence_js, "Evidence frontend should fetch the evidence bundle for copying")
79
- assert_true("/evidence" in index_html, "Article page should link to the dedicated evidence page")
80
  assert_true("copyTextToClipboard" in app_js, "Frontend copy actions should share clipboard fallback handling")
81
- assert_true("copyTextToClipboard" in evidence_js, "Evidence copy actions should share clipboard fallback handling")
82
  assert_true(
83
  "Transcript is visible, but clipboard access is unavailable." in app_js,
84
  "Transcript copy should report unavailable clipboard access",
@@ -99,10 +88,8 @@ def verify_static_assets() -> None:
99
  assert_true("controls.repeat.click()" in app_js, "R should route through the visible repeat command")
100
  assert_true("controls.stop.click()" in app_js, "Escape should route through the visible stop command")
101
  assert_true("reader-node-" in app_js, "Reader nodes should receive stable ids for control context")
102
- assert_true("renderReaderQueue" in app_js, "Frontend should render the semantic reader queue")
103
- assert_true("readerQueueStatus.textContent" in app_js, "Reader queue should expose item count")
104
- assert_true("reader-queue-play" in app_js, "Reader queue should expose click-to-read controls")
105
- assert_true("readerQueueList.addEventListener" in app_js, "Reader queue should handle click-to-read actions")
106
  assert_true("narrate(node.index)" in app_js, "Reader mode should support click-to-read article items")
107
 
108
  submission = (ROOT / "SUBMISSION.md").read_text(encoding="utf-8")
@@ -255,28 +242,12 @@ def verify_routes() -> None:
255
  assert_true("speedValue" in home.text, "Home route should include speed value output")
256
  assert_true("autoAdvanceControl" in home.text, "Home route should include auto-advance control")
257
  assert_true("transcriptLog" in home.text, "Home route should include transcript log")
258
- assert_true("readerQueueList" in home.text, "Home route should include reader queue list")
259
- assert_true("/evidence" in home.text, "Home route should link to the evidence page")
 
 
260
  assert_true("copyEvidenceButton" not in home.text, "Home route should keep judge evidence off the reader sidebar")
261
 
262
- evidence_page = client.get("/evidence")
263
- assert_true(evidence_page.status_code == 200, "Evidence route should return 200")
264
- assert_true("Judging receipts for Tiny Narrator" in evidence_page.text, "Evidence route should include evidence title")
265
- assert_true("demoScriptStatus" in evidence_page.text, "Evidence route should include demo script status")
266
- assert_true("demoScriptList" in evidence_page.text, "Evidence route should include demo script list")
267
- assert_true("demoApiCheckList" in evidence_page.text, "Evidence route should include demo API check list")
268
- assert_true("awardEvidenceList" in evidence_page.text, "Evidence route should include award evidence list")
269
- assert_true("submissionReadinessStatus" in evidence_page.text, "Evidence route should include submission readiness status")
270
- assert_true("submissionReadinessList" in evidence_page.text, "Evidence route should include submission readiness list")
271
- assert_true("copyEvidenceButton" in evidence_page.text, "Evidence route should include copy evidence button")
272
- assert_true("budgetStatus" in evidence_page.text, "Evidence route should include model budget status")
273
- assert_true("modelBudgetList" in evidence_page.text, "Evidence route should include model budget list")
274
- assert_true("runtimeStatusList" in evidence_page.text, "Evidence route should include runtime status list")
275
- assert_true("runtimeSetupStatus" in evidence_page.text, "Evidence route should include runtime setup status")
276
- assert_true("runtimeSetupList" in evidence_page.text, "Evidence route should include runtime setup list")
277
- assert_true("imageReceiptStatus" in evidence_page.text, "Evidence route should include image receipt status")
278
- assert_true("imageReceiptList" in evidence_page.text, "Evidence route should include image receipt list")
279
-
280
  health = client.get("/api/health")
281
  assert_true(health.status_code == 200, "Health route should return 200")
282
  payload = health.json()
 
23
  ROOT / "LICENSE",
24
  ROOT / "SUBMISSION.md",
25
  ROOT / "static" / "index.html",
 
26
  ROOT / "static" / "app.css",
27
  ROOT / "static" / "app.js",
 
28
  ROOT / "static" / "generated" / "desk-reader.svg",
29
  ROOT / "static" / "generated" / "model-map.svg",
30
  ROOT / "static" / "generated" / "field-notes.svg",
 
33
  assert_true(path.exists(), f"Missing required asset: {path}")
34
 
35
  app_js = (ROOT / "static" / "app.js").read_text(encoding="utf-8")
 
36
  index_html = (ROOT / "static" / "index.html").read_text(encoding="utf-8")
 
37
  assert_true('data-reader-type="heading"' in index_html, "Article should mark heading reader nodes")
38
  assert_true('data-reader-type="image"' in index_html, "Article should mark image reader nodes")
39
  assert_true('alt=""' not in index_html, "Article images should not start with empty alt text")
 
45
  for shortcut in ['aria-keyshortcuts="Space"', 'aria-keyshortcuts="N"', 'aria-keyshortcuts="R"', 'aria-keyshortcuts="S"']:
46
  assert_true(shortcut in index_html, f"Reader controls should expose {shortcut}")
47
  assert_true("transcriptLog" in index_html, "Article should expose a visible transcript log")
48
+ assert_true("readerQueueList" not in index_html, "Article sidebar should not include a visible reader queue")
49
  assert_true("repeatButton" in index_html, "Reader controls should expose a visible repeat command")
50
  assert_true("stopButton" in index_html, "Reader controls should expose a visible stop command")
51
+ assert_true("modelStackList" in index_html, "Article sidebar should expose the model stack panel")
52
+ assert_true("modelBudgetStatus" in index_html, "Article sidebar should expose model stack status")
53
+ for removed_id in [
54
  "demoScriptList",
55
  "demoApiCheckList",
56
  "imageReceiptList",
 
60
  "modelBudgetList",
61
  "runtimeSetupList",
62
  ]:
63
+ assert_true(removed_id not in index_html, f"Article sidebar should not include removed evidence panel: {removed_id}")
 
64
  assert_true("loadDemoScript" not in app_js, "Article frontend should not render the structured demo script")
65
+ assert_true("loadModelBudget" in app_js, "Article frontend should render the model stack panel")
66
+ assert_true("/api/model-budget" in app_js, "Article frontend should fetch model budget data")
67
+ assert_true("modelStackList.innerHTML" in app_js, "Article frontend should render model stack items")
68
+ assert_true("Tiny Titan pass" in app_js, "Article frontend should render per-model Tiny Titan pass labels")
69
+ assert_true("/evidence" not in index_html, "Article page should not link to a removed evidence page")
 
 
 
 
 
 
 
70
  assert_true("copyTextToClipboard" in app_js, "Frontend copy actions should share clipboard fallback handling")
 
71
  assert_true(
72
  "Transcript is visible, but clipboard access is unavailable." in app_js,
73
  "Transcript copy should report unavailable clipboard access",
 
88
  assert_true("controls.repeat.click()" in app_js, "R should route through the visible repeat command")
89
  assert_true("controls.stop.click()" in app_js, "Escape should route through the visible stop command")
90
  assert_true("reader-node-" in app_js, "Reader nodes should receive stable ids for control context")
91
+ assert_true("renderReaderQueue" not in app_js, "Frontend should not render a visible reader queue")
92
+ assert_true("readerQueueList" not in app_js, "Frontend should not bind a visible reader queue")
 
 
93
  assert_true("narrate(node.index)" in app_js, "Reader mode should support click-to-read article items")
94
 
95
  submission = (ROOT / "SUBMISSION.md").read_text(encoding="utf-8")
 
242
  assert_true("speedValue" in home.text, "Home route should include speed value output")
243
  assert_true("autoAdvanceControl" in home.text, "Home route should include auto-advance control")
244
  assert_true("transcriptLog" in home.text, "Home route should include transcript log")
245
+ assert_true("readerQueueList" not in home.text, "Home route should not include reader queue list")
246
+ assert_true("modelBudgetStatus" in home.text, "Home route should include model stack status")
247
+ assert_true("modelStackList" in home.text, "Home route should include model stack list")
248
+ assert_true("/evidence" not in home.text, "Home route should not link to a removed evidence page")
249
  assert_true("copyEvidenceButton" not in home.text, "Home route should keep judge evidence off the reader sidebar")
250
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
  health = client.get("/api/health")
252
  assert_true(health.status_code == 200, "Health route should return 200")
253
  payload = health.json()
static/app.css CHANGED
@@ -177,51 +177,6 @@ select:focus-visible {
177
 
178
  .article { min-width: 0; }
179
 
180
- .evidence-shell {
181
- display: grid;
182
- gap: 28px;
183
- width: min(1040px, calc(100% - 40px));
184
- margin: 56px auto 120px;
185
- }
186
-
187
- .evidence-hero {
188
- padding-bottom: 12px;
189
- border-bottom: 2px solid var(--ink);
190
- }
191
-
192
- .evidence-hero h1 {
193
- max-width: 12ch;
194
- }
195
-
196
- .evidence-section {
197
- border: 1px solid var(--line);
198
- border-top: 3px solid var(--ink);
199
- background: var(--surface);
200
- padding: 22px 20px;
201
- }
202
-
203
- .evidence-section h2 {
204
- margin: 0;
205
- padding: 0;
206
- font-family: var(--mono);
207
- font-size: 0.72rem;
208
- font-weight: 700;
209
- text-transform: uppercase;
210
- letter-spacing: 0.12em;
211
- color: var(--ink-soft);
212
- }
213
-
214
- .evidence-section h2::before { display: none; }
215
-
216
- .evidence-link {
217
- color: var(--ink);
218
- text-decoration: none;
219
- }
220
-
221
- .evidence-link:hover {
222
- border-color: var(--accent);
223
- }
224
-
225
  /* ─── ARTICLE TYPOGRAPHY ─── */
226
  .kicker {
227
  display: inline-block;
 
177
 
178
  .article { min-width: 0; }
179
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  /* ─── ARTICLE TYPOGRAPHY ─── */
181
  .kicker {
182
  display: inline-block;
static/app.js CHANGED
@@ -7,8 +7,6 @@ const imageStatus = document.querySelector("#imageStatus");
7
  const voiceStatus = document.querySelector("#voiceStatus");
8
  const latencyStatus = document.querySelector("#latencyStatus");
9
  const liveNarration = document.querySelector("#liveNarration");
10
- const readerQueueStatus = document.querySelector("#readerQueueStatus");
11
- const readerQueueList = document.querySelector("#readerQueueList");
12
  const audio = document.querySelector("#speechAudio");
13
  const voiceControl = document.querySelector("#voiceControl");
14
  const speedControl = document.querySelector("#speedControl");
@@ -17,6 +15,8 @@ const autoAdvanceControl = document.querySelector("#autoAdvanceControl");
17
  const transcriptLog = document.querySelector("#transcriptLog");
18
  const copyTranscriptButton = document.querySelector("#copyTranscriptButton");
19
  const clearTranscriptButton = document.querySelector("#clearTranscriptButton");
 
 
20
 
21
  const controls = {
22
  prev: document.querySelector("#prevButton"),
@@ -126,26 +126,6 @@ function readerItemStatus(node) {
126
  return `${readerTypeLabel(node.type)}, item ${node.index + 1} of ${nodes.length}`;
127
  }
128
 
129
- function readerQueueLabel(node) {
130
- return `${node.index + 1}. ${readerTypeLabel(node.type)}`;
131
- }
132
-
133
- function renderReaderQueue() {
134
- readerQueueStatus.textContent = `${nodes.length} items`;
135
- readerQueueList.innerHTML = nodes
136
- .map((node) => `
137
- <li${node.index === currentIndex ? ' aria-current="true"' : ""}>
138
- <div class="runtime-row">
139
- <span>${escapeHtml(readerQueueLabel(node))}</span>
140
- <span class="runtime-pill">${node.index === currentIndex ? "current" : "queued"}</span>
141
- </div>
142
- <p>${escapeHtml(node.text.slice(0, 120))}</p>
143
- <button class="reader-queue-play" type="button" data-reader-index="${node.index}">Read item</button>
144
- </li>
145
- `)
146
- .join("");
147
- }
148
-
149
  function initialReaderIndex() {
150
  if (!nodes.length) {
151
  return -1;
@@ -232,6 +212,38 @@ function roleLabel(role) {
232
  .join(" ");
233
  }
234
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  async function loadImageDescriptions() {
236
  imageStatus.textContent = "Describing";
237
  try {
@@ -281,7 +293,6 @@ function setActive(index) {
281
  const node = nodes[currentIndex];
282
  if (!node) {
283
  currentStatus.textContent = "No item selected";
284
- renderReaderQueue();
285
  return;
286
  }
287
  node.element.classList.add("reader-active");
@@ -290,7 +301,6 @@ function setActive(index) {
290
  node.element.focus({ preventScroll: true });
291
  node.element.scrollIntoView({ block: "center", behavior: "smooth" });
292
  currentStatus.textContent = readerItemStatus(node);
293
- renderReaderQueue();
294
  }
295
 
296
  function haltPlayback({ clearAutoAdvance = true } = {}) {
@@ -524,12 +534,6 @@ nodes.forEach((node) => {
524
  narrate(node.index);
525
  });
526
  });
527
- readerQueueList.addEventListener("click", (event) => {
528
- const button = event.target.closest(".reader-queue-play");
529
- if (!button) return;
530
- setEnabled(true);
531
- narrate(Number(button.dataset.readerIndex));
532
- });
533
  voiceControl.addEventListener("change", () => {
534
  voiceStatus.textContent = voiceControl.selectedOptions[0]?.textContent || "Custom voice";
535
  });
@@ -582,9 +586,9 @@ controls.play.addEventListener("click", () => {
582
  });
583
 
584
  loadManifest();
 
585
  loadImageDescriptions();
586
  updateSpeedValue();
587
- renderReaderQueue();
588
 
589
  audio.addEventListener("ended", () => {
590
  playing = false;
 
7
  const voiceStatus = document.querySelector("#voiceStatus");
8
  const latencyStatus = document.querySelector("#latencyStatus");
9
  const liveNarration = document.querySelector("#liveNarration");
 
 
10
  const audio = document.querySelector("#speechAudio");
11
  const voiceControl = document.querySelector("#voiceControl");
12
  const speedControl = document.querySelector("#speedControl");
 
15
  const transcriptLog = document.querySelector("#transcriptLog");
16
  const copyTranscriptButton = document.querySelector("#copyTranscriptButton");
17
  const clearTranscriptButton = document.querySelector("#clearTranscriptButton");
18
+ const modelBudgetStatus = document.querySelector("#modelBudgetStatus");
19
+ const modelStackList = document.querySelector("#modelStackList");
20
 
21
  const controls = {
22
  prev: document.querySelector("#prevButton"),
 
126
  return `${readerTypeLabel(node.type)}, item ${node.index + 1} of ${nodes.length}`;
127
  }
128
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  function initialReaderIndex() {
130
  if (!nodes.length) {
131
  return -1;
 
212
  .join(" ");
213
  }
214
 
215
+ async function loadModelBudget() {
216
+ try {
217
+ const payload = await postJson("/api/model-budget");
218
+ modelBudgetStatus.textContent = payload.all_models_within_limit
219
+ ? `All <= ${payload.limit_billion}B`
220
+ : "Review needed";
221
+ modelStackList.innerHTML = payload.models
222
+ .map((model) => `
223
+ <li>
224
+ <div class="budget-row">
225
+ <span>${escapeHtml(roleLabel(model.role))}</span>
226
+ <span class="budget-pill">${escapeHtml(model.params)}</span>
227
+ </div>
228
+ <p>${escapeHtml(model.runtime)} | ${model.within_limit ? "Tiny Titan pass" : "Review parameter budget"}</p>
229
+ <p>${escapeHtml(model.id)}</p>
230
+ </li>
231
+ `)
232
+ .join("");
233
+ } catch {
234
+ modelBudgetStatus.textContent = "Unavailable";
235
+ modelStackList.innerHTML = `
236
+ <li>
237
+ <div class="budget-row">
238
+ <span>Model stack</span>
239
+ <span class="budget-pill">offline</span>
240
+ </div>
241
+ <p>Model budget is unavailable.</p>
242
+ </li>
243
+ `;
244
+ }
245
+ }
246
+
247
  async function loadImageDescriptions() {
248
  imageStatus.textContent = "Describing";
249
  try {
 
293
  const node = nodes[currentIndex];
294
  if (!node) {
295
  currentStatus.textContent = "No item selected";
 
296
  return;
297
  }
298
  node.element.classList.add("reader-active");
 
301
  node.element.focus({ preventScroll: true });
302
  node.element.scrollIntoView({ block: "center", behavior: "smooth" });
303
  currentStatus.textContent = readerItemStatus(node);
 
304
  }
305
 
306
  function haltPlayback({ clearAutoAdvance = true } = {}) {
 
534
  narrate(node.index);
535
  });
536
  });
 
 
 
 
 
 
537
  voiceControl.addEventListener("change", () => {
538
  voiceStatus.textContent = voiceControl.selectedOptions[0]?.textContent || "Custom voice";
539
  });
 
586
  });
587
 
588
  loadManifest();
589
+ loadModelBudget();
590
  loadImageDescriptions();
591
  updateSpeedValue();
 
592
 
593
  audio.addEventListener("ended", () => {
594
  playing = false;
static/evidence.html DELETED
@@ -1,95 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>Tiny Narrator Evidence</title>
7
- <link rel="stylesheet" href="/static/app.css" />
8
- </head>
9
- <body>
10
- <header class="topbar">
11
- <a class="brand" href="/" aria-label="Tiny Narrator home">
12
- <span class="brand-mark" aria-hidden="true">TN</span>
13
- <span>Tiny Narrator</span>
14
- </a>
15
- <nav class="topnav" aria-label="Evidence navigation">
16
- <a href="/">Article</a>
17
- <a href="#runbook">Runbook</a>
18
- <a href="#readiness">Readiness</a>
19
- <a href="#models">Models</a>
20
- </nav>
21
- <a class="reader-toggle evidence-link" href="/">Reader App</a>
22
- </header>
23
-
24
- <main class="evidence-shell" aria-labelledby="evidenceTitle">
25
- <section class="evidence-hero">
26
- <p class="kicker">Hackathon evidence</p>
27
- <h1 id="evidenceTitle">Judging receipts for Tiny Narrator</h1>
28
- <p class="dek">
29
- This page keeps model budget, award evidence, runtime setup, and demo checks visible without crowding
30
- the reader-first article experience.
31
- </p>
32
- </section>
33
-
34
- <section id="runbook" class="evidence-section" aria-labelledby="runbookTitle">
35
- <div class="demo-script-header">
36
- <h2 id="runbookTitle">Judge Runbook</h2>
37
- <span id="demoScriptStatus">Loading</span>
38
- </div>
39
- <ol id="demoScriptList" class="demo-script-list" aria-label="Judge demo runbook"></ol>
40
- <ul id="demoApiCheckList" class="demo-api-check-list" aria-label="Judge API evidence checks"></ul>
41
- </section>
42
-
43
- <section class="evidence-section" aria-labelledby="awardTitle">
44
- <div class="award-header">
45
- <h2 id="awardTitle">Award Evidence</h2>
46
- </div>
47
- <ul id="awardEvidenceList" class="award-list" aria-label="Hackathon bonus evidence"></ul>
48
- </section>
49
-
50
- <section id="readiness" class="evidence-section" aria-labelledby="readinessTitle">
51
- <div class="submission-header">
52
- <h2 id="readinessTitle">Submission Readiness</h2>
53
- <span id="submissionReadinessStatus">Checking</span>
54
- <button id="copyEvidenceButton" type="button">Copy Evidence</button>
55
- </div>
56
- <p id="evidenceCopyStatus" class="live-narration" aria-live="polite">Evidence bundle ready to copy.</p>
57
- <ul id="submissionReadinessList" class="award-list" aria-label="Submission readiness checks"></ul>
58
- </section>
59
-
60
- <section id="models" class="evidence-section" aria-labelledby="budgetTitle">
61
- <div class="budget-header">
62
- <h2 id="budgetTitle">Model Budget</h2>
63
- <span id="budgetStatus">Checking</span>
64
- </div>
65
- <ul id="modelBudgetList" class="budget-list" aria-label="Tiny Titan model budget"></ul>
66
- </section>
67
-
68
- <section class="evidence-section" aria-labelledby="runtimeTitle">
69
- <div class="runtime-header">
70
- <h2 id="runtimeTitle">Runtime Status</h2>
71
- <span id="runtimeStatusSummary">Checking</span>
72
- </div>
73
- <ul id="runtimeStatusList" class="runtime-list" aria-label="Live model runtime status"></ul>
74
- </section>
75
-
76
- <section class="evidence-section" aria-labelledby="setupTitle">
77
- <div class="runtime-header">
78
- <h2 id="setupTitle">Runtime Plan</h2>
79
- <span id="runtimeSetupStatus">Loading</span>
80
- </div>
81
- <ul id="runtimeSetupList" class="runtime-list" aria-label="Model runtime setup plan"></ul>
82
- </section>
83
-
84
- <section class="evidence-section" aria-labelledby="imageReceiptTitle">
85
- <div class="image-receipt-header">
86
- <h2 id="imageReceiptTitle">Image Receipts</h2>
87
- <span id="imageReceiptStatus">Loading</span>
88
- </div>
89
- <ul id="imageReceiptList" class="image-receipt-list" aria-label="Generated image provenance"></ul>
90
- </section>
91
- </main>
92
-
93
- <script src="/static/evidence.js" type="module"></script>
94
- </body>
95
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
static/evidence.js DELETED
@@ -1,341 +0,0 @@
1
- const demoScriptStatus = document.querySelector("#demoScriptStatus");
2
- const demoScriptList = document.querySelector("#demoScriptList");
3
- const demoApiCheckList = document.querySelector("#demoApiCheckList");
4
- const awardEvidenceList = document.querySelector("#awardEvidenceList");
5
- const submissionReadinessStatus = document.querySelector("#submissionReadinessStatus");
6
- const submissionReadinessList = document.querySelector("#submissionReadinessList");
7
- const copyEvidenceButton = document.querySelector("#copyEvidenceButton");
8
- const evidenceCopyStatus = document.querySelector("#evidenceCopyStatus");
9
- const budgetStatus = document.querySelector("#budgetStatus");
10
- const modelBudgetList = document.querySelector("#modelBudgetList");
11
- const runtimeStatusSummary = document.querySelector("#runtimeStatusSummary");
12
- const runtimeStatusList = document.querySelector("#runtimeStatusList");
13
- const runtimeSetupStatus = document.querySelector("#runtimeSetupStatus");
14
- const runtimeSetupList = document.querySelector("#runtimeSetupList");
15
- const imageReceiptStatus = document.querySelector("#imageReceiptStatus");
16
- const imageReceiptList = document.querySelector("#imageReceiptList");
17
-
18
- function escapeHtml(value) {
19
- return String(value).replace(/[&<>"']/g, (char) => ({
20
- "&": "&amp;",
21
- "<": "&lt;",
22
- ">": "&gt;",
23
- '"': "&quot;",
24
- "'": "&#39;",
25
- }[char]));
26
- }
27
-
28
- function roleLabel(role) {
29
- return role
30
- .split("_")
31
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
32
- .join(" ");
33
- }
34
-
35
- async function postJson(url, payload) {
36
- const options = payload === undefined ? {} : {
37
- method: "POST",
38
- headers: { "Content-Type": "application/json" },
39
- body: JSON.stringify(payload),
40
- };
41
- const response = await fetch(url, options);
42
- if (!response.ok) {
43
- throw new Error(`${response.status} ${response.statusText}`);
44
- }
45
- return response.json();
46
- }
47
-
48
- async function copyTextToClipboard(text, successMessage, emptyMessage, unavailableMessage) {
49
- if (!text) {
50
- evidenceCopyStatus.textContent = emptyMessage;
51
- return false;
52
- }
53
- if (!navigator.clipboard) {
54
- evidenceCopyStatus.textContent = unavailableMessage;
55
- return false;
56
- }
57
- await navigator.clipboard.writeText(text);
58
- evidenceCopyStatus.textContent = successMessage;
59
- return true;
60
- }
61
-
62
- async function loadDemoScript() {
63
- try {
64
- const payload = await postJson("/api/demo-script");
65
- demoScriptStatus.textContent = `${payload.estimated_minutes} min`;
66
- demoScriptList.innerHTML = payload.actions
67
- .map((item) => `
68
- <li>
69
- <div class="demo-script-title">
70
- <span>${escapeHtml(item.label)}</span>
71
- <span>${escapeHtml(`Step ${item.step}`)}</span>
72
- </div>
73
- <p>${escapeHtml(item.action)}</p>
74
- <p>${escapeHtml(item.evidence)}</p>
75
- </li>
76
- `)
77
- .join("");
78
- demoApiCheckList.innerHTML = payload.api_checks
79
- .map((item) => `
80
- <li>
81
- <span>${escapeHtml(item.method)}</span>
82
- <code>${escapeHtml(item.path)}</code>
83
- <p>${escapeHtml(item.expect)}${item.sample_body ? " Sample body included." : ""}</p>
84
- <code class="demo-api-command">${escapeHtml(item.curl)}</code>
85
- <button class="demo-command-copy" type="button" data-command="${escapeHtml(item.curl)}">Copy curl</button>
86
- <code class="demo-api-command">${escapeHtml(item.powershell)}</code>
87
- <button class="demo-command-copy" type="button" data-command="${escapeHtml(item.powershell)}">Copy PowerShell</button>
88
- </li>
89
- `)
90
- .join("");
91
- } catch {
92
- demoScriptStatus.textContent = "Unavailable";
93
- demoScriptList.innerHTML = `
94
- <li>
95
- <div class="demo-script-title">
96
- <span>Judge runbook</span>
97
- <span>offline</span>
98
- </div>
99
- <p>The structured demo script is unavailable.</p>
100
- </li>
101
- `;
102
- demoApiCheckList.innerHTML = "";
103
- }
104
- }
105
-
106
- async function loadAwardEvidence() {
107
- try {
108
- const payload = await postJson("/api/award-evidence");
109
- awardEvidenceList.innerHTML = payload.items
110
- .map((item) => `
111
- <li>
112
- <div class="award-title">
113
- <span>${escapeHtml(item.label)}</span>
114
- <span class="award-status">${escapeHtml(item.status)}</span>
115
- </div>
116
- <p class="award-evidence">${escapeHtml(item.evidence)}</p>
117
- </li>
118
- `)
119
- .join("");
120
- } catch {
121
- awardEvidenceList.innerHTML = `
122
- <li>
123
- <div class="award-title">
124
- <span>Checklist</span>
125
- <span class="award-status">offline</span>
126
- </div>
127
- <p class="award-evidence">Award evidence is unavailable.</p>
128
- </li>
129
- `;
130
- }
131
- }
132
-
133
- async function loadSubmissionReadiness() {
134
- try {
135
- const payload = await postJson("/api/submission-readiness");
136
- submissionReadinessStatus.textContent = payload.all_passed
137
- ? `${payload.passed_checks}/${payload.total_checks} pass`
138
- : "Review needed";
139
- submissionReadinessList.innerHTML = payload.checks
140
- .map((item) => `
141
- <li>
142
- <div class="award-title">
143
- <span>${escapeHtml(item.label)}</span>
144
- <span class="award-status">${escapeHtml(item.status)}</span>
145
- </div>
146
- <p class="award-evidence">${escapeHtml(item.evidence)}</p>
147
- </li>
148
- `)
149
- .join("");
150
- } catch {
151
- submissionReadinessStatus.textContent = "Unavailable";
152
- submissionReadinessList.innerHTML = `
153
- <li>
154
- <div class="award-title">
155
- <span>Submission readiness</span>
156
- <span class="award-status">offline</span>
157
- </div>
158
- <p class="award-evidence">Submission readiness evidence is unavailable.</p>
159
- </li>
160
- `;
161
- }
162
- }
163
-
164
- async function loadModelBudget() {
165
- try {
166
- const payload = await postJson("/api/model-budget");
167
- budgetStatus.textContent = payload.all_models_within_limit
168
- ? `All <= ${payload.limit_billion}B`
169
- : "Review needed";
170
- modelBudgetList.innerHTML = payload.models
171
- .map((model) => `
172
- <li>
173
- <div class="budget-row">
174
- <span>${escapeHtml(roleLabel(model.role))}</span>
175
- <span class="budget-pill">${escapeHtml(model.params)}</span>
176
- </div>
177
- <p>${model.within_limit ? "Tiny Titan pass" : "Review parameter budget"}</p>
178
- <p>${escapeHtml(model.id)}</p>
179
- </li>
180
- `)
181
- .join("");
182
- } catch {
183
- budgetStatus.textContent = "Unavailable";
184
- modelBudgetList.innerHTML = `
185
- <li>
186
- <div class="budget-row">
187
- <span>Model budget</span>
188
- <span class="budget-pill">offline</span>
189
- </div>
190
- <p>Tiny Titan evidence is unavailable.</p>
191
- </li>
192
- `;
193
- }
194
- }
195
-
196
- async function loadRuntimeStatus() {
197
- try {
198
- const payload = await postJson("/api/runtime-status");
199
- const brain = payload.reader_brain;
200
- const speech = payload.speech;
201
- const brainLabel = brain.available ? "llama.cpp online" : "llama.cpp fallback";
202
- const speechLabel = speech.available ? "Kokoro online" : "voice fallback";
203
- runtimeStatusSummary.textContent = `${brainLabel}, ${speechLabel}`;
204
- const runtimeItems = [
205
- ["Reader brain", payload.reader_brain],
206
- ["Vision", payload.vision],
207
- ["Speech", payload.speech],
208
- ["Image generation", payload.image_generation],
209
- ];
210
- runtimeStatusList.innerHTML = runtimeItems
211
- .map(([label, item]) => `
212
- <li>
213
- <div class="runtime-row">
214
- <span>${escapeHtml(label)}</span>
215
- <span class="runtime-pill">${escapeHtml(item.status)}</span>
216
- </div>
217
- <p>${escapeHtml(item.model)}${item.fallback ? ` | ${escapeHtml(item.fallback)}` : ""}</p>
218
- </li>
219
- `)
220
- .join("");
221
- } catch {
222
- runtimeStatusSummary.textContent = "Fallback ready";
223
- runtimeStatusList.innerHTML = `
224
- <li>
225
- <div class="runtime-row">
226
- <span>Runtime status</span>
227
- <span class="runtime-pill">offline</span>
228
- </div>
229
- <p>Live runtime status is unavailable; deterministic fallbacks remain documented.</p>
230
- </li>
231
- `;
232
- }
233
- }
234
-
235
- async function loadRuntimeSetup() {
236
- try {
237
- const payload = await postJson("/api/runtime-setup");
238
- runtimeSetupStatus.textContent = `${payload.steps.length} paths`;
239
- runtimeSetupList.innerHTML = payload.steps
240
- .map((step) => `
241
- <li>
242
- <div class="runtime-row">
243
- <span>${escapeHtml(step.label)}</span>
244
- <span class="runtime-pill">${escapeHtml(step.runtime)}</span>
245
- </div>
246
- <code class="runtime-command">${escapeHtml(step.command)}</code>
247
- <button class="runtime-command-copy" type="button" data-command="${escapeHtml(step.command)}">Copy command</button>
248
- <p>${escapeHtml(step.fallback)}</p>
249
- </li>
250
- `)
251
- .join("");
252
- } catch {
253
- runtimeSetupStatus.textContent = "Unavailable";
254
- runtimeSetupList.innerHTML = `
255
- <li>
256
- <div class="runtime-row">
257
- <span>Runtime plan</span>
258
- <span class="runtime-pill">offline</span>
259
- </div>
260
- <p>Setup evidence is unavailable.</p>
261
- </li>
262
- `;
263
- }
264
- }
265
-
266
- async function loadImageDescriptions() {
267
- try {
268
- const payload = await postJson("/api/image-descriptions");
269
- imageReceiptStatus.textContent = `${payload.descriptions.length} receipts`;
270
- imageReceiptList.innerHTML = payload.descriptions
271
- .map((description) => `
272
- <li>
273
- <div class="image-receipt-row">
274
- <span>${escapeHtml(description.id)}</span>
275
- <span class="image-receipt-pill">${escapeHtml(description.generation_status)}</span>
276
- </div>
277
- <p>${escapeHtml(description.generation_model)} | seed ${escapeHtml(String(description.seed))}</p>
278
- <p>${escapeHtml(description.prompt)}</p>
279
- </li>
280
- `)
281
- .join("");
282
- } catch {
283
- imageReceiptStatus.textContent = "Unavailable";
284
- imageReceiptList.innerHTML = `
285
- <li>
286
- <div class="image-receipt-row">
287
- <span>Image receipts</span>
288
- <span class="image-receipt-pill">offline</span>
289
- </div>
290
- <p>Generated image provenance is unavailable.</p>
291
- </li>
292
- `;
293
- }
294
- }
295
-
296
- copyEvidenceButton.addEventListener("click", async () => {
297
- try {
298
- const payload = await postJson("/api/evidence-bundle");
299
- const text = JSON.stringify(payload, null, 2);
300
- await copyTextToClipboard(
301
- text,
302
- "Evidence bundle copied.",
303
- "Evidence bundle is empty.",
304
- "Evidence bundle loaded, but clipboard access is unavailable.",
305
- );
306
- } catch (error) {
307
- evidenceCopyStatus.textContent = `Evidence bundle failed: ${error.message}`;
308
- }
309
- });
310
-
311
- demoApiCheckList.addEventListener("click", async (event) => {
312
- const button = event.target.closest(".demo-command-copy");
313
- if (!button) return;
314
- const command = button.dataset.command || "";
315
- await copyTextToClipboard(
316
- command,
317
- "Demo command copied.",
318
- "Demo command is empty.",
319
- "Command is visible, but clipboard access is unavailable.",
320
- );
321
- });
322
-
323
- runtimeSetupList.addEventListener("click", async (event) => {
324
- const button = event.target.closest(".runtime-command-copy");
325
- if (!button) return;
326
- const command = button.dataset.command || "";
327
- await copyTextToClipboard(
328
- command,
329
- "Runtime setup command copied.",
330
- "Runtime setup command is empty.",
331
- "Runtime setup command is visible, but clipboard access is unavailable.",
332
- );
333
- });
334
-
335
- loadDemoScript();
336
- loadAwardEvidence();
337
- loadSubmissionReadiness();
338
- loadModelBudget();
339
- loadRuntimeStatus();
340
- loadRuntimeSetup();
341
- loadImageDescriptions();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
static/index.html CHANGED
@@ -16,7 +16,6 @@
16
  <a href="#why">Why</a>
17
  <a href="#models">Models</a>
18
  <a href="#notes">Field Notes</a>
19
- <a href="/evidence">Evidence</a>
20
  </nav>
21
  <button id="readerToggle" class="reader-toggle" type="button" aria-pressed="false">
22
  <span class="toggle-dot" aria-hidden="true"></span>
@@ -118,17 +117,17 @@
118
  <p id="liveNarration" class="live-narration" aria-live="polite">
119
  Turn on screen reader mode to begin.
120
  </p>
121
- <div class="reader-queue-header">
122
- <h3>Reader Queue</h3>
123
- <span id="readerQueueStatus">Loading</span>
124
- </div>
125
- <ol id="readerQueueList" class="runtime-list" aria-label="Semantic reader queue"></ol>
126
  <div class="history-header">
127
  <h3>Transcript</h3>
128
  <button id="copyTranscriptButton" type="button">Copy</button>
129
  <button id="clearTranscriptButton" type="button">Clear</button>
130
  </div>
131
  <ol id="transcriptLog" class="transcript-log" aria-label="Narration transcript"></ol>
 
 
 
 
 
132
  </aside>
133
  </main>
134
 
 
16
  <a href="#why">Why</a>
17
  <a href="#models">Models</a>
18
  <a href="#notes">Field Notes</a>
 
19
  </nav>
20
  <button id="readerToggle" class="reader-toggle" type="button" aria-pressed="false">
21
  <span class="toggle-dot" aria-hidden="true"></span>
 
117
  <p id="liveNarration" class="live-narration" aria-live="polite">
118
  Turn on screen reader mode to begin.
119
  </p>
 
 
 
 
 
120
  <div class="history-header">
121
  <h3>Transcript</h3>
122
  <button id="copyTranscriptButton" type="button">Copy</button>
123
  <button id="clearTranscriptButton" type="button">Clear</button>
124
  </div>
125
  <ol id="transcriptLog" class="transcript-log" aria-label="Narration transcript"></ol>
126
+ <div class="budget-header">
127
+ <h3>Model Stack</h3>
128
+ <span id="modelBudgetStatus">Checking</span>
129
+ </div>
130
+ <ul id="modelStackList" class="budget-list" aria-label="Tiny Titan model stack"></ul>
131
  </aside>
132
  </main>
133