cvpfus Codex commited on
Commit
79c2c78
·
1 Parent(s): 680e5da

Expose Tiny Titan model budget

Browse files

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

Files changed (7) hide show
  1. FIELD_NOTES.md +2 -0
  2. README.md +3 -1
  3. app.py +42 -1
  4. scripts/verify.py +17 -0
  5. static/app.css +55 -0
  6. static/app.js +41 -0
  7. static/index.html +5 -0
FIELD_NOTES.md CHANGED
@@ -24,6 +24,8 @@ 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
  ## Reader Mode Behavior
28
 
29
  The frontend creates a reading queue from semantic nodes: headings, paragraphs, quotes, figures, captions, and controls. It speaks one item at a time, keeps keyboard focus synchronized with the active node, and exposes the current narration through an `aria-live` region.
 
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 and a boolean `all_models_within_limit` check. The session panel renders that payload so the Tiny Titan claim is visible during the demo.
28
+
29
  ## Reader Mode Behavior
30
 
31
  The frontend creates a reading queue from semantic nodes: headings, paragraphs, quotes, figures, captions, and controls. It speaks one item at a time, keeps keyboard focus synchronized with the active node, and exposes the current narration through an `aria-live` region.
README.md CHANGED
@@ -69,6 +69,8 @@ python scripts/verify.py
69
 
70
  The verifier checks syntax, static assets, deterministic fallback model paths, and generated speech file behavior.
71
 
 
 
72
  ## Screen Reader Mode
73
 
74
  The frontend builds a reading queue from semantic article nodes. When screen-reader mode is on:
@@ -98,6 +100,6 @@ Navigation commands interrupt current speech before starting the next request, m
98
 
99
  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.
100
 
101
- The session panel also renders a manifest-backed demo checklist for Tiny Titan, Llama Champion, Off-Brand, and Field Notes evidence.
102
 
103
  `/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.
 
69
 
70
  The verifier checks syntax, static assets, deterministic fallback model paths, and generated speech file behavior.
71
 
72
+ `/api/model-budget` exposes numeric parameter counts for every model role and reports whether the full stack stays within the 4B Tiny Titan limit.
73
+
74
  ## Screen Reader Mode
75
 
76
  The frontend builds a reading queue from semantic article nodes. When screen-reader mode is on:
 
100
 
101
  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.
102
 
103
+ The session panel also renders a manifest-backed demo checklist for Tiny Titan, Llama Champion, Off-Brand, and Field Notes evidence. A model-budget panel reads `/api/model-budget` so judges can see each role's parameter count in the live app.
104
 
105
  `/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.
app.py CHANGED
@@ -27,29 +27,34 @@ LLAMA_CPP_BASE_URL = os.getenv("LLAMA_CPP_BASE_URL", "http://localhost:8080/v1")
27
  LLAMA_CPP_MODEL = os.getenv("LLAMA_CPP_MODEL", "narrator-brain")
28
  GRADIO_SERVER_NAME = os.getenv("GRADIO_SERVER_NAME", "0.0.0.0")
29
  GRADIO_SERVER_PORT = int(os.getenv("PORT", os.getenv("GRADIO_SERVER_PORT", "7860")))
 
30
 
31
- MODEL_MANIFEST: dict[str, dict[str, str]] = {
32
  "reader_brain": {
33
  "id": "nvidia/NVIDIA-Nemotron-3-Nano-4B-GGUF",
34
  "params": "3.97B",
 
35
  "runtime": "llama.cpp",
36
  "role": "Plans concise narration and reading-order phrasing.",
37
  },
38
  "vision": {
39
  "id": "openbmb/MiniCPM-V-2",
40
  "params": "3B",
 
41
  "runtime": "Python integration planned",
42
  "role": "Describes images and OCR-like visual details.",
43
  },
44
  "speech": {
45
  "id": "hexgrad/Kokoro-82M",
46
  "params": "82M",
 
47
  "runtime": "Python",
48
  "role": "Speaks the final narration.",
49
  },
50
  "image_generation": {
51
  "id": "black-forest-labs/FLUX.2-klein-4B",
52
  "params": "4B",
 
53
  "runtime": "Python integration planned",
54
  "role": "Creates article illustrations.",
55
  },
@@ -116,6 +121,31 @@ AWARD_EVIDENCE: list[dict[str, str]] = [
116
  },
117
  ]
