ffmpeg-skill 3.1.5 → 3.1.6
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 +4 -3
- package/package.json +1 -1
- package/references/ci-platform-pitfalls.md +3 -2
- package/references/gotchas.md +5 -1
- package/references/scripts.md +5 -4
- package/scripts/_common/__init__.py +7 -5
- package/scripts/_common/color.py +40 -1
- package/scripts/_common/decision.py +177 -10
- package/scripts/_common/emit.py +2 -0
- package/scripts/_common/probe.py +95 -2
- package/scripts/_common/runner.py +4 -1
- package/scripts/_common/timeline.py +5 -4
- package/scripts/_contract.py +2 -2
- package/scripts/broll.py +35 -14
- package/scripts/caption.py +10 -8
- package/scripts/color.py +37 -8
- package/scripts/crop.py +8 -3
- package/scripts/cropdetect.py +13 -9
- package/scripts/cut.py +4 -4
- package/scripts/export.py +11 -2
- package/scripts/fit.py +8 -4
- package/scripts/freeze.py +38 -10
- package/scripts/graphics.py +104 -67
- package/scripts/grid.py +49 -7
- package/scripts/insert.py +9 -1
- package/scripts/join.py +29 -19
- package/scripts/look.py +41 -12
- package/scripts/loop.py +45 -6
- package/scripts/multicam.py +16 -7
- package/scripts/overlay.py +68 -14
- package/scripts/proxy.py +8 -4
- package/scripts/redact.py +10 -5
- package/scripts/reverse.py +27 -2
- package/scripts/scenes.py +16 -5
- package/scripts/sequence.py +6 -2
- package/scripts/silence.py +61 -30
- package/scripts/speedramp.py +17 -5
- package/scripts/straighten.py +17 -5
- package/scripts/sync.py +8 -2
- package/scripts/waveform.py +8 -3
package/scripts/silence.py
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
Detects quiet stretches with ffmpeg's silencedetect, keeps a margin on each
|
|
5
5
|
side so words are not clipped, drops gaps shorter than --min-silence, and
|
|
6
|
-
writes a frame-accurate re-encode
|
|
6
|
+
writes a frame-accurate re-encode: the kept ranges are re-cut from the source the way
|
|
7
|
+
cut.py --segments --accurate cuts them (both streams on one set of frame-grid bounds).
|
|
7
8
|
|
|
8
9
|
Examples:
|
|
9
10
|
python3 silence.py talk.mp4 # -35 dB, gaps >= 0.6 s, 0.2 s margin
|
|
@@ -13,6 +14,7 @@ Examples:
|
|
|
13
14
|
"""
|
|
14
15
|
import argparse
|
|
15
16
|
import json
|
|
17
|
+
import math
|
|
16
18
|
import os
|
|
17
19
|
import sys
|
|
18
20
|
from typing import Any, Dict, List, Optional, Tuple
|
|
@@ -25,7 +27,8 @@ from _common import asr as _asr
|
|
|
25
27
|
from _common.emit import warn
|
|
26
28
|
from _common import guard_sidecar, refuse_existing_outputs, refuse_output_is_input, _check_output_path
|
|
27
29
|
from _common.emit import refuse
|
|
28
|
-
from _common import detect_silences as detect, STATE,
|
|
30
|
+
from _common import detect_silences as detect, STATE, add_common, apply_common, default_output, die, emit, info, is_audio_output, print_json, probe, X264_PRESETS, measured_level_dbfs, fmt_secs
|
|
31
|
+
from _common.decision import frame_rate, stream_starts
|
|
29
32
|
|
|
30
33
|
|
|
31
34
|
|
|
@@ -97,16 +100,38 @@ def keep_ranges(silences: List[Tuple[float, float]], duration: float, margin: fl
|
|
|
97
100
|
return keeps
|
|
98
101
|
|
|
99
102
|
|
|
103
|
+
def rendered_ranges(keeps: List[Tuple[float, float]], meta: Dict[str, Any], duration: float,
|
|
104
|
+
video_out: bool) -> List[Tuple[float, float]]:
|
|
105
|
+
"""The source ranges the render holds, and is given: with a picture, each bound moves to the
|
|
106
|
+
nearest frame time, for the sound too, so cut.py's re-cut (which cuts at the first frame at or
|
|
107
|
+
after a bound) cuts exactly there. A measured bound sits a few microseconds off a frame
|
|
108
|
+
(silencedetect's 4.00002), which "at or after" would have turned into a whole frame dropped.
|
|
109
|
+
Sound alone is cut where asked. --timemap is built from these, so its times are the output's."""
|
|
110
|
+
rate = frame_rate(meta) if video_out else 0.0
|
|
111
|
+
if not rate:
|
|
112
|
+
return list(keeps)
|
|
113
|
+
v0 = stream_starts(meta)[0]
|
|
114
|
+
|
|
115
|
+
def snap(t: float) -> float:
|
|
116
|
+
return min(v0 + round((t - v0) * rate) / rate, duration)
|
|
117
|
+
return [(a, b) for a, b in ((snap(s), snap(e)) for s, e in keeps) if b > a]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _edl_time(t: float, on_frames: bool) -> str:
|
|
121
|
+
"""An EDL bound: a frame time truncated to the millisecond (below 1000 fps the frame at or
|
|
122
|
+
after it is still that frame), any other time rounded as before."""
|
|
123
|
+
return f"{math.floor(t * 1000 + 1e-6) / 1000:.3f}" if on_frames else f"{t:.3f}"
|
|
124
|
+
|
|
125
|
+
|
|
100
126
|
def time_map(keeps: List[Tuple[float, float]], duration: float) -> Dict[str, Any]:
|
|
101
127
|
"""Each kept range with its time in the source and in the output, and each removed range with
|
|
102
|
-
the output time it was cut at: re-timing subtitles, or finding a removed sentence again.
|
|
128
|
+
the output time it was cut at: re-timing subtitles, or finding a removed sentence again.
|
|
129
|
+
`keeps` are the ranges the render holds (rendered_ranges): the output advances by each one's
|
|
130
|
+
exact length, rounded only for display."""
|
|
103
131
|
kept: List[Dict[str, float]] = []
|
|
104
132
|
removed: List[Dict[str, float]] = []
|
|
105
133
|
out, prev = 0.0, 0.0
|
|
106
134
|
for s, e in keeps:
|
|
107
|
-
# the render cuts on bounds rounded to the millisecond (between(t,s,e) below): advance by
|
|
108
|
-
# that rounded length, or every off-grid range adds its own error to every later time
|
|
109
|
-
s, e = round(s, 3), round(e, 3)
|
|
110
135
|
if s - prev > 0.0005:
|
|
111
136
|
removed.append({"source_start": round(prev, 3), "source_end": round(s, 3), "output_time": round(out, 3)})
|
|
112
137
|
kept.append({"source_start": round(s, 3), "source_end": round(e, 3),
|
|
@@ -502,7 +527,11 @@ def main() -> int:
|
|
|
502
527
|
# time-range removal, so it reuses keep_ranges() and the same select chain.
|
|
503
528
|
removals = merge_spans(list(silences) + list(filler_ranges))
|
|
504
529
|
keeps = keep_ranges(removals, duration, args.margin, args.min_keep)
|
|
505
|
-
|
|
530
|
+
# what the render holds (on a picture, each bound on its nearest frame): the report, the EDL
|
|
531
|
+
# and the time map all describe these, so a re-cut or a sum of `keep` is the rendered edit
|
|
532
|
+
video_out = bool(meta.get("video")) and not (media_out and is_audio_output(media_out))
|
|
533
|
+
held = rendered_ranges(keeps, meta, duration, video_out)
|
|
534
|
+
kept = sum(e - s for s, e in held)
|
|
506
535
|
removed = max(0.0, duration - kept)
|
|
507
536
|
# `removed_seconds` keeps the meaning it has had since this tool existed: the seconds of
|
|
508
537
|
# SILENCE this run removes. It must not quietly start counting filler time as well, because a
|
|
@@ -512,11 +541,11 @@ def main() -> int:
|
|
|
512
541
|
silence_only = removed
|
|
513
542
|
if args.filler:
|
|
514
543
|
silence_keeps = keep_ranges(merge_spans(list(silences)), duration, args.margin, args.min_keep)
|
|
515
|
-
silence_only = max(0.0, duration - sum(e - s for s, e in silence_keeps))
|
|
544
|
+
silence_only = max(0.0, duration - sum(e - s for s, e in rendered_ranges(silence_keeps, meta, duration, video_out)))
|
|
516
545
|
kinds = silence_kinds(args.input, silences, duration, args.threshold)
|
|
517
546
|
summary = {
|
|
518
547
|
"silences": [[round(s, 3), None if e == float("inf") else round(e, 3)] for s, e in silences],
|
|
519
|
-
"keep": [[round(s, 3), round(e, 3)] for s, e in
|
|
548
|
+
"keep": [[round(s, 3), round(e, 3)] for s, e in held],
|
|
520
549
|
"input_duration": round(duration, 3),
|
|
521
550
|
"kept_duration": round(kept, 3),
|
|
522
551
|
"removed_seconds": round(silence_only, 3),
|
|
@@ -572,7 +601,7 @@ def main() -> int:
|
|
|
572
601
|
summary["hint"] = f"no passage sits below {args.threshold:g} dBFS for {args.min_silence:g}s; try a higher --threshold (e.g. -25) or a shorter --min-silence"
|
|
573
602
|
info("hint: " + summary["hint"])
|
|
574
603
|
|
|
575
|
-
tm = time_map(
|
|
604
|
+
tm = time_map(held, duration)
|
|
576
605
|
points: Any = False # False: not measured (a dry run over a pending input); None: the decode failed
|
|
577
606
|
if not (STATE.dry_run and not os.path.exists(args.input)):
|
|
578
607
|
points = cut_levels(args.input, tm, duration, list(filler_ranges))
|
|
@@ -604,8 +633,11 @@ def main() -> int:
|
|
|
604
633
|
if args.edl:
|
|
605
634
|
if not STATE.dry_run: # the EDL is an artifact like the cut itself: a plan writes nothing
|
|
606
635
|
with open(args.edl, "w", encoding="utf-8") as fh:
|
|
607
|
-
|
|
608
|
-
|
|
636
|
+
on_frames = bool(video_out and frame_rate(meta))
|
|
637
|
+
for s, e in held:
|
|
638
|
+
# a frame-time bound is written down to the millisecond, not rounded up past
|
|
639
|
+
# its frame: cut.py --segments cuts at the first frame at or after it (#416 review)
|
|
640
|
+
fh.write(f"{_edl_time(s, on_frames)}-{_edl_time(e, on_frames)}\n")
|
|
609
641
|
info(f"wrote {args.edl}")
|
|
610
642
|
|
|
611
643
|
if args.list or args.filler_list:
|
|
@@ -615,29 +647,28 @@ def main() -> int:
|
|
|
615
647
|
else:
|
|
616
648
|
print_json(summary)
|
|
617
649
|
return 0
|
|
618
|
-
if not keeps:
|
|
650
|
+
if not keeps or not held:
|
|
619
651
|
die("nothing would be kept; raise --threshold (e.g. -45) or check the audio")
|
|
620
652
|
if not silences or removed < 0.05:
|
|
621
653
|
info("no removable silence found; output would equal the input")
|
|
622
654
|
|
|
623
655
|
output = args.output or default_output(args.input, "tight")
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
#
|
|
628
|
-
#
|
|
629
|
-
#
|
|
630
|
-
#
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
run(cmd)
|
|
656
|
+
# the join of more than 32 ranges concats a list of chunks, so its last command never names the
|
|
657
|
+
# source and run()'s own check cannot see -o replace it (#416 review)
|
|
658
|
+
refuse_output_is_input(output, args.input)
|
|
659
|
+
# The kept ranges are re-cut from the source as cut.py --segments --accurate cuts them (#409):
|
|
660
|
+
# each range is its own seeked input, both streams cut on the same frame-grid bounds and shifted
|
|
661
|
+
# by one constant, the sound padded to the range's origin, at most 32 ranges per ffmpeg call.
|
|
662
|
+
# The one-pass select/ametadata graph this replaced rounded every range twice, the picture to
|
|
663
|
+
# whole frames and the sound to whole AAC frames, and the errors added up (150 ms over 21 ranges
|
|
664
|
+
# on a 61 s source); its asetpts=N/SR also moved a sound that starts late by its whole offset.
|
|
665
|
+
# No declick fade: the joins sit in the --margin the tool keeps, as they always have.
|
|
666
|
+
# imported here, not at the top: the contract and MCP load every tool's parser on its own
|
|
667
|
+
import cut as _cut
|
|
668
|
+
_cut.DECLICK_S = 0.0
|
|
669
|
+
import tempfile
|
|
670
|
+
with tempfile.TemporaryDirectory(prefix="ffskill_silence_") as tmp:
|
|
671
|
+
_cut.join_from_source(args.input, held, output, meta, args.crf, args.preset, tmp)
|
|
641
672
|
r = probe(output, role="output")
|
|
642
673
|
info(f"wrote {output} ({fmt_secs(r['duration'])}, expected ~{kept:.3f}s)")
|
|
643
674
|
finish_time_map()
|
package/scripts/speedramp.py
CHANGED
|
@@ -19,6 +19,7 @@ import argparse
|
|
|
19
19
|
import sys
|
|
20
20
|
from typing import List, Tuple
|
|
21
21
|
|
|
22
|
+
from _common.decision import av_trim, frame_rate, grid_at_or_after, stream_starts
|
|
22
23
|
from _common import add_common, apply_common, aac_args, default_output, die, emit, ffmpeg_base, info, probe, run, video_args, X264_PRESETS, fmt_secs, time_arg
|
|
23
24
|
|
|
24
25
|
MAX_SPEED = 20.0
|
|
@@ -89,15 +90,26 @@ def main() -> int:
|
|
|
89
90
|
# the last factor. The end bound is omitted only when the stated end is the file's end
|
|
90
91
|
# (within 50 ms): a trim a few milliseconds short of the last packet drops that packet.
|
|
91
92
|
last_runs_to_eof = abs(segments[-1][1] - dur) <= 0.05
|
|
93
|
+
# Every boundary is a frame time and both streams of a segment are cut on it and shifted by the
|
|
94
|
+
# same constant before the speed change (#409), as cut.py's re-cut does. Rebasing each stream
|
|
95
|
+
# of each segment to its own first sample moved a sound that starts 0.3 s late 0.3 s early, and
|
|
96
|
+
# the factor scaled that (+570 ms at 0.5x). The first segment starts at the first frame.
|
|
97
|
+
v0 = stream_starts(meta)[0]
|
|
98
|
+
rate = frame_rate(meta)
|
|
99
|
+
bounds = [v0] + [grid_at_or_after(end, v0, rate) for _, end, _ in segments[:-1]]
|
|
92
100
|
for i, (start, end, factor) in enumerate(segments):
|
|
93
101
|
vlabel, alabel = f"v{i}", f"a{i}"
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
102
|
+
to_eof = i == len(segments) - 1 and last_runs_to_eof
|
|
103
|
+
seg_end = None if to_eof else (bounds[i + 1] if i + 1 < len(bounds) else grid_at_or_after(end, v0, rate))
|
|
104
|
+
if seg_end is not None and seg_end <= bounds[i] + 1e-6:
|
|
105
|
+
# both ends on the same frame: the trims would be empty and the factor reach no frame
|
|
106
|
+
die(f"--segment {start:g}-{end:g}:{factor:g} holds no frame: both ends fall on the frame at "
|
|
107
|
+
f"{bounds[i]:.3f}s; widen it or merge it into a neighbour")
|
|
108
|
+
vtrim, atrim = av_trim(bounds[i], seg_end, rate)
|
|
109
|
+
vparts.append(f"[0:v]{vtrim},setpts=PTS/{factor:.6f}[{vlabel}]")
|
|
97
110
|
labels.append(f"[{vlabel}]")
|
|
98
111
|
if has_audio:
|
|
99
|
-
|
|
100
|
-
aparts.append(f"[0:a]{atrim},asetpts=PTS-STARTPTS,{atempo_chain(factor)}[{alabel}]")
|
|
112
|
+
aparts.append(f"[0:a]{atrim},{atempo_chain(factor)}[{alabel}]")
|
|
101
113
|
|
|
102
114
|
if has_audio:
|
|
103
115
|
concat_inputs = "".join(f"[v{i}][a{i}]" for i in range(len(segments)))
|
package/scripts/straighten.py
CHANGED
|
@@ -23,6 +23,8 @@ import math
|
|
|
23
23
|
import sys
|
|
24
24
|
|
|
25
25
|
from _common import add_common, apply_common, aac_args, cfr_args, default_output, die, drawn_chain, emit, ffmpeg_base, info, probe, run_keeping_subtitles, validate_color, video_args, X264_PRESETS, fmt_secs
|
|
26
|
+
from _common.emit import note
|
|
27
|
+
from _common.probe import display_size, square_pixels, square_pixels_note
|
|
26
28
|
|
|
27
29
|
|
|
28
30
|
def main() -> int:
|
|
@@ -68,15 +70,25 @@ def main() -> int:
|
|
|
68
70
|
rotate = drawn_chain(f"rotate={radians:.8f}:fillcolor={args.fill_color}:{geometry}", meta, [args.fill_color],
|
|
69
71
|
f"rotate={radians:.8f}:fillcolor=black:{geometry}")
|
|
70
72
|
else:
|
|
71
|
-
# Pre-scale the frame up
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
s =
|
|
73
|
+
# Pre-scale the frame up so the rotated content fully covers the original W:H window with
|
|
74
|
+
# no black corner, then rotate in place (canvas stays at the scaled size) and crop back
|
|
75
|
+
# down to the original W:H, centred. The window turned back by the angle must fit inside
|
|
76
|
+
# the scaled frame: W*cos + H*sin <= s*W and W*sin + H*cos <= s*H, so
|
|
77
|
+
# s = |cos| + max(W/H, H/W) * |sin| for the displayed (squared, upright) frame. Through
|
|
78
|
+
# 3.1.5 it was |cos| + |sin|, a square's factor, which left two corners black on 16:9
|
|
79
|
+
# (#416 review).
|
|
80
|
+
dw, dh = display_size(meta["video"])
|
|
81
|
+
aspect = max(dw / dh, dh / dw) if dw and dh else 1.0
|
|
82
|
+
s = abs(math.cos(radians)) + aspect * abs(math.sin(radians))
|
|
76
83
|
# crop dimensions must round down to even (4:2:0 chroma); trunc(.../2)*2 floors to the
|
|
77
84
|
# nearest even value instead of leaving an odd width/height that the encoder would refuse.
|
|
78
85
|
rotate = f"scale=iw*{s:.8f}:ih*{s:.8f},rotate={radians:.8f},crop=trunc(iw/{s:.8f}/2)*2:trunc(ih/{s:.8f}/2)*2"
|
|
79
86
|
|
|
87
|
+
# a rotation of non-square pixels is a shear on screen: the picture is squared at its
|
|
88
|
+
# display aspect first (#411)
|
|
89
|
+
if square_pixels(meta["video"]):
|
|
90
|
+
rotate = square_pixels(meta["video"]) + "," + rotate
|
|
91
|
+
note(square_pixels_note(meta["video"]))
|
|
80
92
|
cmd = ffmpeg_base() + ["-i", args.input, "-vf", rotate, "-map", "0:v:0"]
|
|
81
93
|
if has_audio:
|
|
82
94
|
cmd += ["-map", f"0:a:{args.audio_stream}?"]
|
package/scripts/sync.py
CHANGED
|
@@ -34,6 +34,7 @@ from typing import Dict, List
|
|
|
34
34
|
|
|
35
35
|
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
36
|
from _common.emit import record_skip, warn
|
|
37
|
+
from _common.decision import stream_starts
|
|
37
38
|
|
|
38
39
|
SR = 8000 # decode sample rate
|
|
39
40
|
|
|
@@ -355,6 +356,11 @@ def main() -> int:
|
|
|
355
356
|
if abs(drift_ratio - 1.0) > 1e-7:
|
|
356
357
|
# the second file runs long by drift_ratio -> play it faster by that ratio (pitch shift is ~ppm, inaudible)
|
|
357
358
|
drift_af = [f"asetrate={sec_sr * drift_ratio:.6f}", f"aresample={sec_sr}"]
|
|
359
|
+
# the offset is measured between the files' clocks (decode_pcm_mono pads a late sound), so
|
|
360
|
+
# the second's sound is placed on its own clock first: one that starts 0.3 s after its
|
|
361
|
+
# file's start keeps that 0.3 s instead of being delayed from its first sample (#409).
|
|
362
|
+
# Only then, so the command for an ordinary file is the one it always was.
|
|
363
|
+
on_clock = ["aresample=async=1:first_pts=0"] if stream_starts(second_meta)[1] > 0.0005 else []
|
|
358
364
|
|
|
359
365
|
if args.replace_audio:
|
|
360
366
|
cmd = ffmpeg_base() + ["-i", args.reference]
|
|
@@ -367,7 +373,7 @@ def main() -> int:
|
|
|
367
373
|
# its length instead of -shortest, which cut the reference's tail whenever the second
|
|
368
374
|
# file started earlier (head_trim > 0) or was simply shorter
|
|
369
375
|
ref_dur = probe(args.reference).get("duration")
|
|
370
|
-
af_parts = drift_af + ([f"adelay={delay_ms}:all=1"] if delay_ms > 0 else []) + ["apad"]
|
|
376
|
+
af_parts = on_clock + drift_af + ([f"adelay={delay_ms}:all=1"] if delay_ms > 0 else []) + ["apad"]
|
|
371
377
|
cmd += ["-af", ",".join(af_parts)]
|
|
372
378
|
cmd += aac_args() + (["-t", f"{ref_dur:.3f}"] if ref_dur else ["-shortest"]) + [output]
|
|
373
379
|
proc = run(cmd, check=False)
|
|
@@ -391,7 +397,7 @@ def main() -> int:
|
|
|
391
397
|
if head_trim > 0:
|
|
392
398
|
cmd += ["-ss", f"{head_trim:.4f}"]
|
|
393
399
|
cmd += ["-i", args.second]
|
|
394
|
-
af_parts =
|
|
400
|
+
af_parts = on_clock + drift_af
|
|
395
401
|
if delay_ms > 0:
|
|
396
402
|
af_parts.append(f"adelay={delay_ms}:all=1")
|
|
397
403
|
if has_video:
|
package/scripts/waveform.py
CHANGED
|
@@ -35,12 +35,12 @@ import os
|
|
|
35
35
|
import sys
|
|
36
36
|
|
|
37
37
|
from _platforms import PLATFORMS, PLATFORM_CHOICES, resolve as resolve_platform
|
|
38
|
-
from _common.probe import SILENT_PEAK_DBFS
|
|
38
|
+
from _common.probe import SILENT_PEAK_DBFS, square_pixels, square_pixels_note
|
|
39
39
|
from _common import still_input_args # noqa: E402
|
|
40
40
|
from _common import (add_common, apply_common, aac_args, default_output, die, draws_chroma, emit, ffmpeg_base,
|
|
41
41
|
info, load_brand, pad_filters, probe, rgb_chain_to_bt709, run, STATE, validate_color, video_args, X264_PRESETS,
|
|
42
42
|
fmt_secs, child_args, run_tool, measured_level_dbfs, sibling_temp)
|
|
43
|
-
from _common.emit import absorb_stage_hw, warn
|
|
43
|
+
from _common.emit import absorb_stage_hw, note, warn
|
|
44
44
|
|
|
45
45
|
WAVEFORM_MODES = ["point", "line", "p2p", "cline"]
|
|
46
46
|
|
|
@@ -184,7 +184,12 @@ def main() -> int:
|
|
|
184
184
|
plate = pad_filters(args.width, args.height,
|
|
185
185
|
"blur" if args.image_fit == "blur" else "color",
|
|
186
186
|
args.background, 20)
|
|
187
|
-
|
|
187
|
+
# a non-square-pixel still is squared at its display aspect before it is fitted (#411)
|
|
188
|
+
plate_v = probe(args.image, role="input").get("video") if not STATE.dry_run else None
|
|
189
|
+
plate_sq = square_pixels(plate_v)
|
|
190
|
+
if plate_sq:
|
|
191
|
+
note(square_pixels_note(plate_v, "--image"))
|
|
192
|
+
graph = f"[1:v]{plate_sq + ',' if plate_sq else ''}{plate},format=rgba[plate];[0:a:{args.audio_stream}]{vf},format=rgba"
|
|
188
193
|
if args.opacity < 1.0:
|
|
189
194
|
graph += f",colorchannelmixer=aa={args.opacity:g}"
|
|
190
195
|
# the composite is RGBA (the plate is): converted to the BT.709 the output is tagged with,
|