new arguments
This commit is contained in:
+9
-3
@@ -1,4 +1,6 @@
|
|||||||
usage: video-randomizer.py [-h] [-o OUTPUT] [-d DURATION] [-s SAMPLE] [-p HEIGHT] [-w WIDTH] [-f FRAMERATE] [-i IGNORE] [--dry] [-q] [-qf] [--crf CRF] [-r SEED] [--ffmpeg FFMPEG] file [file ...]
|
usage: video-randomizer.py [-h] [-o OUTPUT] [-d DURATION] [-s SAMPLE] [-p HEIGHT] [-w WIDTH] [-f FRAMERATE] [-i IGNORE] [--dry] [-q] [-qf] [--crf CRF] [-r SEED]
|
||||||
|
[--ffmpeg FFMPEG] [-nc] [-na] [-ab AUDIO_BITRATE]
|
||||||
|
file [file ...]
|
||||||
|
|
||||||
randomize videos by taking small random samples and merging them together
|
randomize videos by taking small random samples and merging them together
|
||||||
|
|
||||||
@@ -14,11 +16,11 @@ optional arguments:
|
|||||||
-s SAMPLE, --sample SAMPLE
|
-s SAMPLE, --sample SAMPLE
|
||||||
floating samples duration in seconds (default: 1s)
|
floating samples duration in seconds (default: 1s)
|
||||||
-p HEIGHT, --height HEIGHT
|
-p HEIGHT, --height HEIGHT
|
||||||
output video height (default: 1080p)
|
output video height (default: 1080p if multiple videos)
|
||||||
-w WIDTH, --width WIDTH
|
-w WIDTH, --width WIDTH
|
||||||
output video height (default: auto for 16:9)
|
output video height (default: auto for 16:9)
|
||||||
-f FRAMERATE, --framerate FRAMERATE
|
-f FRAMERATE, --framerate FRAMERATE
|
||||||
output video framerate (default: 30fps)
|
output video framerate (default: 30fps if multiple videos)
|
||||||
-i IGNORE, --ignore IGNORE
|
-i IGNORE, --ignore IGNORE
|
||||||
video input content start/end ignore in % (default: 10)
|
video input content start/end ignore in % (default: 10)
|
||||||
--dry dry mode, do not output video
|
--dry dry mode, do not output video
|
||||||
@@ -27,3 +29,7 @@ optional arguments:
|
|||||||
--crf CRF libx264 Constant Rate Factor (default: 23)
|
--crf CRF libx264 Constant Rate Factor (default: 23)
|
||||||
-r SEED, --seed SEED random seed
|
-r SEED, --seed SEED random seed
|
||||||
--ffmpeg FFMPEG ffmpeg binary path (default is found on PATH)
|
--ffmpeg FFMPEG ffmpeg binary path (default is found on PATH)
|
||||||
|
-nc, --no-convert don't convert videos (default for one video, might fail on multiple)
|
||||||
|
-na, --no-audio only keep video track
|
||||||
|
-ab AUDIO_BITRATE, --audio-bitrate AUDIO_BITRATE
|
||||||
|
audio bitrate in Kbps (default: 128)
|
||||||
+349
-296
@@ -1,296 +1,349 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
|
||||||
import typing
|
import typing
|
||||||
import cv2
|
import cv2
|
||||||
import os
|
import os
|
||||||
import math
|
import math
|
||||||
import argparse
|
import argparse
|
||||||
import tempfile
|
import tempfile
|
||||||
import hashlib
|
import hashlib
|
||||||
import random
|
import random
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
import sys
|
import sys
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
CWD = os.path.abspath(os.path.dirname(__file__))
|
CWD = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
def parse_args() -> argparse.Namespace:
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="randomize videos by taking small random samples and merging them together",
|
description="randomize videos by taking small random samples and merging them together",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-o",
|
"-o",
|
||||||
"--output",
|
"--output",
|
||||||
type=str,
|
type=str,
|
||||||
default=None,
|
default=None,
|
||||||
help="output video path (default: random_[time].mp4)",
|
help="output video path (default: random_[time].mp4)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-d",
|
"-d",
|
||||||
"--duration",
|
"--duration",
|
||||||
type=float,
|
type=float,
|
||||||
default=60,
|
default=60,
|
||||||
help="floating duration of output video in seconds (default: 60s)",
|
help="floating duration of output video in seconds (default: 60s)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-s",
|
"-s",
|
||||||
"--sample",
|
"--sample",
|
||||||
type=float,
|
type=float,
|
||||||
default=1,
|
default=1,
|
||||||
help="floating samples duration in seconds (default: 1s)",
|
help="floating samples duration in seconds (default: 1s)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-p",
|
"-p",
|
||||||
"--height",
|
"--height",
|
||||||
type=int,
|
type=int,
|
||||||
default=None,
|
default=None,
|
||||||
help="output video height (default: 1080p)",
|
help="output video height (default: 1080p if multiple videos)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-w",
|
"-w",
|
||||||
"--width",
|
"--width",
|
||||||
type=int,
|
type=int,
|
||||||
default=None,
|
default=None,
|
||||||
help="output video height (default: auto for 16:9)",
|
help="output video height (default: auto for 16:9)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-f",
|
"-f",
|
||||||
"--framerate",
|
"--framerate",
|
||||||
type=int,
|
type=float,
|
||||||
default=30,
|
default=None,
|
||||||
help="output video framerate (default: 30fps)",
|
help="output video framerate (default: 30fps if multiple videos)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-i",
|
"-i",
|
||||||
"--ignore",
|
"--ignore",
|
||||||
type=float,
|
type=float,
|
||||||
default=10,
|
default=10,
|
||||||
help="video input content start/end ignore in %% (default: 10)",
|
help="video input content start/end ignore in %% (default: 10)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--dry",
|
"--dry",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
default=False,
|
default=False,
|
||||||
help="dry mode, do not output video",
|
help="dry mode, do not output video",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-q",
|
"-q",
|
||||||
"--quiet",
|
"--quiet",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
default=False,
|
default=False,
|
||||||
help="silent mode",
|
help="silent mode",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-qf",
|
"-qf",
|
||||||
"--quiet-ffmpeg",
|
"--quiet-ffmpeg",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
default=False,
|
default=False,
|
||||||
help="do not output ffmpeg stdout",
|
help="do not output ffmpeg stdout",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--crf",
|
"--crf",
|
||||||
type=int,
|
type=int,
|
||||||
default=23,
|
default=23,
|
||||||
help="libx264 Constant Rate Factor (default: 23)",
|
help="libx264 Constant Rate Factor (default: 23)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-r",
|
"-r",
|
||||||
"--seed",
|
"--seed",
|
||||||
type=int,
|
type=int,
|
||||||
default=random.randrange(sys.maxsize),
|
default=random.randrange(sys.maxsize),
|
||||||
help="random seed",
|
help="random seed",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--ffmpeg",
|
"--ffmpeg",
|
||||||
type=str,
|
type=str,
|
||||||
default=None,
|
default=None,
|
||||||
help="ffmpeg binary path (default is found on PATH)",
|
help="ffmpeg binary path (default is found on PATH)",
|
||||||
)
|
)
|
||||||
parser.add_argument("file", type=str, nargs="+", help="input files")
|
parser.add_argument(
|
||||||
return parser.parse_args()
|
"-nc",
|
||||||
|
"--no-convert",
|
||||||
|
action="store_true",
|
||||||
# === UTILS ===
|
default=False,
|
||||||
|
help="don't convert videos (default for one video, might fail on multiple)",
|
||||||
|
)
|
||||||
def get_file_hash(path: str) -> str:
|
parser.add_argument(
|
||||||
with open(path, mode="rb") as f:
|
"-na",
|
||||||
return hashlib.md5(f.read(8192)).hexdigest()
|
"--no-audio",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
def get_video_frame_count(path: str) -> int:
|
help="only keep video track",
|
||||||
return cv2.VideoCapture(path).get(cv2.CAP_PROP_FRAME_COUNT)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-ab",
|
||||||
def get_timestamp(frame_number: int, framerate: float) -> str:
|
"--audio-bitrate",
|
||||||
t = frame_number / framerate
|
type=int,
|
||||||
return f"{t//60:.0f}:{t%60:.3f}"
|
default=128,
|
||||||
|
help="audio bitrate in Kbps (default: 128)",
|
||||||
|
)
|
||||||
def execute(cmd: typing.List[str], silent: bool = False) -> int:
|
parser.add_argument("file", type=str, nargs="+", help="input files")
|
||||||
out = subprocess.DEVNULL if silent else None
|
return parser.parse_args()
|
||||||
popen = subprocess.Popen(cmd, stdout=out, stderr=out, universal_newlines=True)
|
|
||||||
return popen.wait()
|
|
||||||
|
# === UTILS ===
|
||||||
|
|
||||||
def get_ffmpeg_bin(args: argparse.Namespace) -> str:
|
|
||||||
if args.ffmpeg and os.path.exists(args.ffmpeg):
|
def get_file_hash(path: str) -> str:
|
||||||
return args.ffmpeg
|
with open(path, mode="rb") as f:
|
||||||
path = shutil.which("ffmpeg")
|
return hashlib.md5(f.read(8192)).hexdigest()
|
||||||
if not path:
|
|
||||||
print("ffmpeg not found on PATH")
|
|
||||||
sys.exit(1)
|
def get_video_frame_count(path: str) -> int:
|
||||||
return path
|
return cv2.VideoCapture(path).get(cv2.CAP_PROP_FRAME_COUNT)
|
||||||
|
|
||||||
|
|
||||||
def ffmpeg(parameters: typing.List[str], args: argparse.Namespace) -> bool:
|
def get_framerate(path: str, args: argparse.Namespace) -> float:
|
||||||
ffmpeg_bin = get_ffmpeg_bin(args)
|
if not args.no_convert:
|
||||||
cmd = [ffmpeg_bin] + parameters
|
return args.framerate
|
||||||
if not args.quiet:
|
return cv2.VideoCapture(path).get(cv2.CAP_PROP_FPS)
|
||||||
print(f"$ {' '.join(cmd)}")
|
|
||||||
return execute(cmd, args.quiet or args.quiet_ffmpeg) == 0
|
|
||||||
|
def get_timestamp(frame_number: int, framerate: float) -> str:
|
||||||
|
t = frame_number / framerate
|
||||||
def get_scale(args: argparse.Namespace) -> str:
|
return f"{t//60:.0f}:{t%60:.3f}"
|
||||||
if args.width is None and args.height is None:
|
|
||||||
return "1920:1080"
|
|
||||||
elif args.height is None:
|
def execute(cmd: typing.List[str], silent: bool = False) -> int:
|
||||||
return f"{args.width}:{round(args.width * 9 / 16)}"
|
out = subprocess.DEVNULL if silent else None
|
||||||
elif args.width is None:
|
popen = subprocess.Popen(cmd, stdout=out, stderr=out, universal_newlines=True)
|
||||||
return f"{round(args.height * 16 / 9)}:{args.height}"
|
return popen.wait()
|
||||||
else:
|
|
||||||
return f"{args.width}:{args.height}"
|
|
||||||
|
def get_ffmpeg_bin(args: argparse.Namespace) -> str:
|
||||||
|
if args.ffmpeg and os.path.exists(args.ffmpeg):
|
||||||
# === MAIN ===
|
return args.ffmpeg
|
||||||
|
path = shutil.which("ffmpeg")
|
||||||
|
if not path:
|
||||||
def get_output_file(args: argparse.Namespace) -> str:
|
print("ffmpeg not found on PATH")
|
||||||
if args.output is not None:
|
sys.exit(1)
|
||||||
return args.output
|
return path
|
||||||
return f"random_{round(time.time())}.mp4"
|
|
||||||
|
|
||||||
|
def ffmpeg(parameters: typing.List[str], args: argparse.Namespace) -> bool:
|
||||||
def get_build_dir(args: argparse.Namespace) -> str:
|
ffmpeg_bin = get_ffmpeg_bin(args)
|
||||||
path = os.path.join(
|
cmd = [ffmpeg_bin] + parameters
|
||||||
os.getcwd(), f"build_{get_scale(args).replace(':','x')}_{args.framerate}fps"
|
if not args.quiet:
|
||||||
)
|
print(f"$ {' '.join(cmd)}")
|
||||||
if not os.path.exists(path):
|
return execute(cmd, args.quiet or args.quiet_ffmpeg) == 0
|
||||||
os.mkdir(path)
|
|
||||||
return path
|
|
||||||
|
def get_scale(args: argparse.Namespace) -> str:
|
||||||
|
if args.width is None and args.height is None:
|
||||||
def convert_video(in_path: str, out_path: str, args: argparse.Namespace) -> bool:
|
return "1920:1080"
|
||||||
parameters = [
|
elif args.height is None:
|
||||||
"-y",
|
return f"{args.width}:{round(args.width * 9 / 16)}"
|
||||||
"-f",
|
elif args.width is None:
|
||||||
"mp4",
|
return f"{round(args.height * 16 / 9)}:{args.height}"
|
||||||
"-i",
|
else:
|
||||||
in_path,
|
return f"{args.width}:{args.height}"
|
||||||
"-c:v",
|
|
||||||
"libx264",
|
|
||||||
"-vf",
|
def no_convert(args: argparse.Namespace) -> bool:
|
||||||
f"scale={get_scale(args)},fps={args.framerate}",
|
if args.no_convert:
|
||||||
"-crf",
|
return True
|
||||||
str(args.crf),
|
if args.no_audio or args.width is not None or args.height is not None or args.framerate is not None:
|
||||||
"-video_track_timescale",
|
return False
|
||||||
"90000",
|
return len(args.file) == 1
|
||||||
"-an",
|
|
||||||
out_path,
|
|
||||||
]
|
# === MAIN ===
|
||||||
return ffmpeg(parameters, args)
|
|
||||||
|
|
||||||
|
def fix_arguments(args: argparse.Namespace) -> argparse.Namespace:
|
||||||
def convert_all_videos(build_dir: str, args: argparse.Namespace) -> typing.List[str]:
|
args.no_convert = no_convert(args)
|
||||||
converted = []
|
args.framerate = args.framerate if args.framerate is not None else 30
|
||||||
to_convert = []
|
return args
|
||||||
for path in args.file:
|
|
||||||
if os.path.exists(path):
|
|
||||||
output_path = os.path.join(build_dir, get_file_hash(path) + ".mp4")
|
def get_output_file(args: argparse.Namespace) -> str:
|
||||||
if os.path.exists(output_path):
|
if args.output is not None:
|
||||||
converted += [output_path]
|
return args.output
|
||||||
else:
|
return f"random_{round(time.time())}.mp4"
|
||||||
to_convert += [(path, output_path)]
|
|
||||||
if not args.quiet:
|
|
||||||
print(f"Found {len(converted) + len(to_convert)} videos")
|
def get_build_dir(args: argparse.Namespace) -> str:
|
||||||
print(f"{len(converted)} already converted")
|
path = os.path.join(
|
||||||
if len(to_convert):
|
os.getcwd(), f"build_{get_scale(args).replace(':','x')}_{args.framerate}fps{'_na' if args.no_audio else ''}"
|
||||||
if not args.quiet:
|
)
|
||||||
print(f"Converting {len(to_convert)} videos...")
|
if not os.path.exists(path) and not args.no_convert:
|
||||||
for i, data in enumerate(to_convert):
|
os.mkdir(path)
|
||||||
in_path, out_path = data
|
return path
|
||||||
result = convert_video(in_path, out_path, args)
|
|
||||||
if not args.quiet:
|
|
||||||
print(
|
def convert_video(in_path: str, out_path: str, args: argparse.Namespace) -> bool:
|
||||||
f"[{i + 1} / {len(to_convert)}] {'OK' if result else 'KO'} {in_path} -> {out_path}"
|
parameters = [
|
||||||
)
|
"-y",
|
||||||
if result:
|
"-f",
|
||||||
converted += [out_path]
|
"mp4",
|
||||||
return converted
|
"-i",
|
||||||
|
in_path,
|
||||||
|
"-c:v",
|
||||||
def generate_concat_file(videos: typing.List[str], args: argparse.Namespace) -> str:
|
"libx264",
|
||||||
random.seed(args.seed)
|
"-vf",
|
||||||
if not args.quiet:
|
f"scale={get_scale(args)},fps={args.framerate}",
|
||||||
print(f"Random seed: {args.seed}")
|
"-crf",
|
||||||
with tempfile.NamedTemporaryFile(delete=False) as tmp:
|
str(args.crf),
|
||||||
tmp.write("ffconcat version 1.0\n".encode())
|
"-video_track_timescale",
|
||||||
t = 0
|
"90000",
|
||||||
while t < args.duration:
|
]
|
||||||
file = random.choice(videos)
|
if args.no_audio:
|
||||||
framecount = get_video_frame_count(file)
|
parameters += ["-an"]
|
||||||
if framecount > 0:
|
else:
|
||||||
tmp.write(f"file '{file}'\n".encode())
|
parameters += ["-c:a", "aac", "-b:a", f"{args.audio_bitrate}k"]
|
||||||
inpoint = round(
|
parameters += [out_path]
|
||||||
random.random() * framecount * (1 - args.ignore / 100.0 * 2)
|
return ffmpeg(parameters, args)
|
||||||
)
|
|
||||||
tmp.write(
|
|
||||||
f"inpoint {get_timestamp(inpoint, args.framerate)}\n".encode()
|
def convert_all_videos(build_dir: str, args: argparse.Namespace) -> typing.List[str]:
|
||||||
)
|
converted = []
|
||||||
outpoint = inpoint + round(args.sample * args.framerate)
|
to_convert = []
|
||||||
tmp.write(
|
for path in args.file:
|
||||||
f"outpoint {get_timestamp(outpoint, args.framerate)}\n".encode()
|
if os.path.exists(path):
|
||||||
)
|
if args.no_convert:
|
||||||
t += args.sample
|
converted += [os.path.realpath(path)]
|
||||||
if not args.quiet:
|
else:
|
||||||
print(f"FFMPEG concat file: {tmp.name}")
|
output_path = os.path.join(build_dir, get_file_hash(path) + ".mp4")
|
||||||
return tmp.name
|
if os.path.exists(output_path):
|
||||||
|
converted += [output_path]
|
||||||
|
else:
|
||||||
def make_output_video(
|
to_convert += [(path, output_path)]
|
||||||
concat_file: str, output_file: str, args: argparse.Namespace
|
if not args.quiet:
|
||||||
) -> None:
|
print(f"Found {len(converted) + len(to_convert)} videos")
|
||||||
parameters = [
|
print(f"{len(converted)} already converted")
|
||||||
"-y",
|
if len(to_convert):
|
||||||
"-f",
|
if not args.quiet:
|
||||||
"concat",
|
print(f"Converting {len(to_convert)} videos...")
|
||||||
"-safe",
|
for i, data in enumerate(to_convert):
|
||||||
"0",
|
in_path, out_path = data
|
||||||
"-i",
|
result = convert_video(in_path, out_path, args)
|
||||||
concat_file,
|
if not args.quiet:
|
||||||
"-c:v",
|
print(
|
||||||
"libx264",
|
f"[{i + 1} / {len(to_convert)}] {'OK' if result else 'KO'} {in_path} -> {out_path}"
|
||||||
"-async",
|
)
|
||||||
"1",
|
if result:
|
||||||
"-an",
|
converted += [out_path]
|
||||||
output_file,
|
return converted
|
||||||
]
|
|
||||||
if not ffmpeg(parameters, args):
|
|
||||||
sys.exit(1)
|
def generate_concat_file(videos: typing.List[str], args: argparse.Namespace) -> str:
|
||||||
|
random.seed(args.seed)
|
||||||
|
if not args.quiet:
|
||||||
if __name__ == "__main__":
|
print(f"Random seed: {args.seed}")
|
||||||
args = parse_args()
|
with tempfile.NamedTemporaryFile(delete=False) as tmp:
|
||||||
|
tmp.write("ffconcat version 1.0\n".encode())
|
||||||
output_file = get_output_file(args)
|
t = 0
|
||||||
|
while t < args.duration:
|
||||||
build_dir = get_build_dir(args)
|
file = random.choice(videos)
|
||||||
|
framecount = get_video_frame_count(file)
|
||||||
videos = convert_all_videos(build_dir, args)
|
framerate = get_framerate(file, args)
|
||||||
|
if framecount > 0:
|
||||||
concat_file = generate_concat_file(videos, args)
|
tmp.write(f"file '{file}'\n".encode())
|
||||||
|
inpoint = round(
|
||||||
if not args.dry:
|
random.random() * framecount * (1 - args.ignore / 100.0 * 2)
|
||||||
make_output_video(concat_file, output_file, args)
|
)
|
||||||
|
tmp.write(
|
||||||
|
f"inpoint {get_timestamp(inpoint, framerate)}\n".encode()
|
||||||
|
)
|
||||||
|
outpoint = inpoint + round(args.sample * framerate)
|
||||||
|
tmp.write(
|
||||||
|
f"outpoint {get_timestamp(outpoint, framerate)}\n".encode()
|
||||||
|
)
|
||||||
|
t += args.sample
|
||||||
|
if not args.quiet:
|
||||||
|
print(f"FFMPEG concat file: {tmp.name}")
|
||||||
|
return tmp.name
|
||||||
|
|
||||||
|
|
||||||
|
def make_output_video(
|
||||||
|
concat_file: str, output_file: str, args: argparse.Namespace
|
||||||
|
) -> None:
|
||||||
|
parameters = [
|
||||||
|
"-y",
|
||||||
|
"-f",
|
||||||
|
"concat",
|
||||||
|
"-safe",
|
||||||
|
"0",
|
||||||
|
"-i",
|
||||||
|
concat_file,
|
||||||
|
"-c:v",
|
||||||
|
"libx264",
|
||||||
|
"-async",
|
||||||
|
"1"
|
||||||
|
]
|
||||||
|
if args.no_audio:
|
||||||
|
parameters += ["-an"]
|
||||||
|
else:
|
||||||
|
parameters += ["-c:a", "aac", "-b:a", f"{args.audio_bitrate}k"]
|
||||||
|
parameters += [output_file]
|
||||||
|
if not ffmpeg(parameters, args):
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
args = parse_args()
|
||||||
|
|
||||||
|
args = fix_arguments(args)
|
||||||
|
|
||||||
|
output_file = get_output_file(args)
|
||||||
|
|
||||||
|
build_dir = get_build_dir(args)
|
||||||
|
|
||||||
|
videos = convert_all_videos(build_dir, args)
|
||||||
|
|
||||||
|
concat_file = generate_concat_file(videos, args)
|
||||||
|
|
||||||
|
if not args.dry:
|
||||||
|
make_output_video(concat_file, output_file, args)
|
||||||
|
|||||||
Reference in New Issue
Block a user