Skip to content

Commit d399eb1

Browse files
committed
[cli] Move output command state out of CliContext
To simplify things, all output commands can be treated similarily as they all act on the result of the processing pipeline. This allows new commands to be added without needing to explicitly define their values in CliContext, and makes the values that do remain there much more meaningful. It also allows commands to be specified multiple times gracefully and with different options, so for example `save-images` can now be run twice with different encoding parameters.
1 parent d460a95 commit d399eb1

File tree

7 files changed

+496
-585
lines changed

7 files changed

+496
-585
lines changed

scenedetect/__main__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@
2222

2323
def main():
2424
"""PySceneDetect command-line interface (CLI) entry point."""
25-
cli_ctx = CliContext()
25+
context = CliContext()
2626
try:
2727
# Process command line arguments and subcommands to initialize the context.
28-
scenedetect.main(obj=cli_ctx) # Parse CLI arguments with registered callbacks.
28+
scenedetect.main(obj=context) # Parse CLI arguments with registered callbacks.
2929
except SystemExit as exit:
3030
help_command = any(arg in sys.argv for arg in ["-h", "--help"])
3131
if help_command or exit.code != 0:
@@ -38,12 +38,12 @@ def main():
3838
# no progress bars get created, we instead create a fake context manager. This is done here
3939
# to avoid needing a separate context manager at each point a progress bar is created.
4040
log_redirect = (
41-
FakeTqdmLoggingRedirect() if cli_ctx.quiet_mode else logging_redirect_tqdm(loggers=[logger])
41+
FakeTqdmLoggingRedirect() if context.quiet_mode else logging_redirect_tqdm(loggers=[logger])
4242
)
4343

4444
with log_redirect:
4545
try:
46-
run_scenedetect(cli_ctx)
46+
run_scenedetect(context)
4747
except KeyboardInterrupt:
4848
logger.info("Stopped.")
4949
if __debug__:

scenedetect/_cli/__init__.py

Lines changed: 169 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,17 @@
2525
import click
2626

2727
import scenedetect
28-
from scenedetect._cli.config import CHOICE_MAP, CONFIG_FILE_PATH, CONFIG_MAP
29-
from scenedetect._cli.context import USER_CONFIG, CliContext
28+
import scenedetect._cli.commands as cli_commands
29+
from scenedetect._cli.config import (
30+
CHOICE_MAP,
31+
CONFIG_FILE_PATH,
32+
CONFIG_MAP,
33+
DEFAULT_JPG_QUALITY,
34+
DEFAULT_WEBP_QUALITY,
35+
USER_CONFIG,
36+
TimecodeFormat,
37+
)
38+
from scenedetect._cli.context import CliContext, check_split_video_requirements
3039
from scenedetect.backends import AVAILABLE_BACKENDS
3140
from scenedetect.detectors import (
3241
AdaptiveDetector,
@@ -35,7 +44,8 @@
3544
HistogramDetector,
3645
ThresholdDetector,
3746
)
38-
from scenedetect.platform import get_system_version_info
47+
from scenedetect.platform import get_cv2_imwrite_params, get_system_version_info
48+
from scenedetect.scene_manager import Interpolation
3949

4050
_PROGRAM_VERSION = scenedetect.__version__
4151
"""Used to avoid name conflict with named `scenedetect` command below."""
@@ -958,13 +968,20 @@ def export_html_command(
958968
image_height: ty.Optional[int],
959969
):
960970
"""Export scene list to HTML file. Requires save-images unless --no-images is specified."""
961-
assert isinstance(ctx.obj, CliContext)
962-
ctx.obj.handle_export_html(
963-
filename=filename,
964-
no_images=no_images,
965-
image_width=image_width,
966-
image_height=image_height,
967-
)
971+
ctx = ctx.obj
972+
assert isinstance(ctx, CliContext)
973+
ctx.ensure_input_open()
974+
no_images = no_images or ctx.config.get_value("export-html", "no-images")
975+
if not ctx.save_images and not no_images:
976+
raise click.BadArgumentUsage(
977+
"export-html requires that save-images precedes it or --no-images is specified."
978+
)
979+
export_html_args = {
980+
"html_name_format": ctx.config.get_value("export-html", "filename", filename),
981+
"image_width": ctx.config.get_value("export-html", "image-width", image_width),
982+
"image_height": ctx.config.get_value("export-html", "image-height", image_height),
983+
}
984+
ctx.add_command(cli_commands.export_html, export_html_args)
968985

969986

