Oloka-Video-Dev / apps /server /src /composition /composition-materializer.unit.test.ts
phucsd's picture
Deploy GitHub commit 6006fd642da2fab96feb00a92e19a545cdd9b311
5bbf908 verified
Raw History Blame Contribute Delete
7.64 kB
import type { CompositionDocumentV1 } from "@oloka/contracts";
import { describe, expect, it } from "vitest";
import {
materializeCompositionPreview,
readMaterializationStage,
} from "./composition-materializer.js";
describe("composition preview materializer", () => {
it("exposes only the fixed materialization stage attached to a failure", () => {
const error = new Error("sensitive materializer detail");
Object.defineProperty(error, "materializationStage", {
value: "load-font",
});
expect(readMaterializationStage(error)).toBe("load-font");
expect(
readMaterializationStage({ materializationStage: "private/path" }),
).toBeUndefined();
});
it("produces identical self-contained bytes for the full structured fixture", async () => {
const assets = [
{
id: AUDIO_ID,
mime: "audio/wav",
bytes: Buffer.from("deterministic-audio"),
},
{
id: IMAGE_ID,
mime: "image/png",
bytes: Buffer.from("deterministic-image"),
},
];
const originalOrder = assets.map((asset) => asset.id);
const first = await materializeCompositionPreview(fixture(), assets);
const second = await materializeCompositionPreview(
fixture(),
[...assets].reverse(),
);
const html = Buffer.from(first.bytes).toString("utf8");
expect(first.checksumSha256).toBe(second.checksumSha256);
expect(
Buffer.compare(Buffer.from(first.bytes), Buffer.from(second.bytes)),
).toBe(0);
expect(first.bytes.byteLength).toBeGreaterThan(2_000_000);
expect(assets.map((asset) => asset.id)).toEqual(originalOrder);
expect(html).toContain("data:font/ttf;base64,");
expect(html).toContain("data:image/png;base64,");
expect(html).toContain("data:audio/wav;base64,");
expect(html).toContain('data-kind="shape"');
expect(html).toContain('data-kind="text"');
expect(html).toContain("ducking");
expect(html).not.toContain("nonce=");
expect(html).not.toMatch(/channel=["'][a-f0-9]{32}/);
});
it("materializes all four exact frame presets deterministically", async () => {
const presets = [
["16:9", 1920, 1080],
["9:16", 1080, 1920],
["1:1", 1080, 1080],
["4:5", 1080, 1350],
] as const;
for (const [aspectRatio, width, height] of presets) {
const first = await materializeCompositionPreview(
fixture(aspectRatio),
[],
);
const second = await materializeCompositionPreview(
fixture(aspectRatio),
[],
);
const html = Buffer.from(first.bytes).toString("utf8");
expect(first.checksumSha256).toBe(second.checksumSha256);
expect(html).toContain(`data-width="${width}" data-height="${height}"`);
}
});
});
const IMAGE_ID = "30000000-0000-4000-8000-000000000001";
const AUDIO_ID = "30000000-0000-4000-8000-000000000002";
function fixture(
aspectRatio: CompositionDocumentV1["aspectRatio"] = "16:9",
): CompositionDocumentV1 {
const dimensions = (
{
"16:9": [1920, 1080],
"9:16": [1080, 1920],
"1:1": [1080, 1080],
"4:5": [1080, 1350],
} satisfies Record<
CompositionDocumentV1["aspectRatio"],
readonly [number, number]
>
)[aspectRatio];
return {
schemaVersion: 1,
compositionId: "10000000-0000-4000-8000-000000000001",
name: "Bản dựng tiếng Việt",
aspectRatio,
width: dimensions[0],
height: dimensions[1],
fps: 30,
durationMs: 4_000,
background: "#102030",
scenes: [
{
id: "20000000-0000-4000-8000-000000000001",
order: 0,
startMs: 0,
durationMs: 4_000,
text: "Cộng hòa Xã hội Chủ nghĩa Việt Nam\nDấu kết hợp: Việt Nam!",
narration: {
itemKey: "scene-1",
text: "Xin chào Việt Nam",
silence: false,
locale: "vi-VN",
rate: 1,
gainDb: 0,
},
assetReferences: [
{ assetId: IMAGE_ID, usage: "visual" },
{ assetId: AUDIO_ID, usage: "audio" },
],
style: {
layout: "center",
foreground: "#ffffff",
accent: "#ffd23f",
paddingPercent: 8,
gapPercent: 4,
},
tracks: [
{
id: "40000000-0000-4000-8000-000000000001",
type: "overlay",
order: 0,
clips: [
{
id: "50000000-0000-4000-8000-000000000001",
kind: "shape",
startMs: 0,
durationMs: 4_000,
layerOrder: 0,
transitionIn: {
type: "wipe",
durationMs: 400,
easing: "ease-out",
},
transitionOut: {
type: "fade",
durationMs: 300,
easing: "ease-in",
},
animations: [
{
property: "scale",
easing: "ease-in-out",
keyframes: [
{ offset: 0, value: 0.9 },
{ offset: 1, value: 1 },
],
},
],
primitive: "rectangle",
fill: "#102030cc",
stroke: "#ffd23f",
strokeWidth: 4,
radius: 32,
bounds: {
x: 0.1,
y: 0.2,
width: 0.8,
height: 0.6,
anchor: "center",
},
},
{
id: "50000000-0000-4000-8000-000000000002",
kind: "text",
startMs: 0,
durationMs: 4_000,
layerOrder: 1,
animations: [],
text: "Nội dung nhiều dòng, có dấu tiếng Việt.",
typography: {
fontToken: "oloka-sans-v1",
size: 64,
weight: 700,
lineHeight: 1.2,
align: "center",
color: "#ffffff",
maxLines: 3,
overflow: "wrap",
},
bounds: {
x: 0.15,
y: 0.3,
width: 0.7,
height: 0.4,
anchor: "center",
},
},
],
},
],
},
],
voiceConfig: {
voiceToken: "neutral-vi-v1",
locale: "vi-VN",
rate: 1,
gainDb: 0,
},
captionConfig: {
enabled: true,
preset: "Bold",
fontToken: "oloka-sans-v1",
safeMarginPercent: 8,
maxLines: 3,
},
bgmConfig: {
assetId: AUDIO_ID,
volume: 0.5,
ducking: {
enabled: true,
targetVolume: 0.2,
attackMs: 150,
releaseMs: 300,
},
},
manifests: {
dependency: { schemaVersion: 1, hashSha256: "a".repeat(64) },
asset: { schemaVersion: 1, hashSha256: "b".repeat(64) },
font: { schemaVersion: 1, hashSha256: "c".repeat(64) },
caption: { schemaVersion: 1, hashSha256: "d".repeat(64) },
},
runtimeVersions: {
materializer: "oloka-composition-v1.0.0",
renderer: "oloka-browser-v1",
renderProtocol: 1,
hyperframes: "0.7.104",
hyperframesRuntimeSha256: "e".repeat(64),
templateRegistry: "oloka-registry-v1",
},
metadata: { locale: "vi-VN", title: "Bản dựng", safeAreaMode: "action" },
};
}