Skill Tracer
Skills › HyperFrames › media-use › operations.md

operations.md

335 lines · skills/media-use/references/operations.md at a595755

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1# Media operations: agent guidance
2
3media-use resolves and remembers assets. For **operating** on them: cutting,
4reframing, stitching, transforming, it does not wrap every action as a bespoke
5command. Instead it points you at the right local tool (decision OP1). Run the
6tool, then register the output with `resolve --from <output> --type <type>` so the
7result lands in the ledger and the global cache like any other asset.
8
9The ffmpeg and local-model operations below run locally. Direct `heygen` recipes
10use the HeyGen CLI's own login and HeyGen/API billing; they do not inherit a
11HyperFrames subscription. Use those recipes only when that separate payer was
12selected. `hyperframes media-use resolve` uses the HyperFrames-selected credential.
13ffmpeg is assumed present (it backs the engine already).

Media operations: agent guidance

media-use resolves and remembers assets. For operating on them: cutting, reframing, stitching, transforming, it does not wrap every action as a bespoke command. Instead it points you at the right local tool (decision OP1). Run the tool, then register the output with resolve --from <output> --type <type> so the result lands in the ledger and the global cache like any other asset.

The ffmpeg and local-model operations below run locally. Direct heygen recipes use the HeyGen CLI's own login and HeyGen/API billing; they do not inherit a HyperFrames subscription. Use those recipes only when that separate payer was selected. hyperframes media-use resolve uses the HyperFrames-selected credential. ffmpeg is assumed present (it backs the engine already).

Raw lines 1 to 14
15## Cut / trim: keep a slice
17```bash
18ffmpeg -i in.mp4 -ss 00:00:12 -to 00:00:20 -c copy out.mp4 # 0:12–0:20, no re-encode
19```
21In-composition trimming usually needs **no new file**: a clip plays a sub-window
22via `data-media-start` + `data-duration` (see `/hyperframes` → `references/format.md`). Only cut a
23physical file when exporting/assembling outside the composition.

Cut / trim: keep a slice

ffmpeg -i in.mp4 -ss 00:00:12 -to 00:00:20 -c copy out.mp4   # 0:12–0:20, no re-encode

In-composition trimming usually needs no new file: a clip plays a sub-window via data-media-start + data-duration (see /hyperframes → references/format.md). Only cut a physical file when exporting/assembling outside the composition.

Raw lines 15 to 24
Names a skill hyperframes :22
25## Reframe / crop: change aspect ratio
27```bash
28# 16:9 -> 9:16, crop centered
29ffmpeg -i in.mp4 -vf "crop=ih*9/16:ih,scale=1080:1920" out.mp4
30```
32For a non-destructive crop, set a `clip-path` on the element in the composition
33itself (render-time, source file untouched) instead of re-encoding with ffmpeg.

Reframe / crop: change aspect ratio

# 16:9 -> 9:16, crop centered
ffmpeg -i in.mp4 -vf "crop=ih*9/16:ih,scale=1080:1920" out.mp4

For a non-destructive crop, set a clip-path on the element in the composition itself (render-time, source file untouched) instead of re-encoding with ffmpeg.

Raw lines 25 to 34
35## Montage / stitch: join clips
37```bash
38printf "file '%s'\n" a.mp4 b.mp4 c.mp4 > list.txt
39ffmpeg -f concat -safe 0 -i list.txt -c copy out.mp4
40```

Montage / stitch: join clips

printf "file '%s'\n" a.mp4 b.mp4 c.mp4 > list.txt
ffmpeg -f concat -safe 0 -i list.txt -c copy out.mp4
Raw lines 35 to 41
42## Silence-cut / highlight: trim dead air, grab the best moment
44```bash
45auto-editor in.mp4 --edit audio:threshold=4% -o tight.mp4 # pip install auto-editor
46scenedetect -i in.mp4 detect-adaptive list-scenes # pip install scenedetect
47```

Silence-cut / highlight: trim dead air, grab the best moment

