hyperframes 0.8.88 → 0.8.90

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.
Files changed (100) hide show
  1. package/dist/{add-6EIX4VO2.js → add-QCKUCHLO.js} +5 -5
  2. package/dist/{asset-BYFQENI4.js → asset-X5G7FLIA.js} +2 -2
  3. package/dist/{auth-CNYWEFA6.js → auth-HUTOY6UC.js} +1 -1
  4. package/dist/{autoUpdate-FN4SJBTE.js → autoUpdate-GVXUAVME.js} +2 -2
  5. package/dist/{backgroundChecks-ODS4VEB5.js → backgroundChecks-DP64TQBS.js} +3 -3
  6. package/dist/backgroundChecksWorker.js +3 -3
  7. package/dist/{beats-RTVNXBLK.js → beats-2BAVXPN4.js} +4 -4
  8. package/dist/{benchmark-KKPDM7PU.js → benchmark-AXEIQN5S.js} +5 -5
  9. package/dist/{browser-ADH34IWX.js → browser-GOL5PWIJ.js} +3 -3
  10. package/dist/{catalog-5NIRSEF7.js → catalog-UDASP3IK.js} +5 -5
  11. package/dist/{check-PRVHJKEC.js → check-IMDTJ2RH.js} +12 -12
  12. package/dist/{checkBrowser-CY3FMWQV.js → checkBrowser-OCN4Z4FT.js} +10 -10
  13. package/dist/{chunk-UP5KFGFZ.js → chunk-4HDAVJMN.js} +2 -2
  14. package/dist/{chunk-ZDAF2XYX.js → chunk-6QBQPTRJ.js} +2 -2
  15. package/dist/{chunk-DDXTUSZ3.js → chunk-77SYQXQN.js} +2 -2
  16. package/dist/{chunk-XDDHE52R.js → chunk-AAW5AF54.js} +2 -2
  17. package/dist/{chunk-MIYTJ6RX.js → chunk-CYG65ZCX.js} +12 -4
  18. package/dist/{chunk-BM25TRQX.js → chunk-DGPMOHUO.js} +3 -3
  19. package/dist/{chunk-6BPWGLCF.js → chunk-FPWCGP7T.js} +1 -1
  20. package/dist/{chunk-UX3MPYCP.js → chunk-GK6OCEV7.js} +2 -2
  21. package/dist/{chunk-ICWOWWC6.js → chunk-GSWG25MG.js} +1 -1
  22. package/dist/{chunk-RXRF3N5D.js → chunk-GVHRNT6C.js} +1 -1
  23. package/dist/{chunk-AV7ASJOK.js → chunk-KGDCJK72.js} +1 -1
  24. package/dist/{chunk-F6D4LJQG.js → chunk-KRXGOOP3.js} +1 -1
  25. package/dist/{chunk-VGS3ZUDB.js → chunk-OV4CCBM3.js} +1 -1
  26. package/dist/{chunk-4ARA5OTM.js → chunk-PQ3I6CAC.js} +1 -1
  27. package/dist/{chunk-I7MD2PC7.js → chunk-TQGNT45W.js} +1 -1
  28. package/dist/{chunk-RIEQ7B3Q.js → chunk-U325D5ZG.js} +1 -1
  29. package/dist/{chunk-Q2FGKCVO.js → chunk-WRZG77MO.js} +3 -3
  30. package/dist/{chunk-2KCIKXV3.js → chunk-YQ5SWLDS.js} +2 -2
  31. package/dist/{clean-ILEUS4I4.js → clean-F7NNEFPY.js} +4 -4
  32. package/dist/cli.js +44 -44
  33. package/dist/{cloud-YKZ6FMFO.js → cloud-GSFIMHVY.js} +4 -4
  34. package/dist/{compare-A7HXMQA2.js → compare-26UBKHZI.js} +7 -7
  35. package/dist/{component-GSYJQJH2.js → component-5QG3ILQS.js} +3 -3
  36. package/dist/{compositions-4K3YFH25.js → compositions-IFRNCQ5G.js} +5 -5
  37. package/dist/{delete-5R7CYMLA.js → delete-Z2L6F346.js} +2 -2
  38. package/dist/{doctor-HGGU5EPV.js → doctor-WLSP4XWV.js} +3 -3
  39. package/dist/{events-CT5MGBWG.js → events-FERXZJ74.js} +3 -3
  40. package/dist/{events-RQHXK2BA.js → events-SFI5FAYU.js} +2 -2
  41. package/dist/{feedback-S5TP76SF.js → feedback-6ZIZEN2J.js} +4 -4
  42. package/dist/{figma-JVTSWBQC.js → figma-OKUQYGPU.js} +3 -3
  43. package/dist/fontLocalizeCli.js +1 -1
  44. package/dist/{get-B6QNBIOY.js → get-JFVMDJ4K.js} +2 -2
  45. package/dist/{grade-compare-53VJQ6AR.js → grade-compare-TGVT536U.js} +7 -7
  46. package/dist/{help-74I7OBNZ.js → help-JHJAEATI.js} +1 -1
  47. package/dist/{history-JAICBG5J.js → history-IKW2UMZJ.js} +6 -6
  48. package/dist/hyperframes-player.global.js +1 -1
  49. package/dist/{info-DPLY233F.js → info-GOMASHNG.js} +5 -5
  50. package/dist/{init-GH473WVQ.js → init-VHK4JK2I.js} +6 -6
  51. package/dist/{inspect-ALZBOAVC.js → inspect-NR4M3D3O.js} +9 -9
  52. package/dist/{keyframes-3IHD4566.js → keyframes-4Z6UHXTR.js} +6 -6
  53. package/dist/{layout-DIE6IDNL.js → layout-F5STB2UB.js} +9 -9
  54. package/dist/{lint-BK7RM2VI.js → lint-XIKDK5NE.js} +6 -6
  55. package/dist/{list-IETRGPUL.js → list-PV5AVDEA.js} +2 -2
  56. package/dist/{login-6MOHVUEO.js → login-4JVYU5EP.js} +4 -4
  57. package/dist/{media-treatment-GLTDA25U.js → media-treatment-DQWRIN3R.js} +5 -5
  58. package/dist/{motionShot-J2Q4CGMA.js → motionShot-F7PMQYSA.js} +1 -1
  59. package/dist/{normalize-audio-225AHD54.js → normalize-audio-AVLP7MKR.js} +4 -4
  60. package/dist/{play-N2G5Z7OL.js → play-4GAAK5T4.js} +7 -7
  61. package/dist/{present-TVY4QRSP.js → present-WIRBZKR2.js} +7 -7
  62. package/dist/{preview-H5D4G6V6.js → preview-NUQRYW6F.js} +5 -5
  63. package/dist/{render-LDKZKOVA.js → render-LUZG33CD.js} +5 -5
  64. package/dist/{render-JDBG7432.js → render-MDIGXG66.js} +5 -5
  65. package/dist/skills/media-use/audio/references/bgm.md +2 -0
  66. package/dist/skills/media-use/audio/references/sfx.md +2 -3
  67. package/dist/skills/media-use/audio/references/tts.md +1 -1
  68. package/dist/skills/media-use/audio/scripts/audio.mjs +5 -2
  69. package/dist/skills/media-use/audio/scripts/audio.test.mjs +61 -8
  70. package/dist/skills/media-use/audio/scripts/gemini-pipeline.test.mjs +14 -3
  71. package/dist/skills/media-use/audio/scripts/lib/bgm.mjs +8 -4
  72. package/dist/skills/media-use/audio/scripts/lib/bgm.test.mjs +45 -1
  73. package/dist/skills/media-use/audio/scripts/lib/media-record.mjs +43 -2
  74. package/dist/skills/media-use/audio/scripts/lib/media-record.test.mjs +54 -2
  75. package/dist/skills/media-use/audio/scripts/lib/sfx.mjs +36 -17
  76. package/dist/skills/media-use/audio/scripts/lib/sfx.test.mjs +53 -1
  77. package/dist/skills/media-use/scripts/lib/manifest.mjs +3 -0
  78. package/dist/skills/media-use/scripts/resolve.mjs +4 -4
  79. package/dist/{skills-QWPGK2MN.js → skills-YZOZUC2R.js} +5 -5
  80. package/dist/{skillsUpdateCheck-JY23Q7EF.js → skillsUpdateCheck-V2MIGABK.js} +3 -3
  81. package/dist/{snapshot-HCLMYOWI.js → snapshot-DEKOD4F6.js} +7 -7
  82. package/dist/{staticProjectServer-VPVGLNV2.js → staticProjectServer-JSX6RZC3.js} +3 -3
  83. package/dist/studio/assets/{hyperframes-player-w3Bbv0kd.js → hyperframes-player-DDjER_ii.js} +1 -1
  84. package/dist/studio/assets/{index-DldknZPp.js → index-5SFeUXP8.js} +1 -1
  85. package/dist/studio/assets/{index-DE6cZYR5.js → index-BKE3_N4v.js} +180 -180
  86. package/dist/studio/assets/{index-B6PGJTzp.js → index-CVsCxnlz.js} +1 -1
  87. package/dist/studio/index.d.ts +39 -5
  88. package/dist/studio/index.html +1 -1
  89. package/dist/studio/index.js +156 -65
  90. package/dist/studio/index.js.map +1 -1
  91. package/dist/{studio-api-6YLFY6OJ.js → studio-api-JLNPQ3TO.js} +1 -1
  92. package/dist/{studioServer-LZDGVKYT.js → studioServer-ZCL433KM.js} +8 -5
  93. package/dist/{telemetry-Z3RLGWGK.js → telemetry-RPKG2UZI.js} +3 -3
  94. package/dist/{timeline-WOKBCYLM.js → timeline-DUIEKLUY.js} +6 -6
  95. package/dist/{tokens-RTP2IROL.js → tokens-KPHKWWZ6.js} +2 -2
  96. package/dist/{transcribe-ASVWZ3GP.js → transcribe-TNYWMNAY.js} +3 -3
  97. package/dist/{updateCheck-5LM5GJCF.js → updateCheck-PBVAHMGC.js} +2 -2
  98. package/dist/{upgrade-YWWAFHLF.js → upgrade-CFBD6XYU.js} +2 -2
  99. package/dist/{validate-EWNJ4VOI.js → validate-INYRDUJN.js} +6 -6
  100. package/package.json +1 -1
