Add pipeline code
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
"""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 []
|
||||
Reference in New Issue
Block a user