Repository navigation
feat(cli): cloud render carries the system fonts a project uses in its zip - #7165
Conversation
There was a problem hiding this comment.
Verdict: REQUEST CHANGES · 3 blocker · 0 important · 0 nits · head 18464205
Three required CI jobs fail on this head because of this diff (two ↓ inline):
- Build ›
verify:packed-manifests:@hyperframes/cli unpacked size 23960655 bytes is over its budget of 23722874 by more than 1%. Either shrink what the new static@hyperframes/core/compilerimport pulls into the CLI bundle, or raisescripts/package-size-budgets.jsonand give the reason in the PR body.
What I checked
I read all 4 files (the body says 5). The fails without the fix: line holds up: with the render.ts call removed, render.fonts.test.ts fails 1/1 (fetchSpy called). Both new suites pass at head. I ran the real resolver: a PingFang SC fixture returns ["PingFang SC"] (3.3 s cold), Montserrat returns [] (0.4 s), and a generic-only fixture returns []. The server runs normalizeSystemFontPrimaryFamilies before injecting and this check doesn't, but that step only prefixes Inter onto generic-primary values, so it can't add a false block. I found no logic blockers. Once CI is green I'll re-review and approve.
| ); | ||
|
|
||
| if (project.kind === "dir") { | ||
| const missing = await unresolvedCloudFonts(project.dir, args.composition); |
There was a problem hiding this comment.
blocker: Typecheck fails here: render.ts(281,50): error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string' (I reproduced it locally with tsc --noEmit). project.dir doesn't narrow to string in this spot. Pass project.dir ?? "." to match resolveProjectInput, or widen the dir parameter.
| consolas: "JetBrains Mono", | ||
| }; | ||
|
|
||
| export function lookAlikeFor(family: string): string | undefined { |
There was a problem hiding this comment.
blocker: The Fallow audit gate fails with fallow/unused-export: nothing outside this module imports lookAlikeFor. Drop the export. The tests already cover it through describeUnresolvedFonts.
There was a problem hiding this comment.
Verdict: REQUEST CHANGES · 1 blocker · 0 important · 0 nits · 18464205 → 34ca2b91: 3 resolved, 0 open, 1 new
The earlier type, export and budget blockers are fixed, and CI is green. The new blocker ↓: the headline case, PingFang SC, can never be embedded, and the person is then told the font isn't on their computer.
What I checked
The fails without the fix: line holds: with the prepareLocalArchive call removed, render.fonts.test.ts fails, and both suites pass at head (6/6). Heads-up: #7166 also edits the CLI budget line (to 25158406), so whichever lands second must keep the larger value.
| return families | ||
| .map((family) => { | ||
| const swap = lookAlikeFor(family); | ||
| return `${family} isn't on this computer, so the cloud can't have it: ${ |
There was a problem hiding this comment.
blocker: This message is false for the fonts this PR is for. embedSystemFonts relies on core's locateSystemFontVariants, which matches a family by a name derived from the file name (toFamilyName). I ran it on real macOS/Windows file names: PingFang.ttc → "Ping Fang", ヒラギノ角ゴシック W3.ttc → itself, SFNS.ttf → "SFNS", msyh.ttc → "msyh", segoeui.ttf → "segoeui". None of them equals pingfang sc, hiragino sans, sf pro, microsoft yahei or segoe ui. Newer macOS also keeps PingFang under /System/Library/AssetsV2/…, which fontDirectories() doesn't scan. So on a Mac that has PingFang SC, the render stops with "PingFang SC isn't on this computer". The body says no real PingFang file was tried, and this is the case that matters.
The smallest fix that makes this mergeable is to say what we know, e.g. HyperFrames couldn't find a font file for ${family} on this computer, so the cloud can't have it: …, and to scope the PR body to fonts whose file name matches their family (e.g. Helvetica Neue, Arial, Georgia, Menlo). Matching by the font's internal name table (or fc-list/CoreText) can follow separately, if Miguel still wants PingFang to upload.
34ca2b9 to
1824673
Compare
…cluding .ttc faces
There was a problem hiding this comment.
Verdict: APPROVE · 34ca2b91 → 18246739: 1 resolved, 0 open, 0 new. The message now says it couldn't find a font file, and the body scopes the PR to files matched by name, with PingFang/Hiragino/SF Pro/YaHei/Segoe as a follow-up. The tests pass (6/6), and CI and Comments are green.
There was a problem hiding this comment.
Verdict: REQUEST CHANGES · 2 blocker · 0 important · 0 nits · 18246739 → 99676881: 0 resolved, 0 open, 2 new
The new name-table reader works on a real collection. I ran it on wqy-zenhei.ttc (3 faces) and each extracted face round-trips through fontTools. Chrome loads the injected face (WenQuanYi Zen Hei Mono:500:loaded). The fails-without-fix claim holds: with the fallback off, 3 tests fail. Core src/fonts passes (278 tests). Two blockers ↓.
Measured
At injectDeterministicFontFaces with system capture on, one real 11 MB CJK face from the .ttc took 31.3 s cold (woff2 compression) and 56 ms warm. The page came out at 6.2 MB.
| const { weight, style } = inferWeightAndStyle(c.path); | ||
| variants.push({ path: c.path, format: c.format, weight, style }); | ||
| } | ||
| if (variants.length === 0) return dedupeVariants(variantsByInnerName(normalized)); |
There was a problem hiding this comment.
blocker: This fallback is in the shared core locator, so it also changes Studio preview and local renders. Contrary to the body ("existing behaviour is unchanged"), they call injectDeterministicFontFaces with system capture on by default (studioServer.ts:571, and compileStage when allowSystemFontCapture is unset). Today a Mac project using PingFang SC resolves nothing there, and the browser draws the installed font natively. After this, every weight of PingFang SC is extracted and embedded. On a real 11 MB CJK .ttc face I measured 31 s cold to compress one weight and 6.2 MB of page per weight, so a 6-weight family is minutes on first open and tens of MB in every preview HTML. Keep the inner-name fallback opt-in, e.g. a byInnerName option set only by the cloud preflight path, or say in the body that local and Studio change too and show it's acceptable.
| return dedupeVariants(variants); | ||
| } | ||
|
|
||
| // A file name rarely equals the family (PingFang.ttc holds "PingFang SC"), so fall back to the names inside. |
There was a problem hiding this comment.
blocker: The Comments check fails: RAISED comment share: packages/core/src/fonts/systemFontLocator.ts is 1.9% (7/367), at the base 1.5% (5/329), with 2 comment lines added (this one and the face docblock at 268). Fold them into names, e.g. collectionFaceOffset, and drop this line.
…old, cut comments
There was a problem hiding this comment.
Verdict: APPROVE · 99676881 → 82b4f0d8: 2 resolved, 0 open, 0 new. Inner-name lookup is now opt-in (systemFontsByInnerName) and set only by the cloud preflight. I checked on a real .ttc: the default lookup returns [], and opting in returns the face. Core src/fonts passes (281 tests), the CLI font tests pass (6/6), and CI and Comments are green.
Ports: heygen-com#7165 (font preflight and system TTC names), heygen-com#7166 (font shorthand var(), weight-suffixed names, linked font sheets), heygen-com#7168 (render lock and --wait, effect-scan/session watchdog without the heygen-com#7104 retry, font_face_local_only lint), heygen-com#6972, heygen-com#7183 (adapted), heygen-com#7184; docs heygen-com#7132, heygen-com#7196; prerequisites heygen-com#6665, heygen-com#6697, heygen-com#6716 (cache-dir helper only). Deferred heygen-com#7182, heygen-com#7176. Package size budgets follow upstream's heygen-com#7168 increments. Watermarks: commit 15b1de4, PR 7189; monitored 92. Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com> Co-authored-by: WaterrrForever <miao.yang@heygen.com> Co-authored-by: Ular Kimsanov <ularkimsanov7@gmail.com> Co-authored-by: James Russo <james.russo@heygen.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What changed
hyperframes cloud renderon a project folder now carries the system fonts the project uses inside its zip. Before it zips, it asks the cloud compile step's own resolver (injectDeterministicFontFaces, fail-closed) which families the cloud cannot resolve; for those it captures the font file from this computer (producer's system-font capture, woff2) and adds it to the entry page as an inline@font-facein a<style data-hf-system-fonts>block. The server reads inline@font-facetoday, so no new request field and no server change.If no font file is found for a family, the command stops before anything is uploaded or charged:
HyperFrames couldn't find a font file for Zapfino on this computer, so the cloud can't have it: switch to a font in your project or render on this computer.A small fixed table adds a free look-alike for common system fonts (PingFang SC->Noto Sans SC). A network failure while checking never blocks; the server decides then.Uploading system fonts (Apple's included) was Miguel's call: he was told the licence point and said to upload them.
What I measured
Why: a Pro person's cloud render failed with
FONT_FETCH_FAILED: Unresolved fonts in fail-closed mode; 8 renders failed that way in 24 h (PingFang SC,font-Montserrat-400,var(--font-display)).Run for real on miga with the producer's resolver: a font file present only on the machine ends up in the entry page as an inline
@font-face(41 KB page,left: []), and a family with no file on the machine comes back as["Nonexistentfontxyz"]. A first version of the probe was a bare fragment, which the resolver ignores; the unit test now pins that the probe is a full document. Cost of the check: 3 Google families 0.08 to 0.3 s warm, about 1.3 s with a cold font cache.fails without the fix: with the inner-name fallback disabled, 3 tests fail (and a new test pins that it is off unless asked) (
systemFontLocator.innerName.test.tsPingFang SC and PingFang TC from one.ttc,deterministicFonts-systemTtc.test.tsembeds that face alone); the file-name match test (Georgia.ttf) still passes.render.fonts.test.tsfails with the call inprepareLocalArchiveremoved;fontPreflight.test.tscovers embed, not-on-this-machine, nothing-to-do and network-failure. The core fonts suite (23 files, 281 tests), the cloud suites (18 files, 189 tests), typecheck and lint pass on miga.Answers to the review at 1846420: the type error is gone with the call moved into
prepareLocalArchive(it has the resolved project dir);lookAlikeForis no longer exported; the bundling reusesbundleWithLocalizedFonts(no new import of the compiler).What I did NOT exercise
The core locator now also finds a font by the family names written inside the file (the
nametable, each face of a.ttc), not only by file name, and loads one face of a collection as a standalone font (a browser cannot load a.ttcwhole). It also scans macOS'sAssetsV2font folder, where newer macOS keeps PingFang. No parser dependency is added:fontkitexists in the repo only as a CLI dependency (cli/src/capture/fontMetadataExtractor.ts) and core does not depend on it, so this is a small reader (sfntNames.ts, about 110 lines). The name-table lookup is opt-in (systemFontsByInnerName), set only by the cloud font capture here, so Studio preview and local renders behave exactly as before (they never look inside files). It runs only when the file-name match finds nothing, and keeps the regular and the bold face of a family, not every weight: the reviewer measured one real 11 MB CJK face at 31 s cold to compress and 6.2 MB of page, so a full 6-weight family would be minutes and tens of MB in the zip's entry page.Not run against a real PingFang.ttc (the fixtures are synthetic fonts with PingFang's names and layout, built in the tests), against the real cloud, or on Windows or macOS. Not covered: Windows and Linux fonts whose family name is only in a localized or legacy name record; the new name scan reads the font headers of every installed font on first miss (not timed on a Mac). Not checked: the server reading a multi-MB inline
@font-face(the system-font size warning is at 5 MB). A linked project.cssis still refused by the server until its font change lands.--asset-idand--urlsources skip the check;--dry-runruns it.Size
About 130 lines of code and 90 of tests in 5 files; nothing open to fold it into.