118
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  ARTICLE_MANIFEST: dict[str, Any] = {
120
  "title": "A tiny model reader that turns articles into guided narration",
121
  "reader_controls": [
@@ -131,6 +161,7 @@ ARTICLE_MANIFEST: dict[str, Any] = {
131
  "bonus_targets": ["Tiny Titan", "Llama Champion", "Off-Brand", "Field Notes"],
132
  "images": ARTICLE_IMAGES,
133
  "models": MODEL_MANIFEST,
 
134
  "reader_settings": READER_SETTINGS,
135
  "award_evidence": AWARD_EVIDENCE,
136
  }
@@ -441,6 +472,11 @@ async def award_evidence() -> JSONResponse:
441
  return _json({"ok": True, "items": AWARD_EVIDENCE})
442
 
443
 
 
 
 
 
 
444
  @app.get("/api/runtime-status")
445
  async def runtime_status() -> JSONResponse:
446
  return _json(_runtime_status_core())
@@ -486,6 +522,11 @@ def describe_article_images_api() -> str:
486
  return json.dumps(describe_article_images_core())
487
 
488
 
 
 
 
 
 
489
  @app.api(name="speak")
490
  def speak_api(text: str, voice: str = "af_heart", speed: float = 1.0) -> str:
491
  return json.dumps(speak_core(text, voice, speed))
 
27
  LLAMA_CPP_MODEL = os.getenv("LLAMA_CPP_MODEL", "narrator-brain")
28
  GRADIO_SERVER_NAME = os.getenv("GRADIO_SERVER_NAME", "0.0.0.0")
29
  GRADIO_SERVER_PORT = int(os.getenv("PORT", os.getenv("GRADIO_SERVER_PORT", "7860")))
30
+ TINY_TITAN_LIMIT_B = 4.0
31
 
32
+ MODEL_MANIFEST: dict[str, dict[str, Any]] = {
33
  "reader_brain": {
34
  "id": "nvidia/NVIDIA-Nemotron-3-Nano-4B-GGUF",
35
  "params": "3.97B",
36
+ "params_billion": 3.97,
37
  "runtime": "llama.cpp",
38
  "role": "Plans concise narration and reading-order phrasing.",
39
  },
40
  "vision": {
41
  "id": "openbmb/MiniCPM-V-2",
42
  "params": "3B",
43
+ "params_billion": 3.0,
44
  "runtime": "Python integration planned",
45
  "role": "Describes images and OCR-like visual details.",
46
  },
47
  "speech": {
48
  "id": "hexgrad/Kokoro-82M",
49
  "params": "82M",
50
+ "params_billion": 0.082,
51
  "runtime": "Python",
52
  "role": "Speaks the final narration.",
53
  },
54
  "image_generation": {
55
  "id": "black-forest-labs/FLUX.2-klein-4B",
56
  "params": "4B",
57
+ "params_billion": 4.0,
58
  "runtime": "Python integration planned",
59
  "role": "Creates article illustrations.",
60
  },
 
121
  },
122
  ]
123
 
124
+
125
+ def model_budget_core() -> dict[str, Any]:
126
+ models = []
127
+ for role, model in MODEL_MANIFEST.items():
128
+ params_billion = float(model["params_billion"])
129
+ models.append(
130
+ {
131
+ "role": role,
132
+ "id": model["id"],
133
+ "runtime": model["runtime"],
134
+ "params": model["params"],
135
+ "params_billion": params_billion,
136
+ "limit_billion": TINY_TITAN_LIMIT_B,
137
+ "within_limit": params_billion <= TINY_TITAN_LIMIT_B,
138
+ }
139
+ )
140
+ return {
141
+ "ok": True,
142
+ "award": "Tiny Titan",
143
+ "limit_billion": TINY_TITAN_LIMIT_B,
144
+ "all_models_within_limit": all(model["within_limit"] for model in models),
145
+ "models": models,
146
+ }
147
+
148
+
149
  ARTICLE_MANIFEST: dict[str, Any] = {
150
  "title": "A tiny model reader that turns articles into guided narration",
151
  "reader_controls": [
 
161
  "bonus_targets": ["Tiny Titan", "Llama Champion", "Off-Brand", "Field Notes"],
162
  "images": ARTICLE_IMAGES,
163
  "models": MODEL_MANIFEST,
164
+ "model_budget": model_budget_core(),
165
  "reader_settings": READER_SETTINGS,
166
  "award_evidence": AWARD_EVIDENCE,
167
  }
 
472
  return _json({"ok": True, "items": AWARD_EVIDENCE})
473
 
474
 
475
+ @app.get("/api/model-budget")
476
+ async def model_budget() -> JSONResponse:
477
+ return _json(model_budget_core())
478
+
479
+
480
  @app.get("/api/runtime-status")
481
  async def runtime_status() -> JSONResponse:
482
  return _json(_runtime_status_core())
 
522
  return json.dumps(describe_article_images_core())
523
 
524
 
525
+ @app.api(name="model_budget")
526
+ def model_budget_api() -> str:
527
+ return json.dumps(model_budget_core())
528
+
529
+
530
  @app.api(name="speak")
531
  def speak_api(text: str, voice: str = "af_heart", speed: float = 1.0) -> str:
532
  return json.dumps(speak_core(text, voice, speed))
scripts/verify.py CHANGED
@@ -99,6 +99,8 @@ def verify_routes() -> None:
99
  assert_true("autoAdvanceControl" in home.text, "Home route should include auto-advance control")
100
  assert_true("transcriptLog" in home.text, "Home route should include transcript log")
101
  assert_true("awardEvidenceList" in home.text, "Home route should include award evidence list")
 
 
102
 
103
  health = client.get("/api/health")
104
  assert_true(health.status_code == 200, "Health route should return 200")
@@ -137,6 +139,10 @@ def verify_routes() -> None:
137
  len(manifest_payload["award_evidence"]) == 4,
138
  "Manifest should expose four award evidence items",
139
  )
 
 
 
 
140
 
141
  awards = client.get("/api/award-evidence")
142
  assert_true(awards.status_code == 200, "Award evidence route should return 200")
@@ -147,6 +153,17 @@ def verify_routes() -> None:
147
  "Award evidence route should cover the targeted bonuses",
148
  )
