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
|
||||
|
||||
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
|
||||
|
Reference in New Issue
Block a user