diff --git a/apps/web/src/api/voice-clone.ts b/apps/web/src/api/voice-clone.ts deleted file mode 100644 index 7054ab164..000000000 --- a/apps/web/src/api/voice-clone.ts +++ /dev/null @@ -1,208 +0,0 @@ -/** - * 音色克隆 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 -} diff --git a/apps/web/src/api/voice-clone/clones.ts b/apps/web/src/api/voice-clone/clones.ts new file mode 100644 index 000000000..45714b5fd --- /dev/null +++ b/apps/web/src/api/voice-clone/clones.ts @@ -0,0 +1,87 @@ +/** + * 音色克隆 API 函数 + */ +import apiClient from "../client" +import { toVoiceClone } from "./utils" +import type { + VoiceClone, + VoiceCloneProfile, + CreateVoiceCloneRequest, + CreateVoiceCloneRequestFull, + VoiceCloneListParams, + ListVoiceCloneResponse, + VoiceCloneStatusResponse, +} from "./types" + +/** 获取克隆音色列表(返回前端兼容数组) */ +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 +} diff --git a/apps/web/src/api/voice-clone/index.ts b/apps/web/src/api/voice-clone/index.ts new file mode 100644 index 000000000..cd5e1b23c --- /dev/null +++ b/apps/web/src/api/voice-clone/index.ts @@ -0,0 +1,32 @@ +/** + * 音色克隆 API — 目录化入口 + * 保持与原 voice-clone.ts 相同导出,向后兼容 + */ + +// 类型 +export type { + VoiceCloneStatus, + VoiceClone, + CreateVoiceCloneRequest, + VoiceCloneMetadata, + VoiceCloneProfile, + ListVoiceCloneResponse, + VoiceCloneStatusResponse, + CreateVoiceCloneRequestFull, + VoiceCloneListParams, +} from "./types" + +// 工具函数 +export { toVoiceClone, formatDuration } from "./utils" + +// API 函数 +export { + getVoiceClones, + getVoiceClonesWithTotal, + getVoiceCloneDetail, + createVoiceClone, + deleteVoiceClone, + updateVoiceClone, + getVoiceCloneStatus, + retryVoiceClone, +} from "./clones" diff --git a/apps/web/src/api/voice-clone/types.ts b/apps/web/src/api/voice-clone/types.ts new file mode 100644 index 000000000..56b780cfa --- /dev/null +++ b/apps/web/src/api/voice-clone/types.ts @@ -0,0 +1,92 @@ +/** + * 音色克隆类型定义 + */ + +/** 克隆音色状态(前端展示用) */ +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 +} + +/** 音色克隆元数据 */ +export interface VoiceCloneMetadata { + duration?: number + sample_rate?: number + 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 +} + +/** 查询参数 */ +export interface VoiceCloneListParams { + status?: string + skip?: number + limit?: number +} diff --git a/apps/web/src/api/voice-clone/utils.ts b/apps/web/src/api/voice-clone/utils.ts new file mode 100644 index 000000000..43a42a5b1 --- /dev/null +++ b/apps/web/src/api/voice-clone/utils.ts @@ -0,0 +1,30 @@ +/** + * 音色克隆工具函数 + */ +import type { VoiceCloneProfile, VoiceClone } from "./types" + +/** + * 将后端 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")}` +}