fix(skills): stage a preset's own fonts/ folder in every build-frame.mjs - #3978
Open
miga-heygen wants to merge 1 commit into
Open
miga-heygen wants to merge 1 commit into
miga-heygen wants to merge 1 commit into
Conversation
miga-heygen
force-pushed
the
fix/skills-stage-preset-fonts
branch
from
September 16, 2026 03:45
88ceace to
186f713
Compare
The code-editorial preset ships licensed WOFF2 files and its FRAME.md promises build-frame.mjs stages them into assets/fonts/. Only the pr-to-video copy did, with the six code-editorial filenames hard-coded and a hard exit for any other preset that ships a fonts/ folder; the product-launch-video and faceless-explainer copies never read the folder at all, so a frame worker following the font_family_without_font_face rule found no file behind the preset's families and fell back to a system stack. Make preset font staging directory-driven and shared by all three workflows: each font file is assigned to the longest family the remixed frame.md still names, weight and style are read from the filename under the same naming contract captions.mjs parses, faces the brand remix replaced are skipped, faces the captured brand already staged are not staged twice, and OFL/LICENSE texts travel with any staged face. Both sources now emit one @font-face block in frame.md. The filename → face helpers move out of the brand-font block into a new lib/font-faces.mjs (byte-identical across the three skills, guarded by a test) so the two staging paths cannot drift. Review corrections folded in: the three build-frame.mjs copies are formatted with the repo-pinned oxfmt; captions.mjs imports the weight/style/format/family-key helpers from lib/font-faces.mjs instead of carrying a hand copy (the longest-family-first ranking all three sites rely on is one exported rankFontFamilies), and its test checks the import plus the negative "never assert font-style:normal" guard against the library; an overlapping-family test ("Inter" + "Inter Tight") pins the longest-family assignment; the skipped-file message reads "matching no family frame.md names" since a filename may never have named one. Robustness corrections: stagePresetFonts only considers plain files (statSync().isFile() in a try/catch), so a subdirectory or dangling symlink named like a font neither crashes copyFileSync with EISDIR nor stat with ENOENT; dedup is keyed on the face (family, numeric weight, style) rather than the staged filename, so a word-named brand file and a numeric-named preset file for the same face yield one @font-face rule with the brand's bytes; a preset face's staged bytes are refreshed from the preset on every run (a skills update may ship re-subsetted files), skipping only a self-copy, while a capture file never overwrites a file already at its staged path, so a face hand-placed in assets/fonts/ keeps winning over the download as it did before; and files that follow the staged-name contract (assets/fonts/ and a preset's fonts/) are matched by family-key prefix on the stem while only loosely named capture files get the single-family shortcut and substring match, so a rerun after brand tokens arrive cannot relabel preset faces already in assets/fonts/ as the brand font — not even a brand named "Garamond" against EBGaramond-*.woff2. Refs #3583 Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
miga-heygen
force-pushed
the
fix/skills-stage-preset-fonts
branch
from
September 16, 2026 04:03
186f713 to
9f30a6d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Every workflow's
build-frame.mjs(faceless-explainer, pr-to-video, product-launch-video) now stages a frame preset's ownfonts/folder intoassets/fonts/and appends one@font-faceblock toframe.md, covering both font sources (captured brand files + preset files). The filename → face helpers move into a sharedscripts/lib/font-faces.mjs, byte-identical across the three skills and consumed by bothbuild-frame.mjsandcaptions.mjs.Refs #3583 (the media-use font-resolver half of that issue stays open).
Supersedes #3950 — carries srikarsunchu's change with signed commits so it can merge under the repo's signed-commit ruleset, plus: repo-oxfmt formatting, captions.mjs now imports the shared font-faces library instead of a hand copy, overlapping-family test, isFile guard.
Why
code-editorialships licensed WOFF2 files and itsFRAME.mdpromises they are staged. Only the pr-to-video copy did so, with the six filenames hard-coded and a hard exit for any other preset shipping afonts/folder; the other two copies never read the folder. A frame worker followingfont_family_without_font_facethen found no file behind the preset's families and fell back to a system stack.How
lib/font-faces.mjs(new, ×3 identical):FONT_FORMAT,normFontName,fontExtOf,fontWeightInfo,fontStyleOf,fontFamiliesNamed,rankFontFamilies(longest family key first — the one ranking the brand block,stagePresetFontsandcaptions.mjsall use),familyOfFaceFile,stageFontFile,stagePresetFonts.stagePresetFontsis directory-driven: each plain file (statSync(...).isFile()in a try/catch, so a subdirectory or dangling symlink named like a font cannot reachcopyFileSyncand throwEISDIR/ENOENT) is assigned to the longest family the remixedframe.mdstill names whose key its stem starts with (familyOfFaceFile; a brand renamed to "Garamond" does not claimEBGaramond-400.woff2); files matching no named family are reported as skipped ("matching no family frame.md names"); faces the brand already staged are not staged twice (stagedFacesis shared across both sources and keyed on family + numeric weight + style, soInter-Regular.ttffrom the capture andInter-400.woff2from the preset are one face — first source wins); staged bytes are refreshed from the source only for the preset'sfonts/folder (stageFontFile({ refresh: true }); a skills update may ship re-subsetted files), skipping only a self-copy, while a capture file never overwrites a file already at its staged path, so a hand-placed face inassets/fonts/keeps winning over the download;OFL-*/LICENSE*texts travel with any staged face.build-frame.mjs: the brand block callsstageFontFile; the preset block callsstagePresetFonts; one## Font loading (auto-generated)section lists both sources. The brand scan's "single family owns every file" shortcut now applies only tocapture/assets/fonts(hash-named downloads); files already inassets/fonts/follow the staged-name contract and are matched by family-key prefix, because a previous run may have put the preset's faces there and a rerun after brand tokens arrive must not relabel EB Garamond's bytes as the brand font. pr-to-video drops its hard-coded six-file list and thedie()on a missing file — a preset folder is now read as-is, so a missing file simply is not staged (this is the intended change: the hard exit fired for any other preset shipping afonts/folder).captions.mjs(×3 identical):brandFontFacesimports weight/style/format/key helpers from./lib/font-faces.mjs; the privateweightOf/styleOf/fmtOf/normcopies are deleted. Behaviour unchanged (the oldfmtOf"opentype" fallback was unreachable behind the extension gate).code-editorial/FRAME.mdnotes that families the brand remix replaced are skipped.bunx oxfmt --checkpasses on all changed.mjs);skills-manifest.jsonregenerated.Tests
build-frame-fonts.test.mjs(new, ×3; tmp dirs removed intest.after): staging by family/weight/style from a fake preset; remix-replaced family skipped; brand-staged face not re-staged from the preset; preset withoutfonts/stages nothing; shippedcode-editorialstages six faces + three OFL texts;fontFamiliesNamed,fontWeightInfo,stageFontFileunits; overlapping families "Inter" + "Inter Tight" →InterTight-700.woff2lands on "Inter Tight",Inter-400.woff2on "Inter" (fails with the length sort reversed); directory entry / dangling symlink named like a font is neither staged nor a crash (fails with theisFileguard removed,EISDIR; fails with the try/catch removed,ENOENT); word-vs-numeric dedup across sources (OtherMono-Bold.woff2captured +OtherMono-700.woff2preset → one file, one rule; fails with the dedup keyed on the weight token); stale bytes refreshed bystageFontFilewithrefresh: trueand left alone without it (fails with the flag ignored either way); capture keeps a staged file end-to-end (brand-grotesk-400.woff2captured vs a pre-existingBrandGrotesk-400.woff2; fails with the capture source refreshing); preset refresh end-to-end on a rerun after the staged bytes went stale (fails withstagePresetFontsnot passing the flag); hash-named capture file19cfc7226ec3afaa-s.woff2staged as the single brand family and two brand families told apart by substring on CDN-style names (both fail with the capture rule replaced by the staged-name prefix rule); rerun after brand tokens does not relabel staged preset faces (brand "Sans" vs stagedAcmeSans-*; fails with the shortcut applied toassets/fontsor with substring matching); substring family ("Tight" owns neitherInter-400norInterTight-700); font-faces.mjs byte-identity across the three skills.captions.test.mjs(×3): style-token check now targetslib/font-faces.mjs, restores the negative "must not assertfont-style:normal" check, and assertscaptions.mjsimports its helpers from the library (replacing the import with a stub fails the round-trip tests).env -u NODE_ENV node --test "skills/**/*.test.mjs": 684/687 pass; the single failure (media-use/scripts/resolve.test.mjs) is a missing@hyperframes/parsers/distbuild in a fresh clone and fails identically onmain.bunx tsx scripts/lint-skills.tsclean;bunx oxlint0 warnings on the 15 changed.mjs;bun packages/cli/scripts/gen-skills-manifest.ts --checkin sync.— Miga