"""Stage 4, narration script. One JSON file per audio file, Audible style: opening-credits, chapter-NN (one chapter per file), closing-credits, retail-sample. A script is a list of segments {kind, added, text}. Author text is verbatim (added=false). Everything else is added=true. Each inserted part (preview, summary) is wrapped: cue chime-start, label, text, cue chime-end.""" import json import os import bookio import llm import structure LABELS = {"chapter_preview": "Chapter preview", "chapter_summary": "Chapter summary", "concept_summary": "Concept summary"} PROMPTS = { "chapter_preview": "In two sentences of plain spoken English, say what this chapter is about. Use only what the text says.", "chapter_summary": "In three sentences of plain spoken English, summarize this chapter. Use only what the text says.", "concept_summary": "In two sentences of plain spoken English, restate the main concept in this passage. Use only what the text says.", } def switch(cfg, book, chapter, name): """Chapter switch beats book switch beats settings default.""" for source in (chapter.get("switches", {}), book.get("summaries", {})): if source.get(name) is not None: return bool(source[name]) return bool(cfg["summaries"][name]) def seg(kind, text, added=True): return {"kind": kind, "added": added, "text": text} def inserted(kind, text): return [seg("cue", "chime-start"), seg("label", LABELS[kind]), seg(kind, text), seg("cue", "chime-end")] def spoken_header(ch): n = ch["number"] word = structure.WORDS[n - 1].title() if n <= len(structure.WORDS) else str(n) return "Chapter %s. %s." % (word, ch["title"]) def credits(book, cfg, closing): by = "written by %s" % book["author"] if cfg.get("narrator"): by += ", narrated by %s" % cfg["narrator"] return [seg("credits", ("This is the end of %s, %s." if closing else "This is %s, %s.") % (book["title"], by))] def chapter_script(cfg, book, ch, paragraphs): segs = [seg("header", spoken_header(ch))] full = "\n\n".join(paragraphs) if switch(cfg, book, ch, "chapter_preview"): segs += inserted("chapter_preview", llm.complete(cfg, PROMPTS["chapter_preview"], full)) mid = None if switch(cfg, book, ch, "concept_summary"): total, running = sum(len(p.split()) for p in paragraphs), 0 for i, p in enumerate(paragraphs): running += len(p.split()) if running * 2 >= total: mid = i break for i, p in enumerate(paragraphs): segs.append(seg("author", p, added=False)) if i == mid: segs += inserted("concept_summary", llm.complete(cfg, PROMPTS["concept_summary"], "\n\n".join(paragraphs[:i + 1]))) if switch(cfg, book, ch, "chapter_summary"): segs += inserted("chapter_summary", llm.complete(cfg, PROMPTS["chapter_summary"], full)) return segs def retail_sample(cfg, ch, paragraphs): budget, segs, words = cfg["script"]["retail_sample_words"], [seg("header", spoken_header(ch))], 0 for p in paragraphs: n = len(p.split()) if segs[1:] and words + n > budget: break segs.append(seg("author", p, added=False)) words += n return segs def write(book_dir, name, segs): os.makedirs(os.path.join(book_dir, "script"), exist_ok=True) with open(os.path.join(book_dir, "script", name + ".json"), "w", encoding="utf-8") as f: json.dump(segs, f, indent=2, ensure_ascii=False) f.write("\n") def run(cfg, book_dir, book, chapter_numbers): """Writes script/*.json and the audio file list in book.json. Returns warnings.""" files = [("opening-credits", credits(book, cfg, False))] chosen = [c for c in book["chapters"] if c["number"] in chapter_numbers] for ch in chosen: with open(os.path.join(book_dir, ch["text_file"]), encoding="utf-8") as f: paragraphs = [p for p in f.read().split("\n\n") if p.strip()] files.append(("chapter-%02d" % ch["number"], chapter_script(cfg, book, ch, [p.strip() for p in paragraphs]))) if ch is chosen[0]: sample = ("retail-sample", retail_sample(cfg, ch, [p.strip() for p in paragraphs])) files += [("closing-credits", credits(book, cfg, True)), sample] for name, segs in files: write(book_dir, name, segs) book["audio_files"] = [n for n, _ in files] return []