mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 14:50:02 +00:00
fix(skills): declare the caption brand font's style axis, not just its weight (#3300)
A font filename encodes more than a weight, but only the weight was ever read out of it, so two faces of one family collapsed onto a single slot. Google Fonts ships Newsreader as Newsreader-Italic-VariableFont_opsz,wght.ttf and Newsreader-VariableFont_opsz,wght.ttf. The italic sorts first, both scored 400, so the italic claimed the family's only 400 slot, the upright was dropped as a duplicate, and the surviving face was declared with no font-style at all. @font-face is deliberately global — the composition CSS scoper exempts it, since a face declaration cannot be scoped — so mounting captions re-pointed the whole document's Newsreader at the italic file and every sibling composition rendered in italics. Faces now carry a font-style descriptor and dedupe on weight AND style. The same fix had to land in build-frame.mjs, which renames captured fonts BEFORE captions.mjs sees them. It dropped the style token while renaming, so an italic file arrived as "Newsreader-Regular.ttf" and was then asserted upright — leaving the global normal slot pointing at italic bytes even once brandFontFaces understood styles. The staged filename is a contract: it must carry every axis that distinguishes one face from another, and the dedup key must be the whole face. Second axis, same misparse: weight parsing matched WORDS only, so a Fontsource capture (inter-latin-500-normal.woff2) scored a whole family 400 and shipped one of its faces. A numeric axis in the filename now wins over the word heuristic, anchored so it is not read out of the middle of a hash-named capture file — a non-digit before it and no alphanumeric after, which keeps both the 4-digit guard and separator-free names like Roboto900.ttf. Tests pin both ends of the contract: a round-trip asserting the names build-frame stages map back to the right weight+style through the real brandFontFaces, plus a source check that no copy reverts to a weight-only name or a hardcoded font-style:normal. captions.mjs also gains a parity pin across the three workflows that ship it. Not addressed: a VariableFont file is still declared at a single font-weight rather than its range, so weights it could interpolate are still synthesized.
This commit is contained in:
@@ -429,8 +429,15 @@ if (brandFonts.length || (brandColors.length && presetColors.length)) {
|
||||
// ── stage brand font files + emit @font-face ──────────────────────────────────
|
||||
// A brand font is rarely a Google font, so renaming the family in frame.md is not enough:
|
||||
// nothing loads the actual face. If the capture downloaded font files, copy them to
|
||||
// assets/fonts/ under CLEAN, weight-named names (so captions.mjs' family-prefix matcher
|
||||
// assets/fonts/ under CLEAN, face-named names (so captions.mjs' family-prefix matcher
|
||||
// finds them too) and append a ready-to-paste, ROOT-RELATIVE @font-face block to frame.md.
|
||||
//
|
||||
// The staged NAME is a contract, not cosmetics: captions.mjs derives each face's weight and
|
||||
// style back out of it. So the name has to carry every axis that distinguishes one face from
|
||||
// another, and the dedup key has to be the whole face. Naming on weight alone made Google's
|
||||
// two-file Newsreader download (upright + italic, both scoring "Regular") collide on one
|
||||
// slot: the italic sorts first, took the name, the upright was never staged, and the block
|
||||
// below then asserted font-style:normal over italic bytes.
|
||||
if (brandFonts.length) {
|
||||
const norm = (s) =>
|
||||
String(s)
|
||||
@@ -440,6 +447,16 @@ if (brandFonts.length) {
|
||||
const FMT = { woff2: "woff2", woff: "woff", ttf: "truetype", otf: "opentype" };
|
||||
const weightInfo = (name) => {
|
||||
const s = name.toLowerCase();
|
||||
// A numeric axis is the font's own answer, so it beats the word heuristic. Fontsource
|
||||
// names every face that way and carries no weight WORD at all, so word-only parsing
|
||||
// scored a whole family "Regular" and staged exactly one of its faces.
|
||||
//
|
||||
// A weight token must not be buried inside a longer run: this reads capture files,
|
||||
// which are commonly hash-named, and "Newsreader-a1b200c3.woff2" is not a 200-weight
|
||||
// face. Hence a non-digit before (which also stops "2100" reading as 100) and no
|
||||
// alphanumeric after. "Roboto900.ttf" still parses.
|
||||
const numeric = /(?:^|[^0-9])([1-9]00)(?![0-9a-z])/.exec(s);
|
||||
if (numeric) return { n: Number(numeric[1]), w: numeric[1] };
|
||||
if (/black|heavy|ultra|extrabold/.test(s)) return { n: 800, w: "ExtraBold" };
|
||||
if (/semibold|demibold/.test(s)) return { n: 600, w: "SemiBold" };
|
||||
if (/bold/.test(s)) return { n: 700, w: "Bold" };
|
||||
@@ -447,6 +464,7 @@ if (brandFonts.length) {
|
||||
if (/light|thin/.test(s)) return { n: 300, w: "Light" };
|
||||
return { n: 400, w: "Regular" };
|
||||
};
|
||||
const styleOf = (name) => (/italic|oblique/i.test(name) ? "italic" : "normal");
|
||||
const fams = [...new Set(brandFonts)];
|
||||
const srcDirs = [
|
||||
join(hyperframesDir, "capture/assets/fonts"),
|
||||
@@ -467,13 +485,14 @@ if (brandFonts.length) {
|
||||
const fam = famOf(f);
|
||||
if (!fam) continue;
|
||||
const { n, w } = weightInfo(f);
|
||||
const clean = `${fam.replace(/[^A-Za-z0-9]/g, "")}-${w}.${extOf(f)}`;
|
||||
const style = styleOf(f);
|
||||
const clean = `${fam.replace(/[^A-Za-z0-9]/g, "")}-${w}${style === "italic" ? "-Italic" : ""}.${extOf(f)}`;
|
||||
if (stagedNames.has(clean)) continue;
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
if (!existsSync(join(outDir, clean))) copyFileSync(join(d, f), join(outDir, clean));
|
||||
stagedNames.add(clean);
|
||||
faces.push(
|
||||
`@font-face{font-family:"${fam}";font-weight:${n};font-style:normal;font-display:block;src:url("assets/fonts/${clean}") format("${FMT[extOf(f)]}");}`,
|
||||
`@font-face{font-family:"${fam}";font-weight:${n};font-style:${style};font-display:block;src:url("assets/fonts/${clean}") format("${FMT[extOf(f)]}");}`,
|
||||
);
|
||||
}
|
||||
if (faces.length) {
|
||||
|
||||
Reference in New Issue
Block a user