@glissade/cli 0.49.0-pre.0 → 0.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build.js +0 -0
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +36 -31
- package/dist/index.js +1 -1
- package/dist/localize.js +1 -1
- package/dist/loudness.js +2 -2
- package/dist/master.js +1 -1
- package/dist/parity.js +37 -17
- package/dist/render.js +99 -68
- package/dist/shards.js +1 -1
- package/package.json +10 -10
package/dist/build.js
CHANGED
|
Binary file
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { c as parseFrameRange,
|
|
2
|
+
import { c as parseFrameRange, f as render, l as parseLocalesList, p as renderLocales, t as LocaleArgsError } from "./render.js";
|
|
3
3
|
import { n as parseCaptionsMode } from "./captions.js";
|
|
4
4
|
//#region src/args.ts
|
|
5
5
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { A as peakClampBinds, C as PublishProfile, D as measureFile, E as loudnessPathFor, M as resolveProfile, O as measureLoudnessCommand, S as PUBLISH_PROFILES, T as computeMixHash, _ as LOUDNESS_SCHEMA_VERSION, a as MasterMemberResult, b as MeasureLoudnessOptions, d as masterAfChain, f as masterCommand, g as DEFAULT_PROFILE_ID, h as CommittedLimiter, i as MasterLimiter, j as readLoudness, k as parseLoudnormJson, l as MemberMeasure, m as planMaster, n as MasterConfig, o as MasterPlan, p as normalizeMasterConfig, r as MasterError, s as MasterResult, t as DEFAULT_MAX_GR_DB, u as MemberPlan, v as LoudnessError, w as computeGainDb, x as MeasureLoudnessResult, y as LoudnessMeasurement } from "./master.js";
|
|
2
2
|
import { AudioClip, CompiledTimeline, Key, Timeline } from "@glissade/core";
|
|
3
3
|
import { DisplayList, Scene, SceneModule, VideoFrameSource } from "@glissade/scene";
|
|
4
|
+
import { SkiaBackend } from "@glissade/backend-skia";
|
|
4
5
|
import { NarrationTiming } from "@glissade/narrate";
|
|
5
6
|
import { Image } from "@napi-rs/canvas";
|
|
6
7
|
import * as _glissade_core_i18n0 from "@glissade/core/i18n";
|
|
@@ -130,6 +131,37 @@ declare function probeEntryHeader(file: string): {
|
|
|
130
131
|
/** Remove the whole `.gscache` dir (a `gs cache clear` convenience / test cleanup). */
|
|
131
132
|
declare function clearFrameCache(dir: string): void;
|
|
132
133
|
//#endregion
|
|
134
|
+
//#region src/videoSource.d.ts
|
|
135
|
+
declare class VideoProbeError extends Error {
|
|
136
|
+
constructor(path: string, detail: string);
|
|
137
|
+
}
|
|
138
|
+
interface VideoInfo {
|
|
139
|
+
fps: number;
|
|
140
|
+
duration: number;
|
|
141
|
+
width: number;
|
|
142
|
+
height: number;
|
|
143
|
+
}
|
|
144
|
+
declare function probeVideo(path: string): VideoInfo;
|
|
145
|
+
/**
|
|
146
|
+
* v1 strategy: warm() extracts the full requested media range once at the
|
|
147
|
+
* source's own fps; getFrameSync indexes the extracted sequence. Disk-bound,
|
|
148
|
+
* trivially correct; the streaming-pipe variant is a later optimization.
|
|
149
|
+
*/
|
|
150
|
+
declare class FfmpegVideoFrameSource implements VideoFrameSource {
|
|
151
|
+
readonly fps: number;
|
|
152
|
+
readonly duration: number;
|
|
153
|
+
private readonly path;
|
|
154
|
+
private framesDir;
|
|
155
|
+
private warmedFrom;
|
|
156
|
+
private warmedTo;
|
|
157
|
+
private readonly cache;
|
|
158
|
+
constructor(path: string, info?: VideoInfo);
|
|
159
|
+
private frameIndex;
|
|
160
|
+
warm(fromT: number, toT: number): Promise<void>;
|
|
161
|
+
getFrameSync(mediaT: number): Image;
|
|
162
|
+
close(): void;
|
|
163
|
+
}
|
|
164
|
+
//#endregion
|
|
133
165
|
//#region src/render.d.ts
|
|
134
166
|
interface RenderOptions {
|
|
135
167
|
modulePath: string;
|
|
@@ -307,6 +339,10 @@ declare class SceneModuleError extends Error {
|
|
|
307
339
|
|
|
308
340
|
declare function loadSceneModule(modulePath: string, locale?: string, messageTableOverride?: _glissade_core_i18n0.MessageTable): Promise<SceneModule>;
|
|
309
341
|
declare function ffmpegAvailable(): boolean;
|
|
342
|
+
/** What `prepareSkiaRenderEnv` needs to make a Skia render FAITHFUL (fonts+axes,
|
|
343
|
+
* Yoga, assets) — the setup that `gs render` and any parity/preview render must
|
|
344
|
+
* share so a headless render is byte-faithful to `gs render` by construction. */
|
|
345
|
+
|
|
310
346
|
declare function render(opts: RenderOptions): Promise<{
|
|
311
347
|
frames: number;
|
|
312
348
|
out: string;
|
|
@@ -424,37 +460,6 @@ declare function renderSharded(a: RenderShardedArgs): Promise<{
|
|
|
424
460
|
}>;
|
|
425
461
|
/** The FFV1 lossless intermediate retained beside a `--incremental` output for the next splice. */
|
|
426
462
|
//#endregion
|
|
427
|
-
//#region src/videoSource.d.ts
|
|
428
|
-
declare class VideoProbeError extends Error {
|
|
429
|
-
constructor(path: string, detail: string);
|
|
430
|
-
}
|
|
431
|
-
interface VideoInfo {
|
|
432
|
-
fps: number;
|
|
433
|
-
duration: number;
|
|
434
|
-
width: number;
|
|
435
|
-
height: number;
|
|
436
|
-
}
|
|
437
|
-
declare function probeVideo(path: string): VideoInfo;
|
|
438
|
-
/**
|
|
439
|
-
* v1 strategy: warm() extracts the full requested media range once at the
|
|
440
|
-
* source's own fps; getFrameSync indexes the extracted sequence. Disk-bound,
|
|
441
|
-
* trivially correct; the streaming-pipe variant is a later optimization.
|
|
442
|
-
*/
|
|
443
|
-
declare class FfmpegVideoFrameSource implements VideoFrameSource {
|
|
444
|
-
readonly fps: number;
|
|
445
|
-
readonly duration: number;
|
|
446
|
-
private readonly path;
|
|
447
|
-
private framesDir;
|
|
448
|
-
private warmedFrom;
|
|
449
|
-
private warmedTo;
|
|
450
|
-
private readonly cache;
|
|
451
|
-
constructor(path: string, info?: VideoInfo);
|
|
452
|
-
private frameIndex;
|
|
453
|
-
warm(fromT: number, toT: number): Promise<void>;
|
|
454
|
-
getFrameSync(mediaT: number): Image;
|
|
455
|
-
close(): void;
|
|
456
|
-
}
|
|
457
|
-
//#endregion
|
|
458
463
|
//#region src/audioMix.d.ts
|
|
459
464
|
declare class AudioMixError extends Error {
|
|
460
465
|
constructor(detail: string);
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as glissadeVersion } from "./version.js";
|
|
2
|
-
import { a as ffmpegAvailable,
|
|
2
|
+
import { a as ffmpegAvailable, f as render, h as resolveLoudnessGainDb, i as collectAudioClips, l as parseLocalesList, n as SceneModuleError, o as loadSceneModule, p as renderLocales, r as buildMixWav, s as localeOutPath, t as LocaleArgsError, u as planFinalAudio } from "./render.js";
|
|
3
3
|
import { a as computeGainDb, d as parseLoudnormJson, f as peakClampBinds, i as PUBLISH_PROFILES, l as measureFile, m as resolveProfile, n as LOUDNESS_SCHEMA_VERSION, o as computeMixHash, p as readLoudness, r as LoudnessError, s as loudnessPathFor, t as DEFAULT_PROFILE_ID, u as measureLoudnessCommand } from "./loudness.js";
|
|
4
4
|
import { i as masterCommand, n as MasterError, o as normalizeMasterConfig, r as masterAfChain, s as planMaster, t as DEFAULT_MAX_GR_DB } from "./master.js";
|
|
5
5
|
import { a as pickEncoder, i as parseEncoderList, n as availableEncoders, t as NoEncoderError } from "./encoders.js";
|
package/dist/localize.js
CHANGED
|
@@ -156,7 +156,7 @@ async function harvestMessageIds(modulePath) {
|
|
|
156
156
|
get: (_t, prop) => typeof prop === "string" ? prop : void 0
|
|
157
157
|
});
|
|
158
158
|
const { getConsumedMessageIds } = await import("@glissade/core/i18n");
|
|
159
|
-
const { loadSceneModule } = await import("./render.js").then((n) => n.
|
|
159
|
+
const { loadSceneModule } = await import("./render.js").then((n) => n.m);
|
|
160
160
|
const mod = await loadSceneModule(modulePath, void 0, recording);
|
|
161
161
|
mod.createScene();
|
|
162
162
|
const tIds = new Set(getConsumedMessageIds());
|
package/dist/loudness.js
CHANGED
|
@@ -225,7 +225,7 @@ function measureFile(audioPath) {
|
|
|
225
225
|
*/
|
|
226
226
|
async function measureLoudnessCommand(opts) {
|
|
227
227
|
const profile = resolveProfile(opts.profile ?? "youtube");
|
|
228
|
-
const { buildMixWav } = await import("./render.js").then((n) => n.
|
|
228
|
+
const { buildMixWav } = await import("./render.js").then((n) => n.m);
|
|
229
229
|
const tmp = mkdtempSync(join(tmpdir(), "glissade-loudness-"));
|
|
230
230
|
try {
|
|
231
231
|
const wavPath = join(tmp, "mix.wav");
|
|
@@ -239,7 +239,7 @@ async function measureLoudnessCommand(opts) {
|
|
|
239
239
|
const { inputI, inputTp, inputLra } = measureFile(wavPath);
|
|
240
240
|
const gain = computeGainDb(profile, inputI, inputTp);
|
|
241
241
|
const clampBound = peakClampBinds(profile, inputI, inputTp);
|
|
242
|
-
const { collectMixAudioInputs } = await import("./render.js").then((n) => n.
|
|
242
|
+
const { collectMixAudioInputs } = await import("./render.js").then((n) => n.m);
|
|
243
243
|
const extraInputs = await collectMixAudioInputs({
|
|
244
244
|
modulePath: opts.modulePath,
|
|
245
245
|
narration: opts.narration ?? "auto",
|
package/dist/master.js
CHANGED
|
@@ -132,7 +132,7 @@ async function runMaster(members, opts = {}, onLog) {
|
|
|
132
132
|
const consistency = opts.consistency ?? "shared-target";
|
|
133
133
|
const limiter = opts.limiter || null;
|
|
134
134
|
const log = onLog ?? (() => {});
|
|
135
|
-
const { buildMixWav, collectMixAudioInputs } = await import("./render.js").then((n) => n.
|
|
135
|
+
const { buildMixWav, collectMixAudioInputs } = await import("./render.js").then((n) => n.m);
|
|
136
136
|
if (members.length === 0) throw new MasterError("master: no members to master");
|
|
137
137
|
const ceilingDb = limiter?.ceilingDb ?? profile.truePeakDb;
|
|
138
138
|
const committedLimiter = limiter ? {
|
package/dist/parity.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { o as loadSceneModule } from "./render.js";
|
|
1
|
+
import { d as prepareSkiaRenderEnv, o as loadSceneModule } from "./render.js";
|
|
2
2
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { basename, join } from "node:path";
|
|
4
|
-
import { evaluate } from "@glissade/scene";
|
|
4
|
+
import { evaluate, withDeterminismGuards } from "@glissade/scene";
|
|
5
5
|
import { SkiaBackend, heatmapRgba, ssimMap } from "@glissade/backend-skia";
|
|
6
6
|
import { exportLottie, importLottie } from "@glissade/lottie";
|
|
7
7
|
//#region src/parity.ts
|
|
@@ -57,32 +57,51 @@ function parseBackends(raw) {
|
|
|
57
57
|
}
|
|
58
58
|
return list;
|
|
59
59
|
}
|
|
60
|
-
/**
|
|
61
|
-
|
|
60
|
+
/**
|
|
61
|
+
* skia = the direct headless render — the reference every leg is diffed against.
|
|
62
|
+
* CRITICAL: it renders through the SAME environment as `gs render`
|
|
63
|
+
* (`prepareSkiaRenderEnv`: font faces incl. variable-font axes registered, Yoga
|
|
64
|
+
* for flexbox, assets decoded) and wraps `evaluate` in the determinism guard. Without
|
|
65
|
+
* this the reference would render a variable-font scene at DEFAULT weight (the face
|
|
66
|
+
* never registered) and match a fontAxes-dropping Lottie leg at a FALSE SSIM 1.0 —
|
|
67
|
+
* a fidelity gate silently reporting perfect on a real interchange loss.
|
|
68
|
+
*
|
|
69
|
+
* The scene + backend are built ONCE and reused across frames (evaluate is pure of
|
|
70
|
+
* time — the ONE contract — exactly as `gs render`'s frame loop reuses them).
|
|
71
|
+
*/
|
|
72
|
+
async function skiaSource(mod, w, h, modulePath) {
|
|
73
|
+
const scene = mod.createScene();
|
|
74
|
+
const backend = new SkiaBackend(w, h);
|
|
75
|
+
await prepareSkiaRenderEnv({
|
|
76
|
+
scene,
|
|
77
|
+
doc: mod.timeline,
|
|
78
|
+
backend,
|
|
79
|
+
modulePath
|
|
80
|
+
});
|
|
62
81
|
return async (t) => {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
scene.setTextMeasurer(backend);
|
|
66
|
-
backend.render(evaluate(scene, mod.timeline, t));
|
|
82
|
+
const dl = withDeterminismGuards("throw", () => evaluate(scene, mod.timeline, t));
|
|
83
|
+
backend.render(dl);
|
|
67
84
|
return backend.readPixels();
|
|
68
85
|
};
|
|
69
86
|
}
|
|
70
87
|
/**
|
|
71
88
|
* lottie = the export↔import round-trip rendered on Skia (roundtrip.test's renderPixels
|
|
72
|
-
* shape). The document is exported/imported ONCE (it's time-independent);
|
|
73
|
-
*
|
|
89
|
+
* shape). The document is exported/imported ONCE (it's time-independent); the re-imported
|
|
90
|
+
* module renders through the SAME faithful environment as the reference (so its render is
|
|
91
|
+
* honest too). Asset URLs resolve against the ORIGINAL module path. This leg measures the
|
|
92
|
+
* Lottie bijection per frame — e.g. the dropped `fontAxes` surfaces as a real SSIM drop.
|
|
74
93
|
*/
|
|
75
|
-
function lottieSource(mod, w, h, fps) {
|
|
94
|
+
async function lottieSource(mod, w, h, fps, modulePath) {
|
|
76
95
|
return skiaSource(importLottie(exportLottie(mod, {
|
|
77
96
|
width: w,
|
|
78
97
|
height: h,
|
|
79
98
|
fps
|
|
80
|
-
})).toSceneModule(), w, h);
|
|
99
|
+
})).toSceneModule(), w, h, modulePath);
|
|
81
100
|
}
|
|
82
|
-
function makeSource(backend, mod, w, h, fps) {
|
|
101
|
+
function makeSource(backend, mod, w, h, fps, modulePath) {
|
|
83
102
|
switch (backend) {
|
|
84
|
-
case "skia": return skiaSource(mod, w, h);
|
|
85
|
-
case "lottie": return lottieSource(mod, w, h, fps);
|
|
103
|
+
case "skia": return skiaSource(mod, w, h, modulePath);
|
|
104
|
+
case "lottie": return lottieSource(mod, w, h, fps, modulePath);
|
|
86
105
|
default: throw new ParityBackendError(`unknown parity backend '${backend}'`);
|
|
87
106
|
}
|
|
88
107
|
}
|
|
@@ -97,8 +116,9 @@ async function parityCommand(opts) {
|
|
|
97
116
|
const frames = opts.frames ?? DEFAULT_PARITY_FRAMES;
|
|
98
117
|
const floor = opts.min ?? .98;
|
|
99
118
|
const name = opts.name ?? basename(opts.modulePath).replace(/\.[jt]sx?$/, "");
|
|
100
|
-
const reference = skiaSource(mod, w, h);
|
|
101
|
-
const sources =
|
|
119
|
+
const reference = await skiaSource(mod, w, h, opts.modulePath);
|
|
120
|
+
const sources = /* @__PURE__ */ new Map();
|
|
121
|
+
for (const b of compared) sources.set(b, await makeSource(b, mod, w, h, fps, opts.modulePath));
|
|
102
122
|
if (opts.heatmapDir && !existsSync(opts.heatmapDir)) mkdirSync(opts.heatmapDir, { recursive: true });
|
|
103
123
|
const results = [];
|
|
104
124
|
let belowFloor = 0;
|
package/dist/render.js
CHANGED
|
@@ -166,6 +166,7 @@ var render_exports = /* @__PURE__ */ __exportAll({
|
|
|
166
166
|
parseFrameRange: () => parseFrameRange,
|
|
167
167
|
parseLocalesList: () => parseLocalesList,
|
|
168
168
|
planFinalAudio: () => planFinalAudio,
|
|
169
|
+
prepareSkiaRenderEnv: () => prepareSkiaRenderEnv,
|
|
169
170
|
render: () => render,
|
|
170
171
|
renderLocales: () => renderLocales,
|
|
171
172
|
resolveLoudnessGainDb: () => resolveLoudnessGainDb
|
|
@@ -358,6 +359,95 @@ async function loadSceneModule(modulePath, locale, messageTableOverride) {
|
|
|
358
359
|
function ffmpegAvailable() {
|
|
359
360
|
return spawnSync("ffmpeg", ["-version"], { stdio: "ignore" }).status === 0;
|
|
360
361
|
}
|
|
362
|
+
/**
|
|
363
|
+
* Prepare the Skia render environment so `evaluate(scene, doc, t)` → `backend.render`
|
|
364
|
+
* is FAITHFUL: line-break measurer, Yoga (flexbox scenes), font faces registered
|
|
365
|
+
* under their families (variable-font axes included, so `fontAxes` reaches the
|
|
366
|
+
* glyphs), font validation, and image/video asset decode bound to the backend.
|
|
367
|
+
*
|
|
368
|
+
* This is the SINGLE SOURCE OF TRUTH for render setup — `gs render` AND `gs parity`
|
|
369
|
+
* both call it, so a parity render cannot silently diverge from a real render (e.g.
|
|
370
|
+
* report a false-perfect SSIM on a variable-font scene by rendering BOTH legs at the
|
|
371
|
+
* default weight because neither registered the face). Extracted verbatim from the
|
|
372
|
+
* `render()` body; the ordering (yoga → asset-validate → font-register → validate →
|
|
373
|
+
* asset-decode) is preserved so `gs render` stays byte-identical.
|
|
374
|
+
*/
|
|
375
|
+
async function prepareSkiaRenderEnv(o) {
|
|
376
|
+
const { scene, doc, backend, modulePath } = o;
|
|
377
|
+
scene.setTextMeasurer(backend);
|
|
378
|
+
if ([...scene.nodes.values()].some((n) => n.constructor.isLayoutNode === true)) {
|
|
379
|
+
const { loadYogaLayoutEngine } = await import("@glissade/scene/layout");
|
|
380
|
+
await loadYogaLayoutEngine();
|
|
381
|
+
}
|
|
382
|
+
validateAssetReferences(collectAssetReferences(scene.root), Object.keys(doc.assets ?? {}));
|
|
383
|
+
const videoSources = [];
|
|
384
|
+
const { resolveAssetPath: resolveAsset } = await import("./audioMix.js").then((n) => n.i);
|
|
385
|
+
const { createHash } = await import("node:crypto");
|
|
386
|
+
const assetDigests = /* @__PURE__ */ new Map();
|
|
387
|
+
const digestBytes = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
388
|
+
const registeredFamilies = /* @__PURE__ */ new Set();
|
|
389
|
+
const fontRegistry = buildFontRegistry(doc.assets);
|
|
390
|
+
if (fontRegistry.faces().length > 0) {
|
|
391
|
+
const { GlobalFonts } = await import("@napi-rs/canvas");
|
|
392
|
+
let ingest;
|
|
393
|
+
for (const face of fontRegistry.faces()) {
|
|
394
|
+
registeredFamilies.add(face.family.toLowerCase());
|
|
395
|
+
const path = resolveAsset(face.url, modulePath);
|
|
396
|
+
if (/\.woff2?$/i.test(face.url)) {
|
|
397
|
+
ingest ??= await import("@glissade/core/font-ingest");
|
|
398
|
+
const src = await readFile(path);
|
|
399
|
+
assetDigests.set(`font:${face.family}:${face.url}`, digestBytes(src));
|
|
400
|
+
const result = await ingest.ingestFont({
|
|
401
|
+
family: face.family,
|
|
402
|
+
src
|
|
403
|
+
});
|
|
404
|
+
GlobalFonts.register(Buffer.from(result.bytes), face.family);
|
|
405
|
+
} else {
|
|
406
|
+
assetDigests.set(`font:${face.family}:${face.url}`, digestBytes(await readFile(path)));
|
|
407
|
+
GlobalFonts.registerFromPath(path, face.family);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
const osCatalog = !!o.allowSystemFonts && !o.strictFonts ? new Set((await import("@napi-rs/canvas")).GlobalFonts.families.map((f) => f.family.toLowerCase())) : /* @__PURE__ */ new Set();
|
|
412
|
+
const osFamilies = buildFontExemptSet(registeredFamilies, {
|
|
413
|
+
allowSystemFonts: !!o.allowSystemFonts,
|
|
414
|
+
strict: !!o.strictFonts,
|
|
415
|
+
osCatalog
|
|
416
|
+
});
|
|
417
|
+
const localizedUsages = o.locale !== void 0 && o.locale !== "" ? collectLocalizedTextUsages(scene, doc) : [];
|
|
418
|
+
await validateSceneFonts(scene, doc, async (url) => {
|
|
419
|
+
try {
|
|
420
|
+
const buf = await readFile(resolveAsset(url, modulePath));
|
|
421
|
+
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
422
|
+
} catch {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
}, {
|
|
426
|
+
mode: o.strictFonts ? "strict" : "dev",
|
|
427
|
+
osFamilies,
|
|
428
|
+
extraUsages: localizedUsages
|
|
429
|
+
});
|
|
430
|
+
for (const [assetId, ref] of Object.entries(doc.assets ?? {})) if (ref.kind === "font") {} else if (ref.kind === "image") {
|
|
431
|
+
const { loadImage } = await import("@napi-rs/canvas");
|
|
432
|
+
const imgPath = resolveAsset(ref.url, modulePath);
|
|
433
|
+
assetDigests.set(`image:${assetId}`, digestBytes(await readFile(imgPath)));
|
|
434
|
+
backend.setImageAsset(assetId, await loadImage(imgPath));
|
|
435
|
+
} else if (ref.kind === "video") {
|
|
436
|
+
if (!ffmpegAvailable()) throw new Error(`video asset '${assetId}' needs FFmpeg on PATH for frame extraction (§5.4)`);
|
|
437
|
+
const { FfmpegVideoFrameSource } = await import("./videoSource.js").then((n) => n.i);
|
|
438
|
+
const videoPath = resolveAsset(ref.url, modulePath);
|
|
439
|
+
assetDigests.set(`video:${assetId}`, digestBytes(await readFile(videoPath)));
|
|
440
|
+
const source = new FfmpegVideoFrameSource(videoPath);
|
|
441
|
+
await source.warm(0, source.duration);
|
|
442
|
+
backend.setVideoAsset(assetId, source);
|
|
443
|
+
videoSources.push(source);
|
|
444
|
+
}
|
|
445
|
+
return {
|
|
446
|
+
assetDigests,
|
|
447
|
+
videoSources,
|
|
448
|
+
registeredFamilies
|
|
449
|
+
};
|
|
450
|
+
}
|
|
361
451
|
async function render(opts) {
|
|
362
452
|
const mod = await loadSceneModule(opts.modulePath, opts.locale);
|
|
363
453
|
const scene = mod.createScene();
|
|
@@ -427,74 +517,15 @@ async function render(opts) {
|
|
|
427
517
|
const framesDir = isVideo ? mkdtempSync(join(tmpdir(), "glissade-frames-")) : singleFile ? dirname(resolve(opts.out)) : resolve(opts.out);
|
|
428
518
|
mkdirSync(framesDir, { recursive: true });
|
|
429
519
|
const backend = new SkiaBackend(scene.size.w, scene.size.h);
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
const { createHash } = await import("node:crypto");
|
|
439
|
-
const assetDigests = /* @__PURE__ */ new Map();
|
|
440
|
-
const digestBytes = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
441
|
-
const registeredFamilies = /* @__PURE__ */ new Set();
|
|
442
|
-
const fontRegistry = buildFontRegistry(doc.assets);
|
|
443
|
-
if (fontRegistry.faces().length > 0) {
|
|
444
|
-
const { GlobalFonts } = await import("@napi-rs/canvas");
|
|
445
|
-
let ingest;
|
|
446
|
-
for (const face of fontRegistry.faces()) {
|
|
447
|
-
registeredFamilies.add(face.family.toLowerCase());
|
|
448
|
-
const path = resolveAsset(face.url, opts.modulePath);
|
|
449
|
-
if (/\.woff2?$/i.test(face.url)) {
|
|
450
|
-
ingest ??= await import("@glissade/core/font-ingest");
|
|
451
|
-
const src = await readFile(path);
|
|
452
|
-
assetDigests.set(`font:${face.family}:${face.url}`, digestBytes(src));
|
|
453
|
-
const result = await ingest.ingestFont({
|
|
454
|
-
family: face.family,
|
|
455
|
-
src
|
|
456
|
-
});
|
|
457
|
-
GlobalFonts.register(Buffer.from(result.bytes), face.family);
|
|
458
|
-
} else {
|
|
459
|
-
assetDigests.set(`font:${face.family}:${face.url}`, digestBytes(await readFile(path)));
|
|
460
|
-
GlobalFonts.registerFromPath(path, face.family);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
const osCatalog = !!opts.allowSystemFonts && !opts.strictFonts ? new Set((await import("@napi-rs/canvas")).GlobalFonts.families.map((f) => f.family.toLowerCase())) : /* @__PURE__ */ new Set();
|
|
465
|
-
const osFamilies = buildFontExemptSet(registeredFamilies, {
|
|
466
|
-
allowSystemFonts: !!opts.allowSystemFonts,
|
|
467
|
-
strict: !!opts.strictFonts,
|
|
468
|
-
osCatalog
|
|
469
|
-
});
|
|
470
|
-
const localizedUsages = opts.locale !== void 0 && opts.locale !== "" ? collectLocalizedTextUsages(scene, doc) : [];
|
|
471
|
-
await validateSceneFonts(scene, doc, async (url) => {
|
|
472
|
-
try {
|
|
473
|
-
const buf = await readFile(resolveAsset(url, opts.modulePath));
|
|
474
|
-
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
475
|
-
} catch {
|
|
476
|
-
return;
|
|
477
|
-
}
|
|
478
|
-
}, {
|
|
479
|
-
mode: opts.strictFonts ? "strict" : "dev",
|
|
480
|
-
osFamilies,
|
|
481
|
-
extraUsages: localizedUsages
|
|
520
|
+
const { assetDigests, videoSources } = await prepareSkiaRenderEnv({
|
|
521
|
+
scene,
|
|
522
|
+
doc,
|
|
523
|
+
backend,
|
|
524
|
+
modulePath: opts.modulePath,
|
|
525
|
+
...opts.strictFonts !== void 0 ? { strictFonts: opts.strictFonts } : {},
|
|
526
|
+
...opts.allowSystemFonts !== void 0 ? { allowSystemFonts: opts.allowSystemFonts } : {},
|
|
527
|
+
...opts.locale !== void 0 ? { locale: opts.locale } : {}
|
|
482
528
|
});
|
|
483
|
-
for (const [assetId, ref] of Object.entries(doc.assets ?? {})) if (ref.kind === "font") {} else if (ref.kind === "image") {
|
|
484
|
-
const { loadImage } = await import("@napi-rs/canvas");
|
|
485
|
-
const imgPath = resolveAsset(ref.url, opts.modulePath);
|
|
486
|
-
assetDigests.set(`image:${assetId}`, digestBytes(await readFile(imgPath)));
|
|
487
|
-
backend.setImageAsset(assetId, await loadImage(imgPath));
|
|
488
|
-
} else if (ref.kind === "video") {
|
|
489
|
-
if (!ffmpegAvailable()) throw new Error(`video asset '${assetId}' needs FFmpeg on PATH for frame extraction (§5.4)`);
|
|
490
|
-
const { FfmpegVideoFrameSource } = await import("./videoSource.js").then((n) => n.i);
|
|
491
|
-
const videoPath = resolveAsset(ref.url, opts.modulePath);
|
|
492
|
-
assetDigests.set(`video:${assetId}`, digestBytes(await readFile(videoPath)));
|
|
493
|
-
const source = new FfmpegVideoFrameSource(videoPath);
|
|
494
|
-
await source.warm(0, source.duration);
|
|
495
|
-
backend.setVideoAsset(assetId, source);
|
|
496
|
-
videoSources.push(source);
|
|
497
|
-
}
|
|
498
529
|
let frameCache;
|
|
499
530
|
let keyCtx;
|
|
500
531
|
const cacheOn = !!(opts.cache && opts.cache.mode !== "off");
|
|
@@ -995,4 +1026,4 @@ async function buildMixWav(opts, wavOut) {
|
|
|
995
1026
|
});
|
|
996
1027
|
}
|
|
997
1028
|
//#endregion
|
|
998
|
-
export {
|
|
1029
|
+
export { frameKeyDigest as _, ffmpegAvailable as a, parseFrameRange as c, prepareSkiaRenderEnv as d, render as f, changedFrameRanges as g, resolveLoudnessGainDb as h, collectAudioClips as i, parseLocalesList as l, render_exports as m, SceneModuleError as n, loadSceneModule as o, renderLocales as p, buildMixWav as r, localeOutPath as s, LocaleArgsError as t, planFinalAudio as u, readRenderManifest as v, writeRenderManifest as y };
|
package/dist/shards.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime.js";
|
|
2
|
-
import { _ as
|
|
2
|
+
import { _ as frameKeyDigest, g as changedFrameRanges, u as planFinalAudio, v as readRenderManifest, y as writeRenderManifest } from "./render.js";
|
|
3
3
|
import { a as pickEncoder } from "./encoders.js";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import { existsSync, mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glissade/cli",
|
|
3
|
-
"version": "0.49.0
|
|
3
|
+
"version": "0.49.0",
|
|
4
4
|
"description": "glissade CLI: headless rendering via backend-skia (+ FFmpeg mux).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"engines": {
|
|
@@ -28,15 +28,15 @@
|
|
|
28
28
|
"@napi-rs/canvas": "^0.1.65",
|
|
29
29
|
"esbuild": "0.28.0",
|
|
30
30
|
"jiti": "^2.4.2",
|
|
31
|
-
"@glissade/backend-skia": "0.49.0
|
|
32
|
-
"@glissade/core": "0.49.0
|
|
33
|
-
"@glissade/interact": "0.49.0
|
|
34
|
-
"@glissade/lottie": "0.49.0
|
|
35
|
-
"@glissade/narrate": "0.49.0
|
|
36
|
-
"@glissade/player": "0.49.0
|
|
37
|
-
"@glissade/scene": "0.49.0
|
|
38
|
-
"@glissade/sfx": "0.49.0
|
|
39
|
-
"@glissade/svg": "0.49.0
|
|
31
|
+
"@glissade/backend-skia": "0.49.0",
|
|
32
|
+
"@glissade/core": "0.49.0",
|
|
33
|
+
"@glissade/interact": "0.49.0",
|
|
34
|
+
"@glissade/lottie": "0.49.0",
|
|
35
|
+
"@glissade/narrate": "0.49.0",
|
|
36
|
+
"@glissade/player": "0.49.0",
|
|
37
|
+
"@glissade/scene": "0.49.0",
|
|
38
|
+
"@glissade/sfx": "0.49.0",
|
|
39
|
+
"@glissade/svg": "0.49.0"
|
|
40
40
|
},
|
|
41
41
|
"repository": {
|
|
42
42
|
"type": "git",
|