@glissade/cli 0.14.0 → 0.15.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/cacheVerify.js +1 -1
- package/dist/cli.js +37 -8
- package/dist/diff.js +1 -1
- package/dist/fonts.js +1 -1
- package/dist/index.d.ts +84 -6
- package/dist/index.js +2 -2
- package/dist/loudness.js +21 -10
- package/dist/narrationLintCommand.js +1 -1
- package/dist/prepare.js +1 -1
- package/dist/render.js +179 -59
- 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
|
/**
|
|
@@ -73,7 +73,7 @@ const USAGE = `usage:
|
|
|
73
73
|
gs narration-lint <scene-module|script.narration.timing.json> [--json] [--fix] [--max-cps <n>]
|
|
74
74
|
gs sfx <scene-module|script.sfx.json> [--verbose]
|
|
75
75
|
gs prepare <scene-module> [--provider <id>] [--align <id>] [--force]
|
|
76
|
-
gs measure-loudness <scene-module> [--profile <youtube|shorts|podcast|broadcast|ebu>]
|
|
76
|
+
gs measure-loudness <scene-module> [--profile <youtube|shorts|podcast|broadcast|ebu>] [--locale <code>]
|
|
77
77
|
gs fonts audit <scene-module> list registered families, formats, and missing-glyph runs (§3.6)
|
|
78
78
|
gs cache verify <scene-module> [--range a..b] [--sample <n>] assert cache hits == cold renders (§3.5)
|
|
79
79
|
|
|
@@ -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)
|
|
@@ -142,6 +147,9 @@ measure-loudness options (the explicit publish-loudness measure step; commits *.
|
|
|
142
147
|
--profile <id> youtube (default) | shorts (both -14 LUFS) | podcast (-16) | broadcast/ebu (-23); all cap at -1 dBTP
|
|
143
148
|
measures the final mix (ebur128) and commits a deterministic peak-clamped gain; render applies it
|
|
144
149
|
as a pure scalar. Needs ffmpeg. Brickwall limiter deferred — peaky un-normalized profiles warn.
|
|
150
|
+
--locale <code> (0.15) measure the per-locale mix (the localized narration sibling) and commit
|
|
151
|
+
<stem>.<locale>.loudness.json. A localized 'gs render --locale <code>' REQUIRES this per-locale
|
|
152
|
+
file (the base measurement can't gate the per-locale mix). No --locale measures the base mix.
|
|
145
153
|
|
|
146
154
|
narrate options (the explicit TTS prepare step; render itself stays offline):
|
|
147
155
|
--provider <id> fake | espeak | piper | kokoro | openai (default: the script's provider, else espeak)
|
|
@@ -289,12 +297,14 @@ async function main() {
|
|
|
289
297
|
if (command === "measure-loudness") {
|
|
290
298
|
const { measureLoudnessCommand } = await import("./loudness.js").then((n) => n.c);
|
|
291
299
|
try {
|
|
300
|
+
const mlLocale = flags.get("locale");
|
|
292
301
|
const result = await measureLoudnessCommand({
|
|
293
302
|
modulePath,
|
|
294
303
|
...flags.has("profile") ? { profile: flags.get("profile") } : {},
|
|
295
304
|
...flags.get("narration") === "off" ? { narration: "off" } : {},
|
|
296
305
|
...flags.get("music") === "off" ? { music: "off" } : {},
|
|
297
|
-
...flags.get("sfx") === "off" ? { sfx: "off" } : {}
|
|
306
|
+
...flags.get("sfx") === "off" ? { sfx: "off" } : {},
|
|
307
|
+
...mlLocale !== void 0 && mlLocale !== "" ? { locale: mlLocale } : {}
|
|
298
308
|
});
|
|
299
309
|
const m = result.measurement;
|
|
300
310
|
process.stderr.write(`gs measure-loudness: profile '${m.profileId}' — in ${m.inputI} LUFS / ${m.inputTp} dBTP → gain ${m.gain >= 0 ? "+" : ""}${m.gain} dB (out ~${(m.inputI + m.gain).toFixed(2)} LUFS / ~${(m.inputTp + m.gain).toFixed(2)} dBTP) → ${result.loudnessPath}\n`);
|
|
@@ -443,10 +453,19 @@ async function main() {
|
|
|
443
453
|
};
|
|
444
454
|
}
|
|
445
455
|
if (flags.has("watch")) process.stderr.write("note: --watch is not yet implemented in this release; rendering once\n");
|
|
456
|
+
let locales;
|
|
457
|
+
if (flags.has("locales") && flags.get("locales")) {
|
|
458
|
+
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)");
|
|
459
|
+
try {
|
|
460
|
+
locales = parseLocalesList(flags.get("locales"));
|
|
461
|
+
} catch (err) {
|
|
462
|
+
fail(err instanceof LocaleArgsError ? err.message : err instanceof Error ? err.message : String(err));
|
|
463
|
+
}
|
|
464
|
+
}
|
|
446
465
|
const fpsFlag = flags.get("fps");
|
|
447
466
|
const started = performance.now();
|
|
448
467
|
try {
|
|
449
|
-
const
|
|
468
|
+
const sharedOpts = {
|
|
450
469
|
modulePath,
|
|
451
470
|
out: flags.get("out") ?? "out",
|
|
452
471
|
...fpsFlag ? { fps: parseInt(fpsFlag, 10) } : {},
|
|
@@ -464,7 +483,6 @@ async function main() {
|
|
|
464
483
|
...flags.has("lossless-intermediate") ? { losslessIntermediate: true } : {},
|
|
465
484
|
...flags.has("allow-gpu-shards") ? { allowGpuShards: true } : {},
|
|
466
485
|
...cache !== void 0 ? { cache } : {},
|
|
467
|
-
...flags.has("locale") && flags.get("locale") ? { locale: flags.get("locale") } : {},
|
|
468
486
|
captions: parseCaptionsModeOrFail(flags.get("captions")),
|
|
469
487
|
narration: flags.get("narration") === "off" ? "off" : "auto",
|
|
470
488
|
music: flags.get("music") === "off" ? "off" : "auto",
|
|
@@ -475,10 +493,21 @@ async function main() {
|
|
|
475
493
|
if (n % 30 === 0 || n === total) process.stderr.write(`\rrendering ${n}/${total} frames`);
|
|
476
494
|
} else if (n % 300 === 0 || n === total) process.stderr.write(`rendering ${n}/${total} frames\n`);
|
|
477
495
|
}
|
|
478
|
-
}
|
|
479
|
-
const secs = ((performance.now() - started) / 1e3).toFixed(2);
|
|
496
|
+
};
|
|
480
497
|
const cr = process.stderr.isTTY ? "\r" : "";
|
|
481
|
-
|
|
498
|
+
if (locales) {
|
|
499
|
+
const fan = await renderLocales(sharedOpts, locales);
|
|
500
|
+
const secs = ((performance.now() - started) / 1e3).toFixed(2);
|
|
501
|
+
for (const { locale, result } of fan) process.stderr.write(`${cr}rendered [${locale}] ${result.frames} frames → ${result.out}\n`);
|
|
502
|
+
process.stderr.write(`done: ${fan.length} locale${fan.length === 1 ? "" : "s"} in ${secs}s\n`);
|
|
503
|
+
} else {
|
|
504
|
+
const result = await render({
|
|
505
|
+
...sharedOpts,
|
|
506
|
+
...flags.has("locale") && flags.get("locale") ? { locale: flags.get("locale") } : {}
|
|
507
|
+
});
|
|
508
|
+
const secs = ((performance.now() - started) / 1e3).toFixed(2);
|
|
509
|
+
process.stderr.write(`${cr}rendered ${result.frames} frames in ${secs}s → ${result.out}\n`);
|
|
510
|
+
}
|
|
482
511
|
} catch (err) {
|
|
483
512
|
fail(err instanceof Error ? err.message : String(err));
|
|
484
513
|
}
|
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,63 @@ 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
|
+
* 0.15 FIX 2: a per-locale loudness dead-end (no `<stem>.<locale>.loudness.json`
|
|
274
|
+
* committed) also aborts the batch — but a bare error mid-fan-out reads like a
|
|
275
|
+
* generic crash, so each per-locale failure is wrapped to NAME the failing locale.
|
|
276
|
+
* It still fails loudly (never swallowed): the wrapped message stays actionable.
|
|
277
|
+
*/
|
|
278
|
+
declare function renderLocales(opts: Omit<RenderOptions, 'locale'>, locales: readonly string[]): Promise<{
|
|
279
|
+
locale: string;
|
|
280
|
+
result: {
|
|
281
|
+
frames: number;
|
|
282
|
+
out: string;
|
|
283
|
+
};
|
|
284
|
+
}[]>;
|
|
285
|
+
/** A scene module's default export is not a valid `SceneModule`. */
|
|
231
286
|
declare class SceneModuleError extends Error {
|
|
232
287
|
constructor(modulePath: string, detail: string);
|
|
233
288
|
}
|
|
@@ -276,8 +331,16 @@ declare function collectAudioClips(opts: Pick<RenderOptions, 'modulePath' | 'nar
|
|
|
276
331
|
* `.wav`/music stem invalidates the measurement here too (the §5.3 stale-gain
|
|
277
332
|
* gate). `timelineClips` defaults to the scene's compiled timeline audio when
|
|
278
333
|
* omitted, so a bare `{ modulePath }` call still gates correctly.
|
|
334
|
+
*
|
|
335
|
+
* 0.15 FIX 2 (localized loudness): a localized render (`--locale zh`) mixes the
|
|
336
|
+
* per-locale narration → a DIFFERENT mixHash than the base measurement. So when a
|
|
337
|
+
* locale is set it reads the per-locale file `<stem>.<locale>.loudness.json` FIRST.
|
|
338
|
+
* When that per-locale file is MISSING it throws an ACTIONABLE per-locale error
|
|
339
|
+
* (naming the file + the `gs measure-loudness … --locale` to run) instead of the
|
|
340
|
+
* generic stale-mixHash message — there is otherwise no supported way to commit a
|
|
341
|
+
* per-locale measurement, so a base file would always read as stale and dead-end.
|
|
279
342
|
*/
|
|
280
|
-
declare function resolveLoudnessGainDb(opts: Pick<RenderOptions, 'modulePath' | 'loudness' | 'narration' | 'music' | 'sfx'>, timelineClips?: AudioClip[]): Promise<number | null>;
|
|
343
|
+
declare function resolveLoudnessGainDb(opts: Pick<RenderOptions, 'modulePath' | 'loudness' | 'narration' | 'music' | 'sfx' | 'locale'>, timelineClips?: AudioClip[]): Promise<number | null>;
|
|
281
344
|
/**
|
|
282
345
|
* Collect audio clips + auto-mixed siblings and plan the FFmpeg audio graph,
|
|
283
346
|
* returning the `-i`/`-filter_complex`/`-map` argument fragments. Shared by the
|
|
@@ -303,7 +366,7 @@ declare function planFinalAudio(opts: RenderOptions, timelineClips: AudioClip[],
|
|
|
303
366
|
* gain — measurement is of the un-gained mix), so the measured content is exactly
|
|
304
367
|
* what render will later mix. Returns false when the scene has no audio.
|
|
305
368
|
*/
|
|
306
|
-
declare function buildMixWav(opts: Pick<RenderOptions, 'modulePath' | 'narration' | 'music' | 'sfx'>, wavOut: string): Promise<boolean>;
|
|
369
|
+
declare function buildMixWav(opts: Pick<RenderOptions, 'modulePath' | 'narration' | 'music' | 'sfx' | 'locale'>, wavOut: string): Promise<boolean>;
|
|
307
370
|
//#endregion
|
|
308
371
|
//#region src/loudness.d.ts
|
|
309
372
|
/**
|
|
@@ -412,10 +475,18 @@ declare function peakClampBinds(profile: PublishProfile, inputI: number, inputTp
|
|
|
412
475
|
* hash. The scene module path itself is excluded.
|
|
413
476
|
*/
|
|
414
477
|
declare function computeMixHash(modulePath: string, extraInputs?: readonly string[]): string;
|
|
415
|
-
/**
|
|
416
|
-
|
|
478
|
+
/**
|
|
479
|
+
* The committed loudness measurement path for a scene module.
|
|
480
|
+
*
|
|
481
|
+
* Base (no locale): `<stem>.loudness.json` — UNCHANGED. With a locale set (0.15
|
|
482
|
+
* FIX 2): `<stem>.<locale>.loudness.json`. A localized render mixes the per-locale
|
|
483
|
+
* narration (e.g. the zh wavs) → a DIFFERENT mixHash than the base, so it needs its
|
|
484
|
+
* OWN committed measurement; one base file can't gate a localized render (it would
|
|
485
|
+
* always read as a stale mixHash, with no supported way to commit a per-locale one).
|
|
486
|
+
*/
|
|
487
|
+
declare function loudnessPathFor(modulePath: string, locale?: string): string;
|
|
417
488
|
/** Read + validate a committed measurement, or null when none is committed. */
|
|
418
|
-
declare function readLoudness(modulePath: string): LoudnessMeasurement | null;
|
|
489
|
+
declare function readLoudness(modulePath: string, locale?: string): LoudnessMeasurement | null;
|
|
419
490
|
/**
|
|
420
491
|
* Parse the JSON block ffmpeg's `loudnorm=print_format=json` prints to stderr.
|
|
421
492
|
* It carries `input_i` / `input_tp` / `input_lra` (the measured loudness),
|
|
@@ -445,6 +516,13 @@ interface MeasureLoudnessOptions {
|
|
|
445
516
|
narration?: 'auto' | 'off';
|
|
446
517
|
music?: 'auto' | 'off';
|
|
447
518
|
sfx?: 'auto' | 'off';
|
|
519
|
+
/**
|
|
520
|
+
* locale code (0.15 FIX 2). When set, measure the per-locale mix (the localized
|
|
521
|
+
* narration sibling) and commit `<stem>.<locale>.loudness.json` instead of the
|
|
522
|
+
* base file. Mirrors `gs render --locale`, so the measured content matches what
|
|
523
|
+
* a localized render mixes.
|
|
524
|
+
*/
|
|
525
|
+
locale?: string;
|
|
448
526
|
}
|
|
449
527
|
interface MeasureLoudnessResult {
|
|
450
528
|
measurement: LoudnessMeasurement;
|
|
@@ -833,4 +911,4 @@ declare function cacheVerifyCommand(opts: CacheVerifyOptions): Promise<CacheVeri
|
|
|
833
911
|
/** The @glissade/cli package version (the glissade VERSION for the cache key). */
|
|
834
912
|
declare function glissadeVersion(): string;
|
|
835
913
|
//#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 };
|
|
914
|
+
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
|
@@ -146,13 +146,22 @@ function computeMixHash(modulePath, extraInputs = []) {
|
|
|
146
146
|
}
|
|
147
147
|
return `sha256:${h.digest("hex")}`;
|
|
148
148
|
}
|
|
149
|
-
/**
|
|
150
|
-
|
|
151
|
-
|
|
149
|
+
/**
|
|
150
|
+
* The committed loudness measurement path for a scene module.
|
|
151
|
+
*
|
|
152
|
+
* Base (no locale): `<stem>.loudness.json` — UNCHANGED. With a locale set (0.15
|
|
153
|
+
* FIX 2): `<stem>.<locale>.loudness.json`. A localized render mixes the per-locale
|
|
154
|
+
* narration (e.g. the zh wavs) → a DIFFERENT mixHash than the base, so it needs its
|
|
155
|
+
* OWN committed measurement; one base file can't gate a localized render (it would
|
|
156
|
+
* always read as a stale mixHash, with no supported way to commit a per-locale one).
|
|
157
|
+
*/
|
|
158
|
+
function loudnessPathFor(modulePath, locale) {
|
|
159
|
+
const stem = modulePath.replace(/\.[jt]sx?$/, "");
|
|
160
|
+
return locale !== void 0 && locale !== "" ? `${stem}.${locale}.loudness.json` : `${stem}.loudness.json`;
|
|
152
161
|
}
|
|
153
162
|
/** Read + validate a committed measurement, or null when none is committed. */
|
|
154
|
-
function readLoudness(modulePath) {
|
|
155
|
-
const path = loudnessPathFor(modulePath);
|
|
163
|
+
function readLoudness(modulePath, locale) {
|
|
164
|
+
const path = loudnessPathFor(modulePath, locale);
|
|
156
165
|
if (!existsSync(path)) return null;
|
|
157
166
|
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
158
167
|
if (raw.loudnessVersion !== 1) throw new LoudnessError(`${path}: unsupported loudnessVersion ${String(raw.loudnessVersion)} (expected 1); re-run gs measure-loudness`);
|
|
@@ -214,7 +223,7 @@ function measureFile(audioPath) {
|
|
|
214
223
|
*/
|
|
215
224
|
async function measureLoudnessCommand(opts) {
|
|
216
225
|
const profile = resolveProfile(opts.profile ?? "youtube");
|
|
217
|
-
const { buildMixWav } = await import("./render.js").then((n) => n.
|
|
226
|
+
const { buildMixWav } = await import("./render.js").then((n) => n.p);
|
|
218
227
|
const tmp = mkdtempSync(join(tmpdir(), "glissade-loudness-"));
|
|
219
228
|
try {
|
|
220
229
|
const wavPath = join(tmp, "mix.wav");
|
|
@@ -222,17 +231,19 @@ async function measureLoudnessCommand(opts) {
|
|
|
222
231
|
modulePath: opts.modulePath,
|
|
223
232
|
narration: opts.narration ?? "auto",
|
|
224
233
|
music: opts.music ?? "auto",
|
|
225
|
-
sfx: opts.sfx ?? "auto"
|
|
234
|
+
sfx: opts.sfx ?? "auto",
|
|
235
|
+
...opts.locale !== void 0 && opts.locale !== "" ? { locale: opts.locale } : {}
|
|
226
236
|
}, wavPath)) throw new LoudnessError(`${opts.modulePath} has no audio to measure (no timeline audio and no narration/music/sfx manifests)`);
|
|
227
237
|
const { inputI, inputTp, inputLra } = measureFile(wavPath);
|
|
228
238
|
const gain = computeGainDb(profile, inputI, inputTp);
|
|
229
239
|
const clampBound = peakClampBinds(profile, inputI, inputTp);
|
|
230
|
-
const { collectMixAudioInputs } = await import("./render.js").then((n) => n.
|
|
240
|
+
const { collectMixAudioInputs } = await import("./render.js").then((n) => n.p);
|
|
231
241
|
const extraInputs = await collectMixAudioInputs({
|
|
232
242
|
modulePath: opts.modulePath,
|
|
233
243
|
narration: opts.narration ?? "auto",
|
|
234
244
|
music: opts.music ?? "auto",
|
|
235
|
-
sfx: opts.sfx ?? "auto"
|
|
245
|
+
sfx: opts.sfx ?? "auto",
|
|
246
|
+
...opts.locale !== void 0 && opts.locale !== "" ? { locale: opts.locale } : {}
|
|
236
247
|
});
|
|
237
248
|
const mixHash = computeMixHash(opts.modulePath, extraInputs);
|
|
238
249
|
const measurement = {
|
|
@@ -244,7 +255,7 @@ async function measureLoudnessCommand(opts) {
|
|
|
244
255
|
gain: floorGain2(gain),
|
|
245
256
|
mixHash
|
|
246
257
|
};
|
|
247
|
-
const loudnessPath = loudnessPathFor(opts.modulePath);
|
|
258
|
+
const loudnessPath = loudnessPathFor(opts.modulePath, opts.locale);
|
|
248
259
|
writeFileSync(loudnessPath, JSON.stringify(measurement, null, 2) + "\n");
|
|
249
260
|
return {
|
|
250
261
|
measurement,
|
|
@@ -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,103 @@ 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
|
+
* 0.15 FIX 2: a per-locale loudness dead-end (no `<stem>.<locale>.loudness.json`
|
|
167
|
+
* committed) also aborts the batch — but a bare error mid-fan-out reads like a
|
|
168
|
+
* generic crash, so each per-locale failure is wrapped to NAME the failing locale.
|
|
169
|
+
* It still fails loudly (never swallowed): the wrapped message stays actionable.
|
|
170
|
+
*/
|
|
171
|
+
async function renderLocales(opts, locales) {
|
|
172
|
+
const results = [];
|
|
173
|
+
for (const locale of locales) {
|
|
174
|
+
let result;
|
|
175
|
+
try {
|
|
176
|
+
result = await render({
|
|
177
|
+
...opts,
|
|
178
|
+
out: localeOutPath(opts.out, locale, opts.format),
|
|
179
|
+
locale
|
|
180
|
+
});
|
|
181
|
+
} catch (err) {
|
|
182
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
183
|
+
if (err instanceof Error && msg.includes(`'${locale}'`)) throw err;
|
|
184
|
+
throw new Error(`--locales: locale '${locale}' failed — ${msg}`, { cause: err });
|
|
185
|
+
}
|
|
186
|
+
results.push({
|
|
187
|
+
locale,
|
|
188
|
+
result
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return results;
|
|
192
|
+
}
|
|
193
|
+
/** A scene module's default export is not a valid `SceneModule`. */
|
|
99
194
|
var SceneModuleError = class extends Error {
|
|
100
195
|
constructor(modulePath, detail) {
|
|
101
196
|
super(`${modulePath}: ${detail}\nA scene module default-exports { createScene(): Scene, timeline: Timeline } (SceneModule).`);
|
|
@@ -455,23 +550,26 @@ async function collectAudioClips(opts, timelineClips) {
|
|
|
455
550
|
* clips — the narration/music/sfx siblings still resolve from `modulePath`.
|
|
456
551
|
*/
|
|
457
552
|
async function collectMixAudioInputs(opts, timelineClips) {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
paths
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
553
|
+
const { preservingMessageTable } = await import("@glissade/core/i18n");
|
|
554
|
+
return preservingMessageTable(async () => {
|
|
555
|
+
let tl = timelineClips;
|
|
556
|
+
if (tl === void 0) try {
|
|
557
|
+
const mod = await loadSceneModule(opts.modulePath, opts.locale);
|
|
558
|
+
const { compileTimeline } = await import("@glissade/core");
|
|
559
|
+
tl = [...compileTimeline(mod.timeline).audio];
|
|
560
|
+
} catch {
|
|
561
|
+
tl = [];
|
|
562
|
+
}
|
|
563
|
+
const clips = await collectAudioClips(opts, tl);
|
|
564
|
+
const { resolveAssetPath } = await import("./audioMix.js").then((n) => n.i);
|
|
565
|
+
const paths = /* @__PURE__ */ new Set();
|
|
566
|
+
for (const c of clips) try {
|
|
567
|
+
paths.add(resolveAssetPath(c.asset.url, opts.modulePath));
|
|
568
|
+
} catch {
|
|
569
|
+
paths.add(c.asset.url);
|
|
570
|
+
}
|
|
571
|
+
return [...paths].sort();
|
|
572
|
+
});
|
|
475
573
|
}
|
|
476
574
|
/**
|
|
477
575
|
* Resolve the committed publish gain (dB) for a render: read `<scene>.loudness.json`
|
|
@@ -485,15 +583,34 @@ async function collectMixAudioInputs(opts, timelineClips) {
|
|
|
485
583
|
* `.wav`/music stem invalidates the measurement here too (the §5.3 stale-gain
|
|
486
584
|
* gate). `timelineClips` defaults to the scene's compiled timeline audio when
|
|
487
585
|
* omitted, so a bare `{ modulePath }` call still gates correctly.
|
|
586
|
+
*
|
|
587
|
+
* 0.15 FIX 2 (localized loudness): a localized render (`--locale zh`) mixes the
|
|
588
|
+
* per-locale narration → a DIFFERENT mixHash than the base measurement. So when a
|
|
589
|
+
* locale is set it reads the per-locale file `<stem>.<locale>.loudness.json` FIRST.
|
|
590
|
+
* When that per-locale file is MISSING it throws an ACTIONABLE per-locale error
|
|
591
|
+
* (naming the file + the `gs measure-loudness … --locale` to run) instead of the
|
|
592
|
+
* generic stale-mixHash message — there is otherwise no supported way to commit a
|
|
593
|
+
* per-locale measurement, so a base file would always read as stale and dead-end.
|
|
488
594
|
*/
|
|
489
595
|
async function resolveLoudnessGainDb(opts, timelineClips) {
|
|
490
596
|
if ((opts.loudness ?? "auto") === "off") return null;
|
|
491
597
|
const { readLoudness, computeMixHash, loudnessPathFor } = await import("./loudness.js").then((n) => n.c);
|
|
492
|
-
const
|
|
493
|
-
|
|
598
|
+
const hasLocale = opts.locale !== void 0 && opts.locale !== "";
|
|
599
|
+
const measurement = readLoudness(opts.modulePath, opts.locale);
|
|
600
|
+
if (!measurement) {
|
|
601
|
+
if (hasLocale && readLoudness(opts.modulePath) !== null) {
|
|
602
|
+
const perLocale = loudnessPathFor(opts.modulePath, opts.locale);
|
|
603
|
+
throw new Error(`loudness: no ${perLocale} for locale '${opts.locale}' — a localized render mixes the per-locale narration, which has a different loudness than the base mix, so it needs its OWN measurement. Run \`gs measure-loudness ${opts.modulePath} --locale ${opts.locale}\` to commit it (or pass --loudness off to render this locale without normalization).`);
|
|
604
|
+
}
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
494
607
|
const extraInputs = await collectMixAudioInputs(opts, timelineClips);
|
|
495
608
|
const actual = computeMixHash(opts.modulePath, extraInputs);
|
|
496
|
-
if (actual !== measurement.mixHash)
|
|
609
|
+
if (actual !== measurement.mixHash) {
|
|
610
|
+
const path = loudnessPathFor(opts.modulePath, opts.locale);
|
|
611
|
+
const reRun = hasLocale ? `gs measure-loudness ${opts.modulePath} --locale ${opts.locale}` : `gs measure-loudness ${opts.modulePath}`;
|
|
612
|
+
throw new Error(`loudness: ${path} is stale — the mix inputs changed since it was measured (committed mixHash ${measurement.mixHash.slice(0, 23)}…, current ${actual.slice(0, 23)}…). Re-run \`${reRun}\` (or pass --loudness off to render without normalization).`);
|
|
613
|
+
}
|
|
497
614
|
return measurement.gain;
|
|
498
615
|
}
|
|
499
616
|
/**
|
|
@@ -548,43 +665,46 @@ async function planFinalAudio(opts, timelineClips, duration, container) {
|
|
|
548
665
|
*/
|
|
549
666
|
async function buildMixWav(opts, wavOut) {
|
|
550
667
|
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
|
-
|
|
668
|
+
const { preservingMessageTable } = await import("@glissade/core/i18n");
|
|
669
|
+
return preservingMessageTable(async () => {
|
|
670
|
+
const mod = await loadSceneModule(opts.modulePath, opts.locale);
|
|
671
|
+
const scene = mod.createScene();
|
|
672
|
+
const { compileTimeline } = await import("@glissade/core");
|
|
673
|
+
const compiled = compileTimeline(mod.timeline);
|
|
674
|
+
const duration = compiled.duration;
|
|
675
|
+
const audioClips = await collectAudioClips(opts, [...compiled.audio]);
|
|
676
|
+
const { planAudioMix } = await import("./audioMix.js").then((n) => n.i);
|
|
677
|
+
const mix = planAudioMix(audioClips, opts.modulePath, duration);
|
|
678
|
+
if (!mix) return false;
|
|
679
|
+
const result = spawnSync("ffmpeg", [
|
|
680
|
+
"-y",
|
|
681
|
+
"-hide_banner",
|
|
682
|
+
"-nostats",
|
|
683
|
+
"-f",
|
|
684
|
+
"lavfi",
|
|
685
|
+
"-i",
|
|
686
|
+
"anullsrc=channel_layout=stereo:sample_rate=48000",
|
|
687
|
+
...mix.inputs.flatMap((p) => ["-i", p]),
|
|
688
|
+
"-filter_complex",
|
|
689
|
+
mix.filterComplex,
|
|
690
|
+
"-map",
|
|
691
|
+
"[aout]",
|
|
692
|
+
"-c:a",
|
|
693
|
+
"pcm_s16le",
|
|
694
|
+
"-ar",
|
|
695
|
+
"48000",
|
|
696
|
+
"-t",
|
|
697
|
+
String(duration),
|
|
698
|
+
wavOut
|
|
699
|
+
], { stdio: [
|
|
700
|
+
"ignore",
|
|
701
|
+
"ignore",
|
|
702
|
+
"pipe"
|
|
703
|
+
] });
|
|
704
|
+
if (result.status !== 0) throw new Error(`ffmpeg mix-to-WAV failed (exit ${result.status}):\n${result.stderr?.toString().slice(-2e3)}`);
|
|
705
|
+
scene.size;
|
|
706
|
+
return true;
|
|
707
|
+
});
|
|
588
708
|
}
|
|
589
709
|
//#endregion
|
|
590
|
-
export {
|
|
710
|
+
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.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.15.0-pre.1",
|
|
27
|
+
"@glissade/core": "0.15.0-pre.1",
|
|
28
|
+
"@glissade/interact": "0.15.0-pre.1",
|
|
29
|
+
"@glissade/lottie": "0.15.0-pre.1",
|
|
30
|
+
"@glissade/svg": "0.15.0-pre.1",
|
|
31
|
+
"@glissade/narrate": "0.15.0-pre.1",
|
|
32
|
+
"@glissade/player": "0.15.0-pre.1",
|
|
33
|
+
"@glissade/scene": "0.15.0-pre.1",
|
|
34
|
+
"@glissade/sfx": "0.15.0-pre.1"
|
|
35
35
|
},
|
|
36
36
|
"repository": {
|
|
37
37
|
"type": "git",
|