hyperframes 0.8.81 → 0.8.83

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/backgroundChecksWorker.js +890 -0
  2. package/dist/cli.js +4272 -2792
  3. package/dist/commands/layout-audit.browser.js +55 -25
  4. package/dist/fontLocalizeCli.js +714 -344
  5. package/dist/hyperframe-runtime.js +83 -58
  6. package/dist/hyperframe.manifest.json +1 -1
  7. package/dist/hyperframe.runtime.iife.js +83 -58
  8. package/dist/hyperframes-player.global.js +3 -3
  9. package/dist/registry/localSemantic.js +7 -3
  10. package/dist/renderSetupWorker.js +62 -32
  11. package/dist/sherpaWorker.js +179 -0
  12. package/dist/skills/hyperframes/SKILL.md +2 -0
  13. package/dist/skills/hyperframes/references/plugin-installation.md +43 -0
  14. package/dist/skills/hyperframes/references/skill-lifecycle.md +2 -0
  15. package/dist/skills/hyperframes/scripts/plugin-cli.mjs +88 -0
  16. package/dist/skills/hyperframes/scripts/plugin-cli.test.mjs +117 -0
  17. package/dist/skills/hyperframes-cli/SKILL.md +2 -0
  18. package/dist/skills/hyperframes-cli/references/upgrade-info-misc.md +1 -0
  19. package/dist/skills/media-use/SKILL.md +2 -0
  20. package/dist/skills/media-use/references/operations.md +7 -5
  21. package/dist/skills/media-use/references/setup-providers.md +9 -9
  22. package/dist/skills/media-use/registry/localSemantic.js +7 -3
  23. package/dist/skills/media-use/scripts/lib/ff-binaries.mjs +37 -0
  24. package/dist/skills/media-use/scripts/lib/probe.mjs +3 -1
  25. package/dist/skills/media-use/scripts/lib/probe.test.mjs +118 -1
  26. package/dist/skills/media-use/scripts/lib/tts-local-provider.mjs +5 -3
  27. package/dist/skills/media-use/scripts/resolve-plugin.test.mjs +62 -0
  28. package/dist/skills/media-use/scripts/resolve.mjs +45 -19
  29. package/dist/skills/media-use/scripts/transcribe.mjs +39 -25
  30. package/dist/skills/media-use/scripts/transcribe.test.mjs +112 -0
  31. package/dist/skills/registry/localSemantic.js +7 -3
  32. package/dist/studio/assets/{hyperframes-player-C68UawR3.js → hyperframes-player-BAerMCex.js} +1 -1
  33. package/dist/studio/assets/{index-CMzPGv9P.js → index-BUhcVhNy.js} +1 -1
  34. package/dist/studio/assets/{index-hNVM9Pd2.js → index-BYhK_KMc.js} +187 -187
  35. package/dist/studio/assets/{index-CS5SImRT.js → index-Iv0HI7eM.js} +1 -1
  36. package/dist/studio/{chunk-I7XNAIOF.js → chunk-3IOCVWB6.js} +6 -97
  37. package/dist/studio/chunk-3IOCVWB6.js.map +1 -0
  38. package/dist/studio/chunk-62DDWOSH.js +189 -0
  39. package/dist/studio/chunk-62DDWOSH.js.map +1 -0
  40. package/dist/studio/chunk-LPZMAGS7.js +1871 -0
  41. package/dist/studio/chunk-LPZMAGS7.js.map +1 -0
  42. package/dist/studio/chunk-QZ6HNNFG.js +934 -0
  43. package/dist/studio/chunk-QZ6HNNFG.js.map +1 -0
  44. package/dist/studio/chunk-S65AQH6M.js +101 -0
  45. package/dist/studio/chunk-S65AQH6M.js.map +1 -0
  46. package/dist/studio/{domEditingLayers-MHFDKAQ7.js → domEditingLayers-ZOOMN7CQ.js} +3 -2
  47. package/dist/studio/index.d.ts +834 -330
  48. package/dist/studio/index.html +1 -1
  49. package/dist/studio/index.js +26378 -28802
  50. package/dist/studio/index.js.map +1 -1
  51. package/dist/studio/player.d.ts +30 -0
  52. package/dist/studio/player.js +10 -0
  53. package/dist/studio/player.js.map +1 -0
  54. package/dist/studio/ui.d.ts +294 -0
  55. package/dist/studio/ui.js +66 -0
  56. package/dist/studio/ui.js.map +1 -0
  57. package/dist/templates/_shared/AGENTS.md +5 -1
  58. package/dist/templates/_shared/CLAUDE.md +5 -1
  59. package/package.json +1 -1
  60. package/dist/studio/chunk-I7XNAIOF.js.map +0 -1
  61. /package/dist/studio/{domEditingLayers-MHFDKAQ7.js.map → domEditingLayers-ZOOMN7CQ.js.map} +0 -0
@@ -24631,6 +24631,7 @@ function readClipTiming(attributes2, options = {}) {
24631
24631
  }
24632
24632
 
24633
24633
  // ../parsers/dist/compositionDuration.js
24634
+ var TIMING_TOLERANCE_SECONDS = 0.05;
24634
24635
  var isPositiveFinite = (value) => value !== null && Number.isFinite(value) && value > 0;
