@glissade/cli 0.4.3 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/music.js +55 -0
- package/dist/render.js +14 -1
- package/package.json +8 -8
package/dist/cli.js
CHANGED
|
@@ -53,6 +53,7 @@ render options:
|
|
|
53
53
|
--state <name> render one machine state's timeline linearly
|
|
54
54
|
--force downgrade a trace hash mismatch to a warning
|
|
55
55
|
--captions <m> burn (default) | sidecar | off; burn/sidecar also write .srt/.vtt
|
|
56
|
+
--music <m> auto (default): mix a sibling *.music.timing.json bed, ducked under narration | off
|
|
56
57
|
|
|
57
58
|
dev options:
|
|
58
59
|
--record add a Record button; writes .trace.json sidecars next to the module
|
|
@@ -135,6 +136,7 @@ async function main() {
|
|
|
135
136
|
...flags.has("state") ? { state: flags.get("state") } : {},
|
|
136
137
|
...flags.has("force") ? { force: true } : {},
|
|
137
138
|
captions: parseCaptionsModeOrFail(flags.get("captions")),
|
|
139
|
+
music: flags.get("music") === "off" ? "off" : "auto",
|
|
138
140
|
onProgress: (n, total) => {
|
|
139
141
|
if (process.stderr.isTTY) {
|
|
140
142
|
if (n % 30 === 0 || n === total) process.stderr.write(`\rrendering ${n}/${total} frames`);
|
package/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ interface RenderOptions {
|
|
|
18
18
|
force?: boolean;
|
|
19
19
|
/** burn (default): captions render in-frame; sidecar/off hide the caption node. */
|
|
20
20
|
captions?: 'burn' | 'sidecar' | 'off';
|
|
21
|
+
/** auto (default): mix a sibling *.music.timing.json bed, ducked under narration. */
|
|
22
|
+
music?: 'auto' | 'off';
|
|
21
23
|
onProgress?: (frame: number, total: number) => void;
|
|
22
24
|
}
|
|
23
25
|
declare class SceneModuleError extends Error {
|
package/dist/music.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { o as resolveAssetPath } from "./audioMix.js";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { music, validateMusicTiming } from "@glissade/narrate";
|
|
4
|
+
//#region src/music.ts
|
|
5
|
+
/**
|
|
6
|
+
* Render-time music auto-mix (the narration parity): a committed
|
|
7
|
+
* `<module>.music.timing.json` with a `stem` field next to the scene mixes
|
|
8
|
+
* its bed automatically — and when a narration timing manifest ALSO sits
|
|
9
|
+
* there, the bed auto-ducks under the voice. Zero-config
|
|
10
|
+
* narrated-explainer-with-bed; everything derives from the two manifests.
|
|
11
|
+
*/
|
|
12
|
+
/** `<module>.music.timing.json`, when the scene has a music bed. */
|
|
13
|
+
function musicPathFor(modulePath) {
|
|
14
|
+
const candidate = modulePath.replace(/\.[jt]sx?$/, "") + ".music.timing.json";
|
|
15
|
+
return existsSync(candidate) ? candidate : null;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Build the bed clip from the manifests. The stem path stays relative — the
|
|
19
|
+
* manifest sits next to the module, and the mixer resolves clip urls against
|
|
20
|
+
* the module dir, so the bases coincide.
|
|
21
|
+
*/
|
|
22
|
+
function buildMusicClip(musicManifestPath, narrationTimingPath) {
|
|
23
|
+
const timing = JSON.parse(readFileSync(musicManifestPath, "utf8"));
|
|
24
|
+
validateMusicTiming(timing);
|
|
25
|
+
if (!timing.stem) return null;
|
|
26
|
+
const anchors = music(timing);
|
|
27
|
+
if (narrationTimingPath) {
|
|
28
|
+
const narration = JSON.parse(readFileSync(narrationTimingPath, "utf8"));
|
|
29
|
+
return {
|
|
30
|
+
clip: anchors.clip(void 0, { duckUnder: narration }),
|
|
31
|
+
note: `music bed '${timing.stem}'${timing.gainDb ? ` at ${timing.gainDb}dB` : ""}, ducked under narration`
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
clip: anchors.clip(),
|
|
36
|
+
note: `music bed '${timing.stem}'${timing.gainDb ? ` at ${timing.gainDb}dB` : ""}`
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* True when the timeline's audio already references the bed stem (same
|
|
41
|
+
* resolved file) — auto-mix must then SKIP, or the bed plays twice and adds
|
|
42
|
+
* +6dB of coherent doubling (measured downstream before this guard existed).
|
|
43
|
+
*/
|
|
44
|
+
function bedAlreadyReferenced(clips, bedUrl, modulePath) {
|
|
45
|
+
const bedPath = resolveAssetPath(bedUrl, modulePath);
|
|
46
|
+
return clips.some((c) => {
|
|
47
|
+
try {
|
|
48
|
+
return resolveAssetPath(c.asset.url, modulePath) === bedPath;
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
export { bedAlreadyReferenced, buildMusicClip, musicPathFor };
|
package/dist/render.js
CHANGED
|
@@ -130,8 +130,21 @@ async function render(opts) {
|
|
|
130
130
|
"+faststart"
|
|
131
131
|
]
|
|
132
132
|
];
|
|
133
|
+
const audioClips = [...compiled.audio];
|
|
134
|
+
if ((opts.music ?? "auto") === "auto") {
|
|
135
|
+
const { bedAlreadyReferenced, buildMusicClip, musicPathFor } = await import("./music.js");
|
|
136
|
+
const musicPath = musicPathFor(opts.modulePath);
|
|
137
|
+
if (musicPath) {
|
|
138
|
+
const bed = buildMusicClip(musicPath, timingPathFor(opts.modulePath));
|
|
139
|
+
if (bed) if (bedAlreadyReferenced(audioClips, bed.clip.asset.url, opts.modulePath)) process.stderr.write("note: music bed already in the timeline audio — auto-mix skipped\n");
|
|
140
|
+
else {
|
|
141
|
+
audioClips.push(bed.clip);
|
|
142
|
+
process.stderr.write(`note: auto-mixing ${bed.note}\n`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
133
146
|
const { planAudioMix } = await import("./audioMix.js").then((n) => n.r);
|
|
134
|
-
const mix = planAudioMix(
|
|
147
|
+
const mix = planAudioMix(audioClips, opts.modulePath, duration);
|
|
135
148
|
if (mix?.hasEasedGain) process.stderr.write("note: eased gain keys are approximated linearly in the FFmpeg mix\n");
|
|
136
149
|
const audioInputs = mix ? mix.inputs.flatMap((p) => ["-i", p]) : [];
|
|
137
150
|
const audioEnc = mix ? pickEncoder("audio", container) : null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glissade/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
4
4
|
"description": "glissade CLI: headless rendering via backend-skia (+ FFmpeg mux).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -20,13 +20,13 @@
|
|
|
20
20
|
"@napi-rs/canvas": "^0.1.65",
|
|
21
21
|
"esbuild": "^0.28.0",
|
|
22
22
|
"jiti": "^2.4.2",
|
|
23
|
-
"@glissade/backend-skia": "0.4.
|
|
24
|
-
"@glissade/core": "0.4.
|
|
25
|
-
"@glissade/interact": "0.4.
|
|
26
|
-
"@glissade/lottie": "0.4.
|
|
27
|
-
"@glissade/narrate": "0.4.
|
|
28
|
-
"@glissade/player": "0.4.
|
|
29
|
-
"@glissade/scene": "0.4.
|
|
23
|
+
"@glissade/backend-skia": "0.4.5",
|
|
24
|
+
"@glissade/core": "0.4.5",
|
|
25
|
+
"@glissade/interact": "0.4.5",
|
|
26
|
+
"@glissade/lottie": "0.4.5",
|
|
27
|
+
"@glissade/narrate": "0.4.5",
|
|
28
|
+
"@glissade/player": "0.4.5",
|
|
29
|
+
"@glissade/scene": "0.4.5"
|
|
30
30
|
},
|
|
31
31
|
"repository": {
|
|
32
32
|
"type": "git",
|