Aillom/aillom-vox-client
1
1// Basic AillomVox Client2const connectBtn = document.getElementById('connectBtn');3const disconnectBtn = document.getElementById('disconnectBtn');4const statusDiv = document.getElementById('status');5const apiKeyInput = document.getElementById('apiKey');6 7let socket;8let audioContext;9let processor;10let mediaStream;11 12// ๐ฏ ULTRAVOX PATTERN: Track scheduled audio sources for instant barge-in clearing13let scheduledSources = [];14let nextPlayTime = 0;15 16connectBtn.onclick = async () => {17 const apiKey = apiKeyInput.value.trim();18 if (!apiKey) return alert('Please enter an API Key');19 20 // 1. Initialize Audio Context (Must be user-initiated)21 audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 });22 23 // 2. Connect to WebSocket24 // Note: Replace 'your-server-url' with actual server if hosted elsewhere25 // For local dev with aillom-vox, use localhost:808026 // For production, use wss://vox.aillom.com/ws27 const wsUrl = window.location.hostname === 'localhost'28 ? 'ws://localhost:8080/ws'29 : 'wss://vox.aillom.com/ws';30 31 socket = new WebSocket(wsUrl);32 socket.binaryType = 'arraybuffer';33 34 socket.onopen = async () => {35 statusDiv.textContent = 'Connected. Handshaking...';36 37 // 3. Send Configuration Handshake38 const handshake = {39 type: 'config',40 apikey: apiKey,41 provider: 'aillomvox',42 voice: 'Edward',43 language: 'en-US',44 sample_rate: 16000,45 system_prompt: 'You are a helpful assistant. Be concise and friendly.',46 tools: []47 };48 socket.send(JSON.stringify(handshake));49 50 // 4. Start Microphone and Audio Processing51 await startMicrophone();52 53 statusDiv.textContent = '๐ข Online - Speak now!';54 toggleButtons(true);55 };56 57 socket.onmessage = (event) => {58 if (typeof event.data === 'string') {59 const msg = JSON.parse(event.data);60 console.log('Server Message:', msg);61 62 switch (msg.type) {63 case 'hangup':64 disconnect();65 break;66 67 case 'playback_clear_buffer':68 // ๐ฏ ULTRAVOX PATTERN: Instant barge-in โ clear all buffered audio69 clearPlaybackBuffer();70 break;71 72 case 'transcript':73 if (msg.final) {74 console.log(`[${msg.role}] ${msg.text}`);75 }76 break;77 78 case 'error':79 console.error('Server error:', msg.message);80 break;81 82 case 'state':83 // ๐ฏ ULTRAVOX P1: Conversation state machine84 statusDiv.textContent = msg.state === 'listening' ? '๐ข Listening...'85 : msg.state === 'thinking' ? '๐ก Thinking...'86 : msg.state === 'speaking' ? '๐ Speaking...'87 : `๐ข ${msg.state}`;88 break;89 }90 } else {91 // Audio Data (PCM 16-bit) received from server -> Play it92 playAudioChunk(event.data);93 }94 };95 96 socket.onclose = () => {97 statusDiv.textContent = '๐ด Disconnected';98 disconnect();99 };100};101 102disconnectBtn.onclick = disconnect;103 104function disconnect() {105 clearPlaybackBuffer();106 if (socket) socket.close();107 if (audioContext) audioContext.close();108 if (mediaStream) mediaStream.getTracks().forEach(t => t.stop());109 toggleButtons(false);110}111 112function toggleButtons(connected) {113 connectBtn.disabled = connected;114 disconnectBtn.disabled = !connected;115 apiKeyInput.disabled = connected;116}117 118async function startMicrophone() {119 mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });120 const source = audioContext.createMediaStreamSource(mediaStream);121 122 // Simple Processor (Buffer Size 4096)123 processor = audioContext.createScriptProcessor(4096, 1, 1);124 125 processor.onaudioprocess = (e) => {126 if (socket.readyState !== WebSocket.OPEN) return;127 128 const inputData = e.inputBuffer.getChannelData(0);129 // Convert Float32 to Int16 for Server130 const pcmData = floatTo16BitPCM(inputData);131 socket.send(pcmData);132 };133 134 source.connect(processor);135 processor.connect(audioContext.destination);136}137 138/**139 * ๐ฏ ULTRAVOX PATTERN: Clear all buffered/scheduled audio instantly140 * Called when server detects barge-in (user speaking while AI is talking)141 * Stops all AudioBufferSourceNodes that haven't finished playing yet142 */143function clearPlaybackBuffer() {144 for (const source of scheduledSources) {145 try { source.stop(); } catch (e) { /* already stopped */ }146 }147 scheduledSources = [];148 nextPlayTime = 0;149 console.log('[AillomVox] ๐ Playback buffer cleared (barge-in)');150}151 152/**153 * ๐ฏ ULTRAVOX PATTERN: Sequential audio scheduling154 * Instead of calling source.start() immediately (which causes overlap),155 * schedule each chunk to play after the previous one finishes.156 * This allows proper cancellation via clearPlaybackBuffer().157 */158function playAudioChunk(arrayBuffer) {159 if (!audioContext || audioContext.state === 'closed') return;160 161 const float32Data = new Float32Array(arrayBuffer.byteLength / 2);162 const dataView = new DataView(arrayBuffer);163 164 for (let i = 0; i < float32Data.length; i++) {165 const int16 = dataView.getInt16(i * 2, true); // Little Endian166 float32Data[i] = int16 < 0 ? int16 / 0x8000 : int16 / 0x7FFF;167 }168 169 const buffer = audioContext.createBuffer(1, float32Data.length, 16000);170 buffer.getChannelData(0).set(float32Data);171 172 const source = audioContext.createBufferSource();173 source.buffer = buffer;174 source.connect(audioContext.destination);175 176 // Schedule sequentially: each chunk plays after the previous one ends177 const now = audioContext.currentTime;178 const startTime = Math.max(now, nextPlayTime);179 source.start(startTime);180 nextPlayTime = startTime + buffer.duration;181 182 // Track for cancellation on barge-in183 scheduledSources.push(source);184 source.onended = () => {185 scheduledSources = scheduledSources.filter(s => s !== source);186 };187}188 189function floatTo16BitPCM(input) {190 const output = new Int16Array(input.length);191 for (let i = 0; i < input.length; i++) {192 const s = Math.max(-1, Math.min(1, input[i]));193 output[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;194 }195 return output.buffer;196}197 