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.
72 lines
2.9 KiB
72 lines
2.9 KiB
#!/usr/bin/env python3
|
|
"""Phase 4 Step 3.5: Verify matched blobs have correct voice_id and text content.
|
|
|
|
Usage: python3 scripts/verify_blobs.py <chapter_number>
|
|
|
|
Compares each matched blob's metadata (voice_id, text) against the generation plan.
|
|
Clears blob_path for any mismatched entries so they can be retried.
|
|
"""
|
|
import json, re, os, sys, html
|
|
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
|
|
|
|
plan_path = BOOK_DIR / f"audio/chapter_{CHAPTER_N}/generation_plan.json"
|
|
assets_path = PROJECT_DIR / ".hilo/assets.json"
|
|
|
|
plan = json.loads(plan_path.read_text())
|
|
assets = json.loads(assets_path.read_text())
|
|
|
|
|
|
def clean_text(text):
|
|
# Unescape HTML entities (plan stores <#N#> from HTML-escaped source scripts)
|
|
text = html.unescape(text)
|
|
# Strip pause markers
|
|
text = re.sub(r'<#[\d.]+#>', '', text).strip()
|
|
# Normalize smart/curly quotes to ASCII (TTS metadata normalizes these on storage)
|
|
text = text.replace('\u201c', '"').replace('\u201d', '"')
|
|
text = text.replace('\u2018', "'").replace('\u2019', "'")
|
|
return text
|
|
|
|
|
|
blob_meta = {}
|
|
for a in assets:
|
|
if a.get("type") == "audio" and a.get("blobRef"):
|
|
blob_meta[a["blobRef"]] = {
|
|
"voice_id": a.get("metadata", {}).get("voice_id", ""),
|
|
"text": clean_text(a.get("metadata", {}).get("prompt", "")),
|
|
}
|
|
|
|
errors = []
|
|
for entry in plan:
|
|
if not entry.get("blob_path"):
|
|
continue
|
|
blob_ref = Path(entry["blob_path"]).name
|
|
meta = blob_meta.get(blob_ref)
|
|
if not meta:
|
|
errors.append(f" [{entry['index']}] blob {blob_ref} not found in assets.json")
|
|
continue
|
|
expected_text = clean_text(entry["text"])
|
|
if meta["voice_id"] != entry["voice_id"]:
|
|
errors.append(f" [{entry['index']}] voice_id mismatch: plan={entry['voice_id']}, blob={meta['voice_id']}")
|
|
if meta["text"] != expected_text:
|
|
errors.append(f" [{entry['index']}] text mismatch:\n plan: {expected_text[:80]}\n blob: {meta['text'][:80]}")
|
|
|
|
if errors:
|
|
print(f"VERIFICATION FAILED: {len(errors)} mismatches found")
|
|
for e in errors:
|
|
print(e)
|
|
for entry in plan:
|
|
blob_ref = Path(entry.get("blob_path", "")).name if entry.get("blob_path") else ""
|
|
meta = blob_meta.get(blob_ref)
|
|
if meta and (meta["voice_id"] != entry["voice_id"] or meta["text"] != clean_text(entry["text"])):
|
|
entry["blob_path"] = None
|
|
with open(plan_path, 'w', encoding='utf-8') as f:
|
|
json.dump(plan, f, ensure_ascii=False, indent=2)
|
|
print("Mismatched entries cleared -- run Failure & Retry")
|
|
else:
|
|
print(f"Verification passed: all {len([e for e in plan if e.get('blob_path')])} blobs correct. Proceed to Step 4.")
|
|
|