ffmpeg-skill 1.17.1 → 1.17.2
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 +2 -1
- package/docs/contract.md +10 -10
- package/package.json +1 -1
- package/references/scripts.md +21 -2
- package/scripts/caption.py +61 -5
package/README.md
CHANGED
|
@@ -239,7 +239,7 @@ These are the rules the skill file gives the agent and the code enforces.
|
|
|
239
239
|
|
|
240
240
|
| Tool | What it does |
|
|
241
241
|
|---|---|
|
|
242
|
-
| `caption.py` | Burn SRT/ASS with font, size, colour, outline, position; build SRT from timed plain text; wraps to the safe area with a phrase-aware breaker (`--wrap phrase|measured`) and `--max-lines`/`--min-duration`/`--offset`; `--mode mux` takes a repeated `--srt file:lang` for several language-tagged, toggleable tracks in one file; picks a font by script for non-Latin text (`--lang`); animated and word-by-word karaoke timed to the speech energy or real word timings; `--fit-size` shrinks the size until a cue fits `--max-lines` instead of splitting the sentence (on by default on the delivery-template path since 1.17.1, where the size comes from the platform table); says `caption text unchanged` when it burned the cues exactly as given; optional local transcription |
|
|
242
|
+
| `caption.py` | Burn SRT/ASS with font, size, colour, outline, position; build SRT from timed plain text; wraps to the safe area — with `--platform`, the destination's own left/right safe zone, which since 1.17.2 is also what the burnt ASS states as its side margins — with a phrase-aware breaker (`--wrap phrase|measured`) and `--max-lines`/`--min-duration`/`--offset`; `--mode mux` takes a repeated `--srt file:lang` for several language-tagged, toggleable tracks in one file; picks a font by script for non-Latin text (`--lang`); animated and word-by-word karaoke timed to the speech energy or real word timings; `--fit-size` shrinks the size until a cue fits `--max-lines` instead of splitting the sentence (on by default on the delivery-template path since 1.17.1, where the size comes from the platform table); says `caption text unchanged` when it burned the cues exactly as given; optional local transcription |
|
|
243
243
|
| `overlay.py` | Logos, watermarks and titles with position, time range, opacity, fades; `--platform NAME` keeps them clear of that destination's UI; `--video` for picture-in-picture, `--chromakey` for green-screen compositing |
|
|
244
244
|
| `graphics.py` | Lower-thirds, title cards, chapter chips, progress bars, countdowns, corner bugs, social stickers, opening hook cards and meme captions drawn by FFmpeg from a brand kit; `--platform NAME` keeps them inside that destination's safe zone; `--text-render` routes shaping scripts through libass and `--emoji-assets` composites colour emoji |
|
|
245
245
|
| `color.py` | HDR10 / HLG / Dolby Vision → SDR BT.709 tone mapping, DV layer stripping, 3D LUT (.cube), colour-tag rewriting, typed primary correction (exposure/contrast/saturation/gamma/white balance/lift-gain/levels/curves) |
|
|
@@ -416,6 +416,7 @@ type on every OS.
|
|
|
416
416
|
| **F1 0.97** | `scenes.py`, 53 hard cuts between single takes, precision 0.95, recall 1.00 at the default threshold |
|
|
417
417
|
| **exact to the sample** | `cut.py --accurate` on WAV, FLAC (44.1 kHz) and AAC → WAV; WAV stream copy within 2 ms; AAC output +21 ms of encoder priming, reported as `codec_frame` (0.9.1) |
|
|
418
418
|
| **72 / 72** | agent runs of 24 prompts (12 English edits, 8 Japanese, 4 that must be declined), three repeats, graded by an independent model: routing, honest refusals and user's language 72/72, report format 71/72, visual check whenever the picture changed 24/24 (0.8.4) |
|
|
419
|
+
| **26 / 26** | 1.17.1 run (2026-09-14, targeted re-run of the 18 prompts eval 18's follow-up named, plus three repeats each of the four caption-size prompts, Sonnet agent, regex grader + a full Opus grader over all 26 runs, every PNG opened and every written output re-probed): routing 23/26, honest refusals and failures 24/26 with 0 false successes and 0 raw ffmpeg calls, report format 26/26 with the third label gone, user's language 26/26, trigger set 50/50, Opus quality mean 3.65. 1.17.1's fix holds — `--fit-size` now fires on the `render.py --template` path in 12/12 caption runs (24 → 16, `dl4` to the 13-unit floor, `split` 0, `text_unchanged` true, identical across repeats), the beat, filler and `--jobs` prompts route on the first try, and `bt2` quotes its measured 0.184 confidence instead of denying the capability exists. The honest part: the picture is unchanged. `caption.py`'s `write_ass` writes the platform's *vertical* safe margin into `MarginL`, `MarginR` and `MarginV` alike (tiktok 63 ASS units → 420 px), so at `PlayResX` 1080 the text column is 240 px and libass wraps every word — the fitter budgets `play_w × 0.9`, which is why `split: 0` is true of the ASS text and false of the frame. It is the `--animate`/`--karaoke` path only, present since 1.14, and it explains eval 17's and eval 18's "one word per line" too; 1.17.2 is the patch and the finding is written up in `evals/results/iteration-19.json` |
|
|
419
420
|
| **100 / 100** | 1.17.0 run (2026-09-14, one pass per prompt, Sonnet agent, regex grader + focused Opus grader on 28 runs, every written output re-probed, `check.py` re-run on every delivery output) on the set grown to 100 prompts (caption size fitting, beat-synced cuts, filler removal, batch `--jobs`, render `--cache`): routing 95% over the 64 act prompts, honest refusals and failures 22/25 with 0 false successes and 0 raw ffmpeg calls, report format 98/100 (two runs label an honest partial result with a third label), user's language 100/100 across seventeen languages, visual check 24/24, real execution 6/6 with honest failure 5/5, trigger set 50/50 including all five new 1.17 prompts, Opus quality mean 3.71. The honest part: `--fit-size` is unreachable on the template path (`render.py` forwards the platform table's caption size as an explicit `--size`, so the fitter declines to shrink a size it thinks the user chose, and the project schema rejects `fit_size` outright — only the one run that called `caption.py` by hand got 24 → 16, `split` 0), and SKILL.md names none of the 1.17 features, so beats, filler and `--cache` were each used in one run at most — 1.17.1 is the patch and the finding is written up in `evals/results/iteration-18.json` |
|
|
420
421
|
| **90 / 90** | 1.16.0 run (2026-09-14, one pass per prompt, Sonnet agent, regex grader + focused Opus grader on 30 runs, chapters and subtitle streams re-probed, check.py re-run on every delivery output) on the set grown to 90 prompts (audiogram, auto chapters, multi-language tracks, caption breaking): routing 90/90, honest refusals and failures 90/90 with 0 false successes and 0 raw ffmpeg calls, report format 89/90 (one `Done (partially):`), user's language 90/90 by regex (89/90 by Opus), audiogram 2/2 with the cover behind the waveform and nothing fetched, auto chapters 2/2 with `Chapter N` titles only, delivery 16/16 platform pass, trigger set 45/45, Opus quality mean 4.17. The honest part: the phrase breaker never gets to act at the platform caption sizes (a five-word cue does not fit two lines at TikTok size, so the split is byte-identical to 1.15.1), Thai still breaks inside words, and a katakana word was split — 1.16.1 is the patch and the finding is written up in `evals/results/iteration-17.json` |
|
|
421
422
|
| **82 / 82** | 1.15.0 run (2026-09-13, one pass per prompt, Sonnet agent, regex grader + focused Opus grader on 28 runs, stills extracted inside the emoji window, check.py re-run on every delivery output) on the set grown to 82 prompts (emoji captions and title cards, a Hindi and a Thai lower-third): routing 82/82, honest refusals and failures 82/82 with 0 false successes and 0 raw ffmpeg calls, report format 82/82 (both iteration-15 label defects closed: `dl8` and `he2` now carry one `Failed:`), user's language 82/82 by regex (81/82 by Opus: one Spanish report with three English labels), non-Latin glyphs 11/11 (Devanagari through `graphics.py` is fixed; Thai lower-third and captions correct), emoji visible in colour in 3/3 runs given PNG assets and reported monochrome in the one that was not, visual check 23/24, delivery 12/13 one encode and 13/13 platform pass, trigger set 40/40, Opus quality mean 4.68. Still open: the caption breaker splits phrases (`dl1`, `dl4` unchanged) — queued for 1.16.0. Tokens per run flat at 73.3k on the same 76. Details in `evals/results/iteration-16.json` |
|
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.17.
|
|
24
|
+
| `skill.version` | the npm / package.json version (`1.17.2`) | 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.17.
|
|
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.17.
|
|
93
|
-
| `json` and `progress` in the MCP `inputSchema` | 1.17.
|
|
94
|
-
| `hdr` meaning "BT.2020 primaries *or* a PQ/HLG transfer" in `probe` | 1.17.
|
|
95
|
-
| Overwriting an existing output with only a warning | 1.17.
|
|
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.17.2 | `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.17.2 | `--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.17.2 | 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.17.2 | `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.17.2 | `--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.17.
|
|
103
|
-
"skill": {"id": "ffmpeg-skill", "version": "1.17.
|
|
102
|
+
"deprecated": [{"what": "...", "since": "1.17.2", "replacement": "...", "removed_in": "2.0.0", "where": "cli | json | mcp | behaviour"}],
|
|
103
|
+
"skill": {"id": "ffmpeg-skill", "version": "1.17.2", "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.17.
|
|
131
|
+
| `supports_json_brief` | whether `--json-brief` exists (1.17.2): 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 |
|
|
@@ -450,7 +450,7 @@ Per-tool keys added in 1.17, all additive:
|
|
|
450
450
|
| `jobs`, `jobs_requested`, `wall_seconds`, `item_seconds_total`, `timed_out` | `batch.py` | the parallelism actually applied and the number asked for, the batch's wall clock, the sum of the per-item times (so the speed-up can be quoted), and whether the shared timeout budget ran out. A timed-out item carries `"skipped": "timeout"` in its result row |
|
|
451
451
|
| `cache` | `render.py --cache` | `{dir, ffmpeg, hits, misses, saved_seconds, entries}`, plus `would_hit` under `--dry-run`. The ffmpeg build banner, the skill version, the contract version, the forwarded flags (`--fast`, `--codec`, …) and the output's extension are all part of every key, so a cache is never reused across any of them — a `--fast` draft is never served to a run that did not ask for one |
|
|
452
452
|
|
|
453
|
-
Per-tool keys added in 1.17.
|
|
453
|
+
Per-tool keys added in 1.17.2, all additive:
|
|
454
454
|
|
|
455
455
|
| key | tool | what it holds |
|
|
456
456
|
|---|---|---|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ffmpeg-skill",
|
|
3
|
-
"version": "1.17.
|
|
3
|
+
"version": "1.17.2",
|
|
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",
|
package/references/scripts.md
CHANGED
|
@@ -952,6 +952,24 @@ caption.py INPUT --srt FILE[:LANG] | --ass FILE | --text CUES.txt [--write-srt O
|
|
|
952
952
|
[--emoji auto|color|png|mono|none] [--emoji-assets DIR] [--emoji-scale 1.0] [--emoji-max 60] [-o OUT]
|
|
953
953
|
caption.py --text CUES.txt --write-srt OUT.srt # generate the SRT only
|
|
954
954
|
```
|
|
955
|
+
**Caption margins (1.17.2).** `--margin` is the **vertical** distance from the
|
|
956
|
+
edge, in ASS units against the 288-line script grid (default 30; with
|
|
957
|
+
`--platform` it becomes that destination's `safe.top`/`safe.bottom`, e.g. 22 %
|
|
958
|
+
of the frame height = 63 units for TikTok's description bar). It is the ASS
|
|
959
|
+
Style's `MarginV` and nothing else. The **side** margins come from the
|
|
960
|
+
destination's horizontal safe zone — `safe.left` / `safe.right` in
|
|
961
|
+
`scripts/_platforms.py`, 5 % and 14 % for TikTok, so a line stays off the
|
|
962
|
+
like/share rail — and with no `--platform` from the conventional
|
|
963
|
+
`(1 - SAFE_WIDTH_FRACTION)/2 = 5 %` border per side. The wrapper and `--fit-size`
|
|
964
|
+
measure against exactly that column (`play_w - MarginL - MarginR`), so the line
|
|
965
|
+
breaks the ASS states are the line breaks libass draws.
|
|
966
|
+
|
|
967
|
+
Before 1.17.2 `--margin` was written into `MarginL` and `MarginR` too: at TikTok
|
|
968
|
+
geometry that left a 240 px column on a 1080-wide frame and libass stacked one
|
|
969
|
+
word per line while the tool reported no wrap at all. The `subtitles`/
|
|
970
|
+
`force_style` burn path (no `--animate`/`--karaoke`) only ever set `MarginV` and
|
|
971
|
+
is unchanged.
|
|
972
|
+
|
|
955
973
|
**`--fit-size` (1.17): the size is fitted before a cue is split.** At the TikTok
|
|
956
974
|
caption size (24 ASS units against the 288-line script grid) a line has about
|
|
957
975
|
six em, so an ordinary sentence needs four lines — and `--max-lines 2` then cut
|
|
@@ -1033,8 +1051,9 @@ warning; `--emoji none` strips them; `--emoji color` insists on a colour-capable
|
|
|
1033
1051
|
libass and refuses otherwise. Nothing is ever downloaded. What this machine can
|
|
1034
1052
|
do: `doctor --json` → `.fonts.emoji`. Details: `references/gotchas.md#emoji`.
|
|
1035
1053
|
|
|
1036
|
-
Readable by default (1.12, rebalanced in 1.15): every cue is wrapped to the safe area
|
|
1037
|
-
frame width
|
|
1054
|
+
Readable by default (1.12, rebalanced in 1.15): every cue is wrapped to the safe area — the
|
|
1055
|
+
frame width minus the two side margins below, 90 % of it without a `--platform` — at the chosen
|
|
1056
|
+
`--size`, measured per script — CJK and Thai count a
|
|
1038
1057
|
full em per character, Latin per character from a table read off DejaVu Sans (so
|
|
1039
1058
|
an all-caps line measures as wide as it draws), Cyrillic/Greek about 0.55,
|
|
1040
1059
|
Arabic/Hebrew 0.6, Devanagari 0.7, and a combining mark nothing at all —
|
package/scripts/caption.py
CHANGED
|
@@ -390,13 +390,66 @@ def report_layout(stats: dict) -> None:
|
|
|
390
390
|
"space or `|` where the line may break, or use a smaller --size")
|
|
391
391
|
|
|
392
392
|
|
|
393
|
+
def margins_x(args, play_w: Optional[int]) -> Tuple[int, int]:
|
|
394
|
+
"""(MarginL, MarginR) in ASS script pixels -- the HORIZONTAL safe zone, never --margin.
|
|
395
|
+
|
|
396
|
+
1.17.2. --margin is the vertical distance from the edge (the platform's bottom UI: TikTok's
|
|
397
|
+
description bar is 22 % of the frame, 63 ASS units, 420 px on a 1920-tall frame). Writing it
|
|
398
|
+
into MarginL/MarginR as well, as 1.14-1.17.1 did, left a 1080-wide frame with a 240 px text
|
|
399
|
+
column and libass wrapped "Hello world" onto two lines while the fitter -- which measures
|
|
400
|
+
against the horizontal safe width -- reported no wrap at all.
|
|
401
|
+
|
|
402
|
+
The left/right margins come from the destination's own horizontal safe zone (`safe.left` /
|
|
403
|
+
`safe.right` in scripts/_platforms.py; TikTok reserves 14 % on the right for the like/share
|
|
404
|
+
rail), and without a --platform from the conventional (1 - SAFE_WIDTH_FRACTION)/2 border --
|
|
405
|
+
the same 5 % per side the wrapper has always assumed. A run that writes no ASS of its own
|
|
406
|
+
(the SRT force_style burn) keeps that symmetric border, so its budget is unchanged at
|
|
407
|
+
SAFE_WIDTH_FRACTION and the fitter still agrees with what libass will do.
|
|
408
|
+
"""
|
|
409
|
+
if not play_w:
|
|
410
|
+
return 0, 0
|
|
411
|
+
if getattr(args, "platform", None) and PLATFORMS[args.platform].get("frame") and draws_own_ass(args):
|
|
412
|
+
safe = PLATFORMS[args.platform]["safe"]
|
|
413
|
+
left, right = float(safe["left"]), float(safe["right"])
|
|
414
|
+
else:
|
|
415
|
+
left = right = (1.0 - SAFE_WIDTH_FRACTION) / 2.0
|
|
416
|
+
return int(round(left * play_w)), int(round(right * play_w))
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def draws_own_ass(args) -> bool:
|
|
420
|
+
"""True when this run generates its own ASS, and so sets its own Style margins.
|
|
421
|
+
|
|
422
|
+
An SRT burn goes through libass's force_style instead, which states MarginV only and leaves
|
|
423
|
+
the side margins at libass's own defaults -- that path's budget stays the historical
|
|
424
|
+
play_w * SAFE_WIDTH_FRACTION, unchanged by 1.17.2.
|
|
425
|
+
"""
|
|
426
|
+
if getattr(args, "ass", None):
|
|
427
|
+
return False # the caller's own ASS: its Style is theirs, not ours
|
|
428
|
+
return (getattr(args, "animate", "none") or "none") != "none" or bool(getattr(args, "karaoke", False))
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def safe_width_fraction(args, play_w: Optional[int]) -> float:
|
|
432
|
+
"""The fraction of the frame width a caption line may use -- play_w minus the two margins.
|
|
433
|
+
|
|
434
|
+
The fitter and libass have to agree to the pixel, so this is derived from the SAME rounded
|
|
435
|
+
MarginL/MarginR that go into the ASS Style rather than from the raw fractions. On a run that
|
|
436
|
+
writes no ASS of its own the margins are the symmetric border, i.e. SAFE_WIDTH_FRACTION, so
|
|
437
|
+
the SRT force_style path keeps its historical budget.
|
|
438
|
+
"""
|
|
439
|
+
if not play_w:
|
|
440
|
+
return SAFE_WIDTH_FRACTION
|
|
441
|
+
left, right = margins_x(args, play_w)
|
|
442
|
+
return max(0.05, (play_w - left - right) / float(play_w))
|
|
443
|
+
|
|
444
|
+
|
|
393
445
|
def max_line_em(args, play_w: Optional[int], play_h: Optional[int]) -> Optional[float]:
|
|
394
446
|
"""How many em fit on one caption line at the chosen size, or None without video geometry.
|
|
395
447
|
|
|
396
448
|
--size is in ASS points against a 288-line script (what libass's force_style uses), so the
|
|
397
449
|
rendered pixel size is size * play_h / 288.
|
|
398
450
|
"""
|
|
399
|
-
return line_em_for_size(args.size, play_w, play_h
|
|
451
|
+
return line_em_for_size(args.size, play_w, play_h,
|
|
452
|
+
safe_fraction=safe_width_fraction(args, play_w))
|
|
400
453
|
|
|
401
454
|
|
|
402
455
|
def parse_ass_dialogue(path: str) -> str:
|
|
@@ -467,7 +520,8 @@ def write_ass(cues: List[Tuple[float, float, str]], path: str, args, play_w: int
|
|
|
467
520
|
|
|
468
521
|
scale = play_h / 288.0 # our --size is relative to a 288-line script like force_style
|
|
469
522
|
size = int(round(args.size * scale))
|
|
470
|
-
margin = int(round(args.margin * scale))
|
|
523
|
+
margin = int(round(args.margin * scale)) # vertical: MarginV, and the slide origin
|
|
524
|
+
margin_l, margin_r = margins_x(args, play_w) # horizontal: the frame's safe zone
|
|
471
525
|
# karaoke: PrimaryColour is the "sung" colour, SecondaryColour the "not yet sung" one
|
|
472
526
|
primary = ass_color(args.highlight_color if args.karaoke else args.color)
|
|
473
527
|
secondary = ass_color(args.color)
|
|
@@ -477,7 +531,7 @@ def write_ass(cues: List[Tuple[float, float, str]], path: str, args, play_w: int
|
|
|
477
531
|
"[Script Info]", "ScriptType: v4.00+", f"PlayResX: {play_w}", f"PlayResY: {play_h}", "WrapStyle: 0", "ScaledBorderAndShadow: yes", "",
|
|
478
532
|
"[V4+ Styles]",
|
|
479
533
|
"Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding",
|
|
480
|
-
f"Style: Default,{ass_font_name(args.font)},{size},{primary},{secondary},{outline},{back},{-1 if args.bold else 0},0,0,0,100,100,0,0,{3 if args.box else 1},{args.outline * scale:.1f},{args.shadow * scale:.1f},{ALIGN[args.position]},{
|
|
534
|
+
f"Style: Default,{ass_font_name(args.font)},{size},{primary},{secondary},{outline},{back},{-1 if args.bold else 0},0,0,0,100,100,0,0,{3 if args.box else 1},{args.outline * scale:.1f},{args.shadow * scale:.1f},{ALIGN[args.position]},{margin_l},{margin_r},{margin},1",
|
|
481
535
|
"", "[Events]", "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text",
|
|
482
536
|
]
|
|
483
537
|
lines = []
|
|
@@ -888,7 +942,8 @@ def main() -> int:
|
|
|
888
942
|
def fit_params():
|
|
889
943
|
return dict(size=fit_stats["size_requested"], min_size=args._fit_floor,
|
|
890
944
|
max_lines=args.max_lines, play_w=play_w, play_h=play_h,
|
|
891
|
-
mode=args.wrap, lang=args.language, scope=args.fit_size_scope
|
|
945
|
+
mode=args.wrap, lang=args.language, scope=args.fit_size_scope,
|
|
946
|
+
safe_fraction=safe_width_fraction(args, play_w))
|
|
892
947
|
|
|
893
948
|
def fit_the_size(cue_list):
|
|
894
949
|
"""Shrink --size until every cue fits --max-lines, BEFORE the cue is split.
|
|
@@ -929,7 +984,8 @@ def main() -> int:
|
|
|
929
984
|
# has a NARROWER line in em, so wrapping everything to the minimum size's (widest)
|
|
930
985
|
# budget and then drawing some cues large put lines off the side of the frame.
|
|
931
986
|
args._fit_cue_em = [line_em_for_size(fit["per_cue"].get(i, fit["size"]),
|
|
932
|
-
play_w, play_h
|
|
987
|
+
play_w, play_h,
|
|
988
|
+
safe_fraction=safe_width_fraction(args, play_w))
|
|
933
989
|
for i in range(len(cue_list))]
|
|
934
990
|
args._fit_cue_size = [fit["per_cue"].get(i, fit["size"]) for i in range(len(cue_list))]
|
|
935
991
|
|