/** * 音色克隆 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 VoiceCloneMetadata { /** 语音时长(秒) */ duration?: number /** 采样率(Hz) */ sample_rate?: number /** 音色 ID(克隆完成后分配) */ voice_id?: string /** 其他扩展字段 */ [key: string]: unknown } /** 后端克隆档案响应 */ 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_: VoiceCloneMetadata | 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_?: VoiceCloneMetadata } /* ── 辅助函数 ─────────────────────────────────────────── */ /** * 将后端 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 }