Files
hyperframes/packages/core
Vance IngallsandClaude Opus 5 7393095be8 fix(studio,core,engine): close the defects a max-effort review found in the fixes
A review of the five fix commits found eleven real defects, including a
regression one of them introduced. Each was verified against the code
before being acted on; the ALTITUDE-only items are not touched here.

REGRESSION, from "group rows survive a collapse". Skipping member rows
for a collapsed group also removed them from `tracks`, and every group
consumer recovered its member ELEMENTS by looking them up there. Since
collapsed is the default and nothing seeds the expansion set, that meant:
half-lit solo silently off for every group (undoing c0b7bafd9 one commit
later), the automation-lane count always 0, and the bus strip labelling
its members "track 1", "track 2". Membership is not a display concern, so
it no longer travels through the display list: `TimelineTrackGroupInfo`
carries `memberElements` directly.

Group bus. `reanchor` wrote `fader.gain.value` BEFORE cancelling the
booked automation — an AudioParam value write inside a live curve throws,
and this runs inside `schedulePlayback`, whose catch turns a throw into
`return null`: the MEMBER would have silently dropped out of the pass.
Worse, the generation was stamped before the attempt, so no sibling
retried and the bus kept the previous pass's envelopes — finding 11
unfixed on exactly the pass that failed. Now: clear first, stamp only on
success, and isolate the call. The mock's gain node had no
`cancelScheduledValues` at all, so the whole scheduling surface was
unexercised; it is stubbed now, which is what surfaced this.

`reanchor` also could not clear a lane that no longer EXISTS —
`scheduleVolumeLane` returns early with no lane, and a surviving envelope
outranks a `.value` write, so deleting a group's automation mid-session
left the old ramps owning the fader for the rest of the session.

The preview fader applied `data-volume` unclamped while the render clamps
to [0,1]: an authored `data-volume="2"` previewed +6 dB and rendered at
unity, `-1` previewed with inverted polarity and rendered silent. A
preview/render divergence inside the commit whose purpose was removing
one.

Pitch shift. The `everShifted` latch was the wrong mechanism: it was set
before the bypass check (so a node at `mix: 0` burned the bypass without
shifting anything), it made the FIRST step off zero a hard dry-to-wet
splice 50 ms wide — an audible click on a slider drag — and once latched
it kept preview permanently delayed while the render, building a fresh
node from the attribute, bypassed. Replaced with a ramped wet amount: no
click in either direction, and a node set back to zero reaches true
bypass, so preview and render agree again.

Silent no-ops. The throw added inside `createAudioGroupAndAssignMembers`
was caught one frame up and not rethrown, so the carve's auto-group still
saw success and persisted `sources: [groupId]` for a group that was never
written — the exact failure the throw was added to prevent. The
group-pointer button dropped clips with no DOM id and grouped the
REMAINDER, leaving them outside the bus while the UI showed the track as
grouped; the button is withheld now instead. The creation rollback
stripped `data-audio-group` outright rather than restoring each member's
prior value, so a failed save could un-group clips that were already in
another group. `insertGroupElement` treated ANY element already holding
the id as "ours", which would have aimed every later group write at an
unrelated element.

`setAudioMuteHidden` rescheduled Web Audio mid-play without `stopAll()`.
Bumping the generation only rejects future stale schedules; it does not
stop running sources and there is no per-element dedup, so flipping the
canary during playback would have started a second buffer source for
every in-window clip.

`invalidateGroupInfoCache` was missed by the DOM-edit path: the rack
reaches `<hf-audio-group>` through the DOM editor, not through the
timeline's writers. Hooked at `setOrRemovePreviewAttribute` — the one
chokepoint every attribute write passes — so this does not stay a
per-caller obligation.

Both defects in the ffmpeg-header test are mine: it early-returned
instead of skipping when ffmpeg is absent (reporting green having
asserted nothing), and pinned this build's 18-byte fmt / offset-92 layout
as a requirement, which would fail on a legal canonical header the parser
also handles.

Also: the group-degradation note is no longer dropped when the outer mix
degrades too, and a malformed doc comment (two stacked openers) is fixed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 16:39:39 -07:00
..

@hyperframes/core

Types, parsers, generators, compiler, linter, runtime, and frame adapters for the Hyperframes video framework.

Install

npm install @hyperframes/core

Most users don't need to install core directly — the CLI, producer, and studio packages depend on it internally.

What's inside

Module Description
Types TimelineElement, CompositionSpec, Asset, canvas dimensions, defaults
Parsers parseHtml — extract timeline elements from HTML; parseGsapScript — parse GSAP animations
Generators generateHyperframesHtml — produce valid Hyperframes HTML from a composition spec
Compiler compileTimingAttrs — resolve data-start / data-duration into absolute times
Linter lintHyperframeHtml — validate Hyperframes HTML (missing attributes, overlapping tracks, etc.)
Runtime IIFE script injected into the browser — manages seek, media playback, and the window.__hf protocol
Frame Adapters Pluggable animation drivers (GSAP, Lottie, CSS, or custom)

Frame Adapters

A frame adapter tells the engine how to seek your animation to a specific frame:

import { createGSAPFrameAdapter } from "@hyperframes/core";

const adapter = createGSAPFrameAdapter({
  getTimeline: () => gsap.timeline(),
  compositionId: "my-video",
});

Implement FrameAdapter for custom animation runtimes:

import type { FrameAdapter } from "@hyperframes/core";

const myAdapter: FrameAdapter = {
  id: "my-adapter",
  getDurationFrames: () => 300,
  seekFrame: (frame) => {
    /* seek your animation */
  },
};

Parsing and generating HTML

import { parseHtml, generateHyperframesHtml } from "@hyperframes/core";

const { elements, metadata } = parseHtml(htmlString);
const html = generateHyperframesHtml(spec);

Linting

import { lintHyperframeHtml } from "@hyperframes/core/lint";

const result = lintHyperframeHtml(htmlString);
// result.findings: { severity, message, elementId }[]

Documentation

Full documentation: hyperframes.heygen.com/packages/core