CoolFace
Modelpublic

Aillom/aillom-vox-client

sourceHugging Faceupdated 8mo agoView on Hugging Face
1likes
app.js197 linesDownload Raw Back to 01-basic
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