diff --git a/main.py b/main.py index 7c07b76..3481bcb 100644 --- a/main.py +++ b/main.py @@ -9,6 +9,20 @@ from pathlib import Path from typing import List, Tuple, Optional class MediaGrader: + # Configuration constants + DEFAULT_FPS = 30 + BASE_FRAME_DELAY_MS = 33 # ~30 FPS + KEY_REPEAT_THRESHOLD_SEC = 0.5 + WINDOW_MAX_WIDTH = 1200 + WINDOW_MAX_HEIGHT = 800 + WINDOW_MAX_SCALE_UP = 2.0 + SPEED_INCREMENT = 0.1 + MIN_PLAYBACK_SPEED = 0.1 + MAX_PLAYBACK_SPEED = 100.0 + FAST_SEEK_MULTIPLIER = 500 + IFRAME_SNAP_INTERVAL = 30 + IMAGE_DISPLAY_DELAY_MS = 100 + def __init__(self, directory: str, seek_frames: int = 30, snap_to_iframe: bool = False): self.directory = Path(directory) self.seek_frames = seek_frames @@ -23,13 +37,12 @@ class MediaGrader: # Key repeat tracking self.last_key_time = 0 - self.key_repeat_delay = 0.1 # 100ms between repeats self.last_key = None # Seeking modes self.fine_seek_frames = 1 # Frame-by-frame self.coarse_seek_frames = self.seek_frames # User-configurable - self.fast_seek_frames = self.seek_frames * 5 # 5x the normal seek + self.fast_seek_frames = self.seek_frames * self.FAST_SEEK_MULTIPLIER # Supported media extensions self.extensions = ['*.png', '*.jpg', '*.jpeg', '*.gif', '*.mp4', '*.avi', '*.mov', '*.mkv'] @@ -60,6 +73,15 @@ class MediaGrader: """Check if file is a video""" return file_path.suffix.lower() in ['.mp4', '.avi', '.mov', '.mkv', '.gif'] + def calculate_frame_delay(self) -> int: + """Calculate frame delay in milliseconds based on playback speed""" + if not self.is_playing: + return 0 # No delay when paused + + # Base delay for 30 FPS, adjusted by playback speed + delay_ms = int(self.BASE_FRAME_DELAY_MS / self.playback_speed) + return max(1, delay_ms) # Minimum 1ms delay + def load_media(self, file_path: Path) -> bool: """Load media file for display""" if self.current_cap: @@ -102,19 +124,14 @@ class MediaGrader: """Auto-resize window to fit media while respecting screen limits""" height, width = frame.shape[:2] - # Get screen size (approximate - OpenCV doesn't have direct access) - # Use reasonable defaults for common screen sizes - max_width = 1200 - max_height = 800 - # Calculate scaling factor to fit within max dimensions - scale_w = max_width / width if width > max_width else 1.0 - scale_h = max_height / height if height > max_height else 1.0 + scale_w = self.WINDOW_MAX_WIDTH / width if width > self.WINDOW_MAX_WIDTH else 1.0 + scale_h = self.WINDOW_MAX_HEIGHT / height if height > self.WINDOW_MAX_HEIGHT else 1.0 scale = min(scale_w, scale_h) # Don't scale up small images too much - if scale > 2.0: - scale = 2.0 + if scale > self.WINDOW_MAX_SCALE_UP: + scale = self.WINDOW_MAX_SCALE_UP new_width = int(width * scale) new_height = int(height * scale) @@ -130,7 +147,7 @@ class MediaGrader: if self.snap_to_iframe and frames_delta < 0: # Find previous I-frame (approximation) - new_frame = max(0, new_frame - (new_frame % 30)) + new_frame = max(0, new_frame - (new_frame % self.IFRAME_SNAP_INTERVAL)) self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, new_frame) self.current_frame = new_frame @@ -139,19 +156,17 @@ class MediaGrader: """Handle seeking keys with different granularities. Returns True if key was handled.""" current_time = time.time() - # Determine seek amount based on modifier keys and timing + # Determine seek amount based on key and timing seek_amount = 0 - if key == 81: # Left arrow - # Use different seek amounts based on key repeat pattern - if self.last_key == key and (current_time - self.last_key_time) < 0.5: - # Fast repeat - use larger seek + # Try different arrow key detection methods + if key == 2424832 or key == 81: # Left arrow (different systems) + if self.last_key == key and (current_time - self.last_key_time) < self.KEY_REPEAT_THRESHOLD_SEC: seek_amount = -self.fast_seek_frames else: - # Normal seek seek_amount = -self.coarse_seek_frames - elif key == 83: # Right arrow - if self.last_key == key and (current_time - self.last_key_time) < 0.5: + elif key == 2555904 or key == 83: # Right arrow (different systems) + if self.last_key == key and (current_time - self.last_key_time) < self.KEY_REPEAT_THRESHOLD_SEC: seek_amount = self.fast_seek_frames else: seek_amount = self.coarse_seek_frames @@ -159,10 +174,6 @@ class MediaGrader: seek_amount = -self.fine_seek_frames elif key == ord('.'): # Period - fine seek forward seek_amount = self.fine_seek_frames - elif key == ord('['): # Left bracket - medium seek backward - seek_amount = -self.coarse_seek_frames - elif key == ord(']'): # Right bracket - medium seek forward - seek_amount = self.coarse_seek_frames else: return False @@ -221,7 +232,6 @@ class MediaGrader: print(" Space: Pause/Play") print(" Left/Right: Seek backward/forward (accelerates on repeat)") print(" , / . : Frame-by-frame seek (fine control)") - print(" [ / ] : Normal seek (medium control)") print(" A/D: Decrease/Increase playback speed") print(" 1-5: Grade and move file") print(" N: Next file") @@ -241,46 +251,51 @@ class MediaGrader: window_title = f"Media Grader - {current_file.name} ({self.current_index + 1}/{len(self.media_files)})" cv2.setWindowTitle('Media Grader', window_title) - delay = int(33 / self.playback_speed) if self.is_video(current_file) else 30 window_resized = False while True: - result = self.display_media(current_file) - if result is None or not result[0]: - break + # Only advance frame if playing (for videos) + if self.is_playing or not self.is_video(current_file): + result = self.display_media(current_file) + if result is None or not result[0]: + break + + ret, frame = result + + # Auto-resize window on first frame + if not window_resized: + self.auto_resize_window(frame) + window_resized = True + + # Add info overlay + info_text = f"Speed: {self.playback_speed:.1f}x | Frame: {self.current_frame}/{self.total_frames} | File: {self.current_index + 1}/{len(self.media_files)}" + help_text = "Seek: ←→ (accel) ,. (fine) | A/D speed | 1-5 grade | Space pause | Q quit" + + # White background for text visibility + cv2.putText(frame, info_text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2) + cv2.putText(frame, info_text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 0), 1) + cv2.putText(frame, help_text, (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2) + cv2.putText(frame, help_text, (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1) + + cv2.imshow('Media Grader', frame) - ret, frame = result - - # Auto-resize window on first frame - if not window_resized: - self.auto_resize_window(frame) - window_resized = True - - # Add info overlay - info_text = f"Speed: {self.playback_speed:.1f}x | Frame: {self.current_frame}/{self.total_frames} | File: {self.current_index + 1}/{len(self.media_files)}" - help_text = "Seek: ←→ (accel) ,. (fine) [] (med) | A/D speed | 1-5 grade | Space pause | Q quit" - - # White background for text visibility - cv2.putText(frame, info_text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2) - cv2.putText(frame, info_text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 0), 1) - cv2.putText(frame, help_text, (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2) - cv2.putText(frame, help_text, (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1) - - cv2.imshow('Media Grader', frame) + # Calculate appropriate delay + delay = self.calculate_frame_delay() if self.is_video(current_file) else self.IMAGE_DISPLAY_DELAY_MS key = cv2.waitKey(delay) & 0xFF + # Debug: print key codes to help with arrow key detection + if key != 255: # 255 means no key pressed + print(f"Key pressed: {key}") + if key == ord('q') or key == 27: # Q or ESC return elif key == ord(' '): # Space - pause/play self.is_playing = not self.is_playing - delay = int(33 / self.playback_speed) if self.is_playing and self.is_video(current_file) else 30 elif key == ord('a'): # A - decrease speed - self.playback_speed = max(0.1, self.playback_speed - 0.1) - delay = int(33 / self.playback_speed) if self.is_video(current_file) else 30 + self.playback_speed = max(self.MIN_PLAYBACK_SPEED, self.playback_speed - self.SPEED_INCREMENT) elif key == ord('d'): # D - increase speed - self.playback_speed = min(5.0, self.playback_speed + 0.1) - delay = int(33 / self.playback_speed) if self.is_video(current_file) else 30 + self.playback_speed = min(self.MAX_PLAYBACK_SPEED, self.playback_speed + self.SPEED_INCREMENT) elif self.handle_seeking_key(key): # Seeking was handled pass @@ -294,10 +309,6 @@ class MediaGrader: if not self.grade_media(grade): return break - - if not self.is_playing and not self.is_video(current_file): - # For images, wait indefinitely when paused - continue if key not in [ord('p')]: # Don't increment for previous self.current_index += 1