129 lines
4.1 KiB
TypeScript
129 lines
4.1 KiB
TypeScript
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,
|
||
};
|
||
}
|