Files
UmbraAudio/scripts/reattribute_all.py
B.D. Murphy c86e323d7a Initial commit: UmbraAudio audiobook generation program
Quality-first multi-pass audiobook generation from English manuscripts.

Includes:
- umbraaudio/ core package (pipeline, config, voices, web review UI)
- scripts/ CLI utilities + model download script
- tts_server/ remote TTS server
- specs/ design docs, tests/, pyproject.toml + uv.lock
- Qwen3-TTS git submodule (pinned 022e286) with local demo.py UI patch
  saved at scripts/qwen3_tts_demo_umbra_patch.patch
- .env with runtime config/secrets

Excluded (see .gitignore): models/ (downloaded via scripts/download_models.sh),
output/, logs/, .venv, .umbra runtime data, and book projects
(DealHunterShip_*), which are committed separately.
2026-08-30 07:58:24 -07:00

148 lines
4.2 KiB
Python

#!/usr/bin/env python3
"""Re-run LLM speaker attribution on existing utterances without re-segmenting.
Preserves utterance IDs and mechanical segmentation; only updates speaker_name.
"""
from __future__ import annotations
import sqlite3
import sys
import time
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from umbraaudio.analysis.llm_attributor import LLMAttributor
from umbraaudio.config import AppConfig, LLMConfig
from umbraaudio.voices.model_coordinator import ModelCoordinator, PipelineStage
def main():
import argparse
parser = argparse.ArgumentParser()
parser.add_argument(
"--project",
type=str,
default=None,
help="Project name or path",
)
parser.add_argument(
"--chapters",
type=str,
default=None,
help="Comma-separated chapter IDs (e.g. '5,6,7')",
)
parser.add_argument(
"--model",
type=str,
default="google/gemma-4-e4b",
help="LLM model name",
)
parser.add_argument(
"--sleep",
type=float,
default=1.0,
help="Seconds to sleep between LLM batches",
)
args = parser.parse_args()
from umbraaudio.config import load_config
cfg = load_config(args.project)
config = cfg.llm
coordinator = ModelCoordinator(cfg)
if not coordinator.switch_to(
PipelineStage.CHAPTER_ANALYSIS, lm_model=args.model
).get("ok"):
print("Failed to switch models to CHAPTER_ANALYSIS stage")
sys.exit(1)
db_path = cfg.db_path
if not db_path.exists():
print(f"Database not found: {db_path}")
return
db = sqlite3.connect(str(db_path))
db.row_factory = sqlite3.Row
if args.chapters:
chapter_ids = [int(x.strip()) for x in args.chapters.split(",")]
placeholders = ",".join("?" * len(chapter_ids))
chapters = db.execute(
f"SELECT id, title, chapter_index FROM chapters "
f"WHERE id IN ({placeholders}) ORDER BY chapter_index",
chapter_ids,
).fetchall()
else:
chapters = db.execute(
"SELECT id, title, chapter_index FROM chapters ORDER BY chapter_index"
).fetchall()
print(f"Found {len(chapters)} chapters to attribute")
print(f"Model: {args.model}")
attributor = LLMAttributor(config, model=args.model, data_dir=cfg.data_dir)
total_dialogue = 0
total_narrator_left = 0
for ch in chapters:
ch_id = ch["id"]
title = ch["title"] or f"Chapter {ch['chapter_index']}"
utts = db.execute(
"SELECT id, utterance_index, text, type, speaker_name "
"FROM utterances WHERE chapter_id=? ORDER BY utterance_index",
(ch_id,),
).fetchall()
if not utts:
print(f" {title}: no utterances, skipping")
continue
utt_list = [dict(u) for u in utts]
dialogue_before = sum(1 for u in utt_list if u.get("type") == "dialogue")
narrator_before = sum(
1
for u in utt_list
if u.get("type") == "dialogue"
and (u.get("speaker_name") or "narrator").lower() == "narrator"
)
attributed = attributor.attribute_speakers(utt_list, title, ch_id)
time.sleep(0.5)
for u in attributed:
db.execute(
"UPDATE utterances SET speaker_name=? WHERE id=?",
(u.get("speaker_name", "Narrator"), u["id"]),
)
db.commit()
narrator_after = sum(
1
for u in attributed
if u.get("type") == "dialogue"
and (u.get("speaker_name") or "narrator").lower() == "narrator"
)
total_dialogue += dialogue_before
total_narrator_left += narrator_after
print(
f" {title} (id={ch_id}): dialogue {dialogue_before}, "
f"narrator-left {narrator_before} -> {narrator_after}"
)
db.close()
coordinator.switch_to(PipelineStage.IDLE)
print(
f"\nTotal: {total_dialogue} dialogue utterances, "
f"{total_narrator_left} still attributed to narrator"
)
print("Done!")
if __name__ == "__main__":
main()