feat(gamebook): text narration + --only + bump
--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:
+1
-1
Submodule ESP32_ZACUS updated: 5519ed2f72...fd7af95bcf
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user