A local meeting co-pilot that writes live diligence notes into SAIF's CRM during founder calls. It uses a fine-tuned local Qwen 2.5 3B model to generate notes from a running transcript of Zoom audio, and writes those notes into the CRM via Liveblocks as they're produced.
Two processes communicating over a local Unix socket:
- Python transcriber — captures system audio via BlackHole, runs MLX Whisper for transcription, runs the fine-tuned model for bullet generation
- Node writer — maintains a Y.Doc synced with Liveblocks, writes bullets as ProseMirror nodes, serves the control panel UI
Both processes are orchestrated by launch.sh. Start with one command, stop with Ctrl+C.
- Spec A adapter — a trained LoRA adapter must exist at
../saif-copilot-training/adapters/ - Liveblocks secret key — from the SAIF CRM Liveblocks dashboard
- Supabase access — URL and service role key for the SAIF CRM database
- Node.js 20+
- Python 3.12+ (arm64) — MLX requires native Apple Silicon Python. Use Homebrew:
brew install python@3.12 - BlackHole 2ch — for capturing Zoom audio (see setup below)
-
Install BlackHole 2ch:
brew install blackhole-2ch
Or download from https://existential.audio/blackhole/
-
Open Audio MIDI Setup on macOS (search Spotlight for it).
-
Click the + button at the bottom left → Create Multi-Output Device.
-
Check both BlackHole 2ch and your normal output (speakers or headphones) in the device list. This lets you hear Zoom audio while BlackHole also captures it.
-
In Zoom → Settings → Audio → Speaker, select the Multi-Output Device.
-
The runtime will automatically detect BlackHole 2ch as its input source. If BlackHole is not installed, it falls back to the default microphone (useful for testing).
# 1. Install Node dependencies
npm install
# 2. Create arm64 Python virtualenv (MLX requires Apple Silicon native Python)
/opt/homebrew/bin/python3.12 -m venv venv
./venv/bin/pip install -r requirements.txt
# 3. Create .env.local from example and fill in values
cp .env.example .env.local
# Edit .env.local with your keys./launch.shThis will:
- Verify your environment is set up correctly (including arm64 Python check)
- Start the Node writer (HTTP + WebSocket server)
- Open the control panel in a floating Electron window
- Start the Python transcriber (loads Whisper model, begins audio capture)
Default: mlx-community/whisper-base-mlx (fast, good for M1 Pro).
If transcription accuracy is poor, switch to the larger model in .env.local:
WHISPER_MODEL=mlx-community/whisper-small-mlx
The model is downloaded on first run from Hugging Face Hub and cached locally.
Session 3 adds real audio capture and transcription:
- Real audio capture via BlackHole 2ch (or fallback to default mic)
- Real transcription via MLX Whisper — transcript appears live in the control panel
- Verbatim bullets — every ~20 seconds, the latest transcript chunk is written to Liveblocks as a
TRANSCRIPT: ...bullet (temporary, replaced with model inference in Session 4) - Real Liveblocks writes — bullets appear in CRM in real time (from Session 2)
- Status indicators — the UI status dot reflects what's happening (listening, transcribing, idle, error)
| Session | What it adds |
|---|---|
| 4 | Model inference with the fine-tuned Qwen adapter — real diligence notes |
| 5 | Safety nets, adaptive cadence, dry-run mode |
- No real model inference yet — bullets are verbatim transcript, prefixed with
TRANSCRIPT: - No output sanity checking (Session 5)
- No adaptive cadence (Session 5)
- The "Pause generation" toggle has no effect yet (nothing to pause until Session 4)
- Silent audio chunks are skipped — if RMS is below threshold, no transcription runs