Skip to content

Feature: Windows/Linux voice support via browser audio playback #405

Description

@MarkoSteenbergen

Feature Request: Windows/Linux Support via Browser Audio Playback

Summary

Add cross-platform support for the voice system by streaming audio to a browser client instead of relying on macOS afplay.

Current Behavior

  • Voice system uses afplay for audio playback (macOS only)
  • Windows/Linux users cannot use voice notifications

Proposed Enhancement

Architecture

Hook (Stop) → events.jsonl → Observability Server → WebSocket → Browser → Voice Server → ElevenLabs → Audio

Instead of playing audio locally via afplay, the voice server returns audio data to a browser client that handles playback.

1. Voice Server Returns Audio (instead of playing locally)

// POST /tts returns audio/mpeg instead of playing via afplay
return new Response(audioBuffer, {
  headers: {
    "Content-Type": "audio/mpeg",
    "Access-Control-Allow-Origin": "*"
  }
});

2. Browser Client Fetches and Plays Audio

const response = await fetch(`${voiceServerUrl}/tts`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ message: text, speaker: speaker })
});

const audioBlob = await response.blob();
const audio = new Audio(URL.createObjectURL(audioBlob));
await audio.play();

3. Audio Queue for Sequential Playback

Prevent overlapping speech when multiple events arrive:

const audioQueue: { text: string; speaker: string }[] = []
let isPlaying = false

async function processAudioQueue(): Promise<void> {
  if (audioQueue.length === 0) {
    isPlaying = false;
    return;
  }
  isPlaying = true;
  const item = audioQueue.shift()!;
  // fetch and play...
  currentAudio.onended = () => processAudioQueue();
}

4. WebSocket Event Streaming

Observability server watches the events JSONL file and streams new events via WebSocket:

// Server watches file, broadcasts new events
ws.send(JSON.stringify({ type: 'event', data: newEvent }));

// Client receives and triggers TTS
ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
  if (shouldSpeak(data.data)) {
    queueSpeech(extractMessage(data.data), data.data.agent_name);
  }
};

Benefits

  • Works on Windows, Linux, and macOS
  • Browser handles audio context/permissions
  • No platform-specific audio commands needed
  • Can run on headless server with remote browser

Implementation Notes

  • Voice server needs CORS headers for browser access
  • Browser requires user interaction to unlock audio context (one-time)
  • Fallback to browser's built-in SpeechSynthesis if ElevenLabs fails

Testing Done

  • Tested on Windows with Dutch voices
  • Audio queue prevents race conditions
  • Works with multiple speakers/agents

Submitted by: Marko (happy PAI user)
Reference implementation: Custom observability + voice server setup

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions