@hyperframes/producer 0.2.2-alpha.4 → 0.2.3-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +188 -6
- package/dist/index.js.map +2 -2
- package/dist/public-server.js +188 -6
- package/dist/public-server.js.map +2 -2
- package/dist/services/deterministicFonts.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/public-server.js
CHANGED
|
@@ -108452,6 +108452,34 @@ function getSingleClassSelector(selector) {
|
|
|
108452
108452
|
const match2 = selector.trim().match(/^\.(?<name>[A-Za-z0-9_-]+)$/);
|
|
108453
108453
|
return match2?.groups?.name || null;
|
|
108454
108454
|
}
|
|
108455
|
+
function cssTransformToGsapProps(cssTransform) {
|
|
108456
|
+
const parts = [];
|
|
108457
|
+
const translateMatch = cssTransform.match(
|
|
108458
|
+
/translate\(\s*(-?[\d.]+)(%|px)?\s*,\s*(-?[\d.]+)(%|px)?\s*\)/
|
|
108459
|
+
);
|
|
108460
|
+
if (translateMatch) {
|
|
108461
|
+
const [, xVal, xUnit, yVal, yUnit] = translateMatch;
|
|
108462
|
+
if (xUnit === "%") parts.push(`xPercent: ${xVal}`);
|
|
108463
|
+
else parts.push(`x: ${xVal}`);
|
|
108464
|
+
if (yUnit === "%") parts.push(`yPercent: ${yVal}`);
|
|
108465
|
+
else parts.push(`y: ${yVal}`);
|
|
108466
|
+
}
|
|
108467
|
+
const txMatch = cssTransform.match(/translateX\(\s*(-?[\d.]+)(%|px)?\s*\)/);
|
|
108468
|
+
if (txMatch) {
|
|
108469
|
+
const [, val, unit] = txMatch;
|
|
108470
|
+
parts.push(unit === "%" ? `xPercent: ${val}` : `x: ${val}`);
|
|
108471
|
+
}
|
|
108472
|
+
const tyMatch = cssTransform.match(/translateY\(\s*(-?[\d.]+)(%|px)?\s*\)/);
|
|
108473
|
+
if (tyMatch) {
|
|
108474
|
+
const [, val, unit] = tyMatch;
|
|
108475
|
+
parts.push(unit === "%" ? `yPercent: ${val}` : `y: ${val}`);
|
|
108476
|
+
}
|
|
108477
|
+
const scaleMatch = cssTransform.match(/scale\(\s*([\d.]+)\s*\)/);
|
|
108478
|
+
if (scaleMatch) {
|
|
108479
|
+
parts.push(`scale: ${scaleMatch[1]}`);
|
|
108480
|
+
}
|
|
108481
|
+
return parts.length > 0 ? parts.join(", ") : null;
|
|
108482
|
+
}
|
|
108455
108483
|
var gsapRules = [
|
|
108456
108484
|
// overlapping_gsap_tweens + gsap_animates_clip_element + unscoped_gsap_selector
|
|
108457
108485
|
({ tags, scripts, rootCompositionId }) => {
|
|
@@ -108509,14 +108537,18 @@ ${right2.raw}`)
|
|
|
108509
108537
|
const sel = win.targetSelector;
|
|
108510
108538
|
const clipInfo = clipIds.get(sel) || clipClasses.get(sel);
|
|
108511
108539
|
if (!clipInfo) continue;
|
|
108540
|
+
const conflictingProps = win.properties.filter(
|
|
108541
|
+
(p) => p === "visibility" || p === "display"
|
|
108542
|
+
);
|
|
108543
|
+
if (conflictingProps.length === 0) continue;
|
|
108512
108544
|
const elDesc = `<${clipInfo.tag}${clipInfo.id ? ` id="${clipInfo.id}"` : ""} class="${clipInfo.classes}">`;
|
|
108513
108545
|
findings.push({
|
|
108514
108546
|
code: "gsap_animates_clip_element",
|
|
108515
108547
|
severity: "error",
|
|
108516
|
-
message: `GSAP animation
|
|
108548
|
+
message: `GSAP animation sets ${conflictingProps.join(", ")} on a clip element. Selector "${sel}" resolves to element ${elDesc}. The framework manages clip visibility via ${conflictingProps.join("/")} \u2014 do not animate these properties on clip elements.`,
|
|
108517
108549
|
selector: sel,
|
|
108518
108550
|
elementId: clipInfo.id || void 0,
|
|
108519
|
-
fixHint: "
|
|
108551
|
+
fixHint: "Remove the visibility/display tween, or move the content into a child <div> and target that instead.",
|
|
108520
108552
|
snippet: truncateSnippet(win.raw)
|
|
108521
108553
|
});
|
|
108522
108554
|
}
|
|
@@ -108581,12 +108613,14 @@ ${right2.raw}`)
|
|
|
108581
108613
|
}
|
|
108582
108614
|
for (const [sel, { cssTransform, props, raw: raw2 }] of conflicts) {
|
|
108583
108615
|
const propList = [...props].join("/");
|
|
108616
|
+
const gsapEquivalent = cssTransformToGsapProps(cssTransform);
|
|
108617
|
+
const fixHint = gsapEquivalent ? `Remove \`transform: ${cssTransform}\` from CSS and replace with GSAP properties: ${gsapEquivalent}. Example: tl.fromTo('${sel}', { ${gsapEquivalent} }, { ${gsapEquivalent}, ...yourAnimation }). tl.fromTo is exempt from this rule.` : `Remove the transform from CSS and use tl.fromTo('${sel}', { xPercent: -50, x: -1000 }, { xPercent: -50, x: 0 }) so GSAP owns the full transform state. tl.fromTo is exempt from this rule.`;
|
|
108584
108618
|
findings.push({
|
|
108585
108619
|
code: "gsap_css_transform_conflict",
|
|
108586
108620
|
severity: "warning",
|
|
108587
108621
|
message: `"${sel}" has CSS \`transform: ${cssTransform}\` and a GSAP tween animates ${propList}. GSAP will overwrite the full CSS transform, discarding any translateX(-50%) centering or CSS scale value.`,
|
|
108588
108622
|
selector: sel,
|
|
108589
|
-
fixHint
|
|
108623
|
+
fixHint,
|
|
108590
108624
|
snippet: truncateSnippet(raw2)
|
|
108591
108625
|
});
|
|
108592
108626
|
}
|
|
@@ -108613,6 +108647,62 @@ ${right2.raw}`)
|
|
|
108613
108647
|
fixHint: 'Add <script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script> before your animation script.'
|
|
108614
108648
|
}
|
|
108615
108649
|
];
|
|
108650
|
+
},
|
|
108651
|
+
// audio_reactive_single_tween_per_group
|
|
108652
|
+
({ scripts, styles }) => {
|
|
108653
|
+
const findings = [];
|
|
108654
|
+
const isCaptionFile = styles.some((s) => /\.caption[-_]?(?:group|word)/i.test(s.content));
|
|
108655
|
+
if (!isCaptionFile) return findings;
|
|
108656
|
+
for (const script of scripts) {
|
|
108657
|
+
const content = script.content;
|
|
108658
|
+
const hasAudioData = /AUDIO|audio[-_]?data|bands\[/.test(content);
|
|
108659
|
+
if (!hasAudioData) continue;
|
|
108660
|
+
const hasCaptionLoop = /forEach/.test(content) && /caption|group|cg-/.test(content);
|
|
108661
|
+
if (!hasCaptionLoop) continue;
|
|
108662
|
+
const hasInnerSamplingLoop = /for\s*\(\s*var\s+\w+\s*=\s*group\.start/.test(content) || /for\s*\(\s*var\s+at\s*=/.test(content) || /while\s*\(\s*\w+\s*<\s*group\.end/.test(content);
|
|
108663
|
+
if (!hasInnerSamplingLoop) {
|
|
108664
|
+
const hasPeakTween = /peak(?:Bass|Treble|Energy)/.test(content) && /group\.start/.test(content);
|
|
108665
|
+
if (hasPeakTween) {
|
|
108666
|
+
findings.push({
|
|
108667
|
+
code: "audio_reactive_single_tween_per_group",
|
|
108668
|
+
severity: "warning",
|
|
108669
|
+
message: "Audio-reactive captions use a single tween per group based on peak values. This sets one static value at group.start \u2014 not perceptible as audio reactivity.",
|
|
108670
|
+
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 point for visible pulsing."
|
|
108671
|
+
});
|
|
108672
|
+
}
|
|
108673
|
+
}
|
|
108674
|
+
}
|
|
108675
|
+
return findings;
|
|
108676
|
+
},
|
|
108677
|
+
// scene_layer_missing_visibility_kill
|
|
108678
|
+
({ scripts, tags }) => {
|
|
108679
|
+
const findings = [];
|
|
108680
|
+
const sceneElements = tags.filter((t) => {
|
|
108681
|
+
const id = readAttr(t.raw, "id") || "";
|
|
108682
|
+
return /^scene\d+$/i.test(id);
|
|
108683
|
+
});
|
|
108684
|
+
if (sceneElements.length < 2) return findings;
|
|
108685
|
+
for (const script of scripts) {
|
|
108686
|
+
const content = script.content;
|
|
108687
|
+
for (const tag of sceneElements) {
|
|
108688
|
+
const id = readAttr(tag.raw, "id") || "";
|
|
108689
|
+
const exitPattern = new RegExp(`["']#${id}["'][^)]*opacity\\s*:\\s*0`);
|
|
108690
|
+
const hasExit = exitPattern.test(content);
|
|
108691
|
+
if (!hasExit) continue;
|
|
108692
|
+
const killPattern = new RegExp(`["']#${id}["'][^)]*visibility\\s*:\\s*["']hidden["']`);
|
|
108693
|
+
const hasKill = killPattern.test(content);
|
|
108694
|
+
if (!hasKill) {
|
|
108695
|
+
findings.push({
|
|
108696
|
+
code: "scene_layer_missing_visibility_kill",
|
|
108697
|
+
severity: "warning",
|
|
108698
|
+
elementId: id,
|
|
108699
|
+
message: `Scene layer "#${id}" exits via opacity tween but has no visibility: hidden hard kill. When scrubbing or when tweens conflict, the scene may remain partially visible and overlap the next scene.`,
|
|
108700
|
+
fixHint: `Add \`tl.set("#${id}", { visibility: "hidden" }, <exit-end-time>)\` after the scene's exit tweens.`
|
|
108701
|
+
});
|
|
108702
|
+
}
|
|
108703
|
+
}
|
|
108704
|
+
}
|
|
108705
|
+
return findings;
|
|
108616
108706
|
}
|
|
108617
108707
|
];
|
|
108618
108708
|
|
|
@@ -108720,6 +108810,80 @@ var captionRules = [
|
|
|
108720
108810
|
}
|
|
108721
108811
|
}
|
|
108722
108812
|
return findings;
|
|
108813
|
+
},
|
|
108814
|
+
// caption_overflow_clips_scaled_words
|
|
108815
|
+
({ styles, scripts }) => {
|
|
108816
|
+
const findings = [];
|
|
108817
|
+
const hasScaledWords = scripts.some(
|
|
108818
|
+
(s) => /scale\s*:\s*1\.[2-9]/.test(s.content) && /caption|word|cg-/.test(s.content)
|
|
108819
|
+
);
|
|
108820
|
+
if (!hasScaledWords) return findings;
|
|
108821
|
+
for (const style of styles) {
|
|
108822
|
+
const captionBlocks = style.content.matchAll(
|
|
108823
|
+
/(\.caption[-_]?(?:group|container)|#caption[-_]?(?:layer|container))\s*\{([^}]+)\}/gi
|
|
108824
|
+
);
|
|
108825
|
+
for (const [, selector, body] of captionBlocks) {
|
|
108826
|
+
if (!body) continue;
|
|
108827
|
+
if (/overflow\s*:\s*hidden/i.test(body)) {
|
|
108828
|
+
findings.push({
|
|
108829
|
+
code: "caption_overflow_clips_scaled_words",
|
|
108830
|
+
severity: "warning",
|
|
108831
|
+
selector: (selector ?? "").trim(),
|
|
108832
|
+
message: `"${(selector ?? "").trim()}" has overflow: hidden but GSAP scales caption words above 1.0x. Scaled emphasis words and their glow effects will be clipped.`,
|
|
108833
|
+
fixHint: "Use overflow: visible on caption containers. Rely on fitTextFontSize with reduced maxWidth to prevent overflow instead."
|
|
108834
|
+
});
|
|
108835
|
+
}
|
|
108836
|
+
}
|
|
108837
|
+
}
|
|
108838
|
+
return findings;
|
|
108839
|
+
},
|
|
108840
|
+
// caption_textshadow_on_group_container
|
|
108841
|
+
({ scripts, styles }) => {
|
|
108842
|
+
const findings = [];
|
|
108843
|
+
const isCaptionFile = styles.some((s) => /\.caption[-_]?(?:group|word)/i.test(s.content));
|
|
108844
|
+
if (!isCaptionFile) return findings;
|
|
108845
|
+
for (const script of scripts) {
|
|
108846
|
+
const groupShadowPattern = /\.to\s*\(\s*(?:div|groupEl|el|captionEl|document\.getElementById\s*\(\s*["']cg-)\s*[^,]*,\s*\{[^}]*textShadow/g;
|
|
108847
|
+
const selectorShadowPattern = /\.to\s*\(\s*["'](?:#cg-\d+|\.caption[-_]?group)["']\s*,\s*\{[^}]*textShadow/g;
|
|
108848
|
+
if (groupShadowPattern.test(script.content) || selectorShadowPattern.test(script.content)) {
|
|
108849
|
+
findings.push({
|
|
108850
|
+
code: "caption_textshadow_on_group_container",
|
|
108851
|
+
severity: "warning",
|
|
108852
|
+
message: "textShadow is tweened on a caption group container. When children have semi-transparent color (e.g., inactive karaoke words at rgba opacity), the glow renders as a visible rectangle behind the entire group.",
|
|
108853
|
+
fixHint: "Apply textShadow to individual active word elements instead of the group container. Use scale on the group for bass-reactive pulsing."
|
|
108854
|
+
});
|
|
108855
|
+
}
|
|
108856
|
+
}
|
|
108857
|
+
return findings;
|
|
108858
|
+
},
|
|
108859
|
+
// caption_fittext_scale_mismatch
|
|
108860
|
+
({ scripts }) => {
|
|
108861
|
+
const findings = [];
|
|
108862
|
+
for (const script of scripts) {
|
|
108863
|
+
const content = script.content;
|
|
108864
|
+
const fitTextMatch = content.match(/fitTextFontSize\s*\([^)]*maxWidth\s*:\s*(\d+)/);
|
|
108865
|
+
if (!fitTextMatch) continue;
|
|
108866
|
+
const maxWidth = parseInt(fitTextMatch[1] ?? "0", 10);
|
|
108867
|
+
if (!maxWidth) continue;
|
|
108868
|
+
const scaleMatches = [...content.matchAll(/scale\s*:\s*(1\.\d+)/g)];
|
|
108869
|
+
const captionContext = /caption|word|cg-|karaoke/i.test(content);
|
|
108870
|
+
if (!captionContext || scaleMatches.length === 0) continue;
|
|
108871
|
+
let maxScale = 1;
|
|
108872
|
+
for (const m of scaleMatches) {
|
|
108873
|
+
const val = parseFloat(m[1] ?? "1");
|
|
108874
|
+
if (val > maxScale) maxScale = val;
|
|
108875
|
+
}
|
|
108876
|
+
const effectiveWidth = maxWidth * maxScale;
|
|
108877
|
+
if (effectiveWidth > 1760) {
|
|
108878
|
+
findings.push({
|
|
108879
|
+
code: "caption_fittext_scale_mismatch",
|
|
108880
|
+
severity: "warning",
|
|
108881
|
+
message: `fitTextFontSize uses maxWidth: ${maxWidth}px but emphasis words scale up to ${maxScale}x. Effective width ${Math.round(effectiveWidth)}px may overflow the composition (1920px minus margins).`,
|
|
108882
|
+
fixHint: `Reduce maxWidth to ${Math.floor(1700 / maxScale)}px to leave headroom for scaled emphasis words.`
|
|
108883
|
+
});
|
|
108884
|
+
}
|
|
108885
|
+
}
|
|
108886
|
+
return findings;
|
|
108723
108887
|
}
|
|
108724
108888
|
];
|
|
108725
108889
|
|
|
@@ -109299,7 +109463,8 @@ async function initializeSession(session) {
|
|
|
109299
109463
|
const type = msg.type();
|
|
109300
109464
|
const text = msg.text();
|
|
109301
109465
|
const isFontLoadError = type === "error" && text.startsWith("Failed to load resource") && /fonts\.googleapis|fonts\.gstatic|\.woff2?(\b|$)/i.test(text);
|
|
109302
|
-
const
|
|
109466
|
+
const isResourceLoadError = type === "error" && text.startsWith("Failed to load resource") && !isFontLoadError;
|
|
109467
|
+
const prefix = isResourceLoadError ? "[non-blocking]" : type === "error" ? "[Browser:ERROR]" : type === "warn" ? "[Browser:WARN]" : "[Browser]";
|
|
109303
109468
|
if (!isFontLoadError) {
|
|
109304
109469
|
console.log(`${prefix} ${text}`);
|
|
109305
109470
|
}
|
|
@@ -111932,6 +112097,23 @@ function buildFontFaceCss(requestedFamilies) {
|
|
|
111932
112097
|
unresolved: unresolved.sort()
|
|
111933
112098
|
};
|
|
111934
112099
|
}
|
|
112100
|
+
function warnUnresolvedFonts(unresolved) {
|
|
112101
|
+
const mapped = Object.entries(FONT_ALIASES).reduce((acc, [alias, canonical]) => {
|
|
112102
|
+
const display = alias === canonical ? alias : `${alias} \u2192 ${canonical}`;
|
|
112103
|
+
if (!acc.includes(display)) acc.push(display);
|
|
112104
|
+
return acc;
|
|
112105
|
+
}, []).sort();
|
|
112106
|
+
console.warn(
|
|
112107
|
+
`[Compiler] No deterministic font mapping for: ${unresolved.join(", ")}
|
|
112108
|
+
Mapped fonts: ${mapped.join(", ")}
|
|
112109
|
+
To fix, pick one:
|
|
112110
|
+
1. Use a mapped font name instead (see list above)
|
|
112111
|
+
2. Add a @font-face block in your HTML with a local or hosted font file
|
|
112112
|
+
3. Install the font locally on the render machine (Docker: add to Dockerfile)
|
|
112113
|
+
4. Add an alias to FONT_ALIASES in deterministicFonts.ts (for contributors)
|
|
112114
|
+
Docs: https://hyperframes.heygen.com/docs/fonts`
|
|
112115
|
+
);
|
|
112116
|
+
}
|
|
111935
112117
|
function injectDeterministicFontFaces(html) {
|
|
111936
112118
|
const existingFaces = extractExistingFontFaces(html);
|
|
111937
112119
|
const requestedFamilies = extractRequestedFontFamilies(html);
|
|
@@ -111947,7 +112129,7 @@ function injectDeterministicFontFaces(html) {
|
|
|
111947
112129
|
const { css, unresolved } = buildFontFaceCss(pendingFamilies);
|
|
111948
112130
|
if (!css) {
|
|
111949
112131
|
if (unresolved.length > 0) {
|
|
111950
|
-
|
|
112132
|
+
warnUnresolvedFonts(unresolved);
|
|
111951
112133
|
}
|
|
111952
112134
|
return html;
|
|
111953
112135
|
}
|
|
@@ -111964,7 +112146,7 @@ function injectDeterministicFontFaces(html) {
|
|
|
111964
112146
|
`[Compiler] Injected deterministic @font-face rules for ${pendingFamilies.size - unresolved.length} requested font families`
|
|
111965
112147
|
);
|
|
111966
112148
|
if (unresolved.length > 0) {
|
|
111967
|
-
|
|
112149
|
+
warnUnresolvedFonts(unresolved);
|
|
111968
112150
|
}
|
|
111969
112151
|
return document3.toString();
|
|
111970
112152
|
}
|