Spaces:
Running
Running
File size: 8,004 Bytes
3bf43cb 68f1146 3bf43cb c1d045b 3bf43cb c1d045b 34aa951 c1d045b 3bf43cb c1d045b 3bf43cb 8c6b0a2 3bf43cb 68f1146 3bf43cb 68f1146 34aa951 3bf43cb 34aa951 3bf43cb 34aa951 3bf43cb c1d045b 8c6b0a2 3bf43cb 3861ad6 3bf43cb 162cea3 3861ad6 3bf43cb | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | """Prepare a released interview end to end: video parts -> transcript -> brief -> back-test.
Run on a GPU machine (Colab, a lab workstation, or a GPU Space). Partner media
and catalogue records stay under private/ and are never committed.
python scripts/prepare_interview.py \
--id RG-50.030.0001 \
--title "Oral history interview with ..." \
--media private/RG-50.030.0001/media/*.mp4 \
--catalogue private/RG-50.030.0001/catalogue.json \
--donor private/RG-50.030.0001/donor.json \
--device cuda
Steps
1. media.prepare checks parts (e.g. 01.06 ... 06.06), extracts and joins audio
2. asr.transcribe faster-whisper large-v3, per-segment language ID, flags
3. transcript review writes transcript.txt and STOPS unless --skip-review:
correct speaker roles and obvious errors, then rerun with --transcript
4. pipeline spatio-textual + period gazetteer + agent + validators
5. backtest suggested headings vs the catalogue's own subjects
Outputs go to data/interviews/<id>/ (brief.json, backtest.json). Only commit
them, or deploy them to a public Space, when the museum has confirmed the
interview's release terms allow it.
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
from curatorbot import transcript as tx # noqa: E402
def main() -> int:
p = argparse.ArgumentParser()
p.add_argument("--id", required=True)
p.add_argument("--title", default="")
p.add_argument("--media", nargs="*", default=[], help="Recordings, or folders holding them (searched at any depth)")
p.add_argument("--transcript", help="Reviewed transcript (.txt Q/A or .json); skips transcription")
p.add_argument("--catalogue", help="JSON with 'subjects' (and optionally 'title', 'summary')")
p.add_argument("--donor", help="JSON of donor-reported fields, if any")
p.add_argument("--note", default="", help="Shown above the brief; a default is chosen from --status")
p.add_argument("--model", default="large-v3")
p.add_argument("--device", default="cuda")
p.add_argument("--prompt", default="Auschwitz, Birkenau, Bergen-Belsen, Theresienstadt, Treblinka, Majdanek, Łódź, Litzmannstadt, Lemberg, Judenrat, Kapo, Appell, UNRRA, HIAS")
p.add_argument("--skip-review", action="store_true", help="Analyse the raw ASR transcript without stopping for review")
p.add_argument("--status", choices=["released", "under_review"], default="released",
help="released: catalogued interview, back-tested; under_review: new submission (e.g. from Box)")
p.add_argument("--progress-file", help="Also keep the latest progress here (e.g. in Google Drive, to check from another device)")
p.add_argument("--source-url", help="Public catalogue page for a released interview")
a = p.parse_args()
if not a.note:
a.note = ("Released interview, already catalogued: CuratorBot's suggestions are compared with the catalogue record."
if a.status == "released" else
"Submission under review: not yet catalogued. Suggestions are drafts for the reviewing curator.")
out_dir = ROOT / "data" / "interviews" / a.id
work = ROOT / "work" / a.id
out_dir.mkdir(parents=True, exist_ok=True)
work.mkdir(parents=True, exist_ok=True)
catalogue = json.loads(Path(a.catalogue).read_text(encoding="utf-8")) if a.catalogue else None
donor = json.loads(Path(a.donor).read_text(encoding="utf-8")) if a.donor else {}
media_info = None
from curatorbot.progress import Progress
prog = Progress(a.progress_file or work / "progress.json")
if a.transcript:
src = Path(a.transcript)
if src.suffix == ".json":
d = json.loads(src.read_text(encoding="utf-8"))
segments, media_info = d["segments"], d.get("media")
else:
segments = tx.parse_qa_text(src.read_text(encoding="utf-8"))
side = work / "transcript.json"
if side.exists():
media_info = json.loads(side.read_text(encoding="utf-8")).get("media")
else:
if not a.media:
p.error("Give --media files or a reviewed --transcript.")
from curatorbot.asr import transcribe
from curatorbot.intake import find_media
from curatorbot.media import prepare
files = [f for m in a.media for f in find_media(m)]
if not files:
p.error(f"No recordings found in: {', '.join(a.media)}")
prog.start("Starting", f"{a.id}: {len(files)} recording(s): " + ", ".join(f.name for f in files))
prep = prepare(files, workdir=work, progress=prog)
for issue in prep["issues"]:
print("WARNING:", issue)
res = transcribe(prep["audio"], size=a.model, device=a.device, initial_prompt=a.prompt, progress=prog)
segments = tx.from_asr(res["chunks"])
media_info = {
"parts": [{"part": x["part"], "duration": x["duration"], "audio": x.get("audio_codec") or ""} for x in prep["parts"]],
"duration": prep["duration"], "issues": prep["issues"], "simulated": False,
"languages": _language_spans(res["chunks"], prep["duration"]),
"flags": [{"at": tx.fmt_time(f["start"]), "text": f["reason"]} for f in res["flags"]][:50],
}
(work / "transcript.json").write_text(json.dumps({"media": media_info, "segments": segments}, ensure_ascii=False, indent=1), encoding="utf-8")
(work / "transcript.txt").write_text(tx.to_qa_text(segments), encoding="utf-8")
prog.start("Done", f"transcript {work / 'transcript.txt'}: {len(segments)} turns, {prep['duration']} of audio, "
f"{len(res['flags'])} listening flag(s)")
if not a.skip_review:
print("Review speaker roles and errors, then rerun with --transcript", work / "transcript.txt")
return 0
from curatorbot.pipeline import build_brief
brief = build_brief(segments, interview_id=a.id, title=a.title or (catalogue or {}).get("title") or a.id,
donor=donor, note=a.note, media=media_info, status=a.status,
source={"url": a.source_url or (catalogue or {}).get("url", "")}, progress=prog)
(out_dir / "brief.json").write_text(json.dumps(brief, ensure_ascii=False), encoding="utf-8")
print("Brief:", out_dir / "brief.json", brief["agent"].get("validation"))
print("Places:", brief.get("placeCoverage"))
if catalogue:
from curatorbot.backtest import score_brief
bt = score_brief(brief, catalogue)
(out_dir / "backtest.json").write_text(json.dumps(bt, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"Back-test: recall {bt['recall_exact']:.0%} exact, {bt['recall_head']:.0%} by main heading; "
f"{len(bt['missed'])} catalogue headings missed.")
print(f"Model only: precision@10 {bt['precision_exact']:.0%}; R-precision {bt['model_r_precision']:.0%} "
f"(top {len(catalogue.get('subjects', []))} of {bt['model_suggested_all']} suggestions).")
w = bt["with_rules"]
print(f"With form headings by rule: R-precision {w['r_precision']:.0%} (top {w['r']}), "
f"all {w['suggested']} suggestions: precision {w['precision_exact']:.0%}, recall {w['recall_exact']:.0%} exact.")
return 0
def _language_spans(chunks, duration):
spans = []
for c in chunks:
lang = c.get("lang") or "en"
if spans and spans[-1]["code"] == lang:
spans[-1]["to"] = tx.fmt_time(c["end"])
else:
spans.append({"lang": lang, "code": lang, "from": tx.fmt_time(c["start"]), "to": tx.fmt_time(c["end"])})
if spans:
spans[0]["from"], spans[-1]["to"] = "00:00:00", duration
return spans
if __name__ == "__main__":
raise SystemExit(main())
|