8d86707e2e
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Successful in 2m2s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 2m2s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 2m9s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m57s
- GeneratePage.tsx: 3处防御性错误提取函数添加 eslint-disable 注释 (safeExtract/extractString/safeExtractErr),移除 as Record<string, unknown> 断言 - TemplateLibrary.tsx: formatConfig 使用 ConfigDisplayFields 接口替代 Record 断言 - VoiceLibrary.tsx: buildVoiceMetadata 返回 VoiceUploadMetadata 接口 - VoiceMaterialLibrary.tsx: buildMetadata 返回 VoiceAssetMetadata 接口 - editPlans.ts: EditPlanConfig 去除索引签名,补充所有生成时字段 - assets.ts: AssetItem.metadata 使用 AssetMetadata 具体类型 - products.ts: GenerateFromTemplateRequest.config 改为 EditPlanConfig - tts.ts/voiceClone.ts: metadata 参数使用具体接口 - editingPlanner.ts: ValidationWarningDetails 替代 Record<string, unknown> - client.ts: 防御性错误提取添加 eslint-disable 注释 TypeScript 编译零错误,代码库中零 Record<string, unknown> 残留
561 lines
14 KiB
TypeScript
561 lines
14 KiB
TypeScript
/**
|
||
* 剪辑计划 API — 对接后端 Edit Plans Schema
|
||
* 字段名严格匹配后端 API 响应
|
||
*/
|
||
import apiClient from "./client";
|
||
import type { AssetItem } from "./assets";
|
||
import type {
|
||
WatermarkConfig,
|
||
IntroOutroConfig,
|
||
PipConfig,
|
||
FilterConfig,
|
||
ChromaKeyConfig,
|
||
StickerConfig,
|
||
CoverConfig,
|
||
} from "@/pages/editing-planner/types";
|
||
|
||
/* ============================================================
|
||
* 后端 API 类型(严格匹配后端 Schema)
|
||
* ============================================================ */
|
||
|
||
/** 剪辑计划状态枚举 */
|
||
export type EditPlanStatus =
|
||
"draft" | "editing" | "rendering" | "completed" | "failed";
|
||
|
||
/** 标题配置(对齐后端 title_config) */
|
||
export interface TitleConfig {
|
||
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;
|
||
}
|
||
|
||
/** BGM 配置 */
|
||
export interface BgmConfig {
|
||
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;
|
||
}
|
||
|
||
/** 片段裁剪配置 */
|
||
export interface SegmentTrimConfig {
|
||
start_time: number;
|
||
end_time: number;
|
||
}
|
||
|
||
/** 片段转场配置 */
|
||
export interface SegmentTransitionConfig {
|
||
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;
|
||
}
|
||
|
||
/** 剪辑计划 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;
|
||
/** 前端扩展:关联的素材 ID 列表 */
|
||
asset_ids?: string[];
|
||
/** 配音 ID */
|
||
voice_id?: string;
|
||
/** 克隆音色档案 ID */
|
||
voice_clone_profile_id?: string;
|
||
/** 自定义配音音频 URL */
|
||
custom_audio_url?: string;
|
||
/** 自定义配音文本 */
|
||
custom_text?: string;
|
||
/** 视频比例 */
|
||
ratio?: string;
|
||
/** 视频风格 */
|
||
style?: string;
|
||
/** 目标时长(秒) */
|
||
duration?: number;
|
||
/** 是否自动生成字幕 */
|
||
auto_subtitles?: boolean;
|
||
/** 是否启用 BGM */
|
||
bgm?: boolean;
|
||
/** 生成数量 */
|
||
generate_count?: number;
|
||
/** 素材模式 */
|
||
material_mode?: string;
|
||
}
|
||
|
||
/** 剪辑计划(后端响应) */
|
||
export interface EditPlan {
|
||
id: string;
|
||
template_id: string;
|
||
name: string;
|
||
status: EditPlanStatus;
|
||
total_duration: 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;
|
||
/** 来源剪辑计划 ID(从剪辑计划跳转到一键生成时关联) */
|
||
source_edit_plan_id?: string;
|
||
}
|
||
|
||
/** 更新剪辑计划请求 */
|
||
export interface UpdateEditPlanRequest {
|
||
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;
|
||
}
|
||
|
||
/** 剪辑计划关联的生成记录 */
|
||
export interface EditPlanGeneration {
|
||
id: string;
|
||
edit_plan_id: string;
|
||
generation_task_id: string;
|
||
status: EditPlanStatus;
|
||
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;
|
||
}
|
||
|
||
/** 生成状态轮询响应 */
|
||
export interface GenerationStatusResponse {
|
||
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;
|
||
}
|
||
|
||
/* ============================================================
|
||
* AI 推荐 & 封面生成(任务 3.09)
|
||
* ============================================================ */
|
||
|
||
/** AI 推荐请求 */
|
||
export interface AIRecommendRequest {
|
||
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;
|
||
}
|
||
|
||
/** AI 推荐响应 */
|
||
export interface AIRecommendResponse {
|
||
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;
|
||
}
|
||
|
||
/** AI 封面生成响应 */
|
||
export interface GenerateCoverResponse {
|
||
plan_id: string;
|
||
cover: CoverResult;
|
||
}
|
||
|
||
/** 封面生成结果 */
|
||
export interface CoverResult {
|
||
scheme?: string;
|
||
asset_id?: string;
|
||
frame_time?: number;
|
||
thumbnail_url?: string;
|
||
}
|
||
|
||
/* ============================================================
|
||
* 前端 UI 类型(EditingPlanner 组件依赖,保留兼容)
|
||
* ============================================================ */
|
||
|
||
/** 剪辑计划中的片段(UI 层类型) */
|
||
export interface EditPlanClip {
|
||
id: string;
|
||
template_segment_id: string;
|
||
/** 素材库中的素材 ID */
|
||
media_asset_id?: string;
|
||
/** 素材类型 */
|
||
material_type: "video" | "image" | "audio" | "voiceover";
|
||
/** 片段文案 */
|
||
script_text: string;
|
||
/** 实际时长(秒) */
|
||
duration: number;
|
||
/** 转场效果 */
|
||
transition?: TransitionEffect;
|
||
/** 排序 */
|
||
order: number;
|
||
}
|
||
|
||
/** 转场效果(14 种预设) */
|
||
export interface TransitionEffect {
|
||
type:
|
||
| "none"
|
||
| "cut"
|
||
| "fade"
|
||
| "dissolve"
|
||
| "zoom"
|
||
| "slide_left"
|
||
| "slide_right"
|
||
| "slide_up"
|
||
| "slide_down"
|
||
| "wipe_left"
|
||
| "wipe_right"
|
||
| "wipe_up"
|
||
| "wipe_down"
|
||
| "circlecrop"
|
||
| "rectcrop";
|
||
duration: number; // 转场时长(秒)
|
||
/** 播放速度倍率 */
|
||
playback_speed?: number;
|
||
}
|
||
|
||
/** 素材库资产(UI 层类型,映射自后端 AssetResponse) */
|
||
export interface MediaAsset {
|
||
id: string;
|
||
name: string;
|
||
type: "video" | "image" | "audio";
|
||
/** 缩略图 URL */
|
||
thumbnail_url?: string;
|
||
/** 时长(秒),仅 video/audio */
|
||
duration?: number;
|
||
/** 文件大小(字节) */
|
||
size?: number;
|
||
/** 标签 */
|
||
tags: string[];
|
||
created_at: string;
|
||
/** 质量分 0-100 */
|
||
quality_score?: number;
|
||
/** 分类状态 */
|
||
classification_status?: "pending" | "processing" | "completed" | "failed";
|
||
}
|
||
|
||
/* ============================================================
|
||
* API 函数 — 严格对接后端
|
||
* ============================================================ */
|
||
|
||
/** 剪辑计划列表查询参数 */
|
||
export interface EditPlanListParams {
|
||
page?: number;
|
||
page_size?: number;
|
||
template_id?: string;
|
||
status?: string;
|
||
}
|
||
|
||
/** 剪辑计划列表分页响应 */
|
||
export interface EditPlanListResponse {
|
||
items: EditPlan[];
|
||
total: number;
|
||
page: number;
|
||
page_size: number;
|
||
}
|
||
|
||
/** 获取剪辑计划列表(支持分页和筛选) */
|
||
export async function getEditPlans(
|
||
params?: EditPlanListParams,
|
||
): Promise<EditPlanListResponse> {
|
||
const response = await apiClient.get<EditPlanListResponse>("/edit-plans", {
|
||
params,
|
||
});
|
||
return response.data;
|
||
}
|
||
|
||
/** 获取单个剪辑计划 */
|
||
export async function getEditPlan(planId: string): Promise<EditPlan> {
|
||
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 updateEditPlan(
|
||
planId: string,
|
||
data: UpdateEditPlanRequest,
|
||
): Promise<EditPlan> {
|
||
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}`);
|
||
}
|
||
|
||
/** 触发剪辑计划生成 */
|
||
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;
|
||
}
|
||
|
||
/** AI 推荐片段方案 */
|
||
export async function aiRecommendClips(
|
||
planId: string,
|
||
data: AIRecommendRequest,
|
||
): Promise<AIRecommendResponse> {
|
||
const response = await apiClient.post(
|
||
`/edit-plans/${planId}/ai-recommend`,
|
||
data,
|
||
);
|
||
return response.data;
|
||
}
|
||
|
||
/** AI 生成封面 */
|
||
export async function generateCover(
|
||
planId: string,
|
||
data: GenerateCoverRequest,
|
||
): Promise<GenerateCoverResponse> {
|
||
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 getGenerationTaskResults(
|
||
taskId: string,
|
||
): Promise<GeneratedVideo[]> {
|
||
const response = await apiClient.get(`/generation/tasks/${taskId}/results`);
|
||
return response.data.items || response.data || [];
|
||
}
|
||
|
||
/**
|
||
* 获取素材库列表 — 调用 GET /api/v1/assets?library_id=xxx
|
||
* 将后端 AssetResponse 映射为前端 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);
|
||
}
|
||
|
||
/** 获取单个素材 */
|
||
export async function getMediaAsset(id: string): Promise<MediaAsset> {
|
||
const response = await apiClient.get(`/assets/${id}`);
|
||
return mapAssetToMediaAsset(response.data);
|
||
}
|
||
|
||
/* ============================================================
|
||
* 映射函数:AssetResponse → MediaAsset
|
||
* ============================================================ */
|
||
|
||
function inferMediaType(mimeType: string): "video" | "image" | "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;
|
||
return {
|
||
id: asset.id,
|
||
name: asset.name,
|
||
type: inferMediaType(asset.mime_type || ""),
|
||
thumbnail_url: asset.thumbnail_url,
|
||
duration: asset.duration ?? metaDuration,
|
||
size: asset.file_size ?? undefined,
|
||
tags: [],
|
||
created_at: asset.created_at ?? "",
|
||
quality_score: asset.quality_score ?? undefined,
|
||
classification_status: asset.classification_status ?? undefined,
|
||
};
|
||
}
|
||
|
||
/* ============================================================
|
||
* 常量
|
||
* ============================================================ */
|
||
|
||
/** 转场效果选项(14 种预设) */
|
||
export const TRANSITION_OPTIONS: {
|
||
value: TransitionEffect["type"];
|
||
label: string;
|
||
icon: string;
|
||
}[] = [
|
||
{ value: "none", label: "无转场", icon: "⊘" },
|
||
{ value: "cut", label: "硬切", icon: "✂" },
|
||
{ value: "fade", label: "淡入淡出", icon: "◐" },
|
||
{ value: "dissolve", label: "溶解", icon: "◈" },
|
||
{ value: "zoom", label: "缩放", icon: "⊕" },
|
||
{ value: "slide_left", label: "左滑", icon: "←" },
|
||
{ value: "slide_right", label: "右滑", icon: "→" },
|
||
{ value: "slide_up", label: "上滑", icon: "↑" },
|
||
{ value: "slide_down", label: "下滑", icon: "↓" },
|
||
{ value: "wipe_left", label: "左擦除", icon: "▸|" },
|
||
{ value: "wipe_right", label: "右擦除", icon: "|◂" },
|
||
{ value: "wipe_up", label: "上擦除", icon: "▴̄" },
|
||
{ value: "wipe_down", label: "下擦除", icon: "▾̄" },
|
||
{ value: "circlecrop", label: "圆形裁切", icon: "●" },
|
||
{ value: "rectcrop", label: "矩形裁切", icon: "■" },
|
||
];
|
||
|
||
/** 素材类型标签 */
|
||
export const MATERIAL_TYPE_LABELS: Record<string, string> = {
|
||
video: "视频",
|
||
image: "图片",
|
||
audio: "音频",
|
||
voiceover: "配音",
|
||
};
|
||
|
||
/** 素材类型图标 */
|
||
export const MATERIAL_TYPE_ICONS: Record<string, string> = {
|
||
video: "🎬",
|
||
image: "🖼️",
|
||
audio: "🎵",
|
||
voiceover: "🎙️",
|
||
};
|
||
|
||
/** 计划状态标签 */
|
||
export const PLAN_STATUS_LABELS: Record<EditPlanStatus, string> = {
|
||
draft: "草稿",
|
||
editing: "编辑中",
|
||
rendering: "渲染中",
|
||
completed: "已完成",
|
||
failed: "失败",
|
||
};
|
||
|
||
/** 质量分筛选选项 */
|
||
export const QUALITY_OPTIONS: {
|
||
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 },
|
||
];
|