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.
- package/dist/backgroundChecksWorker.js +890 -0
- package/dist/cli.js +4272 -2792
- package/dist/commands/layout-audit.browser.js +55 -25
- package/dist/fontLocalizeCli.js +714 -344
- package/dist/hyperframe-runtime.js +83 -58
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +83 -58
- package/dist/hyperframes-player.global.js +3 -3
- package/dist/registry/localSemantic.js +7 -3
- package/dist/renderSetupWorker.js +62 -32
- package/dist/sherpaWorker.js +179 -0
- package/dist/skills/hyperframes/SKILL.md +2 -0
- package/dist/skills/hyperframes/references/plugin-installation.md +43 -0
- package/dist/skills/hyperframes/references/skill-lifecycle.md +2 -0
- package/dist/skills/hyperframes/scripts/plugin-cli.mjs +88 -0
- package/dist/skills/hyperframes/scripts/plugin-cli.test.mjs +117 -0
- package/dist/skills/hyperframes-cli/SKILL.md +2 -0
- package/dist/skills/hyperframes-cli/references/upgrade-info-misc.md +1 -0
- package/dist/skills/media-use/SKILL.md +2 -0
- package/dist/skills/media-use/references/operations.md +7 -5
- package/dist/skills/media-use/references/setup-providers.md +9 -9
- package/dist/skills/media-use/registry/localSemantic.js +7 -3
- package/dist/skills/media-use/scripts/lib/ff-binaries.mjs +37 -0
- package/dist/skills/media-use/scripts/lib/probe.mjs +3 -1
- package/dist/skills/media-use/scripts/lib/probe.test.mjs +118 -1
- package/dist/skills/media-use/scripts/lib/tts-local-provider.mjs +5 -3
- package/dist/skills/media-use/scripts/resolve-plugin.test.mjs +62 -0
- package/dist/skills/media-use/scripts/resolve.mjs +45 -19
- package/dist/skills/media-use/scripts/transcribe.mjs +39 -25
- package/dist/skills/media-use/scripts/transcribe.test.mjs +112 -0
- package/dist/skills/registry/localSemantic.js +7 -3
- package/dist/studio/assets/{hyperframes-player-C68UawR3.js → hyperframes-player-BAerMCex.js} +1 -1
- package/dist/studio/assets/{index-CMzPGv9P.js → index-BUhcVhNy.js} +1 -1
- package/dist/studio/assets/{index-hNVM9Pd2.js → index-BYhK_KMc.js} +187 -187
- package/dist/studio/assets/{index-CS5SImRT.js → index-Iv0HI7eM.js} +1 -1
- package/dist/studio/{chunk-I7XNAIOF.js → chunk-3IOCVWB6.js} +6 -97
- package/dist/studio/chunk-3IOCVWB6.js.map +1 -0
- package/dist/studio/chunk-62DDWOSH.js +189 -0
- package/dist/studio/chunk-62DDWOSH.js.map +1 -0
- package/dist/studio/chunk-LPZMAGS7.js +1871 -0
- package/dist/studio/chunk-LPZMAGS7.js.map +1 -0
- package/dist/studio/chunk-QZ6HNNFG.js +934 -0
- package/dist/studio/chunk-QZ6HNNFG.js.map +1 -0
- package/dist/studio/chunk-S65AQH6M.js +101 -0
- package/dist/studio/chunk-S65AQH6M.js.map +1 -0
- package/dist/studio/{domEditingLayers-MHFDKAQ7.js → domEditingLayers-ZOOMN7CQ.js} +3 -2
- package/dist/studio/index.d.ts +834 -330
- package/dist/studio/index.html +1 -1
- package/dist/studio/index.js +26378 -28802
- package/dist/studio/index.js.map +1 -1
- package/dist/studio/player.d.ts +30 -0
- package/dist/studio/player.js +10 -0
- package/dist/studio/player.js.map +1 -0
- package/dist/studio/ui.d.ts +294 -0
- package/dist/studio/ui.js +66 -0
- package/dist/studio/ui.js.map +1 -0
- package/dist/templates/_shared/AGENTS.md +5 -1
- package/dist/templates/_shared/CLAUDE.md +5 -1
- package/package.json +1 -1
- package/dist/studio/chunk-I7XNAIOF.js.map +0 -1
- /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
|
-
|
|
31566
|
-
|
|
31567
|
-
|
|
31568
|
-
|
|
31569
|
-
|
|
31570
|
-
|
|
31571
|
-
|
|
31572
|
-
|
|
31573
|
-
|
|
31574
|
-
|
|
31575
|
-
|
|
31576
|
-
|
|
31577
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
31652
|
-
const unique = [...
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
31748
|
+
missingByFile.set(file, (missingByFile.get(file) ?? /* @__PURE__ */ new Set()).add(url));
|
|
31719
31749
|
}
|
|
31720
31750
|
}
|
|
31721
31751
|
}
|
|
31722
|
-
|
|
31723
|
-
|
|
31724
|
-
|
|
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.
|