feat(voice): add native macOS say TTS engine option#1315
Open
camerondgray wants to merge 1 commit into
Open
Conversation
… no API key)
Adds an opt-in `voice_engine` config ("elevenlabs" | "say") to the Pulse voice
module. Setting it to "say" routes all TTS through the native macOS `say` binary
instead of the ElevenLabs cloud API.
Why:
- Cost: ElevenLabs usage is metered; `say` is free.
- Privacy: notification text currently POSTs to api.elevenlabs.io. With the
`say` engine nothing leaves the machine — fully offline.
- Resilience: no API key or quota dependency.
Behavior:
- Default is unchanged ("elevenlabs") — fully non-breaking.
- Selectable via PULSE.toml [voice] voice_engine, or the PAI_VOICE_ENGINE env var.
- Per-voice macOS voice via daidentity.voices.<name>.sayVoice, or a
default_say_voice / PAI_SAY_VOICE fallback; omit for the system voice.
- ElevenLabs `speed` maps to a `say` words-per-minute rate (clamped 100-320).
- Pronunciation preprocessing and per-voice volume both still apply.
- The /notify HTTP contract is unchanged, so existing callers need no edits.
Security: `say` is invoked via spawn() with an argument array (no shell), and
`--` terminates option parsing so message text can never be treated as flags.
Testing: drove /notify through handleVoiceRequest with voice_engine="say" and
confirmed audio plays with no API key; verified default stays "elevenlabs" and
an unrecognized engine value warns and falls back. macOS only.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🗣️ feat(voice): add native macOS
sayTTS engine optionSummary
Adds an opt-in
voice_enginesetting to the Pulse voice module so PAI can speak through the native macOSsaybinary instead of the ElevenLabs cloud API. Settingvoice_engine = "say"makes all voice notifications free, fully offline, and key-free. Default behavior is unchanged.🎯 Motivation and Context
Problem:
api.elevenlabs.io. For a personal infrastructure tool, sending all assistant speech to a third party is undesirable for many users.Solution:
Introduce a
voice_enginesetting ("elevenlabs"|"say"). The"say"engine routes TTS through/usr/bin/say— no API key, no network, nothing leaves the machine. ElevenLabs stays the default, so existing installs are unaffected.📋 Changes
Single file:
Releases/v5.0.0/.claude/PAI/PULSE/VoiceServer/voice.tsVoiceConfiggainsvoice_engine?: "elevenlabs" | "say"anddefault_say_voice?.VoiceEntrygainssayVoice?so eachdaidentity.voices.<name>can map to a macOS voice.speakWithSay()+sayRateFromSpeed();playAudio()refactored to share aplayAudioFile()helper so thesaypath keeps the existing per-voice volume control.sendNotification()branches on the engine. The ElevenLabs path and the/notifyHTTP contract are unchanged, so existing callers need no edits.startVoice()resolves the engine (config →PAI_VOICE_ENGINEenv → defaultelevenlabs), warns on an unrecognized value, and only requires an API key for the ElevenLabs engine.voiceHealth()reports the active engine.⚙️ Usage
Or via environment:
PAI_VOICE_ENGINE=say(and optionallyPAI_SAY_VOICE=Samantha).Optional per-voice mapping in
settings.json:Run
say -v '?'to list installed voices; higher-quality voices can be added in System Settings → Accessibility → Spoken Content.✅ Benefits
/notifycallers are unaffected.🧪 How Has This Been Tested?
voice_engine="say"speaks via/notify→handleVoiceRequestwith no API key set (system voice and a named "Samantha" voice both verified audibly)elevenlabs— confirmed viavoiceHealth()PAI_VOICE_ENGINE=sayenvironment selection workselevenlabsafplay -v); pronunciation preprocessing applied on both engines📊 Types of Changes
✅ Checklist
sayis invoked viaspawn()with an argument array, and--terminates option parsing so message text can never be treated as flags (consistent with the execSync→execFileSync hardening in security: replace execSync with execFileSync in tab-setter.ts #1046)🖥️ Platform
macOS only (
sayis a macOS binary). On non-macOS hosts the engine should be left at theelevenlabsdefault; selectingsaythere fails gracefully through the existing voice-error path.