Skip to content

feat(cli): let transcribe --to write srt or vtt straight from a media file - #5215

Merged
miguel-heygen merged 12 commits into
mainfrom
feat/cli-transcribe-media-to-captions
Oct 9, 2026
Merged

miguel-heygen merged 12 commits into
mainfrom
feat/cli-transcribe-media-to-captions

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

What changed

hyperframes transcribe video.mp4 --to srt used to stop with "--to can only export from transcript files. Run transcribe first.", so getting captions from a recording took two commands. Now --to srt or --to vtt on an audio or video file transcribes it and writes the sidecar in the same run, next to transcript.json (or at --output).

  • Transcripts keep working as before: --to on a .json, .srt or .vtt still exports instead of importing, with the same cue rules.
  • Both paths share one sidecar writer, so cue grouping, --preserve-cues and --output behave the same.
  • Under --json the run still prints one result line; it gains format and outputPath when a sidecar was written.
  • A bad --output fails at once, before any decoding, so a typo does not cost a long transcription. That covers a missing folder ("Output folder not found") and an --output that is a folder or ends in a slash ("--output is a folder", naming a file path to use instead). A caption path that would land on the input file or on the transcript.json the run writes is refused too ("The caption file would overwrite ..."). The check compares files, not path text: right before the caption write, by when the transcript exists, a destination that is the same file as the input or transcript.json is refused, so the operating system resolves every link, relative link, folder alias (/var and /private/var) and letter case. An early check refuses the common cases before transcribing. When a disk reports inode 0 (some network shares), existing files are compared by their fully resolved paths instead, so links are still caught and two different files are never called the same. It also covers the default path, so transcribe transcript.srt --to srt no longer rewrites its own input. A trailing -o with no path fails ("--output needs a file path").
  • If the caption file cannot be written after transcribing, the run exits 1 and says so: "Transcript saved to ..., but the caption file ... could not be written: ", instead of reporting a failed transcription.
  • Transcribed words are treated as word-level when grouping cues, rather than guessed from spacing. For Chinese or Japanese this gives readable caption lines instead of one cue per recognised word. Exporting the saved transcript.json later still guesses from spacing, so it may split those cues differently; the docs say so.
  • Audio with no speech succeeds with 0 words as before, and with --to writes an empty sidecar (an empty SRT, a header-only VTT). Exporting an empty transcript file still fails with "No words found".
  • With --optional, a run skipped because whisper-cpp is unavailable writes no sidecar and exits 0. Before, --to on media always failed with exit 1.
  • The CLI docs and --help examples show the new form.

Validation

  • New tests: a media file with --to vtt --json writes transcript.json, a VTT that matches the formatter's output, and exactly one stdout result naming both; a missing --output folder fails before transcription starts; silent audio with --to srt writes an empty sidecar. The first two fail on the previous code. New tests also cover an --output that is a folder, ends in a slash or sits under a file, an --output that names the input or transcript.json directly, through a link, through a linked folder, by its real path or in other letter case, an export whose default path is its own input, a trailing -o, a caption file that cannot be written after transcribing, and CJK grouping. Each new test fails when its fix is removed. Links to the transcript the run is about to create (direct, and relative inside a linked folder) keep the transcript and report that the caption file could not be written. All 63 transcribe tests pass three runs in a row. On macOS, the built-from-source CLI refuses each alias (case variant, /private path, link, trailing -o, export onto its own input) and leaves the files untouched.
  • End to end with the built CLI and the real Parakeet engine on a 3.5 s generated speech clip: transcribe hello.wav --to srt --json exits 0, prints one result line, and writes three correct cues ("Write HTML.", "Render video.", "Built for agents.").
  • transcript.json is named by one constant, used by both engines, the import, init and the check.
  • Format, lint, type check and comment checks pass.

No visible change

CLI only; no Studio or player code changes.

@mintlify

mintlify Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
hyperframes 🟢 Ready View Preview Oct 9, 2026, 9:22 PM

💡 Tip: Enable Automations to automatically generate PRs for you.

