Skip to main content
Glama
Bichev
by Bichev
voiceService.ts5.59 kB
import OpenAI from 'openai'; // Initialize OpenAI const apiKey = import.meta.env.VITE_OPENAI_API_KEY; const openai = apiKey ? new OpenAI({ apiKey: apiKey, dangerouslyAllowBrowser: true // Note: In production, this should be done server-side }) : null; export interface VoiceServiceConfig { language?: string; // ISO 639-1 language code (e.g., 'en', 'es', 'fr', 'de', 'ru', 'zh') voice?: 'alloy' | 'echo' | 'fable' | 'onyx' | 'nova' | 'shimmer'; speed?: number; // 0.25 to 4.0 } class VoiceService { private mediaRecorder: MediaRecorder | null = null; private audioChunks: Blob[] = []; private currentAudio: HTMLAudioElement | null = null; /** * Check if OpenAI API is available */ isAvailable(): boolean { return !!openai; } /** * Record audio from microphone */ async startRecording(): Promise<void> { if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { throw new Error('Media devices not supported in this browser'); } try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); // Use audio/webm for better browser compatibility const mimeType = MediaRecorder.isTypeSupported('audio/webm') ? 'audio/webm' : 'audio/mp4'; this.mediaRecorder = new MediaRecorder(stream, { mimeType }); this.audioChunks = []; this.mediaRecorder.ondataavailable = (event) => { if (event.data.size > 0) { this.audioChunks.push(event.data); } }; this.mediaRecorder.start(); } catch (error) { console.error('Error accessing microphone:', error); throw new Error('Failed to access microphone. Please check permissions.'); } } /** * Stop recording and return audio blob */ async stopRecording(): Promise<Blob> { return new Promise((resolve, reject) => { if (!this.mediaRecorder) { reject(new Error('No active recording')); return; } this.mediaRecorder.onstop = () => { const audioBlob = new Blob(this.audioChunks, { type: 'audio/webm' }); // Stop all tracks if (this.mediaRecorder?.stream) { this.mediaRecorder.stream.getTracks().forEach(track => track.stop()); } resolve(audioBlob); }; this.mediaRecorder.stop(); }); } /** * Transcribe audio using OpenAI Whisper API * Supports multiple languages automatically */ async transcribeAudio(audioBlob: Blob, config?: VoiceServiceConfig): Promise<string> { if (!openai) { throw new Error('OpenAI API key not configured. Please add VITE_OPENAI_API_KEY to your .env file'); } try { // Convert blob to File object (required by OpenAI API) const audioFile = new File([audioBlob], 'audio.webm', { type: audioBlob.type }); const transcription = await openai.audio.transcriptions.create({ file: audioFile, model: 'whisper-1', language: config?.language, // Optional: specify language code or let Whisper auto-detect response_format: 'text', }); return transcription as unknown as string; } catch (error) { console.error('Transcription error:', error); throw new Error('Failed to transcribe audio. Please try again.'); } } /** * Convert text to speech using OpenAI TTS API */ async textToSpeech(text: string, config?: VoiceServiceConfig): Promise<Blob> { if (!openai) { throw new Error('OpenAI API key not configured. Please add VITE_OPENAI_API_KEY to your .env file'); } try { const response = await openai.audio.speech.create({ model: 'tts-1', // or 'tts-1-hd' for higher quality voice: config?.voice || 'alloy', input: text, speed: config?.speed || 1.0, }); const audioBlob = await response.blob(); return audioBlob; } catch (error) { console.error('Text-to-speech error:', error); throw new Error('Failed to generate speech. Please try again.'); } } /** * Play audio blob */ playAudio(audioBlob: Blob): Promise<void> { return new Promise((resolve, reject) => { try { // Stop any currently playing audio this.stopAudio(); const audioUrl = URL.createObjectURL(audioBlob); this.currentAudio = new Audio(audioUrl); this.currentAudio.onended = () => { URL.revokeObjectURL(audioUrl); resolve(); }; this.currentAudio.onerror = () => { URL.revokeObjectURL(audioUrl); reject(new Error('Failed to play audio')); }; this.currentAudio.play(); } catch (error) { console.error('Audio playback error:', error); reject(error); } }); } /** * Stop currently playing audio */ stopAudio(): void { if (this.currentAudio) { this.currentAudio.pause(); this.currentAudio.currentTime = 0; this.currentAudio = null; } } /** * Check if audio is currently playing */ isPlaying(): boolean { return this.currentAudio !== null && !this.currentAudio.paused; } /** * Cancel recording */ cancelRecording(): void { if (this.mediaRecorder && this.mediaRecorder.state === 'recording') { this.mediaRecorder.stop(); if (this.mediaRecorder.stream) { this.mediaRecorder.stream.getTracks().forEach(track => track.stop()); } } this.audioChunks = []; } } // Export singleton instance export const voiceService = new VoiceService();

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Bichev/coinbase-chat-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server