hyperframes 0.8.57 → 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 +5300 -3428
- 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-c23wnIfc.js → hyperframes-player-BBub3ZdI.js} +3 -3
- package/dist/studio/assets/{index-IDNr3X1H.js → index-BxZ5Cw5w.js} +1 -1
- package/dist/studio/assets/{index-DgkAzx3n.js → index-CI7WON-E.js} +1 -1
- package/dist/studio/assets/{index-gbOCDCuZ.js → index-D1KNPijK.js} +192 -192
- package/dist/studio/assets/{index-DKJpHYYd.css → index-DZBHG9TW.css} +1 -1
- package/dist/studio/index.d.ts +577 -8
- package/dist/studio/index.html +2 -2
- package/dist/studio/index.js +22537 -21600
- package/dist/studio/index.js.map +1 -1
- package/dist/studio/styles.css +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { strict as assert } from "node:assert";
|
|
2
|
+
import {
|
|
3
|
+
mkdtempSync,
|
|
4
|
+
rmSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
statSync,
|
|
9
|
+
symlinkSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { test } from "node:test";
|
|
15
|
+
import {
|
|
16
|
+
LIBRARY_LUT_OFFLINE_CODE,
|
|
17
|
+
RESOLVABLE_PRESET_IDS,
|
|
18
|
+
freezeLibraryLut,
|
|
19
|
+
matchColorLook,
|
|
20
|
+
readBundledLutIndex,
|
|
21
|
+
} from "./lut-preset-provider.mjs";
|
|
22
|
+
import { buildCube } from "./cube-build.mjs";
|
|
23
|
+
import { validateCube, validateCubeFile } from "./cube-validate.mjs";
|
|
24
|
+
|
|
25
|
+
const REPO_ROOT = join(import.meta.dirname, "..", "..", "..", "..", "..");
|
|
26
|
+
|
|
27
|
+
function corePresetIdsFromSource() {
|
|
28
|
+
const src = readFileSync(join(REPO_ROOT, "packages/core/src/colorGrading.ts"), "utf8");
|
|
29
|
+
const match = src.match(/export type HfColorGradingPresetId =([\s\S]*?);/);
|
|
30
|
+
assert.ok(match, "core preset union should be readable");
|
|
31
|
+
return [...match[1].matchAll(/"([^"]+)"/g)].map((m) => m[1]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
test("warm daylight and warm natural light resolve to the core warm-daylight preset", () => {
|
|
35
|
+
assert.deepEqual(matchColorLook("warm daylight"), {
|
|
36
|
+
kind: "preset",
|
|
37
|
+
preset: "warm-daylight",
|
|
38
|
+
score: 2,
|
|
39
|
+
});
|
|
40
|
+
assert.equal(matchColorLook("warm natural light").preset, "warm-daylight");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("high contrast punchy resolves to deep-contrast", () => {
|
|
44
|
+
assert.equal(matchColorLook("high contrast punchy").preset, "deep-contrast");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("removed preset phrases resolve to surviving looks", () => {
|
|
48
|
+
assert.equal(matchColorLook("natural lift").preset, "soft-boost");
|
|
49
|
+
assert.equal(matchColorLook("fresh pop").preset, "bright-pop");
|
|
50
|
+
assert.equal(matchColorLook("warm clean").preset, "warm-daylight");
|
|
51
|
+
assert.equal(matchColorLook("cool clean").preset, "clean-studio");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("complete-filter intent aliases resolve deterministically", () => {
|
|
55
|
+
assert.equal(matchColorLook("analog tape").preset, "vhs-playback");
|
|
56
|
+
assert.equal(matchColorLook("creator video").preset, "creator-camcorder");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("library look freezes a validated cube from params offline (--local-only)", async () => {
|
|
60
|
+
const projectDir = mkdtempSync(join(tmpdir(), "mu-lut-provider-"));
|
|
61
|
+
try {
|
|
62
|
+
const match = matchColorLook("teal orange blockbuster");
|
|
63
|
+
assert.equal(match.kind, "library");
|
|
64
|
+
// localOnly forces the deterministic params path (no network); online, the
|
|
65
|
+
// same look downloads its .cube from the CDN url (via "url").
|
|
66
|
+
const frozen = await freezeLibraryLut(match, { projectDir, type: "grade", localOnly: true });
|
|
67
|
+
assert.match(frozen.localPath, /^\.media\/luts\/grade_001\.cube$/);
|
|
68
|
+
assert.ok(existsSync(join(projectDir, frozen.localPath)));
|
|
69
|
+
assert.equal(validateCubeFile(join(projectDir, frozen.localPath)).ok, true);
|
|
70
|
+
assert.equal(frozen.lut.src, frozen.localPath);
|
|
71
|
+
assert.equal(frozen.metadata.provenance.via, "params-fallback");
|
|
72
|
+
} finally {
|
|
73
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("every resolver preset exists in packages/core/src/colorGrading.ts", () => {
|
|
78
|
+
const corePresetIds = corePresetIdsFromSource();
|
|
79
|
+
assert.deepEqual(
|
|
80
|
+
RESOLVABLE_PRESET_IDS.filter((id) => !corePresetIds.includes(id)),
|
|
81
|
+
[],
|
|
82
|
+
);
|
|
83
|
+
for (const id of RESOLVABLE_PRESET_IDS) {
|
|
84
|
+
const match = matchColorLook(id);
|
|
85
|
+
assert.equal(match.kind, "preset");
|
|
86
|
+
assert.equal(match.preset, id);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("zero-overlap intent returns no preset or library match", () => {
|
|
91
|
+
assert.equal(matchColorLook("zqxv imaginary neutron look"), null);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("bundled LUT index entries resolve from params or url", () => {
|
|
95
|
+
for (const entry of readBundledLutIndex()) {
|
|
96
|
+
assert.ok(entry.id);
|
|
97
|
+
assert.ok(entry.description);
|
|
98
|
+
assert.ok(entry.params || entry.url, `${entry.id} should define params or url`);
|
|
99
|
+
if (entry.params) {
|
|
100
|
+
assert.equal(typeof entry.params, "object");
|
|
101
|
+
assert.equal(validateCube(buildCube(entry.params)).ok, true, `${entry.id} params validate`);
|
|
102
|
+
}
|
|
103
|
+
if (entry.url) assert.equal(typeof entry.url, "string");
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("url library entries respect localOnly and freeze through fetch", async () => {
|
|
108
|
+
const projectDir = mkdtempSync(join(tmpdir(), "mu-lut-url-provider-"));
|
|
109
|
+
const match = {
|
|
110
|
+
kind: "library",
|
|
111
|
+
id: "cdn-look",
|
|
112
|
+
description: "CDN-hosted look",
|
|
113
|
+
tags: ["cdn"],
|
|
114
|
+
intensity: 0.7,
|
|
115
|
+
url: "https://example.invalid/look.cube",
|
|
116
|
+
};
|
|
117
|
+
const originalFetch = globalThis.fetch;
|
|
118
|
+
let fetchCalls = 0;
|
|
119
|
+
try {
|
|
120
|
+
globalThis.fetch = async () => {
|
|
121
|
+
fetchCalls++;
|
|
122
|
+
throw new Error("network should be skipped under localOnly");
|
|
123
|
+
};
|
|
124
|
+
await assert.rejects(
|
|
125
|
+
freezeLibraryLut(match, { projectDir, type: "lut", localOnly: true }),
|
|
126
|
+
(err) => {
|
|
127
|
+
assert.equal(err.code, LIBRARY_LUT_OFFLINE_CODE);
|
|
128
|
+
assert.match(err.message, /--local-only/);
|
|
129
|
+
return true;
|
|
130
|
+
},
|
|
131
|
+
);
|
|
132
|
+
assert.equal(fetchCalls, 0);
|
|
133
|
+
|
|
134
|
+
const cube = buildCube({ contrast: 0.1 });
|
|
135
|
+
const body = Buffer.from(cube);
|
|
136
|
+
globalThis.fetch = async (url) => {
|
|
137
|
+
fetchCalls++;
|
|
138
|
+
assert.equal(url, match.url);
|
|
139
|
+
const directory = join(projectDir, ".media/luts");
|
|
140
|
+
const staging = readdirSync(directory).filter((name) => name.startsWith(".lut-download-"));
|
|
141
|
+
assert.equal(staging.length, 1);
|
|
142
|
+
if (process.platform !== "win32") {
|
|
143
|
+
assert.equal(statSync(join(directory, staging[0])).mode & 0o777, 0o700);
|
|
144
|
+
}
|
|
145
|
+
const victim = join(projectDir, "victim.cube");
|
|
146
|
+
writeFileSync(victim, "unchanged");
|
|
147
|
+
symlinkSync(victim, join(directory, "lut_001.cube.tmp"));
|
|
148
|
+
return {
|
|
149
|
+
ok: true,
|
|
150
|
+
headers: { get: () => String(body.length) },
|
|
151
|
+
body: [body],
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const frozen = await freezeLibraryLut(match, { projectDir, type: "lut" });
|
|
156
|
+
assert.equal(fetchCalls, 1);
|
|
157
|
+
assert.match(frozen.localPath, /^\.media\/luts\/lut_001\.cube$/);
|
|
158
|
+
assert.equal(validateCubeFile(join(projectDir, frozen.localPath)).ok, true);
|
|
159
|
+
assert.equal(frozen.metadata.provenance.via, "url");
|
|
160
|
+
assert.equal(readFileSync(join(projectDir, "victim.cube"), "utf8"), "unchanged");
|
|
161
|
+
assert.deepEqual(readdirSync(join(projectDir, ".media/luts")).sort(), [
|
|
162
|
+
"lut_001.cube",
|
|
163
|
+
"lut_001.cube.tmp",
|
|
164
|
+
]);
|
|
165
|
+
} finally {
|
|
166
|
+
globalThis.fetch = originalFetch;
|
|
167
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("failed URL library freeze releases its reservation", async () => {
|
|
172
|
+
const projectDir = mkdtempSync(join(tmpdir(), "mu-lut-failure-"));
|
|
173
|
+
const originalFetch = globalThis.fetch;
|
|
174
|
+
try {
|
|
175
|
+
globalThis.fetch = async () => ({
|
|
176
|
+
ok: true,
|
|
177
|
+
headers: { get: () => "12" },
|
|
178
|
+
body: [Buffer.from("not a cube\n")],
|
|
179
|
+
});
|
|
180
|
+
await assert.rejects(
|
|
181
|
+
freezeLibraryLut(
|
|
182
|
+
{
|
|
183
|
+
kind: "library",
|
|
184
|
+
id: "broken-cdn-look",
|
|
185
|
+
description: "Broken CDN look",
|
|
186
|
+
tags: ["broken"],
|
|
187
|
+
intensity: 1,
|
|
188
|
+
url: "https://example.com/broken.cube",
|
|
189
|
+
},
|
|
190
|
+
{ projectDir, type: "lut" },
|
|
191
|
+
),
|
|
192
|
+
/failed to freeze library LUT/,
|
|
193
|
+
);
|
|
194
|
+
assert.deepStrictEqual(readdirSync(join(projectDir, ".media/luts")), []);
|
|
195
|
+
} finally {
|
|
196
|
+
globalThis.fetch = originalFetch;
|
|
197
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
198
|
+
}
|
|
199
|
+
});
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readFileSync,
|
|
3
|
+
appendFileSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
openSync,
|
|
8
|
+
closeSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
statSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
|
|
15
|
+
const MANIFEST_FILE = "manifest.jsonl";
|
|
16
|
+
const INDEX_FILE = "index.md";
|
|
17
|
+
|
|
18
|
+
const TYPE_DIRS = {
|
|
19
|
+
bgm: "audio/bgm",
|
|
20
|
+
sfx: "audio/sfx",
|
|
21
|
+
voice: "audio/voice",
|
|
22
|
+
image: "images",
|
|
23
|
+
icon: "images",
|
|
24
|
+
logo: "images",
|
|
25
|
+
brand: "images",
|
|
26
|
+
video: "video",
|
|
27
|
+
grade: "luts",
|
|
28
|
+
lut: "luts",
|
|
29
|
+
recipe: "recipes",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function mediaDir(projectDir) {
|
|
33
|
+
return join(projectDir, ".media");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function manifestPath(projectDir) {
|
|
37
|
+
return join(mediaDir(projectDir), MANIFEST_FILE);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function indexPath(projectDir) {
|
|
41
|
+
return join(mediaDir(projectDir), INDEX_FILE);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function typeSubdir(type) {
|
|
45
|
+
const sub = TYPE_DIRS[type];
|
|
46
|
+
if (!sub) throw new Error(`unknown media type: ${type}`);
|
|
47
|
+
return sub;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function typeDirPath(projectDir, type) {
|
|
51
|
+
return join(mediaDir(projectDir), typeSubdir(type));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function readManifest(projectDir) {
|
|
55
|
+
const p = manifestPath(projectDir);
|
|
56
|
+
if (!existsSync(p)) return [];
|
|
57
|
+
const raw = readFileSync(p, "utf8");
|
|
58
|
+
const records = [];
|
|
59
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
60
|
+
const trimmed = line.trim();
|
|
61
|
+
if (!trimmed) continue;
|
|
62
|
+
try {
|
|
63
|
+
records.push(JSON.parse(trimmed));
|
|
64
|
+
} catch {
|
|
65
|
+
// ponytail: skip malformed lines, don't crash
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return records;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function appendRecord(projectDir, record) {
|
|
72
|
+
const dir = mediaDir(projectDir);
|
|
73
|
+
mkdirSync(dir, { recursive: true });
|
|
74
|
+
const typeDir = typeDirPath(projectDir, record.type);
|
|
75
|
+
mkdirSync(typeDir, { recursive: true });
|
|
76
|
+
|
|
77
|
+
const p = manifestPath(projectDir);
|
|
78
|
+
const line = JSON.stringify(record) + "\n";
|
|
79
|
+
appendFileSync(p, line);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Match prompts forgivingly. Agents rarely re-emit a byte-identical intent, so
|
|
83
|
+
// keying cache lookups on exact equality meant "Calm piano" and "calm piano"
|
|
84
|
+
// re-searched and re-downloaded. Normalize (trim, lowercase, collapse internal
|
|
85
|
+
// whitespace) on both sides; the raw prompt is still stored for audit.
|
|
86
|
+
export function normalizePrompt(prompt) {
|
|
87
|
+
return String(prompt ?? "")
|
|
88
|
+
.trim()
|
|
89
|
+
.toLowerCase()
|
|
90
|
+
.replace(/\s+/g, " ");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function findByPrompt(projectDir, prompt, type) {
|
|
94
|
+
const key = normalizePrompt(prompt);
|
|
95
|
+
if (!key) return null;
|
|
96
|
+
const records = readManifest(projectDir);
|
|
97
|
+
return (
|
|
98
|
+
records.find(
|
|
99
|
+
(r) => normalizePrompt(r.provenance?.prompt) === key && (type == null || r.type === type),
|
|
100
|
+
) || null
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function findByEntity(projectDir, entity) {
|
|
105
|
+
const lower = entity.toLowerCase();
|
|
106
|
+
const records = readManifest(projectDir);
|
|
107
|
+
return records.find((r) => r.entity && r.entity.toLowerCase() === lower) || null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function nextId(projectDir, type) {
|
|
111
|
+
const records = readManifest(projectDir);
|
|
112
|
+
const prefix = type;
|
|
113
|
+
let max = 0;
|
|
114
|
+
for (const r of records) {
|
|
115
|
+
if (r.type !== type) continue;
|
|
116
|
+
const m = r.id?.match(new RegExp(`^${prefix}_(\\d+)$`));
|
|
117
|
+
if (m) max = Math.max(max, parseInt(m[1], 10));
|
|
118
|
+
}
|
|
119
|
+
return `${prefix}_${String(max + 1).padStart(3, "0")}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Sync sleep (no busy-spin) for the allocation lock retry.
|
|
123
|
+
function sleepMs(ms) {
|
|
124
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Coarse per-project lock so concurrent resolves don't race on id allocation.
|
|
128
|
+
// ponytail: one lock file with a 15s stale-steal (a crashed holder can't wedge
|
|
129
|
+
// the project); fine for agent-scale concurrency — revisit if throughput needs
|
|
130
|
+
// finer locking. Date.now() is available here (a normal Node CLI, not a
|
|
131
|
+
// workflow DSL), so mtime-based staleness is safe.
|
|
132
|
+
const LOCK_STALE_MS = 15000;
|
|
133
|
+
const LOCK_TIMEOUT_MS = 20000;
|
|
134
|
+
|
|
135
|
+
function withLock(dir, fn) {
|
|
136
|
+
const lock = join(dir, ".lock");
|
|
137
|
+
const start = Date.now();
|
|
138
|
+
for (;;) {
|
|
139
|
+
try {
|
|
140
|
+
closeSync(openSync(lock, "wx")); // O_EXCL: atomic acquire
|
|
141
|
+
break;
|
|
142
|
+
} catch (err) {
|
|
143
|
+
if (err.code !== "EEXIST") throw err;
|
|
144
|
+
try {
|
|
145
|
+
if (Date.now() - statSync(lock).mtimeMs > LOCK_STALE_MS) {
|
|
146
|
+
rmSync(lock, { force: true }); // steal a stale lock from a dead holder
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
} catch {
|
|
150
|
+
continue; // lock vanished between check and stat — retry the acquire
|
|
151
|
+
}
|
|
152
|
+
if (Date.now() - start > LOCK_TIMEOUT_MS) {
|
|
153
|
+
throw new Error("media-use: timed out acquiring .media/.lock");
|
|
154
|
+
}
|
|
155
|
+
sleepMs(25);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
return fn();
|
|
160
|
+
} finally {
|
|
161
|
+
rmSync(lock, { force: true });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Atomically allocate the next free id for `type` AND reserve its file, so a
|
|
166
|
+
// slow download/copy between allocation and appendRecord can't let a concurrent
|
|
167
|
+
// caller grab the same id (the MU-23 clobber). Under the lock we take the max id
|
|
168
|
+
// across BOTH the manifest and any already-reserved files in the type dir, then
|
|
169
|
+
// O_EXCL-create an empty placeholder at the target path; freeze/copy overwrites
|
|
170
|
+
// it. Returns { id, localPath }.
|
|
171
|
+
export function allocateId(projectDir, type, ext) {
|
|
172
|
+
mkdirSync(mediaDir(projectDir), { recursive: true });
|
|
173
|
+
const typeDir = typeDirPath(projectDir, type);
|
|
174
|
+
mkdirSync(typeDir, { recursive: true });
|
|
175
|
+
return withLock(mediaDir(projectDir), () => {
|
|
176
|
+
const re = new RegExp(`^${type}_(\\d+)`);
|
|
177
|
+
let max = 0;
|
|
178
|
+
for (const r of readManifest(projectDir)) {
|
|
179
|
+
if (r.type !== type) continue;
|
|
180
|
+
const m = r.id?.match(re);
|
|
181
|
+
if (m) max = Math.max(max, parseInt(m[1], 10));
|
|
182
|
+
}
|
|
183
|
+
for (const f of readdirSync(typeDir)) {
|
|
184
|
+
const m = f.match(re);
|
|
185
|
+
if (m) max = Math.max(max, parseInt(m[1], 10)); // skip ids reserved but not yet appended
|
|
186
|
+
}
|
|
187
|
+
const id = `${type}_${String(max + 1).padStart(3, "0")}`;
|
|
188
|
+
const localPath = `.media/${typeSubdir(type)}/${id}${ext}`;
|
|
189
|
+
writeFileSync(join(projectDir, localPath), "", { flag: "wx" }); // durable reservation
|
|
190
|
+
return { id, localPath };
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function reservedFile(projectDir, type, ext) {
|
|
195
|
+
const allocation = allocateId(projectDir, type, ext);
|
|
196
|
+
return { ...allocation, fullPath: join(projectDir, allocation.localPath) };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function rollbackReservation(reservation) {
|
|
200
|
+
rmSync(reservation.fullPath, { force: true });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// A reservation is committed only when populate returns a non-null value.
|
|
204
|
+
// Throwing/rejecting or returning null means no usable asset was produced, so
|
|
205
|
+
// the placeholder must be released. Keeping this transaction beside allocateId
|
|
206
|
+
// prevents individual provider/cache/LUT paths from forgetting the rollback.
|
|
207
|
+
export function withReservedFileSync(projectDir, type, ext, populate) {
|
|
208
|
+
const reservation = reservedFile(projectDir, type, ext);
|
|
209
|
+
try {
|
|
210
|
+
const result = populate(reservation);
|
|
211
|
+
if (result == null) rollbackReservation(reservation);
|
|
212
|
+
return result;
|
|
213
|
+
} catch (error) {
|
|
214
|
+
rollbackReservation(reservation);
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export async function withReservedFile(projectDir, type, ext, populate) {
|
|
220
|
+
const reservation = reservedFile(projectDir, type, ext);
|
|
221
|
+
try {
|
|
222
|
+
const result = await populate(reservation);
|
|
223
|
+
if (result == null) rollbackReservation(reservation);
|
|
224
|
+
return result;
|
|
225
|
+
} catch (error) {
|
|
226
|
+
rollbackReservation(reservation);
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
}
|