auto-editor in.mp4 --edit audio:threshold=4% -o tight.mp4   # pip install auto-editor
scenedetect -i in.mp4 detect-adaptive list-scenes           # pip install scenedetect
Raw lines 42 to 48
49## Transforms with a quality choice (process)
51These have a local option AND a higher-quality HeyGen option. Run the local
52one for free/offline; use HeyGen when quality matters: `media-use generate` on the
53sign-in alone, the rest through the `heygen` CLI. Showing the user a
54**side-by-side** (local vs HeyGen) is the honest way to let them choose.
56| Op | Local (free) | HeyGen (quality) |
57| ----------------------- | -------------------------------------------------- | -------------------------------------------------------------------------------- |
58| Background removal | `hyperframes remove-background in.png` (u2net) | `hyperframes media-use generate --type cutout --video in.mp4`; stills stay local |
59| Upscale | `realesrgan-ncnn-vulkan -i in.png -o out.png -s 4` | n/a |
60| Lipsync (dub) | n/a | `hyperframes media-use generate --type lipsync` |
61| Translate | n/a | `heygen video-translate` |
62| Clone a voice | n/a | `heygen voice clone create` |
63| Text to video | n/a | `heygen video-agent create` |
64| Clips from a long video | n/a | `heygen ai-clipping` |
66After any op: `resolve --from out.ext --type <type>` to register the derived
67asset (it records provenance and auto-promotes to the global cache).
69> ponytail: media-use doesn't re-wrap ffmpeg/heygen here, that's deliberate
70> (OP1). The value it adds is the ledger + global reuse on the _output_, via
71> `--from`. Add a thin `process` verb only if agents repeatedly fumble these
72> recipes.

Transforms with a quality choice (process)

These have a local option AND a higher-quality HeyGen option. Run the local one for free/offline; use HeyGen when quality matters: media-use generate on the sign-in alone, the rest through the heygen CLI. Showing the user a side-by-side (local vs HeyGen) is the honest way to let them choose.

OpLocal (free)HeyGen (quality)
Background removalhyperframes remove-background in.png (u2net)hyperframes media-use generate --type cutout --video in.mp4; stills stay local
Upscalerealesrgan-ncnn-vulkan -i in.png -o out.png -s 4n/a
Lipsync (dub)n/ahyperframes media-use generate --type lipsync
Translaten/aheygen video-translate
Clone a voicen/aheygen voice clone create
Text to videon/aheygen video-agent create
Clips from a long videon/aheygen ai-clipping

After any op: resolve --from out.ext --type <type> to register the derived asset (it records provenance and auto-promotes to the global cache).

ponytail: media-use doesn't re-wrap ffmpeg/heygen here, that's deliberate (OP1). The value it adds is the ledger + global reuse on the output, via --from. Add a thin process verb only if agents repeatedly fumble these recipes.
Raw lines 49 to 73
74## Exact error-diffusion dither
76Use the local processor when the requested look specifically calls for
77Floyd-Steinberg, Atkinson/Macintosh, Jarvis-Judice-Ninke, Stucki, Burkes, or a
78Sierra variant. These are sequential error-diffusion algorithms, not the
79realtime Bayer `effects.dither` shader.
81```bash
82node <SKILL_DIR>/scripts/dither.mjs \
83 --input source.mp4 \
84 --out source.atkinson.mp4 \
85 --algorithm atkinson \
86 --palette '#0f380f,#306230,#8bac0f,#9bbc0f' \
87 --point-size 3
89npx hyperframes media-use resolve \
90 --from source.atkinson.mp4 --type video --project .
91```
93Available algorithms: `floyd-steinberg`, `atkinson`,
94`jarvis-judice-ninke`, `stucki`, `burkes`, `sierra`, `sierra-lite`, and
95`two-row-sierra`. The default is balanced Floyd-Steinberg with a black/white
96palette. Palettes contain 2-6 `#rrggbb` colors in authored dark-to-light order;
97reversing the order intentionally inverts the mapping. `--point-size` controls
981-20px blocks; `--brightness` and `--contrast` accept 0.5-2; `--detail` accepts
990.1-1.
101The processor supports ordinary SDR images and MP4 video, preserves video
102audio, and emits BT.709 MP4. It rejects tagged PQ/HLG input rather than silently
103tone-mapping it. To animate the transformation, keep the original and processed
104files as two real media layers and use the seek-safe GSAP timeline to reveal or
105crossfade between them. Use the realtime Bayer shader instead when the dither
106amount itself must animate continuously.

