/** * 音色克隆 API * 任务 3.11:替换 Mock 数据,对接后端真实 API(3.05) * 任务 3.15:新增 progress 字段用于进度展示 */ import apiClient from "./client"; /* ── 前端兼容类型 ─────────────────────────────────────── */ /** 克隆音色状态(前端展示用) */ export type VoiceCloneStatus = "ready" | "processing" | "failed"; /** 克隆音色条目(前端展示用) */ export interface VoiceClone { id: string; name: string; description: string; duration_seconds: number; status: VoiceCloneStatus; /** 克隆进度 0-100,仅 processing 状态时有值 */ progress: number; sample_url?: string; language: string; gender: string; error_message: string | null; created_at: string; updated_at: string; } /** 创建克隆请求(前端简化版) */ export interface CreateVoiceCloneRequest { name: string; audio_url: string; description?: string; } /* ── 后端 API 类型 ────────────────────────────────────── */ /** 后端克隆档案响应 */ export interface VoiceCloneProfile { id: string; user_id: string; name: string; description: string; source_audio_url: string; voice_id: string | null; voice_model: string; language: string; gender: string; status: "pending" | "processing" | "ready" | "failed"; error_message: string | null; retry_count: number; max_retries: number; metadata_: Record | null; created_at: string; updated_at: string; } /** 后端克隆列表响应 */ export interface ListVoiceCloneResponse { items: VoiceCloneProfile[]; total: number; } /** 后端克隆状态响应 */ export interface VoiceCloneStatusResponse { id: string; status: "pending" | "processing" | "ready" | "failed"; error_message: string | null; voice_id: string | null; retry_count: number; } /** 后端创建克隆请求(完整版) */ export interface CreateVoiceCloneRequestFull { name: string; description?: string; source_audio_url: string; voice_model?: string; language?: string; gender?: string; max_retries?: number; metadata_?: Record; } /* ── 辅助函数 ─────────────────────────────────────────── */ /** * 将后端 VoiceCloneProfile 转换为前端 VoiceClone * 后端 status "pending" 映射为前端 "processing" */ export const toVoiceClone = (profile: VoiceCloneProfile): VoiceClone => ({ id: profile.id, name: profile.name, description: profile.description || "", duration_seconds: 0, status: profile.status === "pending" ? "processing" : profile.status, progress: 0, sample_url: profile.source_audio_url || undefined, language: profile.language || "", gender: profile.gender || "", error_message: profile.error_message || null, created_at: profile.created_at, updated_at: profile.updated_at, }); /** 格式化时长 */ export const formatDuration = (seconds: number): string => { const m = Math.floor(seconds / 60); const s = seconds % 60; return `${m}:${String(s).padStart(2, "0")}`; }; /* ── 查询参数 ─────────────────────────────────────────── */ export interface VoiceCloneListParams { status?: string; skip?: number; limit?: number; } /* ── API 函数 ─────────────────────────────────────────── */ /** 获取克隆音色列表(返回前端兼容数组) */ export const getVoiceClones = async ( params?: VoiceCloneListParams, ): Promise => { const searchParams = new URLSearchParams(); if (params?.status) searchParams.set("status", params.status); if (params?.skip !== undefined) searchParams.set("skip", String(params.skip)); if (params?.limit !== undefined) searchParams.set("limit", String(params.limit)); const qs = searchParams.toString(); const response = await apiClient.get( `/voice-clones${qs ? `?${qs}` : ""}`, ); return response.data.items.map(toVoiceClone); }; /** 获取克隆音色列表(返回完整响应含 total) */ export const getVoiceClonesWithTotal = async ( params?: VoiceCloneListParams, ): Promise => { const searchParams = new URLSearchParams(); if (params?.status) searchParams.set("status", params.status); if (params?.skip !== undefined) searchParams.set("skip", String(params.skip)); if (params?.limit !== undefined) searchParams.set("limit", String(params.limit)); const qs = searchParams.toString(); const response = await apiClient.get( `/voice-clones${qs ? `?${qs}` : ""}`, ); return response.data; }; /** 获取单个克隆音色详情 */ export const getVoiceCloneDetail = async ( id: string, ): Promise => { const response = await apiClient.get( `/voice-clones/${id}`, ); return response.data; }; /** 创建克隆音色 */ export const createVoiceClone = async ( data: CreateVoiceCloneRequest, ): Promise => { const payload: CreateVoiceCloneRequestFull = { name: data.name, description: data.description, source_audio_url: data.audio_url, }; const response = await apiClient.post( "/voice-clones", payload, ); return response.data; }; /** 删除克隆音色 */ export const deleteVoiceClone = async (id: string): Promise => { await apiClient.delete(`/voice-clones/${id}`); }; /** 更新克隆音色名称(stub — 后端暂无 PATCH 端点) */ export const updateVoiceClone = async ( id: string, data: Partial>, ): Promise => { // 后端暂未提供更新端点,暂用详情接口模拟 const response = await apiClient.get( `/voice-clones/${id}`, ); return toVoiceClone({ ...response.data, ...data, updated_at: new Date().toISOString(), }); }; /** 获取克隆状态 */ export const getVoiceCloneStatus = async ( id: string, ): Promise => { const response = await apiClient.get( `/voice-clones/${id}/status`, ); return response.data; }; /** 重试克隆 */ export const retryVoiceClone = async ( id: string, ): Promise => { const response = await apiClient.post( `/voice-clones/${id}/retry`, ); return response.data; };