Compare commits

..

2 Commits

Author SHA1 Message Date
xiaoxia ff845c3086 Merge branch 'develop' into refactor/products-api
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 30s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 43s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m17s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m16s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 1m59s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 28s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m54s
AI Code Review / AI Code Review (pull_request) Successful in 1m19s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m36s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 46s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 34s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 15m19s
CI/CD Pipeline / CI Gate (pull_request) 失败: CI/CD Pipeline / Validate - Code Quality (pull_request) [frontend-only]
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 20s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Has been cancelled
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 / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 17s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
2026-07-27 12:34:58 +08:00
xiaoxia 17d6530438 refactor(api): 拆分 products.ts 为目录结构(types/utils/products/index)
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 26s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 37s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m25s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m26s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 57s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 2m27s
AI Code Review / AI Code Review (pull_request) Successful in 1m16s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 32s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 59s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 56s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 2m29s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 6m6s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 21s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (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 / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
将 174 行的 products.ts 拆分为目录化结构:
- types.ts: 类型定义(ProductItem/VideoItem/ReviewStatus 等)
- utils.ts: 数据转换函数(mapVideoToProductItem)
- products.ts: 全部 7 个 API 函数
- index.ts: 统一入口 re-export,保持 @/api/products 路径向后兼容

主入口从 174 行减少到约 40 行,按职责清晰分层。
2026-07-26 17:07:23 +08:00
10 changed files with 425 additions and 415 deletions
-174
View File
@@ -1,174 +0,0 @@
/**
* 成品 / 视频相关 API
* 后端实际接口:/videos
*/
import apiClient from "./client"
/** 复核状态 */
export type ReviewStatus = "pending_review" | "approved" | "rejected"
/** 成品条目 */
export interface ProductItem {
id: string
title: string
video_url?: string
thumbnail_url?: string
duration_seconds?: number
file_size?: number
resolution?: string
status: "processing" | "completed" | "failed"
/** 复核状态 */
review_status?: ReviewStatus
/** 所属项目 ID */
project_id?: string
/** 所属项目名称 */
project_name?: string
/** 查重率(百分比) */
duplicate_rate?: number
created_at?: string
updated_at?: string
}
/** 列表查询参数 */
export interface ProductListParams {
page?: number
page_size?: number
project_id?: string
review_status?: ReviewStatus | "all"
}
/** 分页响应 */
export interface ProductListResponse {
items: ProductItem[]
total: number
page: number
page_size: number
}
/** 批量下载任务状态 */
export interface BatchDownloadStatus {
job_id: string
status: "processing" | "completed" | "failed"
/** 完成后返回的下载 URL */
download_url?: string
/** 进度百分比 */
progress?: number
}
/** 后端 /videos 接口返回的原始视频条目 */
interface VideoItem {
id: string
project_id: string
generation_task_id: string
name: string
file_url: string
file_size: number
duration: number
thumbnail_url: string | null
width: number
height: number
fps: number
status: string
review_status: ReviewStatus
generation_params: Record<string, unknown>
download_url: string
generated_at: string
}
/**
* 将后端 VideoItem 映射为 ProductItem 格式
*/
function mapVideoToProductItem(video: VideoItem): ProductItem {
return {
id: video.id,
title: video.name || "未命名视频",
// 优先用 download_url(带签名)播放,file_url 无签名无法访问
video_url: video.download_url || video.file_url,
thumbnail_url: video.thumbnail_url || undefined,
duration_seconds: video.duration,
file_size: video.file_size,
resolution: video.width && video.height ? `${video.width}x${video.height}` : undefined,
status:
video.status === "completed"
? "completed"
: video.status === "failed"
? "failed"
: "processing",
review_status: video.review_status,
project_id: video.project_id,
// 后端 /videos 接口暂无 project_name 字段
project_name: undefined,
// 后端字段名为 generated_at,映射为 created_at 供前端统一使用
created_at: video.generated_at,
updated_at: video.generated_at,
// 后端 /videos 接口暂无 duplicate_rate 字段
duplicate_rate: undefined,
}
}
/** 获取成品列表(支持分页和筛选) */
export const getProducts = async (params?: ProductListParams): Promise<ProductItem[]> => {
const response = await apiClient.get("/videos", { params })
const data = response.data
const videos: VideoItem[] = Array.isArray(data?.items)
? data.items
: Array.isArray(data)
? data
: []
return videos.map(mapVideoToProductItem)
}
/** 获取单个成品详情 */
export const getProduct = async (productId: string): Promise<ProductItem> => {
const response = await apiClient.get(`/videos/${productId}`)
return mapVideoToProductItem(response.data as VideoItem)
}
/**
* 删除成品
* 注意:后端暂未实现 /videos DELETE 接口,调用会返回 405
* 待后端实现后自动生效
*/
export const deleteProduct = async (productId: string): Promise<void> => {
await apiClient.delete(`/videos/${productId}`)
}
/**
* 获取成品下载链接
* 直接使用列表返回的 download_url(带OSS签名)
*/
export const getProductDownloadUrl = async (
productId: string,
): Promise<{ url: string; expires_at: string }> => {
// 优先从列表缓存取;如果没有则调详情接口
const product = await getProduct(productId)
if (!product.video_url) throw new Error("下载链接不可用")
return { url: product.video_url, expires_at: "" }
}
/** 更新复核状态 — TODO: 后端暂无对应端点,暂存本地状态 */
export const updateReviewStatus = async (
productId: string,
status: ReviewStatus,
): Promise<ProductItem> => {
// 后端暂无 /videos/{id}/review 端点
// 暂时返回当前状态,后续可扩展
const product = await getProduct(productId)
return { ...product, review_status: status }
}
/** 发起批量下载 — TODO: 后端暂无对应端点 */
export const batchDownload = async (videoIds: string[]): Promise<{ job_id: string }> => {
// 后端暂无 /videos/batch-download 端点
// 暂时返回模拟 job_id,后续可扩展
console.warn("[batchDownload] 后端暂无批量下载端点", videoIds)
return { job_id: `mock-${Date.now()}` }
}
/** 查询批量下载状态 — TODO: 后端暂无对应端点 */
export const getBatchDownloadStatus = async (jobId: string): Promise<BatchDownloadStatus> => {
// 后端暂无 /videos/batch-download/{jobId} 端点
// 暂时返回模拟状态,后续可扩展
console.warn("[getBatchDownloadStatus] 后端暂无批量下载状态端点", jobId)
return { job_id: jobId, status: "processing", progress: 0 }
}
+28
View File
@@ -0,0 +1,28 @@
/**
* 成品 / 视频相关 API — 目录化入口
* 保持与原 products.ts 相同导出,向后兼容
*/
// 类型
export type {
ReviewStatus,
ProductItem,
ProductListParams,
ProductListResponse,
BatchDownloadStatus,
VideoItem,
} from "./types"
// 工具函数
export { mapVideoToProductItem } from "./utils"
// API 函数
export {
getProducts,
getProduct,
deleteProduct,
getProductDownloadUrl,
updateReviewStatus,
batchDownload,
getBatchDownloadStatus,
} from "./products"
+80
View File
@@ -0,0 +1,80 @@
/**
* 成品 / 视频相关 API 函数
* 后端实际接口:/videos
*/
import apiClient from "../client"
import type {
BatchDownloadStatus,
ProductItem,
ProductListParams,
VideoItem,
ReviewStatus,
} from "./types"
import { mapVideoToProductItem } from "./utils"
/** 获取成品列表(支持分页和筛选) */
export const getProducts = async (params?: ProductListParams): Promise<ProductItem[]> => {
const response = await apiClient.get("/videos", { params })
const data = response.data
const videos: VideoItem[] = Array.isArray(data?.items)
? data.items
: Array.isArray(data)
? data
: []
return videos.map(mapVideoToProductItem)
}
/** 获取单个成品详情 */
export const getProduct = async (productId: string): Promise<ProductItem> => {
const response = await apiClient.get(`/videos/${productId}`)
return mapVideoToProductItem(response.data as VideoItem)
}
/**
* 删除成品
* 注意:后端暂未实现 /videos DELETE 接口,调用会返回 405
* 待后端实现后自动生效
*/
export const deleteProduct = async (productId: string): Promise<void> => {
await apiClient.delete(`/videos/${productId}`)
}
/**
* 获取成品下载链接
* 直接使用列表返回的 download_url(带OSS签名)
*/
export const getProductDownloadUrl = async (
productId: string,
): Promise<{ url: string; expires_at: string }> => {
// 优先从列表缓存取;如果没有则调详情接口
const product = await getProduct(productId)
if (!product.video_url) throw new Error("下载链接不可用")
return { url: product.video_url, expires_at: "" }
}
/** 更新复核状态 — TODO: 后端暂无对应端点,暂存本地状态 */
export const updateReviewStatus = async (
productId: string,
status: ReviewStatus,
): Promise<ProductItem> => {
// 后端暂无 /videos/{id}/review 端点
// 暂时返回当前状态,后续可扩展
const product = await getProduct(productId)
return { ...product, review_status: status }
}
/** 发起批量下载 — TODO: 后端暂无对应端点 */
export const batchDownload = async (videoIds: string[]): Promise<{ job_id: string }> => {
// 后端暂无 /videos/batch-download 端点
// 暂时返回模拟 job_id,后续可扩展
console.warn("[batchDownload] 后端暂无批量下载端点", videoIds)
return { job_id: `mock-${Date.now()}` }
}
/** 查询批量下载状态 — TODO: 后端暂无对应端点 */
export const getBatchDownloadStatus = async (jobId: string): Promise<BatchDownloadStatus> => {
// 后端暂无 /videos/batch-download/{jobId} 端点
// 暂时返回模拟状态,后续可扩展
console.warn("[getBatchDownloadStatus] 后端暂无批量下载状态端点", jobId)
return { job_id: jobId, status: "processing", progress: 0 }
}
+74
View File
@@ -0,0 +1,74 @@
/**
* 成品 / 视频相关类型定义
*/
/** 复核状态 */
export type ReviewStatus = "pending_review" | "approved" | "rejected"
/** 成品条目 */
export interface ProductItem {
id: string
title: string
video_url?: string
thumbnail_url?: string
duration_seconds?: number
file_size?: number
resolution?: string
status: "processing" | "completed" | "failed"
/** 复核状态 */
review_status?: ReviewStatus
/** 所属项目 ID */
project_id?: string
/** 所属项目名称 */
project_name?: string
/** 查重率(百分比) */
duplicate_rate?: number
created_at?: string
updated_at?: string
}
/** 列表查询参数 */
export interface ProductListParams {
page?: number
page_size?: number
project_id?: string
review_status?: ReviewStatus | "all"
}
/** 分页响应 */
export interface ProductListResponse {
items: ProductItem[]
total: number
page: number
page_size: number
}
/** 批量下载任务状态 */
export interface BatchDownloadStatus {
job_id: string
status: "processing" | "completed" | "failed"
/** 完成后返回的下载 URL */
download_url?: string
/** 进度百分比 */
progress?: number
}
/** 后端 /videos 接口返回的原始视频条目 */
export interface VideoItem {
id: string
project_id: string
generation_task_id: string
name: string
file_url: string
file_size: number
duration: number
thumbnail_url: string | null
width: number
height: number
fps: number
status: string
review_status: ReviewStatus
generation_params: Record<string, unknown>
download_url: string
generated_at: string
}
+35
View File
@@ -0,0 +1,35 @@
/**
* 成品数据转换工具函数
*/
import type { ProductItem, VideoItem } from "./types"
/**
* 将后端 VideoItem 映射为 ProductItem 格式
*/
export function mapVideoToProductItem(video: VideoItem): ProductItem {
return {
id: video.id,
title: video.name || "未命名视频",
// 优先用 download_url(带签名)播放,file_url 无签名无法访问
video_url: video.download_url || video.file_url,
thumbnail_url: video.thumbnail_url || undefined,
duration_seconds: video.duration,
file_size: video.file_size,
resolution: video.width && video.height ? `${video.width}x${video.height}` : undefined,
status:
video.status === "completed"
? "completed"
: video.status === "failed"
? "failed"
: "processing",
review_status: video.review_status,
project_id: video.project_id,
// 后端 /videos 接口暂无 project_name 字段
project_name: undefined,
// 后端字段名为 generated_at,映射为 created_at 供前端统一使用
created_at: video.generated_at,
updated_at: video.generated_at,
// 后端 /videos 接口暂无 duplicate_rate 字段
duplicate_rate: undefined,
}
}
+208
View File
@@ -0,0 +1,208 @@
/**
* 音色克隆 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
}
-87
View File
@@ -1,87 +0,0 @@
/**
* 音色克隆 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
}
-32
View File
@@ -1,32 +0,0 @@
/**
* 音色克隆 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"
-92
View File
@@ -1,92 +0,0 @@
/**
* 音色克隆类型定义
*/
/** 克隆音色状态(前端展示用) */
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
}
-30
View File
@@ -1,30 +0,0 @@
/**
* 音色克隆工具函数
*/
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")}`
}