Exact error-diffusion dither

Use the local processor when the requested look specifically calls for Floyd-Steinberg, Atkinson/Macintosh, Jarvis-Judice-Ninke, Stucki, Burkes, or a Sierra variant. These are sequential error-diffusion algorithms, not the realtime Bayer effects.dither shader.

node <SKILL_DIR>/scripts/dither.mjs \
  --input source.mp4 \
  --out source.atkinson.mp4 \
  --algorithm atkinson \
  --palette '#0f380f,#306230,#8bac0f,#9bbc0f' \
  --point-size 3

npx hyperframes media-use resolve \
  --from source.atkinson.mp4 --type video --project .

Available algorithms: floyd-steinberg, atkinson, jarvis-judice-ninke, stucki, burkes, sierra, sierra-lite, and two-row-sierra. The default is balanced Floyd-Steinberg with a black/white palette. Palettes contain 2-6 #rrggbb colors in authored dark-to-light order; reversing the order intentionally inverts the mapping. --point-size controls 1-20px blocks; --brightness and --contrast accept 0.5-2; --detail accepts 0.1-1.

The processor supports ordinary SDR images and MP4 video, preserves video audio, and emits BT.709 MP4. It rejects tagged PQ/HLG input rather than silently tone-mapping it. To animate the transformation, keep the original and processed files as two real media layers and use the seek-safe GSAP timeline to reveal or crossfade between them. Use the realtime Bayer shader instead when the dither amount itself must animate continuously.

Raw lines 74 to 107
Runs code scripts/dither.mjs :82
108## Transcription (default: Parakeet, better than whisper.cpp)
110`transcribe.mjs` is the default local transcription path. It runs **NVIDIA
111Parakeet-TDT via parakeet-mlx**, which beats whisper.cpp on the Open ASR
112Leaderboard (avg WER ~6.05% vs 7.44%; on NOISY audio 4.73% vs 5.96%, where
113whisper-large-v3 hallucinated to 308% WER on meetings) and is 5-10x faster.
114It emits `{ text, words:[{text,start,end}] }` with word timestamps (merged from
115Parakeet's sub-word tokens), feeding transcript-cut, captions, and the audio
116engine directly.
118```bash
119# install once: uv venv ~/.venvs/parakeet && VIRTUAL_ENV=~/.venvs/parakeet uv pip install parakeet-mlx
120node <SKILL_DIR>/scripts/transcribe.mjs --input talk.mp4 --out talk.transcribe.json
122# the hyperframes CLI runs Parakeet on macOS, Linux and Windows once it is installed:
123npx hyperframes models install parakeet # once, ~640 MB, ask the user first
124npx hyperframes transcribe talk.mp4 --engine parakeet # or --engine auto (default)
125```
127VERIFIED on 24GB: accurate, ~3s (cached) for 8s audio. Parakeet covers 25
128European languages, English included. For other languages, or when parakeet-mlx is not
129installed, transcribe.mjs falls back to `hyperframes transcribe`, which uses the
130CLI's Parakeet when `models install parakeet` has run and whisper.cpp (99
131languages) otherwise. `--engine parakeet|whisper` forces one, except that a
132`--language` other than English runs whisper whenever it is installed, since
133Parakeet detects the language itself. (Cohere
134Transcribe tops the leaderboard on paper but its mlx-audio quants produced
135garbage and ran 40-70x slower on a Mac in testing, so it is not wired in.)

Transcription (default: Parakeet, better than whisper.cpp)