149
 
 
 
 
 
 
 
 
 
 
 
 
150
  runtime = client.get("/api/runtime-status")
151
  assert_true(runtime.status_code == 200, "Runtime status route should return 200")
152
  runtime_payload = runtime.json()
 
99
  assert_true("autoAdvanceControl" in home.text, "Home route should include auto-advance control")
100
  assert_true("transcriptLog" in home.text, "Home route should include transcript log")
101
  assert_true("awardEvidenceList" in home.text, "Home route should include award evidence list")
102
+ assert_true("budgetStatus" in home.text, "Home route should include model budget status")
103
+ assert_true("modelBudgetList" in home.text, "Home route should include model budget list")
104
 
105
  health = client.get("/api/health")
106
  assert_true(health.status_code == 200, "Health route should return 200")
 
139
  len(manifest_payload["award_evidence"]) == 4,
140
  "Manifest should expose four award evidence items",
141
  )
142
+ assert_true(
143
+ manifest_payload["model_budget"]["all_models_within_limit"],
144
+ "Manifest should prove every model is within the Tiny Titan limit",
145
+ )
146
 
147
  awards = client.get("/api/award-evidence")
148
  assert_true(awards.status_code == 200, "Award evidence route should return 200")
 
153
  "Award evidence route should cover the targeted bonuses",
154
  )
155
 
156
+ budget = client.get("/api/model-budget")
157
+ assert_true(budget.status_code == 200, "Model budget route should return 200")
158
+ budget_payload = budget.json()
159
+ assert_true(budget_payload["ok"], "Model budget payload should be ok")
160
+ assert_true(budget_payload["limit_billion"] == 4.0, "Tiny Titan limit should be 4B")
161
+ assert_true(budget_payload["all_models_within_limit"], "All models should stay within the Tiny Titan limit")
162
+ assert_true(
163
+ all(item["params_billion"] <= budget_payload["limit_billion"] for item in budget_payload["models"]),
164
+ "Every model budget item should be at or below the limit",
165
+ )
166
+
167
  runtime = client.get("/api/runtime-status")
168
  assert_true(runtime.status_code == 200, "Runtime status route should return 200")
169
  runtime_payload = runtime.json()
static/app.css CHANGED
@@ -290,6 +290,21 @@ dd {
290
  margin-top: 22px;
291
  }
