hyperframes 0.7.19 → 0.7.21

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 CHANGED
@@ -50,7 +50,7 @@ var VERSION;
50
50
  var init_version = __esm({
51
51
  "src/version.ts"() {
52
52
  "use strict";
53
- VERSION = true ? "0.7.19" : "0.0.0-dev";
53
+ VERSION = true ? "0.7.21" : "0.0.0-dev";
54
54
  }
55
55
  });
56
56
 
@@ -54774,7 +54774,7 @@ function shouldClampMediaDuration(declaredDuration, maxDuration) {
54774
54774
  return declaredDuration > maxDuration + MEDIA_DURATION_CLAMP_EPSILON_SECONDS;
54775
54775
  }
54776
54776
  function getAttr(tag, attr2) {
54777
- const match = tag.match(new RegExp(`${attr2}=["']([^"']+)["']`));
54777
+ const match = tag.match(new RegExp(`(?<![\\w-])${attr2}=["']([^"']+)["']`));
54778
54778
  return match ? match[1] ?? null : null;
54779
54779
  }
54780
54780
  function hasAttr(tag, attr2) {
@@ -65567,13 +65567,15 @@ function findRootTag(source) {
65567
65567
  function readAttr(tagSource, attr2) {
65568
65568
  if (!tagSource) return null;
65569
65569
  const escaped = attr2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
65570
- const match = tagSource.match(new RegExp(`\\b${escaped}\\s*=\\s*["']([^"']+)["']`, "i"));
65570
+ const match = tagSource.match(new RegExp(`(?<![\\w-])${escaped}\\s*=\\s*["']([^"']+)["']`, "i"));
65571
65571
  return match?.[1] || null;
65572
65572
  }
65573
65573
  function readJsonAttr(tagSource, attr2) {
65574
65574
  if (!tagSource) return null;
65575
65575
  const escaped = attr2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
65576
- const match = tagSource.match(new RegExp(`\\b${escaped}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`, "i"));
65576
+ const match = tagSource.match(
65577
+ new RegExp(`(?<![\\w-])${escaped}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`, "i")
65578
+ );
65577
65579
  if (!match) return null;
65578
65580
  return match[1] ?? match[2] ?? null;
65579
65581
  }
@@ -67672,6 +67674,31 @@ var init_dist4 = __esm({
67672
67674
  }
67673
67675
  return findings;
67674
67676
  },
67677
+ // media_crossorigin_breaks_preview — `crossorigin` on <video>/<audio> forces a
67678
+ // CORS-checked fetch. The server-side renderer downloads media directly (no CORS),
67679
+ // so it always works there; but Studio preview runs in the browser, where a media
67680
+ // host that omits Access-Control-Allow-Origin silently fails the load — the media
67681
+ // shows BLANK/black in preview while renders look fine, hiding the bug. Plain
67682
+ // displayed media never needs crossorigin; it's only required to read pixels/samples
67683
+ // back (canvas/WebGL texture, WebAudio createMediaElementSource) AND only when the
67684
+ // host is known CORS-enabled.
67685
+ ({ tags }) => {
67686
+ const findings = [];
67687
+ for (const tag of tags) {
67688
+ if (tag.name !== "video" && tag.name !== "audio") continue;
67689
+ if (!hasAttrName(tag.raw, "crossorigin")) continue;
67690
+ const elementId = readAttr(tag.raw, "id") || void 0;
67691
+ findings.push({
67692
+ code: "media_crossorigin_breaks_preview",
67693
+ severity: "error",
67694
+ message: `<${tag.name}${elementId ? ` id="${elementId}"` : ""}> has crossorigin, which forces a CORS-checked fetch. If the media host omits Access-Control-Allow-Origin, the load silently fails in Studio preview (media shows BLANK/black) while server-side renders still work \u2014 hiding the bug.`,
67695
+ elementId,
67696
+ fixHint: "Remove the crossorigin attribute unless you read the media back via canvas/WebGL/WebAudio AND the host is known to send CORS headers. Plain displayed media never needs it.",
67697
+ snippet: truncateSnippet(tag.raw)
67698
+ });
67699
+ }
67700
+ return findings;
67701
+ },
67675
67702
  // video_audio_double_source — catches audible <video> paired with a separate
67676
67703
  // <audio> pointing to the same file, which causes double playback at runtime
67677
67704
  ({ tags }) => {
@@ -99121,6 +99148,17 @@ var init_captureStageError = __esm({
99121
99148
  }
99122
99149
  });
99123
99150
 
99151
+ // ../producer/src/services/render/captureBeyondViewport.ts
99152
+ function resolveVideoCaptureBeyondViewport(videoCount, browserGpuMode) {
99153
+ if (videoCount <= 0) return void 0;
99154
+ return browserGpuMode === "hardware";
99155
+ }
99156
+ var init_captureBeyondViewport = __esm({
99157
+ "../producer/src/services/render/captureBeyondViewport.ts"() {
99158
+ "use strict";
99159
+ }
99160
+ });
99161
+
99124
99162
  // ../producer/src/services/render/captureCost.ts
99125
99163
  import { join as join34 } from "path";
99126
99164
  function estimateCaptureCostMultiplier(compiled) {
@@ -104896,6 +104934,15 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
104896
104934
  }
104897
104935
  const framesDir = join49(workDir, "captured-frames");
104898
104936
  if (!existsSync43(framesDir)) mkdirSync23(framesDir, { recursive: true });
104937
+ const resolvedBrowserGpuMode = await resolveBrowserGpuMode(cfg.browserGpuMode, {
104938
+ chromePath: resolveHeadlessShellPath(cfg),
104939
+ browserTimeout: cfg.browserTimeout
104940
+ });
104941
+ updateCaptureObservability({ browserGpuMode: resolvedBrowserGpuMode });
104942
+ const videoCaptureBeyondViewport = resolveVideoCaptureBeyondViewport(
104943
+ composition.videos.length,
104944
+ resolvedBrowserGpuMode
104945
+ );
104899
104946
  const captureOptions = {
104900
104947
  width,
104901
104948
  height,
@@ -104904,7 +104951,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
104904
104951
  quality: needsAlpha ? void 0 : job.config.quality === "draft" ? 80 : 95,
104905
104952
  variables: job.config.variables,
104906
104953
  deviceScaleFactor,
104907
- ...composition.videos.length > 0 ? { captureBeyondViewport: true } : {}
104954
+ ...videoCaptureBeyondViewport !== void 0 ? { captureBeyondViewport: videoCaptureBeyondViewport } : {}
104908
104955
  };
104909
104956
  resolvedCaptureBeyondViewport = captureOptions.captureBeyondViewport ?? resolvedCaptureBeyondViewport;
104910
104957
  if (resolvedCaptureBeyondViewport !== void 0) {
@@ -105425,6 +105472,7 @@ var init_renderOrchestrator = __esm({
105425
105472
  init_hdrMode();
105426
105473
  init_perfSummary();
105427
105474
  init_captureStageError();
105475
+ init_captureBeyondViewport();
105428
105476
  init_captureCost();
105429
105477
  init_observability();
105430
105478
  init_compileStage();
@@ -107260,6 +107308,10 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
107260
107308
  rebuildExtractedFramesFromPlanDir(planDir, planVideos.extracted)
107261
107309
  )
107262
107310
  ) : null;
107311
+ const videoCaptureBeyondViewport = resolveVideoCaptureBeyondViewport(
107312
+ planVideos?.videos.length ?? 0,
107313
+ "software"
107314
+ );
107263
107315
  const workDir = `${outputChunkPath}.work.${process.pid}.${randomBytes2(4).toString("hex")}`;
107264
107316
  mkdirSync27(workDir, { recursive: true });
107265
107317
  const framesDir = join57(workDir, "captured-frames");
@@ -107286,7 +107338,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
107286
107338
  // declare `data-composition-variables` leave this undefined and the
107287
107339
  // engine skips the `evaluateOnNewDocument` injection.
107288
107340
  variables: encoder.variables,
107289
- ...(planVideos?.videos.length ?? 0) > 0 ? { captureBeyondViewport: true } : {},
107341
+ ...videoCaptureBeyondViewport !== void 0 ? { captureBeyondViewport: videoCaptureBeyondViewport } : {},
107290
107342
  // lock the BeginFrame warmup loop to a fixed iteration count so
107291
107343
  // `beginFrameTimeTicks` is host-independent. Only chunks ever set this.
107292
107344
  lockWarmupTicks: true
@@ -107454,6 +107506,7 @@ var init_renderChunk = __esm({
107454
107506
  init_logger();
107455
107507
  init_encodeStage();
107456
107508
  init_captureStage();
107509
+ init_captureBeyondViewport();
107457
107510
  init_freezePlan();
107458
107511
  init_planHash();
107459
107512
  init_runtimeEnvSnapshot();
@@ -20,6 +20,7 @@ Everything runs through `npx hyperframes` unless project instructions specify a
20
20
  - Deliver: `npx hyperframes render --quality high --output out.mp4`
21
21
  - CI / cross-host repro: `npx hyperframes render --docker --strict --output out.mp4`
22
22
  - Cloud (long / large): `npx hyperframes lambda render ./my-project --width 1920 --height 1080 --wait` (see Lambda below)
23
+ 8. **Report feedback** — after verifying the output, `npx hyperframes feedback --rating <1-5> --comment "..."` once per task (see Agent Conventions).
23
24
 
24
25
  Run lint, validate, and inspect before preview. `lint` catches missing `data-composition-id`, overlapping tracks, and unregistered timelines. `validate` loads the composition in headless Chrome and reports runtime console errors plus WCAG contrast issues. `inspect` seeks through the timeline and reports text spilling out of bubbles/containers or off the canvas — and, when a `*.motion.json` sidecar is present, verifies motion intent (entrances firing under seek, stagger order, in-frame, liveness) against that same seeked timeline.
25
26
 
@@ -38,6 +39,7 @@ Cross-cutting rules that hold for every command:
38
39
  - **Use Studio context for user-directed edits.** When the user says "this selected element", "the thing I clicked", "current selection", or similar, ask them to select it in Studio, then run `npx hyperframes preview --context --json --context-fields selection`. Use the returned `selection.target.hfId` / `selector`, `selection.sourceFile`, `selection.currentTime`, and `selection.thumbnailUrl` to anchor the edit. If `selection` is `null` and `errors.selection.code` is `no-selection`, ask the user to click the element and rerun; do not guess from screenshots.
39
40
  - **Keep Studio context compact.** `preview --context --json` returns compact selection by default. Add `--context-fields selection`, `--context-fields selection,lint`, or `--context-fields lint` to avoid bloating agent context. Use `--context-detail full` only when you need heavy fields like computed styles, inline styles, or text-field metadata.
40
41
  - **Post-render verification.** After `render` returns exit 0, confirm the output file exists and has plausible size before reporting success: `[ -s "$OUTPUT" ] || echo "render produced no output"`. The CLI prints `◇ <path>` on success; for long renders also sanity-check duration with `ffprobe -i "$OUTPUT" -show_format -v error`.
42
+ - **Report feedback after a successful render.** Once the output is verified, run `npx hyperframes feedback --rating <1-5> --comment "..."` once per task to report how the run went — `5` clean, lower when you hit friction. Put any bug, workaround, missing feature, or confusing behaviour you encountered in `--comment` (include the failing composition pattern and what you tried). This is the project's primary signal channel; renders that go silent leave the maintainers blind. Skip only when telemetry is disabled or the user opted out.
41
43
 
42
44
  ## Routing
43
45
 
@@ -140,6 +140,17 @@ npx hyperframes render --docker # byte-identical
140
140
 
141
141
  **Parametrized renders:** the composition declares its variables on the `<html>` root with **`data-composition-variables`** — a JSON **array of declarations** (`{id, type, label, default}` per entry) that defines the schema. Scripts inside read the resolved values via `window.__hyperframes.getVariables()`. The CLI `--variables '{"title":"Q4 Report"}'` is a JSON **object keyed by id** that overrides those declared defaults for one render; missing keys fall through, so the same composition runs unchanged in dev preview and in production. Sub-comp hosts can also override per-instance with `data-variable-values`. See the `hyperframes-core` skill for the full pattern.
142
142
 
143
+ ### feedback (report after rendering)
144
+
145
+ After a render is verified, send one feedback line per task. This is the maintainers' primary signal — a render that finishes silently tells them nothing.
146
+
147
+ ```bash
148
+ npx hyperframes feedback --rating 5 # clean run, no notes
149
+ npx hyperframes feedback --rating 3 --comment "bg <video> renders grey in multi-scene; worked around with --format png-sequence"
150
+ ```
151
+
152
+ `--rating` is 1-5 (required); `--comment` is free text — use it for any bug, workaround, missing feature, or confusing behaviour, plus the composition pattern that triggered it and what you tried. Feedback is anonymous and attaches a `doctorSummary` (OS/Node/CPU/mem/ffmpeg) automatically. No-ops when telemetry is disabled.
153
+
143
154
  ## publish
144
155
 
145
156
  ```bash
@@ -1 +1 @@
1
- import{g as P}from"./index-DXWeH-EY.js";function j(c,d){for(var s=0;s<d.length;s++){const a=d[s];if(typeof a!="string"&&!Array.isArray(a)){for(const i in a)if(i!=="default"&&!(i in c)){const l=Object.getOwnPropertyDescriptor(a,i);l&&Object.defineProperty(c,i,l.get?l:{enumerable:!0,get:()=>a[i]})}}}return Object.freeze(Object.defineProperty(c,Symbol.toStringTag,{value:"Module"}))}var v={},w;function k(){if(w)return v;w=1,Object.defineProperty(v,"__esModule",{value:!0}),v.default=d;var c=window.OfflineAudioContext||window.webkitOfflineAudioContext;function d(e){var r=a(e);return r.start(0),[i,y,O(e.sampleRate),s].reduce(function(t,o){return o(t)},r.buffer.getChannelData(0))}function s(e){return e.sort(function(r,t){return t.count-r.count}).splice(0,5)[0].tempo}function a(e){var r=e.length,t=e.numberOfChannels,o=e.sampleRate,n=new c(t,r,o),u=n.createBufferSource();u.buffer=e;var f=n.createBiquadFilter();return f.type="lowpass",u.connect(f),f.connect(n.destination),u}function i(e){for(var r=[],t=.9,o=.3,n=15;r.length<n&&t>=o;)r=l(e,t),t-=.05;if(r.length<n)throw new Error("Could not find enough samples for a reliable detection.");return r}function l(e,r){for(var t=[],o=0,n=e.length;o<n;o+=1)e[o]>r&&(t.push(o),o+=1e4);return t}function y(e){var r=[];return e.forEach(function(t,o){for(var n=function(x){var g=e[o+x]-t,_=r.some(function(h){if(h.interval===g)return h.count+=1});_||r.push({interval:g,count:1})},u=0;u<10;u+=1)n(u)}),r}function O(e){return function(r){var t=[];return r.forEach(function(o){if(o.interval!==0){for(var n=60/(o.interval/e);n<90;)n*=2;for(;n>180;)n/=2;n=Math.round(n);var u=t.some(function(f){if(f.tempo===n)return f.count+=o.count});u||t.push({tempo:n,count:o.count})}}),t}}return v}var p,b;function q(){return b||(b=1,p=k().default),p}var m=q();const A=P(m),D=j({__proto__:null,default:A},[m]);export{D as i};
1
+ import{g as P}from"./index-jbPe1Dih.js";function j(c,d){for(var s=0;s<d.length;s++){const a=d[s];if(typeof a!="string"&&!Array.isArray(a)){for(const i in a)if(i!=="default"&&!(i in c)){const l=Object.getOwnPropertyDescriptor(a,i);l&&Object.defineProperty(c,i,l.get?l:{enumerable:!0,get:()=>a[i]})}}}return Object.freeze(Object.defineProperty(c,Symbol.toStringTag,{value:"Module"}))}var v={},w;function k(){if(w)return v;w=1,Object.defineProperty(v,"__esModule",{value:!0}),v.default=d;var c=window.OfflineAudioContext||window.webkitOfflineAudioContext;function d(e){var r=a(e);return r.start(0),[i,y,O(e.sampleRate),s].reduce(function(t,o){return o(t)},r.buffer.getChannelData(0))}function s(e){return e.sort(function(r,t){return t.count-r.count}).splice(0,5)[0].tempo}function a(e){var r=e.length,t=e.numberOfChannels,o=e.sampleRate,n=new c(t,r,o),u=n.createBufferSource();u.buffer=e;var f=n.createBiquadFilter();return f.type="lowpass",u.connect(f),f.connect(n.destination),u}function i(e){for(var r=[],t=.9,o=.3,n=15;r.length<n&&t>=o;)r=l(e,t),t-=.05;if(r.length<n)throw new Error("Could not find enough samples for a reliable detection.");return r}function l(e,r){for(var t=[],o=0,n=e.length;o<n;o+=1)e[o]>r&&(t.push(o),o+=1e4);return t}function y(e){var r=[];return e.forEach(function(t,o){for(var n=function(x){var g=e[o+x]-t,_=r.some(function(h){if(h.interval===g)return h.count+=1});_||r.push({interval:g,count:1})},u=0;u<10;u+=1)n(u)}),r}function O(e){return function(r){var t=[];return r.forEach(function(o){if(o.interval!==0){for(var n=60/(o.interval/e);n<90;)n*=2;for(;n>180;)n/=2;n=Math.round(n);var u=t.some(function(f){if(f.tempo===n)return f.count+=o.count});u||t.push({tempo:n,count:o.count})}}),t}}return v}var p,b;function q(){return b||(b=1,p=k().default),p}var m=q();const A=P(m),D=j({__proto__:null,default:A},[m]);export{D as i};