Setting the file. One moment. Audio Duck · Media Use · heygen-com/hyperframes · Skills Docs⋯
scripts/11 files
scripts/audio-duck.mjs
scripts/audio-duck.mjs
JavaScript·113 lines·4 KB
:
args
}
=
parseArgs
({
10 options: {
11 meta: { type: "string" },
12 target: { type: "string" },
13 duck: { type: "string", default: "0.25" },
14 attack: { type: "string", default: "0.15" },
15 release: { type: "string", default: "0.4" },
16 "merge-gap": { type: "string", default: "0.6" },
17 sequential: { type: "boolean", default: false },
18 gap: { type: "string", default: "0" },
19 offsets: { type: "string" },
20 composition: { type: "string" },
21 json: { type: "boolean", default: false },
22 help: { type: "boolean", short: "h", default: false },
23 },
24 strict: true,
25});
26
27if (args.help) {
28 console.log(`media-use audio-duck — generate a volume ducking lane (data-automation)
29
30Usage:
31 node audio-duck.mjs --meta audio_meta.json --target "#bgm"
32
33Options:
34 --meta audio_meta.json or JSON word transcript
35 --target GSAP selector for the background audio element
36 --duck Duck multiplier (default: 0.25)
37 --attack Duck-in duration seconds (default: 0.15)
38 --release Restore duration seconds (default: 0.4)
39 --merge-gap Bridge speech gaps smaller than this many seconds (default: 0.6)
40 --sequential Place multi-line meta back to back at composition time
41 --gap Extra seconds between sequential lines (default: 0)
42 --offsets Explicit placement, "l1=0,l2=3.4" (voice id = start seconds)
43 --composition Read target data-volume and data-start from this HTML file
44 --json Output { spans, keyframes, lane }
45 --help, -h Show this help`);
46 process.exit(0);
47}
48
49try {
50 run();
51 await track("media_use_duck", { sequential: !!args.sequential });
52} catch (err) {
53 if (args.json) console.log(JSON.stringify({ ok: false, error: err.message }));
54 else console.error(`error: ${err.message}`);
55 process.exit(1);
56}
57
58function run() {
59 if (!args.meta || !args.target) throw new Error("--meta and --target are required");
60 const meta = JSON.parse(readFileSync(resolve(args.meta), "utf8"));
61 const target = args.target;
62 const { baseVolume, clipStart } = readTargetAttrs(args.composition, target);
63 const offsets = args.offsets
64 ? Object.fromEntries(
65 args.offsets.split(",").map((pair) => {
66 const [id, t] = pair.split("=");
67 return [id.trim(), Number(t)];
68 }),
69 )
70 : undefined;
71 const spans = speechSpans(meta, {
72 mergeGap: Number(args["merge-gap"]),
73 sequential: args.sequential,
74 gap: Number(args.gap),
75 offsets,
76 });
77 const keyframes = duckKeyframes(spans, {
78 duck: Number(args.duck),
79 attack: Number(args.attack),
80 release: Number(args.release),
81 baseVolume,
82 });
83
84 const lane = duckLane(keyframes, { clipStart, baseVolume });
85
86 if (args.json) {
87 console.log(JSON.stringify({ spans, keyframes, lane }));
88 return;
89 }
90
91 console.log(`<!-- auto-duck: ${target} under narration; add to its <audio> element -->`);
92 console.log(`data-automation='${JSON.stringify(lane)}'`);
93}
94
95function readTargetAttrs(composition, target) {
96 if (!composition || !target.startsWith("#")) return { baseVolume: 1, clipStart: 0 };
97 const id = target.slice(1);
98 const html = readFileSync(resolve(composition), "utf8");
99 // ponytail: regex is enough here because this only reads one attribute from
100 // one user-authored composition element, not arbitrary HTML.
101 const tag = html.match(new RegExp(`<[^>]*\\bid=["']${escapeRegExp(id)}["'][^>]*>`, "i"))?.[0];
102 const attr = (name) => Number(tag?.match(new RegExp(`\\b${name}=["']([^"']+)["']`, "i"))?.[1]);
103 const volume = attr("data-volume");
104 const start = attr("data-start");
105 return {
106 baseVolume: Number.isFinite(volume) ? volume : 1,
107 clipStart: Number.isFinite(start) ? start : 0,
108 };
109}
110
111function escapeRegExp(value) {
112 return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
113}