ffmpeg-skill 1.13.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -29
- package/SKILL.md +46 -41
- package/bin/install.js +1 -1
- package/docs/contract.md +72 -10
- package/package.json +4 -2
- package/references/gotchas.md +85 -1
- package/references/scripts.md +146 -15
- package/scripts/_ass_overlay.py +155 -0
- package/scripts/_common.py +596 -37
- package/scripts/_contract.py +27 -9
- package/scripts/_platforms.py +251 -0
- package/scripts/caption.py +343 -93
- package/scripts/check.py +13 -16
- package/scripts/export.py +87 -11
- package/scripts/fit.py +21 -2
- package/scripts/graphics.py +381 -20
- package/scripts/look.py +35 -0
- package/scripts/overlay.py +72 -15
- package/scripts/render.py +313 -29
- package/scripts/report.py +73 -1
- package/templates/facebook.json +47 -0
- package/templates/linkedin.json +47 -0
- package/templates/podcast.json +22 -0
- package/templates/reels.json +47 -0
- package/templates/shorts.json +47 -0
- package/templates/tiktok.json +47 -0
- package/templates/x.json +47 -0
- package/templates/youtube-shorts.json +47 -0
- package/templates/youtube.json +47 -0
package/docs/contract.md
CHANGED
|
@@ -21,7 +21,7 @@ The contract is derived from the code that runs, not maintained beside it:
|
|
|
21
21
|
| Field | Meaning | Changes when |
|
|
22
22
|
|---|---|---|
|
|
23
23
|
| `contract_version` | shape of this document (`1.0`) | a key is renamed, removed or changes meaning |
|
|
24
|
-
| `skill.version` | the npm / package.json version (`1.
|
|
24
|
+
| `skill.version` | the npm / package.json version (`1.15.0`) | any release |
|
|
25
25
|
|
|
26
26
|
A release that adds a tool or a flag keeps `contract_version`; a breaking change to the
|
|
27
27
|
ToolSpec shape bumps it. Consumers pin on `contract_version` and read `skill.version`
|
|
@@ -88,19 +88,19 @@ spelling keeps working until 2.0.
|
|
|
88
88
|
|
|
89
89
|
| What 2.0 removes | Since | Replacement | To be ready today |
|
|
90
90
|
|---|---|---|---|
|
|
91
|
-
| The per-tool v1 success keys next to `result_v2` (`output`, `probe`, `commands`, `verified`, `verification` and each tool's own keys at the top level) | 1.
|
|
92
|
-
| `--crf` as an alias of `--quality` on every re-encoding tool that takes `--quality` (`export.py` keeps `--crf`: its preset chooses the encoder) | 1.
|
|
93
|
-
| `json` and `progress` in the MCP `inputSchema` | 1.
|
|
94
|
-
| `hdr` meaning "BT.2020 primaries *or* a PQ/HLG transfer" in `probe` | 1.
|
|
95
|
-
| Overwriting an existing output with only a warning | 1.
|
|
91
|
+
| The per-tool v1 success keys next to `result_v2` (`output`, `probe`, `commands`, `verified`, `verification` and each tool's own keys at the top level) | 1.15.0 | `result_v2`, promoted to the top level in 2.0 | Run with `FFMPEG_SKILL_RESULT_V2=1` and read `result_v2` (`metrics`, `notes`, `details`) instead of the top-level keys |
|
|
92
|
+
| `--crf` as an alias of `--quality` on every re-encoding tool that takes `--quality` (`export.py` keeps `--crf`: its preset chooses the encoder) | 1.15.0 | `--quality N` (the same CRF scale, codec-neutral) | Pass `--quality`; `--crf` warns on stderr and is marked in `--help` |
|
|
93
|
+
| `json` and `progress` in the MCP `inputSchema` | 1.15.0 | nothing: the transport sets them itself | Stop sending them from an MCP client; run the server with `FFMPEG_SKILL_MCP_LEAN=1` to see the 2.0 schema |
|
|
94
|
+
| `hdr` meaning "BT.2020 primaries *or* a PQ/HLG transfer" in `probe` | 1.15.0 | `hdr_signal` (true only for PQ / HLG / Dolby Vision); in 2.0 `hdr` takes that meaning | Key on `hdr_signal` for "is this a real HDR signal" and on `hdr_format` for the `BT.2020 SDR` case |
|
|
95
|
+
| Overwriting an existing output with only a warning | 1.15.0 | `--overwrite` as explicit consent (refused without it from 2.0) | Set `FFMPEG_SKILL_NO_OVERWRITE=1` (the recommended agent setting) and pass `--overwrite` where a replacement is intended |
|
|
96
96
|
|
|
97
97
|
## Skill
|
|
98
98
|
|
|
99
99
|
```json
|
|
100
100
|
{
|
|
101
101
|
"contract_version": "1.0",
|
|
102
|
-
"deprecated": [{"what": "...", "since": "1.
|
|
103
|
-
"skill": {"id": "ffmpeg-skill", "version": "1.
|
|
102
|
+
"deprecated": [{"what": "...", "since": "1.15.0", "replacement": "...", "removed_in": "2.0.0", "where": "cli | json | mcp | behaviour"}],
|
|
103
|
+
"skill": {"id": "ffmpeg-skill", "version": "1.15.0", "execution_mode": "local", "kind": "execution",
|
|
104
104
|
"entrypoints": {"cli": "...", "mcp": "...", "contract": "...", "doctor": "..."},
|
|
105
105
|
"not_provided": ["AI reasoning", "decisions", "production plans", "project IR", "approvals", "network access", "transcription engine"]},
|
|
106
106
|
"requirements": {"python": ">=3.9 (standard library only)", "ffmpeg": ">=5.0", "ffprobe": ">=5.0"},
|
|
@@ -128,7 +128,7 @@ One entry per tool under `tools`, sorted by id. Tool ids are stable:
|
|
|
128
128
|
| `output_schema` | what `--json` prints on stdout |
|
|
129
129
|
| `supports_dry_run`, `dry_run` | whether `--dry-run` plans without running ffmpeg or writing files |
|
|
130
130
|
| `supports_json` | whether `--json` exists |
|
|
131
|
-
| `supports_json_brief` | whether `--json-brief` exists (1.
|
|
131
|
+
| `supports_json_brief` | whether `--json-brief` exists (1.15.0): the same success document with `probe` replaced by a compact `summary` (`duration_s`, `width`, `height`, `fps`, `vcodec`, `acodec`, `channels`, and `lufs` when the tool measured one), `commands` replaced by the number of commands run, and the per-step `verification` list dropped (its verdict stays in `verified`). Tool-specific keys are unchanged, `--json`'s own output is unchanged, and a failure prints the same failure document either way |
|
|
132
132
|
| `mutates_input` | always `false`: no tool overwrites its input |
|
|
133
133
|
| `produces_artifact` | writes a file (media, PNG, HTML, EDL) |
|
|
134
134
|
| `verification` | `{required, tools}`: which tools to run on the output afterwards |
|
|
@@ -264,6 +264,46 @@ downstream analysis/preview, distinct from `export.py`'s delivery
|
|
|
264
264
|
presets) resolves to `proxy` - itself a mechanical resize + re-encode
|
|
265
265
|
with no opinion on which asset should be proxied or what for.
|
|
266
266
|
|
|
267
|
+
## Delivery table and templates (1.14)
|
|
268
|
+
|
|
269
|
+
`scripts/_platforms.py` is the one table every delivery tool reads. Per destination
|
|
270
|
+
(`tiktok`, `reels`, `shorts`, `youtube`, `youtube-hdr`, `youtube-av1`, `x`, `linkedin`,
|
|
271
|
+
`facebook`, `podcast`, plus the `broadcast` / `custom` compliance targets):
|
|
272
|
+
|
|
273
|
+
| field | meaning |
|
|
274
|
+
|---|---|
|
|
275
|
+
| `frame` | `{w, h, aspect}` the destination is delivered at, or `null` for an audio-only one |
|
|
276
|
+
| `fps` | the frame rate a delivery is conformed to (`null`: leave the source's alone) |
|
|
277
|
+
| `spec` | `check.py`'s row values: `max_duration`, `aspects`, `min_height`, `fps_max`, `codecs`, `max_bytes`, `lufs`, `lufs_tol`, `tp`, `sdr_only` |
|
|
278
|
+
| `safe` | the fraction of the frame the app's own UI covers, per edge (`top`, `bottom`, `left`, `right`) |
|
|
279
|
+
| `caption` | caption defaults a template uses: `size` (fraction of frame height), `position`, `box`, `outline`, `animate` |
|
|
280
|
+
| `preset` | the `export.py` preset that writes this destination |
|
|
281
|
+
| `check` | the `check.py` platform a delivery is verified against |
|
|
282
|
+
|
|
283
|
+
It is an internal module (leading underscore), not a tool: the public tool count is unchanged.
|
|
284
|
+
`check.py`'s `SPECS`, `export.py`'s `PRESETS` (each platform preset's frame and duration cap)
|
|
285
|
+
and `export.py`'s `PLATFORM_OF` are all built from it, so the loudness `export.py --normalize`
|
|
286
|
+
targets, the frame it writes, the cap it trims at and the spec `check.py` enforces are one
|
|
287
|
+
value. Two presets deliberately differ from their destination's row and say so in the code:
|
|
288
|
+
`youtube4k` delivers to YouTube at 2160p, and no `youtube*` preset trims at YouTube's 12-hour
|
|
289
|
+
limit (`check.py` reports it instead). `_platforms.resolve()` is the one alias map --
|
|
290
|
+
`youtube-shorts`/`yt-shorts` = `shorts`, `yt` = `youtube`, `instagram`/`ig` = `reels`,
|
|
291
|
+
`twitter` = `x`, `fb` = `facebook` -- and `check.py --platform`, `export.py --preset`,
|
|
292
|
+
`caption.py`/`graphics.py`/`overlay.py --platform`, `look.py --safe` and
|
|
293
|
+
`render.py --template` all accept those spellings.
|
|
294
|
+
|
|
295
|
+
New in the same release, all additive: `export.py --preset tiktok|shorts|linkedin|facebook`
|
|
296
|
+
(real presets, not aliases of `reels`/`youtube`), `--preset youtube-hdr` (HEVC Main10 keeping
|
|
297
|
+
the source's HDR tags; `kind: input` on an SDR source) and `--preset youtube-av1`
|
|
298
|
+
(`kind: missing_tool` when the build has neither SVT-AV1 nor libaom); `caption.py --platform`
|
|
299
|
+
and `graphics.py --platform` / `--margin` and `overlay.py --platform` (margins from the safe
|
|
300
|
+
zone, an explicit `--margin`/`--position` wins); `look.py --safe NAME`; `fit.py --fit blur`; `report.py --pack`;
|
|
301
|
+
`graphics.py --template sticker|hook|meme`; and `render.py --template NAME INPUT`
|
|
302
|
+
(`--cues/--srt/--logo/--title/--brand/--chapters/--fit/-o/--write-project/--list-templates`),
|
|
303
|
+
which fills a `templates/<name>.json` project shipped with the skill. `--template all` or a
|
|
304
|
+
comma-separated list renders every named destination and writes a `<stem>_pack.md` table.
|
|
305
|
+
A project may now carry `"template"` (the name it was filled from) and `"frame": {"fit": ...}`.
|
|
306
|
+
|
|
267
307
|
## Capabilities
|
|
268
308
|
|
|
269
309
|
Names: `ffmpeg`, `ffprobe`, `encoder:<name>`, `filter:<name>`, `bsf:<name>`,
|
|
@@ -321,7 +361,8 @@ typeface the caller didn't ask for.
|
|
|
321
361
|
|
|
322
362
|
Since 1.12 the same field also carries `scripts`: one entry per writing system the tools detect,
|
|
323
363
|
`{"ja": {"status": "available"|"missing"|"unknown", "file": "/path/to/font.ttc"|null}, "zh": ...,
|
|
324
|
-
"ko": ..., "ar": ..., "he": ..., "hi": ..., "
|
|
364
|
+
"ko": ..., "ar": ..., "he": ..., "hi": ..., "bn": ..., "ta": ..., "th": ..., "lo": ..., "ru": ...,
|
|
365
|
+
"el": ...}` (`bn`, `ta` and `lo` were added in 1.15). It answers "which
|
|
325
366
|
languages can this machine actually render", which no filter or encoder capability asks:
|
|
326
367
|
`available` means `fc-list :lang=<code>` (Linux/macOS) or a known system font file (Windows) covers
|
|
327
368
|
the script, `missing` means fontconfig knows none, `unknown` means there is no working fontconfig to
|
|
@@ -338,6 +379,25 @@ line to one screen width; a longer explanation, and the per-script files, are `-
|
|
|
338
379
|
`--lang`/`--language` (caption, graphics) is the hint that says whether Han-only text is Chinese,
|
|
339
380
|
Japanese or Korean.
|
|
340
381
|
|
|
382
|
+
Since 1.15 `fonts` also carries `emoji`: `{"mode": "color"|"png"|"mono"|"none", "color_font": "Noto
|
|
383
|
+
Color Emoji"|null, "color_font_file": "..."|null, "libass_color": true|false|null, "assets":
|
|
384
|
+
"/path"|null, "detail": "...", "fix": "..."}`. `libass_color` comes from a **render probe** — one
|
|
385
|
+
64x64 frame with an emoji cue through `subtitles=`, chroma-tested — because an installed colour
|
|
386
|
+
emoji family proves nothing: Noto Color Emoji installs cleanly on builds whose libass still draws a
|
|
387
|
+
monochrome outline. `null` means the probe was not run: `contract --json --static` (and every other
|
|
388
|
+
static/JSON-only path) skips it, exactly as it skips the rest of the environment detection. `mode`
|
|
389
|
+
is `color` when the probe says colour, else `png` when an emoji assets directory resolves, else
|
|
390
|
+
`mono` when some installed face has a glyph, else `none`. Informational like the rest of `fonts`:
|
|
391
|
+
it never moves `ok` or any tool's `usable`.
|
|
392
|
+
|
|
393
|
+
`caption.py`, `graphics.py` and `overlay.py` gained `--emoji auto|color|png|mono|none`,
|
|
394
|
+
`--emoji-assets DIR`, `--emoji-scale FLOAT` and `--emoji-max N` in 1.15; `graphics.py` also gained
|
|
395
|
+
`--text-render auto|ass|drawtext` and `--write-ass PATH`. New success keys: `emoji`
|
|
396
|
+
(`{"mode", "count", "clusters", "assets", "missing", "overlays"}`) on `caption.py` and
|
|
397
|
+
`graphics.py`, and `text_renderer` (`"ass"`|`"drawtext"`), `script` and `ass` (the generated file,
|
|
398
|
+
when one was written) on `graphics.py`. `--json-brief` carries `emoji.mode` and `emoji.count` only.
|
|
399
|
+
All additive: `contract_version` stays 1.0.
|
|
400
|
+
|
|
341
401
|
## Invocation
|
|
342
402
|
|
|
343
403
|
Structured arguments are the canonical way to call a tool, on the CLI or through MCP.
|
|
@@ -483,6 +543,8 @@ ffmpeg-skill contains no agent-specific code.
|
|
|
483
543
|
|
|
484
544
|
## Where things live
|
|
485
545
|
|
|
546
|
+
- `scripts/_platforms.py`: the delivery table (destinations, specs, safe zones) read by check/export/render/caption/graphics/look
|
|
547
|
+
- `templates/*.json`: the shipped delivery templates `render.py --template NAME` fills
|
|
486
548
|
- `scripts/_contract.py`: the generator (`--json`, `--static`, `doctor`)
|
|
487
549
|
- `bin/install.js`: `ffmpeg-skill contract` and `ffmpeg-skill doctor`
|
|
488
550
|
- `tests/test_contract.py`: schema, consistency (scripts = MCP = installer), MCP inputSchema derived from the contract (equality, determinism, drift, round trips), dry-run, JSON shapes, verification policy, real-media run
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ffmpeg-skill",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.0",
|
|
4
4
|
"description": "Agent Skill that gives coding agents (Claude Code, Cursor, Codex) a local video editor: 42 FFmpeg tools with a machine-readable contract, contract-derived MCP server, FFmpeg capability detection, probe-first / verify-last workflow. Cut, join, silence removal, fit, captions and karaoke, overlays, motion graphics, HDR to SDR, LUTs, audio clean-up and typed dynamics, sync with drift correction, multicam, loudness, delivery checks, project rendering, batch. No API keys, no cloud, no dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ffmpeg",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"files": [
|
|
30
30
|
"bin/",
|
|
31
31
|
"scripts/",
|
|
32
|
+
"templates/",
|
|
32
33
|
"mcp/",
|
|
33
34
|
"references/scripts.md",
|
|
34
35
|
"references/devices.md",
|
|
@@ -42,7 +43,8 @@
|
|
|
42
43
|
"scripts": {
|
|
43
44
|
"test": "python3 tests/test_all.py && python3 tests/test_contract.py",
|
|
44
45
|
"release-check": "bash tests/release_check.sh",
|
|
45
|
-
"demo": "
|
|
46
|
+
"demo": "python3 demos/build.py",
|
|
47
|
+
"demo:pipeline": "bash examples/make_demo.sh",
|
|
46
48
|
"contract": "python3 scripts/_contract.py --json",
|
|
47
49
|
"doctor": "python3 scripts/_contract.py doctor"
|
|
48
50
|
},
|
package/references/gotchas.md
CHANGED
|
@@ -109,7 +109,7 @@ libass and drawtext draw an empty box per character they have no glyph for, and
|
|
|
109
109
|
ffmpeg still exits 0 — a video full of tofu is the classic "it worked" failure.
|
|
110
110
|
Since 1.12 `caption.py`, `graphics.py` and `overlay.py --text` detect the script
|
|
111
111
|
of the text they are about to draw (Japanese, Chinese, Korean, Arabic, Hebrew,
|
|
112
|
-
Devanagari, Thai, Cyrillic, Greek) and resolve a font file that covers it,
|
|
112
|
+
Devanagari, Bengali, Tamil, Thai, Lao, Cyrillic, Greek) and resolve a font file that covers it,
|
|
113
113
|
printing one line — `font: /usr/share/fonts/.../wqy-zenhei.ttc (covers ko)`.
|
|
114
114
|
**No font for the script is a failed job** (`kind: input`), not a warning.
|
|
115
115
|
|
|
@@ -145,6 +145,75 @@ printing one line — `font: /usr/share/fonts/.../wqy-zenhei.ttc (covers ko)`.
|
|
|
145
145
|
letterforms. Nothing in the tools checks this, so on an unknown machine a
|
|
146
146
|
caption is the safe place for Arabic/Hebrew.
|
|
147
147
|
|
|
148
|
+
**Shaping is a second problem, and it is not the same one.** A font that covers
|
|
149
|
+
the script still has to be *shaped*: Devanagari matras are reordered, Thai and
|
|
150
|
+
Lao marks re-cluster. drawtext does bidi and Arabic joining correctly on a build
|
|
151
|
+
compiled with fribidi (so Arabic and Hebrew are already right today), but it
|
|
152
|
+
never reorders or re-clusters, because it does not use harfbuzz on any build.
|
|
153
|
+
libass does. Since 1.15 `graphics.py` therefore renders a shaping script
|
|
154
|
+
(`hi bn ta te kn ml gu pa si th lo km my`) through libass automatically — one
|
|
155
|
+
generated `<output>_gfx.ass` next to the file, `text_renderer: "ass"` in the
|
|
156
|
+
JSON — and `--text-render auto|ass|drawtext` overrides it; `--text-render
|
|
157
|
+
drawtext` with such a script is a refusal, not a wrong frame. Latin, CJK and
|
|
158
|
+
Arabic output is unchanged. `overlay.py --text` has no ASS route yet and refuses
|
|
159
|
+
a shaping script, naming `caption.py`/`graphics.py`.
|
|
160
|
+
|
|
161
|
+
### Emoji
|
|
162
|
+
Colour emoji need **either** a libass that can draw colour glyphs **or** a
|
|
163
|
+
directory of PNGs — and on most builds only the second one exists.
|
|
164
|
+
|
|
165
|
+
- **An installed colour emoji font proves nothing.** Noto Color Emoji installs
|
|
166
|
+
cleanly on Ubuntu and libass on the distro ffmpeg still renders a monochrome
|
|
167
|
+
outline (`Glyph 0x1F389 not found, broken font? Trying all charmaps`). The
|
|
168
|
+
only honest test is a render, which is what `doctor` runs:
|
|
169
|
+
`doctor --json` → `.fonts.emoji` (`mode`: `color` / `png` / `mono` / `none`,
|
|
170
|
+
plus `libass_color` from that render). `contract --json --static` is the way
|
|
171
|
+
to get the contract without running it; `doctor` itself always probes.
|
|
172
|
+
- **drawtext cannot load an emoji font at all.** Not a degraded render: a
|
|
173
|
+
CBDT/sbix face fails filter initialisation outright (`Could not set font size
|
|
174
|
+
to 48 pixels: invalid library handle`, and at the font's own strike
|
|
175
|
+
`Monocromatic (1bpp) fonts are not supported.`) and no file is written. That
|
|
176
|
+
is why the colour route is a PNG overlay, never a drawtext font.
|
|
177
|
+
- **PNGs**: `--emoji-assets DIR`, a directory of files named by code point in
|
|
178
|
+
the Twemoji/Noto convention — lowercase hex joined by `-`: `1f389.png`,
|
|
179
|
+
`1f1ef-1f1f5.png`, `1f469-200d-1f4bb.png`. Twemoji's `assets/72x72`
|
|
180
|
+
(CC-BY 4.0) and Noto Emoji's `png/128` (OFL/Apache-2.0) are the two people
|
|
181
|
+
already have. **The skill never downloads anything**: a missing directory is
|
|
182
|
+
`kind: input`, never a silent fetch. The same directory can come from
|
|
183
|
+
`brand.json` (`styles.caption.emoji_assets`) or `FFMPEG_SKILL_EMOJI_ASSETS`.
|
|
184
|
+
- **Mixed lines are the normal case.** The text stays in the ASS with an
|
|
185
|
+
invisible placeholder reserving exactly the emoji's box, and the PNG is
|
|
186
|
+
composited on top after the `ass=` filter — libass keeps the shaping and the
|
|
187
|
+
karaoke, including inside a `\kf` run (the placeholder is its own
|
|
188
|
+
zero-duration segment).
|
|
189
|
+
- **What counts as an emoji.** A cluster only starts at an emoji *base*.
|
|
190
|
+
U+200D ZWJ and U+200C ZWNJ are ordinary Indic/Persian orthography (`क्ष` is
|
|
191
|
+
ka + virama + ZWJ + ssa) and are glue only *between* two emoji; a VS16 or a
|
|
192
|
+
skin-tone modifier after a plain letter is not an emoji either. U+FE0E
|
|
193
|
+
(VS15) asks for the character, so `❤︎` is text and `❤️` is an emoji.
|
|
194
|
+
- **Placement tolerance.** The position comes from the same averaged em table
|
|
195
|
+
the wrap uses, so an emoji at the start or end of a line is exact and one in
|
|
196
|
+
the middle of a Latin line drifts by the accumulated rounding of the
|
|
197
|
+
characters before it — **measured at 17 px, 0.28 em, about 3 % of the line
|
|
198
|
+
width** on a 24 px caption over a 1280-wide frame, always landing inside the
|
|
199
|
+
reserved gap rather than on a glyph. It never leaves the safe area. An RTL
|
|
200
|
+
line (Arabic, Hebrew) is laid out right-to-left, so the position is measured
|
|
201
|
+
from the rendered end of the line, not the logical prefix.
|
|
202
|
+
- **Degraded paths are honest, not silent.** `mode: mono` means libass draws
|
|
203
|
+
whatever glyph the text font has (it has a fallback chain), exits 0, and says
|
|
204
|
+
so in a `warning:` line and in `notes` — so a `graphics.py` run that would
|
|
205
|
+
otherwise use drawtext, which loads one font file and has **no** fallback
|
|
206
|
+
chain, is routed to libass rather than reporting a monochrome glyph it would
|
|
207
|
+
actually draw as an empty box; a run pinned to `--text-render drawtext`
|
|
208
|
+
degrades to `none` and says so. `mode: none` strips the cluster from the drawn
|
|
209
|
+
text (a missing decoration must not fail a delivery) but refuses a
|
|
210
|
+
`graphics.py` template whose text is *only* emoji — that frame would be blank.
|
|
211
|
+
`--emoji-max` (default 60) caps the overlay count; `--emoji-max 0` means none.
|
|
212
|
+
- **`--animate` moves the PNG too.** The overlay gets a matching alpha fade, so
|
|
213
|
+
the emoji arrives and leaves with the line instead of popping in.
|
|
214
|
+
- `overlay.py --text` has no PNG route: `--emoji png|color` there names
|
|
215
|
+
`caption.py`/`graphics.py` instead.
|
|
216
|
+
|
|
148
217
|
### Reframing, fps and duration
|
|
149
218
|
`--fit crop` to reach 9:16 from 16:9 throws away 70 % of the width: a wide shot
|
|
150
219
|
loses people at the edges. Check the sheet; pad (bars), `--crop-x`/`--crop-y`
|
|
@@ -158,6 +227,21 @@ visibly choppy for sports, gaming, drone pans. Keep 60 when the platform allows.
|
|
|
158
227
|
trim it drops two thirds of the words. Ask which, or propose a highlight cut with
|
|
159
228
|
`scenes.py`.
|
|
160
229
|
|
|
230
|
+
### Platform safe zones
|
|
231
|
+
Every vertical app draws its own UI over the delivery: TikTok covers roughly the
|
|
232
|
+
bottom 22 % (description and caption block), the right 14 % (like/comment/share
|
|
233
|
+
column) and the top 10 % (status bar and tabs); Reels 20/12/8 %; Shorts 18/12/6 %.
|
|
234
|
+
The feed destinations (YouTube, X, LinkedIn, Facebook) have no persistent overlay
|
|
235
|
+
and use the conventional 5 % title-safe border instead. A file can pass every
|
|
236
|
+
`check.py` row and still be unreadable because the caption sits under the
|
|
237
|
+
description.
|
|
238
|
+
|
|
239
|
+
The fractions live in one table (`scripts/_platforms.py`). `caption.py --platform
|
|
240
|
+
NAME`, `graphics.py --platform NAME` and `overlay.py --platform NAME` take their
|
|
241
|
+
margins from it, `render.py --template NAME` passes it to all three, and
|
|
242
|
+
`look.py --safe tiktok` shades the zones on a frame or contact sheet so you can
|
|
243
|
+
see what the app covers. An explicit `--margin` always wins.
|
|
244
|
+
|
|
161
245
|
### Dimensions and rotation
|
|
162
246
|
`yuv420p` needs even width/height; `fit.py` and `export.py` round to even values
|
|
163
247
|
automatically. Phone footage often carries a `rotation` tag; `probe.py` reports
|
package/references/scripts.md
CHANGED
|
@@ -29,6 +29,7 @@ between tools.
|
|
|
29
29
|
- silence.py — remove dead air / jump cuts
|
|
30
30
|
- join.py — concatenate with transitions
|
|
31
31
|
- render.py — the whole edit in one project.json
|
|
32
|
+
- delivery templates — one command per destination (`--template`)
|
|
32
33
|
- scenes.py — scene changes and highlight candidates
|
|
33
34
|
- check.py — pre-delivery compliance
|
|
34
35
|
- batch.py — same recipe over a folder, cached
|
|
@@ -76,7 +77,7 @@ re-encode, the result's `lossless_alternative` names the nearest keyframe
|
|
|
76
77
|
### fit.py — target duration and/or aspect, rotate/flip
|
|
77
78
|
```
|
|
78
79
|
fit.py INPUT [--duration T --method speed|trim [--from-center] [--max-speed 4]]
|
|
79
|
-
[--aspect 16:9|9:16|1:1|4:5|W:H --fit pad|crop [--width W] [--height H] [--pad-color black] [--pad-fill color|blur [--pad-blur 20]]]
|
|
80
|
+
[--aspect 16:9|9:16|1:1|4:5|W:H --fit pad|crop|blur [--width W] [--height H] [--pad-color black] [--pad-fill color|blur [--pad-blur 20]]]
|
|
80
81
|
[--rotate 90|180|270] [--flip h|v] [--fps N] [-o OUT]
|
|
81
82
|
```
|
|
82
83
|
`speed` retimes video and audio together (pitch-preserving `atempo`); it
|
|
@@ -94,7 +95,13 @@ automatically even without it.
|
|
|
94
95
|
`--pad-fill blur` fills the letterbox/pillarbox bars with a blurred, scaled-to-cover copy
|
|
95
96
|
of the frame (the look every phone editor gives landscape footage posted as a Short/Reel)
|
|
96
97
|
instead of the solid `--pad-color`; `--pad-blur` is the blur radius. `export.py --fit pad`
|
|
97
|
-
takes the same two flags.
|
|
98
|
+
takes the same two flags. `--fit blur` (1.14) is the same fill named in one word and with the
|
|
99
|
+
background dimmed (`eq brightness=-0.15`) so the picture in front reads as the subject: the
|
|
100
|
+
whole frame is kept (nothing cropped), the borders are a blurred copy of it rather than black.
|
|
101
|
+
A delivery template asks for it as `"frame": {"aspect": "9:16", "fit": "blur"}`, or
|
|
102
|
+
`render.py --template tiktok clip.mp4 --fit blur`. The dimming is applied to SDR sources only:
|
|
103
|
+
an `eq` on PQ/HLG code values is not the −15 % perceptual dim it is on SDR, so an HDR source
|
|
104
|
+
keeps a blurred but undimmed background (and is never silently tone-mapped); `info` says so.
|
|
98
105
|
|
|
99
106
|
### crop.py — crop to an exact pixel rectangle
|
|
100
107
|
```
|
|
@@ -430,6 +437,56 @@ more than two steps or the user is likely to ask for changes: edit the JSON,
|
|
|
430
437
|
re-render, and the result is reproducible. `--dry-run --json` prints the
|
|
431
438
|
complete command plan for review.
|
|
432
439
|
|
|
440
|
+
### Delivery templates — one command per destination (1.14)
|
|
441
|
+
```
|
|
442
|
+
render.py --template tiktok INPUT [--cues cues.txt | --srt subs.srt] [--logo logo.png] [--title "..."]
|
|
443
|
+
[--brand brand.json] [--chapters chapters.txt] [--fit crop|pad|blur] [-o OUT]
|
|
444
|
+
render.py --template all INPUT ... # or a comma list: one delivery per destination + <stem>_pack.md
|
|
445
|
+
render.py --list-templates # the table below, from the running install
|
|
446
|
+
render.py --template tiktok INPUT --write-project project.json # fill it, edit it, render it later
|
|
447
|
+
```
|
|
448
|
+
A template is a `render.py` project shipped in `templates/<name>.json` with `$INPUT`, `$OUTPUT`,
|
|
449
|
+
`$CUES`/`$SRT`, `$LOGO`, `$TITLE`, `$BRAND` and `$CHAPTERS` placeholders. Filling it substitutes
|
|
450
|
+
what the run was given and **drops any block whose placeholder has no value** — no `--logo` means
|
|
451
|
+
no overlay stage at all, not an overlay of nothing. The filled project then renders through the
|
|
452
|
+
normal stages, so `--dry-run --json`, `--stop-after` and the work directory behave as always. An
|
|
453
|
+
unknown name is refused (`kind: input`) with the list. Output defaults to
|
|
454
|
+
`<input>_<template>.mp4` **next to the input** (`.m4a` for an audio-only destination such as
|
|
455
|
+
`podcast`) — the same rule for one template and for a pack, whose `-o` names the directory.
|
|
456
|
+
Alias spellings are accepted everywhere one name is: `youtube-shorts`/`yt-shorts` = `shorts`,
|
|
457
|
+
`yt` = `youtube`, `instagram`/`ig` = `reels`, `twitter` = `x`, `fb` = `facebook`
|
|
458
|
+
(`check.py --platform`, `export.py --preset`, `caption.py`/`graphics.py`/`overlay.py
|
|
459
|
+
--platform`, `look.py --safe`, `render.py --template`).
|
|
460
|
+
|
|
461
|
+
Under `--dry-run` a pack prints every child's planned commands and its table reads `planned`
|
|
462
|
+
with no size or duration: nothing was encoded, so nothing is reported as verified. `--chapters`
|
|
463
|
+
reaches a pack's audio destination like it does the single-template form.
|
|
464
|
+
|
|
465
|
+
Each template's frame, duration limit, loudness target and safe zones come from the one delivery
|
|
466
|
+
table (`scripts/_platforms.py`). Safe zones are the fraction of the frame the app's own UI covers;
|
|
467
|
+
the template places captions, graphics **and the `--logo` overlay** clear of them, and
|
|
468
|
+
`caption.py --platform`, `graphics.py --platform` and `overlay.py --platform` apply them to a
|
|
469
|
+
hand-built step:
|
|
470
|
+
|
|
471
|
+
| template | frame | max duration | loudness | safe top | safe bottom | safe left | safe right |
|
|
472
|
+
|---|---|---|---|---|---|---|---|
|
|
473
|
+
| `tiktok` | 1080x1920 (9:16) | 600 s | -14 LUFS / -1 dBTP | 0.10 | 0.22 | 0.05 | 0.14 |
|
|
474
|
+
| `reels` | 1080x1920 (9:16) | 90 s | -14 LUFS / -1 dBTP | 0.08 | 0.20 | 0.05 | 0.12 |
|
|
475
|
+
| `shorts` | 1080x1920 (9:16) | 180 s | -14 LUFS / -1 dBTP | 0.06 | 0.18 | 0.05 | 0.12 |
|
|
476
|
+
| `youtube-shorts` | 1080x1920 (9:16) | 180 s | -14 LUFS / -1 dBTP | 0.06 | 0.18 | 0.05 | 0.12 |
|
|
477
|
+
| `youtube` | 1920x1080 (16:9) | 43200 s | -14 LUFS / -1 dBTP | 0.05 | 0.05 | 0.05 | 0.05 |
|
|
478
|
+
| `x` | 1280x720 (16:9) | 140 s | -14 LUFS / -1 dBTP | 0.05 | 0.05 | 0.05 | 0.05 |
|
|
479
|
+
| `linkedin` | 1080x1080 (1:1) | 600 s | -14 LUFS / -1 dBTP | 0.05 | 0.05 | 0.05 | 0.05 |
|
|
480
|
+
| `facebook` | 1920x1080 (16:9) | 14400 s | -14 LUFS / -1 dBTP | 0.05 | 0.05 | 0.05 | 0.05 |
|
|
481
|
+
| `podcast` | audio only | — | -16 LUFS / -1 dBTP | 0.00 | 0.00 | 0.00 | 0.00 |
|
|
482
|
+
|
|
483
|
+
`podcast` is audio: silence trim, −16 LUFS / −1 dBTP, chapter markers when `--chapters` is given,
|
|
484
|
+
and `check.py --platform podcast`. `--template all` renders `tiktok, reels, shorts, youtube, x,
|
|
485
|
+
linkedin, facebook` (not the audio template, not the `youtube-shorts` alias) into
|
|
486
|
+
`<stem>_<platform>.mp4`, runs each platform's check and writes `<stem>_pack.md` with one row per
|
|
487
|
+
destination; `report.py --pack <stem>_pack.md` renders that table as a single HTML page. A pack
|
|
488
|
+
whose destinations did not all pass exits non-zero with the per-destination rows in `pack`.
|
|
489
|
+
|
|
433
490
|
### scenes.py — scene changes and highlight candidates
|
|
434
491
|
```
|
|
435
492
|
scenes.py INPUT [--threshold 10] [--min-scene 1] [--highlights N [--target SECONDS] [--max-scene 15]] [--edl picks.txt] [--sheet scenes.png] [--json]
|
|
@@ -446,7 +503,7 @@ and why (energy, scene length).
|
|
|
446
503
|
|
|
447
504
|
### check.py — pre-delivery compliance
|
|
448
505
|
```
|
|
449
|
-
check.py INPUT --platform youtube|shorts|reels|tiktok|x|linkedin|broadcast|podcast|custom [--no-loudness] [--json]
|
|
506
|
+
check.py INPUT --platform youtube|shorts|reels|tiktok|x|linkedin|facebook|broadcast|podcast|custom [--no-loudness] [--json]
|
|
450
507
|
[--max-duration S] [--aspect 9:16] [--lufs -14] [--tp -1] [--max-mb N]
|
|
451
508
|
```
|
|
452
509
|
PASS/WARN/FAIL per check with the script that fixes it. Run it as the final
|
|
@@ -458,7 +515,11 @@ line, not FAIL: name the platform when the file is a delivery for it.
|
|
|
458
515
|
stereo, WARN above — podcast players downmix 5.1 unpredictably) and `chapters`
|
|
459
516
|
(PASS when the container carries at least one marker, WARN `none` otherwise —
|
|
460
517
|
write them with `metadata.py --chapters`). Neither can FAIL a delivery, and
|
|
461
|
-
neither appears for another platform.
|
|
518
|
+
neither appears for another platform. Since 1.14 the per-platform numbers (duration, aspects,
|
|
519
|
+
minimum height, fps, codecs, size, LUFS, true peak, SDR-only) come from the one delivery table
|
|
520
|
+
in `scripts/_platforms.py`, which `export.py` and the `render.py` templates read too -- so the
|
|
521
|
+
loudness a preset normalises to and the loudness this tool checks are the same value by
|
|
522
|
+
construction, not by two lists agreeing.
|
|
462
523
|
|
|
463
524
|
### batch.py — same recipe over a folder, cached
|
|
464
525
|
```
|
|
@@ -486,18 +547,50 @@ Inside this skill, call the scripts directly; the server is for other hosts.
|
|
|
486
547
|
|
|
487
548
|
### graphics.py — motion-graphics templates
|
|
488
549
|
```
|
|
489
|
-
graphics.py INPUT --template lower-third|title|chapter|progress|countdown|bug
|
|
490
|
-
[--
|
|
550
|
+
graphics.py INPUT --template lower-third|title|chapter|progress|countdown|bug|sticker|hook|meme
|
|
551
|
+
[--name] [--title] [--subtitle] [--text] [--top] [--bottom] [--duration 3]
|
|
552
|
+
[--from N] [--start S] [--end E] [--position CORNER] [--margin PX] [--platform NAME]
|
|
553
|
+
[--brand brand.json] [--primary RRGGBB] [--scale 1.0] [--lang XX]
|
|
554
|
+
[--text-render auto|ass|drawtext] [--write-ass OUT.ass]
|
|
555
|
+
[--emoji auto|color|png|mono|none] [--emoji-assets DIR] [--emoji-scale 1.0] [--emoji-max 60] [-o OUT]
|
|
491
556
|
```
|
|
492
557
|
Drawn with drawbox/drawtext/overlay — no PNG assets needed. Sizes scale with
|
|
493
558
|
the frame's short side; colours, font and safe margin come from `--brand`.
|
|
494
559
|
Lower-third slides in over 0.4 s and out over 0.3 s; title/chapter/bug fade.
|
|
495
560
|
Non-Latin `--name`/`--title`/`--subtitle` text picks a font file by script the
|
|
496
561
|
same way `caption.py` does (`--lang XX` disambiguates Han-only text; no font for
|
|
497
|
-
the script fails the job).
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
562
|
+
the script fails the job). Arabic and Hebrew are already correct through drawtext
|
|
563
|
+
on a build with `--enable-libfribidi` (bidi + joining). What drawtext cannot do on
|
|
564
|
+
any build is reorder and re-cluster — Devanagari matras, Thai/Lao mark stacking —
|
|
565
|
+
because it does not use harfbuzz. Since 1.15 `--text-render auto` (the default)
|
|
566
|
+
therefore routes those scripts through libass: the template's geometry is written
|
|
567
|
+
as a generated `<output>_gfx.ass` (`--write-ass PATH` names it) and burned with
|
|
568
|
+
`ass=`, reported as `text_renderer: "ass"` with `script` and `ass` in the JSON.
|
|
569
|
+
Latin/CJK/Arabic frames are pixel-identical to 1.14 (the drawtext command
|
|
570
|
+
line is not: since 1.15 every drawn label is passed as `textfile=<tmp>:expansion=none`
|
|
571
|
+
rather than `text=`, so a `--dry-run` compared against 1.14 differs by design). `--text-render ass` forces the
|
|
572
|
+
route; `--text-render drawtext` with a shaping script is refused by name rather
|
|
573
|
+
than rendering a wrong frame. See `references/gotchas.md#fonts-by-script`.
|
|
574
|
+
`--emoji*` works as on `caption.py` below; a template whose text is *only* emoji
|
|
575
|
+
and that this machine can draw none of is `kind: input`, because that frame would
|
|
576
|
+
be blank. `--emoji none` strips the clusters from the drawn text, and a job whose
|
|
577
|
+
emoji would fall to `mode: mono` is routed through libass (which has a font
|
|
578
|
+
fallback chain) instead of drawtext (which does not, and would draw an empty box).
|
|
579
|
+
|
|
580
|
+
Every drawn label goes to drawtext as `textfile=<path>:expansion=none`. The file
|
|
581
|
+
is UTF-8, mode 0600, in a private per-run temp directory created with
|
|
582
|
+
`tempfile.mkdtemp()`, written only when the command that names it actually runs
|
|
583
|
+
(so `--dry-run` and the ASS route write nothing) and removed when the process
|
|
584
|
+
ends. A plan printed by `--dry-run` therefore names a path that does not exist.
|
|
585
|
+
|
|
586
|
+
All three are usable from a `render.py` project too: a `graphics[]` entry takes `text`, `top`,
|
|
587
|
+
`bottom`, `duration`, `margin` and `platform` alongside the older keys.
|
|
588
|
+
1.14 adds three social templates: `sticker` (`--text`, a filled chip that pops in at
|
|
589
|
+
`--position`), `hook` (`--title --duration 3`, the full-width opening card with a thin progress
|
|
590
|
+
bar along the top that empties as the card's time runs out) and `meme` (`--top` / `--bottom`,
|
|
591
|
+
upper-case white with a heavy black outline). `--platform NAME` takes each edge's margin from
|
|
592
|
+
that destination's safe zone (see "Delivery templates" above), so a sticker stays off TikTok's
|
|
593
|
+
like column; `--margin PX` sets all four edges and wins over `--platform`.
|
|
501
594
|
|
|
502
595
|
### brand.json — one file for fonts, colours, logo, margins
|
|
503
596
|
```json
|
|
@@ -555,9 +648,13 @@ seen before, and fix or report what fails.
|
|
|
555
648
|
look.py INPUT [--tiles 4x3] [--width 1280] [-o sheet.png] # contact sheet with timecodes
|
|
556
649
|
look.py INPUT --at 2.5 [--at 7] [-o basename] # single frames -> basename_2.500s.png
|
|
557
650
|
look.py BEFORE --compare AFTER --at 4 [-o cmp.png] # side-by-side frame
|
|
651
|
+
look.py INPUT --safe tiktok [--at 3] # shade what the app's UI covers
|
|
558
652
|
```
|
|
559
653
|
Outputs PNG. View it with the Read tool (or any image viewer) and judge the
|
|
560
|
-
frame like an editor would. Use `--compare` to show before/after to the user.
|
|
654
|
+
frame like an editor would. Use `--compare` to show before/after to the user. `--safe NAME` (1.14) shades the zones that
|
|
655
|
+
destination's own UI covers -- TikTok's description block and like column, the Reels/Shorts
|
|
656
|
+
chrome -- on the sheet or the frame, so "is the caption readable" can be answered about the app
|
|
657
|
+
rather than about the file.
|
|
561
658
|
|
|
562
659
|
### caption.py — subtitles (static, animated, karaoke)
|
|
563
660
|
```
|
|
@@ -566,7 +663,8 @@ caption.py INPUT --srt FILE | --ass FILE | --text CUES.txt [--write-srt OUT.srt]
|
|
|
566
663
|
[--max-lines N] [--min-duration S]
|
|
567
664
|
[--font NAME] [--fonts-dir DIR] [--size N] [--color RRGGBB] [--outline N] [--outline-color RRGGBB]
|
|
568
665
|
[--bold] [--box] [--position bottom|top|center|top-left|...] [--margin N]
|
|
569
|
-
[--animate none|fade|pop|slide] [--karaoke [--highlight-color RRGGBB]] [--write-ass OUT.ass]
|
|
666
|
+
[--animate none|fade|pop|slide] [--karaoke [--highlight-color RRGGBB]] [--write-ass OUT.ass]
|
|
667
|
+
[--emoji auto|color|png|mono|none] [--emoji-assets DIR] [--emoji-scale 1.0] [--emoji-max 60] [-o OUT]
|
|
570
668
|
caption.py --text CUES.txt --write-srt OUT.srt # generate the SRT only
|
|
571
669
|
```
|
|
572
670
|
Text cue format, one per line: `0:00-0:03 Hello`, `00:00:03.500 --> 00:00:06 Two | lines`,
|
|
@@ -580,7 +678,18 @@ word from `--color` to `--highlight-color` across the cue; `--karaoke-timing
|
|
|
580
678
|
energy` (default) follows the speech loudness in the audio, `even` splits the
|
|
581
679
|
cue equally (word timing is derived, not transcribed). The ASS is kept next to the
|
|
582
680
|
user can hand-tune timings and re-run with `--ass`.
|
|
583
|
-
|
|
681
|
+
Emoji (1.15): `--emoji-assets DIR` is a directory of PNGs named by code point
|
|
682
|
+
(`1f389.png`, `1f1ef-1f1f5.png`, `1f469-200d-1f4bb.png` — the Twemoji/Noto
|
|
683
|
+
convention), also read from `brand.json` `styles.caption.emoji_assets` and
|
|
684
|
+
`FFMPEG_SKILL_EMOJI_ASSETS`. With one, the cue text keeps its place in the ASS
|
|
685
|
+
with an invisible placeholder reserving the emoji's box and each PNG is
|
|
686
|
+
composited on top (`--emoji-scale` sizes the box, `--emoji-max` caps the count).
|
|
687
|
+
Without one the run still succeeds and says `emoji: {"mode": "mono"}` plus a
|
|
688
|
+
warning; `--emoji none` strips them; `--emoji color` insists on a colour-capable
|
|
689
|
+
libass and refuses otherwise. Nothing is ever downloaded. What this machine can
|
|
690
|
+
do: `doctor --json` → `.fonts.emoji`. Details: `references/gotchas.md#emoji`.
|
|
691
|
+
|
|
692
|
+
Readable by default (1.12, rebalanced in 1.15): every cue is wrapped to the safe area (90 % of the
|
|
584
693
|
frame width) at the chosen `--size`, measured per script — CJK and Thai count a
|
|
585
694
|
full em per character, Latin per character from a table read off DejaVu Sans (so
|
|
586
695
|
an all-caps line measures as wide as it draws), Cyrillic/Greek about 0.55,
|
|
@@ -634,9 +743,13 @@ stream selection would have picked.
|
|
|
634
743
|
### overlay.py — logo, image, title, video picture-in-picture, chroma key
|
|
635
744
|
```
|
|
636
745
|
overlay.py INPUT --image PNG [--scale W | --scale-percent P] | --text "..." [--font-file F.ttf] [--font-size N] [--box]
|
|
746
|
+
[--emoji auto|mono|none] [--emoji-assets DIR]
|
|
637
747
|
| --video CLIP [--chromakey COLOR [--chromakey-similarity 0-1] [--chromakey-blend 0-1]]
|
|
638
|
-
[--position top-right|bottom-left|center|X,Y] [--margin N] [--start T] [--end T] [--fade S] [--opacity 0-1] [-o OUT]
|
|
748
|
+
[--position top-right|bottom-left|center|X,Y] [--margin N] [--platform NAME] [--start T] [--end T] [--fade S] [--opacity 0-1] [-o OUT]
|
|
639
749
|
```
|
|
750
|
+
`--platform NAME` (1.14) takes each edge's margin from that destination's safe zone
|
|
751
|
+
(`scripts/_platforms.py`), so a template's top-left logo clears TikTok's status bar instead of
|
|
752
|
+
sitting 24 px into it; an explicit `--margin` (or a brand `safe_margin`) wins.
|
|
640
753
|
Alpha in PNGs is respected. Fades apply to the overlay only; the video keeps
|
|
641
754
|
playing. `--video` composites a second video as a picture-in-picture layer
|
|
642
755
|
(same position/scale/opacity/time-range knobs as `--image`); only the main
|
|
@@ -646,6 +759,14 @@ input's audio is kept, the PiP layer's own audio is dropped. `--chromakey`
|
|
|
646
759
|
`--fade S` fades the overlay in at `--start` (or 0); the fade-out happens
|
|
647
760
|
only at `--end`, so a logo with no `--end` stays to the last frame.
|
|
648
761
|
|
|
762
|
+
Since 1.15 the drawn text goes to drawtext in a **file** (`textfile=`,
|
|
763
|
+
`expansion=none`), so `'` and `%` survive verbatim — `--text "it's 100% done"`
|
|
764
|
+
used to lose both. `overlay.py` still draws through drawtext, which cannot shape
|
|
765
|
+
Devanagari/Thai-class scripts and cannot load a colour emoji font: a shaping
|
|
766
|
+
script is refused by name pointing at `caption.py`/`graphics.py`, and
|
|
767
|
+
`--emoji png|color` is refused the same way (`--emoji mono`, the default here,
|
|
768
|
+
draws whatever glyph the text font has; `--emoji none` strips them).
|
|
769
|
+
|
|
649
770
|
### sync.py — offset detection, alignment, drift correction
|
|
650
771
|
```
|
|
651
772
|
sync.py REFERENCE SECOND [--json] [--max-offset 30] [--analyze-seconds 120] [--fix-drift [--drift-window 60]]
|
|
@@ -772,7 +893,8 @@ integrated loudness ended more than 1 LU from the target because of it.
|
|
|
772
893
|
|
|
773
894
|
### export.py — delivery presets
|
|
774
895
|
```
|
|
775
|
-
export.py INPUT --preset youtube|youtube4k|reels|x|prores|h265|gif
|
|
896
|
+
export.py INPUT --preset youtube|youtube4k|reels|tiktok|shorts|linkedin|facebook|x|youtube-hdr|youtube-av1|prores|h265|gif|copy
|
|
897
|
+
[--fit pad|crop] [--no-scale] [--allow-long] [--crf N] [--normalize] [-o OUT]
|
|
776
898
|
export.py --list
|
|
777
899
|
```
|
|
778
900
|
Scales into the preset frame (pad by default), tags BT.709, sets `+faststart`,
|
|
@@ -784,6 +906,15 @@ that fixes it -- or pass `--normalize`, which runs that call on the written
|
|
|
784
906
|
file itself (audio re-encoded, video copied; `loudness.normalized: true`) so a
|
|
785
907
|
platform export is one command instead of export, loudness, export again.
|
|
786
908
|
|
|
909
|
+
Since 1.14 each social destination is its own preset rather than an alias: `tiktok`
|
|
910
|
+
(1080x1920, max 600 s), `shorts` (1080x1920, max 180 s), `reels` (1080x1920, max 90 s),
|
|
911
|
+
`linkedin` (1080x1080), `facebook` (1920x1080), each with its platform's loudness spec from the
|
|
912
|
+
same table `check.py` reads, so `--normalize` and the check agree. `youtube-hdr` writes HEVC
|
|
913
|
+
Main10 keeping the source's own HDR10/HLG tags and refuses an SDR source (`kind: input`, hinting
|
|
914
|
+
at `--preset youtube`) rather than labelling SDR as HDR; `youtube-av1` encodes AV1 with
|
|
915
|
+
SVT-AV1 (libaom fallback) and refuses with `kind: missing_tool` on an ffmpeg built with
|
|
916
|
+
neither.
|
|
917
|
+
|
|
787
918
|
### proxy.py — low-bitrate proxy for analysis/preview
|
|
788
919
|
```
|
|
789
920
|
proxy.py INPUT [--width W | --scale F] [--crf N] [--fps N] [--no-audio] [-o OUT]
|