105 lines
4.3 KiB
Python
105 lines
4.3 KiB
Python
"""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 []
|