scenedetect 0.7.1.dev0__tar.gz → 0.7.2__tar.gz

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.
Files changed (90) hide show
  1. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/PKG-INFO +2 -2
  2. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/README.md +29 -14
  3. scenedetect-0.7.2/docs/LATEST_VERSION +1 -0
  4. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/docs/api.rst +3 -3
  5. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/docs/cli/backends.rst +1 -1
  6. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/docs/cli.rst +45 -7
  7. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/docs/index.rst +1 -1
  8. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/packaging/package-info.rst +1 -1
  9. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect/__init__.py +46 -38
  10. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect/_cli/__init__.py +68 -18
  11. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect/_cli/commands.py +23 -0
  12. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect/_cli/config.py +9 -4
  13. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect/_cli/context.py +4 -0
  14. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect/_cli/controller.py +0 -1
  15. scenedetect-0.7.2/scenedetect/_fan_out.py +244 -0
  16. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect/backends/__init__.py +6 -4
  17. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect/backends/moviepy.py +8 -2
  18. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect/backends/opencv.py +43 -7
  19. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect/backends/pyav.py +75 -38
  20. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect/common.py +38 -11
  21. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect/detector.py +19 -13
  22. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect/detectors/__init__.py +5 -5
  23. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect/detectors/adaptive_detector.py +2 -1
  24. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect/detectors/hash_detector.py +2 -2
  25. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect/detectors/histogram_detector.py +8 -6
  26. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect/detectors/threshold_detector.py +24 -6
  27. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect/output/__init__.py +139 -41
  28. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect/platform.py +10 -6
  29. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect/scene_manager.py +37 -24
  30. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect/video_stream.py +1 -1
  31. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect.cfg +18 -4
  32. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect.egg-info/PKG-INFO +2 -2
  33. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect.egg-info/SOURCES.txt +3 -0
  34. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/tests/test_api.py +7 -3
  35. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/tests/test_backend_opencv.py +44 -1
  36. scenedetect-0.7.2/tests/test_backend_pyav.py +222 -0
  37. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/tests/test_cli.py +90 -26
  38. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/tests/test_detectors.py +50 -4
  39. scenedetect-0.7.2/tests/test_fan_out.py +236 -0
  40. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/tests/test_output.py +145 -6
  41. scenedetect-0.7.2/tests/test_threshold_detector.py +165 -0
  42. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/tests/test_timecode.py +136 -7
  43. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/tests/test_vfr.py +29 -29
  44. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/tests/test_video_stream.py +48 -32
  45. scenedetect-0.7.1.dev0/docs/LATEST_VERSION +0 -1
  46. scenedetect-0.7.1.dev0/tests/test_backend_pyav.py +0 -93
  47. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/LICENSE +0 -0
  48. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/docs/Makefile +0 -0
  49. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/docs/_static/favicon.ico +0 -0
  50. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/docs/_static/pyscenedetect.css +0 -0
  51. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/docs/_static/pyscenedetect_logo.png +0 -0
  52. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/docs/_static/pyscenedetect_logo_small.png +0 -0
  53. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/docs/_templates/navigation.html +0 -0
  54. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/docs/api/backends.rst +0 -0
  55. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/docs/api/common.rst +0 -0
  56. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/docs/api/detector.rst +0 -0
  57. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/docs/api/detectors.rst +0 -0
  58. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/docs/api/migration_guide.rst +0 -0
  59. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/docs/api/output.rst +0 -0
  60. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/docs/api/platform.rst +0 -0
  61. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/docs/api/scene_manager.rst +0 -0
  62. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/docs/api/stats_manager.rst +0 -0
  63. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/docs/api/video_stream.rst +0 -0
  64. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/docs/cli/config_file.rst +0 -0
  65. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/docs/conf.py +0 -0
  66. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/docs/generate_cli_docs.py +0 -0
  67. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/docs/make.bat +0 -0
  68. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/pyproject.toml +0 -0
  69. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect/__main__.py +0 -0
  70. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect/_thirdparty/__init__.py +0 -0
  71. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect/_thirdparty/simpletable.py +0 -0
  72. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect/backends/concat.py +0 -0
  73. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect/detectors/content_detector.py +0 -0
  74. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect/detectors/transnet_v2.py +0 -0
  75. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect/frame_timecode.py +0 -0
  76. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect/output/image.py +0 -0
  77. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect/output/video.py +0 -0
  78. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect/scene_detector.py +0 -0
  79. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect/stats_manager.py +0 -0
  80. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect/video_splitter.py +0 -0
  81. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect.egg-info/dependency_links.txt +0 -0
  82. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect.egg-info/entry_points.txt +0 -0
  83. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect.egg-info/requires.txt +0 -0
  84. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/scenedetect.egg-info/top_level.txt +0 -0
  85. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/setup.cfg +0 -0
  86. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/tests/test_benchmark_evaluator.py +0 -0
  87. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/tests/test_concat.py +0 -0
  88. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/tests/test_platform.py +0 -0
  89. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/tests/test_scene_manager.py +0 -0
  90. {scenedetect-0.7.1.dev0 → scenedetect-0.7.2}/tests/test_stats_manager.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scenedetect