@@ -1,9 +1,10 @@
1
1
  import { strict as assert } from "node:assert";
2
2
  import { test } from "node:test";
3
- import { mkdtempSync, rmSync, existsSync, writeFileSync } from "node:fs";
3
+ import { mkdtempSync, mkdirSync, readFileSync, rmSync, existsSync, writeFileSync } from "node:fs";
4
4
  import { join, dirname } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
6
  import { fileURLToPath } from "node:url";
7
+ import { recordInManifest } from "./lib/media-record.mjs";
7
8
  import { resolveSfx } from "./lib/sfx.mjs";
8
9
 
9
10
  // Proves the relocated engine (skills/media-use/audio/) still resolves its
@@ -48,9 +49,12 @@ test("an unknown cue is reported, not fatal", async () => {
48
49
  }
49
50
  });
50
51
 
51
- test("a person's own file under a bundled name is not labelled as the library's", async () => {
52
+ test("a person's own file under a bundled name survives, and the cue gets the next free name", async () => {
52
53
  const dir = mkdtempSync(join(tmpdir(), "mu-audio-"));
53
54
  try {
55
+ const own = join(dir, "assets", "sfx", "whoosh.mp3");
56
+ mkdirSync(dirname(own), { recursive: true });
57
+ writeFileSync(own, "the person's own whoosh");
54
58
  const resolve = () =>
55
59
  resolveSfx({
56
60
  cues: [{ id: "1", name: "whoosh" }],
@@ -58,16 +62,65 @@ test("a person's own file under a bundled name is not labelled as the library's"
58
62
  hyperframesDir: dir,
59
63
  sfxLibDir,
60
64
  });
61
- const copied = await resolve();
62
- const reused = await resolve();
63
- writeFileSync(join(dir, copied.sfx[0].file), "the person's own whoosh");
64
- const own = await resolve();
65
65
 
66
+ const first = await resolve();
67
+ const second = await resolve();
68
+
69
+ assert.equal(readFileSync(own, "utf8"), "the person's own whoosh");
70
+ assert.deepEqual(
71
+ [first, second].map(({ sfx }) => [sfx[0].file, sfx[0].source]),
72
+ [
73
+ ["assets/sfx/whoosh-2.mp3", "local"],
74
+ ["assets/sfx/whoosh-2.mp3", "local"],
75
+ ],
76
+ );
66
77
  assert.deepEqual(
67
- [copied, reused, own].map(({ sfx }) => sfx[0].source),
68
- ["local", "local", "project"],
78
+ readFileSync(join(dir, "assets/sfx/whoosh-2.mp3")),
79
+ readFileSync(join(sfxLibDir, "whoosh.mp3")),
69
80
  );
70
81
  } finally {
71
82
  rmSync(dir, { recursive: true, force: true });
72
83
  }
73
84
  });
85
+
86
+ test("two effects never share a file when one's name is taken by the person, run after run", async () => {
87
+ const dir = mkdtempSync(join(tmpdir(), "mu-audio-"));
88
+ try {
89
+ mkdirSync(join(dir, "assets", "sfx"), { recursive: true });
90
+ writeFileSync(join(dir, "assets", "sfx", "glitch.mp3"), "the person's own glitch");
91
+ const realFetch = globalThis.fetch;
92
+ globalThis.fetch = async (url) => {
93
+ const query = new URL(url).searchParams.get("query");
94
+ if (query)
95
+ return Response.json({ data: [{ audio_url: `https://sound.test/${query}`, score: 0.6 }] });
96
+ return new Response(`bytes of ${new URL(url).pathname}`);
97
+ };
98
+ const run = async (names) => {
99
+ const { sfx } = await resolveSfx({
100
+ cues: names.map((name, index) => ({ id: String(index), name })),
101
+ heygenOK: true,
102
+ headers: {},
103
+ hyperframesDir: dir,
104
+ sfxLibDir,
105
+ });
106
+ const files = sfx.map(({ file }) => file);
107
+ recordInManifest(
108
+ dir,
109
+ [...new Set(files)].map((path) => ({ path, type: "sfx", source: "search" })),
110
+ );
111
+ return files;
112
+ };
113
+ try {
114
+ assert.deepEqual(await run(["glitch"]), ["assets/sfx/glitch-2.mp3"]);
115
+ assert.deepEqual(await run(["glitch", "glitch 2", "glitch"]), [
116
+ "assets/sfx/glitch-2.mp3",
117
+ "assets/sfx/glitch-2-2.mp3",
118
+ "assets/sfx/glitch-2.mp3",
119
+ ]);
120
+ } finally {
121
+ globalThis.fetch = realFetch;
122
+ }
123
+ } finally {
124
+ rmSync(dir, { recursive: true, force: true });
125
+ }
126
+ });
@@ -9,9 +9,15 @@ import { spawnSync } from "node:child_process";
9
9
  // external audio tools are fixtures. This is not a live synthesis/render test.
10
10
  for (const expired of [false, true]) {
11
11
  for (const only of ["tts", "tts,bgm,sfx"]) {
12
+ // With expired credentials the person already has an assets/voice/intro.wav, which the engine must keep.
13
+ const voicePath = expired ? "assets/voice/intro-2.wav" : "assets/voice/intro.wav";
12
14
  test(`Gemini engine returns caption metadata with ${expired ? "expired" : "absent"} HeyGen credentials (${only})`, (t) => {
13
15
  const dir = mkdtempSync(join(tmpdir(), "hf-gemini-pipeline-"));
14
16
  t.after(() => rmSync(dir, { recursive: true, force: true }));
17
+ if (expired) {
18
+ mkdirSync(join(dir, "assets/voice"), { recursive: true });
19
+ writeFileSync(join(dir, "assets/voice/intro.wav"), "the person's own intro");
20
+ }
15
21
  const config = join(dir, "heygen");
16
22
  mkdirSync(config);
17
23
  if (expired)
@@ -48,7 +54,7 @@ const assert = require('node:assert/strict');
48
54
  const fs = require('node:fs');
49
55
  const path = require('node:path');
50
56
  const args = process.argv.slice(2);
51
- assert.deepEqual(args.slice(0, 3), ['hyperframes', 'transcribe', 'assets/voice/intro.wav']);
57
+ assert.deepEqual(args.slice(0, 3), ['hyperframes', 'transcribe', '${voicePath}']);
52
58
  assert.ok(fs.existsSync(args[2]));
53
59
  assert.equal(args[args.indexOf('--model')+1], 'small.en');
54
60
  fs.writeFileSync(path.join(args[args.indexOf('--dir')+1], 'transcript.json'), JSON.stringify([
@@ -99,7 +105,7 @@ fs.writeFileSync(path.join(args[args.indexOf('--dir')+1], 'transcript.json'), JS
99
105
  assert.deepEqual(meta.voices, [
100
106
  {
101
107
  id: "intro",
102
- path: "assets/voice/intro.wav",
108
+ path: voicePath,
103
109
  duration_s: 1.25,
104
110
  words: [
105
111
  { id: "w0", text: "Hello", start: 0.1, end: 0.4 },
@@ -113,8 +119,13 @@ fs.writeFileSync(path.join(args[args.indexOf('--dir')+1], 'transcript.json'), JS
113
119
  manifest
114
120
  .map((line) => JSON.parse(line))
115
121
  .map(({ path, type, source }) => [path, type, source]),
116
- [["assets/voice/intro.wav", "voice", "generated"]],
122
+ [[voicePath, "voice", "generated"]],
117
123
  );
124
+ if (expired)
125
+ assert.equal(
126
+ readFileSync(join(dir, "assets/voice/intro.wav"), "utf8"),
127
+ "the person's own intro",
128
+ );
118
129
  });
119
130
  }
120
131
  }
@@ -12,9 +12,10 @@
12
12
  // Missing/failed BGM never blocks a render.
13
13
 
14
14
  import { spawn, spawnSync } from "node:child_process";
15
- import { existsSync, mkdirSync, openSync, closeSync } from "node:fs";
15
+ import { existsSync, mkdirSync, openSync, closeSync, rmSync } from "node:fs";
16
16
  import { join } from "node:path";
17
17
  import { downloadTo, searchSounds } from "./heygen.mjs";
18
+ import { agentWritePath } from "./media-record.mjs";
18
19
  import { pythonInvocation } from "./python.mjs";
19
20
 
20
21
  const r3 = (x) => Number(x.toFixed(3));
@@ -45,12 +46,12 @@ function pipInstall(deps) {
45
46
  }
46
47
 
47
48
  // ── retrieval (HeyGen music library) ──────────────────────────────────────────
48
- export async function retrieveBgm({ query, headers, hyperframesDir, hasVoice }) {
49
+ export async function retrieveBgm({ query, headers, hyperframesDir, hasVoice, anomalies }) {
49
50
  const q = query || "calm cinematic underscore";
50
51
  const results = await searchSounds(q, "music", headers, { limit: 5 });
51
52
  if (!results.length) return null;
52
53
  const top = results[0];
53
- const rel = "assets/bgm/track.mp3";
54
+ const rel = agentWritePath(hyperframesDir, "assets/bgm/track.mp3", { anomalies });
54
55
  await downloadTo(top.audio_url, join(hyperframesDir, rel));
55
56
  return {
56
57
  path: rel,
@@ -113,8 +114,9 @@ export function generateBgmDetached({
113
114
  lyriaRecipe,
114
115
  seedSeconds = 28,
115
116
  hasVoice,
117
+ anomalies,
116
118
  }) {
117
- const rel = "assets/bgm/track.wav";
119
+ const rel = agentWritePath(hyperframesDir, "assets/bgm/track.wav", { anomalies });
118
120
  const abs = join(hyperframesDir, rel);
119
121
  mkdirSync(join(hyperframesDir, "assets", "bgm"), { recursive: true });
120
122
  const log = join(hyperframesDir, "assets", "bgm", `bgm-${Date.now()}.log`);
@@ -141,6 +143,7 @@ export function generateBgmDetached({
141
143
  "--prompt",
142
144
  prompt,
143
145
  ]);
146
+ rmSync(abs, { force: true }); // wait-bgm takes any file here as the finished track
144
147
  const proc = spawn(cmd, args, { detached: true, stdio: ["ignore", fd, fd] });
145
148
  proc.unref();
146
149
  closeSync(fd);
@@ -159,6 +162,7 @@ export function generateBgmDetached({
159
162
  const loops = targetS > seedS ? Math.ceil(targetS / seedS) : 1;
160
163
  const script = musicgenScript({ prompt, abs, targetS, seedS });
161
164
  const { cmd, args } = pythonInvocation(["-c", script]);
165
+ rmSync(abs, { force: true });
162
166
  const proc = spawn(cmd, args, { detached: true, stdio: ["ignore", fd, fd] });
163
167
  proc.unref();
164
168
  closeSync(fd);
@@ -1,6 +1,15 @@
1
1
  import { test } from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { BGM_BED_VOLUME, BGM_SILENT_VOLUME, bgmDefaultVolume } from "./bgm.mjs";
3
+ import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { appendRecord } from "../../../scripts/lib/manifest.mjs";
7
+ import {
8
+ BGM_BED_VOLUME,
9
+ BGM_SILENT_VOLUME,
10
+ bgmDefaultVolume,
11
+ generateBgmDetached,
12
+ } from "./bgm.mjs";
4
13
 
5
14
  // Regression: narrated pipelines used to ship BGM at 0.8 (≈ -2 dB), ~16 dB
6
15
  // hotter than a music bed under a voice should be. The default under narration
@@ -28,3 +37,38 @@ test("the narrated default is well below the voice (≈ 0 dBFS)", () => {
28
37
  `bed should sit ≥16 dB under the voice, got ${separation.toFixed(1)} dB`,
29
38
  );
30
39
  });
40
+
41
+ test(
42
+ "generating music again clears the engine's old track, so waiting cannot mistake it for the new one",
43
+ { skip: process.platform === "win32" && "the fake python is a shell script" },
44
+ (t) => {
45
+ const dir = mkdtempSync(join(tmpdir(), "hf-bgm-"));
46
+ const path = process.env.PATH;
47
+ t.after(() => {
48
+ process.env.PATH = path;
49
+ rmSync(dir, { recursive: true, force: true });
50
+ });
51
+ mkdirSync(join(dir, "bin"));
52
+ for (const name of ["python3", "python"])
53
+ writeFileSync(join(dir, "bin", name), "#!/bin/sh\nexit 0\n", { mode: 0o755 });
54
+ process.env.PATH = `${join(dir, "bin")}:${path}`;
55
+ mkdirSync(join(dir, "assets/bgm"), { recursive: true });
56
+ writeFileSync(join(dir, "assets/bgm/track.wav"), "last run's track");
57
+ appendRecord(dir, {
58
+ id: "bgm_001",
59
+ type: "bgm",
60
+ path: "assets/bgm/track.wav",
61
+ source: "generated",
62
+ });
63
+
64
+ const gen = generateBgmDetached({
65
+ prompt: "calm",
66
+ durationS: 5,
67
+ hyperframesDir: dir,
68
+ anomalies: [],
69
+ });
70
+
71
+ assert.equal(gen.path, "assets/bgm/track.wav");
72
+ assert.equal(existsSync(join(dir, "assets/bgm/track.wav")), false);
73
+ },
74
+ );
@@ -1,6 +1,48 @@
1
- import { recordInPlace } from "../../../scripts/lib/manifest.mjs";
1
+ import { existsSync } from "node:fs";
2
+ import { extname, join } from "node:path";
3
+ import { AGENT_SOURCES, latestRecordFor, recordInPlace } from "../../../scripts/lib/manifest.mjs";
2
4
  import { regenerateIndex } from "../../../scripts/lib/index-gen.mjs";
3
5
 
6
+ /**
7
+ * Where the engine may write `rel`: there, unless another file of this run has `taken` it or the file there is
8
+ * the person's (not `reusable` and not recorded as agent-made). Then the first free `name-2.ext`, with an anomaly.
9
+ */
10
+ export function agentWritePath(
11
+ hyperframesDir,
12
+ rel,
13
+ { anomalies, taken = new Set(), reusable = () => false },
14
+ ) {
15
+ const free = (path) =>
16
+ !taken.has(path) &&
17
+ (!existsSync(join(hyperframesDir, path)) ||
18
+ reusable(path) ||
19
+ AGENT_SOURCES.includes(latestRecordFor(hyperframesDir, path)?.source));
20
+ if (free(rel)) return rel;
21
+ const ext = extname(rel);
22
+ const stem = rel.slice(0, rel.length - ext.length);
23
+ let n = 2;
24
+ while (!free(`${stem}-${n}${ext}`)) n++;
25
+ const path = `${stem}-${n}${ext}`;
26
+ const why = taken.has(rel)
27
+ ? "another file of this run goes there"
28
+ : "the file there is yours (the media manifest does not record it as made by the engine)";
29
+ const note = `${rel}: kept, because ${why}; writing ${path} instead (audio_meta.json has the path used)`;
30
+ if (!anomalies.includes(note)) anomalies.push(note);
31
+ return path;
32
+ }
33
+
34
+ /** Each spoken line's file, picked before lines synthesize concurrently so two never land on one free name. */
35
+ export function voicePaths(hyperframesDir, lines, anomalies) {
36
+ const taken = new Set();
37
+ const paths = new Map();
38
+ for (const line of lines.filter((l) => String(l.text ?? "").trim())) {
39
+ const rel = agentWritePath(hyperframesDir, `assets/voice/${line.id}.wav`, { anomalies, taken });
40
+ taken.add(rel);
41
+ paths.set(String(line.id), rel);
42
+ }
43
+ return paths;
44
+ }
45
+
4
46
  const SFX_SOURCES = { heygen: "search", local: "bundled" };
5
47
 
6
48
  // The files one engine run wrote, each with how it was made, for the project's media manifest.
@@ -32,7 +74,6 @@ export function writtenAssets({ only, lines, voices, ttsProvider, bgm, bgmFields
32
74
  if (only.has("sfx")) {
33
75
  for (const cue of new Map(sfx.map((entry) => [entry.file, entry])).values()) {
34
76
  const source = SFX_SOURCES[cue.source];
35
- if (!source) continue;
36
77
  const provider = source === "search" ? "heygen" : "bundled.sfx";
37
78
  assets.push({
38
79
  path: cue.file,
@@ -4,7 +4,7 @@ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "nod
4
4
  import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
6
  import { findByPrompt, readManifest } from "../../../scripts/lib/manifest.mjs";
7
- import { recordInManifest, writtenAssets } from "./media-record.mjs";
7
+ import { agentWritePath, recordInManifest, voicePaths, writtenAssets } from "./media-record.mjs";
8
8
 
9
9
  const noBgm = { bgm: null, bgmFields: { bgm_pending: false } };
10
10
 
@@ -98,7 +98,6 @@ test("music and sound effects are marked by where they came from", () => {
98
98
  { file: "assets/sfx/whoosh.mp3", name: "whoosh", source: "local" },
99
99
  { file: "assets/sfx/whoosh.mp3", name: "whoosh", source: "local" },
100
100
  { file: "assets/sfx/glass.mp3", name: "glass", source: "heygen" },
101
- { file: "assets/sfx/pop.mp3", name: "pop", source: "project" },
102
101
  ],
103
102
  });
104
103
 
@@ -139,6 +138,59 @@ test("a file that cannot be recorded becomes an anomaly, not a failure", (t) =>
139
138
  assert.match(anomalies[0], /^assets\/voice\/01\.wav: not recorded in the media manifest/);
140
139
  });
141
140
 
141
+ test("the engine writes over only its own files, else the next free name", (t) => {
142
+ const dir = project(t);
143
+ mkdirSync(join(dir, "assets/sfx"), { recursive: true });
144
+ const names = ["mine", "made", "adopted", "twice", "twice-2", "kept", "kept-2"];
145
+ for (const name of names) writeFileSync(join(dir, `assets/sfx/${name}.mp3`), name);
146
+ for (const path of ["assets/sfx/made.mp3", "assets/sfx/kept-2.mp3"])
147
+ recordInManifest(dir, [{ path, type: "sfx", source: "search" }]);
148
+ recordInManifest(dir, [{ path: "assets/sfx/adopted.mp3", type: "sfx", source: "existing" }]);
149
+ const anomalies = [];
150
+ const at = (name, reusable) =>
151
+ agentWritePath(dir, `assets/sfx/${name}.mp3`, { anomalies, reusable });
152
+
153
+ assert.equal(at("new"), "assets/sfx/new.mp3");
154
+ assert.equal(at("made"), "assets/sfx/made.mp3");
155
+ assert.equal(
156
+ at("mine", (path) => path === "assets/sfx/mine.mp3"),
157
+ "assets/sfx/mine.mp3",
158
+ );
159
+ assert.deepEqual(anomalies, []);
160
+ assert.equal(at("mine"), "assets/sfx/mine-2.mp3");
161
+ assert.equal(at("adopted"), "assets/sfx/adopted-2.mp3");
162
+ assert.equal(at("twice"), "assets/sfx/twice-3.mp3");
163
+ assert.equal(at("kept"), "assets/sfx/kept-2.mp3");
164
+ assert.equal(anomalies.length, 4);
165
+ assert.match(
166
+ anomalies[0],
167
+ /^assets\/sfx\/mine\.mp3: kept, .* writing assets\/sfx\/mine-2\.mp3 instead/,
168
+ );
169
+ });
170
+
171
+ test("two spoken lines never share a file when one's name is taken by the person", (t) => {
172
+ const dir = project(t);
173
+ mkdirSync(join(dir, "assets/voice"), { recursive: true });
174
+ writeFileSync(join(dir, "assets/voice/hook.wav"), "the person's own hook");
175
+ const anomalies = [];
176
+
177
+ const paths = voicePaths(
178
+ dir,
179
+ [
180
+ { id: "hook", text: "First" },
181
+ { id: "hook-2", text: "Second" },
182
+ { id: "blank", text: " " },
183
+ ],
184
+ anomalies,
185
+ );
186
+
187
+ assert.deepEqual(Object.fromEntries(paths), {
188
+ hook: "assets/voice/hook-2.wav",
189
+ "hook-2": "assets/voice/hook-2-2.wav",
190
+ });
191
+ assert.equal(anomalies.length, 2);
192
+ });
193
+
142
194
  test("a record never takes the id of a download still in flight", (t) => {
143
195
  const dir = project(t);
144
196
  mkdirSync(join(dir, ".media/audio/bgm"), { recursive: true });
@@ -15,6 +15,8 @@
15
15
  import { copyFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
16
16
  import { join } from "node:path";
17
17
  import { downloadTo, searchSounds } from "./heygen.mjs";
18
+ import { latestRecordFor } from "../../../scripts/lib/manifest.mjs";
19
+ import { agentWritePath } from "./media-record.mjs";
18
20
 
19
21
  const SFX_VOLUME = 0.35;
20
22
  const slug = (s) =>
@@ -31,6 +33,8 @@ export async function resolveSfx({ cues, heygenOK, headers, hyperframesDir, sfxL
31
33
  const sfx = [];
32
34
  const anomalies = [];
33
35
  const destDir = join(hyperframesDir, "assets", "sfx");
36
+ // Each effect's file this run: one effect named twice gets one file, and two effects never share one.
37
+ const fileFor = new Map();
34
38
 
35
39
  // Dedupe identical (id,name) cues — the same effect named twice in one line
36
40
  // downloads/copies once.
@@ -57,8 +61,14 @@ export async function resolveSfx({ cues, heygenOK, headers, hyperframesDir, sfxL
57
61
  continue;
58
62
  }
59
63
  const top = results[0];
60
- const file = `assets/sfx/${slug(name)}.mp3`;
64
+ const file =
65
+ fileFor.get(slug(name)) ??
66
+ agentWritePath(hyperframesDir, `assets/sfx/${slug(name)}.mp3`, {
67
+ anomalies,
68
+ taken: new Set(fileFor.values()),
69
+ });
61
70
  await downloadTo(top.audio_url, join(hyperframesDir, file));
71
+ fileFor.set(slug(name), file);
62
72
  sfx.push({
63
73
  id,
64
74
  name,
@@ -111,32 +121,41 @@ export async function resolveSfx({ cues, heygenOK, headers, hyperframesDir, sfxL
111
121
  continue;
112
122
  }
113
123
  const src = join(sfxLibDir, hit.file);
114
- const destRel = `assets/sfx/${hit.file}`;
124
+ const library = existsSync(src) ? readFileSync(src) : null;
125
+ const isLibraryCopy = (rel) =>
126
+ library &&
127
+ existsSync(join(hyperframesDir, rel)) &&
128
+ readFileSync(join(hyperframesDir, rel)).equals(library);
129
+ const destRel =
130
+ fileFor.get(hit.file) ??
131
+ agentWritePath(hyperframesDir, `assets/sfx/${hit.file}`, {
132
+ anomalies,
133
+ taken: new Set(fileFor.values()),
134
+ // An unrecorded copy of the library file is one an engine run made before the manifest had it.
135
+ reusable: (rel) => !latestRecordFor(hyperframesDir, rel) && isLibraryCopy(rel),
136
+ });
115
137
  const dest = join(hyperframesDir, destRel);
116
138
  // The bundled library may be incomplete: some installs of the skill ship
117
139
  // manifest.json without the actual mp3s. Pushing an sfx entry that points at
118
140
  // a file we never copied produces a dangling reference that silently drops
119
141
  // downstream ("not on disk"). Surface it as a loud anomaly and skip the cue
120
142
  // instead, so the audio_meta never references a missing file.
121
- const reused = existsSync(dest);
122
- if (!reused) {
123
- if (!existsSync(src)) {
124
- anomalies.push(
125
- `sfx "${name}" (id ${id}): bundled file ${hit.file} missing from the offline ` +
126
- `library (${sfxLibDir}) — skipped. Reinstall the media-use skill to ` +
127
- `restore assets/sfx/*.mp3, or configure a HeyGen credential for retrieval.`,
128
- );
129
- continue;
130
- }
131
- copyFileSync(src, dest);
132
- }
133
- // A file already there that is not the library's copy is the person's own.
134
- const own = reused && !(existsSync(src) && readFileSync(dest).equals(readFileSync(src)));
143
+ if (library) {
144
+ if (!isLibraryCopy(destRel)) copyFileSync(src, dest);
145
+ } else if (!existsSync(dest)) {
146
+ anomalies.push(
147
+ `sfx "${name}" (id ${id}): bundled file ${hit.file} missing from the offline ` +
148
+ `library (${sfxLibDir}) — skipped. Reinstall the media-use skill to ` +
149
+ `restore assets/sfx/*.mp3, or configure a HeyGen credential for retrieval.`,
150
+ );
151
+ continue;
152
+ } // else the engine's earlier copy at dest stands in for the file this install lacks
153
+ fileFor.set(hit.file, destRel);
135
154
  sfx.push({
136
155
  id,
137
156
  name,
138
157
  file: destRel,
139
- source: own ? "project" : "local",
158
+ source: "local",
140
159
  offset_s: 0,
141
160
  duration_s: r3(hit.duration),
142
161
  volume: SFX_VOLUME,
@@ -1,8 +1,9 @@
1
1
  import { test } from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { mkdtempSync, mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs";
3
+ import { mkdtempSync, mkdirSync, writeFileSync, existsSync, readFileSync, rmSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
+ import { appendRecord } from "../../../scripts/lib/manifest.mjs";
6
7
  import { resolveSfx } from "./sfx.mjs";
7
8
 
8
9
  // Offline (no HeyGen) SFX resolution: the bundled library may ship manifest.json
@@ -67,3 +68,54 @@ test("offline: a matched-but-missing bundled file yields an anomaly and NO dangl
67
68
  assert.ok(!existsSync(join(projDir, "assets/sfx/whoosh.mp3")), "nothing copied");
68
69
  });
69
70
  });
71
+
72
+ const whooshLibrary = (libDir, withFile) => {
73
+ writeFileSync(
74
+ join(libDir, "manifest.json"),
75
+ JSON.stringify({ whoosh: { file: "whoosh.mp3", duration: 0.8 } }),
76
+ );
77
+ if (withFile) writeFileSync(join(libDir, "whoosh.mp3"), "ID3-library-bytes");
78
+ };
79
+ const record = (projDir, source) =>
80
+ appendRecord(projDir, { id: "sfx_001", type: "sfx", path: "assets/sfx/whoosh.mp3", source });
81
+ const offline = (libDir, projDir) =>
82
+ resolveSfx({
83
+ cues: [{ id: "s1", name: "whoosh" }],
84
+ heygenOK: false,
85
+ hyperframesDir: projDir,
86
+ sfxLibDir: libDir,
87
+ });
88
+
89
+ test("offline: the engine's earlier copy stands in for a library file this install lacks", async () => {
90
+ await withDirs(async ({ libDir, projDir }) => {
91
+ whooshLibrary(libDir, false);
92
+ mkdirSync(join(projDir, "assets/sfx"), { recursive: true });
93
+ writeFileSync(join(projDir, "assets/sfx/whoosh.mp3"), "engine copy");
94
+ record(projDir, "bundled");
95
+
96
+ const { sfx, anomalies } = await offline(libDir, projDir);
97
+
98
+ assert.deepEqual(
99
+ sfx.map(({ file }) => file),
100
+ ["assets/sfx/whoosh.mp3"],
101
+ );
102
+ assert.deepEqual(anomalies, []);
103
+ });
104
+ });
105
+
106
+ test("offline: a person's file with the library's bytes stays theirs once recorded as their own", async () => {
107
+ await withDirs(async ({ libDir, projDir }) => {
108
+ whooshLibrary(libDir, true);
109
+ mkdirSync(join(projDir, "assets/sfx"), { recursive: true });
110
+ writeFileSync(join(projDir, "assets/sfx/whoosh.mp3"), "ID3-library-bytes");
111
+ record(projDir, "existing");
112
+
113
+ const { sfx } = await offline(libDir, projDir);
114
+
115
+ assert.deepEqual(
116
+ sfx.map(({ file }) => file),
117
+ ["assets/sfx/whoosh-2.mp3"],
118
+ );
119
+ assert.equal(readFileSync(join(projDir, "assets/sfx/whoosh.mp3"), "utf8"), "ID3-library-bytes");
120
+ });
121
+ });
@@ -79,6 +79,9 @@ export function appendRecord(projectDir, record) {
79
79
  appendFileSync(p, line);
80
80
  }
81
81
 
82
+ /** Sources that mean the agent made or fetched the file; any other file is the person's own. */
83
+ export const AGENT_SOURCES = ["generated", "search", "bundled"];
84
+
82
85
  /** The record a path has now: the manifest only appends, so the last one for a path wins. */
83
86
  export function latestRecordFor(projectDir, path) {
84
87
  return readManifest(projectDir).findLast((record) => record.path === path);
@@ -5,6 +5,7 @@ import { existsSync, statSync, writeFileSync, renameSync, rmSync, realpathSync }
5
5
  import { resolve, join, extname, basename, relative, isAbsolute, sep } from "node:path";
6
6
  import { parseArgs } from "node:util";
7
7
  import {
8
+ AGENT_SOURCES,
8
9
  appendRecord,
9
10
  latestRecordFor,
10
11
  recordInPlace,
@@ -60,7 +61,6 @@ import {
60
61
  } from "./lib/local-media-search.mjs";
61
62
 
62
63
  const INGEST_TYPES = listTypes();
63
- const RECORDED_SOURCES = ["generated", "search", "bundled"];
64
64
  const DEFAULT_EXT = {
65
65
  bgm: ".wav",
66
66
  sfx: ".mp3",
@@ -130,7 +130,7 @@ Options:
130
130
  --reuse <sha> Import a specific global-cache asset (by content sha/prefix,
131
131
  from --candidates) into this project
132
132
  --from <file> Freeze a local file or direct public URL (ingest)
133
- --source <how> With --from: how the file was made (${RECORDED_SOURCES.join(" | ")}).
133
+ --source <how> With --from: how the file was made (${AGENT_SOURCES.join(" | ")}).
134
134
  A file already inside the project is then recorded where it is
135
135
  --params <json> Build an explicit parametric LUT (lut/grade only)
136
136
  --for <media> Analyze a local image/video and add measured grade adjust
@@ -918,8 +918,8 @@ async function ingest(src) {
918
918
  console.error(`error: refusing to ingest a 0-byte file: ${src}`);
919
919
  process.exit(2);
920
920
  }
921
- if (args.source && !RECORDED_SOURCES.includes(args.source)) {
922
- console.error(`error: --source takes one of: ${RECORDED_SOURCES.join(", ")}`);
921
+ if (args.source && !AGENT_SOURCES.includes(args.source)) {
922
+ console.error(`error: --source takes one of: ${AGENT_SOURCES.join(", ")}`);
923
923
  process.exit(2);
924
924
  }
925
925
  if (args.source && (type === "lut" || type === "grade")) {
@@ -9,8 +9,8 @@ import {
9
9
  } from "./chunk-WMQNIQDJ.js";
10
10
  import {
11
11
  trackSkillsInstallSkipped
12
- } from "./chunk-AV7ASJOK.js";
13
- import "./chunk-4ARA5OTM.js";
12
+ } from "./chunk-KGDCJK72.js";
13
+ import "./chunk-PQ3I6CAC.js";
14
14
  import {
15
15
  diag
16
16
  } from "./chunk-VWG7OQKU.js";
@@ -50,15 +50,15 @@ import {
50
50
  isCoreSkill,
51
51
  presentSkills,
52
52
  pruneOrphanedLockEntries
53
- } from "./chunk-VGS3ZUDB.js";
53
+ } from "./chunk-OV4CCBM3.js";
54
54
  import {
55
55
  withMeta
56
- } from "./chunk-RIEQ7B3Q.js";
56
+ } from "./chunk-U325D5ZG.js";
57
57
  import "./chunk-K4WIOZFP.js";
58
58
  import "./chunk-XUXGIA7H.js";
59
59
  import "./chunk-WERLJGIC.js";
60
60
  import "./chunk-ISN5GRYI.js";
61
- import "./chunk-DDXTUSZ3.js";
61
+ import "./chunk-77SYQXQN.js";
62
62
  import "./chunk-KNU5COPB.js";
63
63
  import {
64
64
  buildNpxCommand
@@ -9,13 +9,13 @@ import {
9
9
  printSkillsUpdateNotice,
10
10
  refreshSkillsCache,
11
11
  skillsCheckDue
12
- } from "./chunk-VGS3ZUDB.js";
13
- import "./chunk-RIEQ7B3Q.js";
12
+ } from "./chunk-OV4CCBM3.js";
13
+ import "./chunk-U325D5ZG.js";
14
14
  import "./chunk-K4WIOZFP.js";
15
15
  import "./chunk-XUXGIA7H.js";
16
16
  import "./chunk-WERLJGIC.js";
17
17
  import "./chunk-ISN5GRYI.js";
18
- import "./chunk-DDXTUSZ3.js";
18
+ import "./chunk-77SYQXQN.js";
19
19
  import "./chunk-KNU5COPB.js";
20
20
  import "./chunk-M2WBOQ2Q.js";
21
21
  export {