ai-chatbot/lib/video-agent/skills/script-normalizer.ts
Your Name ec988be30a
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
Update agent workflow and video task handling
2026-03-13 11:21:42 +08:00

129 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
};
}