Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.nextevi.com/llms.txt

Use this file to discover all available pages before exploring further.

WebSocket API Examples

Complete working examples showing how to integrate NextEVI’s WebSocket API in various scenarios and platforms.

Basic Voice Chat

A minimal implementation for browser-based voice chat:
<!DOCTYPE html>
<html>
<head>
    <title>NextEVI Voice Chat</title>
</head>
<body>
    <div id="status">Disconnected</div>
    <button id="connectBtn">Connect</button>
    <button id="recordBtn" disabled>Start Recording</button>
    <div id="messages"></div>

    <script>
        class NextEVIChat {
            constructor() {
                this.ws = null;
                this.isRecording = false;
                this.mediaRecorder = null;
                this.audioContext = null;
                
                this.connectBtn = document.getElementById('connectBtn');
                this.recordBtn = document.getElementById('recordBtn');
                this.status = document.getElementById('status');
                this.messages = document.getElementById('messages');
                
                this.setupEventListeners();
            }
            
            setupEventListeners() {
                this.connectBtn.addEventListener('click', () => this.connect());
                this.recordBtn.addEventListener('click', () => this.toggleRecording());
            }
            
            connect() {
                const connectionId = 'conn-' + Math.random().toString(36).substr(2, 9);
                const wsUrl = `wss://api.nextevi.com/ws/voice/${connectionId}?api_key=oak_your_api_key&config_id=your_config_id`;
                
                this.ws = new WebSocket(wsUrl);
                
                this.ws.onopen = () => {
                    this.status.textContent = 'Connected';
                    this.connectBtn.disabled = true;
                    this.recordBtn.disabled = false;
                    
                    // Configure session
                    this.sendMessage({
                        type: "session_settings",
                        timestamp: Date.now() / 1000,
                        message_id: "settings-1",
                        data: {
                            emotion_detection: { enabled: true },
                            turn_detection: { enabled: true },
                            audio: { sample_rate: 24000, channels: 1, encoding: "linear16" }
                        }
                    });
                };
                
                this.ws.onmessage = (event) => {
                    const message = JSON.parse(event.data);
                    this.handleMessage(message);
                };
                
                this.ws.onerror = (error) => {
                    console.error('WebSocket error:', error);
                    this.status.textContent = 'Error';
                };
                
                this.ws.onclose = () => {
                    this.status.textContent = 'Disconnected';
                    this.connectBtn.disabled = false;
                    this.recordBtn.disabled = true;
                };
            }
            
            sendMessage(message) {
                if (this.ws && this.ws.readyState === WebSocket.OPEN) {
                    this.ws.send(JSON.stringify(message));
                }
            }
            
            handleMessage(message) {
                switch (message.type) {
                    case 'connection_metadata':
                        console.log('Connection established:', message);
                        break;
                        
                    case 'transcription':
                        if (message.data.is_final) {
                            this.addMessage('User', message.data.transcript);
                        }
                        break;
                        
                    case 'llm_response_chunk':
                        if (message.data.is_final) {
                            this.addMessage('AI', message.data.content);
                        }
                        break;
                        
                    case 'tts_chunk':
                        this.playAudio(message.content);
                        break;
                        
                    case 'emotion_update':
                        console.log('Emotions detected:', message.data.top_emotions);
                        break;
                        
                    case 'error':
                        console.error('Server error:', message.data);
                        break;
                        
                    default:
                        console.log('Unknown message:', message);
                }
            }
            
            addMessage(sender, content) {
                const messageDiv = document.createElement('div');
                messageDiv.innerHTML = `<strong>${sender}:</strong> ${content}`;
                this.messages.appendChild(messageDiv);
                this.messages.scrollTop = this.messages.scrollHeight;
            }
            
            async toggleRecording() {
                if (!this.isRecording) {
                    await this.startRecording();
                } else {
                    this.stopRecording();
                }
            }
            
            async startRecording() {
                try {
                    const stream = await navigator.mediaDevices.getUserMedia({ 
                        audio: { 
                            sampleRate: 24000,
                            channelCount: 1,
                            echoCancellation: true,
                            noiseSuppression: true
                        } 
                    });
                    
                    this.audioContext = new AudioContext({ sampleRate: 24000 });
                    const source = this.audioContext.createMediaStreamSource(stream);
                    const processor = this.audioContext.createScriptProcessor(4096, 1, 1);
                    
                    processor.onaudioprocess = (event) => {
                        const inputBuffer = event.inputBuffer;
                        const inputData = inputBuffer.getChannelData(0);
                        
                        // Convert float32 to int16
                        const int16Array = new Int16Array(inputData.length);
                        for (let i = 0; i < inputData.length; i++) {
                            int16Array[i] = Math.max(-32768, Math.min(32767, inputData[i] * 32768));
                        }
                        
                        // Send as binary data
                        if (this.ws && this.ws.readyState === WebSocket.OPEN) {
                            this.ws.send(int16Array.buffer);
                        }
                    };
                    
                    source.connect(processor);
                    processor.connect(this.audioContext.destination);
                    
                    this.isRecording = true;
                    this.recordBtn.textContent = 'Stop Recording';
                    this.status.textContent = 'Recording...';
                    
                } catch (error) {
                    console.error('Error starting recording:', error);
                    this.status.textContent = 'Microphone access denied';
                }
            }
            
            stopRecording() {
                if (this.audioContext) {
                    this.audioContext.close();
                    this.audioContext = null;
                }
                
                this.isRecording = false;
                this.recordBtn.textContent = 'Start Recording';
                this.status.textContent = 'Connected';
            }
            
            playAudio(base64Audio) {
                try {
                    const audioData = atob(base64Audio);
                    const arrayBuffer = new ArrayBuffer(audioData.length);
                    const uint8Array = new Uint8Array(arrayBuffer);
                    
                    for (let i = 0; i < audioData.length; i++) {
                        uint8Array[i] = audioData.charCodeAt(i);
                    }
                    
                    const audioBlob = new Blob([arrayBuffer], { type: 'audio/wav' });
                    const audioUrl = URL.createObjectURL(audioBlob);
                    const audio = new Audio(audioUrl);
                    
                    audio.play().catch(console.error);
                    
                } catch (error) {
                    console.error('Error playing audio:', error);
                }
            }
        }
        
        // Initialize when page loads
        document.addEventListener('DOMContentLoaded', () => {
            new NextEVIChat();
        });
    </script>
