ffmpeg-skill 1.4.15 → 1.5.0

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/README.md CHANGED
@@ -355,6 +355,7 @@ FFmpeg 8 shortened the flag column of `ffmpeg -filters`. A parser anchored on th
355
355
  | **F1 0.97** | `scenes.py`, 53 hard cuts between single takes, precision 0.95, recall 1.00 at the default threshold |
356
356
  | **exact to the sample** | `cut.py --accurate` on WAV, FLAC (44.1 kHz) and AAC → WAV; WAV stream copy within 2 ms; AAC output +21 ms of encoder priming, reported as `codec_frame` (0.9.1) |
357
357
  | **72 / 72** | agent runs of 24 prompts (12 English edits, 8 Japanese, 4 that must be declined), three repeats, graded by an independent model: routing, honest refusals and user's language 72/72, report format 71/72, visual check whenever the picture changed 24/24 (0.8.4) |
358
+ | **36 / 36** | 1.4.15 re-run (2026-09-12, one pass per prompt, Sonnet agent, regex grader + manual review): 24-prompt set routing 20/20, honest refusals 5/5, visual check 8/8, report format 25/25, user's language 9/9; exec set real execution 6/6, honest failure on bad inputs 5/5 with 0 false successes, audio-as-audio 3/3, one Japanese report with English labels; trigger set 22/22. Both iteration-5 defects gone (no raw ffmpeg fallback, music no longer shortens the video). Details in `evals/results/iteration-6.json` |
358
359
  | **36 / 36** | 1.4.0 re-run (2026-09-11, one pass per prompt, Sonnet agent, regex grader + manual review): 24-prompt set routing 20/20, honest refusals 5/5, visual check 8/8, user's language 8/9; exec set real execution 6/6, honest failure on bad inputs 5/5 with 0 false successes, audio-as-audio 3/3; trigger set 22/22. Details and the six findings in `evals/results/iteration-5.json` |
359
360
  | **6 / 6** | 0.9.1 audio evals (audio join, extraction, track selection, sample-accurate trim, typed dynamics; 2 in Japanese): routing, report format and audio-as-audio handling 6/6 |
360
361
 
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 (`1.4.15`) | any release |
24
+ | `skill.version` | the npm / package.json version (`1.5.0`) | 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`
@@ -83,7 +83,7 @@ on, the line says so.
83
83
  ```json
84
84
  {
85
85
  "contract_version": "1.0",
86
- "skill": {"id": "ffmpeg-skill", "version": "1.4.15", "execution_mode": "local", "kind": "execution",
86
+ "skill": {"id": "ffmpeg-skill", "version": "1.5.0", "execution_mode": "local", "kind": "execution",
87
87
  "entrypoints": {"cli": "...", "mcp": "...", "contract": "...", "doctor": "..."},
88
88
  "not_provided": ["AI reasoning", "decisions", "production plans", "project IR", "approvals", "network access", "transcription engine"]},
89
89
  "requirements": {"python": ">=3.9 (standard library only)", "ffmpeg": ">=5.0", "ffprobe": ">=5.0"},
@@ -299,6 +299,15 @@ Success (`exit 0`): one document matching `output_schema`, always with
299
299
  `status: "completed"`, `output`, `dry_run`, `commands`, and `probe` of the output when a
300
300
  file was written. `probe` prints its measurement document directly.
301
301
 
302
+ With `FFMPEG_SKILL_RESULT_V2=1` in the environment, every writing tool's success document
303
+ also carries `result_v2`: a preview of the one shape 2.0 will use for every tool
304
+ (issue #189). `{"schema": 2, "output", "probe", "commands", "metrics", "notes", "dropped":
305
+ {"non_av_streams"}, "details"}` -- `metrics` holds the numbers a caller keys on (loudness's
306
+ measurement dicts flattened, plus any numeric top-level key such as `expected_duration` or
307
+ `offset_seconds`), `notes` the free text, `details` the tool's remaining keys unchanged. The
308
+ 1.x keys are not moved; the environment variable only adds the key, and its absence is the
309
+ default until 2.0.
310
+
302
311
  Success is decided by `verify_output` in `_common.py`, not by the ffmpeg exit code alone:
303
312
  the file must exist, be non-empty and give ffprobe at least one stream. A tool that ran
304
313
  ffmpeg successfully but has no usable artifact fails with `kind: output` (a 0-byte file is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ffmpeg-skill",
3
- "version": "1.4.15",
3
+ "version": "1.5.0",
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",
@@ -348,11 +348,44 @@ def emit(output: Optional[str], **extra: Any) -> None:
348
348
  if meta:
349
349
  doc["probe"] = meta
350
350
  doc.update(extra)
351
+ if os.environ.get("FFMPEG_SKILL_RESULT_V2", "") not in ("", "0"):
352
+ doc["result_v2"] = _result_v2(output, meta, extra)
351
353
  print_json(doc)
352
354
  elif output:
353
355
  print(output)
354
356
 
355
357
 
358
+ _V2_HANDLED = ("result", "measured", "notes", "dropped_non_av_streams")
359
+
360
+
361
+ def _result_v2(output: Optional[str], meta: Dict[str, Any], extra: Dict[str, Any]) -> Dict[str, Any]:
362
+ """The 2.0 success-document shape, previewed in 1.x as a parallel `result_v2` key when
363
+ FFMPEG_SKILL_RESULT_V2=1 (issue #189 B). Every tool gets the same six slots: `output`,
364
+ `probe`, `commands`, `metrics` (numbers a caller keys on: loudness's `result`/`measured`
365
+ dicts flattened, plus every top-level numeric extra such as `expected_duration` or
366
+ `offset_seconds`), `notes` (free text), `dropped` (what did not make it into the output),
367
+ and `details` (the tool's remaining extras, unchanged). The 1.x keys stay where they are;
368
+ this key is additive and its shape is what 2.0 promotes to the top level."""
369
+ metrics: Dict[str, Any] = {}
370
+ for key in ("measured", "result"):
371
+ if isinstance(extra.get(key), dict):
372
+ metrics.update(extra[key])
373
+ for key, value in extra.items():
374
+ if key not in _V2_HANDLED and isinstance(value, (int, float)) and not isinstance(value, bool):
375
+ metrics[key] = value
376
+ notes = extra.get("notes")
377
+ return {
378
+ "schema": 2,
379
+ "output": output,
380
+ "probe": meta or None,
381
+ "commands": list(STATE.commands),
382
+ "metrics": metrics,
383
+ "notes": list(notes) if isinstance(notes, (list, tuple)) else ([notes] if notes else []),
384
+ "dropped": {"non_av_streams": bool(extra.get("dropped_non_av_streams", False))},
385
+ "details": {k: v for k, v in extra.items() if k not in _V2_HANDLED and k not in metrics},
386
+ }
387
+
388
+
356
389
  def _cmdline(cmd: Sequence[str]) -> str:
357
390
  return " ".join(shell_quote(c) for c in cmd)
358
391