Enable editing multiple videos quickly by running croppa on a folder
This commit is contained in:
127
croppa/main.py
127
croppa/main.py
@@ -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'}
|
||||
|
||||
def __init__(self, path: str):
|
||||
self.path = Path(path)
|
||||
|
||||
if not self.cap.isOpened():
|
||||
raise ValueError(f"Could not open video file: {video_path}")
|
||||
# Video file management
|
||||
self.video_files = []
|
||||
self.current_video_index = 0
|
||||
|
||||
# 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))
|
||||
# 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}")
|
||||
|
||||
# Playback state
|
||||
self.current_frame = 0
|
||||
self.is_playing = False
|
||||
self.playback_speed = 1.0
|
||||
self.current_display_frame = None
|
||||
# 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:
|
||||
|
Reference in New Issue
Block a user