ffmpeg-skill 3.1.7 → 3.2.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 +4 -4
- package/SKILL.md +4 -4
- package/docs/contract.md +25 -2
- package/package.json +1 -1
- package/references/scripts.md +63 -11
- package/scripts/_common/decision.py +13 -1
- package/scripts/_common/emit.py +22 -0
- package/scripts/_common/runner.py +102 -22
- package/scripts/_contract.py +17 -8
- package/scripts/batch.py +175 -28
- package/scripts/check.py +7 -0
- package/scripts/color.py +3 -0
- package/scripts/cut.py +3 -0
- package/scripts/export.py +317 -12
- package/scripts/look.py +60 -5
- package/scripts/loop.py +182 -18
- package/scripts/loudness.py +136 -62
- package/scripts/metadata.py +9 -2
- package/scripts/render.py +39 -4
- package/scripts/sync.py +3 -0
- package/scripts/waveform.py +8 -0
package/scripts/render.py
CHANGED
|
@@ -75,7 +75,7 @@ from typing import Any, Dict, List, Optional, Sequence
|
|
|
75
75
|
from export import PRESETS, PLATFORM_OF
|
|
76
76
|
from _platforms import PLATFORMS, caption_defaults, resolve as resolve_platform
|
|
77
77
|
from _common import guard_sidecar, STATE, add_common, aspect_ratio, brand_caption_style, load_brand, apply_common, child_args, die, emit, info, probe, require_tool, run, run_tool, place_output, refuse_output_is_input, _check_existing_output, _check_output_path, fingerprint, PLAN_VERSION, ffmpeg_version, X264_PRESETS
|
|
78
|
-
from _common.runner import HW_ENV, HW_FORCED_ENV, add_hw_orchestrator_args, found_exe
|
|
78
|
+
from _common.runner import HW_ENV, HW_FORCED_ENV, add_hw_orchestrator_args, child_exit_code, found_exe, kept_clause, kept_deliverables, mark_delivered, remember_written
|
|
79
79
|
from _common.emit import absorb_stage, audit, record_fallback, record_skip, refuse, warn
|
|
80
80
|
import subprocess
|
|
81
81
|
from _contract import CONTRACT_VERSION
|
|
@@ -351,6 +351,7 @@ def render_pack(names: List[str], args) -> int:
|
|
|
351
351
|
rows: List[Dict[str, Any]] = []
|
|
352
352
|
outputs: List[str] = []
|
|
353
353
|
failed: List[str] = []
|
|
354
|
+
stopped: List[Dict[str, Any]] = [] # destinations a timeout stopped: {kind, exit_code}
|
|
354
355
|
for name in names:
|
|
355
356
|
# the destination decides the container: an audio-only destination in a pack must not be
|
|
356
357
|
# handed a .mp4 name the single-template form would never have written (review 12)
|
|
@@ -394,6 +395,9 @@ def render_pack(names: List[str], args) -> int:
|
|
|
394
395
|
outputs.append(out)
|
|
395
396
|
if not ok:
|
|
396
397
|
failed.append(name)
|
|
398
|
+
err = doc.get("error") or {}
|
|
399
|
+
if err.get("kind") in ("timeout", "interrupted"):
|
|
400
|
+
stopped.append({"kind": err["kind"], "exit_code": child_exit_code(doc, proc.returncode)})
|
|
397
401
|
pack = str(outdir / f"{stem}_pack.md")
|
|
398
402
|
lines = [f"# Social pack — {stem}", "",
|
|
399
403
|
"| platform | file | size | duration | check |", "|---|---|---|---|---|"]
|
|
@@ -410,8 +414,11 @@ def render_pack(names: List[str], args) -> int:
|
|
|
410
414
|
Path(pack).write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
411
415
|
info(("[dry-run] would write " if STATE.dry_run else "wrote ") + pack)
|
|
412
416
|
if failed:
|
|
417
|
+
# a destination that was stopped makes the pack a stopped run: its kind and exit code (124,
|
|
418
|
+
# 130/143), as a stage's are render.py's (docs/contract.md); otherwise a verification failure
|
|
413
419
|
die(f"pack: {len(failed)} of {len(rows)} destinations failed: {', '.join(failed)}",
|
|
414
|
-
|
|
420
|
+
code=stopped[0]["exit_code"] if stopped else 1, kind=stopped[0]["kind"] if stopped else "verification",
|
|
421
|
+
output=pack, dry_run=STATE.dry_run, pack=rows, outputs=outputs)
|
|
415
422
|
emit(pack, pack=rows, outputs=outputs, stages=["pack"],
|
|
416
423
|
verification=[{"step": "check", "ok": r["ok"], "platform": r["platform"]} for r in rows])
|
|
417
424
|
return 0
|
|
@@ -577,6 +584,8 @@ def sh(script: str, *argv: Any, extra: List[str] = None, stage: str = None) -> s
|
|
|
577
584
|
# child's document standing, which a caller reading _LAST_DOC would misattribute.
|
|
578
585
|
_LAST_DOC.clear()
|
|
579
586
|
_LAST_DOC["cached"] = True
|
|
587
|
+
if not STATE.dry_run:
|
|
588
|
+
remember_written(dest) # placed by this run, as a stage's own write would be (#415)
|
|
580
589
|
return dest
|
|
581
590
|
CACHE["misses"].append(stage)
|
|
582
591
|
_refuse_uncached_earlier_stage(stage)
|
|
@@ -603,12 +612,27 @@ def sh(script: str, *argv: Any, extra: List[str] = None, stage: str = None) -> s
|
|
|
603
612
|
extra_fields = {"hint": err["hint"]} if err.get("hint") else {}
|
|
604
613
|
error_extra = absorb_stage(Path(script).stem, doc) # the stage's reason/details and records ride along
|
|
605
614
|
extra_fields.update(_stage_failure_hw(doc))
|
|
606
|
-
|
|
615
|
+
why = err.get('message') or (proc.stderr.strip().splitlines() or ['?'])[-1][:300]
|
|
616
|
+
if kept_deliverables() and err.get("kind") in ("timeout", "interrupted"):
|
|
617
|
+
# a stage after the delivery (chapters, check): the stage's own "nothing was written"
|
|
618
|
+
# is about its own file, while the delivery is on disk (#415)
|
|
619
|
+
why = why.replace("; nothing was written", "") + f" ({kept_clause()})"
|
|
620
|
+
extra_fields.setdefault("output", kept_deliverables()[-1])
|
|
621
|
+
elif err.get("kind") in ("timeout", "interrupted") and doc.get("output") and os.path.isfile(str(doc["output"])):
|
|
622
|
+
# the stage was stopped after writing its own deliverable and kept it (export.py's
|
|
623
|
+
# --normalize pass): its message says so, and the file is this run's output too
|
|
624
|
+
extra_fields.setdefault("output", str(doc["output"]))
|
|
625
|
+
die(f"{script} failed: {why}",
|
|
607
626
|
code=int(doc.get("exit_code") or 1), kind=err.get("kind") or "input", stage=script, error_extra=error_extra, **extra_fields)
|
|
608
627
|
_LAST_DOC.clear()
|
|
609
628
|
_LAST_DOC.update(doc if isinstance(doc, dict) else {})
|
|
610
629
|
absorb_stage(Path(script).stem, doc) # a stage's GPU facts (ran on VideoToolbox, fell back and why) and its diagnostics reach this result
|
|
611
630
|
out_path = str(doc.get("output") or "")
|
|
631
|
+
if not STATE.dry_run:
|
|
632
|
+
# A file a stage of this run wrote is this run's, not someone else's: the chapters stage
|
|
633
|
+
# places its tagged copy over the export's delivery, which the output guard would
|
|
634
|
+
# otherwise refuse as an existing file without --overwrite (#415).
|
|
635
|
+
remember_written(out_path, dest)
|
|
612
636
|
if key:
|
|
613
637
|
cache_store(stage, key, out_path or (dest or ""), time.time() - started, doc)
|
|
614
638
|
return out_path
|
|
@@ -951,8 +975,10 @@ def execute_plan(plan: Dict[str, Any], path: str) -> int:
|
|
|
951
975
|
if proc.returncode != 0 or doc.get("status") != "completed":
|
|
952
976
|
err = doc.get("error") or {}
|
|
953
977
|
error_extra = absorb_stage(tool, doc) # the tool's reason/details and records ride along
|
|
978
|
+
# the tool's exit code is the plan's (124 timeout, 130/143 interrupted), as sh() passes a stage's on
|
|
954
979
|
die(f"{tool} failed while executing the plan: {err.get('message') or proc.stderr.strip()[-300:]}",
|
|
955
|
-
kind=err.get("kind", "ffmpeg"), plan=path, tool=tool,
|
|
980
|
+
code=child_exit_code(doc, proc.returncode), kind=err.get("kind", "ffmpeg"), plan=path, tool=tool,
|
|
981
|
+
error_extra=error_extra, **_stage_failure_hw(doc))
|
|
956
982
|
absorb_stage(tool, doc) # the tool's GPU facts (fell back and why, ran on VideoToolbox) and diagnostics reach this result
|
|
957
983
|
output = doc.get("output") or plan.get("output")
|
|
958
984
|
check_result = None
|
|
@@ -1611,6 +1637,8 @@ def main() -> int:
|
|
|
1611
1637
|
place_output(current, output)
|
|
1612
1638
|
info(("[dry-run] would copy" if STATE.dry_run else "copied") + f" final stage to {output}")
|
|
1613
1639
|
current = output
|
|
1640
|
+
if not STATE.dry_run:
|
|
1641
|
+
mark_delivered(output) # the chapters and check stages run on it: a stop from here keeps it, and says so (#415)
|
|
1614
1642
|
|
|
1615
1643
|
# ---- chapters (metadata.py on the delivered file: streams copied, markers written)
|
|
1616
1644
|
ch = proj.get("chapters")
|
|
@@ -1648,6 +1676,13 @@ def main() -> int:
|
|
|
1648
1676
|
exit_code = 1
|
|
1649
1677
|
elif check_result.get("error") or check_result.get("status") == "failed":
|
|
1650
1678
|
err = check_result.get("error")
|
|
1679
|
+
if isinstance(err, dict) and err.get("kind") in ("timeout", "interrupted"):
|
|
1680
|
+
# stopped, not failed: the run exits with check.py's own code (124, 130/143), as a
|
|
1681
|
+
# stage that stopped does, and says the delivery it had written is kept (#415)
|
|
1682
|
+
why = str(err.get("message") or "").replace("; nothing was written", "")
|
|
1683
|
+
die(f"check.py failed: {why} ({kept_clause()})",
|
|
1684
|
+
code=int(check_result.get("exit_code") or err.get("exit_code") or 1), kind=err["kind"],
|
|
1685
|
+
stage="check.py", output=output, stages=stages_done)
|
|
1651
1686
|
info(f"check: could not run check.py — {err.get('message') if isinstance(err, dict) else err}")
|
|
1652
1687
|
exit_code = 1
|
|
1653
1688
|
else:
|
package/scripts/sync.py
CHANGED
|
@@ -32,6 +32,7 @@ import os
|
|
|
32
32
|
import sys
|
|
33
33
|
from typing import Dict, List
|
|
34
34
|
|
|
35
|
+
from _common.runner import fail_if_classified
|
|
35
36
|
from _common import video_args, add_common, apply_common, emit, aac_args, audio_codec_for, default_output, die, ffmpeg_base, info, probe, require_tool, run, run_analysis, x264_args, decode_pcm_mono, rms_envelope
|
|
36
37
|
from _common.emit import record_skip, warn
|
|
37
38
|
from _common.decision import stream_starts
|
|
@@ -377,6 +378,7 @@ def main() -> int:
|
|
|
377
378
|
cmd += ["-af", ",".join(af_parts)]
|
|
378
379
|
cmd += aac_args() + (["-t", f"{ref_dur:.3f}"] if ref_dur else ["-shortest"]) + [output]
|
|
379
380
|
proc = run(cmd, check=False)
|
|
381
|
+
fail_if_classified(proc) # a cause the re-encode would hit too is reported, not retried (#415)
|
|
380
382
|
if proc.returncode != 0:
|
|
381
383
|
cmd = [c for c in cmd if c != "copy"]
|
|
382
384
|
idx = cmd.index("-c:v"); del cmd[idx]
|
|
@@ -389,6 +391,7 @@ def main() -> int:
|
|
|
389
391
|
cmd += ["-movflags", "+faststart"]
|
|
390
392
|
cmd += ["-avoid_negative_ts", "make_zero", output]
|
|
391
393
|
proc = run(cmd, check=False)
|
|
394
|
+
fail_if_classified(proc)
|
|
392
395
|
if proc.returncode != 0:
|
|
393
396
|
cmd = ffmpeg_base() + ["-ss", f"{head_trim:.4f}", "-i", args.second] + (video_args(probe(args.reference) if args.replace_audio else probe(args.second), args.crf) if has_video else []) + audio_codec_for(output) + [output]
|
|
394
397
|
run(cmd)
|
package/scripts/waveform.py
CHANGED
|
@@ -284,6 +284,14 @@ def main() -> int:
|
|
|
284
284
|
notes.append(f"the visualisation band and {args.platform}'s bottom safe zone overlap by "
|
|
285
285
|
f"{overlap}px: the captions or the app's own UI will sit over the waveform "
|
|
286
286
|
f"(lower --vis-height to {max(0.05, (args.height - safe_px) / args.height):.2f})")
|
|
287
|
+
if STATE.dry_run and (args.title or args.srt or args.text):
|
|
288
|
+
# graphics.py / caption.py plan on the render this dry run did not write, whose frame they
|
|
289
|
+
# cannot measure: their sizes and offsets come out 0 in the plan, and the real run takes
|
|
290
|
+
# them from the WxH render (#415). Said here, so a plan is not read as the real numbers.
|
|
291
|
+
later = " and ".join(n for n, on in (("title", args.title), ("captions", args.srt or args.text)) if on)
|
|
292
|
+
notes.append(f"the {later} stage was planned on the visualisation render this dry run did not write, so the "
|
|
293
|
+
f"font sizes and offsets in its planned command are placeholders (0); the real run sizes them "
|
|
294
|
+
f"from the {args.width}x{args.height} render")
|
|
287
295
|
if silent_peak is not None:
|
|
288
296
|
warn(f"{args.input} is silent (peak {silent_peak:g} dBFS); the visualisation is a flat line", "silent_input",
|
|
289
297
|
path=args.input, peak_db=silent_peak)
|