Files
transcriptor-ai/README.md
T
sttlab-tech 3525e6b8fb initial commit: live meeting transcription pipeline
Go orchestrator: spawns audiotee for capture (system audio + mic, two
tracks), segments with an energy-threshold VAD, transcribes each segment
via qwen-asr (subprocess per segment, vendored as a pinned git submodule
in third_party/qwen-asr), and serves the live transcript over SSE while
also writing it durably to a text file. Includes a glossary/prompt-leakage
guard (internal/asr/leak.go) that discards a segment if the model echoes
the biasing prompt instead of transcribing.

cmd/transcriptor-ai is the main orchestrator; cmd/transcript-tail is a
minimal terminal SSE client. See CLAUDE.md for the full architecture and
the reasoning behind each choice (Go over Python/Swift/Rust, SSE over
WebSocket, why the VAD is a hand-rolled heuristic, etc).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-09 13:18:57 +02:00

121 lines
4.8 KiB
Markdown

# transcriptor-ai
Local, no-cloud meeting transcription for macOS. Captures system audio and microphone as two
separate tracks, transcribes both live, and serves a live-updating transcript over HTTP (SSE).
Everything runs on-device — no external API calls at runtime.
## How it works
```
audiotee (system audio + mic, 2 tracks)
→ VAD (speech segmentation, per track)
→ qwen-asr (speech-to-text, per track)
→ live transcript, served over SSE
```
Two tracks (system audio vs microphone) act as a cheap "me vs them" diarization: whatever you
hear vs whatever you say, without needing a real speaker-diarization model.
## Prerequisites
- macOS (Apple Silicon)
- Go 1.26+ (`brew install go`)
- **[audiotee](../audiotee)** — built and signed with a stable identity so its audio
permissions survive rebuilds. Follow that repo's own README/CONTEXT.md first; you should
end up with a working binary, e.g. at `~/bin/audiotee`.
- **qwen-asr** — vendored as a git submodule (`third_party/qwen-asr`, pinned to a specific
commit), built automatically by `scripts/build-dist.sh` below. You still need to download a
model yourself (not part of the build — see "Models"), since that's a multi-GB download,
not something to fetch on every build:
```bash
git submodule update --init third_party/qwen-asr # if you haven't run build-dist.sh yet
cd third_party/qwen-asr && ./download_model.sh --model large # or --model small
```
`llama.cpp` (`brew install llama.cpp`) will be needed once post-processing (glossary
correction, LLM reread) is implemented, but isn't used yet — no need to install it today.
## Build
```bash
git clone --recurse-submodules <this repo> # or: git submodule update --init
scripts/build-dist.sh
```
Builds `qwen_asr` (from the submodule) and `transcriptor-ai` into `dist/`. Also build
`transcript-tail` directly, it's not part of `dist/` (that's for what `transcriptor-ui`, the
future native app, embeds — see `CLAUDE.md`):
```bash
go build -o transcript-tail ./cmd/transcript-tail
```
## Usage
Start the transcriber:
```bash
./dist/transcriptor-ai \
-asr-binary ./dist/qwen_asr \
-asr-model-dir ./third_party/qwen-asr/qwen3-asr-1.7b \
-transcript-file ~/Desktop/meeting-transcript.txt
```
In another terminal, follow the live transcript:
```bash
./transcript-tail
```
Output looks like:
```
[mic ] Bonjour, ceci est un test.
[system…] Hello, this is a partial hypothesis that may still be revised
```
(no marker = a closed/final segment; `` = a partial hypothesis, ASR-in-progress)
Stop with Ctrl+C — shutdown is graceful (in-flight transcriptions finish before exiting).
### Flags
`transcriptor-ai`:
| Flag | Default | Description |
|---|---|---|
| `-audiotee-binary` | PATH, then `~/bin/audiotee` | Path to the `audiotee` binary |
| `-asr-binary` | *(required)* | Path to the `qwen_asr` binary |
| `-asr-model-dir` | *(required)* | Path to a qwen-asr model directory |
| `-prompt` | *(empty)* | Glossary biasing text passed to qwen-asr (e.g. `"Preserve spelling: Kubernetes, PostgreSQL"`) |
| `-http-addr` | `:8420` | Address to serve the live transcript SSE endpoint on |
| `-transcript-file` | *(empty)* | Path to append the live transcript to as plain text, flushed to disk after every line. Not written if empty. |
| `-capture-system` | `true` | Transcribe the system audio track. Note: audiotee's system tap always runs regardless of this flag — disabling it only stops *transcribing* that track, it doesn't skip the underlying capture (audiotee has no flag for that). |
| `-capture-mic` | `true` | Capture and transcribe the microphone track. Unlike `-capture-system`, disabling this actually skips mic capture entirely (no `--capture-mic` passed to audiotee, no microphone permission requested). |
At least one of `-capture-system`/`-capture-mic` must stay enabled.
`transcript-tail`:
| Flag | Default | Description |
|---|---|---|
| `-url` | `http://localhost:8420/events` | transcriptor-ai SSE endpoint to follow |
### Consuming the transcript programmatically
The SSE endpoint (`http://localhost:8420/events` by default) streams one JSON object per
transcript segment:
```json
{"track": "system", "text": "...", "is_final": false, "timestamp": "2026-08-07T14:32:01.123Z"}
```
Any SSE client works, e.g. `curl -N http://localhost:8420/events`, or a browser's
`EventSource`.
## Status
Live transcription (capture → VAD → ASR → SSE) works end to end, tested with real speech
(English + French, code-switching) through an actual microphone. Not yet implemented:
post-processing (glossary fuzzy-matching, LLM reread) and real-time content analysis — see
`CLAUDE.md` for what's deliberately deferred and why.
## Related
- [audiotee](../audiotee) — the system/mic audio capture CLI this project depends on.
- [antirez/qwen-asr](https://github.com/antirez/qwen-asr) — the speech-to-text engine used.