refactor(main.py): introduce frame caching and separate frame loading/advancing logic

This commit is contained in:
2025-08-18 16:36:18 +02:00
parent e2758b5390
commit 3900d1cb11

193
main.py
View File

@@ -49,6 +49,9 @@ class MediaGrader:
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 * self.FAST_SEEK_MULTIPLIER self.fast_seek_frames = self.seek_frames * self.FAST_SEEK_MULTIPLIER
# Current frame cache for display
self.current_display_frame = None
# Supported media extensions # Supported media extensions
self.extensions = [ self.extensions = [
"*.png", "*.png",
@@ -121,30 +124,50 @@ class MediaGrader:
self.total_frames = 1 self.total_frames = 1
self.current_frame = 0 self.current_frame = 0
# Load initial frame
self.load_current_frame()
return True return True
def display_media(self, file_path: Path) -> Optional[Tuple[bool, any]]: def load_current_frame(self):
"""Display current media file""" """Load the current frame into display cache"""
if self.is_video(file_path): if self.is_video(self.media_files[self.current_index]):
if not self.current_cap: if not self.current_cap:
return None return False
# For high-speed playback, skip frames # Read frame at current position
frames_to_skip = self.calculate_frames_to_skip() ret, frame = self.current_cap.read()
if ret:
for _ in range(frames_to_skip + 1): # +1 to read at least one frame self.current_display_frame = frame
ret, frame = self.current_cap.read() self.current_frame = int(self.current_cap.get(cv2.CAP_PROP_POS_FRAMES))
if not ret: return True
return False, None return False
self.current_frame = int(self.current_cap.get(cv2.CAP_PROP_POS_FRAMES))
return True, frame
else: else:
# Display image # Load image
frame = cv2.imread(str(file_path)) frame = cv2.imread(str(self.media_files[self.current_index]))
if frame is None: if frame is not None:
return False, None self.current_display_frame = frame
return True, frame return True
return False
def advance_frame(self):
"""Advance to next frame(s) based on playback speed"""
if (
not self.is_video(self.media_files[self.current_index])
or not self.is_playing
):
return
# Skip frames for high-speed playback
frames_to_skip = self.calculate_frames_to_skip()
for _ in range(frames_to_skip + 1): # +1 to advance at least one frame
ret, frame = self.current_cap.read()
if not ret:
return False
self.current_display_frame = frame
self.current_frame = int(self.current_cap.get(cv2.CAP_PROP_POS_FRAMES))
return True
def auto_resize_window(self, frame): def auto_resize_window(self, frame):
"""Auto-resize window to fit media while respecting screen limits""" """Auto-resize window to fit media while respecting screen limits"""
@@ -184,7 +207,9 @@ class MediaGrader:
new_frame = max(0, new_frame - (new_frame % self.IFRAME_SNAP_INTERVAL)) 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
# Load the frame we just seeked to and display it immediately
self.load_current_frame()
print(f"Seeked by {frames_delta} frames to frame {new_frame}") print(f"Seeked by {frames_delta} frames to frame {new_frame}")
def handle_seeking_key(self, key: int) -> bool: def handle_seeking_key(self, key: int) -> bool:
@@ -288,9 +313,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 (hold for FAST seek)") print(" A/D: Seek backward/forward (hold for FAST seek)")
print(" , / . : Frame-by-frame seek (fine control)") print(" , / . : Frame-by-frame seek (fine control)")
print(" A/D: Decrease/Increase playback speed") print(" W/S: 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")
print(" P: Previous file") print(" P: Previous file")
@@ -312,70 +337,62 @@ class MediaGrader:
window_resized = False window_resized = False
while True: while True:
# Always try to get and display a frame (for seeking while paused) # Always display the current cached frame
result = self.display_media(current_file) if self.current_display_frame is not None:
if result is None or not result[0]: frame = self.current_display_frame.copy()
break
ret, frame = result # Auto-resize window on first frame
if not window_resized:
self.auto_resize_window(frame)
window_resized = True
# Auto-resize window on first frame # Add info overlay
if not window_resized: 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)} | {'Playing' if self.is_playing else 'PAUSED'}"
self.auto_resize_window(frame) help_text = "Seek: A/D (hold=FAST) ,. (fine) | W/S speed | 1-5 grade | Space pause | Q quit"
window_resized = True
# Add info overlay # White background for text visibility
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)}" cv2.putText(
help_text = "Seek: ←→ (hold=FAST) ,. (fine) | A/D speed | 1-5 grade | Space pause | Q quit" 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,
)
# White background for text visibility cv2.imshow("Media Grader", frame)
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
# Calculate appropriate delay - shorter for paused videos to enable seeking
if self.is_video(current_file): if self.is_video(current_file):
if self.is_playing: delay = self.calculate_frame_delay()
delay = self.calculate_frame_delay()
else:
delay = (
30 # Short delay when paused to enable responsive seeking
)
else: else:
delay = self.IMAGE_DISPLAY_DELAY_MS delay = self.IMAGE_DISPLAY_DELAY_MS
@@ -390,20 +407,20 @@ class MediaGrader:
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
print(f"{'Playing' if self.is_playing else 'Paused'}") print(f"{'Playing' if self.is_playing else 'Paused'}")
elif key == ord("w"): # A - decrease speed elif key == ord("s"): # W - decrease speed
self.playback_speed = max( self.playback_speed = max(
self.MIN_PLAYBACK_SPEED, self.MIN_PLAYBACK_SPEED,
self.playback_speed - self.SPEED_INCREMENT, self.playback_speed - self.SPEED_INCREMENT,
) )
print(f"Speed: {self.playback_speed:.1f}x") print(f"Speed: {self.playback_speed:.1f}x")
elif key == ord("s"): # D - increase speed elif key == ord("w"): # S - increase speed
self.playback_speed = min( self.playback_speed = min(
self.MAX_PLAYBACK_SPEED, self.MAX_PLAYBACK_SPEED,
self.playback_speed + self.SPEED_INCREMENT, self.playback_speed + self.SPEED_INCREMENT,
) )
print(f"Speed: {self.playback_speed:.1f}x") print(f"Speed: {self.playback_speed:.1f}x")
elif self.handle_seeking_key(key): elif self.handle_seeking_key(key):
# Seeking was handled # Seeking was handled and frame was updated
pass pass
elif key == ord("n"): # Next file elif key == ord("n"): # Next file
break break
@@ -421,11 +438,11 @@ class MediaGrader:
self.last_key = None self.last_key = None
print("Key released") print("Key released")
# Only advance to next frame if playing AND it's a video # Advance frame only if playing (and it's a video)
if not self.is_playing and self.is_video(current_file): if self.is_playing and self.is_video(current_file):
# When paused, seek back one frame to stay on current frame if not self.advance_frame():
# (since display_media already advanced us) # End of video
continue break
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