// Voice API Client - Connects to Python voice server (Coqui TTS) // // This client provides TypeScript bindings to the Python FastAPI voice service // for voice cloning and text-to-speech synthesis. export interface VoiceConfig { apiUrl: string timeout?: number } export interface VoiceModel { name: string description?: string } export interface CloneVoiceRequest { voiceName: string audioPath?: string audioData?: string // base64 encoded audio } export interface SynthesizeRequest { text: string voiceName: string language?: string } export interface VoiceListResponse { voices: VoiceModel[] count: number } export interface CloneVoiceResponse { success: boolean voiceName: string message: string } export class VoiceApiClient { private apiUrl: string private timeout: number constructor(config: VoiceConfig) { this.apiUrl = config.apiUrl.replace(/\/$/, '') this.timeout = config.timeout ?? 30000 } /** * List all available voice models */ async listVoices(): Promise { const response = await fetch(`${this.apiUrl}/voices`, { method: 'GET', headers: { 'Content-Type': 'application/json' }, signal: AbortSignal.timeout(this.timeout), }) if (!response.ok) { throw new Error(`Failed to list voices: ${response.status} ${response.statusText}`) } return response.json() as Promise } /** * Clone a voice from audio sample(s) */ async cloneVoice(request: CloneVoiceRequest): Promise { const response = await fetch(`${this.apiUrl}/clone`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(request), signal: AbortSignal.timeout(this.timeout), }) if (!response.ok) { throw new Error(`Failed to clone voice: ${response.status} ${response.statusText}`) } return response.json() as Promise } /** * Synthesize speech with a cloned voice * Returns audio data as a Blob */ async synthesize(request: SynthesizeRequest): Promise { const response = await fetch(`${this.apiUrl}/synthesize`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(request), signal: AbortSignal.timeout(this.timeout), }) if (!response.ok) { throw new Error(`Failed to synthesize: ${response.status} ${response.statusText}`) } return response.blob() } /** * Stream speech synthesis for real-time applications */ async *streamSynthesize(request: SynthesizeRequest): AsyncGenerator { const response = await fetch(`${this.apiUrl}/synthesize_stream`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(request), signal: AbortSignal.timeout(this.timeout), }) if (!response.ok) { throw new Error(`Failed to stream synthesize: ${response.status} ${response.statusText}`) } if (!response.body) { throw new Error('Empty response body') } const reader = response.body.getReader() const decoder = new TextDecoder() try { while (true) { const { done, value } = await reader.read() if (done) break yield value } } finally { reader.releaseLock() } } /** * Check if voice server is available */ async healthCheck(): Promise { try { const response = await fetch(`${this.apiUrl}/health`, { method: 'GET', signal: AbortSignal.timeout(5000), }) return response.ok } catch { return false } } } // Default client instance let defaultClient: VoiceApiClient | null = null /** * Initialize the default voice client */ export function initVoiceClient(config: VoiceConfig): VoiceApiClient { defaultClient = new VoiceApiClient(config) return defaultClient } /** * Get the default voice client */ export function getVoiceClient(): VoiceApiClient | null { return defaultClient }