Compare commits

..

2 Commits

Author SHA1 Message Date
xiaoxia 42ef2d5ad2 Merge branch 'develop' into refactor/voices-api
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 7s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 31s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m53s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m45s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 23s
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 API Image (pull_request) Successful in 1m53s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 2m51s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 38s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 38s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m5s
AI Code Review / AI Code Review (pull_request) Successful in 1m59s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 15m14s
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 / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 19s
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 18s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
2026-07-27 12:34:56 +08:00
xiaoxia 35f1939030 refactor(api): 拆分 voices.ts 为目录结构(types/voices/index)
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 15s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 35s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m31s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m31s
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 31s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 2m20s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 43s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 45s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 23s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 2m26s
AI Code Review / AI Code Review (pull_request) Successful in 3m8s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 5m45s
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 39s
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
将 145 行的 voices.ts 拆分为目录化结构:
- types.ts: 类型定义(统一音色/预设音色/旧接口类型)
- voices.ts: 全部 7 个 API 函数(新统一音色 + 旧接口兼容)
- index.ts: 统一入口 re-export,保持 @/api/voices 路径向后兼容

主入口从 145 行减少到约 30 行,按职责清晰分层。
2026-07-26 17:11:18 +08:00
8 changed files with 290 additions and 301 deletions
+174
View File
@@ -0,0 +1,174 @@
/**
* 成品 / 视频相关 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
@@ -1,28 +0,0 @@
/**
* 成品 / 视频相关 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
@@ -1,80 +0,0 @@
/**
* 成品 / 视频相关 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
@@ -1,74 +0,0 @@
/**
* 成品 / 视频相关类型定义
*/
/** 复核状态 */
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
@@ -1,35 +0,0 @@
/**
* 成品数据转换工具函数
*/
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,
}
}
+26
View File
@@ -0,0 +1,26 @@
/**
* 配音相关 API — 目录化入口
* 保持与原 voices.ts 相同导出,向后兼容
*/
// 类型
export type {
UnifiedVoiceItem,
UnifiedVoiceListResponse,
PresetVoiceItem,
PresetVoiceListResponse,
UnifiedVoiceListParams,
VoiceItem,
CreateVoiceRequest,
} from "./types"
// API 函数
export {
fetchVoices,
fetchPresetVoices,
getVoices,
createVoice,
updateVoice,
deleteVoice,
generateAIVoice,
} from "./voices"
+81
View File
@@ -0,0 +1,81 @@
/**
* 配音相关类型定义
*/
/** 统一音色条目(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 interface VoiceItem {
id: string
name: string
text: string
voice_type?: string
duration_seconds?: number
storage_key?: string
audio_url?: string
status?: string
is_favorite?: boolean
created_at?: string
updated_at?: string
}
/** 创建配音请求(旧) */
export interface CreateVoiceRequest {
name: string
text: string
voice_type?: string
}
@@ -1,68 +1,15 @@
/**
* API
* Phase 1
*
* API
* 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
}
import apiClient from "../client"
import type {
CreateVoiceRequest,
PresetVoiceListResponse,
UnifiedVoiceListParams,
UnifiedVoiceListResponse,
VoiceItem,
} from "./types"
/** 获取统一音色列表(推荐) */
export const fetchVoices = async (
@@ -86,28 +33,6 @@ export const fetchPresetVoices = async (): Promise<PresetVoiceListResponse> => {
/* ── 向后兼容(旧接口) ────────────────────────────────── */
/** 配音条目(旧) */
export interface VoiceItem {
id: string
name: string
text: string
voice_type?: string
duration_seconds?: number
storage_key?: string
audio_url?: string
status?: string
is_favorite?: boolean
created_at?: string
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/legacy")