ai-chatbot/lib/video-canvas/director-scene.ts

184 lines
5.6 KiB
TypeScript

import { readObjectTracks, type DirectorObjectTrack } from "./director-object-tracks";
import { directorBodies } from "./director-extensions";
import { directorPoses, type DirectorPoseName } from "./director-poses";
import {
defaultDirectorMotion,
readDirectorMotion,
validDirectorPose,
type DirectorMotion,
} from "./director-motion";
export type DirectorVector = [number, number, number];
export type DirectorObject = {
id: string;
name: string;
kind: "actor" | "box" | "sphere" | "model";
url?: string;
color?: string;
visible?: boolean;
locked?: boolean;
pose?: DirectorPoseName;
body?: keyof typeof directorBodies;
position: DirectorVector;
rotation: DirectorVector;
scale: DirectorVector;
};
export type DirectorCamera = {
followObjectId?: string;
id: string;
name: string;
position: DirectorVector;
target: DirectorVector;
fov: number;
};
export type DirectorSnapshot = {
id: string;
title: string;
url: string;
width: number;
height: number;
cameraId: string;
};
export type DirectorScene = {
objects: DirectorObject[];
cameras: DirectorCamera[];
activeCameraId: string;
ratio: string;
snapshots: DirectorSnapshot[];
motion: DirectorMotion;
motionTracks?: Record<string, DirectorMotion>;
objectTracks?: Record<string, DirectorObjectTrack>;
backgroundUrl?: string;
backgroundHistory?: string[];
};
export const directorRatios = ["16:9", "9:16", "1:1", "4:3", "3:4", "21:9"];
export function directorImageSize(ratio: string) {
const [w, h] = (directorRatios.includes(ratio) ? ratio : "16:9").split(":").map(Number);
return w >= h
? { width: 1280, height: Math.round((1280 * h) / w) }
: { width: Math.round((1280 * w) / h), height: 1280 };
}
export function defaultDirectorScene(): DirectorScene {
return {
objects: [
{
id: "actor-a",
name: "基础人偶",
kind: "actor",
position: [0, 0, 0],
rotation: [0, 0, 0],
scale: [1, 1, 1],
},
],
cameras: [
{
id: "camera-1",
name: "机位1",
position: [4, 3, 7],
target: [0, 1, 0],
fov: 50,
},
],
activeCameraId: "camera-1",
ratio: "16:9",
snapshots: [],
motion: defaultDirectorMotion(),
motionTracks: {},
objectTracks: {},
};
}
export function readDirectorScene(value: unknown): DirectorScene {
const fallback = defaultDirectorScene();
if (!value || typeof value !== "object") return fallback;
const v = value as DirectorScene;
const vector = (a: unknown): a is DirectorVector =>
Array.isArray(a) &&
a.length === 3 &&
a.every((n) => typeof n === "number" && Number.isFinite(n));
const cameras = Array.isArray(v.cameras)
? v.cameras.filter(
(c) =>
c &&
typeof c.id === "string" &&
typeof c.name === "string" &&
vector(c.position) &&
vector(c.target) &&
Number.isFinite(c.fov) &&
c.fov >= 15 &&
c.fov <= 90 &&
validDirectorPose(c),
)
: [];
const objects = Array.isArray(v.objects)
? v.objects.filter(
(o) =>
o &&
typeof o.id === "string" &&
typeof o.name === "string" &&
["actor", "box", "sphere", "model"].includes(o.kind) &&
vector(o.position) &&
vector(o.rotation) &&
vector(o.scale) &&
o.scale.every((n) => n > 0) &&
(o.kind !== "model" || /^https?:\/\//i.test(o.url || "")),
)
: fallback.objects;
const snapshots = Array.isArray(v.snapshots)
? v.snapshots.filter(
(s) =>
s &&
typeof s.id === "string" &&
typeof s.title === "string" &&
/^https?:\/\//i.test(s.url) &&
s.width > 0 &&
s.height > 0,
)
: [];
const validCameras = cameras.length ? cameras : fallback.cameras;
return {
objects: objects.map((o) => {
const clean = { ...o };
if (
o.color !== undefined &&
(typeof o.color !== "string" || !/^#[0-9a-f]{6}$/i.test(o.color))
)
delete clean.color;
if (o.visible !== undefined) clean.visible = o.visible !== false;
if (o.locked !== undefined) clean.locked = o.locked === true;
if (o.pose !== undefined && !Object.hasOwn(directorPoses, o.pose)) delete clean.pose;
if (o.body !== undefined && !Object.hasOwn(directorBodies, o.body)) delete clean.body;
return clean;
}),
cameras: validCameras.map((c) => {
const clean = { ...c };
if (!objects.some((o) => o.id === c.followObjectId)) delete clean.followObjectId;
return clean;
}),
activeCameraId: validCameras.some((c) => c.id === v.activeCameraId)
? v.activeCameraId
: validCameras[0].id,
ratio: directorRatios.includes(v.ratio) ? v.ratio : "16:9",
snapshots,
motion: readDirectorMotion(v.motion),
...(typeof v.backgroundUrl === "string" && /^https?:\/\//i.test(v.backgroundUrl)
? { backgroundUrl: v.backgroundUrl }
: {}),
...(Array.isArray(v.backgroundHistory)
? {
backgroundHistory: [
...new Set(
v.backgroundHistory.filter(
(url) => typeof url === "string" && /^https?:\/\//i.test(url),
),
),
].slice(-20),
}
: {}),
objectTracks: readObjectTracks(v.objectTracks, objects, readDirectorMotion(v.motion).duration),
motionTracks: Object.fromEntries(
Object.entries(v.motionTracks || {})
.filter(([id]) => validCameras.some((c) => c.id === id))
.map(([id, motion]) => [id, readDirectorMotion(motion)]),
),
};
}