Compare commits
4 Commits
b7e4fac9e7
...
0fb591d0b3
Author | SHA1 | Date | |
---|---|---|---|
0fb591d0b3 | |||
709e637e88 | |||
252cda9ad3 | |||
d29d45d4fd |
100
croppa/main.py
100
croppa/main.py
@@ -19,7 +19,7 @@ class VideoEditor:
|
|||||||
MAX_PLAYBACK_SPEED = 10.0
|
MAX_PLAYBACK_SPEED = 10.0
|
||||||
|
|
||||||
# Seek multiplier configuration
|
# Seek multiplier configuration
|
||||||
SEEK_MULTIPLIER_INCREMENT = 1.0
|
SEEK_MULTIPLIER_INCREMENT = 2.0
|
||||||
MIN_SEEK_MULTIPLIER = 1.0
|
MIN_SEEK_MULTIPLIER = 1.0
|
||||||
MAX_SEEK_MULTIPLIER = 100.0
|
MAX_SEEK_MULTIPLIER = 100.0
|
||||||
|
|
||||||
@@ -116,10 +116,6 @@ class VideoEditor:
|
|||||||
self.brightness = 0 # -100 to 100
|
self.brightness = 0 # -100 to 100
|
||||||
self.contrast = 1.0 # 0.1 to 3.0
|
self.contrast = 1.0 # 0.1 to 3.0
|
||||||
|
|
||||||
# Cut points
|
|
||||||
self.cut_start_frame = None
|
|
||||||
self.cut_end_frame = None
|
|
||||||
|
|
||||||
# Marker looping state
|
# Marker looping state
|
||||||
self.looping_between_markers = False
|
self.looping_between_markers = False
|
||||||
|
|
||||||
@@ -150,8 +146,11 @@ class VideoEditor:
|
|||||||
def _get_state_file_path(self) -> Path:
|
def _get_state_file_path(self) -> Path:
|
||||||
"""Get the state file path for the current media file"""
|
"""Get the state file path for the current media file"""
|
||||||
if not hasattr(self, 'video_path') or not self.video_path:
|
if not hasattr(self, 'video_path') or not self.video_path:
|
||||||
|
print("DEBUG: No video_path available for state file")
|
||||||
return None
|
return None
|
||||||
return self.video_path.with_suffix('.json')
|
state_path = self.video_path.with_suffix('.json')
|
||||||
|
print(f"DEBUG: State file path would be: {state_path}")
|
||||||
|
return state_path
|
||||||
|
|
||||||
def save_state(self):
|
def save_state(self):
|
||||||
"""Save current editor state to JSON file"""
|
"""Save current editor state to JSON file"""
|
||||||
@@ -190,13 +189,20 @@ class VideoEditor:
|
|||||||
def load_state(self) -> bool:
|
def load_state(self) -> bool:
|
||||||
"""Load editor state from JSON file"""
|
"""Load editor state from JSON file"""
|
||||||
state_file = self._get_state_file_path()
|
state_file = self._get_state_file_path()
|
||||||
if not state_file or not state_file.exists():
|
if not state_file:
|
||||||
|
print("No state file path available")
|
||||||
|
return False
|
||||||
|
if not state_file.exists():
|
||||||
|
print(f"State file does not exist: {state_file}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
print(f"Loading state from: {state_file}")
|
||||||
try:
|
try:
|
||||||
with open(state_file, 'r') as f:
|
with open(state_file, 'r') as f:
|
||||||
state = json.load(f)
|
state = json.load(f)
|
||||||
|
|
||||||
|
print(f"State file contents: {state}")
|
||||||
|
|
||||||
# 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']
|
||||||
@@ -214,8 +220,16 @@ class VideoEditor:
|
|||||||
self.contrast = state['contrast']
|
self.contrast = state['contrast']
|
||||||
if 'cut_start_frame' in state:
|
if 'cut_start_frame' in state:
|
||||||
self.cut_start_frame = state['cut_start_frame']
|
self.cut_start_frame = state['cut_start_frame']
|
||||||
|
print(f"Restored cut_start_frame: {self.cut_start_frame}")
|
||||||
if 'cut_end_frame' in state:
|
if 'cut_end_frame' in state:
|
||||||
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}")
|
||||||
|
|
||||||
|
# Calculate and show marker positions on timeline
|
||||||
|
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)
|
||||||
|
end_progress = self.cut_end_frame / max(1, self.total_frames - 1)
|
||||||
|
print(f"Markers will be drawn at: Start {start_progress:.4f} ({self.cut_start_frame}/{self.total_frames}), End {end_progress:.4f} ({self.cut_end_frame}/{self.total_frames})")
|
||||||
if 'looping_between_markers' in state:
|
if 'looping_between_markers' in state:
|
||||||
self.looping_between_markers = state['looping_between_markers']
|
self.looping_between_markers = state['looping_between_markers']
|
||||||
if 'display_offset' in state:
|
if 'display_offset' in state:
|
||||||
@@ -460,6 +474,12 @@ class VideoEditor:
|
|||||||
# Try to load saved state for this media file
|
# Try to load saved state for this media file
|
||||||
if self.load_state():
|
if self.load_state():
|
||||||
print("Loaded saved state for this media file")
|
print("Loaded saved state for this media file")
|
||||||
|
if self.cut_start_frame is not None:
|
||||||
|
print(f" Cut start frame: {self.cut_start_frame}")
|
||||||
|
if self.cut_end_frame is not None:
|
||||||
|
print(f" Cut end frame: {self.cut_end_frame}")
|
||||||
|
else:
|
||||||
|
print("No saved state found for this media file")
|
||||||
|
|
||||||
def switch_to_video(self, index: int):
|
def switch_to_video(self, index: int):
|
||||||
"""Switch to a specific video by index"""
|
"""Switch to a specific video by index"""
|
||||||
@@ -1500,7 +1520,7 @@ class VideoEditor:
|
|||||||
"""Start video rendering in a separate thread"""
|
"""Start video rendering in a separate thread"""
|
||||||
# Check if already rendering
|
# Check if already rendering
|
||||||
if self.render_thread and self.render_thread.is_alive():
|
if self.render_thread and self.render_thread.is_alive():
|
||||||
print("Render already in progress!")
|
print("Render already in progress! Use 'X' to cancel first.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Reset render state
|
# Reset render state
|
||||||
@@ -1515,10 +1535,12 @@ class VideoEditor:
|
|||||||
self.render_thread.start()
|
self.render_thread.start()
|
||||||
|
|
||||||
print(f"Started rendering to {output_path} in background thread...")
|
print(f"Started rendering to {output_path} in background thread...")
|
||||||
|
print("You can continue editing while rendering. Press 'X' to cancel.")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _render_video_worker(self, output_path: str):
|
def _render_video_worker(self, output_path: str):
|
||||||
"""Worker method that runs in the render thread"""
|
"""Worker method that runs in the render thread"""
|
||||||
|
render_cap = None
|
||||||
try:
|
try:
|
||||||
if not output_path.endswith(".mp4"):
|
if not output_path.endswith(".mp4"):
|
||||||
output_path += ".mp4"
|
output_path += ".mp4"
|
||||||
@@ -1528,6 +1550,12 @@ class VideoEditor:
|
|||||||
# Send progress update to main thread
|
# Send progress update to main thread
|
||||||
self.render_progress_queue.put(("init", "Initializing render...", 0.0, 0.0))
|
self.render_progress_queue.put(("init", "Initializing render...", 0.0, 0.0))
|
||||||
|
|
||||||
|
# Create a separate VideoCapture for the render thread to avoid thread safety issues
|
||||||
|
render_cap = cv2.VideoCapture(str(self.video_path))
|
||||||
|
if not render_cap.isOpened():
|
||||||
|
self.render_progress_queue.put(("error", "Could not open video for rendering!", 1.0, 0.0))
|
||||||
|
return False
|
||||||
|
|
||||||
# Determine frame range
|
# Determine frame range
|
||||||
start_frame = self.cut_start_frame if self.cut_start_frame is not None else 0
|
start_frame = self.cut_start_frame if self.cut_start_frame is not None else 0
|
||||||
end_frame = (
|
end_frame = (
|
||||||
@@ -1583,9 +1611,9 @@ class VideoEditor:
|
|||||||
self.render_progress_queue.put(("cancelled", "Render cancelled", 0.0, 0.0))
|
self.render_progress_queue.put(("cancelled", "Render cancelled", 0.0, 0.0))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Read frame
|
# Read frame using the separate VideoCapture
|
||||||
self.cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)
|
render_cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)
|
||||||
ret, frame = self.cap.read()
|
ret, frame = render_cap.read()
|
||||||
|
|
||||||
if not ret:
|
if not ret:
|
||||||
break
|
break
|
||||||
@@ -1634,9 +1662,20 @@ class VideoEditor:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.render_progress_queue.put(("error", f"Render error: {str(e)}", 1.0, 0.0))
|
error_msg = str(e)
|
||||||
print(f"Render error: {e}")
|
# Handle specific FFmpeg threading errors
|
||||||
|
if "async_lock" in error_msg or "pthread_frame" in error_msg:
|
||||||
|
error_msg = "FFmpeg threading error - try restarting the application"
|
||||||
|
elif "Assertion" in error_msg:
|
||||||
|
error_msg = "Video codec error - the video file may be corrupted or incompatible"
|
||||||
|
|
||||||
|
self.render_progress_queue.put(("error", f"Render error: {error_msg}", 1.0, 0.0))
|
||||||
|
print(f"Render error: {error_msg}")
|
||||||
return False
|
return False
|
||||||
|
finally:
|
||||||
|
# Always clean up the render VideoCapture
|
||||||
|
if render_cap:
|
||||||
|
render_cap.release()
|
||||||
|
|
||||||
def update_render_progress(self):
|
def update_render_progress(self):
|
||||||
"""Process progress updates from the render thread"""
|
"""Process progress updates from the render thread"""
|
||||||
@@ -1792,7 +1831,8 @@ class VideoEditor:
|
|||||||
if len(self.video_files) > 1:
|
if len(self.video_files) > 1:
|
||||||
print(" N: Next file")
|
print(" N: Next file")
|
||||||
print(" n: Previous file")
|
print(" n: Previous file")
|
||||||
print(" Enter: Save image")
|
print(" Enter: Save image (overwrites if '_edited_' in name)")
|
||||||
|
print(" Shift+Enter: Save image as _edited_edited")
|
||||||
print(" Q/ESC: Quit")
|
print(" Q/ESC: Quit")
|
||||||
print()
|
print()
|
||||||
else:
|
else:
|
||||||
@@ -1823,7 +1863,8 @@ class VideoEditor:
|
|||||||
if len(self.video_files) > 1:
|
if len(self.video_files) > 1:
|
||||||
print(" N: Next video")
|
print(" N: Next video")
|
||||||
print(" n: Previous video")
|
print(" n: Previous video")
|
||||||
print(" Enter: Render video")
|
print(" Enter: Render video (overwrites if '_edited_' in name)")
|
||||||
|
print(" Shift+Enter: Render video as _edited_edited")
|
||||||
print(" X: Cancel render")
|
print(" X: Cancel render")
|
||||||
print(" Q/ESC: Quit")
|
print(" Q/ESC: Quit")
|
||||||
print()
|
print()
|
||||||
@@ -1968,11 +2009,11 @@ class VideoEditor:
|
|||||||
if len(self.video_files) > 1:
|
if len(self.video_files) > 1:
|
||||||
self.next_video()
|
self.next_video()
|
||||||
elif key == 13: # Enter
|
elif key == 13: # Enter
|
||||||
output_name = self._get_next_edited_filename(self.video_path)
|
# Only overwrite if file already contains "_edited_" in name
|
||||||
output_path = str(self.video_path.parent / output_name)
|
if "_edited_" in self.video_path.stem:
|
||||||
|
output_path = str(self.video_path)
|
||||||
# If we're overwriting the same file, use a temporary file first
|
|
||||||
if output_name == self.video_path.name:
|
# If we're overwriting the same file, use a temporary file first
|
||||||
import tempfile
|
import tempfile
|
||||||
temp_dir = self.video_path.parent
|
temp_dir = self.video_path.parent
|
||||||
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)
|
||||||
@@ -2006,8 +2047,23 @@ class VideoEditor:
|
|||||||
if os.path.exists(temp_path):
|
if os.path.exists(temp_path):
|
||||||
os.remove(temp_path)
|
os.remove(temp_path)
|
||||||
else:
|
else:
|
||||||
# Normal case - render to new file
|
print("Enter key only overwrites files with '_edited_' in the name. Use Shift+Enter to create new files.")
|
||||||
success = self.render_video(output_path)
|
elif key == ord("\r"): # Shift+Enter (carriage return)
|
||||||
|
# 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