20
20
21
21
import inspect
22
22
import logging
23
+ import os
24
+ import os .path
23
25
import typing as ty
24
26
25
27
import click
32
34
CONFIG_MAP ,
33
35
DEFAULT_JPG_QUALITY ,
34
36
DEFAULT_WEBP_QUALITY ,
35
- USER_CONFIG ,
36
37
TimecodeFormat ,
37
38
)
38
- from scenedetect ._cli .context import CliContext , check_split_video_requirements
39
+ from scenedetect ._cli .context import USER_CONFIG , CliContext , check_split_video_requirements
39
40
from scenedetect .backends import AVAILABLE_BACKENDS
40
41
from scenedetect .detectors import (
41
42
AdaptiveDetector ,
@@ -315,8 +316,10 @@ def scenedetect(
315
316
316
317
Global options (e.g. -i/--input, -c/--config) must be specified before any commands and their options. The order of commands is not strict, but each command must only be specified once.
317
318
"""
318
- assert isinstance (ctx .obj , CliContext )
319
- ctx .obj .handle_options (
319
+ ctx = ctx .obj
320
+ assert isinstance (ctx , CliContext )
321
+
322
+ ctx .handle_options (
320
323
input_path = input ,
321
324
output = output ,
322
325
framerate = framerate ,
@@ -344,7 +347,6 @@ def scenedetect(
344
347
@click .pass_context
345
348
def help_command (ctx : click .Context , command_name : str ):
346
349
"""Print help for command (`help [command]`)."""
347
- assert isinstance (ctx .obj , CliContext )
348
350
assert isinstance (ctx .parent .command , click .MultiCommand )
349
351
parent_command = ctx .parent .command
350
352
all_commands = set (parent_command .list_commands (ctx ))
@@ -368,7 +370,6 @@ def help_command(ctx: click.Context, command_name: str):
368
370
@click .pass_context
369
371
def about_command (ctx : click .Context ):
370
372
"""Print license/copyright info."""
371
- assert isinstance (ctx .obj , CliContext )
372
373
click .echo ("" )
373
374
click .echo (click .style (_LINE_SEPARATOR , fg = "cyan" ))
374
375
click .echo (click .style (" About PySceneDetect %s" % _PROGRAM_VERSION , fg = "yellow" ))
@@ -381,7 +382,6 @@ def about_command(ctx: click.Context):
381
382
@click .pass_context
382
383
def version_command (ctx : click .Context ):
383
384
"""Print PySceneDetect version."""
384
- assert isinstance (ctx .obj , CliContext )
385
385
click .echo ("" )
386
386
click .echo (get_system_version_info ())
387
387
ctx .exit ()
@@ -431,12 +431,23 @@ def time_command(
431
431
432
432
{scenedetect_with_video} time --start 0 --end 1000
433
433
"""
434
- assert isinstance (ctx .obj , CliContext )
435
- ctx .obj .handle_time (
436
- start = start ,
437
- duration = duration ,
438
- end = end ,
439
- )
434
+ ctx = ctx .obj
435
+ assert isinstance (ctx , CliContext )
436
+
437
+ if duration is not None and end is not None :
438
+ raise click .BadParameter (
439
+ "Only one of --duration/-d or --end/-e can be specified, not both." ,
440
+ param_hint = "time" ,
441
+ )
442
+ logger .debug ("Setting video time:\n start: %s, duration: %s, end: %s" , start , duration , end )
443
+ # *NOTE*: The Python API uses 0-based frame indices, but the CLI uses 1-based indices to
444
+ # match the default start number used by `ffmpeg` when saving frames as images. As such,
445
+ # we must correct start time if set as frames. See the test_cli_time* tests for for details.
446
+ ctx .start_time = ctx .parse_timecode (start , correct_pts = True )
447
+ ctx .end_time = ctx .parse_timecode (end )
448
+ ctx .duration = ctx .parse_timecode (duration )
449
+ if ctx .start_time and ctx .end_time and (ctx .start_time + 1 ) > ctx .end_time :
450
+ raise click .BadParameter ("-e/--end time must be greater than -s/--start" )
440
451
441
452
442
453
@click .command ("detect-content" , cls = _Command )
@@ -535,17 +546,17 @@ def detect_content_command(
535
546
536
547
{scenedetect_with_video} detect-content --threshold 27.5
537
548
"""
538
- assert isinstance (ctx .obj , CliContext )
539
- detector_args = ctx .obj .get_detect_content_params (
549
+ ctx = ctx .obj
550
+ assert isinstance (ctx , CliContext )
551
+ detector_args = ctx .get_detect_content_params (
540
552
threshold = threshold ,
541
553
luma_only = luma_only ,
542
554
min_scene_len = min_scene_len ,
543
555
weights = weights ,
544
556
kernel_size = kernel_size ,
545
557
filter_mode = filter_mode ,
546
558
)
547
- logger .debug ("Adding detector: ContentDetector(%s)" , detector_args )
548
- ctx .obj .add_detector (ContentDetector (** detector_args ))
559
+ ctx .add_detector (ContentDetector , detector_args )
549
560
550
561
551
562
@click .command ("detect-adaptive" , cls = _Command )
@@ -646,8 +657,9 @@ def detect_adaptive_command(
646
657
647
658
{scenedetect_with_video} detect-adaptive --threshold 3.2
648
659
"""
649
- assert isinstance (ctx .obj , CliContext )
650
- detector_args = ctx .obj .get_detect_adaptive_params (
660
+ ctx = ctx .obj
661
+ assert isinstance (ctx , CliContext )
662
+ detector_args = ctx .get_detect_adaptive_params (
651
663
threshold = threshold ,
652
664
min_content_val = min_content_val ,
653
665
min_delta_hsv = min_delta_hsv ,
@@ -657,8 +669,7 @@ def detect_adaptive_command(
657
669
weights = weights ,
658
670
kernel_size = kernel_size ,
659
671
)
660
- logger .debug ("Adding detector: AdaptiveDetector(%s)" , detector_args )
661
- ctx .obj .add_detector (AdaptiveDetector (** detector_args ))
672
+ ctx .add_detector (AdaptiveDetector , detector_args )
662
673
663
674
664
675
@click .command ("detect-threshold" , cls = _Command )
@@ -725,15 +736,15 @@ def detect_threshold_command(
725
736
726
737
{scenedetect_with_video} detect-threshold --threshold 15
727
738
"""
728
- assert isinstance (ctx .obj , CliContext )
729
- detector_args = ctx .obj .get_detect_threshold_params (
739
+ ctx = ctx .obj
740
+ assert isinstance (ctx , CliContext )
741
+ detector_args = ctx .get_detect_threshold_params (
730
742
threshold = threshold ,
731
743
fade_bias = fade_bias ,
732
744
add_last_scene = add_last_scene ,
733
745
min_scene_len = min_scene_len ,
734
746
)
735
- logger .debug ("Adding detector: ThresholdDetector(%s)" , detector_args )
736
- ctx .obj .add_detector (ThresholdDetector (** detector_args ))
747
+ ctx .add_detector (ThresholdDetector , detector_args )
737
748
738
749
739
750
@click .command ("detect-hist" , cls = _Command )
@@ -795,14 +806,12 @@ def detect_hist_command(
795
806
796
807
{scenedetect_with_video} detect-hist --threshold 0.1 --bins 240
797
808
"""
798
- assert isinstance (ctx .obj , CliContext )
799
-
800
- assert isinstance (ctx .obj , CliContext )
801
- detector_args = ctx .obj .get_detect_hist_params (
809
+ ctx = ctx .obj
810
+ assert isinstance (ctx , CliContext )
811
+ detector_args = ctx .get_detect_hist_params (
802
812
threshold = threshold , bins = bins , min_scene_len = min_scene_len
803
813
)
804
- logger .debug ("Adding detector: HistogramDetector(%s)" , detector_args )
805
- ctx .obj .add_detector (HistogramDetector (** detector_args ))
814
+ ctx .add_detector (HistogramDetector , detector_args )
806
815
807
816
808
817
@click .command ("detect-hash" , cls = _Command )
@@ -880,14 +889,12 @@ def detect_hash_command(
880
889
881
890
{scenedetect_with_video} detect-hash --size 32 --lowpass 3
882
891
"""
883
- assert isinstance (ctx .obj , CliContext )
884
-
885
- assert isinstance (ctx .obj , CliContext )
886
- detector_args = ctx .obj .get_detect_hash_params (
892
+ ctx = ctx .obj
893
+ assert isinstance (ctx , CliContext )
894
+ detector_args = ctx .get_detect_hash_params (
887
895
threshold = threshold , size = size , lowpass = lowpass , min_scene_len = min_scene_len
888
896
)
889
- logger .debug ("Adding detector: HashDetector(%s)" , detector_args )
890
- ctx .obj .add_detector (HashDetector (** detector_args ))
897
+ ctx .add_detector (HashDetector , detector_args )
891
898
892
899
893
900
@click .command ("load-scenes" , cls = _Command )
@@ -921,9 +928,23 @@ def load_scenes_command(
921
928
922
929
{scenedetect_with_video} load-scenes -i scenes.csv --start-col-name "Start Timecode"
923
930
"""
924
- assert isinstance (ctx .obj , CliContext )
925
- logger .debug ("Loading scenes from %s (start_col_name = %s)" , input , start_col_name )
926
- ctx .obj .handle_load_scenes (input = input , start_col_name = start_col_name )
931
+ ctx = ctx .obj
932
+ assert isinstance (ctx , CliContext )
933
+
934
+ logger .debug ("Will load scenes from %s (start_col_name = %s)" , input , start_col_name )
935
+ if ctx .scene_manager .get_num_detectors () > 0 :
936
+ raise click .ClickException ("The load-scenes command cannot be used with detectors." )
937
+ if ctx .load_scenes_input :
938
+ raise click .ClickException ("The load-scenes command must only be specified once." )
939
+ input = os .path .abspath (input )
940
+ if not os .path .exists (input ):
941
+ raise click .BadParameter (
942
+ f"Could not load scenes, file does not exist: { input } " , param_hint = "-i/--input"
943
+ )
944
+ ctx .load_scenes_input = input
945
+ ctx .load_scenes_column_name = ctx .config .get_value (
946
+ "load-scenes" , "start-col-name" , start_col_name
947
+ )
927
948
928
949
929
950
@click .command ("export-html" , cls = _Command )
@@ -970,7 +991,7 @@ def export_html_command(
970
991
"""Export scene list to HTML file. Requires save-images unless --no-images is specified."""
971
992
ctx = ctx .obj
972
993
assert isinstance (ctx , CliContext )
973
- ctx . ensure_input_open ()
994
+
974
995
no_images = no_images or ctx .config .get_value ("export-html" , "no-images" )
975
996
if not ctx .save_images and not no_images :
976
997
raise click .BadArgumentUsage (
@@ -1037,7 +1058,7 @@ def list_scenes_command(
1037
1058
"""Create scene list CSV file (will be named $VIDEO_NAME-Scenes.csv by default)."""
1038
1059
ctx = ctx .obj
1039
1060
assert isinstance (ctx , CliContext )
1040
- ctx . ensure_input_open ()
1061
+
1041
1062
no_output_file = no_output_file or ctx .config .get_value ("list-scenes" , "no-output-file" )
1042
1063
scene_list_dir = ctx .config .get_value ("list-scenes" , "output" , output , ignore_default = True )
1043
1064
scene_list_name_format = ctx .config .get_value ("list-scenes" , "filename" , filename )
@@ -1162,7 +1183,7 @@ def split_video_command(
1162
1183
"""
1163
1184
ctx = ctx .obj
1164
1185
assert isinstance (ctx , CliContext )
1165
- ctx . ensure_input_open ()
1186
+
1166
1187
check_split_video_requirements (use_mkvmerge = mkvmerge )
1167
1188
if "%" in ctx .video_stream .path or "://" in ctx .video_stream .path :
1168
1189
error = "The split-video command is incompatible with image sequences/URLs."
@@ -1362,7 +1383,7 @@ def save_images_command(
1362
1383
"""
1363
1384
ctx = ctx .obj
1364
1385
assert isinstance (ctx , CliContext )
1365
- ctx . ensure_input_open ()
1386
+
1366
1387
if "://" in ctx .video_stream .path :
1367
1388
error_str = "\n The save-images command is incompatible with URLs."
1368
1389
logger .error (error_str )
0 commit comments