dsh-hyperframes 0.10.1 → 0.11.0
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/CHANGELOG.md +13 -0
- package/README.en.md +4 -3
- package/README.md +4 -3
- package/docs/WORKBENCH.md +6 -0
- package/lib/client.js +40 -2
- package/lib/media-templates.d.ts +1 -1
- package/lib/media-templates.js +1 -1
- package/package.json +2 -2
- package/plugin.json +1 -1
- package/skills/NOTICE.md +1 -1
- package/skills/UPSTREAM.json +93 -8
- package/skills/embedded-captions/SKILL.md +1 -1
- package/skills/faceless-explainer/SKILL.md +1 -1
- package/skills/general-video/SKILL.md +1 -1
- package/skills/hyperframes-animation/SKILL.md +1 -1
- package/skills/hyperframes-audio/SKILL.md +1 -1
- package/skills/hyperframes-core/SKILL.md +1 -1
- package/skills/hyperframes-creative/SKILL.md +1 -1
- package/skills/hyperframes-keyframes/SKILL.md +1 -1
- package/skills/hyperframes-studio/SKILL.md +1 -1
- package/skills/media-use/scripts/lib/adopt.mjs +128 -0
- package/skills/media-use/scripts/lib/adopt.test.mjs +95 -0
- package/skills/media-use/scripts/lib/bgm-provider.mjs +20 -0
- package/skills/media-use/scripts/lib/brand-provider.mjs +59 -0
- package/skills/media-use/scripts/lib/bundled-sfx-provider.mjs +119 -0
- package/skills/media-use/scripts/lib/bundled-sfx-provider.test.mjs +139 -0
- package/skills/media-use/scripts/lib/cache.mjs +162 -0
- package/skills/media-use/scripts/lib/cache.test.mjs +110 -0
- package/skills/media-use/scripts/lib/candidates.mjs +91 -0
- package/skills/media-use/scripts/lib/candidates.test.mjs +155 -0
- package/skills/media-use/scripts/lib/codex-provider.mjs +161 -0
- package/skills/media-use/scripts/lib/codex-provider.test.mjs +34 -0
- package/skills/media-use/scripts/lib/config-lock.d.mts +3 -0
- package/skills/media-use/scripts/lib/config-lock.test.mjs +79 -0
- package/skills/media-use/scripts/lib/coverage.test.mjs +136 -0
- package/skills/media-use/scripts/lib/cube-build.mjs +181 -0
- package/skills/media-use/scripts/lib/cube-build.test.mjs +80 -0
- package/skills/media-use/scripts/lib/cube-validate.mjs +197 -0
- package/skills/media-use/scripts/lib/cube-validate.test.mjs +125 -0
- package/skills/media-use/scripts/lib/cutlist.test.mjs +174 -0
- package/skills/media-use/scripts/lib/duck.test.mjs +319 -0
- package/skills/media-use/scripts/lib/error-diffusion.test.mjs +102 -0
- package/skills/media-use/scripts/lib/ff-binaries.mjs +37 -0
- package/skills/media-use/scripts/lib/freeze.mjs +72 -0
- package/skills/media-use/scripts/lib/freeze.test.mjs +138 -0
- package/skills/media-use/scripts/lib/grade-analyzer.mjs +277 -0
- package/skills/media-use/scripts/lib/grade-analyzer.test.mjs +285 -0
- package/skills/media-use/scripts/lib/heygen-cli.mjs +208 -0
- package/skills/media-use/scripts/lib/heygen-cli.test.mjs +315 -0
- package/skills/media-use/scripts/lib/heygen-search.mjs +45 -0
- package/skills/media-use/scripts/lib/heygen-search.test.mjs +49 -0
- package/skills/media-use/scripts/lib/heygen-video-provider.mjs +138 -0
- package/skills/media-use/scripts/lib/heygen-video-provider.test.mjs +435 -0
- package/skills/media-use/scripts/lib/image-provider.mjs +44 -0
- package/skills/media-use/scripts/lib/local-media-search.mjs +32 -0
- package/skills/media-use/scripts/lib/local-models.mjs +318 -0
- package/skills/media-use/scripts/lib/local-models.test.mjs +220 -0
- package/skills/media-use/scripts/lib/local-run.mjs +78 -0
- package/skills/media-use/scripts/lib/local-run.test.mjs +121 -0
- package/skills/media-use/scripts/lib/logo-provider.mjs +244 -0
- package/skills/media-use/scripts/lib/logo-provider.test.mjs +298 -0
- package/skills/media-use/scripts/lib/ltx-video-provider.mjs +94 -0
- package/skills/media-use/scripts/lib/ltx-video-provider.test.mjs +316 -0
- package/skills/media-use/scripts/lib/lut-preset-provider.mjs +271 -0
- package/skills/media-use/scripts/lib/lut-preset-provider.test.mjs +199 -0
- package/skills/media-use/scripts/lib/main-module.mjs +13 -0
- package/skills/media-use/scripts/lib/manifest.d.mts +20 -0
- package/skills/media-use/scripts/lib/manifest.test.mjs +397 -0
- package/skills/media-use/scripts/lib/match.mjs +47 -0
- package/skills/media-use/scripts/lib/media-fetch.test.mjs +197 -0
- package/skills/media-use/scripts/lib/media-home.test.mjs +61 -0
- package/skills/media-use/scripts/lib/media-search.d.mts +9 -0
- package/skills/media-use/scripts/lib/media-search.mjs +107 -0
- package/skills/media-use/scripts/lib/mflux-provider.mjs +128 -0
- package/skills/media-use/scripts/lib/mflux-provider.test.mjs +174 -0
- package/skills/media-use/scripts/lib/misses.mjs +49 -0
- package/skills/media-use/scripts/lib/misses.test.mjs +89 -0
- package/skills/media-use/scripts/lib/npx-sync.mjs +2 -28
- package/skills/media-use/scripts/lib/npx-sync.test.mjs +50 -0
- package/skills/media-use/scripts/lib/openverse.mjs +134 -0
- package/skills/media-use/scripts/lib/openverse.test.mjs +219 -0
- package/skills/media-use/scripts/lib/parakeet-words.test.mjs +44 -0
- package/skills/media-use/scripts/lib/prefs-store.concurrent.test.mjs +173 -0
- package/skills/media-use/scripts/lib/prefs-store.test.mjs +245 -0
- package/skills/media-use/scripts/lib/probe.mjs +46 -0
- package/skills/media-use/scripts/lib/probe.test.mjs +221 -0
- package/skills/media-use/scripts/lib/providers.mjs +5 -0
- package/skills/media-use/scripts/lib/recipe-store.test.mjs +391 -0
- package/skills/media-use/scripts/lib/registry.mjs +214 -0
- package/skills/media-use/scripts/lib/registry.test.mjs +262 -0
- package/skills/media-use/scripts/lib/search.mjs +24 -0
- package/skills/media-use/scripts/lib/search.test.mjs +32 -0
- package/skills/media-use/scripts/lib/sfx-provider.mjs +33 -0
- package/skills/media-use/scripts/lib/specs.mjs +132 -0
- package/skills/media-use/scripts/lib/specs.test.mjs +157 -0
- package/skills/media-use/scripts/lib/stats.mjs +117 -0
- package/skills/media-use/scripts/lib/stats.test.mjs +165 -0
- package/skills/media-use/scripts/lib/svg-sanitize.mjs +21 -0
- package/skills/media-use/scripts/lib/telemetry.test.mjs +437 -53
- package/skills/media-use/scripts/lib/test-home.test.mjs +10 -0
- package/skills/media-use/scripts/lib/transcriptCutFade.test.mjs +33 -0
- package/skills/media-use/scripts/lib/tts-local-provider.mjs +94 -0
- package/skills/media-use/scripts/lib/tts-local-provider.test.mjs +72 -0
- package/skills/media-use/scripts/lib/usage.mjs +57 -0
- package/skills/media-use/scripts/lib/usage.test.mjs +54 -0
- package/skills/media-use/scripts/lib/voice-provider.mjs +57 -0
- package/skills/media-use/scripts/lib/voice-provider.test.mjs +46 -0
- package/skills/media-use/scripts/resolve.mjs +1360 -27
- package/skills/motion-graphics/SKILL.md +1 -1
- package/skills/music-to-video/SKILL.md +1 -1
- package/skills/pr-to-video/SKILL.md +1 -1
- package/skills/product-launch-video/SKILL.md +1 -1
- package/skills/talking-head-recut/SKILL.md +1 -1
|
@@ -1,73 +1,457 @@
|
|
|
1
|
-
import assert from "node:assert
|
|
2
|
-
import {
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
1
|
+
import { strict as assert } from "node:assert";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
5
3
|
import { test } from "node:test";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
4
|
+
import {
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
mkdtempSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
chmodSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { __anonymousIdForTest, __resetTelemetryForTest, optedOut, track } from "./telemetry.mjs";
|
|
16
|
+
|
|
17
|
+
function sandbox() {
|
|
18
|
+
const root = mkdtempSync(join(tmpdir(), "mu-telemetry-"));
|
|
19
|
+
const home = join(root, "home");
|
|
20
|
+
mkdirSync(home, { recursive: true });
|
|
21
|
+
process.env.HOME = home;
|
|
22
|
+
process.env.HYPERFRAMES_MEDIA_HOME = home;
|
|
23
|
+
return { root, home };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function restoreEnv(saved) {
|
|
27
|
+
for (const k of Object.keys(process.env)) if (!(k in saved)) delete process.env[k];
|
|
28
|
+
Object.assign(process.env, saved);
|
|
30
29
|
}
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
for (const
|
|
34
|
-
|
|
31
|
+
function withoutTelemetryOptOut() {
|
|
32
|
+
for (const k of ["DO_NOT_TRACK", "HYPERFRAMES_NO_TELEMETRY", "CI", "NODE_ENV"])
|
|
33
|
+
delete process.env[k];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseFetchBodies(calls) {
|
|
37
|
+
return calls.flatMap((call) => JSON.parse(call.options.body).batch);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
test("optedOut respects DO_NOT_TRACK / HYPERFRAMES_NO_TELEMETRY / CI", () => {
|
|
41
|
+
const saved = { ...process.env };
|
|
42
|
+
try {
|
|
43
|
+
for (const k of ["DO_NOT_TRACK", "HYPERFRAMES_NO_TELEMETRY", "CI", "NODE_ENV"])
|
|
44
|
+
delete process.env[k];
|
|
45
|
+
assert.equal(optedOut(), false, "default: tracking allowed");
|
|
46
|
+
process.env.DO_NOT_TRACK = "1";
|
|
47
|
+
assert.equal(optedOut(), true, "DO_NOT_TRACK opts out");
|
|
48
|
+
delete process.env.DO_NOT_TRACK;
|
|
49
|
+
process.env.HYPERFRAMES_NO_TELEMETRY = "1";
|
|
50
|
+
assert.equal(optedOut(), true, "HYPERFRAMES_NO_TELEMETRY opts out");
|
|
51
|
+
delete process.env.HYPERFRAMES_NO_TELEMETRY;
|
|
52
|
+
process.env.CI = "true";
|
|
53
|
+
assert.equal(optedOut(), true, "CI opts out");
|
|
54
|
+
} finally {
|
|
55
|
+
for (const k of Object.keys(process.env)) if (!(k in saved)) delete process.env[k];
|
|
56
|
+
Object.assign(process.env, saved);
|
|
57
|
+
}
|
|
35
58
|
});
|
|
36
59
|
|
|
37
|
-
test
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
60
|
+
test("optedOut honors telemetryEnabled: false from the shared config", () => {
|
|
61
|
+
const savedEnv = { ...process.env };
|
|
62
|
+
const { root, home } = sandbox();
|
|
63
|
+
try {
|
|
64
|
+
withoutTelemetryOptOut();
|
|
65
|
+
const configPath = join(home, ".hyperframes", "config.json");
|
|
66
|
+
assert.equal(optedOut(), false, "no config: tracking allowed");
|
|
67
|
+
mkdirSync(join(home, ".hyperframes"), { recursive: true });
|
|
68
|
+
writeFileSync(configPath, JSON.stringify({ telemetryEnabled: false }));
|
|
69
|
+
assert.equal(optedOut(), true, "telemetryEnabled: false opts out");
|
|
70
|
+
writeFileSync(configPath, JSON.stringify({ telemetryEnabled: true }));
|
|
71
|
+
assert.equal(optedOut(), false, "telemetryEnabled: true allows tracking");
|
|
72
|
+
writeFileSync(configPath, "{not json");
|
|
73
|
+
assert.equal(optedOut(), false, "unreadable config: tracking allowed");
|
|
74
|
+
} finally {
|
|
75
|
+
restoreEnv(savedEnv);
|
|
76
|
+
rmSync(root, { recursive: true, force: true });
|
|
41
77
|
}
|
|
42
|
-
rmSync(home, { recursive: true, force: true });
|
|
43
78
|
});
|
|
44
79
|
|
|
45
|
-
test("
|
|
46
|
-
|
|
47
|
-
|
|
80
|
+
test("track is a no-op (no network, resolves) when opted out", async () => {
|
|
81
|
+
const savedEnv = { ...process.env };
|
|
82
|
+
const originalFetch = globalThis.fetch;
|
|
83
|
+
const { root, home } = sandbox();
|
|
84
|
+
const calls = [];
|
|
85
|
+
globalThis.fetch = async (...args) => {
|
|
86
|
+
calls.push(args);
|
|
87
|
+
return { ok: true };
|
|
88
|
+
};
|
|
89
|
+
process.env.DO_NOT_TRACK = "1";
|
|
90
|
+
try {
|
|
91
|
+
// must resolve immediately without throwing or hitting the network
|
|
92
|
+
await track("media_use_resolve", { type: "bgm", source: "search" });
|
|
93
|
+
assert.equal(calls.length, 0);
|
|
94
|
+
assert.equal(existsSync(join(home, ".hyperframes/config.json")), false);
|
|
95
|
+
assert.equal(existsSync(join(home, ".media/telemetry-notice-shown")), false);
|
|
96
|
+
} finally {
|
|
97
|
+
globalThis.fetch = originalFetch;
|
|
98
|
+
restoreEnv(savedEnv);
|
|
99
|
+
rmSync(root, { recursive: true, force: true });
|
|
100
|
+
__resetTelemetryForTest();
|
|
101
|
+
}
|
|
48
102
|
});
|
|
49
103
|
|
|
50
|
-
test("
|
|
51
|
-
|
|
52
|
-
|
|
104
|
+
test("anonymous id uses the shared hyperframes config", () => {
|
|
105
|
+
const savedEnv = { ...process.env };
|
|
106
|
+
const { root, home } = sandbox();
|
|
107
|
+
try {
|
|
108
|
+
withoutTelemetryOptOut();
|
|
109
|
+
mkdirSync(join(home, ".hyperframes"), { recursive: true });
|
|
110
|
+
writeFileSync(
|
|
111
|
+
join(home, ".hyperframes/config.json"),
|
|
112
|
+
JSON.stringify({ anonymousId: "shared-install-id", keep: true }),
|
|
113
|
+
);
|
|
114
|
+
mkdirSync(join(home, ".media"), { recursive: true });
|
|
115
|
+
writeFileSync(join(home, ".media/anon-id"), "old-media-id");
|
|
116
|
+
|
|
117
|
+
assert.equal(__anonymousIdForTest(), "shared-install-id");
|
|
118
|
+
} finally {
|
|
119
|
+
restoreEnv(savedEnv);
|
|
120
|
+
rmSync(root, { recursive: true, force: true });
|
|
121
|
+
__resetTelemetryForTest();
|
|
122
|
+
}
|
|
53
123
|
});
|
|
54
124
|
|
|
55
|
-
test("
|
|
56
|
-
|
|
57
|
-
|
|
125
|
+
test("anonymous id seeds missing config once and reuses it", () => {
|
|
126
|
+
const savedEnv = { ...process.env };
|
|
127
|
+
const { root, home } = sandbox();
|
|
128
|
+
try {
|
|
129
|
+
withoutTelemetryOptOut();
|
|
130
|
+
const first = __anonymousIdForTest();
|
|
131
|
+
const configPath = join(home, ".hyperframes/config.json");
|
|
132
|
+
assert.ok(existsSync(configPath));
|
|
133
|
+
assert.match(
|
|
134
|
+
first,
|
|
135
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
|
136
|
+
);
|
|
137
|
+
const second = __anonymousIdForTest();
|
|
138
|
+
assert.equal(second, first);
|
|
139
|
+
assert.equal(JSON.parse(readFileSync(configPath, "utf8")).anonymousId, first);
|
|
140
|
+
} finally {
|
|
141
|
+
restoreEnv(savedEnv);
|
|
142
|
+
rmSync(root, { recursive: true, force: true });
|
|
143
|
+
__resetTelemetryForTest();
|
|
144
|
+
}
|
|
58
145
|
});
|
|
59
146
|
|
|
60
|
-
test("
|
|
61
|
-
|
|
62
|
-
|
|
147
|
+
test("anonymous id adopts a legacy ~/.media/anon-id on upgrade (persona continuity)", () => {
|
|
148
|
+
const savedEnv = { ...process.env };
|
|
149
|
+
const { root, home } = sandbox();
|
|
150
|
+
try {
|
|
151
|
+
withoutTelemetryOptOut();
|
|
152
|
+
mkdirSync(join(home, ".media"), { recursive: true });
|
|
153
|
+
writeFileSync(join(home, ".media/anon-id"), "legacy-media-id");
|
|
154
|
+
// no ~/.hyperframes/config.json yet — the old media-use-only id must carry over
|
|
155
|
+
assert.equal(__anonymousIdForTest(), "legacy-media-id");
|
|
156
|
+
// and it is persisted into the shared config so CLI/studio see the same id
|
|
157
|
+
assert.equal(
|
|
158
|
+
JSON.parse(readFileSync(join(home, ".hyperframes/config.json"), "utf8")).anonymousId,
|
|
159
|
+
"legacy-media-id",
|
|
160
|
+
);
|
|
161
|
+
} finally {
|
|
162
|
+
restoreEnv(savedEnv);
|
|
163
|
+
rmSync(root, { recursive: true, force: true });
|
|
164
|
+
__resetTelemetryForTest();
|
|
165
|
+
}
|
|
63
166
|
});
|
|
64
167
|
|
|
65
|
-
test("
|
|
66
|
-
|
|
67
|
-
|
|
168
|
+
test("track identifies a signed-in HeyGen account once and still sends events", async () => {
|
|
169
|
+
const savedEnv = { ...process.env };
|
|
170
|
+
const originalFetch = globalThis.fetch;
|
|
171
|
+
const { root, home } = sandbox();
|
|
172
|
+
const calls = [];
|
|
173
|
+
globalThis.fetch = async (url, options) => {
|
|
174
|
+
calls.push({ url, options });
|
|
175
|
+
return { ok: true };
|
|
176
|
+
};
|
|
68
177
|
try {
|
|
69
|
-
|
|
178
|
+
withoutTelemetryOptOut();
|
|
179
|
+
mkdirSync(join(home, ".hyperframes"), { recursive: true });
|
|
180
|
+
writeFileSync(
|
|
181
|
+
join(home, ".hyperframes/config.json"),
|
|
182
|
+
JSON.stringify({ anonymousId: "anon-1" }),
|
|
183
|
+
);
|
|
184
|
+
mkdirSync(join(home, ".heygen"), { recursive: true });
|
|
185
|
+
writeFileSync(
|
|
186
|
+
join(home, ".heygen/credentials"),
|
|
187
|
+
JSON.stringify({ user: { email: "alice@example.com", username: "alice" } }),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
await track("media_use_resolve", { type: "bgm", source: "search" });
|
|
191
|
+
await track("media_use_resolve", { type: "image", source: "generated" });
|
|
192
|
+
|
|
193
|
+
const batch = parseFetchBodies(calls);
|
|
194
|
+
const identify = batch.filter((item) => item.event === "$identify");
|
|
195
|
+
assert.equal(identify.length, 1);
|
|
196
|
+
assert.equal(identify[0].distinct_id, "alice@example.com");
|
|
197
|
+
assert.equal(identify[0].properties.$anon_distinct_id, "anon-1");
|
|
198
|
+
assert.equal(batch.filter((item) => item.event === "media_use_resolve").length, 2);
|
|
70
199
|
} finally {
|
|
71
|
-
|
|
200
|
+
globalThis.fetch = originalFetch;
|
|
201
|
+
restoreEnv(savedEnv);
|
|
202
|
+
rmSync(root, { recursive: true, force: true });
|
|
203
|
+
__resetTelemetryForTest();
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("track identifies with a lowercased email regardless of stored casing", async () => {
|
|
208
|
+
const savedEnv = { ...process.env };
|
|
209
|
+
const originalFetch = globalThis.fetch;
|
|
210
|
+
const { root, home } = sandbox();
|
|
211
|
+
const calls = [];
|
|
212
|
+
globalThis.fetch = async (url, options) => {
|
|
213
|
+
calls.push({ url, options });
|
|
214
|
+
return { ok: true };
|
|
215
|
+
};
|
|
216
|
+
try {
|
|
217
|
+
withoutTelemetryOptOut();
|
|
218
|
+
mkdirSync(join(home, ".hyperframes"), { recursive: true });
|
|
219
|
+
writeFileSync(
|
|
220
|
+
join(home, ".hyperframes/config.json"),
|
|
221
|
+
JSON.stringify({ anonymousId: "anon-3" }),
|
|
222
|
+
);
|
|
223
|
+
mkdirSync(join(home, ".heygen"), { recursive: true });
|
|
224
|
+
writeFileSync(
|
|
225
|
+
join(home, ".heygen/credentials"),
|
|
226
|
+
JSON.stringify({ user: { email: "Alice@Example.com", username: "alice" } }),
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
await track("media_use_resolve", { type: "bgm", source: "search" });
|
|
230
|
+
|
|
231
|
+
const batch = parseFetchBodies(calls);
|
|
232
|
+
const identify = batch.filter((item) => item.event === "$identify");
|
|
233
|
+
assert.equal(identify.length, 1);
|
|
234
|
+
// Lowercased so this joins with heygen-cli's own identify call regardless
|
|
235
|
+
// of the account's stored email casing -- otherwise the same person could
|
|
236
|
+
// split into two PostHog profiles by email case alone.
|
|
237
|
+
assert.equal(identify[0].distinct_id, "alice@example.com");
|
|
238
|
+
} finally {
|
|
239
|
+
globalThis.fetch = originalFetch;
|
|
240
|
+
restoreEnv(savedEnv);
|
|
241
|
+
rmSync(root, { recursive: true, force: true });
|
|
242
|
+
__resetTelemetryForTest();
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("track does not identify when signed out", async () => {
|
|
247
|
+
const savedEnv = { ...process.env };
|
|
248
|
+
const originalFetch = globalThis.fetch;
|
|
249
|
+
const { root, home } = sandbox();
|
|
250
|
+
const calls = [];
|
|
251
|
+
globalThis.fetch = async (url, options) => {
|
|
252
|
+
calls.push({ url, options });
|
|
253
|
+
return { ok: true };
|
|
254
|
+
};
|
|
255
|
+
try {
|
|
256
|
+
withoutTelemetryOptOut();
|
|
257
|
+
mkdirSync(join(home, ".hyperframes"), { recursive: true });
|
|
258
|
+
writeFileSync(
|
|
259
|
+
join(home, ".hyperframes/config.json"),
|
|
260
|
+
JSON.stringify({ anonymousId: "anon-2" }),
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
await track("media_use_resolve", { type: "bgm", source: "search" });
|
|
264
|
+
|
|
265
|
+
const batch = parseFetchBodies(calls);
|
|
266
|
+
assert.equal(
|
|
267
|
+
batch.some((item) => item.event === "$identify"),
|
|
268
|
+
false,
|
|
269
|
+
);
|
|
270
|
+
assert.equal(batch[0].event, "media_use_resolve");
|
|
271
|
+
assert.equal(batch[0].distinct_id, "anon-2");
|
|
272
|
+
} finally {
|
|
273
|
+
globalThis.fetch = originalFetch;
|
|
274
|
+
restoreEnv(savedEnv);
|
|
275
|
+
rmSync(root, { recursive: true, force: true });
|
|
276
|
+
__resetTelemetryForTest();
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("first run notice prints to stderr once and never stdout", async () => {
|
|
281
|
+
const savedEnv = { ...process.env };
|
|
282
|
+
const originalFetch = globalThis.fetch;
|
|
283
|
+
const originalError = console.error;
|
|
284
|
+
const originalLog = console.log;
|
|
285
|
+
const { root, home } = sandbox();
|
|
286
|
+
const stderr = [];
|
|
287
|
+
const stdout = [];
|
|
288
|
+
globalThis.fetch = async () => ({ ok: true });
|
|
289
|
+
console.error = (...args) => stderr.push(args.join(" "));
|
|
290
|
+
console.log = (...args) => stdout.push(args.join(" "));
|
|
291
|
+
try {
|
|
292
|
+
withoutTelemetryOptOut();
|
|
293
|
+
await track("media_use_resolve", { type: "bgm" });
|
|
294
|
+
await track("media_use_resolve", { type: "sfx" });
|
|
295
|
+
|
|
296
|
+
assert.equal(stderr.length, 1);
|
|
297
|
+
assert.match(stderr[0], /media-use sends usage telemetry/);
|
|
298
|
+
assert.equal(stdout.length, 0);
|
|
299
|
+
// notice-shown lives in the shared config (config.telemetryNoticeShown), so
|
|
300
|
+
// the CLI and media-use show it once per person — not a media-use-only marker.
|
|
301
|
+
assert.equal(
|
|
302
|
+
JSON.parse(readFileSync(join(home, ".hyperframes/config.json"), "utf8")).telemetryNoticeShown,
|
|
303
|
+
true,
|
|
304
|
+
);
|
|
305
|
+
} finally {
|
|
306
|
+
globalThis.fetch = originalFetch;
|
|
307
|
+
console.error = originalError;
|
|
308
|
+
console.log = originalLog;
|
|
309
|
+
restoreEnv(savedEnv);
|
|
310
|
+
rmSync(root, { recursive: true, force: true });
|
|
311
|
+
__resetTelemetryForTest();
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("warns once on stderr when MEDIA_USE_TELEMETRY_HOST is set outside a test/CI context", async () => {
|
|
316
|
+
const savedEnv = { ...process.env };
|
|
317
|
+
const originalFetch = globalThis.fetch;
|
|
318
|
+
const originalError = console.error;
|
|
319
|
+
const { root } = sandbox();
|
|
320
|
+
const stderr = [];
|
|
321
|
+
globalThis.fetch = async () => ({ ok: true });
|
|
322
|
+
console.error = (...args) => stderr.push(args.join(" "));
|
|
323
|
+
try {
|
|
324
|
+
// No opt-out, no CI/NODE_ENV signal — mirrors a real user's shell where
|
|
325
|
+
// this test-only var leaked in by accident and tracking is not disabled.
|
|
326
|
+
withoutTelemetryOptOut();
|
|
327
|
+
process.env.MEDIA_USE_TELEMETRY_HOST = "http://127.0.0.1:1"; // nothing listens; irrelevant here
|
|
328
|
+
await track("media_use_resolve", { type: "bgm" });
|
|
329
|
+
await track("media_use_resolve", { type: "bgm" });
|
|
330
|
+
|
|
331
|
+
const warnings = stderr.filter((line) => line.includes("MEDIA_USE_TELEMETRY_HOST"));
|
|
332
|
+
assert.equal(warnings.length, 1, "expected exactly one warning across repeated calls");
|
|
333
|
+
} finally {
|
|
334
|
+
globalThis.fetch = originalFetch;
|
|
335
|
+
console.error = originalError;
|
|
336
|
+
restoreEnv(savedEnv);
|
|
337
|
+
rmSync(root, { recursive: true, force: true });
|
|
338
|
+
__resetTelemetryForTest();
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("does not warn when MEDIA_USE_TELEMETRY_HOST is set the way the U7 interception test sets it", async () => {
|
|
343
|
+
const savedEnv = { ...process.env };
|
|
344
|
+
const originalFetch = globalThis.fetch;
|
|
345
|
+
const originalError = console.error;
|
|
346
|
+
const { root } = sandbox();
|
|
347
|
+
const stderr = [];
|
|
348
|
+
globalThis.fetch = async () => ({ ok: true });
|
|
349
|
+
console.error = (...args) => stderr.push(args.join(" "));
|
|
350
|
+
try {
|
|
351
|
+
withoutTelemetryOptOut();
|
|
352
|
+
// Same env shape resolve.test.mjs's U7 test uses to allow tracking through:
|
|
353
|
+
// DO_NOT_TRACK=0, CI unset/empty, NODE_ENV=test.
|
|
354
|
+
process.env.DO_NOT_TRACK = "0";
|
|
355
|
+
process.env.CI = "";
|
|
356
|
+
process.env.NODE_ENV = "test";
|
|
357
|
+
process.env.MEDIA_USE_TELEMETRY_HOST = "http://127.0.0.1:1";
|
|
358
|
+
await track("media_use_resolve", { type: "bgm" });
|
|
359
|
+
|
|
360
|
+
assert.equal(
|
|
361
|
+
stderr.some((line) => line.includes("MEDIA_USE_TELEMETRY_HOST")),
|
|
362
|
+
false,
|
|
363
|
+
"the U7 test's own use of the override must not print a spurious warning",
|
|
364
|
+
);
|
|
365
|
+
} finally {
|
|
366
|
+
globalThis.fetch = originalFetch;
|
|
367
|
+
console.error = originalError;
|
|
368
|
+
restoreEnv(savedEnv);
|
|
369
|
+
rmSync(root, { recursive: true, force: true });
|
|
370
|
+
__resetTelemetryForTest();
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test("read-only telemetry state degrades without throwing", async () => {
|
|
375
|
+
const savedEnv = { ...process.env };
|
|
376
|
+
const originalFetch = globalThis.fetch;
|
|
377
|
+
const { root, home } = sandbox();
|
|
378
|
+
globalThis.fetch = async () => ({ ok: true });
|
|
379
|
+
try {
|
|
380
|
+
withoutTelemetryOptOut();
|
|
381
|
+
writeFileSync(join(home, ".hyperframes"), "not a directory");
|
|
382
|
+
writeFileSync(join(home, ".media"), "not a directory");
|
|
383
|
+
await track("media_use_resolve", { type: "bgm" });
|
|
384
|
+
} finally {
|
|
385
|
+
globalThis.fetch = originalFetch;
|
|
386
|
+
try {
|
|
387
|
+
chmodSync(join(home, ".hyperframes"), 0o600);
|
|
388
|
+
} catch {
|
|
389
|
+
// best effort for cleanup on platforms with different chmod behavior
|
|
390
|
+
}
|
|
391
|
+
restoreEnv(savedEnv);
|
|
392
|
+
rmSync(root, { recursive: true, force: true });
|
|
393
|
+
__resetTelemetryForTest();
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
async function trackQuietly(home) {
|
|
398
|
+
const originalFetch = globalThis.fetch;
|
|
399
|
+
const originalError = console.error;
|
|
400
|
+
globalThis.fetch = async () => ({ ok: true });
|
|
401
|
+
console.error = () => {};
|
|
402
|
+
try {
|
|
403
|
+
withoutTelemetryOptOut();
|
|
404
|
+
await track("media_use_resolve", { type: "bgm" });
|
|
405
|
+
return readFileSync(join(home, ".hyperframes/config.json"), "utf8");
|
|
406
|
+
} finally {
|
|
407
|
+
globalThis.fetch = originalFetch;
|
|
408
|
+
console.error = originalError;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
test("waits for another process's locked settings write, then keeps the no it saved", async () => {
|
|
413
|
+
const savedEnv = { ...process.env };
|
|
414
|
+
const { root, home } = sandbox();
|
|
415
|
+
const dir = join(home, ".hyperframes");
|
|
416
|
+
mkdirSync(dir, { recursive: true });
|
|
417
|
+
const holder = spawn(process.execPath, [
|
|
418
|
+
"-e",
|
|
419
|
+
`const fs = require("fs");
|
|
420
|
+
const dir = process.argv[1];
|
|
421
|
+
fs.writeFileSync(dir + "/config.json.lock", "", { flag: "wx" });
|
|
422
|
+
process.stdout.write("locked\\n");
|
|
423
|
+
setTimeout(() => {
|
|
424
|
+
fs.writeFileSync(dir + "/other.tmp", JSON.stringify({ localEmbeddingEnabled: false }));
|
|
425
|
+
fs.renameSync(dir + "/other.tmp", dir + "/config.json");
|
|
426
|
+
fs.rmSync(dir + "/config.json.lock");
|
|
427
|
+
}, 1000);`,
|
|
428
|
+
dir,
|
|
429
|
+
]);
|
|
430
|
+
const exited = new Promise((done) => holder.on("exit", done));
|
|
431
|
+
try {
|
|
432
|
+
await new Promise((locked) => holder.stdout.once("data", locked));
|
|
433
|
+
const saved = JSON.parse(await trackQuietly(home));
|
|
434
|
+
await exited;
|
|
435
|
+
assert.equal(saved.localEmbeddingEnabled, false);
|
|
436
|
+
assert.equal(saved.telemetryNoticeShown, true);
|
|
437
|
+
assert.equal(typeof saved.anonymousId, "string");
|
|
438
|
+
} finally {
|
|
439
|
+
restoreEnv(savedEnv);
|
|
440
|
+
rmSync(root, { recursive: true, force: true });
|
|
441
|
+
__resetTelemetryForTest();
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
test("leaves a settings file it cannot read untouched", async () => {
|
|
446
|
+
const savedEnv = { ...process.env };
|
|
447
|
+
const { root, home } = sandbox();
|
|
448
|
+
mkdirSync(join(home, ".hyperframes"), { recursive: true });
|
|
449
|
+
writeFileSync(join(home, ".hyperframes/config.json"), "{not json");
|
|
450
|
+
try {
|
|
451
|
+
assert.equal(await trackQuietly(home), "{not json");
|
|
452
|
+
} finally {
|
|
453
|
+
restoreEnv(savedEnv);
|
|
454
|
+
rmSync(root, { recursive: true, force: true });
|
|
455
|
+
__resetTelemetryForTest();
|
|
72
456
|
}
|
|
73
457
|
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { homedir, tmpdir, userInfo } from "node:os";
|
|
4
|
+
import { sep } from "node:path";
|
|
5
|
+
import { test } from "node:test";
|
|
6
|
+
|
|
7
|
+
test("the skills test run has a home folder of its own, not the user's", () => {
|
|
8
|
+
assert.ok(realpathSync(homedir()).startsWith(realpathSync(tmpdir()) + sep));
|
|
9
|
+
assert.notEqual(homedir(), userInfo().homedir);
|
|
10
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { fadeFilterFor } from "./transcriptCutFade.mjs";
|
|
4
|
+
|
|
5
|
+
test("fades in only at the first segment's trailing splice", () => {
|
|
6
|
+
assert.equal(fadeFilterFor(2, { fadeIn: false, fadeOut: true }), "afade=t=out:st=1.97:d=0.03");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("fades both edges of a middle segment's two splices", () => {
|
|
10
|
+
assert.equal(
|
|
11
|
+
fadeFilterFor(2, { fadeIn: true, fadeOut: true }),
|
|
12
|
+
"afade=t=in:st=0:d=0.03,afade=t=out:st=1.97:d=0.03",
|
|
13
|
+
);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("fades out only at the last segment's leading splice", () => {
|
|
17
|
+
assert.equal(fadeFilterFor(2, { fadeIn: true, fadeOut: false }), "afade=t=in:st=0:d=0.03");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("a single kept segment borders no splice at all", () => {
|
|
21
|
+
assert.equal(fadeFilterFor(2, { fadeIn: false, fadeOut: false }), null);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("still scales the ramp down on a short segment with both edges faded", () => {
|
|
25
|
+
assert.equal(
|
|
26
|
+
fadeFilterFor(0.08, { fadeIn: true, fadeOut: true }),
|
|
27
|
+
"afade=t=in:st=0:d=0.02,afade=t=out:st=0.06:d=0.02",
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("skips a degenerate segment even when a splice is requested", () => {
|
|
32
|
+
assert.equal(fadeFilterFor(0.005, { fadeIn: true, fadeOut: true }), null);
|
|
33
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, statSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { ffprobeBinary } from "./ff-binaries.mjs";
|
|
6
|
+
import { resolveSpawnCommand } from "../../audio/scripts/lib/tts.mjs";
|
|
7
|
+
|
|
8
|
+
// Local voiceover via the packaged Kokoro-82M TTS (the `hyperframes tts` CLI),
|
|
9
|
+
// the free/private default now that HeyGen TTS costs wallet credits. Kokoro runs
|
|
10
|
+
// on-device (CPU, faster-than-realtime, bundled voices, native word timestamps),
|
|
11
|
+
// so no key and no per-call charge. When Kokoro is not set up, this returns null
|
|
12
|
+
// and the registry falls through to the HeyGen TTS upsell.
|
|
13
|
+
//
|
|
14
|
+
// Delegated to the hyperframes CLI (same as transcribe / remove-background), not
|
|
15
|
+
// re-implemented here. ffprobe reads the duration back for the ledger.
|
|
16
|
+
|
|
17
|
+
function probeDurationSeconds(ffprobe, file) {
|
|
18
|
+
try {
|
|
19
|
+
const out = execFileSync(
|
|
20
|
+
ffprobe,
|
|
21
|
+
["-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", "--", file],
|
|
22
|
+
{ encoding: "utf8", timeout: 15000 },
|
|
23
|
+
);
|
|
24
|
+
const d = parseFloat(String(out).trim());
|
|
25
|
+
return Number.isFinite(d) ? d : undefined;
|
|
26
|
+
} catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// `platform`/`execFn`/`env`/`pathExists` params (defaulting to the real
|
|
32
|
+
// values) exist so tests can exercise the win32 branch without mocking
|
|
33
|
+
// node:child_process (its ESM exports are non-configurable) — same idiom as
|
|
34
|
+
// spawnP in ../../audio/scripts/lib/tts.mjs.
|
|
35
|
+
export async function localTtsGenerate(
|
|
36
|
+
intent,
|
|
37
|
+
ctx,
|
|
38
|
+
platform = process.platform,
|
|
39
|
+
execFn = execFileSync,
|
|
40
|
+
env = process.env,
|
|
41
|
+
pathExists = existsSync,
|
|
42
|
+
) {
|
|
43
|
+
const ffprobe = ffprobeBinary();
|
|
44
|
+
const outPath = join(tmpdir(), `media-use-kokoro-${process.pid}-${Date.now()}.wav`);
|
|
45
|
+
const argv = ["hyperframes", "tts", intent, "--output", outPath];
|
|
46
|
+
if (ctx?.voice) argv.push("--voice", ctx.voice);
|
|
47
|
+
if (ctx?.lang && ctx.lang !== "en") argv.push("--lang", ctx.lang);
|
|
48
|
+
// On Windows a bare "npx" is npx.cmd, which execFileSync cannot exec
|
|
49
|
+
// (spawnSync npx ENOENT) — resolveSpawnCommand reroutes it through
|
|
50
|
+
// node + npx-cli.js, same as the audio engine's TTS spawns.
|
|
51
|
+
const resolved = resolveSpawnCommand(
|
|
52
|
+
"npx",
|
|
53
|
+
argv,
|
|
54
|
+
{ encoding: "utf8", timeout: 300000, stdio: ["ignore", "pipe", "pipe"] },
|
|
55
|
+
platform,
|
|
56
|
+
env,
|
|
57
|
+
pathExists,
|
|
58
|
+
);
|
|
59
|
+
if (!resolved) {
|
|
60
|
+
// npx-on-win32 with no resolvable npx-cli.js — same terminal condition
|
|
61
|
+
// spawnP warns about. Fall through to the next provider rather than crash.
|
|
62
|
+
console.error(
|
|
63
|
+
"media-use: local voice not enabled (kokoro). Cannot run npx on Windows: " +
|
|
64
|
+
"npm's npx-cli.js was not found (install npm with Node, or run via npx/npm run so npm_execpath is set).",
|
|
65
|
+
);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
execFn(resolved.cmd, resolved.args, resolved.opts);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
// `hyperframes tts` prints its "kokoro-onnx not installed" hint to stdout
|
|
72
|
+
// (clack UI), so read both streams and surface the actionable enable-command
|
|
73
|
+
// rather than a bare "Command failed": otherwise resolve silently falls
|
|
74
|
+
// through to the PAID HeyGen TTS upsell when free local voice was one pip away.
|
|
75
|
+
const out = `${err.stdout?.toString() ?? ""}${err.stderr?.toString() ?? ""}`.trim();
|
|
76
|
+
const hint = /not installed|pip install kokoro/i.test(out)
|
|
77
|
+
? "install for free on-device voice: pip install kokoro-onnx soundfile (or set HYPERFRAMES_PYTHON to a venv that has it)"
|
|
78
|
+
: out.slice(-200) || err.message;
|
|
79
|
+
console.error(`media-use: local voice not enabled (kokoro). ${hint}`);
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
if (!existsSync(outPath) || statSync(outPath).size === 0) return null;
|
|
83
|
+
return {
|
|
84
|
+
localPath: outPath,
|
|
85
|
+
ext: ".wav",
|
|
86
|
+
source: "generated",
|
|
87
|
+
metadata: {
|
|
88
|
+
description: intent,
|
|
89
|
+
provider: "kokoro.local",
|
|
90
|
+
duration: probeDurationSeconds(ffprobe, outPath),
|
|
91
|
+
provenance: { engine: "kokoro-82m", prompt: intent },
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|