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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user