export type VideoCanvasMotionMention = { custom: boolean; id: string; index: number; label: string; motionId: string; offset: number; previewUrl: string; prompt: string; }; function normalizeMotionMentionId(value: unknown) { return String(value || "") .trim() .replace(/[^A-Za-z0-9_-]+/g, "-") .slice(0, 120); } export function normalizeVideoCanvasMotionMentions( value: unknown, ): VideoCanvasMotionMention[] { if (!Array.isArray(value)) return []; return value.slice(0, 100).flatMap((item, position) => { if (!item || typeof item !== "object") return []; const record = item as Record; const label = String(record.label || "").trim().slice(0, 100); if (!label) return []; const motionId = normalizeMotionMentionId(record.motionId) || `motion-${position + 1}`; return [{ custom: record.custom === true, id: normalizeMotionMentionId(record.id) || `motion-mention-${motionId}-${position + 1}`, index: Math.max( 1, Math.min(100, Math.round(Number(record.index) || position + 1)), ), label, motionId, offset: Math.max(0, Math.round(Number(record.offset) || 0)), previewUrl: String(record.previewUrl || "").trim().slice(0, 4096), prompt: String(record.prompt || label).trim().slice(0, 2000) || label, }]; }); } export function composeVideoCanvasMotionPrompt( prompt: string, motionMentions: VideoCanvasMotionMention[], ) { return [ String(prompt || "").trim(), ...motionMentions.map((mention) => { const instruction = mention.prompt.trim() || mention.label; return `运镜要求「${mention.label}」:${instruction}`; }), ] .filter(Boolean) .join(" "); }