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>
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user