add real-time toggle and batch (post-hoc) transcription

Two capture modes: the existing live SSE transcription, and a new
"Transcribe a recording..." flow that runs transcriptor-ai's new
transcribe subcommand against a previously recorded session's PCM
files. The raw PCM recording (previously an undocumented temp-dir
debug tee) is now a real, user-configurable artifact via a new
"Save recordings to" Settings row, since batch transcription depends
on it rather than just "listening back later".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
sttlab-tech
2026-09-18 16:45:16 +02:00
parent 49839b8532
commit b3948e7ca9
5 changed files with 394 additions and 8 deletions
+84
View File
@@ -499,6 +499,90 @@ succeeded."
Not done yet — everything under "Future" in Goal (question-detection/CR-generation
post-processing) — still explicitly out of scope until asked for.
## Real-time toggle + batch (post-hoc) transcription (2026-09-18) — implemented
Motivated by a discussion about splitting the architecture (UI on this MacBook, AI on a local
Ubuntu server): live streaming turned out to bring real complexity (network audio protocol,
buffering/reconnect, no-disk-storage constraints), while **batch transcription after the
meeting sidesteps nearly all of it** — and it turned out the raw audio was already being
recorded, just as an undocumented debug tee (see below). The network/server split itself is
still **not implemented** — this is the local groundwork (two capture modes) that a future
server-side batch pass could build on, not the split itself.
**Found first**: `transcriptor-ai` already wrote every session's raw audio to
`os.TempDir()/transcriptor-ai-{system,mic}.pcm`, unconditionally, undocumented in this repo's
own CLAUDE.md — a debug tee ("for listening back later"), not a designed feature: fixed
filenames (a second session silently overwrote the first), no configurable location, no
cleanup. This session promoted it into a real, deliberate artifact — the user's own framing:
"les PCM sont déjà stockés" — rather than building batch transcription on top of an accidental
side effect.
**`transcriptor-ai` changes** (`cmd/transcriptor-ai/main.go`, new `cmd/transcriptor-ai/transcribe.go`):
- `-recording-dir` (optional, defaults to the OS temp dir if empty) + session-start-timestamped
filenames (`<yyyy-MM-dd_HH-mm-ss>-system.pcm` / `-mic.pcm`, matching the transcript file's
existing timestamp convention) replace the old fixed names — no more silent overwrite between
sessions. The mic file is only created when `-capture-mic` is actually on, so a disabled track
never leaves behind a misleading empty file.
- `-realtime` (default `true`). `false` skips VAD/ASR/SSE entirely (no HTTP server started) and
only captures+records — capture itself, and the PCM tee, run unconditionally either way.
Implemented with nil-channel `select` cases (`segments`/`httpServer` left nil when disabled)
rather than duplicating the run loop — standard Go idiom, kept the diff to `run()` itself
fairly contained rather than forking into two code paths.
- New `transcribe` subcommand (`transcriptor-ai transcribe -system-pcm=... -mic-pcm=...
-asr-binary=... -asr-model-dir=... -transcript-file=...`, at least one of
`-system-pcm`/`-mic-pcm` required): replays a previously recorded PCM file as a stream of
`capture.Chunk`s (synthetic timestamps advanced by bytes-read/audio-duration, not wall clock,
since a file is read as fast as disk I/O allows — see `bytesToDuration`/`readPCMFile`) through
the **same, unmodified** `internal/vad`/`internal/asr` pipeline live mode uses, then
`transcribeWorker` (also unmodified/reused) writes the result to `-transcript-file`. No new
segmentation or ASR logic — the batch mode is entirely a different *source* for
`capture.Chunk`, not a different pipeline. No HTTP server in this mode; a `Broadcaster` is
still constructed (Publish-to-zero-subscribers is a harmless no-op) purely to satisfy
`transcribeWorker`'s existing signature without special-casing a nil broadcaster there.
- **Verified for real, not just "it compiles"**: built a standalone binary, synthesized a PCM
file (silence/tone/silence/tone/silence, tones above VAD's speech threshold) with a throwaway
Python script, and a stub `qwen_asr` shell script that just echoes fixed text reading stdin.
Ran `transcriptor-ai transcribe` against them — got exactly 2 correctly VAD-segmented,
transcribed, file-written lines (matching the 2 tone bursts), clean exit 0. Confirms the
concurrency (two-goroutine file readers → shared chunk channel → VAD → sequential ASR → file)
doesn't deadlock or panic. **Not yet verified against a real recorded session + real
qwen_asr/model** — do that before trusting batch transcription quality, this only proves the
plumbing.
**`transcriptor-ui` (Swift) changes**:
- `TranscriptorProcess.Config` gained `recordingDir` (required, passed as `-recording-dir`) and
`realtime` (passed as `-realtime=\(value)`, same boolean-flag `-flag=value` requirement
documented above for `-capture-system`/`-capture-mic`).
- `ContentView`: new "Real-time transcription" `Toggle`, `@State`-backed (not `@AppStorage`) —
deliberately matching `captureSystem`/`captureMic`'s existing treatment, not the
`@AppStorage`-persisted settings-page values; revisit together if the user ever wants the
main-window toggles remembered across relaunches. When off, `ContentView.start()` skips
building a live transcript file path entirely and never connects `SSEClient` (nothing would
ever arrive on it).
- New `BatchTranscriber.swift` — a separate, one-shot `Process` runner (`@Observable`, async
`run(_:)` that throws on nonzero exit) distinct from `TranscriptorProcess`: no SIGINT-based
graceful-drain protocol to replicate, since batch mode isn't an interactive session.
- New "Transcribe a recording…" button in `ContentView`: `NSOpenPanel` (filtered to `.pcm`,
starting in the recordings directory) lets the user pick *either* file of a session — the
sibling track is located automatically from the shared `<timestamp>-system.pcm` /
`<timestamp>-mic.pcm` naming convention (`ContentView.recordingSession(from:)`). **Scope
deliberately restricted to this app's own recordings, not arbitrary audio files** — an
explicit choice this session ("pour le moment, juste pcm"), not an oversight; supporting
arbitrary formats would need an `ffmpeg`-style decode step this doesn't have. Output transcript
is named `<session-timestamp>-batch.txt` — suffixed distinctly from the live transcript's own
`<timestamp>.txt` so a batch pass can never collide with or silently append into a live
transcript from the same session.
- `SettingsView`: new "Save recordings to" row (`RecordingSaveLocation`, default
`~/Documents/Transcriptor/Recordings`, `@AppStorage`-persisted, `NSOpenPanel` "Choose…" —
same pattern as the existing transcript-directory row), promoting the recording location from
an invisible temp-dir default to a real, user-visible, user-configurable setting.
- **Verification status**: `xcodebuild` succeeds (embedding the freshly rebuilt
`transcriptor-ai` with all the above), and the built app launches and quits cleanly with no
crash. **Not yet verified interactively** — the new toggle/button haven't been clicked, and
batch transcription hasn't been run end to end from the UI against a real recorded session.
Do that (start a short real session, stop it, then use "Transcribe a recording…" on the
resulting `.pcm` pair) before considering this feature done, not just built.
## Installing to /Applications (not automated — do this manually when needed)
There's no install script; this was done by hand once (2026-08-08) and documented here so it