</body>
</html>

Node.js Server Integration

Server-side integration example with proper error handling:
const WebSocket = require('ws');
const fs = require('fs');
const { v4: uuidv4 } = require('uuid');

class NextEVIServer {
    constructor(apiKey, configId) {
        this.apiKey = apiKey;
        this.configId = configId;
        this.ws = null;
        this.connectionId = `conn-${uuidv4()}`;
    }
    
    async connect() {
        return new Promise((resolve, reject) => {
            const wsUrl = `wss://api.nextevi.com/ws/voice/${this.connectionId}?api_key=${this.apiKey}&config_id=${this.configId}`;
            
            this.ws = new WebSocket(wsUrl);
            
            this.ws.on('open', () => {
                console.log('Connected to NextEVI');
                
                // Configure session
                this.sendMessage({
                    type: "session_settings",
                    timestamp: Date.now() / 1000,
                    message_id: uuidv4(),
                    data: {
                        emotion_detection: { enabled: true },
                        turn_detection: { enabled: true },
                        audio: { sample_rate: 24000, channels: 1, encoding: "linear16" }
                    }
                });
                
                resolve();
            });
            
            this.ws.on('message', (data) => {
                try {
                    const message = JSON.parse(data);
                    this.handleMessage(message);
                } catch (error) {
                    console.error('Error parsing message:', error);
                }
            });
            
            this.ws.on('error', (error) => {
                console.error('WebSocket error:', error);
                reject(error);
            });
            
            this.ws.on('close', (code, reason) => {
                console.log(`Connection closed: ${code} ${reason}`);
            });
        });
    }
    
