cli.py
Language Python5.3 KB132 lines
"""transcribe: the command line.transcribe FILE [FILE ...] transcribe files, outputs beside each one (or --out)transcribe inbox process everything waiting in the watch folderEngine: local by default (faster-whisper, nothing leaves the machine), or--engine deepinfra for DeepInfra's hosted Whisper, which uploads the audio."""from __future__ import annotationsimport argparseimport osimport sysfrom pathlib import Pathfrom . import __version__from .render import FORMATSdef _formats(value: str) -> tuple[str, ...]:out = tuple(f.strip().lower().lstrip(".") for f in value.split(",") if f.strip())bad = [f for f in out if f not in FORMATS]if bad or not out:raise argparse.ArgumentTypeError(f"formats must be from {', '.join(FORMATS)}")return outdef _env_path(name: str) -> Path | None:v = os.environ.get(name, "").strip()return Path(v).expanduser() if v else Nonedef build_parser() -> argparse.ArgumentParser:p = argparse.ArgumentParser(prog="transcribe", description=__doc__,formatter_class=argparse.RawDescriptionHelpFormatter)p.add_argument("--version", action="version", version=f"%(prog)s {__version__}")p.add_argument("targets", nargs="+", metavar="FILE|inbox",help="audio or video files, or the word 'inbox'")p.add_argument("--engine", choices=("local", "deepinfra"),default=os.environ.get("TP_ENGINE", "local"),help="local (default) or deepinfra (hosted; uploads audio)")p.add_argument("--language", default=os.environ.get("TP_LANGUAGE") or None,help="language code such as en or es (default: detect)")p.add_argument("--no-diarize", action="store_true", help="skip speaker separation")p.add_argument("--speakers", type=int, metavar="N",help="exact number of speakers, when you know it")p.add_argument("--min-speakers", type=int, metavar="N")p.add_argument("--max-speakers", type=int, metavar="N")p.add_argument("--formats", type=_formats,default=_formats(os.environ.get("TP_FORMATS", "txt,md,json")),help="comma list from txt,md,json,srt,vtt (default txt,md,json)")p.add_argument("--no-timestamps", action="store_true", help="plain .txt without [HH:MM:SS]")p.add_argument("--out", type=Path, help="output folder for files (default: beside each file)")p.add_argument("--root", type=Path, default=_env_path("TP_ROOT"),help="watch-folder root holding inbox/ transcripts/ done/ failed/ (env TP_ROOT)")p.add_argument("--vault-inbox", type=Path, default=_env_path("TP_VAULT_INBOX"),help="also file the .md transcript here (env TP_VAULT_INBOX)")p.add_argument("--settle", type=float, default=5.0,help="seconds a file's size must hold still before it is taken (inbox)")return pdef options_from(args):from .pipeline import Optionslo, hi = args.min_speakers, args.max_speakersif args.speakers:lo = hi = args.speakersreturn Options(engine=args.engine, language=args.language, diarize=not args.no_diarize,min_speakers=lo, max_speakers=hi, formats=args.formats,timestamps=not args.no_timestamps)# pyannote warns at import that torchcodec cannot find FFmpeg. The pipeline# never uses it (audio reaches pyannote already decoded), so it is only noise.# The message starts with a line break, hence the leading \s*.QUIET = [("ignore", r"\s*torchcodec is not installed correctly")]def main(argv: list[str] | None = None) -> int:import warningsfor action, message in QUIET:warnings.filterwarnings(action, message=message)args = build_parser().parse_args(argv)from . import stepsfrom .pipeline import processopts = options_from(args)try:transcriber = steps.transcriber(opts)aligner = steps.aligner(opts)diarizer = steps.diarizer(opts)except Exception as e:print(f"transcribe: {e}", file=sys.stderr)return 2def handle(src: Path, out_dir: Path):return process(src, out_dir, opts, transcriber, aligner, diarizer,vault_inbox=args.vault_inbox)if args.targets == ["inbox"]:if args.root is None:print("transcribe inbox: set --root or TP_ROOT to the watch-folder root.",file=sys.stderr)return 2from .inbox import Layout, runlayout = Layout(args.root.expanduser())report = run(layout, lambda src: handle(src, layout.transcripts), settle=args.settle)return 1 if report["failed"] else 0failed = 0for t in args.targets:src = Path(t).expanduser()if not src.is_file():print(f"{src}: not a file", file=sys.stderr)failed += 1continueprint(f"{src.name} ({opts.engine})")try:res = handle(src, args.out or src.parent)except Exception as e:print(f" FAILED: {e}", file=sys.stderr)failed += 1continueprint(f" {res.sentences} sentences, {len(res.speakers)} speakers")for p in res.outputs.values():print(f" -> {p}")return 1 if failed else 0if __name__ == "__main__":raise SystemExit(main())