ffmpeg-skill 3.8.19 → 3.8.20
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 +2 -2
- package/package.json +1 -1
- package/scripts/_common/color.py +10 -5
- package/scripts/waveform.py +5 -3
package/docs/contract.md
CHANGED
|
@@ -26,7 +26,7 @@ The contract is derived from the code that runs, not maintained beside it:
|
|
|
26
26
|
| Field | Meaning | Changes when |
|
|
27
27
|
|---|---|---|
|
|
28
28
|
| `contract_version` | shape of this document (`2.0`) | a key is renamed, removed or changes meaning |
|
|
29
|
-
| `skill.version` | the npm / package.json version (`3.8.
|
|
29
|
+
| `skill.version` | the npm / package.json version (`3.8.20`) | any release |
|
|
30
30
|
|
|
31
31
|
A release that adds a tool or a flag keeps `contract_version`; a breaking change to the
|
|
32
32
|
ToolSpec shape bumps it. Consumers pin on `contract_version` and read `skill.version`
|
|
@@ -154,7 +154,7 @@ JSON would have been rewritten for that. The flat keys, typed per tool in `outpu
|
|
|
154
154
|
"deprecated": [],
|
|
155
155
|
"removed": [{"what": "...", "since": "1.10.0", "replacement": "...", "removed_in": "2.0.0", "where": "cli | json | mcp | behaviour"},
|
|
156
156
|
{"what": "running on FFmpeg 5.x (the requirements.ffmpeg floor)", "since": "2.14.0", "replacement": "FFmpeg 6.1 or later; ...", "removed_in": "3.0.0", "where": "behaviour"}],
|
|
157
|
-
"skill": {"id": "ffmpeg-skill", "version": "3.8.
|
|
157
|
+
"skill": {"id": "ffmpeg-skill", "version": "3.8.20", "execution_mode": "local", "kind": "execution",
|
|
158
158
|
"entrypoints": {"cli": "...", "mcp": "...", "contract": "...", "doctor": "..."},
|
|
159
159
|
"not_provided": ["AI reasoning", "decisions", "production plans", "project IR", "approvals", "network access", "transcription engine"]},
|
|
160
160
|
"requirements": {"python": ">=3.10 (standard library only)", "ffmpeg": ">=6.1", "ffprobe": ">=6.1"},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ffmpeg-skill",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.20",
|
|
4
4
|
"mcpName": "io.github.kajisho5/ffmpeg-skill",
|
|
5
5
|
"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.",
|
|
6
6
|
"keywords": [
|
package/scripts/_common/color.py
CHANGED
|
@@ -360,12 +360,16 @@ def color_hex(value: str) -> str:
|
|
|
360
360
|
return v.upper()
|
|
361
361
|
|
|
362
362
|
|
|
363
|
-
|
|
363
|
+
# Six or eight hex digits, with 0x, with # or bare (#535: bare ones starting with a digit, 0B1D2A,
|
|
364
|
+
# were refused, and seven passed with a prefix and failed inside ffmpeg), or a word that starts
|
|
365
|
+
# with a letter; alpha is 0..1 ("red@2" used to reach ffmpeg). Nothing else reaches the graph.
|
|
366
|
+
_HEX_DIGITS = r"(?:[0-9A-Fa-f]{8}|[0-9A-Fa-f]{6})"
|
|
367
|
+
_COLOR_TOKEN_RE = re.compile(r"^((?:0[xX]|#)?%s|[A-Za-z][A-Za-z0-9]*)(@(?:0(?:\.\d+)?|1(?:\.0+)?|\.\d+))?$" % _HEX_DIGITS)
|
|
364
368
|
|
|
365
369
|
|
|
366
370
|
def validate_color(value: str, flag: str = "--color") -> str:
|
|
367
|
-
"""Refuse a colour argument that isn't a plain ffmpeg colour token (named colour,
|
|
368
|
-
#RRGGBB[AA], optionally with an @alpha suffix). Every caller that string-formats a colour flag
|
|
371
|
+
"""Refuse a colour argument that isn't a plain ffmpeg colour token (named colour, RRGGBB[AA],
|
|
372
|
+
0xRRGGBB[AA], #RRGGBB[AA], optionally with an @alpha suffix). Every caller that string-formats a colour flag
|
|
369
373
|
straight into a filter graph (color=c=..., tpad=...:color=..., rotate=...:fillcolor=...) must
|
|
370
374
|
validate it first -- ffmpeg filter options are comma/colon-delimited, so an unvalidated value
|
|
371
375
|
containing those characters lets a caller splice in an entirely different filter (a real,
|
|
@@ -374,9 +378,10 @@ def validate_color(value: str, flag: str = "--color") -> str:
|
|
|
374
378
|
invariant every other typed flag in this codebase already holds to."""
|
|
375
379
|
m = _COLOR_TOKEN_RE.match(value)
|
|
376
380
|
if not m:
|
|
377
|
-
die(f"{flag} must be a plain colour (a name,
|
|
381
|
+
die(f"{flag} must be a plain colour (a name, or six or eight hex digits RRGGBB[AA], bare or after 0x or #, "
|
|
382
|
+
f"optionally @alpha), got '{value}'")
|
|
378
383
|
word = m.group(1)
|
|
379
|
-
if word[0]
|
|
384
|
+
if word[0].isalpha() and word.lower() not in FFMPEG_COLOR_NAMES and not _BARE_HEX.match(word):
|
|
380
385
|
# A189 (#465): any word passed, and an unknown name failed inside ffmpeg ("Cannot find
|
|
381
386
|
# color") as kind ffmpeg, after a dry run that had said completed. ffmpeg reads a word as
|
|
382
387
|
# one of its names, or else as bare hex digits.
|
package/scripts/waveform.py
CHANGED
|
@@ -38,7 +38,7 @@ import sys
|
|
|
38
38
|
from _platforms import PLATFORMS, PLATFORM_CHOICES, resolve as resolve_platform
|
|
39
39
|
from _common.probe import SILENT_PEAK_DBFS, refuse_not_a_still, square_pixels, square_pixels_note
|
|
40
40
|
from _common import still_input_args # noqa: E402
|
|
41
|
-
from _common import (add_common, apply_common, aac_args, default_output, die, draws_chroma, emit, ffmpeg_base,
|
|
41
|
+
from _common import (add_common, apply_common, aac_args, color_hex, default_output, die, draws_chroma, emit, ffmpeg_base,
|
|
42
42
|
info, load_brand, pad_filters, probe, rgb_chain_to_bt709, run, STATE, validate_color, video_args, X264_PRESETS,
|
|
43
43
|
fmt_secs, child_args, run_tool, measured_level_dbfs, sibling_temp)
|
|
44
44
|
from _common.emit import absorb_stage_hw, note, warn
|
|
@@ -110,10 +110,12 @@ def main() -> int:
|
|
|
110
110
|
brand = load_brand(args.brand) if args.brand else None
|
|
111
111
|
if brand:
|
|
112
112
|
bc = brand.get("colors") or {}
|
|
113
|
+
# read as caption.py, graphics.py and overlay.py read a brand colour (#535): a brand file
|
|
114
|
+
# writes six hex digits with or without # or 0x
|
|
113
115
|
if args.color == "lime" and bc.get("primary"):
|
|
114
|
-
args.color = bc["primary"]
|
|
116
|
+
args.color = color_hex(bc["primary"])
|
|
115
117
|
if args.background == "black" and bc.get("background"):
|
|
116
|
-
args.background = bc["background"]
|
|
118
|
+
args.background = color_hex(bc["background"])
|
|
117
119
|
if args.image:
|
|
118
120
|
if "://" in args.image:
|
|
119
121
|
die("--image must be a readable local file; this skill has no network access, so save "
|