3
- Version: 0.7.1.dev0
3
+ Version: 0.7.2
4
4
  Summary: Video scene cut/shot detection program and Python library (bundles the opencv-python OpenCV variant and the scenedetect CLI).
5
5
  Author-email: Brandon Castellano <brandon248@gmail.com>
6
6
  License-Expression: BSD-3-Clause
@@ -66,7 +66,7 @@ Github Repo: https://github.com/Breakthrough/PySceneDetect/
66
66
 
67
67
  Install: ``pip install --upgrade scenedetect`` (or ``scenedetect-headless`` for servers)
68
68
 
69
- Packages: `scenedetect <https://pypi.org/project/scenedetect/>`_ (CLI + ``opencv-python``), `scenedetect-headless <https://pypi.org/project/scenedetect-headless/>`_ (CLI + ``opencv-python-headless``), and `scenedetect-core <https://pypi.org/project/scenedetect-core/>`_ (library only, minimal dependencies, bring your own OpenCV variant). All provide the same ``scenedetect`` module -- install only one.
69
+ Packages: `scenedetect <https://pypi.org/project/scenedetect/>`_ (CLI + ``opencv-python``) and `scenedetect-headless <https://pypi.org/project/scenedetect-headless/>`_ (CLI + ``opencv-python-headless``). Both provide the same ``scenedetect`` module -- install only one.
70
70
 
71
71
  ----------------------------------------------------------
72
72
 
@@ -13,7 +13,7 @@
13
13
 
14
14
  ----------------------------------------------------------
15
15
 
16
- ### Latest Release: v0.7 (May 3, 2026)
16
+ ### Latest Release: v0.7.2 (October 11, 2026)
17
17
 
