ffmpeg-skill 2.5.0 → 2.5.1
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/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 (`2.5.
|
|
24
|
+
| `skill.version` | the npm / package.json version (`2.5.1`) | 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`
|
|
@@ -119,7 +119,7 @@ JSON would have been rewritten for that. The flat keys, typed per tool in `outpu
|
|
|
119
119
|
"contract_version": "1.0",
|
|
120
120
|
"deprecated": [],
|
|
121
121
|
"removed": [{"what": "...", "since": "1.10.0", "replacement": "...", "removed_in": "2.0.0", "where": "cli | json | mcp | behaviour"}],
|
|
122
|
-
"skill": {"id": "ffmpeg-skill", "version": "2.5.
|
|
122
|
+
"skill": {"id": "ffmpeg-skill", "version": "2.5.1", "execution_mode": "local", "kind": "execution",
|
|
123
123
|
"entrypoints": {"cli": "...", "mcp": "...", "contract": "...", "doctor": "..."},
|
|
124
124
|
"not_provided": ["AI reasoning", "decisions", "production plans", "project IR", "approvals", "network access", "transcription engine"]},
|
|
125
125
|
"requirements": {"python": ">=3.9 (standard library only)", "ffmpeg": ">=5.0", "ffprobe": ">=5.0"},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ffmpeg-skill",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.1",
|
|
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",
|
|
@@ -94,7 +94,7 @@ from _common.color import (
|
|
|
94
94
|
bt709_tag_args, color_hex, _COLOR_TOKEN_RE, _sdr_bt709, validate_color
|
|
95
95
|
)
|
|
96
96
|
from _common.text import (
|
|
97
|
-
ADVANCE_EM, BIDI_SCRIPTS, _char_em, char_script, default_font_file, detect_script, drawtext_boxborderw,
|
|
97
|
+
ADVANCE_EM, BIDI_SCRIPTS, _char_em, char_script, default_font_file, detect_script, drawtext_boxborderw, drawtext_center_align,
|
|
98
98
|
drawtext_shaping, drawtext_text_opts, emoji_asset_for, EMOJI_ASSET_HINT, emoji_clusters, emoji_codepoint_name,
|
|
99
99
|
_EMOJI_COLOR_FAMILIES, _emoji_color_font, emoji_filter_chain, _emoji_name_candidates, EMOJI_RANGES,
|
|
100
100
|
_EMOJI_REGIONAL, emoji_support, _EMOJI_SUPPORT_CACHE, _EMOJI_TAIL, escape_drawtext, _family_rank, FC_LANG,
|
|
@@ -188,7 +188,7 @@ __all__ = [
|
|
|
188
188
|
"BEAT_REFRACTORY_S", "BEAT_WINDOW_S", "BEAT_SUPPORT_DIVISOR", "BEAT_ALIGN_DIVISOR",
|
|
189
189
|
"BEAT_Z_FLOOR", "BEAT_Z_SPAN",
|
|
190
190
|
"_onset_strength", "_pick_onsets", "_autocorrelation_peak", "_grid_score",
|
|
191
|
-
"default_font_file", "default_output", "DEFAULT_TIMEOUT", "detect_script", "die", "drawtext_boxborderw",
|
|
191
|
+
"default_font_file", "default_output", "DEFAULT_TIMEOUT", "detect_script", "die", "drawtext_boxborderw", "drawtext_center_align",
|
|
192
192
|
"_DRAWTEXT_PENDING", "drawtext_shaping", "drawtext_text_opts", "_DRAWTEXT_TMPDIR", "_drawtext_tmpdir",
|
|
193
193
|
"dry_run_input_pending", "emit", "emoji_asset_for", "EMOJI_ASSET_HINT", "emoji_clusters",
|
|
194
194
|
"emoji_codepoint_name", "_EMOJI_COLOR_FAMILIES", "_emoji_color_font", "emoji_filter_chain",
|
|
@@ -97,6 +97,13 @@ def escape_drawtext(text: str) -> str:
|
|
|
97
97
|
)
|
|
98
98
|
|
|
99
99
|
|
|
100
|
+
def drawtext_center_align() -> str:
|
|
101
|
+
"""`:text_align=C` for a centred drawtext, so each line of a multi-line label is centred on
|
|
102
|
+
its own. The option arrived in FFmpeg 6.1 (5.x and 6.0 reject it); there the block is still
|
|
103
|
+
centred as a whole by its x expression, with its lines left-aligned inside it."""
|
|
104
|
+
return ":text_align=C" if ffmpeg_version() >= (6, 1) else ""
|
|
105
|
+
|
|
106
|
+
|
|
100
107
|
def drawtext_text_opts(text: str, tmpdir: "Optional[str]" = None) -> str:
|
|
101
108
|
r"""`textfile=<path>:expansion=none` for drawtext -- the one route that is provably safe for
|
|
102
109
|
every character on every build shape this repo uses.
|
|
@@ -106,8 +113,11 @@ def drawtext_text_opts(text: str, tmpdir: "Optional[str]" = None) -> str:
|
|
|
106
113
|
`%{...}` scanner, which is the reason `%` was unsafe (a bare `\%` logs "Stray %" on one build
|
|
107
114
|
and fails the whole filter chain on another). With the scanner off, `'`, `%`, `:`, `,`, `[`,
|
|
108
115
|
`]`, `;` and `\` all reach the picture verbatim -- 1.15 fixes `overlay.py --text "it's 100%
|
|
109
|
-
done"` losing both characters.
|
|
110
|
-
|
|
116
|
+
done"` losing both characters. A newline is kept (`\r\n` and `\r` become `\n`): drawtext
|
|
117
|
+
draws it as a line break, and graphics.py's wrap puts them there on purpose (stripping them
|
|
118
|
+
ran a wrapped title off both edges of the frame). A tab, vertical tab or form
|
|
119
|
+
feed becomes a space, as the wrap measures it; other control characters are stripped (that
|
|
120
|
+
dates from the inline-escape route, where they broke the graph parser).
|
|
111
121
|
|
|
112
122
|
The file is UTF-8, mode 0600, in a private per-run directory (see _drawtext_tmpdir) that is
|
|
113
123
|
removed when the process ends. It is *registered* here and written by run() only if the
|
|
@@ -115,7 +125,8 @@ def drawtext_text_opts(text: str, tmpdir: "Optional[str]" = None) -> str:
|
|
|
115
125
|
printed plan therefore names a path that no longer exists once the run is over, which is the
|
|
116
126
|
same promise every other temp file in this skill makes.
|
|
117
127
|
"""
|
|
118
|
-
cleaned =
|
|
128
|
+
cleaned = (text or "").replace("\r\n", "\n").replace("\r", "\n")
|
|
129
|
+
cleaned = re.sub(r"[\x00-\x08\x0e-\x1f\x7f]", "", re.sub(r"[\t\x0b\x0c]", " ", cleaned))
|
|
119
130
|
import hashlib
|
|
120
131
|
name = "t_" + hashlib.sha256(cleaned.encode("utf-8")).hexdigest()[:16] + ".txt"
|
|
121
132
|
if tmpdir is None:
|
package/scripts/_common/text.py
CHANGED
|
@@ -31,7 +31,7 @@ from _common.emoji import (
|
|
|
31
31
|
)
|
|
32
32
|
from _common.drawtext import (
|
|
33
33
|
drawtext_boxborderw, SHAPING_SCRIPTS, BIDI_SCRIPTS, _SHAPING_BUILD_CACHE, drawtext_shaping, needs_shaping,
|
|
34
|
-
escape_drawtext, drawtext_text_opts,
|
|
34
|
+
escape_drawtext, drawtext_text_opts, drawtext_center_align,
|
|
35
35
|
)
|
|
36
36
|
from _common.wrap import (
|
|
37
37
|
ADVANCE_EM, NO_SPACE_SCRIPTS, NO_BOUNDARY_SCRIPTS, LATIN_EM, LEADING_VOWELS, _is_mark, _char_em,
|
package/scripts/graphics.py
CHANGED
|
@@ -35,7 +35,7 @@ from _platforms import PLATFORMS, PLATFORM_CHOICES, safe_margins_px, resolve as
|
|
|
35
35
|
from _common import (aac_args, add_common, brand_caption_style, script_font_for_text, apply_common, cfr_args,
|
|
36
36
|
color_hex, default_font_file, default_output, die, emit, escape_drawtext, escape_filter_path,
|
|
37
37
|
ffmpeg_base, info, load_brand, parse_time, probe, run, run_keeping_subtitles, video_args,
|
|
38
|
-
drawtext_boxborderw, X264_PRESETS, time_arg, fmt_secs, STATE, drawtext_text_opts,
|
|
38
|
+
drawtext_boxborderw, drawtext_center_align, X264_PRESETS, time_arg, fmt_secs, STATE, drawtext_text_opts,
|
|
39
39
|
LANGUAGE_NAMES, needs_shaping, detect_script, BIDI_SCRIPTS, font_family_of_file, font_family_for_script, has_emoji,
|
|
40
40
|
emoji_clusters, emoji_codepoint_name, char_script, emoji_filter_chain, emoji_asset_for, emoji_support, resolve_emoji_assets,
|
|
41
41
|
EMOJI_ASSET_HINT, text_width_em, drawtext_shaping, wrap_text, WRAP_MODES,
|
|
@@ -356,14 +356,14 @@ def main() -> int:
|
|
|
356
356
|
h2 = int(base * 0.045)
|
|
357
357
|
filters.append(f"drawbox=x=0:y=0:w=iw:h=ih:color={ff_color(bg, 0.55)}:t=fill:{en}")
|
|
358
358
|
add_text(args.title,
|
|
359
|
-
f"drawtext={drawtext_text_opts(args.title)}:{fo}:fontsize={h1}:fontcolor={ff_color(text_c)}:x=(w-text_w)/2:y=(h-text_h)/2-{h2 if args.subtitle else 0}:alpha='{fade_a}':{en}",
|
|
359
|
+
f"drawtext={drawtext_text_opts(args.title)}:{fo}:fontsize={h1}:fontcolor={ff_color(text_c)}{drawtext_center_align()}:x=(w-text_w)/2:y=(h-text_h)/2-{h2 if args.subtitle else 0}:alpha='{fade_a}':{en}",
|
|
360
360
|
size=h1, color=text_c, font=ass_font_family(), align=5, x=W / 2,
|
|
361
361
|
y=H / 2 - (h2 if args.subtitle else 0), outline=max(1.0, h1 / 20.0),
|
|
362
362
|
outline_color="000000", start=s, end=e, fade=(300, 300))
|
|
363
363
|
filters.append(f"drawbox=x=(iw-{int(base * 0.12)})/2:y=(ih)/2+{h1 // 2 + (0 if args.subtitle else 0)}:w={int(base * 0.12)}:h={max(2, int(base * 0.006))}:color={ff_color(primary)}:t=fill:{en}")
|
|
364
364
|
if args.subtitle:
|
|
365
365
|
add_text(args.subtitle,
|
|
366
|
-
f"drawtext={drawtext_text_opts(args.subtitle)}:{fo}:fontsize={h2}:fontcolor={ff_color(primary)}:x=(w-text_w)/2:y=(h-text_h)/2+{h1 // 2 + int(base * 0.03)}:alpha='{fade_a}':{en}",
|
|
366
|
+
f"drawtext={drawtext_text_opts(args.subtitle)}:{fo}:fontsize={h2}:fontcolor={ff_color(primary)}{drawtext_center_align()}:x=(w-text_w)/2:y=(h-text_h)/2+{h1 // 2 + int(base * 0.03)}:alpha='{fade_a}':{en}",
|
|
367
367
|
size=h2, color=primary, font=ass_font_family(), align=5, x=W / 2,
|
|
368
368
|
y=H / 2 + h1 // 2 + int(base * 0.03), outline=max(1.0, h2 / 20.0),
|
|
369
369
|
outline_color="000000", start=s, end=e, fade=(300, 300))
|
|
@@ -444,8 +444,8 @@ def main() -> int:
|
|
|
444
444
|
filters.append(f"drawbox=x=0:y={y0}:w=iw:h={band_h}:color={ff_color(bg, 0.78)}:t=fill:{hen}")
|
|
445
445
|
hook_title = wrapped(args.title, h1)
|
|
446
446
|
add_text(hook_title,
|
|
447
|
-
f"drawtext={drawtext_text_opts(hook_title)}:{fo}:fontsize={h1}:fontcolor={ff_color(text_c)}
|
|
448
|
-
f"x=(w-text_w)/2:y=(h-text_h)/2:{hen}",
|
|
447
|
+
f"drawtext={drawtext_text_opts(hook_title)}:{fo}:fontsize={h1}:fontcolor={ff_color(text_c)}"
|
|
448
|
+
f"{drawtext_center_align()}:x=(w-text_w)/2:y=(h-text_h)/2:{hen}",
|
|
449
449
|
size=h1, color=text_c, font=ass_font_family(), align=5, x=W / 2, y=H / 2,
|
|
450
450
|
outline=max(1.0, h1 / 20.0), outline_color="000000", start=s, end=he)
|
|
451
451
|
filters.append(f"drawbox=x=0:y=0:w='iw*max(0,1-(t-{s:.3f})/{max(0.001, he - s):.3f})':h={bar_h}:"
|
|
@@ -466,7 +466,7 @@ def main() -> int:
|
|
|
466
466
|
meme_line = wrapped(text.upper(), fs)
|
|
467
467
|
add_text(meme_line,
|
|
468
468
|
f"drawtext={drawtext_text_opts(meme_line)}:{fo}:fontsize={fs}:fontcolor={white}:"
|
|
469
|
-
f"borderw={bw}:bordercolor={black}:x=(w-text_w)/2:y={y}:{en}",
|
|
469
|
+
f"borderw={bw}:bordercolor={black}{drawtext_center_align()}:x=(w-text_w)/2:y={y}:{en}",
|
|
470
470
|
size=fs, color="FFFFFF", font=ass_font_family(), bold=True,
|
|
471
471
|
align=(8 if y == f"{m_top}" else 2), x=W / 2,
|
|
472
472
|
y=(m_top if y == f"{m_top}" else H - m_bottom),
|