import type { LanguageCode } from '@/lib/constants/languages'; import { useServerStore } from '@/stores/serverStore'; import type { ActiveTasksResponse, ApplyEffectsRequest, AvailableEffectsResponse, CudaStatus, EffectConfig, EffectPresetCreate, EffectPresetResponse, GenerationRequest, GenerationResponse, GenerationVersionResponse, HealthResponse, HistoryListResponse, HistoryQuery, HistoryResponse, ModelDownloadRequest, ModelStatusListResponse, PresetVoice, ProfileSampleResponse, StoryCreate, StoryDetailResponse, StoryItemBatchUpdate, StoryItemCreate, StoryItemDetail, StoryItemMove, StoryItemReorder, StoryItemSplit, StoryItemTrim, StoryItemVersionUpdate, StoryResponse, TranscriptionResponse, VoiceProfileCreate, VoiceProfileResponse, WhisperModelSize, } from './types'; function formatErrorDetail(detail: unknown, fallback: string): string { if (typeof detail === 'string') return detail; if (Array.isArray(detail)) { return detail .map((e: Record) => e.msg || e.message || JSON.stringify(e)) .join('; '); } if (detail && typeof detail === 'object') { const obj = detail as Record; if (typeof obj.message === 'string') return obj.message; return JSON.stringify(detail); } return fallback; } class ApiClient { private getBaseUrl(): string { const serverUrl = useServerStore.getState().serverUrl; return serverUrl; } private async request(endpoint: string, options?: RequestInit): Promise { const url = `${this.getBaseUrl()}${endpoint}`; const response = await fetch(url, { ...options, headers: { 'Content-Type': 'application/json', ...options?.headers, }, }); if (!response.ok) { const error = await response.json().catch(() => ({ detail: response.statusText, })); throw new Error(formatErrorDetail(error.detail, `HTTP error! status: ${response.status}`)); } return response.json(); } // Health async getHealth(): Promise { return this.request('/health'); } // Profiles async createProfile(data: VoiceProfileCreate): Promise { return this.request('/profiles', { method: 'POST', body: JSON.stringify(data), }); } async listProfiles(): Promise { return this.request('/profiles'); } async getProfile(profileId: string): Promise { return this.request(`/profiles/${profileId}`); } async listPresetVoices(engine: string): Promise<{ engine: string; voices: PresetVoice[] }> { return this.request<{ engine: string; voices: PresetVoice[] }>(`/profiles/presets/${engine}`); } async updateProfile(profileId: string, data: VoiceProfileCreate): Promise { return this.request(`/profiles/${profileId}`, { method: 'PUT', body: JSON.stringify(data), }); } async deleteProfile(profileId: string): Promise { await this.request(`/profiles/${profileId}`, { method: 'DELETE', }); } async addProfileSample( profileId: string, file: File, referenceText: string, ): Promise { const url = `${this.getBaseUrl()}/profiles/${profileId}/samples`; const formData = new FormData(); formData.append('file', file); formData.append('reference_text', referenceText); const response = await fetch(url, { method: 'POST', body: formData, }); if (!response.ok) { const error = await response.json().catch(() => ({ detail: response.statusText, })); throw new Error(formatErrorDetail(error.detail, `HTTP error! status: ${response.status}`)); } return response.json(); } async listProfileSamples(profileId: string): Promise { return this.request(`/profiles/${profileId}/samples`); } async deleteProfileSample(sampleId: string): Promise { await this.request(`/profiles/samples/${sampleId}`, { method: 'DELETE', }); } async updateProfileSample( sampleId: string, referenceText: string, ): Promise { return this.request(`/profiles/samples/${sampleId}`, { method: 'PUT', body: JSON.stringify({ reference_text: referenceText }), }); } async exportProfile(profileId: string): Promise { const url = `${this.getBaseUrl()}/profiles/${profileId}/export`; const response = await fetch(url); if (!response.ok) { const error = await response.json().catch(() => ({ detail: response.statusText, })); throw new Error(formatErrorDetail(error.detail, `HTTP error! status: ${response.status}`)); } return response.blob(); } async importProfile(file: File): Promise { const url = `${this.getBaseUrl()}/profiles/import`; const formData = new FormData(); formData.append('file', file); const response = await fetch(url, { method: 'POST', body: formData, }); if (!response.ok) { const error = await response.json().catch(() => ({ detail: response.statusText, })); throw new Error(formatErrorDetail(error.detail, `HTTP error! status: ${response.status}`)); } return response.json(); } async uploadAvatar(profileId: string, file: File): Promise { const url = `${this.getBaseUrl()}/profiles/${profileId}/avatar`; const formData = new FormData(); formData.append('file', file); const response = await fetch(url, { method: 'POST', body: formData, }); if (!response.ok) { const error = await response.json().catch(() => ({ detail: response.statusText, })); throw new Error(formatErrorDetail(error.detail, `HTTP error! status: ${response.status}`)); } return response.json(); } async deleteAvatar(profileId: string): Promise { await this.request(`/profiles/${profileId}/avatar`, { method: 'DELETE', }); } // Generation async generateSpeech(data: GenerationRequest): Promise { return this.request('/generate', { method: 'POST', body: JSON.stringify(data), }); } async retryGeneration(generationId: string): Promise { return this.request(`/generate/${generationId}/retry`, { method: 'POST', }); } async cancelGeneration(generationId: string): Promise<{ message: string }> { return this.request<{ message: string }>(`/generate/${generationId}/cancel`, { method: 'POST', }); } async regenerateGeneration(generationId: string): Promise { return this.request(`/generate/${generationId}/regenerate`, { method: 'POST', }); } async toggleFavorite(generationId: string): Promise<{ is_favorited: boolean }> { return this.request<{ is_favorited: boolean }>(`/history/${generationId}/favorite`, { method: 'POST', }); } // History async listHistory(query?: HistoryQuery): Promise { const params = new URLSearchParams(); if (query?.profile_id) params.append('profile_id', query.profile_id); if (query?.search) params.append('search', query.search); if (query?.limit) params.append('limit', query.limit.toString()); if (query?.offset) params.append('offset', query.offset.toString()); const queryString = params.toString(); const endpoint = queryString ? `/history?${queryString}` : '/history'; return this.request(endpoint); } async getGeneration(generationId: string): Promise { return this.request(`/history/${generationId}`); } async deleteGeneration(generationId: string): Promise { await this.request(`/history/${generationId}`, { method: 'DELETE', }); } async clearFailedGenerations(): Promise<{ deleted: number }> { return this.request<{ deleted: number }>(`/history/failed`, { method: 'DELETE', }); } async exportGeneration(generationId: string): Promise { const url = `${this.getBaseUrl()}/history/${generationId}/export`; const response = await fetch(url); if (!response.ok) { const error = await response.json().catch(() => ({ detail: response.statusText, })); throw new Error(formatErrorDetail(error.detail, `HTTP error! status: ${response.status}`)); } return response.blob(); } async exportGenerationAudio(generationId: string): Promise { const url = `${this.getBaseUrl()}/history/${generationId}/export-audio`; const response = await fetch(url); if (!response.ok) { const error = await response.json().catch(() => ({ detail: response.statusText, })); throw new Error(formatErrorDetail(error.detail, `HTTP error! status: ${response.status}`)); } return response.blob(); } async importGeneration(file: File): Promise<{ id: string; profile_id: string; profile_name: string; text: string; message: string; }> { const url = `${this.getBaseUrl()}/history/import`; const formData = new FormData(); formData.append('file', file); const response = await fetch(url, { method: 'POST', body: formData, }); if (!response.ok) { const error = await response.json().catch(() => ({ detail: response.statusText, })); throw new Error(formatErrorDetail(error.detail, `HTTP error! status: ${response.status}`)); } return response.json(); } // Generation status SSE getGenerationStatusUrl(generationId: string): string { return `${this.getBaseUrl()}/generate/${generationId}/status`; } // Audio getAudioUrl(audioId: string): string { return `${this.getBaseUrl()}/audio/${audioId}`; } getSampleUrl(sampleId: string): string { return `${this.getBaseUrl()}/samples/${sampleId}`; } // Transcription async transcribeAudio( file: File, language?: LanguageCode, model?: WhisperModelSize, ): Promise { const formData = new FormData(); formData.append('file', file); if (language) { formData.append('language', language); } if (model) { formData.append('model', model); } const url = `${this.getBaseUrl()}/transcribe`; const response = await fetch(url, { method: 'POST', body: formData, }); if (!response.ok) { const error = await response.json().catch(() => ({ detail: response.statusText, })); throw new Error(formatErrorDetail(error.detail, `HTTP error! status: ${response.status}`)); } return response.json(); } // Model Management async getModelStatus(): Promise { return this.request('/models/status'); } async getModelsCacheDir(): Promise<{ path: string }> { return this.request<{ path: string }>('/models/cache-dir'); } async migrateModels( destination: string, ): Promise<{ source: string; destination: string; moved: number; errors: string[] }> { return this.request('/models/migrate', { method: 'POST', body: JSON.stringify({ destination }), }); } getMigrationProgressUrl(): string { return `${this.getBaseUrl()}/models/migrate/progress`; } async triggerModelDownload(modelName: string): Promise<{ message: string }> { console.log( '[API] triggerModelDownload called for:', modelName, 'at', new Date().toISOString(), ); const result = await this.request<{ message: string }>('/models/download', { method: 'POST', body: JSON.stringify({ model_name: modelName } as ModelDownloadRequest), }); console.log('[API] triggerModelDownload response:', result); return result; } async deleteModel(modelName: string): Promise<{ message: string }> { return this.request<{ message: string }>(`/models/${modelName}`, { method: 'DELETE', }); } async unloadModel(modelName: string): Promise<{ message: string }> { return this.request<{ message: string }>(`/models/${modelName}/unload`, { method: 'POST', }); } async cancelDownload(modelName: string): Promise<{ message: string }> { return this.request<{ message: string }>('/models/download/cancel', { method: 'POST', body: JSON.stringify({ model_name: modelName } as ModelDownloadRequest), }); } // Task Management async getActiveTasks(): Promise { return this.request('/tasks/active'); } async clearAllTasks(): Promise<{ message: string }> { return this.request<{ message: string }>('/tasks/clear', { method: 'POST' }); } // Audio Channels async listChannels(): Promise< Array<{ id: string; name: string; is_default: boolean; device_ids: string[]; created_at: string; }> > { return this.request('/channels'); } async createChannel(data: { name: string; device_ids: string[] }): Promise<{ id: string; name: string; is_default: boolean; device_ids: string[]; created_at: string; }> { return this.request('/channels', { method: 'POST', body: JSON.stringify(data), }); } async updateChannel( channelId: string, data: { name?: string; device_ids?: string[]; }, ): Promise<{ id: string; name: string; is_default: boolean; device_ids: string[]; created_at: string; }> { return this.request(`/channels/${channelId}`, { method: 'PUT', body: JSON.stringify(data), }); } async deleteChannel(channelId: string): Promise<{ message: string }> { return this.request(`/channels/${channelId}`, { method: 'DELETE', }); } async getChannelVoices(channelId: string): Promise<{ profile_ids: string[] }> { return this.request(`/channels/${channelId}/voices`); } async setChannelVoices(channelId: string, profileIds: string[]): Promise<{ message: string }> { return this.request(`/channels/${channelId}/voices`, { method: 'PUT', body: JSON.stringify({ profile_ids: profileIds }), }); } async getProfileChannels(profileId: string): Promise<{ channel_ids: string[] }> { return this.request(`/profiles/${profileId}/channels`); } async setProfileChannels(profileId: string, channelIds: string[]): Promise<{ message: string }> { return this.request(`/profiles/${profileId}/channels`, { method: 'PUT', body: JSON.stringify({ channel_ids: channelIds }), }); } // CUDA Backend Management async getCudaStatus(): Promise { return this.request('/backend/cuda-status'); } async downloadCudaBackend(): Promise<{ message: string; progress_key: string }> { return this.request<{ message: string; progress_key: string }>('/backend/download-cuda', { method: 'POST', }); } async deleteCudaBackend(): Promise<{ message: string }> { return this.request<{ message: string }>('/backend/cuda', { method: 'DELETE', }); } // Stories async listStories(): Promise { return this.request('/stories'); } async createStory(data: StoryCreate): Promise { return this.request('/stories', { method: 'POST', body: JSON.stringify(data), }); } async getStory(storyId: string): Promise { return this.request(`/stories/${storyId}`); } async updateStory(storyId: string, data: StoryCreate): Promise { return this.request(`/stories/${storyId}`, { method: 'PUT', body: JSON.stringify(data), }); } async deleteStory(storyId: string): Promise { await this.request(`/stories/${storyId}`, { method: 'DELETE', }); } async addStoryItem(storyId: string, data: StoryItemCreate): Promise { return this.request(`/stories/${storyId}/items`, { method: 'POST', body: JSON.stringify(data), }); } async removeStoryItem(storyId: string, itemId: string): Promise { await this.request(`/stories/${storyId}/items/${itemId}`, { method: 'DELETE', }); } async updateStoryItemTimes(storyId: string, data: StoryItemBatchUpdate): Promise { await this.request(`/stories/${storyId}/items/times`, { method: 'PUT', body: JSON.stringify(data), }); } async reorderStoryItems(storyId: string, data: StoryItemReorder): Promise { return this.request(`/stories/${storyId}/items/reorder`, { method: 'PUT', body: JSON.stringify(data), }); } async moveStoryItem( storyId: string, itemId: string, data: StoryItemMove, ): Promise { return this.request(`/stories/${storyId}/items/${itemId}/move`, { method: 'PUT', body: JSON.stringify(data), }); } async trimStoryItem( storyId: string, itemId: string, data: StoryItemTrim, ): Promise { return this.request(`/stories/${storyId}/items/${itemId}/trim`, { method: 'PUT', body: JSON.stringify(data), }); } async splitStoryItem( storyId: string, itemId: string, data: StoryItemSplit, ): Promise { return this.request(`/stories/${storyId}/items/${itemId}/split`, { method: 'POST', body: JSON.stringify(data), }); } async duplicateStoryItem(storyId: string, itemId: string): Promise { return this.request(`/stories/${storyId}/items/${itemId}/duplicate`, { method: 'POST', }); } async setStoryItemVersion( storyId: string, itemId: string, data: StoryItemVersionUpdate, ): Promise { return this.request(`/stories/${storyId}/items/${itemId}/version`, { method: 'PUT', body: JSON.stringify(data), }); } async exportStoryAudio(storyId: string): Promise { const url = `${this.getBaseUrl()}/stories/${storyId}/export-audio`; const response = await fetch(url); if (!response.ok) { const error = await response.json().catch(() => ({ detail: response.statusText, })); throw new Error(formatErrorDetail(error.detail, `HTTP error! status: ${response.status}`)); } return response.blob(); } // Effects & Versions async getAvailableEffects(): Promise { return this.request('/effects/available'); } async listEffectPresets(): Promise { return this.request('/effects/presets'); } async createEffectPreset(data: EffectPresetCreate): Promise { return this.request('/effects/presets', { method: 'POST', body: JSON.stringify(data), }); } async updateEffectPreset( presetId: string, data: { name?: string; description?: string; effects_chain?: EffectConfig[] }, ): Promise { return this.request(`/effects/presets/${presetId}`, { method: 'PUT', body: JSON.stringify(data), }); } async deleteEffectPreset(presetId: string): Promise { await this.request(`/effects/presets/${presetId}`, { method: 'DELETE', }); } async listGenerationVersions(generationId: string): Promise { return this.request(`/generations/${generationId}/versions`); } async applyEffectsToGeneration( generationId: string, data: ApplyEffectsRequest, ): Promise { return this.request( `/generations/${generationId}/versions/apply-effects`, { method: 'POST', body: JSON.stringify(data), }, ); } async setDefaultVersion( generationId: string, versionId: string, ): Promise { return this.request( `/generations/${generationId}/versions/${versionId}/set-default`, { method: 'PUT' }, ); } async deleteGenerationVersion(generationId: string, versionId: string): Promise { await this.request(`/generations/${generationId}/versions/${versionId}`, { method: 'DELETE', }); } getVersionAudioUrl(versionId: string): string { return `${this.getBaseUrl()}/audio/version/${versionId}`; } async updateProfileEffects( profileId: string, effectsChain: EffectConfig[] | null, ): Promise { return this.request(`/profiles/${profileId}/effects`, { method: 'PUT', body: JSON.stringify({ effects_chain: effectsChain }), }); } async previewEffects(generationId: string, effectsChain: EffectConfig[]): Promise { const url = `${this.getBaseUrl()}/effects/preview/${generationId}`; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ effects_chain: effectsChain }), }); if (!response.ok) { const error = await response.json().catch(() => ({ detail: response.statusText, })); throw new Error(formatErrorDetail(error.detail, `HTTP error! status: ${response.status}`)); } return response.blob(); } } export const apiClient = new ApiClient();