24635
24636
  function resolveCompositionDuration(input2) {
24636
24637
  const pendingClips = input2.clipEndsSeconds.filter((end) => end === null).length;
@@ -31090,6 +31091,27 @@ function captionFindings(rows, severity) {
31090
31091
  }
31091
31092
  return findings;
31092
31093
  }
31094
+ var hundredths = (seconds) => Math.round(seconds * 100) / 100;
31095
+ function clipsPastRootFindings(root, rows) {
31096
+ if (root.attrs["data-composition-id"] === void 0) return [];
31097
+ const rootDuration = readDataDurationSeconds((name) => root.attrs[name]);
31098
+ if (rootDuration === null) return [];
31099
+ const limit = hundredths(rootDuration);
31100
+ return rows.flatMap((row) => {
31101
+ const { start, end } = readClipTiming({ getAttribute: (name) => row.attrs[name] ?? null });
31102
+ if (start === null || end === null || end <= rootDuration + TIMING_TOLERANCE_SECONDS) return [];
31103
+ return [
31104
+ {
31105
+ code: "clip_ends_past_root_duration",
31106
+ severity: "warning",
31107
+ message: `${describe(row)} runs from ${hundredths(start)}s to ${hundredths(end)}s, past the root composition's data-duration of ${limit}s, so it is cut off in previews, posters and renders.`,
31108
+ elementId: row.attrs.id,
31109
+ fixHint: `Extend the root data-duration to ${hundredths(end)}, or make ${describe(row)} end at or before ${limit}s.`,
31110
+ snippet: truncateSnippet(row.open.raw)
31111
+ }
31112
+ ];
31113
+ });
31114
+ }
31093
31115
  var structureRules = [
31094
31116
  (ctx) => {
31095
31117
  if (ctx.options.isSubComposition) return [];
@@ -31100,7 +31122,8 @@ var structureRules = [
31100
31122
  return [
31101
31123
  ...nestedStructureFindings(rows, severity),
31102
31124
  ...missingDurationFindings(rows, severity),
31103
- ...captionFindings(rows, severity)
31125
+ ...captionFindings(rows, severity),
31126
+ ...clipsPastRootFindings(root, rows)
31104
31127
  ];
31105
31128
  }
31106
31129
  ];
@@ -31411,6 +31434,7 @@ async function lintHevcPreviewCodec(candidates) {
31411
31434
  function parseSubCompHtml(html) {
31412
31435
  return parseHTML(html).document;
31413
31436
  }
31437
+ var sourceFile = (projectDir, compSrcPath) => resolve4(projectDir, compSrcPath ?? "index.html");
31414
31438
  function querySelectorAllIncludingTemplates(root, selector) {
31415
31439
  const matches2 = [...root.querySelectorAll(selector)];
31416
31440
  for (const template of root.querySelectorAll("template")) {
@@ -31562,20 +31586,20 @@ async function lintProject(projectDir, entryFile, hostOptions = {}) {
31562
31586
  ...await lintVideoMediaStartPastEof(projectDir, allHtmlSources),
31563
31587
  ...await lintHevcPreviewCodec(collectLocalVideoCandidates(projectDir, allHtmlSources))
31564
31588
  ];
31565
- if (projectFindings.length > 0) {
31566
- for (const finding of projectFindings) {
31567
- rootResult.findings.push(finding);
31568
- if (finding.severity === "error") {
31569
- rootResult.errorCount++;
31570
- rootResult.ok = false;
31571
- totalErrors++;
31572
- } else if (finding.severity === "warning") {
31573
- rootResult.warningCount++;
31574
- totalWarnings++;
31575
- } else {
31576
- rootResult.infoCount++;
31577
- totalInfos++;
31578
- }
31589
+ for (const finding of projectFindings) {
31590
+ const ownFile = finding.file && resolve4(projectDir, finding.file);
31591
+ const owner = results.find((entry) => resolve4(projectDir, entry.file) === ownFile)?.result ?? rootResult;
31592
+ owner.findings.push(finding);
31593
+ if (finding.severity === "error") {
31594
+ owner.errorCount++;
31595
+ owner.ok = false;
31596
+ totalErrors++;
31597
+ } else if (finding.severity === "warning") {
31598
+ owner.warningCount++;
31599
+ totalWarnings++;
31600
+ } else {
31601
+ owner.infoCount++;
31602
+ totalInfos++;
31579
31603
  }
31580
31604
  }
31581
31605
  return { results, totalErrors, totalWarnings, totalInfos };
@@ -31635,7 +31659,7 @@ function lintProjectAudioFiles(projectDir, htmlSources) {
31635
31659
  function lintAudioSrcNotFound(projectDir, htmlSources) {
31636
31660
  const findings = [];
31637
31661
  const audioSrcRe = mediaSrcTagRe("audio");
31638
- const missingSrcs = [];
31662
+ const missingByFile = /* @__PURE__ */ new Map();
31639
31663
  for (const { html, compSrcPath } of htmlSources) {
31640
31664
  let match;
31641
31665
  while ((match = audioSrcRe.exec(html)) !== null) {
@@ -31644,15 +31668,17 @@ function lintAudioSrcNotFound(projectDir, htmlSources) {
31644
31668
  if (isUnresolvedAssetPlaceholder(src)) continue;
31645
31669
  const rootRelative = compSrcPath ? rewriteAssetPath(compSrcPath, src, (path) => existsSync22(join22(projectDir, path))) : src;
31646
31670
  if (!existsSync22(resolveProjectRelativeSrc(rootRelative, projectDir))) {
31647
- missingSrcs.push(src);
31671
+ const file = sourceFile(projectDir, compSrcPath);
31672
+ missingByFile.set(file, (missingByFile.get(file) ?? /* @__PURE__ */ new Set()).add(src));
31648
31673
  }
31649
31674
  }
31650
31675
  }
31651
- if (missingSrcs.length > 0) {
31652
- const unique = [...new Set(missingSrcs)];
31676
+ for (const [file, srcs] of missingByFile) {
31677
+ const unique = [...srcs];
31653
31678
  findings.push({
31654
31679
  code: "audio_src_not_found",
31655
31680
  severity: "error",
31681
+ file,
31656
31682
  message: `<audio> element references file(s) not found in the project: ${unique.join(", ")}. The rendered video will be silent.`,
31657
31683
  fixHint: unique.length === 1 ? `Add the file "${unique[0]}" to the project directory, or update the src attribute to point to an existing file.` : `Add the missing files to the project directory, or update the src attributes to point to existing files.`
31658
31684
  });
@@ -31678,19 +31704,22 @@ function lintMissingLocalAsset(projectDir, htmlSources) {
31678
31704
  const resolvedAsset = resolveExistingLocalAsset(projectDir, rootRelative);
31679
31705
  if (resolvedAsset) continue;
31680
31706
  const resolvedKey = resolve4(projectDir, rootRelative);
31681
- let bucket = missingByTag.get(tagName19);
31707
+ const file = sourceFile(projectDir, compSrcPath);
31708
+ const bucketKey = `${file}\0${tagName19}`;
31709
+ let bucket = missingByTag.get(bucketKey);
31682
31710
  if (!bucket) {
31683
- bucket = /* @__PURE__ */ new Map();
31684
- missingByTag.set(tagName19, bucket);
31711
+ bucket = { file, tagName: tagName19, byResolved: /* @__PURE__ */ new Map() };
31712
+ missingByTag.set(bucketKey, bucket);
31685
31713
  }
31686
- if (!bucket.has(resolvedKey)) bucket.set(resolvedKey, src);
31714
+ if (!bucket.byResolved.has(resolvedKey)) bucket.byResolved.set(resolvedKey, src);
31687
31715
  }
31688
31716
  }
31689
- for (const [tagName19, byResolved] of missingByTag) {
31717
+ for (const { file, tagName: tagName19, byResolved } of missingByTag.values()) {
31690
31718
  const unique = [...byResolved.values()];
31691
31719
  findings.push({
31692
31720
  code: "missing_local_asset",
31693
31721
  severity: "error",
31722
+ file,
31694
31723
  message: `<${tagName19}> element references local file(s) not found in the project: ${unique.join(", ")}. The renderer will silently skip these and produce a video with missing visuals.`,
31695
31724
  fixHint: unique.length === 1 ? `Add "${unique[0]}" to the project directory, or update the src attribute to point to an existing file. Common cause: captured asset filenames are unreliable (heygen-logo.svg often contains Google, nvidia-logo.svg may contain Autodesk, etc.). Open the contact sheets and verify the file actually exists at this path before referencing it.` : "Add the missing files to the project directory, or update the src attributes to point to existing files. Captured asset filenames are unreliable \u2014 verify against capture/contact-sheets/ and capture/extracted/asset-descriptions.md."
31696
31725
  });
@@ -31698,8 +31727,9 @@ function lintMissingLocalAsset(projectDir, htmlSources) {
31698
31727
  return findings;
31699
31728
  }
31700
31729
  function lintTextureMaskAssetNotFound(projectDir, htmlSources) {
31701
- const missing = /* @__PURE__ */ new Map();
31730
+ const missingByFile = /* @__PURE__ */ new Map();
31702
31731
  for (const { html, compSrcPath } of htmlSources) {
31732
+ const file = sourceFile(projectDir, compSrcPath);
31703
31733
  for (const cssSource of collectCssSources(projectDir, html, compSrcPath)) {
31704
31734
  let match;
31705
31735
  const pattern = new RegExp(MASK_IMAGE_URL_RE.source, MASK_IMAGE_URL_RE.flags);
@@ -31715,20 +31745,20 @@ function lintTextureMaskAssetNotFound(projectDir, htmlSources) {
31715
31745
  cssSource.rootRelativePath
31716
31746
  );
31717
31747
  if (candidates.some(existsSync22)) continue;
31718
- missing.set(url, candidates[0] ?? resolve4(projectDir, url));
31748
+ missingByFile.set(file, (missingByFile.get(file) ?? /* @__PURE__ */ new Set()).add(url));
31719
31749
  }
31720
31750
  }
31721
31751
  }
31722
- if (missing.size === 0) return [];
31723
- const urls = [...missing.keys()];
31724
- return [
31725
- {
31752
+ return [...missingByFile].map(([file, found]) => {
31753
+ const urls = [...found];
31754
+ return {
31726
31755
  code: "texture_mask_asset_not_found",
31727
31756
  severity: "error",
31757
+ file,
31728
31758
  message: `CSS mask-image references file(s) not found in the project: ${urls.join(", ")}.`,
31729
31759
  fixHint: urls.length === 1 ? `Add "${urls[0]}" to the project, or update the mask-image URL to point to an existing texture mask.` : "Add the missing texture mask files to the project, or update the mask-image URLs to point to existing files."
31730
- }
31731
- ];
31760
+ };
31761
+ });
31732
31762
  }
31733
31763
  function lintMultipleRootCompositions(projectDir) {
31734
31764
  const findings = [];
@@ -0,0 +1,179 @@
1
+ import { createRequire as __hf_createRequire } from "node:module";
2
+ import { fileURLToPath as __hf_fileURLToPath } from "node:url";
3
+ import { dirname as __hf_dirname } from "node:path";
4
+ var require = __hf_createRequire(import.meta.url);
5
+ var __filename = __hf_fileURLToPath(import.meta.url);
6
+ var __dirname = __hf_dirname(__filename);
7
+
8
+ // src/utils/optionalPackages.ts
9
+ import { spawn } from "child_process";
10
+ import { randomUUID } from "crypto";
11
+ import {
12
+ existsSync,
13
+ mkdirSync,
14
+ readdirSync,
15
+ readFileSync,
16
+ realpathSync,
17
+ renameSync,
18
+ rmSync,
19
+ statSync,
20
+ writeFileSync
21
+ } from "fs";
22
+ import { createRequire } from "module";
23
+ import { homedir } from "os";
24
+ import { basename, dirname, join, sep } from "path";
25
+ var CACHE_DIR = join(homedir(), ".cache", "hyperframes", "optional");
26
+ function manifestPath(dir, name) {
27
+ return join(dir, "node_modules", name, "package.json");
28
+ }
29
+ function isInstalled(dir, name) {
30
+ return existsSync(manifestPath(dir, name));
31
+ }
32
+ function loadInstalled(dir, name) {
33
+ if (!isInstalled(dir, name)) return null;
34
+ return createRequire(join(dir, "package.json"))(name);
35
+ }
36
+ var STAGING_PID_TRUSTED_FOR_MS = 6 * 60 * 60 * 1e3;
37
+
38
+ // src/whisper/parakeet.ts
39
+ import { execFileSync } from "child_process";
40
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, mkdtempSync, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
41
+ import { homedir as homedir2, tmpdir } from "os";
42
+ import { basename as basename2, extname, join as join2 } from "path";
43
+ var SHERPA_RESULT_PREFIX = "HYPERFRAMES_PARAKEET_RESULT:";
44
+ var SHERPA_ERROR_PREFIX = "HYPERFRAMES_PARAKEET_ERROR:";
45
+ function meanEnergy(samples, from, to) {
46
+ let energy = 0;
47
+ for (let i = from; i < to; i++) energy += samples[i] * samples[i];
48
+ return energy / Math.max(1, to - from);
49
+ }
50
+ function droppedSpeechGaps(samples, sampleRate, decoded) {
51
+ if (decoded.tokens.length === 0) return [];
52
+ const loud = 0.3 * meanEnergy(samples, 0, samples.length);
53
+ const { timestamps, durations } = decoded;
54
+ const gapStarts = [0, ...timestamps.map((t, i) => t + (durations?.[i] ?? 0))];
55
+ const gapEnds = [...timestamps, samples.length / sampleRate];
56
+ const at = (seconds) => Math.round(seconds * sampleRate);
57
+ return gapStarts.map((from, i) => [from, gapEnds[i]]).filter(([from, to]) => to - from >= 1 && meanEnergy(samples, at(from), at(to)) > loud);
58
+ }
59
+ function overlap(a, b) {
60
+ for (let n = Math.min(a.length, b.length); n > 0; n--) {
61
+ if (a.slice(-n).every((text, i) => text === b[i])) return n;
62
+ }
63
+ return 0;
64
+ }
65
+ function wordsOf(tokens) {
66
+ const words = [];
67
+ for (const t of tokens) {
68
+ if (t.text.startsWith(" ") || words.length === 0) words.push([t]);
69
+ else words.at(-1).push(t);
70
+ }
71
+ return words;
72
+ }
73
+ var letters = (word) => word.map((t) => t.text).join("").toLowerCase().replace(/[^\p{L}\p{N}]/gu, "");
74
+ var EDGE_SECONDS = 0.3;
75
+ function spliceGap(decoded, patch, offset, [from, to]) {
76
+ const tokens = (w, shift) => w.tokens.map((text, i) => ({
77
+ text,
78
+ start: shift + w.timestamps[i],
79
+ duration: w.durations?.[i] ?? 0
80
+ }));
81
+ const kept = wordsOf(tokens(decoded, 0));
82
+ const added = wordsOf(tokens(patch, offset)).filter(
83
+ ([first]) => first.start >= from && first.start < to
84
+ );
85
+ const before = kept.filter(([first]) => first.start < from).map(letters);
86
+ const after = kept.filter(([first]) => first.start >= to).map(letters);
87
+ const head = added.filter(([first]) => first.start < from + EDGE_SECONDS).map(letters);
88
+ const tail = added.filter(([first]) => first.start >= to - EDGE_SECONDS).map(letters);
89
+ const fresh = added.slice(overlap(before, head), added.length - overlap(tail, after)).map(([first, ...rest]) => [{ ...first, text: ` ${first.text.trimStart()}` }, ...rest]);
90
+ const merged = [...kept, ...fresh].sort((a, b) => a[0].start - b[0].start).flat();
91
+ return {
92
+ tokens: merged.map((t) => t.text),
93
+ timestamps: merged.map((t) => t.start),
94
+ durations: merged.map((t) => t.duration)
95
+ };
96
+ }
97
+ function quietestBlockCenter(samples, lo, hi, block) {
98
+ let best = hi;
99
+ let bestEnergy = Infinity;
100
+ for (let s = lo; s + block <= hi; s += block) {
101
+ const energy = meanEnergy(samples, s, s + block);
102
+ if (energy < bestEnergy) {
103
+ bestEnergy = energy;
104
+ best = s + block / 2;
105
+ }
106
+ }
107
+ return best;
108
+ }
109
+ function silenceCuts(samples, sampleRate, windowSeconds = 60) {
110
+ const window = windowSeconds * sampleRate;
111
+ const slack = 5 * sampleRate;
112
+ const cuts = [0];
113
+ while (cuts.at(-1) + window < samples.length) {
114
+ const target = cuts.at(-1) + window;
115
+ const hi = Math.min(target + slack, samples.length);
116
+ cuts.push(quietestBlockCenter(samples, target - slack, hi, sampleRate / 10));
117
+ }
118
+ if (samples.length > 0) cuts.push(samples.length);
119
+ return cuts;
120
+ }
121
+
122
+ // src/whisper/sherpaWorker.ts
123
+ var { wavPath, runtimeDir, config } = JSON.parse(process.env.HYPERFRAMES_PARAKEET_INPUT ?? "{}");
124
+ var parentPid = process.ppid;
125
+ var RETRY_PAD_SECONDS = 0.5;
126
+ var GAP_MARGIN_SECONDS = 0.5;
127
+ function decodeWindow(recognizer, wave, padSeconds = 0) {
128
+ const pad = Math.round(padSeconds * wave.sampleRate);
129
+ const samples = new Float32Array(pad + wave.samples.length);
130
+ samples.set(wave.samples, pad);
131
+ const stream = recognizer.createStream();
132
+ stream.acceptWaveform({ sampleRate: wave.sampleRate, samples });
133
+ recognizer.decode(stream);
134
+ const { tokens, timestamps, durations } = recognizer.getResult(stream);
135
+ return { tokens, timestamps: timestamps.map((t) => Math.max(0, t - padSeconds)), durations };
136
+ }
137
+ try {
138
+ const sherpa = loadInstalled(runtimeDir, "sherpa-onnx-node");
139
+ if (!sherpa) throw new Error(`sherpa-onnx-node is not installed in ${runtimeDir}`);
140
+ const recognizer = new sherpa.OfflineRecognizer(config);
141
+ const wave = sherpa.readWave(wavPath);
142
+ const cuts = silenceCuts(wave.samples, wave.sampleRate);
143
+ const windows = [];
144
+ for (let k = 0; k + 1 < cuts.length; k++) {
145
+ if (process.ppid !== parentPid) throw new Error("the CLI exited, so the decode stopped");
146
+ const slice = {
147
+ sampleRate: wave.sampleRate,
148
+ samples: wave.samples.subarray(cuts[k], cuts[k + 1])
149
+ };
150
+ let decoded = decodeWindow(recognizer, slice);
151
+ const gaps = droppedSpeechGaps(slice.samples, slice.sampleRate, decoded);
152
+ let spliced = decoded;
153
+ for (const [from, to] of gaps) {
154
+ const lo = Math.max(0, from - GAP_MARGIN_SECONDS);
155
+ const at = (seconds) => Math.round(seconds * slice.sampleRate);
156
+ const samples = slice.samples.subarray(at(lo), at(to + GAP_MARGIN_SECONDS));
157
+ const patch = decodeWindow(
158
+ recognizer,
159
+ { sampleRate: slice.sampleRate, samples },
160
+ RETRY_PAD_SECONDS
161
+ );
162
+ spliced = spliceGap(spliced, patch, lo, [from, to]);
163
+ }
164
+ if (spliced.tokens.length > decoded.tokens.length) decoded = spliced;
165
+ else if (gaps.length > 0) {
166
+ const retry = decodeWindow(recognizer, slice, RETRY_PAD_SECONDS);
167
+ if (retry.tokens.length > decoded.tokens.length) decoded = retry;
168
+ }
169
+ windows.push({ offset: cuts[k] / wave.sampleRate, ...decoded });
170
+ }
171
+ process.stdout.write(`${SHERPA_RESULT_PREFIX}${JSON.stringify(windows)}
172
+ `);
173
+ } catch (err) {
174
+ const message = (err instanceof Error ? err.message : String(err)).replace(/\s+/g, " ").trim();
175
+ process.stderr.write(`${SHERPA_ERROR_PREFIX}${message}
176
+ `, () => {
177
+ throw err;
178
+ });
179
+ }
@@ -12,6 +12,8 @@ description: >
12
12
  browser session.
13
13
  ---
14
14
 
15
+ **Plugin installs:** Before setup or freshness commands, follow [plugin execution rules](references/plugin-installation.md) when this skill is inside a HyperFrames plugin. Standalone installs keep the update instructions below.
16
+
15
17
  # HyperFrames entry point
16
18
 
17
19
  HyperFrames **renders video from HTML** — a composition is an HTML file whose DOM declares timing with `data-*` attributes, whose animation runtime is seekable, and whose media playback is owned by the framework. The full authoring contract lives in `/hyperframes-core`; read it before writing composition HTML. Brief, storyboard, review, production, dispatch, and frame-worker contracts live in this skill's `references/`.
@@ -0,0 +1,43 @@
1
+ # Running from an installed plugin
2
+
3
+ Check the directory two levels above the loaded `SKILL.md` for `plugin.json`,
4
+ `.claude-plugin/plugin.json`, `.codex-plugin/plugin.json`, or
5
+ `.cursor-plugin/plugin.json` identifying `hyperframes`, or a Gemini extension manifest.
6
+ That directory is `<PLUGIN_ROOT>`. If none exists, this is a standalone skill;
7
+ follow the normal installation and freshness instructions.
8
+
9
+ For a plugin installation, these rules replace the standalone update commands
10
+ in every workflow and reference:
11
+
12
+ - Do not run `hyperframes skills update`, `skills check`, or `npx skills add`.
13
+ The agent's plugin manager owns installation and updates. Read the workflow at
14
+ `<PLUGIN_ROOT>/skills/<name>/SKILL.md`; report a missing bundled skill instead
15
+ of downloading a different release. Resolve all skill references inside this
16
+ bundle, even if a standalone copy is also installed. Claude exposes the router
17
+ as `/hyperframes:hyperframes`; other clients may expose `/hyperframes`.
18
+ - Replace `npx hyperframes` (or bare `hyperframes`) in command examples with:
19
+
20
+ ```bash
21
+ node "<PLUGIN_ROOT>/skills/hyperframes/scripts/plugin-cli.mjs" <command> <args...>
22
+ ```
23
+
24
+ Run in the user's project directory, never in the plugin directory. The launcher
25
+ selects the manifest's CLI version and disables automatic standalone skill
26
+ installation, including during `init`. The first call may download that exact
27
+ CLI version through npm. A download failure is an error, not permission to use
28
+ latest. Existing project dependency files and lockfiles must not be overwritten
29
+ to match the plugin; surface a runtime incompatibility before changing them.
30
+
31
+ - For bundled Node helpers, use the same
32
+ launcher with `--script <absolute-script-path> <args...>`. This passes the
33
+ release version to their dependency loader. External providers, registry
34
+ downloads, and existing project dependencies have their own versions; a plugin
35
+ version alone does not freeze those inputs.
36
+ - Treat the plugin directory as read-only. Put outputs and temporary work in the
37
+ project or a temporary directory. Pass this plugin root and launcher convention
38
+ to any delegated workflow so it does not fall back to global skills.
39
+
40
+ To get newer skills, use the client's plugin update flow and reload the session.
41
+ Do not silently update an installed plugin during a video task.
42
+
43
+ The launcher suppresses CLI update notices as well as standalone skill refreshes, including when running an older CLI with a stale notice cache. It passes the release as `HYPERFRAMES_PLUGIN_VERSION` for plugin-aware helpers and as `HYPERFRAMES_SKILL_PKG_VERSION` for the existing helper-package bootstrap. Our release process versions those packages together.
@@ -2,6 +2,8 @@
2
2
 
3
3
  Read this reference when installing or updating skills, diagnosing unexpected workflow behavior, or running HyperFrames setup in CI.
4
4
 
5
+ For a plugin installation, follow [plugin execution rules](plugin-installation.md); the plugin manager owns updates and all workflows are bundled. The commands below apply only to standalone skills.
6
+
5
7
  HyperFrames installs the core set eagerly and workflow skills lazily.
6
8
 
7
9
  - **Core set:** `/hyperframes`, the `hyperframes-*` domain skills, and `/media-use`.
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ // Run from the user's project; locate the release from this installed file.
3
+ import { spawnSync } from "node:child_process";
4
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
5
+ import { join, win32 } from "node:path";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
+
8
+ const root = fileURLToPath(new URL("../../../", import.meta.url));
9
+ const manifests = [
10
+ "plugin.json",
11
+ ".claude-plugin/plugin.json",
12
+ ".codex-plugin/plugin.json",
13
+ ".cursor-plugin/plugin.json",
14
+ "gemini-extension.json",
15
+ ];
16
+
17
+ export function pluginVersion(pluginRoot = root) {
18
+ for (const path of manifests) {
19
+ const file = join(pluginRoot, path);
20
+ if (!existsSync(file)) continue;
21
+ const manifest = JSON.parse(readFileSync(file, "utf8"));
22
+ if (
23
+ manifest.name !== "hyperframes" ||
24
+ !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(manifest.version ?? "")
25
+ ) {
26
+ throw new Error(`Invalid HyperFrames plugin release: ${file}`);
27
+ }
28
+ return manifest.version;
29
+ }
30
+ throw new Error(
31
+ "No HyperFrames plugin manifest found. Use the standalone skills installation instructions.",
32
+ );
33
+ }
34
+
35
+ export function invocation(
36
+ args,
37
+ {
38
+ version = pluginVersion(),
39
+ env = process.env,
40
+ platform = process.platform,
41
+ node = process.execPath,
42
+ pathExists = existsSync,
43
+ } = {},
44
+ ) {
45
+ if (args[0] === "skills")
46
+ throw new Error(
47
+ "Update HyperFrames through your agent's plugin manager; bundled skills are release-managed.",
48
+ );
49
+ const childEnv = {
50
+ ...env,
51
+ HYPERFRAMES_SKIP_SKILLS: "1",
52
+ HYPERFRAMES_SKILL_PKG_VERSION: version,
53
+ HYPERFRAMES_PLUGIN_VERSION: version,
54
+ HYPERFRAMES_NO_UPDATE_CHECK: "1",
55
+ };
56
+ if (args[0] === "--script") {
57
+ if (!args[1]) throw new Error("--script requires a Node script path.");
58
+ return { command: node, args: args.slice(1), env: childEnv };
59
+ }
60
+ const cliArgs = ["--yes", `hyperframes@${version}`, ...args];
61
+ if (platform !== "win32") return { command: "npx", args: cliArgs, env: childEnv };
62
+ const candidates = [
63
+ env.npm_execpath && win32.join(win32.dirname(env.npm_execpath), "npx-cli.js"),
64
+ win32.join(win32.dirname(node), "node_modules", "npm", "bin", "npx-cli.js"),
65
+ ].filter(Boolean);
66
+ const npx = candidates.find(pathExists);
67
+ if (!npx)
68
+ throw new Error(
69
+ "Cannot find npx-cli.js. Install Node.js with npm or run from an npm environment.",
70
+ );
71
+ return { command: node, args: [npx, ...cliArgs], env: childEnv };
72
+ }
73
+
74
+ if (process.argv[1] && pathToFileURL(realpathSync(process.argv[1])).href === import.meta.url) {
75
+ try {
76
+ const command = invocation(process.argv.slice(2));
77
+ const result = spawnSync(command.command, command.args, {
78
+ env: command.env,
79
+ stdio: "inherit",
80
+ windowsHide: true,
81
+ });
82
+ if (result.error) throw result.error;
83
+ process.exitCode = result.status ?? 1;
84
+ } catch (error) {
85
+ console.error(error.message);
86
+ process.exitCode = 1;
87
+ }
88
+ }
@@ -0,0 +1,117 @@
1
+ import assert from "node:assert/strict";
2
+ import { spawnSync } from "node:child_process";
3
+ import {
4
+ copyFileSync,
5
+ mkdirSync,
6
+ mkdtempSync,
7
+ readFileSync,
8
+ realpathSync,
9
+ rmSync,
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 { invocation, pluginVersion } from "./plugin-cli.mjs";
16
+
17
+ function fixture(t, manifest = "plugin.json", version = "1.2.3") {
18
+ const root = mkdtempSync(join(tmpdir(), "hf-plugin-"));
19
+ t.after(() => rmSync(root, { recursive: true, force: true }));
20
+ mkdirSync(join(root, "skills/hyperframes/scripts"), { recursive: true });
21
+ mkdirSync(join(root, manifest, ".."), { recursive: true });
22
+ writeFileSync(join(root, manifest), JSON.stringify({ name: "hyperframes", version }));
23
+ const launcher = join(root, "skills/hyperframes/scripts/plugin-cli.mjs");
24
+ copyFileSync(new URL("./plugin-cli.mjs", import.meta.url), launcher);
25
+ return { root, launcher };
26
+ }
27
+
28
+ for (const manifest of [
29
+ "plugin.json",
30
+ ".claude-plugin/plugin.json",
31
+ ".codex-plugin/plugin.json",
32
+ ".cursor-plugin/plugin.json",
33
+ "gemini-extension.json",
34
+ ]) {
35
+ test(`locates ${manifest} without repository packages`, (t) => {
36
+ const { root } = fixture(t, manifest);
37
+ assert.equal(pluginVersion(root), "1.2.3");
38
+ });
39
+ }
40
+
41
+ test("rejects missing and floating plugin versions", (t) => {
42
+ const { root } = fixture(t, "plugin.json", "latest");
43
+ assert.throws(() => pluginVersion(root), /Invalid HyperFrames plugin release/);
44
+ rmSync(join(root, "plugin.json"));
45
+ assert.throws(() => pluginVersion(root), /No HyperFrames plugin manifest/);
46
+ });
47
+
48
+ test("pins CLI, suppresses standalone refresh, preserves unrelated environment", () => {
49
+ const result = invocation(["init", "project with spaces", "--non-interactive"], {
50
+ version: "1.2.3",
51
+ platform: "darwin",
52
+ env: { PATH: "/bin", HYPERFRAMES_SKIP_SKILLS: "0" },
53
+ });
54
+ assert.equal(result.command, "npx");
55
+ assert.deepEqual(result.args, [
56
+ "--yes",
57
+ "hyperframes@1.2.3",
58
+ "init",
59
+ "project with spaces",
60
+ "--non-interactive",
61
+ ]);
62
+ assert.deepEqual(result.env, {
63
+ PATH: "/bin",
64
+ HYPERFRAMES_SKIP_SKILLS: "1",
65
+ HYPERFRAMES_SKILL_PKG_VERSION: "1.2.3",
66
+ HYPERFRAMES_PLUGIN_VERSION: "1.2.3",
67
+ HYPERFRAMES_NO_UPDATE_CHECK: "1",
68
+ });
69
+ assert.throws(() => invocation(["skills", "update"], { version: "1.2.3" }), /plugin manager/);
70
+ });
71
+
72
+ test("Windows launches npx through Node without shell argument interpretation", () => {
73
+ const result = invocation(["render", "a & b"], {
74
+ version: "1.2.3",
75
+ platform: "win32",
76
+ node: String.raw`C:\Program Files\nodejs\node.exe`,
77
+ env: { npm_execpath: String.raw`C:\Program Files\nodejs\node_modules\npm\bin\npm-cli.js` },
78
+ pathExists: (p) => p === String.raw`C:\Program Files\nodejs\node_modules\npm\bin\npx-cli.js`,
79
+ });
80
+ assert.equal(result.command, String.raw`C:\Program Files\nodejs\node.exe`);
81
+ assert.deepEqual(result.args, [
82
+ String.raw`C:\Program Files\nodejs\node_modules\npm\bin\npx-cli.js`,
83
+ "--yes",
84
+ "hyperframes@1.2.3",
85
+ "render",
86
+ "a & b",
87
+ ]);
88
+ assert.throws(
89
+ () => invocation([], { version: "1.2.3", platform: "win32", env: {}, pathExists: () => false }),
90
+ /Cannot find npx/,
91
+ );
92
+ });
93
+
94
+ test("installed helper keeps cwd, arguments, release environment and exit code", (t) => {
95
+ const { root, launcher } = fixture(t);
96
+ const project = join(root, "user project");
97
+ mkdirSync(project);
98
+ const helper = join(root, "helper.mjs");
99
+ writeFileSync(
100
+ helper,
101
+ "console.log(JSON.stringify({ cwd: process.cwd(), args: process.argv.slice(2), skip: process.env.HYPERFRAMES_SKIP_SKILLS, version: process.env.HYPERFRAMES_SKILL_PKG_VERSION })); process.exit(7);",
102
+ );
103
+ const before = readFileSync(join(root, "plugin.json"), "utf8");
104
+ const result = spawnSync(
105
+ process.execPath,
106
+ [launcher, "--script", helper, "literal $value & spaces"],
107
+ { cwd: project, encoding: "utf8" },
108
+ );
109
+ assert.equal(result.status, 7, result.stderr);
110
+ assert.deepEqual(JSON.parse(result.stdout), {
111
+ cwd: realpathSync(project),
112
+ args: ["literal $value & spaces"],
113
+ skip: "1",
114
+ version: "1.2.3",
115
+ });
116
+ assert.equal(readFileSync(join(root, "plugin.json"), "utf8"), before);
117
+ });
@@ -9,6 +9,8 @@ description: >
9
9
  HeyGen-hosted cloud, AWS Lambda, and Google Cloud Run rendering.
10
10
  ---
11
11
 
12
+ **Plugin installs:** Before setup or freshness commands, follow [plugin execution rules](../hyperframes/references/plugin-installation.md) when this skill is inside a HyperFrames plugin. Standalone installs keep the update instructions below.
13
+
12
14
  # HyperFrames CLI
13
15
 
14
16
  Run commands as `npx hyperframes ...` unless project instructions provide a wrapper. Obey the wrapper when present. The CLI requires Node.js 22 or newer and FFmpeg.
@@ -111,6 +111,7 @@ Events include two fingerprint properties used to distinguish managed-sandbox ru
111
111
 
112
112
  - **`sandbox_runtime`**: `gvisor` / `firecracker` / `docker` / `kvm` / `wsl` / `null`. gVisor via kernel string + `/proc/version`. Firecracker via `/dev/vsock` + DMI sys_vendor. Docker via `/.dockerenv` + cgroup.
113
113
  - **`agent_runtime`**: `claude_code` / `codex` / `cursor` / `copilot_agent` / `jules` / `replit` / `devin` / `aider` / `gemini_cli` / `hermes` / `openclaw` / `null`. Detected by the existence of well-known vendor env vars; the values themselves are never read.
114
+ - **`client`**: set only when an app launched the CLI with a `HYPERFRAMES_CLIENT` tag (`<app>/<version>/<channel>`), sent as-is when it is a short slug.
114
115
 
115
116
  ## Asset Preprocessing
116
117
 
@@ -3,6 +3,8 @@ name: media-use
3
3
  description: Agent Media OS, the single skill for every media need in a HyperFrames project. Resolve BGM, SFX, image, icon, brand logo, voice, color grade, or LUT into a frozen local file or paste-ready block + ledger record (one verb, `resolve`); generate via TTS / music / image models when the catalog misses; produce voiceover, transcription, captions, and background removal through one shared audio engine; operate on media (cut / reframe / transform); and reuse assets across projects. Also use for vague feedback that real footage looks dark, flat, boring, should feel retro/camcorder/print/ASCII, needs privacy, or needs a media reveal.
4
4
  ---
5
5
 
6
+ **Plugin installs:** Before setup or freshness commands, follow [plugin execution rules](../hyperframes/references/plugin-installation.md) when this skill is inside a HyperFrames plugin. Standalone installs keep the update instructions below.
7
+
6
8
  # media-use
7
9
 
8
10
  The media OS for HyperFrames: resolve · generate · operate · remember — every media type, one skill, zero context noise.