mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-15 12:50:42 -07:00
ModelManagement.tsx reads migrationResult.moved (added in #433) but apiClient.migrateModels() was typed as returning only { source, destination }. The backend actually returns { moved: int, errors: list[str], source, destination } (backend/routes/models.py:140, 168). Widen the TS return type so the check typechecks under the new CI gate from #418. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
758 lines
22 KiB
TypeScript
758 lines
22 KiB
TypeScript
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<string, unknown>) => e.msg || e.message || JSON.stringify(e))
|
|
.join('; ');
|
|
}
|
|
if (detail && typeof detail === 'object') {
|
|
const obj = detail as Record<string, unknown>;
|
|
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<T>(endpoint: string, options?: RequestInit): Promise<T> {
|
|
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<HealthResponse> {
|
|
return this.request<HealthResponse>('/health');
|
|
}
|
|
|
|
// Profiles
|
|
async createProfile(data: VoiceProfileCreate): Promise<VoiceProfileResponse> {
|
|
return this.request<VoiceProfileResponse>('/profiles', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async listProfiles(): Promise<VoiceProfileResponse[]> {
|
|
return this.request<VoiceProfileResponse[]>('/profiles');
|
|
}
|
|
|
|
async getProfile(profileId: string): Promise<VoiceProfileResponse> {
|
|
return this.request<VoiceProfileResponse>(`/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<VoiceProfileResponse> {
|
|
return this.request<VoiceProfileResponse>(`/profiles/${profileId}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async deleteProfile(profileId: string): Promise<void> {
|
|
await this.request<void>(`/profiles/${profileId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
async addProfileSample(
|
|
profileId: string,
|
|
file: File,
|
|
referenceText: string,
|
|
): Promise<ProfileSampleResponse> {
|
|
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<ProfileSampleResponse[]> {
|
|
return this.request<ProfileSampleResponse[]>(`/profiles/${profileId}/samples`);
|
|
}
|
|
|
|
async deleteProfileSample(sampleId: string): Promise<void> {
|
|
await this.request<void>(`/profiles/samples/${sampleId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
async updateProfileSample(
|
|
sampleId: string,
|
|
referenceText: string,
|
|
): Promise<ProfileSampleResponse> {
|
|
return this.request<ProfileSampleResponse>(`/profiles/samples/${sampleId}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify({ reference_text: referenceText }),
|
|
});
|
|
}
|
|
|
|
async exportProfile(profileId: string): Promise<Blob> {
|
|
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<VoiceProfileResponse> {
|
|
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<VoiceProfileResponse> {
|
|
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<void> {
|
|
await this.request<void>(`/profiles/${profileId}/avatar`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
// Generation
|
|
async generateSpeech(data: GenerationRequest): Promise<GenerationResponse> {
|
|
return this.request<GenerationResponse>('/generate', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async retryGeneration(generationId: string): Promise<GenerationResponse> {
|
|
return this.request<GenerationResponse>(`/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<GenerationResponse> {
|
|
return this.request<GenerationResponse>(`/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<HistoryListResponse> {
|
|
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<HistoryListResponse>(endpoint);
|
|
}
|
|
|
|
async getGeneration(generationId: string): Promise<HistoryResponse> {
|
|
return this.request<HistoryResponse>(`/history/${generationId}`);
|
|
}
|
|
|
|
async deleteGeneration(generationId: string): Promise<void> {
|
|
await this.request<void>(`/history/${generationId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
async clearFailedGenerations(): Promise<{ deleted: number }> {
|
|
return this.request<{ deleted: number }>(`/history/failed`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
async exportGeneration(generationId: string): Promise<Blob> {
|
|
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<Blob> {
|
|
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<TranscriptionResponse> {
|
|
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<ModelStatusListResponse> {
|
|
return this.request<ModelStatusListResponse>('/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<ActiveTasksResponse> {
|
|
return this.request<ActiveTasksResponse>('/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<CudaStatus> {
|
|
return this.request<CudaStatus>('/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<StoryResponse[]> {
|
|
return this.request<StoryResponse[]>('/stories');
|
|
}
|
|
|
|
async createStory(data: StoryCreate): Promise<StoryResponse> {
|
|
return this.request<StoryResponse>('/stories', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async getStory(storyId: string): Promise<StoryDetailResponse> {
|
|
return this.request<StoryDetailResponse>(`/stories/${storyId}`);
|
|
}
|
|
|
|
async updateStory(storyId: string, data: StoryCreate): Promise<StoryResponse> {
|
|
return this.request<StoryResponse>(`/stories/${storyId}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async deleteStory(storyId: string): Promise<void> {
|
|
await this.request<void>(`/stories/${storyId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
async addStoryItem(storyId: string, data: StoryItemCreate): Promise<StoryItemDetail> {
|
|
return this.request<StoryItemDetail>(`/stories/${storyId}/items`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async removeStoryItem(storyId: string, itemId: string): Promise<void> {
|
|
await this.request<void>(`/stories/${storyId}/items/${itemId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
async updateStoryItemTimes(storyId: string, data: StoryItemBatchUpdate): Promise<void> {
|
|
await this.request<void>(`/stories/${storyId}/items/times`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async reorderStoryItems(storyId: string, data: StoryItemReorder): Promise<StoryItemDetail[]> {
|
|
return this.request<StoryItemDetail[]>(`/stories/${storyId}/items/reorder`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async moveStoryItem(
|
|
storyId: string,
|
|
itemId: string,
|
|
data: StoryItemMove,
|
|
): Promise<StoryItemDetail> {
|
|
return this.request<StoryItemDetail>(`/stories/${storyId}/items/${itemId}/move`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async trimStoryItem(
|
|
storyId: string,
|
|
itemId: string,
|
|
data: StoryItemTrim,
|
|
): Promise<StoryItemDetail> {
|
|
return this.request<StoryItemDetail>(`/stories/${storyId}/items/${itemId}/trim`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async splitStoryItem(
|
|
storyId: string,
|
|
itemId: string,
|
|
data: StoryItemSplit,
|
|
): Promise<StoryItemDetail[]> {
|
|
return this.request<StoryItemDetail[]>(`/stories/${storyId}/items/${itemId}/split`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async duplicateStoryItem(storyId: string, itemId: string): Promise<StoryItemDetail> {
|
|
return this.request<StoryItemDetail>(`/stories/${storyId}/items/${itemId}/duplicate`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
async setStoryItemVersion(
|
|
storyId: string,
|
|
itemId: string,
|
|
data: StoryItemVersionUpdate,
|
|
): Promise<StoryItemDetail> {
|
|
return this.request<StoryItemDetail>(`/stories/${storyId}/items/${itemId}/version`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async exportStoryAudio(storyId: string): Promise<Blob> {
|
|
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<AvailableEffectsResponse> {
|
|
return this.request<AvailableEffectsResponse>('/effects/available');
|
|
}
|
|
|
|
async listEffectPresets(): Promise<EffectPresetResponse[]> {
|
|
return this.request<EffectPresetResponse[]>('/effects/presets');
|
|
}
|
|
|
|
async createEffectPreset(data: EffectPresetCreate): Promise<EffectPresetResponse> {
|
|
return this.request<EffectPresetResponse>('/effects/presets', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async updateEffectPreset(
|
|
presetId: string,
|
|
data: { name?: string; description?: string; effects_chain?: EffectConfig[] },
|
|
): Promise<EffectPresetResponse> {
|
|
return this.request<EffectPresetResponse>(`/effects/presets/${presetId}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async deleteEffectPreset(presetId: string): Promise<void> {
|
|
await this.request<void>(`/effects/presets/${presetId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
async listGenerationVersions(generationId: string): Promise<GenerationVersionResponse[]> {
|
|
return this.request<GenerationVersionResponse[]>(`/generations/${generationId}/versions`);
|
|
}
|
|
|
|
async applyEffectsToGeneration(
|
|
generationId: string,
|
|
data: ApplyEffectsRequest,
|
|
): Promise<GenerationVersionResponse> {
|
|
return this.request<GenerationVersionResponse>(
|
|
`/generations/${generationId}/versions/apply-effects`,
|
|
{
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
},
|
|
);
|
|
}
|
|
|
|
async setDefaultVersion(
|
|
generationId: string,
|
|
versionId: string,
|
|
): Promise<GenerationVersionResponse> {
|
|
return this.request<GenerationVersionResponse>(
|
|
`/generations/${generationId}/versions/${versionId}/set-default`,
|
|
{ method: 'PUT' },
|
|
);
|
|
}
|
|
|
|
async deleteGenerationVersion(generationId: string, versionId: string): Promise<void> {
|
|
await this.request<void>(`/generations/${generationId}/versions/${versionId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
getVersionAudioUrl(versionId: string): string {
|
|
return `${this.getBaseUrl()}/audio/version/${versionId}`;
|
|
}
|
|
|
|
async updateProfileEffects(
|
|
profileId: string,
|
|
effectsChain: EffectConfig[] | null,
|
|
): Promise<VoiceProfileResponse> {
|
|
return this.request<VoiceProfileResponse>(`/profiles/${profileId}/effects`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify({ effects_chain: effectsChain }),
|
|
});
|
|
}
|
|
|
|
async previewEffects(generationId: string, effectsChain: EffectConfig[]): Promise<Blob> {
|
|
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();
|