Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3562b33ad0 | |||
| 01ca228c07 |
@@ -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<VoiceClone[]> => {
|
||||
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<ListVoiceCloneResponse>(`/voice-clones${qs ? `?${qs}` : ""}`)
|
||||
return response.data.items.map(toVoiceClone)
|
||||
}
|
||||
|
||||
/** 获取克隆音色列表(返回完整响应含 total) */
|
||||
export const getVoiceClonesWithTotal = async (
|
||||
params?: VoiceCloneListParams,
|
||||
): Promise<ListVoiceCloneResponse> => {
|
||||
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<ListVoiceCloneResponse>(`/voice-clones${qs ? `?${qs}` : ""}`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** 获取单个克隆音色详情 */
|
||||
export const getVoiceCloneDetail = async (id: string): Promise<VoiceCloneProfile> => {
|
||||
const response = await apiClient.get<VoiceCloneProfile>(`/voice-clones/${id}`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** 创建克隆音色 */
|
||||
export const createVoiceClone = async (
|
||||
data: CreateVoiceCloneRequest,
|
||||
): Promise<VoiceCloneProfile> => {
|
||||
const payload: CreateVoiceCloneRequestFull = {
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
source_audio_url: data.audio_url,
|
||||
}
|
||||
const response = await apiClient.post<VoiceCloneProfile>("/voice-clones", payload)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** 删除克隆音色 */
|
||||
export const deleteVoiceClone = async (id: string): Promise<void> => {
|
||||
await apiClient.delete(`/voice-clones/${id}`)
|
||||
}
|
||||
|
||||
/** 更新克隆音色名称(stub — 后端暂无 PATCH 端点) */
|
||||
export const updateVoiceClone = async (
|
||||
id: string,
|
||||
data: Partial<Pick<VoiceClone, "name">>,
|
||||
): Promise<VoiceClone> => {
|
||||
// 后端暂未提供更新端点,暂用详情接口模拟
|
||||
const response = await apiClient.get<VoiceCloneProfile>(`/voice-clones/${id}`)
|
||||
return toVoiceClone({
|
||||
...response.data,
|
||||
...data,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取克隆状态 */
|
||||
export const getVoiceCloneStatus = async (id: string): Promise<VoiceCloneStatusResponse> => {
|
||||
const response = await apiClient.get<VoiceCloneStatusResponse>(`/voice-clones/${id}/status`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** 重试克隆 */
|
||||
export const retryVoiceClone = async (id: string): Promise<VoiceCloneProfile> => {
|
||||
const response = await apiClient.post<VoiceCloneProfile>(`/voice-clones/${id}/retry`)
|
||||
return response.data
|
||||
}
|
||||
@@ -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<VoiceClone[]> => {
|
||||
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<ListVoiceCloneResponse>(`/voice-clones${qs ? `?${qs}` : ""}`)
|
||||
return response.data.items.map(toVoiceClone)
|
||||
}
|
||||
|
||||
/** 获取克隆音色列表(返回完整响应含 total) */
|
||||
export const getVoiceClonesWithTotal = async (
|
||||
params?: VoiceCloneListParams,
|
||||
): Promise<ListVoiceCloneResponse> => {
|
||||
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<ListVoiceCloneResponse>(`/voice-clones${qs ? `?${qs}` : ""}`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** 获取单个克隆音色详情 */
|
||||
export const getVoiceCloneDetail = async (id: string): Promise<VoiceCloneProfile> => {
|
||||
const response = await apiClient.get<VoiceCloneProfile>(`/voice-clones/${id}`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** 创建克隆音色 */
|
||||
export const createVoiceClone = async (
|
||||
data: CreateVoiceCloneRequest,
|
||||
): Promise<VoiceCloneProfile> => {
|
||||
const payload: CreateVoiceCloneRequestFull = {
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
source_audio_url: data.audio_url,
|
||||
}
|
||||
const response = await apiClient.post<VoiceCloneProfile>("/voice-clones", payload)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** 删除克隆音色 */
|
||||
export const deleteVoiceClone = async (id: string): Promise<void> => {
|
||||
await apiClient.delete(`/voice-clones/${id}`)
|
||||
}
|
||||
|
||||
/** 更新克隆音色名称(stub — 后端暂无 PATCH 端点) */
|
||||
export const updateVoiceClone = async (
|
||||
id: string,
|
||||
data: Partial<Pick<VoiceClone, "name">>,
|
||||
): Promise<VoiceClone> => {
|
||||
const response = await apiClient.get<VoiceCloneProfile>(`/voice-clones/${id}`)
|
||||
return toVoiceClone({
|
||||
...response.data,
|
||||
...data,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取克隆状态 */
|
||||
export const getVoiceCloneStatus = async (id: string): Promise<VoiceCloneStatusResponse> => {
|
||||
const response = await apiClient.get<VoiceCloneStatusResponse>(`/voice-clones/${id}/status`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** 重试克隆 */
|
||||
export const retryVoiceClone = async (id: string): Promise<VoiceCloneProfile> => {
|
||||
const response = await apiClient.post<VoiceCloneProfile>(`/voice-clones/${id}/retry`)
|
||||
return response.data
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")}`
|
||||
}
|
||||
Reference in New Issue
Block a user