@glissade/cli 0.14.0-pre.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/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,8 @@ 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)
113
116
  --locale <code> resolve the scene against messages.<code>.json (node-id text + free-standing t() keys) and prefer
114
117
  the <base>.<code>.narration.timing.json sibling (0.14). No --locale resolves the BASE files
115
118
 
@@ -456,6 +459,7 @@ async function main() {
456
459
  ...flags.has("state") ? { state: flags.get("state") } : {},
457
460
  ...flags.has("force") ? { force: true } : {},
458
461
  ...flags.has("strict") ? { strictFonts: true } : {},
462
+ ...flags.has("allow-system-fonts") ? { allowSystemFonts: true } : {},
459
463
  ...workers !== void 0 ? { workers } : {},
460
464
  ...flags.has("lossless-intermediate") ? { losslessIntermediate: true } : {},
461
465
  ...flags.has("allow-gpu-shards") ? { allowGpuShards: true } : {},
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
@@ -209,6 +217,17 @@ interface RenderOptions {
209
217
  locale?: string;
210
218
  onProgress?: (frame: number, total: number) => void;
211
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
+
212
231
  declare class SceneModuleError extends Error {
213
232
  constructor(modulePath: string, detail: string);
214
233
  }
package/dist/locale.js CHANGED
@@ -9,6 +9,18 @@ import { dirname, join } from "node:path";
9
9
  * resolves the BASE files (`messages.json` is NOT consulted, the base narration
10
10
  * sibling is used) → byte-identical to today. Every locale is opt-in.
11
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";
12
24
  /**
13
25
  * The per-locale message table filename, resolved RELATIVE to the scene module
14
26
  * directory: `messages.<locale>.json`. DEFAULT convention; one-line change.
@@ -26,5 +38,30 @@ function loadMessageTable(modulePath, locale) {
26
38
  if (!existsSync(file)) return void 0;
27
39
  return JSON.parse(readFileSync(file, "utf8"));
28
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
+ };
29
66
  //#endregion
30
- export { loadMessageTable };
67
+ export { UnknownLocaleError, loadMessageTable, localeNarrationPathFor, messagesFileFor };
package/dist/render.js CHANGED
@@ -1,13 +1,13 @@
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
12
  //#region src/assetValidation.ts
13
13
  /**
@@ -70,6 +70,7 @@ function validateAssetReferences(refs, declaredIds) {
70
70
  */
71
71
  var render_exports = /* @__PURE__ */ __exportAll({
72
72
  SceneModuleError: () => SceneModuleError,
73
+ buildFontExemptSet: () => buildFontExemptSet,
73
74
  buildMixWav: () => buildMixWav,
74
75
  collectAudioClips: () => collectAudioClips,
75
76
  collectMixAudioInputs: () => collectMixAudioInputs,
@@ -80,6 +81,21 @@ var render_exports = /* @__PURE__ */ __exportAll({
80
81
  render: () => render,
81
82
  resolveLoudnessGainDb: () => resolveLoudnessGainDb
82
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
+ }
83
99
  var SceneModuleError = class extends Error {
84
100
  constructor(modulePath, detail) {
85
101
  super(`${modulePath}: ${detail}\nA scene module default-exports { createScene(): Scene, timeline: Timeline } (SceneModule).`);
@@ -123,11 +139,16 @@ async function render(opts) {
123
139
  ...opts.force !== void 0 ? { force: opts.force } : {}
124
140
  });
125
141
  if (opts.locale !== void 0 && opts.locale !== "") {
126
- const { loadMessageTable } = await import("./locale.js");
142
+ const { loadMessageTable, messagesFileFor, localeNarrationPathFor, UnknownLocaleError } = await import("./locale.js");
127
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);
128
146
  if (table) {
129
- const { localize } = await import("@glissade/core/i18n");
130
- doc = localize(doc, table, { locale: opts.locale });
147
+ const { localize, getConsumedMessageIds } = await import("@glissade/core/i18n");
148
+ doc = localize(doc, table, {
149
+ locale: opts.locale,
150
+ consumedIds: getConsumedMessageIds()
151
+ });
131
152
  }
132
153
  }
133
154
  const fps = opts.fps ?? doc.fps ?? 60;
@@ -185,11 +206,13 @@ async function render(opts) {
185
206
  const { createHash } = await import("node:crypto");
186
207
  const assetDigests = /* @__PURE__ */ new Map();
187
208
  const digestBytes = (bytes) => createHash("sha256").update(bytes).digest("hex");
209
+ const registeredFamilies = /* @__PURE__ */ new Set();
188
210
  const fontRegistry = buildFontRegistry(doc.assets);
189
211
  if (fontRegistry.faces().length > 0) {
190
212
  const { GlobalFonts } = await import("@napi-rs/canvas");
191
213
  let ingest;
192
214
  for (const face of fontRegistry.faces()) {
215
+ registeredFamilies.add(face.family.toLowerCase());
193
216
  const path = resolveAsset(face.url, opts.modulePath);
194
217
  if (/\.woff2?$/i.test(face.url)) {
195
218
  ingest ??= await import("@glissade/core/font-ingest");
@@ -206,8 +229,13 @@ async function render(opts) {
206
229
  }
207
230
  }
208
231
  }
209
- const { GlobalFonts: GlobalFontsForValidation } = await import("@napi-rs/canvas");
210
- const osFamilies = new Set(GlobalFontsForValidation.families.map((f) => f.family.toLowerCase()));
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) : [];
211
239
  await validateSceneFonts(scene, doc, async (url) => {
212
240
  try {
213
241
  const buf = await readFile(resolveAsset(url, opts.modulePath));
@@ -217,7 +245,8 @@ async function render(opts) {
217
245
  }
218
246
  }, {
219
247
  mode: opts.strictFonts ? "strict" : "dev",
220
- osFamilies
248
+ osFamilies,
249
+ extraUsages: localizedUsages
221
250
  });
222
251
  for (const [assetId, ref] of Object.entries(doc.assets ?? {})) if (ref.kind === "font") {} else if (ref.kind === "image") {
223
252
  const { loadImage } = await import("@napi-rs/canvas");
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.14.0-pre.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.14.0-pre.0",
27
- "@glissade/core": "0.14.0-pre.0",
28
- "@glissade/interact": "0.14.0-pre.0",
29
- "@glissade/lottie": "0.14.0-pre.0",
30
- "@glissade/svg": "0.14.0-pre.0",
31
- "@glissade/narrate": "0.14.0-pre.0",
32
- "@glissade/player": "0.14.0-pre.0",
33
- "@glissade/scene": "0.14.0-pre.0",
34
- "@glissade/sfx": "0.14.0-pre.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",