970987
@click.command("list-scenes", cls=_Command)
@@ -1018,14 +1035,23 @@ def list_scenes_command(
10181035
skip_cuts: bool,
10191036
):
10201037
"""Create scene list CSV file (will be named $VIDEO_NAME-Scenes.csv by default)."""
1021-
assert isinstance(ctx.obj, CliContext)
1022-
ctx.obj.handle_list_scenes(
1023-
output=output,
1024-
filename=filename,
1025-
no_output_file=no_output_file,
1026-
quiet=quiet,
1027-
skip_cuts=skip_cuts,
1028-
)
1038+
ctx = ctx.obj
1039+
assert isinstance(ctx, CliContext)
1040+
ctx.ensure_input_open()
1041+
no_output_file = no_output_file or ctx.config.get_value("list-scenes", "no-output-file")
1042+
scene_list_dir = ctx.config.get_value("list-scenes", "output", output, ignore_default=True)
1043+
scene_list_name_format = ctx.config.get_value("list-scenes", "filename", filename)
1044+
list_scenes_args = {
1045+
"cut_format": TimecodeFormat[ctx.config.get_value("list-scenes", "cut-format").upper()],
1046+
"display_scenes": ctx.config.get_value("list-scenes", "display-scenes"),
1047+
"display_cuts": ctx.config.get_value("list-scenes", "display-cuts"),
1048+
"scene_list_output": not no_output_file,
1049+
"scene_list_name_format": scene_list_name_format,
1050+
"skip_cuts": skip_cuts or ctx.config.get_value("list-scenes", "skip-cuts"),
1051+
"output_dir": scene_list_dir,
1052+
"quiet": quiet or ctx.config.get_value("list-scenes", "quiet") or ctx.quiet_mode,
1053+
}
1054+
ctx.add_command(cli_commands.list_scenes, list_scenes_args)
10291055

10301056

