diff --git a/apps/web/src/api/assets.ts b/apps/web/src/api/assets.ts index 5a7e7c797..987590a79 100644 --- a/apps/web/src/api/assets.ts +++ b/apps/web/src/api/assets.ts @@ -204,7 +204,7 @@ export const createAsset = async (data: { /** 更新素材(名称、metadata 等) */ export const updateAsset = async ( assetId: string, - data: { name?: string; metadata?: Record }, + data: { name?: string; metadata?: AssetMetadata }, ): Promise => { const response = await apiClient.put(`/assets/${assetId}`, data); return response.data; diff --git a/apps/web/src/api/client.ts b/apps/web/src/api/client.ts index a4f7caada..0fe4a7dea 100644 --- a/apps/web/src/api/client.ts +++ b/apps/web/src/api/client.ts @@ -129,7 +129,8 @@ apiClient.interceptors.response.use( const safeExtractString = (val: unknown): string => { if (typeof val === "string") return val; if (typeof val === "object" && val !== null) { - const obj = val as Record; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- 防御性错误提取,后端错误格式不固定 + const obj = val as Record; if (typeof obj.message === "string") return obj.message; if (typeof obj.msg === "string") return obj.msg; if (typeof obj.detail === "string") return obj.detail; diff --git a/apps/web/src/api/editPlans.ts b/apps/web/src/api/editPlans.ts index c4912dd6e..1ea05ca86 100644 --- a/apps/web/src/api/editPlans.ts +++ b/apps/web/src/api/editPlans.ts @@ -97,6 +97,30 @@ export interface EditPlanConfig { 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; } /** 剪辑计划(后端响应) */ @@ -170,7 +194,7 @@ export interface GenerationStatusResponse { export interface GeneratedVideo { id: string; project_id?: string; - generation_task_id: string; + generation_task_id?: string; name: string; file_url: string; file_size?: number; @@ -182,6 +206,8 @@ export interface GeneratedVideo { status: string; review_status?: string; download_url?: string; + created_at?: string; + updated_at?: string; } /* ============================================================ @@ -226,13 +252,15 @@ export interface GenerateCoverRequest { /** AI 封面生成响应 */ export interface GenerateCoverResponse { plan_id: string; - cover: { - scheme?: string; - asset_id?: string; - frame_time?: number; - thumbnail_url?: string; - [key: string]: unknown; - }; + cover: CoverResult; +} + +/** 封面生成结果 */ +export interface CoverResult { + scheme?: string; + asset_id?: string; + frame_time?: number; + thumbnail_url?: string; } /* ============================================================ diff --git a/apps/web/src/api/editingPlanner.ts b/apps/web/src/api/editingPlanner.ts index 58ac0f3bc..9b41fff37 100644 --- a/apps/web/src/api/editingPlanner.ts +++ b/apps/web/src/api/editingPlanner.ts @@ -139,11 +139,23 @@ export interface GenerateFromTemplatePayload { voiceover_duration: number; } +/** 验证警告详情 */ +export interface ValidationWarningDetails { + /** 相关字段名 */ + field?: string; + /** 期望值 */ + expected?: string | number; + /** 实际值 */ + actual?: string | number; + /** 建议值 */ + suggested?: string | number; +} + /** 验证/生成响应 */ export interface ValidateWarning { code: string; message: string; - details?: Record; + details?: ValidationWarningDetails; } /** 使用模板生成响应 */ diff --git a/apps/web/src/api/products.ts b/apps/web/src/api/products.ts index cdecae5dd..594519d56 100644 --- a/apps/web/src/api/products.ts +++ b/apps/web/src/api/products.ts @@ -60,33 +60,26 @@ export interface BatchDownloadStatus { /** * 将 generation task 数据映射为 ProductItem 格式 */ -function mapTaskToProductItem( - task: GeneratedVideo | Record, -): ProductItem { - const video = task as GeneratedVideo; +function mapTaskToProductItem(task: GeneratedVideo): ProductItem { return { - id: video.id, - title: video.name || "未命名视频", - video_url: video.file_url, - thumbnail_url: video.thumbnail_url, - duration_seconds: video.duration, - file_size: video.file_size, + id: task.id, + title: task.name || "未命名视频", + video_url: task.file_url, + thumbnail_url: task.thumbnail_url, + duration_seconds: task.duration, + file_size: task.file_size, resolution: - video.width && video.height - ? `${video.width}x${video.height}` - : undefined, + task.width && task.height ? `${task.width}x${task.height}` : undefined, status: - video.status === "completed" + task.status === "completed" ? "completed" - : video.status === "failed" + : task.status === "failed" ? "failed" : "processing", - review_status: video.review_status as ReviewStatus | undefined, - project_id: video.project_id, - created_at: (task as Record).created_at as - string | undefined, - updated_at: (task as Record).updated_at as - string | undefined, + review_status: task.review_status as ReviewStatus | undefined, + project_id: task.project_id, + created_at: task.created_at, + updated_at: task.updated_at, }; } diff --git a/apps/web/src/api/tts.ts b/apps/web/src/api/tts.ts index 234ec2454..e59c7fbc9 100644 --- a/apps/web/src/api/tts.ts +++ b/apps/web/src/api/tts.ts @@ -8,6 +8,18 @@ import apiClient from "./client"; /* ── 类型定义 ──────────────────────────────────── */ +/** TTS 元数据(合成时附带的扩展信息) */ +export interface TTSMetadata { + /** 语音时长(秒) */ + duration?: number; + /** 采样率(Hz) */ + sample_rate?: number; + /** 语言 */ + language?: string; + /** 其他扩展字段 */ + [key: string]: unknown; +} + /** TTS 合成请求参数 */ export interface TTSSynthesizeRequest { text: string; @@ -18,7 +30,7 @@ export interface TTSSynthesizeRequest { voice_model?: string; voice_clone_profile_id?: string; format?: string; - metadata?: Record; + metadata?: TTSMetadata; } /** TTS 合成创建响应 */ @@ -49,7 +61,7 @@ export interface TTSJob { error_message: string | null; retry_count: number; max_retries: number; - metadata_: Record | null; + metadata_: TTSMetadata | null; created_at: string; updated_at: string; } diff --git a/apps/web/src/api/voiceClone.ts b/apps/web/src/api/voiceClone.ts index 43072d812..c0d5690f1 100644 --- a/apps/web/src/api/voiceClone.ts +++ b/apps/web/src/api/voiceClone.ts @@ -36,6 +36,18 @@ export interface CreateVoiceCloneRequest { /* ── 后端 API 类型 ────────────────────────────────────── */ +/** 音色克隆元数据(克隆时附带的扩展信息) */ +export interface VoiceCloneMetadata { + /** 语音时长(秒) */ + duration?: number; + /** 采样率(Hz) */ + sample_rate?: number; + /** 音色 ID(克隆完成后分配) */ + voice_id?: string; + /** 其他扩展字段 */ + [key: string]: unknown; +} + /** 后端克隆档案响应 */ export interface VoiceCloneProfile { id: string; @@ -51,7 +63,7 @@ export interface VoiceCloneProfile { error_message: string | null; retry_count: number; max_retries: number; - metadata_: Record | null; + metadata_: VoiceCloneMetadata | null; created_at: string; updated_at: string; } @@ -80,7 +92,7 @@ export interface CreateVoiceCloneRequestFull { language?: string; gender?: string; max_retries?: number; - metadata_?: Record; + metadata_?: VoiceCloneMetadata; } /* ── 辅助函数 ─────────────────────────────────────────── */ diff --git a/apps/web/src/pages/generate/GeneratePage.tsx b/apps/web/src/pages/generate/GeneratePage.tsx index 288c5a4da..ff57f4c33 100644 --- a/apps/web/src/pages/generate/GeneratePage.tsx +++ b/apps/web/src/pages/generate/GeneratePage.tsx @@ -202,8 +202,8 @@ const GeneratePage: React.FC = () => { try { const plan = await getEditPlan(editPlanId); if (plan.name) setTitle(plan.name); - const cfg = plan.config as Record; - if (cfg && Array.isArray(cfg.asset_ids)) { + const cfg = plan.config; + if (cfg?.asset_ids) { setSelectedMaterials( cfg.asset_ids.filter((v): v is string => typeof v === "string"), ); @@ -477,7 +477,13 @@ const GeneratePage: React.FC = () => { setGenerateError(null); try { - const voiceConfig: Record = {}; + const voiceConfig: Pick< + EditPlanConfig, + | "voice_id" + | "voice_clone_profile_id" + | "custom_audio_url" + | "custom_text" + > = {}; if (voiceMode === "preset") { voiceConfig.voice_id = selectedVoice || undefined; } else if (voiceMode === "clone") { @@ -502,7 +508,7 @@ const GeneratePage: React.FC = () => { bgm, generate_count: generateCount, material_mode: materialMode, - } as EditPlanConfig, + }, total_duration: duration, source_edit_plan_id: editPlanId || undefined, }); @@ -561,7 +567,8 @@ const GeneratePage: React.FC = () => { const safeExtract = (val: unknown): string => { if (typeof val === "string") return val; if (typeof val === "object" && val !== null) { - const obj = val as Record; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- 防御性错误提取,后端错误格式不固定 + const obj = val as Record; if (typeof obj.message === "string") return obj.message; if (typeof obj.msg === "string") return obj.msg; if (typeof obj.detail === "string") return obj.detail; @@ -621,7 +628,8 @@ const GeneratePage: React.FC = () => { const extractString = (val: unknown): string => { if (typeof val === "string") return val; if (typeof val === "object" && val !== null) { - const obj = val as Record; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- 防御性错误提取,后端错误格式不固定 + const obj = val as Record; if (typeof obj.message === "string") return obj.message; if (typeof obj.msg === "string") return obj.msg; if (typeof obj.detail === "string") return obj.detail; @@ -651,7 +659,8 @@ const GeneratePage: React.FC = () => { const safeExtractErr = (val: unknown): string => { if (typeof val === "string") return val; if (typeof val === "object" && val !== null) { - const obj = val as Record; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- 防御性错误提取,后端错误格式不固定 + const obj = val as Record; if (typeof obj.message === "string") return obj.message; if (typeof obj.msg === "string") return obj.msg; if (typeof obj.detail === "string") return obj.detail; diff --git a/apps/web/src/pages/templates/TemplateLibrary.tsx b/apps/web/src/pages/templates/TemplateLibrary.tsx index 79a764176..45a3b05e5 100644 --- a/apps/web/src/pages/templates/TemplateLibrary.tsx +++ b/apps/web/src/pages/templates/TemplateLibrary.tsx @@ -99,10 +99,20 @@ const formatDuration = (seconds: number): string => { return `${m}分${s > 0 ? `${s}秒` : ""}`; }; +/** 配置展示字段(formatConfig 提取通用配置的可读属性) */ +interface ConfigDisplayFields { + font_size?: string | number; + font_family?: string; + color?: string; + position?: string; + volume?: string | number; + name?: string; +} + /** 格式化配置对象为可读文本 */ const formatConfig = (config?: object): string => { if (!config || Object.keys(config).length === 0) return "默认"; - const c = config as Record; + const c = config as ConfigDisplayFields; const parts: string[] = []; if (c.font_size) parts.push(`字号: ${c.font_size}`); if (c.font_family) parts.push(`字体: ${c.font_family}`); diff --git a/apps/web/src/pages/voice-materials/VoiceMaterialLibrary.tsx b/apps/web/src/pages/voice-materials/VoiceMaterialLibrary.tsx index 48cae3779..5b5e3101d 100644 --- a/apps/web/src/pages/voice-materials/VoiceMaterialLibrary.tsx +++ b/apps/web/src/pages/voice-materials/VoiceMaterialLibrary.tsx @@ -130,12 +130,20 @@ const mapAssetToMaterial = (asset: AssetItem): VoiceMaterial => { }; }; +/** 配音素材上传元数据(传递给 createAsset 的 metadata) */ +interface VoiceAssetMetadata { + gender: VoiceGender; + description: string; + duration: number; + [key: string]: unknown; +} + /** 前端表单数据 → 后端 metadata(标签走独立 API,不再写 metadata.style) */ const buildMetadata = (data: { gender: VoiceGender; description: string; duration?: number; -}): Record => ({ +}): VoiceAssetMetadata => ({ gender: data.gender, description: data.description, duration: data.duration || 0, diff --git a/apps/web/src/pages/voices/VoiceLibrary.tsx b/apps/web/src/pages/voices/VoiceLibrary.tsx index 5b742e478..e8e83b0d2 100644 --- a/apps/web/src/pages/voices/VoiceLibrary.tsx +++ b/apps/web/src/pages/voices/VoiceLibrary.tsx @@ -623,12 +623,20 @@ const getAudioDuration = (file: File): Promise => audio.src = url; }); +/** 音色上传元数据(传递给 createAsset 的 metadata) */ +interface VoiceUploadMetadata { + gender?: string; + description?: string; + duration?: number; + [key: string]: unknown; +} + const buildVoiceMetadata = (data: { gender?: string; description?: string; duration?: number; -}): Record => { - const metadata: Record = {}; +}): VoiceUploadMetadata => { + const metadata: VoiceUploadMetadata = {}; if (data.gender) metadata.gender = data.gender; if (data.description) metadata.description = data.description; if (data.duration) metadata.duration = Math.round(data.duration);