/** * 剪辑计划 API — 对接后端 Edit Plans Schema * 字段名严格匹配后端 API 响应 */ import apiClient from "./client"; import type { AssetItem } from "./assets"; /* ============================================================ * 后端 API 类型(严格匹配后端 Schema) * ============================================================ */ /** 剪辑计划状态枚举 */ export type EditPlanStatus = "draft" | "editing" | "rendering" | "completed" | "failed"; /** 剪辑计划(后端响应) */ export interface EditPlan { id: string; template_id: string; name: string; status: EditPlanStatus; total_duration: number; config: Record; created_at: string; updated_at: string; } /** 创建剪辑计划请求(后端要求 template_id + name 必填) */ export interface CreateEditPlanRequest { template_id: string; name: string; config?: Record; total_duration?: number; /** 来源剪辑计划 ID(从剪辑计划跳转到一键生成时关联) */ source_edit_plan_id?: string; } /** 更新剪辑计划请求 */ export interface UpdateEditPlanRequest { name?: string; config?: Record; 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[]; } /* ============================================================ * 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: Record; } /** AI 推荐响应 */ export interface AIRecommendResponse { plan_id: string; clips: AIRecommendClipItem[]; config: Record; 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: Record; } /* ============================================================ * 前端 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; } /** 转场效果 */ export interface TransitionEffect { type: "none" | "fade" | "dissolve" | "wipe" | "zoom" | "slide"; duration: 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 async function getEditPlans(params?: { page?: number; page_size?: number; template_id?: string; status?: string; }): Promise { const response = await apiClient.get("/edit-plans", { params }); return response.data.items || []; } /** 获取单个剪辑计划 */ 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 || []; } /** * 获取素材库列表 — 调用 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 { const meta = (asset.metadata || {}) as Record; const ext = asset as AssetItem & Record; return { id: asset.id, name: asset.name, type: inferMediaType(asset.mime_type || ""), thumbnail_url: typeof ext.thumbnail_url === "string" ? ext.thumbnail_url : undefined, duration: typeof ext.duration === "number" ? ext.duration : typeof meta.duration === "number" ? (meta.duration as number) : undefined, size: asset.file_size ?? undefined, tags: [], created_at: asset.created_at ?? "", quality_score: asset.quality_score ?? undefined, classification_status: (asset.classification_status ?? undefined) as MediaAsset["classification_status"], }; } /* ============================================================ * 常量 * ============================================================ */ /** 转场效果选项 */ export const TRANSITION_OPTIONS: { value: TransitionEffect["type"]; label: string; }[] = [ { value: "none", label: "无转场" }, { value: "fade", label: "淡入淡出" }, { value: "dissolve", label: "溶解" }, { value: "wipe", label: "擦除" }, { value: "zoom", label: "缩放" }, { value: "slide", label: "滑动" }, ]; /** 素材类型标签 */ 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 }, ];