Skip to content

Replace realtime Scribe streaming with post-recording batch transcription - #14

Merged
tadaspetra merged 4 commits into
mainfrom
batch-transcription
Jul 9, 2026
Merged

Replace realtime Scribe streaming with post-recording batch transcription#14
tadaspetra merged 4 commits into
mainfrom
batch-transcription

Conversation

@tadaspetra

@tadaspetra tadaspetra commented Jul 2, 2026

Copy link
Copy Markdown
Owner

What changed

Two commits that together rearchitect transcription end-to-end:

1. Realtime → batch transcription (main process)

  • New main-process transcription-service: extracts audio from the finalized take via ffmpeg stream copy (source file untouched; remux also fixes MediaRecorder's incomplete WebM headers), uploads to ElevenLabs batch Scribe (scribe_v2, word-level timestamps). The API key stays in the main process — the renderer no longer receives a Scribe token.
  • New batch-transcript renderer helper: groups word timestamps into speech segments using the same 1.5s silence threshold the old VAD used.
  • Removed dead realtime code: WebSocket/AudioWorklet/PCM pipeline, audio-processor.ts, scribe-status.ts, scribe token IPC.

2. On-demand "Transcribe & Cut" UI (recording screen overhaul)

  • Recording screen: transcript panel and "keep silences" checkbox removed; recorder failures surface on a compact notice line.
  • Stop flow is instant and offline: finalized files + recovery checkpoint (.pending-recording.json) land on disk, then the take enters the timeline immediately as one full-length section. No network call anywhere in the stop flow.
  • New timeline-toolbar button "Transcribe & Cut": transcribes the selected section's take (fallback: latest take on the timeline) and replaces its sections with speech-cut ones as a single undo step. Failure, timeout, no-speech, or a timeline edit made while transcription was in flight leaves the timeline unchanged with a visible status.
  • System-audio "keep" regions captured during recording are stashed per take (session-scoped) and merged into the cut.
  • Retired settings.autoCutSilences; normalization drops it from existing project files.

Why

The realtime Scribe session was killed server-side during long pauses, silently ending transcription mid-take. Batch transcription of the whole file removes that failure mode; making it an explicit editor action keeps the stop flow instant, keeps footage durability fully offline, and makes cutting a deliberate, reversible choice.

Behavior notes for review

  • Word timestamps map into take time via per-file recorder start offsets (audioStartOffsetMs/cameraStartOffsetMs) instead of the old wall-clock estimate.
  • Takes without mic audio show a message and trigger no network call.
  • System-audio keep-regions do not survive an app restart (session-scoped map); a cut after restart proceeds on mic speech alone.

Tests

  • tests/unit/transcribe-cut.test.ts — source routing, target-take resolution, section replacement (15)
  • tests/unit/batch-transcript.test.ts — segment grouping, offsets, timeout bounds (15)
  • tests/unit/transcription-service.test.ts — ffmpeg args, temp-file cleanup on all failure paths (10)
  • tests/unit/register-handlers.test.tstranscription:transcribe handler (3)
  • project-domain, media-cleanup, scribe-service suites updated

pnpm run check passes (lint, typecheck, 454 unit/integration tests, Electron smoke, packaging smoke). Docs updated: feature-inventory, runbook, target-architecture, .env.example, AGENTS.md.

Residual risk: the real-network Transcribe & Cut path is exercised only manually — suggested spot-check: record a short take with a deliberate 30s pause, confirm it lands on the timeline instantly, then click Transcribe & Cut and verify the cut boundaries and that Cmd+Z restores the full take.

🤖 Generated with Claude Code

tadaspetra and others added 2 commits July 2, 2026 09:07
…tion

The realtime WebSocket session died during long silences (idle/session
limits), killing the transcript for the rest of the take. Transcription is
now a batch pass over the finalized recording, ordered so footage durability
never depends on the network:

- Stop flow saves the recovery checkpoint before any transcription call;
  failure or timeout falls back to full-duration sections with a visible
  warning instead of blocking or losing the take.
- New main-process transcription-service extracts audio from the finalized
  file via ffmpeg stream copy and calls batch Scribe (scribe_v2, word
  timestamps); the API key never reaches the renderer.
- Word timestamps map into recording time via per-file recorder start
  offsets instead of the old wall-clock estimate.
- Record is blocked while the previous take is still post-processing so
  overlapping take flows cannot race on the shared recovery checkpoint.
- Removed the WebSocket/AudioWorklet/PCM pipeline, live segment editing UI,
  scribe token IPC, and scribe-status module (net -215 lines).

Tests: segment builder + timeout helper (15), transcription service temp-file
cleanup and failure paths (10), IPC handler validation (3); media-cleanup and
register-handlers suites updated. pnpm run check passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 9, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

Recording UI overhaul: the transcript panel and "keep silences" checkbox are
gone from the recording screen. Stopping a recording now puts the take on the
timeline immediately as one full-length section (no network in the stop
flow), and a new "Transcribe & Cut" button in the timeline toolbar runs the
batch transcription and silence cutting only when asked.

- Transcribe & Cut targets the selected section's take (fallback: latest
  take on the timeline), replaces its sections with speech-cut ones as a
  single undo step, and syncs the persisted take snapshot in take-local
  coordinates.
- Failure, timeout, no-speech, or a timeline edit made while transcription
  was in flight leaves the timeline unchanged and reports a visible status
  next to the button.
- System-audio "keep" regions captured during recording are stashed per take
  for the session and merged into the cut so audible screen sound is not
  trimmed.
- Recorder failures now surface on a compact notice line in the recording
  sidebar (replacing the transcript-panel status).
- Retired the settings.autoCutSilences project setting; normalization drops
  it from existing project files.

Tests: transcribe-cut pure helpers (15) covering source routing, target-take
resolution, and section replacement; project-domain tests updated for the
retired setting. pnpm run check passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 9, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

Real-world testing of Transcribe & Cut found playback continuing on the old
uncut sections after the timeline visually updated. Root cause: the draw
loop's boundary check looked up the active section by OBJECT IDENTITY
(sections.indexOf(activePlaybackSection)); when the section list is
wholesale-replaced (Transcribe & Cut, undo/redo restoring copies) a stale
reference returns -1 and sections[-1 + 1] silently redirected playback to
the first section, replaying old content.

- Extract the per-frame advance decision into a pure module
  (renderer/features/timeline/playback-advance.ts): id-based lookup,
  geometry always read from the current sections array, and an explicit
  'stale' result when the active id is gone so the draw loop re-resolves
  from timeline time instead of guessing an index.
- Refresh activePlaybackSection to the current array's object every frame,
  and drop it before the post-cut re-seek in transcribeAndCutTake.
- Fix the "Cannot close a closed AudioContext" unhandled rejection:
  cleanupAllMedia closed a context that stopAudioMeter had already closed,
  and close() rejects asynchronously past a sync try/catch. Both paths now
  guard on state === 'closed' and swallow the close promise rejection.

Tests: playback-advance unit suite (9) including the stale-reference
regression and undo-copy geometry case; media-cleanup tests for the
already-closed guard and rejected close() promise. pnpm run check passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 9, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@tadaspetra
tadaspetra merged commit 48c855d into main Jul 9, 2026
2 checks passed
@tadaspetra
tadaspetra deleted the batch-transcription branch July 9, 2026 02:44
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.

1 participant