ci: Prettier纳入两层防御体系 (#520)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Successful in 1m6s
CI Build & Deploy Pipeline / Build Staging Web Image (push) Successful in 1m49s
CI Build & Deploy Pipeline / Build Production API Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Production Web Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Production Worker Image (push) Has been skipped
CI Build & Deploy Pipeline / Deploy Production (push) Has been skipped
CI Build & Deploy Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Validate Code Quality And Tests (push) Successful in 3m1s
CI/CD Pipeline / Unit Tests (push) Successful in 3m20s
CI/CD Pipeline / Integration Tests (push) Successful in 1m29s
CI Build & Deploy Pipeline / Build Staging API Image (push) Successful in 7m4s
CI Build & Deploy Pipeline / Build Staging Worker Image (push) Successful in 7m54s
CI Build & Deploy Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 45s
CI Build & Deploy Pipeline / Staging E2E Tests (push) Failing after 2m50s
CI Build & Deploy Pipeline / Staging API Integration Tests (push) Successful in 3m28s

Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
This commit was merged in pull request #520.
This commit is contained in:
2026-07-18 18:08:19 +08:00
committed by auto-approve-bot
parent 1ee779a566
commit 0fcb77b991
145 changed files with 10310 additions and 12869 deletions
+219 -242
View File
@@ -2,163 +2,157 @@
* 素材相关 API
* Phase 1 重构:去掉 project_id,素材直接归属用户
*/
import apiClient from "./client";
import { getOrCreateDefaultProject } from "./projects";
import apiClient from "./client"
import { getOrCreateDefaultProject } from "./projects"
/** 素材元数据 */
export interface AssetMetadata {
/** 时长(秒) */
duration?: number;
duration?: number
/** 宽度(像素) */
width?: number;
width?: number
/** 高度(像素) */
height?: number;
height?: number
/** 比特率(bps */
bitrate?: number;
bitrate?: number
/** 编码格式 */
codec?: string;
codec?: string
/** 帧率 */
fps?: number;
fps?: number
/** 采样率(Hz */
sample_rate?: number;
sample_rate?: number
/** 声道数 */
channels?: number;
channels?: number
/** 其他扩展字段 */
[key: string]: unknown;
[key: string]: unknown
}
/** 素材分类状态 */
export type AssetClassificationStatus =
"pending" | "processing" | "completed" | "failed";
export type AssetClassificationStatus = "pending" | "processing" | "completed" | "failed"
/** 素材条目 */
export interface AssetItem {
id: string;
library_id: string;
name: string;
storage_key: string;
mime_type: string;
metadata: AssetMetadata;
file_size?: number;
file_url?: string;
thumbnail_url?: string;
id: string
library_id: string
name: string
storage_key: string
mime_type: string
metadata: AssetMetadata
file_size?: number
file_url?: string
thumbnail_url?: string
/** 时长(秒),视频/音频素材由后端从 metadata 提取到顶层 */
duration?: number;
status?: string;
classification_status?: AssetClassificationStatus | null;
quality_score?: number | null;
tag_ids?: string[];
created_at?: string;
duration?: number
status?: string
classification_status?: AssetClassificationStatus | null
quality_score?: number | null
tag_ids?: string[]
created_at?: string
}
/** 素材库 */
export interface AssetLibraryItem {
id: string;
name: string;
kind: "video" | "voice" | "image";
asset_count?: number;
total_size?: number;
created_at?: string;
id: string
name: string
kind: "video" | "voice" | "image"
asset_count?: number
total_size?: number
created_at?: string
}
/** 入库任务 */
export interface IngestJob {
id: string;
library_id: string;
storage_key: string;
status: "pending" | "processing" | "completed" | "failed";
error_message: string;
result_asset_id: string;
id: string
library_id: string
storage_key: string
status: "pending" | "processing" | "completed" | "failed"
error_message: string
result_asset_id: string
}
/** 分类任务 */
export interface ClassificationJob {
id: string;
asset_id: string;
status: "pending" | "processing" | "completed" | "failed";
classification: string;
confidence: number;
error_message: string;
id: string
asset_id: string
status: "pending" | "processing" | "completed" | "failed"
classification: string
confidence: number
error_message: string
}
/** 素材诊断信息 */
export interface AssetDiagnosis {
readiness_score: number;
readiness_label: string;
total_assets: number;
ready_assets: number;
video_assets: number;
image_assets: number;
voice_assets: number;
total_duration_seconds: number;
estimated_video_count: number;
used_assets: number;
unused_assets: number;
pending_review_assets: number;
readiness_score: number
readiness_label: string
total_assets: number
ready_assets: number
video_assets: number
image_assets: number
voice_assets: number
total_duration_seconds: number
estimated_video_count: number
used_assets: number
unused_assets: number
pending_review_assets: number
smart_views: Array<{
key: string;
label: string;
count: number;
description: string;
}>;
key: string
label: string
count: number
description: string
}>
gaps: Array<{
key: string;
severity: "critical" | "warning" | "info";
message: string;
recommendation: string;
}>;
key: string
severity: "critical" | "warning" | "info"
message: string
recommendation: string
}>
}
// ─── 素材诊断 ──────────────────────────────────────────────
/** 获取素材诊断信息(可选 asset_id 查单素材,否则全局诊断) */
export const getAssetDiagnosis = async (
assetId?: string,
): Promise<AssetDiagnosis> => {
const params: Record<string, string> = {};
if (assetId) params.asset_id = assetId;
const response = await apiClient.get("/asset-diagnosis", { params });
return response.data;
};
export const getAssetDiagnosis = async (assetId?: string): Promise<AssetDiagnosis> => {
const params: Record<string, string> = {}
if (assetId) params.asset_id = assetId
const response = await apiClient.get("/asset-diagnosis", { params })
return response.data
}
// ─── 素材库 ────────────────────────────────────────────────
/** 获取当前用户的所有素材库 */
export const getAssetLibraries = async (): Promise<AssetLibraryItem[]> => {
const response = await apiClient.get("/asset-libraries");
return response.data.items || [];
};
const response = await apiClient.get("/asset-libraries")
return response.data.items || []
}
/** 创建素材库(自动获取或创建默认项目以提供 project_id */
export const createAssetLibrary = async (data: {
name: string;
kind: "video" | "voice" | "image";
name: string
kind: "video" | "voice" | "image"
}): Promise<AssetLibraryItem> => {
// 后端要求 project_id,前端自动管理默认项目
const project = await getOrCreateDefaultProject();
const project = await getOrCreateDefaultProject()
const response = await apiClient.post("/asset-libraries", {
project_id: project.id,
...data,
});
return response.data;
};
})
return response.data
}
/** 确保项目下指定 kind 的默认素材库存在(不存在则自动创建) */
export const ensureDefaultLibrary = async (data: {
project_id: string;
kind: "video" | "voice" | "image";
project_id: string
kind: "video" | "voice" | "image"
}): Promise<AssetLibraryItem> => {
const response = await apiClient.post(
"/asset-libraries/ensure-default",
data,
);
return response.data;
};
const response = await apiClient.post("/asset-libraries/ensure-default", data)
return response.data
}
/** 删除素材库 */
export const deleteAssetLibrary = async (libraryId: string): Promise<void> => {
await apiClient.delete(`/asset-libraries/${libraryId}`);
};
await apiClient.delete(`/asset-libraries/${libraryId}`)
}
// ─── 素材 ──────────────────────────────────────────────────
@@ -166,49 +160,49 @@ export const deleteAssetLibrary = async (libraryId: string): Promise<void> => {
export const getAssets = async (libraryId: string): Promise<AssetItem[]> => {
const response = await apiClient.get("/assets", {
params: { library_id: libraryId },
});
return response.data.items || [];
};
})
return response.data.items || []
}
/** 按类型获取素材(如 voice/video/image),支持可选筛选 */
export const getAssetsByKind = async (
kind: string,
filters?: {
keyword?: string;
gender?: string;
style?: string;
tag_ids?: string[];
keyword?: string
gender?: string
style?: string
tag_ids?: string[]
},
): Promise<AssetItem[]> => {
const params: Record<string, string> = { kind };
if (filters?.keyword) params.keyword = filters.keyword;
if (filters?.gender) params.gender = filters.gender;
if (filters?.style) params.style = filters.style;
if (filters?.tag_ids?.length) params.tag_ids = filters.tag_ids.join(",");
const response = await apiClient.get("/assets", { params });
return response.data.items || [];
};
const params: Record<string, string> = { kind }
if (filters?.keyword) params.keyword = filters.keyword
if (filters?.gender) params.gender = filters.gender
if (filters?.style) params.style = filters.style
if (filters?.tag_ids?.length) params.tag_ids = filters.tag_ids.join(",")
const response = await apiClient.get("/assets", { params })
return response.data.items || []
}
/** 创建素材(上传文件后调用,附带 metadata) */
export const createAsset = async (data: {
library_id: string;
name: string;
storage_key: string;
mime_type: string;
metadata?: AssetMetadata;
library_id: string
name: string
storage_key: string
mime_type: string
metadata?: AssetMetadata
}): Promise<AssetItem> => {
const response = await apiClient.post("/assets", data);
return response.data;
};
const response = await apiClient.post("/assets", data)
return response.data
}
/** 更新素材(名称、metadata 等) */
export const updateAsset = async (
assetId: string,
data: { name?: string; metadata?: AssetMetadata },
): Promise<AssetItem> => {
const response = await apiClient.put(`/assets/${assetId}`, data);
return response.data;
};
const response = await apiClient.put(`/assets/${assetId}`, data)
return response.data
}
/** 更新素材审核状态 */
export const updateAssetReviewStatus = async (
@@ -217,14 +211,14 @@ export const updateAssetReviewStatus = async (
): Promise<AssetItem> => {
const response = await apiClient.patch(`/assets/${assetId}/review`, {
review_status: reviewStatus,
});
return response.data;
};
})
return response.data
}
/** 删除素材 */
export const deleteAsset = async (assetId: string): Promise<void> => {
await apiClient.delete(`/assets/${assetId}`);
};
await apiClient.delete(`/assets/${assetId}`)
}
// ─── 上传 ──────────────────────────────────────────────────
@@ -235,47 +229,47 @@ export const uploadAsset = async (
const response = await apiClient.post("/upload", formData, {
headers: { "Content-Type": "multipart/form-data" },
timeout: 30 * 60 * 1000,
});
return response.data;
};
})
return response.data
}
/** 预签名直传准备 */
export const prepareDirectUpload = async (data: {
project_id: string;
library_id: string;
filename: string;
content_type: string;
file_size: number;
project_id: string
library_id: string
filename: string
content_type: string
file_size: number
}): Promise<{
upload_url: string;
method: string;
storage_key: string;
expires_at: string;
fields: Record<string, string>;
max_size_bytes: number;
upload_url: string
method: string
storage_key: string
expires_at: string
fields: Record<string, string>
max_size_bytes: number
}> => {
const response = await apiClient.post("/upload/direct/prepare", data);
return response.data;
};
const response = await apiClient.post("/upload/direct/prepare", data)
return response.data
}
/** 直传完成确认 */
export const completeDirectUpload = async (data: {
project_id: string;
library_id: string;
storage_key: string;
project_id: string
library_id: string
storage_key: string
}): Promise<{ storage_key: string; ingest_job_id: string }> => {
const response = await apiClient.post("/upload/direct/complete", data);
return response.data;
};
const response = await apiClient.post("/upload/direct/complete", data)
return response.data
}
/** 直传上传(大文件推荐),支持可选进度回调 */
export const uploadAssetDirect = async (data: {
file: File;
library_id: string;
onProgress?: (percent: number) => void;
file: File
library_id: string
onProgress?: (percent: number) => void
}): Promise<{ storage_key: string; ingest_job_id: string }> => {
// 后端要求 project_id,前端自动获取默认项目
const project = await getOrCreateDefaultProject();
const project = await getOrCreateDefaultProject()
const prepared = await prepareDirectUpload({
project_id: project.id,
@@ -283,166 +277,149 @@ export const uploadAssetDirect = async (data: {
filename: data.file.name,
content_type: data.file.type || "application/octet-stream",
file_size: data.file.size,
});
})
const directForm = new FormData();
Object.entries(prepared.fields).forEach(([key, value]) =>
directForm.append(key, value),
);
directForm.append("file", data.file);
const directForm = new FormData()
Object.entries(prepared.fields).forEach(([key, value]) => directForm.append(key, value))
directForm.append("file", data.file)
// 使用 XMLHttpRequest 以获取上传进度 + 超时控制 + 详细错误诊断
await new Promise<void>((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(prepared.method, prepared.upload_url);
const xhr = new XMLHttpRequest()
xhr.open(prepared.method, prepared.upload_url)
// 超时 10 分钟
xhr.timeout = 10 * 60 * 1000;
xhr.timeout = 10 * 60 * 1000
xhr.upload.onprogress = (e) => {
if (e.lengthComputable && data.onProgress) {
data.onProgress(Math.round((e.loaded / e.total) * 100));
data.onProgress(Math.round((e.loaded / e.total) * 100))
}
};
}
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve();
resolve()
} else {
// 解析 OSS 返回的 XML 错误信息
let ossError = "";
let ossError = ""
try {
const codeMatch = xhr.responseText.match(/<Code>([^<]+)<\/Code>/);
const msgMatch = xhr.responseText.match(
/<Message>([^<]+)<\/Message>/,
);
const codeMatch = xhr.responseText.match(/<Code>([^<]+)<\/Code>/)
const msgMatch = xhr.responseText.match(/<Message>([^<]+)<\/Message>/)
if (codeMatch || msgMatch) {
ossError = ` [OSS: ${codeMatch?.[1] || "unknown"} - ${msgMatch?.[1] || "unknown"}]`;
ossError = ` [OSS: ${codeMatch?.[1] || "unknown"} - ${msgMatch?.[1] || "unknown"}]`
}
} catch {
// 无法解析响应体
}
const detail = `OSS 直传失败: HTTP ${xhr.status} ${xhr.statusText}${ossError}`;
const detail = `OSS 直传失败: HTTP ${xhr.status} ${xhr.statusText}${ossError}`
console.error("[OSS Upload] 直传失败:", {
url: prepared.upload_url,
storage_key: prepared.storage_key,
status: xhr.status,
statusText: xhr.statusText,
});
reject(new Error(detail));
})
reject(new Error(detail))
}
};
}
xhr.onerror = () => {
console.error("[OSS Upload] 网络错误:", {
url: prepared.upload_url,
storage_key: prepared.storage_key,
});
reject(new Error("OSS 上传网络错误,请检查网络连接"));
};
})
reject(new Error("OSS 上传网络错误,请检查网络连接"))
}
xhr.ontimeout = () => {
console.error("[OSS Upload] 上传超时:", {
url: prepared.upload_url,
storage_key: prepared.storage_key,
});
reject(new Error("OSS 上传超时(10分钟),请检查网络或尝试更小的文件"));
};
xhr.send(directForm);
});
})
reject(new Error("OSS 上传超时(10分钟),请检查网络或尝试更小的文件"))
}
xhr.send(directForm)
})
return completeDirectUpload({
project_id: project.id,
library_id: data.library_id,
storage_key: prepared.storage_key,
});
};
})
}
// ─── 入库 / 分类任务 ───────────────────────────────────────
/** 查询入库任务状态 */
export const getIngestJob = async (jobId: string): Promise<IngestJob> => {
const response = await apiClient.get(`/ingest-jobs/${jobId}`);
return response.data;
};
const response = await apiClient.get(`/ingest-jobs/${jobId}`)
return response.data
}
/** 提交素材分类任务 */
export const submitClassificationJob = async (data: {
asset_id: string;
asset_id: string
}): Promise<ClassificationJob> => {
const response = await apiClient.post("/classification-jobs", data);
return response.data;
};
const response = await apiClient.post("/classification-jobs", data)
return response.data
}
/** 查询分类任务状态 */
export const getClassificationJob = async (
jobId: string,
): Promise<ClassificationJob> => {
const response = await apiClient.get(`/classification-jobs/${jobId}`);
return response.data;
};
export const getClassificationJob = async (jobId: string): Promise<ClassificationJob> => {
const response = await apiClient.get(`/classification-jobs/${jobId}`)
return response.data
}
// ─── 批量操作 ───────────────────────────────────────────────
/** 批量操作结果 */
export interface BatchOperationResult {
succeeded: string[];
failed: string[];
total: number;
success_count: number;
failure_count: number;
succeeded: string[]
failed: string[]
total: number
success_count: number
failure_count: number
}
/** 统一批量操作结果归一化,防御后端字段缺失或格式不一致 */
const normalizeBatchResult = (
raw: Record<string, unknown>,
): BatchOperationResult => {
const succeeded = Array.isArray(raw.succeeded)
? (raw.succeeded as string[])
: [];
const failed = Array.isArray(raw.failed) ? (raw.failed as string[]) : [];
const success_count =
typeof raw.success_count === "number"
? raw.success_count
: succeeded.length;
const failure_count =
typeof raw.failure_count === "number" ? raw.failure_count : failed.length;
const total =
typeof raw.total === "number" ? raw.total : success_count + failure_count;
return { succeeded, failed, total, success_count, failure_count };
};
const normalizeBatchResult = (raw: Record<string, unknown>): BatchOperationResult => {
const succeeded = Array.isArray(raw.succeeded) ? (raw.succeeded as string[]) : []
const failed = Array.isArray(raw.failed) ? (raw.failed as string[]) : []
const success_count = typeof raw.success_count === "number" ? raw.success_count : succeeded.length
const failure_count = typeof raw.failure_count === "number" ? raw.failure_count : failed.length
const total = typeof raw.total === "number" ? raw.total : success_count + failure_count
return { succeeded, failed, total, success_count, failure_count }
}
/** 批量删除素材 */
export const batchDeleteAssets = async (
assetIds: string[],
): Promise<BatchOperationResult> => {
export const batchDeleteAssets = async (assetIds: string[]): Promise<BatchOperationResult> => {
const response = await apiClient.post("/assets/batch-delete", {
asset_ids: assetIds,
});
return normalizeBatchResult((response.data || {}) as Record<string, unknown>);
};
})
return normalizeBatchResult((response.data || {}) as Record<string, unknown>)
}
/** 批量打标签 */
export const batchTagAssets = async (data: {
asset_ids: string[];
tags: string[];
mode: "add" | "replace";
asset_ids: string[]
tags: string[]
mode: "add" | "replace"
}): Promise<BatchOperationResult> => {
const response = await apiClient.post("/assets/batch-tag", data);
return normalizeBatchResult((response.data || {}) as Record<string, unknown>);
};
const response = await apiClient.post("/assets/batch-tag", data)
return normalizeBatchResult((response.data || {}) as Record<string, unknown>)
}
/** 批量改分类 */
export const batchClassifyAssets = async (data: {
asset_ids: string[];
category: string;
asset_ids: string[]
category: string
}): Promise<BatchOperationResult> => {
const response = await apiClient.post("/assets/batch-classify", data);
return normalizeBatchResult((response.data || {}) as Record<string, unknown>);
};
const response = await apiClient.post("/assets/batch-classify", data)
return normalizeBatchResult((response.data || {}) as Record<string, unknown>)
}
/** 批量智能标记 */
export const batchMarkAssets = async (data: {
asset_ids: string[];
smart_view: "recommended" | "caution" | "high_risk";
asset_ids: string[]
smart_view: "recommended" | "caution" | "high_risk"
}): Promise<BatchOperationResult> => {
const response = await apiClient.post("/assets/batch-mark", data);
return normalizeBatchResult((response.data || {}) as Record<string, unknown>);
};
const response = await apiClient.post("/assets/batch-mark", data)
return normalizeBatchResult((response.data || {}) as Record<string, unknown>)
}
+7 -7
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { normalizeUser } from "./auth";
import { describe, expect, it } from "vitest"
import { normalizeUser } from "./auth"
describe("normalizeUser", () => {
it("normalizes canonical API current-user fields", () => {
@@ -20,8 +20,8 @@ describe("normalizeUser", () => {
is_email_verified: true,
email_verified: true,
created_at: undefined,
});
});
})
})
it("keeps compatibility with legacy UI-shaped user fields", () => {
expect(
@@ -42,6 +42,6 @@ describe("normalizeUser", () => {
is_email_verified: false,
email_verified: false,
created_at: "2026-06-22T00:00:00Z",
});
});
});
})
})
})
+64 -72
View File
@@ -1,58 +1,58 @@
/**
* 认证相关 API
*/
import axios from "axios";
import apiClient from "./client";
import axios from "axios"
import apiClient from "./client"
// 类型定义
export interface LoginRequest {
email: string;
password: string;
email: string
password: string
}
export interface LoginResponse {
access_token: string;
refresh_token?: string | null;
token_type: string;
expires_in: number;
user_id: string;
email: string;
username: string;
display_name: string;
access_token: string
refresh_token?: string | null
token_type: string
expires_in: number
user_id: string
email: string
username: string
display_name: string
}
export interface RegisterRequest {
email: string;
password: string;
username: string;
display_name?: string;
email: string
password: string
username: string
display_name?: string
}
export interface User {
id: string;
user_id: string;
email: string;
username: string;
display_name: string;
is_email_verified: boolean;
email_verified: boolean;
created_at?: string;
id: string
user_id: string
email: string
username: string
display_name: string
is_email_verified: boolean
email_verified: boolean
created_at?: string
}
export interface UserResponse {
id?: string;
user_id?: string;
email: string;
username: string;
display_name: string;
is_email_verified?: boolean;
email_verified?: boolean;
created_at?: string;
id?: string
user_id?: string
email: string
username: string
display_name: string
is_email_verified?: boolean
email_verified?: boolean
created_at?: string
}
export const normalizeUser = (data: UserResponse): User => {
const userId = data.id ?? data.user_id ?? "";
const emailVerified = data.is_email_verified ?? data.email_verified ?? false;
const userId = data.id ?? data.user_id ?? ""
const emailVerified = data.is_email_verified ?? data.email_verified ?? false
return {
id: userId,
@@ -63,52 +63,46 @@ export const normalizeUser = (data: UserResponse): User => {
is_email_verified: emailVerified,
email_verified: emailVerified,
created_at: data.created_at,
};
};
}
}
// 登录
export const login = async (data: LoginRequest): Promise<LoginResponse> => {
const response = await apiClient.post("/auth/login", data);
return response.data;
};
const response = await apiClient.post("/auth/login", data)
return response.data
}
// 刷新 access_token(使用裸 axios 避免拦截器递归)
export const refreshAccessToken = async (
refreshToken: string,
): Promise<LoginResponse> => {
const baseURL = apiClient.defaults.baseURL ?? "";
export const refreshAccessToken = async (refreshToken: string): Promise<LoginResponse> => {
const baseURL = apiClient.defaults.baseURL ?? ""
const response = await axios.post(`${baseURL}/auth/refresh`, {
refresh_token: refreshToken,
});
return response.data;
};
})
return response.data
}
// 注册
export const register = async (
data: RegisterRequest,
): Promise<{ message: string }> => {
const response = await apiClient.post("/auth/register", data);
return response.data;
};
export const register = async (data: RegisterRequest): Promise<{ message: string }> => {
const response = await apiClient.post("/auth/register", data)
return response.data
}
// 登出
export const logout = async (): Promise<void> => {
await apiClient.post("/auth/logout");
};
await apiClient.post("/auth/logout")
}
// 获取当前用户
export const getCurrentUser = async (): Promise<User> => {
const response = await apiClient.get<UserResponse>("/auth/me");
return normalizeUser(response.data);
};
const response = await apiClient.get<UserResponse>("/auth/me")
return normalizeUser(response.data)
}
// 请求密码重置
export const requestPasswordReset = async (
email: string,
): Promise<{ message: string }> => {
const response = await apiClient.post("/auth/forgot-password", { email });
return response.data;
};
export const requestPasswordReset = async (email: string): Promise<{ message: string }> => {
const response = await apiClient.post("/auth/forgot-password", { email })
return response.data
}
// 重置密码
export const resetPassword = async (
@@ -118,14 +112,12 @@ export const resetPassword = async (
const response = await apiClient.post("/auth/reset-password", {
token,
new_password: newPassword,
});
return response.data;
};
})
return response.data
}
// 验证邮箱
export const verifyEmail = async (
token: string,
): Promise<{ message: string }> => {
const response = await apiClient.post("/auth/verify-email", { token });
return response.data;
};
export const verifyEmail = async (token: string): Promise<{ message: string }> => {
const response = await apiClient.post("/auth/verify-email", { token })
return response.data
}
+25 -27
View File
@@ -2,48 +2,48 @@
* BGM 预设音乐 API
* 对接后端 BGM 混音能力:预设列表查询(按风格分类 + 关键词搜索)
*/
import apiClient from "./client";
import apiClient from "./client"
/* ──────────── 类型 ──────────── */
/** BGM 风格分类 */
export type BgmCategory = "轻快" | "治愈" | "科技" | "电商";
export type BgmCategory = "轻快" | "治愈" | "科技" | "电商"
/** BGM 预设项 */
export interface BgmPreset {
id: string;
name: string;
category: BgmCategory;
id: string
name: string
category: BgmCategory
/** 音频文件 URL */
url: string;
url: string
/** 时长(秒) */
duration: number;
duration: number
/** 关键词标签 */
tags: string[];
tags: string[]
/** 封面图 URL */
cover_url?: string;
cover_url?: string
}
/** BGM 预设列表查询参数 */
export interface BgmPresetsQuery {
category?: BgmCategory | string;
keyword?: string;
category?: BgmCategory | string
keyword?: string
}
/** BGM 混音配置(嵌入剪辑计划) */
export interface BgmMixConfig {
/** 是否启用 BGM */
enabled: boolean;
enabled: boolean
/** 选中的 BGM ID */
music_id: string;
music_id: string
/** BGM 音量 0-100 */
volume: number;
volume: number
/** 淡入时长(秒) 0-3 */
fade_in: number;
fade_in: number
/** 淡出时长(秒) 0-3 */
fade_out: number;
fade_out: number
/** 人声闪避(sidechain */
voice_dodge: boolean;
voice_dodge: boolean
}
/** 默认 BGM 混音配置 */
@@ -54,17 +54,15 @@ export const DEFAULT_BGM_MIX_CONFIG: BgmMixConfig = {
fade_in: 0.5,
fade_out: 0.5,
voice_dodge: true,
};
}
/* ──────────── API ──────────── */
/** 获取 BGM 预设列表 */
export const getBgmPresets = async (
params?: BgmPresetsQuery,
): Promise<BgmPreset[]> => {
const searchParams: Record<string, string> = {};
if (params?.category) searchParams.category = params.category;
if (params?.keyword) searchParams.keyword = params.keyword;
const res = await apiClient.get("/bgm/presets", { params: searchParams });
return res.data?.data ?? res.data ?? [];
};
export const getBgmPresets = async (params?: BgmPresetsQuery): Promise<BgmPreset[]> => {
const searchParams: Record<string, string> = {}
if (params?.category) searchParams.category = params.category
if (params?.keyword) searchParams.keyword = params.keyword
const res = await apiClient.get("/bgm/presets", { params: searchParams })
return res.data?.data ?? res.data ?? []
}
+78 -89
View File
@@ -2,10 +2,10 @@
* API 客户端配置
* 封装 Axios 实例,配置拦截器和 Token 管理
*/
import axios, { AxiosError, InternalAxiosRequestConfig } from "axios";
import { message } from "antd";
import { useAuthStore } from "@/store/authStore";
import { refreshAccessToken } from "./auth";
import axios, { AxiosError, InternalAxiosRequestConfig } from "axios"
import { message } from "antd"
import { useAuthStore } from "@/store/authStore"
import { refreshAccessToken } from "./auth"
// 创建 Axios 实例
const apiClient = axios.create({
@@ -14,161 +14,150 @@ const apiClient = axios.create({
headers: {
"Content-Type": "application/json",
},
});
})
// ── Token 刷新队列 ─────────────────────────────────────────
let isRefreshing = false;
let isRefreshing = false
let failedQueue: Array<{
resolve: (value?: unknown) => void;
reject: (reason?: unknown) => void;
}> = [];
resolve: (value?: unknown) => void
reject: (reason?: unknown) => void
}> = []
const processQueue = (error: unknown | null, token: string | null = null) => {
failedQueue.forEach((prom) => {
if (error) {
prom.reject(error);
prom.reject(error)
} else {
prom.resolve(token);
prom.resolve(token)
}
});
failedQueue = [];
};
})
failedQueue = []
}
// 请求拦截器:添加 Token
apiClient.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const token = localStorage.getItem("access_token");
const token = localStorage.getItem("access_token")
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`;
config.headers.Authorization = `Bearer ${token}`
}
return config;
return config
},
(error: AxiosError) => {
return Promise.reject(error);
return Promise.reject(error)
},
);
)
// 响应拦截器:统一错误提示 + 401 自动刷新 Token
apiClient.interceptors.response.use(
(response) => response,
async (
error: AxiosError<{ detail?: string; message?: string; msg?: string }>,
) => {
async (error: AxiosError<{ detail?: string; message?: string; msg?: string }>) => {
const originalRequest = error.config as InternalAxiosRequestConfig & {
_retry?: boolean;
};
_retry?: boolean
}
// 401 → 尝试刷新 Token
if (
error.response?.status === 401 &&
originalRequest &&
!originalRequest._retry
) {
const refreshToken = useAuthStore.getState().refreshToken;
if (error.response?.status === 401 && originalRequest && !originalRequest._retry) {
const refreshToken = useAuthStore.getState().refreshToken
// 无 refresh_token → 直接登出
if (!refreshToken) {
useAuthStore.getState().clearAuth();
window.location.href = "/";
return Promise.reject(error);
useAuthStore.getState().clearAuth()
window.location.href = "/"
return Promise.reject(error)
}
// 已在刷新中 → 排队等待
if (isRefreshing) {
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
failedQueue.push({ resolve, reject })
}).then((token) => {
if (originalRequest.headers) {
originalRequest.headers.Authorization = `Bearer ${token}`;
originalRequest.headers.Authorization = `Bearer ${token}`
}
return apiClient(originalRequest);
});
return apiClient(originalRequest)
})
}
originalRequest._retry = true;
isRefreshing = true;
originalRequest._retry = true
isRefreshing = true
try {
const data = await refreshAccessToken(refreshToken);
const newAccessToken = data.access_token;
const newRefreshToken = data.refresh_token ?? refreshToken;
const data = await refreshAccessToken(refreshToken)
const newAccessToken = data.access_token
const newRefreshToken = data.refresh_token ?? refreshToken
// 更新 Zustand + localStorage
useAuthStore
.getState()
.setAuth(
useAuthStore.getState().user!,
newAccessToken,
newRefreshToken,
);
.setAuth(useAuthStore.getState().user!, newAccessToken, newRefreshToken)
// 处理排队的请求
processQueue(null, newAccessToken);
processQueue(null, newAccessToken)
// 重试原始请求
if (originalRequest.headers) {
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`
}
return apiClient(originalRequest);
return apiClient(originalRequest)
} catch (refreshError) {
// 刷新失败 → 登出
processQueue(refreshError, null);
useAuthStore.getState().clearAuth();
window.location.href = "/";
return Promise.reject(refreshError);
processQueue(refreshError, null)
useAuthStore.getState().clearAuth()
window.location.href = "/"
return Promise.reject(refreshError)
} finally {
isRefreshing = false;
isRefreshing = false
}
}
// 提取后端返回的错误信息(detail / message / msg
// 注意:后端返回的字段可能是对象 {code, message} 而非字符串,需要安全提取
const data = error.response?.data;
const rawServerMsg = data?.detail || data?.message || data?.msg;
const data = error.response?.data
const rawServerMsg = data?.detail || data?.message || data?.msg
// 安全提取字符串:递归处理嵌套对象(后端可能返回 {code, message: {code, message}} 等)
const safeExtractString = (val: unknown): string => {
if (typeof val === "string") return val;
if (typeof val === "string") return val
if (typeof val === "object" && val !== null) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- 防御性错误提取,后端错误格式不固定
const obj = val as Record<string, any>;
if (typeof obj.message === "string") return obj.message;
if (typeof obj.msg === "string") return obj.msg;
if (typeof obj.detail === "string") return obj.detail;
const obj = val as Record<string, any>
if (typeof obj.message === "string") return obj.message
if (typeof obj.msg === "string") return obj.msg
if (typeof obj.detail === "string") return obj.detail
// 嵌套对象:递归提取
if (typeof obj.message === "object" && obj.message !== null)
return safeExtractString(obj.message);
if (typeof obj.msg === "object" && obj.msg !== null)
return safeExtractString(obj.msg);
return JSON.stringify(val);
return safeExtractString(obj.message)
if (typeof obj.msg === "object" && obj.msg !== null) return safeExtractString(obj.msg)
return JSON.stringify(val)
}
return "";
};
const serverMsg = safeExtractString(rawServerMsg);
let handled = false;
return ""
}
const serverMsg = safeExtractString(rawServerMsg)
let handled = false
if (error.code === "ECONNABORTED" || error.message?.includes("timeout")) {
message.error("请求超时,请检查网络后重试");
handled = true;
message.error("请求超时,请检查网络后重试")
handled = true
} else if (!error.response) {
message.error("网络连接异常,请检查网络设置");
handled = true;
message.error("网络连接异常,请检查网络设置")
handled = true
} else if (serverMsg) {
message.error(serverMsg);
handled = true;
message.error(serverMsg)
handled = true
} else {
const status = error.response?.status;
const status = error.response?.status
if (status === 413) {
message.error("文件过大,请缩小后重试");
handled = true;
message.error("文件过大,请缩小后重试")
handled = true
} else if (status === 415) {
message.error("不支持的文件格式");
handled = true;
message.error("不支持的文件格式")
handled = true
} else if (status === 503) {
message.error("服务暂不可用,请稍后再试");
handled = true;
message.error("服务暂不可用,请稍后再试")
handled = true
} else if (status && status >= 500) {
message.error("服务器繁忙,请稍后再试");
handled = true;
message.error("服务器繁忙,请稍后再试")
handled = true
}
// 其他 4xx 且无具体信息时不弹通用提示,由各组件自行处理
}
@@ -176,11 +165,11 @@ apiClient.interceptors.response.use(
// 标记已展示过提示,组件 onError 可据此跳过重复 toast
if (handled) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(error as any).__msgShown = true;
;(error as any).__msgShown = true
}
return Promise.reject(error);
return Promise.reject(error)
},
);
)
export default apiClient;
export default apiClient
+43 -54
View File
@@ -2,109 +2,98 @@
* 查重 API 模块
* 提供视频查重相关接口
*/
import apiClient from "./client";
import apiClient from "./client"
/** 查重记录状态 */
export type DuplicationStatus =
"pending" | "processing" | "completed" | "failed";
export type DuplicationStatus = "pending" | "processing" | "completed" | "failed"
/** 查重记录 */
export interface DuplicationRecord {
id: string;
id: string
/** 原始文件名 */
filename: string;
filename: string
/** 文件大小(字节) */
file_size: number;
file_size: number
/** 时长(秒) */
duration_seconds?: number;
duration_seconds?: number
/** 状态 */
status: DuplicationStatus;
status: DuplicationStatus
/** 查重率(0-100 */
duplicate_rate?: number;
duplicate_rate?: number
/** 重复片段数 */
duplicate_count?: number;
duplicate_count?: number
/** 创建时间 */
created_at: string;
created_at: string
/** 更新时间 */
updated_at: string;
updated_at: string
}
/** 重复片段详情 */
export interface DuplicateSegment {
id: string;
id: string
/** 在原始视频中的起始时间(秒) */
source_start: number;
source_start: number
/** 在原始视频中的结束时间(秒) */
source_end: number;
source_end: number
/** 匹配到的已有视频 ID */
matched_video_id: string;
matched_video_id: string
/** 匹配到的已有视频名称 */
matched_video_name: string;
matched_video_name: string
/** 匹配片段在已有视频中的起始时间 */
matched_start: number;
matched_start: number
/** 匹配片段在已有视频中的结束时间 */
matched_end: number;
matched_end: number
/** 相似度(0-100 */
similarity: number;
similarity: number
}
/** 查重详情 */
export interface DuplicationDetail extends DuplicationRecord {
/** 重复片段列表 */
segments: DuplicateSegment[];
segments: DuplicateSegment[]
}
/** 上传查重响应 */
export interface DuplicationUploadResponse {
/** 查重记录 ID */
id: string;
id: string
/** 状态 */
status: DuplicationStatus;
status: DuplicationStatus
/** 消息 */
message: string;
message: string
}
// ============ API 函数 ============
/** 上传视频进行查重 */
export const uploadForDuplication = async (
file: File,
): Promise<DuplicationUploadResponse> => {
const formData = new FormData();
formData.append("file", file);
export const uploadForDuplication = async (file: File): Promise<DuplicationUploadResponse> => {
const formData = new FormData()
formData.append("file", file)
const response = await apiClient.post("/duplication/upload", formData, {
headers: { "Content-Type": "multipart/form-data" },
});
return response.data;
};
})
return response.data
}
/** 获取查重记录列表 */
export const getDuplicationRecords = async (): Promise<DuplicationRecord[]> => {
const response = await apiClient.get("/duplication/records");
return response.data;
};
const response = await apiClient.get("/duplication/records")
return response.data
}
/** 获取查重详情 */
export const getDuplicationDetail = async (
recordId: string,
): Promise<DuplicationDetail> => {
const response = await apiClient.get(`/duplication/records/${recordId}`);
return response.data;
};
export const getDuplicationDetail = async (recordId: string): Promise<DuplicationDetail> => {
const response = await apiClient.get(`/duplication/records/${recordId}`)
return response.data
}
/** 删除查重记录 */
export const deleteDuplicationRecord = async (
recordId: string,
): Promise<void> => {
await apiClient.delete(`/duplication/records/${recordId}`);
};
export const deleteDuplicationRecord = async (recordId: string): Promise<void> => {
await apiClient.delete(`/duplication/records/${recordId}`)
}
/** 重新查重 */
export const retryDuplication = async (
recordId: string,
): Promise<DuplicationUploadResponse> => {
const response = await apiClient.post(
`/duplication/records/${recordId}/retry`,
);
return response.data;
};
export const retryDuplication = async (recordId: string): Promise<DuplicationUploadResponse> => {
const response = await apiClient.post(`/duplication/records/${recordId}/retry`)
return response.data
}
+297 -342
View File
@@ -2,8 +2,8 @@
* 剪辑计划 API — 对接后端 Edit Plans Schema
* 字段名严格匹配后端 API 响应
*/
import apiClient from "./client";
import type { AssetItem } from "./assets";
import apiClient from "./client"
import type { AssetItem } from "./assets"
import type {
WatermarkConfig,
IntroOutroConfig,
@@ -12,7 +12,7 @@ import type {
ChromaKeyConfig,
StickerConfig,
CoverConfig,
} from "@/pages/editing-planner/types";
} from "@/pages/editing-planner/types"
/* ============================================================
* 后端 API 类型(严格匹配后端 Schema)
@@ -20,203 +20,203 @@ import type {
/** 剪辑计划状态枚举 */
export type EditPlanStatus =
"draft" | "editing" | "rendering" | "completed" | "failed" | "cancelled";
"draft" | "editing" | "rendering" | "completed" | "failed" | "cancelled"
/** 标题配置(对齐后端 title_config */
export interface TitleConfig {
ai_auto_select: boolean;
content: string;
font_preset: string;
font_color: string;
font_size: number;
position: string;
ai_auto_select: boolean
content: string
font_preset: string
font_color: string
font_size: number
position: string
}
/** 字幕配置 */
export interface SubtitleConfig {
enabled: boolean;
position: string;
font: string;
color: string;
size: number;
animation: string;
enabled: boolean
position: string
font: string
color: string
size: number
animation: string
}
/** BGM 配置 */
export interface BgmConfig {
enabled: boolean;
music_id: string;
enabled: boolean
music_id: string
}
/** 片段 TTS 配置 */
export interface SegmentTtsConfig {
mode: string;
text: string;
voice_id: string;
speed: number;
pitch: number;
volume: number;
subtitle_sync: boolean;
mode: string
text: string
voice_id: string
speed: number
pitch: number
volume: number
subtitle_sync: boolean
}
/** 片段裁剪配置 */
export interface SegmentTrimConfig {
start_time: number;
end_time: number;
start_time: number
end_time: number
}
/** 片段转场配置 */
export interface SegmentTransitionConfig {
type: string;
duration: number;
type: string
duration: number
}
/** 剪辑计划中的单个片段(config 内部 segments 项) */
export interface EditPlanSegment {
segment_order: number;
duration_min: number;
duration_max: number;
material_type: string;
transition?: SegmentTransitionConfig;
playback_speed?: number;
tts_config?: SegmentTtsConfig;
trim_config?: SegmentTrimConfig;
segment_order: number
duration_min: number
duration_max: number
material_type: string
transition?: SegmentTransitionConfig
playback_speed?: number
tts_config?: SegmentTtsConfig
trim_config?: SegmentTrimConfig
}
/** 剪辑计划 config 完整类型(对齐后端 config JSON 结构) */
export interface EditPlanConfig {
title_config?: TitleConfig;
subtitle_config?: SubtitleConfig;
bgm_config?: BgmConfig;
estimated_duration?: number;
segments?: EditPlanSegment[];
watermark_config?: WatermarkConfig;
intro_outro_config?: IntroOutroConfig;
pip_config?: PipConfig;
filter_config?: FilterConfig;
green_screen_config?: ChromaKeyConfig;
sticker_config?: StickerConfig;
cover_config?: CoverConfig;
title_config?: TitleConfig
subtitle_config?: SubtitleConfig
bgm_config?: BgmConfig
estimated_duration?: number
segments?: EditPlanSegment[]
watermark_config?: WatermarkConfig
intro_outro_config?: IntroOutroConfig
pip_config?: PipConfig
filter_config?: FilterConfig
green_screen_config?: ChromaKeyConfig
sticker_config?: StickerConfig
cover_config?: CoverConfig
/** 前端扩展:关联的素材 ID 列表 */
asset_ids?: string[];
asset_ids?: string[]
/** 配音 ID */
voice_id?: string;
voice_id?: string
/** 克隆音色档案 ID */
voice_clone_profile_id?: string;
voice_clone_profile_id?: string
/** 自定义配音音频 URL */
custom_audio_url?: string;
custom_audio_url?: string
/** 自定义配音文本 */
custom_text?: string;
custom_text?: string
/** 视频比例 */
ratio?: string;
ratio?: string
/** 视频风格 */
style?: string;
style?: string
/** 目标时长(秒) */
duration?: number;
duration?: number
/** 是否自动生成字幕 */
auto_subtitles?: boolean;
auto_subtitles?: boolean
/** 是否启用 BGM */
bgm?: boolean;
bgm?: boolean
/** 生成数量 */
generate_count?: number;
generate_count?: number
/** 素材模式 */
material_mode?: string;
material_mode?: string
}
/** 剪辑计划(后端响应) */
export interface EditPlan {
id: string;
template_id: string;
name: string;
status: EditPlanStatus;
total_duration: number;
id: string
template_id: string
name: string
status: EditPlanStatus
total_duration: number
/** 生成视频数量(后端 EditPlanResponse.result_count */
result_count: number;
config: EditPlanConfig;
created_at: string;
updated_at: string;
result_count: number
config: EditPlanConfig
created_at: string
updated_at: string
}
/** 创建剪辑计划请求(后端要求 template_id + name 必填) */
export interface CreateEditPlanRequest {
template_id: string;
name: string;
config?: EditPlanConfig;
total_duration?: number;
template_id: string
name: string
config?: EditPlanConfig
total_duration?: number
/** 来源剪辑计划 ID(从剪辑计划跳转到一键生成时关联) */
source_edit_plan_id?: string;
source_edit_plan_id?: string
}
/** 更新剪辑计划请求 */
export interface UpdateEditPlanRequest {
name?: string;
config?: EditPlanConfig;
total_duration?: number;
status?: EditPlanStatus;
name?: string
config?: EditPlanConfig
total_duration?: number
status?: EditPlanStatus
}
/** 生成响应 */
export interface GenerateResponse {
plan_id: string;
plan_status: EditPlanStatus;
generation_task_id: string;
clip_count: number;
plan_id: string
plan_status: EditPlanStatus
generation_task_id: string
clip_count: number
}
/** 剪辑计划关联的生成记录(实际是 GenerationTask 对象) */
export interface EditPlanGeneration {
id: string; // 即 generation_task_id
source_edit_plan_id: string;
template_id: string;
asset_ids: string[];
status: EditPlanStatus;
progress: number;
result_count: number;
error_message: string;
error_info: Record<string, unknown>;
logs: Array<Record<string, unknown>>;
retry_count: number;
created_at?: string;
updated_at?: string;
id: string // 即 generation_task_id
source_edit_plan_id: string
template_id: string
asset_ids: string[]
status: EditPlanStatus
progress: number
result_count: number
error_message: string
error_info: Record<string, unknown>
logs: Array<Record<string, unknown>>
retry_count: number
created_at?: string
updated_at?: string
}
/** 片段生成状态 */
export interface ClipStatusItem {
clip_id: string;
clip_type: string;
order: number;
status: string;
asset_id?: string;
text_content?: string;
duration?: number;
clip_id: string
clip_type: string
order: number
status: string
asset_id?: string
text_content?: string
duration?: number
}
/** 生成状态轮询响应 */
export interface GenerationStatusResponse {
plan_id: string;
plan_status: EditPlanStatus;
generation_task_id?: string;
clips: ClipStatusItem[];
plan_id: string
plan_status: EditPlanStatus
generation_task_id?: string
clips: ClipStatusItem[]
}
/** 生成视频详情(对应后端 GeneratedVideoResponse */
export interface GeneratedVideo {
id: string;
project_id?: string;
generation_task_id?: string;
name: string;
file_url: string;
file_size?: number;
duration?: number;
thumbnail_url?: string;
width?: number;
height?: number;
fps?: number;
status: string;
review_status?: string;
download_url?: string;
created_at?: string;
updated_at?: string;
id: string
project_id?: string
generation_task_id?: string
name: string
file_url: string
file_size?: number
duration?: number
thumbnail_url?: string
width?: number
height?: number
fps?: number
status: string
review_status?: string
download_url?: string
created_at?: string
updated_at?: string
}
/* ============================================================
@@ -225,51 +225,51 @@ export interface GeneratedVideo {
/** AI 推荐请求 */
export interface AIRecommendRequest {
asset_ids: string[];
editing_mode?: string;
target_duration?: number;
asset_ids: string[]
editing_mode?: string
target_duration?: number
}
/** AI 推荐单个片段 */
export interface AIRecommendClipItem {
clip_type: string;
order: number;
text_content: string;
duration: number;
transition_effect: string;
asset_id: string;
start_time: number;
config: EditPlanConfig;
clip_type: string
order: number
text_content: string
duration: number
transition_effect: string
asset_id: string
start_time: number
config: EditPlanConfig
}
/** AI 推荐响应 */
export interface AIRecommendResponse {
plan_id: string;
clips: AIRecommendClipItem[];
config: EditPlanConfig;
total_duration: number;
confidence: number;
plan_id: string
clips: AIRecommendClipItem[]
config: EditPlanConfig
total_duration: number
confidence: number
}
/** AI 封面生成请求 */
export interface GenerateCoverRequest {
asset_ids: string[];
cover_type?: "ai_frame" | "manual" | "upload" | "ai_regenerate";
frame_time?: number;
asset_ids: string[]
cover_type?: "ai_frame" | "manual" | "upload" | "ai_regenerate"
frame_time?: number
}
/** AI 封面生成响应 */
export interface GenerateCoverResponse {
plan_id: string;
cover: CoverResult;
plan_id: string
cover: CoverResult
}
/** 封面生成结果 */
export interface CoverResult {
scheme?: string;
asset_id?: string;
frame_time?: number;
thumbnail_url?: string;
scheme?: string
asset_id?: string
frame_time?: number
thumbnail_url?: string
}
/* ============================================================
@@ -293,30 +293,30 @@ export interface TransitionEffect {
| "wipe_up"
| "wipe_down"
| "circlecrop"
| "rectcrop";
duration: number; // 转场时长(秒)
| "rectcrop"
duration: number // 转场时长(秒)
/** 播放速度倍率 */
playback_speed?: number;
playback_speed?: number
}
/** 素材库资产(UI 层类型,映射自后端 AssetResponse */
export interface MediaAsset {
id: string;
name: string;
type: "video" | "image" | "audio";
id: string
name: string
type: "video" | "image" | "audio"
/** 缩略图 URL */
thumbnail_url?: string;
thumbnail_url?: string
/** 时长(秒),仅 video/audio */
duration?: number;
duration?: number
/** 文件大小(字节) */
size?: number;
size?: number
/** 标签 */
tags: string[];
created_at: string;
tags: string[]
created_at: string
/** 质量分 0-100 */
quality_score?: number;
quality_score?: number
/** 分类状态 */
classification_status?: "pending" | "processing" | "completed" | "failed";
classification_status?: "pending" | "processing" | "completed" | "failed"
}
/* ============================================================
@@ -325,42 +325,38 @@ export interface MediaAsset {
/** 剪辑计划列表查询参数 */
export interface EditPlanListParams {
page?: number;
page_size?: number;
template_id?: string;
status?: string;
page?: number
page_size?: number
template_id?: string
status?: string
}
/** 剪辑计划列表分页响应 */
export interface EditPlanListResponse {
items: EditPlan[];
total: number;
page: number;
page_size: number;
items: EditPlan[]
total: number
page: number
page_size: number
}
/** 获取剪辑计划列表(支持分页和筛选) */
export async function getEditPlans(
params?: EditPlanListParams,
): Promise<EditPlanListResponse> {
export async function getEditPlans(params?: EditPlanListParams): Promise<EditPlanListResponse> {
const response = await apiClient.get<EditPlanListResponse>("/edit-plans", {
params,
});
return response.data;
})
return response.data
}
/** 获取单个剪辑计划 */
export async function getEditPlan(planId: string): Promise<EditPlan> {
const response = await apiClient.get(`/edit-plans/${planId}`);
return response.data;
const response = await apiClient.get(`/edit-plans/${planId}`)
return response.data
}
/** 创建剪辑计划 */
export async function createEditPlan(
data: CreateEditPlanRequest,
): Promise<EditPlan> {
const response = await apiClient.post("/edit-plans", data);
return response.data;
export async function createEditPlan(data: CreateEditPlanRequest): Promise<EditPlan> {
const response = await apiClient.post("/edit-plans", data)
return response.data
}
/** 更新剪辑计划 */
@@ -368,31 +364,25 @@ export async function updateEditPlan(
planId: string,
data: UpdateEditPlanRequest,
): Promise<EditPlan> {
const response = await apiClient.put(`/edit-plans/${planId}`, data);
return response.data;
const response = await apiClient.put(`/edit-plans/${planId}`, data)
return response.data
}
/** 删除剪辑计划 */
export async function deleteEditPlan(planId: string): Promise<void> {
await apiClient.delete(`/edit-plans/${planId}`);
await apiClient.delete(`/edit-plans/${planId}`)
}
/** 触发剪辑计划生成 */
export async function generateEditPlan(
planId: string,
): Promise<GenerateResponse> {
const response = await apiClient.post(`/edit-plans/${planId}/generate`);
return response.data;
export async function generateEditPlan(planId: string): Promise<GenerateResponse> {
const response = await apiClient.post(`/edit-plans/${planId}/generate`)
return response.data
}
/** 获取剪辑计划生成状态(轮询用) */
export async function getGenerationStatus(
planId: string,
): Promise<GenerationStatusResponse> {
const response = await apiClient.get(
`/edit-plans/${planId}/generation-status`,
);
return response.data;
export async function getGenerationStatus(planId: string): Promise<GenerationStatusResponse> {
const response = await apiClient.get(`/edit-plans/${planId}/generation-status`)
return response.data
}
/** AI 推荐片段方案 */
@@ -400,11 +390,8 @@ export async function aiRecommendClips(
planId: string,
data: AIRecommendRequest,
): Promise<AIRecommendResponse> {
const response = await apiClient.post(
`/edit-plans/${planId}/ai-recommend`,
data,
);
return response.data;
const response = await apiClient.post(`/edit-plans/${planId}/ai-recommend`, data)
return response.data
}
/** AI 生成封面 */
@@ -412,32 +399,25 @@ export async function generateCover(
planId: string,
data: GenerateCoverRequest,
): Promise<GenerateCoverResponse> {
const response = await apiClient.post(
`/edit-plans/${planId}/generate-cover`,
data,
);
return response.data;
const response = await apiClient.post(`/edit-plans/${planId}/generate-cover`, data)
return response.data
}
/** 获取剪辑计划关联的生成记录 */
export async function getEditPlanGenerations(
planId: string,
): Promise<EditPlanGeneration[]> {
const response = await apiClient.get(`/edit-plans/${planId}/generations`);
return response.data.items || [];
export async function getEditPlanGenerations(planId: string): Promise<EditPlanGeneration[]> {
const response = await apiClient.get(`/edit-plans/${planId}/generations`)
return response.data.items || []
}
/** 获取生成任务的视频结果列表 */
export async function getGenerationTaskResults(
taskId: string,
): Promise<GeneratedVideo[]> {
const response = await apiClient.get(`/generation/tasks/${taskId}/results`);
return response.data.items || response.data || [];
export async function getGenerationTaskResults(taskId: string): Promise<GeneratedVideo[]> {
const response = await apiClient.get(`/generation/tasks/${taskId}/results`)
return response.data.items || response.data || []
}
/** 取消生成任务 */
export async function cancelGeneration(planId: string): Promise<void> {
await apiClient.post(`/edit-plans/${planId}/cancel`);
await apiClient.post(`/edit-plans/${planId}/cancel`)
}
/* ============================================================
@@ -445,66 +425,66 @@ export async function cancelGeneration(planId: string): Promise<void> {
* ============================================================ */
/** 片段状态 */
export type EditPlanClipStatus = "pending" | "processing" | "ready" | "failed";
export type EditPlanClipStatus = "pending" | "processing" | "ready" | "failed"
/** 剪辑片段(后端响应) */
export interface EditPlanClip {
id: string;
plan_id: string;
clip_type: string; // main / intro / outro / overlay / background / b_roll 等
order: number;
asset_id: string;
text_content: string;
start_time: number;
duration: number;
transition_effect: string;
transition_duration: number;
playback_speed: number;
status: EditPlanClipStatus;
config: Record<string, unknown>;
created_at?: string;
updated_at?: string;
id: string
plan_id: string
clip_type: string // main / intro / outro / overlay / background / b_roll 等
order: number
asset_id: string
text_content: string
start_time: number
duration: number
transition_effect: string
transition_duration: number
playback_speed: number
status: EditPlanClipStatus
config: Record<string, unknown>
created_at?: string
updated_at?: string
}
/** 创建片段请求 */
export interface CreateEditPlanClipRequest {
clip_type: string;
order: number;
asset_id?: string;
text_content?: string;
start_time?: number;
duration?: number;
transition_effect?: string;
transition_duration?: number;
playback_speed?: number;
config?: Record<string, unknown>;
clip_type: string
order: number
asset_id?: string
text_content?: string
start_time?: number
duration?: number
transition_effect?: string
transition_duration?: number
playback_speed?: number
config?: Record<string, unknown>
}
/** 更新片段请求 */
export interface UpdateEditPlanClipRequest {
clip_type?: string;
order?: number;
asset_id?: string;
text_content?: string;
start_time?: number;
duration?: number;
transition_effect?: string;
transition_duration?: number;
playback_speed?: number;
config?: Record<string, unknown>;
clip_type?: string
order?: number
asset_id?: string
text_content?: string
start_time?: number
duration?: number
transition_effect?: string
transition_duration?: number
playback_speed?: number
config?: Record<string, unknown>
}
/** 片段列表响应 */
export interface EditPlanClipListResponse {
items: EditPlanClip[];
total: number;
items: EditPlanClip[]
total: number
}
/** 片段列表查询参数 */
export interface EditPlanClipListParams {
status?: string;
skip?: number;
limit?: number;
status?: string
skip?: number
limit?: number
}
/** 获取片段列表 */
@@ -512,22 +492,16 @@ export async function getEditPlanClips(
planId: string,
params?: EditPlanClipListParams,
): Promise<EditPlanClipListResponse> {
const response = await apiClient.get<EditPlanClipListResponse>(
`/edit-plans/${planId}/clips`,
{ params },
);
return response.data;
const response = await apiClient.get<EditPlanClipListResponse>(`/edit-plans/${planId}/clips`, {
params,
})
return response.data
}
/** 获取单个片段详情 */
export async function getEditPlanClip(
planId: string,
clipId: string,
): Promise<EditPlanClip> {
const response = await apiClient.get<EditPlanClip>(
`/edit-plans/${planId}/clips/${clipId}`,
);
return response.data;
export async function getEditPlanClip(planId: string, clipId: string): Promise<EditPlanClip> {
const response = await apiClient.get<EditPlanClip>(`/edit-plans/${planId}/clips/${clipId}`)
return response.data
}
/** 创建片段 */
@@ -535,11 +509,8 @@ export async function createEditPlanClip(
planId: string,
data: CreateEditPlanClipRequest,
): Promise<EditPlanClip> {
const response = await apiClient.post<EditPlanClip>(
`/edit-plans/${planId}/clips`,
data,
);
return response.data;
const response = await apiClient.post<EditPlanClip>(`/edit-plans/${planId}/clips`, data)
return response.data
}
/** 更新片段 */
@@ -548,19 +519,13 @@ export async function updateEditPlanClip(
clipId: string,
data: UpdateEditPlanClipRequest,
): Promise<EditPlanClip> {
const response = await apiClient.put<EditPlanClip>(
`/edit-plans/${planId}/clips/${clipId}`,
data,
);
return response.data;
const response = await apiClient.put<EditPlanClip>(`/edit-plans/${planId}/clips/${clipId}`, data)
return response.data
}
/** 删除片段 */
export async function deleteEditPlanClip(
planId: string,
clipId: string,
): Promise<void> {
await apiClient.delete(`/edit-plans/${planId}/clips/${clipId}`);
export async function deleteEditPlanClip(planId: string, clipId: string): Promise<void> {
await apiClient.delete(`/edit-plans/${planId}/clips/${clipId}`)
}
/* ============================================================
@@ -569,30 +534,30 @@ export async function deleteEditPlanClip(
/** 重排序条目 */
export interface ClipReorderItem {
clip_id: string;
new_order: number;
clip_id: string
new_order: number
}
/** 重排序响应 */
export interface ClipReorderResponse {
success: boolean;
updated_count: number;
message: string;
success: boolean
updated_count: number
message: string
}
/** 批量删除响应 */
export interface ClipBatchDeleteResponse {
success: boolean;
deleted_count: number;
message: string;
success: boolean
deleted_count: number
message: string
}
/** 从素材批量创建响应 */
export interface ClipsFromAssetsResponse {
success: boolean;
created_count: number;
message: string;
clip_ids: string[];
success: boolean
created_count: number
message: string
clip_ids: string[]
}
/** 片段重排序(拖拽排序后一次性提交) */
@@ -603,8 +568,8 @@ export async function reorderEditPlanClips(
const response = await apiClient.post<ClipReorderResponse>(
`/edit-plans/${planId}/clips/reorder`,
{ items },
);
return response.data;
)
return response.data
}
/** 批量删除片段 */
@@ -615,8 +580,8 @@ export async function batchDeleteEditPlanClips(
const response = await apiClient.post<ClipBatchDeleteResponse>(
`/edit-plans/${planId}/clips/batch-delete`,
{ clip_ids: clipIds },
);
return response.data;
)
return response.data
}
/** 从素材批量创建片段(追加到时间线末尾) */
@@ -628,8 +593,8 @@ export async function createClipsFromAssets(
const response = await apiClient.post<ClipsFromAssetsResponse>(
`/edit-plans/${planId}/clips/from-assets`,
{ asset_ids: assetIds, clip_type: clipType },
);
return response.data;
)
return response.data
}
/* ============================================================
@@ -638,40 +603,32 @@ export async function createClipsFromAssets(
/** 复制计划请求 */
export interface CopyEditPlanRequest {
name?: string;
project_id?: string;
name?: string
project_id?: string
}
/** 复制剪辑计划(含所有片段配置) */
export async function copyEditPlan(
planId: string,
data?: CopyEditPlanRequest,
): Promise<EditPlan> {
const response = await apiClient.post<EditPlan>(
`/edit-plans/${planId}/copy`,
data || {},
);
return response.data;
export async function copyEditPlan(planId: string, data?: CopyEditPlanRequest): Promise<EditPlan> {
const response = await apiClient.post<EditPlan>(`/edit-plans/${planId}/copy`, data || {})
return response.data
}
/**
* 获取素材库列表 — 调用 GET /api/v1/assets?library_id=xxx
* 将后端 AssetResponse 映射为前端 MediaAsset 类型
*/
export async function getMediaAssets(
libraryId?: string,
): Promise<MediaAsset[]> {
export async function getMediaAssets(libraryId?: string): Promise<MediaAsset[]> {
const response = await apiClient.get("/assets", {
params: libraryId ? { library_id: libraryId } : undefined,
});
const items: AssetItem[] = response.data.items || [];
return items.map(mapAssetToMediaAsset);
})
const items: AssetItem[] = response.data.items || []
return items.map(mapAssetToMediaAsset)
}
/** 获取单个素材 */
export async function getMediaAsset(id: string): Promise<MediaAsset> {
const response = await apiClient.get(`/assets/${id}`);
return mapAssetToMediaAsset(response.data);
const response = await apiClient.get(`/assets/${id}`)
return mapAssetToMediaAsset(response.data)
}
/* ============================================================
@@ -679,17 +636,15 @@ export async function getMediaAsset(id: string): Promise<MediaAsset> {
* ============================================================ */
function inferMediaType(mimeType: string): "video" | "image" | "audio" {
if (mimeType.startsWith("video/")) return "video";
if (mimeType.startsWith("image/")) return "image";
return "audio";
if (mimeType.startsWith("video/")) return "video"
if (mimeType.startsWith("image/")) return "image"
return "audio"
}
function mapAssetToMediaAsset(asset: AssetItem): MediaAsset {
// 优先取顶层 duration,其次从 metadata 回退
const metaDuration =
typeof asset.metadata?.duration === "number"
? asset.metadata.duration
: undefined;
typeof asset.metadata?.duration === "number" ? asset.metadata.duration : undefined
return {
id: asset.id,
name: asset.name,
@@ -701,7 +656,7 @@ function mapAssetToMediaAsset(asset: AssetItem): MediaAsset {
created_at: asset.created_at ?? "",
quality_score: asset.quality_score ?? undefined,
classification_status: asset.classification_status ?? undefined,
};
}
}
/* ============================================================
@@ -710,9 +665,9 @@ function mapAssetToMediaAsset(asset: AssetItem): MediaAsset {
/** 转场效果选项(14 种预设) */
export const TRANSITION_OPTIONS: {
value: TransitionEffect["type"];
label: string;
icon: string;
value: TransitionEffect["type"]
label: string
icon: string
}[] = [
{ value: "none", label: "无转场", icon: "⊘" },
{ value: "cut", label: "硬切", icon: "✂" },
@@ -729,7 +684,7 @@ export const TRANSITION_OPTIONS: {
{ value: "wipe_down", label: "下擦除", icon: "▾̄" },
{ value: "circlecrop", label: "圆形裁切", icon: "●" },
{ value: "rectcrop", label: "矩形裁切", icon: "■" },
];
]
/** 素材类型标签 */
export const MATERIAL_TYPE_LABELS: Record<string, string> = {
@@ -737,7 +692,7 @@ export const MATERIAL_TYPE_LABELS: Record<string, string> = {
image: "图片",
audio: "音频",
voiceover: "配音",
};
}
/** 素材类型图标 */
export const MATERIAL_TYPE_ICONS: Record<string, string> = {
@@ -745,7 +700,7 @@ export const MATERIAL_TYPE_ICONS: Record<string, string> = {
image: "🖼️",
audio: "🎵",
voiceover: "🎙️",
};
}
/** 计划状态标签 */
export const PLAN_STATUS_LABELS: Record<EditPlanStatus, string> = {
@@ -755,17 +710,17 @@ export const PLAN_STATUS_LABELS: Record<EditPlanStatus, string> = {
completed: "已完成",
failed: "失败",
cancelled: "已取消",
};
}
/** 质量分筛选选项 */
export const QUALITY_OPTIONS: {
value: string;
label: string;
min?: number;
max?: number;
value: string
label: string
min?: number
max?: number
}[] = [
{ value: "all", label: "全部质量" },
{ value: "high", label: "高质量 (80-100)", min: 80, max: 100 },
{ value: "medium", label: "中质量 (50-79)", min: 50, max: 79 },
{ value: "low", label: "低质量 (0-49)", min: 0, max: 49 },
];
]
+104 -112
View File
@@ -2,7 +2,7 @@
* 剪辑计划编辑器 API
* 对接后端 /api/v1/templates 路由
*/
import apiClient from "./client";
import apiClient from "./client"
import type {
WatermarkConfig,
IntroOutroConfig,
@@ -11,12 +11,12 @@ import type {
ChromaKeyConfig,
StickerConfig,
CoverConfig,
} from "@/pages/editing-planner/types";
} from "@/pages/editing-planner/types"
/* ──────────── 类型定义 ──────────── */
/** 模板模式(后端枚举值) */
export type TemplateMode = "pip" | "voice_over" | "one_take" | "voice_pip";
export type TemplateMode = "pip" | "voice_over" | "one_take" | "voice_pip"
/** 模式显示名称映射 */
export const MODE_LABELS: Record<TemplateMode, string> = {
@@ -24,7 +24,7 @@ export const MODE_LABELS: Record<TemplateMode, string> = {
voice_over: "人物口播",
one_take: "一镜到底",
voice_pip: "口播+混剪",
};
}
/** 模式颜色映射 */
export const MODE_COLORS: Record<TemplateMode, string> = {
@@ -32,211 +32,203 @@ export const MODE_COLORS: Record<TemplateMode, string> = {
voice_over: "green",
one_take: "orange",
voice_pip: "purple",
};
}
/** 标题配置 */
export interface TitleConfig {
ai_auto_select: boolean;
content: string;
font_preset: string;
font_color: string;
font_size: number;
position: string;
ai_auto_select: boolean
content: string
font_preset: string
font_color: string
font_size: number
position: string
}
/** 字幕配置 */
export interface SubtitleConfig {
enabled: boolean;
position: string;
font: string;
color: string;
size: number;
animation: string;
enabled: boolean
position: string
font: string
color: string
size: number
animation: string
}
/** BGM 配置 */
export interface BgmConfig {
enabled: boolean;
music_id: string;
enabled: boolean
music_id: string
}
/** 模板片段 */
export interface TemplateSegment {
id?: string;
segment_order: number;
duration_min: number;
duration_max: number;
material_type: string | null;
id?: string
segment_order: number
duration_min: number
duration_max: number
material_type: string | null
}
/** 剪辑模板 */
export interface EditingTemplate {
id: string;
name: string;
mode: TemplateMode;
category: string;
tags: string[];
title_config: TitleConfig;
subtitle_config: SubtitleConfig;
bgm_config: BgmConfig;
estimated_duration: number;
segments: TemplateSegment[];
id: string
name: string
mode: TemplateMode
category: string
tags: string[]
title_config: TitleConfig
subtitle_config: SubtitleConfig
bgm_config: BgmConfig
estimated_duration: number
segments: TemplateSegment[]
/** 水印配置(后端就绪后启用) */
watermark_config?: WatermarkConfig;
watermark_config?: WatermarkConfig
/** 片头片尾配置(后端就绪后启用) */
intro_outro_config?: IntroOutroConfig;
intro_outro_config?: IntroOutroConfig
/** 画中画配置 */
pip_config?: PipConfig;
pip_config?: PipConfig
/** 滤镜调色配置 */
filter_config?: FilterConfig;
filter_config?: FilterConfig
/** 绿幕抠像配置 */
green_screen_config?: ChromaKeyConfig;
green_screen_config?: ChromaKeyConfig
/** 贴纸配置 */
sticker_config?: StickerConfig;
sticker_config?: StickerConfig
/** 封面配置 */
cover_config?: CoverConfig;
is_active?: boolean;
created_at: string;
updated_at: string;
cover_config?: CoverConfig
is_active?: boolean
created_at: string
updated_at: string
}
/** 模板分类 */
export interface TemplateCategory {
id: string;
name: string;
created_at?: string;
id: string
name: string
created_at?: string
}
/** 创建/更新模板请求体 */
export interface SaveTemplatePayload {
name: string;
mode: TemplateMode;
category: string;
tags: string[];
title_config: TitleConfig;
subtitle_config: SubtitleConfig;
bgm_config: BgmConfig;
estimated_duration: number;
segments: Omit<TemplateSegment, "id">[];
name: string
mode: TemplateMode
category: string
tags: string[]
title_config: TitleConfig
subtitle_config: SubtitleConfig
bgm_config: BgmConfig
estimated_duration: number
segments: Omit<TemplateSegment, "id">[]
/** 水印配置(后端就绪后启用) */
watermark_config?: WatermarkConfig;
watermark_config?: WatermarkConfig
/** 片头片尾配置(后端就绪后启用) */
intro_outro_config?: IntroOutroConfig;
intro_outro_config?: IntroOutroConfig
/** 画中画配置 */
pip_config?: PipConfig;
pip_config?: PipConfig
/** 滤镜调色配置 */
filter_config?: FilterConfig;
filter_config?: FilterConfig
/** 绿幕抠像配置 */
green_screen_config?: ChromaKeyConfig;
green_screen_config?: ChromaKeyConfig
/** 贴纸配置 */
sticker_config?: StickerConfig;
sticker_config?: StickerConfig
/** 封面配置 */
cover_config?: CoverConfig;
cover_config?: CoverConfig
}
/** 使用模板生成请求体 */
export interface GenerateFromTemplatePayload {
voiceover_duration: number;
voiceover_duration: number
}
/** 验证警告详情 */
export interface ValidationWarningDetails {
/** 相关字段名 */
field?: string;
field?: string
/** 期望值 */
expected?: string | number;
expected?: string | number
/** 实际值 */
actual?: string | number;
actual?: string | number
/** 建议值 */
suggested?: string | number;
suggested?: string | number
}
/** 验证/生成响应 */
export interface ValidateWarning {
code: string;
message: string;
details?: ValidationWarningDetails;
code: string
message: string
details?: ValidationWarningDetails
}
/** 使用模板生成响应 */
export interface GenerateFromTemplateResponse {
template: EditingTemplate;
warnings: ValidateWarning[];
template: EditingTemplate
warnings: ValidateWarning[]
}
/** 列表响应(带分页) */
export interface ListTemplatesResponse {
items: EditingTemplate[];
total: number;
items: EditingTemplate[]
total: number
}
/** 分类列表响应 */
export interface ListCategoriesResponse {
items: TemplateCategory[];
items: TemplateCategory[]
}
// ============ API 函数 ============
/** 获取模板列表 */
export const getEditingTemplates = async (params?: {
category?: string;
tag?: string;
skip?: number;
limit?: number;
category?: string
tag?: string
skip?: number
limit?: number
}): Promise<EditingTemplate[]> => {
const response = await apiClient.get<ListTemplatesResponse>("/templates", {
params: {
skip: params?.skip ?? 0,
limit: params?.limit ?? 50,
},
});
let list = response.data.items;
if (params?.category)
list = list.filter((t) => t.category === params.category);
if (params?.tag) list = list.filter((t) => t.tags.includes(params.tag!));
return list;
};
})
let list = response.data.items
if (params?.category) list = list.filter((t) => t.category === params.category)
if (params?.tag) list = list.filter((t) => t.tags.includes(params.tag!))
return list
}
/** 获取模板详情 */
export const getEditingTemplate = async (
id: string,
): Promise<EditingTemplate> => {
const response = await apiClient.get<EditingTemplate>(`/templates/${id}`);
return response.data;
};
export const getEditingTemplate = async (id: string): Promise<EditingTemplate> => {
const response = await apiClient.get<EditingTemplate>(`/templates/${id}`)
return response.data
}
/** 创建模板 */
export const createEditingTemplate = async (
data: SaveTemplatePayload,
): Promise<EditingTemplate> => {
const response = await apiClient.post<EditingTemplate>("/templates", data);
return response.data;
};
const response = await apiClient.post<EditingTemplate>("/templates", data)
return response.data
}
/** 更新模板 */
export const updateEditingTemplate = async (
id: string,
data: SaveTemplatePayload,
): Promise<EditingTemplate> => {
const response = await apiClient.patch<EditingTemplate>(
`/templates/${id}`,
data,
);
return response.data;
};
const response = await apiClient.patch<EditingTemplate>(`/templates/${id}`, data)
return response.data
}
/** 删除模板 */
export const deleteEditingTemplate = async (id: string): Promise<void> => {
await apiClient.delete(`/templates/${id}`);
};
await apiClient.delete(`/templates/${id}`)
}
/** 获取模板分类列表 */
export const getTemplateCategories = async (): Promise<TemplateCategory[]> => {
const response = await apiClient.get<ListCategoriesResponse>(
"/templates/categories/list",
);
return response.data.items;
};
const response = await apiClient.get<ListCategoriesResponse>("/templates/categories/list")
return response.data.items
}
/** 使用模板生成视频(调用 validate 端点) */
export const generateFromTemplate = async (
@@ -246,6 +238,6 @@ export const generateFromTemplate = async (
const response = await apiClient.post<GenerateFromTemplateResponse>(
`/templates/${templateId}/validate`,
data,
);
return response.data;
};
)
return response.data
}
Executable → Regular
+58 -70
View File
@@ -3,59 +3,59 @@
* 包含:列表查询、复核状态、批量下载
* 后端无 /products 路由,实际从 /generation/tasks 端点获取数据
*/
import apiClient from "./client";
import { getGenerationTaskResults } from "./editPlans";
import type { GeneratedVideo } from "./editPlans";
import apiClient from "./client"
import { getGenerationTaskResults } from "./editPlans"
import type { GeneratedVideo } from "./editPlans"
/** 复核状态 */
export type ReviewStatus = "pending_review" | "approved" | "rejected";
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";
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;
review_status?: ReviewStatus
/** 所属项目 ID */
project_id?: string;
project_id?: string
/** 所属项目名称 */
project_name?: string;
project_name?: string
/** 查重率(百分比) */
duplicate_rate?: number;
created_at?: string;
updated_at?: 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";
page?: number
page_size?: number
project_id?: string
review_status?: ReviewStatus | "all"
}
/** 分页响应 */
export interface ProductListResponse {
items: ProductItem[];
total: number;
page: number;
page_size: number;
items: ProductItem[]
total: number
page: number
page_size: number
}
/** 批量下载任务状态 */
export interface BatchDownloadStatus {
job_id: string;
status: "processing" | "completed" | "failed";
job_id: string
status: "processing" | "completed" | "failed"
/** 完成后返回的下载 URL */
download_url?: string;
download_url?: string
/** 进度百分比 */
progress?: number;
progress?: number
}
/**
@@ -69,8 +69,7 @@ function mapTaskToProductItem(task: GeneratedVideo): ProductItem {
thumbnail_url: task.thumbnail_url,
duration_seconds: task.duration,
file_size: task.file_size,
resolution:
task.width && task.height ? `${task.width}x${task.height}` : undefined,
resolution: task.width && task.height ? `${task.width}x${task.height}` : undefined,
status:
task.status === "completed"
? "completed"
@@ -81,43 +80,36 @@ function mapTaskToProductItem(task: GeneratedVideo): ProductItem {
project_id: task.project_id,
created_at: task.created_at,
updated_at: task.updated_at,
};
}
}
/** 获取成品列表(支持分页和筛选)— 实际从 generation tasks 获取 */
export const getProducts = async (
params?: ProductListParams,
): Promise<ProductItem[]> => {
const response = await apiClient.get("/generation/tasks", { params });
const data = response.data;
const tasks = Array.isArray(data?.items)
? data.items
: Array.isArray(data)
? data
: [];
return tasks.map(mapTaskToProductItem);
};
export const getProducts = async (params?: ProductListParams): Promise<ProductItem[]> => {
const response = await apiClient.get("/generation/tasks", { params })
const tasks = response.data.items || response.data || []
return tasks.map(mapTaskToProductItem)
}
/** 获取单个成品详情 — 通过 task ID 获取结果 */
export const getProduct = async (productId: string): Promise<ProductItem> => {
const response = await apiClient.get(`/generation/tasks/${productId}`);
return mapTaskToProductItem(response.data);
};
const response = await apiClient.get(`/generation/tasks/${productId}`)
return mapTaskToProductItem(response.data)
}
/** 删除成品 — 删除 generation task */
export const deleteProduct = async (productId: string): Promise<void> => {
await apiClient.delete(`/generation/tasks/${productId}`);
};
await apiClient.delete(`/generation/tasks/${productId}`)
}
/** 获取成品下载链接 — 从 generation task results 获取 */
export const getProductDownloadUrl = async (
productId: string,
): Promise<{ url: string; expires_at: string }> => {
const videos = await getGenerationTaskResults(productId);
const video = videos[0];
if (!video?.download_url) throw new Error("下载链接不可用");
return { url: video.download_url, expires_at: "" };
};
const videos = await getGenerationTaskResults(productId)
const video = videos[0]
if (!video?.download_url) throw new Error("下载链接不可用")
return { url: video.download_url, expires_at: "" }
}
/** 更新复核状态 — TODO: 后端暂无对应端点,暂存本地状态 */
export const updateReviewStatus = async (
@@ -126,26 +118,22 @@ export const updateReviewStatus = async (
): Promise<ProductItem> => {
// 后端暂无 /generation/tasks/{id}/review 端点
// 暂时返回当前状态,后续可扩展
const product = await getProduct(productId);
return { ...product, review_status: status };
};
const product = await getProduct(productId)
return { ...product, review_status: status }
}
/** 发起批量下载 — TODO: 后端暂无对应端点 */
export const batchDownload = async (
videoIds: string[],
): Promise<{ job_id: string }> => {
export const batchDownload = async (videoIds: string[]): Promise<{ job_id: string }> => {
// 后端暂无 /generation/tasks/batch-download 端点
// 暂时返回模拟 job_id,后续可扩展
console.warn("[batchDownload] 后端暂无批量下载端点", videoIds);
return { job_id: `mock-${Date.now()}` };
};
console.warn("[batchDownload] 后端暂无批量下载端点", videoIds)
return { job_id: `mock-${Date.now()}` }
}
/** 查询批量下载状态 — TODO: 后端暂无对应端点 */
export const getBatchDownloadStatus = async (
jobId: string,
): Promise<BatchDownloadStatus> => {
export const getBatchDownloadStatus = async (jobId: string): Promise<BatchDownloadStatus> => {
// 后端暂无 /generation/tasks/batch-download/{jobId} 端点
// 暂时返回模拟状态,后续可扩展
console.warn("[getBatchDownloadStatus] 后端暂无批量下载状态端点", jobId);
return { job_id: jobId, status: "processing", progress: 0 };
};
console.warn("[getBatchDownloadStatus] 后端暂无批量下载状态端点", jobId)
return { job_id: jobId, status: "processing", progress: 0 }
}
+21 -22
View File
@@ -2,60 +2,59 @@
* 项目相关 API
* 素材库需要 project_id,前端自动管理默认项目
*/
import apiClient from "./client";
import apiClient from "./client"
export interface ProjectItem {
id: string;
name: string;
description: string;
id: string
name: string
description: string
}
/** 后端 ProjectResponse 只返回 id, name, description */
interface BackendProjectResponse {
id: string;
name: string;
description: string;
id: string
name: string
description: string
}
/** 后端 ListProjectsResponse 返回 { items: [...] } */
interface BackendListProjectsResponse {
items: BackendProjectResponse[];
items: BackendProjectResponse[]
}
const toProjectItem = (item: BackendProjectResponse): ProjectItem => ({
id: item.id,
name: item.name,
description: item.description,
});
})
/** 获取当前用户的项目列表 */
export const getProjects = async (): Promise<ProjectItem[]> => {
const response =
await apiClient.get<BackendListProjectsResponse>("/projects");
return (response.data.items || []).map(toProjectItem);
};
const response = await apiClient.get<BackendListProjectsResponse>("/projects")
return (response.data.items || []).map(toProjectItem)
}
/** 创建项目 */
export const createProject = async (data: {
name: string;
description?: string;
name: string
description?: string
}): Promise<ProjectItem> => {
const response = await apiClient.post<BackendProjectResponse>("/projects", {
name: data.name,
description: data.description || "",
});
return toProjectItem(response.data);
};
})
return toProjectItem(response.data)
}
/** 获取或创建默认项目(素材库需要 project_id */
export const getOrCreateDefaultProject = async (): Promise<ProjectItem> => {
const projects = await getProjects();
const projects = await getProjects()
if (projects.length > 0) {
return projects[0];
return projects[0]
}
// 没有项目时自动创建默认项目
return createProject({
name: "默认项目",
description: "系统自动创建的默认项目",
});
};
})
}
+53 -55
View File
@@ -2,100 +2,98 @@
* 订阅 API 模块
* 对接后端订阅管理接口
*/
import apiClient from "./client";
import apiClient from "./client"
/** 套餐类型 */
export type PlanType = "free" | "standard" | "pro" | "enterprise";
export type PlanType = "free" | "standard" | "pro" | "enterprise"
/** 订阅状态 */
export type SubscriptionStatus = "active" | "expired" | "cancelled" | "trial";
export type SubscriptionStatus = "active" | "expired" | "cancelled" | "trial"
/** 账单状态 */
export type BillingStatus = "paid" | "pending" | "failed" | "refunded";
export type BillingStatus = "paid" | "pending" | "failed" | "refunded"
/** 计费周期 */
export type BillingCycle = "monthly" | "yearly";
export type BillingCycle = "monthly" | "yearly"
/** 套餐信息 */
export interface Plan {
id: PlanType;
name: string;
price: number | null;
yearly_price?: number | null;
description: string;
recommended: boolean;
features: string[];
id: PlanType
name: string
price: number | null
yearly_price?: number | null
description: string
recommended: boolean
features: string[]
}
/** 当前订阅信息 */
export interface SubscriptionInfo {
id: string;
plan_id: PlanType;
plan_name: string;
status: SubscriptionStatus;
billing_cycle: BillingCycle;
current_period_start: string;
current_period_end: string;
amount: number;
auto_renew: boolean;
created_at: string;
id: string
plan_id: PlanType
plan_name: string
status: SubscriptionStatus
billing_cycle: BillingCycle
current_period_start: string
current_period_end: string
amount: number
auto_renew: boolean
created_at: string
}
/** 账单记录 */
export interface BillingRecord {
id: string;
plan_name: string;
amount: number;
billing_cycle: BillingCycle;
status: BillingStatus;
payment_method: string;
created_at: string;
invoice_url?: string;
id: string
plan_name: string
amount: number
billing_cycle: BillingCycle
status: BillingStatus
payment_method: string
created_at: string
invoice_url?: string
}
/** 升级/降级请求 */
export interface ChangePlanRequest {
target_plan_id: PlanType;
billing_cycle: BillingCycle;
target_plan_id: PlanType
billing_cycle: BillingCycle
}
/** 升级/降级响应 */
export interface ChangePlanResponse {
success: boolean;
message: string;
new_subscription?: SubscriptionInfo;
success: boolean
message: string
new_subscription?: SubscriptionInfo
}
// ============ API 函数 ============
/** 获取当前订阅信息 */
export const getCurrentSubscription = async (): Promise<SubscriptionInfo> => {
const response = await apiClient.get("/subscription/current");
return response.data;
};
const response = await apiClient.get("/subscription/current")
return response.data
}
/** 获取账单记录列表 */
export const getBillingRecords = async (): Promise<BillingRecord[]> => {
const response = await apiClient.get("/subscription/billing-records");
return response.data;
};
const response = await apiClient.get("/subscription/billing-records")
return response.data
}
/** 升级/降级套餐 */
export const changePlan = async (
request: ChangePlanRequest,
): Promise<ChangePlanResponse> => {
const response = await apiClient.post("/subscription/change-plan", request);
return response.data;
};
export const changePlan = async (request: ChangePlanRequest): Promise<ChangePlanResponse> => {
const response = await apiClient.post("/subscription/change-plan", request)
return response.data
}
/** 取消订阅 */
export const cancelSubscription = async (): Promise<{
success: boolean;
message: string;
success: boolean
message: string
}> => {
const response = await apiClient.post("/subscription/cancel");
return response.data;
};
const response = await apiClient.post("/subscription/cancel")
return response.data
}
/** 切换自动续费 */
export const toggleAutoRenew = async (
@@ -103,6 +101,6 @@ export const toggleAutoRenew = async (
): Promise<{ success: boolean; message: string }> => {
const response = await apiClient.post("/subscription/toggle-auto-renew", {
enabled,
});
return response.data;
};
})
return response.data
}
+20 -26
View File
@@ -2,45 +2,39 @@
* 标签 CRUD API
* P3 标签体系:对接后端标签表
*/
import apiClient from "./client";
import apiClient from "./client"
export interface TagItem {
id: string;
name: string;
created_at?: string;
usage_count?: number;
id: string
name: string
created_at?: string
usage_count?: number
}
/** 获取当前用户所有标签 */
export const getTags = async (): Promise<TagItem[]> => {
const response = await apiClient.get("/tags");
return response.data.items || [];
};
const response = await apiClient.get("/tags")
return response.data.items || []
}
/** 创建标签(同名返回 409) */
export const createTag = async (name: string): Promise<TagItem> => {
const response = await apiClient.post("/tags", { name });
return response.data;
};
const response = await apiClient.post("/tags", { name })
return response.data
}
/** 删除标签(同时清理素材关联) */
export const deleteTag = async (tagId: string): Promise<void> => {
await apiClient.delete(`/tags/${tagId}`);
};
await apiClient.delete(`/tags/${tagId}`)
}
/** 为素材添加标签(最多 50 个) */
export const tagAsset = async (
assetId: string,
tagIds: string[],
): Promise<void> => {
if (tagIds.length === 0) return;
await apiClient.post(`/assets/${assetId}/tags`, { tag_ids: tagIds });
};
export const tagAsset = async (assetId: string, tagIds: string[]): Promise<void> => {
if (tagIds.length === 0) return
await apiClient.post(`/assets/${assetId}/tags`, { tag_ids: tagIds })
}
/** 移除素材的某个标签 */
export const untagAsset = async (
assetId: string,
tagId: string,
): Promise<void> => {
await apiClient.delete(`/assets/${assetId}/tags/${tagId}`);
};
export const untagAsset = async (assetId: string, tagId: string): Promise<void> => {
await apiClient.delete(`/assets/${assetId}/tags/${tagId}`)
}
+63 -69
View File
@@ -6,85 +6,84 @@
* - GET /api/v1/tasks/{task_id} — 任务详情(含 error_info
* - POST /api/v1/tasks/{task_id}/retry — 重试失败任务
*/
import apiClient from "./client";
import apiClient from "./client"
/* ──────────── 类型定义 ──────────── */
/** 任务状态 */
export type TaskStatus =
"pending" | "waiting" | "running" | "completed" | "failed" | "cancelled";
export type TaskStatus = "pending" | "waiting" | "running" | "completed" | "failed" | "cancelled"
/** 任务类型 */
export type TaskType = "ingest" | "generation" | string;
export type TaskType = "ingest" | "generation" | string
/** 错误详情 */
export interface TaskErrorInfo {
error_type: string;
error_message: string;
failed_step: string;
stack_trace?: string;
error_type: string
error_message: string
failed_step: string
stack_trace?: string
}
/** 任务条目(对应用户级 UserTaskResponse */
export interface TaskItem {
id: string;
task_type: TaskType;
project_id: string;
template_id?: string;
status: TaskStatus;
progress: number;
current_step: string;
error_message: string;
user_message: string;
retryable: boolean;
source_id: string;
id: string
task_type: TaskType
project_id: string
template_id?: string
status: TaskStatus
progress: number
current_step: string
error_message: string
user_message: string
retryable: boolean
source_id: string
/** 错误详情(失败任务) */
error_info?: TaskErrorInfo;
error_info?: TaskErrorInfo
/** 耗时(秒) */
duration_seconds?: number;
created_at?: string | null;
updated_at?: string | null;
duration_seconds?: number
created_at?: string | null
updated_at?: string | null
}
/** 任务列表查询参数 */
export interface TaskListParams {
page?: number;
page_size?: number;
status?: TaskStatus | "all";
task_type?: TaskType | "all";
page?: number
page_size?: number
status?: TaskStatus | "all"
task_type?: TaskType | "all"
}
/** 任务列表分页响应 */
export interface TaskListResponse {
items: TaskItem[];
total: number;
page: number;
page_size: number;
items: TaskItem[]
total: number
page: number
page_size: number
}
/** 创建生成任务请求参数 */
export interface CreateGenerationTaskRequest {
template_id: string;
asset_ids: string[];
title_ids: string[];
voice_ids: string[];
template_id: string
asset_ids: string[]
title_ids: string[]
voice_ids: string[]
}
/** 创建生成任务响应(对齐后端 GenerationTaskResponse */
export interface CreateGenerationTaskResponse {
id: string;
project_id: string;
asset_library_id: string;
strategy_id: string;
voice_library_id: string;
template_id: string;
asset_ids: string[];
title_ids: string[];
voice_ids: string[];
status: string;
progress: number;
result_count: number;
error_message: string;
id: string
project_id: string
asset_library_id: string
strategy_id: string
voice_library_id: string
template_id: string
asset_ids: string[]
title_ids: string[]
voice_ids: string[]
status: string
progress: number
result_count: number
error_message: string
}
/* ──────────── API 函数 ──────────── */
@@ -93,37 +92,32 @@ export interface CreateGenerationTaskResponse {
export const createGenerationTask = async (
params: CreateGenerationTaskRequest,
): Promise<CreateGenerationTaskResponse> => {
const { data } = await apiClient.post<CreateGenerationTaskResponse>(
"/generation/tasks",
params,
);
return data;
};
const { data } = await apiClient.post<CreateGenerationTaskResponse>("/generation/tasks", params)
return data
}
/** 获取任务列表(支持分页和筛选) */
export const getTasks = async (
params?: TaskListParams,
): Promise<TaskListResponse> => {
export const getTasks = async (params?: TaskListParams): Promise<TaskListResponse> => {
const { data } = await apiClient.get<TaskListResponse>("/tasks", {
params,
});
return data;
};
})
return data
}
/** 获取当前用户的所有任务(兼容旧接口,跨 project) */
export const getUserTasks = async (): Promise<TaskItem[]> => {
const { data } = await apiClient.get("/tasks");
return data.items || data || [];
};
const { data } = await apiClient.get("/tasks")
return data.items || data || []
}
/** 获取单个任务详情(含 error_info */
export const getTask = async (taskId: string): Promise<TaskItem> => {
const { data } = await apiClient.get(`/tasks/${taskId}`);
return data;
};
const { data } = await apiClient.get(`/tasks/${taskId}`)
return data
}
/** 重试失败的任务 */
export const retryTask = async (taskId: string): Promise<TaskItem> => {
const { data } = await apiClient.post(`/tasks/${taskId}/retry`);
return data;
};
const { data } = await apiClient.post(`/tasks/${taskId}/retry`)
return data
}
+74 -84
View File
@@ -7,142 +7,132 @@
* - POST /api/v1/templates/{id}/generate — 从模板生成剪辑计划
* - POST /api/v1/templates/{id}/toggle-favorite — 收藏/取消收藏
*/
import apiClient from "./client";
import type { TitleConfig, SubtitleConfig, BgmConfig } from "./editingPlanner";
import type { EditPlanConfig } from "./editPlans";
import apiClient from "./client"
import type { TitleConfig, SubtitleConfig, BgmConfig } from "./editingPlanner"
import type { EditPlanConfig } from "./editPlans"
/* ──────────── 类型定义 ──────────── */
/** 模板条目(后端 TemplateResponse */
export interface TemplateItem {
id: string;
user_id?: string;
name: string;
description?: string;
mode?: string;
category: string;
tags?: string[];
id: string
user_id?: string
name: string
description?: string
mode?: string
category: string
tags?: string[]
/** 预估时长(后端字段名 estimated_duration */
estimated_duration?: number;
estimated_duration?: number
/** @deprecated 后端已改名为 estimated_duration,保留兼容 */
target_duration?: number;
clip_count?: number;
target_duration?: number
clip_count?: number
/** 使用次数 */
usage_count?: number;
thumbnail_url?: string;
preview_url?: string;
is_active?: boolean;
is_favorite?: boolean;
usage_count?: number
thumbnail_url?: string
preview_url?: string
is_active?: boolean
is_favorite?: boolean
/** 素材规则(片段配置) */
segments?: TemplateSegment[];
segments?: TemplateSegment[]
/** 字幕样式 */
subtitle_config?: SubtitleConfig;
subtitle_config?: SubtitleConfig
/** BGM 配置 */
bgm_config?: BgmConfig;
bgm_config?: BgmConfig
/** 标题配置 */
title_config?: TitleConfig;
title_config?: TitleConfig
/** 视频比例 */
aspect_ratio?: string;
created_at?: string;
updated_at?: string;
aspect_ratio?: string
created_at?: string
updated_at?: string
}
/** 模板片段(素材规则) */
export interface TemplateSegment {
id?: string;
segment_order: number;
duration_min: number;
duration_max: number;
material_type: string | null;
description?: string;
id?: string
segment_order: number
duration_min: number
duration_max: number
material_type: string | null
description?: string
}
/** 模板列表查询参数 */
export interface TemplateListParams {
page?: number;
page_size?: number;
category?: string;
tags?: string;
keyword?: string;
page?: number
page_size?: number
category?: string
tags?: string
keyword?: string
/** 时长筛选(秒):short < 30, medium 30-120, long > 120 */
duration_range?: "short" | "medium" | "long";
duration_range?: "short" | "medium" | "long"
}
/** 模板列表分页响应 */
export interface TemplateListResponse {
items: TemplateItem[];
total: number;
page: number;
page_size: number;
items: TemplateItem[]
total: number
page: number
page_size: number
}
/** 从模板生成剪辑计划请求 */
export interface GenerateFromTemplateRequest {
asset_ids?: string[];
name?: string;
config?: EditPlanConfig;
asset_ids?: string[]
name?: string
config?: EditPlanConfig
}
/** 从模板生成剪辑计划响应 */
export interface GenerateFromTemplateResponse {
plan_id: string;
template_id: string;
status: string;
name: string;
plan_id: string
template_id: string
status: string
name: string
}
/** 复制模板响应 */
export interface CopyTemplateResponse {
id: string;
name: string;
source_template_id: string;
id: string
name: string
source_template_id: string
}
/* ──────────── API 函数 ──────────── */
/** 获取模板列表(支持分页和筛选) */
export const getTemplates = async (
params?: TemplateListParams,
): Promise<TemplateListResponse> => {
export const getTemplates = async (params?: TemplateListParams): Promise<TemplateListResponse> => {
const { data } = await apiClient.get<TemplateListResponse>("/templates", {
params,
});
return data;
};
})
return data
}
/** 获取模板列表(兼容旧接口,返回数组) */
export const getTemplatesList = async (): Promise<TemplateItem[]> => {
const response = await apiClient.get("/templates");
return response.data.items || response.data || [];
};
const response = await apiClient.get("/templates")
return response.data.items || response.data || []
}
/** 获取单个模板详情 */
export const getTemplate = async (
templateId: string,
): Promise<TemplateItem> => {
const response = await apiClient.get(`/templates/${templateId}`);
return response.data;
};
export const getTemplate = async (templateId: string): Promise<TemplateItem> => {
const response = await apiClient.get(`/templates/${templateId}`)
return response.data
}
/** 收藏 / 取消收藏模板 */
export const toggleFavoriteTemplate = async (
templateId: string,
): Promise<{ is_favorite: boolean }> => {
const response = await apiClient.post(
`/templates/${templateId}/toggle-favorite`,
);
return response.data;
};
const response = await apiClient.post(`/templates/${templateId}/toggle-favorite`)
return response.data
}
/** 复制模板(创建副本到我的模板) */
export const copyTemplate = async (
templateId: string,
): Promise<CopyTemplateResponse> => {
const response = await apiClient.post<CopyTemplateResponse>(
`/templates/${templateId}/copy`,
);
return response.data;
};
export const copyTemplate = async (templateId: string): Promise<CopyTemplateResponse> => {
const response = await apiClient.post<CopyTemplateResponse>(`/templates/${templateId}/copy`)
return response.data
}
/** 从模板生成剪辑计划 */
export const generateFromTemplate = async (
@@ -152,9 +142,9 @@ export const generateFromTemplate = async (
const response = await apiClient.post<GenerateFromTemplateResponse>(
`/templates/${templateId}/generate`,
data,
);
return response.data;
};
)
return response.data
}
/* ──────────── 常量 ──────────── */
@@ -167,7 +157,7 @@ export const TEMPLATE_CATEGORY_OPTIONS = [
{ value: "品牌", label: "品牌" },
{ value: "混剪", label: "混剪" },
{ value: "Vlog", label: "Vlog" },
];
]
/** 时长筛选选项 */
export const TEMPLATE_DURATION_OPTIONS = [
@@ -175,4 +165,4 @@ export const TEMPLATE_DURATION_OPTIONS = [
{ value: "short", label: "30秒以内" },
{ value: "medium", label: "30秒-2分钟" },
{ value: "long", label: "2分钟以上" },
];
]
+57 -69
View File
@@ -3,51 +3,51 @@
* Phase 1 新增:全局标题库
* 注意:后端 schema 使用 name + text 字段,前端 UI 用 content 展示
*/
import apiClient from "./client";
import apiClient from "./client"
/** 标题条目(前端展示用) */
export interface TitleItem {
id: string;
content: string;
category?: string;
source?: string;
word_count?: number;
is_favorite?: boolean;
created_at?: string;
updated_at?: string;
id: string
content: string
category?: string
source?: string
word_count?: number
is_favorite?: boolean
created_at?: string
updated_at?: string
}
/** 后端标题响应格式 */
interface BackendTitleResponse {
id: string;
user_id: string;
name: string;
text: string;
category: string;
description: string;
tags: string[];
usage_count: number;
is_active: boolean;
created_at: string;
updated_at: string;
id: string
user_id: string
name: string
text: string
category: string
description: string
tags: string[]
usage_count: number
is_active: boolean
created_at: string
updated_at: string
}
/** 后端创建标题请求格式 */
interface BackendCreateTitleRequest {
name: string;
text: string;
category: string;
description?: string;
tags?: string[];
name: string
text: string
category: string
description?: string
tags?: string[]
}
/** 后端更新标题请求格式 */
interface BackendUpdateTitleRequest {
name?: string;
text?: string;
category?: string;
description?: string;
tags?: string[];
name?: string
text?: string
category?: string
description?: string
tags?: string[]
}
/** 将后端响应映射为前端 TitleItem */
@@ -58,73 +58,61 @@ const toTitleItem = (item: BackendTitleResponse): TitleItem => ({
word_count: item.text?.length || 0,
created_at: item.created_at,
updated_at: item.updated_at,
});
})
/** 创建标题请求(前端接口,保持向后兼容) */
export interface CreateTitleRequest {
content: string;
category?: string;
content: string
category?: string
}
/** 获取当前用户的所有标题 */
export const getTitles = async (): Promise<TitleItem[]> => {
const response = await apiClient.get<
{ items: BackendTitleResponse[] } | BackendTitleResponse[]
>("/titles");
const response = await apiClient.get<{ items: BackendTitleResponse[] } | BackendTitleResponse[]>(
"/titles",
)
// 兼容两种后端返回格式:{ items: [...] } 或直接 [...]
const items = Array.isArray(response.data)
? response.data
: response.data.items || [];
return items.map(toTitleItem);
};
const items = Array.isArray(response.data) ? response.data : response.data.items || []
return items.map(toTitleItem)
}
/** 创建标题 */
export const createTitle = async (
data: CreateTitleRequest,
): Promise<TitleItem> => {
export const createTitle = async (data: CreateTitleRequest): Promise<TitleItem> => {
// 后端要求 name(≤255)和 text(≤500),name 从 content 截取
const payload: BackendCreateTitleRequest = {
name: data.content.slice(0, 255),
text: data.content.slice(0, 500),
category: data.category || "default",
};
const response = await apiClient.post<BackendTitleResponse>(
"/titles",
payload,
);
return toTitleItem(response.data);
};
}
const response = await apiClient.post<BackendTitleResponse>("/titles", payload)
return toTitleItem(response.data)
}
/** 更新标题 */
export const updateTitle = async (
titleId: string,
data: Partial<CreateTitleRequest>,
): Promise<TitleItem> => {
const payload: BackendUpdateTitleRequest = {};
const payload: BackendUpdateTitleRequest = {}
if (data.content !== undefined) {
payload.name = data.content.slice(0, 255);
payload.text = data.content.slice(0, 500);
payload.name = data.content.slice(0, 255)
payload.text = data.content.slice(0, 500)
}
if (data.category !== undefined) {
payload.category = data.category;
payload.category = data.category
}
// 后端用 PUT,非 PATCH
const response = await apiClient.put<BackendTitleResponse>(
`/titles/${titleId}`,
payload,
);
return toTitleItem(response.data);
};
const response = await apiClient.put<BackendTitleResponse>(`/titles/${titleId}`, payload)
return toTitleItem(response.data)
}
/** 删除标题 */
export const deleteTitle = async (titleId: string): Promise<void> => {
await apiClient.delete(`/titles/${titleId}`);
};
await apiClient.delete(`/titles/${titleId}`)
}
/** 批量导入标题 */
export const batchImportTitles = async (
titles: string[],
): Promise<{ imported_count: number }> => {
const response = await apiClient.post("/titles/batch-import", { titles });
return response.data;
};
export const batchImportTitles = async (titles: string[]): Promise<{ imported_count: number }> => {
const response = await apiClient.post("/titles/batch-import", { titles })
return response.data
}
+95 -110
View File
@@ -4,91 +4,91 @@
*
* 任务 3.14 新增
*/
import apiClient from "./client";
import apiClient from "./client"
/* ── 类型定义 ──────────────────────────────────── */
/** TTS 元数据(合成时附带的扩展信息) */
export interface TTSMetadata {
/** 语音时长(秒) */
duration?: number;
duration?: number
/** 采样率(Hz */
sample_rate?: number;
sample_rate?: number
/** 语言 */
language?: string;
language?: string
/** 其他扩展字段 */
[key: string]: unknown;
[key: string]: unknown
}
/** TTS 合成请求参数 */
export interface TTSSynthesizeRequest {
text: string;
voice_id?: string;
output_name?: string;
language?: string;
speed?: number;
voice_model?: string;
voice_clone_profile_id?: string;
format?: string;
metadata?: TTSMetadata;
text: string
voice_id?: string
output_name?: string
language?: string
speed?: number
voice_model?: string
voice_clone_profile_id?: string
format?: string
metadata?: TTSMetadata
}
/** TTS 合成创建响应 */
export interface TTSSynthesizeResponse {
job_id: string;
status: string;
message: string;
job_id: string
status: string
message: string
}
/** TTS 任务详情 */
export interface TTSJob {
id: string;
user_id: string;
project_id: string | null;
text: string;
voice_id: string | null;
voice_model: string | null;
voice_clone_profile_id: string | null;
language: string;
speed: number;
output_name: string | null;
output_audio_url: string | null;
output_format: string;
duration_seconds: number | null;
file_size_bytes: number | null;
sample_rate: number | null;
status: string;
error_message: string | null;
retry_count: number;
max_retries: number;
metadata_: TTSMetadata | null;
created_at: string;
updated_at: string;
id: string
user_id: string
project_id: string | null
text: string
voice_id: string | null
voice_model: string | null
voice_clone_profile_id: string | null
language: string
speed: number
output_name: string | null
output_audio_url: string | null
output_format: string
duration_seconds: number | null
file_size_bytes: number | null
sample_rate: number | null
status: string
error_message: string | null
retry_count: number
max_retries: number
metadata_: TTSMetadata | null
created_at: string
updated_at: string
}
/** TTS 任务状态(轻量轮询用) */
export interface TTSJobStatus {
id: string;
status: string;
output_audio_url: string | null;
error_message: string | null;
duration_seconds: number | null;
retry_count: number;
id: string
status: string
output_audio_url: string | null
error_message: string | null
duration_seconds: number | null
retry_count: number
}
/** TTS 任务列表响应 */
export interface TTSJobListResponse {
items: TTSJob[];
total: number;
skip: number;
limit: number;
items: TTSJob[]
total: number
skip: number
limit: number
}
/** TTS 任务列表查询参数 */
export interface TTSJobListParams {
status?: string;
skip?: number;
limit?: number;
status?: string
skip?: number
limit?: number
}
/* ── API 函数 ──────────────────────────────────── */
@@ -97,47 +97,37 @@ export interface TTSJobListParams {
export const synthesizeSpeech = async (
data: TTSSynthesizeRequest,
): Promise<TTSSynthesizeResponse> => {
const response = await apiClient.post<TTSSynthesizeResponse>(
"/tts/synthesize",
data,
);
return response.data;
};
const response = await apiClient.post<TTSSynthesizeResponse>("/tts/synthesize", data)
return response.data
}
/** 获取 TTS 任务详情 */
export const getTTSJob = async (jobId: string): Promise<TTSJob> => {
const response = await apiClient.get<TTSJob>(`/tts/jobs/${jobId}`);
return response.data;
};
const response = await apiClient.get<TTSJob>(`/tts/jobs/${jobId}`)
return response.data
}
/** 获取 TTS 任务状态(轻量轮询) */
export const getTTSJobStatus = async (jobId: string): Promise<TTSJobStatus> => {
const response = await apiClient.get<TTSJobStatus>(
`/tts/jobs/${jobId}/status`,
);
return response.data;
};
const response = await apiClient.get<TTSJobStatus>(`/tts/jobs/${jobId}/status`)
return response.data
}
/** 获取 TTS 任务列表 */
export const getTTSJobs = async (
params?: TTSJobListParams,
): Promise<TTSJobListResponse> => {
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<TTSJobListResponse>(
`/tts/jobs${qs ? `?${qs}` : ""}`,
);
return response.data;
};
export const getTTSJobs = async (params?: TTSJobListParams): Promise<TTSJobListResponse> => {
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<TTSJobListResponse>(`/tts/jobs${qs ? `?${qs}` : ""}`)
return response.data
}
/** 存为素材请求参数 */
export interface SaveTtsToLibraryRequest {
name?: string;
tag_ids?: string[];
name?: string
tag_ids?: string[]
}
/** 将 TTS 合成结果保存到配音素材库 */
@@ -145,59 +135,54 @@ export const saveTtsToLibrary = async (
jobId: string,
data?: SaveTtsToLibraryRequest,
): Promise<void> => {
await apiClient.post(`/tts/jobs/${jobId}/save-to-library`, data ?? {});
};
await apiClient.post(`/tts/jobs/${jobId}/save-to-library`, data ?? {})
}
/** 删除 TTS 任务 */
export const deleteTTSJob = async (jobId: string): Promise<void> => {
await apiClient.delete(`/tts/jobs/${jobId}`);
};
await apiClient.delete(`/tts/jobs/${jobId}`)
}
/* ── 音色列表 ──────────────────────────────────── */
/** TTS 音色 */
export interface TTSVoice {
id: string;
name: string;
id: string
name: string
/** 音色分类标签:male/female/young/service/news/emotion */
category?: string;
category?: string
/** 语言 */
language?: string;
language?: string
/** 试听 URL */
preview_url?: string;
preview_url?: string
/** 描述 */
description?: string;
description?: string
}
/** 获取 TTS 音色列表 */
export const getTtsVoices = async (): Promise<TTSVoice[]> => {
const response = await apiClient.get<TTSVoice[]>("/tts/voices");
return response.data;
};
const response = await apiClient.get<TTSVoice[]>("/tts/voices")
return response.data
}
/* ── TTS 试听 ──────────────────────────────────── */
/** TTS 试听请求参数 */
export interface TTSPreviewRequest {
text: string;
voice_id: string;
speed?: number;
pitch?: number;
text: string
voice_id: string
speed?: number
pitch?: number
}
/** TTS 试听响应 */
export interface TTSPreviewResponse {
audio_url: string;
duration?: number;
audio_url: string
duration?: number
}
/** TTS 试听 */
export const previewTts = async (
data: TTSPreviewRequest,
): Promise<TTSPreviewResponse> => {
const response = await apiClient.post<TTSPreviewResponse>(
"/tts/preview",
data,
);
return response.data;
};
export const previewTts = async (data: TTSPreviewRequest): Promise<TTSPreviewResponse> => {
const response = await apiClient.post<TTSPreviewResponse>("/tts/preview", data)
return response.data
}
+98 -123
View File
@@ -3,35 +3,35 @@
* 任务 3.11:替换 Mock 数据,对接后端真实 API(3.05)
* 任务 3.15:新增 progress 字段用于进度展示
*/
import apiClient from "./client";
import apiClient from "./client"
/* ── 前端兼容类型 ─────────────────────────────────────── */
/** 克隆音色状态(前端展示用) */
export type VoiceCloneStatus = "ready" | "processing" | "failed";
export type VoiceCloneStatus = "ready" | "processing" | "failed"
/** 克隆音色条目(前端展示用) */
export interface VoiceClone {
id: string;
name: string;
description: string;
duration_seconds: number;
status: VoiceCloneStatus;
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;
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;
name: string
audio_url: string
description?: string
}
/* ── 后端 API 类型 ────────────────────────────────────── */
@@ -39,60 +39,60 @@ export interface CreateVoiceCloneRequest {
/** 音色克隆元数据(克隆时附带的扩展信息) */
export interface VoiceCloneMetadata {
/** 语音时长(秒) */
duration?: number;
duration?: number
/** 采样率(Hz */
sample_rate?: number;
sample_rate?: number
/** 音色 ID(克隆完成后分配) */
voice_id?: string;
voice_id?: string
/** 其他扩展字段 */
[key: string]: unknown;
[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;
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;
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;
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;
name: string
description?: string
source_audio_url: string
voice_model?: string
language?: string
gender?: string
max_retries?: number
metadata_?: VoiceCloneMetadata
}
/* ── 辅助函数 ─────────────────────────────────────────── */
@@ -114,66 +114,54 @@ export const toVoiceClone = (profile: VoiceCloneProfile): VoiceClone => ({
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")}`;
};
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;
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);
};
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;
};
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 getVoiceCloneDetail = async (id: string): Promise<VoiceCloneProfile> => {
const response = await apiClient.get<VoiceCloneProfile>(`/voice-clones/${id}`)
return response.data
}
/** 创建克隆音色 */
export const createVoiceClone = async (
@@ -183,18 +171,15 @@ export const createVoiceClone = async (
name: data.name,
description: data.description,
source_audio_url: data.audio_url,
};
const response = await apiClient.post<VoiceCloneProfile>(
"/voice-clones",
payload,
);
return response.data;
};
}
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}`);
};
await apiClient.delete(`/voice-clones/${id}`)
}
/** 更新克隆音色名称(stub — 后端暂无 PATCH 端点) */
export const updateVoiceClone = async (
@@ -202,32 +187,22 @@ export const updateVoiceClone = async (
data: Partial<Pick<VoiceClone, "name">>,
): Promise<VoiceClone> => {
// 后端暂未提供更新端点,暂用详情接口模拟
const response = await apiClient.get<VoiceCloneProfile>(
`/voice-clones/${id}`,
);
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 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;
};
export const retryVoiceClone = async (id: string): Promise<VoiceCloneProfile> => {
const response = await apiClient.post<VoiceCloneProfile>(`/voice-clones/${id}/retry`)
return response.data
}
+81 -87
View File
@@ -4,148 +4,142 @@
*
* 任务 3.11:新增统一音色 API(对接后端 3.04),保留旧接口向后兼容
*/
import apiClient from "./client";
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;
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;
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[];
voice_id: string
name: string
description: string
gender: string
language: string
preview_url: string | null
tags: string[]
}
/** 预设音色列表响应 */
export interface PresetVoiceListResponse {
items: PresetVoiceItem[];
total: number;
items: PresetVoiceItem[]
total: number
}
/** 统一列表查询参数 */
export interface UnifiedVoiceListParams {
type?: "preset" | "clone";
status?: string;
skip?: number;
limit?: number;
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;
};
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;
};
const response = await apiClient.get<PresetVoiceListResponse>("/voices/presets")
return response.data
}
/* ── 向后兼容(旧接口) ────────────────────────────────── */
/** 配音条目(旧) */
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;
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;
name: string
text: string
voice_type?: string
}
/** 获取当前用户的所有配音(旧 → /voices/legacy */
export const getVoices = async (): Promise<VoiceItem[]> => {
const response = await apiClient.get("/voices/legacy");
return response.data.items || response.data || [];
};
const response = await apiClient.get("/voices/legacy")
return response.data.items || response.data || []
}
/** 创建配音 */
export const createVoice = async (
data: CreateVoiceRequest,
): Promise<VoiceItem> => {
const response = await apiClient.post("/voices", data);
return response.data;
};
export const createVoice = async (data: CreateVoiceRequest): Promise<VoiceItem> => {
const response = await apiClient.post("/voices", data)
return response.data
}
/** 更新配音 */
export const updateVoice = async (
voiceId: string,
data: Partial<CreateVoiceRequest>,
): Promise<VoiceItem> => {
const response = await apiClient.put(`/voices/${voiceId}`, data);
return response.data;
};
const response = await apiClient.put(`/voices/${voiceId}`, data)
return response.data
}
/** 删除配音 */
export const deleteVoice = async (voiceId: string): Promise<void> => {
await apiClient.delete(`/voices/${voiceId}`);
};
await apiClient.delete(`/voices/${voiceId}`)
}
/** AI 生成配音 */
export const generateAIVoice = async (data: {
text: string;
voice_type?: string;
speed?: number;
text: string
voice_type?: string
speed?: number
}): Promise<VoiceItem> => {
const response = await apiClient.post("/voices/generate", data);
return response.data;
};
const response = await apiClient.post("/voices/generate", data)
return response.data
}