transcribe.mjs is the default local transcription path. It runs NVIDIA Parakeet-TDT via parakeet-mlx, which beats whisper.cpp on the Open ASR Leaderboard (avg WER ~6.05% vs 7.44%; on NOISY audio 4.73% vs 5.96%, where whisper-large-v3 hallucinated to 308% WER on meetings) and is 5-10x faster. It emits { text, words:[{text,start,end}] } with word timestamps (merged from Parakeet's sub-word tokens), feeding transcript-cut, captions, and the audio engine directly.

# install once: uv venv ~/.venvs/parakeet && VIRTUAL_ENV=~/.venvs/parakeet uv pip install parakeet-mlx
node <SKILL_DIR>/scripts/transcribe.mjs --input talk.mp4 --out talk.transcribe.json

# the hyperframes CLI runs Parakeet on macOS, Linux and Windows once it is installed:
npx hyperframes models install parakeet                 # once, ~640 MB, ask the user first
npx hyperframes transcribe talk.mp4 --engine parakeet   # or --engine auto (default)

VERIFIED on 24GB: accurate, ~3s (cached) for 8s audio. Parakeet covers 25 European languages, English included. For other languages, or when parakeet-mlx is not installed, transcribe.mjs falls back to hyperframes transcribe, which uses the CLI's Parakeet when models install parakeet has run and whisper.cpp (99 languages) otherwise. --engine parakeet|whisper forces one, except that a --language other than English runs whisper whenever it is installed, since Parakeet detects the language itself. (Cohere Transcribe tops the leaderboard on paper but its mlx-audio quants produced garbage and ran 40-70x slower on a Mac in testing, so it is not wired in.)

Raw lines 108 to 136
Runs code scripts/transcribe.mjs :120
Involves a person ask the user :123
137## Text-based editing (transcript cut)
139`transcript-cut.mjs` is a compiler, not a wrapper: it turns word timestamps and
140agent cut decisions into exact kept segments. It is provided even though the rest
141of this file is guidance-only.
143```bash
144node <SKILL_DIR>/scripts/transcript-cut.mjs \
145 --input talk.mp4 \
146 --transcript talk.transcribe.json \
147 --remove "12.41-15.02,88.3-91.7" \
148 --remove-fillers "um,uh,like" \
149 --cut-silence 0.8 \
150 --out talk.cut.mp4
152resolve --from talk.cut.mp4 --type video
153```
155Use `--plan` first when you want to inspect the kept segment JSON before encoding.

Text-based editing (transcript cut)

transcript-cut.mjs is a compiler, not a wrapper: it turns word timestamps and agent cut decisions into exact kept segments. It is provided even though the rest of this file is guidance-only.

node <SKILL_DIR>/scripts/transcript-cut.mjs \
  --input talk.mp4 \
  --transcript talk.transcribe.json \
  --remove "12.41-15.02,88.3-91.7" \
  --remove-fillers "um,uh,like" \
  --cut-silence 0.8 \
  --out talk.cut.mp4

resolve --from talk.cut.mp4 --type video

Use --plan first when you want to inspect the kept segment JSON before encoding.

Raw lines 137 to 156
Runs code scripts/transcript-cut.mjs :144
157## Ducking (declare in-composition / bake for export)
159B1, declare ducking in the composition. `audio-duck.mjs` emits a volume lane
160as a `data-automation` attribute. Add it to the background `<audio>` element;
161the source file stays untouched. Lane times are clip-local, so pass
162`--composition` to let the script subtract the element's `data-start`.
164```bash
165node <SKILL_DIR>/scripts/audio-duck.mjs \
166 --meta audio_meta.json \
167 --target "#bgm" \
168 --composition index.html
169```
171```html
172<!-- auto-duck: #bgm under narration; add to its <audio> element -->
173data-automation='{"version":1,"lanes":[{"target":"volume","points":[{"t":0,"v":0.6},{"t":3.42,"v":0.6},{"t":3.57,"v":0.15},{"t":9.87,"v":0.15},{"t":10.27,"v":0.6}]}]}'
174```
176B2, bake ducking only for exported or standalone files.
178```bash
179ffmpeg -i bgm.mp3 -i voice.wav \
180 -filter_complex "[0][1]sidechaincompress=threshold=0.03:ratio=8:attack=200:release=400[ducked]" \
181 -map "[ducked]" bgm.ducked.wav
182```
184Declare inside compositions. Bake only for assets leaving the hyperframes
185pipeline.

Ducking (declare in-composition / bake for export)

B1, declare ducking in the composition. audio-duck.mjs emits a volume lane as a data-automation attribute. Add it to the background <audio> element; the source file stays untouched. Lane times are clip-local, so pass --composition to let the script subtract the element's data-start.

node <SKILL_DIR>/scripts/audio-duck.mjs \
  --meta audio_meta.json \
  --target "#bgm" \
  --composition index.html
<!-- auto-duck: #bgm under narration; add to its <audio> element -->
data-automation='{"version":1,"lanes":[{"target":"volume","points":[{"t":0,"v":0.6},{"t":3.42,"v":0.6},{"t":3.57,"v":0.15},{"t":9.87,"v":0.15},{"t":10.27,"v":0.6}]}]}'

B2, bake ducking only for exported or standalone files.

ffmpeg -i bgm.mp3 -i voice.wav \
  -filter_complex "[0][1]sidechaincompress=threshold=0.03:ratio=8:attack=200:release=400[ducked]" \
  -map "[ducked]" bgm.ducked.wav

Declare inside compositions. Bake only for assets leaving the hyperframes pipeline.

Raw lines 157 to 186
Runs code scripts/audio-duck.mjs :165
187## Publish loudness
189Two-pass `loudnorm` measures first, then applies the measured values with the
190target LUFS baked in.
192Socials target, -14 LUFS:
194```bash
195ffmpeg -i mix.wav \
196 -af loudnorm=I=-14:TP=-1.5:LRA=11:print_format=json \
197 -f null -
199ffmpeg -i mix.wav \
200 -af loudnorm=I=-14:TP=-1.5:LRA=11:measured_I=<input_i>:measured_TP=<input_tp>:measured_LRA=<input_lra>:measured_thresh=<input_thresh>:offset=<target_offset>:linear=true:print_format=summary \
201 mix.social.wav
202```
204Podcast target, -16 LUFS:
206```bash
207ffmpeg -i mix.wav \
208 -af loudnorm=I=-16:TP=-1.5:LRA=11:print_format=json \
209 -f null -
211ffmpeg -i mix.wav \
212 -af loudnorm=I=-16:TP=-1.5:LRA=11:measured_I=<input_i>:measured_TP=<input_tp>:measured_LRA=<input_lra>:measured_thresh=<input_thresh>:offset=<target_offset>:linear=true:print_format=summary \
213 mix.podcast.wav
214```

Publish loudness

Two-pass loudnorm measures first, then applies the measured values with the target LUFS baked in.

Socials target, -14 LUFS:

ffmpeg -i mix.wav \
  -af loudnorm=I=-14:TP=-1.5:LRA=11:print_format=json \
  -f null -

ffmpeg -i mix.wav \
  -af loudnorm=I=-14:TP=-1.5:LRA=11:measured_I=<input_i>:measured_TP=<input_tp>:measured_LRA=<input_lra>:measured_thresh=<input_thresh>:offset=<target_offset>:linear=true:print_format=summary \
  mix.social.wav

Podcast target, -16 LUFS:

ffmpeg -i mix.wav \
  -af loudnorm=I=-16:TP=-1.5:LRA=11:print_format=json \
  -f null -

ffmpeg -i mix.wav \
  -af loudnorm=I=-16:TP=-1.5:LRA=11:measured_I=<input_i>:measured_TP=<input_tp>:measured_LRA=<input_lra>:measured_thresh=<input_thresh>:offset=<target_offset>:linear=true:print_format=summary \
  mix.podcast.wav
Raw lines 187 to 215
216## Generate: images (local first, cloud upsell)
218`resolve --type image` retrieves from the HeyGen catalog first; on a miss it
219GENERATES. Two paths, best-for-the-machine picked automatically:
2211. **Local (default, free, private): mflux** (FLUX-on-MLX). `resolve` spec-checks
222 AVAILABLE RAM and runs the best FLUX-class model that fits, using the
223 CLI-bundled `imagegen` ladder (its modules are not shipped in the standalone
224 skill). The RAM ladder:
226 | Tier | Model | Needs (available RAM) | Notes |
227 | ------ | -------------------- | --------------------- | ----------------------------------- |
228 | medium | FLUX.1 schnell int4 | ~8GB (`--low-ram`) | ~20s/512px on 24GB. VERIFIED. Fast. |
229 | large | FLUX.2 Klein 4B int4 | ~32GB | higher quality, full-resident |
230 | xlarge | Qwen-Image | ~64GB | top quality, 64GB+ Macs only |
232 Gotchas baked into the table: the official FLUX repos are HF-gated, so it
233 points at non-gated community 4-bit re-uploads; and `--low-ram` is MANDATORY
234 at the medium tier (without it a 768x512 run swap-thrashed to 90 minutes on
235 24GB; with it, 20 seconds).
2372. **Cloud upsell (better quality): the `codex` CLI** `image_gen` tool, on the
238 user's ChatGPT subscription (codex owns auth, no key here, no per-call
239 charge). It is the automatic fallback when no local model fits AND the
240 explicit "make it better" choice on any machine. Users who just want codex
241 can ask for it directly. Verified: prompt -> raster -> frozen + ledgered.
243`--local-only` keeps mflux (once cached) and skips codex (network).

