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.
- package/dist/cli.js +4483 -3217
- package/dist/docs/data-attributes.md +2 -1
- package/dist/fontLocalizeCli.js +73 -12
- package/dist/hyperframe-runtime.js +25 -25
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +25 -25
- package/dist/hyperframes-player.global.js +3 -3
- package/dist/registry/localSemantic.js +548 -0
- package/dist/skills/hyperframes-cli/SKILL.md +2 -2
- package/dist/skills/media-use/SKILL.md +96 -0
- package/dist/skills/media-use/audio/assets/sfx/CREDITS.md +35 -0
- package/dist/skills/media-use/audio/assets/sfx/chime.mp3 +0 -0
- package/dist/skills/media-use/audio/assets/sfx/click-soft.mp3 +0 -0
- package/dist/skills/media-use/audio/assets/sfx/click.mp3 +0 -0
- package/dist/skills/media-use/audio/assets/sfx/error.mp3 +0 -0
- package/dist/skills/media-use/audio/assets/sfx/glitch-1.mp3 +0 -0
- package/dist/skills/media-use/audio/assets/sfx/glitch-2.mp3 +0 -0
- package/dist/skills/media-use/audio/assets/sfx/glitch-3.mp3 +0 -0
- package/dist/skills/media-use/audio/assets/sfx/impact-bass-1.mp3 +0 -0
- package/dist/skills/media-use/audio/assets/sfx/impact-bass-2.mp3 +0 -0
- package/dist/skills/media-use/audio/assets/sfx/key-press.mp3 +0 -0
- package/dist/skills/media-use/audio/assets/sfx/manifest.json +97 -0
- package/dist/skills/media-use/audio/assets/sfx/notification.mp3 +0 -0
- package/dist/skills/media-use/audio/assets/sfx/ping.mp3 +0 -0
- package/dist/skills/media-use/audio/assets/sfx/pop.mp3 +0 -0
- package/dist/skills/media-use/audio/assets/sfx/riser.mp3 +0 -0
- package/dist/skills/media-use/audio/assets/sfx/sparkle.mp3 +0 -0
- package/dist/skills/media-use/audio/assets/sfx/typing.mp3 +0 -0
- package/dist/skills/media-use/audio/assets/sfx/whoosh-cinematic.mp3 +0 -0
- package/dist/skills/media-use/audio/assets/sfx/whoosh-short.mp3 +0 -0
- package/dist/skills/media-use/audio/assets/sfx/whoosh.mp3 +0 -0
- package/dist/skills/media-use/audio/references/bgm.md +74 -0
- package/dist/skills/media-use/audio/references/captions/authoring.md +163 -0
- package/dist/skills/media-use/audio/references/captions/motion.md +87 -0
- package/dist/skills/media-use/audio/references/captions/transcript-handling.md +97 -0
- package/dist/skills/media-use/audio/references/remove-background.md +143 -0
- package/dist/skills/media-use/audio/references/requirements.md +29 -0
- package/dist/skills/media-use/audio/references/sfx.md +42 -0
- package/dist/skills/media-use/audio/references/transcribe.md +52 -0
- package/dist/skills/media-use/audio/references/tts-to-captions.md +26 -0
- package/dist/skills/media-use/audio/references/tts.md +165 -0
- package/dist/skills/media-use/audio/scripts/audio.mjs +295 -0
- package/dist/skills/media-use/audio/scripts/audio.test.mjs +49 -0
- package/dist/skills/media-use/audio/scripts/heygen-tts.mjs +121 -0
- package/dist/skills/media-use/audio/scripts/lib/audio-meta.mjs +39 -0
- package/dist/skills/media-use/audio/scripts/lib/audio-meta.test.mjs +136 -0
- package/dist/skills/media-use/audio/scripts/lib/bgm.mjs +257 -0
- package/dist/skills/media-use/audio/scripts/lib/bgm.test.mjs +30 -0
- package/dist/skills/media-use/audio/scripts/lib/concurrency.mjs +14 -0
- package/dist/skills/media-use/audio/scripts/lib/concurrency.test.mjs +41 -0
- package/dist/skills/media-use/audio/scripts/lib/heygen.mjs +156 -0
- package/dist/skills/media-use/audio/scripts/lib/heygen.test.mjs +103 -0
- package/dist/skills/media-use/audio/scripts/lib/python.mjs +63 -0
- package/dist/skills/media-use/audio/scripts/lib/python.test.mjs +68 -0
- package/dist/skills/media-use/audio/scripts/lib/sfx.mjs +143 -0
- package/dist/skills/media-use/audio/scripts/lib/sfx.test.mjs +69 -0
- package/dist/skills/media-use/audio/scripts/lib/tts.mjs +379 -0
- package/dist/skills/media-use/audio/scripts/lib/tts.spawn.test.mjs +158 -0
- package/dist/skills/media-use/audio/scripts/lib/tts.test.mjs +157 -0
- package/dist/skills/media-use/audio/scripts/lyria-recipe.py +136 -0
- package/dist/skills/media-use/audio/scripts/wait-bgm.mjs +171 -0
- package/dist/skills/media-use/audio/scripts/wait-bgm.test.mjs +65 -0
- package/dist/skills/media-use/luts/README.md +28 -0
- package/dist/skills/media-use/luts/index.json +57 -0
- package/dist/skills/media-use/references/audio.md +19 -0
- package/dist/skills/media-use/references/grading.md +160 -0
- package/dist/skills/media-use/references/media-treatment-recipes.md +929 -0
- package/dist/skills/media-use/references/media-treatments.md +202 -0
- package/dist/skills/media-use/references/memory.md +36 -0
- package/dist/skills/media-use/references/meta.md +61 -0
- package/dist/skills/media-use/references/operations.md +325 -0
- package/dist/skills/media-use/references/resolve.md +150 -0
- package/dist/skills/media-use/references/setup-providers.md +80 -0
- package/dist/skills/media-use/references/telemetry-dashboard.md +49 -0
- package/dist/skills/media-use/registry/localSemantic.js +548 -0
- package/dist/skills/media-use/scripts/audio-duck.mjs +113 -0
- package/dist/skills/media-use/scripts/compatibility.test.mjs +27 -0
- package/dist/skills/media-use/scripts/dither.mjs +328 -0
- package/dist/skills/media-use/scripts/dither.test.mjs +176 -0
- package/dist/skills/media-use/scripts/eval.mjs +369 -0
- package/dist/skills/media-use/scripts/lib/adopt.mjs +128 -0
- package/dist/skills/media-use/scripts/lib/adopt.test.mjs +95 -0
- package/dist/skills/media-use/scripts/lib/bgm-provider.mjs +20 -0
- package/dist/skills/media-use/scripts/lib/brand-provider.mjs +59 -0
- package/dist/skills/media-use/scripts/lib/bundled-sfx-provider.mjs +119 -0
- package/dist/skills/media-use/scripts/lib/bundled-sfx-provider.test.mjs +139 -0
- package/dist/skills/media-use/scripts/lib/cache.mjs +150 -0
- package/dist/skills/media-use/scripts/lib/candidates.mjs +91 -0
- package/dist/skills/media-use/scripts/lib/candidates.test.mjs +155 -0
- package/dist/skills/media-use/scripts/lib/codex-provider.mjs +161 -0
- package/dist/skills/media-use/scripts/lib/codex-provider.test.mjs +34 -0
- package/dist/skills/media-use/scripts/lib/coverage.test.mjs +132 -0
- package/dist/skills/media-use/scripts/lib/cube-build.mjs +181 -0
- package/dist/skills/media-use/scripts/lib/cube-build.test.mjs +80 -0
- package/dist/skills/media-use/scripts/lib/cube-validate.mjs +198 -0
- package/dist/skills/media-use/scripts/lib/cube-validate.test.mjs +125 -0
- package/dist/skills/media-use/scripts/lib/cutlist.mjs +184 -0
- package/dist/skills/media-use/scripts/lib/cutlist.test.mjs +162 -0
- package/dist/skills/media-use/scripts/lib/duck.mjs +95 -0
- package/dist/skills/media-use/scripts/lib/duck.test.mjs +153 -0
- package/dist/skills/media-use/scripts/lib/error-diffusion.mjs +230 -0
- package/dist/skills/media-use/scripts/lib/error-diffusion.test.mjs +102 -0
- package/dist/skills/media-use/scripts/lib/freeze.mjs +62 -0
- package/dist/skills/media-use/scripts/lib/freeze.test.mjs +46 -0
- package/dist/skills/media-use/scripts/lib/grade-analyzer.mjs +265 -0
- package/dist/skills/media-use/scripts/lib/grade-analyzer.test.mjs +285 -0
- package/dist/skills/media-use/scripts/lib/heygen-cli.mjs +191 -0
- package/dist/skills/media-use/scripts/lib/heygen-cli.test.mjs +301 -0
- package/dist/skills/media-use/scripts/lib/heygen-search.mjs +45 -0
- package/dist/skills/media-use/scripts/lib/heygen-search.test.mjs +49 -0
- package/dist/skills/media-use/scripts/lib/heygen-video-provider.mjs +138 -0
- package/dist/skills/media-use/scripts/lib/heygen-video-provider.test.mjs +435 -0
- package/dist/skills/media-use/scripts/lib/image-provider.mjs +44 -0
- package/dist/skills/media-use/scripts/lib/index-gen.mjs +63 -0
- package/dist/skills/media-use/scripts/lib/local-media-search.mjs +32 -0
- package/dist/skills/media-use/scripts/lib/local-models.mjs +318 -0
- package/dist/skills/media-use/scripts/lib/local-models.test.mjs +220 -0
- package/dist/skills/media-use/scripts/lib/local-run.mjs +78 -0
- package/dist/skills/media-use/scripts/lib/local-run.test.mjs +121 -0
- package/dist/skills/media-use/scripts/lib/logo-provider.mjs +223 -0
- package/dist/skills/media-use/scripts/lib/logo-provider.test.mjs +154 -0
- package/dist/skills/media-use/scripts/lib/ltx-video-provider.mjs +94 -0
- package/dist/skills/media-use/scripts/lib/ltx-video-provider.test.mjs +316 -0
- package/dist/skills/media-use/scripts/lib/lut-preset-provider.mjs +271 -0
- package/dist/skills/media-use/scripts/lib/lut-preset-provider.test.mjs +199 -0
- package/dist/skills/media-use/scripts/lib/manifest.mjs +229 -0
- package/dist/skills/media-use/scripts/lib/manifest.test.mjs +388 -0
- package/dist/skills/media-use/scripts/lib/match.mjs +47 -0
- package/dist/skills/media-use/scripts/lib/media-fetch.mjs +71 -0
- package/dist/skills/media-use/scripts/lib/media-fetch.test.mjs +169 -0
- package/dist/skills/media-use/scripts/lib/media-search.d.mts +9 -0
- package/dist/skills/media-use/scripts/lib/media-search.mjs +107 -0
- package/dist/skills/media-use/scripts/lib/mflux-provider.mjs +128 -0
- package/dist/skills/media-use/scripts/lib/mflux-provider.test.mjs +174 -0
- package/dist/skills/media-use/scripts/lib/misses.mjs +49 -0
- package/dist/skills/media-use/scripts/lib/misses.test.mjs +78 -0
- package/dist/skills/media-use/scripts/lib/npx-sync.mjs +31 -0
- package/dist/skills/media-use/scripts/lib/npx-sync.test.mjs +50 -0
- package/dist/skills/media-use/scripts/lib/parakeet-words.mjs +27 -0
- package/dist/skills/media-use/scripts/lib/parakeet-words.test.mjs +44 -0
- package/dist/skills/media-use/scripts/lib/prefs-store.mjs +180 -0
- package/dist/skills/media-use/scripts/lib/prefs-store.test.mjs +189 -0
- package/dist/skills/media-use/scripts/lib/probe.mjs +39 -0
- package/dist/skills/media-use/scripts/lib/probe.test.mjs +31 -0
- package/dist/skills/media-use/scripts/lib/providers.mjs +5 -0
- package/dist/skills/media-use/scripts/lib/recipe-store.mjs +367 -0
- package/dist/skills/media-use/scripts/lib/recipe-store.test.mjs +367 -0
- package/dist/skills/media-use/scripts/lib/registry.mjs +218 -0
- package/dist/skills/media-use/scripts/lib/registry.test.mjs +262 -0
- package/dist/skills/media-use/scripts/lib/search.mjs +24 -0
- package/dist/skills/media-use/scripts/lib/search.test.mjs +32 -0
- package/dist/skills/media-use/scripts/lib/sfx-provider.mjs +23 -0
- package/dist/skills/media-use/scripts/lib/specs.mjs +132 -0
- package/dist/skills/media-use/scripts/lib/specs.test.mjs +157 -0
- package/dist/skills/media-use/scripts/lib/stats.mjs +117 -0
- package/dist/skills/media-use/scripts/lib/stats.test.mjs +165 -0
- package/dist/skills/media-use/scripts/lib/telemetry.mjs +193 -0
- package/dist/skills/media-use/scripts/lib/telemetry.test.mjs +373 -0
- package/dist/skills/media-use/scripts/lib/tts-local-provider.mjs +92 -0
- package/dist/skills/media-use/scripts/lib/tts-local-provider.test.mjs +72 -0
- package/dist/skills/media-use/scripts/lib/usage.mjs +57 -0
- package/dist/skills/media-use/scripts/lib/usage.test.mjs +54 -0
- package/dist/skills/media-use/scripts/lib/voice-provider.mjs +57 -0
- package/dist/skills/media-use/scripts/lib/voice-provider.test.mjs +46 -0
- package/dist/skills/media-use/scripts/lib/words.mjs +18 -0
- package/dist/skills/media-use/scripts/prefs.mjs +70 -0
- package/dist/skills/media-use/scripts/recipe.mjs +103 -0
- package/dist/skills/media-use/scripts/resolve.mjs +1287 -0
- package/dist/skills/media-use/scripts/transcribe.mjs +180 -0
- package/dist/skills/media-use/scripts/transcript-cut.mjs +239 -0
- package/dist/skills/registry/localSemantic.js +548 -0
- package/dist/studio/assets/{hyperframes-player-zGyOcwwh.js → hyperframes-player-BBub3ZdI.js} +3 -3
- package/dist/studio/assets/{index-ZJaRCDYV.js → index-BxZ5Cw5w.js} +1 -1
- package/dist/studio/assets/{index-BcE2OSVA.js → index-CI7WON-E.js} +1 -1
- package/dist/studio/assets/{index-d8ferjRc.js → index-D1KNPijK.js} +190 -190
- package/dist/studio/assets/{index-DKJpHYYd.css → index-DZBHG9TW.css} +1 -1
- package/dist/studio/index.d.ts +548 -8
- package/dist/studio/index.html +2 -2
- package/dist/studio/index.js +21596 -20725
- package/dist/studio/index.js.map +1 -1
- package/dist/studio/styles.css +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { strict as assert } from "node:assert";
|
|
2
|
+
import {
|
|
3
|
+
mkdtempSync,
|
|
4
|
+
rmSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
existsSync,
|
|
9
|
+
readdirSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import {
|
|
14
|
+
readManifest,
|
|
15
|
+
appendRecord,
|
|
16
|
+
findByPrompt,
|
|
17
|
+
findByEntity,
|
|
18
|
+
nextId,
|
|
19
|
+
allocateId,
|
|
20
|
+
withReservedFileSync,
|
|
21
|
+
normalizePrompt,
|
|
22
|
+
manifestPath,
|
|
23
|
+
mediaDir,
|
|
24
|
+
typeDirPath,
|
|
25
|
+
typeSubdir,
|
|
26
|
+
} from "./manifest.mjs";
|
|
27
|
+
import { regenerateIndex, generateIndexContent } from "./index-gen.mjs";
|
|
28
|
+
import {
|
|
29
|
+
contentHash,
|
|
30
|
+
cachePut,
|
|
31
|
+
cacheGet,
|
|
32
|
+
cacheGetByEntity,
|
|
33
|
+
importFromCache,
|
|
34
|
+
promote,
|
|
35
|
+
} from "./cache.mjs";
|
|
36
|
+
|
|
37
|
+
let tmp;
|
|
38
|
+
|
|
39
|
+
function setup() {
|
|
40
|
+
tmp = mkdtempSync(join(tmpdir(), "mu-test-"));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function cleanup() {
|
|
44
|
+
if (tmp) rmSync(tmp, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function makeRecord(overrides = {}) {
|
|
48
|
+
return {
|
|
49
|
+
id: "bgm_001",
|
|
50
|
+
type: "bgm",
|
|
51
|
+
path: ".media/audio/bgm/bgm_001.wav",
|
|
52
|
+
source: "search",
|
|
53
|
+
description: "soft minimal ambient",
|
|
54
|
+
duration: 11,
|
|
55
|
+
provenance: { provider: "heygen.audio.sounds", prompt: "subtle tech" },
|
|
56
|
+
...overrides,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function runTests() {
|
|
61
|
+
const tests = [];
|
|
62
|
+
|
|
63
|
+
function test(name, fn) {
|
|
64
|
+
tests.push({ name, fn });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// --- manifest.mjs ---
|
|
68
|
+
|
|
69
|
+
test("readManifest returns empty array when no manifest exists", () => {
|
|
70
|
+
setup();
|
|
71
|
+
const result = readManifest(tmp);
|
|
72
|
+
assert.deepStrictEqual(result, []);
|
|
73
|
+
cleanup();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("appendRecord writes valid JSONL and readManifest parses it back", () => {
|
|
77
|
+
setup();
|
|
78
|
+
const record = makeRecord();
|
|
79
|
+
appendRecord(tmp, record);
|
|
80
|
+
const records = readManifest(tmp);
|
|
81
|
+
assert.equal(records.length, 1);
|
|
82
|
+
assert.deepStrictEqual(records[0], record);
|
|
83
|
+
cleanup();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("appendRecord creates .media/ and type subdirs on first write", () => {
|
|
87
|
+
setup();
|
|
88
|
+
appendRecord(tmp, makeRecord());
|
|
89
|
+
assert.ok(existsSync(mediaDir(tmp)));
|
|
90
|
+
assert.ok(existsSync(typeDirPath(tmp, "bgm")));
|
|
91
|
+
cleanup();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("lut and grade artifacts use the shared .media/luts subdir", () => {
|
|
95
|
+
assert.equal(typeSubdir("lut"), "luts");
|
|
96
|
+
assert.equal(typeSubdir("grade"), "luts");
|
|
97
|
+
|
|
98
|
+
setup();
|
|
99
|
+
const allocated = allocateId(tmp, "lut", ".cube");
|
|
100
|
+
assert.equal(allocated.localPath, ".media/luts/lut_001.cube");
|
|
101
|
+
assert.ok(existsSync(join(tmp, allocated.localPath)));
|
|
102
|
+
cleanup();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("appendRecord appends multiple records", () => {
|
|
106
|
+
setup();
|
|
107
|
+
appendRecord(tmp, makeRecord({ id: "bgm_001" }));
|
|
108
|
+
appendRecord(tmp, makeRecord({ id: "bgm_002", provenance: { prompt: "energetic" } }));
|
|
109
|
+
const records = readManifest(tmp);
|
|
110
|
+
assert.equal(records.length, 2);
|
|
111
|
+
assert.equal(records[0].id, "bgm_001");
|
|
112
|
+
assert.equal(records[1].id, "bgm_002");
|
|
113
|
+
cleanup();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("findByPrompt returns exact-match record", () => {
|
|
117
|
+
setup();
|
|
118
|
+
appendRecord(tmp, makeRecord());
|
|
119
|
+
const found = findByPrompt(tmp, "subtle tech", "bgm");
|
|
120
|
+
assert.ok(found);
|
|
121
|
+
assert.equal(found.id, "bgm_001");
|
|
122
|
+
cleanup();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("findByPrompt returns null on miss", () => {
|
|
126
|
+
setup();
|
|
127
|
+
appendRecord(tmp, makeRecord());
|
|
128
|
+
assert.equal(findByPrompt(tmp, "nonexistent", "bgm"), null);
|
|
129
|
+
cleanup();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("findByPrompt filters by type", () => {
|
|
133
|
+
setup();
|
|
134
|
+
appendRecord(tmp, makeRecord({ type: "sfx" }));
|
|
135
|
+
assert.equal(findByPrompt(tmp, "subtle tech", "bgm"), null);
|
|
136
|
+
assert.ok(findByPrompt(tmp, "subtle tech", "sfx"));
|
|
137
|
+
cleanup();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("findByPrompt matches across case and whitespace variants", () => {
|
|
141
|
+
setup();
|
|
142
|
+
appendRecord(tmp, makeRecord({ provenance: { provider: "x", prompt: "calm ambient piano" } }));
|
|
143
|
+
assert.ok(findByPrompt(tmp, "Calm Ambient Piano", "bgm"), "case-insensitive");
|
|
144
|
+
assert.ok(findByPrompt(tmp, " calm ambient piano ", "bgm"), "whitespace-insensitive");
|
|
145
|
+
assert.equal(findByPrompt(tmp, "calm ambient guitar", "bgm"), null, "still a real miss");
|
|
146
|
+
cleanup();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("normalizePrompt trims, lowercases, collapses whitespace", () => {
|
|
150
|
+
assert.equal(normalizePrompt(" Upbeat Tech Launch "), "upbeat tech launch");
|
|
151
|
+
assert.equal(normalizePrompt(null), "");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("allocateId reserves the id on disk so a pre-append caller can't reuse it (MU-23)", () => {
|
|
155
|
+
setup();
|
|
156
|
+
const a = allocateId(tmp, "bgm", ".wav");
|
|
157
|
+
assert.equal(a.id, "bgm_001");
|
|
158
|
+
assert.ok(existsSync(join(tmp, a.localPath)), "placeholder reserved on disk");
|
|
159
|
+
// Second allocation BEFORE any manifest append (the download window) must not
|
|
160
|
+
// hand back bgm_001 again, even with a different extension.
|
|
161
|
+
const b = allocateId(tmp, "bgm", ".mp3");
|
|
162
|
+
assert.equal(b.id, "bgm_002");
|
|
163
|
+
assert.notEqual(a.localPath, b.localPath);
|
|
164
|
+
// Lock file is released (not left behind).
|
|
165
|
+
assert.ok(!existsSync(join(tmp, ".media", ".lock")), "lock released");
|
|
166
|
+
cleanup();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("allocateId continues past the highest manifest id", () => {
|
|
170
|
+
setup();
|
|
171
|
+
appendRecord(tmp, makeRecord({ id: "bgm_005" }));
|
|
172
|
+
assert.equal(allocateId(tmp, "bgm", ".wav").id, "bgm_006");
|
|
173
|
+
cleanup();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("failed reservation rollback preserves another completed reservation", () => {
|
|
177
|
+
setup();
|
|
178
|
+
const kept = withReservedFileSync(tmp, "bgm", ".wav", (reservation) => {
|
|
179
|
+
writeFileSync(reservation.fullPath, "completed asset");
|
|
180
|
+
return reservation;
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
assert.throws(
|
|
184
|
+
() =>
|
|
185
|
+
withReservedFileSync(tmp, "bgm", ".wav", () => {
|
|
186
|
+
throw new Error("populate failed");
|
|
187
|
+
}),
|
|
188
|
+
/populate failed/,
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
assert.equal(kept.id, "bgm_001");
|
|
192
|
+
assert.equal(readFileSync(kept.fullPath, "utf8"), "completed asset");
|
|
193
|
+
assert.deepStrictEqual(readdirSync(typeDirPath(tmp, "bgm")), ["bgm_001.wav"]);
|
|
194
|
+
assert.equal(allocateId(tmp, "bgm", ".wav").id, "bgm_002");
|
|
195
|
+
cleanup();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("empty reservation result releases the placeholder", () => {
|
|
199
|
+
setup();
|
|
200
|
+
assert.equal(
|
|
201
|
+
withReservedFileSync(tmp, "image", ".jpg", () => null),
|
|
202
|
+
null,
|
|
203
|
+
);
|
|
204
|
+
assert.deepStrictEqual(readdirSync(typeDirPath(tmp, "image")), []);
|
|
205
|
+
cleanup();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("findByEntity matches case-insensitively", () => {
|
|
209
|
+
setup();
|
|
210
|
+
appendRecord(tmp, makeRecord({ entity: "GitHub", type: "icon" }));
|
|
211
|
+
assert.ok(findByEntity(tmp, "github"));
|
|
212
|
+
assert.ok(findByEntity(tmp, "GITHUB"));
|
|
213
|
+
assert.equal(findByEntity(tmp, "gitlab"), null);
|
|
214
|
+
cleanup();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("nextId generates sequential ids", () => {
|
|
218
|
+
setup();
|
|
219
|
+
assert.equal(nextId(tmp, "bgm"), "bgm_001");
|
|
220
|
+
appendRecord(tmp, makeRecord({ id: "bgm_001" }));
|
|
221
|
+
assert.equal(nextId(tmp, "bgm"), "bgm_002");
|
|
222
|
+
appendRecord(tmp, makeRecord({ id: "bgm_002" }));
|
|
223
|
+
assert.equal(nextId(tmp, "bgm"), "bgm_003");
|
|
224
|
+
cleanup();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// --- index-gen.mjs ---
|
|
228
|
+
|
|
229
|
+
test("regenerateIndex produces plain-column table", () => {
|
|
230
|
+
setup();
|
|
231
|
+
appendRecord(tmp, makeRecord());
|
|
232
|
+
regenerateIndex(tmp);
|
|
233
|
+
const content = readFileSync(join(tmp, ".media", "index.md"), "utf8");
|
|
234
|
+
assert.ok(content.includes("# .media · 1 asset"));
|
|
235
|
+
assert.ok(content.includes("bgm_001"));
|
|
236
|
+
assert.ok(content.includes("soft minimal ambient"));
|
|
237
|
+
assert.ok(content.includes("11s"));
|
|
238
|
+
cleanup();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("regenerateIndex handles empty manifest", () => {
|
|
242
|
+
setup();
|
|
243
|
+
mkdirSync(join(tmp, ".media"), { recursive: true });
|
|
244
|
+
writeFileSync(manifestPath(tmp), "");
|
|
245
|
+
regenerateIndex(tmp);
|
|
246
|
+
const content = readFileSync(join(tmp, ".media", "index.md"), "utf8");
|
|
247
|
+
assert.ok(content.includes("# .media · 0 assets"));
|
|
248
|
+
cleanup();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("generateIndexContent includes dims for images", () => {
|
|
252
|
+
const records = [
|
|
253
|
+
makeRecord({ id: "img_001", type: "image", width: 1920, height: 1080, duration: null }),
|
|
254
|
+
];
|
|
255
|
+
const content = generateIndexContent(records);
|
|
256
|
+
assert.ok(content.includes("1920×1080"));
|
|
257
|
+
assert.ok(content.includes("img_001"));
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("regenerateIndex matches manifest content after multiple writes", () => {
|
|
261
|
+
setup();
|
|
262
|
+
appendRecord(tmp, makeRecord({ id: "bgm_001" }));
|
|
263
|
+
appendRecord(
|
|
264
|
+
tmp,
|
|
265
|
+
makeRecord({ id: "sfx_001", type: "sfx", description: "whoosh", duration: 3 }),
|
|
266
|
+
);
|
|
267
|
+
regenerateIndex(tmp);
|
|
268
|
+
const content = readFileSync(join(tmp, ".media", "index.md"), "utf8");
|
|
269
|
+
assert.ok(content.includes("# .media · 2 assets"));
|
|
270
|
+
assert.ok(content.includes("bgm_001"));
|
|
271
|
+
assert.ok(content.includes("sfx_001"));
|
|
272
|
+
assert.ok(content.includes("whoosh"));
|
|
273
|
+
cleanup();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// --- cache.mjs ---
|
|
277
|
+
|
|
278
|
+
test("cacheGet returns null when cache is empty", () => {
|
|
279
|
+
const result = cacheGet("nonexistent prompt", "bgm");
|
|
280
|
+
assert.equal(result, null);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("cachePut + cacheGet round-trip", () => {
|
|
284
|
+
setup();
|
|
285
|
+
const filePath = join(tmp, "test.wav");
|
|
286
|
+
writeFileSync(filePath, "fake audio bytes for testing");
|
|
287
|
+
const record = makeRecord({ provenance: { prompt: "cache test" } });
|
|
288
|
+
|
|
289
|
+
const { sha } = cachePut(filePath, record);
|
|
290
|
+
assert.ok(sha);
|
|
291
|
+
assert.equal(sha.length, 64);
|
|
292
|
+
|
|
293
|
+
const found = cacheGet("cache test", "bgm");
|
|
294
|
+
assert.ok(found);
|
|
295
|
+
assert.equal(found.reusable, true);
|
|
296
|
+
assert.equal(found.sha, sha);
|
|
297
|
+
|
|
298
|
+
// cross-project reuse must survive trivial prompt variation, not just
|
|
299
|
+
// byte-identical intents (the whole point of normalizePrompt).
|
|
300
|
+
assert.ok(cacheGet(" Cache Test ", "bgm"), "cacheGet is case/whitespace-insensitive");
|
|
301
|
+
cleanup();
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("cacheGetByEntity finds cached asset", () => {
|
|
305
|
+
setup();
|
|
306
|
+
const filePath = join(tmp, "logo.png");
|
|
307
|
+
writeFileSync(filePath, "fake png bytes");
|
|
308
|
+
const record = makeRecord({
|
|
309
|
+
type: "icon",
|
|
310
|
+
entity: "TestCorp",
|
|
311
|
+
provenance: { prompt: "TestCorp logo" },
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
cachePut(filePath, record);
|
|
315
|
+
const found = cacheGetByEntity("testcorp");
|
|
316
|
+
assert.ok(found);
|
|
317
|
+
assert.equal(found.entity, "TestCorp");
|
|
318
|
+
cleanup();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("contentHash is deterministic", () => {
|
|
322
|
+
setup();
|
|
323
|
+
const filePath = join(tmp, "det.bin");
|
|
324
|
+
writeFileSync(filePath, "deterministic content");
|
|
325
|
+
const h1 = contentHash(filePath);
|
|
326
|
+
const h2 = contentHash(filePath);
|
|
327
|
+
assert.equal(h1, h2);
|
|
328
|
+
cleanup();
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test("promote copies project asset to global cache", () => {
|
|
332
|
+
setup();
|
|
333
|
+
const record = makeRecord();
|
|
334
|
+
appendRecord(tmp, record);
|
|
335
|
+
const filePath = join(tmp, record.path);
|
|
336
|
+
mkdirSync(join(filePath, ".."), { recursive: true });
|
|
337
|
+
writeFileSync(filePath, "promotable audio data");
|
|
338
|
+
|
|
339
|
+
const { sha } = promote(tmp, "bgm_001");
|
|
340
|
+
assert.ok(sha);
|
|
341
|
+
|
|
342
|
+
const cached = cacheGet("subtle tech", "bgm");
|
|
343
|
+
assert.ok(cached);
|
|
344
|
+
assert.equal(cached.sha, sha);
|
|
345
|
+
cleanup();
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test("importFromCache copies cached file into project", () => {
|
|
349
|
+
setup();
|
|
350
|
+
const filePath = join(tmp, "source.wav");
|
|
351
|
+
writeFileSync(filePath, "importable audio");
|
|
352
|
+
const record = makeRecord({ provenance: { prompt: "import test" } });
|
|
353
|
+
const { sha } = cachePut(filePath, record);
|
|
354
|
+
|
|
355
|
+
const cached = cacheGet("import test", "bgm");
|
|
356
|
+
const projectDir = mkdtempSync(join(tmpdir(), "mu-import-"));
|
|
357
|
+
const imported = importFromCache(cached, projectDir, "bgm_001", ".media/audio/bgm/bgm_001.wav");
|
|
358
|
+
|
|
359
|
+
assert.ok(imported);
|
|
360
|
+
assert.equal(imported.id, "bgm_001");
|
|
361
|
+
assert.equal(imported.provenance.imported_from, sha);
|
|
362
|
+
assert.ok(existsSync(join(projectDir, ".media/audio/bgm/bgm_001.wav")));
|
|
363
|
+
|
|
364
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
365
|
+
cleanup();
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// --- run ---
|
|
369
|
+
|
|
370
|
+
let passed = 0;
|
|
371
|
+
let failed = 0;
|
|
372
|
+
for (const { name, fn } of tests) {
|
|
373
|
+
try {
|
|
374
|
+
fn();
|
|
375
|
+
passed++;
|
|
376
|
+
console.log(` \x1b[32m✓\x1b[0m ${name}`);
|
|
377
|
+
} catch (err) {
|
|
378
|
+
failed++;
|
|
379
|
+
console.log(` \x1b[31m✗\x1b[0m ${name}`);
|
|
380
|
+
console.log(` ${err.message}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
384
|
+
if (failed > 0) process.exit(1);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
console.log("media-use · manifest/index/cache tests\n");
|
|
388
|
+
runTests();
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Shared lexical-matching helpers used by both the assets/ scan (adopt.mjs) and
|
|
2
|
+
// the reuse-candidate ranker (candidates.mjs), and the type-equivalence check
|
|
3
|
+
// used by resolve.mjs and candidates.mjs. Kept in one place so the icon<->image
|
|
4
|
+
// equivalence and the token rules can't drift between the "do" path (resolve)
|
|
5
|
+
// and the "look" path (candidates).
|
|
6
|
+
|
|
7
|
+
// Common filler words that should never, on their own, make two strings match.
|
|
8
|
+
const MATCH_STOPWORDS = new Set([
|
|
9
|
+
"the",
|
|
10
|
+
"and",
|
|
11
|
+
"for",
|
|
12
|
+
"with",
|
|
13
|
+
"from",
|
|
14
|
+
"this",
|
|
15
|
+
"that",
|
|
16
|
+
"your",
|
|
17
|
+
"our",
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
// Split into lowercased word tokens of length >= 3, minus stopwords.
|
|
21
|
+
export function matchTokens(text) {
|
|
22
|
+
return new Set(
|
|
23
|
+
String(text)
|
|
24
|
+
.toLowerCase()
|
|
25
|
+
.split(/[^a-z0-9]+/)
|
|
26
|
+
.filter((t) => t.length >= 3 && !MATCH_STOPWORDS.has(t)),
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Count of shared meaningful word tokens between two strings. 0 = no lexical
|
|
31
|
+
// overlap (the candidate ranker still surfaces these, ordered after overlaps).
|
|
32
|
+
export function tokenOverlap(a, b) {
|
|
33
|
+
const ta = matchTokens(a);
|
|
34
|
+
const tb = matchTokens(b);
|
|
35
|
+
let n = 0;
|
|
36
|
+
for (const t of ta) if (tb.has(t)) n++;
|
|
37
|
+
return n;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// icon, image, and logo are interchangeable: all live in images/, and
|
|
41
|
+
// figma-imported brand marks are recorded as type image while agents ask for
|
|
42
|
+
// logos as icon or logo.
|
|
43
|
+
export function typesMatch(a, b) {
|
|
44
|
+
if (a === b) return true;
|
|
45
|
+
const visual = new Set(["icon", "image", "logo"]);
|
|
46
|
+
return visual.has(a) && visual.has(b);
|
|
47
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Media downloads use public HTTP(S) URLs. Validate every redirect target;
|
|
2
|
+
// a provider result must meet the same host policy as a direct ingest URL.
|
|
3
|
+
// Public HTTPS-to-HTTP redirects are allowed, matching direct HTTP support.
|
|
4
|
+
// This is a literal-host policy, not DNS pinning: DNS resolution remains trusted.
|
|
5
|
+
|
|
6
|
+
import { BlockList, isIP } from "node:net";
|
|
7
|
+
|
|
8
|
+
const blocked = new BlockList();
|
|
9
|
+
for (const [network, prefix] of [
|
|
10
|
+
["0.0.0.0", 8],
|
|
11
|
+
["10.0.0.0", 8],
|
|
12
|
+
["100.64.0.0", 10],
|
|
13
|
+
["127.0.0.0", 8],
|
|
14
|
+
["169.254.0.0", 16],
|
|
15
|
+
["172.16.0.0", 12],
|
|
16
|
+
["192.0.0.0", 24],
|
|
17
|
+
["192.0.2.0", 24],
|
|
18
|
+
["192.88.99.0", 24],
|
|
19
|
+
["192.168.0.0", 16],
|
|
20
|
+
["198.18.0.0", 15],
|
|
21
|
+
["198.51.100.0", 24],
|
|
22
|
+
["203.0.113.0", 24],
|
|
23
|
+
["224.0.0.0", 4],
|
|
24
|
+
["240.0.0.0", 4],
|
|
25
|
+
])
|
|
26
|
+
blocked.addSubnet(network, prefix, "ipv4");
|
|
27
|
+
for (const [network, prefix] of [
|
|
28
|
+
["::", 128],
|
|
29
|
+
["::1", 128],
|
|
30
|
+
["fc00::", 7],
|
|
31
|
+
["fe80::", 10],
|
|
32
|
+
["fec0::", 10],
|
|
33
|
+
["ff00::", 8],
|
|
34
|
+
["2001:db8::", 32],
|
|
35
|
+
])
|
|
36
|
+
blocked.addSubnet(network, prefix, "ipv6");
|
|
37
|
+
|
|
38
|
+
export function isPublicMediaUrl(value) {
|
|
39
|
+
try {
|
|
40
|
+
const url = new URL(value);
|
|
41
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
42
|
+
const host = url.hostname.replace(/\.$/, "");
|
|
43
|
+
if (
|
|
44
|
+
host === "localhost" ||
|
|
45
|
+
host.endsWith(".localhost") ||
|
|
46
|
+
host.endsWith(".local") ||
|
|
47
|
+
host.endsWith(".internal")
|
|
48
|
+
)
|
|
49
|
+
return false;
|
|
50
|
+
const address = host.replace(/^\[|\]$/g, "");
|
|
51
|
+
const family = isIP(address);
|
|
52
|
+
return family === 0 || !blocked.check(address, family === 4 ? "ipv4" : "ipv6");
|
|
53
|
+
} catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function fetchMedia(url, { method = "GET", signal, fetchImpl = fetch } = {}) {
|
|
59
|
+
let current = String(url);
|
|
60
|
+
for (let hop = 0; hop <= 5; hop++) {
|
|
61
|
+
if (!isPublicMediaUrl(current))
|
|
62
|
+
throw new Error("Media download blocked: URL is not public HTTP(S)");
|
|
63
|
+
const response = await fetchImpl(current, { method, signal, redirect: "manual" });
|
|
64
|
+
if (!(response.status >= 300 && response.status < 400)) return response;
|
|
65
|
+
const location = response.headers.get("location");
|
|
66
|
+
if (!location) return response;
|
|
67
|
+
await response.body?.cancel();
|
|
68
|
+
current = new URL(location, current).href;
|
|
69
|
+
}
|
|
70
|
+
throw new Error("Media download exceeded redirect limit");
|
|
71
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtempSync, readdirSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { test } from "node:test";
|
|
6
|
+
import { fetchMedia, isPublicMediaUrl } from "./media-fetch.mjs";
|
|
7
|
+
import { freezeUrl } from "./freeze.mjs";
|
|
8
|
+
import { downloadTo } from "../../audio/scripts/lib/heygen.mjs";
|
|
9
|
+
import { synthesizeHeygen } from "../../audio/scripts/lib/tts.mjs";
|
|
10
|
+
import { faviconSearch } from "./logo-provider.mjs";
|
|
11
|
+
|
|
12
|
+
test("blocks private, reserved, alternate-encoded and mapped hosts", () => {
|
|
13
|
+
for (const host of [
|
|
14
|
+
"localhost",
|
|
15
|
+
"localhost.",
|
|
16
|
+
"a.localhost",
|
|
17
|
+
"svc.internal",
|
|
18
|
+
"svc.local",
|
|
19
|
+
"127.1",
|
|
20
|
+
"2130706433",
|
|
21
|
+
"0x7f000001",
|
|
22
|
+
"10.0.0.1",
|
|
23
|
+
"169.254.169.254",
|
|
24
|
+
"100.100.100.200",
|
|
25
|
+
"192.168.1.1",
|
|
26
|
+
"172.31.0.1",
|
|
27
|
+
"224.0.0.1",
|
|
28
|
+
"[::]",
|
|
29
|
+
"[::1]",
|
|
30
|
+
"[::ffff:127.0.0.1]",
|
|
31
|
+
"[::ffff:a00:1]",
|
|
32
|
+
"[fe80::1]",
|
|
33
|
+
"[fd00::1]",
|
|
34
|
+
"[ff02::1]",
|
|
35
|
+
]) {
|
|
36
|
+
assert.equal(isPublicMediaUrl(`https://${host}/asset`), false, host);
|
|
37
|
+
}
|
|
38
|
+
for (const url of [
|
|
39
|
+
"https://cdn.example/asset.cube",
|
|
40
|
+
"http://example.com/clip.mp4",
|
|
41
|
+
"https://8.8.8.8/a",
|
|
42
|
+
"https://[::ffff:8.8.8.8]/a",
|
|
43
|
+
])
|
|
44
|
+
assert.equal(isPublicMediaUrl(url), true, url);
|
|
45
|
+
assert.equal(isPublicMediaUrl("file:///tmp/a.mp4"), false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("follows public relative redirects manually and forwards cancellation", async () => {
|
|
49
|
+
const calls = [];
|
|
50
|
+
const signal = new AbortController().signal;
|
|
51
|
+
const response = await fetchMedia("https://cdn.example/a", {
|
|
52
|
+
method: "HEAD",
|
|
53
|
+
signal,
|
|
54
|
+
fetchImpl: async (url, options) => {
|
|
55
|
+
calls.push(url);
|
|
56
|
+
assert.equal(options.redirect, "manual");
|
|
57
|
+
assert.equal(options.signal, signal);
|
|
58
|
+
assert.equal(options.method, "HEAD");
|
|
59
|
+
return calls.length === 1
|
|
60
|
+
? new Response(null, { status: 302, headers: { location: "/b" } })
|
|
61
|
+
: new Response("media");
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
assert.deepEqual(calls, ["https://cdn.example/a", "https://cdn.example/b"]);
|
|
65
|
+
assert.equal(await response.text(), "media");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("rejects private redirect targets before requesting them and cancels redirect bodies", async () => {
|
|
69
|
+
let calls = 0;
|
|
70
|
+
let cancelled = false;
|
|
71
|
+
await assert.rejects(
|
|
72
|
+
fetchMedia("https://cdn.example/a", {
|
|
73
|
+
fetchImpl: async () => {
|
|
74
|
+
calls++;
|
|
75
|
+
return new Response(
|
|
76
|
+
new ReadableStream({
|
|
77
|
+
cancel() {
|
|
78
|
+
cancelled = true;
|
|
79
|
+
},
|
|
80
|
+
}),
|
|
81
|
+
{
|
|
82
|
+
status: 302,
|
|
83
|
+
headers: { location: "http://169.254.169.254/latest/meta-data" },
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
},
|
|
87
|
+
}),
|
|
88
|
+
/blocked/,
|
|
89
|
+
);
|
|
90
|
+
assert.equal(calls, 1);
|
|
91
|
+
assert.equal(cancelled, true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("bounds redirect loops", async () => {
|
|
95
|
+
let calls = 0;
|
|
96
|
+
await assert.rejects(
|
|
97
|
+
fetchMedia("https://cdn.example/a", {
|
|
98
|
+
fetchImpl: async () => {
|
|
99
|
+
calls++;
|
|
100
|
+
return new Response(null, { status: 302, headers: { location: "/a" } });
|
|
101
|
+
},
|
|
102
|
+
}),
|
|
103
|
+
/redirect limit/,
|
|
104
|
+
);
|
|
105
|
+
assert.equal(calls, 6);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("freeze refuses a public-to-private redirect without writing response bytes", async (t) => {
|
|
109
|
+
const dir = mkdtempSync(join(tmpdir(), "hf-freeze-redirect-"));
|
|
110
|
+
t.mock.method(globalThis, "fetch", async (_url, options) =>
|
|
111
|
+
options?.redirect === "manual"
|
|
112
|
+
? new Response(null, { status: 302, headers: { location: "http://127.0.0.1/private.mp4" } })
|
|
113
|
+
: new Response("private response"),
|
|
114
|
+
);
|
|
115
|
+
try {
|
|
116
|
+
await assert.rejects(freezeUrl("https://cdn.example/a.mp4", join(dir, "out.mp4")), /blocked/);
|
|
117
|
+
assert.deepEqual(readdirSync(dir), []);
|
|
118
|
+
} finally {
|
|
119
|
+
rmSync(dir, { recursive: true, force: true });
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
for (const entry of ["heygen audio", "tts mp3", "tts wav", "favicon"]) {
|
|
124
|
+
test(`${entry} blocks private redirects before saving or transcoding`, async (t) => {
|
|
125
|
+
const dir = mkdtempSync(join(tmpdir(), "hf-provider-redirect-"));
|
|
126
|
+
const requests = [];
|
|
127
|
+
const fetchImpl = t.mock.method(globalThis, "fetch", async (url, options) => {
|
|
128
|
+
requests.push(url);
|
|
129
|
+
return options?.redirect === "manual"
|
|
130
|
+
? new Response(null, {
|
|
131
|
+
status: 302,
|
|
132
|
+
headers: { location: "http://169.254.169.254/private" },
|
|
133
|
+
})
|
|
134
|
+
: new Response(new Uint8Array(600));
|
|
135
|
+
});
|
|
136
|
+
try {
|
|
137
|
+
if (entry === "heygen audio") {
|
|
138
|
+
await assert.rejects(
|
|
139
|
+
downloadTo("https://cdn.example/audio", join(dir, "audio.mp3")),
|
|
140
|
+
/blocked/,
|
|
141
|
+
);
|
|
142
|
+
} else if (entry === "favicon") {
|
|
143
|
+
assert.equal(await faviconSearch("GitHub logo"), null);
|
|
144
|
+
} else {
|
|
145
|
+
const result = await synthesizeHeygen(
|
|
146
|
+
{
|
|
147
|
+
text: "hi",
|
|
148
|
+
voiceId: "v1",
|
|
149
|
+
lang: "en",
|
|
150
|
+
speed: 1,
|
|
151
|
+
wavAbs: join(dir, entry === "tts wav" ? "audio.wav" : "audio.mp3"),
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
heygenAuthHeaders: () => ({}),
|
|
155
|
+
heygenJSON: async () => ({ data: { audio_url: "https://cdn.example/audio" } }),
|
|
156
|
+
fetch: fetchImpl,
|
|
157
|
+
transcodeToWav: () => assert.fail("private bytes must not reach ffmpeg"),
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
assert.equal(result.ok, false);
|
|
161
|
+
assert.match(result.error, /blocked/);
|
|
162
|
+
}
|
|
163
|
+
assert.equal(requests.length, 1);
|
|
164
|
+
assert.deepEqual(readdirSync(dir), []);
|
|
165
|
+
} finally {
|
|
166
|
+
rmSync(dir, { recursive: true, force: true });
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|