Wednesday, May 27, 2026

Setting Up HyperFrames on a Linux Server [GPU-Free, Japanese Font Tofu Fix Included]

This is a build log for engineers who want to generate videos programmatically on a Linux server. Using the open-source video rendering engine HyperFrames, I set up an environment that converts HTML/CSS/GSAP animations into MP4 files via headless Chrome and FFmpeg — on an Ubuntu server without sudo privileges. This covers everything, including the fix for the hardest part: Japanese font tofu (text rendering as □□□).

What You Can Do

  • Convert HTML/CSS/GSAP animations into MP4 videos automatically
  • Set up a video generation environment on a Linux server without GPU or Docker
  • Understand and fix the Japanese font tofu problem (text showing as □□□)

Tools Used

  • HyperFrames — HTML/CSS to MP4 rendering engine (OSS, Apache 2.0)
  • nvm — Node.js version manager (installs without sudo)
  • Node.js 22 — runtime for HyperFrames (v22+ required)
  • FFmpeg — video encoder (installed via static binary, no sudo)
  • Python 3 — for the caller wrapper script

Background

I came across HyperFrames on X (Twitter). A post describing it as an OSS tool that converts HTML to video without GPU or Docker spread widely among engineers. It looked like a perfect fit for my use case, so I dug in.

I needed a way to generate MP4 videos automatically from other scripts over SSH — without occupying a GPU and without Docker. HyperFrames fit the bill: CPU rendering, standard Web technologies, and a regular Linux user environment.

Environment: Ubuntu 22.04, no sudo (regular user).

Steps

1. Install Node.js 22+ via nvm

HyperFrames requires Node.js 22 or higher. Most Ubuntu environments ship an outdated Node.js via apt, so we install via nvm at the user level.

$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
$ source ~/.bashrc
$ nvm install 22 && nvm alias default 22
$ node -v
v22.x.x

Common pitfall: nvm installs node under ~/.nvm/versions/node/v22.x.x/bin/. When scripts run over SSH, ~/.bashrc is not sourced, so this path is missing from PATH. Add the following to the top of any SSH-invoked script:

export NVM_DIR="$HOME/.nvm"
source "$NVM_DIR/nvm.sh"

2. Install FFmpeg (static binary)

To install FFmpeg without sudo, grab the pre-built static binary and place it in ~/.local/bin/.

$ mkdir -p ~/.local/bin
$ cd /tmp
$ curl -L https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz \
    | tar xJ
$ cp ffmpeg-*-static/ffmpeg ~/.local/bin/
$ echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
$ source ~/.bashrc
$ ffmpeg -version | head -1

3. Install HyperFrames

$ npm install -g hyperframes
$ hyperframes --version

4. Install Headless Chrome

HyperFrames uses headless Chrome for frame capture. The following command downloads it automatically.

$ hyperframes browser ensure

Make sure node and ffmpeg are in PATH before running. Verify with which node && which ffmpeg.

5. Fix Japanese Font Tofu (the Hardest Part)

With font-family: 'Noto Sans JP' in the HTML template, Japanese text renders as tofu (□□□). The HyperFrames log says "Fetched 1116 font face(s) for Noto Sans JP from Google Fonts" — yet the text is still broken.

Root Cause

Google Fonts delivers Noto Sans JP split across Unicode range subsets. HyperFrames only cached the Latin subset (81 KB). The CJK subset containing Japanese glyphs (4–5 MB per weight) was never downloaded.

Since headless Chrome reads system fonts directly, the fix is to install the full TTF into the system font directory.

Fix

Use curl to download the TTF directly. Browser User-Agent spoofing is required — without it, Google Fonts returns a woff2 subset URL instead of the full TTF, and the tofu problem persists.

$ mkdir -p ~/.local/share/fonts

# Spoof User-Agent to get the full TTF (required)
$ curl -L -A 'Mozilla/5.0 (X11; Linux x86_64)' \
    'https://fonts.gstatic.com/s/notosansjp/v56/-F6ofjtqLzI2JPCgQBnw7HFowAA.ttf' \
    -o ~/.local/share/fonts/NotoSansJP-Regular.ttf

# Rebuild font cache
$ fc-cache -f ~/.local/share/fonts/

# Verify
$ fc-list | grep "Noto Sans JP"

If fc-list shows NotoSansJP, it worked. Note: the version number in the URL (v56) may change. For a URL-independent alternative, download the full zip from the Google Fonts page, extract to ~/.local/share/fonts/, and run fc-cache -f.

Before / After

Here are the actual videos before and after the fix.

Before — Japanese text renders as □□□ (tofu)
After — Japanese text renders correctly

6. Python Wrapper Script (Optional)

If you need to call HyperFrames over SSH from other scripts, a thin Python wrapper helps. Key design points:

  • Accept --prompt "text" and generate the HTML composition internally (isolates callers from HyperFrames' HTML schema)
  • Support style selection via arguments (fade / slide / motion)
  • Use fcntl.flock for mutual exclusion when called from multiple processes

The caller only needs to know "SSH one command, get back an MP4 file" — internal HTML template changes don't require updating callers.

Completed System

Caller Script (SSH)
▼ --prompt "text" --output video.mp4
Python Wrapper (HTML gen + lock)
HyperFrames (Node.js + Chrome + FFmpeg)
MP4 Video (720p / 30fps)

No daemon, command execution only. One SSH call returns a video file.

FAQ

Q. I get nvm: command not found

A. This happens when ~/.bashrc is not sourced during SSH execution. Add source "$HOME/.nvm/nvm.sh" to the top of your script.

Q. hyperframes browser ensure fails

A. Check that both node and ffmpeg are in PATH. Run which node && which ffmpeg — both paths should appear.

Q. Chinese or Korean text is also tofu

A. Same root cause. Install the full TTF for that font into ~/.local/share/fonts/ and run fc-cache -f.

Q. The video is black

A. Chrome may be capturing frames before the first render. Increase the delay in your composition config or adjust when CSS animations start.

Q. Does Node.js 20 work?

A. Node.js 22+ is the official requirement. Switch with nvm install 22 && nvm use 22.

※ The steps and commands in this post were verified at the time of writing (May 2026). They may break with library or OS updates. If something doesn't work, let me know in the comments.

Summary

I set up HyperFrames on a sudo-less Ubuntu server. The biggest pain point was Japanese font tofu — caused by Google Fonts' subset delivery and headless Chrome reading system fonts directly. Downloading the full TTF with a browser User-Agent and installing it to the system font directory fixes it cleanly.

If this post helped, share it on X (Twitter) — much appreciated.

A Promo Video Built with HyperFrames

To put it all together, I made a 30-second promo video for this blog — ON THE HAND — using HyperFrames. It combines past post images with fade transitions and text captions. Japanese text renders correctly after the font fix.

ON THE HAND blog promo video (generated with HyperFrames, 30 sec)

App by the Author

I built an iOS reading tracker app called My Bookstore, available on the App Store. Great for keeping a simple reading list.

View on App Store →

Related Posts

References

※ This blog is experimenting with Claude Code-assisted updates.

No comments:

Post a Comment