pipeline.py
Language Python3.6 KB88 lines
"""One recording through the whole pipeline.normalize -> transcribe (local or DeepInfra) -> align (word timestamps)-> diarize (who spoke when) -> sentences and speaker turns -> write outputs.The model-backed steps are passed in as callables, so tests can drive the wholeflow with stand-ins and the CLI wires in the real ones (steps.py)."""from __future__ import annotationsimport datetime as dtimport tempfilefrom dataclasses import dataclassfrom pathlib import Pathfrom typing import Callablefrom . import audio, renderfrom .engines.local import DEFAULT_MODELfrom .filing import write_uniquefrom .shape import rename_speakers, sentences_from_segmentsTranscriber = Callable[[Path, Path, "Options"], dict] # (wav16k, source, opts) -> resultAligner = Callable[[dict, Path, "Options"], dict] # (result, wav16k, opts) -> result with wordsDiarizer = Callable[[dict, Path, "Options"], dict] # (result, wav16k, opts) -> result with speakers@dataclassclass Options:engine: str = "local" # local | deepinfralanguage: str | None = Nonediarize: bool = Truemin_speakers: int | None = Nonemax_speakers: int | None = Noneformats: tuple[str, ...] = ("txt", "md", "json")timestamps: bool = Truemodel: str = DEFAULT_MODEL@dataclassclass Result:outputs: dict[str, Path]speakers: list[str]duration: floatsentences: intlanguage: str | Nonedef process(source: Path, out_dir: Path, opts: Options, transcriber: Transcriber,aligner: Aligner | None, diarizer: Diarizer | None,vault_inbox: Path | None = None) -> Result:source = Path(source)with tempfile.TemporaryDirectory(prefix="tp-") as tmp:wav = audio.to_wav16k(source, Path(tmp) / "audio.wav")duration = audio.wav_duration(wav)result = transcriber(wav, source, opts)if aligner is not None and result.get("segments"):result = aligner(result, wav, opts)if opts.diarize and diarizer is not None and result.get("segments"):result = diarizer(result, wav, opts)sentences, speakers = rename_speakers(sentences_from_segments(result.get("segments", [])))stem = source.stemrecorded = dt.datetime.fromtimestamp(source.stat().st_mtime).date().isoformat() \if source.exists() else dt.date.today().isoformat()meta = {"title": stem, "source": source.name, "recorded": recorded,"transcribed": dt.datetime.now().isoformat(timespec="seconds"),"duration": audio.hms(duration), "engine": opts.engine,"model": result.get("model"), "language": result.get("language"),"speakers": speakers, "diarized": bool(opts.diarize and diarizer is not None)}outputs: dict[str, Path] = {}writers = {"txt": lambda: render.to_txt(sentences, opts.timestamps),"md": lambda: render.to_markdown(sentences, meta),"json": lambda: render.to_json(sentences, result, meta),"srt": lambda: render.to_srt(sentences),"vtt": lambda: render.to_vtt(sentences),}for fmt in opts.formats:if fmt not in render.FORMATS:*rest, last = render.FORMATSraise ValueError(f"Unknown format {fmt!r}: use {', '.join(rest)} or {last}.")outputs[fmt] = write_unique(out_dir, f"{stem}.{fmt}", writers[fmt]())if vault_inbox is not None:outputs["vault"] = write_unique(vault_inbox, f"{stem}.md",render.to_markdown(sentences, meta))return Result(outputs, speakers, duration, len(sentences), result.get("language"))