    sendMessage(message) {
        if (this.ws && this.ws.readyState === WebSocket.OPEN) {
            this.ws.send(JSON.stringify(message));
        }
    }
    
    sendAudioFile(filePath) {
        const audioData = fs.readFileSync(filePath);
        const base64Audio = audioData.toString('base64');
        
        this.sendMessage({
            type: "audio_input",
            timestamp: Date.now() / 1000,
            message_id: uuidv4(),
            data: {
                audio: base64Audio,
                chunk_id: `chunk-${Date.now()}`
            }
        });
    }
    
    handleMessage(message) {
        switch (message.type) {
            case 'connection_metadata':
                console.log('Connection established:', message.data.connection_id);
                break;
                
            case 'transcription':
                if (message.data.is_final) {
                    console.log('Transcription:', message.data.transcript);
                    
                    // Log emotions if detected
                    if (message.data.emotions) {
                        console.log('Emotions:', message.data.emotions);
                    }
                }
                break;
                
            case 'llm_response_chunk':
                process.stdout.write(message.data.content);
                if (message.data.is_final) {
                    console.log('\n--- Response complete ---');
                }
                break;
                
            case 'tts_chunk':
                // Save audio to file
                this.saveAudioChunk(message.content);
                break;
                
            case 'emotion_update':
                console.log('Top emotions:', message.data.top_emotions);
                break;
                
            case 'error':
                console.error('Server error:', message.data);
                break;
                
            default:
                console.log('Unknown message type:', message.type);
        }
    }
    
    saveAudioChunk(base64Audio) {
        const audioData = Buffer.from(base64Audio, 'base64');
        const fileName = `audio_chunk_${Date.now()}.wav`;
        
        fs.writeFileSync(fileName, audioData);
        console.log(`Audio saved to ${fileName}`);
    }
    
    disconnect() {
        if (this.ws) {
            this.ws.close();
        }
    }
}

// Usage example
async function main() {
    const nextevi = new NextEVIServer('oak_your_api_key', 'your_config_id');
    
    try {
        await nextevi.connect();
        
        // Send an audio file for processing
        nextevi.sendAudioFile('./sample_audio.wav');
        
        // Keep connection alive for responses
        setTimeout(() => {
            nextevi.disconnect();
        }, 30000);
        
    } catch (error) {
        console.error('Failed to connect:', error);
    }
}

main();

Python AsyncIO Implementation

Asynchronous Python client with streaming audio:
import asyncio
import websockets
import json
import base64
import time
import uuid
import wave
import pyaudio

