Skip to content

fix(cli): media-use sends a picture on a machine with no ffmpeg - #7112

Merged
miguel-heygen merged 3 commits into
mainfrom
fix/media-use-picture-without-ffmpeg
Nov 4, 2026
Merged

miguel-heygen merged 3 commits into
mainfrom
fix/media-use-picture-without-ffmpeg

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

What

media-use generate sends a picture (a reference, a first frame or an avatar photo) on a machine with no ffmpeg. Before, every picture went through ffmpeg to fit it in 2048 px, so on such a machine the step stopped with "ffmpeg is not installed" before anything was sent.

Why

A picture needs no video tool. Agents on fresh Windows machines run media-use generate with a reference picture and hit the ffmpeg error as their first failure. Sound and motion clips still need ffmpeg and still ask for it.

Related work

None.

How

  • generate prepares the pictures before building the body: sharp turns each upright (EXIF orientation), fits it in 2048 px and writes a JPEG. sharp is the CLI's optional dependency, loaded through loadSharp, which installs it once on first use.
  • Which pictures: the route's own body, built once with a media that only records the paths it would send as images. So the route stays the one owner of what it sends, and --avatar with a stale --photo still ignores the photo.
  • ffmpeg stays the fallback, so a machine with ffmpeg keeps everything it had: when sharp cannot load (an optional dependency skipped, offline, HYPERFRAMES_NO_OPTIONAL_INSTALL), every picture goes through the same ffmpeg resize as before, and so does a picture sharp cannot read (its own build has no HEIC).
  • aspectOf reads the first frame's size from the prepared picture: with sharp, or with ffprobe when sharp is missing.

No visible change

CLI behavior only, no UI.

Test plan

  • generate.test.ts › "sends a picture with no ffmpeg on the machine, shrunk to 2048 px": HYPERFRAMES_FFMPEG_PATH points at no file, a 3000x1000 PNG reference goes out as a 2048x683 JPEG. fails without the fix: on main it throws "ffmpeg is not installed".
  • generate.test.ts › "sends a picture sharp cannot read through ffmpeg": a BMP reference goes out as a JPEG of its own size.
  • generate.test.ts › "sends pictures through ffmpeg when sharp cannot be loaded": loadSharp rejects, a first frame still goes out and the result is cropped to its shape (probed with ffprobe). Fails at 18f2d1f.
  • generate.test.ts › "prepares only the pictures the route sends: --avatar leaves a stale --photo alone". Fails at 18f2d1f ("no file at …").
  • The existing first-frame tests ("crops to the first frame's shape when no --aspect is passed", "keeps a portrait shot from a phone photo stored sideways") pass with the picture prepared by sharp.
  • runOnce in the pickVoice re-run tests now moves the fake clock until the run ends instead of in one 31-minute jump: the avatar's photo is now read asynchronously before the first poll, so a single jump landed before polling started and the test timed out.
  • generate.test.ts 54 of 54, three runs in a row; with main's generate.ts, only the no-ffmpeg test fails.
  • Comments follow CONTRIBUTING.md "Comments": generate.ts adds no comment lines over its base share (the Comments check).

Size

Two files, about 100 lines with the tests.

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: REQUEST CHANGES · 1 blocker · 0 important · 1 nit · head 18f2d1f

The no-ffmpeg path works, and the fails without the fix: test is named and valid. One regression for machines that have ffmpeg but no usable sharp, inline.

  • nit: shrunk runs needFile on every picture in the request, including a photo that the avatar route ignores when --avatar is set. A stale --photo path next to --avatar now fails with "no file", where before it was ignored.
Scope

Read the full diff (2 files). The image media() calls are references (image and video routes), firstFrame and photo (generate.ts:196,224,227,281), and picturesOf covers all of them, so pictures.get(path)! cannot miss. assetsOf never uploads a picture. The runOnce fake-clock loop ends when the run settles. CI was still running at review time.

Comment thread packages/cli/src/heygen/generate.ts Outdated
async function shrunk(paths: string[], work: string): Promise<Map<string, string>> {
const out = new Map<string, string>();
if (!paths.length) return out;
const sharp = await loadSharp("media-use pictures");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocker: if sharp can't be loaded, every picture request fails, even when ffmpeg is installed. sharp is an optionalDependency, so npm skips it silently on platforms without a prebuilt binary and under --omit=optional. loadSharp then tries a runtime install, which rejects offline, on a failed install, or right away when HYPERFRAMES_NO_OPTIONAL_INSTALL is set (optionalPackages.ts:206). On main those machines sent pictures through ffmpeg. Now generate throws before building the body, which contradicts the body's "a machine with ffmpeg keeps every format it had". The fix is to treat a rejected loadSharp like an unreadable picture and use the existing ffmpeg resize for every path. Do the same in aspectOf (:543): if sharp is missing there, fall back to probeVideo on the prepared JPEG. A test that stubs loadSharp to reject while ffmpeg is present would pin this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Fixed in 174bdef.

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: REQUEST CHANGES — 2 resolved, 0 open, 1 new · 18f2d1f → 174bdef. Both fixes are correct: sharpOrNone falls back to ffmpeg in shrunk and to probeVideo in aspectOf, and picturesOf now reads the route's own pure body. New: the Comments check fails on this head. generate.ts comment share went from 6.5% (49/750) to 6.6% (51/775), 2 lines over. Cut 2 comment lines, for example the new one in shrunk, and this is an approve.

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE — 1 resolved, 0 open, 0 new · 174bdef → 7b14df4. Only comments changed, and the Comments check is now green.

@miguel-heygen
miguel-heygen added this pull request to the merge queue Nov 3, 2026
Merged via the queue into main with commit 3adc9f1 Nov 4, 2026
118 of 121 checks passed
@miguel-heygen
miguel-heygen deleted the fix/media-use-picture-without-ffmpeg branch November 4, 2026 00:07
This was referenced Nov 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant