ffmpeg-skill 0.7.0 → 0.7.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/bin/install.js CHANGED
@@ -88,7 +88,11 @@ for (const t of targets) {
88
88
  }
89
89
  fs.rmSync(t.dir, { recursive: true, force: true });
90
90
  fs.mkdirSync(t.dir, { recursive: true });
91
- for (const item of PAYLOAD) copyRecursive(path.join(ROOT, item), path.join(t.dir, item));
91
+ for (const item of PAYLOAD) {
92
+ const src = path.join(ROOT, item);
93
+ if (!fs.existsSync(src)) { if (item !== 'SKILL.md' && item !== 'scripts') continue; throw new Error(`missing ${item} in package`); }
94
+ copyRecursive(src, path.join(t.dir, item));
95
+ }
92
96
  console.log(`installed ${t.label}: ${t.dir}`);
93
97
  } catch (err) {
94
98
  failed = true;
package/mcp/server.py ADDED
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env python3
2
+ """ffmpeg-skill as an MCP server (stdio, JSON-RPC 2.0) — standard library only.
3
+
4
+ Every script in ../scripts becomes a tool; arguments are passed as an argv list
5
+ or as a flat object of flags. Results are the script's --json output.
6
+
7
+ Run:
8
+ python3 mcp/server.py # stdio transport
9
+ Claude Desktop / Claude Code config example:
10
+ {"mcpServers": {"ffmpeg-skill": {"command": "python3", "args": ["/path/to/ffmpeg-skill/mcp/server.py"]}}}
11
+ """
12
+ import json
13
+ import os
14
+ import subprocess
15
+ import sys
16
+ from pathlib import Path
17
+ from typing import Any, Dict, List
18
+
19
+ HERE = Path(__file__).resolve().parent
20
+ SCRIPTS = HERE.parent / "scripts"
21
+ PROTOCOL_VERSION = "2024-11-05"
22
+
23
+ TOOLS: Dict[str, str] = {
24
+ "probe": "Inspect a media file: duration, fps/VFR, resolution, codecs, HDR, rotation, audio. Args: inputs (list), analyze (bool), compact (bool).",
25
+ "cut": "Cut a range or several segments, lossless when possible. Args: input, start, end, duration, segments, accurate, output.",
26
+ "fit": "Fit to a duration (speed/trim) and/or aspect (pad/crop). Args: input, duration, method, aspect, fit, width, fps, smooth, output.",
27
+ "caption": "Burn SRT/ASS or timed text; animated/karaoke styles; brand. Args: input, srt, ass, text, animate, karaoke, font, size, position, brand, output.",
28
+ "overlay": "Composite a logo/image/text with timing and fade. Args: input, image, text, logo, position, start, end, fade, opacity, scale, brand, output.",
29
+ "graphics": "Motion-graphics templates: lower-third, title, chapter, progress, countdown, bug. Args: input, template, name, title, subtitle, start, end, brand, output.",
30
+ "sync": "Detect offset between two recordings by audio, optional drift fix. Args: reference, second, fix_drift, replace_audio, trim_second, output.",
31
+ "multicam": "Align N cameras/recorders and switch between them. Args: inputs (list), switch, auto, audio, fix_drift, output.",
32
+ "audio": "Denoise/voice chain, music bed with ducking, fades, downmix, replace. Args: input, voice, denoise, music, duck, music_volume, fade_in, fade_out, downmix, replace, output.",
33
+ "loudness": "Two-pass EBU R128 normalisation. Args: input, lufs, tp, measure_only, output.",
34
+ "silence": "Remove dead air / list silences. Args: input, threshold, min_silence, margin, list, edl, output.",
35
+ "join": "Concatenate clips with transitions, normalising size/fps/audio. Args: inputs (list), transition, duration, width, height, fps, output.",
36
+ "color": "HDR→SDR tone mapping, LUTs, retag, strip Dolby Vision. Args: input, to_sdr, lut, retag, strip_dovi, tonemap, output.",
37
+ "export": "Platform presets: youtube, youtube4k, reels, x, prores, h265, gif. Args: input, preset, fit, output.",
38
+ "check": "Pre-delivery compliance per platform. Args: input, platform, no_loudness.",
39
+ "scenes": "Scene changes, audio peaks, highlight proposals. Args: input, highlights, target, edl, sheet.",
40
+ "look": "Contact sheet / frames / before-after PNG for visual checks. Args: input, at (list), tiles, compare, output.",
41
+ "render": "Render a whole edit from project.json. Args: project, fast, stop_after, init.",
42
+ "verify": "Run the toolchain on real files and report PASS/FAIL. Args: paths (list), quick, report.",
43
+ "report": "HTML delivery report. Args: after, before, platform, commands, notes, title, output.",
44
+ }
45
+ POSITIONAL = {"probe": ["inputs"], "sync": ["reference", "second"], "multicam": ["inputs"], "join": ["inputs"], "verify": ["paths"], "render": ["project"]}
46
+
47
+
48
+ def build_argv(name: str, args: Dict[str, Any]) -> List[str]:
49
+ if isinstance(args.get("argv"), list):
50
+ argv = [str(a) for a in args["argv"]]
51
+ if name not in ("look", "probe") and "--json" not in argv and "--help" not in argv:
52
+ argv.append("--json")
53
+ return argv
54
+ argv: List[str] = []
55
+ args = dict(args)
56
+ pos = POSITIONAL.get(name, ["input"])
57
+ for key in pos:
58
+ val = args.pop(key, None)
59
+ if val is None:
60
+ continue
61
+ if isinstance(val, list):
62
+ argv += [str(v) for v in val]
63
+ else:
64
+ argv.append(str(val))
65
+ for key, val in args.items():
66
+ if val is None or val is False:
67
+ continue
68
+ flag = "--" + key.replace("_", "-")
69
+ if key == "output":
70
+ flag = "-o"
71
+ if key == "lufs" and name == "loudness":
72
+ flag = "-I"
73
+ if val is True:
74
+ argv.append(flag)
75
+ elif isinstance(val, list):
76
+ for v in val:
77
+ argv += [flag, str(v)]
78
+ else:
79
+ argv += [flag, str(val)]
80
+ if name not in ("look", "probe") and "--json" not in argv and "--help" not in argv:
81
+ argv.append("--json")
82
+ return argv
83
+
84
+
85
+ def call_tool(name: str, args: Dict[str, Any]) -> Dict[str, Any]:
86
+ script = SCRIPTS / f"{name}.py"
87
+ if not script.exists():
88
+ return {"isError": True, "content": [{"type": "text", "text": f"unknown tool {name}"}]}
89
+ argv = build_argv(name, args or {})
90
+ proc = subprocess.run([sys.executable, str(script)] + argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
91
+ stdout = proc.stdout.strip()
92
+ text = stdout
93
+ structured = None
94
+ try:
95
+ structured = json.loads(stdout) if stdout.startswith("{") or stdout.startswith("[") else None
96
+ except ValueError:
97
+ structured = None
98
+ if proc.returncode != 0:
99
+ err = proc.stderr.strip().splitlines()
100
+ tail = "\n".join(err[-12:])
101
+ return {"isError": True, "content": [{"type": "text", "text": f"{name} failed (exit {proc.returncode})\n{tail}"}]}
102
+ if structured is None:
103
+ text = stdout or "\n".join(proc.stderr.strip().splitlines()[-5:])
104
+ result: Dict[str, Any] = {"content": [{"type": "text", "text": text}]}
105
+ if structured is not None:
106
+ result["structuredContent"] = structured if isinstance(structured, dict) else {"result": structured}
107
+ return result
108
+
109
+
110
+ def tool_list() -> List[Dict[str, Any]]:
111
+ out = []
112
+ for name, desc in TOOLS.items():
113
+ out.append({
114
+ "name": name,
115
+ "description": desc + " Pass either named args (flags without dashes, underscores for hyphens) or argv (raw CLI list). Media paths must be absolute.",
116
+ "inputSchema": {"type": "object", "properties": {"argv": {"type": "array", "items": {"type": "string"}, "description": "raw CLI arguments"}}, "additionalProperties": True},
117
+ })
118
+ return out
119
+
120
+
121
+ def handle(req: Dict[str, Any]) -> Dict[str, Any]:
122
+ method = req.get("method")
123
+ params = req.get("params") or {}
124
+ if method == "initialize":
125
+ return {"protocolVersion": PROTOCOL_VERSION, "capabilities": {"tools": {}}, "serverInfo": {"name": "ffmpeg-skill", "version": version()}}
126
+ if method == "tools/list":
127
+ return {"tools": tool_list()}
128
+ if method == "tools/call":
129
+ return call_tool(params.get("name", ""), params.get("arguments") or {})
130
+ if method == "ping":
131
+ return {}
132
+ raise KeyError(method)
133
+
134
+
135
+ def version() -> str:
136
+ try:
137
+ return json.loads((HERE.parent / "package.json").read_text())["version"]
138
+ except Exception:
139
+ return "0"
140
+
141
+
142
+ def main() -> int:
143
+ if "--list" in sys.argv:
144
+ for t in tool_list():
145
+ print(f"{t['name']:10s} {t['description'].split(' Pass either')[0]}")
146
+ return 0
147
+ if "--call" in sys.argv: # debugging helper: --call NAME '{"input": "..."}'
148
+ i = sys.argv.index("--call")
149
+ name = sys.argv[i + 1]
150
+ args = json.loads(sys.argv[i + 2]) if len(sys.argv) > i + 2 else {}
151
+ print(json.dumps(call_tool(name, args), indent=2))
152
+ return 0
153
+ stdin = sys.stdin.buffer
154
+ stdout = sys.stdout.buffer
155
+ for raw in stdin:
156
+ line = raw.decode("utf-8", errors="replace").strip()
157
+ if not line:
158
+ continue
159
+ try:
160
+ req = json.loads(line)
161
+ except ValueError:
162
+ continue
163
+ if "id" not in req: # notification
164
+ continue
165
+ try:
166
+ result = handle(req)
167
+ resp = {"jsonrpc": "2.0", "id": req["id"], "result": result}
168
+ except KeyError as exc:
169
+ resp = {"jsonrpc": "2.0", "id": req["id"], "error": {"code": -32601, "message": f"method not found: {exc}"}}
170
+ except Exception as exc: # noqa: BLE001
171
+ resp = {"jsonrpc": "2.0", "id": req["id"], "error": {"code": -32000, "message": str(exc)}}
172
+ stdout.write((json.dumps(resp) + "\n").encode("utf-8"))
173
+ stdout.flush()
174
+ return 0
175
+
176
+
177
+ if __name__ == "__main__":
178
+ sys.exit(main())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ffmpeg-skill",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Agent Skill that lets coding agents (Claude Code, Cursor, Codex) do professional video editing with local FFmpeg: MCP server, batch processing, declarative project rendering, brand kits, motion-graphics templates, HTML delivery reports, scene detection, delivery checks, cut, silence removal, transitions, multicam, captions, sync with drift correction, HDR to SDR, LUTs, audio clean-up and ducking, loudness, platform exports. No API keys, no cloud, no dependencies.",
5
5
  "keywords": ["ffmpeg", "video", "agent-skill", "claude-code", "cursor", "codex", "skill", "video-editing"],
6
6
  "license": "MIT",
@@ -17,6 +17,7 @@
17
17
  "files": [
18
18
  "bin/",
19
19
  "scripts/",
20
+ "mcp/",
20
21
  "SKILL.md",
21
22
  "README.md",
22
23
  "LICENSE"