class NextEVIPythonClient:
    def __init__(self, api_key, config_id):
        self.api_key = api_key
        self.config_id = config_id
        self.connection_id = f"conn-{uuid.uuid4()}"
        self.ws = None
        self.audio = None
        self.stream = None
        
    async def connect(self):
        """Connect to NextEVI WebSocket"""
        uri = f"wss://api.nextevi.com/ws/voice/{self.connection_id}?api_key={self.api_key}&config_id={self.config_id}"
        
        self.ws = await websockets.connect(uri)
        
        # Configure session
        await self.send_message({
            "type": "session_settings",
            "timestamp": time.time(),
            "message_id": str(uuid.uuid4()),
            "data": {
                "emotion_detection": {"enabled": True},
                "turn_detection": {"enabled": True},
                "audio": {"sample_rate": 24000, "channels": 1, "encoding": "linear16"}
            }
        })
        
        print("Connected to NextEVI")
        
    async def send_message(self, message):
        """Send JSON message to server"""
        if self.ws:
            await self.ws.send(json.dumps(message))
    
    async def send_audio_file(self, file_path):
        """Send audio file to server"""
        with wave.open(file_path, 'rb') as wav_file:
            frames = wav_file.readframes(wav_file.getnframes())
            audio_b64 = base64.b64encode(frames).decode('utf-8')
            
            await self.send_message({
                "type": "audio_input",
                "timestamp": time.time(),
                "message_id": str(uuid.uuid4()),
                "data": {
                    "audio": audio_b64,
                    "chunk_id": f"chunk-{time.time()}"
                }
            })
    
    async def start_microphone_streaming(self):
        """Start streaming from microphone"""
        self.audio = pyaudio.PyAudio()
        
        self.stream = self.audio.open(
            format=pyaudio.paInt16,
            channels=1,
            rate=24000,
            input=True,
            frames_per_buffer=4096
        )
        
        print("Started microphone streaming")
        
        while True:
            try:
                # Read audio data
                audio_data = self.stream.read(4096, exception_on_overflow=False)
                audio_b64 = base64.b64encode(audio_data).decode('utf-8')
                
                # Send audio chunk
                await self.send_message({
                    "type": "audio_input",
                    "timestamp": time.time(),
                    "message_id": str(uuid.uuid4()),
                    "data": {
                        "audio": audio_b64,
                        "chunk_id": f"chunk-{time.time()}"
                    }
                })
                
                # Small delay to prevent overwhelming the server
                await asyncio.sleep(0.1)
                
            except Exception as e:
                print(f"Error streaming audio: {e}")
                break
    
    async def listen_for_messages(self):
        """Listen for incoming messages"""
        try:
            async for message in self.ws:
                try:
                    data = json.loads(message)
                    await self.handle_message(data)
                except json.JSONDecodeError:
                    print(f"Failed to parse message: {message}")
                    
        except websockets.exceptions.ConnectionClosed:
            print("Connection closed by server")
        except Exception as e:
            print(f"Error listening for messages: {e}")
    
    async def handle_message(self, message):
        """Handle incoming messages"""
        msg_type = message.get('type')
        data = message.get('data', {})
        
        if msg_type == 'connection_metadata':
            print(f"Connection established: {data.get('connection_id')}")
            
        elif msg_type == 'transcription':
            if data.get('is_final'):
                transcript = data.get('transcript')
                confidence = data.get('confidence', 0)
                print(f"Transcription: {transcript} (confidence: {confidence:.2f})")
                
        elif msg_type == 'llm_response_chunk':
            content = data.get('content', '')
            is_final = data.get('is_final', False)
            
            print(content, end='', flush=True)
            if is_final:
                print("\n--- Response complete ---")
                
        elif msg_type == 'tts_chunk':
            # Save audio chunk
            audio_b64 = message.get('content', '')
            if audio_b64:
                audio_data = base64.b64decode(audio_b64)
                filename = f"tts_chunk_{time.time()}.wav"
                
                with open(filename, 'wb') as f:
                    f.write(audio_data)
                print(f"Saved audio chunk: {filename}")
                
        elif msg_type == 'emotion_update':
            top_emotions = data.get('top_emotions', [])
            if top_emotions:
                emotion_str = ", ".join([f"{e['name']}: {e['score']:.2f}" for e in top_emotions])
                print(f"Emotions detected: {emotion_str}")
                
        elif msg_type == 'error':
            print(f"Server error: {data}")
            
        else:
            print(f"Unknown message type: {msg_type}")
    
    async def run_conversation(self, audio_file=None):
        """Run a conversation session"""
        await self.connect()
        
        # Start message listener
        listen_task = asyncio.create_task(self.listen_for_messages())
        
        if audio_file:
            # Send audio file
            await asyncio.sleep(1)  # Wait for connection to stabilize
            await self.send_audio_file(audio_file)
            
            # Wait for processing
            await asyncio.sleep(10)
        else:
            # Start microphone streaming
            stream_task = asyncio.create_task(self.start_microphone_streaming())
            
            try:
                # Run both tasks concurrently
                await asyncio.gather(listen_task, stream_task)
            except KeyboardInterrupt:
                print("\nStopping conversation...")
                stream_task.cancel()
        
        listen_task.cancel()
        await self.disconnect()
    
    async def disconnect(self):
        """Clean up resources"""
        if self.stream:
            self.stream.stop_stream()
            self.stream.close()
        
        if self.audio:
            self.audio.terminate()
        
        if self.ws:
            await self.ws.close()
        
        print("Disconnected from NextEVI")

