feat(main.py): refactor media grading logic with constants and improved controls

This commit is contained in:
2025-08-18 16:25:56 +02:00
parent 2b9988b592
commit 0a048d3078

123
main.py
View File

@@ -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