Compare commits
8 Commits
0fb591d0b3
...
3a8f8d26d3
Author | SHA1 | Date | |
---|---|---|---|
3a8f8d26d3 | |||
56d6e04b48 | |||
6efbfa0c11 | |||
d1b26fe8b4 | |||
1da8efc528 | |||
0dbf82f76b | |||
4651ba51f1 | |||
2961fe088d |
163
croppa/main.py
163
croppa/main.py
@@ -206,11 +206,12 @@ class VideoEditor:
|
|||||||
# Restore state values
|
# Restore state values
|
||||||
if 'current_frame' in state:
|
if 'current_frame' in state:
|
||||||
self.current_frame = state['current_frame']
|
self.current_frame = state['current_frame']
|
||||||
if 'crop_rect' in state and state['crop_rect']:
|
if 'crop_rect' in state and state['crop_rect'] is not None:
|
||||||
self.crop_rect = tuple(state['crop_rect'])
|
self.crop_rect = tuple(state['crop_rect'])
|
||||||
|
print(f"DEBUG: Loaded crop_rect: {self.crop_rect}")
|
||||||
if 'zoom_factor' in state:
|
if 'zoom_factor' in state:
|
||||||
self.zoom_factor = state['zoom_factor']
|
self.zoom_factor = state['zoom_factor']
|
||||||
if 'zoom_center' in state and state['zoom_center']:
|
if 'zoom_center' in state and state['zoom_center'] is not None:
|
||||||
self.zoom_center = tuple(state['zoom_center'])
|
self.zoom_center = tuple(state['zoom_center'])
|
||||||
if 'rotation_angle' in state:
|
if 'rotation_angle' in state:
|
||||||
self.rotation_angle = state['rotation_angle']
|
self.rotation_angle = state['rotation_angle']
|
||||||
@@ -225,6 +226,14 @@ class VideoEditor:
|
|||||||
self.cut_end_frame = state['cut_end_frame']
|
self.cut_end_frame = state['cut_end_frame']
|
||||||
print(f"Restored cut_end_frame: {self.cut_end_frame}")
|
print(f"Restored cut_end_frame: {self.cut_end_frame}")
|
||||||
|
|
||||||
|
# Validate cut markers against current video length
|
||||||
|
if self.cut_start_frame is not None and self.cut_start_frame >= self.total_frames:
|
||||||
|
print(f"DEBUG: cut_start_frame {self.cut_start_frame} is beyond video length {self.total_frames}, clearing")
|
||||||
|
self.cut_start_frame = None
|
||||||
|
if self.cut_end_frame is not None and self.cut_end_frame >= self.total_frames:
|
||||||
|
print(f"DEBUG: cut_end_frame {self.cut_end_frame} is beyond video length {self.total_frames}, clearing")
|
||||||
|
self.cut_end_frame = None
|
||||||
|
|
||||||
# Calculate and show marker positions on timeline
|
# Calculate and show marker positions on timeline
|
||||||
if self.cut_start_frame is not None and self.cut_end_frame is not None:
|
if self.cut_start_frame is not None and self.cut_end_frame is not None:
|
||||||
start_progress = self.cut_start_frame / max(1, self.total_frames - 1)
|
start_progress = self.cut_start_frame / max(1, self.total_frames - 1)
|
||||||
@@ -266,33 +275,6 @@ class VideoEditor:
|
|||||||
"""Check if file is a supported media format (video or image)"""
|
"""Check if file is a supported media format (video or image)"""
|
||||||
return self._is_video_file(file_path) or self._is_image_file(file_path)
|
return self._is_video_file(file_path) or self._is_image_file(file_path)
|
||||||
|
|
||||||
def _get_next_edited_filename(self, video_path: Path) -> str:
|
|
||||||
"""Generate the next available _edited_%03d filename, or overwrite if already edited"""
|
|
||||||
directory = video_path.parent
|
|
||||||
base_name = video_path.stem
|
|
||||||
extension = video_path.suffix
|
|
||||||
|
|
||||||
# Check if the current video already contains "_edited_" in its name
|
|
||||||
if "_edited_" in base_name:
|
|
||||||
# If it's already an edited video, overwrite it instead of creating _edited_edited
|
|
||||||
return video_path.name
|
|
||||||
|
|
||||||
# Pattern to match existing edited files: basename_edited_001.ext, basename_edited_002.ext, etc.
|
|
||||||
pattern = re.compile(rf"^{re.escape(base_name)}_edited_(\d{{3}}){re.escape(extension)}$")
|
|
||||||
|
|
||||||
existing_numbers = set()
|
|
||||||
for file_path in directory.iterdir():
|
|
||||||
if file_path.is_file():
|
|
||||||
match = pattern.match(file_path.name)
|
|
||||||
if match:
|
|
||||||
existing_numbers.add(int(match.group(1)))
|
|
||||||
|
|
||||||
# Find the next available number starting from 1
|
|
||||||
next_number = 1
|
|
||||||
while next_number in existing_numbers:
|
|
||||||
next_number += 1
|
|
||||||
|
|
||||||
return f"{base_name}_edited_{next_number:03d}{extension}"
|
|
||||||
|
|
||||||
def _get_next_screenshot_filename(self, video_path: Path) -> str:
|
def _get_next_screenshot_filename(self, video_path: Path) -> str:
|
||||||
"""Generate the next available screenshot filename: video_frame_00001.jpg, video_frame_00002.jpg, etc."""
|
"""Generate the next available screenshot filename: video_frame_00001.jpg, video_frame_00002.jpg, etc."""
|
||||||
@@ -1400,6 +1382,7 @@ class VideoEditor:
|
|||||||
if self.crop_rect:
|
if self.crop_rect:
|
||||||
self.crop_history.append(self.crop_rect)
|
self.crop_history.append(self.crop_rect)
|
||||||
self.crop_rect = (original_x, original_y, original_w, original_h)
|
self.crop_rect = (original_x, original_y, original_w, original_h)
|
||||||
|
self.save_state() # Save state when crop is set
|
||||||
|
|
||||||
def seek_to_timeline_position(self, mouse_x, bar_x_start, bar_width):
|
def seek_to_timeline_position(self, mouse_x, bar_x_start, bar_width):
|
||||||
"""Seek to position based on mouse click on timeline"""
|
"""Seek to position based on mouse click on timeline"""
|
||||||
@@ -1414,6 +1397,7 @@ class VideoEditor:
|
|||||||
self.crop_rect = self.crop_history.pop()
|
self.crop_rect = self.crop_history.pop()
|
||||||
else:
|
else:
|
||||||
self.crop_rect = None
|
self.crop_rect = None
|
||||||
|
self.save_state() # Save state when crop is undone
|
||||||
|
|
||||||
def toggle_marker_looping(self):
|
def toggle_marker_looping(self):
|
||||||
"""Toggle looping between cut markers"""
|
"""Toggle looping between cut markers"""
|
||||||
@@ -1434,7 +1418,8 @@ class VideoEditor:
|
|||||||
self.seek_to_frame(self.cut_start_frame)
|
self.seek_to_frame(self.cut_start_frame)
|
||||||
else:
|
else:
|
||||||
print("Marker looping DISABLED")
|
print("Marker looping DISABLED")
|
||||||
|
|
||||||
|
self.save_state() # Save state when looping is toggled
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -1508,6 +1493,8 @@ class VideoEditor:
|
|||||||
new_x = x + amount
|
new_x = x + amount
|
||||||
new_w = w - amount
|
new_w = w - amount
|
||||||
self.crop_rect = (new_x, y, new_w, h)
|
self.crop_rect = (new_x, y, new_w, h)
|
||||||
|
|
||||||
|
self.save_state() # Save state when crop is adjusted
|
||||||
|
|
||||||
def render_video(self, output_path: str):
|
def render_video(self, output_path: str):
|
||||||
"""Render video or save image with current edits applied"""
|
"""Render video or save image with current edits applied"""
|
||||||
@@ -1690,6 +1677,9 @@ class VideoEditor:
|
|||||||
self.update_progress_bar(progress, text, fps)
|
self.update_progress_bar(progress, text, fps)
|
||||||
elif update_type == "complete":
|
elif update_type == "complete":
|
||||||
self.update_progress_bar(progress, text, fps)
|
self.update_progress_bar(progress, text, fps)
|
||||||
|
# Handle file overwrite if this was an overwrite operation
|
||||||
|
if hasattr(self, 'overwrite_temp_path') and self.overwrite_temp_path:
|
||||||
|
self._handle_overwrite_completion()
|
||||||
elif update_type == "error":
|
elif update_type == "error":
|
||||||
self.update_progress_bar(progress, text, fps)
|
self.update_progress_bar(progress, text, fps)
|
||||||
elif update_type == "cancelled":
|
elif update_type == "cancelled":
|
||||||
@@ -1700,6 +1690,44 @@ class VideoEditor:
|
|||||||
# No more updates in queue
|
# No more updates in queue
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def _handle_overwrite_completion(self):
|
||||||
|
"""Handle file replacement after successful render"""
|
||||||
|
try:
|
||||||
|
print("Replacing original file...")
|
||||||
|
# Release current video capture before replacing the file
|
||||||
|
if hasattr(self, 'cap') and self.cap:
|
||||||
|
self.cap.release()
|
||||||
|
|
||||||
|
# Replace the original file with the temporary file
|
||||||
|
import shutil
|
||||||
|
print(f"DEBUG: Moving {self.overwrite_temp_path} to {self.overwrite_target_path}")
|
||||||
|
try:
|
||||||
|
shutil.move(self.overwrite_temp_path, self.overwrite_target_path)
|
||||||
|
print("DEBUG: File move successful")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"DEBUG: File move failed: {e}")
|
||||||
|
# Try to clean up temp file
|
||||||
|
if os.path.exists(self.overwrite_temp_path):
|
||||||
|
os.remove(self.overwrite_temp_path)
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Small delay to ensure file system operations are complete
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._load_video(self.video_path)
|
||||||
|
self.load_current_frame()
|
||||||
|
print("File reloaded successfully")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Could not reload file after overwrite: {e}")
|
||||||
|
print("The file was saved successfully, but you may need to restart the editor to continue editing it.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error during file overwrite: {e}")
|
||||||
|
finally:
|
||||||
|
# Clean up overwrite state
|
||||||
|
self.overwrite_temp_path = None
|
||||||
|
self.overwrite_target_path = None
|
||||||
|
|
||||||
def cancel_render(self):
|
def cancel_render(self):
|
||||||
"""Cancel the current render operation"""
|
"""Cancel the current render operation"""
|
||||||
if self.render_thread and self.render_thread.is_alive():
|
if self.render_thread and self.render_thread.is_alive():
|
||||||
@@ -1832,7 +1860,7 @@ class VideoEditor:
|
|||||||
print(" N: Next file")
|
print(" N: Next file")
|
||||||
print(" n: Previous file")
|
print(" n: Previous file")
|
||||||
print(" Enter: Save image (overwrites if '_edited_' in name)")
|
print(" Enter: Save image (overwrites if '_edited_' in name)")
|
||||||
print(" Shift+Enter: Save image as _edited_edited")
|
print(" n: Save image as _edited_edited")
|
||||||
print(" Q/ESC: Quit")
|
print(" Q/ESC: Quit")
|
||||||
print()
|
print()
|
||||||
else:
|
else:
|
||||||
@@ -1864,7 +1892,7 @@ class VideoEditor:
|
|||||||
print(" N: Next video")
|
print(" N: Next video")
|
||||||
print(" n: Previous video")
|
print(" n: Previous video")
|
||||||
print(" Enter: Render video (overwrites if '_edited_' in name)")
|
print(" Enter: Render video (overwrites if '_edited_' in name)")
|
||||||
print(" Shift+Enter: Render video as _edited_edited")
|
print(" n: Render video as _edited_edited")
|
||||||
print(" X: Cancel render")
|
print(" X: Cancel render")
|
||||||
print(" Q/ESC: Quit")
|
print(" Q/ESC: Quit")
|
||||||
print()
|
print()
|
||||||
@@ -1992,25 +2020,48 @@ class VideoEditor:
|
|||||||
if self.crop_rect:
|
if self.crop_rect:
|
||||||
self.crop_history.append(self.crop_rect)
|
self.crop_history.append(self.crop_rect)
|
||||||
self.crop_rect = None
|
self.crop_rect = None
|
||||||
|
self.save_state() # Save state when crop is cleared
|
||||||
elif key == ord("1"):
|
elif key == ord("1"):
|
||||||
# Cut markers only for videos
|
# Cut markers only for videos
|
||||||
if not self.is_image_mode:
|
if not self.is_image_mode:
|
||||||
self.cut_start_frame = self.current_frame
|
self.cut_start_frame = self.current_frame
|
||||||
print(f"Set cut start at frame {self.current_frame}")
|
print(f"Set cut start at frame {self.current_frame}")
|
||||||
|
self.save_state() # Save state when cut start is set
|
||||||
elif key == ord("2"):
|
elif key == ord("2"):
|
||||||
# Cut markers only for videos
|
# Cut markers only for videos
|
||||||
if not self.is_image_mode:
|
if not self.is_image_mode:
|
||||||
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}")
|
||||||
|
self.save_state() # Save state when cut end is set
|
||||||
elif key == ord("N"):
|
elif key == ord("N"):
|
||||||
if len(self.video_files) > 1:
|
if len(self.video_files) > 1:
|
||||||
self.previous_video()
|
self.previous_video()
|
||||||
elif key == ord("n"):
|
elif key == ord("n"):
|
||||||
if len(self.video_files) > 1:
|
if len(self.video_files) > 1:
|
||||||
self.next_video()
|
self.next_video()
|
||||||
|
else:
|
||||||
|
# n - Create _edited_edited file
|
||||||
|
directory = self.video_path.parent
|
||||||
|
base_name = self.video_path.stem
|
||||||
|
extension = self.video_path.suffix
|
||||||
|
|
||||||
|
# Create _edited_edited filename
|
||||||
|
if "_edited_" in base_name:
|
||||||
|
# If already edited, create _edited_edited
|
||||||
|
new_name = f"{base_name}_edited{extension}"
|
||||||
|
else:
|
||||||
|
# If not edited, create _edited_edited
|
||||||
|
new_name = f"{base_name}_edited_edited{extension}"
|
||||||
|
|
||||||
|
output_path = str(directory / new_name)
|
||||||
|
success = self.render_video(output_path)
|
||||||
elif key == 13: # Enter
|
elif key == 13: # Enter
|
||||||
# Only overwrite if file already contains "_edited_" in name
|
# Only overwrite if file already contains "_edited_" in name
|
||||||
|
print(f"DEBUG: Checking if '{self.video_path.stem}' contains '_edited_'")
|
||||||
if "_edited_" in self.video_path.stem:
|
if "_edited_" in self.video_path.stem:
|
||||||
|
print("DEBUG: File contains '_edited_', proceeding with overwrite")
|
||||||
|
print(f"DEBUG: Original file path: {self.video_path}")
|
||||||
|
print(f"DEBUG: Original file exists: {self.video_path.exists()}")
|
||||||
output_path = str(self.video_path)
|
output_path = str(self.video_path)
|
||||||
|
|
||||||
# If we're overwriting the same file, use a temporary file first
|
# If we're overwriting the same file, use a temporary file first
|
||||||
@@ -2019,51 +2070,17 @@ class VideoEditor:
|
|||||||
temp_fd, temp_path = tempfile.mkstemp(suffix=self.video_path.suffix, dir=temp_dir)
|
temp_fd, temp_path = tempfile.mkstemp(suffix=self.video_path.suffix, dir=temp_dir)
|
||||||
os.close(temp_fd) # Close the file descriptor, we just need the path
|
os.close(temp_fd) # Close the file descriptor, we just need the path
|
||||||
|
|
||||||
|
print(f"DEBUG: Created temp file: {temp_path}")
|
||||||
print("Rendering to temporary file first...")
|
print("Rendering to temporary file first...")
|
||||||
|
|
||||||
success = self.render_video(temp_path)
|
success = self.render_video(temp_path)
|
||||||
|
|
||||||
if success:
|
# Store the temp path so we can replace the file when render completes
|
||||||
print("Replacing original file...")
|
self.overwrite_temp_path = temp_path
|
||||||
# Release current video capture before replacing the file
|
self.overwrite_target_path = str(self.video_path)
|
||||||
if hasattr(self, 'cap') and self.cap:
|
|
||||||
self.cap.release()
|
|
||||||
|
|
||||||
# Replace the original file with the temporary file
|
|
||||||
import shutil
|
|
||||||
shutil.move(temp_path, str(self.video_path))
|
|
||||||
|
|
||||||
# Small delay to ensure file system operations are complete
|
|
||||||
time.sleep(0.1)
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._load_video(self.video_path)
|
|
||||||
self.load_current_frame()
|
|
||||||
print("File reloaded successfully")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Warning: Could not reload file after overwrite: {e}")
|
|
||||||
print("The file was saved successfully, but you may need to restart the editor to continue editing it.")
|
|
||||||
else:
|
|
||||||
# Clean up temp file if rendering failed
|
|
||||||
if os.path.exists(temp_path):
|
|
||||||
os.remove(temp_path)
|
|
||||||
else:
|
else:
|
||||||
print("Enter key only overwrites files with '_edited_' in the name. Use Shift+Enter to create new files.")
|
print(f"DEBUG: File '{self.video_path.stem}' does not contain '_edited_'")
|
||||||
elif key == ord("\r"): # Shift+Enter (carriage return)
|
print("Enter key only overwrites files with '_edited_' in the name. Use 'n' to create new files.")
|
||||||
# Create _edited_edited file
|
|
||||||
directory = self.video_path.parent
|
|
||||||
base_name = self.video_path.stem
|
|
||||||
extension = self.video_path.suffix
|
|
||||||
|
|
||||||
# Create _edited_edited filename
|
|
||||||
if "_edited_" in base_name:
|
|
||||||
# If already edited, create _edited_edited
|
|
||||||
new_name = f"{base_name}_edited{extension}"
|
|
||||||
else:
|
|
||||||
# If not edited, create _edited_edited
|
|
||||||
new_name = f"{base_name}_edited_edited{extension}"
|
|
||||||
|
|
||||||
output_path = str(directory / new_name)
|
|
||||||
success = self.render_video(output_path)
|
|
||||||
elif key == ord("t"):
|
elif key == ord("t"):
|
||||||
# Marker looping only for videos
|
# Marker looping only for videos
|
||||||
if not self.is_image_mode:
|
if not self.is_image_mode:
|
||||||
|
Reference in New Issue
Block a user