Generate: images (local first, cloud upsell)

resolve --type image retrieves from the HeyGen catalog first; on a miss it GENERATES. Two paths, best-for-the-machine picked automatically:

  1. Local (default, free, private): mflux (FLUX-on-MLX). resolve spec-checks AVAILABLE RAM and runs the best FLUX-class model that fits, using the CLI-bundled imagegen ladder (its modules are not shipped in the standalone skill). The RAM ladder:
TierModelNeeds (available RAM)Notes
mediumFLUX.1 schnell int4~8GB (--low-ram)~20s/512px on 24GB. VERIFIED. Fast.
largeFLUX.2 Klein 4B int4~32GBhigher quality, full-resident
xlargeQwen-Image~64GBtop quality, 64GB+ Macs only

Gotchas baked into the table: the official FLUX repos are HF-gated, so it points at non-gated community 4-bit re-uploads; and --low-ram is MANDATORY at the medium tier (without it a 768x512 run swap-thrashed to 90 minutes on 24GB; with it, 20 seconds).

  1. **Cloud upsell (better quality): the codex CLI** image_gen tool, on the user's ChatGPT subscription (codex owns auth, no key here, no per-call charge). It is the automatic fallback when no local model fits AND the explicit "make it better" choice on any machine. Users who just want codex can ask for it directly. Verified: prompt -> raster -> frozen + ledgered.

