@hyperframes/producer 0.2.2 → 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/index.js
CHANGED
|
@@ -105663,6 +105663,34 @@ function getSingleClassSelector(selector) {
|
|
|
105663
105663
|
const match2 = selector.trim().match(/^\.(?<name>[A-Za-z0-9_-]+)$/);
|
|
105664
105664
|
return match2?.groups?.name || null;
|
|
105665
105665
|
}
|
|
105666
|
+
function cssTransformToGsapProps(cssTransform) {
|
|
105667
|
+
const parts = [];
|
|
105668
|
+
const translateMatch = cssTransform.match(
|
|
105669
|
+
/translate\(\s*(-?[\d.]+)(%|px)?\s*,\s*(-?[\d.]+)(%|px)?\s*\)/
|
|
105670
|
+
);
|
|
105671
|
+
if (translateMatch) {
|
|
105672
|
+
const [, xVal, xUnit, yVal, yUnit] = translateMatch;
|
|
105673
|
+
if (xUnit === "%") parts.push(`xPercent: ${xVal}`);
|
|
105674
|
+
else parts.push(`x: ${xVal}`);
|
|
105675
|
+
if (yUnit === "%") parts.push(`yPercent: ${yVal}`);
|
|
105676
|
+
else parts.push(`y: ${yVal}`);
|
|
105677
|
+
}
|
|
105678
|
+
const txMatch = cssTransform.match(/translateX\(\s*(-?[\d.]+)(%|px)?\s*\)/);
|
|
105679
|
+
if (txMatch) {
|
|
105680
|
+
const [, val, unit] = txMatch;
|
|
105681
|
+
parts.push(unit === "%" ? `xPercent: ${val}` : `x: ${val}`);
|
|
105682
|
+
}
|
|
105683
|
+
const tyMatch = cssTransform.match(/translateY\(\s*(-?[\d.]+)(%|px)?\s*\)/);
|
|
105684
|
+
if (tyMatch) {
|
|
105685
|
+
const [, val, unit] = tyMatch;
|
|
105686
|
+
parts.push(unit === "%" ? `yPercent: ${val}` : `y: ${val}`);
|
|
105687
|
+
}
|
|
105688
|
+
const scaleMatch = cssTransform.match(/scale\(\s*([\d.]+)\s*\)/);
|
|
105689
|
+
if (scaleMatch) {
|
|
105690
|
+
parts.push(`scale: ${scaleMatch[1]}`);
|
|
105691
|
+
}
|
|
105692
|
+
return parts.length > 0 ? parts.join(", ") : null;
|
|
105693
|
+
}
|
|
105666
105694
|
var gsapRules = [
|
|
105667
105695
|
// overlapping_gsap_tweens + gsap_animates_clip_element + unscoped_gsap_selector
|
|
105668
105696
|
({ tags, scripts, rootCompositionId }) => {
|
|
@@ -105720,14 +105748,18 @@ ${right2.raw}`)
|
|
|
105720
105748
|
const sel = win.targetSelector;
|
|
105721
105749
|
const clipInfo = clipIds.get(sel) || clipClasses.get(sel);
|
|
105722
105750
|
if (!clipInfo) continue;
|
|
105751
|
+
const conflictingProps = win.properties.filter(
|
|
105752
|
+
(p) => p === "visibility" || p === "display"
|
|
105753
|
+
);
|
|
105754
|
+
if (conflictingProps.length === 0) continue;
|
|
105723
105755
|
const elDesc = `<${clipInfo.tag}${clipInfo.id ? ` id="${clipInfo.id}"` : ""} class="${clipInfo.classes}">`;
|
|
105724
105756
|
findings.push({
|
|
105725
105757
|
code: "gsap_animates_clip_element",
|
|
105726
105758
|
severity: "error",
|
|
105727
|
-
message: `GSAP animation
|
|
105759
|
+
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.`,
|
|
105728
105760
|
selector: sel,
|
|
105729
105761
|
elementId: clipInfo.id || void 0,
|
|
105730
|
-
fixHint: "
|
|
105762
|
+
fixHint: "Remove the visibility/display tween, or move the content into a child <div> and target that instead.",
|
|
105731
105763
|
snippet: truncateSnippet(win.raw)
|
|
105732
105764
|
});
|
|
105733
105765
|
}
|
|
@@ -105792,12 +105824,14 @@ ${right2.raw}`)
|
|
|
105792
105824
|
}
|
|
105793
105825
|
for (const [sel, { cssTransform, props, raw: raw2 }] of conflicts) {
|
|
105794
105826
|
const propList = [...props].join("/");
|
|
105827
|
+
const gsapEquivalent = cssTransformToGsapProps(cssTransform);
|
|
105828
|
+
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.`;
|
|
105795
105829
|
findings.push({
|
|
105796
105830
|
code: "gsap_css_transform_conflict",
|
|
105797
105831
|
severity: "warning",
|
|
105798
105832
|
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.`,
|
|
105799
105833
|
selector: sel,
|
|
105800
|
-
fixHint
|
|
105834
|
+
fixHint,
|
|
105801
105835
|
snippet: truncateSnippet(raw2)
|
|
105802
105836
|
});
|
|
105803
105837
|
}
|
|
@@ -105824,6 +105858,62 @@ ${right2.raw}`)
|
|
|
105824
105858
|
fixHint: 'Add <script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script> before your animation script.'
|
|
105825
105859
|
}
|
|
105826
105860
|
];
|
|
105861
|
+
},
|
|
105862
|
+
// audio_reactive_single_tween_per_group
|
|
105863
|
+
({ scripts, styles }) => {
|
|
105864
|
+
const findings = [];
|
|
105865
|
+
const isCaptionFile = styles.some((s) => /\.caption[-_]?(?:group|word)/i.test(s.content));
|
|
105866
|
+
if (!isCaptionFile) return findings;
|
|
105867
|
+
for (const script of scripts) {
|
|
105868
|
+
const content = script.content;
|
|
105869
|
+
const hasAudioData = /AUDIO|audio[-_]?data|bands\[/.test(content);
|
|
105870
|
+
if (!hasAudioData) continue;
|
|
105871
|
+
const hasCaptionLoop = /forEach/.test(content) && /caption|group|cg-/.test(content);
|
|
105872
|
+
if (!hasCaptionLoop) continue;
|
|
105873
|
+
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);
|
|
105874
|
+
if (!hasInnerSamplingLoop) {
|
|
105875
|
+
const hasPeakTween = /peak(?:Bass|Treble|Energy)/.test(content) && /group\.start/.test(content);
|
|
105876
|
+
if (hasPeakTween) {
|
|
105877
|
+
findings.push({
|
|
105878
|
+
code: "audio_reactive_single_tween_per_group",
|
|
105879
|
+
severity: "warning",
|
|
105880
|
+
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.",
|
|
105881
|
+
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."
|
|
105882
|
+
});
|
|
105883
|
+
}
|
|
105884
|
+
}
|
|
105885
|
+
}
|
|
105886
|
+
return findings;
|
|
105887
|
+
},
|
|
105888
|
+
// scene_layer_missing_visibility_kill
|
|
105889
|
+
({ scripts, tags }) => {
|
|
105890
|
+
const findings = [];
|
|
105891
|
+
const sceneElements = tags.filter((t) => {
|
|
105892
|
+
const id = readAttr(t.raw, "id") || "";
|
|
105893
|
+
return /^scene\d+$/i.test(id);
|
|
105894
|
+
});
|
|
105895
|
+
if (sceneElements.length < 2) return findings;
|
|
105896
|
+
for (const script of scripts) {
|
|
105897
|
+
const content = script.content;
|
|
105898
|
+
for (const tag of sceneElements) {
|
|
105899
|
+
const id = readAttr(tag.raw, "id") || "";
|
|
105900
|
+
const exitPattern = new RegExp(`["']#${id}["'][^)]*opacity\\s*:\\s*0`);
|
|
105901
|
+
const hasExit = exitPattern.test(content);
|
|
105902
|
+
if (!hasExit) continue;
|
|
105903
|
+
const killPattern = new RegExp(`["']#${id}["'][^)]*visibility\\s*:\\s*["']hidden["']`);
|
|
105904
|
+
const hasKill = killPattern.test(content);
|
|
105905
|
+
if (!hasKill) {
|
|
105906
|
+
findings.push({
|
|
105907
|
+
code: "scene_layer_missing_visibility_kill",
|
|
105908
|
+
severity: "warning",
|
|
105909
|
+
elementId: id,
|
|
105910
|
+
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.`,
|
|
105911
|
+
fixHint: `Add \`tl.set("#${id}", { visibility: "hidden" }, <exit-end-time>)\` after the scene's exit tweens.`
|
|
105912
|
+
});
|
|
105913
|
+
}
|
|
105914
|
+
}
|
|
105915
|
+
}
|
|
105916
|
+
return findings;
|
|
105827
105917
|
}
|
|
105828
105918
|
];
|
|
105829
105919
|
|
|
@@ -105931,6 +106021,80 @@ var captionRules = [
|
|
|
105931
106021
|
}
|
|
105932
106022
|
}
|
|
105933
106023
|
return findings;
|
|
106024
|
+
},
|
|
106025
|
+
// caption_overflow_clips_scaled_words
|
|
106026
|
+
({ styles, scripts }) => {
|
|
106027
|
+
const findings = [];
|
|
106028
|
+
const hasScaledWords = scripts.some(
|
|
106029
|
+
(s) => /scale\s*:\s*1\.[2-9]/.test(s.content) && /caption|word|cg-/.test(s.content)
|
|
106030
|
+
);
|
|
106031
|
+
if (!hasScaledWords) return findings;
|
|
106032
|
+
for (const style of styles) {
|
|
106033
|
+
const captionBlocks = style.content.matchAll(
|
|
106034
|
+
/(\.caption[-_]?(?:group|container)|#caption[-_]?(?:layer|container))\s*\{([^}]+)\}/gi
|
|
106035
|
+
);
|
|
106036
|
+
for (const [, selector, body] of captionBlocks) {
|
|
106037
|
+
if (!body) continue;
|
|
106038
|
+
if (/overflow\s*:\s*hidden/i.test(body)) {
|
|
106039
|
+
findings.push({
|
|
106040
|
+
code: "caption_overflow_clips_scaled_words",
|
|
106041
|
+
severity: "warning",
|
|
106042
|
+
selector: (selector ?? "").trim(),
|
|
106043
|
+
message: `"${(selector ?? "").trim()}" has overflow: hidden but GSAP scales caption words above 1.0x. Scaled emphasis words and their glow effects will be clipped.`,
|
|
106044
|
+
fixHint: "Use overflow: visible on caption containers. Rely on fitTextFontSize with reduced maxWidth to prevent overflow instead."
|
|
106045
|
+
});
|
|
106046
|
+
}
|
|
106047
|
+
}
|
|
106048
|
+
}
|
|
106049
|
+
return findings;
|
|
106050
|
+
},
|
|
106051
|
+
// caption_textshadow_on_group_container
|
|
106052
|
+
({ scripts, styles }) => {
|
|
106053
|
+
const findings = [];
|
|
106054
|
+
const isCaptionFile = styles.some((s) => /\.caption[-_]?(?:group|word)/i.test(s.content));
|
|
106055
|
+
if (!isCaptionFile) return findings;
|
|
106056
|
+
for (const script of scripts) {
|
|
106057
|
+
const groupShadowPattern = /\.to\s*\(\s*(?:div|groupEl|el|captionEl|document\.getElementById\s*\(\s*["']cg-)\s*[^,]*,\s*\{[^}]*textShadow/g;
|
|
106058
|
+
const selectorShadowPattern = /\.to\s*\(\s*["'](?:#cg-\d+|\.caption[-_]?group)["']\s*,\s*\{[^}]*textShadow/g;
|
|
106059
|
+
if (groupShadowPattern.test(script.content) || selectorShadowPattern.test(script.content)) {
|
|
106060
|
+
findings.push({
|
|
106061
|
+
code: "caption_textshadow_on_group_container",
|
|
106062
|
+
severity: "warning",
|
|
106063
|
+
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.",
|
|
106064
|
+
fixHint: "Apply textShadow to individual active word elements instead of the group container. Use scale on the group for bass-reactive pulsing."
|
|
106065
|
+
});
|
|
106066
|
+
}
|
|
106067
|
+
}
|
|
106068
|
+
return findings;
|
|
106069
|
+
},
|
|
106070
|
+
// caption_fittext_scale_mismatch
|
|
106071
|
+
({ scripts }) => {
|
|
106072
|
+
const findings = [];
|
|
106073
|
+
for (const script of scripts) {
|
|
106074
|
+
const content = script.content;
|
|
106075
|
+
const fitTextMatch = content.match(/fitTextFontSize\s*\([^)]*maxWidth\s*:\s*(\d+)/);
|
|
106076
|
+
if (!fitTextMatch) continue;
|
|
106077
|
+
const maxWidth = parseInt(fitTextMatch[1] ?? "0", 10);
|
|
106078
|
+
if (!maxWidth) continue;
|
|
106079
|
+
const scaleMatches = [...content.matchAll(/scale\s*:\s*(1\.\d+)/g)];
|
|
106080
|
+
const captionContext = /caption|word|cg-|karaoke/i.test(content);
|
|
106081
|
+
if (!captionContext || scaleMatches.length === 0) continue;
|
|
106082
|
+
let maxScale = 1;
|
|
106083
|
+
for (const m of scaleMatches) {
|
|
106084
|
+
const val = parseFloat(m[1] ?? "1");
|
|
106085
|
+
if (val > maxScale) maxScale = val;
|
|
106086
|
+
}
|
|
106087
|
+
const effectiveWidth = maxWidth * maxScale;
|
|
106088
|
+
if (effectiveWidth > 1760) {
|
|
106089
|
+
findings.push({
|
|
106090
|
+
code: "caption_fittext_scale_mismatch",
|
|
106091
|
+
severity: "warning",
|
|
106092
|
+
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).`,
|
|
106093
|
+
fixHint: `Reduce maxWidth to ${Math.floor(1700 / maxScale)}px to leave headroom for scaled emphasis words.`
|
|
106094
|
+
});
|
|
106095
|
+
}
|
|
106096
|
+
}
|
|
106097
|
+
return findings;
|
|
105934
106098
|
}
|
|
105935
106099
|
];
|
|
105936
106100
|
|
|
@@ -106510,7 +106674,8 @@ async function initializeSession(session) {
|
|
|
106510
106674
|
const type = msg.type();
|
|
106511
106675
|
const text = msg.text();
|
|
106512
106676
|
const isFontLoadError = type === "error" && text.startsWith("Failed to load resource") && /fonts\.googleapis|fonts\.gstatic|\.woff2?(\b|$)/i.test(text);
|
|
106513
|
-
const
|
|
106677
|
+
const isResourceLoadError = type === "error" && text.startsWith("Failed to load resource") && !isFontLoadError;
|
|
106678
|
+
const prefix = isResourceLoadError ? "[non-blocking]" : type === "error" ? "[Browser:ERROR]" : type === "warn" ? "[Browser:WARN]" : "[Browser]";
|
|
106514
106679
|
if (!isFontLoadError) {
|
|
106515
106680
|
console.log(`${prefix} ${text}`);
|
|
106516
106681
|
}
|
|
@@ -111767,6 +111932,23 @@ function buildFontFaceCss(requestedFamilies) {
|
|
|
111767
111932
|
unresolved: unresolved.sort()
|
|
111768
111933
|
};
|
|
111769
111934
|
}
|
|
111935
|
+
function warnUnresolvedFonts(unresolved) {
|
|
111936
|
+
const mapped = Object.entries(FONT_ALIASES).reduce((acc, [alias, canonical]) => {
|
|
111937
|
+
const display = alias === canonical ? alias : `${alias} \u2192 ${canonical}`;
|
|
111938
|
+
if (!acc.includes(display)) acc.push(display);
|
|
111939
|
+
return acc;
|
|
111940
|
+
}, []).sort();
|
|
111941
|
+
console.warn(
|
|
111942
|
+
`[Compiler] No deterministic font mapping for: ${unresolved.join(", ")}
|
|
111943
|
+
Mapped fonts: ${mapped.join(", ")}
|
|
111944
|
+
To fix, pick one:
|
|
111945
|
+
1. Use a mapped font name instead (see list above)
|
|
111946
|
+
2. Add a @font-face block in your HTML with a local or hosted font file
|
|
111947
|
+
3. Install the font locally on the render machine (Docker: add to Dockerfile)
|
|
111948
|
+
4. Add an alias to FONT_ALIASES in deterministicFonts.ts (for contributors)
|
|
111949
|
+
Docs: https://hyperframes.heygen.com/docs/fonts`
|
|
111950
|
+
);
|
|
111951
|
+
}
|
|
111770
111952
|
function injectDeterministicFontFaces(html) {
|
|
111771
111953
|
const existingFaces = extractExistingFontFaces(html);
|
|
111772
111954
|
const requestedFamilies = extractRequestedFontFamilies(html);
|
|
@@ -111782,7 +111964,7 @@ function injectDeterministicFontFaces(html) {
|
|
|
111782
111964
|
const { css, unresolved } = buildFontFaceCss(pendingFamilies);
|
|
111783
111965
|
if (!css) {
|
|
111784
111966
|
if (unresolved.length > 0) {
|
|
111785
|
-
|
|
111967
|
+
warnUnresolvedFonts(unresolved);
|
|
111786
111968
|
}
|
|
111787
111969
|
return html;
|
|
111788
111970
|
}
|
|
@@ -111799,7 +111981,7 @@ function injectDeterministicFontFaces(html) {
|
|
|
111799
111981
|
`[Compiler] Injected deterministic @font-face rules for ${pendingFamilies.size - unresolved.length} requested font families`
|
|
111800
111982
|
);
|
|
111801
111983
|
if (unresolved.length > 0) {
|
|
111802
|
-
|
|
111984
|
+
warnUnresolvedFonts(unresolved);
|
|
111803
111985
|
}
|
|
111804
111986
|
return document3.toString();
|
|
111805
111987
|
}
|