|
25 | 25 | import click
|
26 | 26 |
|
27 | 27 | 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 |
30 | 39 | from scenedetect.backends import AVAILABLE_BACKENDS
|
31 | 40 | from scenedetect.detectors import (
|
32 | 41 | AdaptiveDetector,
|
|
35 | 44 | HistogramDetector,
|
36 | 45 | ThresholdDetector,
|
37 | 46 | )
|
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 |
39 | 49 |
|
40 | 50 | _PROGRAM_VERSION = scenedetect.__version__
|
41 | 51 | """Used to avoid name conflict with named `scenedetect` command below."""
|
@@ -958,13 +968,20 @@ def export_html_command(
|
958 | 968 | image_height: ty.Optional[int],
|
959 | 969 | ):
|
960 | 970 | """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) |
968 | 985 |
|
969 | 986 |
|
970 | 987 | @click.command("list-scenes", cls=_Command)
|
@@ -1018,14 +1035,23 @@ def list_scenes_command(
|
1018 | 1035 | skip_cuts: bool,
|
1019 | 1036 | ):
|
1020 | 1037 | """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) |
1029 | 1055 |
|
1030 | 1056 |
|
1031 | 1057 | @click.command("split-video", cls=_Command)
|
@@ -1134,18 +1160,73 @@ def split_video_command(
|
1134 | 1160 |
|
1135 | 1161 | {scenedetect_with_video} split-video --filename \\$VIDEO_NAME-Clip-\\$SCENE_NUMBER
|
1136 | 1162 | """
|
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) |
1149 | 1230 |
|
1150 | 1231 |
|
1151 | 1232 | @click.command("save-images", cls=_Command)
|
@@ -1279,21 +1360,65 @@ def save_images_command(
|
1279 | 1360 |
|
1280 | 1361 | {scenedetect_with_video} save-images --filename \\$SCENE_NUMBER-img\\$IMAGE_NUMBER
|
1281 | 1362 | """
|
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") |
1296 | 1389 | )
|
| 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 |
1297 | 1422 |
|
1298 | 1423 |
|
1299 | 1424 | # ----------------------------------------------------------------------
|
|
0 commit comments