Popiai-skill仓库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

83 lines
3.1 KiB

#!/usr/bin/env python3
"""Phase 4.6: Generate assembly report markdown with timestamps and segment details.
Usage: python3 scripts/generate_assembly_report.py <chapter_number>
Requires ffprobe to be available for duration extraction.
"""
import json, subprocess, re, os, sys
from pathlib import Path
PROJECT_DIR = Path(os.getcwd())
AUDIOBOOK_ROOT = PROJECT_DIR / ".audiobook"
BOOK_DIR = next(d for d in sorted(AUDIOBOOK_ROOT.iterdir()) if d.is_dir() and (d / ".audiobook-state.json").exists())
CHAPTER_N = int(sys.argv[1]) if len(sys.argv) > 1 else 1
state = json.loads((BOOK_DIR / ".audiobook-state.json").read_text())
book_name = state.get("bookName", BOOK_DIR.name)
assets = json.loads((PROJECT_DIR / ".hilo/assets.json").read_text())
plan = json.loads((BOOK_DIR / f"audio/chapter_{CHAPTER_N}/generation_plan.json").read_text())
ordered = sorted(plan, key=lambda x: x["index"])
blob_to_local = {e["blobRef"]: e["name"] for e in assets if e.get("type") == "audio" and e.get("blobRef") and e.get("name")}
def clean_text(t):
t = re.sub(r'<#[\d.]+#>', '', t)
return re.sub(r' +', ' ', t).strip()
def fmt(s):
h = int(s // 3600)
m = int((s % 3600) // 60)
return f"{h:02d}:{m:02d}:{s % 60:05.2f}"
rows, cursor = [], 0.0
for entry in ordered:
blob_ref = Path(entry["blob_path"]).name
dur = float(subprocess.run(
["ffprobe", "-v", "error", "-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1", entry["blob_path"]],
capture_output=True, text=True).stdout.strip())
start, end = cursor, cursor + dur
cursor = end
rows.append({
"index": entry["index"],
"start": fmt(start),
"end": fmt(end),
"voice_id": entry["voice_id"],
"local_file": blob_to_local.get(blob_ref, blob_ref),
"text": clean_text(entry["text"])
})
# Detect language
lang = state.get("lang", "")
if not lang:
_all_text = " ".join(r["text"] for r in rows)
lang = "zh" if re.search(r'[\u4e00-\u9fff]', _all_text) else "en"
if lang == "zh":
tip1 = "> 如需修改某段,请在对话框中注明 **# 编号** 和修改意见。"
tip2 = "> Local File 列即项目根目录下的实际文件名,与画布展示一致。"
else:
tip1 = "> To request a fix, mention the segment **# number** and your change in the chat."
tip2 = "> The **Local File** column shows the actual filename in the project root, matching canvas."
lines = [
f"# {book_name} -- Chapter {CHAPTER_N} Audio Assembly", "",
f"**Total duration:** {rows[-1]['end']} ",
f"**Segments:** {len(rows)} ", "",
tip1, tip2, "",
"| # | Start | End | Voice ID | Local File | Text |",
"|--:|-------|-----|----------|------------|------|",
]
for r in rows:
text = r["text"].replace("|", "\\|").replace("\n", " ")
lines.append(f"| {r['index']} | `{r['start']}` | `{r['end']}` | `{r['voice_id']}` | `{r['local_file']}` | {text} |")
report_name = f"{book_name}_chapter_{CHAPTER_N}_assembly.md"
Path(PROJECT_DIR / report_name).write_text("\n".join(lines), encoding="utf-8")
print(f"Assembly report written: {PROJECT_DIR / report_name}")