feat(gamebook): text narration + --only + bump
Validate Zacus refactor / validate (pull_request) Failing after 8m11s
Repo State / repo-state (pull_request) Failing after 13m35s
Repo State / repo-state (push) Failing after 14m50s
Validate Zacus refactor / validate (push) Failing after 20m57s

--narrate-text makes WAV narration the passage text alone (the BOX-3
shows choices as buttons, no need to speak them). --only builds a
subset incrementally; library.json always lists ALL books so it never
drops tiles. Bumps ESP32_ZACUS for BOX-3 audio.
This commit was merged in pull request #189.
This commit is contained in:
clement
2026-06-20 01:04:41 +02:00
parent e478454070
commit 0c1802285a
2 changed files with 22 additions and 8 deletions
+21 -7
View File
@@ -115,7 +115,7 @@ def narration(text: str, choices: list[dict]) -> str:
def build_book(path: Path, force: bool, dry: bool, ip: str,
json_only: bool = False) -> dict | None:
json_only: bool = False, narrate_text: bool = False) -> dict | None:
"""Synthesise + (optionally) stage one book. Returns its library entry."""
bid = path.stem
book = yaml.safe_load(path.read_text())
@@ -135,7 +135,11 @@ def build_book(path: Path, force: bool, dry: bool, ip: str,
wav = f"{bid}_{pid}.wav"
out = OUT_DIR / wav
if not json_only: # text-only (BOX-3 touch) skips audio
if not say_wav(narration(p["text"], choices), voice, out, force):
# narrate_text: read only the passage text (BOX-3 shows the choices
# as tappable buttons, so they don't need to be spoken).
spoken = (" ".join(p["text"].split()) if narrate_text
else narration(p["text"], choices))
if not say_wav(spoken, voice, out, force):
print(f" ✗ synth {bid}/{pid}"); n_fail += 1; continue
n_ok += 1
manifest["passages"][pid] = {
@@ -169,24 +173,34 @@ def main() -> int:
ap.add_argument("--dry-run", action="store_true", help="synthesise only, no push")
ap.add_argument("--json-only", action="store_true",
help="build/push only the JSON (no WAVs) — for the BOX-3 touch UI")
ap.add_argument("--narrate-text", action="store_true",
help="WAV narration = passage text only (no spoken choices) — for the BOX-3")
ap.add_argument("--only", default="", help="comma-separated book ids to build")
args = ap.parse_args()
books = sorted(p for p in BOOKS_DIR.glob("*.yaml") if p.stem not in EXCLUDE)
if len(books) > MAX_TILES:
print(f"note: {len(books)} books found, library shows first {MAX_TILES}")
books = books[:MAX_TILES]
print(f"library: {len(books)} books -> "
only = {s.strip() for s in args.only.split(",") if s.strip()}
build_books = [p for p in books if not only or p.stem in only]
print(f"library: {len(books)} books (building {len(build_books)}) -> "
f"{'(dry-run)' if args.dry_run else f'{args.ip}:/sdcard/gamebook/'}")
entries, fail = [], 0
for path in books:
e = build_book(path, args.force, args.dry_run, args.ip, args.json_only)
fail = 0
for path in build_books:
e = build_book(path, args.force, args.dry_run, args.ip, args.json_only,
args.narrate_text)
if e is None:
fail += 1
continue
fail += e.pop("_fail")
entries.append(e)
# library.json always lists ALL books (so --only doesn't drop the others'
# tiles): build the entries straight from the YAML titles.
entries = [{"id": p.stem,
"title": fold(yaml.safe_load(p.read_text()).get("title", p.stem))[:40],
"book": f"{p.stem}.json"} for p in books]
library = {"library": entries}
(OUT_DIR / "library.json").write_text(json.dumps(library, ensure_ascii=False, indent=1))
if not args.dry_run: