mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fix(studio): Studio's own text reads without em or en dashes (#5414)
* fix(studio): text in Studio reads without em or en dashes * fix(studio): the dash check also catches HTML entities, and peak reads Unavailable * test(studio): the dash check test covers every entity form * fix(studio): messages Studio shows from lint, sdk, core and the renderer drop dashes too * fix(producer): the gate check stops early on the engine's new fallback wording
This commit is contained in:
@@ -122,7 +122,7 @@ runtime expression, but it cannot always rewrite that source safely.
|
||||
|
||||
- **Unroll to edit** rewrites supported helper- or loop-generated motion as
|
||||
explicit tweens that Studio can edit.
|
||||
- **Computed value — edit it in the Code tab** means the value must be changed
|
||||
- **Computed value. Edit it in the Code tab.**: the value must be changed
|
||||
in source.
|
||||
- When the intent is easier to state than the implementation, copy the element
|
||||
context to your agent and describe the result you want preserved.
|
||||
|
||||
+3
-2
@@ -26,7 +26,7 @@
|
||||
"sync-schemas:check": "tsx scripts/sync-schemas.ts --check",
|
||||
"sync:package-subpaths": "node scripts/package-subpaths.mjs --write",
|
||||
"check:package-subpaths": "node scripts/package-subpaths.mjs",
|
||||
"lint": "bun run check:docs-snippet-motion && bun run check:tracked-artifacts && bun run check:workspace-contracts && bun run check:gcp-cloud-run-dockerfile && bun run check:package-cycles && bun run check:package-subpaths && bun run check:cli-process-ownership && oxlint . && tsx scripts/lint-skills.ts && node scripts/check-skill-mirror.mjs",
|
||||
"lint": "bun run check:docs-snippet-motion && bun run check:studio-copy-dashes && bun run check:tracked-artifacts && bun run check:workspace-contracts && bun run check:gcp-cloud-run-dockerfile && bun run check:package-cycles && bun run check:package-subpaths && bun run check:cli-process-ownership && oxlint . && tsx scripts/lint-skills.ts && node scripts/check-skill-mirror.mjs",
|
||||
"check:gcp-cloud-run-dockerfile": "bun run --cwd packages/gcp-cloud-run test:dockerfile-workspaces",
|
||||
"lint:skills": "tsx scripts/lint-skills.ts",
|
||||
"check:skill-mirror": "node scripts/check-skill-mirror.mjs",
|
||||
@@ -40,6 +40,7 @@
|
||||
"check:workspace-contracts": "node scripts/check-workspace-contracts.mjs",
|
||||
"check:package-cycles": "node scripts/check-package-cycles.mjs",
|
||||
"check:cli-process-ownership": "node scripts/check-cli-process-ownership.mjs",
|
||||
"check:studio-copy-dashes": "node scripts/check-studio-copy-dashes.mjs",
|
||||
"format": "oxfmt .",
|
||||
"test": "bun run test:unit",
|
||||
"test:unit": "bun run --filter '*' test",
|
||||
@@ -52,7 +53,7 @@
|
||||
"player:perf": "bun run --filter @hyperframes/player perf",
|
||||
"format:check": "oxfmt --check .",
|
||||
"knip": "knip",
|
||||
"test:scripts": "node --import tsx --test scripts/animejs-v4-guidance.test.mjs scripts/check-tracked-artifacts.test.mjs scripts/check-registry-set-delta.test.mjs scripts/check-no-main-deletions.test.mjs scripts/check-pr-captures.test.mjs scripts/check-comment-citations.test.mjs scripts/comments-workflow.test.mjs scripts/comment-ratchet.test.mjs scripts/check-docs-snippet-motion.test.mjs scripts/registry-target-paths.test.mjs scripts/check-workspace-contracts.test.mjs scripts/check-media-use-copy-parity.test.mjs scripts/check-svg-sanitize-parity.test.mjs scripts/check-media-use-svg-sanitize-generated.test.mjs scripts/check-package-cycles.test.mjs scripts/check-cli-process-ownership.test.mjs scripts/check-large-files.test.mjs scripts/prune-fallow-audit-caches.test.mjs scripts/package-subpaths.test.mjs scripts/validate-release-channel.test.mjs scripts/clawhub-sync.test.mjs scripts/publish-workflow.test.mjs scripts/pr-edit-concurrency.test.mjs scripts/merge-queue-workflows.test.mjs scripts/install-workspace-dependencies.test.mjs scripts/draft-changelog.test.ts scripts/set-version.test.ts scripts/release-prepare.test.ts scripts/cli-options.test.ts scripts/changelog-weekly.test.ts scripts/claude-plugin-compression.test.ts scripts/catalog-payload-assets.test.ts scripts/host-registry-assets.test.ts scripts/catalog-preview-temp.test.ts scripts/backfill-block-previews.test.ts scripts/catalog-hosted-files.test.ts scripts/player-cdn-pin.test.ts scripts/studio-runtime-smoke.test.mjs scripts/verify-packed-manifests.test.mjs scripts/lint-skills.test.mjs scripts/creator-editing-recipes.test.mjs packages/gcp-cloud-run/check-dockerfile-workspaces.test.mjs packages/core/scripts/writeGeneratedFile.test.ts scripts/catalog-publication.test.mjs scripts/ci/resolve-workflow-pr.test.mjs scripts/ci/base-file.test.mjs scripts/check-catalog-source-pr.test.mjs scripts/generate-registry-items.test.ts scripts/catalog-drift.test.ts scripts/catalog-fetch-mirror.test.ts scripts/catalog-script-inlining.test.ts scripts/catalog-detail.test.ts scripts/generate-catalog-pages.test.ts scripts/verify-catalog-payloads.test.ts scripts/registry-skill-files.test.ts scripts/creator-editing-capabilities.test.mjs scripts/generate-catalog-previews.test.ts scripts/registry-primitive-payloads.test.ts registry/components/pan-stations/pan-stations.test.mjs && vitest run scripts/catalog/ scripts/contrastRatchet.test.ts scripts/generate-catalog-payloads.test.ts",
|
||||
"test:scripts": "node --import tsx --test scripts/animejs-v4-guidance.test.mjs scripts/check-tracked-artifacts.test.mjs scripts/check-registry-set-delta.test.mjs scripts/check-no-main-deletions.test.mjs scripts/check-pr-captures.test.mjs scripts/check-comment-citations.test.mjs scripts/comments-workflow.test.mjs scripts/comment-ratchet.test.mjs scripts/check-docs-snippet-motion.test.mjs scripts/registry-target-paths.test.mjs scripts/check-workspace-contracts.test.mjs scripts/check-media-use-copy-parity.test.mjs scripts/check-svg-sanitize-parity.test.mjs scripts/check-media-use-svg-sanitize-generated.test.mjs scripts/check-package-cycles.test.mjs scripts/check-cli-process-ownership.test.mjs scripts/check-studio-copy-dashes.test.mjs scripts/check-large-files.test.mjs scripts/prune-fallow-audit-caches.test.mjs scripts/package-subpaths.test.mjs scripts/validate-release-channel.test.mjs scripts/clawhub-sync.test.mjs scripts/publish-workflow.test.mjs scripts/pr-edit-concurrency.test.mjs scripts/merge-queue-workflows.test.mjs scripts/install-workspace-dependencies.test.mjs scripts/draft-changelog.test.ts scripts/set-version.test.ts scripts/release-prepare.test.ts scripts/cli-options.test.ts scripts/changelog-weekly.test.ts scripts/claude-plugin-compression.test.ts scripts/catalog-payload-assets.test.ts scripts/host-registry-assets.test.ts scripts/catalog-preview-temp.test.ts scripts/backfill-block-previews.test.ts scripts/catalog-hosted-files.test.ts scripts/player-cdn-pin.test.ts scripts/studio-runtime-smoke.test.mjs scripts/verify-packed-manifests.test.mjs scripts/lint-skills.test.mjs scripts/creator-editing-recipes.test.mjs packages/gcp-cloud-run/check-dockerfile-workspaces.test.mjs packages/core/scripts/writeGeneratedFile.test.ts scripts/catalog-publication.test.mjs scripts/ci/resolve-workflow-pr.test.mjs scripts/ci/base-file.test.mjs scripts/check-catalog-source-pr.test.mjs scripts/generate-registry-items.test.ts scripts/catalog-drift.test.ts scripts/catalog-fetch-mirror.test.ts scripts/catalog-script-inlining.test.ts scripts/catalog-detail.test.ts scripts/generate-catalog-pages.test.ts scripts/verify-catalog-payloads.test.ts scripts/registry-skill-files.test.ts scripts/creator-editing-capabilities.test.mjs scripts/generate-catalog-previews.test.ts scripts/registry-primitive-payloads.test.ts registry/components/pan-stations/pan-stations.test.mjs && vitest run scripts/catalog/ scripts/contrastRatchet.test.ts scripts/generate-catalog-payloads.test.ts",
|
||||
"typecheck:scripts": "tsc --noEmit -p scripts/tsconfig.json",
|
||||
"test:skills": "HYPERFRAMES_MEDIA_HOME_REQUIRED=1 node --import ./packages/cli/scripts/test-home.mjs --test 'skills/**/*.test.mjs' 'packages/cli/src/media-use/**/*.test.mjs'",
|
||||
"generate:previews": "tsx scripts/generate-template-previews.ts",
|
||||
|
||||
@@ -450,7 +450,7 @@ it("surfaces the runtime's web-audio-bypass console.info line as its own info fi
|
||||
"info",
|
||||
'[hyperframes] runtime_web_audio_bypass: "https://cdn.example.com/track.mp3" ' +
|
||||
"(cross_origin_no_cors): Web Audio capture withheld; the track plays through native " +
|
||||
"HTMLMediaElement output. Native playback cannot reproduce: fx-chain — proxy or download " +
|
||||
"HTMLMediaElement output. Native playback cannot reproduce: fx-chain. Fix: proxy or download " +
|
||||
"the asset to a same-origin URL to keep it.",
|
||||
);
|
||||
const authorInfo = fakeConsoleMessage("info", "debug runtime_web_audio_bypass lookalike");
|
||||
|
||||
@@ -21,7 +21,7 @@ const dbToLin = (db) => Math.pow(10, db / 20);
|
||||
* Per channel matters: the followers advance once per sample, so a single shared
|
||||
* follower stepped once per channel per sample. On stereo that ran a 20 ms attack
|
||||
* as 10 ms, and gave the right channel a gain computed from an envelope that had
|
||||
* already traversed the left — so the two channels ducked by different amounts
|
||||
* already traversed the left, so the two channels ducked by different amounts
|
||||
* from the same input and the image pumped.
|
||||
*/
|
||||
class EnvBank {
|
||||
@@ -233,7 +233,7 @@ function readTap(ring, write, delaySamples) {
|
||||
return ring[i0] * (1 - frac) + ring[i1] * frac;
|
||||
}
|
||||
|
||||
/** Equal-power-ish crossfade, zero at a tap's reset point — hides the splice. */
|
||||
/** Equal-power-ish crossfade, zero at a tap's reset point, which hides the splice. */
|
||||
function xfade(phase) {
|
||||
return Math.sin(Math.PI * phase);
|
||||
}
|
||||
@@ -245,7 +245,7 @@ function xfade(phase) {
|
||||
* out, which hides the splice each tap makes when it wraps.
|
||||
*
|
||||
* write/phase are block-level state, advanced once per SAMPLE across all
|
||||
* channels together (not once per channel) — advancing them inside the
|
||||
* channels together (not once per channel). Advancing them inside the
|
||||
* per-channel loop would move the tap 2x/4x too fast on a stereo/quad input.
|
||||
*/
|
||||
class HfPitchshift extends AudioWorkletProcessor {
|
||||
@@ -258,13 +258,13 @@ class HfPitchshift extends AudioWorkletProcessor {
|
||||
this.phase = 0;
|
||||
// Samples written so far, capped at one grain. The taps read up to a grain
|
||||
// behind the write head, so until this fills they would read the ring's
|
||||
// zeros — the head of every clip came out attenuated or silent.
|
||||
// zeros, so the head of every clip came out attenuated or silent.
|
||||
this.filled = 0;
|
||||
// How much of the wet (pitch-shifted) path is currently in the output, and
|
||||
// where it is heading. Crossing between dry and wet is a ~50 ms jump in the
|
||||
// signal, so it is RAMPED rather than switched: a hard swap either way is a
|
||||
// click. Ramping in both directions is also what lets a node return to true
|
||||
// bypass at semitones 0 — a one-way latch left preview stuck with the delay
|
||||
// bypass at semitones 0. A one-way latch left preview stuck with the delay
|
||||
// that the render, building a fresh node from the attribute, does not have.
|
||||
this.wet = 0;
|
||||
this.wetTarget = 0;
|
||||
@@ -292,7 +292,7 @@ class HfPitchshift extends AudioWorkletProcessor {
|
||||
|
||||
// Nothing to shift, or mixed fully out. The grain delay is ~grain/2
|
||||
// whatever the ratio, so at semitones=0 this degenerated into a pure 50 ms
|
||||
// delay of the signal — while the copy for that exact setting reads
|
||||
// delay of the signal, while the copy for that exact setting reads
|
||||
// "Unchanged pitch".
|
||||
this.wetTarget = semitones === 0 ? 0 : mix;
|
||||
|
||||
@@ -371,7 +371,7 @@ export function ensureAudioFxWorklets(ctx: BaseAudioContext): Promise<void> {
|
||||
modulePromise = (async () => {
|
||||
if (!ctx.audioWorklet) {
|
||||
throw new Error(
|
||||
"AudioWorklet is unavailable — the page needs a secure context (https, localhost or file://)",
|
||||
"AudioWorklet is unavailable: the page needs a secure context (https, localhost or file://)",
|
||||
);
|
||||
}
|
||||
// A data: URL rather than a blob:, because a blob inherits the page origin
|
||||
|
||||
@@ -103,7 +103,7 @@ const freq = (
|
||||
automatable: true,
|
||||
});
|
||||
|
||||
const qParam = (def = 0.707, hint = "Bandwidth — higher is narrower."): HfAudioFxNumberParam => ({
|
||||
const qParam = (def = 0.707, hint = "Bandwidth: higher is narrower."): HfAudioFxNumberParam => ({
|
||||
kind: "number",
|
||||
key: "q",
|
||||
label: "Q",
|
||||
@@ -187,7 +187,7 @@ export const HF_AUDIO_FX: readonly HfAudioFxDef[] = [
|
||||
id: "highpass",
|
||||
label: "High-pass",
|
||||
group: "filter",
|
||||
description: "Remove low frequencies — the usual fix for rumble on a voice.",
|
||||
description: "Remove low frequencies: the usual fix for rumble on a voice.",
|
||||
params: [freq("frequency", "Cutoff", 300, 20, 20000), qParam(0.707), poles],
|
||||
web: "biquad-highpass",
|
||||
},
|
||||
@@ -195,7 +195,7 @@ export const HF_AUDIO_FX: readonly HfAudioFxDef[] = [
|
||||
id: "lowpass",
|
||||
label: "Low-pass",
|
||||
group: "filter",
|
||||
description: "Remove high frequencies — darkens or muffles a track.",
|
||||
description: "Remove high frequencies, which darkens or muffles a track.",
|
||||
params: [freq("frequency", "Cutoff", 8000, 100, 20000), qParam(0.707), poles],
|
||||
web: "biquad-lowpass",
|
||||
},
|
||||
@@ -288,7 +288,7 @@ export const HF_AUDIO_FX: readonly HfAudioFxDef[] = [
|
||||
id: "limiter",
|
||||
label: "Limiter",
|
||||
group: "dynamics",
|
||||
description: "Hard ceiling — nothing gets past the limit.",
|
||||
description: "Hard ceiling: nothing gets past the limit.",
|
||||
params: [
|
||||
{
|
||||
kind: "number",
|
||||
@@ -533,7 +533,7 @@ export const HF_AUDIO_FX: readonly HfAudioFxDef[] = [
|
||||
max: 250,
|
||||
step: 1,
|
||||
default: 1,
|
||||
hint: "Repeats each sample N times — a crude downsample.",
|
||||
hint: "Repeats each sample N times, a crude downsample.",
|
||||
},
|
||||
{
|
||||
kind: "number",
|
||||
|
||||
@@ -63,7 +63,7 @@ export const EFFECT_COPY: Record<string, EffectCopy> = {
|
||||
},
|
||||
highpass: {
|
||||
title: "Remove Rumble",
|
||||
does: "Cuts the very bottom — traffic, footsteps, air conditioning, hands on the mic.",
|
||||
does: "Cuts the very bottom: traffic, footsteps, air conditioning, hands on the mic.",
|
||||
reachFor: "There's a low hum or thump under everything.",
|
||||
primary: "frequency",
|
||||
primaryEnds: { low: "Only the deepest", high: "Thins it out" },
|
||||
@@ -94,7 +94,7 @@ export const EFFECT_COPY: Record<string, EffectCopy> = {
|
||||
peaking: {
|
||||
title: "Shape One Range",
|
||||
does: "Lifts or lowers one part of the sound and leaves the rest alone.",
|
||||
reachFor: "One quality is wrong — boomy, boxy, harsh — but the rest is fine.",
|
||||
reachFor: "One quality is wrong (boomy, boxy, harsh) but the rest is fine.",
|
||||
primary: "gain",
|
||||
primaryEnds: { low: "Take it out", high: "Bring it forward" },
|
||||
band: [20, 20000],
|
||||
@@ -184,7 +184,7 @@ export const EFFECT_COPY: Record<string, EffectCopy> = {
|
||||
},
|
||||
gate: {
|
||||
title: "Silence the Gaps",
|
||||
does: "Mutes the pauses. Whatever sits underneath stays — this closes the gaps, it does not remove noise.",
|
||||
does: "Mutes the pauses. Whatever sits underneath stays: this closes the gaps, it does not remove noise.",
|
||||
reachFor: "You can hear the room in the gaps.",
|
||||
primary: "strength",
|
||||
primaryEnds: { low: "Only true silence", high: "Cuts quiet parts too" },
|
||||
@@ -192,7 +192,7 @@ export const EFFECT_COPY: Record<string, EffectCopy> = {
|
||||
threshold: { label: "Quieter than this is a gap" },
|
||||
range: {
|
||||
label: "How far to duck the gaps",
|
||||
hint: "Not all the way down, usually — total silence sounds broken.",
|
||||
hint: "Not all the way down, usually. Total silence sounds broken.",
|
||||
},
|
||||
ratio: { label: "How hard" },
|
||||
attack: { label: "How fast it opens" },
|
||||
@@ -410,7 +410,7 @@ export const SUMMARY: Record<string, (p: P) => string> = {
|
||||
? "Doing nothing yet"
|
||||
: `${n(p.gain) > 0 ? "More" : "Less"} sparkle above ${hz(p.frequency)}`,
|
||||
compressor: (p) =>
|
||||
`Evening out — ${strength(Math.min(1, (n(p.ratio, 3) - 1) / 7), ["gentle", "moderate", "firm"])}`,
|
||||
`Evening out, ${strength(Math.min(1, (n(p.ratio, 3) - 1) / 7), ["gentle", "moderate", "firm"])}`,
|
||||
limiter: (p) => `Nothing louder than ${n(p.limit, -1)} dB`,
|
||||
truepeak: (p) => `Peaks pulled down toward ${n(p.ceiling, -1)} dBTP`,
|
||||
gate: (p) => `Closing gaps quieter than ${n(p.threshold, -45)} dB`,
|
||||
|
||||
@@ -183,7 +183,7 @@ export function removeAudioEq(chain: HfAudioFxChain, eqId: string): HfAudioFxCha
|
||||
*/
|
||||
export function audioEqSummary(bands: readonly HfAudioEqBand[]): string {
|
||||
const moved = bands.filter((b) => Math.abs(b.gain) >= 0.1);
|
||||
if (moved.length === 0) return "Flat — nothing changed yet";
|
||||
if (moved.length === 0) return "Flat: nothing changed yet";
|
||||
return moved
|
||||
.map((b) => `${b.name} ${b.gain > 0 ? "+" : "−"}${Math.abs(Number(b.gain.toFixed(1)))}`)
|
||||
.join(", ");
|
||||
|
||||
@@ -51,7 +51,7 @@ export const HF_AUDIO_FX_JOBS: readonly HfAudioFxJob[] = [
|
||||
{
|
||||
id: "tame-boominess",
|
||||
label: "Tame Boominess",
|
||||
does: "Too much low-end body — it booms.",
|
||||
does: "Too much low-end body, so it booms.",
|
||||
type: "peaking",
|
||||
params: { frequency: 200, gain: -4, q: 1.4 },
|
||||
},
|
||||
@@ -72,7 +72,7 @@ export const HF_AUDIO_FX_JOBS: readonly HfAudioFxJob[] = [
|
||||
{
|
||||
id: "add-clarity",
|
||||
label: "Add Clarity",
|
||||
does: "It is hard to make out — it sits back.",
|
||||
does: "It is hard to make out because it sits back.",
|
||||
type: "peaking",
|
||||
params: { frequency: 3000, gain: 2.5, q: 1 },
|
||||
},
|
||||
|
||||
@@ -111,7 +111,7 @@ export const HF_AUDIO_FX_PRESETS: readonly HfAudioFxPreset[] = [
|
||||
"voice-broadcast",
|
||||
"voice",
|
||||
"Broadcast",
|
||||
"Denser and more forward — a radio-presenter sound.",
|
||||
"Denser and more forward: a radio-presenter sound.",
|
||||
[
|
||||
{ type: "highpass", label: "Remove Rumble", params: { frequency: 90, q: 0.707, poles: "2" } },
|
||||
{ type: "peaking", label: "Reduce Boxiness", params: { frequency: 400, gain: -3, q: 1.4 } },
|
||||
@@ -159,7 +159,7 @@ export const HF_AUDIO_FX_PRESETS: readonly HfAudioFxPreset[] = [
|
||||
"room-gate",
|
||||
"repair",
|
||||
"Quiet Between Phrases",
|
||||
"Silences the gaps between words. Room tone under speech stays — this closes the pauses, it does not remove noise.",
|
||||
"Silences the gaps between words. Room tone under speech stays: this closes the pauses, it does not remove noise.",
|
||||
[
|
||||
{
|
||||
type: "gate",
|
||||
@@ -184,23 +184,17 @@ export const HF_AUDIO_FX_PRESETS: readonly HfAudioFxPreset[] = [
|
||||
),
|
||||
|
||||
// ------------------------------------------------------------ character --
|
||||
preset(
|
||||
"telephone",
|
||||
"character",
|
||||
"Telephone",
|
||||
"Down the line — the narrow band of a phone call.",
|
||||
[
|
||||
...steep("highpass", 300, "Strip the Bass"),
|
||||
...steep("lowpass", 3400, "Strip the Treble"),
|
||||
{ type: "peaking", label: "Phone Honk", params: { frequency: 1200, gain: 6, q: 1.2 } },
|
||||
{ type: "peaking", label: "De-mud", params: { frequency: 550, gain: -4, q: 1 } },
|
||||
{
|
||||
type: "saturate",
|
||||
label: "Circuit Grit",
|
||||
params: { type: "tanh", threshold: -9, output: -2 },
|
||||
},
|
||||
],
|
||||
),
|
||||
preset("telephone", "character", "Telephone", "Down the line: the narrow band of a phone call.", [
|
||||
...steep("highpass", 300, "Strip the Bass"),
|
||||
...steep("lowpass", 3400, "Strip the Treble"),
|
||||
{ type: "peaking", label: "Phone Honk", params: { frequency: 1200, gain: 6, q: 1.2 } },
|
||||
{ type: "peaking", label: "De-mud", params: { frequency: 550, gain: -4, q: 1 } },
|
||||
{
|
||||
type: "saturate",
|
||||
label: "Circuit Grit",
|
||||
params: { type: "tanh", threshold: -9, output: -2 },
|
||||
},
|
||||
]),
|
||||
preset("radio-am", "character", "AM Radio", "Narrow, gritty and a little crushed.", [
|
||||
// Narrower than the megaphone at BOTH ends, which is most of the difference
|
||||
// between them: an AM channel is a few kHz wide and the receiver rolls off
|
||||
@@ -324,7 +318,7 @@ export const HF_AUDIO_FX_PRESETS: readonly HfAudioFxPreset[] = [
|
||||
"doofus-worble",
|
||||
"character",
|
||||
"Doofus Worble",
|
||||
"Seasick and wobbling — no straight signal left.",
|
||||
"Seasick and wobbling, no straight signal left.",
|
||||
[
|
||||
{
|
||||
type: "chorus",
|
||||
@@ -357,7 +351,7 @@ export const HF_AUDIO_FX_PRESETS: readonly HfAudioFxPreset[] = [
|
||||
]),
|
||||
|
||||
// ---------------------------------------------------------------- space --
|
||||
preset("room-tight", "space", "Tight Room", "A small hard room — presence without wash.", [
|
||||
preset("room-tight", "space", "Tight Room", "A small hard room: presence without wash.", [
|
||||
{
|
||||
type: "reverb",
|
||||
label: "Tight Room",
|
||||
@@ -380,7 +374,7 @@ export const HF_AUDIO_FX_PRESETS: readonly HfAudioFxPreset[] = [
|
||||
preset("hall", "space", "Hall", "Long and open, for something that should sit far back.", [
|
||||
{ type: "reverb", label: "Hall", params: { size: 0.9, damping: 0.3, wet: 0.4, dry: 0.75 } },
|
||||
]),
|
||||
preset("slap-echo", "space", "Slap Echo", "One quick repeat — rockabilly vocal, not a wash.", [
|
||||
preset("slap-echo", "space", "Slap Echo", "One quick repeat: rockabilly vocal, not a wash.", [
|
||||
{ type: "delay", label: "Slap Echo", params: { time: 110, feedback: 0.12, mix: 0.22 } },
|
||||
]),
|
||||
preset("dub-throw", "space", "Dub Throw", "Repeats that trail off well behind the beat.", [
|
||||
|
||||
@@ -242,7 +242,7 @@ export function removeLevelling(chain: HfAudioFxChain): {
|
||||
/** What the module says when it is closed. */
|
||||
export function levellingSummary(points: readonly HfAutomationPoint[]): string {
|
||||
const moves = points.filter((p) => p.v !== 0);
|
||||
if (moves.length === 0) return "Already even — nothing to do";
|
||||
if (moves.length === 0) return "Already even: nothing to do";
|
||||
const lift = Math.max(...moves.map((p) => p.v));
|
||||
const cut = Math.min(...moves.map((p) => p.v));
|
||||
const parts: string[] = [];
|
||||
|
||||
@@ -200,12 +200,12 @@ export function createFigmaClient(options: FigmaClientOptions): FigmaClient {
|
||||
[
|
||||
"FIGMA_TOKEN is missing. One-time setup:",
|
||||
" 1. figma.com/settings → Security → Personal access tokens → Generate new token",
|
||||
" 2. Scopes (read-only is all this integration ever needs — it never writes to figma):",
|
||||
" 2. Scopes (read-only is all this integration ever needs: it never writes to figma):",
|
||||
" File content: Read-only · File metadata: Read-only",
|
||||
" Library content: Read-only (needed for the `tokens` published-styles fallback)",
|
||||
" Variables: Read-only (optional — brand variables, requires figma Enterprise;",
|
||||
" Variables: Read-only (optional: brand variables, requires figma Enterprise;",
|
||||
" without it `tokens` falls back to published styles)",
|
||||
' 3. export FIGMA_TOKEN="figd_…" — add it to your shell profile or the project .env',
|
||||
' 3. export FIGMA_TOKEN="figd_…", then add it to your shell profile or the project .env',
|
||||
" so future sessions skip this step",
|
||||
"Then re-run this command.",
|
||||
].join("\n"),
|
||||
@@ -237,26 +237,26 @@ export function createFigmaClient(options: FigmaClientOptions): FigmaClient {
|
||||
if (body && /invalid token/i.test(body))
|
||||
return new FigmaClientError(
|
||||
"BAD_TOKEN",
|
||||
"figma rejected the token (403 Invalid token) — it is invalid, expired, or revoked. Re-mint at figma.com/settings → Security, then update FIGMA_TOKEN.",
|
||||
"figma rejected the token (403 Invalid token): it is invalid, expired, or revoked. Re-mint at figma.com/settings → Security, then update FIGMA_TOKEN.",
|
||||
403,
|
||||
opts.endpoint,
|
||||
);
|
||||
if (opts.enterpriseGated)
|
||||
return new FigmaClientError(
|
||||
"REQUIRES_ENTERPRISE",
|
||||
"figma variables require an Enterprise plan (403) — fall back to styles",
|
||||
"figma variables require an Enterprise plan (403), so fall back to styles",
|
||||
403,
|
||||
opts.endpoint,
|
||||
);
|
||||
if (body && /scope/i.test(body))
|
||||
return new FigmaClientError(
|
||||
"FORBIDDEN",
|
||||
`figma denied access (403): ${body} — add the named scope at figma.com/settings → Security → Personal access tokens.`,
|
||||
`figma denied access (403): ${body}. Add the named scope at figma.com/settings → Security → Personal access tokens.`,
|
||||
403,
|
||||
opts.endpoint,
|
||||
);
|
||||
const scopeLine = opts.scopeHint
|
||||
? `This endpoint needs the "${opts.scopeHint}" scope — add it at figma.com/settings → Security → Personal access tokens.`
|
||||
? `This endpoint needs the "${opts.scopeHint}" scope. Add it at figma.com/settings → Security → Personal access tokens.`
|
||||
: "The token is missing a read scope, or your account can't view this file. Check File content: Read-only + File metadata: Read-only at figma.com/settings → Security.";
|
||||
return new FigmaClientError(
|
||||
"FORBIDDEN",
|
||||
@@ -272,7 +272,7 @@ export function createFigmaClient(options: FigmaClientOptions): FigmaClient {
|
||||
if (res.status === 401)
|
||||
throw new FigmaClientError(
|
||||
"BAD_TOKEN",
|
||||
"figma rejected the token (401) — it is expired or revoked. Re-mint at figma.com/settings → Security, then update FIGMA_TOKEN.",
|
||||
"figma rejected the token (401): it is expired or revoked. Re-mint at figma.com/settings → Security, then update FIGMA_TOKEN.",
|
||||
401,
|
||||
opts.endpoint,
|
||||
);
|
||||
@@ -280,7 +280,7 @@ export function createFigmaClient(options: FigmaClientOptions): FigmaClient {
|
||||
if (res.status === 429)
|
||||
throw new FigmaClientError(
|
||||
"RATE_LIMITED",
|
||||
`figma rate limit hit (429) and still limited after ${maxRetries} retries — wait a minute and re-run, or import fewer nodes per call.`,
|
||||
`figma rate limit hit (429) and still limited after ${maxRetries} retries. Wait a minute and re-run, or import fewer nodes per call.`,
|
||||
429,
|
||||
opts.endpoint,
|
||||
);
|
||||
|
||||
@@ -36,7 +36,7 @@ export function emitTimelineScript(spec: TimelineSpec): string {
|
||||
: 'typeof gsap === "undefined"';
|
||||
const libs = needsCustomEase ? "gsap + CustomEase" : "gsap";
|
||||
lines.push(
|
||||
`if (${missing}) { console.warn(${lit(`figma timeline ${spec.timelineId}: ${libs} not loaded — add the CDN <script> tags before this one`)}); return; }`,
|
||||
`if (${missing}) { console.warn(${lit(`figma timeline ${spec.timelineId}: ${libs} not loaded. Add the CDN <script> tags before this one`)}); return; }`,
|
||||
);
|
||||
for (const ce of spec.customEases) {
|
||||
const [x1, y1, x2, y2] = ce.bezier;
|
||||
|
||||
@@ -778,11 +778,11 @@ async function systemFamilyFaceRules(
|
||||
}
|
||||
if (totalBytes > SYSTEM_FONT_SIZE_LIMIT) {
|
||||
log.warn(
|
||||
`[Compiler] System font "${lookupFamily}" is large (${(totalBytes / 1024 / 1024).toFixed(1)} MB total across ${variants.length} variant(s)) — embedding anyway. Consider font subsetting for production.`,
|
||||
`[Compiler] System font "${lookupFamily}" is large (${(totalBytes / 1024 / 1024).toFixed(1)} MB total across ${variants.length} variant(s)), embedding anyway. Consider font subsetting for production.`,
|
||||
);
|
||||
}
|
||||
log.info(
|
||||
`[Compiler] Embedded system font "${lookupFamily}" — ${variants.length} variant(s), ${(totalBytes / 1024).toFixed(0)} KB total`,
|
||||
`[Compiler] Embedded system font "${lookupFamily}": ${variants.length} variant(s), ${(totalBytes / 1024).toFixed(0)} KB total`,
|
||||
);
|
||||
return rules;
|
||||
}
|
||||
@@ -1126,7 +1126,7 @@ function fontCacheDir(slug: string): string {
|
||||
if (firstFallback) {
|
||||
STDERR_LOGGER.warn(
|
||||
`Font cache directory is unwritable (${dir}). ` +
|
||||
`Using temporary fallback — fonts will re-download each run. ` +
|
||||
`Using temporary fallback, so fonts will re-download each run. ` +
|
||||
`Fix with: chmod 755 ${resolveFontCacheRoot()}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -267,7 +267,7 @@ export function injectCompositionCssVariables(doc: Document): void {
|
||||
}
|
||||
for (const group of detectSlugCollisions(allIds)) {
|
||||
console.warn(
|
||||
`composition variables ${group.join(", ")} collapse to the same CSS property ${cssVariableName(group[0] ?? "")} — rename one to avoid cross-talk`,
|
||||
`composition variables ${group.join(", ")} collapse to the same CSS property ${cssVariableName(group[0] ?? "")}. Rename one to avoid cross-talk`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4107,7 +4107,7 @@ export function initSandboxRuntimeModular(): void {
|
||||
);
|
||||
// eslint-disable-next-line no-console -- loud author-facing warning; this render would otherwise freeze at t=0
|
||||
console.warn(
|
||||
`[hyperframes] Root timeline not bound — render will freeze at t=0. ` +
|
||||
`[hyperframes] Root timeline not bound, so render will freeze at t=0. ` +
|
||||
(rootCompositionId
|
||||
? `Root data-composition-id is "${rootCompositionId}" but window.__timelines has no such key. `
|
||||
: `Root composition element has no data-composition-id. `) +
|
||||
|
||||
@@ -496,7 +496,7 @@ function resolveCaptureSource(
|
||||
reportVfxError(
|
||||
`${label}: a capturing chain needs ` +
|
||||
`<canvas layoutsubtree class="hf-vfx-src"><div class="hf-vfx-in">…</div></canvas> in ` +
|
||||
`source — inside the element for its own pixels, or beside it with ` +
|
||||
`source: inside the element for its own pixels, or beside it with ` +
|
||||
`data-vfx-for="${owner.id}" for the layers below it.`,
|
||||
);
|
||||
return undefined;
|
||||
@@ -978,7 +978,7 @@ function captureSource(
|
||||
reportVfxFrameError(
|
||||
`${describeHost(entry.host)}: the .hf-vfx-in wrapper measures 0×0, so its capture ` +
|
||||
`would be empty. Inside a layoutsubtree canvas an inset or percentage box has no ` +
|
||||
`size — the wrapper must state an explicit width and height in px.`,
|
||||
`size, so the wrapper must state an explicit width and height in px.`,
|
||||
);
|
||||
}
|
||||
return captureEmpty(entry, src, size, mode);
|
||||
|
||||
@@ -299,7 +299,7 @@ export function reportWebAudioMediaRoute(el: HTMLMediaElement, route: WebAudioMe
|
||||
// same contract mediaProxy.ts's diagnostics use.
|
||||
const lostNote =
|
||||
lost.length > 0
|
||||
? ` Native playback cannot reproduce: ${lost.join(", ")} — ${REMEDY_BY_REASON[route.reason]}`
|
||||
? ` Native playback cannot reproduce: ${lost.join(", ")}. Fix: ${REMEDY_BY_REASON[route.reason]}`
|
||||
: "";
|
||||
console.info(
|
||||
`[hyperframes] ${DIAGNOSTIC_BYPASS_CODE}: "${route.asset}" (${route.reason}): ` +
|
||||
|
||||
@@ -58,7 +58,7 @@ uniform sampler2D u_src;
|
||||
uniform sampler2D u_src2;
|
||||
uniform float u_mode;
|
||||
|
||||
// The matte texel is premultiplied, so its rgb is already "over black" — the
|
||||
// The matte texel is premultiplied, so its rgb is already "over black": the
|
||||
// composite After Effects reads a luma matte through.
|
||||
float hfCoverage(vec4 matte, float mode) {
|
||||
if (mode < 1.5) return matte.a;
|
||||
|
||||
@@ -100,7 +100,7 @@ export const NOISE_FRAG = `#version 300 es
|
||||
precision highp float;
|
||||
// NOT decoration: a fragment shader's default integer precision is mediump,
|
||||
// and a mediump uint is only guaranteed 16 bits (ESSL 3.00 §4.5.2). Every step
|
||||
// below — the Jenkins mix, both LCG rounds — is defined by 32-bit wraparound,
|
||||
// below (the Jenkins mix, both LCG rounds) is defined by 32-bit wraparound,
|
||||
// so without this the hash is a different function on any implementation that
|
||||
// honours the minimum. ANGLE and SwiftShader both use 32-bit ints, which is
|
||||
// exactly why no test on this machine would catch it.
|
||||
|
||||
@@ -1071,7 +1071,7 @@ export function resolveConfig(overrides?: Partial<EngineConfig>): EngineConfig {
|
||||
merged.enableStreamingEncode = false;
|
||||
merged.streamingEncodeAutoDisabledOnWin32Compound = true;
|
||||
console.error(
|
||||
"[hyperframes] Windows compound-workaround auto-detected — disabling streaming-encode " +
|
||||
"[hyperframes] Windows compound-workaround auto-detected, disabling streaming-encode " +
|
||||
"(platform=win32, software-GPU forced, workers=1). Field signal ts=1784131903. " +
|
||||
"Override: PRODUCER_ENABLE_STREAMING_ENCODE=true.",
|
||||
);
|
||||
|
||||
@@ -636,7 +636,7 @@ function buildUnverifiedHardwareGpuWarning(
|
||||
if (cause === "probe-error") {
|
||||
return (
|
||||
"[hyperframes] browserGpuMode=hardware was requested, but the GPU probe could not run, " +
|
||||
"so hardware acceleration is UNVERIFIED — if Chrome falls back to software WebGL the " +
|
||||
"so hardware acceleration is UNVERIFIED. If Chrome falls back to software WebGL the " +
|
||||
"capture will run at CPU speed. Honouring the explicit request anyway.\n" +
|
||||
" This is a probe failure, not evidence of a missing GPU: see the " +
|
||||
"`browserGpuMode probe → software (probe failed ...)` line above for the underlying " +
|
||||
@@ -655,7 +655,7 @@ function buildUnverifiedHardwareGpuWarning(
|
||||
"installed.";
|
||||
return (
|
||||
"[hyperframes] browserGpuMode=hardware was requested, but the WebGL probe found no " +
|
||||
"hardware GPU — Chrome will silently fall back to software WebGL and the capture will " +
|
||||
"hardware GPU, so Chrome will silently fall back to software WebGL and the capture will " +
|
||||
"run at CPU speed. Honouring the explicit request anyway.\n" +
|
||||
` ${remediation}\n` +
|
||||
" Pass --no-browser-gpu to select deterministic SwiftShader instead of waiting on a " +
|
||||
|
||||
@@ -682,7 +682,7 @@ export async function initDrawElementWorkerEncode(page: Page): Promise<void> {
|
||||
// frame's id so the node-side promise rejects instead of hanging.
|
||||
const workerSrc = `
|
||||
// Reuse one OffscreenCanvas across frames (dimensions are constant for a
|
||||
// render) — a fresh canvas per frame churns ~w*h*4 bytes of backing store
|
||||
// render): a fresh canvas per frame churns ~w*h*4 bytes of backing store
|
||||
// every frame and pressures GC on the encode hot path.
|
||||
let oc = null, c = null;
|
||||
self.onmessage = async (e) => {
|
||||
|
||||
@@ -942,7 +942,7 @@ async function initDrawElementOrTransparentBackground(
|
||||
session.deGateReason = "render_mode_hint";
|
||||
session.deFallbackTrigger = "render_mode_hint";
|
||||
console.log(
|
||||
"[engine] fast capture: falling back to screenshot — render-mode compatibility " +
|
||||
"[engine] fast capture falls back to screenshot: render-mode compatibility " +
|
||||
"hint forced screenshot capture (e.g. raw requestAnimationFrame composition).",
|
||||
);
|
||||
}
|
||||
@@ -994,7 +994,7 @@ async function initDrawElementOrTransparentBackground(
|
||||
session.deGateReason = "unsupported_chrome";
|
||||
session.deFallbackTrigger = "unsupported_chrome";
|
||||
console.log(
|
||||
`[engine] fast capture: falling back to ${session.launchCaptureMode} capture — ` +
|
||||
`[engine] fast capture falls back to ${session.launchCaptureMode} capture: ` +
|
||||
"this Chrome build does not implement canvas.drawElementImage (Dev/Canary-only " +
|
||||
"feature); run `hyperframes browser ensure --force` to fetch a supported " +
|
||||
"build, or set HYPERFRAMES_BROWSER_PATH to one.",
|
||||
@@ -1015,7 +1015,7 @@ async function initDrawElementOrTransparentBackground(
|
||||
session.deGateReason = "ffmpeg_no_psnr_filter";
|
||||
session.deFallbackTrigger = "ffmpeg_no_psnr_filter";
|
||||
console.warn(
|
||||
`[engine] fast capture: falling back to ${session.launchCaptureMode} capture — ` +
|
||||
`[engine] fast capture falls back to ${session.launchCaptureMode} capture: ` +
|
||||
"host ffmpeg is missing or was built without the `psnr` filter " +
|
||||
"(libpostproc), so drawElement self-verification cannot run. Install " +
|
||||
"an ffmpeg build that includes libpostproc (or set HYPERFRAMES_FFMPEG_PATH " +
|
||||
@@ -1046,8 +1046,8 @@ async function initDrawElementOrTransparentBackground(
|
||||
// Page.captureScreenshot hangs for the full protocol timeout, while
|
||||
// beginFrameCapture is the platform's normal baseline path.
|
||||
console.log(
|
||||
`[engine] fast capture: falling back to ${session.launchCaptureMode} capture — ` +
|
||||
"SwiftShader (software rasterizer — no GPU egress to skip, drawElement is " +
|
||||
`[engine] fast capture falls back to ${session.launchCaptureMode} capture: ` +
|
||||
"SwiftShader (software rasterizer, no GPU egress to skip, drawElement is " +
|
||||
"parity-or-slower; see fast-capture-limitations.md)",
|
||||
);
|
||||
await routeToFallback();
|
||||
@@ -1066,7 +1066,7 @@ async function initDrawElementOrTransparentBackground(
|
||||
// low-cardinality prefix, `deFallbackTrigger` the full value.
|
||||
session.deFallbackTrigger = cssFx;
|
||||
console.log(
|
||||
`[engine] fast capture: falling back to ${session.launchCaptureMode} capture — ` +
|
||||
`[engine] fast capture falls back to ${session.launchCaptureMode} capture: ` +
|
||||
`${cssFx} detected (drawElementImage cannot reproduce it; see fast-capture-limitations.md)`,
|
||||
);
|
||||
await routeToFallback();
|
||||
@@ -1101,7 +1101,7 @@ async function initDrawElementOrTransparentBackground(
|
||||
session.deGateReason = "at_risk_timeline";
|
||||
session.deFallbackTrigger = "at_risk_timeline";
|
||||
console.log(
|
||||
`[engine] fast capture: falling back to ${session.launchCaptureMode} capture — ` +
|
||||
`[engine] fast capture falls back to ${session.launchCaptureMode} capture: ` +
|
||||
`${atRisk.size}/${totalFrames} frames animate a compositor-incompatible prop ` +
|
||||
`(blend/3D/clip/mask); drawElementImage drops these mid-animation ` +
|
||||
`(deterministic timeline gate; see fast-capture-limitations.md Lim 7)`,
|
||||
@@ -1119,7 +1119,7 @@ async function initDrawElementOrTransparentBackground(
|
||||
session.deGateReason = "3d_init_failed";
|
||||
session.deFallbackTrigger = "3d_init_failed";
|
||||
console.log(
|
||||
`[engine] fast capture: falling back to ${session.launchCaptureMode} capture — ` +
|
||||
`[engine] fast capture falls back to ${session.launchCaptureMode} capture: ` +
|
||||
`3D projection init failed (${threeD.reason ?? "unknown"})`,
|
||||
);
|
||||
await routeToFallback();
|
||||
@@ -1264,7 +1264,7 @@ export async function createCaptureSession(
|
||||
(config?.enablePageSideCompositing ?? DEFAULT_CONFIG.enablePageSideCompositing)
|
||||
) {
|
||||
console.warn(
|
||||
"[engine] useDrawElement is incompatible with page-side shader compositing — " +
|
||||
"[engine] useDrawElement is incompatible with page-side shader compositing, " +
|
||||
"ignoring enablePageSideCompositing for this render. Prefer resolveConfig, " +
|
||||
"which disables page-side compositing automatically for fast-capture renders.",
|
||||
);
|
||||
@@ -1453,7 +1453,7 @@ async function constructCaptureSession(
|
||||
// of silently ignoring the old escape hatch.
|
||||
if (process.env.HF_FAST_CAPTURE_AUTOALPHA !== undefined) {
|
||||
console.warn(
|
||||
"[engine] HF_FAST_CAPTURE_AUTOALPHA is retired and ignored — the requestPaint " +
|
||||
"[engine] HF_FAST_CAPTURE_AUTOALPHA is retired and ignored: the requestPaint " +
|
||||
"paint contract captures animated opacity natively (see drawElementService.ts).",
|
||||
);
|
||||
}
|
||||
@@ -1626,7 +1626,7 @@ export const HF_READY_DIAGNOSTIC_EXPR = `(async function() {
|
||||
await new Promise(function(r) { setTimeout(r, 0); });
|
||||
}
|
||||
// A thenable resolving via its own setTimeout(0) can still misreport as
|
||||
// pending here (macrotask registration order, not resolution order) — a
|
||||
// pending here (macrotask registration order, not resolution order), a
|
||||
// narrow case that self-corrects on the next ~1s diagnostic tick.
|
||||
var pendingBuildReadyKeys = keys.filter(function(key) { return !settled[key]; });
|
||||
var rejectedBuildReadyKeys = keys.filter(function(key) { return settled[key] === "rejected"; });
|
||||
@@ -1661,24 +1661,24 @@ export function buildZeroDurationDiagnostic(diag: HfDiagnostic): string {
|
||||
if (diag.pendingBuildReadyKeys.length > 0) {
|
||||
hints.push(
|
||||
`window.__hf.buildReady never resolved for: ${diag.pendingBuildReadyKeys.join(", ")}. ` +
|
||||
"The runtime holds render-ready until every registered buildReady promise settles — " +
|
||||
"find where the composition registers that key and confirm it actually resolves.",
|
||||
"The runtime holds render-ready until every registered buildReady promise settles. " +
|
||||
"Find where the composition registers that key and confirm it actually resolves.",
|
||||
);
|
||||
}
|
||||
if (diag.rejectedBuildReadyKeys.length > 0) {
|
||||
hints.push(
|
||||
`window.__hf.buildReady rejected for: ${diag.rejectedBuildReadyKeys.join(", ")}. ` +
|
||||
"That key's build promise failed rather than hanging — find why it rejects.",
|
||||
"That key's build promise failed rather than hanging. Find why it rejects.",
|
||||
);
|
||||
}
|
||||
if (!diag.hasPlayer) {
|
||||
hints.push("window.__player was never set — the HyperFrames runtime did not initialize.");
|
||||
hints.push("window.__player was never set: the HyperFrames runtime did not initialize.");
|
||||
}
|
||||
if (!diag.hasTimeline) {
|
||||
hints.push(
|
||||
"No GSAP timeline registered (window.__timelines is empty). " +
|
||||
"CSS/WAAPI/Lottie animations are usually auto-detected (the runtime infers " +
|
||||
"duration from the longest running animation) — this composition's duration " +
|
||||
"duration from the longest running animation), but this composition's duration " +
|
||||
"could not be inferred, which usually means an infinite/unbounded animation " +
|
||||
"(e.g. animation-iteration-count: infinite, repeat: -1 WAAPI, or a looping Lottie " +
|
||||
"clip) or a Three.js scene with no discoverable AnimationClip.",
|
||||
@@ -1690,7 +1690,7 @@ export function buildZeroDurationDiagnostic(diag: HfDiagnostic): string {
|
||||
);
|
||||
}
|
||||
if (diag.hasSeek && diag.duration === 0 && diag.renderReady) {
|
||||
hints.push("The runtime finished initializing but reported zero duration — this is permanent.");
|
||||
hints.push("The runtime finished initializing but reported zero duration. This is permanent.");
|
||||
}
|
||||
return (
|
||||
`[FrameCapture] Composition has zero duration.\n` +
|
||||
@@ -1866,7 +1866,7 @@ export async function pollSubCompositionTimelines(
|
||||
scriptFailureBail = true;
|
||||
console.warn(
|
||||
`[FrameCapture] Sub-composition timeline wait cut short after ${now - start}ms: ` +
|
||||
`script resource(s) failed to load (${failures.join(", ")}) — ` +
|
||||
`script resource(s) failed to load (${failures.join(", ")}), so ` +
|
||||
`the timeline registration they carry can never arrive. ` +
|
||||
`Fix the script reference; the render proceeds without those animations.`,
|
||||
);
|
||||
@@ -2133,7 +2133,7 @@ export function buildLiveMapWarning(libraries: readonly string[]): CaptureWarnin
|
||||
message:
|
||||
`Live map viewport(s) detected in the composition (${libraries.join(", ")}). ` +
|
||||
`Map tiles load over the network at render time, which the deterministic-render ` +
|
||||
`contract forbids — frames captured before tiles arrive ship a blank or partial map ` +
|
||||
`contract forbids: frames captured before tiles arrive ship a blank or partial map ` +
|
||||
`with no error. Bake the map to a video first (see the motion-graphics maps skill / ` +
|
||||
`bake-basemap.mjs) and use the baked file as the imagery layer.`,
|
||||
details: { sources: [...libraries] },
|
||||
@@ -2495,7 +2495,7 @@ export async function initializeSession(session: CaptureSession): Promise<void>
|
||||
});
|
||||
console.warn(
|
||||
`[FrameCapture] Some image elements did not load within ${pageReadyTimeout}ms: ${failedImages}. ` +
|
||||
`Continuing render — affected images may appear blank/missing in early frames.`,
|
||||
`Continuing render. Affected images may appear blank/missing in early frames.`,
|
||||
);
|
||||
}
|
||||
await decodeAllImages(page);
|
||||
@@ -2517,7 +2517,7 @@ export async function initializeSession(session: CaptureSession): Promise<void>
|
||||
}, skipVideoIds);
|
||||
console.warn(
|
||||
`[FrameCapture] Some video elements did not decode within ${pageReadyTimeout}ms: ${failedVideos}. ` +
|
||||
`Continuing render — affected videos will appear as blank/black frames.`,
|
||||
`Continuing render. Affected videos will appear as blank/black frames.`,
|
||||
);
|
||||
}
|
||||
recordCaptureWarnings(
|
||||
@@ -2651,7 +2651,7 @@ export async function initializeSession(session: CaptureSession): Promise<void>
|
||||
});
|
||||
console.warn(
|
||||
`[FrameCapture] Some image elements did not load within ${pageReadyTimeout}ms: ${failedImages}. ` +
|
||||
`Continuing render — affected images may appear blank/missing in early frames.`,
|
||||
`Continuing render. Affected images may appear blank/missing in early frames.`,
|
||||
);
|
||||
}
|
||||
await decodeAllImages(page);
|
||||
@@ -2673,7 +2673,7 @@ export async function initializeSession(session: CaptureSession): Promise<void>
|
||||
}, bfSkipVideoIds);
|
||||
console.warn(
|
||||
`[FrameCapture] Some video elements did not decode within ${pageReadyTimeout}ms: ${failedVideos}. ` +
|
||||
`Continuing render — affected videos will appear as blank/black frames.`,
|
||||
`Continuing render. Affected videos will appear as blank/black frames.`,
|
||||
);
|
||||
}
|
||||
recordCaptureWarnings(
|
||||
@@ -4384,7 +4384,7 @@ export async function captureFrameToBuffer(
|
||||
// deadline rejects from outside it, so that catch never runs.
|
||||
session.deFrameTimeouts = (session.deFrameTimeouts ?? 0) + 1;
|
||||
console.log(
|
||||
`[engine] fast capture: frame ${frameIndex} — capture exceeded ` +
|
||||
`[engine] fast capture frame ${frameIndex}: capture exceeded ` +
|
||||
`${DE_FRAME_TIMEOUT_MS}ms; renderer stalled after drawElementImage ` +
|
||||
`(PRINFRA-488). Failing the drawElement attempt so the whole render ` +
|
||||
`retries via screenshot.`,
|
||||
|
||||
@@ -229,7 +229,7 @@ export function synthesizeSilentWorkerExitError(
|
||||
`worker ${result.workerId} exited without terminal error string ` +
|
||||
`(framesCaptured=${result.framesCaptured}, expected=${expectedFrames}, ` +
|
||||
`range=[${result.startFrame}, ${result.endFrame})). ` +
|
||||
`Field signal ts=1784042064 — this class of failure has been reported; ` +
|
||||
`Field signal ts=1784042064: this class of failure has been reported; ` +
|
||||
`consider re-run with --workers=1 to isolate.`
|
||||
);
|
||||
}
|
||||
@@ -783,7 +783,7 @@ async function psnrForDiskSample(
|
||||
const detail = err instanceof Error ? err.message : String(err);
|
||||
throw new Error(
|
||||
`[Parallel] drawElement disk self-verify aborted (worker ${workerId}, frame ${idx}): ` +
|
||||
`ffmpeg or the \`psnr\` filter is unavailable — ${detail}. The preflight in ` +
|
||||
`ffmpeg or the \`psnr\` filter is unavailable (${detail}). The preflight in ` +
|
||||
"initDrawElementOrTransparentBackground normally catches this at bootstrap; if you " +
|
||||
"hit this after a successful preflight, ffmpeg was replaced mid-render or " +
|
||||
"HYPERFRAMES_FFMPEG_PATH now points at a different binary.",
|
||||
|
||||
@@ -131,7 +131,7 @@ async function sendBeginFrame(
|
||||
}
|
||||
if (isPending) {
|
||||
throw new Error(
|
||||
`[BeginFrame] Frame still pending after ${PENDING_FRAME_RETRIES} retries — CPU overloaded by parallel renders. ` +
|
||||
`[BeginFrame] Frame still pending after ${PENDING_FRAME_RETRIES} retries: CPU overloaded by parallel renders. ` +
|
||||
`Reduce concurrent renders or use --docker for isolation.`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -101,8 +101,8 @@ export function getCgroupMemoryLimitMb(): number | null {
|
||||
// write their machine-readable payload to stdout. A banner on stdout corrupts that
|
||||
// payload for any JSON consumer (it broke the Video Agent's hyperframes check parse).
|
||||
console.warn(
|
||||
`[SystemMemory] cgroup memory limit detected: ${_cachedCgroupLimitMb} MiB — ` +
|
||||
`it governs memory-adaptive render behaviour instead of host RAM.`,
|
||||
`[SystemMemory] cgroup memory limit detected: ${_cachedCgroupLimitMb} MiB. ` +
|
||||
`It governs memory-adaptive render behaviour instead of host RAM.`,
|
||||
);
|
||||
}
|
||||
return _cachedCgroupLimitMb;
|
||||
|
||||
@@ -544,7 +544,7 @@ export async function initThreeDProjectionInPage(): Promise<ThreeDProjectionResu
|
||||
groups: 0,
|
||||
quads: 0,
|
||||
reason:
|
||||
"degenerate 3D geometry (zero-size or inline-box quad) — " +
|
||||
"degenerate 3D geometry (zero-size or inline-box quad): " +
|
||||
"quad projection cannot reproduce Blink's lenient rendering",
|
||||
};
|
||||
}
|
||||
@@ -556,7 +556,7 @@ export async function initThreeDProjectionInPage(): Promise<ThreeDProjectionResu
|
||||
groups: 0,
|
||||
quads: 0,
|
||||
reason:
|
||||
"3D quad contains GSAP-animated descendants — static texture " +
|
||||
"3D quad contains GSAP-animated descendants, a static texture " +
|
||||
"would freeze them at init state",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ export function diffGpuParityFrames(
|
||||
): GpuParityDiffResult {
|
||||
if (a.width !== b.width || a.height !== b.height) {
|
||||
throw new Error(
|
||||
`diffGpuParityFrames: frame size mismatch — A is ${a.width}x${a.height}, ` +
|
||||
`diffGpuParityFrames: frame size mismatch: A is ${a.width}x${a.height}, ` +
|
||||
`B is ${b.width}x${b.height}`,
|
||||
);
|
||||
}
|
||||
@@ -169,7 +169,7 @@ export function diffGpuParityFrames(
|
||||
if (contentSum <= blackSum) {
|
||||
throw new Error(
|
||||
`diffGpuParityFrames: contentSumThreshold (${contentSum}) must be strictly greater ` +
|
||||
`than blackSumThreshold (${blackSum}) — overlapping thresholds would double-classify ` +
|
||||
`than blackSumThreshold (${blackSum}): overlapping thresholds would double-classify ` +
|
||||
`borderline pixels`,
|
||||
);
|
||||
}
|
||||
@@ -298,7 +298,7 @@ export function verifyGpuParity(
|
||||
reason:
|
||||
`hardware-GPU frame has ${diff.blackOnlyInA.pixels} pixel(s) ` +
|
||||
`(${(diff.blackOnlyInA.fraction * 100).toFixed(3)}%) that are solid-black ` +
|
||||
`while software-GPU frame has content there${region} — likely shape-dependent ` +
|
||||
`while software-GPU frame has content there${region}, likely a shape-dependent ` +
|
||||
`hardware-GPU capture bug`,
|
||||
diff,
|
||||
};
|
||||
@@ -311,7 +311,7 @@ export function verifyGpuParity(
|
||||
reason:
|
||||
`software-GPU frame has ${diff.blackOnlyInB.pixels} pixel(s) ` +
|
||||
`(${(diff.blackOnlyInB.fraction * 100).toFixed(3)}%) that are solid-black ` +
|
||||
`while hardware-GPU frame has content there${region} — unexpected inverse ` +
|
||||
`while hardware-GPU frame has content there${region}, an unexpected inverse ` +
|
||||
`pattern, worth investigating`,
|
||||
diff,
|
||||
};
|
||||
|
||||
@@ -214,7 +214,7 @@ describe("HDR auto-promotion reporting", () => {
|
||||
output: { colorSpace: "BT.2020", codec: "HEVC Main10" },
|
||||
});
|
||||
expect(formatHdrAutoPromotionWarning(promotion!)).toBe(
|
||||
'[Render] HDR auto-promotion triggered by "assets/hero-hdr.mp4" — output: BT.2020 / HEVC Main10',
|
||||
'[Render] HDR auto-promotion triggered by "assets/hero-hdr.mp4", output: BT.2020 / HEVC Main10',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ export function findHdrAutoPromotion(
|
||||
}
|
||||
|
||||
export function formatHdrAutoPromotionWarning(promotion: HdrAutoPromotion): string {
|
||||
return `[Render] HDR auto-promotion triggered by "${promotion.triggeringAsset}" — output: ${promotion.output.colorSpace} / ${promotion.output.codec}`;
|
||||
return `[Render] HDR auto-promotion triggered by "${promotion.triggeringAsset}", output: ${promotion.output.colorSpace} / ${promotion.output.codec}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -461,7 +461,7 @@ function lintMissingLocalAsset(
|
||||
"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 — verify against capture/contact-sheets/ and capture/extracted/asset-descriptions.md.",
|
||||
"Captured asset filenames are unreliable. Verify against capture/contact-sheets/ and capture/extracted/asset-descriptions.md.",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -689,8 +689,8 @@ function lintMissingOrEmptySubComposition(
|
||||
message: `data-composition-src references "${srcPath}", but ${problem}.`,
|
||||
fixHint: folder
|
||||
? `Point data-composition-src at the HTML file inside the folder, such as "${srcPath}/index.html".`
|
||||
: `Fix this before rendering — the render pre-flight rejects unusable sub-compositions. ` +
|
||||
`Write valid HTML into "${srcPath}" — it needs a <template> or <body> containing an element with ` +
|
||||
: `Fix this before rendering: the render pre-flight rejects unusable sub-compositions. ` +
|
||||
`Write valid HTML into "${srcPath}". It needs a <template> or <body> containing an element with ` +
|
||||
`data-composition-id, data-width, and data-height. Preview/studio still tolerates and skips the ` +
|
||||
"scene while you author it. If a scene-authoring step is still running, wait for it to finish " +
|
||||
"before referencing the file, or re-run the step that generates it.",
|
||||
|
||||
@@ -949,7 +949,7 @@ export const compositionRules: Array<(ctx: LintContext) => HyperframeLintFinding
|
||||
message:
|
||||
"Composition starts with a bare element instead of a proper HTML document. " +
|
||||
"An index.html that contains data-composition-id but no <!DOCTYPE html>, <html>, or <body> " +
|
||||
"is a fragment — browsers quirks-mode it, the preview server cannot load it, and " +
|
||||
"is a fragment: browsers quirks-mode it, the preview server cannot load it, and " +
|
||||
"the bundler will fail to inject runtime scripts.",
|
||||
fixHint:
|
||||
'Wrap the composition in <!DOCTYPE html><html><head><meta charset="UTF-8"></head><body>...</body></html>.',
|
||||
@@ -1126,7 +1126,7 @@ export const compositionRules: Array<(ctx: LintContext) => HyperframeLintFinding
|
||||
findings.push({
|
||||
code: "unknown_variable_binding",
|
||||
severity: "warning",
|
||||
message: `<${tag.name}> binds ${attr}="${id}" but no variable "${id}" is declared in data-composition-variables — the binding will silently keep the authored fallback.`,
|
||||
message: `<${tag.name}> binds ${attr}="${id}" but no variable "${id}" is declared in data-composition-variables, so the binding will silently keep the authored fallback.`,
|
||||
fixHint: `Declare the variable on the composition root (<html>, or the [data-composition-id] root element for a template/fragment comp): data-composition-variables='[{"id":"${id}","type":"${attr === "data-var-src" ? "image" : "string"}","label":"${id}","default":"..."}]', or fix the binding id.`,
|
||||
elementId: readAttr(tag.raw, "id") || undefined,
|
||||
snippet: truncateSnippet(tag.raw),
|
||||
@@ -1172,8 +1172,8 @@ export const compositionRules: Array<(ctx: LintContext) => HyperframeLintFinding
|
||||
{
|
||||
code: "html_dir_attribute_breaks_render",
|
||||
severity: "error",
|
||||
message: `<html dir="${dir}"> renders correctly in preview/snapshot but produces a fully blank/black video from render — a confirmed, silent failure.`,
|
||||
fixHint: `Remove dir="${dir}" from <html>. Keep lang, and scope ${scopedDirection} to individual text-containing elements instead — text still shapes correctly via the browser's own bidi algorithm.`,
|
||||
message: `<html dir="${dir}"> renders correctly in preview/snapshot but produces a fully blank/black video from render. This is a confirmed, silent failure.`,
|
||||
fixHint: `Remove dir="${dir}" from <html>. Keep lang, and scope ${scopedDirection} to individual text-containing elements instead. Text still shapes correctly via the browser's own bidi algorithm.`,
|
||||
snippet: truncateSnippet(htmlTag.raw),
|
||||
},
|
||||
];
|
||||
@@ -1232,7 +1232,7 @@ export const compositionRules: Array<(ctx: LintContext) => HyperframeLintFinding
|
||||
findings.push({
|
||||
code: "subcomposition_blanks_before_host",
|
||||
severity: "warning",
|
||||
message: `<${t.tag.name}${elementId ? ` id="${elementId}"` : ""}> sub-composition ends at ${round3(t.end)}s but the composition runs to ${round3(rootDuration)}s — its slot will be blank for ~${gap}s.`,
|
||||
message: `<${t.tag.name}${elementId ? ` id="${elementId}"` : ""}> sub-composition ends at ${round3(t.end)}s but the composition runs to ${round3(rootDuration)}s, so its slot will be blank for ~${gap}s.`,
|
||||
elementId,
|
||||
fixHint: `data-duration is the slot's visible window. Set this sub-composition's data-duration to ${round3(rootDuration - t.start)} to fill the host window, or add another clip to cover the remaining ~${gap}s.`,
|
||||
snippet: truncateSnippet(t.tag.raw),
|
||||
@@ -1278,7 +1278,7 @@ export const compositionRules: Array<(ctx: LintContext) => HyperframeLintFinding
|
||||
`At render, every sub-composition rule is scoped to [data-composition-id="${rootCompositionId}"] <selector>, so a selector whose leftmost part is the ROOT's own class becomes a descendant selector that cannot match the scoped element itself. ` +
|
||||
`Since #1886 the producer preserves the authored root as an inner wrapper, so this no longer renders the scene unstyled, but #root is the shape the scoper special-cases and the registry blocks model. Use it so preview, render, and Studio agree.`,
|
||||
selector: example,
|
||||
fixHint: `Give the root id="root" and style it with \`#root { ... }\` plus plain descendant selectors (\`.kicker\`, \`#hero\`) — the runtime already scopes each sub-composition by data-composition-id, so a class namespace on the root is redundant.`,
|
||||
fixHint: `Give the root id="root" and style it with \`#root { ... }\` plus plain descendant selectors (\`.kicker\`, \`#hero\`). The runtime already scopes each sub-composition by data-composition-id, so a class namespace on the root is redundant.`,
|
||||
snippet: truncateSnippet(rootTag.raw),
|
||||
},
|
||||
];
|
||||
@@ -1404,7 +1404,7 @@ export const compositionRules: Array<(ctx: LintContext) => HyperframeLintFinding
|
||||
severity: "error",
|
||||
message:
|
||||
"Root composition uses Three.js with no data-duration. The runtime cannot discover a " +
|
||||
"Three.js scene's duration automatically (no AnimationClip/AnimationMixer inspection) — " +
|
||||
"Three.js scene's duration automatically (no AnimationClip/AnimationMixer inspection), so " +
|
||||
'render will fail with "Composition has zero duration".',
|
||||
fixHint: 'Add data-duration="<seconds>" to the root element.',
|
||||
snippet: truncateSnippet(rootTag.raw),
|
||||
@@ -1514,7 +1514,7 @@ export const compositionRules: Array<(ctx: LintContext) => HyperframeLintFinding
|
||||
message:
|
||||
`This composition has ${heavyCount} elements carrying "heavy overlay" CSS ` +
|
||||
`(filter:blur, radial-gradient, or clip-path). Field signal: a composition with ` +
|
||||
`~40 such elements — including opacity:0 / visibility:hidden ones — captures ` +
|
||||
`~40 such elements (including opacity:0 / visibility:hidden ones) captures ` +
|
||||
`solid-black for the first ~half of the render, recovering near the end. Reproduces ` +
|
||||
`identically via drawElement, forced screenshot capture, and snapshot, so the capture ` +
|
||||
`layer itself is the offender (not encoder/mux). Independent of duration. Presence ` +
|
||||
@@ -1522,7 +1522,7 @@ export const compositionRules: Array<(ctx: LintContext) => HyperframeLintFinding
|
||||
fixHint:
|
||||
`${splitTarget} and concat the pieces (FFmpeg or the runtime's slideshow) so each ` +
|
||||
`capture only sees a small subset of heavy overlays at once. Even hidden overlays ` +
|
||||
`(opacity:0 / visibility:hidden) contribute — either remove truly unused ones from ` +
|
||||
`(opacity:0 / visibility:hidden) contribute: either remove truly unused ones from ` +
|
||||
`the source or scope them into their own per-transition sub-composition. If an ` +
|
||||
`overlay is genuinely inert for the whole clip, use display:none so it never enters ` +
|
||||
`the render tree. Field ref ts=1784040753 (#hyperframes-cli-feedback).`,
|
||||
@@ -1577,10 +1577,10 @@ export const compositionRules: Array<(ctx: LintContext) => HyperframeLintFinding
|
||||
`\`zoom: ${hit.value}\` on ${hit.where} rescales the painted composition inside a frame that does not rescale with it. ` +
|
||||
`The capture frame is sized from the root's data-width/data-height in LAYOUT pixels, and \`zoom\` changes only what is painted inside it, so ` +
|
||||
(enlarging
|
||||
? `content past the frame's edge renders zero pixels — it is inside the declared composition and absent from the output, with nothing else reporting it.`
|
||||
? `content past the frame's edge renders zero pixels. It is inside the declared composition and absent from the output, with nothing else reporting it.`
|
||||
: `the composition paints into part of the frame and the remainder of every output frame is dead space.`),
|
||||
fixHint:
|
||||
`Author the composition at its real size — set data-width/data-height (and the root's width/height) to the dimensions you want — and drop the canvas-level \`zoom\`. ` +
|
||||
`Author the composition at its real size: set data-width/data-height (and the root's width/height) to the dimensions you want, and drop the canvas-level \`zoom\`. ` +
|
||||
`To scale the OUTPUT without changing layout, pass \`--output-resolution\` to render, which supersamples. ` +
|
||||
`\`zoom\` on descendants is fine and is not flagged: it is honoured exactly as specified.`,
|
||||
snippet: hit.snippet,
|
||||
@@ -1629,12 +1629,12 @@ export const compositionRules: Array<(ctx: LintContext) => HyperframeLintFinding
|
||||
severity: "warning",
|
||||
message:
|
||||
`${over.join(" and ")} exceeds the ${INSPECTION_VIEWPORT_CAP}px viewport cap that check, validate, snapshot, compare and layout clamp to. ` +
|
||||
`render is unaffected and produces the full size, so the video is correct — but every inspection command captures a ${INSPECTION_VIEWPORT_CAP}px-wide frame, ` +
|
||||
`render is unaffected and produces the full size, so the video is correct, but every inspection command captures a ${INSPECTION_VIEWPORT_CAP}px-wide frame, ` +
|
||||
`and anything beyond that is missing from what they report. Measured at 0.8.71: a 5000x400 composition renders 5000x400 with all content, ` +
|
||||
`while snapshot returns 4096x400 with the element at left:4500 absent.`,
|
||||
fixHint:
|
||||
`Keep the authored size if the output needs it, and verify the region past ${INSPECTION_VIEWPORT_CAP}px from a render rather than from check/snapshot/compare — ` +
|
||||
`a clean inspection result does not cover it. If the large canvas is only there to gain resolution, author at the layout size and pass ` +
|
||||
`Keep the authored size if the output needs it, and verify the region past ${INSPECTION_VIEWPORT_CAP}px from a render rather than from check/snapshot/compare. ` +
|
||||
`A clean inspection result does not cover it. If the large canvas is only there to gain resolution, author at the layout size and pass ` +
|
||||
`\`--output-resolution\` to render instead, which supersamples without changing layout.`,
|
||||
snippet: truncateSnippet(rootTag.raw),
|
||||
},
|
||||
|
||||
@@ -448,7 +448,7 @@ function describeRootDimensionsDrift(
|
||||
const declared = `Root composition declares data-width="${dataWidth}" data-height="${dataHeight}"`;
|
||||
if (!bodyCssMismatch) {
|
||||
return {
|
||||
message: `${declared}, but ${viewportMismatch}. The viewport meta has no effect on capture — the renderer sizes the viewport from the root's own data-width/data-height — so this is stale metadata, not a clipping risk.`,
|
||||
message: `${declared}, but ${viewportMismatch}. The viewport meta has no effect on capture (the renderer sizes the viewport from the root's own data-width/data-height), so this is stale metadata, not a clipping risk.`,
|
||||
fixHint:
|
||||
"update the meta viewport to match, or scaffold with `hyperframes init --resolution portrait`",
|
||||
};
|
||||
@@ -776,7 +776,7 @@ export const coreRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [
|
||||
findings.push({
|
||||
code: "unclosed_tag_swallowed_element",
|
||||
severity: "error",
|
||||
message: `<${tag.name}> is missing its closing \`>\` before the next \`<\` — the following element is swallowed as bogus attribute text and never becomes a real node.`,
|
||||
message: `<${tag.name}> is missing its closing \`>\` before the next \`<\`, so the following element is swallowed as bogus attribute text and never becomes a real node.`,
|
||||
fixHint: "Close the previous tag's `>` before opening the next element.",
|
||||
snippet: truncateSnippet(tag.raw),
|
||||
});
|
||||
|
||||
@@ -182,7 +182,7 @@ export const fontRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [
|
||||
severity: "warning",
|
||||
message:
|
||||
`Font ${aliased.length === 1 ? "family" : "families"} will be substituted at render time: ${aliased.join(", ")}. ` +
|
||||
"In distributed/Lambda rendering system-font capture is disabled — these fonts will fall " +
|
||||
"In distributed/Lambda rendering system-font capture is disabled, so these fonts will fall " +
|
||||
"back to OS defaults. Embed explicit @font-face declarations instead.",
|
||||
},
|
||||
];
|
||||
@@ -216,7 +216,7 @@ export const fontRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [
|
||||
"Add @font-face { font-family: '...'; src: url('capture/assets/fonts/...woff2'); } " +
|
||||
"for each font family, pointing to the captured .woff2 files. For an OS-bundled " +
|
||||
"system font (e.g. Hiragino Sans, Microsoft YaHei) that has no downloadable file, " +
|
||||
"use src: local('Exact Font Name') instead — the declaration alone satisfies this " +
|
||||
"use src: local('Exact Font Name') instead. The declaration alone satisfies this " +
|
||||
"check without needing a font file.",
|
||||
});
|
||||
return findings;
|
||||
|
||||
@@ -1250,7 +1250,7 @@ export const gsapRules: LintRule<LintContext>[] = [
|
||||
findings.push({
|
||||
code: "gsap_animates_clip_element",
|
||||
severity: "error",
|
||||
message: `GSAP animation sets ${conflictingProps.join(", ")} on a clip element. Selector "${sel}" resolves to element ${elDesc}. The framework manages clip visibility, and autoAlpha writes visibility as well as opacity — do not animate these properties on clip elements.`,
|
||||
message: `GSAP animation sets ${conflictingProps.join(", ")} on a clip element. Selector "${sel}" resolves to element ${elDesc}. The framework manages clip visibility, and autoAlpha writes visibility as well as opacity. Do not animate these properties on clip elements.`,
|
||||
selector: sel,
|
||||
elementId: clipInfo.id || undefined,
|
||||
fixHint:
|
||||
@@ -1527,7 +1527,7 @@ export const gsapRules: LintRule<LintContext>[] = [
|
||||
severity: "warning",
|
||||
message:
|
||||
"Audio-reactive captions use a single tween per group based on peak values. " +
|
||||
"This sets one static value at group.start — not perceptible as audio reactivity.",
|
||||
"This sets one static value at group.start, which is not perceptible as audio reactivity.",
|
||||
fixHint:
|
||||
"Sample audio data at 100-200ms intervals throughout each group's lifetime " +
|
||||
"(for loop from group.start to group.end) and create a tween at each sample " +
|
||||
@@ -1647,7 +1647,7 @@ export const gsapRules: LintRule<LintContext>[] = [
|
||||
severity: "error",
|
||||
message:
|
||||
"GSAP timeline is created but never registered in window.__timelines. " +
|
||||
"The runtime discovers timelines from this registry — without registration, " +
|
||||
"The runtime discovers timelines from this registry. Without registration, " +
|
||||
"animations will not play during preview or render.",
|
||||
fixHint:
|
||||
"Add `window.__timelines = window.__timelines || {};` and " +
|
||||
@@ -1746,13 +1746,13 @@ export const gsapRules: LintRule<LintContext>[] = [
|
||||
severity: "error",
|
||||
message:
|
||||
`"${sel}" has CSS \`opacity: 0\` and a gsap.${win.method}() that also sets opacity to 0. ` +
|
||||
`gsap.from() animates FROM the specified value TO the current CSS value — ` +
|
||||
`since CSS is already 0, the element animates from 0→0 and never becomes visible.`,
|
||||
`gsap.from() animates FROM the specified value TO the current CSS value. ` +
|
||||
`Since CSS is already 0, the element animates from 0→0 and never becomes visible.`,
|
||||
selector: sel,
|
||||
fixHint:
|
||||
`Remove \`opacity: 0\` from the CSS/inline style on "${sel}". ` +
|
||||
`Let gsap.from({opacity: 0}) handle the initial hidden state — ` +
|
||||
`it will animate FROM 0 TO the CSS value (1 by default).`,
|
||||
`Let gsap.from({opacity: 0}) handle the initial hidden state. ` +
|
||||
`It will animate FROM 0 TO the CSS value (1 by default).`,
|
||||
snippet: truncateSnippet(win.raw),
|
||||
});
|
||||
}
|
||||
@@ -1879,13 +1879,13 @@ export const gsapRules: LintRule<LintContext>[] = [
|
||||
`GSAP tween on "${call.selector}" uses motion that snaps to integer device pixels: ` +
|
||||
`${flagged.join(", ")}. Layout and text-reflow properties snap during browser layout; ` +
|
||||
"roundProps rounds the tween value. Slow motion or an ease-out tail then stutters under " +
|
||||
"the seek-by-frame capture engine — animate transforms (x/y/scale/opacity) instead.";
|
||||
"the seek-by-frame capture engine. Animate transforms (x/y/scale/opacity) instead.";
|
||||
|
||||
const fixes: string[] = [];
|
||||
if (layoutProps.length > 0) {
|
||||
const tokens = [...new Set(layoutProps.flatMap((p) => LAYOUT_FIX[p] ?? []))];
|
||||
fixes.push(
|
||||
`replace ${layoutProps.join("/")} with the transform equivalent (${tokens.join(", ")}) — ` +
|
||||
`replace ${layoutProps.join("/")} with the transform equivalent (${tokens.join(", ")}), ` +
|
||||
`e.g. tl.fromTo("${call.selector}", { x: -1300 }, { x: 0, ...yourAnimation })`,
|
||||
);
|
||||
}
|
||||
@@ -1903,7 +1903,7 @@ export const gsapRules: LintRule<LintContext>[] = [
|
||||
if (spacing.length > 0) {
|
||||
parts.push(
|
||||
`for ${spacing.join("/")}, split the text into per-character elements and animate ` +
|
||||
"each glyph's x (the spread) — uniform scale is NOT equivalent — or hold the final value statically",
|
||||
"each glyph's x (the spread), since uniform scale is NOT equivalent, or hold the final value statically",
|
||||
);
|
||||
}
|
||||
fixes.push(
|
||||
@@ -1980,7 +1980,7 @@ export const gsapRules: LintRule<LintContext>[] = [
|
||||
`Relative value(s) ${values} on "${win.targetSelector}" start while another writer for the same ` +
|
||||
`propert${sharedProps.length > 1 ? "ies" : "y"} is active between ${formatTime(win.position)} and ${formatTime(overlapEnd)}. ` +
|
||||
"Relative tweens capture their base at tween init: the sequential path inits mid-flight of the other " +
|
||||
"writer, a cold render worker landing later inits with its end state — the same frame renders at two " +
|
||||
"writer, a cold render worker landing later inits with its end state, so the same frame renders at two " +
|
||||
"different positions (snap at chunk boundaries).",
|
||||
selector: win.targetSelector,
|
||||
fixHint:
|
||||
@@ -2063,7 +2063,7 @@ export const gsapRules: LintRule<LintContext>[] = [
|
||||
const reason = readsSensitive
|
||||
? "reads transform-sensitive geometry, so its result depends on the worker's own seek order"
|
||||
: badMember
|
||||
? `accesses .${badMember} on its first parameter — GSAP function values receive (index, target, targets), ` +
|
||||
? `accesses .${badMember} on its first parameter. GSAP function values receive (index, target, targets), ` +
|
||||
"so the first parameter is a NUMBER and this throws at tween init"
|
||||
: "measures layout at tween init, which is deterministic across cold render workers only while the measured layout never animates";
|
||||
findings.push({
|
||||
@@ -2072,7 +2072,7 @@ export const gsapRules: LintRule<LintContext>[] = [
|
||||
message: `Function-valued tween var for ${prop} on "${anim.targetSelector}" ${reason}. Each render worker initializes tweens independently.`,
|
||||
selector: anim.targetSelector,
|
||||
fixHint: badMember
|
||||
? "Use the SECOND parameter for the element: (index, target) => ... — or index arithmetic like (i) => i * 20."
|
||||
? "Use the SECOND parameter for the element: (index, target) => ..., or index arithmetic like (i) => i * 20."
|
||||
: "Compute the value once at build time (before the timeline is registered) and pass a constant, or derive it from fixed composition coordinates.",
|
||||
snippet: truncateSnippet(raw),
|
||||
});
|
||||
@@ -2120,12 +2120,12 @@ export const gsapRules: LintRule<LintContext>[] = [
|
||||
severity: "warning",
|
||||
message:
|
||||
"Timeline callback reaches DOM measurement (getBoundingClientRect/getTotalLength/getComputedStyle/...). " +
|
||||
"The renderer seeks with suppressEvents=false, so callbacks re-fire on every seek — and a cold render " +
|
||||
"The renderer seeks with suppressEvents=false, so callbacks re-fire on every seek, and a cold render " +
|
||||
"worker runs them against whatever DOM state its own non-linear seek order produced. Measured geometry is " +
|
||||
"seek-order-dependent, and values measured at build time (before the callback ran) are stale or zero.",
|
||||
selector: truncateSnippet(site, 120),
|
||||
fixHint:
|
||||
"Do all measurement and DOM setup synchronously at build time, before registering the timeline — " +
|
||||
"Do all measurement and DOM setup synchronously at build time, before registering the timeline, " +
|
||||
"or derive geometry from fixed composition coordinates instead of measuring.",
|
||||
snippet: truncateSnippet(snippet),
|
||||
});
|
||||
@@ -2211,12 +2211,12 @@ export const gsapRules: LintRule<LintContext>[] = [
|
||||
message:
|
||||
`\`${match[0].includes(".push") ? "push" : boundName}\` captures the return of \`.${method}()\` on timeline \`${timelineVars[0]}\`, ` +
|
||||
"but a timeline's `.to()`/`.from()`/`.set()` returns THE TIMELINE ITSELF, not the tween it created. " +
|
||||
"Every captured value is the same master timeline, so a tween-scoped call on it — `.kill()` above all — " +
|
||||
"Every captured value is the same master timeline, so a tween-scoped call on it (`.kill()` above all) " +
|
||||
"hits the whole composition: it interrupts the timeline and detaches it from its parent, which freezes a " +
|
||||
"parent-driven seek while an explicit `tl.progress()` keeps working. Only `gsap.to()` returns a tween.",
|
||||
fixHint:
|
||||
"To build a group of keyframes you can later discard, put them in a nested timeline: " +
|
||||
"`const nested = gsap.timeline(); nested.to(...); tl.add(nested, 0);` — then `nested.kill()` " +
|
||||
"`const nested = gsap.timeline(); nested.to(...); tl.add(nested, 0);`. Then `nested.kill()` " +
|
||||
"replaces exactly those keyframes and cannot reach `tl`. Children keep their absolute times when the " +
|
||||
"nest is added at 0. To hold a single real tween, create it with `gsap.to(...)` and place it with `tl.add(tween, at)`.",
|
||||
snippet: truncateSnippet(source.slice(contextStart, match.index + match[0].length + 60)),
|
||||
@@ -2333,8 +2333,8 @@ export const gsapRules: LintRule<LintContext>[] = [
|
||||
severity: "error",
|
||||
message:
|
||||
`GSAP writes strokeDasharray on "${targetLabel}", but its CSS ("${conflictToken}") declares a multi-component ` +
|
||||
'stroke-dasharray. GSAP merges dash lists per component, so the CSS gap survives (e.g. "641.4px, 10px") — ' +
|
||||
"the draw-on hide only hides one gap's worth and the line stays visible the whole scene.",
|
||||
'stroke-dasharray. GSAP merges dash lists per component, so the CSS gap survives (e.g. "641.4px, 10px"). ' +
|
||||
"The draw-on hide only hides one gap's worth and the line stays visible the whole scene.",
|
||||
selector: quotedSelector ?? undefined,
|
||||
fixHint:
|
||||
`Remove the CSS stroke-dasharray from "${conflictToken}" (decorative dashes belong on a separate element), ` +
|
||||
@@ -2494,11 +2494,11 @@ export const gsapRules: LintRule<LintContext>[] = [
|
||||
code: "svg_measure_before_path_d",
|
||||
severity: sameVarAssignmentExists ? "warning" : "error",
|
||||
message: sameVarAssignmentExists
|
||||
? `getTotalLength() is called on "${tokenLabel}", whose \`d\` is only assigned inside a function body — ` +
|
||||
"if the measure runs before that function (e.g. the function is a timeline callback), the length is 0 " +
|
||||
? `getTotalLength() is called on "${tokenLabel}", whose \`d\` is only assigned inside a function body. ` +
|
||||
"If the measure runs before that function (e.g. the function is a timeline callback), the length is 0 " +
|
||||
"and the dash animation is dead."
|
||||
: `getTotalLength() is called on "${tokenLabel}", but the path has no static \`d\` attribute and no d ` +
|
||||
"assignment exists anywhere — getTotalLength() returns 0 in Chrome, silently killing dash animations.",
|
||||
"assignment exists anywhere, so getTotalLength() returns 0 in Chrome, silently killing dash animations.",
|
||||
selector: tokenLabel,
|
||||
fixHint:
|
||||
"Assign the path's `d` synchronously at build time (top level, before measuring), or author a static " +
|
||||
|
||||
@@ -544,7 +544,7 @@ export const mediaRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> =
|
||||
message: `<video${elementId ? ` id="${elementId}"` : ""}> has data-start but declares neither muted nor data-has-audio. If the file has sound, add data-has-audio="true" (the sound stays on this clip). If it is silent, add muted.`,
|
||||
elementId,
|
||||
fixHint:
|
||||
'Add data-has-audio="true" when the file has sound (recommended — keeps picture and sound on one clip), or add `muted` for silent footage.',
|
||||
'Add data-has-audio="true" when the file has sound (recommended: it keeps picture and sound on one clip), or add `muted` for silent footage.',
|
||||
snippet: truncateSnippet(tag.raw),
|
||||
});
|
||||
}
|
||||
@@ -637,7 +637,7 @@ export const mediaRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> =
|
||||
severity: "error",
|
||||
message: `Self-closing <${tagName}/> is invalid HTML. The browser will leave the tag open, swallowing all subsequent elements as invisible fallback content. This makes compositions INVISIBLE.`,
|
||||
elementId,
|
||||
fixHint: `Change <${tagName} .../> to <${tagName} ...></${tagName}> — media elements MUST have explicit closing tags.`,
|
||||
fixHint: `Change <${tagName} .../> to <${tagName} ...></${tagName}>. Media elements MUST have explicit closing tags.`,
|
||||
snippet: truncateSnippet(scMatch[0]),
|
||||
});
|
||||
}
|
||||
@@ -685,7 +685,7 @@ export const mediaRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> =
|
||||
findings.push({
|
||||
code: "base64_media_prohibited",
|
||||
severity: "error",
|
||||
message: `Inline base64 audio/video detected (${(dataSize / 1024).toFixed(0)} KB)${isSuspicious ? " — likely fabricated data" : ""}. Base64 media is prohibited — it bloats file size and breaks rendering.`,
|
||||
message: `Inline base64 audio/video detected (${(dataSize / 1024).toFixed(0)} KB)${isSuspicious ? ", likely fabricated data" : ""}. Base64 media is prohibited: it bloats file size and breaks rendering.`,
|
||||
fixHint:
|
||||
"Use a relative path (assets/music.mp3) or HTTPS URL for the audio/video src. Never embed media as base64.",
|
||||
snippet: truncateSnippet((b64Match[1] ?? "").slice(0, 80) + "..."),
|
||||
@@ -716,7 +716,7 @@ export const mediaRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> =
|
||||
findings.push({
|
||||
code: "media_missing_id",
|
||||
severity: "error",
|
||||
message: `<${tag.name}> has data-start but no id attribute. The renderer requires id to discover media elements — this ${tag.name === "audio" ? "audio will be SILENT" : "video will be FROZEN"} in renders.`,
|
||||
message: `<${tag.name}> has data-start but no id attribute. The renderer requires id to discover media elements, so this ${tag.name === "audio" ? "audio will be SILENT" : "video will be FROZEN"} in renders.`,
|
||||
fixHint: `Add a unique id attribute: <${tag.name} id="my-${tag.name}" ...>`,
|
||||
snippet: truncateSnippet(tag.raw),
|
||||
});
|
||||
@@ -778,7 +778,7 @@ export const mediaRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> =
|
||||
findings.push({
|
||||
code: "media_crossorigin_breaks_preview",
|
||||
severity: "error",
|
||||
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 — hiding the bug.`,
|
||||
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, which hides the bug.`,
|
||||
elementId,
|
||||
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.",
|
||||
@@ -819,7 +819,7 @@ export const mediaRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> =
|
||||
findings.push({
|
||||
code: "video_audio_double_source",
|
||||
severity: "error",
|
||||
message: `<audio${audioInfo.id ? ` id="${audioInfo.id}"` : ""}> and <video${videoInfo.id ? ` id="${videoInfo.id}"` : ""}> both point to the same source at the same time. The unmuted video already provides audio — the duplicate <audio> will cause double playback and echo.`,
|
||||
message: `<audio${audioInfo.id ? ` id="${audioInfo.id}"` : ""}> and <video${videoInfo.id ? ` id="${videoInfo.id}"` : ""}> both point to the same source at the same time. The unmuted video already provides audio, so the duplicate <audio> will cause double playback and echo.`,
|
||||
elementId: audioInfo.id,
|
||||
fixHint:
|
||||
"Remove the <audio> element and let the video carry its own sound (recommended), or mute the video (add `muted`) and keep the separate <audio> when picture and sound must be cut independently.",
|
||||
@@ -886,7 +886,7 @@ function findVolumeTweenOverridesGainFindings(ctx: LintContext): HyperframeLintF
|
||||
findings.push({
|
||||
code: "audio_volume_tween_overrides_gain",
|
||||
severity: "warning",
|
||||
message: `#${id} has data-volume="${volume}" (${db}) and a GSAP tween on \`volume\`. Tween values are absolute — they REPLACE this gain rather than scale it — so wherever the tween names a value the clip plays at that value, not at ${db}.`,
|
||||
message: `#${id} has data-volume="${volume}" (${db}) and a GSAP tween on \`volume\`. Tween values are absolute: they REPLACE this gain rather than scale it, so wherever the tween names a value the clip plays at that value, not at ${db}.`,
|
||||
elementId: id,
|
||||
fixHint:
|
||||
"Write the tween's targets in the same absolute gain (e.g. `volume: 1.95`, not `volume: 1`), or reset data-volume to 1 and let the tween carry the level on its own.",
|
||||
@@ -931,7 +931,7 @@ function findVolumeDoubleAutomationFindings(ctx: LintContext): HyperframeLintFin
|
||||
findings.push({
|
||||
code: "audio_volume_double_automation",
|
||||
severity: "warning",
|
||||
message: `#${id} has both a volume automation lane and a GSAP tween on \`volume\`. The lane wins — the tween is ignored in preview and in the render.`,
|
||||
message: `#${id} has both a volume automation lane and a GSAP tween on \`volume\`. The lane wins, and the tween is ignored in preview and in the render.`,
|
||||
elementId: id,
|
||||
fixHint:
|
||||
"Keep one of them: delete the volume lane to go back to tweening, or drop the tween and shape the level in the automation lane.",
|
||||
@@ -997,7 +997,7 @@ function findCarveUngroupedSourcesFindings(ctx: LintContext): HyperframeLintFind
|
||||
message: `${elementId ? `#${elementId}'s` : "This"} carve names ${clipIds.length} voice clips directly (${clipIds.join(", ")}) instead of a group.`,
|
||||
elementId,
|
||||
fixHint:
|
||||
"Group the voice clips and carve against the group — a hand-rolled clip list silently rots when a clip is added.",
|
||||
"Group the voice clips and carve against the group. A hand-rolled clip list silently rots when a clip is added.",
|
||||
snippet: truncateSnippet(tag.raw),
|
||||
});
|
||||
}
|
||||
@@ -1103,7 +1103,7 @@ function findAudioGroupTimingAttrFindings(ctx: LintContext): HyperframeLintFindi
|
||||
findings.push({
|
||||
code: "audio_group_timing_attrs",
|
||||
severity: "warning",
|
||||
message: `${elementId ? `#${elementId}` : "This audio group"} carries ${present.map((attr) => `\`${attr}\``).join(", ")}, which a bus has no use for — its members carry the timing and its automation clock is composition time.`,
|
||||
message: `${elementId ? `#${elementId}` : "This audio group"} carries ${present.map((attr) => `\`${attr}\``).join(", ")}, which a bus has no use for: its members carry the timing and its automation clock is composition time.`,
|
||||
elementId,
|
||||
fixHint: `Remove ${present.map((attr) => `\`${attr}\``).join(", ")} from the group element.`,
|
||||
snippet: truncateSnippet(tag.raw),
|
||||
@@ -1134,7 +1134,7 @@ function findAudioGroupCarveAttrFindings(ctx: LintContext): HyperframeLintFindin
|
||||
findings.push({
|
||||
code: "audio_group_carve_attr",
|
||||
severity: "warning",
|
||||
message: `${elementId ? `#${elementId}` : "This audio group"} carries \`data-fx-carve\`, which belongs on the clip being carved — a bus has no audio of its own to level-match against, and a carve here stacks with any its members already have.`,
|
||||
message: `${elementId ? `#${elementId}` : "This audio group"} carries \`data-fx-carve\`, which belongs on the clip being carved. A bus has no audio of its own to level-match against, and a carve here stacks with any its members already have.`,
|
||||
elementId,
|
||||
fixHint:
|
||||
"Remove `data-fx-carve` and the `fromCarve` nodes it wrote into this bus's `data-fx-chain`, and carve the bed clip instead.",
|
||||
|
||||
@@ -106,7 +106,7 @@ function buildAspectMismatch(
|
||||
outputResolution,
|
||||
);
|
||||
const suggestion = suggestedResolution
|
||||
? ` The composition is ${describeOrientation(compositionWidth, compositionHeight)} — ` +
|
||||
? ` The composition is ${describeOrientation(compositionWidth, compositionHeight)}, so ` +
|
||||
`use --resolution ${suggestedResolution} instead.`
|
||||
: ` Pick a preset whose orientation matches, or omit --resolution to render at the composition's native dimensions.`;
|
||||
return {
|
||||
|
||||
@@ -193,7 +193,7 @@ export function resolveSlideshow(
|
||||
for (const seq of manifest.slideSequences ?? []) {
|
||||
// Flag duplicate sequence ids rather than silently overwriting the earlier one.
|
||||
if (Object.prototype.hasOwnProperty.call(sequences, seq.id)) {
|
||||
errors.push(`duplicate slideSequence id "${seq.id}" — only the last definition is kept`);
|
||||
errors.push(`duplicate slideSequence id "${seq.id}": only the last definition is kept`);
|
||||
}
|
||||
sequences[seq.id] = {
|
||||
id: seq.id,
|
||||
|
||||
@@ -1387,7 +1387,7 @@ class HyperframesPlayer extends HTMLElement {
|
||||
});
|
||||
const win = doc.defaultView as (Window & { __renderReady?: boolean }) | null;
|
||||
console.warn(
|
||||
`[hyperframes-player] assets-loading timed out after ${ASSETS_READY_TIMEOUT_MS}ms — playing anyway`,
|
||||
`[hyperframes-player] assets-loading timed out after ${ASSETS_READY_TIMEOUT_MS}ms, playing anyway`,
|
||||
{
|
||||
stuckMedia: pendingMedia.map(
|
||||
(el) => el.currentSrc || el.getAttribute("src") || `<${el.tagName.toLowerCase()}>`,
|
||||
@@ -1550,7 +1550,7 @@ class HyperframesPlayer extends HTMLElement {
|
||||
if (applied) this._sendDisplayScale();
|
||||
if (!applied && this._ready && !this._rescaleWarned) {
|
||||
this._rescaleWarned = true;
|
||||
console.warn("[hyperframes-player] rescale no-op after ready — zero-size player element", {
|
||||
console.warn("[hyperframes-player] rescale no-op after ready: zero-size player element", {
|
||||
src: this.getAttribute("src"),
|
||||
offsetWidth: this.offsetWidth,
|
||||
offsetHeight: this.offsetHeight,
|
||||
|
||||
@@ -144,7 +144,7 @@ function injectKeyframesOnce(): void {
|
||||
.hf-hotspot-pill,
|
||||
.hf-nav-spinner { animation: none !important; }
|
||||
}
|
||||
/* Nav-button hover (replaces inline onmouseover/onmouseout — CSP-safe).
|
||||
/* Nav-button hover (replaces inline onmouseover/onmouseout, CSP-safe).
|
||||
!important beats the inline base color set on each button. */
|
||||
[data-hf-nav-cluster] button:hover {
|
||||
background: rgba(255,255,255,0.12) !important;
|
||||
@@ -474,7 +474,7 @@ export class HyperframesSlideshow extends HTMLElement {
|
||||
const cleaned = dropInvalidSlides(resolved);
|
||||
if (cleaned.slides.length === 0 && manifest.slides.length > 0) {
|
||||
console.error(
|
||||
"[hyperframes-slideshow] no main-line slides resolved — the scene timeline may not have loaded in time, or sceneIds/timing are invalid:",
|
||||
"[hyperframes-slideshow] no main-line slides resolved. The scene timeline may not have loaded in time, or sceneIds/timing are invalid:",
|
||||
errors,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export const PLAYER_STYLES = /* css */ `
|
||||
}
|
||||
|
||||
/* Opt-in: an interactive composition (e.g. a live slideshow/app with playable
|
||||
media or controls) — let pointer events reach the iframe content. */
|
||||
media or controls): let pointer events reach the iframe content. */
|
||||
:host([interactive]) .hfp-container,
|
||||
:host([interactive]) .hfp-iframe {
|
||||
pointer-events: auto;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# Init-only gate classification over the full 500. The gate decision is logged at init
|
||||
# ("drawElement canvas injected" OR "falling back to ... — <reason>"), so launch the fast
|
||||
# ("drawElement canvas injected" OR "falls back to ... capture: <reason>"), so launch the fast
|
||||
# render, wait for that line, kill before frames render. Writes /tmp/gatecheck/<pre>.txt.
|
||||
# Resumable. PAR workers. Usage: PAR=4 bash de-gatecheck.sh
|
||||
set -euo pipefail
|
||||
@@ -20,7 +20,7 @@ worker(){
|
||||
local t=0
|
||||
while kill -0 "$p" 2>/dev/null; do
|
||||
sleep 1; t=$((t+1))
|
||||
if grep -qE "drawElement canvas injected|falling back to (screenshot|beginframe) capture —|Fast capture: composition uses|render-mode compatibility hint" "$log" 2>/dev/null; then break; fi
|
||||
if grep -qE "drawElement canvas injected|falls back to (screenshot|beginframe) capture:|Fast capture: composition uses|render-mode compatibility hint" "$log" 2>/dev/null; then break; fi
|
||||
[ "$t" -ge "${CAP:-120}" ] && break
|
||||
done
|
||||
# kill the render tree (don't need full render)
|
||||
@@ -31,13 +31,13 @@ worker(){
|
||||
if grep -q "drawElement canvas injected" "$log" 2>/dev/null; then
|
||||
echo "$pre drawelement" > "$OUT/$pre.txt"
|
||||
else
|
||||
local reason; reason=$(grep -oE "falling back to (screenshot|beginframe) capture — [^(]*" "$log" | head -1 | sed -E 's/.*— //' || true)
|
||||
local reason; reason=$(grep -oE "falls back to (screenshot|beginframe) capture: [^(]*" "$log" | head -1 | sed -E 's/.*capture: //' || true)
|
||||
# Compile-time gates (3D / mix-blend-mode) log a different shape than the
|
||||
# engine's init-time gates — "Fast capture: composition uses X — disabling".
|
||||
if [ -z "$reason" ]; then
|
||||
reason=$(grep -oE "Fast capture: composition uses [a-z0-9 -]*" "$log" | head -1 | sed -E 's/.*uses /compile:/' || true)
|
||||
fi
|
||||
# Compat-hint routing (raw rAF etc.): "fast capture: falling back to screenshot — render-mode ..."
|
||||
# Compat-hint routing (raw rAF etc.): "fast capture falls back to screenshot: render-mode ..."
|
||||
if [ -z "$reason" ] && grep -q "render-mode compatibility hint" "$log" 2>/dev/null; then
|
||||
reason="render-mode compat hint (raw rAF/alpha)"
|
||||
fi
|
||||
|
||||
@@ -384,7 +384,7 @@ function validateOutputResolutionOverride(body: Record<string, unknown>): string
|
||||
return `Invalid outputResolution "${body.outputResolution}". Must be one of: landscape, portrait, landscape-4k, portrait-4k, square, square-4k (aliases: 1080p, 4k, …).`;
|
||||
}
|
||||
if (normalized !== undefined && (body.format === "webm" || body.format === "mov")) {
|
||||
return `outputResolution is not supported with format "${body.format}" — the alpha (webm/mov) capture path can't supersample. Use format "mp4", or omit outputResolution to render at the composition's native dimensions.`;
|
||||
return `outputResolution is not supported with format "${body.format}": the alpha (webm/mov) capture path can't supersample. Use format "mp4", or omit outputResolution to render at the composition's native dimensions.`;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -426,7 +426,7 @@ export class PlanTooLargeError extends Error {
|
||||
`Lambda's 10 GB /tmp budget alongside the chunk worker's frame buffer and ffmpeg's ` +
|
||||
`working set. To unblock: use the content-addressed \`planV2()\` transport, shorten ` +
|
||||
`the composition, lower the framerate, or use the in-process renderer ` +
|
||||
`(\`executeRenderJob\`) — it has no planDir size cap.` +
|
||||
`(\`executeRenderJob\`), which has no planDir size cap.` +
|
||||
observationSuffix +
|
||||
breakdownSuffix,
|
||||
);
|
||||
@@ -468,8 +468,8 @@ export class FormatNotSupportedInDistributedError extends Error {
|
||||
constructor(format: string, reason: string) {
|
||||
super(
|
||||
`[plan] format ${JSON.stringify(format)} is not supported in distributed mode: ${reason}. ` +
|
||||
`Render with the in-process renderer (\`executeRenderJob\`) — it has full format ` +
|
||||
`support — or pick a distributed-supported format: mp4 SDR, mov ProRes 4444, ` +
|
||||
`Render with the in-process renderer (\`executeRenderJob\`), which has full format ` +
|
||||
`support, or pick a distributed-supported format: mp4 SDR, mov ProRes 4444, ` +
|
||||
`png-sequence, or webm VP9.`,
|
||||
);
|
||||
this.name = "FormatNotSupportedInDistributedError";
|
||||
@@ -682,7 +682,7 @@ async function readFontSnapshotSha(): Promise<string> {
|
||||
const data = module.EMBEDDED_FONT_DATA;
|
||||
if (!data || typeof data !== "object") {
|
||||
throw new Error(
|
||||
"[plan] EMBEDDED_FONT_DATA missing from @hyperframes/core/fonts/embedded-data — was `bun run build:fonts` run?",
|
||||
"[plan] EMBEDDED_FONT_DATA missing from @hyperframes/core/fonts/embedded-data. Was `bun run build:fonts` run?",
|
||||
);
|
||||
}
|
||||
// Hash a canonical key fingerprint, not the raw font bytes — the bytes are
|
||||
@@ -778,7 +778,7 @@ function resolveEncoderTriple(config: DistributedRenderConfig): EncoderTriple {
|
||||
throw new Error(
|
||||
`[plan] DistributedRenderConfig.codec is only valid for format="mp4"; received ` +
|
||||
`codec=${JSON.stringify(config.codec)} with format=${JSON.stringify(config.format)}. ` +
|
||||
`Omit codec for non-mp4 formats — mov is always ProRes 4444, webm is always ` +
|
||||
`Omit codec for non-mp4 formats: mov is always ProRes 4444, webm is always ` +
|
||||
`libvpx-vp9, and png-sequence has no encoder.`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -251,10 +251,10 @@ export function validateJsonSafeValue(value: unknown, field: string): void {
|
||||
/** Per-typeof rejection messages for JSON-unsafe leaves. */
|
||||
const LEAF_REJECTIONS: Partial<Record<string, string>> = {
|
||||
undefined:
|
||||
"undefined leaves are silently dropped by JSON.stringify — use null if you mean an absent value",
|
||||
"undefined leaves are silently dropped by JSON.stringify. Use null if you mean an absent value",
|
||||
function: "functions are not JSON-serializable",
|
||||
symbol: "Symbols are not JSON-serializable",
|
||||
bigint: "BigInt values throw at JSON.stringify — encode as a string if you need 64-bit integers",
|
||||
bigint: "BigInt values throw at JSON.stringify. Encode as a string if you need 64-bit integers",
|
||||
};
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
@@ -279,7 +279,7 @@ function walkVariables(value: unknown, path: string, seen: WeakSet<object>): voi
|
||||
if (seen.has(value as object)) {
|
||||
throw new InvalidConfigError(
|
||||
path,
|
||||
"circular reference detected — JSON.stringify cannot serialize cycles",
|
||||
"circular reference detected: JSON.stringify cannot serialize cycles",
|
||||
);
|
||||
}
|
||||
seen.add(value as object);
|
||||
|
||||
@@ -241,7 +241,7 @@ export function buildVirtualTimeShim(options: VirtualTimeShimOptions): string {
|
||||
const seededRandomBlock = seedRandomFromFrame
|
||||
? String.raw`
|
||||
// Seeded Math.random / crypto.getRandomValues, keyed by virtual time.
|
||||
// Mulberry32 — single uint32 state, deterministic, fast.
|
||||
// Mulberry32: single uint32 state, deterministic, fast.
|
||||
var rngState = 0;
|
||||
function mulberry32() {
|
||||
rngState |= 0; rngState = (rngState + 0x6D2B79F5) | 0;
|
||||
@@ -252,7 +252,7 @@ export function buildVirtualTimeShim(options: VirtualTimeShimOptions): string {
|
||||
}
|
||||
function reseedRngFromTime(ms) {
|
||||
var ms32 = Math.max(0, Math.floor(Number(ms) || 0)) | 0;
|
||||
// Knuth's multiplicative hash + golden-ratio offset — gives a well-
|
||||
// Knuth's multiplicative hash + golden-ratio offset gives a well-
|
||||
// distributed seed even for frame 0 (otherwise rngState=0 degenerates
|
||||
// the PRNG's first few outputs).
|
||||
rngState = (Math.imul(ms32, -1640531527) + 0x9E3779B9) | 0;
|
||||
@@ -518,7 +518,7 @@ function buildRenderModeScript(fps: Fps | undefined): string {
|
||||
};
|
||||
window.__playerReady = true;
|
||||
// Media-fallback player has no timeline to bind, so render-ready is immediate.
|
||||
// init.ts defers __renderReady until the timeline is bound — different runtime.
|
||||
// init.ts defers __renderReady until the timeline is bound (a different runtime).
|
||||
window.__renderReady = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -260,7 +260,7 @@ export function blitHdrVideoLayer(
|
||||
if (matrix && !isTranslationOnly) {
|
||||
if (clipped && log) {
|
||||
log.debug(
|
||||
`HDR clip rect on affine-transformed element ${el.id} — clip not applied (affine scissor not yet supported)`,
|
||||
`HDR clip rect on affine-transformed element ${el.id}: clip not applied (affine scissor not yet supported)`,
|
||||
);
|
||||
}
|
||||
blitRgb48leAffine(
|
||||
@@ -349,7 +349,7 @@ export function blitHdrImageLayer(
|
||||
return;
|
||||
}
|
||||
if (el.clipRect && log) {
|
||||
log.debug(`HDR clip rect on image element ${el.id} — clip not yet supported for images`);
|
||||
log.debug(`HDR clip rect on image element ${el.id}: clip not yet supported for images`);
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -540,7 +540,7 @@ async function resolveMediaDuration(
|
||||
// as `prepare/ffmpeg_failed` with owner "system".
|
||||
log?.warn(
|
||||
`[compile] Audio "${elementIdentity}" (${src}) is a text document, not a media ` +
|
||||
"file — the element is dropped from the render. Point it at a rendered media file.",
|
||||
"file, so the element is dropped from the render. Point it at a rendered media file.",
|
||||
);
|
||||
}
|
||||
return { duration: null, resolvedPath: filePath };
|
||||
@@ -669,7 +669,7 @@ async function compileHtmlFile(
|
||||
// thread `log` through parseSubCompositions to warn for it too.
|
||||
log?.warn(
|
||||
`[compile] Audio "${r.id}" (${r.src}) is ${r.maxDuration.toFixed(2)}s but its ` +
|
||||
`data-duration is ${r.duration.toFixed(2)}s — the slot is shortened to the media ` +
|
||||
`data-duration is ${r.duration.toFixed(2)}s, so the slot is shortened to the media ` +
|
||||
`length. Set data-duration to ~${r.maxDuration.toFixed(2)}s, trim data-media-start, ` +
|
||||
`or use a longer/looping source if that isn't intended.`,
|
||||
);
|
||||
@@ -1273,7 +1273,7 @@ export async function inlineExternalScripts(html: string): Promise<string> {
|
||||
// browser support for integrity metadata (including casing) can differ.
|
||||
if (download.reason instanceof ScriptIntegrityError) throw download.reason;
|
||||
defaultLogger.warn(
|
||||
`[Compiler] WARNING: Failed to download CDN script: ${src} — ${download.reason}. ` +
|
||||
`[Compiler] WARNING: Failed to download CDN script: ${src} (${download.reason}). ` +
|
||||
`The render may fail if this script is required (e.g. GSAP). ` +
|
||||
`Consider bundling it locally in your project.`,
|
||||
);
|
||||
@@ -1351,7 +1351,7 @@ export function collectExternalAssets(
|
||||
|
||||
if (externalAssets.size > 0) {
|
||||
defaultLogger.info(
|
||||
`[Compiler] Found ${externalAssets.size} asset(s) outside project directory — will copy to render output`,
|
||||
`[Compiler] Found ${externalAssets.size} asset(s) outside project directory, will copy to render output`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1409,7 +1409,7 @@ async function downloadAndRewriteUrls(
|
||||
urlToLocal.set(url, localPath);
|
||||
} catch (err) {
|
||||
const identity = safeDownloadUrlIdentity(url);
|
||||
defaultLogger.warn(`[Compiler] ${warnLabel} — using original URL as fallback.`, {
|
||||
defaultLogger.warn(`[Compiler] ${warnLabel}, using original URL as fallback.`, {
|
||||
urlFingerprint: identity.urlFingerprint,
|
||||
host: identity.host,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
@@ -1601,7 +1601,7 @@ async function fetchExternalStylesheetCss(href: string): Promise<string | null>
|
||||
timeoutMs: 15_000,
|
||||
});
|
||||
} catch (err) {
|
||||
defaultLogger.warn("[Compiler] External stylesheet fetch failed — preserving link tag.", {
|
||||
defaultLogger.warn("[Compiler] External stylesheet fetch failed, preserving link tag.", {
|
||||
urlFingerprint: identity.urlFingerprint,
|
||||
host: identity.host,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
|
||||
@@ -153,7 +153,7 @@ export function resolveRenderWorkerCount(
|
||||
const reasonCodes = new Set(compiled.renderModeHints.reasons.map((r) => r.code));
|
||||
if (reasonCodes.has("htmlInCanvas")) {
|
||||
log.warn(
|
||||
"[Render] html-in-canvas (drawElementImage) detected — pinning to 1 worker (Chrome concurrency limitation).",
|
||||
"[Render] html-in-canvas (drawElementImage) detected, pinning to 1 worker (Chrome concurrency limitation).",
|
||||
{ requestedWorkers },
|
||||
);
|
||||
return 1;
|
||||
@@ -166,7 +166,7 @@ export function resolveRenderWorkerCount(
|
||||
// every path into capture.
|
||||
if (cfg.lowMemoryMode && requestedWorkers === undefined) {
|
||||
log.info(
|
||||
"[Render] Low-memory profile — pinning to 1 capture worker (auto-worker calibration skipped).",
|
||||
"[Render] Low-memory profile, pinning to 1 capture worker (auto-worker calibration skipped).",
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ describe("resolveEffectiveHdrMode", () => {
|
||||
expect(result).toEqual({ transfer: "pq" });
|
||||
expect(log.warn).toHaveBeenCalledTimes(1);
|
||||
expect(log.warn).toHaveBeenCalledWith(
|
||||
'[Render] HDR auto-promotion triggered by "assets/source-hdr.mp4" — output: BT.2020 / HEVC Main10',
|
||||
'[Render] HDR auto-promotion triggered by "assets/source-hdr.mp4", output: BT.2020 / HEVC Main10',
|
||||
);
|
||||
expect(log.info).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -78,7 +78,7 @@ describe("resolveEffectiveHdrMode", () => {
|
||||
log,
|
||||
});
|
||||
expect(result).toBeUndefined();
|
||||
expect(log.info).toHaveBeenCalledWith("[Render] No HDR sources detected — rendering SDR");
|
||||
expect(log.info).toHaveBeenCalledWith("[Render] No HDR sources detected, rendering SDR");
|
||||
});
|
||||
|
||||
it("force-hdr without sources falls back to HLG and warns", () => {
|
||||
@@ -121,7 +121,7 @@ describe("resolveEffectiveHdrMode", () => {
|
||||
});
|
||||
expect(result).toBeUndefined();
|
||||
expect(log.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining(`format is "${fmt}" — falling back to SDR`),
|
||||
expect.stringContaining(`format is "${fmt}", so falling back to SDR`),
|
||||
);
|
||||
expect(log.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("HDR + alpha is not supported"),
|
||||
@@ -143,7 +143,7 @@ describe("resolveEffectiveHdrMode", () => {
|
||||
});
|
||||
expect(result).toBeUndefined();
|
||||
expect(log.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('format is "hls" — falling back to SDR'),
|
||||
expect.stringContaining('format is "hls", so falling back to SDR'),
|
||||
);
|
||||
expect(log.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("HLS output is SDR-only (H.264 in MPEG-TS)"),
|
||||
|
||||
@@ -50,7 +50,7 @@ function formatDowngradeWarning(
|
||||
? "HLS output is SDR-only (H.264 in MPEG-TS)"
|
||||
: "HDR + alpha is not supported";
|
||||
return (
|
||||
`[Render] ${hdrSourceReason}, but format is "${outputFormat}" — falling back to SDR. ` +
|
||||
`[Render] ${hdrSourceReason}, but format is "${outputFormat}", so falling back to SDR. ` +
|
||||
`${formatReason}. Use --format mp4 for HDR10 output.`
|
||||
);
|
||||
}
|
||||
@@ -77,14 +77,14 @@ function logHdrResolution(
|
||||
): void {
|
||||
if (forcedHdrWithoutSources) {
|
||||
input.log.warn(
|
||||
"[Render] HDR forced by --hdr flag, but no HDR sources were detected — defaulting to HLG. SDR-only compositions may look perceptually wrong on HDR displays.",
|
||||
"[Render] HDR forced by --hdr flag, but no HDR sources were detected, defaulting to HLG. SDR-only compositions may look perceptually wrong on HDR displays.",
|
||||
);
|
||||
}
|
||||
if (!effectiveHdr) {
|
||||
input.log.info(
|
||||
hdrMode === "force-sdr"
|
||||
? "[Render] SDR forced by --sdr flag"
|
||||
: "[Render] No HDR sources detected — rendering SDR",
|
||||
: "[Render] No HDR sources detected, rendering SDR",
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -100,10 +100,10 @@ function logHdrResolution(
|
||||
return;
|
||||
}
|
||||
const reason = forcedHdrWithoutSources
|
||||
? "forced by --hdr flag (no HDR sources detected — defaulting to HLG)"
|
||||
? "forced by --hdr flag (no HDR sources detected, defaulting to HLG)"
|
||||
: "forced by --hdr flag";
|
||||
input.log.info(
|
||||
`[Render] HDR ${reason} — output: ${effectiveHdr.transfer.toUpperCase()} (BT.2020, 10-bit H.265)`,
|
||||
`[Render] HDR ${reason}, output: ${effectiveHdr.transfer.toUpperCase()} (BT.2020, 10-bit H.265)`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ export function validateNoGpuEncode(config: ValidateNoGpuEncodeInput): void {
|
||||
"config.useGpu === true. " +
|
||||
"Distributed retries must be byte-identical, but NVENC/QSV/VAAPI " +
|
||||
"produce different output across machines. Set useGpu=false (the " +
|
||||
"default) — software libx264/libx265 is the only supported encoder " +
|
||||
"default): software libx264/libx265 is the only supported encoder " +
|
||||
"in distributed mode.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@ export async function runCompileStage(input: CompileStageInput): Promise<Compile
|
||||
deCompileGate = "3d";
|
||||
log.info(
|
||||
"[Render] Fast capture: composition uses a CSS 3D rendering context " +
|
||||
"(perspective / preserve-3d / backface-visibility) — disabling drawElementImage " +
|
||||
"(perspective / preserve-3d / backface-visibility), so disabling drawElementImage " +
|
||||
"for this render. Capture uses the platform's baseline route.",
|
||||
);
|
||||
}
|
||||
@@ -237,7 +237,7 @@ export async function runCompileStage(input: CompileStageInput): Promise<Compile
|
||||
cfg.useDrawElement = false;
|
||||
deCompileGate = "mix_blend_mode";
|
||||
log.info(
|
||||
"[Render] Fast capture: composition uses mix-blend-mode — disabling drawElementImage " +
|
||||
"[Render] Fast capture: composition uses mix-blend-mode, so disabling drawElementImage " +
|
||||
"for this render. Capture uses the platform's baseline route.",
|
||||
);
|
||||
}
|
||||
@@ -259,7 +259,7 @@ export async function runCompileStage(input: CompileStageInput): Promise<Compile
|
||||
deCompileGate = "ancestor_background_image";
|
||||
log.info(
|
||||
"[Render] Fast capture: composition root's ancestors (body/html/wrapper) carry a " +
|
||||
"background-image — disabling drawElementImage for this render. Capture uses the " +
|
||||
"background-image, so disabling drawElementImage for this render. Capture uses the " +
|
||||
"platform's baseline route.",
|
||||
);
|
||||
}
|
||||
@@ -276,7 +276,7 @@ export async function runCompileStage(input: CompileStageInput): Promise<Compile
|
||||
cfg.useDrawElement = false;
|
||||
deCompileGate = "shader_transitions";
|
||||
log.info(
|
||||
"[Render] Fast capture: composition uses shader transitions — disabling drawElementImage " +
|
||||
"[Render] Fast capture: composition uses shader transitions, so disabling drawElementImage " +
|
||||
"so page-side compositing stays available.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -238,7 +238,7 @@ function collectPlanAssetShas(planDir: string): {
|
||||
if (compositionHtml === null) {
|
||||
throw new Error(
|
||||
`[freezePlan] compiled HTML missing at ${COMPILED_INDEX_RELATIVE_PATH} ` +
|
||||
`— upstream compile stage did not materialize the expected file.`,
|
||||
`(upstream compile stage did not materialize the expected file).`,
|
||||
);
|
||||
}
|
||||
return { compositionHtml, assets };
|
||||
|
||||
@@ -450,7 +450,7 @@ export async function runProbeStage(input: ProbeStageInput): Promise<ProbeStageR
|
||||
} else {
|
||||
beginFrameStalled = true;
|
||||
log.warn(
|
||||
"[Render] BeginFrame liveness probe timed out — this composition stalls " +
|
||||
"[Render] BeginFrame liveness probe timed out: this composition stalls " +
|
||||
"BeginFrame on this host (SwiftShader heavy-layer pattern). Relaunching " +
|
||||
"the probe browser in screenshot capture mode; the render will use " +
|
||||
"screenshot capture throughout.",
|
||||
@@ -706,7 +706,7 @@ export async function runProbeStage(input: ProbeStageInput): Promise<ProbeStageR
|
||||
video.start = win.visibleStart;
|
||||
video.end = win.visibleEnd;
|
||||
log.info(
|
||||
`[Probe] Runtime video discovery: ${video.id} visible ${win.visibleStart.toFixed(2)}s–${win.visibleEnd.toFixed(2)}s`,
|
||||
`[Probe] Runtime video discovery: ${video.id} visible ${win.visibleStart.toFixed(2)}s to ${win.visibleEnd.toFixed(2)}s`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -766,7 +766,7 @@ export async function runProbeStage(input: ProbeStageInput): Promise<ProbeStageR
|
||||
});
|
||||
if (!timelinesInfo.gsapLoaded) {
|
||||
diagnostics.push(
|
||||
"GSAP is not loaded — CDN script may have failed to download. " +
|
||||
"GSAP is not loaded. The CDN script may have failed to download. " +
|
||||
"Bundle GSAP locally in your project instead of using a CDN <script src>.",
|
||||
);
|
||||
} else if (timelinesInfo.timelineKeys.length === 0) {
|
||||
@@ -786,13 +786,13 @@ export async function runProbeStage(input: ProbeStageInput): Promise<ProbeStageR
|
||||
log.warn("Failed to gather browser diagnostics for zero-duration composition", {
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
diagnostics.push("(Could not gather browser diagnostics — page may have crashed)");
|
||||
diagnostics.push("(Could not gather browser diagnostics. The page may have crashed.)");
|
||||
}
|
||||
const hint =
|
||||
diagnostics.length > 0
|
||||
? "\n\nDiagnostics:\n - " + diagnostics.join("\n - ")
|
||||
: "\n\nCheck that GSAP timelines are registered on window.__timelines.";
|
||||
throw new Error("Composition duration is 0 — this would produce a black video." + hint);
|
||||
throw new Error("Composition duration is 0, so this would produce a black video." + hint);
|
||||
}
|
||||
|
||||
// Surface browser-side asset failures (404s, script errors) as warnings.
|
||||
|
||||
@@ -251,7 +251,7 @@ export function assertVideoFrameCoverage(
|
||||
throw new VideoFrameCoverageError(
|
||||
`Video "${worst.videoId}" captured ${worst.capturedFrames} of expected ${worst.expectedFrames} frames ` +
|
||||
`(coverage ${pct}%, threshold ${thresholdPct}%). ` +
|
||||
`check/snapshot may pass while the encoded MP4 renders this clip blank — aborting render ` +
|
||||
`check/snapshot may pass while the encoded MP4 renders this clip blank, so the render is aborted ` +
|
||||
`to prevent shipping a wrong MP4.${suffix} ` +
|
||||
`Set HF_VIDEO_COVERAGE_THRESHOLD=0 to disable this gate.`,
|
||||
{ threshold, worst, failedReports: sorted },
|
||||
|
||||
@@ -1274,7 +1274,7 @@ export async function executeDiskCaptureWithAdaptiveRetry(options: {
|
||||
const madeProgress = captureAttemptMadeProgress(frameCount, remainingCount);
|
||||
if (!madeProgress) {
|
||||
options.log.warn(
|
||||
"[Render] Capture attempt made no forward progress; composition is likely structurally broken — not retrying.",
|
||||
"[Render] Capture attempt made no forward progress; composition is likely structurally broken, not retrying.",
|
||||
{ attempt, frameCount, remainingCount, workers: currentWorkers },
|
||||
);
|
||||
}
|
||||
@@ -1382,7 +1382,7 @@ export async function executeDiskCaptureWithAdaptiveRetry(options: {
|
||||
|
||||
if (!madeProgress) {
|
||||
options.log.warn(
|
||||
"[Render] Capture attempt made no forward progress; composition is likely structurally broken — not retrying.",
|
||||
"[Render] Capture attempt made no forward progress; composition is likely structurally broken, not retrying.",
|
||||
{ attempt, frameCount, remainingCount, workers: currentWorkers },
|
||||
);
|
||||
}
|
||||
@@ -2795,7 +2795,7 @@ export async function executeRenderJob(
|
||||
execution.defer("remove workDir", () => {
|
||||
if (job.config.debug) return;
|
||||
if (job.status === "complete" && process.env.KEEP_TEMP === "1") {
|
||||
log.info("KEEP_TEMP=1 — leaving workDir on disk for inspection", { workDir });
|
||||
log.info("KEEP_TEMP=1, leaving workDir on disk for inspection", { workDir });
|
||||
return;
|
||||
}
|
||||
rmSync(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
||||
@@ -3138,7 +3138,7 @@ async function executeRenderPipeline(input: {
|
||||
captureForceScreenshot = true;
|
||||
updateCaptureObservability({ forceScreenshot: captureForceScreenshot });
|
||||
log.info(
|
||||
"[Render] Low-memory render profile active — " +
|
||||
"[Render] Low-memory render profile active: " +
|
||||
"screenshot capture, auto-worker calibration skipped" +
|
||||
(job.config.workers === undefined ? ", pinned to 1 worker" : "") +
|
||||
". Override with --no-low-memory-mode or PRODUCER_LOW_MEMORY_MODE=false.",
|
||||
@@ -4034,7 +4034,7 @@ async function executeRenderPipeline(input: {
|
||||
cfg.useDrawElement = false;
|
||||
deClampReason = workerCount > 1 ? "parallel" : "disk_path";
|
||||
log.info(
|
||||
"[Render] Fast capture: default-on drawElement disabled for this render — " +
|
||||
"[Render] Fast capture: default-on drawElement disabled for this render: " +
|
||||
(workerCount > 1 ? "parallel capture" : "the disk capture path") +
|
||||
" has no runtime self-verification. Set PRODUCER_EXPERIMENTAL_FAST_CAPTURE=true to override.",
|
||||
);
|
||||
@@ -4185,7 +4185,7 @@ async function executeRenderPipeline(input: {
|
||||
});
|
||||
updateCaptureObservability({ forceScreenshot: captureForceScreenshot });
|
||||
log.info(
|
||||
"[Render] Page-side compositing enabled — bypassing Node-side layered " +
|
||||
"[Render] Page-side compositing enabled, bypassing Node-side layered " +
|
||||
`shader-blend path. Engine will capture one ${needsAlpha ? "RGBA PNG" : "opaque RGB"} ` +
|
||||
"screenshot per output frame.",
|
||||
);
|
||||
@@ -4828,7 +4828,7 @@ async function executeRenderPipeline(input: {
|
||||
cfg.useDrawElement = false;
|
||||
deClampReason = deClampReason ?? "disk_path";
|
||||
log.info(
|
||||
"[Render] Fast capture: drawElement disabled for the disk fallback — " +
|
||||
"[Render] Fast capture: drawElement disabled for the disk fallback: " +
|
||||
"streaming encoder spawn failed and the disk path has no runtime " +
|
||||
"self-verification.",
|
||||
);
|
||||
|
||||
@@ -192,7 +192,7 @@ async function runWebmCheck(workRoot: string): Promise<void> {
|
||||
const decoded = decodePng(readFileSync(framePng));
|
||||
assertAlphaPixel(decoded, TRANSPARENT_X, TRANSPARENT_Y, "transparent", "webm");
|
||||
assertAlphaPixel(decoded, OPAQUE_X, OPAQUE_Y, "opaque-red", "webm");
|
||||
console.log("[webm] PASS — transparent + opaque-red pixels verified");
|
||||
console.log("[webm] PASS: transparent + opaque-red pixels verified");
|
||||
}
|
||||
|
||||
async function runRootBackgroundCheck(workRoot: string): Promise<void> {
|
||||
@@ -216,7 +216,7 @@ async function runRootBackgroundCheck(workRoot: string): Promise<void> {
|
||||
const decoded = decodePng(readFileSync(framePng));
|
||||
assertAlphaPixel(decoded, ROOT_BG_X, ROOT_BG_Y, "opaque-blue", "root-bg");
|
||||
assertAlphaPixel(decoded, OPAQUE_X, OPAQUE_Y, "opaque-red", "root-bg");
|
||||
console.log("[root-bg] PASS — composition root background preserved in alpha output");
|
||||
console.log("[root-bg] PASS: composition root background preserved in alpha output");
|
||||
}
|
||||
|
||||
async function runGifCheck(workRoot: string): Promise<void> {
|
||||
@@ -244,7 +244,7 @@ async function runGifCheck(workRoot: string): Promise<void> {
|
||||
const decoded = decodePng(readFileSync(framePng));
|
||||
assertAlphaPixel(decoded, TRANSPARENT_X, TRANSPARENT_Y, "transparent", "gif");
|
||||
assertAlphaPixel(decoded, OPAQUE_X, OPAQUE_Y, "opaque-red", "gif");
|
||||
console.log("[gif] PASS — transparent + opaque-red pixels verified");
|
||||
console.log("[gif] PASS: transparent + opaque-red pixels verified");
|
||||
}
|
||||
|
||||
async function runGifShaderTransitionCheck(workRoot: string): Promise<void> {
|
||||
@@ -287,7 +287,7 @@ async function runGifShaderTransitionCheck(workRoot: string): Promise<void> {
|
||||
`gif shader transition expected >=20 dB against the golden, got ${transitionPsnr.toFixed(2)} dB`,
|
||||
);
|
||||
console.log(
|
||||
`[gif-shader] PASS — control ${beforePsnr.toFixed(2)} dB, transition ${transitionPsnr.toFixed(2)} dB`,
|
||||
`[gif-shader] PASS: control ${beforePsnr.toFixed(2)} dB, transition ${transitionPsnr.toFixed(2)} dB`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -331,7 +331,7 @@ async function runPngSequenceCheck(workRoot: string): Promise<void> {
|
||||
const decoded = decodePng(readFileSync(join(outDir, firstFrame)));
|
||||
assertAlphaPixel(decoded, TRANSPARENT_X, TRANSPARENT_Y, "transparent", "png-sequence");
|
||||
assertAlphaPixel(decoded, OPAQUE_X, OPAQUE_Y, "opaque-red", "png-sequence");
|
||||
console.log("[png-sequence] PASS — transparent + opaque-red pixels verified");
|
||||
console.log("[png-sequence] PASS: transparent + opaque-red pixels verified");
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
|
||||
@@ -170,7 +170,7 @@ export function computeAudioResidualRmsDb(
|
||||
4,
|
||||
)}s, snapshot=${snapshotDur.seconds.toFixed(4)}s (Δ=${delta.toFixed(
|
||||
4,
|
||||
)}s > ${TOLERANCE_SECONDS}s) — amix=duration=shortest would hide the trailing difference`,
|
||||
)}s > ${TOLERANCE_SECONDS}s): amix=duration=shortest would hide the trailing difference`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ function validateSetAttribute(name: string, value: string | null): void {
|
||||
}
|
||||
if (lower.startsWith("on")) {
|
||||
throw new Error(
|
||||
`setAttribute: event-handler attributes ("${name}") are not permitted — ` +
|
||||
`setAttribute: event-handler attributes ("${name}") are not permitted: ` +
|
||||
`they produce executable HTML that cannot be safely serialized.`,
|
||||
);
|
||||
}
|
||||
@@ -329,7 +329,7 @@ function planLinkedTiming(
|
||||
const tag = el.tagName.toLowerCase();
|
||||
return {
|
||||
partners: [],
|
||||
refusal: `Linked ${tag} would start after the new end — unlink or trim the ${tag} first.`,
|
||||
refusal: `Linked ${tag} would start after the new end. Unlink or trim the ${tag} first.`,
|
||||
};
|
||||
}
|
||||
partners.push({ id, timing: partnerEdit });
|
||||
@@ -1057,7 +1057,7 @@ function fragmentCompositionErr(parsed: ParsedDocument): CanResult | null {
|
||||
return canErr(
|
||||
"E_FRAGMENT_COMPOSITION",
|
||||
"Fragment compositions cannot carry variable declarations.",
|
||||
"The composition has no root element to hold data-composition-variables — add a composition root or convert to a full HTML document.",
|
||||
"The composition has no root element to hold data-composition-variables. Add a composition root or convert to a full HTML document.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1083,7 +1083,7 @@ function invalidVariableIdErr(id: string): CanResult {
|
||||
return canErr(
|
||||
"E_INVALID_VARIABLE_ID",
|
||||
`Variable id ${JSON.stringify(id)} is not a valid identifier.`,
|
||||
"Ids must match /^[A-Za-z_][A-Za-z0-9_-]*$/ — they become CSS custom-property names (--id), data-var-* attribute values, and CLI --variables keys.",
|
||||
"Ids must match /^[A-Za-z_][A-Za-z0-9_-]*$/ because they become CSS custom-property names (--id), data-var-* attribute values, and CLI --variables keys.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1711,7 +1711,7 @@ function gsapAnimationMissing(parsed: ParsedDocument, animationId: string): CanR
|
||||
: canErr(
|
||||
"E_TARGET_NOT_FOUND",
|
||||
`No GSAP animation found with id "${animationId}".`,
|
||||
"Animation ids are positional and shift after edits — re-read them from comp before dispatching.",
|
||||
"Animation ids are positional and shift after edits. Re-read them from comp before dispatching.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1828,7 +1828,7 @@ export function validateOp(parsed: ParsedDocument, op: EditOp): CanResult {
|
||||
return canErr(
|
||||
"E_INVALID_ARGS",
|
||||
`declaration.id ("${op.declaration.id}") must match id ("${op.id}").`,
|
||||
"Variable ids are immutable — rename via removeVariableDeclaration + declareVariable.",
|
||||
"Variable ids are immutable. Rename via removeVariableDeclaration + declareVariable.",
|
||||
);
|
||||
if (
|
||||
findVariableDeclaration(
|
||||
|
||||
@@ -916,7 +916,7 @@ export function init(config: HyperShaderConfig): GsapTimeline {
|
||||
|
||||
const gl = createContext(glCanvas, compWidth, compHeight);
|
||||
if (!gl) {
|
||||
console.warn("[HyperShader] WebGL unavailable — shader transitions disabled.");
|
||||
console.warn("[HyperShader] WebGL unavailable, shader transitions disabled.");
|
||||
const fallback = config.timeline || gsap.timeline({ paused: true });
|
||||
registerTimeline(compId, fallback, config.timeline);
|
||||
return fallback;
|
||||
@@ -1374,7 +1374,7 @@ export function init(config: HyperShaderConfig): GsapTimeline {
|
||||
const isCssFallback = !requestedShader || compiledProg === null;
|
||||
if (requestedShader && compiledProg === null) {
|
||||
console.warn(
|
||||
`[HyperShader] Shader "${t.shader}" failed to compile — falling back to CSS crossfade.`,
|
||||
`[HyperShader] Shader "${t.shader}" failed to compile, falling back to CSS crossfade.`,
|
||||
);
|
||||
}
|
||||
const prog = isCssFallback ? null : compiledProg;
|
||||
|
||||
@@ -386,7 +386,7 @@ export function audioNormalizationPlan(
|
||||
if (wantedGainDb > MAX_AUDIO_GAIN_DB + 1e-9) {
|
||||
throw new Error(
|
||||
`Matching #${targetTrack.id} needs ${wantedGainDb.toFixed(1)} dB, beyond the +${MAX_AUDIO_GAIN_DB} dB authoring ceiling. ` +
|
||||
`A gap this large belongs in the source file, not the mixer — mixer gain raises the noise floor with the signal. ` +
|
||||
`A gap this large belongs in the source file, not the mixer: mixer gain raises the noise floor with the signal. ` +
|
||||
`Normalize ${targetTrack.id}'s asset offline (e.g. ffmpeg loudnorm), or lower #${referenceTrack.id} instead.`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -268,7 +268,7 @@ export function registerThumbnailRoutes(api: Hono, adapter: StudioApiAdapter): v
|
||||
);
|
||||
if (!buffer) {
|
||||
return c.json(
|
||||
{ error: "Thumbnail generation failed — Chrome browser may not be available" },
|
||||
{ error: "Thumbnail generation failed. Chrome browser may not be available" },
|
||||
500,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -408,7 +408,7 @@ export const CaptionOverlay = memo(function CaptionOverlay({ iframeRef }: Captio
|
||||
{wordBoxes.length === 0 && model && model.segments.size > 0 && (
|
||||
<div className="absolute inset-x-0 top-3 flex justify-center pointer-events-none">
|
||||
<span className="px-2.5 py-1 rounded-full bg-black/60 border border-neutral-700 text-2xs text-neutral-300">
|
||||
No captions visible at this frame — scrub to a caption, or select one in the track below
|
||||
No captions visible at this frame. Scrub to a caption, or select one in the track below
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -235,7 +235,7 @@ export function useCaptionSync(projectId: string | null) {
|
||||
// File exists but is unreadable — previous edits would silently not load.
|
||||
useCaptionStore
|
||||
.getState()
|
||||
.setSyncError("caption-overrides.json is corrupt — earlier caption edits didn't load");
|
||||
.setSyncError("caption-overrides.json is corrupt, so earlier caption edits didn't load");
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -50,8 +50,8 @@ export function LinkedSelectionToggle() {
|
||||
<Tooltip
|
||||
label={
|
||||
linkedSelection
|
||||
? "Linked Selection on — a click selects both halves (⌥-click for one)"
|
||||
: "Linked Selection off — a click selects one clip"
|
||||
? "Linked Selection on: a click selects both halves (⌥-click for one)"
|
||||
: "Linked Selection off: a click selects one clip"
|
||||
}
|
||||
>
|
||||
<button
|
||||
|
||||
@@ -113,11 +113,7 @@ export function LintModal({
|
||||
: "bg-accent hover:bg-accent-hover text-on-accent"
|
||||
}`}
|
||||
>
|
||||
{copied
|
||||
? "Copied!"
|
||||
: copyFailed
|
||||
? "Copy failed — check permissions"
|
||||
: "Copy to Agent"}
|
||||
{copied ? "Copied!" : copyFailed ? "Copy failed. Check permissions" : "Copy to Agent"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -23,7 +23,7 @@ function MediaErrorPanel({ name, filePath }: { name: string; filePath: string })
|
||||
<span className="text-sm text-neutral-400 font-medium">{name}</span>
|
||||
<span className="text-[11px] text-neutral-600 font-mono">{filePath}</span>
|
||||
<span className="text-[10px] text-neutral-500">
|
||||
Couldn't load this file — it may be missing or corrupt
|
||||
Couldn't load this file. It may be missing or corrupt
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
@@ -110,7 +110,7 @@ export function MediaPreview({ projectId, filePath }: { projectId: string; fileP
|
||||
</svg>
|
||||
<span className="text-sm text-neutral-400 font-medium">{name}</span>
|
||||
<span className="text-[11px] text-neutral-600 font-mono">{filePath}</span>
|
||||
<span className="text-[10px] text-neutral-600">Binary file — preview not available</span>
|
||||
<span className="text-[10px] text-neutral-600">Binary file: preview not available</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ export function ProjectUnreachableBanner({ projectId }: ProjectUnreachableBanner
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<span>Couldn't open this project — it may have been renamed, moved, or deleted.</span>
|
||||
<span>Couldn't open this project. It may have been renamed, moved, or deleted.</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -174,7 +174,7 @@ export const StudioRightPanels = memo(function StudioRightPanels({
|
||||
// timeline offers that write as a mute. Checked here as well as in the panel
|
||||
// because the button is not the only caller.
|
||||
if (!canHideSelections(domEditGroupSelections)) {
|
||||
showToast("Audio can't be hidden — use the group's own controls", "info");
|
||||
showToast("Audio can't be hidden. Use the group's own controls", "info");
|
||||
return;
|
||||
}
|
||||
const { elements } = usePlayerStore.getState();
|
||||
|
||||
@@ -82,7 +82,7 @@ describe("TimelineToolbar — adaptive thumbnails", () => {
|
||||
it("keeps a user-controlled hidden mode as the rollback path", () => {
|
||||
const { host, root } = renderToolbar();
|
||||
const button = host.querySelector<HTMLButtonElement>(
|
||||
'button[aria-label="Hide thumbnails — labels only"]',
|
||||
'button[aria-label="Hide thumbnails: labels only"]',
|
||||
);
|
||||
if (!button) throw new Error("thumbnail toggle not rendered");
|
||||
|
||||
@@ -90,7 +90,7 @@ describe("TimelineToolbar — adaptive thumbnails", () => {
|
||||
|
||||
expect(usePlayerStore.getState().thumbnailMode).toBe("hidden");
|
||||
expect(button.getAttribute("aria-label")).toBe(
|
||||
"Show thumbnails — posters stay visible; richer previews appear on interaction",
|
||||
"Show thumbnails: posters stay visible; richer previews appear on interaction",
|
||||
);
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
@@ -235,8 +235,8 @@ export const TimelineToolbar = memo(function TimelineToolbar({
|
||||
<Tooltip
|
||||
label={
|
||||
rippleEditEnabled
|
||||
? "Ripple on — keeps the main track gapless"
|
||||
: "Ripple off — deleting a main-track clip leaves a gap"
|
||||
? "Ripple on: keeps the main track gapless"
|
||||
: "Ripple off: deleting a main-track clip leaves a gap"
|
||||
}
|
||||
>
|
||||
<button
|
||||
@@ -465,16 +465,16 @@ export const TimelineToolbar = memo(function TimelineToolbar({
|
||||
<Tooltip
|
||||
label={
|
||||
thumbnailsVisible
|
||||
? "Hide thumbnails — labels only"
|
||||
: "Show thumbnails — posters stay visible; richer previews appear on interaction"
|
||||
? "Hide thumbnails: labels only"
|
||||
: "Show thumbnails: posters stay visible; richer previews appear on interaction"
|
||||
}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={
|
||||
thumbnailsVisible
|
||||
? "Hide thumbnails — labels only"
|
||||
: "Show thumbnails — posters stay visible; richer previews appear on interaction"
|
||||
? "Hide thumbnails: labels only"
|
||||
: "Show thumbnails: posters stay visible; richer previews appear on interaction"
|
||||
}
|
||||
aria-pressed={thumbnailsVisible}
|
||||
onClick={() => setThumbnailMode(thumbnailsVisible ? "hidden" : "adaptive")}
|
||||
|
||||
@@ -217,7 +217,7 @@ export const AnimationCard = memo(function AnimationCard({
|
||||
{typeof animation.position === "number"
|
||||
? `${parseFloat(animation.position.toFixed(3))}s`
|
||||
: animation.position}{" "}
|
||||
– {typeof endTime === "number" ? `${parseFloat(endTime.toFixed(3))}s` : endTime}
|
||||
to {typeof endTime === "number" ? `${parseFloat(endTime.toFixed(3))}s` : endTime}
|
||||
</span>
|
||||
<span
|
||||
className={`ml-auto text-[10px] ${flat ? "text-panel-text-3" : "text-neutral-500"}`}
|
||||
@@ -256,7 +256,7 @@ export const AnimationCard = memo(function AnimationCard({
|
||||
clipPath: "polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)",
|
||||
}}
|
||||
/>
|
||||
Keyframed — click a segment below to edit its curve
|
||||
Keyframed: click a segment below to edit its curve
|
||||
</p>
|
||||
)}
|
||||
{runEase && runEase !== "none" && (
|
||||
@@ -273,7 +273,7 @@ export const AnimationCard = memo(function AnimationCard({
|
||||
setTimeout(() => setCopied(false), 1500);
|
||||
}}
|
||||
className="shrink-0 rounded-sm px-1.5 py-0.5 text-[9px] font-medium text-neutral-500 transition-colors hover:bg-neutral-800 hover:text-neutral-300"
|
||||
title="Copy description to clipboard — paste into agent prompts"
|
||||
title="Copy description for agent prompts"
|
||||
>
|
||||
{copied ? "Copied" : "Copy"}
|
||||
</button>
|
||||
|
||||
@@ -93,7 +93,7 @@ export function PropertyRow({
|
||||
onClick={() => onCommit(isVisible ? "hidden" : "visible")}
|
||||
className="shrink-0 rounded-full transition-colors duration-200 relative"
|
||||
style={{ width: 28, height: 16, background: isVisible ? P.accent : P.borderInput }}
|
||||
title={isVisible ? "Visible — click to hide" : "Hidden — click to show"}
|
||||
title={isVisible ? "Visible: click to hide" : "Hidden: click to show"}
|
||||
>
|
||||
<span
|
||||
className="absolute top-[2px] left-0 rounded-full transition-transform duration-200"
|
||||
|
||||
@@ -58,7 +58,7 @@ export const BlockParamsPanel = memo(function BlockParamsPanel({
|
||||
if (matches === 0) {
|
||||
setCommitState({
|
||||
tone: "error",
|
||||
message: `Couldn't find the current value in ${compositionPath} — it may have been edited by hand.`,
|
||||
message: `Couldn't find the current value in ${compositionPath}. It may have been edited by hand.`,
|
||||
});
|
||||
trackBlockParamCommit({ tone: "error", blockName, key });
|
||||
return;
|
||||
@@ -73,7 +73,7 @@ export const BlockParamsPanel = memo(function BlockParamsPanel({
|
||||
if (matches > 1) {
|
||||
setCommitState({
|
||||
tone: "error",
|
||||
message: `"${previous}" appears ${matches}× in ${compositionPath} — the panel can't tell which one belongs to this parameter, so it won't risk changing unrelated content. Edit the file directly to disambiguate.`,
|
||||
message: `"${previous}" appears ${matches}× in ${compositionPath}. The panel can't tell which one belongs to this parameter, so it won't risk changing unrelated content. Edit the file directly to disambiguate.`,
|
||||
});
|
||||
trackBlockParamCommit({ tone: "error", blockName, key });
|
||||
return;
|
||||
@@ -171,7 +171,7 @@ export const BlockParamsPanel = memo(function BlockParamsPanel({
|
||||
)}
|
||||
{!fileManager && params.length > 0 && (
|
||||
<div className="text-[10px] text-warning-ink">
|
||||
Block params can't be edited here — no project file access.
|
||||
Block params can't be edited here: no project file access.
|
||||
</div>
|
||||
)}
|
||||
{params.map((param) => (
|
||||
|
||||
@@ -17,14 +17,14 @@ export function ComputedTweenNotice({
|
||||
if (editability === "source") {
|
||||
return (
|
||||
<div className="rounded-md border border-neutral-800 bg-neutral-900/50 px-2 py-1.5 text-[9px] text-neutral-400">
|
||||
Computed value — edit it in the Code tab.
|
||||
Computed value. Edit it in the Code tab.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const source = provenance?.fn ? `${provenance.fn}()` : "a loop";
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-2 rounded-md border border-neutral-800 bg-neutral-900/50 px-2 py-1.5 text-[9px] text-neutral-400">
|
||||
<span>Generated by {source} — not directly editable.</span>
|
||||
<span>Generated by {source}, not directly editable.</span>
|
||||
{onUnroll && (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -293,7 +293,7 @@ export const FileTree = memo(function FileTree({
|
||||
)}
|
||||
{children.length === 0 && !inlineInput && (
|
||||
<div className="px-3 py-4 text-center text-[11px] text-neutral-600">
|
||||
No files yet{hasFileOps ? " — use + above to create one" : ""}.
|
||||
No files yet{hasFileOps ? ". Use + above to create one" : ""}.
|
||||
</div>
|
||||
)}
|
||||
{children.map((child) =>
|
||||
|
||||
@@ -63,7 +63,7 @@ export function GsapAnimationList({
|
||||
<div className="space-y-2">
|
||||
{animations.length === 0 && (
|
||||
<p className="text-[11px] leading-4 text-neutral-500">
|
||||
No animations on this element yet — add an effect below to animate it.
|
||||
No animations on this element yet. Add an effect below to animate it.
|
||||
</p>
|
||||
)}
|
||||
{animations.map((animation, index) => (
|
||||
|
||||
@@ -25,8 +25,8 @@ export const GsapAnimationSection = memo(function GsapAnimationSection({
|
||||
<Section title="Animation" icon={<Film size={15} />}>
|
||||
{multipleTimelines && (
|
||||
<p className="mb-2 rounded-lg bg-amber-500/10 px-3 py-2 text-[11px] leading-relaxed text-warning-ink">
|
||||
This file has multiple GSAP timelines. Animation editing is disabled to prevent data loss
|
||||
— consolidate into a single timeline to enable editing.
|
||||
This file has multiple GSAP timelines. Animation editing is disabled to prevent data loss.
|
||||
Consolidate into a single timeline to enable editing.
|
||||
</p>
|
||||
)}
|
||||
{unsupportedTimelinePattern && (
|
||||
|
||||
@@ -127,7 +127,7 @@ export function OffCanvasIndicators({
|
||||
aria-label={`Select off-canvas element ${r.key}`}
|
||||
className="pointer-events-auto absolute inset-0 border-2 border-dashed border-studio-accent/10 rounded-md cursor-pointer hover:border-studio-accent hover:bg-studio-accent/10 transition-colors"
|
||||
style={clipOutside ? { clipPath: clipOutside } : undefined}
|
||||
title={`Off-canvas: ${r.key} — click to select`}
|
||||
title={`Off-canvas: ${r.key}. Click to select`}
|
||||
onClick={handleClick}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
|
||||
@@ -50,7 +50,7 @@ export function PromotableControl({
|
||||
useEffect(() => {
|
||||
if (danglingId != null) {
|
||||
console.warn(
|
||||
`[hyperframes] Control is bound to variable "${danglingId}", but no such declaration exists. The element still carries the binding on disk — re-declare the variable or unbind the element.`,
|
||||
`[hyperframes] Control is bound to variable "${danglingId}", but no such declaration exists. The element still carries the binding on disk. Re-declare the variable or unbind the element.`,
|
||||
);
|
||||
}
|
||||
}, [danglingId]);
|
||||
|
||||
@@ -240,13 +240,13 @@ export const PropertyPanel = memo(function PropertyPanel(props: PropertyPanelPro
|
||||
navigator.clipboard
|
||||
.writeText(text)
|
||||
.then(() => {
|
||||
showToast(`Copied element info for ${element.label} — paste into any AI agent`, "info");
|
||||
showToast(`Copied element info for ${element.label}. Paste it into any AI agent`, "info");
|
||||
setClipboardCopied(true);
|
||||
clearTimeout(clipboardTimerRef.current);
|
||||
clipboardTimerRef.current = setTimeout(() => setClipboardCopied(false), 1500);
|
||||
})
|
||||
.catch(() => {
|
||||
showToast("Couldn't copy to the clipboard — check browser permissions", "error");
|
||||
showToast("Couldn't copy to the clipboard. Check browser permissions", "error");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -160,8 +160,8 @@ export const SnapToolbar = memo(function SnapToolbar() {
|
||||
}}
|
||||
title={
|
||||
prefs.gridVisible
|
||||
? "Grid visible (G) — right-click for spacing options"
|
||||
: "Grid hidden (G) — right-click for spacing options"
|
||||
? "Grid visible (G). Right-click for spacing options"
|
||||
: "Grid hidden (G). Right-click for spacing options"
|
||||
}
|
||||
aria-label="Toggle grid"
|
||||
>
|
||||
|
||||
@@ -308,7 +308,7 @@ export function Transform3DCube({
|
||||
onClick={onKeyframe}
|
||||
title={
|
||||
keyframed
|
||||
? "3D transform is keyframed — click a field diamond to add keyframes"
|
||||
? "3D transform is keyframed. Click a field diamond to add keyframes"
|
||||
: "Keyframe the 3D transform (animate it over time)"
|
||||
}
|
||||
aria-label="Keyframe 3D transform"
|
||||
|
||||
@@ -8,7 +8,7 @@ export const METHOD_LABELS: Record<string, string> = {
|
||||
};
|
||||
|
||||
export const METHOD_TOOLTIPS: Record<string, string> = {
|
||||
set: "Instantly snap to these values — no transition",
|
||||
set: "Instantly snap to these values, no transition",
|
||||
to: "Smoothly animate the element to these target values",
|
||||
from: "Element starts at these values and transitions to its normal state",
|
||||
fromTo: "Animate from one state to another",
|
||||
@@ -83,7 +83,7 @@ export const PROP_TOOLTIPS: Record<string, string> = {
|
||||
perspective:
|
||||
"3D depth context for child elements; set it on a parent when rotating children in 3D",
|
||||
transformPerspective:
|
||||
"3D depth for THIS element's own X/Y rotation — lower = stronger perspective (try 600–1000)",
|
||||
"3D depth for THIS element's own X/Y rotation: lower = stronger perspective (try 600 to 1000)",
|
||||
transformOrigin: "Pivot point for transforms, for example center center or 50% 50%",
|
||||
width: "Element width",
|
||||
height: "Element height",
|
||||
|
||||
@@ -109,7 +109,7 @@ describe("buildTweenSummary", () => {
|
||||
const s = buildTweenSummary(
|
||||
anim({ method: "fromTo", fromProperties: {}, properties: { scale: 2 } }),
|
||||
);
|
||||
expect(s).toContain("from [—]");
|
||||
expect(s).toContain("from [none]");
|
||||
});
|
||||
|
||||
it("handles no properties", () => {
|
||||
|
||||
@@ -64,7 +64,7 @@ export function buildTweenSummary(animation: GsapAnimation): string {
|
||||
const label = (PROP_LABELS[p] ?? p).toLowerCase();
|
||||
return `${label} ${formatPropValue(p, v)}`;
|
||||
});
|
||||
const fromText = fromDescs.length > 0 ? fromDescs.join(", ") : "—";
|
||||
const fromText = fromDescs.length > 0 ? fromDescs.join(", ") : "none";
|
||||
return `Starting at ${pos}s, over ${dur}s, ${target} animates from [${fromText}] to [${propText}] using a ${ease.toLowerCase()} curve.`;
|
||||
}
|
||||
return `Starting at ${pos}s, over ${dur}s, animate ${target}'s ${propText} using a ${ease.toLowerCase()} curve.`;
|
||||
|
||||
@@ -423,7 +423,7 @@ export function ColorGradingControls({
|
||||
void actions
|
||||
.importLut(files, onImportAssets, () => track("button", "Import LUT"))
|
||||
.catch(() =>
|
||||
setLutImportError("LUT import failed — check the .cube file and try again."),
|
||||
setLutImportError("LUT import failed. Check the .cube file and try again."),
|
||||
)
|
||||
.finally(() => setLutImporting(false));
|
||||
event.currentTarget.value = "";
|
||||
|
||||
@@ -135,7 +135,7 @@ export function ImageFillField({
|
||||
commitImage(toProjectRootAssetPath(nextImage), true);
|
||||
}
|
||||
} catch {
|
||||
setUploadError("Upload failed — check the file and try again.");
|
||||
setUploadError("Upload failed. Check the file and try again.");
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ export function FlatTimingRow({
|
||||
{cell("Duration", formatTimingValue(duration), commitDuration)}
|
||||
{derived && (
|
||||
<p className="col-span-3 mt-1 text-[10px] leading-snug text-panel-text-3">
|
||||
Inferred from this element's animation — edit to pin an explicit clip range.
|
||||
Inferred from this element's animation. Edit to pin an explicit clip range.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -150,7 +150,7 @@ export function FlatMotionSection({
|
||||
{multipleTimelines && (
|
||||
<p className="rounded-lg bg-amber-500/10 px-3 py-2 text-[11px] leading-relaxed text-warning-ink">
|
||||
This file has multiple GSAP timelines. Animation editing is disabled to prevent data
|
||||
loss — consolidate into a single timeline to enable editing.
|
||||
loss. Consolidate into a single timeline to enable editing.
|
||||
</p>
|
||||
)}
|
||||
{unsupportedTimelinePattern && (
|
||||
@@ -195,7 +195,7 @@ export function motionSectionLabel(args: {
|
||||
if (args.timingOnly) {
|
||||
return {
|
||||
title: "Timing",
|
||||
summary: `${formatTimingValue(args.start)} – ${formatTimingValue(args.start + args.duration)}`,
|
||||
summary: `${formatTimingValue(args.start)} to ${formatTimingValue(args.start + args.duration)}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -72,7 +72,7 @@ export function FlatRow({
|
||||
<button
|
||||
type="button"
|
||||
data-flat-row-reset="true"
|
||||
title="Remove — fall back to default"
|
||||
title="Remove and fall back to default"
|
||||
onClick={() => {
|
||||
track("button", `Reset ${label}`);
|
||||
onReset();
|
||||
@@ -556,7 +556,7 @@ export function FlatSlider({
|
||||
<button
|
||||
type="button"
|
||||
data-flat-slider-reset="true"
|
||||
title="Remove — fall back to default"
|
||||
title="Remove and fall back to default"
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
track("button", `Reset ${label}`);
|
||||
|
||||
@@ -90,7 +90,7 @@ export function FlatSelectRow({
|
||||
<button
|
||||
type="button"
|
||||
data-flat-select-reset="true"
|
||||
title="Remove — fall back to default"
|
||||
title="Remove and fall back to default"
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
track("button", `Reset ${trackName}`);
|
||||
|
||||
@@ -35,7 +35,7 @@ const ALIGN_OPTIONS = [
|
||||
];
|
||||
|
||||
const CASE_OPTIONS = [
|
||||
{ key: "none", label: "none", node: "–" },
|
||||
{ key: "none", label: "none", node: "None" },
|
||||
{ key: "uppercase", label: "uppercase", node: "AG" },
|
||||
{ key: "lowercase", label: "lowercase", node: "ag" },
|
||||
{ key: "capitalize", label: "capitalize", node: "Ag" },
|
||||
|
||||
@@ -290,7 +290,7 @@ export function FontFamilyField({
|
||||
setFontNotice("No supported font files were imported.");
|
||||
}
|
||||
} catch {
|
||||
setFontNotice("Font import failed — the files were not added. Try again.");
|
||||
setFontNotice("Font import failed. The files were not added. Try again.");
|
||||
} finally {
|
||||
setImportingFonts(false);
|
||||
}
|
||||
@@ -389,10 +389,10 @@ export function FontFamilyField({
|
||||
} else {
|
||||
// Committing an un-imported family would render a silent fallback,
|
||||
// so surface the failure and keep the current font instead.
|
||||
setFontNotice(`Couldn't import "${option.family}" — the font was not applied.`);
|
||||
setFontNotice(`Couldn't import "${option.family}", so the font was not applied.`);
|
||||
}
|
||||
} catch {
|
||||
setFontNotice(`Couldn't import "${option.family}" — the font was not applied.`);
|
||||
setFontNotice(`Couldn't import "${option.family}", so the font was not applied.`);
|
||||
} finally {
|
||||
setImportingFonts(false);
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ export function FxBandRuler({ band, at }: FxBandRulerProps) {
|
||||
return (
|
||||
<span
|
||||
key={range.name}
|
||||
title={`${range.name} — ${range.says}`}
|
||||
title={`${range.name}: ${range.says}`}
|
||||
className={
|
||||
range.name === here.name
|
||||
? "hf-fx-ruler-band bg-panel-accent"
|
||||
@@ -65,7 +65,7 @@ export function FxBandRuler({ band, at }: FxBandRulerProps) {
|
||||
})}
|
||||
</div>
|
||||
<p className="hf-fx-ruler-label truncate pt-0.5 text-[9px] text-panel-text-2">
|
||||
<span className="hf-fx-ruler-name text-panel-text-1">{here.name}</span> — {here.says}
|
||||
<span className="hf-fx-ruler-name text-panel-text-1">{here.name}</span>: {here.says}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -245,11 +245,11 @@ export function FxNodeOpenBody({
|
||||
onClick={onToggleDetails}
|
||||
>
|
||||
<span aria-hidden="true">{details ? "▾" : "▸"}</span>
|
||||
Details — {registryDef.label}
|
||||
Details: {registryDef.label}
|
||||
</button>
|
||||
) : (
|
||||
<p className="hf-fx-node-mechanism border-t border-panel-border-input px-1.5 pt-1 font-mono text-[9px] uppercase tracking-wide text-panel-text-2">
|
||||
Details — {registryDef.label}
|
||||
Details: {registryDef.label}
|
||||
</p>
|
||||
)}
|
||||
{details || !oneKnob ? (
|
||||
|
||||
@@ -84,7 +84,7 @@ export function TimingSection({
|
||||
</div>
|
||||
{derived && (
|
||||
<p className="mt-2 text-[10px] leading-snug text-neutral-500">
|
||||
Inferred from this element’s animation — edit to pin an explicit clip range.
|
||||
Inferred from this element’s animation. Edit to pin an explicit clip range.
|
||||
</p>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
@@ -71,7 +71,7 @@ export function createTransformCommitHandlers({
|
||||
await commitAnimatedTransformValue(
|
||||
axis,
|
||||
parsed,
|
||||
"This element's position can't be edited here yet — it is driven by its animation",
|
||||
"This element's position can't be edited here yet: its animation drives it",
|
||||
)
|
||||
)
|
||||
return;
|
||||
@@ -97,7 +97,7 @@ export function createTransformCommitHandlers({
|
||||
return;
|
||||
}
|
||||
if (hasGsapAnimation) {
|
||||
showToast?.("This element's size can't be edited here yet — it is driven by its animation");
|
||||
showToast?.("This element's size can't be edited here yet: its animation drives it");
|
||||
return;
|
||||
}
|
||||
const current = readStudioBoxSize(element.element);
|
||||
@@ -124,7 +124,7 @@ export function createTransformCommitHandlers({
|
||||
await commitAnimatedTransformValue(
|
||||
"rotation",
|
||||
parsed,
|
||||
"This element's rotation can't be edited here yet — it is driven by its animation",
|
||||
"This element's rotation can't be edited here yet: its animation drives it",
|
||||
)
|
||||
)
|
||||
return;
|
||||
|
||||
@@ -352,7 +352,7 @@ export function useColorGradingController({
|
||||
const reverted = confirmedGradingRef.current;
|
||||
latestGradingRef.current = reverted;
|
||||
setGrading(reverted);
|
||||
setRuntimeStatus({ state: "unavailable", message: "Save failed — reverted" });
|
||||
setRuntimeStatus({ state: "unavailable", message: "Save failed, changes reverted" });
|
||||
};
|
||||
// The callback is the primary result signal; the rejection path supports
|
||||
// other setAttributeLive implementations.
|
||||
|
||||
@@ -489,7 +489,7 @@ export function HotspotTool({
|
||||
)}
|
||||
{sequences.length === 0 && (
|
||||
<p className="text-[10px] text-neutral-500 italic">
|
||||
Create a branch in the Branches section first — hotspots jump to a branch.
|
||||
Create a branch in the Branches section first. Hotspots jump to a branch.
|
||||
</p>
|
||||
)}
|
||||
<label className="text-[11px] text-neutral-400">Hotspot label</label>
|
||||
@@ -508,7 +508,7 @@ export function HotspotTool({
|
||||
onChange={(e) => setTargetSequenceId(e.target.value)}
|
||||
aria-label="Target branch sequence"
|
||||
>
|
||||
<option value="">— select branch —</option>
|
||||
<option value="">Select branch</option>
|
||||
{sequences.map((seq) => (
|
||||
<option key={seq.id} value={seq.id}>
|
||||
{seq.label}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user