From be3ed6487e89a22b0546ba5c5d8bb788e6b99683 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Sun, 26 Jul 2026 16:29:01 +0800 Subject: [PATCH] =?UTF-8?q?refactor(template-editor):=20API=E5=B1=82?= =?UTF-8?q?=E6=8C=89=E6=A8=A1=E5=9D=97=E6=8B=86=E5=88=86=EF=BC=88744?= =?UTF-8?q?=E8=A1=8C=E2=86=9288=E8=A1=8C=E5=85=A5=E5=8F=A3,=20-88%?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 template-editor.ts 单文件拆分为目录结构,按业务模块组织: - types.ts — 所有类型定义(EditPlan/Clip/MediaAsset 等) - constants.ts — 常量(TRANSITION_OPTIONS/QUALITY_OPTIONS 等) - editPlans.ts — 模板草稿 CRUD + 生成 + 复制 + 取消 - clips.ts — 片段 CRUD + 批量操作(重排序/批量删除/从素材创建) - aiFeatures.ts — AI 推荐 + AI 封面生成 - mediaAssets.ts — 素材库 API + AssetItem→MediaAsset 映射 - index.ts — 统一入口,re-export 所有类型/常量/函数 入口文件从 744 行精简到 88 行(仅 re-export), 各模块职责单一,便于维护和 tree-shaking。 导入路径 @/api/template-editor 保持向后兼容。 --- apps/web/src/api/template-editor.ts | 744 ------------------ .../web/src/api/template-editor/aiFeatures.ts | 28 + apps/web/src/api/template-editor/clips.ts | 99 +++ apps/web/src/api/template-editor/constants.ts | 66 ++ apps/web/src/api/template-editor/editPlans.ts | 91 +++ apps/web/src/api/template-editor/index.ts | 88 +++ .../src/api/template-editor/mediaAssets.ts | 44 ++ apps/web/src/api/template-editor/types.ts | 440 +++++++++++ 8 files changed, 856 insertions(+), 744 deletions(-) delete mode 100644 apps/web/src/api/template-editor.ts create mode 100644 apps/web/src/api/template-editor/aiFeatures.ts create mode 100644 apps/web/src/api/template-editor/clips.ts create mode 100644 apps/web/src/api/template-editor/constants.ts create mode 100644 apps/web/src/api/template-editor/editPlans.ts create mode 100644 apps/web/src/api/template-editor/index.ts create mode 100644 apps/web/src/api/template-editor/mediaAssets.ts create mode 100644 apps/web/src/api/template-editor/types.ts diff --git a/apps/web/src/api/template-editor.ts b/apps/web/src/api/template-editor.ts deleted file mode 100644 index d9409a596..000000000 --- a/apps/web/src/api/template-editor.ts +++ /dev/null @@ -1,744 +0,0 @@ -/** - * 模板草稿 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 }, -] diff --git a/apps/web/src/api/template-editor/aiFeatures.ts b/apps/web/src/api/template-editor/aiFeatures.ts new file mode 100644 index 000000000..4ae9d76e4 --- /dev/null +++ b/apps/web/src/api/template-editor/aiFeatures.ts @@ -0,0 +1,28 @@ +/** + * AI 推荐 + 封面生成 API + */ +import apiClient from "../client" +import type { + AIRecommendRequest, + AIRecommendResponse, + GenerateCoverRequest, + GenerateCoverResponse, +} from "./types" + +/** 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 +} diff --git a/apps/web/src/api/template-editor/clips.ts b/apps/web/src/api/template-editor/clips.ts new file mode 100644 index 000000000..cbee754e0 --- /dev/null +++ b/apps/web/src/api/template-editor/clips.ts @@ -0,0 +1,99 @@ +/** + * 片段 CRUD + 批量操作 API + */ +import apiClient from "../client" +import type { + EditPlanClip, + EditPlanClipListParams, + EditPlanClipListResponse, + CreateEditPlanClipRequest, + UpdateEditPlanClipRequest, + ClipReorderItem, + ClipReorderResponse, + ClipBatchDeleteResponse, + ClipsFromAssetsResponse, +} from "./types" + +/** 获取片段列表 */ +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 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 +} diff --git a/apps/web/src/api/template-editor/constants.ts b/apps/web/src/api/template-editor/constants.ts new file mode 100644 index 000000000..bce60cd0f --- /dev/null +++ b/apps/web/src/api/template-editor/constants.ts @@ -0,0 +1,66 @@ +/** + * 模板编辑器常量 + */ +import type { TransitionEffect, EditPlanStatus } from "./types" + +/** 转场效果选项(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 }, +] diff --git a/apps/web/src/api/template-editor/editPlans.ts b/apps/web/src/api/template-editor/editPlans.ts new file mode 100644 index 000000000..c4d0785f1 --- /dev/null +++ b/apps/web/src/api/template-editor/editPlans.ts @@ -0,0 +1,91 @@ +/** + * 模板草稿 CRUD + 生成相关 API + */ +import apiClient from "../client" +import type { + EditPlan, + EditPlanListParams, + EditPlanListResponse, + CreateEditPlanRequest, + UpdateEditPlanRequest, + GenerateResponse, + GenerationStatusResponse, + EditPlanGeneration, + GeneratedVideo, + CopyEditPlanRequest, +} from "./types" + +/** 获取模板草稿列表(支持分页和筛选) */ +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 +} + +/** 获取模板草稿关联的生成记录 */ +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`) +} + +/** 复制模板草稿(含所有片段配置) */ +export async function copyEditPlan( + templateId: string, + data?: CopyEditPlanRequest, +): Promise { + const response = await apiClient.post( + `/templates/${templateId}/editor/copy`, + data || {}, + ) + return response.data +} diff --git a/apps/web/src/api/template-editor/index.ts b/apps/web/src/api/template-editor/index.ts new file mode 100644 index 000000000..751f9024b --- /dev/null +++ b/apps/web/src/api/template-editor/index.ts @@ -0,0 +1,88 @@ +/** + * 模板编辑器 API — 按模块拆分后的统一入口 + * 保持与原 template-editor.ts 相同的导出结构,向后兼容 + */ + +// 类型 +export type { + EditPlanStatus, + TitleConfig, + SubtitleConfig, + BgmConfig, + SegmentTtsConfig, + SegmentTrimConfig, + SegmentTransitionConfig, + EditPlanSegment, + EditPlanConfig, + EditPlan, + CreateEditPlanRequest, + UpdateEditPlanRequest, + EditPlanListParams, + EditPlanListResponse, + GenerateResponse, + EditPlanGeneration, + ClipStatusItem, + GenerationStatusResponse, + GeneratedVideo, + AIRecommendRequest, + AIRecommendClipItem, + AIRecommendResponse, + GenerateCoverRequest, + GenerateCoverResponse, + CoverResult, + EditPlanClipStatus, + EditPlanClip, + CreateEditPlanClipRequest, + UpdateEditPlanClipRequest, + EditPlanClipListResponse, + EditPlanClipListParams, + ClipReorderItem, + ClipReorderResponse, + ClipBatchDeleteResponse, + ClipsFromAssetsResponse, + CopyEditPlanRequest, + TransitionEffect, + MediaAsset, +} from "./types" + +// 常量 +export { + TRANSITION_OPTIONS, + MATERIAL_TYPE_LABELS, + MATERIAL_TYPE_ICONS, + PLAN_STATUS_LABELS, + QUALITY_OPTIONS, +} from "./constants" + +// 模板草稿 CRUD + 生成 +export { + getEditPlans, + getEditPlan, + createEditPlan, + updateEditPlan, + deleteEditPlan, + generateEditPlan, + getGenerationStatus, + getEditPlanGenerations, + getGenerationTaskResults, + cancelGeneration, + copyEditPlan, +} from "./editPlans" + +// 片段 CRUD + 批量操作 +export { + getEditPlanClips, + getEditPlanClip, + createEditPlanClip, + updateEditPlanClip, + deleteEditPlanClip, + reorderEditPlanClips, + batchDeleteEditPlanClips, + createClipsFromAssets, +} from "./clips" + +// AI 推荐 + 封面生成 +export { aiRecommendClips, generateCover } from "./aiFeatures" + +// 素材库 +export { getMediaAssets, getMediaAsset } from "./mediaAssets" diff --git a/apps/web/src/api/template-editor/mediaAssets.ts b/apps/web/src/api/template-editor/mediaAssets.ts new file mode 100644 index 000000000..9e13b7680 --- /dev/null +++ b/apps/web/src/api/template-editor/mediaAssets.ts @@ -0,0 +1,44 @@ +/** + * 素材库 API(对接 /assets 接口,映射为 MediaAsset 类型) + */ +import apiClient from "../client" +import type { AssetItem } from "../assets" +import type { MediaAsset } from "./types" + +const inferMediaType = (mimeType: string): "video" | "image" | "audio" => { + if (mimeType.startsWith("video/")) return "video" + if (mimeType.startsWith("image/")) return "image" + return "audio" +} + +const mapAssetToMediaAsset = (asset: AssetItem): MediaAsset => { + 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, + } +} + +/** 获取素材库列表 */ +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) +} diff --git a/apps/web/src/api/template-editor/types.ts b/apps/web/src/api/template-editor/types.ts new file mode 100644 index 000000000..b9bc8bf89 --- /dev/null +++ b/apps/web/src/api/template-editor/types.ts @@ -0,0 +1,440 @@ +/** + * 模板编辑器 API 类型定义 + * 字段名严格匹配后端 API 响应 + */ +import type { + WatermarkConfig, + IntroOutroConfig, + PipConfig, + FilterConfig, + ChromaKeyConfig, + StickerConfig, + CoverConfig, +} from "@/pages/editing-planner/types" + +/* ── 模板草稿状态 ── */ + +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 +} + +/** 模板草稿中的单个片段 */ +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 完整类型 */ +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 + /** 生成视频数量 */ + result_count: number + config: EditPlanConfig + created_at: string + updated_at: string +} + +/** 创建模板草稿请求 */ +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 EditPlanListParams { + page?: number + page_size?: number + template_id?: string + status?: string +} + +/** 模板草稿列表分页响应 */ +export interface EditPlanListResponse { + items: EditPlan[] + total: number + page: number + page_size: number +} + +/* ── 生成相关 ── */ + +/** 生成响应 */ +export interface GenerateResponse { + plan_id: string + plan_status: EditPlanStatus + generation_task_id: string + clip_count: number +} + +/** 模板草稿关联的生成记录 */ +export interface EditPlanGeneration { + id: string + 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 +} + +/** 生成视频详情 */ +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 推荐 & 封面生成 ── */ + +/** 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 +} + +/* ── 片段 CRUD 相关 ── */ + +/** 片段状态 */ +export type EditPlanClipStatus = "pending" | "processing" | "ready" | "failed" + +/** 剪辑片段(后端响应) */ +export interface EditPlanClip { + id: string + plan_id: string + 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 + 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 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 interface CopyEditPlanRequest { + name?: string + project_id?: string +} + +/* ── 前端 UI 类型 ── */ + +/** 转场效果(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 层类型) */ +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" +} -- 2.54.0