Created
July 24, 2026 18:14
-
-
Save ramiabraham/4aa0fa85f2121e4387ae6777526ef647 to your computer and use it in GitHub Desktop.
A script using pyscenedetect to detect scene cuts in mp4 video files within any directory, and split scenes to separate files. Uses adaptive detection method.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # | |
| # extract_scenes.sh | |
| # | |
| # Batch-detect scene cuts in a directory (and all its subdirectories) of | |
| # .mp4 files using PySceneDetect, and save the results (split clips + scene | |
| # list CSVs) to an output directory. | |
| # | |
| # Requirements: | |
| # pip install scenedetect[opencv] | |
| # ffmpeg must be installed and on PATH (used by scenedetect for splitting) | |
| # | |
| # Usage: | |
| # ./extract_scenes.sh <input_dir> <output_dir> [detector] [threshold] | |
| # | |
| # Arguments: | |
| # input_dir Directory to search recursively for .mp4 files (required) | |
| # output_dir Directory where results will be written (required) | |
| # detector Scene detector to use: content, threshold, adaptive (default: adaptive) | |
| # threshold Detector sensitivity threshold (default: 4.0) | |
| # | |
| # Output: | |
| # The input directory's subfolder structure is mirrored under output_dir. | |
| # For each input video "sub/path/name.mp4", creates: | |
| # <output_dir>/sub/path/name/name-Scene-001.mp4, name-Scene-002.mp4, ... | |
| # <output_dir>/sub/path/name/name-Scenes.csv (detected scene cut timecodes) | |
| # | |
| # Example: | |
| # ./extract_scenes.sh ./videos ./scenes content 30 | |
| set -euo pipefail | |
| # ---- Argument parsing ------------------------------------------------- | |
| if [[ $# -lt 2 ]]; then | |
| echo "Usage: $0 <input_dir> <output_dir> [detector] [threshold]" >&2 | |
| echo " detector: content | threshold | adaptive (default: adaptive)" >&2 | |
| echo " threshold: numeric sensitivity value (default: 4.0)" >&2 | |
| exit 1 | |
| fi | |
| INPUT_DIR="$1" | |
| OUTPUT_DIR="$2" | |
| DETECTOR="${3:-adaptive}" | |
| THRESHOLD="${4:-4.0}" | |
| # ---- Sanity checks ------------------------------------------------------ | |
| if [[ ! -d "$INPUT_DIR" ]]; then | |
| echo "Error: input directory '$INPUT_DIR' does not exist." >&2 | |
| exit 1 | |
| fi | |
| if ! command -v scenedetect >/dev/null 2>&1; then | |
| echo "Error: 'scenedetect' command not found." >&2 | |
| echo "Install it with: pip install scenedetect[opencv]" >&2 | |
| exit 1 | |
| fi | |
| if ! command -v ffmpeg >/dev/null 2>&1; then | |
| echo "Error: 'ffmpeg' not found on PATH. Install ffmpeg to enable video splitting." >&2 | |
| exit 1 | |
| fi | |
| case "$DETECTOR" in | |
| content|threshold|adaptive) ;; | |
| *) | |
| echo "Error: unknown detector '$DETECTOR'. Use content, threshold, or adaptive." >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| mkdir -p "$OUTPUT_DIR" | |
| # ---- Collect input files (recursively, including subdirectories) -------- | |
| mp4_files=() | |
| while IFS= read -r -d '' f; do | |
| base="$(basename "$f")" | |
| if [[ "$base" == ._* ]]; then | |
| continue | |
| fi | |
| mp4_files+=("$f") | |
| done < <(find "$INPUT_DIR" -type f -iname '*.mp4' -print0 | sort -z) | |
| if [[ ${#mp4_files[@]} -eq 0 ]]; then | |
| echo "No .mp4 files found in '$INPUT_DIR' or its subdirectories." >&2 | |
| exit 1 | |
| fi | |
| echo "Found ${#mp4_files[@]} .mp4 file(s) in '$INPUT_DIR' (including subdirectories)." | |
| echo "Detector: $DETECTOR Threshold: $THRESHOLD" | |
| echo "Output directory: $OUTPUT_DIR" | |
| echo "-----------------------------------------------------" | |
| fail_count=0 | |
| # ---- Process each file --------------------------------------------------- | |
| for filepath in "${mp4_files[@]}"; do | |
| filename="$(basename "$filepath")" | |
| name="${filename%.*}" | |
| # Preserve the input's relative subdirectory structure in the output dir, | |
| # so files with the same name in different subfolders don't collide. | |
| rel_dir="$(dirname "$filepath")" | |
| rel_dir="${rel_dir#"$INPUT_DIR"}" | |
| rel_dir="${rel_dir#/}" | |
| video_out_dir="$OUTPUT_DIR/$rel_dir/$name" | |
| mkdir -p "$video_out_dir" | |
| echo "" | |
| echo "Processing: ${rel_dir:+$rel_dir/}$filename" | |
| echo " -> $video_out_dir" | |
| if scenedetect \ | |
| -i "$filepath" \ | |
| -o "$video_out_dir" \ | |
| detect-"$DETECTOR" -t "$THRESHOLD" \ | |
| list-scenes \ | |
| split-video ; then | |
| echo " Done: $filename" | |
| else | |
| echo " FAILED: $filename" >&2 | |
| fail_count=$((fail_count + 1)) | |
| fi | |
| done | |
| echo "" | |
| echo "-----------------------------------------------------" | |
| if [[ $fail_count -gt 0 ]]; then | |
| echo "Completed with $fail_count failure(s). See messages above." | |
| exit 1 | |
| else | |
| echo "All videos processed successfully. Results in '$OUTPUT_DIR'." | |
| fi |
Author
Author
The adaptive detection method with a threshold of 4.0 works especially well for scene detection in highly dynamic video content containing CGI, such as sci-fi like Star Trek TNG.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The python library pyscenedetect must be installed, including dependencies such as
ffmpeg. I recommend installingpipas well, egcurl https://bootstrap.pypa.io/get-pip.py | python3