# Usage examples
async def main():
    client = NextEVIPythonClient('oak_your_api_key', 'your_config_id')
    
    # Option 1: Send audio file
    # await client.run_conversation('sample_audio.wav')
    
    # Option 2: Stream from microphone
    await client.run_conversation()

if __name__ == "__main__":
    asyncio.run(main())

React Integration (Custom Hook)

For React applications not using the official SDK:
import React, { useState, useEffect, useRef, useCallback } from 'react';

interface Message {
  id: string;
  type: 'user' | 'assistant';
  content: string;
  timestamp: Date;
  emotions?: Array<{ name: string; score: number }>;
}

interface UseNextEVIOptions {
  apiKey: string;
  configId: string;
  projectId?: string;
}

export function useNextEVI({ apiKey, configId, projectId }: UseNextEVIOptions) {
  const [isConnected, setIsConnected] = useState(false);
  const [isRecording, setIsRecording] = useState(false);
  const [messages, setMessages] = useState<Message[]>([]);
  const [currentEmotion, setCurrentEmotion] = useState<string | null>(null);
  
  const wsRef = useRef<WebSocket | null>(null);
  const mediaRecorderRef = useRef<MediaRecorder | null>(null);
  const audioContextRef = useRef<AudioContext | null>(null);
  
  const connect = useCallback(async () => {
    const connectionId = `conn-${Math.random().toString(36).substr(2, 9)}`;
    const wsUrl = `wss://api.nextevi.com/ws/voice/${connectionId}?api_key=${apiKey}&config_id=${configId}${projectId ? `&project_id=${projectId}` : ''}`;
    
    const ws = new WebSocket(wsUrl);
    
    ws.onopen = () => {
      setIsConnected(true);
      
      // Configure session
      ws.send(JSON.stringify({
        type: "session_settings",
        timestamp: Date.now() / 1000,
        message_id: `settings-${Date.now()}`,
        data: {
          emotion_detection: { enabled: true },
          turn_detection: { enabled: true },
          audio: { sample_rate: 24000, channels: 1, encoding: "linear16" }
        }
      }));
    };
    
    ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      handleMessage(message);
    };
    
    ws.onclose = () => {
      setIsConnected(false);
      setIsRecording(false);
    };
    
    ws.onerror = (error) => {
      console.error('WebSocket error:', error);
      setIsConnected(false);
    };
    
    wsRef.current = ws;
  }, [apiKey, configId, projectId]);
  
  const handleMessage = (message: any) => {
    switch (message.type) {
      case 'transcription':
        if (message.data.is_final) {
          setMessages(prev => [...prev, {
            id: `msg-${Date.now()}`,
            type: 'user',
            content: message.data.transcript,
            timestamp: new Date()
          }]);
        }
        break;
        
      case 'llm_response_chunk':
        if (message.data.is_final) {
          setMessages(prev => [...prev, {
            id: `msg-${Date.now()}`,
            type: 'assistant',
            content: message.data.content,
            timestamp: new Date()
          }]);
        }
        break;
        
      case 'tts_chunk':
        playAudio(message.content);
        break;
        
      case 'emotion_update':
        const topEmotion = message.data.top_emotions?.[0];
        if (topEmotion) {
          setCurrentEmotion(topEmotion.name);
        }
        break;
    }
  };
  
  const playAudio = (base64Audio: string) => {
    try {
      const audioData = atob(base64Audio);
      const arrayBuffer = new ArrayBuffer(audioData.length);
      const uint8Array = new Uint8Array(arrayBuffer);
      
      for (let i = 0; i < audioData.length; i++) {
        uint8Array[i] = audioData.charCodeAt(i);
      }
      
      const audioBlob = new Blob([arrayBuffer], { type: 'audio/wav' });
      const audioUrl = URL.createObjectURL(audioBlob);
      const audio = new Audio(audioUrl);
      
      audio.play().catch(console.error);
    } catch (error) {
      console.error('Error playing audio:', error);
    }
  };
  
  const startRecording = async () => {
    try {
      const stream = await navigator.mediaDevices.getUserMedia({ 
        audio: { 
          sampleRate: 24000,
          channelCount: 1,
          echoCancellation: true,
          noiseSuppression: true
        } 
      });
      
      audioContextRef.current = new AudioContext({ sampleRate: 24000 });
      const source = audioContextRef.current.createMediaStreamSource(stream);
      const processor = audioContextRef.current.createScriptProcessor(4096, 1, 1);
      
      processor.onaudioprocess = (event) => {
        const inputBuffer = event.inputBuffer;
        const inputData = inputBuffer.getChannelData(0);
        
        // Convert float32 to int16
        const int16Array = new Int16Array(inputData.length);
        for (let i = 0; i < inputData.length; i++) {
          int16Array[i] = Math.max(-32768, Math.min(32767, inputData[i] * 32768));
        }
        
        // Send as binary data
        if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
          wsRef.current.send(int16Array.buffer);
        }
      };
      
      source.connect(processor);
      processor.connect(audioContextRef.current.destination);
      
      setIsRecording(true);
    } catch (error) {
      console.error('Error starting recording:', error);
    }
  };
  
  const stopRecording = () => {
    if (audioContextRef.current) {
      audioContextRef.current.close();
      audioContextRef.current = null;
    }
    setIsRecording(false);
  };
  
  const disconnect = () => {
    stopRecording();
    if (wsRef.current) {
      wsRef.current.close();
      wsRef.current = null;
    }
  };
  
  // Cleanup on unmount
  useEffect(() => {
    return () => {
      disconnect();
    };
  }, []);
  
  return {
    isConnected,
    isRecording,
    messages,
    currentEmotion,
    connect,
    disconnect,
    startRecording,
    stopRecording
  };
}

