hyperframes 0.8.58 → 0.8.60
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 +4529 -3220
- package/dist/docs/data-attributes.md +2 -1
- package/dist/fontLocalizeCli.js +96 -16
- 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-Di7GyCOP.js} +3 -3
- package/dist/studio/assets/{index-DKJpHYYd.css → index-CBKb_0TI.css} +1 -1
- package/dist/studio/assets/{index-d8ferjRc.js → index-Dpq2lkOm.js} +190 -190
- package/dist/studio/assets/{index-ZJaRCDYV.js → index-Dx_pt9m4.js} +1 -1
- package/dist/studio/assets/{index-BcE2OSVA.js → index-jAjhQTrB.js} +1 -1
- package/dist/studio/index.d.ts +549 -13
- package/dist/studio/index.html +2 -2
- package/dist/studio/index.js +23133 -21941
- package/dist/studio/index.js.map +1 -1
- package/dist/studio/styles.css +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
|
+
import { readGlobalManifest } from "./cache.mjs";
|
|
3
|
+
import { readManifest } from "./manifest.mjs";
|
|
4
|
+
import { readMisses } from "./misses.mjs";
|
|
5
|
+
|
|
6
|
+
const TOP_MISSES = 5;
|
|
7
|
+
|
|
8
|
+
function emptyReport() {
|
|
9
|
+
return {
|
|
10
|
+
total_resolves: 0,
|
|
11
|
+
by_type: {},
|
|
12
|
+
by_source: {},
|
|
13
|
+
by_provider: {},
|
|
14
|
+
by_via: {},
|
|
15
|
+
misses: 0,
|
|
16
|
+
hit_rate: null,
|
|
17
|
+
top_missed_intents: {},
|
|
18
|
+
global_cache_assets: 0,
|
|
19
|
+
global_cache_disk_bytes: 0,
|
|
20
|
+
cross_project_reuse: 0,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function increment(map, key) {
|
|
25
|
+
if (!key) return;
|
|
26
|
+
map[key] = (map[key] || 0) + 1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function timestampOf(record) {
|
|
30
|
+
return record?.ts || record?.timestamp || record?.created_at || record?.createdAt || null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function inWindow(record, cutoff) {
|
|
34
|
+
if (!cutoff) return true;
|
|
35
|
+
const ts = timestampOf(record);
|
|
36
|
+
// Older manifest records may not carry a timestamp; keep them in the report
|
|
37
|
+
// because --days can only window records/misses that carry a ts/timestamp.
|
|
38
|
+
if (!ts) return true;
|
|
39
|
+
const time = Date.parse(ts);
|
|
40
|
+
return Number.isNaN(time) ? true : time >= cutoff;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function sourceOf(record) {
|
|
44
|
+
return record?._source || record?.source || record?.provenance?.source || "unknown";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeIntent(intent) {
|
|
48
|
+
return String(intent ?? "")
|
|
49
|
+
.trim()
|
|
50
|
+
.toLowerCase()
|
|
51
|
+
.replace(/\s+/g, " ");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function topMissedIntents(misses) {
|
|
55
|
+
const grouped = {};
|
|
56
|
+
for (const miss of misses) {
|
|
57
|
+
const type = miss?.type || "unknown";
|
|
58
|
+
const intent = normalizeIntent(miss?.intent);
|
|
59
|
+
if (!intent) continue;
|
|
60
|
+
grouped[type] ||= {};
|
|
61
|
+
grouped[type][intent] = (grouped[type][intent] || 0) + 1;
|
|
62
|
+
}
|
|
63
|
+
const out = {};
|
|
64
|
+
for (const [type, intents] of Object.entries(grouped)) {
|
|
65
|
+
out[type] = Object.entries(intents)
|
|
66
|
+
.map(([intent, count]) => ({ intent, count }))
|
|
67
|
+
.sort((a, b) => b.count - a.count || a.intent.localeCompare(b.intent))
|
|
68
|
+
.slice(0, TOP_MISSES);
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function diskBytes(records) {
|
|
74
|
+
let total = 0;
|
|
75
|
+
for (const record of records) {
|
|
76
|
+
const p = record?.cached_path || record?.path;
|
|
77
|
+
if (!p) continue;
|
|
78
|
+
try {
|
|
79
|
+
total += statSync(p).size;
|
|
80
|
+
} catch {
|
|
81
|
+
// cache entries can outlive files; stats skips missing files
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return total;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function buildStats({ projectDir, days, now = Date.now() } = {}) {
|
|
88
|
+
// Only a positive finite --days windows the report; null / NaN / <= 0 mean
|
|
89
|
+
// "all time" rather than silently excluding everything (a negative cutoff
|
|
90
|
+
// would land in the future and drop every record). The reads below are each
|
|
91
|
+
// best-effort (they return [] / skip on IO errors), so there is no top-level
|
|
92
|
+
// catch masking a real logic bug as an all-zero "no usage" report.
|
|
93
|
+
const n = Number(days);
|
|
94
|
+
const cutoff = Number.isFinite(n) && n > 0 ? Number(now) - n * 24 * 60 * 60 * 1000 : null;
|
|
95
|
+
const records = (projectDir ? readManifest(projectDir) : []).filter((r) => inWindow(r, cutoff));
|
|
96
|
+
const misses = readMisses().filter((miss) => inWindow(miss, cutoff));
|
|
97
|
+
const globalRecords = readGlobalManifest();
|
|
98
|
+
const report = emptyReport();
|
|
99
|
+
|
|
100
|
+
report.total_resolves = records.length;
|
|
101
|
+
report.misses = misses.length;
|
|
102
|
+
for (const record of records) {
|
|
103
|
+
increment(report.by_type, record?.type || "unknown");
|
|
104
|
+
increment(report.by_source, sourceOf(record));
|
|
105
|
+
increment(report.by_provider, record?.provenance?.provider);
|
|
106
|
+
increment(report.by_via, record?.provenance?.via);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const attempts = report.total_resolves + report.misses;
|
|
110
|
+
report.hit_rate = attempts === 0 ? null : report.total_resolves / attempts;
|
|
111
|
+
report.top_missed_intents = topMissedIntents(misses);
|
|
112
|
+
report.global_cache_assets = globalRecords.length;
|
|
113
|
+
report.global_cache_disk_bytes = diskBytes(globalRecords);
|
|
114
|
+
report.cross_project_reuse = globalRecords.filter((r) => r?.provenance?.reused_by).length;
|
|
115
|
+
|
|
116
|
+
return report;
|
|
117
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { strict as assert } from "node:assert";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { buildStats } from "./stats.mjs";
|
|
7
|
+
|
|
8
|
+
function sandbox() {
|
|
9
|
+
const root = mkdtempSync(join(tmpdir(), "mu-stats-"));
|
|
10
|
+
const home = join(root, "home");
|
|
11
|
+
const projectDir = join(root, "project");
|
|
12
|
+
mkdirSync(home, { recursive: true });
|
|
13
|
+
mkdirSync(projectDir, { recursive: true });
|
|
14
|
+
process.env.HOME = home;
|
|
15
|
+
return { root, home, projectDir };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function restoreEnv(saved) {
|
|
19
|
+
for (const k of Object.keys(process.env)) if (!(k in saved)) delete process.env[k];
|
|
20
|
+
Object.assign(process.env, saved);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function seedManifest(dir, records) {
|
|
24
|
+
mkdirSync(join(dir, ".media"), { recursive: true });
|
|
25
|
+
writeFileSync(
|
|
26
|
+
join(dir, ".media/manifest.jsonl"),
|
|
27
|
+
records.map((record) => JSON.stringify(record)).join("\n") + "\n",
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isoDaysAgo(days) {
|
|
32
|
+
return new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
test("buildStats aggregates resolves, misses, providers, sources, and reuse", () => {
|
|
36
|
+
const savedEnv = { ...process.env };
|
|
37
|
+
const { root, home, projectDir } = sandbox();
|
|
38
|
+
try {
|
|
39
|
+
seedManifest(projectDir, [
|
|
40
|
+
{
|
|
41
|
+
id: "bgm_001",
|
|
42
|
+
type: "bgm",
|
|
43
|
+
source: "search",
|
|
44
|
+
ts: isoDaysAgo(0),
|
|
45
|
+
provenance: { provider: "heygen.audio.sounds", prompt: "upbeat", via: "url" },
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "image_001",
|
|
49
|
+
type: "image",
|
|
50
|
+
_source: "reused-explicit",
|
|
51
|
+
timestamp: isoDaysAgo(0),
|
|
52
|
+
provenance: { provider: "local", reused_by: "agent" },
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "bgm_002",
|
|
56
|
+
type: "bgm",
|
|
57
|
+
source: "generated",
|
|
58
|
+
provenance: { provider: "codex" },
|
|
59
|
+
},
|
|
60
|
+
]);
|
|
61
|
+
const cachedFile = join(home, ".media/mu-v1-cache/asset.wav");
|
|
62
|
+
mkdirSync(join(cachedFile, ".."), { recursive: true });
|
|
63
|
+
writeFileSync(cachedFile, "12345");
|
|
64
|
+
seedManifest(home, [
|
|
65
|
+
{
|
|
66
|
+
id: "bgm_009",
|
|
67
|
+
type: "bgm",
|
|
68
|
+
reusable: true,
|
|
69
|
+
cached_path: cachedFile,
|
|
70
|
+
provenance: { provider: "heygen.audio.sounds", reused_by: "agent" },
|
|
71
|
+
},
|
|
72
|
+
]);
|
|
73
|
+
writeFileSync(
|
|
74
|
+
join(home, ".media/misses.jsonl"),
|
|
75
|
+
JSON.stringify({
|
|
76
|
+
ts: isoDaysAgo(0),
|
|
77
|
+
type: "bgm",
|
|
78
|
+
intent: "dark cinematic riser",
|
|
79
|
+
}) + "\n",
|
|
80
|
+
{ flag: "a" },
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const stats = buildStats({ projectDir });
|
|
84
|
+
assert.equal(stats.total_resolves, 3);
|
|
85
|
+
assert.deepEqual(stats.by_type, { bgm: 2, image: 1 });
|
|
86
|
+
assert.deepEqual(stats.by_source, { search: 1, "reused-explicit": 1, generated: 1 });
|
|
87
|
+
assert.equal(stats.by_provider["heygen.audio.sounds"], 1);
|
|
88
|
+
assert.equal(stats.by_via.url, 1);
|
|
89
|
+
assert.equal(stats.misses, 1);
|
|
90
|
+
assert.equal(stats.hit_rate, 0.75);
|
|
91
|
+
assert.deepEqual(stats.top_missed_intents.bgm[0], {
|
|
92
|
+
intent: "dark cinematic riser",
|
|
93
|
+
count: 1,
|
|
94
|
+
});
|
|
95
|
+
assert.equal(stats.global_cache_assets, 1);
|
|
96
|
+
assert.equal(stats.global_cache_disk_bytes, 5);
|
|
97
|
+
assert.equal(stats.cross_project_reuse, 1);
|
|
98
|
+
} finally {
|
|
99
|
+
restoreEnv(savedEnv);
|
|
100
|
+
rmSync(root, { recursive: true, force: true });
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("buildStats returns a zeroed report when nothing exists", () => {
|
|
105
|
+
const savedEnv = { ...process.env };
|
|
106
|
+
const { root, projectDir } = sandbox();
|
|
107
|
+
try {
|
|
108
|
+
const stats = buildStats({ projectDir });
|
|
109
|
+
assert.equal(stats.total_resolves, 0);
|
|
110
|
+
assert.deepEqual(stats.by_type, {});
|
|
111
|
+
assert.deepEqual(stats.by_source, {});
|
|
112
|
+
assert.deepEqual(stats.by_provider, {});
|
|
113
|
+
assert.deepEqual(stats.by_via, {});
|
|
114
|
+
assert.equal(stats.misses, 0);
|
|
115
|
+
assert.equal(stats.hit_rate, null);
|
|
116
|
+
assert.deepEqual(stats.top_missed_intents, {});
|
|
117
|
+
assert.equal(stats.global_cache_assets, 0);
|
|
118
|
+
assert.equal(stats.global_cache_disk_bytes, 0);
|
|
119
|
+
assert.equal(stats.cross_project_reuse, 0);
|
|
120
|
+
} finally {
|
|
121
|
+
restoreEnv(savedEnv);
|
|
122
|
+
rmSync(root, { recursive: true, force: true });
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("buildStats returns JSON-safe output", () => {
|
|
127
|
+
const savedEnv = { ...process.env };
|
|
128
|
+
const { root, projectDir } = sandbox();
|
|
129
|
+
try {
|
|
130
|
+
const stats = buildStats({ projectDir });
|
|
131
|
+
assert.deepEqual(JSON.parse(JSON.stringify(stats)), stats);
|
|
132
|
+
} finally {
|
|
133
|
+
restoreEnv(savedEnv);
|
|
134
|
+
rmSync(root, { recursive: true, force: true });
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("buildStats applies days window to timestamped records and misses", () => {
|
|
139
|
+
const savedEnv = { ...process.env };
|
|
140
|
+
const { root, home, projectDir } = sandbox();
|
|
141
|
+
try {
|
|
142
|
+
seedManifest(projectDir, [
|
|
143
|
+
{ id: "bgm_old", type: "bgm", ts: isoDaysAgo(100), provenance: { provider: "old" } },
|
|
144
|
+
{ id: "bgm_new", type: "bgm", ts: isoDaysAgo(1), provenance: { provider: "new" } },
|
|
145
|
+
{ id: "image_untimed", type: "image", provenance: { provider: "none" } },
|
|
146
|
+
]);
|
|
147
|
+
mkdirSync(join(home, ".media"), { recursive: true });
|
|
148
|
+
writeFileSync(
|
|
149
|
+
join(home, ".media/misses.jsonl"),
|
|
150
|
+
[
|
|
151
|
+
JSON.stringify({ ts: isoDaysAgo(100), type: "bgm", intent: "old miss" }),
|
|
152
|
+
JSON.stringify({ ts: isoDaysAgo(1), type: "bgm", intent: "new miss" }),
|
|
153
|
+
].join("\n"),
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const stats = buildStats({ projectDir, days: 7 });
|
|
157
|
+
assert.equal(stats.total_resolves, 2);
|
|
158
|
+
assert.deepEqual(stats.by_type, { bgm: 1, image: 1 });
|
|
159
|
+
assert.equal(stats.misses, 1);
|
|
160
|
+
assert.equal(stats.top_missed_intents.bgm[0].intent, "new miss");
|
|
161
|
+
} finally {
|
|
162
|
+
restoreEnv(savedEnv);
|
|
163
|
+
rmSync(root, { recursive: true, force: true });
|
|
164
|
+
}
|
|
165
|
+
});
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// Usage tracking shares the CLI and Studio identity. Properties stay coarse and
|
|
2
|
+
// never carry intent text, file names, or paths.
|
|
3
|
+
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
const POSTHOG_API_KEY = "phc_zjjbX0PnWxERXrMHhkEJWj9A9BhGVLRReICgsfTMmpx";
|
|
10
|
+
const POSTHOG_HOST = "https://us.i.posthog.com";
|
|
11
|
+
const TIMEOUT_MS = 1500;
|
|
12
|
+
let identifiedAccount = false;
|
|
13
|
+
let warnedNonDefaultHost = false;
|
|
14
|
+
|
|
15
|
+
function isTestOrCiContext() {
|
|
16
|
+
return (
|
|
17
|
+
process.env.CI === "true" ||
|
|
18
|
+
process.env.CI === "1" ||
|
|
19
|
+
process.env.NODE_ENV === "test" ||
|
|
20
|
+
process.env.NODE_ENV === "development"
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function posthogHost() {
|
|
25
|
+
const override = process.env.MEDIA_USE_TELEMETRY_HOST;
|
|
26
|
+
if (override && !warnedNonDefaultHost && !isTestOrCiContext()) {
|
|
27
|
+
warnedNonDefaultHost = true;
|
|
28
|
+
console.error(
|
|
29
|
+
`media-use: telemetry is redirected to a non-default host via MEDIA_USE_TELEMETRY_HOST (${override}) — unset it unless this is intentional.`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return override || POSTHOG_HOST;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** True when telemetry must NOT be sent (opt-out envs, CI, dev). */
|
|
36
|
+
export function optedOut() {
|
|
37
|
+
return (
|
|
38
|
+
process.env.HYPERFRAMES_NO_TELEMETRY === "1" ||
|
|
39
|
+
process.env.DO_NOT_TRACK === "1" ||
|
|
40
|
+
process.env.CI === "true" ||
|
|
41
|
+
process.env.CI === "1" ||
|
|
42
|
+
process.env.NODE_ENV === "development"
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Read and write the shared config so media-use keeps one identity per install.
|
|
47
|
+
function sharedConfigPath() {
|
|
48
|
+
return join(homedir(), ".hyperframes", "config.json");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readSharedConfig() {
|
|
52
|
+
try {
|
|
53
|
+
const file = sharedConfigPath();
|
|
54
|
+
if (existsSync(file)) {
|
|
55
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
56
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// unreadable config → treat as empty; never throw
|
|
60
|
+
}
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function writeSharedConfig(config) {
|
|
65
|
+
const dir = join(homedir(), ".hyperframes");
|
|
66
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
67
|
+
writeFileSync(join(dir, "config.json"), JSON.stringify(config, null, 2) + "\n");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Adopt a pre-existing media-use-only id (~/.media/anon-id from before this
|
|
71
|
+
// change) so upgraders keep their PostHog persona instead of resetting to a new
|
|
72
|
+
// one — otherwise cross-surface continuity would start over on upgrade.
|
|
73
|
+
function legacyMediaAnonId() {
|
|
74
|
+
try {
|
|
75
|
+
const file = join(homedir(), ".media", "anon-id");
|
|
76
|
+
if (existsSync(file)) {
|
|
77
|
+
const id = readFileSync(file, "utf8").trim();
|
|
78
|
+
if (id) return id;
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// ignore
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Stable per-machine id from the shared config; seeds it (adopting a legacy
|
|
87
|
+
// media-use id when present) if absent.
|
|
88
|
+
function anonymousId() {
|
|
89
|
+
try {
|
|
90
|
+
const config = readSharedConfig();
|
|
91
|
+
if (typeof config.anonymousId === "string" && config.anonymousId.trim()) {
|
|
92
|
+
return config.anonymousId.trim();
|
|
93
|
+
}
|
|
94
|
+
const id = legacyMediaAnonId() || randomUUID();
|
|
95
|
+
writeSharedConfig({ ...config, anonymousId: id });
|
|
96
|
+
return id;
|
|
97
|
+
} catch {
|
|
98
|
+
return "anon"; // best-effort; a shared bucket is fine if the fs is read-only
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function heygenAccountDistinctId() {
|
|
103
|
+
const file = join(process.env.HEYGEN_CONFIG_DIR || join(homedir(), ".heygen"), "credentials");
|
|
104
|
+
try {
|
|
105
|
+
if (!existsSync(file)) return null;
|
|
106
|
+
const raw = readFileSync(file, "utf8").trim();
|
|
107
|
+
if (!raw.startsWith("{")) return null;
|
|
108
|
+
const parsed = JSON.parse(raw);
|
|
109
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
110
|
+
const user = parsed.user;
|
|
111
|
+
if (!user || typeof user !== "object" || Array.isArray(user)) return null;
|
|
112
|
+
const id = typeof user.email === "string" && user.email.trim() ? user.email : user.username;
|
|
113
|
+
// Lowercased so this joins with the CLI's own identify call regardless of
|
|
114
|
+
// the account's stored email casing — two different-case distinct ids
|
|
115
|
+
// would otherwise split one person across two PostHog profiles.
|
|
116
|
+
return typeof id === "string" && id.trim() ? id.trim().toLowerCase() : null;
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function showTelemetryNotice() {
|
|
123
|
+
if (optedOut()) return;
|
|
124
|
+
try {
|
|
125
|
+
const config = readSharedConfig();
|
|
126
|
+
// Shared with the CLI (config.telemetryNoticeShown): shown once per person
|
|
127
|
+
// across surfaces, not once per tool.
|
|
128
|
+
if (config.telemetryNoticeShown === true) return;
|
|
129
|
+
console.error(
|
|
130
|
+
[
|
|
131
|
+
"media-use sends usage telemetry: media type, resolution source, and provider; never intent text, file names, or paths.",
|
|
132
|
+
"If you sign in to HeyGen, usage links to your account email or username. Opt out with HYPERFRAMES_NO_TELEMETRY=1 or DO_NOT_TRACK=1.",
|
|
133
|
+
].join("\n"),
|
|
134
|
+
);
|
|
135
|
+
writeSharedConfig({ ...config, telemetryNoticeShown: true });
|
|
136
|
+
} catch {
|
|
137
|
+
// notice is best-effort; never surface into the command
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function postBatch(batch) {
|
|
142
|
+
try {
|
|
143
|
+
await fetch(`${posthogHost()}/batch/`, {
|
|
144
|
+
method: "POST",
|
|
145
|
+
headers: { "Content-Type": "application/json", Connection: "close" },
|
|
146
|
+
body: JSON.stringify({ api_key: POSTHOG_API_KEY, batch }),
|
|
147
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
148
|
+
});
|
|
149
|
+
} catch {
|
|
150
|
+
// telemetry is best-effort; never surface into the command
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function postEvent(event, properties, distinctId) {
|
|
155
|
+
await postBatch([
|
|
156
|
+
{
|
|
157
|
+
event,
|
|
158
|
+
properties: { ...properties, surface: "media-use", $ip: null },
|
|
159
|
+
distinct_id: distinctId,
|
|
160
|
+
timestamp: new Date().toISOString(),
|
|
161
|
+
},
|
|
162
|
+
]);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function identifyAccount(anonId) {
|
|
166
|
+
if (optedOut() || identifiedAccount) return;
|
|
167
|
+
const distinctId = heygenAccountDistinctId();
|
|
168
|
+
if (!distinctId) return;
|
|
169
|
+
identifiedAccount = true;
|
|
170
|
+
await postEvent("$identify", { $anon_distinct_id: anonId }, distinctId);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Fire-and-forget a single event to PostHog. Best-effort: awaited with a short
|
|
175
|
+
* timeout so a short-lived script flushes before exit, but any failure (offline,
|
|
176
|
+
* opted out) is swallowed. `properties` must be non-PII (no intent/paths).
|
|
177
|
+
*/
|
|
178
|
+
export async function track(event, properties = {}) {
|
|
179
|
+
if (optedOut()) return;
|
|
180
|
+
showTelemetryNotice();
|
|
181
|
+
const anonId = anonymousId();
|
|
182
|
+
await identifyAccount(anonId);
|
|
183
|
+
await postEvent(event, properties, anonId);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function __anonymousIdForTest() {
|
|
187
|
+
return anonymousId();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function __resetTelemetryForTest() {
|
|
191
|
+
identifiedAccount = false;
|
|
192
|
+
warnedNonDefaultHost = false;
|
|
193
|
+
}
|