ffmpeg-skill 1.4.1 → 1.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ffmpeg-skill",
3
- "version": "1.4.1",
3
+ "version": "1.4.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",
@@ -180,3 +180,13 @@ admin with Contents: read/write on this repo -- whose "Repository admin" bypass
180
180
  push triggers workflows (GITHUB_TOKEN's do not), so the bump commit carries `[skip ci]`; the
181
181
  tag, Release and npm publish all happen in the originating run. Rotate the PAT before it
182
182
  expires or the next release fails at the same step, cleanly, before anything is published.
183
+
184
+ ### A literal `[skip ci]` anywhere in a PR body skips every workflow on the squash merge
185
+
186
+ Found on the merge of #170 (2026-09-11). The PR body quoted the new bump-commit message
187
+ verbatim, including `[skip ci]`; a squash merge copies the PR body into the merge commit, and
188
+ GitHub honours the marker wherever it appears in the commit message. Nothing ran on `main` for
189
+ that merge -- no tests, no CodeQL, no release -- and the release only happened when the next PR
190
+ merged. Describe the marker in words in PR bodies and commit messages ("the skip-CI marker"),
191
+ or wrap it so it does not match, and after any merge that touches CI check that the push
192
+ actually triggered the expected runs.
package/scripts/color.py CHANGED
@@ -112,26 +112,46 @@ def correction_chain(args: argparse.Namespace) -> str:
112
112
  die(f"--levels-out-black {out_black} must be less than --levels-out-white {out_white}")
113
113
 
114
114
  gm, rm, bm = -tint, tint / 2.0, tint / 2.0
115
- terms = [
115
+ rgb = [
116
116
  f"exposure=exposure={exposure:g}",
117
117
  f"colortemperature=temperature={temperature:g}",
118
118
  f"colorbalance=rs={lift:g}:gs={lift:g}:bs={lift:g}:rm={rm:g}:gm={gm:g}:bm={bm:g}:rh={gain:g}:gh={gain:g}:bh={gain:g}",
119
- f"eq=contrast={contrast:g}:saturation={saturation:g}:gamma={gamma:g}",
120
119
  ]
120
+ terms = [_rgb_stage(rgb), f"eq=contrast={contrast:g}:saturation={saturation:g}:gamma={gamma:g}"]
121
+ rgb2 = []
121
122
  if (in_black, in_white, out_black, out_white) != (0, 255, 0, 255):
122
123
  rimin, rimax = in_black / 255.0, in_white / 255.0
123
124
  romin, romax = out_black / 255.0, out_white / 255.0
124
- terms.append(
125
+ rgb2.append(
125
126
  f"colorlevels=rimin={rimin:g}:gimin={rimin:g}:bimin={rimin:g}:"
126
127
  f"rimax={rimax:g}:gimax={rimax:g}:bimax={rimax:g}:"
127
128
  f"romin={romin:g}:gomin={romin:g}:bomin={romin:g}:"
128
129
  f"romax={romax:g}:gomax={romax:g}:bomax={romax:g}"
129
130
  )
130
131
  if args.curves:
131
- terms.append(f"curves=preset={args.curves}")
132
+ rgb2.append(f"curves=preset={args.curves}")
133
+ if rgb2:
134
+ terms.append(_rgb_stage(rgb2))
132
135
  return ",".join(terms)
133
136
 
134
137
 
138
+ def _rgb_stage(filters: list) -> str:
139
+ """Wrap a run of RGB-only filters in explicit, matching YUV<->RGB conversions.
140
+
141
+ exposure/colortemperature/colorbalance/colorlevels/curves take RGB, so libavfilter inserts a
142
+ swscale conversion on each side. Left to itself, the way in honours the frame's colour tag
143
+ (bt709 on any camera or export.py file) while the way back uses swscale's default matrix
144
+ (bt601): on a bt709-tagged source an all-defaults --correct came out 26 dB PSNR from its
145
+ input and ~8 % less saturated (#159). Untagged sources never showed it because both legs
146
+ then fall back to bt601 and cancel. Pinning both legs to the same matrix restores the
147
+ identity on every source (39 dB, the same as an untagged one always got); bt601 on both
148
+ sides measured better than bt709 on both (34 dB) because swscale's 601 path round-trips
149
+ 8-bit 4:2:0 more exactly. The matrix here is only the working space of the conversion pair,
150
+ never a tag: the output carries the encoder's BT.709 tags as before."""
151
+ return ("scale=in_color_matrix=bt601,format=gbrpf32le," + ",".join(filters)
152
+ + ",scale=out_color_matrix=bt601,format=yuv420p")
153
+
154
+
135
155
  def hdr_to_sdr_chain(meta: dict, tonemap: str, peak: float, desat: float) -> str:
136
156
  v = meta["video"]
137
157
  trc = v.get("color_transfer") or "smpte2084"