/** * 模板草稿 API — 对接后端 Template Editor 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" | "cancelled" /** 标题配置(对齐后端 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 /** 生成视频数量(后端 EditPlanResponse.result_count) */ 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 /** 来源模板草稿 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 } /** 模板草稿关联的生成记录(实际是 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 logs: Array> 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 error_message?: string } /** 生成状态轮询响应 */ export interface GenerationStatusResponse { plan_id: string plan_status: EditPlanStatus generation_task_id?: string error_message?: string clips: ClipStatusItem[] error?: string message?: string } /** 生成视频详情(对应后端 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 组件依赖,保留兼容) * ============================================================ */ /** 转场效果(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("/templates/drafts", { params, }) return response.data } /** 获取单个模板草稿 */ export async function getEditPlan(templateId: string): Promise { const response = await apiClient.get(`/templates/${templateId}/editor`) return response.data } /** 创建模板草稿 */ export async function createEditPlan(data: CreateEditPlanRequest): Promise { const response = await apiClient.post("/templates/drafts", data) return response.data } /** 更新模板草稿 */ export async function updateEditPlan( templateId: string, data: UpdateEditPlanRequest, ): Promise { const response = await apiClient.put(`/templates/${templateId}/editor`, data) return response.data } /** 删除模板草稿 */ export async function deleteEditPlan(templateId: string): Promise { await apiClient.delete(`/templates/${templateId}/editor`) } /** 触发生成 */ export async function generateEditPlan(templateId: string): Promise { const response = await apiClient.post(`/templates/${templateId}/editor/generate`) return response.data } /** 获取生成状态(轮询用) */ export async function getGenerationStatus(templateId: string): Promise { const response = await apiClient.get(`/templates/${templateId}/editor/generation-status`) return response.data } /** AI 推荐片段方案 */ export async function aiRecommendClips( templateId: string, data: AIRecommendRequest, ): Promise { const response = await apiClient.post(`/templates/${templateId}/editor/ai-recommend`, data) return response.data } /** AI 生成封面 */ export async function generateCover( templateId: string, data: GenerateCoverRequest, ): Promise { const response = await apiClient.post(`/templates/${templateId}/editor/generate-cover`, data) return response.data } /** 获取模板草稿关联的生成记录 */ export async function getEditPlanGenerations(templateId: string): Promise { const response = await apiClient.get(`/templates/${templateId}/editor/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 || [] } /** 取消生成任务 */ export async function cancelGeneration(templateId: string): Promise { await apiClient.post(`/templates/${templateId}/editor/cancel`) } /* ============================================================ * 片段 CRUD(后端 EditPlanClip 独立表) * ============================================================ */ /** 片段状态 */ 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 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 } /** 更新片段请求 */ 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 } /** 片段列表响应 */ export interface EditPlanClipListResponse { items: EditPlanClip[] total: number } /** 片段列表查询参数 */ export interface EditPlanClipListParams { status?: string skip?: number limit?: number } /** 获取片段列表 */ export async function getEditPlanClips( templateId: string, params?: EditPlanClipListParams, ): Promise { const response = await apiClient.get( `/templates/${templateId}/editor/clips`, { params, }, ) return response.data } /** 获取单个片段详情 */ export async function getEditPlanClip(templateId: string, clipId: string): Promise { const response = await apiClient.get( `/templates/${templateId}/editor/clips/${clipId}`, ) return response.data } /** 创建片段 */ export async function createEditPlanClip( templateId: string, data: CreateEditPlanClipRequest, ): Promise { const response = await apiClient.post(`/templates/${templateId}/editor/clips`, data) return response.data } /** 更新片段 */ export async function updateEditPlanClip( templateId: string, clipId: string, data: UpdateEditPlanClipRequest, ): Promise { const response = await apiClient.put( `/templates/${templateId}/editor/clips/${clipId}`, data, ) return response.data } /** 删除片段 */ export async function deleteEditPlanClip(templateId: string, clipId: string): Promise { await apiClient.delete(`/templates/${templateId}/editor/clips/${clipId}`) } /* ============================================================ * 片段批量操作 * ============================================================ */ /** 重排序条目 */ export interface ClipReorderItem { clip_id: string new_order: number } /** 重排序响应 */ export interface ClipReorderResponse { success: boolean updated_count: number message: string } /** 批量删除响应 */ export interface ClipBatchDeleteResponse { success: boolean deleted_count: number message: string } /** 从素材批量创建响应 */ export interface ClipsFromAssetsResponse { success: boolean created_count: number message: string clip_ids: string[] } /** 片段重排序(拖拽排序后一次性提交) */ export async function reorderEditPlanClips( templateId: string, items: ClipReorderItem[], ): Promise { const response = await apiClient.post( `/templates/${templateId}/editor/clips/reorder`, { items }, ) return response.data } /** 批量删除片段 */ export async function batchDeleteEditPlanClips( templateId: string, clipIds: string[], ): Promise { const response = await apiClient.post( `/templates/${templateId}/editor/clips/batch-delete`, { clip_ids: clipIds }, ) return response.data } /** 从素材批量创建片段(追加到时间线末尾) */ export async function createClipsFromAssets( templateId: string, assetIds: string[], clipType = "main", ): Promise { const response = await apiClient.post( `/templates/${templateId}/editor/clips/from-assets`, { asset_ids: assetIds, clip_type: clipType }, ) return response.data } /* ============================================================ * 复制计划 * ============================================================ */ /** 复制计划请求 */ export interface CopyEditPlanRequest { name?: string project_id?: string } /** 复制模板草稿(含所有片段配置) */ export async function copyEditPlan( templateId: string, data?: CopyEditPlanRequest, ): Promise { const response = await apiClient.post( `/templates/${templateId}/editor/copy`, data || {}, ) return 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: "失败", cancelled: "已取消", } /** 质量分筛选选项 */ 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 }, ]