// Usage component
export function VoiceChat() {
  const { 
    isConnected, 
    isRecording, 
    messages, 
    currentEmotion,
    connect, 
    disconnect, 
    startRecording, 
    stopRecording 
  } = useNextEVI({
    apiKey: 'oak_your_api_key',
    configId: 'your_config_id'
  });
  
  return (
    <div style={{ padding: '20px', maxWidth: '600px' }}>
      <div style={{ marginBottom: '20px' }}>
        <button 
          onClick={isConnected ? disconnect : connect}
          disabled={false}
        >
          {isConnected ? 'Disconnect' : 'Connect'}
        </button>
        
        <button 
          onClick={isRecording ? stopRecording : startRecording}
          disabled={!isConnected}
          style={{ marginLeft: '10px' }}
        >
          {isRecording ? 'Stop Recording' : 'Start Recording'}
        </button>
        
        <div style={{ marginTop: '10px' }}>
          Status: {isConnected ? 'Connected' : 'Disconnected'}
          {isRecording && ' (Recording)'}
          {currentEmotion && ` - ${currentEmotion} detected`}
        </div>
      </div>
      
      <div style={{ 
        border: '1px solid #ccc', 
        padding: '10px', 
        height: '300px', 
        overflowY: 'scroll' 
      }}>
        {messages.map(message => (
          <div key={message.id} style={{ marginBottom: '10px' }}>
            <strong>{message.type === 'user' ? 'You' : 'AI'}:</strong> {message.content}
            <div style={{ fontSize: '12px', color: '#666' }}>
              {message.timestamp.toLocaleTimeString()}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

Next Steps

Message Protocol

Understand the complete message format

Error Handling

Learn about error codes and troubleshooting