cvpfus Codex commited on
Commit
38c87e2
·
1 Parent(s): 48d1717

Add hackathon award evidence checklist

Browse files

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

Files changed (7) hide show
  1. FIELD_NOTES.md +2 -0
  2. README.md +2 -0
  3. app.py +33 -0
  4. scripts/verify.py +14 -0
  5. static/app.css +43 -0
  6. static/app.js +29 -0
  7. static/index.html +4 -0
FIELD_NOTES.md CHANGED
@@ -52,3 +52,5 @@ Kokoro is still the intended model-backed TTS path. For local resilience, the fr
52
  Reader settings are manifest-backed: the app exposes known Kokoro voices and speed bounds through `/api/article-manifest`, then the custom frontend renders the controls and passes the selected voice to `/api/speak`.
53
 
54
  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.
 
 
 
52
  Reader settings are manifest-backed: the app exposes known Kokoro voices and speed bounds through `/api/article-manifest`, then the custom frontend renders the controls and passes the selected voice to `/api/speak`.
53
 
54
  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.
55
+
56
+ The app exposes `/api/award-evidence` and renders that data in the session panel, so the live demo can point directly to the hackathon bonus targets it is designed to satisfy.
README.md CHANGED
@@ -93,3 +93,5 @@ Kokoro remains the planned tiny-model TTS path. During local demos, if the serve
93
  The reader bar exposes Kokoro voice selection and speaking speed controls. Defaults come from `/api/article-manifest` so the UI, docs, and backend stay aligned.
94
 
95
  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.
 
 
 
93
  The reader bar exposes Kokoro voice selection and speaking speed controls. Defaults come from `/api/article-manifest` so the UI, docs, and backend stay aligned.
94
 
95
  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.
96
+
97
+ The session panel also renders a manifest-backed demo checklist for Tiny Titan, Llama Champion, Off-Brand, and Field Notes evidence.
app.py CHANGED
@@ -87,6 +87,33 @@ READER_SETTINGS: dict[str, Any] = {
87
  "speed": {"min": 0.75, "max": 1.35, "step": 0.05},
88
  }
