Files
xiaoxia-saas/apps/web/src/api/voiceClone.ts
T
CI Bot 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
refactor: 消除全部 Record<string, unknown>,替换为具体类型定义
- 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> 残留
2026-07-14 18:41:25 +08:00

234 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 音色克隆 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;
};