mattrguerra/transcription-pipelinemainPrivate on GitHub, published here in full

cli.py

Language Python5.3 KB132 lines

transcription_pipeline/cli.py

  1. """transcribe: the command line.
  2. transcribe FILE [FILE ...] transcribe files, outputs beside each one (or --out)
  3. transcribe inbox process everything waiting in the watch folder
  4. Engine: local by default (faster-whisper, nothing leaves the machine), or
  5. --engine deepinfra for DeepInfra's hosted Whisper, which uploads the audio.
  6. """
  7. from __future__ import annotations
  8. import argparse
  9. import os
  10. import sys
  11. from pathlib import Path
  12. from . import __version__
  13. from .render import FORMATS
  14. def _formats(value: str) -> tuple[str, ...]:
  15. out = tuple(f.strip().lower().lstrip(".") for f in value.split(",") if f.strip())
  16. bad = [f for f in out if f not in FORMATS]
  17. if bad or not out:
  18. raise argparse.ArgumentTypeError(f"formats must be from {', '.join(FORMATS)}")
  19. return out
  20. def _env_path(name: str) -> Path | None:
  21. v = os.environ.get(name, "").strip()
  22. return Path(v).expanduser() if v else None
  23. def build_parser() -> argparse.ArgumentParser:
  24. p = argparse.ArgumentParser(prog="transcribe", description=__doc__,
  25. formatter_class=argparse.RawDescriptionHelpFormatter)
  26. p.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
  27. p.add_argument("targets", nargs="+", metavar="FILE|inbox",
  28. help="audio or video files, or the word 'inbox'")
  29. p.add_argument("--engine", choices=("local", "deepinfra"),
  30. default=os.environ.get("TP_ENGINE", "local"),
  31. help="local (default) or deepinfra (hosted; uploads audio)")
  32. p.add_argument("--language", default=os.environ.get("TP_LANGUAGE") or None,
  33. help="language code such as en or es (default: detect)")
  34. p.add_argument("--no-diarize", action="store_true", help="skip speaker separation")
  35. p.add_argument("--speakers", type=int, metavar="N",
  36. help="exact number of speakers, when you know it")
  37. p.add_argument("--min-speakers", type=int, metavar="N")
  38. p.add_argument("--max-speakers", type=int, metavar="N")
  39. p.add_argument("--formats", type=_formats,
  40. default=_formats(os.environ.get("TP_FORMATS", "txt,md,json")),
  41. help="comma list from txt,md,json,srt,vtt (default txt,md,json)")
  42. p.add_argument("--no-timestamps", action="store_true", help="plain .txt without [HH:MM:SS]")
  43. p.add_argument("--out", type=Path, help="output folder for files (default: beside each file)")
  44. p.add_argument("--root", type=Path, default=_env_path("TP_ROOT"),
  45. help="watch-folder root holding inbox/ transcripts/ done/ failed/ (env TP_ROOT)")
  46. p.add_argument("--vault-inbox", type=Path, default=_env_path("TP_VAULT_INBOX"),
  47. help="also file the .md transcript here (env TP_VAULT_INBOX)")
  48. p.add_argument("--settle", type=float, default=5.0,
  49. help="seconds a file's size must hold still before it is taken (inbox)")
  50. return p
  51. def options_from(args):
  52. from .pipeline import Options
  53. lo, hi = args.min_speakers, args.max_speakers
  54. if args.speakers:
  55. lo = hi = args.speakers
  56. return Options(engine=args.engine, language=args.language, diarize=not args.no_diarize,
  57. min_speakers=lo, max_speakers=hi, formats=args.formats,
  58. timestamps=not args.no_timestamps)
  59. # pyannote warns at import that torchcodec cannot find FFmpeg. The pipeline
  60. # never uses it (audio reaches pyannote already decoded), so it is only noise.
  61. # The message starts with a line break, hence the leading \s*.
  62. QUIET = [("ignore", r"\s*torchcodec is not installed correctly")]
  63. def main(argv: list[str] | None = None) -> int:
  64. import warnings
  65. for action, message in QUIET:
  66. warnings.filterwarnings(action, message=message)
  67. args = build_parser().parse_args(argv)
  68. from . import steps
  69. from .pipeline import process
  70. opts = options_from(args)
  71. try:
  72. transcriber = steps.transcriber(opts)
  73. aligner = steps.aligner(opts)
  74. diarizer = steps.diarizer(opts)
  75. except Exception as e:
  76. print(f"transcribe: {e}", file=sys.stderr)
  77. return 2
  78. def handle(src: Path, out_dir: Path):
  79. return process(src, out_dir, opts, transcriber, aligner, diarizer,
  80. vault_inbox=args.vault_inbox)
  81. if args.targets == ["inbox"]:
  82. if args.root is None:
  83. print("transcribe inbox: set --root or TP_ROOT to the watch-folder root.",
  84. file=sys.stderr)
  85. return 2
  86. from .inbox import Layout, run
  87. layout = Layout(args.root.expanduser())
  88. report = run(layout, lambda src: handle(src, layout.transcripts), settle=args.settle)
  89. return 1 if report["failed"] else 0
  90. failed = 0
  91. for t in args.targets:
  92. src = Path(t).expanduser()
  93. if not src.is_file():
  94. print(f"{src}: not a file", file=sys.stderr)
  95. failed += 1
  96. continue
  97. print(f"{src.name} ({opts.engine})")
  98. try:
  99. res = handle(src, args.out or src.parent)
  100. except Exception as e:
  101. print(f" FAILED: {e}", file=sys.stderr)
  102. failed += 1
  103. continue
  104. print(f" {res.sentences} sentences, {len(res.speakers)} speakers")
  105. for p in res.outputs.values():
  106. print(f" -> {p}")
  107. return 1 if failed else 0
  108. if __name__ == "__main__":
  109. raise SystemExit(main())