94 lines
2.6 KiB
TypeScript
94 lines
2.6 KiB
TypeScript
import "server-only";
|
|
|
|
import type {
|
|
MappedStoryboardShot,
|
|
StoryboardTile,
|
|
NormalizedStoryboardScript,
|
|
} from "@/lib/video-agent/types";
|
|
|
|
function clampIndex(value: number, length: number) {
|
|
if (length <= 0) return 0;
|
|
const rounded = Math.max(1, Math.round(value));
|
|
return Math.min(length, rounded) - 1;
|
|
}
|
|
|
|
function buildShotPrompt(params: {
|
|
style: string;
|
|
action: string;
|
|
camera: string;
|
|
mood: string;
|
|
language: "zh" | "en" | "mixed";
|
|
}) {
|
|
const { style, action, camera, mood, language } = params;
|
|
if (language === "en") {
|
|
return [
|
|
"Animate this still image into a coherent short scene.",
|
|
`Action: ${action}.`,
|
|
`Camera: ${camera}.`,
|
|
`Mood: ${mood}.`,
|
|
`Style: ${style}.`,
|
|
"Keep identity and key structure stable.",
|
|
].join(" ");
|
|
}
|
|
|
|
return [
|
|
"请基于当前输入图片生成连贯的视频镜头。",
|
|
`动作:${action}。`,
|
|
`镜头:${camera}。`,
|
|
`氛围:${mood}。`,
|
|
`风格:${style}。`,
|
|
"保持主体身份、构图和关键细节稳定。",
|
|
].join("");
|
|
}
|
|
|
|
export function runStoryboardMapperSkill(params: {
|
|
script: NormalizedStoryboardScript;
|
|
tiles: StoryboardTile[];
|
|
}): MappedStoryboardShot[] {
|
|
const { script, tiles } = params;
|
|
if (!tiles.length) return [];
|
|
|
|
const usedTileIndexes = new Set<number>();
|
|
const nextAvailableTile = (startIndex = 1) => {
|
|
for (let index = Math.max(1, startIndex); index <= tiles.length; index += 1) {
|
|
if (!usedTileIndexes.has(index)) return index;
|
|
}
|
|
for (let index = 1; index <= tiles.length; index += 1) {
|
|
if (!usedTileIndexes.has(index)) return index;
|
|
}
|
|
return Math.max(1, Math.min(tiles.length, startIndex));
|
|
};
|
|
|
|
return script.shots.map((shot, index) => {
|
|
const preferredTileIndex =
|
|
shot.preferredTileIndex && shot.preferredTileIndex >= 1 && shot.preferredTileIndex <= tiles.length
|
|
? shot.preferredTileIndex
|
|
: shot.index || index + 1;
|
|
const pickedTileIndex = usedTileIndexes.has(preferredTileIndex)
|
|
? nextAvailableTile(index + 1)
|
|
: preferredTileIndex;
|
|
usedTileIndexes.add(pickedTileIndex);
|
|
const tile =
|
|
tiles[clampIndex(pickedTileIndex, tiles.length)] || tiles[index] || tiles[0];
|
|
const prompt = buildShotPrompt({
|
|
style: script.globalStyle,
|
|
action: shot.action,
|
|
camera: shot.camera,
|
|
mood: shot.mood,
|
|
language: script.language,
|
|
});
|
|
|
|
return {
|
|
shotIndex: shot.index,
|
|
tileIndex: tile.index,
|
|
tileImageUrl: tile.imageUrl,
|
|
durationSec: shot.durationSec,
|
|
prompt,
|
|
negativePrompt: shot.negativePrompt,
|
|
action: shot.action,
|
|
camera: shot.camera,
|
|
mood: shot.mood,
|
|
};
|
|
});
|
|
}
|