--local-only keeps mflux (once cached) and skips codex (network).

Raw lines 216 to 244
245## Generate: video (`resolve --type video`, HeyGen avatar first)
247`resolve --type video "<intent>"` is the default path. It generates a
248script-driven HeyGen avatar video first using the HyperFrames-selected credential
249(the server decides eligibility and charges; explicit API keys use API billing), falling back to local generative LTX only when
250HeyGen is unavailable, uncredentialed, or `--local-only` is passed. The two
251are non-substitutable outputs (a real presenter vs. a generic generative
252clip), so treat the fallback as "HeyGen wasn't reachable," not "upgrade the
253quality":
255- **HeyGen avatar video (default):**
256 The CLI-bundled provider shells the
257 `heygen` CLI — never the raw API — auto-picking a public avatar and a
258 starfish voice (override with `--avatar-id`/`--voice-id`, threaded through
259 as `ctx.avatarId`/`ctx.voiceId`). If the CLI reports `not_authenticated`,
260 the provider prints a HyperFrames sign-in recommendation to stderr and falls through to LTX instead of
261 hard-failing.
262- **Local fallback: LTX 2.3 on MLX** via `dgrauet/ltx-2-mlx`, the `videogen`
263 ladder bundled in the HyperFrames CLI. Generative clips
264 (t2v), spec-gated to RAM. Verified on 24GB: 512x320 x 33f with audio.
266Every generating `heygen` call from media-use — TTS, avatar video, and
267catalog search — sends the allowlisted `X-HeyGen-Client-Source: media-use`
268header (persistent flag, works on every subcommand) via the shared
269CLI-internal `HEYGEN_CLIENT_SOURCE_ARGV` constant, so usage
270tags correctly in billing/resource meta and shows up in the API dashboards.
271Read-only discovery (`avatar list`, `voice list`) doesn't need it.
273For structured bodies `resolve --type video` doesn't expose yet (a specific
274`avatar_id`/`voice_id` combination beyond the ctx overrides, or a
275pre-recorded `audio_url` instead of a script), the raw `heygen video create`
276recipe below remains the escape hatch. This direct command uses the separate
277HeyGen CLI credential and HeyGen/API billing, not the HyperFrames subscription:
279```bash
280# discover an avatar + a starfish voice, then create + wait
281heygen avatar list --ownership public --limit 5
282heygen voice list --engine starfish --limit 5
283heygen video create --headers "X-HeyGen-Client-Source: media-use" --wait -d '{
284 "type": "avatar",
285 "avatar_id": "<avatar-id>",
286 "script": "Your narration here.",
287 "voice_id": "<voice-id>"
288}'
289```
291Avatar videos are deterministic + script-driven (lip-sync from a script or a
292pre-recorded `audio_url`), distinct from the generative LTX clips. After a
293manual recipe renders, `resolve --from <downloaded.mp4> --type video` to
294ledger it (not needed when generating via `resolve --type video` directly —
295that already ledgers the result).

