/** * 剪辑计划 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 { const response = await apiClient.get("/edit-plans", { params, }); return response.data; } /** 获取单个剪辑计划 */ export async function getEditPlan(planId: string): Promise { const response = await apiClient.get(`/edit-plans/${planId}`); return response.data; } /** 创建剪辑计划 */ export async function createEditPlan( data: CreateEditPlanRequest, ): Promise { const response = await apiClient.post("/edit-plans", data); return response.data; } /** 更新剪辑计划 */ export async function updateEditPlan( planId: string, data: UpdateEditPlanRequest, ): Promise { const response = await apiClient.put(`/edit-plans/${planId}`, data); return response.data; } /** 删除剪辑计划 */ export async function deleteEditPlan(planId: string): Promise { await apiClient.delete(`/edit-plans/${planId}`); } /** 触发剪辑计划生成 */ export async function generateEditPlan( planId: string, ): Promise { const response = await apiClient.post(`/edit-plans/${planId}/generate`); return response.data; } /** 获取剪辑计划生成状态(轮询用) */ export async function getGenerationStatus( planId: string, ): Promise { const response = await apiClient.get( `/edit-plans/${planId}/generation-status`, ); return response.data; } /** AI 推荐片段方案 */ export async function aiRecommendClips( planId: string, data: AIRecommendRequest, ): Promise { const response = await apiClient.post( `/edit-plans/${planId}/ai-recommend`, data, ); return response.data; } /** AI 生成封面 */ export async function generateCover( planId: string, data: GenerateCoverRequest, ): Promise { const response = await apiClient.post( `/edit-plans/${planId}/generate-cover`, data, ); return response.data; } /** 获取剪辑计划关联的生成记录 */ export async function getEditPlanGenerations( planId: string, ): Promise { const response = await apiClient.get(`/edit-plans/${planId}/generations`); return response.data.items || []; } /** 获取生成任务的视频结果列表 */ export async function getGenerationTaskResults( taskId: string, ): Promise { 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 { 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 { 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 = { video: "视频", image: "图片", audio: "音频", voiceover: "配音", }; /** 素材类型图标 */ export const MATERIAL_TYPE_ICONS: Record = { video: "🎬", image: "🖼️", audio: "🎵", voiceover: "🎙️", }; /** 计划状态标签 */ export const PLAN_STATUS_LABELS: Record = { 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 }, ];