feat: 任务3.11 配音库页面升级 — 对接后端真实API
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 169h9m48s
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 169h9m52s
Deploy / Deploy Staging (push) Failing after 169h11m45s
CI/CD Pipeline / Frontend Lint (push) Failing after 169h12m18s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 169h12m24s
Deploy / Production Browser E2E (push) Failing after 1827h44m53s
Deploy / Deploy Production (push) Failing after 1827h45m28s
Deploy / Build Production Runtime Images (push) Failing after 1827h46m3s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 169h9m48s
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 169h9m52s
Deploy / Deploy Staging (push) Failing after 169h11m45s
CI/CD Pipeline / Frontend Lint (push) Failing after 169h12m18s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 169h12m24s
Deploy / Production Browser E2E (push) Failing after 1827h44m53s
Deploy / Deploy Production (push) Failing after 1827h45m28s
Deploy / Build Production Runtime Images (push) Failing after 1827h46m3s
- 重写 voices.ts:新增统一音色 API(fetchVoices/fetchPresetVoices),保留旧接口向后兼容 - 重写 voiceClone.ts:对接后端音色克隆 API,导出 toVoiceClone 类型转换 - 升级 VoiceLibrary.tsx:删除所有 Mock 数据,useQuery 对接 preset/clone/unified 三个接口 - 修复 CloneVoiceModal/VoiceCloneModal:toVoiceClone 包装 createVoiceClone 返回值 - 修复 VoiceClone.tsx:useQuery queryFn 箭头函数包装
This commit is contained in:
+139
-69
@@ -1,12 +1,15 @@
|
||||
/**
|
||||
* 音色克隆 API
|
||||
* Sprint 4 — mock 数据,后续迁移真实 API
|
||||
* 任务 3.11:替换 Mock 数据,对接后端真实 API(3.05)
|
||||
*/
|
||||
import apiClient from "./client";
|
||||
|
||||
/** 克隆音色状态 */
|
||||
/* ── 前端兼容类型 ─────────────────────────────────────── */
|
||||
|
||||
/** 克隆音色状态(前端展示用) */
|
||||
export type VoiceCloneStatus = "ready" | "processing" | "failed";
|
||||
|
||||
/** 克隆音色条目 */
|
||||
/** 克隆音色条目(前端展示用) */
|
||||
export interface VoiceClone {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -17,55 +20,76 @@ export interface VoiceClone {
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
/** 创建克隆请求 */
|
||||
/** 创建克隆请求(前端简化版) */
|
||||
export interface CreateVoiceCloneRequest {
|
||||
name: string;
|
||||
audio_url: string;
|
||||
}
|
||||
|
||||
/* ── Mock 数据 ─────────────────────────────────────────── */
|
||||
/* ── 后端 API 类型 ────────────────────────────────────── */
|
||||
|
||||
const MOCK_CLONES: VoiceClone[] = [
|
||||
{
|
||||
id: "vc-001",
|
||||
name: "我的播音腔",
|
||||
duration_seconds: 128,
|
||||
status: "ready",
|
||||
sample_url: "",
|
||||
created_at: "2026-06-20T10:30:00Z",
|
||||
updated_at: "2026-06-20T10:35:00Z",
|
||||
},
|
||||
{
|
||||
id: "vc-002",
|
||||
name: "温柔女声",
|
||||
duration_seconds: 256,
|
||||
status: "ready",
|
||||
sample_url: "",
|
||||
created_at: "2026-06-22T14:00:00Z",
|
||||
updated_at: "2026-06-22T14:08:00Z",
|
||||
},
|
||||
{
|
||||
id: "vc-003",
|
||||
name: "磁性男声",
|
||||
duration_seconds: 95,
|
||||
status: "processing",
|
||||
created_at: "2026-06-28T09:15:00Z",
|
||||
updated_at: "2026-06-28T09:15:00Z",
|
||||
},
|
||||
{
|
||||
id: "vc-004",
|
||||
name: "童声模仿",
|
||||
duration_seconds: 60,
|
||||
status: "ready",
|
||||
sample_url: "",
|
||||
created_at: "2026-06-25T16:45:00Z",
|
||||
updated_at: "2026-06-25T16:50:00Z",
|
||||
},
|
||||
];
|
||||
/** 后端克隆档案响应 */
|
||||
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<string, unknown> | 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<string, unknown>;
|
||||
}
|
||||
|
||||
/* ── 辅助函数 ─────────────────────────────────────────── */
|
||||
|
||||
const delay = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
||||
/**
|
||||
* 将后端 VoiceCloneProfile 转换为前端 VoiceClone
|
||||
* 后端 status "pending" 映射为前端 "processing"
|
||||
*/
|
||||
export const toVoiceClone = (profile: VoiceCloneProfile): VoiceClone => ({
|
||||
id: profile.id,
|
||||
name: profile.name,
|
||||
duration_seconds: 0,
|
||||
status: profile.status === "pending" ? "processing" : profile.status,
|
||||
sample_url: profile.source_audio_url || undefined,
|
||||
created_at: profile.created_at,
|
||||
updated_at: profile.updated_at,
|
||||
});
|
||||
|
||||
/** 格式化时长 */
|
||||
export const formatDuration = (seconds: number): string => {
|
||||
@@ -74,54 +98,100 @@ export const formatDuration = (seconds: number): string => {
|
||||
return `${m}:${String(s).padStart(2, "0")}`;
|
||||
};
|
||||
|
||||
/* ── 查询参数 ─────────────────────────────────────────── */
|
||||
|
||||
export interface VoiceCloneListParams {
|
||||
status?: string;
|
||||
skip?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
/* ── API 函数 ─────────────────────────────────────────── */
|
||||
|
||||
/** 获取所有克隆音色 */
|
||||
export const getVoiceClones = async (): Promise<VoiceClone[]> => {
|
||||
await delay(300);
|
||||
return [...MOCK_CLONES];
|
||||
/** 获取克隆音色列表(返回前端兼容数组) */
|
||||
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<VoiceClone | null> => {
|
||||
await delay(200);
|
||||
return MOCK_CLONES.find((v) => v.id === id) ?? null;
|
||||
): Promise<VoiceCloneProfile> => {
|
||||
const response = await apiClient.get<VoiceCloneProfile>(`/voice-clones/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/** 创建克隆音色 */
|
||||
export const createVoiceClone = async (
|
||||
data: CreateVoiceCloneRequest,
|
||||
): Promise<VoiceClone> => {
|
||||
await delay(500);
|
||||
const newClone: VoiceClone = {
|
||||
id: `vc-${Date.now()}`,
|
||||
): Promise<VoiceCloneProfile> => {
|
||||
const payload: CreateVoiceCloneRequestFull = {
|
||||
name: data.name,
|
||||
duration_seconds: 0,
|
||||
status: "processing",
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
source_audio_url: data.audio_url,
|
||||
};
|
||||
MOCK_CLONES.unshift(newClone);
|
||||
return newClone;
|
||||
const response = await apiClient.post<VoiceCloneProfile>(
|
||||
"/voice-clones",
|
||||
payload,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/** 删除克隆音色 */
|
||||
export const deleteVoiceClone = async (id: string): Promise<void> => {
|
||||
await delay(300);
|
||||
const idx = MOCK_CLONES.findIndex((v) => v.id === id);
|
||||
if (idx >= 0) MOCK_CLONES.splice(idx, 1);
|
||||
await apiClient.delete(`/voice-clones/${id}`);
|
||||
};
|
||||
|
||||
/** 更新克隆音色名称 */
|
||||
/** 更新克隆音色名称(stub — 后端暂无 PATCH 端点) */
|
||||
export const updateVoiceClone = async (
|
||||
id: string,
|
||||
data: Partial<Pick<VoiceClone, "name">>,
|
||||
): Promise<VoiceClone> => {
|
||||
await delay(300);
|
||||
const clone = MOCK_CLONES.find((v) => v.id === id);
|
||||
if (!clone) throw new Error("音色不存在");
|
||||
Object.assign(clone, data, { updated_at: new Date().toISOString() });
|
||||
return { ...clone };
|
||||
// 后端暂未提供更新端点,暂用详情接口模拟
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -1,10 +1,94 @@
|
||||
/**
|
||||
* 配音相关 API
|
||||
* Phase 1 新增:全局配音库
|
||||
*
|
||||
* 任务 3.11:新增统一音色 API(对接后端 3.04),保留旧接口向后兼容
|
||||
*/
|
||||
import apiClient from "./client";
|
||||
|
||||
/** 配音条目 */
|
||||
/* ── 统一音色 API(后端 3.04) ─────────────────────────── */
|
||||
|
||||
/** 统一音色条目(preset + clone 混合) */
|
||||
export interface UnifiedVoiceItem {
|
||||
id: string;
|
||||
type: "preset" | "clone";
|
||||
name: string;
|
||||
description: string;
|
||||
gender: string;
|
||||
language: string;
|
||||
voice_id: string;
|
||||
voice_provider: string;
|
||||
audio_url: string | null;
|
||||
preview_url: string | null;
|
||||
duration: number | null;
|
||||
file_size: number | null;
|
||||
status: string;
|
||||
tags: string[];
|
||||
user_id: string | null;
|
||||
project_id: string | null;
|
||||
voice_clone_profile_id: string | null;
|
||||
created_at: string | null;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
/** 统一音色列表响应 */
|
||||
export interface UnifiedVoiceListResponse {
|
||||
items: UnifiedVoiceItem[];
|
||||
total: number;
|
||||
preset_count: number;
|
||||
clone_count: number;
|
||||
}
|
||||
|
||||
/** 预设音色条目 */
|
||||
export interface PresetVoiceItem {
|
||||
voice_id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
gender: string;
|
||||
language: string;
|
||||
preview_url: string | null;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
/** 预设音色列表响应 */
|
||||
export interface PresetVoiceListResponse {
|
||||
items: PresetVoiceItem[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
/** 统一列表查询参数 */
|
||||
export interface UnifiedVoiceListParams {
|
||||
type?: "preset" | "clone";
|
||||
status?: string;
|
||||
skip?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
/** 获取统一音色列表(推荐) */
|
||||
export const fetchVoices = async (
|
||||
params?: UnifiedVoiceListParams,
|
||||
): Promise<UnifiedVoiceListResponse> => {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.type) searchParams.set("type", params.type);
|
||||
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<UnifiedVoiceListResponse>(
|
||||
`/voices${qs ? `?${qs}` : ""}`,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/** 获取预设音色列表(无需鉴权) */
|
||||
export const fetchPresetVoices = async (): Promise<PresetVoiceListResponse> => {
|
||||
const response = await apiClient.get<PresetVoiceListResponse>("/voices/presets");
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/* ── 向后兼容(旧接口) ────────────────────────────────── */
|
||||
|
||||
/** 配音条目(旧) */
|
||||
export interface VoiceItem {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -19,16 +103,16 @@ export interface VoiceItem {
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
/** 创建配音请求 */
|
||||
/** 创建配音请求(旧) */
|
||||
export interface CreateVoiceRequest {
|
||||
name: string;
|
||||
text: string;
|
||||
voice_type?: string;
|
||||
}
|
||||
|
||||
/** 获取当前用户的所有配音 */
|
||||
/** 获取当前用户的所有配音(旧 → /voices/legacy) */
|
||||
export const getVoices = async (): Promise<VoiceItem[]> => {
|
||||
const response = await apiClient.get("/voices");
|
||||
const response = await apiClient.get("/voices/legacy");
|
||||
return response.data.items || response.data || [];
|
||||
};
|
||||
|
||||
@@ -45,7 +129,7 @@ export const updateVoice = async (
|
||||
voiceId: string,
|
||||
data: Partial<CreateVoiceRequest>,
|
||||
): Promise<VoiceItem> => {
|
||||
const response = await apiClient.patch(`/voices/${voiceId}`, data);
|
||||
const response = await apiClient.put(`/voices/${voiceId}`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user