@glissade/cli 0.13.0 → 0.14.0-pre.1
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/captions.js +12 -2
- package/dist/cli.js +7 -0
- package/dist/index.d.ts +28 -2
- package/dist/locale.js +67 -0
- package/dist/render.js +109 -8
- package/dist/shards.js +2 -1
- package/package.json +10 -10
package/dist/captions.js
CHANGED
|
@@ -21,8 +21,18 @@ function parseCaptionsMode(raw) {
|
|
|
21
21
|
if (raw === "sidecar" || raw === "off") return raw;
|
|
22
22
|
throw new Error(`--captions must be burn, sidecar, or off (got '${raw}')`);
|
|
23
23
|
}
|
|
24
|
-
/**
|
|
25
|
-
|
|
24
|
+
/**
|
|
25
|
+
* `<module>.narration.timing.json`, when the scene has been narrated. With a
|
|
26
|
+
* `locale`, PREFER the locale-tagged sibling `<module>.<locale>.narration.timing.json`
|
|
27
|
+
* (0.14 localization core) and fall back to the base sibling when it is absent —
|
|
28
|
+
* so a locale that reuses the base narration still renders. No `locale` (the
|
|
29
|
+
* base path) resolves the base sibling, byte-identical to today.
|
|
30
|
+
*/
|
|
31
|
+
function timingPathFor(modulePath, locale) {
|
|
32
|
+
if (locale !== void 0 && locale !== "") {
|
|
33
|
+
const localeCandidate = modulePath.replace(/\.[jt]sx?$/, "") + `.${locale}.narration.timing.json`;
|
|
34
|
+
if (existsSync(localeCandidate)) return localeCandidate;
|
|
35
|
+
}
|
|
26
36
|
const candidate = modulePath.replace(/\.[jt]sx?$/, "") + ".narration.timing.json";
|
|
27
37
|
return existsSync(candidate) ? candidate : null;
|
|
28
38
|
}
|
package/dist/cli.js
CHANGED
|
@@ -17,6 +17,7 @@ const KNOWN_BOOLEAN_FLAGS = new Set([
|
|
|
17
17
|
"no-warnings",
|
|
18
18
|
"lossless-intermediate",
|
|
19
19
|
"allow-gpu-shards",
|
|
20
|
+
"allow-system-fonts",
|
|
20
21
|
"verbose",
|
|
21
22
|
"allow-degraded",
|
|
22
23
|
"bisect",
|
|
@@ -110,6 +111,10 @@ render options:
|
|
|
110
111
|
(YouTube needs the 1st chapter at 0:00 — auto-anchored — and each chapter >= 10s; author cue ts accordingly)
|
|
111
112
|
--chapters-kind <k[,k]> cue kinds that become VTT chapters (default: chapter); cues.json keeps all kinds
|
|
112
113
|
--strict fail on an unregistered font family or an uncovered glyph (§3.6; default: warn)
|
|
114
|
+
--allow-system-fonts exempt true-OS-installed families from the unregistered-family check (host-dependent;
|
|
115
|
+
off by default, IGNORED under --strict so the strict verdict stays host-independent; §3.6)
|
|
116
|
+
--locale <code> resolve the scene against messages.<code>.json (node-id text + free-standing t() keys) and prefer
|
|
117
|
+
the <base>.<code>.narration.timing.json sibling (0.14). No --locale resolves the BASE files
|
|
113
118
|
|
|
114
119
|
diff options (DisplayList diff vs a committed baseline — exits non-zero on any divergence):
|
|
115
120
|
--at <t> time in SECONDS to evaluate the scene at (required)
|
|
@@ -454,10 +459,12 @@ async function main() {
|
|
|
454
459
|
...flags.has("state") ? { state: flags.get("state") } : {},
|
|
455
460
|
...flags.has("force") ? { force: true } : {},
|
|
456
461
|
...flags.has("strict") ? { strictFonts: true } : {},
|
|
462
|
+
...flags.has("allow-system-fonts") ? { allowSystemFonts: true } : {},
|
|
457
463
|
...workers !== void 0 ? { workers } : {},
|
|
458
464
|
...flags.has("lossless-intermediate") ? { losslessIntermediate: true } : {},
|
|
459
465
|
...flags.has("allow-gpu-shards") ? { allowGpuShards: true } : {},
|
|
460
466
|
...cache !== void 0 ? { cache } : {},
|
|
467
|
+
...flags.has("locale") && flags.get("locale") ? { locale: flags.get("locale") } : {},
|
|
461
468
|
captions: parseCaptionsModeOrFail(flags.get("captions")),
|
|
462
469
|
narration: flags.get("narration") === "off" ? "off" : "auto",
|
|
463
470
|
music: flags.get("music") === "off" ? "off" : "auto",
|
package/dist/index.d.ts
CHANGED
|
@@ -168,6 +168,14 @@ interface RenderOptions {
|
|
|
168
168
|
chapterKinds?: ReadonlySet<string>;
|
|
169
169
|
/** --strict: font validation throws on an unregistered family / missing glyph (§3.6). Default dev-warn. */
|
|
170
170
|
strictFonts?: boolean;
|
|
171
|
+
/**
|
|
172
|
+
* --allow-system-fonts (§3.6, 0.14 FIX 6): exempt true-OS-installed families
|
|
173
|
+
* (the host `GlobalFonts.families` catalog) from the unregistered-family check.
|
|
174
|
+
* OFF by default — the exempt set is otherwise just the families glissade
|
|
175
|
+
* registered from `doc.assets`, so the verdict is host-independent. IGNORED
|
|
176
|
+
* under --strict (strict stays host-independent regardless of this flag).
|
|
177
|
+
*/
|
|
178
|
+
allowSystemFonts?: boolean;
|
|
171
179
|
/**
|
|
172
180
|
* --workers N (§5.6): split the frame range into N contiguous sub-ranges, render
|
|
173
181
|
* each in a separate `gs` child process, and join the shard videos. Ignored for
|
|
@@ -200,8 +208,26 @@ interface RenderOptions {
|
|
|
200
208
|
mode: CacheMode;
|
|
201
209
|
maxSize?: number;
|
|
202
210
|
};
|
|
211
|
+
/**
|
|
212
|
+
* --locale <code> (0.14 localization core): resolve the scene against a
|
|
213
|
+
* per-locale message table (`messages.<code>.json`) and prefer the
|
|
214
|
+
* locale-tagged narration sibling (`<base>.<code>.narration.timing.json`).
|
|
215
|
+
* Omitted (the base path) resolves the BASE files → byte-identical to today.
|
|
216
|
+
*/
|
|
217
|
+
locale?: string;
|
|
203
218
|
onProgress?: (frame: number, total: number) => void;
|
|
204
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* Build the case-folded set of families EXEMPT from the §3.6 unregistered-family
|
|
222
|
+
* check (FIX 6, 0.14). Seeded from the families glissade actually registered out
|
|
223
|
+
* of `doc.assets` (`registered`, already lower-cased) — NEVER the true-OS
|
|
224
|
+
* `GlobalFonts.families` catalog, which is host-dependent (3 families on a clean
|
|
225
|
+
* Linux CI box, hundreds on a dev macOS) and would make the --strict PASS/FAIL
|
|
226
|
+
* verdict depend on the host. The true-OS `osCatalog` is folded in ONLY when
|
|
227
|
+
* `allowSystemFonts` is set AND `strict` is false — so --strict stays host-
|
|
228
|
+
* independent regardless of the flag. Pure: no I/O, no host queries.
|
|
229
|
+
*/
|
|
230
|
+
|
|
205
231
|
declare class SceneModuleError extends Error {
|
|
206
232
|
constructor(modulePath: string, detail: string);
|
|
207
233
|
}
|
|
@@ -211,7 +237,7 @@ declare class SceneModuleError extends Error {
|
|
|
211
237
|
* ranges are rejected, since a frame index is an integer.
|
|
212
238
|
*/
|
|
213
239
|
|
|
214
|
-
declare function loadSceneModule(modulePath: string): Promise<SceneModule>;
|
|
240
|
+
declare function loadSceneModule(modulePath: string, locale?: string): Promise<SceneModule>;
|
|
215
241
|
declare function ffmpegAvailable(): boolean;
|
|
216
242
|
declare function render(opts: RenderOptions): Promise<{
|
|
217
243
|
frames: number;
|
|
@@ -223,7 +249,7 @@ declare function render(opts: RenderOptions): Promise<{
|
|
|
223
249
|
* render/shard path) and `buildMixWav` (the measure-loudness path), so the mix
|
|
224
250
|
* CONTENT measured at commit-time is byte-for-byte the mix rendered later.
|
|
225
251
|
*/
|
|
226
|
-
declare function collectAudioClips(opts: Pick<RenderOptions, 'modulePath' | 'narration' | 'music' | 'sfx'>, timelineClips: AudioClip[]): Promise<AudioClip[]>;
|
|
252
|
+
declare function collectAudioClips(opts: Pick<RenderOptions, 'modulePath' | 'narration' | 'music' | 'sfx' | 'locale'>, timelineClips: AudioClip[]): Promise<AudioClip[]>;
|
|
227
253
|
/**
|
|
228
254
|
* Resolve the ACTUAL audio files that feed the final mix (timeline clips +
|
|
229
255
|
* auto-mixed narration/music/sfx), as absolute paths. This is what the mixHash
|
package/dist/locale.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
//#region src/locale.ts
|
|
4
|
+
/**
|
|
5
|
+
* gs render --locale <code> (0.14 localization core): resolve the per-locale
|
|
6
|
+
* message table + the locale-tagged narration timing sibling for a scene module.
|
|
7
|
+
*
|
|
8
|
+
* The render-time half of @glissade/core/i18n. The base (no --locale) path
|
|
9
|
+
* resolves the BASE files (`messages.json` is NOT consulted, the base narration
|
|
10
|
+
* sibling is used) → byte-identical to today. Every locale is opt-in.
|
|
11
|
+
*/
|
|
12
|
+
/** Strip a scene module's `.ts`/`.tsx`/`.js`/`.jsx` extension → the file stem. */
|
|
13
|
+
function moduleStem(modulePath) {
|
|
14
|
+
return modulePath.replace(/\.[jt]sx?$/, "");
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The locale-tagged NARRATION timing sibling suffix. A `--locale xx` render
|
|
18
|
+
* prefers `<base>.xx.narration.timing.json` over the base
|
|
19
|
+
* `<base>.narration.timing.json`. DEFAULT convention — the maintainer is
|
|
20
|
+
* confirming the exact suffix with the ai-training consumer; changing it is a
|
|
21
|
+
* one-line edit HERE. `%s` is the locale code.
|
|
22
|
+
*/
|
|
23
|
+
const LOCALE_NARRATION_SUFFIX = ".%s.narration.timing.json";
|
|
24
|
+
/**
|
|
25
|
+
* The per-locale message table filename, resolved RELATIVE to the scene module
|
|
26
|
+
* directory: `messages.<locale>.json`. DEFAULT convention; one-line change.
|
|
27
|
+
*/
|
|
28
|
+
function messagesFileFor(modulePath, locale) {
|
|
29
|
+
return join(dirname(modulePath), `messages.${locale}.json`);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Load the message table for a locale, or `undefined` when no `messages.<locale>.json`
|
|
33
|
+
* exists (a locale with only node-id / narration text and no free-standing t()
|
|
34
|
+
* keys is valid). The base path never calls this.
|
|
35
|
+
*/
|
|
36
|
+
function loadMessageTable(modulePath, locale) {
|
|
37
|
+
const file = messagesFileFor(modulePath, locale);
|
|
38
|
+
if (!existsSync(file)) return void 0;
|
|
39
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The locale-tagged narration timing sibling path for a module, or `undefined`
|
|
43
|
+
* when no `locale` is requested. The render path prefers this sibling when it
|
|
44
|
+
* exists, falling back to the base sibling otherwise (so a locale that reuses
|
|
45
|
+
* the base narration still renders).
|
|
46
|
+
*/
|
|
47
|
+
function localeNarrationPathFor(modulePath, locale) {
|
|
48
|
+
return moduleStem(modulePath) + LOCALE_NARRATION_SUFFIX.replace("%s", locale);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A declared `--locale <code>` resolved to NEITHER a `messages.<code>.json` nor a
|
|
52
|
+
* `<base>.<code>.narration.timing.json` sibling — the locale has no assets at
|
|
53
|
+
* all, so a render would silently fall back to the BASE artifact (wrong-language
|
|
54
|
+
* output, exit 0, no warning). Render hard-throws this instead.
|
|
55
|
+
*
|
|
56
|
+
* (A narration-only locale legitimately has no messages file, and a
|
|
57
|
+
* messages-only locale legitimately reuses the base narration — so this fires
|
|
58
|
+
* only when BOTH are absent.)
|
|
59
|
+
*/
|
|
60
|
+
var UnknownLocaleError = class extends Error {
|
|
61
|
+
constructor(locale, messagesPath, narrationPath) {
|
|
62
|
+
super(`--locale '${locale}': no locale assets found — neither a message table nor a narration sibling resolves. Looked for '${messagesPath}' and '${narrationPath}'. Add one of those files, or drop --locale to render the base language.`);
|
|
63
|
+
this.name = "UnknownLocaleError";
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
//#endregion
|
|
67
|
+
export { UnknownLocaleError, loadMessageTable, localeNarrationPathFor, messagesFileFor };
|
package/dist/render.js
CHANGED
|
@@ -1,14 +1,67 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime.js";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
7
|
import { readFile } from "node:fs/promises";
|
|
8
8
|
import { createJiti } from "jiti";
|
|
9
9
|
import { buildFontRegistry } from "@glissade/core";
|
|
10
|
-
import { evaluate, validateSceneFonts, withDeterminismGuards } from "@glissade/scene";
|
|
10
|
+
import { collectLocalizedTextUsages, evaluate, validateSceneFonts, withDeterminismGuards } from "@glissade/scene";
|
|
11
11
|
import { SkiaBackend } from "@glissade/backend-skia";
|
|
12
|
+
//#region src/assetValidation.ts
|
|
13
|
+
/**
|
|
14
|
+
* Walk a scene's node tree and collect every Image/Video asset reference. Pure
|
|
15
|
+
* (no playhead) — the asset id a node references is structural, not a function
|
|
16
|
+
* of time. `assetId` may be `undefined` at runtime (a JS caller that passed a
|
|
17
|
+
* `src` URL instead of an `assetId` — the common codegen mistake), even though
|
|
18
|
+
* scene's type says `string`; surfaced as-is.
|
|
19
|
+
*/
|
|
20
|
+
function collectAssetReferences(root) {
|
|
21
|
+
const out = [];
|
|
22
|
+
const visit = (node) => {
|
|
23
|
+
const kind = node.constructor.assetKind;
|
|
24
|
+
if (kind !== void 0) out.push({
|
|
25
|
+
assetId: node.assetId,
|
|
26
|
+
kind,
|
|
27
|
+
nodeId: node.id
|
|
28
|
+
});
|
|
29
|
+
if (node.children) for (const child of node.children) visit(child);
|
|
30
|
+
};
|
|
31
|
+
visit(root);
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* An Image/Video node references an asset id that isn't declared in
|
|
36
|
+
* `timeline.assets`. Distinct from ColdAssetError (a declared-but-not-warmed
|
|
37
|
+
* asset) — this is an AUTHORING mistake caught before evaluation, and the
|
|
38
|
+
* message names the most common cause: passing a `src` URL instead of an
|
|
39
|
+
* `assetId` + a `timeline.assets` entry.
|
|
40
|
+
*/
|
|
41
|
+
var UnknownAssetError = class extends Error {
|
|
42
|
+
assetId;
|
|
43
|
+
declared;
|
|
44
|
+
constructor(ref, declared) {
|
|
45
|
+
const id = ref.assetId;
|
|
46
|
+
const at = ref.nodeId !== void 0 ? ` on ${ref.kind} node '${ref.nodeId}'` : ` on an ${ref.kind} node`;
|
|
47
|
+
const declaredList = declared.length > 0 ? declared.join(", ") : "(none)";
|
|
48
|
+
super(`assetId ${id === void 0 ? "<undefined>" : `'${id}'`}${at} is not declared in timeline.assets (declared: ${declaredList}) — an Image/Video needs an \`assetId\` plus a \`timeline.assets\` entry { kind, url }, not a \`src\` URL; remote URLs are not fetched at render (§2.5 offline determinism)`);
|
|
49
|
+
this.name = "UnknownAssetError";
|
|
50
|
+
this.assetId = id;
|
|
51
|
+
this.declared = declared;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Pre-validate Image/Video asset references against the declared timeline asset
|
|
56
|
+
* ids. Throws an UnknownAssetError naming the real mistake when a referenced id
|
|
57
|
+
* is undefined or absent from `declaredIds`. A no-op when every reference is
|
|
58
|
+
* declared.
|
|
59
|
+
*/
|
|
60
|
+
function validateAssetReferences(refs, declaredIds) {
|
|
61
|
+
const declared = new Set(declaredIds);
|
|
62
|
+
for (const ref of refs) if (ref.assetId === void 0 || !declared.has(ref.assetId)) throw new UnknownAssetError(ref, [...declared]);
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
12
65
|
//#region src/render.ts
|
|
13
66
|
/**
|
|
14
67
|
* gs render (DESIGN.md §5.1d, §5.7): load a scene module, evaluate each frame,
|
|
@@ -17,6 +70,7 @@ import { SkiaBackend } from "@glissade/backend-skia";
|
|
|
17
70
|
*/
|
|
18
71
|
var render_exports = /* @__PURE__ */ __exportAll({
|
|
19
72
|
SceneModuleError: () => SceneModuleError,
|
|
73
|
+
buildFontExemptSet: () => buildFontExemptSet,
|
|
20
74
|
buildMixWav: () => buildMixWav,
|
|
21
75
|
collectAudioClips: () => collectAudioClips,
|
|
22
76
|
collectMixAudioInputs: () => collectMixAudioInputs,
|
|
@@ -27,6 +81,21 @@ var render_exports = /* @__PURE__ */ __exportAll({
|
|
|
27
81
|
render: () => render,
|
|
28
82
|
resolveLoudnessGainDb: () => resolveLoudnessGainDb
|
|
29
83
|
});
|
|
84
|
+
/**
|
|
85
|
+
* Build the case-folded set of families EXEMPT from the §3.6 unregistered-family
|
|
86
|
+
* check (FIX 6, 0.14). Seeded from the families glissade actually registered out
|
|
87
|
+
* of `doc.assets` (`registered`, already lower-cased) — NEVER the true-OS
|
|
88
|
+
* `GlobalFonts.families` catalog, which is host-dependent (3 families on a clean
|
|
89
|
+
* Linux CI box, hundreds on a dev macOS) and would make the --strict PASS/FAIL
|
|
90
|
+
* verdict depend on the host. The true-OS `osCatalog` is folded in ONLY when
|
|
91
|
+
* `allowSystemFonts` is set AND `strict` is false — so --strict stays host-
|
|
92
|
+
* independent regardless of the flag. Pure: no I/O, no host queries.
|
|
93
|
+
*/
|
|
94
|
+
function buildFontExemptSet(registered, opts) {
|
|
95
|
+
const exempt = new Set(registered);
|
|
96
|
+
if (opts.allowSystemFonts && !opts.strict) for (const f of opts.osCatalog) exempt.add(f);
|
|
97
|
+
return exempt;
|
|
98
|
+
}
|
|
30
99
|
var SceneModuleError = class extends Error {
|
|
31
100
|
constructor(modulePath, detail) {
|
|
32
101
|
super(`${modulePath}: ${detail}\nA scene module default-exports { createScene(): Scene, timeline: Timeline } (SceneModule).`);
|
|
@@ -46,7 +115,12 @@ function parseFrameRange(flag) {
|
|
|
46
115
|
if (b < a) throw new Error(`--range end (${b}) is before start (${a})`);
|
|
47
116
|
return [a, b];
|
|
48
117
|
}
|
|
49
|
-
async function loadSceneModule(modulePath) {
|
|
118
|
+
async function loadSceneModule(modulePath, locale) {
|
|
119
|
+
const { setMessageTable } = await import("@glissade/core/i18n");
|
|
120
|
+
if (locale !== void 0 && locale !== "") {
|
|
121
|
+
const { loadMessageTable } = await import("./locale.js");
|
|
122
|
+
setMessageTable(loadMessageTable(modulePath, locale));
|
|
123
|
+
} else setMessageTable(void 0);
|
|
50
124
|
const abs = isAbsolute(modulePath) ? modulePath : resolve(process.cwd(), modulePath);
|
|
51
125
|
const loaded = await createJiti(pathToFileURL(process.cwd() + "/").href).import(pathToFileURL(abs).href, { default: true });
|
|
52
126
|
if (typeof loaded?.createScene !== "function" || loaded?.timeline === void 0) throw new SceneModuleError(modulePath, "default export is not a SceneModule");
|
|
@@ -56,7 +130,7 @@ function ffmpegAvailable() {
|
|
|
56
130
|
return spawnSync("ffmpeg", ["-version"], { stdio: "ignore" }).status === 0;
|
|
57
131
|
}
|
|
58
132
|
async function render(opts) {
|
|
59
|
-
const mod = await loadSceneModule(opts.modulePath);
|
|
133
|
+
const mod = await loadSceneModule(opts.modulePath, opts.locale);
|
|
60
134
|
const scene = mod.createScene();
|
|
61
135
|
const { resolveRenderDoc } = await import("./machines.js").then((n) => n.n);
|
|
62
136
|
let doc = resolveRenderDoc(mod, scene, {
|
|
@@ -64,6 +138,19 @@ async function render(opts) {
|
|
|
64
138
|
...opts.state !== void 0 ? { state: opts.state } : {},
|
|
65
139
|
...opts.force !== void 0 ? { force: opts.force } : {}
|
|
66
140
|
});
|
|
141
|
+
if (opts.locale !== void 0 && opts.locale !== "") {
|
|
142
|
+
const { loadMessageTable, messagesFileFor, localeNarrationPathFor, UnknownLocaleError } = await import("./locale.js");
|
|
143
|
+
const table = loadMessageTable(opts.modulePath, opts.locale);
|
|
144
|
+
const narrationPath = localeNarrationPathFor(opts.modulePath, opts.locale);
|
|
145
|
+
if (!table && !existsSync(narrationPath)) throw new UnknownLocaleError(opts.locale, messagesFileFor(opts.modulePath, opts.locale), narrationPath);
|
|
146
|
+
if (table) {
|
|
147
|
+
const { localize, getConsumedMessageIds } = await import("@glissade/core/i18n");
|
|
148
|
+
doc = localize(doc, table, {
|
|
149
|
+
locale: opts.locale,
|
|
150
|
+
consumedIds: getConsumedMessageIds()
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
67
154
|
const fps = opts.fps ?? doc.fps ?? 60;
|
|
68
155
|
const captionsMode = opts.captions ?? "burn";
|
|
69
156
|
const { hideCaptionsDoc, timingPathFor, writeCaptionSidecars } = await import("./captions.js").then((n) => n.t);
|
|
@@ -113,16 +200,19 @@ async function render(opts) {
|
|
|
113
200
|
const { loadYogaLayoutEngine } = await import("@glissade/scene/layout");
|
|
114
201
|
await loadYogaLayoutEngine();
|
|
115
202
|
}
|
|
203
|
+
validateAssetReferences(collectAssetReferences(scene.root), Object.keys(doc.assets ?? {}));
|
|
116
204
|
const videoSources = [];
|
|
117
205
|
const { resolveAssetPath: resolveAsset } = await import("./audioMix.js").then((n) => n.i);
|
|
118
206
|
const { createHash } = await import("node:crypto");
|
|
119
207
|
const assetDigests = /* @__PURE__ */ new Map();
|
|
120
208
|
const digestBytes = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
209
|
+
const registeredFamilies = /* @__PURE__ */ new Set();
|
|
121
210
|
const fontRegistry = buildFontRegistry(doc.assets);
|
|
122
211
|
if (fontRegistry.faces().length > 0) {
|
|
123
212
|
const { GlobalFonts } = await import("@napi-rs/canvas");
|
|
124
213
|
let ingest;
|
|
125
214
|
for (const face of fontRegistry.faces()) {
|
|
215
|
+
registeredFamilies.add(face.family.toLowerCase());
|
|
126
216
|
const path = resolveAsset(face.url, opts.modulePath);
|
|
127
217
|
if (/\.woff2?$/i.test(face.url)) {
|
|
128
218
|
ingest ??= await import("@glissade/core/font-ingest");
|
|
@@ -139,6 +229,13 @@ async function render(opts) {
|
|
|
139
229
|
}
|
|
140
230
|
}
|
|
141
231
|
}
|
|
232
|
+
const osCatalog = !!opts.allowSystemFonts && !opts.strictFonts ? new Set((await import("@napi-rs/canvas")).GlobalFonts.families.map((f) => f.family.toLowerCase())) : /* @__PURE__ */ new Set();
|
|
233
|
+
const osFamilies = buildFontExemptSet(registeredFamilies, {
|
|
234
|
+
allowSystemFonts: !!opts.allowSystemFonts,
|
|
235
|
+
strict: !!opts.strictFonts,
|
|
236
|
+
osCatalog
|
|
237
|
+
});
|
|
238
|
+
const localizedUsages = opts.locale !== void 0 && opts.locale !== "" ? collectLocalizedTextUsages(scene, doc) : [];
|
|
142
239
|
await validateSceneFonts(scene, doc, async (url) => {
|
|
143
240
|
try {
|
|
144
241
|
const buf = await readFile(resolveAsset(url, opts.modulePath));
|
|
@@ -146,7 +243,11 @@ async function render(opts) {
|
|
|
146
243
|
} catch {
|
|
147
244
|
return;
|
|
148
245
|
}
|
|
149
|
-
}, {
|
|
246
|
+
}, {
|
|
247
|
+
mode: opts.strictFonts ? "strict" : "dev",
|
|
248
|
+
osFamilies,
|
|
249
|
+
extraUsages: localizedUsages
|
|
250
|
+
});
|
|
150
251
|
for (const [assetId, ref] of Object.entries(doc.assets ?? {})) if (ref.kind === "font") {} else if (ref.kind === "image") {
|
|
151
252
|
const { loadImage } = await import("@napi-rs/canvas");
|
|
152
253
|
const imgPath = resolveAsset(ref.url, opts.modulePath);
|
|
@@ -208,7 +309,7 @@ async function render(opts) {
|
|
|
208
309
|
for (const source of videoSources) source.close();
|
|
209
310
|
const emitSidecars = (target) => {
|
|
210
311
|
if (captionsMode === "off") return;
|
|
211
|
-
const timingPath = timingPathFor(opts.modulePath);
|
|
312
|
+
const timingPath = timingPathFor(opts.modulePath, opts.locale);
|
|
212
313
|
if (!timingPath) {
|
|
213
314
|
if (captionsMode === "sidecar") process.stderr.write("note: --captions sidecar: no narration timing manifest found; run gs narrate first\n");
|
|
214
315
|
return;
|
|
@@ -305,7 +406,7 @@ async function collectAudioClips(opts, timelineClips) {
|
|
|
305
406
|
const audioClips = [...timelineClips];
|
|
306
407
|
const { bedAlreadyReferenced, buildMusicClip, buildNarrationClips, musicPathFor } = await import("./music.js");
|
|
307
408
|
if ((opts.narration ?? "auto") === "auto") {
|
|
308
|
-
const narrationPath = timingPathFor(opts.modulePath);
|
|
409
|
+
const narrationPath = timingPathFor(opts.modulePath, opts.locale);
|
|
309
410
|
if (narrationPath) {
|
|
310
411
|
const voice = buildNarrationClips(narrationPath);
|
|
311
412
|
if (voice) if (voice.clips.some((c) => bedAlreadyReferenced(audioClips, c.asset.url, opts.modulePath))) process.stderr.write("note: narration already in the timeline audio — auto-mix skipped\n");
|
|
@@ -318,7 +419,7 @@ async function collectAudioClips(opts, timelineClips) {
|
|
|
318
419
|
if ((opts.music ?? "auto") === "auto") {
|
|
319
420
|
const musicPath = musicPathFor(opts.modulePath);
|
|
320
421
|
if (musicPath) {
|
|
321
|
-
const bed = buildMusicClip(musicPath, timingPathFor(opts.modulePath));
|
|
422
|
+
const bed = buildMusicClip(musicPath, timingPathFor(opts.modulePath, opts.locale));
|
|
322
423
|
if (bed) if (bedAlreadyReferenced(audioClips, bed.clip.asset.url, opts.modulePath)) process.stderr.write("note: music bed already in the timeline audio — auto-mix skipped\n");
|
|
323
424
|
else {
|
|
324
425
|
audioClips.push(bed.clip);
|
package/dist/shards.js
CHANGED
|
@@ -149,7 +149,8 @@ async function renderSharded(a) {
|
|
|
149
149
|
...opts.trace !== void 0 ? ["--trace", opts.trace] : [],
|
|
150
150
|
...opts.state !== void 0 ? ["--state", opts.state] : [],
|
|
151
151
|
...opts.force ? ["--force"] : [],
|
|
152
|
-
...opts.strictFonts ? ["--strict"] : []
|
|
152
|
+
...opts.strictFonts ? ["--strict"] : [],
|
|
153
|
+
...opts.allowSystemFonts ? ["--allow-system-fonts"] : []
|
|
153
154
|
];
|
|
154
155
|
const child = spawnSync(process.execPath, childArgs, { stdio: [
|
|
155
156
|
"ignore",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glissade/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0-pre.1",
|
|
4
4
|
"description": "glissade CLI: headless rendering via backend-skia (+ FFmpeg mux).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"engines": {
|
|
@@ -23,15 +23,15 @@
|
|
|
23
23
|
"@napi-rs/canvas": "^0.1.65",
|
|
24
24
|
"esbuild": "^0.28.0",
|
|
25
25
|
"jiti": "^2.4.2",
|
|
26
|
-
"@glissade/backend-skia": "0.
|
|
27
|
-
"@glissade/core": "0.
|
|
28
|
-
"@glissade/interact": "0.
|
|
29
|
-
"@glissade/lottie": "0.
|
|
30
|
-
"@glissade/svg": "0.
|
|
31
|
-
"@glissade/narrate": "0.
|
|
32
|
-
"@glissade/player": "0.
|
|
33
|
-
"@glissade/scene": "0.
|
|
34
|
-
"@glissade/sfx": "0.
|
|
26
|
+
"@glissade/backend-skia": "0.14.0-pre.1",
|
|
27
|
+
"@glissade/core": "0.14.0-pre.1",
|
|
28
|
+
"@glissade/interact": "0.14.0-pre.1",
|
|
29
|
+
"@glissade/lottie": "0.14.0-pre.1",
|
|
30
|
+
"@glissade/svg": "0.14.0-pre.1",
|
|
31
|
+
"@glissade/narrate": "0.14.0-pre.1",
|
|
32
|
+
"@glissade/player": "0.14.0-pre.1",
|
|
33
|
+
"@glissade/scene": "0.14.0-pre.1",
|
|
34
|
+
"@glissade/sfx": "0.14.0-pre.1"
|
|
35
35
|
},
|
|
36
36
|
"repository": {
|
|
37
37
|
"type": "git",
|