import "server-only"; import type { NormalizedStoryboardScript, NormalizedStoryboardShot } from "@/lib/video-agent/types"; function detectLanguage(text: string): "zh" | "en" | "mixed" { const source = String(text || ""); const zhCount = (source.match(/[\u4e00-\u9fff]/g) || []).length; const enCount = (source.match(/[A-Za-z]/g) || []).length; if (zhCount > 0 && enCount > 0) return "mixed"; if (zhCount > 0) return "zh"; return "en"; } function cleanLine(line: string) { return String(line || "") .replace(/^\s*(?:第?\s*\d+\s*(?:镜头|段|幕)|\d+\s*[.)、::\-]?|[一二三四五六七八九十]+\s*[、.::\-]?)\s*/g, "") .replace(/\s+/g, " ") .trim(); } function splitScriptLines(script: string): string[] { const source = String(script || "").trim(); if (!source) return []; const byLine = source .split(/\r?\n+/) .map(cleanLine) .filter(Boolean); if (byLine.length >= 4) return byLine; return source .split(/[。!?!?;;\n]+/g) .map(cleanLine) .filter(Boolean); } function parsePreferredTileIndex(text: string, maxShots: number): number | undefined { const source = String(text || ""); const patterns = [ /第\s*(\d{1,2})\s*(?:张|格|图|镜头)/i, /(?:图|格|镜头|shot)\s*(\d{1,2})/i, /#\s*(\d{1,2})/i, ]; for (const pattern of patterns) { const matched = source.match(pattern); const candidate = Number(matched?.[1] || 0); if (candidate >= 1 && candidate <= maxShots) return candidate; } return undefined; } function inferCamera(text: string) { const source = String(text || ""); if (/推进|推近|close[- ]?in|dolly in/i.test(source)) return "轻微推进"; if (/拉远|远景|zoom out|wide/i.test(source)) return "缓慢拉远"; if (/平移|横移|pan/i.test(source)) return "平稳平移"; if (/旋转|环绕|orbit/i.test(source)) return "环绕镜头"; return "稳定镜头"; } function inferMood(text: string) { const source = String(text || ""); if (/紧张|压迫|惊悚|tense/i.test(source)) return "紧张"; if (/温暖|治愈|暖/i.test(source)) return "温暖"; if (/活力|动感|快节奏|energetic/i.test(source)) return "活力"; if (/浪漫|梦幻|romantic/i.test(source)) return "浪漫"; return "自然"; } function buildDefaultAction(index: number) { const defaults = [ "主体从静止到轻微动作", "主体与环境产生互动", "主体视线发生转移", "主体做细节动作展示", "镜头展示关键部位", "镜头拉近突出质感", "镜头切换到侧面动态", "主体完成动作收束", "镜头定格形成结尾", ]; return defaults[index - 1] || `主体完成第 ${index} 段自然动作`; } export function runScriptNormalizerSkill(params: { scriptText: string; maxShots?: number; totalDurationSec?: number; }): NormalizedStoryboardScript { const maxShots = Math.max(1, Math.min(20, Math.round(params.maxShots || 9))); const raw = String(params.scriptText || "").trim(); const lines = splitScriptLines(raw); const effectiveLines = lines.slice(0, maxShots); const shotCount = Math.max(1, effectiveLines.length); const durationPerShot = Math.max( 2, Math.round(Math.max(5, Number(params.totalDurationSec || 10)) / shotCount) ); const normalizedShots: NormalizedStoryboardShot[] = []; const language = detectLanguage(raw); const fallbackStyle = language === "en" ? "cinematic realistic style" : "写实电影感"; for (let index = 1; index <= shotCount; index += 1) { const line = effectiveLines[index - 1] || ""; const action = cleanLine(line) || buildDefaultAction(index); normalizedShots.push({ index, durationSec: durationPerShot, action, camera: inferCamera(line), mood: inferMood(line), negativePrompt: language === "en" ? "jitter, blur, artifacts, low quality" : "抖动, 畸变, 低清, 伪影", preferredTileIndex: parsePreferredTileIndex(line, maxShots), }); } return { language, globalStyle: fallbackStyle, shotCount: normalizedShots.length, shots: normalizedShots, raw, }; }