Generate: video (resolve --type video, HeyGen avatar first)

resolve --type video "<intent>" is the default path. It generates a script-driven HeyGen avatar video first using the HyperFrames-selected credential (the server decides eligibility and charges; explicit API keys use API billing), falling back to local generative LTX only when HeyGen is unavailable, uncredentialed, or --local-only is passed. The two are non-substitutable outputs (a real presenter vs. a generic generative clip), so treat the fallback as "HeyGen wasn't reachable," not "upgrade the quality":

  • HeyGen avatar video (default): The CLI-bundled provider shells the heygen CLI — never the raw API — auto-picking a public avatar and a starfish voice (override with --avatar-id/--voice-id, threaded through as ctx.avatarId/ctx.voiceId). If the CLI reports not_authenticated, the provider prints a HyperFrames sign-in recommendation to stderr and falls through to LTX instead of hard-failing.
  • Local fallback: LTX 2.3 on MLX via dgrauet/ltx-2-mlx, the videogen ladder bundled in the HyperFrames CLI. Generative clips (t2v), spec-gated to RAM. Verified on 24GB: 512x320 x 33f with audio.

Every generating heygen call from media-use — TTS, avatar video, and catalog search — sends the allowlisted X-HeyGen-Client-Source: media-use header (persistent flag, works on every subcommand) via the shared CLI-internal HEYGEN_CLIENT_SOURCE_ARGV constant, so usage tags correctly in billing/resource meta and shows up in the API dashboards. Read-only discovery (avatar list, voice list) doesn't need it.

For structured bodies resolve --type video doesn't expose yet (a specific avatar_id/voice_id combination beyond the ctx overrides, or a pre-recorded audio_url instead of a script), the raw heygen video create recipe below remains the escape hatch. This direct command uses the separate HeyGen CLI credential and HeyGen/API billing, not the HyperFrames subscription:

# discover an avatar + a starfish voice, then create + wait
heygen avatar list --ownership public --limit 5
heygen voice list --engine starfish --limit 5
heygen video create --headers "X-HeyGen-Client-Source: media-use" --wait -d '{
  "type": "avatar",
  "avatar_id": "<avatar-id>",
  "script": "Your narration here.",
  "voice_id": "<voice-id>"
}'

Avatar videos are deterministic + script-driven (lip-sync from a script or a pre-recorded audio_url), distinct from the generative LTX clips. After a manual recipe renders, resolve --from <downloaded.mp4> --type video to ledger it (not needed when generating via resolve --type video directly — that already ledgers the result).

