Built-in dashboard text-to-speech playback plus optional external sidecar support through Qwen3-TTS (or any local TTS engine). The dashboard speaks activity.tts events directly when tts.enabled=true; the SSE sidecar remains optional for external TTS on another machine. Also exposes an ad-hoc speak helper (scripts/say.sh) so agents can announce one-off messages on demand.
Resources
1Install
npx skillscat add eltmon/overdeck/pan-tts Install via the SkillsCat registry.
pan-tts — Overdeck Activity TTS
Status: built into the dashboard when tts.enabled=true; external sidecar optional.
What It Is
Overdeck has built-in dashboard TTS playback for activity.tts events when tts.enabled=true in ~/.overdeck/config.yaml. The dashboard server resolves the configured voice and POSTs directly to the local Qwen3-TTS daemon, so no external subscriber is needed for the normal local-dashboard path.
pan-tts remains an optional external sidecar for users who want SSE-based TTS on a different machine or want to consume Overdeck's public event stream independently.
The TTS pipeline has three independent components:
- Dashboard playback service — subscribes to internal
activity.ttsevents, resolvestts.voice/tts.statusVoice/tts.voiceMap, and forwards utterances to the daemon when dashboard TTS is enabled. - Qwen3-TTS HTTP daemon (
sync-sources/skills/pan-tts/scripts/tts_daemon.py) — keeps the 1.7B model resident in VRAM, synthesizes speech on demand viaPOST /speak, and plays audio through the default PipeWire sink. This is the component that actually drives the speaker. - Optional SSE subscriber (
~/Projects/pan-tts/) — connects to Overdeck's/events/stream, formats condensed utterances, and forwards them to the daemon for external playback.
Architecture
Overdeck dashboard qwen-tts daemon audio out
──────────────────────── ───────────────── ─────────
activity.tts ──▶ resolve voice ──▶ POST /speak ──▶ PipeWire
mute/filter synthesize (GPU)
priority voice persistent pw-play stream
Optional external path:
/events/stream ──SSE──▶ pan-tts subscriber ──▶ POST /speak ──▶ PipeWireWhy keep the sidecar?
- The dashboard path is the default local playback path when
tts.enabled=true. - The GPU daemon is expensive to start (model load ~10s) and must stay resident.
- The optional subscriber is cheap to restart and can run on another machine without touching the dashboard server.
- The
/speakcontract is simple HTTP; anything that can POST JSON can use the daemon.
Qwen3-TTS HTTP Daemon
Source: sync-sources/skills/pan-tts/scripts/tts_daemon.py
The daemon loads Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice on cuda:0 at startup and exposes three endpoints:
POST /speak— queue a text utterance for synthesis and playbackPOST /extract-embedding— generate a VoiceDesign audio clip and extract a 2048-dim speaker embedding for voice cloningGET /health— queue depth and model status
Running the daemon
pan install # creates packages/qwen-tts-linux-x64/.venv on Linux/CUDA
pan install --skip-tts-daemon
pan tts start
pan tts status
pan tts stop
pan tts restart
pan tts install-systemdpan tts start uses tts.daemonHost and tts.daemonPort from ~/.overdeck/config.yaml, tracks the PID at ~/.overdeck/pids/qwen-tts.pid, and waits for /health before returning. Set tts.daemon.autoStart: true to have pan up start the daemon automatically; the default is off because the model cold-start and VRAM footprint are expensive.
The daemon auto-detects the default PipeWire sink and uses a persistent pw-play stream to avoid audio truncation caused by sink suspend/resume (see PipeWire Suspend below).
PipeWire Suspend
PipeWire suspends idle audio sinks by default. When a sink is suspended, the first ~50–500ms of audio can be dropped while the DAC resumes. The daemon handles this in play_audio():
- A persistent
pw-playsubprocess is kept open for up to 10 seconds after the last utterance, keeping the sink RUNNING between consecutive announcements. - On cold starts (sink is SUSPENDED), 600ms of silence is prepended to cover the resume latency.
- On warm playback (sink already RUNNING), only 50ms of safety silence is added.
This ensures short utterances (e.g. "PAN-1024 ready for merge") are never truncated, while the sink returns to SUSPENDED within a reasonable timeout once the queue is drained.
SSE Subscriber
Source: ~/Projects/pan-tts/src/pan_tts/
The subscriber is a small Python project that connects to Overdeck's SSE feed and speaks activity entries.
Configuration
~/.pan-tts/config.yaml:
endpoint: http://127.0.0.1:3000/events/stream
token: ${OVERDECK_EVENTS_TOKEN} # optional, only if Overdeck has the token set
filters:
types: [activity.tts]
sources: [merge-agent, cloister, review-specialist, test-specialist]
# issueId: PAN-537 # uncomment to focus on one issue
tts:
engine: qwen3
voice: default # voice id from stream-voices VoiceDesign
max_chars: 140 # truncate utterances longer than this
rate: 1.1
queue:
max_depth: 8 # subscriber-side queue; drops low-priority info events when full
drop_info_when_full: trueSecrets (if any) go in ~/.overdeck.env alongside the rest of the pan environment — do not duplicate them here.
Running the subscriber
# one-shot foreground run (smoke test)
cd ~/Projects/pan-tts
uv run pan-tts
# systemd user unit for permanent install
systemctl --user enable --now pan-tts.serviceA pan-tts.service unit template lives at ~/Projects/pan-tts/systemd/pan-tts.service — User= and WorkingDirectory= are pre-filled for this machine.
Ad-Hoc Speak and CLI Smoke Test
For Overdeck's configured system voice, use the built-in CLI smoke test:
pan tts test
pan tts test "Build is green, ready for review."
pan tts voices list
pan tts voices show "Vivian Voice"
pan tts voices play "Vivian Voice"
pan tts voices set-default "Vivian Voice"
pan tts voices map workAgent.finished "Vivian Voice"Voice-map keys are activity.tts eventType values; the lifecycle keys are planning.started, planning.finalized, workAgent.started, and workAgent.finished.
pan tts test reads tts.voice from ~/.overdeck/config.yaml, resolves it in ~/.overdeck/tts-voices.json, and POSTs directly to the local Qwen3-TTS daemon at http://127.0.0.1:8787/speak (or the configured tts.daemonHost/tts.daemonPort). On a fresh install with no saved system voice, the smoke test uses the daemon's default preset (Vivian, override via QWEN_TTS_VOICE) so the audio path can be verified before creating a voice library.
The skill also bundles scripts/say.sh for one-off utterances that bypass Overdeck voice settings:
./scripts/say.sh "Build is green, ready for review."
./scripts/say.sh "Pan 672 merged to main."The script POSTs to the local Qwen3-TTS daemon at http://127.0.0.1:8787/speak (override via QWEN_TTS_ENDPOINT) and sends the daemon token from ~/.overdeck/secrets/qwen-tts.token. It waits for the daemon to acknowledge the request (up to 5 s); audio then plays asynchronously in the daemon's worker thread. Keep utterances short (under ~200 characters); the daemon's queue caps at 6.
Use ad-hoc speak sparingly — the built-in dashboard playback service or optional SSE sidecar already speaks activity TTS events. Ad-hoc speak is for:
- Announcements that don't warrant a dashboard activity entry (local test runs, meta-commentary)
- Pulling the user's attention during long-running work
- Testing the audio path after a restart
Verifying It
- Install the daemon venv:
pan installon Linux/CUDA, or skip the heavy CUDA install withpan install --skip-tts-daemon. - Start the daemon:
pan tts start. - Set
tts.enabled: trueand a default voice in~/.overdeck/config.yaml. pan up— start the dashboard; iftts.daemon.autoStart: true, this also starts the daemon.- In another terminal:
pan start PAN-XXXand listen. You should hear dashboard-emittedactivity.ttsevents. - Optional external sidecar path: start
pan-ttsand watchjournalctl --user -u pan-tts -f.
If nothing speaks:
- Run
pan tts statusand check the dashboard TTS badge /GET /api/tts/healthto confirm the daemon is reachable. - Check that
tts.voicepoints at an existing voice in~/.overdeck/tts-voices.json. - For the optional sidecar path, check
curl -N http://127.0.0.1:3000/events/stream?types=activity.ttsdirectly. - Check
aplay -l— make sure the default audio device is reachable from the user session.
Do Not
- Do not make the external sidecar required for local dashboard playback. It must remain strictly additive.
- Do not speak
detailstext or full agent stdout — utterances must be short, human-friendly, and interruptible. - Do not re-emit TTS events back into pan. The feed is one-way.
Related Docs
docs/EXTERNAL-EVENT-STREAM.md— the public contract this skill depends onpackages/contracts/src/events.ts— canonical event schemassync-sources/skills/pan-tts/scripts/tts_daemon.py— Qwen3-TTS HTTP daemon source