8d86707e2e
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Successful in 2m2s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 2m2s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 2m9s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m57s
- GeneratePage.tsx: 3处防御性错误提取函数添加 eslint-disable 注释 (safeExtract/extractString/safeExtractErr),移除 as Record<string, unknown> 断言 - TemplateLibrary.tsx: formatConfig 使用 ConfigDisplayFields 接口替代 Record 断言 - VoiceLibrary.tsx: buildVoiceMetadata 返回 VoiceUploadMetadata 接口 - VoiceMaterialLibrary.tsx: buildMetadata 返回 VoiceAssetMetadata 接口 - editPlans.ts: EditPlanConfig 去除索引签名,补充所有生成时字段 - assets.ts: AssetItem.metadata 使用 AssetMetadata 具体类型 - products.ts: GenerateFromTemplateRequest.config 改为 EditPlanConfig - tts.ts/voiceClone.ts: metadata 参数使用具体接口 - editingPlanner.ts: ValidationWarningDetails 替代 Record<string, unknown> - client.ts: 防御性错误提取添加 eslint-disable 注释 TypeScript 编译零错误,代码库中零 Record<string, unknown> 残留
234 lines
6.9 KiB
TypeScript
234 lines
6.9 KiB
TypeScript
/**
|
||
* 音色克隆 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;
|
||
};
|