정식 3D 제작 완료 기록과 기존 합성 구성 준비
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import assert from 'node:assert/strict';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
import {PNG} from 'pngjs';
|
||||
import {plans, labels} from './scene-plan.mjs';
|
||||
|
||||
export const rendererDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
export const projectDir = path.dirname(rendererDir);
|
||||
export const repoDir = path.resolve(projectDir, '../..');
|
||||
export const fps = 50;
|
||||
export const read = (file) => JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||
export const artifact = (stage) => read(path.join(projectDir, `stages/${stage}/r1/artifact.json`));
|
||||
export const absolute = (relative) => path.join(repoDir, relative);
|
||||
export const relative = (file) => path.relative(repoDir, file).split(path.sep).join('/');
|
||||
export const write = (file, value) => {
|
||||
fs.mkdirSync(path.dirname(file), {recursive: true});
|
||||
fs.writeFileSync(file, JSON.stringify(value, null, 2) + '\n');
|
||||
};
|
||||
|
||||
export function validateInputs(project, cuts, images, scenes) {
|
||||
for (const id of ['story', 'scenes']) {
|
||||
const review = project.reviews.find((r) => r.review_id === id);
|
||||
assert.equal(review?.decision, 'approved', `${id} approval required`);
|
||||
for (const ref of review.artifact_refs) assert.equal(ref.revision, 1);
|
||||
}
|
||||
for (const id of ['07-cuts', '08-scenes', '11-images']) {
|
||||
assert.equal(project.stages.find((s) => s.stage_id === id)?.status, 'complete', `${id} incomplete`);
|
||||
}
|
||||
assert.equal(cuts.length, 31);
|
||||
assert.equal(new Set(cuts.map((c) => c.id)).size, cuts.length);
|
||||
assert.equal(images.length, 62);
|
||||
assert.equal(new Set(images.map((i) => i.frame_id)).size, images.length);
|
||||
let end = 0;
|
||||
for (const cut of cuts) {
|
||||
assert.equal(cut.start_ms, end, `${cut.id} gap/overlap`);
|
||||
const duration = cut.end_ms - cut.start_ms;
|
||||
assert(duration > 0 && duration <= 5000);
|
||||
assert.equal(duration % 20, 0, '50fps requires exact frame times');
|
||||
for (const position of ['start', 'end']) {
|
||||
const image = images.find((f) => f.frame_id === `${cut.id}-${position}`);
|
||||
assert(image?.media.verified, `${cut.id}-${position} unverified`);
|
||||
assert.equal(image.media.width * 16, image.media.height * 9);
|
||||
}
|
||||
end = cut.end_ms;
|
||||
}
|
||||
assert.equal(end, 110560);
|
||||
assert(end <= project.settings.max_duration_ms);
|
||||
for (const c of scenes.connections.filter((c) => c.mode === 'continuous')) {
|
||||
const get = (id) => images.find((i) => i.frame_id === id).media.path;
|
||||
assert.equal(get(`${c.from_cut_id}-end`), get(`${c.to_cut_id}-start`));
|
||||
}
|
||||
}
|
||||
|
||||
function bodyBox(png) {
|
||||
// Anti-aliased navy magnification guides can resemble cyan; use the largest
|
||||
// connected filled region rather than the union of every matching pixel.
|
||||
const width = 540, height = 410, mask = new Uint8Array(width * height);
|
||||
for (let y = 0; y < height; y++) for (let x = 0; x < width; x++) {
|
||||
const i = ((y + 620) * png.width + x + 220) * 4;
|
||||
const [r, g, b] = png.data.subarray(i, i + 3);
|
||||
mask[y * width + x] = r < 190 && g > 155 && b > 180 && g > r + 15 ? 1 : 0;
|
||||
}
|
||||
const queue = new Int32Array(mask.length); let largest = 0, bounds;
|
||||
for (let seed = 0; seed < mask.length; seed++) {
|
||||
if (!mask[seed]) continue;
|
||||
let head = 0, tail = 1, minX = width, minY = height, maxX = 0, maxY = 0;
|
||||
queue[0] = seed; mask[seed] = 0;
|
||||
while (head < tail) {
|
||||
const cell = queue[head++], x = cell % width, y = Math.floor(cell / width);
|
||||
minX = Math.min(minX, x); maxX = Math.max(maxX, x);
|
||||
minY = Math.min(minY, y); maxY = Math.max(maxY, y);
|
||||
for (const next of [x ? cell - 1 : -1, x + 1 < width ? cell + 1 : -1,
|
||||
y ? cell - width : -1, y + 1 < height ? cell + width : -1]) {
|
||||
if (next >= 0 && mask[next]) {mask[next] = 0; queue[tail++] = next;}
|
||||
}
|
||||
}
|
||||
if (tail > largest) {largest = tail; bounds = [minX + 215, minY + 615, maxX - minX + 11, maxY - minY + 11];}
|
||||
}
|
||||
assert(largest > 10000, 'macro droplet body not found');
|
||||
return bounds;
|
||||
}
|
||||
|
||||
export function prepareData() {
|
||||
const project = read(path.join(projectDir, 'project.json'));
|
||||
const cutStage = artifact('07-cuts').payload;
|
||||
const imageStage = artifact('11-images').payload;
|
||||
const scenes = artifact('08-scenes').payload;
|
||||
const images = imageStage.result?.frames ?? [];
|
||||
validateInputs(project, cutStage.cuts, images, scenes);
|
||||
const publicDir = path.join(rendererDir, 'public');
|
||||
fs.mkdirSync(publicDir, {recursive: true});
|
||||
const copied = new Map();
|
||||
function asset(media) {
|
||||
if (!copied.has(media.path)) {
|
||||
const name = `asset-${copied.size}.png`;
|
||||
const bytes = fs.readFileSync(absolute(media.path));
|
||||
const png = PNG.sync.read(bytes, {checkCRC: true});
|
||||
assert.equal(png.width, media.width); assert.equal(png.height, media.height);
|
||||
fs.writeFileSync(path.join(publicDir, name), bytes);
|
||||
copied.set(media.path, {name, png});
|
||||
}
|
||||
return copied.get(media.path);
|
||||
}
|
||||
const clips = cutStage.cuts.map((c) => {
|
||||
const a = asset(images.find((f) => f.frame_id === `${c.id}-start`).media);
|
||||
const b = asset(images.find((f) => f.frame_id === `${c.id}-end`).media);
|
||||
const plan = plans[c.id];
|
||||
const boxes = plan.kind === 'deform' ? [bodyBox(a.png), bodyBox(b.png)] : null;
|
||||
return {...c, frames: (c.end_ms - c.start_ms) / 20, a: a.name, b: b.name, plan,
|
||||
boxes, background: Array.from(a.png.data.subarray(0, 3))};
|
||||
});
|
||||
const titles = artifact('04-title').payload;
|
||||
const title = titles.candidates.find((t) => t.id === titles.selected_id).text;
|
||||
return {clips, captions: cutStage.captions, labels, title, fps,
|
||||
duration_ms: cutStage.narration_duration_ms ?? clips.at(-1).end_ms};
|
||||
}
|
||||
Reference in New Issue
Block a user