feat(synthesis): Introduce remote playback proxy for streaming audio remotely

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
autocommit 2026-05-17 18:33:50 -07:00
parent 3c2bf76d6e
commit 4c346e2eed

View file

@ -16,6 +16,21 @@ const IS_MACOS = process.platform === 'darwin';
const AUDIO_PLAYER =
process.env['AUDIO_PLAYER'] ?? (IS_MACOS ? '/usr/bin/afplay' : '/usr/bin/pw-play');
// Playback proxy: when set, stream the synthesized wav to a remote host's
// audio output instead of playing locally. Designed for the case where the
// MCP runs on a remote workstation (e.g. apricot, via rclaude) but the
// listener is at the user's local Mac. The remote command writes stdin to
// a temp file then afplays it (afplay can't read a stream directly).
//
// SPEECH_PLAYBACK_HOST=<ssh-target> # e.g. "plum.lan"
// SPEECH_PLAYBACK_PLAYER=<remote-cmd> # default: afplay (macOS), pw-play (linux)
// SPEECH_PLAYBACK_SSH_OPTS=... # extra ssh flags (default: keepalives)
const PLAYBACK_HOST = process.env['SPEECH_PLAYBACK_HOST'];
const PLAYBACK_PLAYER = process.env['SPEECH_PLAYBACK_PLAYER'] ?? 'afplay';
const PLAYBACK_SSH_OPTS =
process.env['SPEECH_PLAYBACK_SSH_OPTS'] ??
'-o BatchMode=yes -o ServerAliveInterval=15 -o ServerAliveCountMax=4';
interface Personality {
voice_id: string | null;
exaggeration: number;
@ -153,9 +168,24 @@ export function synthesisTools(): ToolEntry[] {
// Spawn background process: play audio then cleanup
// Linux: flock serializes across sessions to prevent overlapping speech
// macOS: afplay blocks until done; flock unavailable but overlap unlikely (5-min nag interval)
const playCmd = IS_MACOS
? `${AUDIO_PLAYER} ${tmpFile}; rm -f ${tmpFile}`
: `flock ${NOTIFY_LOCK} -c "${AUDIO_PLAYER} ${tmpFile}; rm -f ${tmpFile}"`;
// Remote: stream wav over ssh to PLAYBACK_HOST, where it's written to
// a remote tmp file and afplayed (afplay can't read from a pipe).
let playCmd: string;
if (PLAYBACK_HOST) {
const remote =
'f=$(mktemp -t splay.XXXXXX) && ' +
`mv "$f" "$f.wav" && f="$f.wav" && ` +
`cat > "$f" && ${PLAYBACK_PLAYER} "$f"; rm -f "$f"`;
// Single-quote-escape the remote command for safe embedding.
const remoteEsc = remote.replace(/'/g, `'\\''`);
playCmd =
`cat ${tmpFile} | ssh ${PLAYBACK_SSH_OPTS} ${PLAYBACK_HOST} '${remoteEsc}'; ` +
`rm -f ${tmpFile}`;
} else if (IS_MACOS) {
playCmd = `${AUDIO_PLAYER} ${tmpFile}; rm -f ${tmpFile}`;
} else {
playCmd = `flock ${NOTIFY_LOCK} -c "${AUDIO_PLAYER} ${tmpFile}; rm -f ${tmpFile}"`;
}
const shell = spawn(
'/bin/bash',
['-c', playCmd],