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
|
MAX_ZOOM = 10.0
|
||||||
ZOOM_INCREMENT = 0.25
|
ZOOM_INCREMENT = 0.25
|
||||||
|
|
||||||
def __init__(self, video_path: str):
|
# Supported video extensions
|
||||||
self.video_path = Path(video_path)
|
VIDEO_EXTENSIONS = {'.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv', '.webm', '.m4v'}
|
||||||
self.cap = cv2.VideoCapture(str(self.video_path))
|
|
||||||
|
def __init__(self, path: str):
|
||||||
|
self.path = Path(path)
|
||||||
|
|
||||||
if not self.cap.isOpened():
|
# Video file management
|
||||||
raise ValueError(f"Could not open video file: {video_path}")
|
self.video_files = []
|
||||||
|
self.current_video_index = 0
|
||||||
|
|
||||||
# Video properties
|
# Determine if path is file or directory
|
||||||
self.total_frames = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
if self.path.is_file():
|
||||||
self.fps = self.cap.get(cv2.CAP_PROP_FPS)
|
self.video_files = [self.path]
|
||||||
self.frame_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
elif self.path.is_dir():
|
||||||
self.frame_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
# 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
|
# Initialize with first video
|
||||||
self.current_frame = 0
|
self._load_video(self.video_files[0])
|
||||||
self.is_playing = False
|
|
||||||
self.playback_speed = 1.0
|
|
||||||
self.current_display_frame = None
|
|
||||||
|
|
||||||
# Mouse and keyboard interaction
|
# Mouse and keyboard interaction
|
||||||
self.mouse_dragging = False
|
self.mouse_dragging = False
|
||||||
@@ -81,6 +86,65 @@ class VideoEditor:
|
|||||||
# Display offset for panning when zoomed
|
# Display offset for panning when zoomed
|
||||||
self.display_offset = [0, 0]
|
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:
|
def load_current_frame(self) -> bool:
|
||||||
"""Load the current frame into display cache"""
|
"""Load the current frame into display cache"""
|
||||||
self.cap.set(cv2.CAP_PROP_POS_FRAMES, self.current_frame)
|
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, (255, 255, 255), 2)
|
||||||
cv2.putText(canvas, info_text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 0), 1)
|
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
|
# Add crop info
|
||||||
if self.crop_rect:
|
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])}"
|
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, y_offset), 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, (0, 0, 0), 1)
|
||||||
|
y_offset += 30
|
||||||
|
|
||||||
# Add cut info
|
# Add cut info
|
||||||
if self.cut_start_frame is not None or self.cut_end_frame is not None:
|
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 '?'}"
|
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, y_offset), 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, (0, 0, 0), 1)
|
||||||
|
|
||||||
# Draw timeline
|
# Draw timeline
|
||||||
self.draw_timeline(canvas)
|
self.draw_timeline(canvas)
|
||||||
@@ -531,6 +605,9 @@ class VideoEditor:
|
|||||||
print(" Ctrl+Scroll: Zoom in/out")
|
print(" Ctrl+Scroll: Zoom in/out")
|
||||||
print(" 1: Set cut start point")
|
print(" 1: Set cut start point")
|
||||||
print(" 2: Set cut end 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(" Enter: Render video")
|
||||||
print(" Q/ESC: Quit")
|
print(" Q/ESC: Quit")
|
||||||
print()
|
print()
|
||||||
@@ -571,6 +648,12 @@ class VideoEditor:
|
|||||||
elif key == ord('2'):
|
elif key == ord('2'):
|
||||||
self.cut_end_frame = self.current_frame
|
self.cut_end_frame = self.current_frame
|
||||||
print(f"Set cut end at 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
|
elif key == 13: # Enter
|
||||||
output_name = f"{self.video_path.stem}_edited.mp4"
|
output_name = f"{self.video_path.stem}_edited.mp4"
|
||||||
self.render_video(str(self.video_path.parent / output_name))
|
self.render_video(str(self.video_path.parent / output_name))
|
||||||
@@ -585,12 +668,12 @@ class VideoEditor:
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description="Fast Video Editor - Crop, Zoom, and Cut videos")
|
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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if not os.path.isfile(args.video):
|
if not os.path.exists(args.video):
|
||||||
print(f"Error: {args.video} is not a valid file")
|
print(f"Error: {args.video} does not exist")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
Reference in New Issue
Block a user