├── requirements.txt ├── video.mp4 ├── README.md └── scene_splitter.py /requirements.txt: -------------------------------------------------------------------------------- 1 | opencv-python 2 | openai 3 | termcolor 4 | numpy 5 | - difflib -------------------------------------------------------------------------------- /video.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echohive42/video-scene-splitter/HEAD/video.mp4 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Video Scene Splitter 2 | 3 | A Python tool that automatically detects and splits videos into separate scenes using OpenAI's GPT-4 Vision API and OpenCV. 4 | 5 | ## Features 6 | - Combines GPT-4 Vision and OpenCV for accurate scene detection 7 | - Splits video into separate files for each scene 8 | - Generates detailed scene analysis report 9 | - Supports MP4 video format 10 | 11 | ## ❤️ Support & Get 400+ AI Projects 12 | 13 | This is one of 400+ fascinating projects in my collection! **[Support me on Patreon](https://www.patreon.com/c/echohive42/membership)** to get: 14 | 15 | - 🎯 Access to 400+ AI projects (and growing weekly!) 16 | - 📥 Full source code & detailed explanations 17 | - 📚 1000x Cursor Course 18 | - 🎓 Live coding sessions & AMAs 19 | - 💬 1-on-1 consultations (higher tiers) 20 | - 🎁 Exclusive discounts on AI tools 21 | 22 | ## 🔧 Prerequisites 23 | 24 | ## Requirements 25 | - Python 3.6+ 26 | - OpenAI API key 27 | - Input video file (video.mp4) 28 | 29 | ## Installation 30 | 1. Clone the repository 31 | 2. Install dependencies: 32 | ```bash 33 | pip install -r requirements.txt 34 | ``` 35 | 36 | ## Setup 37 | Export your OpenAI API key: 38 | ```bash 39 | export OPENAI_API_KEY='your-api-key' 40 | ``` 41 | 42 | ## Usage 43 | 1. Place your video file as `video.mp4` in the project directory 44 | 2. Run the script: 45 | ```bash 46 | python scene_splitter.py 47 | ``` 48 | 49 | ## Output 50 | - `scenes/` directory containing individual scene videos 51 | - `scene_analysis.txt` with detailed scene information 52 | 53 | ## Configuration 54 | Adjust these constants in `scene_splitter.py`: 55 | - `FRAMES_TO_SKIP`: Frames to skip between analyses (default: 15) 56 | - `FRAMES_PER_BATCH`: Frames per API call (default: 4) 57 | - `threshold` in `detect_scene_change()`: Scene change sensitivity (default: 30.0) 58 | 59 | ## How It Works 60 | 1. Extracts frames at regular intervals 61 | 2. Analyzes frames using GPT-4 Vision for high-level scene changes 62 | 3. Uses OpenCV for low-level motion detection 63 | 4. Combines both analyses for accurate scene detection 64 | 5. Splits video at detected scene changes 65 | 66 | ## Notes 67 | - API costs depend on video length and frame analysis frequency 68 | - Processing time varies with video length 69 | - Requires sufficient storage for temporary frames and output videos -------------------------------------------------------------------------------- /scene_splitter.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import os 3 | from openai import OpenAI 4 | import base64 5 | from termcolor import colored 6 | import numpy as np 7 | from datetime import timedelta 8 | from difflib import SequenceMatcher 9 | 10 | # Constants 11 | FRAMES_TO_SKIP = 15 # Reduced to catch more subtle changes 12 | FRAMES_PER_BATCH = 4 13 | VIDEO_PATH = "video.mp4" 14 | API_KEY = os.getenv("OPENAI_API_KEY") 15 | TEMP_FRAME_DIR = "temp_frames" 16 | OUTPUT_DIR = "scenes" 17 | MODEL = "gpt-4o-mini" 18 | SIMILARITY_THRESHOLD = 0.7 # Threshold for scene similarity 19 | 20 | # Create necessary directories 21 | os.makedirs(TEMP_FRAME_DIR, exist_ok=True) 22 | os.makedirs(OUTPUT_DIR, exist_ok=True) 23 | 24 | try: 25 | client = OpenAI(api_key=API_KEY) 26 | print(colored("OpenAI client initialized successfully", "green")) 27 | except Exception as e: 28 | print(colored(f"Error initializing OpenAI client: {str(e)}", "red")) 29 | exit(1) 30 | 31 | def encode_image_to_base64(image_path): 32 | try: 33 | with open(image_path, "rb") as image_file: 34 | return base64.b64encode(image_file.read()).decode('utf-8') 35 | except Exception as e: 36 | print(colored(f"Error encoding image: {str(e)}", "red")) 37 | return None 38 | 39 | def calculate_similarity(text1, text2): 40 | return SequenceMatcher(None, text1, text2).ratio() 41 | 42 | def detect_scene_change(frame1, frame2, threshold=30.0): 43 | """Detect scene change using frame difference""" 44 | try: 45 | # Convert frames to grayscale 46 | gray1 = cv2.cvtColor(frame1, cv2.COLOR_BGR2GRAY) 47 | gray2 = cv2.cvtColor(frame2, cv2.COLOR_BGR2GRAY) 48 | 49 | # Calculate frame difference 50 | score = np.mean(np.abs(gray1 - gray2)) 51 | return score > threshold 52 | 53 | except Exception as e: 54 | print(colored(f"Error in scene change detection: {str(e)}", "red")) 55 | return False 56 | 57 | def analyze_frames_batch(frame_paths, timestamps): 58 | try: 59 | content = [ 60 | { 61 | "type": "text", 62 | "text": "Analyze these consecutive frames for scene changes. A scene change occurs when there is:" 63 | "\n1. A different location or setting" 64 | "\n2. A significant change in camera angle (not minor movements)" 65 | "\n3. A completely different action or subject" 66 | "\n4. A major lighting change" 67 | "\nMinor changes in the same scene should NOT be counted as scene changes." 68 | "\nReturn ONLY frame numbers where definite scene changes occur, or 'None' if no changes." 69 | "\nFormat: Scene changes: [frame numbers or None]" 70 | } 71 | ] 72 | 73 | # Load frames for OpenCV analysis 74 | frames = [] 75 | for frame_path in frame_paths: 76 | frame = cv2.imread(frame_path) 77 | frames.append(frame) 78 | frame_base64 = encode_image_to_base64(frame_path) 79 | if frame_base64: 80 | content.append({ 81 | "type": "image_url", 82 | "image_url": { 83 | "url": f"data:image/jpeg;base64,{frame_base64}" 84 | } 85 | }) 86 | 87 | # Get GPT's analysis 88 | response = client.chat.completions.create( 89 | model=MODEL, 90 | messages=[{"role": "user", "content": content}], 91 | max_tokens=100 # Reduced since we only need scene change numbers 92 | ) 93 | gpt_analysis = response.choices[0].message.content 94 | 95 | # Combine GPT analysis with OpenCV detection 96 | scene_changes = set() 97 | 98 | # Parse GPT's scene changes 99 | if "Scene changes:" in gpt_analysis: 100 | changes_line = gpt_analysis.split("Scene changes:")[-1].strip() 101 | if changes_line.lower() != "none": 102 | gpt_changes = [int(x.strip()) for x in changes_line.split(',') if x.strip().isdigit()] 103 | scene_changes.update(gpt_changes) 104 | 105 | # Add OpenCV-detected changes 106 | for i in range(len(frames)-1): 107 | if detect_scene_change(frames[i], frames[i+1]): 108 | scene_changes.add(i+1) 109 | 110 | # Sort the combined scene changes 111 | scene_changes = sorted(list(scene_changes)) 112 | 113 | return f"Scene changes: {scene_changes if scene_changes else 'None'}" 114 | except Exception as e: 115 | print(colored(f"Error analyzing frames with GPT-4 Vision: {str(e)}", "red")) 116 | return None 117 | 118 | def extract_scene(video_capture, start_frame, end_frame, scene_number): 119 | try: 120 | output_path = os.path.join(OUTPUT_DIR, f"scene_{scene_number}.mp4") 121 | 122 | # Get video properties 123 | fps = video_capture.get(cv2.CAP_PROP_FPS) 124 | width = int(video_capture.get(cv2.CAP_PROP_FRAME_WIDTH)) 125 | height = int(video_capture.get(cv2.CAP_PROP_FRAME_HEIGHT)) 126 | 127 | # Create video writer 128 | fourcc = cv2.VideoWriter_fourcc(*'mp4v') 129 | out = cv2.VideoWriter(output_path, fourcc, fps, (width, height)) 130 | 131 | # Set frame position to start 132 | video_capture.set(cv2.CAP_PROP_POS_FRAMES, start_frame) 133 | 134 | print(colored(f"Extracting scene {scene_number} (frames {start_frame} to {end_frame})", "cyan")) 135 | 136 | # Read and write frames 137 | for _ in range(end_frame - start_frame): 138 | ret, frame = video_capture.read() 139 | if not ret: 140 | break 141 | out.write(frame) 142 | 143 | out.release() 144 | print(colored(f"Scene {scene_number} saved to {output_path}", "green")) 145 | return output_path 146 | except Exception as e: 147 | print(colored(f"Error extracting scene: {str(e)}", "red")) 148 | return None 149 | 150 | def main(): 151 | try: 152 | cap = cv2.VideoCapture(VIDEO_PATH) 153 | if not cap.isOpened(): 154 | raise Exception("Error opening video file") 155 | 156 | print(colored("Successfully opened video file", "green")) 157 | 158 | fps = cap.get(cv2.CAP_PROP_FPS) 159 | total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) 160 | 161 | scenes = [] 162 | frame_count = 0 163 | batch_frames = [] 164 | batch_timestamps = [] 165 | current_scene_start = 0 166 | scene_number = 1 167 | 168 | print(colored("Starting scene analysis...", "cyan")) 169 | 170 | while frame_count < total_frames: 171 | ret, frame = cap.read() 172 | if not ret: 173 | break 174 | 175 | if frame_count % FRAMES_TO_SKIP == 0: 176 | frame_path = os.path.join(TEMP_FRAME_DIR, f"frame_{frame_count}.jpg") 177 | cv2.imwrite(frame_path, frame) 178 | timestamp = timedelta(seconds=frame_count/fps) 179 | 180 | batch_frames.append(frame_path) 181 | batch_timestamps.append(timestamp) 182 | 183 | if len(batch_frames) == FRAMES_PER_BATCH: 184 | print(colored(f"Analyzing batch at timestamp {timestamp}", "cyan")) 185 | analysis = analyze_frames_batch(batch_frames, batch_timestamps) 186 | 187 | if analysis: 188 | # Extract scene changes from analysis 189 | scene_changes = [] 190 | if "Scene changes:" in analysis: 191 | changes_line = analysis.split("Scene changes:")[-1].strip() 192 | if changes_line.lower() != "none": 193 | scene_changes = [int(x.strip()) for x in changes_line.split(',') if x.strip().isdigit()] 194 | 195 | if scene_changes: 196 | for change in scene_changes: 197 | # Calculate actual frame number for the scene change 198 | scene_frame = frame_count - (FRAMES_PER_BATCH - change) * FRAMES_TO_SKIP 199 | 200 | # Extract the scene 201 | scene_path = extract_scene(cap, current_scene_start, scene_frame, scene_number) 202 | 203 | scenes.append({ 204 | 'scene_number': scene_number, 205 | 'start_frame': current_scene_start, 206 | 'end_frame': scene_frame, 207 | 'start_time': str(timedelta(seconds=current_scene_start/fps)), 208 | 'end_time': str(timedelta(seconds=scene_frame/fps)), 209 | 'video_path': scene_path, 210 | 'description': analysis 211 | }) 212 | 213 | current_scene_start = scene_frame 214 | scene_number += 1 215 | 216 | print(colored(f"Batch analysis: {analysis}", "yellow")) 217 | 218 | # Clean up batch frames 219 | for f in batch_frames: 220 | if os.path.exists(f): 221 | os.remove(f) 222 | batch_frames = [] 223 | batch_timestamps = [] 224 | 225 | frame_count += 1 226 | if frame_count % 100 == 0: 227 | print(colored(f"Processed {frame_count}/{total_frames} frames", "cyan")) 228 | 229 | # Extract final scene if needed 230 | if current_scene_start < total_frames - 1: 231 | scene_path = extract_scene(cap, current_scene_start, total_frames - 1, scene_number) 232 | scenes.append({ 233 | 'scene_number': scene_number, 234 | 'start_frame': current_scene_start, 235 | 'end_frame': total_frames - 1, 236 | 'start_time': str(timedelta(seconds=current_scene_start/fps)), 237 | 'end_time': str(timedelta(seconds=(total_frames - 1)/fps)), 238 | 'video_path': scene_path, 239 | 'description': "Final scene" 240 | }) 241 | 242 | # Clean up 243 | cap.release() 244 | if os.path.exists(TEMP_FRAME_DIR): 245 | for f in os.listdir(TEMP_FRAME_DIR): 246 | os.remove(os.path.join(TEMP_FRAME_DIR, f)) 247 | os.rmdir(TEMP_FRAME_DIR) 248 | 249 | # Save detailed analysis 250 | try: 251 | with open('scene_analysis.txt', 'w', encoding='utf-8') as f: 252 | for scene in scenes: 253 | f.write(f"Scene {scene['scene_number']}:\n") 254 | f.write(f"Time Range: {scene['start_time']} - {scene['end_time']}\n") 255 | f.write(f"Frame Range: {scene['start_frame']} - {scene['end_frame']}\n") 256 | f.write(f"Video File: {scene['video_path']}\n") 257 | f.write(f"Analysis:\n{scene['description']}\n") 258 | f.write("-" * 50 + "\n") 259 | print(colored("Scene analysis saved to scene_analysis.txt", "green")) 260 | except Exception as e: 261 | print(colored(f"Error saving scene analysis: {str(e)}", "red")) 262 | 263 | except Exception as e: 264 | print(colored(f"An error occurred: {str(e)}", "red")) 265 | if 'cap' in locals(): 266 | cap.release() 267 | if os.path.exists(TEMP_FRAME_DIR): 268 | for f in os.listdir(TEMP_FRAME_DIR): 269 | os.remove(os.path.join(TEMP_FRAME_DIR, f)) 270 | os.rmdir(TEMP_FRAME_DIR) 271 | 272 | if __name__ == "__main__": 273 | main() --------------------------------------------------------------------------------