89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  ARTICLE_MANIFEST: dict[str, Any] = {
91
  "title": "A tiny model reader that turns articles into guided narration",
92
  "reader_controls": [
@@ -103,6 +130,7 @@ ARTICLE_MANIFEST: dict[str, Any] = {
103
  "images": ARTICLE_IMAGES,
104
  "models": MODEL_MANIFEST,
105
  "reader_settings": READER_SETTINGS,
 
106
  }
107
 
108
  app = Server(title="Tiny Narrator")
@@ -348,6 +376,11 @@ async def article_manifest() -> JSONResponse:
348
  return _json({"ok": True, **ARTICLE_MANIFEST})
349
 
350
 
 
 
 
 
 
351
  @app.post("/api/reader-brain")
352
  async def reader_brain_endpoint(payload: ReaderBrainRequest) -> JSONResponse:
353
  return _json(reader_brain_core(payload.node_type, payload.text, payload.position, payload.mode))
 
87
  "speed": {"min": 0.75, "max": 1.35, "step": 0.05},
88
  }
89
 
90
+ AWARD_EVIDENCE: list[dict[str, str]] = [
91
+ {
92
+ "id": "tiny-titan",
93
+ "label": "Tiny Titan",
94
+ "status": "ready",
95
+ "evidence": "All planned model roles use models at or below 4B parameters.",
96
+ },
97
+ {
98
+ "id": "llama-champion",
99
+ "label": "Llama Champion",
100
+ "status": "ready",
101
+ "evidence": "The reader-brain path targets a GGUF model through a llama.cpp OpenAI-compatible endpoint.",
102
+ },
103
+ {
104
+ "id": "off-brand",
105
+ "label": "Off-Brand",
106
+ "status": "ready",
107
+ "evidence": "The visible app is custom HTML, CSS, and JavaScript served by Gradio Server.",
108
+ },
109
+ {
110
+ "id": "field-notes",
111
+ "label": "Field Notes",
112
+ "status": "ready",
113
+ "evidence": "The repo records model choices, fallbacks, latency reporting, and accessibility behavior.",
114
+ },
115
+ ]
116
+
117
  ARTICLE_MANIFEST: dict[str, Any] = {
118
  "title": "A tiny model reader that turns articles into guided narration",
119
  "reader_controls": [
 
130
  "images": ARTICLE_IMAGES,
131
  "models": MODEL_MANIFEST,
132
  "reader_settings": READER_SETTINGS,
133
+ "award_evidence": AWARD_EVIDENCE,
134
  }
135
 
136
  app = Server(title="Tiny Narrator")
 
376
  return _json({"ok": True, **ARTICLE_MANIFEST})
377
 
378
 
379
+ @app.get("/api/award-evidence")
380
+ async def award_evidence() -> JSONResponse:
381
+ return _json({"ok": True, "items": AWARD_EVIDENCE})
382
+
383
+
384
  @app.post("/api/reader-brain")
385
  async def reader_brain_endpoint(payload: ReaderBrainRequest) -> JSONResponse:
386
  return _json(reader_brain_core(payload.node_type, payload.text, payload.position, payload.mode))
scripts/verify.py CHANGED
@@ -89,6 +89,7 @@ def verify_routes() -> None:
89
  assert_true("voiceControl" in home.text, "Home route should include voice control")
90
  assert_true("speedValue" in home.text, "Home route should include speed value output")
91
  assert_true("transcriptLog" in home.text, "Home route should include transcript log")
 
92
 
93
  health = client.get("/api/health")
94
  assert_true(health.status_code == 200, "Health route should return 200")
@@ -119,6 +120,19 @@ def verify_routes() -> None:
119
  len(manifest_payload["reader_settings"]["voices"]) >= 4,
120
  "Manifest should expose multiple Kokoro voice choices",
121
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
122
 
123
  image_descriptions = client.get("/api/image-descriptions")
124
  assert_true(image_descriptions.status_code == 200, "Image descriptions route should return 200")
 
89
  assert_true("voiceControl" in home.text, "Home route should include voice control")
90
  assert_true("speedValue" in home.text, "Home route should include speed value output")
91
  assert_true("transcriptLog" in home.text, "Home route should include transcript log")
92
+ assert_true("awardEvidenceList" in home.text, "Home route should include award evidence list")
93
 
94
  health = client.get("/api/health")
95
  assert_true(health.status_code == 200, "Health route should return 200")
 
120
  len(manifest_payload["reader_settings"]["voices"]) >= 4,
121
  "Manifest should expose multiple Kokoro voice choices",
122
  )
123
+ assert_true(
124
+ len(manifest_payload["award_evidence"]) == 4,
125
+ "Manifest should expose four award evidence items",
126
+ )
127
+
128
+ awards = client.get("/api/award-evidence")
129
+ assert_true(awards.status_code == 200, "Award evidence route should return 200")
130
+ award_payload = awards.json()
131
+ assert_true(award_payload["ok"], "Award evidence payload should be ok")
132
+ assert_true(
133
+ {item["id"] for item in award_payload["items"]} == {"tiny-titan", "llama-champion", "off-brand", "field-notes"},
134
+ "Award evidence route should cover the targeted bonuses",
135
+ )
136
 
137
  image_descriptions = client.get("/api/image-descriptions")
138
  assert_true(image_descriptions.status_code == 200, "Image descriptions route should return 200")
static/app.css CHANGED
@@ -286,6 +286,10 @@ dd {
286
  margin-top: 22px;
287
  }
288
 
 
 
 
 
289
  .history-header button {
290
  min-height: 34px;
291
  border-radius: 6px;
@@ -327,6 +331,45 @@ dd {
327
  font-size: 0.92rem;
328
  }
329
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
  .reader-bar {
331
  position: fixed;
332
  right: clamp(14px, 3vw, 32px);
 
286
  margin-top: 22px;
287
  }
288
 
289
+ .award-header {
290
+ margin-top: 22px;
291
+ }
292
+
293
  .history-header button {
294
  min-height: 34px;
295
  border-radius: 6px;
 
331
  font-size: 0.92rem;
332
  }
333
 
334
+ .award-list {
335
+ display: grid;
336
+ gap: 10px;
337
+ margin: 12px 0 0;
338
+ padding: 0;
339
+ list-style: none;
340
+ }
341
+
342
+ .award-list li {
343
+ border: 1px solid var(--line);
344
+ border-radius: 8px;
345
+ padding: 10px 12px;
346
+ background: var(--surface);
347
+ }
348
+
349
+ .award-title {
350
+ display: flex;
351
+ align-items: center;
352
+ justify-content: space-between;
353
+ gap: 8px;
354
+ font-size: 0.82rem;
355
+ font-weight: 850;
356
+ text-transform: uppercase;
357
+ }
358
+
359
+ .award-status {
360
+ border-radius: 999px;
361
+ background: rgba(31, 111, 104, 0.12);
362
+ color: var(--accent-strong);
363
+ padding: 2px 8px;
364
+ font-size: 0.7rem;
365
+ }
366
+
367
+ .award-evidence {
368
+ margin: 6px 0 0;
369
+ color: var(--muted);
370
+ font-size: 0.88rem;
371
+ }
372
+
373
  .reader-bar {
374
  position: fixed;
375
  right: clamp(14px, 3vw, 32px);
static/app.js CHANGED
@@ -15,6 +15,7 @@ const speedValue = document.querySelector("#speedValue");
15
  const transcriptLog = document.querySelector("#transcriptLog");
16
  const copyTranscriptButton = document.querySelector("#copyTranscriptButton");
17
  const clearTranscriptButton = document.querySelector("#clearTranscriptButton");
 
18
 
19
  const controls = {
20
  prev: document.querySelector("#prevButton"),
@@ -123,6 +124,33 @@ async function loadManifest() {
123
  }
124
  }
125
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  async function loadImageDescriptions() {
127
  imageStatus.textContent = "Describing";
128
  try {
@@ -403,6 +431,7 @@ controls.play.addEventListener("click", () => {
403
  });
404
 
405
  loadManifest();
 
406
  loadImageDescriptions();
407
  updateSpeedValue();
408
 
 
15
  const transcriptLog = document.querySelector("#transcriptLog");
16
  const copyTranscriptButton = document.querySelector("#copyTranscriptButton");
17
  const clearTranscriptButton = document.querySelector("#clearTranscriptButton");
18
+ const awardEvidenceList = document.querySelector("#awardEvidenceList");
19
 
20
  const controls = {
21
  prev: document.querySelector("#prevButton"),
 
124
  }
125
  }
126
 
127
+ async function loadAwardEvidence() {
128
+ try {
129
+ const payload = await postJson("/api/award-evidence");
130
+ awardEvidenceList.innerHTML = payload.items
131
+ .map((item) => `
132
+ <li>
133
+ <div class="award-title">
134
+ <span>${escapeHtml(item.label)}</span>
135
+ <span class="award-status">${escapeHtml(item.status)}</span>
136
+ </div>
137
+ <p class="award-evidence">${escapeHtml(item.evidence)}</p>
138
+ </li>
139
+ `)
140
+ .join("");
141
+ } catch {
142
+ awardEvidenceList.innerHTML = `
143
+ <li>
144
+ <div class="award-title">
145
+ <span>Checklist</span>
146
+ <span class="award-status">offline</span>
147
+ </div>
148
+ <p class="award-evidence">Award evidence is unavailable.</p>
149
+ </li>
150
+ `;
151
+ }
152
+ }
153
+
154
  async function loadImageDescriptions() {
155
  imageStatus.textContent = "Describing";
156
  try {
 
431
  });
432
 
433
  loadManifest();
434
+ loadAwardEvidence();
435
  loadImageDescriptions();
436
  updateSpeedValue();
437
 
static/index.html CHANGED
@@ -118,6 +118,10 @@
118
  <button id="clearTranscriptButton" type="button">Clear</button>
119
  </div>
120
  <ol id="transcriptLog" class="transcript-log" aria-label="Narration transcript"></ol>
 
 
 
 
121
  </aside>
122
  </main>
123
 
 
118
  <button id="clearTranscriptButton" type="button">Clear</button>
119
  </div>
120
  <ol id="transcriptLog" class="transcript-log" aria-label="Narration transcript"></ol>
121
+ <div class="award-header">
122
+ <h3>Demo Checklist</h3>
123
+ </div>
124
+ <ul id="awardEvidenceList" class="award-list" aria-label="Hackathon bonus evidence"></ul>
125
  </aside>
126
  </main>
127