@glissade/cli 0.14.0-pre.1 → 0.15.0-pre.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/cacheVerify.js +1 -1
- package/dist/cli.js +30 -6
- package/dist/diff.js +1 -1
- package/dist/fonts.js +1 -1
- package/dist/index.d.ts +51 -1
- package/dist/index.js +2 -2
- package/dist/loudness.js +2 -2
- package/dist/narrationLintCommand.js +1 -1
- package/dist/prepare.js +1 -1
- package/dist/render.js +145 -56
- package/dist/shards.js +1 -1
- package/dist/verifyDeterminism.js +1 -1
- package/package.json +10 -10
package/dist/cacheVerify.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime.js";
|
|
2
|
-
import {
|
|
2
|
+
import { o as loadSceneModule } from "./render.js";
|
|
3
3
|
import { i as capsId, n as FrameCache, o as frameCacheKey } from "./frameCache.js";
|
|
4
4
|
import { t as glissadeVersion } from "./version.js";
|
|
5
5
|
import { mkdtempSync, rmSync } from "node:fs";
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { c as render,
|
|
2
|
+
import { c as parseFrameRange, d as render, f as renderLocales, l as parseLocalesList, t as LocaleArgsError } from "./render.js";
|
|
3
3
|
import { n as parseCaptionsMode } from "./captions.js";
|
|
4
4
|
//#region src/args.ts
|
|
5
5
|
/**
|
|
@@ -115,6 +115,11 @@ render options:
|
|
|
115
115
|
off by default, IGNORED under --strict so the strict verdict stays host-independent; §3.6)
|
|
116
116
|
--locale <code> resolve the scene against messages.<code>.json (node-id text + free-standing t() keys) and prefer
|
|
117
117
|
the <base>.<code>.narration.timing.json sibling (0.14). No --locale resolves the BASE files
|
|
118
|
+
--locales <a,b> (0.15) fan out: render the scene ONCE PER comma-separated locale, each over the --locale <code>
|
|
119
|
+
path, to a DISTINCT per-locale output. A video/png --out gets a locale segment before the
|
|
120
|
+
extension (out/ep.mp4 → out/ep.<locale>.mp4); a directory --out gets a per-locale subdir
|
|
121
|
+
(out/ → out/<locale>/). Mutually exclusive with --locale. A locale with NO resolvable assets
|
|
122
|
+
aborts the whole fan-out with the same UnknownLocaleError --locale throws (never silently skipped)
|
|
118
123
|
|
|
119
124
|
diff options (DisplayList diff vs a committed baseline — exits non-zero on any divergence):
|
|
120
125
|
--at <t> time in SECONDS to evaluate the scene at (required)
|
|
@@ -443,10 +448,19 @@ async function main() {
|
|
|
443
448
|
};
|
|
444
449
|
}
|
|
445
450
|
if (flags.has("watch")) process.stderr.write("note: --watch is not yet implemented in this release; rendering once\n");
|
|
451
|
+
let locales;
|
|
452
|
+
if (flags.has("locales") && flags.get("locales")) {
|
|
453
|
+
if (flags.has("locale") && flags.get("locale")) fail("--locale and --locales are mutually exclusive — pass one (--locale renders a single locale; --locales fans out over many)");
|
|
454
|
+
try {
|
|
455
|
+
locales = parseLocalesList(flags.get("locales"));
|
|
456
|
+
} catch (err) {
|
|
457
|
+
fail(err instanceof LocaleArgsError ? err.message : err instanceof Error ? err.message : String(err));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
446
460
|
const fpsFlag = flags.get("fps");
|
|
447
461
|
const started = performance.now();
|
|
448
462
|
try {
|
|
449
|
-
const
|
|
463
|
+
const sharedOpts = {
|
|
450
464
|
modulePath,
|
|
451
465
|
out: flags.get("out") ?? "out",
|
|
452
466
|
...fpsFlag ? { fps: parseInt(fpsFlag, 10) } : {},
|
|
@@ -464,7 +478,6 @@ async function main() {
|
|
|
464
478
|
...flags.has("lossless-intermediate") ? { losslessIntermediate: true } : {},
|
|
465
479
|
...flags.has("allow-gpu-shards") ? { allowGpuShards: true } : {},
|
|
466
480
|
...cache !== void 0 ? { cache } : {},
|
|
467
|
-
...flags.has("locale") && flags.get("locale") ? { locale: flags.get("locale") } : {},
|
|
468
481
|
captions: parseCaptionsModeOrFail(flags.get("captions")),
|
|
469
482
|
narration: flags.get("narration") === "off" ? "off" : "auto",
|
|
470
483
|
music: flags.get("music") === "off" ? "off" : "auto",
|
|
@@ -475,10 +488,21 @@ async function main() {
|
|
|
475
488
|
if (n % 30 === 0 || n === total) process.stderr.write(`\rrendering ${n}/${total} frames`);
|
|
476
489
|
} else if (n % 300 === 0 || n === total) process.stderr.write(`rendering ${n}/${total} frames\n`);
|
|
477
490
|
}
|
|
478
|
-
}
|
|
479
|
-
const secs = ((performance.now() - started) / 1e3).toFixed(2);
|
|
491
|
+
};
|
|
480
492
|
const cr = process.stderr.isTTY ? "\r" : "";
|
|
481
|
-
|
|
493
|
+
if (locales) {
|
|
494
|
+
const fan = await renderLocales(sharedOpts, locales);
|
|
495
|
+
const secs = ((performance.now() - started) / 1e3).toFixed(2);
|
|
496
|
+
for (const { locale, result } of fan) process.stderr.write(`${cr}rendered [${locale}] ${result.frames} frames → ${result.out}\n`);
|
|
497
|
+
process.stderr.write(`done: ${fan.length} locale${fan.length === 1 ? "" : "s"} in ${secs}s\n`);
|
|
498
|
+
} else {
|
|
499
|
+
const result = await render({
|
|
500
|
+
...sharedOpts,
|
|
501
|
+
...flags.has("locale") && flags.get("locale") ? { locale: flags.get("locale") } : {}
|
|
502
|
+
});
|
|
503
|
+
const secs = ((performance.now() - started) / 1e3).toFixed(2);
|
|
504
|
+
process.stderr.write(`${cr}rendered ${result.frames} frames in ${secs}s → ${result.out}\n`);
|
|
505
|
+
}
|
|
482
506
|
} catch (err) {
|
|
483
507
|
fail(err instanceof Error ? err.message : String(err));
|
|
484
508
|
}
|
package/dist/diff.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime.js";
|
|
2
|
-
import {
|
|
2
|
+
import { o as loadSceneModule } from "./render.js";
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { diffDisplayLists, evaluate, formatDisplayDiff, parseDisplaySnapshot, serializeDisplayList } from "@glissade/scene";
|
|
5
5
|
//#region src/diff.ts
|
package/dist/fonts.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -226,8 +226,58 @@ interface RenderOptions {
|
|
|
226
226
|
* verdict depend on the host. The true-OS `osCatalog` is folded in ONLY when
|
|
227
227
|
* `allowSystemFonts` is set AND `strict` is false — so --strict stays host-
|
|
228
228
|
* independent regardless of the flag. Pure: no I/O, no host queries.
|
|
229
|
+
*
|
|
230
|
+
* 0.15 FIX 5 (osFamilies brand-warn gap): when an OS family name COLLIDES with a
|
|
231
|
+
* family glissade actually registered from `doc.assets`, the OS-catalog fold must
|
|
232
|
+
* NOT re-add it as an "OS-only" exemption — a registered family is a declared
|
|
233
|
+
* brand font that must stay subject to glyph-coverage validation, not be waved
|
|
234
|
+
* through as a system family. We therefore skip any osCatalog entry that collides
|
|
235
|
+
* with a registered family (the `registered` seed already carries it, and core's
|
|
236
|
+
* `validateFonts` runs coverage for registered families regardless of exemption).
|
|
237
|
+
* The exemption is for GENUINELY-OS-only families, never registered ones that
|
|
238
|
+
* happen to share a name.
|
|
229
239
|
*/
|
|
230
240
|
|
|
241
|
+
declare class LocaleArgsError extends Error {
|
|
242
|
+
constructor(detail: string);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Parse the `--locales <a,b,c>` comma-separated list into a de-duplicated,
|
|
246
|
+
* order-preserving array of locale codes. Empty / whitespace-only entries are
|
|
247
|
+
* dropped; an all-empty list is rejected (a `--locales` with nothing to render
|
|
248
|
+
* is a user error, not a silent no-op).
|
|
249
|
+
*/
|
|
250
|
+
declare function parseLocalesList(raw: string): string[];
|
|
251
|
+
/**
|
|
252
|
+
* Derive the per-locale output path from the base `out` and a locale code.
|
|
253
|
+
*
|
|
254
|
+
* Convention: a video/PNG FILE path (`out/episode.mp4`, `out/still.png`) gets a
|
|
255
|
+
* locale segment inserted before the extension → `out/episode.<locale>.mp4`. Any
|
|
256
|
+
* other `out` (a PNG-sequence DIRECTORY, the default `out`) gets a per-locale
|
|
257
|
+
* SUBDIR → `out/<locale>` (so `out/en/frame-00000.png`, `out/zh/frame-00000.png`).
|
|
258
|
+
* `--format png-seq` forces the directory convention even for a video-looking
|
|
259
|
+
* name, matching how `render()` itself treats `out` under that flag.
|
|
260
|
+
*/
|
|
261
|
+
declare function localeOutPath(out: string, locale: string, format?: 'png-seq'): string;
|
|
262
|
+
/**
|
|
263
|
+
* §0.15 `--locales` fan-out: render the scene once per locale, writing one
|
|
264
|
+
* artifact per locale to a distinct per-locale path (`localeOutPath`). Each
|
|
265
|
+
* per-locale render IS the 0.14 single-`--locale` render — so `--locales en,zh`
|
|
266
|
+
* ≡ `--locale en` then `--locale zh` with distinct outputs.
|
|
267
|
+
*
|
|
268
|
+
* Fails LOUDLY: a locale in the list with no resolvable assets throws the 0.14
|
|
269
|
+
* `UnknownLocaleError` (naming the bad locale) from inside `render()`, aborting
|
|
270
|
+
* the whole fan-out — locales already rendered keep their artifacts, but the
|
|
271
|
+
* process exits non-zero so a missing-asset locale is never silently skipped.
|
|
272
|
+
*/
|
|
273
|
+
declare function renderLocales(opts: Omit<RenderOptions, 'locale'>, locales: readonly string[]): Promise<{
|
|
274
|
+
locale: string;
|
|
275
|
+
result: {
|
|
276
|
+
frames: number;
|
|
277
|
+
out: string;
|
|
278
|
+
};
|
|
279
|
+
}[]>;
|
|
280
|
+
/** A scene module's default export is not a valid `SceneModule`. */
|
|
231
281
|
declare class SceneModuleError extends Error {
|
|
232
282
|
constructor(modulePath: string, detail: string);
|
|
233
283
|
}
|
|
@@ -833,4 +883,4 @@ declare function cacheVerifyCommand(opts: CacheVerifyOptions): Promise<CacheVeri
|
|
|
833
883
|
/** The @glissade/cli package version (the glissade VERSION for the cache key). */
|
|
834
884
|
declare function glissadeVersion(): string;
|
|
835
885
|
//#endregion
|
|
836
|
-
export { AudioMixError, type AudioMixPlan, type CacheKeyContext, type CacheMode, type CacheVerifyOptions, type CacheVerifyResult, type CaptionProbe, DEFAULT_CACHE_MAX_SIZE, DEFAULT_PROFILE_ID, type DevOptions, type DevServer, type Diagnostic, type DiffOptions, type DiffResult, type EncoderChoice, FfmpegVideoFrameSource, FrameCache, FrameCacheError, type FrameCacheOptions, type ImportCommandResult, type ImportOptions, LOUDNESS_SCHEMA_VERSION, type LintOptions, type LintRule, LoudnessError, type LoudnessMeasurement, MachineExportError, type MachineRenderFlags, type MeasureLoudnessOptions, type MeasureLoudnessResult, type NarrationLintOptions, type NarrationLintResult, NoEncoderError, PUBLISH_PROFILES, type PublishProfile, type RenderOptions, type RenderShardedArgs, SceneModuleError, ShardError, type ShardRange, type VideoInfo, VideoProbeError, applyMixGainDb, atempoChain, availableEncoders, buildCaptionProbe, buildMixWav, cacheVerifyCommand, capsId, clearFrameCache, collectAudioClips, computeGainDb, computeMixHash, dev, diffCommand, evaluateAt, ffmpegAvailable, fixDiff, formatTable, frameCacheKey, gainExpression, glissadeVersion, hasErrors, importCommand, lintNarration, lintTimingPathFor, loadSceneModule, loudnessPathFor, measureFile, measureLoudnessCommand, narrationLintCommand, parseCacheMaxSize, parseEncoderList, parseLoudnormJson, peakClampBinds, pickEncoder, planAudioMix, planFinalAudio, probeEntryHeader, probeVideo, readLoudness, render, renderSharded, resolveAssetPath, resolveLoudnessGainDb, resolveProfile, resolveRenderDoc, sceneHasGpuNodes, snapshotAt, splitFrameRange };
|
|
886
|
+
export { AudioMixError, type AudioMixPlan, type CacheKeyContext, type CacheMode, type CacheVerifyOptions, type CacheVerifyResult, type CaptionProbe, DEFAULT_CACHE_MAX_SIZE, DEFAULT_PROFILE_ID, type DevOptions, type DevServer, type Diagnostic, type DiffOptions, type DiffResult, type EncoderChoice, FfmpegVideoFrameSource, FrameCache, FrameCacheError, type FrameCacheOptions, type ImportCommandResult, type ImportOptions, LOUDNESS_SCHEMA_VERSION, type LintOptions, type LintRule, LocaleArgsError, LoudnessError, type LoudnessMeasurement, MachineExportError, type MachineRenderFlags, type MeasureLoudnessOptions, type MeasureLoudnessResult, type NarrationLintOptions, type NarrationLintResult, NoEncoderError, PUBLISH_PROFILES, type PublishProfile, type RenderOptions, type RenderShardedArgs, SceneModuleError, ShardError, type ShardRange, type VideoInfo, VideoProbeError, applyMixGainDb, atempoChain, availableEncoders, buildCaptionProbe, buildMixWav, cacheVerifyCommand, capsId, clearFrameCache, collectAudioClips, computeGainDb, computeMixHash, dev, diffCommand, evaluateAt, ffmpegAvailable, fixDiff, formatTable, frameCacheKey, gainExpression, glissadeVersion, hasErrors, importCommand, lintNarration, lintTimingPathFor, loadSceneModule, localeOutPath, loudnessPathFor, measureFile, measureLoudnessCommand, narrationLintCommand, parseCacheMaxSize, parseEncoderList, parseLocalesList, parseLoudnormJson, peakClampBinds, pickEncoder, planAudioMix, planFinalAudio, probeEntryHeader, probeVideo, readLoudness, render, renderLocales, renderSharded, resolveAssetPath, resolveLoudnessGainDb, resolveProfile, resolveRenderDoc, sceneHasGpuNodes, snapshotAt, splitFrameRange };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as ffmpegAvailable, d as render, f as renderLocales, i as collectAudioClips, l as parseLocalesList, m as resolveLoudnessGainDb, n as SceneModuleError, o as loadSceneModule, r as buildMixWav, s as localeOutPath, t as LocaleArgsError, u as planFinalAudio } from "./render.js";
|
|
2
2
|
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";
|
|
3
3
|
import { a as pickEncoder, i as parseEncoderList, n as availableEncoders, t as NoEncoderError } from "./encoders.js";
|
|
4
4
|
import { a as splitFrameRange, n as renderSharded, r as sceneHasGpuNodes, t as ShardError } from "./shards.js";
|
|
@@ -12,4 +12,4 @@ import { a as fixDiff, c as lintNarration, n as lintTimingPathFor, o as formatTa
|
|
|
12
12
|
import { a as clearFrameCache, c as parseCacheMaxSize, i as capsId, l as probeEntryHeader, n as FrameCache, o as frameCacheKey, r as FrameCacheError, t as DEFAULT_CACHE_MAX_SIZE } from "./frameCache.js";
|
|
13
13
|
import { t as glissadeVersion } from "./version.js";
|
|
14
14
|
import { t as cacheVerifyCommand } from "./cacheVerify.js";
|
|
15
|
-
export { AudioMixError, DEFAULT_CACHE_MAX_SIZE, DEFAULT_PROFILE_ID, FfmpegVideoFrameSource, FrameCache, FrameCacheError, LOUDNESS_SCHEMA_VERSION, LoudnessError, MachineExportError, NoEncoderError, PUBLISH_PROFILES, SceneModuleError, ShardError, VideoProbeError, applyMixGainDb, atempoChain, availableEncoders, buildCaptionProbe, buildMixWav, cacheVerifyCommand, capsId, clearFrameCache, collectAudioClips, computeGainDb, computeMixHash, dev, diffCommand, evaluateAt, ffmpegAvailable, fixDiff, formatTable, frameCacheKey, gainExpression, glissadeVersion, hasErrors, importCommand, lintNarration, lintTimingPathFor, loadSceneModule, loudnessPathFor, measureFile, measureLoudnessCommand, narrationLintCommand, parseCacheMaxSize, parseEncoderList, parseLoudnormJson, peakClampBinds, pickEncoder, planAudioMix, planFinalAudio, probeEntryHeader, probeVideo, readLoudness, render, renderSharded, resolveAssetPath, resolveLoudnessGainDb, resolveProfile, resolveRenderDoc, sceneHasGpuNodes, snapshotAt, splitFrameRange };
|
|
15
|
+
export { AudioMixError, DEFAULT_CACHE_MAX_SIZE, DEFAULT_PROFILE_ID, FfmpegVideoFrameSource, FrameCache, FrameCacheError, LOUDNESS_SCHEMA_VERSION, LocaleArgsError, LoudnessError, MachineExportError, NoEncoderError, PUBLISH_PROFILES, SceneModuleError, ShardError, VideoProbeError, applyMixGainDb, atempoChain, availableEncoders, buildCaptionProbe, buildMixWav, cacheVerifyCommand, capsId, clearFrameCache, collectAudioClips, computeGainDb, computeMixHash, dev, diffCommand, evaluateAt, ffmpegAvailable, fixDiff, formatTable, frameCacheKey, gainExpression, glissadeVersion, hasErrors, importCommand, lintNarration, lintTimingPathFor, loadSceneModule, localeOutPath, loudnessPathFor, measureFile, measureLoudnessCommand, narrationLintCommand, parseCacheMaxSize, parseEncoderList, parseLocalesList, parseLoudnormJson, peakClampBinds, pickEncoder, planAudioMix, planFinalAudio, probeEntryHeader, probeVideo, readLoudness, render, renderLocales, renderSharded, resolveAssetPath, resolveLoudnessGainDb, resolveProfile, resolveRenderDoc, sceneHasGpuNodes, snapshotAt, splitFrameRange };
|
package/dist/loudness.js
CHANGED
|
@@ -214,7 +214,7 @@ function measureFile(audioPath) {
|
|
|
214
214
|
*/
|
|
215
215
|
async function measureLoudnessCommand(opts) {
|
|
216
216
|
const profile = resolveProfile(opts.profile ?? "youtube");
|
|
217
|
-
const { buildMixWav } = await import("./render.js").then((n) => n.
|
|
217
|
+
const { buildMixWav } = await import("./render.js").then((n) => n.p);
|
|
218
218
|
const tmp = mkdtempSync(join(tmpdir(), "glissade-loudness-"));
|
|
219
219
|
try {
|
|
220
220
|
const wavPath = join(tmp, "mix.wav");
|
|
@@ -227,7 +227,7 @@ async function measureLoudnessCommand(opts) {
|
|
|
227
227
|
const { inputI, inputTp, inputLra } = measureFile(wavPath);
|
|
228
228
|
const gain = computeGainDb(profile, inputI, inputTp);
|
|
229
229
|
const clampBound = peakClampBinds(profile, inputI, inputTp);
|
|
230
|
-
const { collectMixAudioInputs } = await import("./render.js").then((n) => n.
|
|
230
|
+
const { collectMixAudioInputs } = await import("./render.js").then((n) => n.p);
|
|
231
231
|
const extraInputs = await collectMixAudioInputs({
|
|
232
232
|
modulePath: opts.modulePath,
|
|
233
233
|
narration: opts.narration ?? "auto",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime.js";
|
|
2
|
-
import {
|
|
2
|
+
import { o as loadSceneModule } from "./render.js";
|
|
3
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
4
|
import { splitCaption } from "@glissade/narrate";
|
|
5
5
|
//#region src/narrationLint.ts
|
package/dist/prepare.js
CHANGED
package/dist/render.js
CHANGED
|
@@ -69,6 +69,7 @@ function validateAssetReferences(refs, declaredIds) {
|
|
|
69
69
|
* when requested and available. No browser anywhere.
|
|
70
70
|
*/
|
|
71
71
|
var render_exports = /* @__PURE__ */ __exportAll({
|
|
72
|
+
LocaleArgsError: () => LocaleArgsError,
|
|
72
73
|
SceneModuleError: () => SceneModuleError,
|
|
73
74
|
buildFontExemptSet: () => buildFontExemptSet,
|
|
74
75
|
buildMixWav: () => buildMixWav,
|
|
@@ -76,9 +77,12 @@ var render_exports = /* @__PURE__ */ __exportAll({
|
|
|
76
77
|
collectMixAudioInputs: () => collectMixAudioInputs,
|
|
77
78
|
ffmpegAvailable: () => ffmpegAvailable,
|
|
78
79
|
loadSceneModule: () => loadSceneModule,
|
|
80
|
+
localeOutPath: () => localeOutPath,
|
|
79
81
|
parseFrameRange: () => parseFrameRange,
|
|
82
|
+
parseLocalesList: () => parseLocalesList,
|
|
80
83
|
planFinalAudio: () => planFinalAudio,
|
|
81
84
|
render: () => render,
|
|
85
|
+
renderLocales: () => renderLocales,
|
|
82
86
|
resolveLoudnessGainDb: () => resolveLoudnessGainDb
|
|
83
87
|
});
|
|
84
88
|
/**
|
|
@@ -90,12 +94,91 @@ var render_exports = /* @__PURE__ */ __exportAll({
|
|
|
90
94
|
* verdict depend on the host. The true-OS `osCatalog` is folded in ONLY when
|
|
91
95
|
* `allowSystemFonts` is set AND `strict` is false — so --strict stays host-
|
|
92
96
|
* independent regardless of the flag. Pure: no I/O, no host queries.
|
|
97
|
+
*
|
|
98
|
+
* 0.15 FIX 5 (osFamilies brand-warn gap): when an OS family name COLLIDES with a
|
|
99
|
+
* family glissade actually registered from `doc.assets`, the OS-catalog fold must
|
|
100
|
+
* NOT re-add it as an "OS-only" exemption — a registered family is a declared
|
|
101
|
+
* brand font that must stay subject to glyph-coverage validation, not be waved
|
|
102
|
+
* through as a system family. We therefore skip any osCatalog entry that collides
|
|
103
|
+
* with a registered family (the `registered` seed already carries it, and core's
|
|
104
|
+
* `validateFonts` runs coverage for registered families regardless of exemption).
|
|
105
|
+
* The exemption is for GENUINELY-OS-only families, never registered ones that
|
|
106
|
+
* happen to share a name.
|
|
93
107
|
*/
|
|
94
108
|
function buildFontExemptSet(registered, opts) {
|
|
95
109
|
const exempt = new Set(registered);
|
|
96
|
-
if (opts.allowSystemFonts && !opts.strict)
|
|
110
|
+
if (opts.allowSystemFonts && !opts.strict) {
|
|
111
|
+
for (const f of opts.osCatalog) if (!registered.has(f)) exempt.add(f);
|
|
112
|
+
}
|
|
97
113
|
return exempt;
|
|
98
114
|
}
|
|
115
|
+
var LocaleArgsError = class extends Error {
|
|
116
|
+
constructor(detail) {
|
|
117
|
+
super(detail);
|
|
118
|
+
this.name = "LocaleArgsError";
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Parse the `--locales <a,b,c>` comma-separated list into a de-duplicated,
|
|
123
|
+
* order-preserving array of locale codes. Empty / whitespace-only entries are
|
|
124
|
+
* dropped; an all-empty list is rejected (a `--locales` with nothing to render
|
|
125
|
+
* is a user error, not a silent no-op).
|
|
126
|
+
*/
|
|
127
|
+
function parseLocalesList(raw) {
|
|
128
|
+
const seen = /* @__PURE__ */ new Set();
|
|
129
|
+
const out = [];
|
|
130
|
+
for (const part of raw.split(",")) {
|
|
131
|
+
const code = part.trim();
|
|
132
|
+
if (code === "" || seen.has(code)) continue;
|
|
133
|
+
seen.add(code);
|
|
134
|
+
out.push(code);
|
|
135
|
+
}
|
|
136
|
+
if (out.length === 0) throw new LocaleArgsError(`--locales is empty — pass a comma-separated list, e.g. --locales en,zh`);
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Derive the per-locale output path from the base `out` and a locale code.
|
|
141
|
+
*
|
|
142
|
+
* Convention: a video/PNG FILE path (`out/episode.mp4`, `out/still.png`) gets a
|
|
143
|
+
* locale segment inserted before the extension → `out/episode.<locale>.mp4`. Any
|
|
144
|
+
* other `out` (a PNG-sequence DIRECTORY, the default `out`) gets a per-locale
|
|
145
|
+
* SUBDIR → `out/<locale>` (so `out/en/frame-00000.png`, `out/zh/frame-00000.png`).
|
|
146
|
+
* `--format png-seq` forces the directory convention even for a video-looking
|
|
147
|
+
* name, matching how `render()` itself treats `out` under that flag.
|
|
148
|
+
*/
|
|
149
|
+
function localeOutPath(out, locale, format) {
|
|
150
|
+
const isVideoName = format !== "png-seq" && /\.(mp4|webm)$/i.test(out);
|
|
151
|
+
const isPngFile = format !== "png-seq" && /\.png$/i.test(out);
|
|
152
|
+
if (isVideoName || isPngFile) return out.replace(/(\.[^.]+)$/, `.${locale}$1`);
|
|
153
|
+
return join(out, locale);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* §0.15 `--locales` fan-out: render the scene once per locale, writing one
|
|
157
|
+
* artifact per locale to a distinct per-locale path (`localeOutPath`). Each
|
|
158
|
+
* per-locale render IS the 0.14 single-`--locale` render — so `--locales en,zh`
|
|
159
|
+
* ≡ `--locale en` then `--locale zh` with distinct outputs.
|
|
160
|
+
*
|
|
161
|
+
* Fails LOUDLY: a locale in the list with no resolvable assets throws the 0.14
|
|
162
|
+
* `UnknownLocaleError` (naming the bad locale) from inside `render()`, aborting
|
|
163
|
+
* the whole fan-out — locales already rendered keep their artifacts, but the
|
|
164
|
+
* process exits non-zero so a missing-asset locale is never silently skipped.
|
|
165
|
+
*/
|
|
166
|
+
async function renderLocales(opts, locales) {
|
|
167
|
+
const results = [];
|
|
168
|
+
for (const locale of locales) {
|
|
169
|
+
const result = await render({
|
|
170
|
+
...opts,
|
|
171
|
+
out: localeOutPath(opts.out, locale, opts.format),
|
|
172
|
+
locale
|
|
173
|
+
});
|
|
174
|
+
results.push({
|
|
175
|
+
locale,
|
|
176
|
+
result
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return results;
|
|
180
|
+
}
|
|
181
|
+
/** A scene module's default export is not a valid `SceneModule`. */
|
|
99
182
|
var SceneModuleError = class extends Error {
|
|
100
183
|
constructor(modulePath, detail) {
|
|
101
184
|
super(`${modulePath}: ${detail}\nA scene module default-exports { createScene(): Scene, timeline: Timeline } (SceneModule).`);
|
|
@@ -455,23 +538,26 @@ async function collectAudioClips(opts, timelineClips) {
|
|
|
455
538
|
* clips — the narration/music/sfx siblings still resolve from `modulePath`.
|
|
456
539
|
*/
|
|
457
540
|
async function collectMixAudioInputs(opts, timelineClips) {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
paths
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
541
|
+
const { preservingMessageTable } = await import("@glissade/core/i18n");
|
|
542
|
+
return preservingMessageTable(async () => {
|
|
543
|
+
let tl = timelineClips;
|
|
544
|
+
if (tl === void 0) try {
|
|
545
|
+
const mod = await loadSceneModule(opts.modulePath);
|
|
546
|
+
const { compileTimeline } = await import("@glissade/core");
|
|
547
|
+
tl = [...compileTimeline(mod.timeline).audio];
|
|
548
|
+
} catch {
|
|
549
|
+
tl = [];
|
|
550
|
+
}
|
|
551
|
+
const clips = await collectAudioClips(opts, tl);
|
|
552
|
+
const { resolveAssetPath } = await import("./audioMix.js").then((n) => n.i);
|
|
553
|
+
const paths = /* @__PURE__ */ new Set();
|
|
554
|
+
for (const c of clips) try {
|
|
555
|
+
paths.add(resolveAssetPath(c.asset.url, opts.modulePath));
|
|
556
|
+
} catch {
|
|
557
|
+
paths.add(c.asset.url);
|
|
558
|
+
}
|
|
559
|
+
return [...paths].sort();
|
|
560
|
+
});
|
|
475
561
|
}
|
|
476
562
|
/**
|
|
477
563
|
* Resolve the committed publish gain (dB) for a render: read `<scene>.loudness.json`
|
|
@@ -548,43 +634,46 @@ async function planFinalAudio(opts, timelineClips, duration, container) {
|
|
|
548
634
|
*/
|
|
549
635
|
async function buildMixWav(opts, wavOut) {
|
|
550
636
|
if (!ffmpegAvailable()) throw new Error("gs measure-loudness needs FFmpeg on PATH and none was found.");
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
"
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
637
|
+
const { preservingMessageTable } = await import("@glissade/core/i18n");
|
|
638
|
+
return preservingMessageTable(async () => {
|
|
639
|
+
const mod = await loadSceneModule(opts.modulePath);
|
|
640
|
+
const scene = mod.createScene();
|
|
641
|
+
const { compileTimeline } = await import("@glissade/core");
|
|
642
|
+
const compiled = compileTimeline(mod.timeline);
|
|
643
|
+
const duration = compiled.duration;
|
|
644
|
+
const audioClips = await collectAudioClips(opts, [...compiled.audio]);
|
|
645
|
+
const { planAudioMix } = await import("./audioMix.js").then((n) => n.i);
|
|
646
|
+
const mix = planAudioMix(audioClips, opts.modulePath, duration);
|
|
647
|
+
if (!mix) return false;
|
|
648
|
+
const result = spawnSync("ffmpeg", [
|
|
649
|
+
"-y",
|
|
650
|
+
"-hide_banner",
|
|
651
|
+
"-nostats",
|
|
652
|
+
"-f",
|
|
653
|
+
"lavfi",
|
|
654
|
+
"-i",
|
|
655
|
+
"anullsrc=channel_layout=stereo:sample_rate=48000",
|
|
656
|
+
...mix.inputs.flatMap((p) => ["-i", p]),
|
|
657
|
+
"-filter_complex",
|
|
658
|
+
mix.filterComplex,
|
|
659
|
+
"-map",
|
|
660
|
+
"[aout]",
|
|
661
|
+
"-c:a",
|
|
662
|
+
"pcm_s16le",
|
|
663
|
+
"-ar",
|
|
664
|
+
"48000",
|
|
665
|
+
"-t",
|
|
666
|
+
String(duration),
|
|
667
|
+
wavOut
|
|
668
|
+
], { stdio: [
|
|
669
|
+
"ignore",
|
|
670
|
+
"ignore",
|
|
671
|
+
"pipe"
|
|
672
|
+
] });
|
|
673
|
+
if (result.status !== 0) throw new Error(`ffmpeg mix-to-WAV failed (exit ${result.status}):\n${result.stderr?.toString().slice(-2e3)}`);
|
|
674
|
+
scene.size;
|
|
675
|
+
return true;
|
|
676
|
+
});
|
|
588
677
|
}
|
|
589
678
|
//#endregion
|
|
590
|
-
export {
|
|
679
|
+
export { ffmpegAvailable as a, parseFrameRange as c, render as d, renderLocales as f, collectAudioClips as i, parseLocalesList as l, resolveLoudnessGainDb as m, SceneModuleError as n, loadSceneModule as o, render_exports as p, buildMixWav as r, localeOutPath as s, LocaleArgsError as t, planFinalAudio as u };
|
package/dist/shards.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime.js";
|
|
2
|
-
import {
|
|
2
|
+
import { u as planFinalAudio } 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, rmSync, writeFileSync } from "node:fs";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glissade/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0-pre.0",
|
|
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.15.0-pre.0",
|
|
27
|
+
"@glissade/core": "0.15.0-pre.0",
|
|
28
|
+
"@glissade/interact": "0.15.0-pre.0",
|
|
29
|
+
"@glissade/lottie": "0.15.0-pre.0",
|
|
30
|
+
"@glissade/svg": "0.15.0-pre.0",
|
|
31
|
+
"@glissade/narrate": "0.15.0-pre.0",
|
|
32
|
+
"@glissade/player": "0.15.0-pre.0",
|
|
33
|
+
"@glissade/scene": "0.15.0-pre.0",
|
|
34
|
+
"@glissade/sfx": "0.15.0-pre.0"
|
|
35
35
|
},
|
|
36
36
|
"repository": {
|
|
37
37
|
"type": "git",
|