feat(main.py): implement advanced seeking controls and auto-window resizing

This commit is contained in:
2025-08-18 16:17:35 +02:00
parent 1b2fa03ab6
commit 2b9988b592

110
main.py
View File

@@ -4,6 +4,7 @@ import glob
import cv2 import cv2
import argparse import argparse
import shutil import shutil
import time
from pathlib import Path from pathlib import Path
from typing import List, Tuple, Optional from typing import List, Tuple, Optional
@@ -20,6 +21,16 @@ class MediaGrader:
self.current_frame = 0 self.current_frame = 0
self.total_frames = 0 self.total_frames = 0
# 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
# 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']
@@ -87,6 +98,29 @@ class MediaGrader:
return False, None return False, None
return True, frame return True, frame
def auto_resize_window(self, frame):
"""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 = min(scale_w, scale_h)
# Don't scale up small images too much
if scale > 2.0:
scale = 2.0
new_width = int(width * scale)
new_height = int(height * scale)
cv2.resizeWindow('Media Grader', new_width, new_height)
def seek_video(self, frames_delta: int): def seek_video(self, frames_delta: int):
"""Seek video by specified number of frames""" """Seek video by specified number of frames"""
if not self.current_cap or not self.is_video(self.media_files[self.current_index]): if not self.current_cap or not self.is_video(self.media_files[self.current_index]):
@@ -101,6 +135,42 @@ class MediaGrader:
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
def handle_seeking_key(self, key: int) -> bool:
"""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
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
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:
seek_amount = self.fast_seek_frames
else:
seek_amount = self.coarse_seek_frames
elif key == ord(','): # Comma - fine seek backward
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
self.seek_video(seek_amount)
self.last_key = key
self.last_key_time = current_time
return True
def grade_media(self, grade: int): def grade_media(self, grade: int):
"""Move current media file to grade directory""" """Move current media file to grade directory"""
if not self.media_files or grade < 1 or grade > 5: if not self.media_files or grade < 1 or grade > 5:
@@ -149,7 +219,9 @@ class MediaGrader:
print(f"Found {len(self.media_files)} media files") print(f"Found {len(self.media_files)} media files")
print("Controls:") print("Controls:")
print(" Space: Pause/Play") print(" Space: Pause/Play")
print(" Left/Right: Seek backward/forward") 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(" 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")
@@ -169,7 +241,8 @@ 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 0 delay = int(33 / self.playback_speed) if self.is_video(current_file) else 30
window_resized = False
while True: while True:
result = self.display_media(current_file) result = self.display_media(current_file)
@@ -178,10 +251,20 @@ class MediaGrader:
ret, frame = result ret, frame = result
# Auto-resize window on first frame
if not window_resized:
self.auto_resize_window(frame)
window_resized = True
# Add info overlay # 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)}" 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, (255, 255, 255), 2)
cv2.putText(frame, info_text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 0), 1) 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) cv2.imshow('Media Grader', frame)
@@ -191,19 +274,16 @@ class MediaGrader:
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 0 delay = int(33 / self.playback_speed) if self.is_playing and self.is_video(current_file) else 30
elif key == 81 or key == ord('a'): # Left arrow or A - decrease speed elif key == ord('a'): # A - decrease speed
if key == 81: # Left arrow - seek backward self.playback_speed = max(0.1, self.playback_speed - 0.1)
self.seek_video(-self.seek_frames) delay = int(33 / self.playback_speed) if self.is_video(current_file) else 30
else: # A - decrease speed elif key == ord('d'): # D - increase speed
self.playback_speed = max(0.1, self.playback_speed - 0.1) self.playback_speed = min(5.0, self.playback_speed + 0.1)
delay = int(33 / self.playback_speed) if self.is_video(current_file) else 0 delay = int(33 / self.playback_speed) if self.is_video(current_file) else 30
elif key == 83 or key == ord('d'): # Right arrow or D elif self.handle_seeking_key(key):
if key == 83: # Right arrow - seek forward # Seeking was handled
self.seek_video(self.seek_frames) pass
else: # 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 0
elif key == ord('n'): # Next file elif key == ord('n'): # Next file
break break
elif key == ord('p'): # Previous file elif key == ord('p'): # Previous file