Enable editing multiple videos quickly by running croppa on a folder

This commit is contained in:
2025-09-04 14:42:21 +02:00
parent c6cc249ab2
commit 28f11ab190

View File

@@ -32,24 +32,29 @@ class VideoEditor:
MAX_ZOOM = 10.0
ZOOM_INCREMENT = 0.25
def __init__(self, video_path: str):
self.video_path = Path(video_path)
self.cap = cv2.VideoCapture(str(self.video_path))
# Supported video extensions
VIDEO_EXTENSIONS = {'.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv', '.webm', '.m4v'}
if not self.cap.isOpened():
raise ValueError(f"Could not open video file: {video_path}")
def __init__(self, path: str):
self.path = Path(path)
# Video properties
self.total_frames = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT))
self.fps = self.cap.get(cv2.CAP_PROP_FPS)
self.frame_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
self.frame_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
# Video file management
self.video_files = []
self.current_video_index = 0
# Playback state
self.current_frame = 0
self.is_playing = False
self.playback_speed = 1.0
self.current_display_frame = None
# Determine if path is file or directory
if self.path.is_file():
self.video_files = [self.path]
elif self.path.is_dir():
# Load all video files from directory
self.video_files = self._get_video_files_from_directory(self.path)
if not self.video_files:
raise ValueError(f"No video files found in directory: {path}")
else:
raise ValueError(f"Path does not exist: {path}")
# Initialize with first video
self._load_video(self.video_files[0])
# Mouse and keyboard interaction
self.mouse_dragging = False
@@ -81,6 +86,65 @@ class VideoEditor:
# Display offset for panning when zoomed
self.display_offset = [0, 0]
def _get_video_files_from_directory(self, directory: Path) -> List[Path]:
"""Get all video files from a directory, sorted by name"""
video_files = []
for file_path in directory.iterdir():
if file_path.is_file() and file_path.suffix.lower() in self.VIDEO_EXTENSIONS:
video_files.append(file_path)
return sorted(video_files)
def _load_video(self, video_path: Path):
"""Load a video file and initialize video properties"""
if hasattr(self, 'cap') and self.cap:
self.cap.release()
self.video_path = video_path
self.cap = cv2.VideoCapture(str(self.video_path))
if not self.cap.isOpened():
raise ValueError(f"Could not open video file: {video_path}")
# Video properties
self.total_frames = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT))
self.fps = self.cap.get(cv2.CAP_PROP_FPS)
self.frame_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
self.frame_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
# Reset playback state for new video
self.current_frame = 0
self.is_playing = False
self.playback_speed = 1.0
self.current_display_frame = None
# Reset crop, zoom, and cut settings for new video
self.crop_rect = None
self.crop_history = []
self.zoom_factor = 1.0
self.zoom_center = None
self.cut_start_frame = None
self.cut_end_frame = None
self.display_offset = [0, 0]
print(f"Loaded video: {self.video_path.name} ({self.current_video_index + 1}/{len(self.video_files)})")
def switch_to_video(self, index: int):
"""Switch to a specific video by index"""
if 0 <= index < len(self.video_files):
self.current_video_index = index
self._load_video(self.video_files[index])
self.load_current_frame()
def next_video(self):
"""Switch to the next video"""
next_index = (self.current_video_index + 1) % len(self.video_files)
self.switch_to_video(next_index)
def previous_video(self):
"""Switch to the previous video"""
prev_index = (self.current_video_index - 1) % len(self.video_files)
self.switch_to_video(prev_index)
def load_current_frame(self) -> bool:
"""Load the current frame into display cache"""
self.cap.set(cv2.CAP_PROP_POS_FRAMES, self.current_frame)
@@ -273,17 +337,27 @@ class VideoEditor:
cv2.putText(canvas, info_text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
cv2.putText(canvas, info_text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 0), 1)
# Add video navigation info
if len(self.video_files) > 1:
video_text = f"Video: {self.current_video_index + 1}/{len(self.video_files)} - {self.video_path.name}"
cv2.putText(canvas, video_text, (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2)
cv2.putText(canvas, video_text, (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 1)
y_offset = 90
else:
y_offset = 60
# Add crop info
if self.crop_rect:
crop_text = f"Crop: {int(self.crop_rect[0])},{int(self.crop_rect[1])} {int(self.crop_rect[2])}x{int(self.crop_rect[3])}"
cv2.putText(canvas, crop_text, (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2)
cv2.putText(canvas, crop_text, (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 1)
cv2.putText(canvas, crop_text, (10, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2)
cv2.putText(canvas, crop_text, (10, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 1)
y_offset += 30
# Add cut info
if self.cut_start_frame is not None or self.cut_end_frame is not None:
cut_text = f"Cut: {self.cut_start_frame or '?'} - {self.cut_end_frame or '?'}"
cv2.putText(canvas, cut_text, (10, 90), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2)
cv2.putText(canvas, cut_text, (10, 90), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 1)
cv2.putText(canvas, cut_text, (10, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2)
cv2.putText(canvas, cut_text, (10, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 1)
# Draw timeline
self.draw_timeline(canvas)
@@ -531,6 +605,9 @@ class VideoEditor:
print(" Ctrl+Scroll: Zoom in/out")
print(" 1: Set cut start point")
print(" 2: Set cut end point")
if len(self.video_files) > 1:
print(" N: Next video")
print(" n: Previous video")
print(" Enter: Render video")
print(" Q/ESC: Quit")
print()
@@ -571,6 +648,12 @@ class VideoEditor:
elif key == ord('2'):
self.cut_end_frame = self.current_frame
print(f"Set cut end at frame {self.current_frame}")
elif key == ord('n'):
if len(self.video_files) > 1:
self.previous_video()
elif key == ord('N'):
if len(self.video_files) > 1:
self.next_video()
elif key == 13: # Enter
output_name = f"{self.video_path.stem}_edited.mp4"
self.render_video(str(self.video_path.parent / output_name))
@@ -585,12 +668,12 @@ class VideoEditor:
def main():
parser = argparse.ArgumentParser(description="Fast Video Editor - Crop, Zoom, and Cut videos")
parser.add_argument("video", help="Path to video file")
parser.add_argument("video", help="Path to video file or directory containing videos")
args = parser.parse_args()
if not os.path.isfile(args.video):
print(f"Error: {args.video} is not a valid file")
if not os.path.exists(args.video):
print(f"Error: {args.video} does not exist")
sys.exit(1)
try: