Based on the git diff showing extensive package-lock.json changes and modifications to the agent system prompt and configuration, here's the commit message:

```
feat: add AI SDK dependencies and refine agent voice interaction config

- Add @ai-sdk packages for enhanced AI capabilities
- Install Deepgram SDK, livekit-client, and werift for voice/WebRTC
- Expand agent system prompt with multi-tool workflows and Claude Code interaction patterns
- Adjust VAD timing (1.2s silence threshold) and turn detection for better voice UX
- Switch TTS to inworld/inworld-tts-1 with Olivia voice
- Configure interruption timing (0.2s detection, no word requirement)
- Add comprehensive guidelines for conversational responses vs structured output
- Document plan mode triggers and Claude response type handling

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
```
This commit is contained in:
Mohamed Boudra
2025-10-19 15:41:42 +02:00
parent dabcf63ebc
commit 40b199e329
44 changed files with 12183 additions and 1715 deletions
@@ -0,0 +1,184 @@
export interface AudioCaptureConfig {
mimeType?: string;
audioBitsPerSecond?: number;
}
export interface AudioRecorder {
start(): Promise<void>;
stop(): Promise<Blob>;
isRecording(): boolean;
getSupportedMimeType(): string | null;
}
export function createAudioRecorder(config?: AudioCaptureConfig): AudioRecorder {
let mediaRecorder: MediaRecorder | null = null;
let audioChunks: Blob[] = [];
let stream: MediaStream | null = null;
let recording = false;
// Detect supported mime type
function getSupportedMimeType(): string | null {
const types = [
'audio/webm;codecs=opus',
'audio/webm',
'audio/ogg;codecs=opus',
'audio/ogg',
'audio/mp4',
'audio/wav',
];
for (const type of types) {
if (MediaRecorder.isTypeSupported(type)) {
return type;
}
}
return null;
}
async function start(): Promise<void> {
if (recording) {
throw new Error('Already recording');
}
try {
// Request microphone permission with WebRTC audio processing constraints
stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true, // Remove echo from speakers
noiseSuppression: true, // Remove background noise
autoGainControl: true, // Normalize volume
sampleRate: 16000, // Optimal for speech (Whisper works well with this)
channelCount: 1, // Mono audio (sufficient for voice)
},
video: false,
});
// Check which constraints were actually applied
const track = stream.getAudioTracks()[0];
const settings = track.getSettings();
console.log('[AudioRecorder] Audio settings:', settings);
// Try WebM/Opus first (best compression and quality for speech)
const mimeType = config?.mimeType ||
(MediaRecorder.isTypeSupported('audio/webm;codecs=opus')
? 'audio/webm;codecs=opus'
: getSupportedMimeType());
if (!mimeType) {
throw new Error('No supported audio mime type found');
}
// Create MediaRecorder
const options: MediaRecorderOptions = {
mimeType,
};
if (config?.audioBitsPerSecond) {
options.audioBitsPerSecond = config.audioBitsPerSecond;
}
mediaRecorder = new MediaRecorder(stream, options);
audioChunks = [];
// Collect data chunks
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
audioChunks.push(event.data);
console.log(`[AudioRecorder] Chunk received: ${event.data.size} bytes`);
}
};
// Start recording
mediaRecorder.start(100); // Collect chunks every 100ms
recording = true;
console.log('[AudioRecorder] Recording started with constraints:', {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
sampleRate: 16000,
channelCount: 1,
mimeType,
});
} catch (error: any) {
console.error('[AudioRecorder] Failed to start recording:', error);
if (stream) {
stream.getTracks().forEach((track) => track.stop());
stream = null;
}
throw new Error(`Failed to start audio recording: ${error.message}`);
}
}
async function stop(): Promise<Blob> {
if (!recording || !mediaRecorder) {
throw new Error('Not recording');
}
return new Promise((resolve, reject) => {
if (!mediaRecorder) {
reject(new Error('MediaRecorder not initialized'));
return;
}
mediaRecorder.onstop = () => {
// Create blob from chunks
const mimeType = mediaRecorder?.mimeType || 'audio/webm';
const audioBlob = new Blob(audioChunks, { type: mimeType });
console.log('[AudioRecorder] Recording stopped:', {
size: audioBlob.size,
type: mimeType,
chunks: audioChunks.length,
});
// Clean up
if (stream) {
stream.getTracks().forEach((track) => {
track.stop();
console.log('[AudioRecorder] Stopped track:', track.label);
});
stream = null;
}
recording = false;
mediaRecorder = null;
audioChunks = [];
resolve(audioBlob);
};
mediaRecorder.onerror = (event: Event) => {
console.error('[AudioRecorder] MediaRecorder error:', event);
reject(new Error('MediaRecorder error during stop'));
};
// Stop recording
mediaRecorder.stop();
});
}
function isRecordingFunc(): boolean {
return recording;
}
return {
start,
stop,
isRecording: isRecordingFunc,
getSupportedMimeType,
};
}
export function checkMicrophonePermission(): Promise<PermissionState> {
// Check if browser supports permissions API
if (!navigator.permissions) {
return Promise.resolve('prompt' as PermissionState);
}
return navigator.permissions
.query({ name: 'microphone' as PermissionName })
.then((result) => result.state)
.catch(() => 'prompt' as PermissionState);
}
@@ -0,0 +1,116 @@
export interface AudioPlayer {
play(audioData: Blob): Promise<void>;
stop(): void;
isPlaying(): boolean;
clearQueue(): void;
}
export function createAudioPlayer(): AudioPlayer {
let currentAudio: HTMLAudioElement | null = null;
let playing = false;
let queue: Blob[] = [];
let isProcessingQueue = false;
async function play(audioData: Blob): Promise<void> {
// Add to queue
queue.push(audioData);
// Start processing queue if not already processing
if (!isProcessingQueue) {
processQueue();
}
}
async function processQueue(): Promise<void> {
if (isProcessingQueue || queue.length === 0) {
return;
}
isProcessingQueue = true;
while (queue.length > 0) {
const audioData = queue.shift()!;
await playAudio(audioData);
}
isProcessingQueue = false;
}
async function playAudio(audioData: Blob): Promise<void> {
return new Promise((resolve, reject) => {
try {
// Create blob URL
const audioUrl = URL.createObjectURL(audioData);
// Create audio element
const audio = new Audio(audioUrl);
currentAudio = audio;
playing = true;
console.log(`[AudioPlayer] Playing audio (${audioData.size} bytes, type: ${audioData.type})`);
audio.onended = () => {
console.log('[AudioPlayer] Playback finished');
playing = false;
currentAudio = null;
// Clean up blob URL
URL.revokeObjectURL(audioUrl);
resolve();
};
audio.onerror = (error) => {
console.error('[AudioPlayer] Playback error:', error);
playing = false;
currentAudio = null;
// Clean up blob URL
URL.revokeObjectURL(audioUrl);
reject(new Error('Audio playback failed'));
};
// Start playback
audio.play().catch((error) => {
console.error('[AudioPlayer] Failed to start playback:', error);
playing = false;
currentAudio = null;
URL.revokeObjectURL(audioUrl);
reject(error);
});
} catch (error) {
console.error('[AudioPlayer] Error creating audio element:', error);
playing = false;
currentAudio = null;
reject(error);
}
});
}
function stop(): void {
if (currentAudio) {
currentAudio.pause();
currentAudio.currentTime = 0;
currentAudio = null;
}
playing = false;
queue = [];
isProcessingQueue = false;
}
function isPlayingFunc(): boolean {
return playing;
}
function clearQueue(): void {
queue = [];
}
return {
play,
stop,
isPlaying: isPlayingFunc,
clearQueue,
};
}