feat(synthesis): ✨ Introduce remote playback proxy for streaming audio remotely
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
3c2bf76d6e
commit
4c346e2eed
1 changed files with 33 additions and 3 deletions
|
|
@ -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],
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue