Skip to content

Frame-preset fonts (Google Fonts) are never staged as local files — 12/13 presets and media-use have no way to fetch them #3583

Description

@julian-pani

Note on provenance: this report was written by an AI coding agent (Claude) investigating a finding from another agent instance (a "frame worker" sub-agent) during a video build, at the request of a human user who is filing it on the agent's behalf. The user has not independently verified the code-level claims below beyond a light read-through — treat the diagnosis as agent-generated analysis, not a human-confirmed bug report. If it turns out not to be a real bug, it may still be useful as a concrete example of where an agent's reasoning about this codebase went right or wrong — happy to have it closed either way with a note on what it got wrong.

Summary

Building a video from the blue-professional frame-preset, a frame-worker sub-agent correctly followed the font_family_without_font_face rule in skills/hyperframes-core/references/frame-worker-core.md ("Only use fonts that ship as files with the project... Never name a font that has no file") and substituted a system-font stack, because frame.md's named fonts (Space Grotesk, Inter) had no matching .woff2/.ttf/.otf anywhere in the project, the preset's own directory, or the files staged by build-frame.mjs.

Tracing this back, it looks like a structural gap rather than a one-off: most shipped frame-presets name Google Fonts as their default identity typography, but nothing in the toolchain ever fetches those fonts as files unless the calling project happens to already have them from an unrelated brand-capture step.

What I checked

1. build-frame.mjs's font-staging logic is real and working, but scoped narrowly.
(skills/product-launch-video/scripts/build-frame.mjs, same logic duplicated in skills/faceless-explainer/scripts/build-frame.mjs and skills/pr-to-video/scripts/build-frame.mjs)

The "stage brand font files + emit @font-face" block only copies files it finds in the project's capture/assets/fonts/ or assets/fonts/:

const srcDirs = [
  join(hyperframesDir, "capture/assets/fonts"),
  join(hyperframesDir, "assets/fonts"),
].filter((d) => existsSync(d));

Those directories are populated by a prior brand-capture step (scraping an existing website's own font files) — not by anything related to the preset. The whole block is also gated behind if (brandFonts.length), i.e. it only runs at all when tokens.json records fonts from a captured brand. For a project with no such capture (e.g. built from a brief, not a URL), this entire section — remix and staging both — is skipped, and the log line even says so: "fonts: no brand fonts — preset fonts kept". The preset's font names survive into frame.md untouched, with nothing behind them.

Critically, the script never reads <presetDir>/<presetName>/fonts/ — the preset's own directory — at all. It only ever reads presetDir/<presetName>/FRAME.md and presetDir/<presetName>/caption-skin.html.

2. 12 of 13 shipped presets name Google Fonts and ship zero font files.

biennale-yellow:   Archivo, Instrument Serif, JetBrains Mono
blockframe:        Inter, Space Grotesk
blue-professional: Inter, Space Grotesk
bold-poster:       Libre Baskerville, Shrikhand, Space Grotesk
broadside:         Barlow, IBM Plex Mono
capsule:           Bodoni Moda, Space Grotesk
cartesian:         Inter, Playfair Display
cobalt-grid:       DM Mono, Hanken Grotesk, Newsreader
code-editorial:    EB Garamond, Inter, JetBrains Mono   <- ships fonts/*.woff2 (see below)
coral:             Bebas Neue, Inter
creative-mode:     Archivo Black, JetBrains Mono, Space Grotesk
daisy-days:        Fredoka One, Quicksand
editorial-forest:  JetBrains Mono, Source Serif 4

Only code-editorial ships real .woff2 files, in its own fonts/ subfolder (Inter-{400,700}.woff2, EBGaramond-{400,700}.woff2, JetBrainsMono-{400,700}.woff2). But per point 1, nothing copies from a preset's fonts/ folder into a project — so even code-editorial's shipped files go unused by build-frame.mjs. It looks like the intended pattern (ship the font files next to the preset, one preset proves it out) exists but was never wired up on the consuming side, and the other 12 presets never got the treatment either way.

3. media-use (the skill meant to resolve/fetch all project media) has no font asset type.

Its resolve --type <type> table (skills/media-use/SKILL.md) is: bgm, sfx, image, icon, logo, voice, grade, lut. I checked scripts/resolve.mjs and scripts/lib/brand-provider.mjs for any font-fetch path under another name — the only "font" references there read an already-captured tokens.font/typography value for grade-matching purposes; there's no code path that fetches a named font's files. So a frame-worker or orchestrator has nowhere to route a "please get me this Google Font" request even if it wanted to.

4. There's a documented but narrow escape hatch that the frame-worker's contract doesn't use.

skills/hyperframes-creative/references/typography.md says the render compiler itself (not a skill script) will auto-fetch a real Google Font at build time if it's named without a matching @font-face, for local renders — but it (a) trips a font_family_without_font_face lint warning, and (b) is fail-closed (hard error) on distributed/cloud (Lambda) renders. skills/hyperframes-core/references/frame-worker-core.md's own font_family_without_font_face rule never mentions this fallback at all, and instead states the stricter "never name a font that has no file." That's arguably the correct, safer instruction given the cloud-render failure mode — but it means the only way to reliably honor a preset's typography is to ship real files, and nothing does that automatically.

Suggested fix (sketch, not a PR)

This is a design suggestion from the investigating agent, not a vetted implementation — take it as a starting point:

  • Add a font type to media-use's resolve verb: given a family name (+ optional weights), fetch the matching Google Fonts .woff2 file(s) into assets/fonts/ and emit the ledger record, mirroring the existing logo/image resolution pattern.
  • Have preset adoption (build-frame.mjs, or whichever step runs after it) call that for every fontFamily in the adopted FRAME.md that isn't already covered by a captured brand font or by the renderer's pre-bundled family list (documented in typography.md).
  • Separately/optionally: either wire build-frame.mjs to copy a preset's own fonts/ subfolder (proving out the code-editorial pattern for the other 12 presets), or drop that unused folder if the intended path is meant to go through media-use instead.

Why I'm not sending a PR

I only traced the gap; I haven't implemented or tested a fix, and didn't want to guess at API/ledger conventions I haven't fully internalized. Filing this as an issue so a maintainer can weigh in on whether the media-use route or the preset-folder route is the intended fix.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions