feat(main.py): refactor media grading logic with constants and improved controls
This commit is contained in:
123
main.py
123
main.py
@@ -9,6 +9,20 @@ from pathlib import Path
|
|||||||
from typing import List, Tuple, Optional
|
from typing import List, Tuple, Optional
|
||||||
|
|
||||||
class MediaGrader:
|
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):
|
def __init__(self, directory: str, seek_frames: int = 30, snap_to_iframe: bool = False):
|
||||||
self.directory = Path(directory)
|
self.directory = Path(directory)
|
||||||
self.seek_frames = seek_frames
|
self.seek_frames = seek_frames
|
||||||
@@ -23,13 +37,12 @@ class MediaGrader:
|
|||||||
|
|
||||||
# Key repeat tracking
|
# Key repeat tracking
|
||||||
self.last_key_time = 0
|
self.last_key_time = 0
|
||||||
self.key_repeat_delay = 0.1 # 100ms between repeats
|
|
||||||
self.last_key = None
|
self.last_key = None
|
||||||
|
|
||||||
# Seeking modes
|
# Seeking modes
|
||||||
self.fine_seek_frames = 1 # Frame-by-frame
|
self.fine_seek_frames = 1 # Frame-by-frame
|
||||||
self.coarse_seek_frames = self.seek_frames # User-configurable
|
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
|
# Supported media extensions
|
||||||
self.extensions = ['*.png', '*.jpg', '*.jpeg', '*.gif', '*.mp4', '*.avi', '*.mov', '*.mkv']
|
self.extensions = ['*.png', '*.jpg', '*.jpeg', '*.gif', '*.mp4', '*.avi', '*.mov', '*.mkv']
|
||||||
@@ -60,6 +73,15 @@ class MediaGrader:
|
|||||||
"""Check if file is a video"""
|
"""Check if file is a video"""
|
||||||
return file_path.suffix.lower() in ['.mp4', '.avi', '.mov', '.mkv', '.gif']
|
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:
|
def load_media(self, file_path: Path) -> bool:
|
||||||
"""Load media file for display"""
|
"""Load media file for display"""
|
||||||
if self.current_cap:
|
if self.current_cap:
|
||||||
@@ -102,19 +124,14 @@ class MediaGrader:
|
|||||||
"""Auto-resize window to fit media while respecting screen limits"""
|
"""Auto-resize window to fit media while respecting screen limits"""
|
||||||
height, width = frame.shape[:2]
|
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
|
# Calculate scaling factor to fit within max dimensions
|
||||||
scale_w = max_width / width if width > max_width else 1.0
|
scale_w = self.WINDOW_MAX_WIDTH / width if width > self.WINDOW_MAX_WIDTH else 1.0
|
||||||
scale_h = max_height / height if height > max_height 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)
|
scale = min(scale_w, scale_h)
|
||||||
|
|
||||||
# Don't scale up small images too much
|
# Don't scale up small images too much
|
||||||
if scale > 2.0:
|
if scale > self.WINDOW_MAX_SCALE_UP:
|
||||||
scale = 2.0
|
scale = self.WINDOW_MAX_SCALE_UP
|
||||||
|
|
||||||
new_width = int(width * scale)
|
new_width = int(width * scale)
|
||||||
new_height = int(height * scale)
|
new_height = int(height * scale)
|
||||||
@@ -130,7 +147,7 @@ class MediaGrader:
|
|||||||
|
|
||||||
if self.snap_to_iframe and frames_delta < 0:
|
if self.snap_to_iframe and frames_delta < 0:
|
||||||
# Find previous I-frame (approximation)
|
# 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_cap.set(cv2.CAP_PROP_POS_FRAMES, new_frame)
|
||||||
self.current_frame = 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."""
|
"""Handle seeking keys with different granularities. Returns True if key was handled."""
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
|
|
||||||
# Determine seek amount based on modifier keys and timing
|
# Determine seek amount based on key and timing
|
||||||
seek_amount = 0
|
seek_amount = 0
|
||||||
|
|
||||||
if key == 81: # Left arrow
|
# Try different arrow key detection methods
|
||||||
# Use different seek amounts based on key repeat pattern
|
if key == 2424832 or key == 81: # Left arrow (different systems)
|
||||||
if self.last_key == key and (current_time - self.last_key_time) < 0.5:
|
if self.last_key == key and (current_time - self.last_key_time) < self.KEY_REPEAT_THRESHOLD_SEC:
|
||||||
# Fast repeat - use larger seek
|
|
||||||
seek_amount = -self.fast_seek_frames
|
seek_amount = -self.fast_seek_frames
|
||||||
else:
|
else:
|
||||||
# Normal seek
|
|
||||||
seek_amount = -self.coarse_seek_frames
|
seek_amount = -self.coarse_seek_frames
|
||||||
elif key == 83: # Right arrow
|
elif key == 2555904 or key == 83: # Right arrow (different systems)
|
||||||
if self.last_key == key and (current_time - self.last_key_time) < 0.5:
|
if self.last_key == key and (current_time - self.last_key_time) < self.KEY_REPEAT_THRESHOLD_SEC:
|
||||||
seek_amount = self.fast_seek_frames
|
seek_amount = self.fast_seek_frames
|
||||||
else:
|
else:
|
||||||
seek_amount = self.coarse_seek_frames
|
seek_amount = self.coarse_seek_frames
|
||||||
@@ -159,10 +174,6 @@ class MediaGrader:
|
|||||||
seek_amount = -self.fine_seek_frames
|
seek_amount = -self.fine_seek_frames
|
||||||
elif key == ord('.'): # Period - fine seek forward
|
elif key == ord('.'): # Period - fine seek forward
|
||||||
seek_amount = self.fine_seek_frames
|
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:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -221,7 +232,6 @@ class MediaGrader:
|
|||||||
print(" Space: Pause/Play")
|
print(" Space: Pause/Play")
|
||||||
print(" Left/Right: Seek backward/forward (accelerates on repeat)")
|
print(" Left/Right: Seek backward/forward (accelerates on repeat)")
|
||||||
print(" , / . : Frame-by-frame seek (fine control)")
|
print(" , / . : Frame-by-frame seek (fine control)")
|
||||||
print(" [ / ] : Normal seek (medium control)")
|
|
||||||
print(" A/D: Decrease/Increase playback speed")
|
print(" A/D: Decrease/Increase playback speed")
|
||||||
print(" 1-5: Grade and move file")
|
print(" 1-5: Grade and move file")
|
||||||
print(" N: Next 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)})"
|
window_title = f"Media Grader - {current_file.name} ({self.current_index + 1}/{len(self.media_files)})"
|
||||||
cv2.setWindowTitle('Media Grader', window_title)
|
cv2.setWindowTitle('Media Grader', window_title)
|
||||||
|
|
||||||
delay = int(33 / self.playback_speed) if self.is_video(current_file) else 30
|
|
||||||
window_resized = False
|
window_resized = False
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
result = self.display_media(current_file)
|
# Only advance frame if playing (for videos)
|
||||||
if result is None or not result[0]:
|
if self.is_playing or not self.is_video(current_file):
|
||||||
break
|
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
|
# Calculate appropriate delay
|
||||||
|
delay = self.calculate_frame_delay() if self.is_video(current_file) else self.IMAGE_DISPLAY_DELAY_MS
|
||||||
# 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)
|
|
||||||
|
|
||||||
key = cv2.waitKey(delay) & 0xFF
|
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
|
if key == ord('q') or key == 27: # Q or ESC
|
||||||
return
|
return
|
||||||
elif key == ord(' '): # Space - pause/play
|
elif key == ord(' '): # Space - pause/play
|
||||||
self.is_playing = not self.is_playing
|
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
|
elif key == ord('a'): # A - decrease speed
|
||||||
self.playback_speed = max(0.1, self.playback_speed - 0.1)
|
self.playback_speed = max(self.MIN_PLAYBACK_SPEED, self.playback_speed - self.SPEED_INCREMENT)
|
||||||
delay = int(33 / self.playback_speed) if self.is_video(current_file) else 30
|
|
||||||
elif key == ord('d'): # D - increase speed
|
elif key == ord('d'): # D - increase speed
|
||||||
self.playback_speed = min(5.0, self.playback_speed + 0.1)
|
self.playback_speed = min(self.MAX_PLAYBACK_SPEED, self.playback_speed + self.SPEED_INCREMENT)
|
||||||
delay = int(33 / self.playback_speed) if self.is_video(current_file) else 30
|
|
||||||
elif self.handle_seeking_key(key):
|
elif self.handle_seeking_key(key):
|
||||||
# Seeking was handled
|
# Seeking was handled
|
||||||
pass
|
pass
|
||||||
@@ -294,10 +309,6 @@ class MediaGrader:
|
|||||||
if not self.grade_media(grade):
|
if not self.grade_media(grade):
|
||||||
return
|
return
|
||||||
break
|
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
|
if key not in [ord('p')]: # Don't increment for previous
|
||||||
self.current_index += 1
|
self.current_index += 1
|
||||||
|
|||||||
Reference in New Issue
Block a user