292
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  .history-header button {
294
  min-height: 34px;
295
  border-radius: 6px;
@@ -346,6 +361,46 @@ dd {
346
  background: var(--surface);
347
  }
348
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
  .award-title {
350
  display: flex;
351
  align-items: center;
 
290
  margin-top: 22px;
291
  }
292
 
293
+ .budget-header {
294
+ display: flex;
295
+ align-items: center;
296
+ justify-content: space-between;
297
+ gap: 10px;
298
+ margin-top: 22px;
299
+ }
300
+
301
+ .budget-header span {
302
+ color: var(--accent-strong);
303
+ font-size: 0.76rem;
304
+ font-weight: 850;
305
+ text-transform: uppercase;
306
+ }
307
+
308
  .history-header button {
309
  min-height: 34px;
310
  border-radius: 6px;
 
361
  background: var(--surface);
362
  }
363
 
364
+ .budget-list {
365
+ display: grid;
366
+ gap: 8px;
367
+ margin: 12px 0 0;
368
+ padding: 0;
369
+ list-style: none;
370
+ }
371
+
372
+ .budget-list li {
373
+ border: 1px solid var(--line);
374
+ border-radius: 8px;
375
+ padding: 10px 12px;
376
+ background: var(--surface);
377
+ }
378
+
379
+ .budget-row {
380
+ display: flex;
381
+ align-items: center;
382
+ justify-content: space-between;
383
+ gap: 8px;
384
+ font-size: 0.82rem;
385
+ font-weight: 850;
386
+ text-transform: uppercase;
387
+ }
388
+
389
+ .budget-pill {
390
+ border-radius: 999px;
391
+ background: rgba(197, 69, 44, 0.12);
392
+ color: var(--focus);
393
+ padding: 2px 8px;
394
+ font-size: 0.7rem;
395
+ }
396
+
397
+ .budget-list p {
398
+ margin: 6px 0 0;
399
+ color: var(--muted);
400
+ font-size: 0.78rem;
401
+ overflow-wrap: anywhere;
402
+ }
403
+
404
  .award-title {
405
  display: flex;
406
  align-items: center;
static/app.js CHANGED
@@ -18,6 +18,8 @@ const transcriptLog = document.querySelector("#transcriptLog");
18
  const copyTranscriptButton = document.querySelector("#copyTranscriptButton");
19
  const clearTranscriptButton = document.querySelector("#clearTranscriptButton");
20
  const awardEvidenceList = document.querySelector("#awardEvidenceList");
 
 
21
 
22
  const controls = {
23
  prev: document.querySelector("#prevButton"),
@@ -156,6 +158,44 @@ async function loadAwardEvidence() {
156
  }
157
  }
158
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  async function loadRuntimeStatus() {
160
  try {
161
  const payload = await postJson("/api/runtime-status");
@@ -477,6 +517,7 @@ controls.play.addEventListener("click", () => {
477
 
478
  loadManifest();
479
  loadAwardEvidence();
 
480
  loadRuntimeStatus();
481
  loadImageDescriptions();
482
  updateSpeedValue();
 
18
  const copyTranscriptButton = document.querySelector("#copyTranscriptButton");
19
  const clearTranscriptButton = document.querySelector("#clearTranscriptButton");
20
  const awardEvidenceList = document.querySelector("#awardEvidenceList");
21
+ const budgetStatus = document.querySelector("#budgetStatus");
22
+ const modelBudgetList = document.querySelector("#modelBudgetList");
23
 
24
  const controls = {
25
  prev: document.querySelector("#prevButton"),
 
158
  }
159
  }
160
 
161
+ function roleLabel(role) {
162
+ return role
163
+ .split("_")
164
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
165
+ .join(" ");
166
+ }
167
+
168
+ async function loadModelBudget() {
169
+ try {
170
+ const payload = await postJson("/api/model-budget");
171
+ budgetStatus.textContent = payload.all_models_within_limit
172
+ ? `All <= ${payload.limit_billion}B`
173
+ : "Review needed";
174
+ modelBudgetList.innerHTML = payload.models
175
+ .map((model) => `
176
+ <li>
177
+ <div class="budget-row">
178
+ <span>${escapeHtml(roleLabel(model.role))}</span>
179
+ <span class="budget-pill">${escapeHtml(model.params)}</span>
180
+ </div>
181
+ <p>${escapeHtml(model.id)}</p>
182
+ </li>
183
+ `)
184
+ .join("");
185
+ } catch {
186
+ budgetStatus.textContent = "Unavailable";
187
+ modelBudgetList.innerHTML = `
188
+ <li>
189
+ <div class="budget-row">
190
+ <span>Model budget</span>
191
+ <span class="budget-pill">offline</span>
192
+ </div>
193
+ <p>Tiny Titan evidence is unavailable.</p>
194
+ </li>
195
+ `;
196
+ }
197
+ }
198
+
199
  async function loadRuntimeStatus() {
200
  try {
201
  const payload = await postJson("/api/runtime-status");
 
517
 
518
  loadManifest();
519
  loadAwardEvidence();
520
+ loadModelBudget();
521
  loadRuntimeStatus();
522
  loadImageDescriptions();
523
  updateSpeedValue();
static/index.html CHANGED
@@ -126,6 +126,11 @@
126
  <h3>Demo Checklist</h3>
127
  </div>
128
  <ul id="awardEvidenceList" class="award-list" aria-label="Hackathon bonus evidence"></ul>
 
 
 
 
 
129
  </aside>
130
  </main>
131
 
 
126
  <h3>Demo Checklist</h3>
127
  </div>
128
  <ul id="awardEvidenceList" class="award-list" aria-label="Hackathon bonus evidence"></ul>
129
+ <div class="budget-header">
130
+ <h3>Model Budget</h3>
131
+ <span id="budgetStatus">Checking</span>
132
+ </div>
133
+ <ul id="modelBudgetList" class="budget-list" aria-label="Tiny Titan model budget"></ul>
134
  </aside>
135
  </main>
136