hyperframes 0.8.58 → 0.8.59

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 (182) hide show
  1. package/dist/cli.js +4483 -3217
  2. package/dist/docs/data-attributes.md +2 -1
  3. package/dist/fontLocalizeCli.js +73 -12
  4. package/dist/hyperframe-runtime.js +25 -25
  5. package/dist/hyperframe.manifest.json +1 -1
  6. package/dist/hyperframe.runtime.iife.js +25 -25
  7. package/dist/hyperframes-player.global.js +3 -3
  8. package/dist/registry/localSemantic.js +548 -0
  9. package/dist/skills/hyperframes-cli/SKILL.md +2 -2
  10. package/dist/skills/media-use/SKILL.md +96 -0
  11. package/dist/skills/media-use/audio/assets/sfx/CREDITS.md +35 -0
  12. package/dist/skills/media-use/audio/assets/sfx/chime.mp3 +0 -0
  13. package/dist/skills/media-use/audio/assets/sfx/click-soft.mp3 +0 -0
  14. package/dist/skills/media-use/audio/assets/sfx/click.mp3 +0 -0
  15. package/dist/skills/media-use/audio/assets/sfx/error.mp3 +0 -0
  16. package/dist/skills/media-use/audio/assets/sfx/glitch-1.mp3 +0 -0
  17. package/dist/skills/media-use/audio/assets/sfx/glitch-2.mp3 +0 -0
  18. package/dist/skills/media-use/audio/assets/sfx/glitch-3.mp3 +0 -0
  19. package/dist/skills/media-use/audio/assets/sfx/impact-bass-1.mp3 +0 -0
  20. package/dist/skills/media-use/audio/assets/sfx/impact-bass-2.mp3 +0 -0
  21. package/dist/skills/media-use/audio/assets/sfx/key-press.mp3 +0 -0
  22. package/dist/skills/media-use/audio/assets/sfx/manifest.json +97 -0
  23. package/dist/skills/media-use/audio/assets/sfx/notification.mp3 +0 -0
  24. package/dist/skills/media-use/audio/assets/sfx/ping.mp3 +0 -0
  25. package/dist/skills/media-use/audio/assets/sfx/pop.mp3 +0 -0
  26. package/dist/skills/media-use/audio/assets/sfx/riser.mp3 +0 -0
  27. package/dist/skills/media-use/audio/assets/sfx/sparkle.mp3 +0 -0
  28. package/dist/skills/media-use/audio/assets/sfx/typing.mp3 +0 -0
  29. package/dist/skills/media-use/audio/assets/sfx/whoosh-cinematic.mp3 +0 -0
  30. package/dist/skills/media-use/audio/assets/sfx/whoosh-short.mp3 +0 -0
  31. package/dist/skills/media-use/audio/assets/sfx/whoosh.mp3 +0 -0
  32. package/dist/skills/media-use/audio/references/bgm.md +74 -0
  33. package/dist/skills/media-use/audio/references/captions/authoring.md +163 -0
  34. package/dist/skills/media-use/audio/references/captions/motion.md +87 -0
  35. package/dist/skills/media-use/audio/references/captions/transcript-handling.md +97 -0
  36. package/dist/skills/media-use/audio/references/remove-background.md +143 -0
  37. package/dist/skills/media-use/audio/references/requirements.md +29 -0
  38. package/dist/skills/media-use/audio/references/sfx.md +42 -0
  39. package/dist/skills/media-use/audio/references/transcribe.md +52 -0
  40. package/dist/skills/media-use/audio/references/tts-to-captions.md +26 -0
  41. package/dist/skills/media-use/audio/references/tts.md +165 -0
  42. package/dist/skills/media-use/audio/scripts/audio.mjs +295 -0
  43. package/dist/skills/media-use/audio/scripts/audio.test.mjs +49 -0
  44. package/dist/skills/media-use/audio/scripts/heygen-tts.mjs +121 -0
  45. package/dist/skills/media-use/audio/scripts/lib/audio-meta.mjs +39 -0
  46. package/dist/skills/media-use/audio/scripts/lib/audio-meta.test.mjs +136 -0
  47. package/dist/skills/media-use/audio/scripts/lib/bgm.mjs +257 -0
  48. package/dist/skills/media-use/audio/scripts/lib/bgm.test.mjs +30 -0
  49. package/dist/skills/media-use/audio/scripts/lib/concurrency.mjs +14 -0
  50. package/dist/skills/media-use/audio/scripts/lib/concurrency.test.mjs +41 -0
  51. package/dist/skills/media-use/audio/scripts/lib/heygen.mjs +156 -0
  52. package/dist/skills/media-use/audio/scripts/lib/heygen.test.mjs +103 -0
  53. package/dist/skills/media-use/audio/scripts/lib/python.mjs +63 -0
  54. package/dist/skills/media-use/audio/scripts/lib/python.test.mjs +68 -0
  55. package/dist/skills/media-use/audio/scripts/lib/sfx.mjs +143 -0
  56. package/dist/skills/media-use/audio/scripts/lib/sfx.test.mjs +69 -0
  57. package/dist/skills/media-use/audio/scripts/lib/tts.mjs +379 -0
  58. package/dist/skills/media-use/audio/scripts/lib/tts.spawn.test.mjs +158 -0
  59. package/dist/skills/media-use/audio/scripts/lib/tts.test.mjs +157 -0
  60. package/dist/skills/media-use/audio/scripts/lyria-recipe.py +136 -0
  61. package/dist/skills/media-use/audio/scripts/wait-bgm.mjs +171 -0
  62. package/dist/skills/media-use/audio/scripts/wait-bgm.test.mjs +65 -0
  63. package/dist/skills/media-use/luts/README.md +28 -0
  64. package/dist/skills/media-use/luts/index.json +57 -0
  65. package/dist/skills/media-use/references/audio.md +19 -0
  66. package/dist/skills/media-use/references/grading.md +160 -0
  67. package/dist/skills/media-use/references/media-treatment-recipes.md +929 -0
  68. package/dist/skills/media-use/references/media-treatments.md +202 -0
  69. package/dist/skills/media-use/references/memory.md +36 -0
  70. package/dist/skills/media-use/references/meta.md +61 -0
  71. package/dist/skills/media-use/references/operations.md +325 -0
  72. package/dist/skills/media-use/references/resolve.md +150 -0
  73. package/dist/skills/media-use/references/setup-providers.md +80 -0
  74. package/dist/skills/media-use/references/telemetry-dashboard.md +49 -0
  75. package/dist/skills/media-use/registry/localSemantic.js +548 -0
  76. package/dist/skills/media-use/scripts/audio-duck.mjs +113 -0
  77. package/dist/skills/media-use/scripts/compatibility.test.mjs +27 -0
  78. package/dist/skills/media-use/scripts/dither.mjs +328 -0
  79. package/dist/skills/media-use/scripts/dither.test.mjs +176 -0
  80. package/dist/skills/media-use/scripts/eval.mjs +369 -0
  81. package/dist/skills/media-use/scripts/lib/adopt.mjs +128 -0
  82. package/dist/skills/media-use/scripts/lib/adopt.test.mjs +95 -0
  83. package/dist/skills/media-use/scripts/lib/bgm-provider.mjs +20 -0
  84. package/dist/skills/media-use/scripts/lib/brand-provider.mjs +59 -0
  85. package/dist/skills/media-use/scripts/lib/bundled-sfx-provider.mjs +119 -0
  86. package/dist/skills/media-use/scripts/lib/bundled-sfx-provider.test.mjs +139 -0
  87. package/dist/skills/media-use/scripts/lib/cache.mjs +150 -0
  88. package/dist/skills/media-use/scripts/lib/candidates.mjs +91 -0
  89. package/dist/skills/media-use/scripts/lib/candidates.test.mjs +155 -0
  90. package/dist/skills/media-use/scripts/lib/codex-provider.mjs +161 -0
  91. package/dist/skills/media-use/scripts/lib/codex-provider.test.mjs +34 -0
  92. package/dist/skills/media-use/scripts/lib/coverage.test.mjs +132 -0
  93. package/dist/skills/media-use/scripts/lib/cube-build.mjs +181 -0
  94. package/dist/skills/media-use/scripts/lib/cube-build.test.mjs +80 -0
  95. package/dist/skills/media-use/scripts/lib/cube-validate.mjs +198 -0
  96. package/dist/skills/media-use/scripts/lib/cube-validate.test.mjs +125 -0
  97. package/dist/skills/media-use/scripts/lib/cutlist.mjs +184 -0
  98. package/dist/skills/media-use/scripts/lib/cutlist.test.mjs +162 -0
  99. package/dist/skills/media-use/scripts/lib/duck.mjs +95 -0
  100. package/dist/skills/media-use/scripts/lib/duck.test.mjs +153 -0
  101. package/dist/skills/media-use/scripts/lib/error-diffusion.mjs +230 -0
  102. package/dist/skills/media-use/scripts/lib/error-diffusion.test.mjs +102 -0
  103. package/dist/skills/media-use/scripts/lib/freeze.mjs +62 -0
  104. package/dist/skills/media-use/scripts/lib/freeze.test.mjs +46 -0
  105. package/dist/skills/media-use/scripts/lib/grade-analyzer.mjs +265 -0
  106. package/dist/skills/media-use/scripts/lib/grade-analyzer.test.mjs +285 -0
  107. package/dist/skills/media-use/scripts/lib/heygen-cli.mjs +191 -0
  108. package/dist/skills/media-use/scripts/lib/heygen-cli.test.mjs +301 -0
  109. package/dist/skills/media-use/scripts/lib/heygen-search.mjs +45 -0
  110. package/dist/skills/media-use/scripts/lib/heygen-search.test.mjs +49 -0
  111. package/dist/skills/media-use/scripts/lib/heygen-video-provider.mjs +138 -0
  112. package/dist/skills/media-use/scripts/lib/heygen-video-provider.test.mjs +435 -0
  113. package/dist/skills/media-use/scripts/lib/image-provider.mjs +44 -0
  114. package/dist/skills/media-use/scripts/lib/index-gen.mjs +63 -0
  115. package/dist/skills/media-use/scripts/lib/local-media-search.mjs +32 -0
  116. package/dist/skills/media-use/scripts/lib/local-models.mjs +318 -0
  117. package/dist/skills/media-use/scripts/lib/local-models.test.mjs +220 -0
  118. package/dist/skills/media-use/scripts/lib/local-run.mjs +78 -0
  119. package/dist/skills/media-use/scripts/lib/local-run.test.mjs +121 -0
  120. package/dist/skills/media-use/scripts/lib/logo-provider.mjs +223 -0
  121. package/dist/skills/media-use/scripts/lib/logo-provider.test.mjs +154 -0
  122. package/dist/skills/media-use/scripts/lib/ltx-video-provider.mjs +94 -0
  123. package/dist/skills/media-use/scripts/lib/ltx-video-provider.test.mjs +316 -0
  124. package/dist/skills/media-use/scripts/lib/lut-preset-provider.mjs +271 -0
  125. package/dist/skills/media-use/scripts/lib/lut-preset-provider.test.mjs +199 -0
  126. package/dist/skills/media-use/scripts/lib/manifest.mjs +229 -0
  127. package/dist/skills/media-use/scripts/lib/manifest.test.mjs +388 -0
  128. package/dist/skills/media-use/scripts/lib/match.mjs +47 -0
  129. package/dist/skills/media-use/scripts/lib/media-fetch.mjs +71 -0
  130. package/dist/skills/media-use/scripts/lib/media-fetch.test.mjs +169 -0
  131. package/dist/skills/media-use/scripts/lib/media-search.d.mts +9 -0
  132. package/dist/skills/media-use/scripts/lib/media-search.mjs +107 -0
  133. package/dist/skills/media-use/scripts/lib/mflux-provider.mjs +128 -0
  134. package/dist/skills/media-use/scripts/lib/mflux-provider.test.mjs +174 -0
  135. package/dist/skills/media-use/scripts/lib/misses.mjs +49 -0
  136. package/dist/skills/media-use/scripts/lib/misses.test.mjs +78 -0
  137. package/dist/skills/media-use/scripts/lib/npx-sync.mjs +31 -0
  138. package/dist/skills/media-use/scripts/lib/npx-sync.test.mjs +50 -0
  139. package/dist/skills/media-use/scripts/lib/parakeet-words.mjs +27 -0
  140. package/dist/skills/media-use/scripts/lib/parakeet-words.test.mjs +44 -0
  141. package/dist/skills/media-use/scripts/lib/prefs-store.mjs +180 -0
  142. package/dist/skills/media-use/scripts/lib/prefs-store.test.mjs +189 -0
  143. package/dist/skills/media-use/scripts/lib/probe.mjs +39 -0
  144. package/dist/skills/media-use/scripts/lib/probe.test.mjs +31 -0
  145. package/dist/skills/media-use/scripts/lib/providers.mjs +5 -0
  146. package/dist/skills/media-use/scripts/lib/recipe-store.mjs +367 -0
  147. package/dist/skills/media-use/scripts/lib/recipe-store.test.mjs +367 -0
  148. package/dist/skills/media-use/scripts/lib/registry.mjs +218 -0
  149. package/dist/skills/media-use/scripts/lib/registry.test.mjs +262 -0
  150. package/dist/skills/media-use/scripts/lib/search.mjs +24 -0
  151. package/dist/skills/media-use/scripts/lib/search.test.mjs +32 -0
  152. package/dist/skills/media-use/scripts/lib/sfx-provider.mjs +23 -0
  153. package/dist/skills/media-use/scripts/lib/specs.mjs +132 -0
  154. package/dist/skills/media-use/scripts/lib/specs.test.mjs +157 -0
  155. package/dist/skills/media-use/scripts/lib/stats.mjs +117 -0
  156. package/dist/skills/media-use/scripts/lib/stats.test.mjs +165 -0
  157. package/dist/skills/media-use/scripts/lib/telemetry.mjs +193 -0
  158. package/dist/skills/media-use/scripts/lib/telemetry.test.mjs +373 -0
  159. package/dist/skills/media-use/scripts/lib/tts-local-provider.mjs +92 -0
  160. package/dist/skills/media-use/scripts/lib/tts-local-provider.test.mjs +72 -0
  161. package/dist/skills/media-use/scripts/lib/usage.mjs +57 -0
  162. package/dist/skills/media-use/scripts/lib/usage.test.mjs +54 -0
  163. package/dist/skills/media-use/scripts/lib/voice-provider.mjs +57 -0
  164. package/dist/skills/media-use/scripts/lib/voice-provider.test.mjs +46 -0
  165. package/dist/skills/media-use/scripts/lib/words.mjs +18 -0
  166. package/dist/skills/media-use/scripts/prefs.mjs +70 -0
  167. package/dist/skills/media-use/scripts/recipe.mjs +103 -0
  168. package/dist/skills/media-use/scripts/resolve.mjs +1287 -0
  169. package/dist/skills/media-use/scripts/transcribe.mjs +180 -0
  170. package/dist/skills/media-use/scripts/transcript-cut.mjs +239 -0
  171. package/dist/skills/registry/localSemantic.js +548 -0
  172. package/dist/studio/assets/{hyperframes-player-zGyOcwwh.js → hyperframes-player-BBub3ZdI.js} +3 -3
  173. package/dist/studio/assets/{index-ZJaRCDYV.js → index-BxZ5Cw5w.js} +1 -1
  174. package/dist/studio/assets/{index-BcE2OSVA.js → index-CI7WON-E.js} +1 -1
  175. package/dist/studio/assets/{index-d8ferjRc.js → index-D1KNPijK.js} +190 -190
  176. package/dist/studio/assets/{index-DKJpHYYd.css → index-DZBHG9TW.css} +1 -1
  177. package/dist/studio/index.d.ts +548 -8
  178. package/dist/studio/index.html +2 -2
  179. package/dist/studio/index.js +21596 -20725
  180. package/dist/studio/index.js.map +1 -1
  181. package/dist/studio/styles.css +1 -1
  182. package/package.json +1 -1
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Standalone mirror of packages/core/src/colorLuts.ts. media-use cannot import
4
+ // the TypeScript source at runtime, so cube-validate.test.mjs mirrors core
5
+ // parser cases to catch drift in accepted .cube files before freezing them.
6
+
7
+ import { readFileSync } from "node:fs";
8
+ import { resolve as resolvePath } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+
11
+ export const DEFAULT_MAX_CUBE_LUT_SIZE = 64;
12
+
13
+ const DEFAULT_DOMAIN_MIN = [0, 0, 0];
14
+ const DEFAULT_DOMAIN_MAX = [1, 1, 1];
15
+
16
+ class CubeValidateError extends Error {
17
+ constructor(message, lineNumber = null) {
18
+ super(lineNumber == null ? message : `${message} at line ${lineNumber}`);
19
+ this.name = "CubeValidateError";
20
+ this.lineNumber = lineNumber;
21
+ }
22
+ }
23
+
24
+ function stripComment(line) {
25
+ let inQuote = false;
26
+ for (let i = 0; i < line.length; i++) {
27
+ const char = line[i];
28
+ if (char === '"') inQuote = !inQuote;
29
+ if (char === "#" && !inQuote) return line.slice(0, i);
30
+ }
31
+ return line;
32
+ }
33
+
34
+ function parseFiniteNumber(value, lineNumber) {
35
+ const parsed = Number(value);
36
+ if (!Number.isFinite(parsed)) {
37
+ throw new CubeValidateError(`Invalid number "${value}"`, lineNumber);
38
+ }
39
+ return parsed;
40
+ }
41
+
42
+ function parseVec3(parts, keyword, lineNumber) {
43
+ if (parts.length !== 3) {
44
+ throw new CubeValidateError(`${keyword} expects three numbers`, lineNumber);
45
+ }
46
+ return [
47
+ parseFiniteNumber(parts[0], lineNumber),
48
+ parseFiniteNumber(parts[1], lineNumber),
49
+ parseFiniteNumber(parts[2], lineNumber),
50
+ ];
51
+ }
52
+
53
+ function parseSize(value, keyword, lineNumber) {
54
+ if (!value) throw new CubeValidateError(`${keyword} expects a size`, lineNumber);
55
+ const parsed = Number(value);
56
+ if (!Number.isInteger(parsed) || parsed < 2) {
57
+ throw new CubeValidateError(`${keyword} must be an integer greater than 1`, lineNumber);
58
+ }
59
+ return parsed;
60
+ }
61
+
62
+ function validateDomain(domainMin, domainMax) {
63
+ if (
64
+ domainMax[0] <= domainMin[0] ||
65
+ domainMax[1] <= domainMin[1] ||
66
+ domainMax[2] <= domainMin[2]
67
+ ) {
68
+ throw new CubeValidateError("DOMAIN_MAX values must be greater than DOMAIN_MIN values");
69
+ }
70
+ }
71
+
72
+ function isNumericDataLine(token) {
73
+ return /^[+-]?(?:\d|\.\d)/.test(token);
74
+ }
75
+
76
+ function parseCube(input, options = {}) {
77
+ const maxSize = options.maxSize ?? DEFAULT_MAX_CUBE_LUT_SIZE;
78
+ let domainMin = DEFAULT_DOMAIN_MIN;
79
+ let domainMax = DEFAULT_DOMAIN_MAX;
80
+ let lut1dSize = null;
81
+ let lut3dSize = null;
82
+ let rows = 0;
83
+
84
+ const lines = String(input)
85
+ .replace(/^\uFEFF/, "")
86
+ .split(/\r?\n/);
87
+ for (let i = 0; i < lines.length; i++) {
88
+ const lineNumber = i + 1;
89
+ const line = stripComment(lines[i] ?? "").trim();
90
+ if (!line) continue;
91
+ const parts = line.split(/\s+/);
92
+ const keyword = (parts[0] ?? "").toUpperCase();
93
+ const rest = parts.slice(1);
94
+
95
+ if (keyword === "TITLE") continue;
96
+ if (keyword === "DOMAIN_MIN") {
97
+ domainMin = parseVec3(rest, keyword, lineNumber);
98
+ continue;
99
+ }
100
+ if (keyword === "DOMAIN_MAX") {
101
+ domainMax = parseVec3(rest, keyword, lineNumber);
102
+ continue;
103
+ }
104
+ if (keyword === "LUT_3D_INPUT_RANGE") {
105
+ if (rest.length !== 2) {
106
+ throw new CubeValidateError(`${keyword} expects two numbers`, lineNumber);
107
+ }
108
+ const min = parseFiniteNumber(rest[0], lineNumber);
109
+ const max = parseFiniteNumber(rest[1], lineNumber);
110
+ if (max <= min) {
111
+ throw new CubeValidateError("LUT_3D_INPUT_RANGE max must exceed min", lineNumber);
112
+ }
113
+ domainMin = [min, min, min];
114
+ domainMax = [max, max, max];
115
+ continue;
116
+ }
117
+ if (keyword === "LUT_1D_SIZE") {
118
+ lut1dSize = parseSize(rest[0], keyword, lineNumber);
119
+ continue;
120
+ }
121
+ if (keyword === "LUT_3D_SIZE") {
122
+ lut3dSize = parseSize(rest[0], keyword, lineNumber);
123
+ if (lut3dSize > maxSize) {
124
+ throw new CubeValidateError(`LUT_3D_SIZE ${lut3dSize} exceeds max ${maxSize}`, lineNumber);
125
+ }
126
+ continue;
127
+ }
128
+
129
+ if (!isNumericDataLine(keyword)) {
130
+ if (keyword.startsWith("LUT_")) {
131
+ throw new CubeValidateError(`Unsupported cube keyword ${keyword}`, lineNumber);
132
+ }
133
+ continue;
134
+ }
135
+ if (!lut3dSize) {
136
+ if (lut1dSize) {
137
+ throw new CubeValidateError("1D cube LUTs are not supported yet", lineNumber);
138
+ }
139
+ throw new CubeValidateError("LUT data appears before LUT_3D_SIZE", lineNumber);
140
+ }
141
+ if (parts.length !== 3) {
142
+ throw new CubeValidateError("LUT data rows must contain three numbers", lineNumber);
143
+ }
144
+ parseFiniteNumber(parts[0], lineNumber);
145
+ parseFiniteNumber(parts[1], lineNumber);
146
+ parseFiniteNumber(parts[2], lineNumber);
147
+ rows++;
148
+ }
149
+
150
+ if (lut1dSize && lut3dSize) {
151
+ throw new CubeValidateError("Mixed 1D and 3D cube LUTs are not supported yet");
152
+ }
153
+ if (!lut3dSize) {
154
+ if (lut1dSize) throw new CubeValidateError("1D cube LUTs are not supported yet");
155
+ throw new CubeValidateError("Missing LUT_3D_SIZE");
156
+ }
157
+ validateDomain(domainMin, domainMax);
158
+
159
+ const expectedRows = lut3dSize * lut3dSize * lut3dSize;
160
+ if (rows !== expectedRows) {
161
+ throw new CubeValidateError(
162
+ `Expected ${expectedRows} LUT rows for size ${lut3dSize}, found ${rows}`,
163
+ );
164
+ }
165
+
166
+ return { size: lut3dSize };
167
+ }
168
+
169
+ export function validateCube(input, options = {}) {
170
+ try {
171
+ const parsed = parseCube(input, options);
172
+ return { ok: true, size: parsed.size };
173
+ } catch (err) {
174
+ return { ok: false, error: err.message };
175
+ }
176
+ }
177
+
178
+ export function validateCubeFile(filePath, options = {}) {
179
+ return validateCube(readFileSync(filePath, "utf8"), options);
180
+ }
181
+
182
+ function main(argv) {
183
+ const file = argv[2];
184
+ if (!file) {
185
+ console.error("usage: cube-validate.mjs <file.cube>");
186
+ process.exit(2);
187
+ }
188
+ const result = validateCubeFile(file);
189
+ if (!result.ok) {
190
+ console.error(`error: ${result.error}`);
191
+ process.exit(1);
192
+ }
193
+ console.log(`ok: LUT_3D_SIZE ${result.size}`);
194
+ }
195
+
196
+ if (process.argv[1] && resolvePath(process.argv[1]) === fileURLToPath(import.meta.url)) {
197
+ main(process.argv);
198
+ }
@@ -0,0 +1,125 @@
1
+ import { strict as assert } from "node:assert";
2
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { execFileSync } from "node:child_process";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { test } from "node:test";
7
+ import { validateCube } from "./cube-validate.mjs";
8
+
9
+ const IDENTITY_2 = `
10
+ # comment
11
+ TITLE "Identity 2"
12
+ DOMAIN_MIN 0 0 0
13
+ DOMAIN_MAX 1 1 1
14
+ LUT_3D_SIZE 2
15
+ 0 0 0
16
+ 1 0 0
17
+ 0 1 0
18
+ 1 1 0
19
+ 0 0 1
20
+ 1 0 1
21
+ 0 1 1
22
+ 1 1 1
23
+ `;
24
+
25
+ test("accepts a valid minimal 3D cube LUT", () => {
26
+ const result = validateCube(IDENTITY_2);
27
+ assert.deepEqual(result, { ok: true, size: 2 });
28
+ });
29
+
30
+ test("rejects oversize LUTs with the core parser message", () => {
31
+ const result = validateCube("LUT_3D_SIZE 65", { maxSize: 64 });
32
+ assert.equal(result.ok, false);
33
+ assert.match(result.error, /LUT_3D_SIZE 65 exceeds max 64/);
34
+ });
35
+
36
+ test("rejects data rows before LUT_3D_SIZE", () => {
37
+ const result = validateCube("0 0 0\nLUT_3D_SIZE 2");
38
+ assert.equal(result.ok, false);
39
+ assert.match(result.error, /LUT data appears before LUT_3D_SIZE/);
40
+ });
41
+
42
+ test("rejects missing LUT_3D_SIZE", () => {
43
+ const result = validateCube('TITLE "No Size"');
44
+ assert.equal(result.ok, false);
45
+ assert.match(result.error, /Missing LUT_3D_SIZE/);
46
+ });
47
+
48
+ test("rejects row count mismatches with the core parser message", () => {
49
+ const result = validateCube("LUT_3D_SIZE 2\n0 0 0");
50
+ assert.equal(result.ok, false);
51
+ assert.match(result.error, /Expected 8 LUT rows/);
52
+ });
53
+
54
+ test("rejects inverted domains", () => {
55
+ const result = validateCube(`
56
+ DOMAIN_MIN 0 0 0
57
+ DOMAIN_MAX 1 0 1
58
+ LUT_3D_SIZE 2
59
+ 0 0 0
60
+ 1 0 0
61
+ 0 1 0
62
+ 1 1 0
63
+ 0 0 1
64
+ 1 0 1
65
+ 0 1 1
66
+ 1 1 1
67
+ `);
68
+ assert.equal(result.ok, false);
69
+ assert.match(result.error, /DOMAIN_MAX values must be greater than DOMAIN_MIN values/);
70
+ });
71
+
72
+ test("rejects unsupported 1D and mixed cube LUTs", () => {
73
+ const oneD = validateCube("LUT_1D_SIZE 2\n0 0 0\n1 1 1");
74
+ assert.equal(oneD.ok, false);
75
+ assert.match(oneD.error, /1D cube LUTs are not supported yet/);
76
+
77
+ const mixed = validateCube(`
78
+ LUT_1D_SIZE 2
79
+ LUT_3D_SIZE 2
80
+ 0 0 0
81
+ 1 0 0
82
+ 0 1 0
83
+ 1 1 0
84
+ 0 0 1
85
+ 1 0 1
86
+ 0 1 1
87
+ 1 1 1
88
+ `);
89
+ assert.equal(mixed.ok, false);
90
+ assert.match(mixed.error, /Mixed 1D and 3D cube LUTs are not supported yet/);
91
+ });
92
+
93
+ test("CLI exits zero for valid files and non-zero for invalid files", () => {
94
+ const dir = mkdtempSync(join(tmpdir(), "mu-cube-validate-"));
95
+ try {
96
+ const valid = join(dir, "valid.cube");
97
+ const invalid = join(dir, "invalid.cube");
98
+ writeFileSync(valid, IDENTITY_2);
99
+ writeFileSync(invalid, "LUT_3D_SIZE 65");
100
+
101
+ const out = execFileSync(
102
+ process.execPath,
103
+ [new URL("./cube-validate.mjs", import.meta.url).pathname, valid],
104
+ {
105
+ encoding: "utf8",
106
+ },
107
+ );
108
+ assert.match(out, /ok: LUT_3D_SIZE 2/);
109
+
110
+ assert.throws(
111
+ () =>
112
+ execFileSync(
113
+ process.execPath,
114
+ [new URL("./cube-validate.mjs", import.meta.url).pathname, invalid],
115
+ {
116
+ encoding: "utf8",
117
+ stdio: "pipe",
118
+ },
119
+ ),
120
+ (err) => err.status === 1 && String(err.stderr).includes("exceeds max 64"),
121
+ );
122
+ } finally {
123
+ rmSync(dir, { recursive: true, force: true });
124
+ }
125
+ });
@@ -0,0 +1,184 @@
1
+ import { normalizeWords } from "./words.mjs";
2
+
3
+ const MIN_SEGMENT_SECONDS = 0.2;
4
+ const SILENCE_PAD_SECONDS = 0.15;
5
+
6
+ export function compileCutList(transcript, opts = {}) {
7
+ const words = normalizeWords(transcript);
8
+ if (opts.keep != null && hasRemovalSource(opts)) {
9
+ throw new Error("--keep is mutually exclusive with removal options");
10
+ }
11
+
12
+ if (opts.keep != null) {
13
+ const duration = durationFrom(words, opts);
14
+ const ranges = parseTimeRanges(opts.keep);
15
+ return finalizeKept(duration != null ? clampRanges(ranges, duration) : ranges);
16
+ }
17
+
18
+ const duration = durationFrom(words, opts);
19
+ if (!duration) return [];
20
+
21
+ const removals = [
22
+ ...parseTimeRanges(opts.remove),
23
+ ...wordIndexRanges(words, opts.removeWords),
24
+ ...fillerRanges(words, opts.removeFillers),
25
+ ...silenceRanges(words, opts.cutSilence),
26
+ ];
27
+ const mergedRemovals = mergeRanges(clampRanges(removals, duration));
28
+ return finalizeKept(invertRanges(mergedRemovals, duration));
29
+ }
30
+
31
+ function hasRemovalSource(opts) {
32
+ return (
33
+ opts.remove != null ||
34
+ opts.removeWords != null ||
35
+ opts.removeFillers != null ||
36
+ opts.cutSilence != null
37
+ );
38
+ }
39
+
40
+ function durationFrom(words, opts) {
41
+ const explicit = Number(opts.duration ?? opts.totalDuration);
42
+ if (Number.isFinite(explicit) && explicit > 0) return explicit;
43
+ const last = words.at(-1);
44
+ return last && Number.isFinite(last.end) && last.end > 0 ? last.end : null;
45
+ }
46
+
47
+ function parseTimeRanges(value) {
48
+ if (value == null || value === false || value === "") return [];
49
+ if (typeof value === "string") {
50
+ return value
51
+ .split(",")
52
+ .map((part) => part.trim())
53
+ .filter(Boolean)
54
+ .map(parseRangeString);
55
+ }
56
+ if (!Array.isArray(value)) throw new Error("range list must be a string or array");
57
+ return value.map((range) => {
58
+ if (Array.isArray(range)) return cleanRange(Number(range[0]), Number(range[1]));
59
+ return cleanRange(Number(range?.start), Number(range?.end));
60
+ });
61
+ }
62
+
63
+ function parseRangeString(value) {
64
+ const match = value.match(/^([0-9]*\.?[0-9]+)\s*-\s*([0-9]*\.?[0-9]+)$/);
65
+ if (!match) throw new Error(`invalid range: ${value}`);
66
+ return cleanRange(Number(match[1]), Number(match[2]));
67
+ }
68
+
69
+ function cleanRange(start, end) {
70
+ if (!Number.isFinite(start) || !Number.isFinite(end)) {
71
+ throw new Error("range start/end must be finite numbers");
72
+ }
73
+ if (end < start) throw new Error(`range end ${end} is before start ${start}`);
74
+ return { start, end };
75
+ }
76
+
77
+ function wordIndexRanges(words, value) {
78
+ if (value == null || value === false || value === "") return [];
79
+ const ranges = typeof value === "string" ? value.split(",") : value;
80
+ if (!Array.isArray(ranges)) throw new Error("--remove-words must be a string or array");
81
+ return ranges
82
+ .map((range) => (typeof range === "string" ? range.trim() : range))
83
+ .filter(Boolean)
84
+ .map((range) => {
85
+ const [first, last = first] =
86
+ typeof range === "string" ? range.split("-").map((n) => n.trim()) : range;
87
+ const startIndex = Number(first);
88
+ const endIndex = Number(last);
89
+ if (!Number.isInteger(startIndex) || !Number.isInteger(endIndex)) {
90
+ throw new Error(`invalid word range: ${range}`);
91
+ }
92
+ if (startIndex < 0 || endIndex < startIndex || endIndex >= words.length) {
93
+ throw new Error(`word range out of bounds: ${range}`);
94
+ }
95
+ return { start: words[startIndex].start, end: words[endIndex].end };
96
+ });
97
+ }
98
+
99
+ function fillerRanges(words, value) {
100
+ if (value == null || value === false || value === "") return [];
101
+ const fillers = Array.isArray(value)
102
+ ? value
103
+ : String(value)
104
+ .split(",")
105
+ .map((s) => s.trim());
106
+ const set = new Set(fillers.filter(Boolean).map(bareToken));
107
+ if (set.size === 0) return [];
108
+ // Whisper emits words with attached punctuation and arbitrary case
109
+ // ("UM," / "Um."), so compare bare tokens.
110
+ return words
111
+ .filter((word) => set.has(bareToken(word.text)))
112
+ .map((word) => ({ start: word.start, end: word.end }));
113
+ }
114
+
115
+ function bareToken(text) {
116
+ return String(text)
117
+ .toLowerCase()
118
+ .replace(/^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu, "");
119
+ }
120
+
121
+ function silenceRanges(words, value) {
122
+ if (value == null || value === false || value === "") return [];
123
+ const threshold = Number(value);
124
+ if (!Number.isFinite(threshold) || threshold <= 0) {
125
+ throw new Error("--cut-silence must be a positive number");
126
+ }
127
+ const ranges = [];
128
+ for (let i = 0; i < words.length - 1; i++) {
129
+ const current = words[i];
130
+ const next = words[i + 1];
131
+ const gap = next.start - current.end;
132
+ if (gap <= threshold) continue;
133
+ const start = current.end + SILENCE_PAD_SECONDS;
134
+ const end = next.start - SILENCE_PAD_SECONDS;
135
+ if (end > start) ranges.push({ start, end });
136
+ }
137
+ return ranges;
138
+ }
139
+
140
+ function clampRanges(ranges, duration) {
141
+ return ranges
142
+ .map((range) => ({
143
+ start: Math.max(0, Math.min(duration, range.start)),
144
+ end: Math.max(0, Math.min(duration, range.end)),
145
+ }))
146
+ .filter((range) => range.end > range.start);
147
+ }
148
+
149
+ function mergeRanges(ranges) {
150
+ const sorted = ranges
151
+ .map((range) => ({ start: round3(range.start), end: round3(range.end) }))
152
+ .sort((a, b) => a.start - b.start || a.end - b.end);
153
+ const merged = [];
154
+ for (const range of sorted) {
155
+ const prev = merged.at(-1);
156
+ if (prev && range.start <= prev.end) {
157
+ prev.end = Math.max(prev.end, range.end);
158
+ } else {
159
+ merged.push({ ...range });
160
+ }
161
+ }
162
+ return merged;
163
+ }
164
+
165
+ function invertRanges(removals, duration) {
166
+ const kept = [];
167
+ let cursor = 0;
168
+ for (const range of removals) {
169
+ if (range.start > cursor) kept.push({ start: cursor, end: range.start });
170
+ cursor = Math.max(cursor, range.end);
171
+ }
172
+ if (cursor < duration) kept.push({ start: cursor, end: duration });
173
+ return kept;
174
+ }
175
+
176
+ function finalizeKept(ranges) {
177
+ return mergeRanges(ranges)
178
+ .map((range) => ({ start: round3(range.start), end: round3(range.end) }))
179
+ .filter((range) => round3(range.end - range.start) >= MIN_SEGMENT_SECONDS);
180
+ }
181
+
182
+ function round3(n) {
183
+ return Math.round(Number(n) * 1000) / 1000;
184
+ }
@@ -0,0 +1,162 @@
1
+ import { strict as assert } from "node:assert";
2
+ import { execFileSync } from "node:child_process";
3
+ import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
4
+ import { join, dirname } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { fileURLToPath } from "node:url";
7
+ import { test } from "node:test";
8
+ import { compileCutList } from "./cutlist.mjs";
9
+
10
+ const HERE = dirname(fileURLToPath(import.meta.url));
11
+ // These legacy wrappers remain under skills/media-use/scripts because the
12
+ // skill's public audio entrypoints still invoke them; the moved engine tests
13
+ // their implementation here while preserving that compatibility surface.
14
+ const SCRIPT = join(
15
+ HERE,
16
+ "..",
17
+ "..",
18
+ "..",
19
+ "..",
20
+ "..",
21
+ "skills",
22
+ "media-use",
23
+ "scripts",
24
+ "transcript-cut.mjs",
25
+ );
26
+
27
+ test("explicit --remove ranges invert to kept segments", () => {
28
+ const transcript = [
29
+ word("w0", "alpha", 0, 1),
30
+ word("w1", "beta", 1.2, 2),
31
+ word("w2", "gamma", 2.2, 5),
32
+ ];
33
+
34
+ assert.deepEqual(compileCutList(transcript, { remove: "1-2.5" }), [
35
+ { start: 0, end: 1 },
36
+ { start: 2.5, end: 5 },
37
+ ]);
38
+ });
39
+
40
+ test("--remove-words resolves inclusive word-index ranges to time ranges", () => {
41
+ const transcript = [
42
+ word("w0", "zero", 0, 0.5),
43
+ word("w1", "one", 0.6, 1),
44
+ word("w2", "two", 1.1, 1.5),
45
+ word("w3", "three", 2, 3),
46
+ ];
47
+
48
+ assert.deepEqual(compileCutList(transcript, { removeWords: "1-2" }), [
49
+ { start: 0, end: 0.6 },
50
+ { start: 1.5, end: 3 },
51
+ ]);
52
+ });
53
+
54
+ test("--remove-fillers drops case-insensitive matching words", () => {
55
+ const transcript = [
56
+ word("w0", "Hello", 0, 0.5),
57
+ word("w1", "Um", 0.5, 0.7),
58
+ word("w2", "world", 0.8, 1.2),
59
+ word("w3", "LIKE", 1.3, 1.5),
60
+ word("w4", "done", 1.6, 2),
61
+ ];
62
+
63
+ assert.deepEqual(compileCutList(transcript, { removeFillers: "um,like" }), [
64
+ { start: 0, end: 0.5 },
65
+ { start: 0.7, end: 1.3 },
66
+ { start: 1.5, end: 2 },
67
+ ]);
68
+ });
69
+
70
+ test("--cut-silence removes only the center of long inter-word gaps", () => {
71
+ const transcript = [word("w0", "a", 0, 0.5), word("w1", "b", 2, 2.5), word("w2", "c", 2.7, 3)];
72
+
73
+ assert.deepEqual(compileCutList(transcript, { cutSilence: 0.8 }), [
74
+ { start: 0, end: 0.65 },
75
+ { start: 1.85, end: 3 },
76
+ ]);
77
+ });
78
+
79
+ test("overlapping removal sources merge before inversion", () => {
80
+ const transcript = [
81
+ word("w0", "start", 0, 0.5),
82
+ word("w1", "um", 0.9, 1.1),
83
+ word("w2", "middle", 2.5, 2.8),
84
+ word("w3", "more", 3.1, 3.4),
85
+ word("w4", "end", 5.5, 6),
86
+ ];
87
+
88
+ assert.deepEqual(
89
+ compileCutList(transcript, {
90
+ remove: "1-2.7",
91
+ removeWords: "2-3",
92
+ removeFillers: "um",
93
+ }),
94
+ [
95
+ { start: 0, end: 0.9 },
96
+ { start: 3.4, end: 6 },
97
+ ],
98
+ );
99
+ });
100
+
101
+ test("kept slivers shorter than 0.2s are dropped", () => {
102
+ const transcript = [word("w0", "start", 0, 0.5), word("w1", "end", 2.5, 3)];
103
+
104
+ assert.deepEqual(compileCutList(transcript, { remove: "0.1-2.95" }), []);
105
+ });
106
+
107
+ test("--keep is inverse mode and coalesces direct kept ranges", () => {
108
+ const transcript = [word("w0", "start", 0, 0.5), word("w1", "end", 4.5, 5)];
109
+
110
+ assert.deepEqual(compileCutList(transcript, { keep: "3-4,1-2,1.5-2.5,4.1-4.2" }), [
111
+ { start: 1, end: 2.5 },
112
+ { start: 3, end: 4 },
113
+ ]);
114
+ });
115
+
116
+ test("--plan on a fixture transcript prints the exact segment JSON", () => {
117
+ const dir = mkdtempSync(join(tmpdir(), "media-use-cutlist-"));
118
+ try {
119
+ const transcriptPath = join(dir, "fixture.json");
120
+ writeFileSync(
121
+ transcriptPath,
122
+ JSON.stringify([
123
+ word("w0", "hello", 0, 0.4),
124
+ word("w1", "um", 0.5, 0.65),
125
+ word("w2", "there", 0.7, 1),
126
+ word("w3", "pause", 2.2, 2.5),
127
+ word("w4", "end", 2.7, 3.2),
128
+ ]),
129
+ );
130
+
131
+ const out = execFileSync(
132
+ process.execPath,
133
+ [
134
+ SCRIPT,
135
+ "--input",
136
+ "ignored.mp4",
137
+ "--transcript",
138
+ transcriptPath,
139
+ "--remove",
140
+ "0.9-1.2",
141
+ "--remove-fillers",
142
+ "um",
143
+ "--cut-silence",
144
+ "0.8",
145
+ "--plan",
146
+ ],
147
+ { encoding: "utf8" },
148
+ );
149
+
150
+ assert.deepEqual(JSON.parse(out), [
151
+ { start: 0, end: 0.5 },
152
+ { start: 0.65, end: 0.9 },
153
+ { start: 2.05, end: 3.2 },
154
+ ]);
155
+ } finally {
156
+ rmSync(dir, { recursive: true, force: true });
157
+ }
158
+ });
159
+
160
+ function word(id, text, start, end) {
161
+ return { id, text, start, end };
162
+ }