mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
fix: address QA report P0-P2 issues for 10/10 agent experience (#208)
## Summary
Addresses all 8 issues from the QA report to improve agent and user experience.
### P0 — Must Fix
- **Blank template broken captions**: Removed `compositions/captions.html` and its reference from the blank template. Every agent (10/10) hit 404 errors during render.
- **Inner-wrapper example**: Added a clear structural comment in the blank template showing the correct `class="clip"` + inner wrapper pattern.
- **Sub-composition introspection**: `hyperframes compositions` now reads external HTML files referenced via `data-composition-src` and shows their real duration/element count instead of `0.0s / 0 elements`.
### P1 — Fix Soon
- **Font mapping warnings**: Now lists all mapped fonts, suggests alternatives (use a mapped font, add @font-face, install locally), and links to docs.
- **Browser 404s**: Non-font "Failed to load resource" 404s now prefixed with `[non-blocking]` instead of `[Browser:ERROR]`.
- **Render concurrency**: Default workers increased from `cores/2` (max 4) to `cores*3/4` (max 6). Added `--concurrency` alias.
### P2 — Nice to Have
- **Transform conflict fix suggestion**: `gsap_css_transform_conflict` now suggests exact GSAP property replacements (e.g., `xPercent: -50, yPercent: -50`).
- **Upgrade --yes**: Now actually runs the install instead of just printing the command.
## Before / After
### Font mapping warning
**Before:**
```
[Compiler] No deterministic font mapping for: DM Sans
```
**After:**
```
[Compiler] No deterministic font mapping for: DM Sans
Mapped fonts: arial → inter, courier → jetbrains-mono, ...
To fix, pick one:
1. Use a mapped font name instead (see list above)
2. Add a @font-face block in your HTML with a local or hosted font file
3. Install the font locally on the render machine (Docker: add to Dockerfile)
4. Add an alias to FONT_ALIASES in deterministicFonts.ts (for contributors)
```
### Browser 404s during render
**Before:** `[Browser:ERROR] Failed to load resource: the server responded with a status of 404`
**After:** `[non-blocking] Failed to load resource: the server responded with a status of 404`
### Transform conflict lint
**Before:** `Fix: Remove the transform from CSS and use tl.fromTo...`
**After:** `Fix: Remove transform: translate(-50%, -50%) from CSS and replace with GSAP properties: xPercent: -50, yPercent: -50`
### Compositions command
**Before:**
```
overlay 0.0s 1920×1080 0 elements
```
**After:**
```
overlay 8.0s 1920×1080 2 elements ← compositions/overlay.html
```
## Test plan
- [x] All 422 core tests pass
- [x] TypeScript compiles cleanly (all 4 packages)
- [x] Full monorepo build succeeds
- [x] `hyperframes init --template blank` ships without broken captions reference
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { defineCommand } from "citty";
|
||||
import type { Example } from "./_examples.js";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { resolve, dirname } from "node:path";
|
||||
|
||||
export const examples: Example[] = [
|
||||
["List compositions in the current project", "hyperframes compositions"],
|
||||
@@ -17,9 +18,10 @@ interface CompositionInfo {
|
||||
width: number;
|
||||
height: number;
|
||||
elementCount: number;
|
||||
source?: string;
|
||||
}
|
||||
|
||||
function parseCompositions(html: string): CompositionInfo[] {
|
||||
function parseCompositions(html: string, baseDir: string): CompositionInfo[] {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, "text/html");
|
||||
|
||||
@@ -30,6 +32,18 @@ function parseCompositions(html: string): CompositionInfo[] {
|
||||
const id = div.getAttribute("data-composition-id") ?? "unknown";
|
||||
const width = parseInt(div.getAttribute("data-width") ?? "1920", 10);
|
||||
const height = parseInt(div.getAttribute("data-height") ?? "1080", 10);
|
||||
const compositionSrc = div.getAttribute("data-composition-src");
|
||||
|
||||
// If this references an external sub-composition, parse that file
|
||||
if (compositionSrc) {
|
||||
const subPath = resolve(baseDir, compositionSrc);
|
||||
if (existsSync(subPath)) {
|
||||
const subHtml = readFileSync(subPath, "utf-8");
|
||||
const subInfo = parseSubComposition(subHtml, id, width, height);
|
||||
compositions.push({ ...subInfo, source: compositionSrc });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const timedChildren = div.querySelectorAll("[data-start]");
|
||||
let maxEnd = 0;
|
||||
@@ -67,6 +81,62 @@ function parseCompositions(html: string): CompositionInfo[] {
|
||||
return compositions;
|
||||
}
|
||||
|
||||
function parseSubComposition(
|
||||
html: string,
|
||||
fallbackId: string,
|
||||
fallbackWidth: number,
|
||||
fallbackHeight: number,
|
||||
): CompositionInfo {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, "text/html");
|
||||
|
||||
// Sub-compositions may use <template> wrappers or direct divs
|
||||
const compDiv =
|
||||
doc.querySelector("[data-composition-id]") ??
|
||||
doc.querySelector("template [data-composition-id]");
|
||||
|
||||
const id = compDiv?.getAttribute("data-composition-id") ?? fallbackId;
|
||||
const width = parseInt(compDiv?.getAttribute("data-width") ?? String(fallbackWidth), 10);
|
||||
const height = parseInt(compDiv?.getAttribute("data-height") ?? String(fallbackHeight), 10);
|
||||
|
||||
// Count timed elements inside the sub-composition
|
||||
const searchRoot = compDiv ?? doc;
|
||||
const timedChildren = searchRoot.querySelectorAll("[data-start], .clip, .caption-group");
|
||||
let elementCount = timedChildren.length;
|
||||
|
||||
// Parse duration from the composition's own data-duration attribute
|
||||
let duration = 0;
|
||||
const durationAttr = compDiv?.getAttribute("data-duration");
|
||||
if (durationAttr && !durationAttr.startsWith("__")) {
|
||||
duration = parseFloat(durationAttr) || 0;
|
||||
}
|
||||
|
||||
// Also check timed children for max end time
|
||||
if (compDiv) {
|
||||
const timedEls = compDiv.querySelectorAll("[data-start]");
|
||||
timedEls.forEach((el) => {
|
||||
elementCount = Math.max(elementCount, timedEls.length);
|
||||
const start = parseFloat(el.getAttribute("data-start") ?? "0");
|
||||
const endAttr = el.getAttribute("data-end");
|
||||
const durAttr = el.getAttribute("data-duration");
|
||||
|
||||
let end: number;
|
||||
if (endAttr) {
|
||||
end = parseFloat(endAttr);
|
||||
} else if (durAttr) {
|
||||
end = start + parseFloat(durAttr);
|
||||
} else {
|
||||
end = start + 5;
|
||||
}
|
||||
if (end > duration) {
|
||||
duration = end;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return { id, duration, width, height, elementCount };
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
meta: { name: "compositions", description: "List all compositions in a project" },
|
||||
args: {
|
||||
@@ -78,7 +148,7 @@ export default defineCommand({
|
||||
const html = readFileSync(project.indexPath, "utf-8");
|
||||
|
||||
ensureDOMParser();
|
||||
const compositions = parseCompositions(html);
|
||||
const compositions = parseCompositions(html, dirname(project.indexPath));
|
||||
|
||||
if (compositions.length === 0) {
|
||||
console.log(`${c.success("◇")} ${c.accent(project.name)} — no compositions found`);
|
||||
@@ -107,8 +177,9 @@ export default defineCommand({
|
||||
const elements = c.dim(
|
||||
`${comp.elementCount} ${comp.elementCount === 1 ? "element" : "elements"}`,
|
||||
);
|
||||
const source = comp.source ? c.dim(` ← ${comp.source}`) : "";
|
||||
|
||||
console.log(` ${id} ${duration} ${resolution} ${elements}`);
|
||||
console.log(` ${id} ${duration} ${resolution} ${elements}${source}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ export const examples: Example[] = [
|
||||
["Render transparent WebM overlay", "hyperframes render --format webm --output overlay.webm"],
|
||||
["High quality at 60fps", "hyperframes render --fps 60 --quality high --output hd.mp4"],
|
||||
["Deterministic render via Docker", "hyperframes render --docker --output deterministic.mp4"],
|
||||
["Parallel rendering with 4 workers", "hyperframes render --workers 4 --output fast.mp4"],
|
||||
["Parallel rendering with 6 workers", "hyperframes render --workers 6 --output fast.mp4"],
|
||||
];
|
||||
import { cpus, freemem } from "node:os";
|
||||
import { resolve, dirname, join } from "node:path";
|
||||
@@ -28,9 +28,9 @@ const VALID_FORMAT = new Set(["mp4", "webm"]);
|
||||
|
||||
const CPU_CORE_COUNT = cpus().length;
|
||||
|
||||
/** Half of CPU cores, capped at 4. Each worker spawns a Chrome process (~256 MB). */
|
||||
/** 3/4 of CPU cores, capped at 8. Each worker spawns a Chrome process (~256 MB). */
|
||||
function defaultWorkerCount(): number {
|
||||
return Math.max(1, Math.min(Math.floor(CPU_CORE_COUNT / 2), 4));
|
||||
return Math.max(1, Math.min(Math.floor((CPU_CORE_COUNT * 3) / 4), 8));
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
@@ -66,8 +66,8 @@ export default defineCommand({
|
||||
workers: {
|
||||
type: "string",
|
||||
description:
|
||||
"Parallel render workers (1-8 or 'auto'). Default: half your CPU cores, max 4. " +
|
||||
"Each worker launches a separate Chrome process.",
|
||||
"Parallel render workers (number or 'auto'). Default: auto. " +
|
||||
"Each worker launches a separate Chrome process (~256 MB RAM).",
|
||||
},
|
||||
docker: {
|
||||
type: "boolean",
|
||||
@@ -123,8 +123,8 @@ export default defineCommand({
|
||||
let workers: number | undefined;
|
||||
if (args.workers != null && args.workers !== "auto") {
|
||||
const parsed = parseInt(args.workers, 10);
|
||||
if (isNaN(parsed) || parsed < 1 || parsed > 8) {
|
||||
errorBox("Invalid workers", `Got "${args.workers}". Must be 1-8 or "auto".`);
|
||||
if (isNaN(parsed) || parsed < 1) {
|
||||
errorBox("Invalid workers", `Got "${args.workers}". Must be a positive number or "auto".`);
|
||||
process.exit(1);
|
||||
}
|
||||
workers = parsed;
|
||||
@@ -155,7 +155,7 @@ export default defineCommand({
|
||||
const workerLabel =
|
||||
args.workers != null
|
||||
? `${workerCount} workers`
|
||||
: `${workerCount} workers (auto \u2014 half of ${CPU_CORE_COUNT} cores)`;
|
||||
: `${workerCount} workers (auto — ${CPU_CORE_COUNT} cores detected)`;
|
||||
console.log("");
|
||||
console.log(
|
||||
c.accent("\u25C6") +
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { defineCommand } from "citty";
|
||||
import type { Example } from "./_examples.js";
|
||||
import * as clack from "@clack/prompts";
|
||||
import { execSync } from "node:child_process";
|
||||
import { c } from "../ui/colors.js";
|
||||
|
||||
export const examples: Example[] = [
|
||||
["Check for updates interactively", "hyperframes upgrade"],
|
||||
["Check for updates without prompting", "hyperframes upgrade --check"],
|
||||
["Show upgrade commands directly", "hyperframes upgrade --yes"],
|
||||
["Upgrade non-interactively", "hyperframes upgrade --yes"],
|
||||
];
|
||||
import { VERSION } from "../version.js";
|
||||
import { checkForUpdate, withMeta } from "../utils/updateCheck.js";
|
||||
@@ -66,12 +67,26 @@ export default defineCommand({
|
||||
}
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log(` ${c.accent("npm install -g hyperframes@" + result.latest)}`);
|
||||
console.log(` ${c.dim("or")}`);
|
||||
console.log(` ${c.accent("npx hyperframes@" + result.latest + " --version")}`);
|
||||
console.log();
|
||||
|
||||
clack.outro(c.success("Run one of the commands above to upgrade."));
|
||||
const installCmd = `npm install -g hyperframes@${result.latest}`;
|
||||
if (autoYes) {
|
||||
console.log();
|
||||
console.log(` ${c.dim("Running:")} ${c.accent(installCmd)}`);
|
||||
console.log();
|
||||
try {
|
||||
execSync(installCmd, { stdio: "inherit" });
|
||||
clack.outro(c.success(`Upgraded to v${result.latest}`));
|
||||
} catch {
|
||||
clack.outro(c.dim("Install failed. Try running manually:"));
|
||||
console.log(` ${c.accent(installCmd)}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
} else {
|
||||
console.log();
|
||||
console.log(` ${c.accent(installCmd)}`);
|
||||
console.log(` ${c.dim("or")}`);
|
||||
console.log(` ${c.accent("npx hyperframes@" + result.latest + " --version")}`);
|
||||
console.log();
|
||||
clack.outro(c.success("Run one of the commands above to upgrade."));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
<template id="captions-template">
|
||||
<div
|
||||
data-composition-id="captions"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-duration="__VIDEO_DURATION__"
|
||||
>
|
||||
<div id="captions-container"></div>
|
||||
|
||||
<style>
|
||||
[data-composition-id="captions"] {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[data-composition-id="captions"] #captions-container {
|
||||
position: absolute;
|
||||
bottom: 100px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.caption-group {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
font-family: "Inter", sans-serif;
|
||||
font-size: 48px;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
text-shadow:
|
||||
0 2px 8px rgba(0, 0, 0, 0.8),
|
||||
0 0 2px rgba(0, 0, 0, 0.9);
|
||||
white-space: nowrap;
|
||||
max-width: 1600px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
const script = [];
|
||||
|
||||
if (script.length === 0) {
|
||||
window.__timelines["captions"] = tl;
|
||||
return;
|
||||
}
|
||||
|
||||
const container = document.getElementById("captions-container");
|
||||
|
||||
// Group words into lines (max 5 words per line)
|
||||
const lines = [];
|
||||
for (let i = 0; i < script.length; i += 5) {
|
||||
const lineWords = script.slice(i, i + 5);
|
||||
lines.push({
|
||||
text: lineWords.map((w) => w.text).join(" "),
|
||||
start: lineWords[0].start,
|
||||
end: lineWords[lineWords.length - 1].end,
|
||||
});
|
||||
}
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
const el = document.createElement("div");
|
||||
el.className = "caption-group";
|
||||
el.textContent = line.text;
|
||||
container.appendChild(el);
|
||||
|
||||
tl.fromTo(
|
||||
el,
|
||||
{ opacity: 0, y: 20 },
|
||||
{ opacity: 1, y: 0, duration: 0.3, ease: "power3.out" },
|
||||
line.start,
|
||||
);
|
||||
|
||||
const hideTime =
|
||||
index < lines.length - 1 ? Math.min(line.end, lines[index + 1].start) : line.end;
|
||||
|
||||
tl.to(el, { opacity: 0, y: -10, duration: 0.25, ease: "power2.in" }, hideTime - 0.25);
|
||||
tl.set(el, { opacity: 0, visibility: "hidden" }, hideTime);
|
||||
});
|
||||
|
||||
window.__timelines["captions"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</template>
|
||||
@@ -48,21 +48,20 @@
|
||||
data-volume="1"
|
||||
></audio>
|
||||
|
||||
<div
|
||||
id="captions-comp"
|
||||
data-composition-id="captions"
|
||||
data-composition-src="compositions/captions.html"
|
||||
data-start="0"
|
||||
data-duration="__VIDEO_DURATION__"
|
||||
data-track-index="3"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
<!--
|
||||
ANIMATION PATTERN: The clip div controls timing/visibility.
|
||||
Always put your content in a CHILD element and animate THAT.
|
||||
|
||||
<div class="clip" ...> ← timing only, don't animate this
|
||||
<div id="my-title">...</div> ← animate this with GSAP
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
// tl.from("#my-title", { opacity: 0, y: -50, duration: 1 }, 0);
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -184,6 +184,44 @@ function getSingleClassSelector(selector: string): string | null {
|
||||
return match?.groups?.name || null;
|
||||
}
|
||||
|
||||
function cssTransformToGsapProps(cssTransform: string): string | null {
|
||||
const parts: string[] = [];
|
||||
|
||||
// translate(-50%, -50%) or translate(X, Y)
|
||||
const translateMatch = cssTransform.match(
|
||||
/translate\(\s*(-?[\d.]+)(%|px)?\s*,\s*(-?[\d.]+)(%|px)?\s*\)/,
|
||||
);
|
||||
if (translateMatch) {
|
||||
const [, xVal, xUnit, yVal, yUnit] = translateMatch;
|
||||
if (xUnit === "%") parts.push(`xPercent: ${xVal}`);
|
||||
else parts.push(`x: ${xVal}`);
|
||||
if (yUnit === "%") parts.push(`yPercent: ${yVal}`);
|
||||
else parts.push(`y: ${yVal}`);
|
||||
}
|
||||
|
||||
// translateX(-50%) or translateX(px)
|
||||
const txMatch = cssTransform.match(/translateX\(\s*(-?[\d.]+)(%|px)?\s*\)/);
|
||||
if (txMatch) {
|
||||
const [, val, unit] = txMatch;
|
||||
parts.push(unit === "%" ? `xPercent: ${val}` : `x: ${val}`);
|
||||
}
|
||||
|
||||
// translateY(-50%) or translateY(px)
|
||||
const tyMatch = cssTransform.match(/translateY\(\s*(-?[\d.]+)(%|px)?\s*\)/);
|
||||
if (tyMatch) {
|
||||
const [, val, unit] = tyMatch;
|
||||
parts.push(unit === "%" ? `yPercent: ${val}` : `y: ${val}`);
|
||||
}
|
||||
|
||||
// scale(N)
|
||||
const scaleMatch = cssTransform.match(/scale\(\s*([\d.]+)\s*\)/);
|
||||
if (scaleMatch) {
|
||||
parts.push(`scale: ${scaleMatch[1]}`);
|
||||
}
|
||||
|
||||
return parts.length > 0 ? parts.join(", ") : null;
|
||||
}
|
||||
|
||||
// ── GSAP rules ─────────────────────────────────────────────────────────────
|
||||
|
||||
export const gsapRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [
|
||||
@@ -334,6 +372,14 @@ export const gsapRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [
|
||||
|
||||
for (const [sel, { cssTransform, props, raw }] of conflicts) {
|
||||
const propList = [...props].join("/");
|
||||
const gsapEquivalent = cssTransformToGsapProps(cssTransform);
|
||||
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.`;
|
||||
findings.push({
|
||||
code: "gsap_css_transform_conflict",
|
||||
severity: "warning",
|
||||
@@ -342,10 +388,7 @@ export const gsapRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [
|
||||
`${propList}. GSAP will overwrite the full CSS transform, discarding any ` +
|
||||
`translateX(-50%) centering or CSS scale value.`,
|
||||
selector: sel,
|
||||
fixHint:
|
||||
`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.`,
|
||||
fixHint,
|
||||
snippet: truncateSnippet(raw),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -150,17 +150,26 @@ export async function initializeSession(session: CaptureSession): Promise<void>
|
||||
const type = msg.type();
|
||||
const text = msg.text();
|
||||
|
||||
// Suppress font-loading 404s only. These are expected when deterministic
|
||||
// Suppress font-loading 404s entirely. These are expected when deterministic
|
||||
// font injection replaces Google Fonts @import URLs with embedded base64.
|
||||
// Narrowed to font CDN domains and font file extensions to avoid hiding
|
||||
// real asset failures (images, scripts, videos).
|
||||
const isFontLoadError =
|
||||
type === "error" &&
|
||||
text.startsWith("Failed to load resource") &&
|
||||
/fonts\.googleapis|fonts\.gstatic|\.woff2?(\b|$)/i.test(text);
|
||||
|
||||
const prefix =
|
||||
type === "error" ? "[Browser:ERROR]" : type === "warn" ? "[Browser:WARN]" : "[Browser]";
|
||||
// Other "Failed to load resource" 404s are typically non-blocking (e.g.
|
||||
// favicon, sourcemaps, optional assets). Prefix them so users know they
|
||||
// are harmless and don't confuse them with real render errors.
|
||||
const isResourceLoadError =
|
||||
type === "error" && text.startsWith("Failed to load resource") && !isFontLoadError;
|
||||
|
||||
const prefix = isResourceLoadError
|
||||
? "[non-blocking]"
|
||||
: type === "error"
|
||||
? "[Browser:ERROR]"
|
||||
: type === "warn"
|
||||
? "[Browser:WARN]"
|
||||
: "[Browser]";
|
||||
if (!isFontLoadError) {
|
||||
console.log(`${prefix} ${text}`);
|
||||
}
|
||||
|
||||
@@ -243,6 +243,26 @@ function buildFontFaceCss(requestedFamilies: Map<string, string>): {
|
||||
};
|
||||
}
|
||||
|
||||
function warnUnresolvedFonts(unresolved: string[]): void {
|
||||
const mapped = Object.entries(FONT_ALIASES)
|
||||
.reduce<string[]>((acc, [alias, canonical]) => {
|
||||
const display = alias === canonical ? alias : `${alias} → ${canonical}`;
|
||||
if (!acc.includes(display)) acc.push(display);
|
||||
return acc;
|
||||
}, [])
|
||||
.sort();
|
||||
console.warn(
|
||||
`[Compiler] No deterministic font mapping for: ${unresolved.join(", ")}\n` +
|
||||
` Mapped fonts: ${mapped.join(", ")}\n` +
|
||||
` To fix, pick one:\n` +
|
||||
` 1. Use a mapped font name instead (see list above)\n` +
|
||||
` 2. Add a @font-face block in your HTML with a local or hosted font file\n` +
|
||||
` 3. Install the font locally on the render machine (Docker: add to Dockerfile)\n` +
|
||||
` 4. Add an alias to FONT_ALIASES in deterministicFonts.ts (for contributors)\n` +
|
||||
` Docs: https://hyperframes.heygen.com/docs/fonts`,
|
||||
);
|
||||
}
|
||||
|
||||
export function injectDeterministicFontFaces(html: string): string {
|
||||
const existingFaces = extractExistingFontFaces(html);
|
||||
const requestedFamilies = extractRequestedFontFamilies(html);
|
||||
@@ -261,7 +281,7 @@ export function injectDeterministicFontFaces(html: string): string {
|
||||
const { css, unresolved } = buildFontFaceCss(pendingFamilies);
|
||||
if (!css) {
|
||||
if (unresolved.length > 0) {
|
||||
console.warn(`[Compiler] No deterministic font mapping for: ${unresolved.join(", ")}`);
|
||||
warnUnresolvedFonts(unresolved);
|
||||
}
|
||||
return html;
|
||||
}
|
||||
@@ -281,7 +301,7 @@ export function injectDeterministicFontFaces(html: string): string {
|
||||
`[Compiler] Injected deterministic @font-face rules for ${pendingFamilies.size - unresolved.length} requested font families`,
|
||||
);
|
||||
if (unresolved.length > 0) {
|
||||
console.warn(`[Compiler] Unresolved font families left dynamic: ${unresolved.join(", ")}`);
|
||||
warnUnresolvedFonts(unresolved);
|
||||
}
|
||||
|
||||
return document.toString();
|
||||
|
||||
Reference in New Issue
Block a user