Raw lines 245 to 296
297### Image-to-video (animate any still into a talking clip)
299Not wired into `resolve --type video` (deferred — the `avatar` type covers
300the default script-driven case). `heygen video create` takes the raw
301`POST /v3/videos` body, so switching `type`
302from `avatar` to `image` animates **any image of a person** into a lip-synced
303talking video, with no avatar/photo-avatar creation step first. Point `image` at a
304public URL or an uploaded `asset_id`, and drive speech with a `script`+`voice_id`
305or a pre-recorded `audio_url`:
307```bash
308heygen video create --headers "X-HeyGen-Client-Source: media-use" --wait -d '{
309 "type": "image",
310 "image": { "type": "url", "url": "https://example.com/person.jpg" },
311 "script": "Your narration here.",
312 "voice_id": "<voice-id>"
313}'
314```
316Common optional fields: `title`, `resolution` (`4k`/`1080p`/`720p`),
317`aspect_ratio`, `remove_background`, `background`, `voice_settings`,
318`motion_prompt` + `expressiveness` (photo-avatar animation), and
319`callback_url`/`callback_id` for webhooks. Don't hardcode these from memory: the
320CLI self-documents the full, current body with
321`heygen video create --request-schema` (a discriminated union keyed on `type`),
322so read the schema rather than trusting a stale field list. For a still you'll
323reuse across many scripts, create a reusable **Photo Avatar** once instead
324(`heygen avatar create`). Ledger the result with
325`resolve --from <downloaded.mp4> --type video`. Docs:
326<https://developers.heygen.com/image-to-video>.

Image-to-video (animate any still into a talking clip)

Not wired into resolve --type video (deferred — the avatar type covers the default script-driven case). heygen video create takes the raw POST /v3/videos body, so switching type from avatar to image animates any image of a person into a lip-synced talking video, with no avatar/photo-avatar creation step first. Point image at a public URL or an uploaded asset_id, and drive speech with a script+voice_id or a pre-recorded audio_url:

heygen video create --headers "X-HeyGen-Client-Source: media-use" --wait -d '{
  "type": "image",
  "image": { "type": "url", "url": "https://example.com/person.jpg" },
  "script": "Your narration here.",
  "voice_id": "<voice-id>"
}'

Common optional fields: title, resolution (4k/1080p/720p), aspect_ratio, remove_background, background, voice_settings, motion_prompt + expressiveness (photo-avatar animation), and callback_url/callback_id for webhooks. Don't hardcode these from memory: the CLI self-documents the full, current body with heygen video create --request-schema (a discriminated union keyed on type), so read the schema rather than trusting a stale field list. For a still you'll reuse across many scripts, create a reusable Photo Avatar once instead (heygen avatar create). Ledger the result with resolve --from <downloaded.mp4> --type video. Docs: <https://developers.heygen.com/image-to-video>.

Raw lines 297 to 327
328## HEVC / H.265 sources
330HEVC/H.265 sources need no conversion for **render** (FFmpeg pre-decodes all
331input video) or for **preview** (auto-proxy transcodes and caches an H.264
332copy on first use, disable with `--no-proxy` or `media.autoProxy: false` in
333hyperframes.json). A manual H.264 proxy via `ffmpeg -i in.mp4 -c:v libx264
334-crf 18 proxy.mp4`, registered with `resolve --from`, remains available for
335edge cases (e.g. auto-proxy disabled, or ffmpeg unavailable at preview time).

HEVC / H.265 sources

HEVC/H.265 sources need no conversion for render (FFmpeg pre-decodes all input video) or for preview (auto-proxy transcodes and caches an H.264 copy on first use, disable with --no-proxy or media.autoProxy: false in hyperframes.json). A manual H.264 proxy via ffmpeg -i in.mp4 -c:v libx264 -crf 18 proxy.mp4, registered with resolve --from, remains available for edge cases (e.g. auto-proxy disabled, or ffmpeg unavailable at preview time).

Raw lines 328 to 335