ffmpeg-skill 2.23.0 → 2.23.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 +2 -2
- package/package.json +1 -1
- package/scripts/_common/drawtext.py +1 -1
- package/scripts/_common/timeline.py +31 -2
- package/scripts/batch.py +2 -2
- package/scripts/render.py +1 -1
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.23.
|
|
24
|
+
| `skill.version` | the npm / package.json version (`2.23.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`
|
|
@@ -131,7 +131,7 @@ JSON would have been rewritten for that. The flat keys, typed per tool in `outpu
|
|
|
131
131
|
"deprecated": [{"what": "running on Python 3.9 (the requirements.python floor)", "since": "2.10.9", "replacement": "Python 3.10 or later; ...", "removed_in": "3.0.0", "where": "behaviour"},
|
|
132
132
|
{"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"}],
|
|
133
133
|
"removed": [{"what": "...", "since": "1.10.0", "replacement": "...", "removed_in": "2.0.0", "where": "cli | json | mcp | behaviour"}],
|
|
134
|
-
"skill": {"id": "ffmpeg-skill", "version": "2.23.
|
|
134
|
+
"skill": {"id": "ffmpeg-skill", "version": "2.23.1", "execution_mode": "local", "kind": "execution",
|
|
135
135
|
"entrypoints": {"cli": "...", "mcp": "...", "contract": "...", "doctor": "..."},
|
|
136
136
|
"not_provided": ["AI reasoning", "decisions", "production plans", "project IR", "approvals", "network access", "transcription engine"]},
|
|
137
137
|
"requirements": {"python": ">=3.9 (standard library only; 3.9 deprecated since 2.10.9, removed in 3.0.0; 3.10+ recommended)", "ffmpeg": ">=5.0 (5.x deprecated since 2.14.0, removed in 3.0.0; 6.1+ recommended)", "ffprobe": ">=5.0"},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ffmpeg-skill",
|
|
3
|
-
"version": "2.23.
|
|
3
|
+
"version": "2.23.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",
|
|
@@ -60,7 +60,7 @@ def drawtext_fontfile(path: str) -> str:
|
|
|
60
60
|
st = os.stat(path)
|
|
61
61
|
# the path plus size and mtime: a font replaced in place during the run gets a new copy
|
|
62
62
|
key = f"{path}|{st.st_size}|{st.st_mtime_ns}".encode("utf-8")
|
|
63
|
-
name = "font_" + hashlib.sha1(key).hexdigest()[:16] + os.path.splitext(path)[1].lower()
|
|
63
|
+
name = "font_" + hashlib.sha1(key, usedforsecurity=False).hexdigest()[:16] + os.path.splitext(path)[1].lower()
|
|
64
64
|
copy = os.path.join(tmpdir, name)
|
|
65
65
|
if not copy.isascii():
|
|
66
66
|
warn(f"font: {path} is not an ASCII path and no ASCII copy could be placed; drawtext may "
|
|
@@ -492,13 +492,42 @@ def parses(path: str, fmt: str) -> bool:
|
|
|
492
492
|
return why is None
|
|
493
493
|
|
|
494
494
|
|
|
495
|
+
def _xml_root_tag(data: bytes) -> str:
|
|
496
|
+
"""The root element of a well-formed XML document, read with expat directly.
|
|
497
|
+
|
|
498
|
+
ElementTree expands entities declared in an internal DTD, and on Python 3.9/3.10 (both
|
|
499
|
+
supported) that is an entity-expansion ("billion laughs") risk on a file the tool did not
|
|
500
|
+
write. A timeline never declares an entity (to_fcpxml writes a bare `<!DOCTYPE fcpxml>`), so
|
|
501
|
+
a declaration is refused rather than expanded. Errors are ElementTree's ParseError, so a
|
|
502
|
+
failure reads the same as before."""
|
|
503
|
+
from xml.etree.ElementTree import ParseError
|
|
504
|
+
from xml.parsers import expat
|
|
505
|
+
parser = expat.ParserCreate()
|
|
506
|
+
parser.SetParamEntityParsing(expat.XML_PARAM_ENTITY_PARSING_NEVER)
|
|
507
|
+
root: List[str] = []
|
|
508
|
+
|
|
509
|
+
def start(name: str, _attrs: Any) -> None:
|
|
510
|
+
if not root:
|
|
511
|
+
root.append(name)
|
|
512
|
+
|
|
513
|
+
def entity(name: str, *_rest: Any) -> None:
|
|
514
|
+
raise ParseError(f"the document declares the entity {name!r}; a timeline never declares one")
|
|
515
|
+
|
|
516
|
+
parser.StartElementHandler = start
|
|
517
|
+
parser.EntityDeclHandler = entity
|
|
518
|
+
try:
|
|
519
|
+
parser.Parse(data, True)
|
|
520
|
+
except expat.ExpatError as exc:
|
|
521
|
+
raise ParseError(str(exc)) from None
|
|
522
|
+
return root[0]
|
|
523
|
+
|
|
524
|
+
|
|
495
525
|
def parse_error(path: str, fmt: str) -> Optional[str]:
|
|
496
526
|
"""Why `path` does not read back as `fmt`, or None when it does (parses() without the record)."""
|
|
497
527
|
try:
|
|
498
528
|
text = Path(path).read_text(encoding="utf-8")
|
|
499
529
|
if fmt == "fcpxml":
|
|
500
|
-
|
|
501
|
-
tag = ET.fromstring(text.encode("utf-8")).tag
|
|
530
|
+
tag = _xml_root_tag(text.encode("utf-8"))
|
|
502
531
|
return None if tag == "fcpxml" else f"the root element is <{tag}>, not <fcpxml>"
|
|
503
532
|
if fmt == "otio":
|
|
504
533
|
doc = json.loads(text)
|
package/scripts/batch.py
CHANGED
|
@@ -49,7 +49,7 @@ ALLOWED_STEP_SCRIPTS = {p.name for p in HERE.glob("*.py") if not p.name.startswi
|
|
|
49
49
|
|
|
50
50
|
def file_key(path: Path) -> str:
|
|
51
51
|
st = path.stat()
|
|
52
|
-
h = hashlib.sha1()
|
|
52
|
+
h = hashlib.sha1(usedforsecurity=False) # a change detector, not a secret
|
|
53
53
|
h.update(f"{path.name}|{st.st_size}|{int(st.st_mtime)}".encode())
|
|
54
54
|
with open(path, "rb") as fh: # first and last MB: cheap and good enough to detect changes
|
|
55
55
|
h.update(fh.read(1 << 20))
|
|
@@ -75,7 +75,7 @@ def recipe_key(recipe: Dict[str, Any]) -> str:
|
|
|
75
75
|
# content, so say so (each item that needs it then fails on reading it)
|
|
76
76
|
warn(f"recipe.project {recipe['project']} could not be read ({exc}); the cache key covers the recipe only",
|
|
77
77
|
"recipe_project_unreadable", path=str(recipe["project"]), error=str(exc))
|
|
78
|
-
return hashlib.sha1((json.dumps(recipe, sort_keys=True) + "\0" + project_content).encode()).hexdigest()[:12]
|
|
78
|
+
return hashlib.sha1((json.dumps(recipe, sort_keys=True) + "\0" + project_content).encode(), usedforsecurity=False).hexdigest()[:12]
|
|
79
79
|
|
|
80
80
|
|
|
81
81
|
def gpu_key() -> str:
|
package/scripts/render.py
CHANGED
|
@@ -724,7 +724,7 @@ def cache_key(stage: str, script: str, argv: "Sequence[Any]", inputs: "Sequence[
|
|
|
724
724
|
"hw": [os.environ.get(HW_ENV, "").strip().lower() in ("1", "true", "yes", "on"), os.environ.get(HW_FORCED_ENV) == "1"],
|
|
725
725
|
"ffmpeg": CACHE.get("ffmpeg"), "skill": SKILL_VERSION, "contract": CONTRACT_VERSION,
|
|
726
726
|
}
|
|
727
|
-
return hashlib.sha1(json.dumps(payload, sort_keys=True, ensure_ascii=False).encode()).hexdigest()
|
|
727
|
+
return hashlib.sha1(json.dumps(payload, sort_keys=True, ensure_ascii=False).encode(), usedforsecurity=False).hexdigest()
|
|
728
728
|
|
|
729
729
|
|
|
730
730
|
def _cache_miss(stage: str, reason: str, **facts: Any) -> bool:
|