import { A as peakClampBinds, C as PublishProfile, D as measureFile, E as loudnessPathFor, M as resolveProfile, O as measureLoudnessCommand, S as PUBLISH_PROFILES, T as computeMixHash, _ as LOUDNESS_SCHEMA_VERSION, a as MasterMemberResult, b as MeasureLoudnessOptions, d as masterAfChain, f as masterCommand, g as DEFAULT_PROFILE_ID, h as CommittedLimiter, i as MasterLimiter, j as readLoudness, k as parseLoudnormJson, l as MemberMeasure, m as planMaster, n as MasterConfig, o as MasterPlan, p as normalizeMasterConfig, r as MasterError, s as MasterResult, t as DEFAULT_MAX_GR_DB, u as MemberPlan, v as LoudnessError, w as computeGainDb, x as MeasureLoudnessResult, y as LoudnessMeasurement } from "./master.js"; import { AudioClip, CompiledTimeline, Key, Timeline } from "@glissade/core"; import { DisplayList, Scene, SceneModule, VideoFrameSource } from "@glissade/scene"; import { SkiaBackend } from "@glissade/backend-skia"; import { NarrationTiming } from "@glissade/narrate"; import { Image } from "@napi-rs/canvas"; import * as _glissade_core_i18n0 from "@glissade/core/i18n"; //#region src/frameCache.d.ts /** read-write (default with --cache): serve hits, store misses. read-only: serve * hits but never write (a shared/read-only cache). off: bypass entirely — the * exact current equality baseline, no behavior change. */ type CacheMode = 'read-write' | 'read-only' | 'off'; /** * The two key components that have NO source inside `scene` and so MUST be injected * by the CLI (never read from disk). Folding both into the key is THE guard against * silent stale-tile corruption when the toolchain or backend caps change. */ interface CacheKeyContext { /** glissade VERSION — any composite/toolchain change bumps this, invalidating every frame. */ version: string; /** BackendCaps id (filters/shaders/maxTextureSize) — folds the rasterizer's capabilities. */ capsId: string; /** * A digest of the BYTES of every referenced image/video/font asset (a single * sha256 over the sorted assetId→byteDigest map; see `assetsDigest`). The * DisplayList carries only the asset *id*, NOT pixels, so without this an * in-place edit of an asset (same id/url) would collide the key and serve stale * pixels. Empty string when the scene references no binary assets. */ assetsDigest: string; /** * §0.75 `gs render --preview-res`: the OUTPUT raster dims (`"WxH"`) when this is * a scaled preview draft. The post-evaluate DisplayList is in SCENE coordinates * (identical bytes for a scaled and a full-res render of the same frame), so the * output scale is NOT in `serializeDisplayList(dl)` — without folding it here a * scaled RGBA (WxH) would collide the key with the full-res frame and be served * for the wrong tier (a `putPixels` size-mismatch throw, or worse). OMITTED for a * full-res render → byte-identical key to pre-0.75 (the default path is untouched). */ outputDims?: string; } interface FrameCacheOptions { dir: string; mode: CacheMode; /** max total bytes of stored (compressed) entries before LRU eviction; default 2 GB. */ maxSize?: number; } /** Default LRU cap: 2 GB of compressed entries (the card's default). */ declare const DEFAULT_CACHE_MAX_SIZE: number; declare class FrameCacheError extends Error { constructor(message: string); } /** * Stable, content-addressed key for ONE whole frame. Folds EVERY byte-affecting * input: the DisplayList-snapshot bytes (geometry/paint/transform — but only the * asset *id* for image/video/font draws), the glissade version, the backend caps * id, AND the asset-content digest (the BYTES behind those ids). An incomplete key * here IS the only failure mode (a stale frame served for changed content), so the * NEGATIVE gate test asserts that dropping any component makes `gs cache verify` * fail. */ declare function frameCacheKey(dl: DisplayList, ctx: CacheKeyContext): string; /** * Combine per-asset byte digests into ONE stable digest for the key context. Sorts * by assetId so the order assets loaded in never affects the key, then folds each * `id\0byteSha256\0` pair. An empty map → '' (no binary assets → no extra * invalidation, byte-identical key to a pre-asset-digest scene). Each byteDigest is * the caller's sha256 of the asset's raw bytes. */ /** A canonical BackendCaps id (filters sorted, shaders, maxTextureSize) — the §3 caps fold. */ declare function capsId(caps: { filters: ReadonlySet; shaders: boolean; maxTextureSize: number; }): string; interface CacheStats { hits: number; misses: number; stored: number; evicted: number; } /** * The persistent whole-frame raster cache. One instance per render process; shards * each open their own over the SAME dir (content-addressed + atomic writes make * that safe). A `mode:'off'` instance is inert — `get` always misses and `put` is a * no-op — so the cache-off path is the exact current baseline. */ declare class FrameCache { readonly dir: string; readonly mode: CacheMode; readonly maxSize: number; private readonly stats; constructor(opts: FrameCacheOptions); private pathFor; /** * Look up a whole-frame RGBA by key. On a hit, the file's mtime is touched * (access-time bump for the LRU) and the decoded RGBA returned; the caller blits * it via `backend.putPixels` and runs the IDENTICAL encode. Returns undefined on * a miss or in `mode:'off'`. A corrupt/truncated entry is treated as a miss (it * is deleted so it re-renders) rather than a hard error — the cache is advisory. */ get(key: string): Uint8ClampedArray | undefined; /** * Store a whole-frame RGBA under its key. No-op in `read-only`/`off`. The write * is atomic (temp file + rename) so a crash/interrupt never leaves a torn entry * a later run could half-read (the single failure mode). After a store the LRU * size cap is enforced. */ put(key: string, width: number, height: number, rgba: Uint8ClampedArray): void; /** * Evict oldest (by mtime) entries until the total compressed size is within * `maxSize`. Content-addressed + idempotent, so a delete racing a peer's * regeneration is benign. Runs after each store; cheap relative to a frame * rasterize. */ private enforceSizeCap; /** Snapshot of hit/miss/store/evict counters (for the render summary + tests). */ getStats(): Readonly; /** Total compressed bytes currently on disk (for tests / a `--cache` summary). */ diskSize(): number; /** Number of entries currently on disk (tests). */ entryCount(): number; } /** * Parse a `--cache-max-size` flag: a raw byte count or a human size (`2GB`, * `512MB`, `1.5g`). Rejects garbage so a typo doesn't silently default. Case- and * suffix-insensitive (b/k/m/g, optionally with a trailing `b`). */ declare function parseCacheMaxSize(flag: string): number; /** * Read the magic-validated header of an entry WITHOUT inflating its body — a cheap * existence + size probe used by `gs cache verify` to report what it sampled. */ declare function probeEntryHeader(file: string): { width: number; height: number; } | undefined; /** Remove the whole `.gscache` dir (a `gs cache clear` convenience / test cleanup). */ declare function clearFrameCache(dir: string): void; //#endregion //#region src/cert.d.ts type CertCacheMode = 'read-write' | 'read-only' | 'off'; /** * A LOCAL content-addressed render cache keyed by certHash. `put` stores the frame's * final artifact BYTES (the PNG) self-certifyingly; `get` returns `{ bytes, byteHash * }` (trust-on-read by certHash). A HIT serves the SAME bytes a render would (the * certHash captures every byte-determinant), so a cache HIT is byte-identical to a * cold render. `--verify-cache` re-renders a SAMPLE and confirms byteHash — a * mismatch is a determinism break (the b4e6060006 alarm). Distinct from the §3.5 * frame RGBA cache (keyed by the post-evaluate DisplayList): this is keyed by the * PURE cert (computable without evaluate) and stores the final artifact. * * CROSS-VERSION RETIREMENT (0.63.2): the on-disk store is VERSION-NAMESPACED — every * entry lives under a `v${CERT_VERSION}/` subdir of `dir`. A v2 read/write only ever * touches the `v2/` slot, so the shipped v1 entries (written flat under `dir/`) are * ORPHANED and can NEVER be served by a v2 `get()`. This retires the latent * FALSE-HIT entries a v1 cache holds (a v1 cert has no `complete`, so a v1 read could * otherwise serve stale bytes since certHash is unchanged by the schema bump). */ //#endregion //#region src/videoSource.d.ts declare class VideoProbeError extends Error { constructor(path: string, detail: string); } interface VideoInfo { fps: number; duration: number; width: number; height: number; } declare function probeVideo(path: string): VideoInfo; /** * v1 strategy: warm() extracts the full requested media range once at the * source's own fps; getFrameSync indexes the extracted sequence. Disk-bound, * trivially correct; the streaming-pipe variant is a later optimization. */ declare class FfmpegVideoFrameSource implements VideoFrameSource { readonly fps: number; readonly duration: number; private readonly path; private framesDir; private warmedFrom; private warmedTo; private readonly cache; constructor(path: string, info?: VideoInfo); private frameIndex; warm(fromT: number, toT: number): Promise; getFrameSync(mediaT: number): Image; close(): void; } //#endregion //#region src/render.d.ts interface RenderOptions { modulePath: string; out: string; fps?: number; /** seconds; defaults to [0, duration] (programmatic API — the CLI uses frame indices). */ range?: [number, number]; /** inclusive FRAME indices [first, last] (§5: export APIs are frame-indexed). Wins over `range`. */ frameRange?: [number, number]; /** a single FRAME index — a still through the same path. Wins over `frameRange`/`range`. */ frame?: number; /** force a PNG sequence even when `out` has a video extension. */ format?: 'png-seq'; /** InputTrace file: replay → bake → render (v2 §A.6 route 2). */ trace?: string; /** Render one machine state's timeline linearly (route 3). */ state?: string; /** Downgrade a trace hash mismatch to a warning. */ force?: boolean; /** burn (default): captions render in-frame; sidecar/off hide the caption node. */ captions?: 'burn' | 'sidecar' | 'off'; /** auto (default): mix a sibling *.music.timing.json bed, ducked under narration. */ music?: 'auto' | 'off'; /** auto (default): mix the voice from a sibling *.narration.timing.json. */ narration?: 'auto' | 'off'; /** auto (default): mix effect hits from a sibling *.sfx.timing.json. */ sfx?: 'auto' | 'off'; /** * auto (default): apply a committed `.loudness.json` publish gain (a * pure scalar `volume=dB` on the final mix node) when one exists, and * HARD-THROW if its mixHash no longer matches the mix inputs (a re-narrate must * invalidate loudly). 'off' ignores any committed measurement. */ loudness?: 'auto' | 'off'; /** also write WebVTT chapters from cue markers ('vtt'); cues.json is always written when cues exist. */ chapters?: 'vtt' | 'off'; /** cue kinds that become VTT chapters (default just 'chapter'); cues.json keeps all kinds. */ chapterKinds?: ReadonlySet; /** --strict: font validation throws on an unregistered family / missing glyph (§3.6). Default dev-warn. */ strictFonts?: boolean; /** * --allow-system-fonts (§3.6, 0.14 FIX 6): exempt true-OS-installed families * (the host `GlobalFonts.families` catalog) from the unregistered-family check. * OFF by default — the exempt set is otherwise just the families glissade * registered from `doc.assets`, so the verdict is host-independent. IGNORED * under --strict (strict stays host-independent regardless of this flag). */ allowSystemFonts?: boolean; /** * --workers N (§5.6): split the frame range into N contiguous sub-ranges, render * each in a separate `gs` child process, and join the shard videos. Ignored for * a single frame or N <= 1. Only meaningful for a video `out`. */ workers?: number; /** * --lossless-intermediate (§5.6, §8.1): render shards as FFV1 (lossless) and do a * single final encode after the concat — the guaranteed byte-correct join path. * Forced on automatically when the picked encoder can't honor precise boundary * keyframes (mpeg4 / openh264). */ losslessIntermediate?: boolean; /** * --incremental (§8.1, 0.41 dirty-beat): re-render ONLY the frames whose per-frame * content key changed since the last render, splicing the unchanged runs verbatim * out of a retained FFV1 lossless intermediate. Kills the full re-render an edit * that shifts timing (move one beat) otherwise forces — every downstream frame's * DisplayList shifts, defeating both the whole-frame cache and the remux fast path. * Implies the lossless-intermediate pipeline (FFV1 → single final encode); a warm * splice is byte-identical to a cold `--incremental` render by construction. Video * output only; requires the per-frame key (folds the same context the cache uses). */ incremental?: boolean; /** * --allow-gpu-shards (§5.6): sharded GPU/shader output isn't reproducible across * processes/machines, so a scene containing a ShaderEffect refuses to shard unless * this is set. */ allowGpuShards?: boolean; /** * --preview / --final (0.71 two-tier): the ENCODE tier. 'final' (the default when * neither flag is passed) is the byte-exact production encode — bytes are identical * to pre-0.71. 'preview' produces a watchable DRAFT: the SAME rasterized frames * (crf is not in the frame-key digest, so a preview reuses the final's frame cache * — no re-raster), encoded at a higher crf for a faster/lighter h264. The tier is * isolated at the encode-artifact layer (renderManifest.videoQuality) so a preview * never remux-serves a --final request or vice versa. */ tier?: 'preview' | 'final'; /** * --preview-res (0.75 two-tier render scale): render a watchable DRAFT at f× * the OUTPUT RASTER resolution (the whole composition rasterized into a * `round(w*f)×round(h*f)` canvas — fewer pixels to rasterize = the render-time win * for raster-bound scenes). PREVIEW-ONLY: it REQUIRES `tier:'preview'` and is * REJECTED with `--final`/the default (`resolvePreviewRes` throws) — a scaled * render is structurally non-final, so the certified/production master is always * full-res and goldens can never be scaled. `0 < f ≤ 1`; `f === 1` (or omitted) is * the EXACT current unscaled path (byte-identical to pre-0.75). Applied at the * BACKEND OUTPUT raster layer, ORTHOGONAL to a node's `.scale` scene transform. */ previewRes?: number; /** * --cache (§3.5, 0.12): persistent whole-frame raster cache. `dir` is the * `.gscache` directory (shared across runs AND shards); `mode` defaults to 'off' * (the exact current equality baseline). `maxSize` caps the LRU (default 2 GB). * A hit serves a stored RGBA byte-identical to a cold render — it wins REPEATED * renders and the UNCHANGED-PREFIX of a single-segment edit. A full re-narrate * shifts every frame's timing → every DisplayList changes → every frame MISSES. */ cache?: { dir: string; mode: CacheMode; maxSize?: number; }; /** * --certify (0.62): emit the per-frame determinism certificate manifest * (`.cert.json`) + the audio-cert, and (when a cert cache is enabled) * populate the content-addressed render cache. Additive READ — the render path * is byte-identical to a non-certified render (the cert reads the bytes, never * alters them). Off by default. */ certify?: boolean; /** * --cert-cache [] (0.62): the LOCAL content-addressed render cache keyed by * certHash (a PURE fn of inputs — computed WITHOUT rendering). Per-frame: a HIT * serves the pinned artifact bytes (SKIPS evaluate+render), a MISS renders + * stores {bytes, byteHash}. A HIT is byte-identical to a cold render (certHash * captures every byte-determinant). Distinct from `--cache` (the §3.5 RGBA cache * keyed by the post-evaluate DisplayList). Off by default. */ certCache?: { dir: string; mode: CertCacheMode; }; /** * --locale (0.14 localization core): resolve the scene against a * per-locale message table (`messages..json`) and prefer the * locale-tagged narration sibling (`..narration.timing.json`). * Omitted (the base path) resolves the BASE files → byte-identical to today. */ locale?: string; onProgress?: (frame: number, total: number) => void; } /** * Resolve the ffmpeg encode-quality args for an encoder + tier. Pure. crf is an * ENCODE param only — it changes the compressed bytes, never the rasterized frames * (which is why it is NOT folded into the frame-key digest and a preview can reuse * a final's frame cache). The joined string of these args is the manifest * `videoQuality` that isolates the tiers in canRemux. */ declare class LocaleArgsError extends Error { constructor(detail: string); } /** * Parse the `--locales ` comma-separated list into a de-duplicated, * order-preserving array of locale codes. Empty / whitespace-only entries are * dropped; an all-empty list is rejected (a `--locales` with nothing to render * is a user error, not a silent no-op). */ declare function parseLocalesList(raw: string): string[]; /** * Derive the per-locale output path from the base `out` and a locale code. * * Convention: a video/PNG FILE path (`out/episode.mp4`, `out/still.png`) gets a * locale segment inserted before the extension → `out/episode..mp4`. Any * other `out` (a PNG-sequence DIRECTORY, the default `out`) gets a per-locale * SUBDIR → `out/` (so `out/en/frame-00000.png`, `out/zh/frame-00000.png`). * `--format png-seq` forces the directory convention even for a video-looking * name, matching how `render()` itself treats `out` under that flag. */ declare function localeOutPath(out: string, locale: string, format?: 'png-seq'): string; /** * §0.15 `--locales` fan-out: render the scene once per locale, writing one * artifact per locale to a distinct per-locale path (`localeOutPath`). Each * per-locale render IS the 0.14 single-`--locale` render — so `--locales en,zh` * ≡ `--locale en` then `--locale zh` with distinct outputs. * * Fails LOUDLY: a locale in the list with no resolvable assets throws the 0.14 * `UnknownLocaleError` (naming the bad locale) from inside `render()`, aborting * the whole fan-out — locales already rendered keep their artifacts, but the * process exits non-zero so a missing-asset locale is never silently skipped. * * 0.15 FIX 2: a per-locale loudness dead-end (no `..loudness.json` * committed) also aborts the batch — but a bare error mid-fan-out reads like a * generic crash, so each per-locale failure is wrapped to NAME the failing locale. * It still fails loudly (never swallowed): the wrapped message stays actionable. */ declare function renderLocales(opts: Omit, locales: readonly string[]): Promise<{ locale: string; result: { frames: number; out: string; }; }[]>; /** A scene module's default export is not a valid `SceneModule`. */ declare class SceneModuleError extends Error { constructor(modulePath: string, detail: string); } /** * Parse the CLI `--range a..b` flag as INCLUSIVE integer FRAME indices (§5: * export APIs are frame-indexed; Player APIs are seconds). Decimal or malformed * ranges are rejected, since a frame index is an integer. */ declare function loadSceneModule(modulePath: string, locale?: string, messageTableOverride?: _glissade_core_i18n0.MessageTable): Promise; declare function ffmpegAvailable(): boolean; /** What `prepareSkiaRenderEnv` needs to make a Skia render FAITHFUL (fonts+axes, * Yoga, assets) — the setup that `gs render` and any parity/preview render must * share so a headless render is byte-faithful to `gs render` by construction. */ declare function render(opts: RenderOptions): Promise<{ frames: number; out: string; }>; /** Options for `gs render --verify ` / `--verify-cache`. */ declare function collectAudioClips(opts: Pick, timelineClips: AudioClip[]): Promise; /** * Resolve the ACTUAL audio files that feed the final mix (timeline clips + * auto-mixed narration/music/sfx), as absolute paths. This is what the mixHash * must hash the BYTES of: editing a timeline `.wav` or a music stem in place leaves * the timing manifests unchanged, so without folding these bytes a stale publish * gain would be applied silently. Reuses `collectAudioClips` (the same gather the * mix itself uses), then resolves each clip asset URL. De-duplicated + sorted so * measure-time and render-time agree regardless of clip order. `timelineClips` is * the scene's compiled timeline audio; when omitted it is compiled from the module * (so measure-time and the bare-gate path see the SAME timeline clips render does). * A module that can't load (e.g. a unit-test stub path) falls back to no timeline * clips — the narration/music/sfx siblings still resolve from `modulePath`. */ /** * Resolve the committed publish gain (dB) for a render: read `.loudness.json` * (when present and `loudness !== 'off'`), recompute the mixHash over the current * mix inputs, and HARD-THROW on a mismatch — a re-narrate/re-sfx must invalidate * the measurement loudly rather than silently mis-normalize. Returns null when no * measurement is committed or loudness is off (no gain applied). * * The mixHash folds the BYTES of the actual mix audio inputs (timeline clips + * narration/music/sfx) — not just the timing manifests — so an in-place edit of a * `.wav`/music stem invalidates the measurement here too (the §5.3 stale-gain * gate). `timelineClips` defaults to the scene's compiled timeline audio when * omitted, so a bare `{ modulePath }` call still gates correctly. * * 0.15 FIX 2 (localized loudness): a localized render (`--locale zh`) mixes the * per-locale narration → a DIFFERENT mixHash than the base measurement. So when a * locale is set it reads the per-locale file `..loudness.json` FIRST. * When that per-locale file is MISSING it throws an ACTIONABLE per-locale error * (naming the file + the `gs measure-loudness … --locale` to run) instead of the * generic stale-mixHash message — there is otherwise no supported way to commit a * per-locale measurement, so a base file would always read as stale and dead-end. */ declare function resolveLoudnessGainDb(opts: Pick, timelineClips?: AudioClip[]): Promise<{ gainDb: number; limiter?: CommittedLimiter; } | null>; /** * Collect audio clips + auto-mixed siblings and plan the FFmpeg audio graph, * returning the `-i`/`-filter_complex`/`-map` argument fragments. Shared by the * linear `render()` path and the sharded orchestrator (which mixes audio once, * over the concatenated video). Returns empty args when there is nothing to mix. * * When a committed `.loudness.json` applies, its publish gain is appended * as a PURE `volume=dB` scalar on the FINAL mix node — a single scalar in * the existing graph, NOT a second pass. The scalar gain is bit-deterministic * (verified) and golden-hashable; the only non-deterministic stages (mix-to-PCM, * measure-time ebur128) are quarantined to commit/measure-time per §5.3. * * (Note: `collectMixAudioInputs` resolves auto-mixed narration/music/sfx siblings * from `modulePath`, so a `timelineClips: []` default still gates those.) */ declare function planFinalAudio(opts: RenderOptions, timelineClips: AudioClip[], duration: number, container: 'mp4' | 'webm'): Promise<{ audioInputs: string[]; audioArgs: string[]; }>; /** * Build the FINAL mixed audio to a WAV file — the measure-loudness input. Uses * the SAME `collectAudioClips` + `planAudioMix` as the render path (no loudness * gain — measurement is of the un-gained mix), so the measured content is exactly * what render will later mix. Returns false when the scene has no audio. */ declare function buildMixWav(opts: Pick, wavOut: string): Promise; //#endregion //#region src/shards.d.ts declare class ShardError extends Error { constructor(message: string); } interface ShardRange { /** inclusive first frame */ first: number; /** inclusive last frame */ last: number; } /** * Split inclusive `[first, last]` into up to `workers` contiguous sub-ranges, * front-loading the remainder so earlier shards are at most one frame larger. * Never returns more ranges than there are frames. */ declare function splitFrameRange(first: number, last: number, workers: number): ShardRange[]; /** Does this scene contain a GPU/shader node (outside the §3.7 determinism guarantee)? */ declare function sceneHasGpuNodes(scene: Scene): boolean; interface RenderShardedArgs { opts: RenderOptions; scene: Scene; compiled: CompiledTimeline; fps: number; duration: number; firstFrame: number; lastFrame: number; container: 'mp4' | 'webm'; workers: number; timingPathFor: (modulePath: string) => string | null; writeCaptionSidecars: (timingPath: string, target: string) => { srt: string; vtt: string; }; writeCueSidecars: (target: string, markers: CompiledTimeline['markers'], duration: number, chapters: boolean, chapterKinds?: ReadonlySet) => string[]; } /** * Orchestrate a sharded render. Returns the same `{ frames, out }` shape as the * linear `render()`. Throws on a GPU scene without --allow-gpu-shards, or on any * child / ffmpeg failure. */ declare function renderSharded(a: RenderShardedArgs): Promise<{ frames: number; out: string; }>; /** The FFV1 lossless intermediate retained beside a `--incremental` output for the next splice. */ //#endregion //#region src/audioMix.d.ts declare class AudioMixError extends Error { constructor(detail: string); } /** Resolve a clip asset URL relative to the scene module's directory. */ declare function resolveAssetPath(url: string, modulePath: string): string; /** * Piecewise-linear FFmpeg volume expression from gain-track keys. Eases are * approximated linearly with a warning at the call site; keys outside the * clip hold their boundary value. */ declare function gainExpression(keys: Key[]): string; /** atempo only accepts [0.5, 2]; chain filters for rates outside. */ declare function atempoChain(rate: number): string[]; interface AudioMixPlan { /** extra `-i` inputs, in order, starting at input index 1 (0 = video) */ inputs: string[]; /** -filter_complex value producing [aout] */ filterComplex: string; hasEasedGain: boolean; } /** * The publish-loudness filter nodes: a `volume=dB` multiply, and — for a * `gs master` measurement — a REAL true-peak limiter (oversample 4× → `alimiter` * at `ceilingDb − guard` → downsample) that actually holds the inter-sample / * true peak under `ceilingDb`. Shared by the render `filter_complex` apply and the * `gs master` verify pass so the committed limiter and the rendered output are the * identical deterministic chain. Empty (a no-op) at 0 dB with no limiter. */ /** * Append the publish-loudness stage to a mix's `-filter_complex`: the graph's * final `[aout]` label is renamed and the {@link loudnessFilterNodes} (gain +, * for a master, the true-peak limiter) feed the new `[aout]`. Bit-deterministic + * golden-hashable. A 0 dB gain with NO limiter is a no-op (returned unchanged) so * an at-target source preserves the prior, un-gained bytes. */ declare function applyMixGainDb(filterComplex: string, gainDb: number, limiter?: { readonly ceilingDb: number; }): string; /** Build the FFmpeg mix plan for clips that intersect [0, duration]. */ declare function planAudioMix(clips: AudioClip[], modulePath: string, duration: number): AudioMixPlan | null; //#endregion //#region src/encoders.d.ts /** * FFmpeg encoder detection (§5.2 feature-detection extended to the CLI path): * distro "free" FFmpeg builds ship without libx264, so the encoder is picked * from what the installed binary actually offers, in preference order, with * an actionable error when a container has no usable encoder at all. */ declare class NoEncoderError extends Error { constructor(container: string, wanted: string[]); } /** Parse `ffmpeg -encoders` output into the set of encoder names. */ declare function parseEncoderList(output: string): Set; declare function availableEncoders(): Set; /** Test seam: override/clear the cached encoder set. */ interface EncoderChoice { name: string; /** Non-preferred pick worth telling the user about. */ note?: string; } declare function pickEncoder(kind: 'video' | 'audio', container: 'mp4' | 'webm', encoders?: Set): EncoderChoice; //#endregion //#region src/machines.d.ts interface MachineRenderFlags { trace?: string; state?: string; force?: boolean; } declare class MachineExportError extends Error { constructor(message: string); } /** * Decide what document to render. No machines: the module timeline, untouched. * With machines: --state merges one state's timeline over the ambient document; * --trace replays and bakes; neither is a hard error listing the routes. */ declare function resolveRenderDoc(mod: SceneModule, scene: Scene, flags: MachineRenderFlags): Timeline; //#endregion //#region src/dev.d.ts /** * gs dev (v2 §C.5): serve the scene with its machines mounted; --record adds * a Record button that writes .trace.json sidecars next to the module on * stop. This is the walkable capture path — producing a trace must never * require a hand-written harness page. */ interface DevOptions { modulePath: string; port?: number; record?: boolean; } interface DevServer { port: number; close(): Promise; } declare function dev(opts: DevOptions): Promise; //#endregion //#region src/import.d.ts /** * gs import (lottie-import.md §3.1): a vector asset → a generated TypeScript * scene module consumable by gs render. `.json` → Lottie/bodymovin (nodes + * inline Timeline); `.svg` → a static SVG scene (a wrapper deferring to * `@glissade/svg`'s `importSvg`, the single source of truth for the conversion). */ interface ImportOptions { input: string; /** Output directory; the module is written as .ts inside it. */ out: string; allowDegraded?: boolean; } interface ImportCommandResult { out: string; warnings: string[]; } declare function importCommand(opts: ImportOptions): Promise; //#endregion //#region src/export.d.ts /** * gs export --lottie (the inverse of `gs import`): a glissade scene module → a * Lottie/bodymovin .json. Loads the SceneModule (the node tree + Timeline), runs * the pure `@glissade/lottie` exporter, and writes the document. Scope-out / * degrade notes surface as warnings (mirroring `gs import`). */ interface ExportOptions { /** Scene module path. */ input: string; /** Output .json file. */ out: string; width?: number; height?: number; fps?: number; } interface ExportCommandResult { out: string; warnings: string[]; } declare function exportCommand(opts: ExportOptions): Promise; //#endregion //#region src/diff.d.ts interface DiffOptions { modulePath: string; /** seconds (the §5 Player-API time unit; `diff` is a single-frame still). */ at: number; /** baseline path: `.dl.json` (structural diff) or `.png` (byte-compare). */ against: string; } interface DiffResult { /** true when the scene matches the baseline. */ equal: boolean; /** the rendered command-tree / byte-compare report (always populated). */ report: string; /** the freshly-evaluated DisplayList (so callers can re-snapshot). */ actual: DisplayList; } /** Evaluate the scene module at `t` to its DisplayList (single-frame, no audio). */ declare function evaluateAt(modulePath: string, t: number): Promise; declare function diffCommand(opts: DiffOptions): Promise; /** Snapshot a scene's DisplayList at `t` to a `.dl.json` string (the baseline writer). */ declare function snapshotAt(modulePath: string, t: number): Promise; //#endregion //#region src/repin.d.ts /** The default golden frame set + fps — matches the Skia golden harness. */ declare const DEFAULT_FRAMES: number[]; declare const DEFAULT_FPS = 60; interface RepinOptions { modulePath: string; /** directory holding the committed golden PNGs. */ goldenDir: string; /** golden filename prefix (`-f0030.png`); default = the module basename. */ name?: string; /** frame numbers to review; default DEFAULT_FRAMES. */ frames?: number[]; /** frames-per-second for `t = frame / fps`; default 60. */ fps?: number; /** git ref to diff the narration timing sibling against for the cause line; default 'HEAD'. */ since?: string; /** actually overwrite changed/new goldens (default: dry-run report). */ write?: boolean; /** when writing, restrict to these frames (per-frame confirm). */ only?: number[]; /** write per-frame SSIM heat-map PNGs here for visual review. */ heatmapDir?: string; /** refuse to write a frame whose mean SSIM fell below this (unless force). */ floor?: number; /** override the floor guard. */ force?: boolean; } type RepinStatus = 'identical' | 'changed' | 'new' | 'missing'; interface RepinFrame { frame: number; t: number; status: RepinStatus; /** mean SSIM vs the golden (changed frames only). */ ssim?: number; /** worst-tile SSIM (changed frames only). */ minSsim?: number; /** the narration cause line, when a timing shift explains the change. */ cause?: string; /** true when this frame's golden PNG was (re)written. */ wrote: boolean; /** true when a write was refused because the drop fell below --floor. */ blocked?: boolean; /** path of the heat-map PNG, when --heatmap was given. */ heatmap?: string; } interface RepinResult { frames: RepinFrame[]; changed: number; wrote: number; blocked: number; /** true when the timing sibling was resolved AND diffed against `since`. */ causeSource: string | null; report: string; } declare function repinCommand(opts: RepinOptions): Promise; //#endregion //#region src/narrationLint.d.ts type LintRule = 'reading-speed' | 'anchor-budget' | 'caption-fit' | 'beat-drift' | 'silence'; interface Diagnostic { /** which rule fired */ rule: LintRule; /** 1 = HARD (can fail CI); 2 = WARN-only (never fails CI) */ tier: 1 | 2; /** 'error' (Tier-1) | 'warn' (Tier-2) — derived from tier, surfaced for tables */ severity: 'error' | 'warn'; /** the segment / pause / cue id this is about */ id: string; /** a one-line human message (the table cell) */ message: string; /** measured numbers, for --json consumers and the --fix diff */ detail?: Record; } /** * The caption-fit probe: drive the REAL caption node with a cue's text and * report its measured geometry. The CLI builds this from the loaded scene + * the Skia measurer; a pure unit test can stub it. Returns null when the scene * has no caption node (caption-fit then doesn't run). */ interface CaptionProbe { /** scene height (px) — the box the caption must stay inside, bottom-anchored */ readonly sceneH: number; /** the caption node's configured max line count before it's "too tall" */ readonly maxLines: number; /** * Lay out `text` on the real caption node (real width/font/autoFit + the real * breakLines) and report the wrapped line count and the lowest ink pixel's * absolute Y on the scene (so a block that runs off the bottom is caught). */ measure(text: string): { lines: number; bottomY: number; }; } interface LintOptions { /** max chars-per-second over a cue before reading-speed fires; default 17 */ maxCps?: number; /** the caption-fit probe (real geometry); omit to skip caption-fit */ caption?: CaptionProbe; /** include Tier-2 (warn-only) diagnostics; default true */ warnings?: boolean; /** * Caller-supplied caption mode (e.g. the render config is burning captions). * MAY escalate caption-fit to Tier-1 — but the committed script's own * `captionMode` (on the timing manifest) is the authoritative signal and * takes precedence. Omit to defer entirely to the manifest. */ captionMode?: 'burn' | 'sidecar'; } /** * Lint the committed narration timing. Pure: same inputs (timing + the probe's * measurements) → same diagnostics, in any order. Tier-1 diagnostics are * errors a caller can exit non-zero on; Tier-2 are warnings that never gate CI. */ declare function lintNarration(timing: NarrationTiming, opts?: LintOptions): Diagnostic[]; /** True when any Tier-1 diagnostic is present — the CI gate / exit-code signal. */ declare function hasErrors(diags: readonly Diagnostic[]): boolean; /** A human-readable table (the default terminal output). */ declare function formatTable(diags: readonly Diagnostic[]): string; /** * A git-apply-able unified diff that bumps the committed budgets (and * captionSplit, when a caption is too dense) to the values the manifest already * shows — the `--fix` SUGGESTION. NEVER writes anything: it prints a diff the * author reviews and applies (re-narrate is the real fix; this just unblocks CI * when the over-budget value is genuinely the new intent). Targets the SCRIPT * (`*.narration.json`), since budgets are committed there. */ declare function fixDiff(diags: readonly Diagnostic[], scriptPath: string, script: { budgets?: Record; }): string; //#endregion //#region src/narrationLintCommand.d.ts interface NarrationLintOptions { /** a scene module (resolves the sibling timing manifest) or the manifest itself */ input: string; maxCps?: number; /** caption node maxLines for the fit rule; default 2 (captionNode's own default) */ maxLines?: number; json?: boolean; fix?: boolean; /** skip Tier-2 (warn-only) diagnostics */ noWarnings?: boolean; } interface NarrationLintResult { diagnostics: Diagnostic[]; /** true when any Tier-1 diagnostic fired (the CI gate / exit code) */ hasErrors: boolean; /** the rendered human/JSON/diff text to print */ output: string; timingPath: string; } /** `.narration.timing.json` or the manifest path itself. */ declare function lintTimingPathFor(input: string): Promise; /** * Build a caption-fit probe by loading the scene, registering its fonts, and * driving the REAL `captions` node with the Skia measurer. Returns null when * the input is a bare manifest (no scene module) or the scene has no caption * node — caption-fit then doesn't run, and the lint is still exact for the * other rules. Async because font registration + the scene load are. */ declare function buildCaptionProbe(input: string, maxLines: number): Promise; /** Run the lint end-to-end and render the output (JSON / table / --fix diff). */ declare function narrationLintCommand(opts: NarrationLintOptions): Promise; //#endregion //#region src/cacheVerify.d.ts interface CacheVerifyOptions { modulePath: string; /** inclusive frame range [first, last]; default the whole timeline. */ frameRange?: [number, number]; /** fps override (default: timeline fps, else 60). */ fps?: number; /** sample 1-of-N frames for the byte compare (default 1 = every frame). LOGGED. */ sample?: number; /** * INTERNAL test seam: override the cache-key context. The NEGATIVE gate passes a * deliberately INCOMPLETE keyer to prove the verify FAILS when the key drops a * byte-affecting component. Never set by the CLI. */ keyContextOverride?: CacheKeyContext; /** * INTERNAL test seam: a custom keyer (e.g. one that IGNORES the DisplayList) to * simulate a structurally-incomplete key for the NEGATIVE gate. */ keyerOverride?: (dl: DisplayList, ctx: CacheKeyContext) => string; } interface CacheVerifyResult { ok: boolean; /** frames actually byte-compared (the sampled set). */ comparedFrames: number[]; /** total frames in range (compared ⊆ this). */ totalFrames: number; /** the first mismatching frame, when !ok. */ mismatch?: { frame: number; cached: string; cold: string; }; report: string; } /** * The verify gate. Renders the range through a read-WRITE cache once (to WARM it), * then compares — for each SAMPLED frame — a read-ONLY render (serving the warmed * hits) against a cache-OFF render, by encodePng sha256. Equal frame-for-frame is * the contract; the first mismatch fails the gate. */ declare function cacheVerifyCommand(opts: CacheVerifyOptions): Promise; //#endregion //#region src/version.d.ts /** * The glissade VERSION string folded into the persistent frame-cache key * (DESIGN.md §3.5, 0.12). Lockstep `0.x` versioning means the @glissade/cli * package version bumps with EVERY release, so any Raster2D-composite or * Skia-toolchain change ships under a new version — making bump-on-version the * cheapest CORRECT cache invalidation. Read once from the package manifest (the * single source of truth) rather than hard-coded so it can never drift from the * published version. */ /** The @glissade/cli package version (the glissade VERSION for the cache key). */ declare function glissadeVersion(): string; //#endregion export { AudioMixError, type AudioMixPlan, type CacheKeyContext, type CacheMode, type CacheVerifyOptions, type CacheVerifyResult, type CaptionProbe, type CommittedLimiter, DEFAULT_CACHE_MAX_SIZE, DEFAULT_FPS, DEFAULT_FRAMES, DEFAULT_MAX_GR_DB, DEFAULT_PROFILE_ID, type DevOptions, type DevServer, type Diagnostic, type DiffOptions, type DiffResult, type EncoderChoice, type ExportOptions as ExportCommandOptions, type ExportCommandResult, FfmpegVideoFrameSource, FrameCache, FrameCacheError, type FrameCacheOptions, type ImportCommandResult, type ImportOptions, LOUDNESS_SCHEMA_VERSION, type LintOptions, type LintRule, LocaleArgsError, LoudnessError, type LoudnessMeasurement, MachineExportError, type MachineRenderFlags, type MasterConfig, MasterError, type MasterLimiter, type MasterMemberResult, type MasterPlan, type MasterResult, type MeasureLoudnessOptions, type MeasureLoudnessResult, type MemberMeasure, type MemberPlan, type NarrationLintOptions, type NarrationLintResult, NoEncoderError, PUBLISH_PROFILES, type PublishProfile, type RenderOptions, type RenderShardedArgs, type RepinFrame, type RepinOptions, type RepinResult, type RepinStatus, SceneModuleError, ShardError, type ShardRange, type VideoInfo, VideoProbeError, applyMixGainDb, atempoChain, availableEncoders, buildCaptionProbe, buildMixWav, cacheVerifyCommand, capsId, clearFrameCache, collectAudioClips, computeGainDb, computeMixHash, dev, diffCommand, evaluateAt, exportCommand, ffmpegAvailable, fixDiff, formatTable, frameCacheKey, gainExpression, glissadeVersion, hasErrors, importCommand, lintNarration, lintTimingPathFor, loadSceneModule, localeOutPath, loudnessPathFor, masterAfChain, masterCommand, measureFile, measureLoudnessCommand, narrationLintCommand, normalizeMasterConfig, parseCacheMaxSize, parseEncoderList, parseLocalesList, parseLoudnormJson, peakClampBinds, pickEncoder, planAudioMix, planFinalAudio, planMaster, probeEntryHeader, probeVideo, readLoudness, render, renderLocales, renderSharded, repinCommand, resolveAssetPath, resolveLoudnessGainDb, resolveProfile, resolveRenderDoc, sceneHasGpuNodes, snapshotAt, splitFrameRange };