@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 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
- /** `<module>.narration.timing.json`, when the scene has been narrated. */
25
- function timingPathFor(modulePath) {
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
- }, { mode: opts.strictFonts ? "strict" : "dev" });
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.13.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.13.0",
27
- "@glissade/core": "0.13.0",
28
- "@glissade/interact": "0.13.0",
29
- "@glissade/lottie": "0.13.0",
30
- "@glissade/svg": "0.13.0",
31
- "@glissade/narrate": "0.13.0",
32
- "@glissade/player": "0.13.0",
33
- "@glissade/scene": "0.13.0",
34
- "@glissade/sfx": "0.13.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",