18
18
  **Website**: [scenedetect.com](https://www.scenedetect.com)
19
19
 
@@ -29,7 +29,7 @@
29
29
 
30
30
  pip install scenedetect --upgrade
31
31
 
32
- Requires ffmpeg/mkvmerge for video splitting support. Windows builds (MSI installer/portable ZIP) can be found on [the download page](https://scenedetect.com/download/).
32
+ Requires ffmpeg/mkvmerge for video splitting support. Windows builds (MSI installer/portable ZIP) can be found on [the download page](https://scenedetect.com/download/). A Docker image with all dependencies included is available as [`ghcr.io/breakthrough/pyscenedetect`](https://github.com/Breakthrough/PySceneDetect/pkgs/container/pyscenedetect).
33
33
 
34
34
  ----------------------------------------------------------
35
35
 
@@ -49,13 +49,20 @@ Skip the first 10 seconds of the input video:
49
49
 
50
50
  More examples can be found throughout [the documentation](https://www.scenedetect.com/docs/latest/cli.html).
51
51
 
52
+ **Quick Start (Docker)**:
53
+
54
+ The same commands work without installing anything using [the official Docker image](https://github.com/Breakthrough/PySceneDetect/pkgs/container/pyscenedetect), which includes all dependencies (`ffmpeg`/`mkvmerge` included). Mount the folder containing your videos and use it for input/output paths:
55
+
56
+ docker run --rm -v "$(pwd):/files" ghcr.io/breakthrough/pyscenedetect -i /files/video.mp4 split-video -o /files
57
+
52
58
  **Quick Start (Python API)**:
53
59
 
54
60
  To get started, there is a high level function in the library that performs content-aware scene detection on a video (try it from a Python prompt):
55
61
 
56
62
  ```python
57
63
  from scenedetect import detect, ContentDetector
58
- scene_list = detect('my_video.mp4', ContentDetector())
64
+
65
+ scene_list = detect("my_video.mp4", ContentDetector())
59
66
  ```
60
67
 
61
68
  `scene_list` will now be a list containing the start/end times of all scenes found in the video. There also exists a two-pass version `AdaptiveDetector` which handles fast camera movement better, and `ThresholdDetector` for handling fade out/fade in events.
@@ -64,20 +71,28 @@ Try calling `print(scene_list)`, or iterating over each scene:
64
71
 
65
72
  ```python
66
73
  from scenedetect import detect, ContentDetector
67
- scene_list = detect('my_video.mp4', ContentDetector())
74
+
75
+ scene_list = detect("my_video.mp4", ContentDetector())
68
76
  for i, scene in enumerate(scene_list):
69
- print(' Scene %2d: Start %s / Frame %d, End %s / Frame %d' % (
70
- i+1,
71
- scene[0].get_timecode(), scene[0].frame_num,
72
- scene[1].get_timecode(), scene[1].frame_num,))
77
+ print(
78
+ " Scene %2d: Start %s / Frame %d, End %s / Frame %d"
79
+ % (
80
+ i + 1,
81
+ scene[0].get_timecode(),
82
+ scene[0].frame_num,
83
+ scene[1].get_timecode(),
84
+ scene[1].frame_num,
85
+ )
86
+ )
73
87
  ```
74
88
 
75
89
  We can also split the video into each scene if `ffmpeg` is installed (`mkvmerge` is also supported):
76
90
 
77
91
  ```python
78
92
  from scenedetect import detect, ContentDetector, split_video_ffmpeg
79
- scene_list = detect('my_video.mp4', ContentDetector())
80
- split_video_ffmpeg('my_video.mp4', scene_list)
93
+
94
+ scene_list = detect("my_video.mp4", ContentDetector())
95
+ split_video_ffmpeg("my_video.mp4", scene_list)
81
96
  ```
82
97
 
83
98
  For more advanced usage, the API is highly configurable, and can easily integrate with any pipeline. This includes using different detection algorithms, splitting the input video, and much more. The following example shows how to implement a function similar to the above, but using [the `scenedetect` API](https://www.scenedetect.com/docs/latest/api.html):
@@ -87,12 +102,12 @@ from scenedetect import open_video, SceneManager, split_video_ffmpeg
87
102
  from scenedetect.detectors import ContentDetector
88
103
  from scenedetect.video_splitter import split_video_ffmpeg
89
104
 
105
+
90
106
  def split_video_into_scenes(video_path, threshold=27.0):
91
107
  # Open our video, create a scene manager, and add a detector.
92
108
  video = open_video(video_path)
93
109
  scene_manager = SceneManager()
94
- scene_manager.add_detector(
95
- ContentDetector(threshold=threshold))
110
+ scene_manager.add_detector(ContentDetector(threshold=threshold))
96
111
  scene_manager.detect_scenes(video, show_progress=True)
97
112
  scene_list = scene_manager.get_scene_list()
98
113
  split_video_ffmpeg(video_path, scene_list, show_progress=True)
@@ -102,13 +117,13 @@ See [the documentation](https://www.scenedetect.com/docs/latest/api.html) for mo
102
117
 
103
118
  **Benchmark**:
104
119
 
105
- We evaluate the performance of different detectors in terms of accuracy and processing speed. See the [benchmark report](benchmark/README.md) for details.
120
+ We evaluate the performance of different detectors in terms of accuracy and processing speed. See [www.scenedetect.com/benchmarks](https://www.scenedetect.com/benchmarks/) for results, or the [benchmark report](benchmark/README.md) for details on the datasets and methodology.
106
121
 
107
122
  ## Reference
108
123
 
109
124
  - [Documentation](https://www.scenedetect.com/docs/) (covers application and Python API)
110
125
  - [CLI Example](https://www.scenedetect.com/cli/)
111
- - [Config File](https://www.scenedetect.com/docs/0.6.4/cli/config_file.html)
126
+ - [Config File](https://www.scenedetect.com/docs/latest/cli/config_file.html)
112
127
 
113
128
  ## Help & Contributing
114
129
 
@@ -0,0 +1 @@
1
+ 0.7.2
@@ -23,9 +23,9 @@ The `scenedetect` API is easy to integrate with most application workflows, whil
23
23
 
24
24
  * :func:`split_video_ffmpeg <scenedetect.output.split_video_ffmpeg>` and :func:`split_video_mkvmerge <scenedetect.output.split_video_mkvmerge>` split a video based on the detected scenes
25
25
 
26
- * :func:`save_images <scenedetect.scene_manager.save_images>` can save an arbitrary number of images from each scene
26
+ * :func:`save_images <scenedetect.output.save_images>` can save an arbitrary number of images from each scene
27
27
 
28
- * :func:`write_scene_list <scenedetect.scene_manager.write_scene_list>` can be used to save scene/cut info as CSV, :func:`write_scene_list_html <scenedetect.scene_manager.write_scene_list_html>` for HTML
28
+ * :func:`write_scene_list <scenedetect.output.write_scene_list>` can be used to save scene/cut info as CSV, :func:`write_scene_list_html <scenedetect.output.write_scene_list_html>` for HTML
29
29
 
30
30
  * :ref:`scenedetect.backends 🎥 <scenedetect-backends>`: PySceneDetect supports multiple libraries as an input backend:
31
31
 
@@ -58,7 +58,7 @@ Most types/functions are also available directly from the `scenedetect` package
58
58
 
59
59
  .. code:: python
60
60
 
61
- scenedetect<0.8
61
+ scenedetect~=0.7
62
62
 
63
63
 
64
64
  .. _scenedetect-quickstart:
@@ -19,7 +19,7 @@ The `OpenCV <https://opencv.org/>`_ backend (usually `opencv-python <https://pyp
19
19
 
20
20
  It is mostly reliable and fast, although can occasionally run into issues processing videos with multiple audio tracks or small amounts of frame corruption. You can use a custom version of the ``cv2`` package, or install either the `opencv-python` or `opencv-python-headless` packages from `pip`.
21
21
 
22
- The OpenCV backend also supports image sequences as inputs (e.g. ``frame%02d.jpg`` if you want to load frame001.jpg, frame002.jpg, frame003.jpg...). Make sure to specify the framerate manually (``-f``/``--framerate``) to ensure accurate timing calculations.
22
+ The OpenCV backend also supports image sequences as inputs (e.g. ``frame%02d.jpg`` if you want to load frame001.jpg, frame002.jpg, frame003.jpg...). Make sure to specify the framerate manually (``-f``/``--frame-rate``) to ensure accurate timing calculations.
23
23
 
24
24
  Variable framerate (VFR) video is supported. Scene detection uses PTS-derived timestamps from ``CAP_PROP_POS_MSEC`` for accurate timecodes. Seeking compensates for OpenCV's average-fps-based internal seek approximation, so output timecodes remain accurate across the full video.
25
25
 
@@ -61,9 +61,9 @@ Options
61
61
 
62
62
  Stats file (.csv) to write frame metrics. Existing files will be overwritten. Used for tuning detection parameters and data analysis.
63
63
 
64
- .. option:: -f FPS, --framerate FPS
64
+ .. option:: -f FPS, --framerate FPS, --frame-rate FPS
65
65
 
66
- Override framerate with value as frames/sec.
66
+ Override frame rate with value as frames/sec.
67
67
 
68
68
  .. option:: -m TIMECODE, --min-scene-len TIMECODE
69
69
 
@@ -308,13 +308,13 @@ Options
308
308
 
309
309
  Max distance between hash values (0.0 to 1.0) of adjacent frames. Lower values are more sensitive to changes.
310
310
 
311
- Default: ``0.395``
311
+ Default: ``0.35``
312
312
 
313
313
  .. option:: -s SIZE, --size SIZE
314
314
 
315
315
  Size of square of low frequency data to include from the discrete cosine transform.
316
316
 
317
- Default: ``16``
317
+ Default: ``8``
318
318
 
319
319
  .. option:: -l FRAC, --lowpass FRAC
320
320
 
@@ -359,13 +359,13 @@ Options
359
359
 
360
360
  Max difference (0.0 to 1.0) between histograms of adjacent frames. Lower values are more sensitive to changes.
361
361
 
362
- Default: ``0.05``
362
+ Default: ``0.2``
363
363
 
364
364
  .. option:: -b NUM, --bins NUM
365
365
 
366
366
  The number of bins to use for the histogram calculation.
367
367
 
368
- Default: ``256``
368
+ Default: ``128``
369
369
 
370
370
  .. option:: -m TIMECODE, --min-scene-len TIMECODE
371
371
 
@@ -416,6 +416,12 @@ Options
416
416
 
417
417
  Default: ``True``
418
418
 
419
+ .. option:: --min-out-length TIMECODE
420
+
421
+ Minimum time spent faded out before a cut is allowed. Ignores shorter fades even when the minimum scene length is met. Also applies to a final fade-out, including the last processed frame. Zero allows fades of any duration. TIMECODE can be specified in frames (4), seconds with an `s` suffix (0.4s), or timecode (00:00:00.400).
422
+
423
+ Default: ``0.1s``. The Python API default remains ``0``.
424
+
419
425
  .. option:: -m TIMECODE, --min-scene-len TIMECODE
420
426
 
421
427
  Minimum length of any scene. Overrides global option :option:`-m/--min-scene-len <scenedetect -m>`. TIMECODE can be specified in frames (-m 100), in seconds with `s` suffix (-m 3.5s), or timecode (-m 00:01:52.778).
@@ -549,6 +555,10 @@ Options
549
555
 
550
556
  Output directory to save EDL file to. Overrides global option :option:`-o/--output <scenedetect -o>`.
551
557
 
558
+ .. option:: -s TIMECODE, --start-timecode TIMECODE
559
+
560
+ Start timecode added to every event so the EDL aligns with the source media's on-screen timecode. Accepts SMPTE HH:MM:SS:FF or 8 digits (HHMMSSFF, e.g. 01000000).
561
+
552
562
 
553
563
  .. _command-save-fcp:
554
564
 
@@ -707,6 +717,34 @@ Options
707
717
  Width (pixels) of images.
708
718
 
709
719
 
720
+ .. _command-save-keyframes:
721
+
722
+ .. program:: scenedetect save-keyframes
723
+
724
+
725
+ ``save-keyframes``
726
+ ========================================================================
727
+
728
+ Save detected cuts using keyframe format v1.
729
+
730
+ Frame numbers are currently approximate for variable framerate (VFR) video.
731
+
732
+
733
+ Options
734
+ ------------------------------------------------------------------------
735
+
736
+
737
+ .. option:: -f NAME, --filename NAME
738
+
739
+ Filename format to use.
740
+
741
+ Default: ``$VIDEO_NAME-keyframes.txt``
742
+
743
+ .. option:: -o DIR, --output DIR
744
+
745
+ Output directory to save keyframes to. Overrides global option :option:`-o/--output <scenedetect -o>`.
746
+
747
+
710
748
  .. _command-save-otio:
711
749
 
712
750
  .. program:: scenedetect save-otio
@@ -859,7 +897,7 @@ Options
859
897
 
860
898
  .. option:: --expand
861
899
 
862
- Extend the first/last output clips to cover the full input video, even if the :ref:`time <command-time>` command's ``--start``/``--end`` limited the analysis window. Useful for keeping content outside the analyzed region attached to the adjacent split.
900
+ Extend the first/last output clips to cover the full input video, even if `time -s/-e` limited the analysis window. Useful for keeping content outside the analyzed region attached to the adjacent split.
863
901
 
864
902
 
865
903
  .. _command-time:
@@ -8,7 +8,7 @@ PySceneDetect Documentation
8
8
 
9
9
  Welcome to the PySceneDetect docs. The docs are split into two separate parts: one for the command-line interface (the `scenedetect` command) and another for the Python API (the `scenedetect` module).
10
10
 
11
- You can install the latest release of PySceneDetect by running `pip install scenedetect` (or `pip install scenedetect-headless` on servers without GUI libraries), or by downloading the Windows build from `scenedetect.com/download <http://www.scenedetect.com/download/>`_. Library-only installs that need a specific OpenCV variant (e.g. `opencv-contrib-python`) can use `pip install scenedetect-core`, which has no CLI dependencies and lets you supply any OpenCV package. PySceneDetect requires `ffmpeg` or `mkvmerge` for video splitting support.
11
+ You can install the latest release of PySceneDetect by running `pip install scenedetect` (or `pip install scenedetect-headless` on servers without GUI libraries), or by downloading the Windows build from `scenedetect.com/download <http://www.scenedetect.com/download/>`_. PySceneDetect requires `ffmpeg` or `mkvmerge` for video splitting support.
12
12
 
13
13
  .. note::
14
14
 
@@ -25,7 +25,7 @@ Github Repo: https://github.com/Breakthrough/PySceneDetect/
25
25
 
26
26
  Install: ``pip install --upgrade scenedetect`` (or ``scenedetect-headless`` for servers)
27
27
 
28
- Packages: `scenedetect <https://pypi.org/project/scenedetect/>`_ (CLI + ``opencv-python``), `scenedetect-headless <https://pypi.org/project/scenedetect-headless/>`_ (CLI + ``opencv-python-headless``), and `scenedetect-core <https://pypi.org/project/scenedetect-core/>`_ (library only, minimal dependencies, bring your own OpenCV variant). All provide the same ``scenedetect`` module -- install only one.
28
+ Packages: `scenedetect <https://pypi.org/project/scenedetect/>`_ (CLI + ``opencv-python``) and `scenedetect-headless <https://pypi.org/project/scenedetect-headless/>`_ (CLI + ``opencv-python-headless``). Both provide the same ``scenedetect`` module -- install only one.
29
29
 
30
30
  ----------------------------------------------------------
31
31
 
@@ -15,6 +15,7 @@ can be used to open a video for a
15
15
  :class:`SceneManager <scenedetect.scene_manager.SceneManager>`.
16
16
  """
17
17
 
18
+ import warnings
18
19
  from logging import getLogger
19
20
 
20
21
  # OpenCV is a required package, but we don't have it as an explicit dependency since we
@@ -30,54 +31,56 @@ except ModuleNotFoundError as ex:
30
31
 
31
32
  # Commonly used classes/functions exported under the `scenedetect` namespace for brevity.
32
33
  # Note that order of importants is important!
33
- from scenedetect.platform import init_logger # noqa: I001
34
+ from scenedetect.platform import init_logger as init_logger # noqa: I001
34
35
  from scenedetect.common import (
35
- FrameTimecode,
36
- FrameRate,
37
- SceneList,
38
- CutList,
39
- CropRegion,
40
- TimecodePair,
41
- TimecodeLike,
42
- Interpolation,
36
+ FrameTimecode as FrameTimecode,
37
+ FrameRate as FrameRate,
38
+ SceneList as SceneList,
39
+ CutList as CutList,
40
+ CropRegion as CropRegion,
41
+ TimecodePair as TimecodePair,
42
+ TimecodeLike as TimecodeLike,
43
+ Interpolation as Interpolation,
43
44
  )
44
- from scenedetect.platform import StrPath
45
- from scenedetect.video_stream import VideoStream, VideoOpenFailure
45
+ from scenedetect.platform import StrPath as StrPath
46
+ from scenedetect.video_stream import VideoStream as VideoStream
47
+ from scenedetect.video_stream import VideoOpenFailure as VideoOpenFailure
46
48
  from scenedetect.output import (
47
- save_images,
48
- split_video_ffmpeg,
49
- split_video_mkvmerge,
50
- is_ffmpeg_available,
51
- is_mkvmerge_available,
52
- write_scene_list,
53
- write_scene_list_html,
54
- PathFormatter,
55
- VideoMetadata,
56
- SceneMetadata,
49
+ save_images as save_images,
50
+ split_video_ffmpeg as split_video_ffmpeg,
51
+ split_video_mkvmerge as split_video_mkvmerge,
52
+ is_ffmpeg_available as is_ffmpeg_available,
53
+ is_mkvmerge_available as is_mkvmerge_available,
54
+ write_scene_list as write_scene_list,
55
+ write_scene_list_html as write_scene_list_html,
56
+ PathFormatter as PathFormatter,
57
+ VideoMetadata as VideoMetadata,
58
+ SceneMetadata as SceneMetadata,
57
59
  )
58
- from scenedetect.detector import SceneDetector
60
+ from scenedetect.detector import SceneDetector as SceneDetector
59
61
  from scenedetect.detectors import (
60
- ContentDetector,
61
- AdaptiveDetector,
62
- ThresholdDetector,
63
- HistogramDetector,
64
- HashDetector,
62
+ ContentDetector as ContentDetector,
63
+ AdaptiveDetector as AdaptiveDetector,
64
+ ThresholdDetector as ThresholdDetector,
65
+ HistogramDetector as HistogramDetector,
66
+ HashDetector as HashDetector,
65
67
  )
66
68
  from scenedetect.backends import (
67
- AVAILABLE_BACKENDS,
68
- VideoStreamCv2,
69
- VideoStreamAv,
70
- VideoStreamMoviePy,
71
- VideoCaptureAdapter,
72
- VideoStreamConcat,
73
- SourceSpan,
69
+ AVAILABLE_BACKENDS as AVAILABLE_BACKENDS,
70
+ VideoStreamCv2 as VideoStreamCv2,
71
+ VideoStreamAv as VideoStreamAv,
72
+ VideoStreamMoviePy as VideoStreamMoviePy,
73
+ VideoCaptureAdapter as VideoCaptureAdapter,
74
+ VideoStreamConcat as VideoStreamConcat,
75
+ SourceSpan as SourceSpan,
74
76
  )
75
- from scenedetect.stats_manager import StatsManager, StatsFileCorrupt
77
+ from scenedetect.stats_manager import StatsManager as StatsManager
78
+ from scenedetect.stats_manager import StatsFileCorrupt as StatsFileCorrupt
76
79
  from scenedetect.scene_manager import SceneManager
77
80
 
78
81
  # Used for module identification and when printing version & about info
79
82
  # (e.g. calling `scenedetect version` or `scenedetect about`).
80
- __version__ = "0.7.1-dev0"
83
+ __version__ = "0.7.2"
81
84
 
82
85
  init_logger()
83
86
  logger = getLogger("pyscenedetect")
@@ -113,8 +116,13 @@ def open_video(
113
116
  :class:`VideoOpenFailure`: Constructing the VideoStream fails. If multiple backends have
114
117
  been attempted, the error from the first backend will be returned.
115
118
  """
116
- # TODO(https://scenedetect.com/issue/548): emit DeprecationWarning when `framerate=` is
117
- # used, once internal callers and downstream users have had a release to migrate.
119
+ if framerate is not None:
120
+ warnings.warn(
121
+ "`framerate` is deprecated and scheduled for removal in v0.9; "
122
+ "use `frame_rate` instead.",
123
+ DeprecationWarning,
124
+ stacklevel=2,
125
+ )
118
126
  if frame_rate is None:
119
127
  frame_rate = framerate
120
128
  # A list of paths is opened as a single concatenated stream. VideoStreamConcat handles
@@ -22,7 +22,6 @@ import inspect
22
22
  import logging
23
23
  import os
24
24
  import os.path
25
- import typing as ty
26
25
  from copy import copy
27
26
 
28
27
  import click
@@ -231,6 +230,7 @@ Global options (e.g. -i/--input, -c/--config) must be specified before any comma
231
230
  )
232
231
  @click.option(
233
232
  "--frame-rate",
233
+ "--framerate",
234
234
  "-f",
235
235
  "frame_rate",
236
236
  metavar="FPS",
@@ -238,15 +238,6 @@ Global options (e.g. -i/--input, -c/--config) must be specified before any comma
238
238
  default=None,
239
239
  help="Override frame rate with value as frames/sec.",
240
240
  )
241
- @click.option(
242
- "--framerate",
243
- "framerate_legacy",
244
- metavar="FPS",
245
- type=click.FLOAT,
246
- default=None,
247
- hidden=True,
248
- help="[DEPRECATED] Use -f/--frame-rate instead.",
249
- )
250
241
  @click.option(
251
242
  "--min-scene-len",
252
243
  "-m",
@@ -347,7 +338,6 @@ def scenedetect(
347
338
  stats: str | None,
348
339
  config: str | None,
349
340
  frame_rate: float | None,
350
- framerate_legacy: float | None,
351
341
  min_scene_len: str | None,
352
342
  drop_short_scenes: bool | None,
353
343
  merge_last_scene: bool | None,
@@ -362,13 +352,6 @@ def scenedetect(
362
352
  ctx = ctx.obj
363
353
  assert isinstance(ctx, CliContext)
364
354
 
365
- # TODO(https://scenedetect.com/issue/548): emit DeprecationWarning when `--framerate`
366
- # is used, once downstream users have had a release to migrate to `--frame-rate`.
367
- if frame_rate is None:
368
- frame_rate = framerate_legacy
369
- elif framerate_legacy is not None:
370
- logger.warning("Both --frame-rate and --framerate were specified; using --frame-rate.")
371
-
372
355
  ctx.handle_options(
373
356
  input_path=input,
374
357
  output=output,
@@ -516,6 +499,19 @@ def time_command(
516
499
  ctx.duration = ctx.parse_timecode(duration)
517
500
  if ctx.start_time and ctx.end_time and (ctx.start_time + 1) > ctx.end_time:
518
501
  raise click.BadParameter("-e/--end time must be greater than -s/--start")
502
+ # Reject start times past EOF before any backend seeks. OpenCV/PyAV previously
503
+ # continued and produced inconsistent errors; MoviePy raised SeekError (#380).
504
+ video_duration = ctx.video_stream.duration if ctx.video_stream is not None else None
505
+ if (
506
+ ctx.start_time is not None
507
+ and video_duration is not None
508
+ and video_duration > 0
509
+ and ctx.start_time >= video_duration
510
+ ):
511
+ raise click.BadParameter(
512
+ "start time is beyond the end of the video",
513
+ param_hint="-s/--start",
514
+ )
519
515
 
520
516
 
521
517
  DETECT_CONTENT_HELP = """Find fast cuts using differences in HSL (filtered).
@@ -780,6 +776,15 @@ Examples:
780
776
  USER_CONFIG.get_help_string("detect-threshold", "add-last-scene")
781
777
  ),
782
778
  )
779
+ @click.option(
780
+ "--min-out-length",
781
+ metavar="TIMECODE",
782
+ type=click.STRING,
783
+ default=None,
784
+ help="Minimum time spent faded out before a cut is allowed. Ignores shorter fades even when the minimum scene length is met. Also applies to a final fade-out, including the last processed frame. Zero allows fades of any duration. TIMECODE can be specified in frames (4), seconds with an `s` suffix (0.4s), or timecode (00:00:00.400).{}".format(
785
+ USER_CONFIG.get_help_string("detect-threshold", "min-out-length")
786
+ ),
787
+ )
783
788
  @click.option(
784
789
  "--min-scene-len",
785
790
  "-m",
@@ -800,6 +805,7 @@ def detect_threshold_command(
800
805
  fade_bias: float | None,
801
806
  add_last_scene: bool,
802
807
  min_scene_len: str | None,
808
+ min_out_length: str | None,
803
809
  ):
804
810
  ctx = ctx.obj
805
811
  assert isinstance(ctx, CliContext)
@@ -808,6 +814,7 @@ def detect_threshold_command(
808
814
  fade_bias=fade_bias,
809
815
  add_last_scene=add_last_scene,
810
816
  min_scene_len=min_scene_len,
817
+ min_out_length=min_out_length,
811
818
  )
812
819
  ctx.add_detector(ThresholdDetector, detector_args)
813
820
 
@@ -1714,6 +1721,48 @@ def save_qp_command(
1714
1721
  ctx.add_command(cli_commands.save_qp, save_qp_args)
1715
1722
 
1716
1723
 
1724
+ SAVE_KEYFRAMES_HELP = """Save detected cuts using keyframe format v1.
1725
+
1726
+ Frame numbers are currently approximate for variable framerate (VFR) video.
1727
+ """
1728
+
1729
+
1730
+ @click.command("save-keyframes", cls=Command, help=SAVE_KEYFRAMES_HELP)
1731
+ @click.option(
1732
+ "--filename",
1733
+ "-f",
1734
+ metavar="NAME",
1735
+ default=None,
1736
+ type=click.STRING,
1737
+ help="Filename format to use.{}".format(
1738
+ USER_CONFIG.get_help_string("save-keyframes", "filename")
1739
+ ),
1740
+ )
1741
+ @click.option(
1742
+ "--output",
1743
+ "-o",
1744
+ metavar="DIR",
1745
+ type=click.Path(exists=False, dir_okay=True, writable=True, resolve_path=False),
1746
+ help="Output directory to save keyframes to. Overrides global option -o/--output.{}".format(
1747
+ USER_CONFIG.get_help_string("save-keyframes", "output", show_default=False)
1748
+ ),
1749
+ )
1750
+ @click.pass_context
1751
+ def save_keyframes_command(
1752
+ ctx: click.Context,
1753
+ filename: str | None,
1754
+ output: str | None,
1755
+ ):
1756
+ ctx = ctx.obj
1757
+ assert isinstance(ctx, CliContext)
1758
+
1759
+ save_keyframes_args = {
1760
+ "filename": ctx.config.get_value("save-keyframes", "filename", filename),
1761
+ "output": ctx.config.get_value("save-keyframes", "output", output),
1762
+ }
1763
+ ctx.add_command(cli_commands.save_keyframes, save_keyframes_args)
1764
+
1765
+
1717
1766
  SAVE_FCP_HELP = """Save cuts in Final Cut Pro XML format (FCP7 xmeml or FCPX)."""
1718
1767
 
1719
1768
 
@@ -1858,6 +1907,7 @@ scenedetect.add_command(save_edl_command)
1858
1907
  scenedetect.add_command(save_html_command)
1859
1908
  scenedetect.add_command(save_images_command)
1860
1909
  scenedetect.add_command(save_qp_command)
1910
+ scenedetect.add_command(save_keyframes_command)
1861
1911
  scenedetect.add_command(save_fcp_command)
1862
1912
  scenedetect.add_command(save_otio_command)
1863
1913
  scenedetect.add_command(split_video_command)
@@ -102,6 +102,29 @@ def save_qp(
102
102
  logger.info(f"QP file written to: {qp_path}")
103
103
 
104
104
 
105
+ def save_keyframes(
106
+ context: CliContext, scenes: SceneList, cuts: CutList, output: str, filename: str
107
+ ):
108
+ """Handler for the `save-keyframes` command."""
109
+ del scenes # We only use cuts for this handler.
110
+ assert context.video_stream is not None
111
+ keyframes_path = get_and_create_path(
112
+ Template(filename).safe_substitute(VIDEO_NAME=context.video_stream.name),
113
+ output,
114
+ )
115
+ with open(keyframes_path, "w") as keyframes_file:
116
+ keyframes_file.write("# keyframe format v1\n")
117
+ # The keyframe format v1 specification includes an FPS field, but Aegisub does not
118
+ # use it and documents `0` as the conventional value:
119
+ # https://aegisub.org/docs/latest/video/#keyframe-file-specification
120
+ keyframes_file.write("fps 0\n")
121
+ keyframes_file.write("0\n")
122
+ # TODO(https://scenedetect.com/issues/569): Frame numbers are approximate for VFR
123
+ # input until exact presentation-frame ordinals are tracked.
124
+ keyframes_file.writelines(f"{cut.frame_num}\n" for cut in cuts)
125
+ logger.info(f"Keyframes written to: {keyframes_path}")
126
+
127
+
105
128
  def list_scenes(
106
129
  context: CliContext,
107
130
  scenes: SceneList,
@@ -379,17 +379,18 @@ CONFIG_MAP: ConfigDict = {
379
379
  "detect-hash": {
380
380
  "min-scene-len": TimecodeValue(0),
381
381
  "lowpass": RangeValue(2, min_val=1, max_val=256),
382
- "size": RangeValue(16, min_val=1, max_val=256),
383
- "threshold": RangeValue(0.395, min_val=0.0, max_val=1.0),
382
+ "size": RangeValue(8, min_val=1, max_val=256),
383
+ "threshold": RangeValue(0.35, min_val=0.0, max_val=1.0),
384
384
  },
385
385
  "detect-hist": {
386
386
  "min-scene-len": TimecodeValue(0),
387
- "threshold": RangeValue(0.05, min_val=0.0, max_val=1.0),
388
- "bins": RangeValue(256, min_val=1, max_val=256),
387
+ "threshold": RangeValue(0.20, min_val=0.0, max_val=1.0),
388
+ "bins": RangeValue(128, min_val=1, max_val=256),
389
389
  },
390
390
  "detect-threshold": {
391
391
  "add-last-scene": True,
392
392
  "fade-bias": RangeValue(0, min_val=-100.0, max_val=100.0),
393
+ "min-out-length": TimecodeValue("0.1s"),
393
394
  "min-scene-len": TimecodeValue(0),
394
395
  "threshold": RangeValue(12.0, min_val=0.0, max_val=255.0),
395
396
  },
@@ -460,6 +461,10 @@ CONFIG_MAP: ConfigDict = {
460
461
  "filename": "$VIDEO_NAME.qp",
461
462
  "output": None,
462
463
  },
464
+ "save-keyframes": {
465
+ "filename": "$VIDEO_NAME-keyframes.txt",
466
+ "output": None,
467
+ },
463
468
  "save-fcp": {
464
469
  "format": FcpFormat.FCPX,
465
470
  "filename": "$VIDEO_NAME.xml",
@@ -405,6 +405,7 @@ class CliContext:
405
405
  fade_bias: float | None = None,
406
406
  add_last_scene: bool | None = None,
407
407
  min_scene_len: str | None = None,
408
+ min_out_length: str | None = None,
408
409
  ) -> dict[str, ty.Any]:
409
410
  """Handle detect-threshold command options and return args to construct one with."""
410
411
 
@@ -415,6 +416,9 @@ class CliContext:
415
416
  or self.config.get_value("detect-threshold", "add-last-scene"),
416
417
  "fade_bias": self.config.get_value("detect-threshold", "fade-bias", fade_bias),
417
418
  "min_scene_len": min_scene_len_frames,
419
+ "min_out_length": self.parse_timecode(
420
+ self.config.get_value("detect-threshold", "min-out-length", min_out_length)
421
+ ),
418
422
  "threshold": self.config.get_value("detect-threshold", "threshold", threshold),
419
423
  }
420
424
 
@@ -131,7 +131,6 @@ def _detect(context: CliContext) -> tuple[SceneList, CutList] | None:
131
131
  )
132
132
 
133
133
  # Handle case where video failure is most likely due to multiple audio tracks (#179).
134
- # TODO(https://scenedetect.com/issues/380): Ensure this does not erroneusly fire.
135
134
  if num_frames <= 0 and isinstance(context.video_stream, VideoStreamCv2):
136
135
  logger.critical(
137
136
  "Failed to read any frames from video file. This could be caused by the video"