ffmpeg-skill 3.1.6 → 3.1.8
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 +22 -6
- package/package.json +1 -1
- package/references/scripts.md +19 -6
- package/scripts/_common/asr.py +49 -10
- package/scripts/_common/decision.py +13 -1
- package/scripts/_common/fonts.py +26 -0
- package/scripts/_common/probe.py +20 -3
- package/scripts/_common/runner.py +136 -22
- package/scripts/_common/timeline.py +118 -23
- package/scripts/_contract.py +33 -2
- package/scripts/batch.py +175 -28
- package/scripts/caption.py +95 -11
- package/scripts/check.py +7 -0
- package/scripts/color.py +3 -0
- package/scripts/cut.py +3 -0
- package/scripts/export.py +22 -15
- package/scripts/metadata.py +9 -2
- package/scripts/render.py +39 -4
- package/scripts/silence.py +30 -5
- package/scripts/sync.py +3 -0
- package/scripts/waveform.py +8 -0
package/scripts/export.py
CHANGED
|
@@ -38,10 +38,12 @@ import tempfile
|
|
|
38
38
|
from pathlib import Path
|
|
39
39
|
from typing import Dict, List
|
|
40
40
|
|
|
41
|
+
from _common.runner import child_exit_code, fail_if_classified, kept_clause, mark_delivered
|
|
41
42
|
from _common import STATE, run_analysis, add_common, apply_common, bt709_label_filter, bt709_matrix_args, bt709_tag_args, child_args, emit, cfr_args, default_output, die, encoder_args, ffmpeg_base, info, probe, require_tool, run, run_tool, validate_color, pad_filters, add_pad_fill_args, fmt_secs
|
|
42
43
|
from _common.decision import hw_preset_video, restate_last_swap
|
|
43
44
|
from _common.emit import note, record_fallback, record_skip, warn
|
|
44
45
|
from _common.probe import square_pixels, square_pixels_note
|
|
46
|
+
from _common.runner import subtitle_codec_for
|
|
45
47
|
from check import SPECS as PLATFORMS, measure_loudness
|
|
46
48
|
from _platforms import ALIASES as _ALIASES, PLATFORMS as PLATFORM_TABLE, TP_TOLERANCE_DB, resolve as resolve_platform
|
|
47
49
|
# Frame and duration limit come from the one platform table (scripts/_platforms.py) rather than
|
|
@@ -142,19 +144,10 @@ def _loudness_unmeasured(extra: Dict[str, object], notes: List[str], platform: s
|
|
|
142
144
|
|
|
143
145
|
# Text subtitle codecs ffmpeg can convert to the container's own text track: mov_text is the one
|
|
144
146
|
# text track MP4/MOV hold, Matroska holds SubRip/ASS/WebVTT as they are but not mov_text.
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
def _subtitle_codec(codec: str, out_ext: str) -> str:
|
|
150
|
-
"""The -c:s for one source subtitle stream: copy when the output container holds it as it is,
|
|
151
|
-
else the container's own text format (a bitmap track is tried as a copy; a container that
|
|
152
|
-
refuses it is the fallback below)."""
|
|
153
|
-
if out_ext in _MP4_FAMILY and codec in _TEXT_SUBTITLES and codec != "mov_text":
|
|
154
|
-
return "mov_text"
|
|
155
|
-
if out_ext == "mkv" and codec == "mov_text":
|
|
156
|
-
return "srt"
|
|
157
|
-
return "copy"
|
|
147
|
+
# the -c:s for one source subtitle stream (#380), shared with caption.py --mode mux (#414): copy
|
|
148
|
+
# when the output container holds it as it is, else the container's own text format (a bitmap
|
|
149
|
+
# track is tried as a copy; a container that refuses it is the fallback below)
|
|
150
|
+
_subtitle_codec = subtitle_codec_for
|
|
158
151
|
|
|
159
152
|
|
|
160
153
|
def _data_streams(path: str) -> List[Dict[str, object]]:
|
|
@@ -191,6 +184,9 @@ def _run_keeping_non_av(cmd: List[str], output: str, sub_codecs: List[str], data
|
|
|
191
184
|
attempt = run(cmd + extra + [output], check=False)
|
|
192
185
|
if attempt.returncode == 0:
|
|
193
186
|
return dropped
|
|
187
|
+
# a full disk or a missing encoder fails every rung the same way: that error, not a
|
|
188
|
+
# fallback that blames the subtitles and encodes again (#415, #389's rule)
|
|
189
|
+
fail_if_classified(attempt)
|
|
194
190
|
error = " | ".join([ln.strip() for ln in (attempt.stderr or "").splitlines() if ln.strip()][-3:])[-500:]
|
|
195
191
|
going = rungs[n + 1][1]
|
|
196
192
|
record_fallback(f"{os.path.basename(output)} could not hold the source's {' and '.join(t for t in ('subtitle', 'data') if t in going)} "
|
|
@@ -355,7 +351,9 @@ def main() -> int:
|
|
|
355
351
|
notes.append("source is %s (%s). This preset outputs SDR BT.709 tags without converting it; run color.py --to-sdr first for correct colours." % (kind, meta["video"].get("hdr_format")))
|
|
356
352
|
warn(notes[-1], "colour_not_converted", preset=args.preset, hdr_format=meta["video"].get("hdr_format"))
|
|
357
353
|
has_audio = bool(meta.get("audio"))
|
|
358
|
-
|
|
354
|
+
# copy keeps the source's container, so its default name keeps the source's extension (as
|
|
355
|
+
# metadata.py's does): default_output()'s .webm -> .mp4 is for the re-encoding presets
|
|
356
|
+
output = args.output or default_output(args.input, args.preset, p["ext"] or Path(args.input).suffix.lstrip(".") or None)
|
|
359
357
|
out_ext = Path(output).suffix.lstrip(".").lower()
|
|
360
358
|
|
|
361
359
|
vf: List[str] = []
|
|
@@ -474,6 +472,10 @@ def main() -> int:
|
|
|
474
472
|
dropped = _run_keeping_non_av(cmd, output, sub_codecs, len(data)) if (subs or data) else []
|
|
475
473
|
if not (subs or data):
|
|
476
474
|
run(cmd + [output])
|
|
475
|
+
if not STATE.dry_run:
|
|
476
|
+
# the export is on disk: a stop in the reads and the --normalize pass that follow keeps it,
|
|
477
|
+
# and says so (#415)
|
|
478
|
+
mark_delivered(output)
|
|
477
479
|
result = probe(output, role="output")
|
|
478
480
|
v = result["video"]
|
|
479
481
|
info(f"wrote {output} ({fmt_secs(result['duration'])}, {v['width']}x{v['height']}, {v['codec']})")
|
|
@@ -516,7 +518,12 @@ def main() -> int:
|
|
|
516
518
|
err = child.get("error") or {}
|
|
517
519
|
if os.path.exists(tmp):
|
|
518
520
|
os.remove(tmp)
|
|
519
|
-
|
|
521
|
+
# the child's "nothing was written" is about its own temp file; this run's export is
|
|
522
|
+
# on disk, and the child's exit code (124 timeout, 130/143 interrupted) is the
|
|
523
|
+
# run's, as render.py and waveform.py pass a stage's on (#415)
|
|
524
|
+
why = (err.get("message") or proc.stderr.strip()[-300:]).replace("; nothing was written", "")
|
|
525
|
+
die(f"--normalize: loudness.py failed: {why} ({kept_clause()})",
|
|
526
|
+
code=child_exit_code(child, proc.returncode) if proc.returncode else 1,
|
|
520
527
|
kind=err.get("kind") or "ffmpeg", output=output, hint=err.get("hint"))
|
|
521
528
|
os.replace(tmp, output)
|
|
522
529
|
m = measure_loudness(output)
|
package/scripts/metadata.py
CHANGED
|
@@ -221,7 +221,8 @@ def main() -> int:
|
|
|
221
221
|
die(f"chapters file not found: {args.chapters}")
|
|
222
222
|
|
|
223
223
|
meta = probe(args.input)
|
|
224
|
-
|
|
224
|
+
# streams are copied, so the source's own container always holds them (a .webm stays .webm)
|
|
225
|
+
output = args.output or default_output(args.input, "meta", Path(args.input).suffix.lstrip(".") or "mp4")
|
|
225
226
|
if os.path.abspath(output) == os.path.abspath(args.input):
|
|
226
227
|
die("output must differ from the input (metadata.py never rewrites a file in place)")
|
|
227
228
|
out_ext = Path(output).suffix.lower()
|
|
@@ -249,7 +250,13 @@ def main() -> int:
|
|
|
249
250
|
cmd += ["-map", "0", "-map_metadata", "0", "-map_chapters", "0"]
|
|
250
251
|
for key, value in tags.items():
|
|
251
252
|
cmd += ["-metadata", f"{key}={value}"]
|
|
252
|
-
cmd += ["-c", "copy"
|
|
253
|
+
cmd += ["-c", "copy"]
|
|
254
|
+
if out_ext in (".mp4", ".mov", ".m4v", ".m4a"):
|
|
255
|
+
# A stream copy into the MPEG-4 family writes the index (moov) after the media unless told
|
|
256
|
+
# otherwise, so without +faststart a faststart input -- render's export -- came out with
|
|
257
|
+
# its index at the end (#415; every other MP4 write here is faststart since #279)
|
|
258
|
+
cmd += ["-movflags", "+faststart"]
|
|
259
|
+
cmd.append(output)
|
|
253
260
|
run(cmd)
|
|
254
261
|
if tmpdir:
|
|
255
262
|
tmpdir.cleanup()
|
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/silence.py
CHANGED
|
@@ -83,10 +83,29 @@ def speech_aware_silences(path: str, threshold: float, min_silence: float) -> "T
|
|
|
83
83
|
return boundaries, breaths
|
|
84
84
|
|
|
85
85
|
|
|
86
|
+
def inside_margin(silences: List[Tuple[float, float]], duration: float, margin: float) -> List[Tuple[float, float]]:
|
|
87
|
+
"""Each silence with --margin kept on both sides: the stretch actually removed. A silence too
|
|
88
|
+
short to hold two margins removes nothing (it is dropped, never turned inside out).
|
|
89
|
+
|
|
90
|
+
The margin is applied to the silences alone, before they are merged with filler spans (#414):
|
|
91
|
+
a filler word's span is the word itself plus --filler-pad, and taking a 0.2 s margin off each
|
|
92
|
+
side of it turned every word narrower than 2 x --margin inside out -- kept, and still counted
|
|
93
|
+
as removed."""
|
|
94
|
+
out: List[Tuple[float, float]] = []
|
|
95
|
+
for s, e in silences:
|
|
96
|
+
a = s + margin
|
|
97
|
+
b = duration if e == float("inf") else min(duration, e - margin)
|
|
98
|
+
if b > a:
|
|
99
|
+
out.append((a, b))
|
|
100
|
+
return out
|
|
101
|
+
|
|
102
|
+
|
|
86
103
|
def keep_ranges(silences: List[Tuple[float, float]], duration: float, margin: float, min_keep: float) -> List[Tuple[float, float]]:
|
|
87
104
|
keeps: List[Tuple[float, float]] = []
|
|
88
105
|
cursor = 0.0
|
|
89
106
|
for s, e in sorted(silences):
|
|
107
|
+
if e != float("inf") and e - margin <= s + margin:
|
|
108
|
+
continue # narrower than two margins: nothing of it is removed (#414)
|
|
90
109
|
s_adj = max(cursor, s + margin)
|
|
91
110
|
if s_adj - cursor >= min_keep:
|
|
92
111
|
keeps.append((cursor, s_adj))
|
|
@@ -503,6 +522,9 @@ def main() -> int:
|
|
|
503
522
|
media_out = None if (args.list or args.filler_list) else (args.output or default_output(args.input, "tight"))
|
|
504
523
|
guard_sidecar(args.edl, args.input, args.words, args.filler_words, siblings=(media_out,))
|
|
505
524
|
|
|
525
|
+
if not math.isfinite(args.margin) or args.margin < 0:
|
|
526
|
+
# nan made inside_margin() drop every silence and re-encode the whole file unchanged
|
|
527
|
+
die(f"--margin must be a finite number of seconds, 0 or more (got {args.margin:g})", kind="input")
|
|
506
528
|
if args.filler_pad < 0:
|
|
507
529
|
die("--filler-pad cannot be negative: a negative pad turns each word's span inside out "
|
|
508
530
|
"(end before start) and the span is then silently dropped, so nothing is removed",
|
|
@@ -525,8 +547,10 @@ def main() -> int:
|
|
|
525
547
|
filler_info, filler_ranges = resolve_filler(args, meta) if args.filler else (None, [])
|
|
526
548
|
# One sorted, merged removal list through the graph the tool already has: filler removal IS
|
|
527
549
|
# time-range removal, so it reuses keep_ranges() and the same select chain.
|
|
528
|
-
|
|
529
|
-
|
|
550
|
+
# --margin is kept around the silences only (inside_margin), not around filler spans, whose
|
|
551
|
+
# own trim is --filler-pad (#414); the merged list then goes through with no further margin.
|
|
552
|
+
removals = merge_spans(inside_margin(silences, duration, args.margin) + list(filler_ranges))
|
|
553
|
+
keeps = keep_ranges(removals, duration, 0.0, args.min_keep)
|
|
530
554
|
# what the render holds (on a picture, each bound on its nearest frame): the report, the EDL
|
|
531
555
|
# and the time map all describe these, so a re-cut or a sum of `keep` is the rendered edit
|
|
532
556
|
video_out = bool(meta.get("video")) and not (media_out and is_audio_output(media_out))
|
|
@@ -540,7 +564,7 @@ def main() -> int:
|
|
|
540
564
|
# the silences alone; everything removed is `removed_seconds_total`.
|
|
541
565
|
silence_only = removed
|
|
542
566
|
if args.filler:
|
|
543
|
-
silence_keeps = keep_ranges(merge_spans(
|
|
567
|
+
silence_keeps = keep_ranges(merge_spans(inside_margin(silences, duration, args.margin)), duration, 0.0, args.min_keep)
|
|
544
568
|
silence_only = max(0.0, duration - sum(e - s for s, e in rendered_ranges(silence_keeps, meta, duration, video_out)))
|
|
545
569
|
kinds = silence_kinds(args.input, silences, duration, args.threshold)
|
|
546
570
|
summary = {
|
|
@@ -649,8 +673,9 @@ def main() -> int:
|
|
|
649
673
|
return 0
|
|
650
674
|
if not keeps or not held:
|
|
651
675
|
die("nothing would be kept; raise --threshold (e.g. -45) or check the audio")
|
|
652
|
-
if
|
|
653
|
-
|
|
676
|
+
if removed < 0.05:
|
|
677
|
+
# what the render removes, filler spans included: --filler alone has no silences at all
|
|
678
|
+
info(f"no removable {'filler' if filler_only else 'silence or filler' if args.filler else 'silence'} found; output would equal the input")
|
|
654
679
|
|
|
655
680
|
output = args.output or default_output(args.input, "tight")
|
|
656
681
|
# the join of more than 32 ranges concats a list of chunks, so its last command never names the
|
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)
|