10311057
@click.command("split-video", cls=_Command)
@@ -1134,18 +1160,73 @@ def split_video_command(
11341160
11351161
{scenedetect_with_video} split-video --filename \\$VIDEO_NAME-Clip-\\$SCENE_NUMBER
11361162
"""
1137-
assert isinstance(ctx.obj, CliContext)
1138-
ctx.obj.handle_split_video(
1139-
output=output,
1140-
filename=filename,
1141-
quiet=quiet,
1142-
copy=copy,
1143-
high_quality=high_quality,
1144-
rate_factor=rate_factor,
1145-
preset=preset,
1146-
args=args,
1147-
mkvmerge=mkvmerge,
1148-
)
1163+
ctx = ctx.obj
1164+
assert isinstance(ctx, CliContext)
1165+
ctx.ensure_input_open()
1166+
check_split_video_requirements(use_mkvmerge=mkvmerge)
1167+
if "%" in ctx.video_stream.path or "://" in ctx.video_stream.path:
1168+
error = "The split-video command is incompatible with image sequences/URLs."
1169+
raise click.BadParameter(error, param_hint="split-video")
1170+
1171+
# We only load the config values for these flags/options if none of the other
1172+
# encoder flags/options were set via the CLI to avoid any conflicting options
1173+
# (e.g. if the config file sets `high-quality = yes` but `--copy` is specified).
1174+
if not (mkvmerge or copy or high_quality or args or rate_factor or preset):
1175+
mkvmerge = ctx.config.get_value("split-video", "mkvmerge")
1176+
copy = ctx.config.get_value("split-video", "copy")
1177+
high_quality = ctx.config.get_value("split-video", "high-quality")
1178+
rate_factor = ctx.config.get_value("split-video", "rate-factor")
1179+
preset = ctx.config.get_value("split-video", "preset")
1180+
args = ctx.config.get_value("split-video", "args")
1181+
1182+
# Disallow certain combinations of options.
1183+
if mkvmerge or copy:
1184+
command = "mkvmerge (-m)" if mkvmerge else "copy (-c)"
1185+
if high_quality:
1186+
raise click.BadParameter(
1187+
"high-quality (-hq) cannot be used with %s" % (command),
1188+
param_hint="split-video",
1189+
)
1190+
if args:
1191+
raise click.BadParameter(
1192+
"args (-a) cannot be used with %s" % (command), param_hint="split-video"
1193+
)
1194+
if rate_factor:
1195+
raise click.BadParameter(
1196+
"rate-factor (crf) cannot be used with %s" % (command), param_hint="split-video"
1197+
)
1198+
if preset:
1199+
raise click.BadParameter(
1200+
"preset (-p) cannot be used with %s" % (command), param_hint="split-video"
1201+
)
1202+
1203+
# mkvmerge-Specific Options
1204+
if mkvmerge and copy:
1205+
logger.warning("copy mode (-c) ignored due to mkvmerge mode (-m).")
1206+
1207+
# ffmpeg-Specific Options
1208+
if copy:
1209+
args = "-map 0:v:0 -map 0:a? -map 0:s? -c:v copy -c:a copy"
1210+
elif not args:
1211+
if rate_factor is None:
1212+
rate_factor = 22 if not high_quality else 17
1213+
if preset is None:
1214+
preset = "veryfast" if not high_quality else "slow"
1215+
args = (
1216+
"-map 0:v:0 -map 0:a? -map 0:s? "
1217+
f"-c:v libx264 -preset {preset} -crf {rate_factor} -c:a aac"
1218+
)
1219+
if filename:
1220+
logger.info("Output file name format: %s", filename)
1221+
1222+
split_video_args = {
1223+
"name_format": ctx.config.get_value("split-video", "filename", filename),
1224+
"use_mkvmerge": mkvmerge,
1225+
"output_dir": ctx.config.get_value("split-video", "output", output, ignore_default=True),
1226+
"show_output": not quiet,
1227+
"ffmpeg_args": args,
1228+
}
1229+
ctx.add_command(cli_commands.split_video, split_video_args)
11491230

11501231

11511232
@click.command("save-images", cls=_Command)
@@ -1279,21 +1360,65 @@ def save_images_command(
12791360
12801361
{scenedetect_with_video} save-images --filename \\$SCENE_NUMBER-img\\$IMAGE_NUMBER
12811362
"""
1282-
assert isinstance(ctx.obj, CliContext)
1283-
ctx.obj.handle_save_images(
1284-
num_images=num_images,
1285-
output=output,
1286-
filename=filename,
1287-
jpeg=jpeg,
1288-
webp=webp,
1289-
quality=quality,
1290-
png=png,
1291-
compression=compression,
1292-
frame_margin=frame_margin,
1293-
scale=scale,
1294-
height=height,
1295-
width=width,
1363+
ctx = ctx.obj
1364+
assert isinstance(ctx, CliContext)
1365+
ctx.ensure_input_open()
1366+
if "://" in ctx.video_stream.path:
1367+
error_str = "\nThe save-images command is incompatible with URLs."
1368+
logger.error(error_str)
1369+
raise click.BadParameter(error_str, param_hint="save-images")
1370+
num_flags = sum([1 if flag else 0 for flag in [jpeg, webp, png]])
1371+
if num_flags > 1:
1372+
logger.error(".")
1373+
raise click.BadParameter("Only one image type can be specified.", param_hint="save-images")
1374+
elif num_flags == 0:
1375+
image_format = ctx.config.get_value("save-images", "format").lower()
1376+
jpeg = image_format == "jpeg"
1377+
webp = image_format == "webp"
1378+
png = image_format == "png"
1379+
1380+
if not any((scale, height, width)):
1381+
scale = ctx.config.get_value("save-images", "scale")
1382+
height = ctx.config.get_value("save-images", "height")
1383+
width = ctx.config.get_value("save-images", "width")
1384+
scale_method = Interpolation[ctx.config.get_value("save-images", "scale-method").upper()]
1385+
quality = (
1386+
(DEFAULT_WEBP_QUALITY if webp else DEFAULT_JPG_QUALITY)
1387+
if ctx.config.is_default("save-images", "quality")
1388+
else ctx.config.get_value("save-images", "quality")
12961389
)
1390+
compression = ctx.config.get_value("save-images", "compression", compression)
1391+
image_extension = "jpg" if jpeg else "png" if png else "webp"
1392+
valid_params = get_cv2_imwrite_params()
1393+
if image_extension not in valid_params or valid_params[image_extension] is None:
1394+
error_strs = [
1395+
"Image encoder type `%s` not supported." % image_extension.upper(),
1396+
"The specified encoder type could not be found in the current OpenCV module.",
1397+
"To enable this output format, please update the installed version of OpenCV.",
1398+
"If you build OpenCV, ensure the the proper dependencies are enabled. ",
1399+
]
1400+
logger.debug("\n".join(error_strs))
1401+
raise click.BadParameter("\n".join(error_strs), param_hint="save-images")
1402+
output = ctx.config.get_value("save-images", "output", output, ignore_default=True)
1403+
1404+
save_images_args = {
1405+
"encoder_param": compression if png else quality,
1406+
"frame_margin": ctx.config.get_value("save-images", "frame-margin", frame_margin),
1407+
"height": height,
1408+
"image_extension": image_extension,
1409+
"image_name_template": ctx.config.get_value("save-images", "filename", filename),
1410+
"interpolation": scale_method,
1411+
"num_images": ctx.config.get_value("save-images", "num-images", num_images),
1412+
"output_dir": output,
1413+
"scale": scale,
1414+
"show_progress": ctx.quiet_mode,
1415+
"width": width,
1416+
}
1417+
ctx.add_command(cli_commands.save_images, save_images_args)
1418+
1419+
# Record that we added a save-images command to the pipeline so we can allow export-html
1420+
# to run afterwards (it is dependent on the output).
1421+
ctx.save_images = True
12971422

12981423

12991424
# ----------------------------------------------------------------------

0 commit comments

Comments
 (0)