@github-actions

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 2061 (base branch 2061), smooth 1582 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

@miguel-heygen
miguel-heygen marked this pull request as ready for review October 8, 2026 15:36
…transcript

- Refuse an --output that resolves to the input file or the transcript.json the run writes,
  before the engine starts.
- When the caption file cannot be written after transcribing, say the transcript was saved
  and name the caption path and reason, instead of reporting a failed transcription.
- Docs: a one-run export groups cues for languages without spaces; exporting the saved
  transcript later may split them differently.
- The overwrite check compares files, not path text: an existing target is matched by device
  and inode, a new one by its real folder and its name ignoring letter case. Links, folder
  aliases such as /var and /private/var, and other letter case are all refused.
- The check also covers the default caption path, so exporting transcript.srt to srt no
  longer rewrites its own input.
- A trailing -o with no path fails before transcribing.
- One constant names transcript.json for the engines, the import and the check.
- A caption path that is a link is followed to where the write would land, so a link to the
  transcript.json the run is about to create is refused before transcribing.
- A test pins the folder letter-case check on a case-insensitive disk, and says which branch ran.
…writing it

- Right before the caption write, by when the transcript exists, a destination that is the
  same file as the input or transcript.json is refused. The OS resolves every link, relative
  link and alias, so the hand-written link following is gone.
- The run keeps the transcript and says the caption file could not be written and why.
  An export whose caption write fails now says so too, instead of crashing.
- The check before transcribing stays as an early refusal for the common cases.
- Some network shares report inode 0 for every file; such a pair is compared by folder and
  name instead of being called the same file. The check before transcribing and the one at
  write time now share one comparison.
- The link-through-a-file tests accept ENOENT as well as ENOTDIR, the code Windows reports.
- When either file reports inode 0 and both exist, compare their fully resolved real paths,
  so a link to the media or the transcript is still refused. The folder and name compare
  stays for a path that does not exist yet.
- Tests zero the inode on every path, on the caption path only, or on the protected file only.

@somanshreddy somanshreddy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving at 5242dcbb. I found no blockers. It merges cleanly with main.

Checked:

  • Flow: --to on media now transcribes, writes transcript.json, then writes the sidecar.
    • Import and export share writeCaptionSidecar.
    • Media words are passed as word-level (phraseLevelSource=false), so CJK grouping follows the transcriber, not the spacing heuristic.
    • --optional skips stay exit 0 without a sidecar.
  • No-overwrite guard: two layers, both on the path that writes.
    • An early overwriteProblem refuses before decoding. If a file doesn't exist yet, it compares the real parent folder plus the basename, case-insensitively.
    • The write-time sameFile runs after transcript.json exists, so the OS resolves links and aliases by dev/ino, with a realpath fallback when a share reports inode 0.
    • The default export path onto its own input (transcript.srt --to srt) is now refused too.
  • Failure after transcription: exits 1 with the "Transcript saved…, but the caption file … could not be written" message, through setCommandExitCode, so cli.ts still owns the exit code.

Tests: transcribe.test.ts passes 63/63. Mutants: 7/7 caught:

  • write-time check removed;
  • early check removed;
  • inode-0 fallback forced false;
  • case-sensitive basename;
  • media treated as phrase-level;
  • exit code not set;
  • trailing-slash --output check removed.

CI: no required check is red (4 pass, 4 pending).

Nit: when the destination doesn't exist yet, the basename comparison is case-insensitive on every OS. On a case-sensitive Linux disk, -o Transcript.json beside the run's transcript.json is refused even though they would be different files. That fails the safe way and it's a corner case, so it's fine as is. Limiting it to case-insensitive volumes would just be a refinement.

@miguel-heygen
miguel-heygen added this pull request to the merge queue Oct 9, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 9, 2026
@miguel-heygen
miguel-heygen added this pull request to the merge queue Oct 9, 2026
Merged via the queue into main with commit 908f439 Oct 9, 2026
83 checks passed
@miguel-heygen
miguel-heygen deleted the feat/cli-transcribe-media-to-captions branch October 9, 2026 23:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants