cvpfus Codex commited on
Commit
4d86717
·
1 Parent(s): 7711224

Bound generated speech outputs

Browse files

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

Files changed (4) hide show
  1. FIELD_NOTES.md +2 -0
  2. README.md +2 -0
  3. app.py +18 -0
  4. scripts/verify.py +22 -0
FIELD_NOTES.md CHANGED
@@ -57,6 +57,8 @@ The app preloads image descriptions through `/api/image-descriptions`, caches th
57
 
58
  Kokoro is still the intended model-backed TTS path. For local resilience, the frontend can use the browser speech engine when Kokoro is unavailable, and the session panel labels that as a browser fallback instead of model audio.
59
 
 
 
60
  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`.
61
 
62
  Auto-advance is intentionally opt-in. This keeps the default reader mode user-controlled while still supporting a continuous-reading demo path.
 
57
 
58
  Kokoro is still the intended model-backed TTS path. For local resilience, the frontend can use the browser speech engine when Kokoro is unavailable, and the session panel labels that as a browser fallback instead of model audio.
59
 
60
+ Generated speech outputs are capped by a small retention cleanup. That keeps repeated demo sessions from turning the Space output directory into hidden state.
61
+
62
  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`.
63
 
64
  Auto-advance is intentionally opt-in. This keeps the default reader mode user-controlled while still supporting a continuous-reading demo path.
README.md CHANGED
@@ -100,6 +100,8 @@ Image descriptions are preloaded into a local cache and written into the page's
100
 
101
  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.
102
 
 
 
103
  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.
104
 
105
  Auto-advance is available as an opt-in reader control. It stays off by default so users keep manual control unless they choose continuous reading.
 
100
 
101
  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.
102
 
103
+ Generated speech files are pruned automatically so repeated demos do not grow the Space's `outputs` directory without bound.
104
+
105
  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.
106
 
107
  Auto-advance is available as an opt-in reader control. It stays off by default so users keep manual control unless they choose continuous reading.
app.py CHANGED
@@ -558,6 +558,22 @@ def _silent_wav(path: Path, seconds: float = 0.35, sample_rate: int = 24000) ->
558
  wav.writeframes(b"\x00\x00" * frames)
559
 
560
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
561
  def speak_core(text: str, voice: str, speed: float) -> dict[str, Any]:
562
  start = time.perf_counter()
563
  try:
@@ -577,6 +593,8 @@ def speak_core(text: str, voice: str, speed: float) -> dict[str, Any]:
577
  runtime = "fallback"
578
  warning = f"Kokoro unavailable: {exc.__class__.__name__}"
579
 
 
 
580
  return {
581
  "ok": True,
582
  "runtime": runtime,
 
558
  wav.writeframes(b"\x00\x00" * frames)
559
 
560
 
561
+ def _prune_speech_outputs(keep_path: Path, max_files: int = 24) -> None:
562
+ speech_files = sorted(
563
+ OUTPUT_DIR.glob("speech*.wav"),
564
+ key=lambda path: path.stat().st_mtime,
565
+ reverse=True,
566
+ )
567
+ keep_resolved = keep_path.resolve()
568
+ for old_path in speech_files[max_files:]:
569
+ if old_path.resolve() == keep_resolved:
570
+ continue
571
+ try:
572
+ old_path.unlink()
573
+ except OSError:
574
+ pass
575
+
576
+
577
  def speak_core(text: str, voice: str, speed: float) -> dict[str, Any]:
578
  start = time.perf_counter()
579
  try:
 
593
  runtime = "fallback"
594
  warning = f"Kokoro unavailable: {exc.__class__.__name__}"
595
 
596
+ _prune_speech_outputs(output_path)
597
+
598
  return {
599
  "ok": True,
600
  "runtime": runtime,
scripts/verify.py CHANGED
@@ -139,6 +139,27 @@ def verify_core_fallbacks() -> None:
139
  assert_true(isinstance(generated["elapsed_ms"], int), "Image generation should include elapsed_ms")
140
 
141
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  def verify_routes() -> None:
143
  client = TestClient(app.app)
144
 
@@ -308,6 +329,7 @@ def main() -> None:
308
  verify_static_assets()
309
  verify_space_metadata()
310
  verify_core_fallbacks()
 
311
  verify_routes()
312
  print("Tiny Narrator verification passed.")
313
 
 
139
  assert_true(isinstance(generated["elapsed_ms"], int), "Image generation should include elapsed_ms")
140
 
141
 
142
+ def verify_output_retention() -> None:
143
+ keep_path = app.OUTPUT_DIR / "speech-retention-keep.wav"
144
+ _ = app.speak_core("Tiny Narrator retention check.", voice="af_heart", speed=1.0)
145
+ keep_path.write_bytes(b"keep")
146
+ stale_files = []
147
+ for index in range(30):
148
+ stale_path = app.OUTPUT_DIR / f"speech-retention-stale-{index:02d}.wav"
149
+ stale_path.write_bytes(b"stale")
150
+ stale_files.append(stale_path)
151
+
152
+ app._prune_speech_outputs(keep_path, max_files=4)
153
+
154
+ remaining = list(app.OUTPUT_DIR.glob("speech*.wav"))
155
+ assert_true(keep_path.exists(), "Speech retention should keep the active output")
156
+ assert_true(len(remaining) <= 5, "Speech retention should prune stale generated audio")
157
+
158
+ keep_path.unlink(missing_ok=True)
159
+ for stale_path in stale_files:
160
+ stale_path.unlink(missing_ok=True)
161
+
162
+
163
  def verify_routes() -> None:
164
  client = TestClient(app.app)
165
 
 
329
  verify_static_assets()
330
  verify_space_metadata()
331
  verify_core_fallbacks()
332
+ verify_output_retention()
333
  verify_routes()
334
  print("Tiny Narrator verification passed.")
335