feat(main.py): enhance seeking and playback controls for improved user experience
This commit is contained in:
399
main.py
399
main.py
@@ -8,22 +8,26 @@ import time
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Tuple, Optional
|
from typing import List, Tuple, Optional
|
||||||
|
|
||||||
|
|
||||||
class MediaGrader:
|
class MediaGrader:
|
||||||
# Configuration constants
|
# Configuration constants
|
||||||
DEFAULT_FPS = 30
|
DEFAULT_FPS = 30
|
||||||
BASE_FRAME_DELAY_MS = 33 # ~30 FPS
|
BASE_FRAME_DELAY_MS = 33 # ~30 FPS
|
||||||
KEY_REPEAT_THRESHOLD_SEC = 0.5
|
KEY_REPEAT_THRESHOLD_SEC = 0.2 # Faster detection for repeat
|
||||||
|
FAST_SEEK_ACTIVATION_TIME = 0.5 # How long to hold before fast seek
|
||||||
WINDOW_MAX_WIDTH = 1200
|
WINDOW_MAX_WIDTH = 1200
|
||||||
WINDOW_MAX_HEIGHT = 800
|
WINDOW_MAX_HEIGHT = 800
|
||||||
WINDOW_MAX_SCALE_UP = 2.0
|
WINDOW_MAX_SCALE_UP = 2.0
|
||||||
SPEED_INCREMENT = 0.1
|
SPEED_INCREMENT = 0.1
|
||||||
MIN_PLAYBACK_SPEED = 0.1
|
MIN_PLAYBACK_SPEED = 0.1
|
||||||
MAX_PLAYBACK_SPEED = 100.0
|
MAX_PLAYBACK_SPEED = 100.0
|
||||||
FAST_SEEK_MULTIPLIER = 500
|
FAST_SEEK_MULTIPLIER = 5
|
||||||
IFRAME_SNAP_INTERVAL = 30
|
IFRAME_SNAP_INTERVAL = 30
|
||||||
IMAGE_DISPLAY_DELAY_MS = 100
|
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
|
||||||
self.snap_to_iframe = snap_to_iframe
|
self.snap_to_iframe = snap_to_iframe
|
||||||
@@ -34,24 +38,34 @@ class MediaGrader:
|
|||||||
self.is_playing = True
|
self.is_playing = True
|
||||||
self.current_frame = 0
|
self.current_frame = 0
|
||||||
self.total_frames = 0
|
self.total_frames = 0
|
||||||
|
|
||||||
# Key repeat tracking
|
# Key repeat tracking
|
||||||
self.last_key_time = 0
|
self.last_key_time = 0
|
||||||
self.last_key = None
|
self.last_key = None
|
||||||
|
self.key_first_press_time = 0
|
||||||
|
|
||||||
# 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 * self.FAST_SEEK_MULTIPLIER
|
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",
|
||||||
|
]
|
||||||
|
|
||||||
# Create grade directories
|
# Create grade directories
|
||||||
for i in range(1, 6):
|
for i in range(1, 6):
|
||||||
grade_dir = self.directory / str(i)
|
grade_dir = self.directory / str(i)
|
||||||
grade_dir.mkdir(exist_ok=True)
|
grade_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
def find_media_files(self) -> List[Path]:
|
def find_media_files(self) -> List[Path]:
|
||||||
"""Find all media files recursively in the directory"""
|
"""Find all media files recursively in the directory"""
|
||||||
media_files = []
|
media_files = []
|
||||||
@@ -59,34 +73,42 @@ class MediaGrader:
|
|||||||
pattern = str(self.directory / "**" / ext)
|
pattern = str(self.directory / "**" / ext)
|
||||||
files = glob.glob(pattern, recursive=True)
|
files = glob.glob(pattern, recursive=True)
|
||||||
media_files.extend([Path(f) for f in files])
|
media_files.extend([Path(f) for f in files])
|
||||||
|
|
||||||
# Filter out files already in grade directories
|
# Filter out files already in grade directories
|
||||||
filtered_files = []
|
filtered_files = []
|
||||||
for file in media_files:
|
for file in media_files:
|
||||||
# Check if file is not in a grade directory (1-5)
|
# Check if file is not in a grade directory (1-5)
|
||||||
if not any(part in ['1', '2', '3', '4', '5'] for part in file.parts):
|
if not any(part in ["1", "2", "3", "4", "5"] for part in file.parts):
|
||||||
filtered_files.append(file)
|
filtered_files.append(file)
|
||||||
|
|
||||||
return sorted(filtered_files)
|
return sorted(filtered_files)
|
||||||
|
|
||||||
def is_video(self, file_path: Path) -> bool:
|
def is_video(self, file_path: Path) -> bool:
|
||||||
"""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:
|
def calculate_frame_delay(self) -> int:
|
||||||
"""Calculate frame delay in milliseconds based on playback speed"""
|
"""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
|
# Base delay for 30 FPS, adjusted by playback speed
|
||||||
delay_ms = int(self.BASE_FRAME_DELAY_MS / self.playback_speed)
|
delay_ms = int(self.BASE_FRAME_DELAY_MS / self.playback_speed)
|
||||||
return max(1, delay_ms) # Minimum 1ms delay
|
return max(1, delay_ms) # Minimum 1ms delay
|
||||||
|
|
||||||
|
def calculate_frames_to_skip(self) -> int:
|
||||||
|
"""Calculate how many frames to skip for high-speed playback"""
|
||||||
|
if self.playback_speed <= 1.0:
|
||||||
|
return 0
|
||||||
|
elif self.playback_speed <= 2.0:
|
||||||
|
return 0 # No skipping for moderate speeds
|
||||||
|
elif self.playback_speed <= 5.0:
|
||||||
|
return int(self.playback_speed - 1) # Skip some frames
|
||||||
|
else:
|
||||||
|
return int(self.playback_speed * 2) # Skip many frames for very high speeds
|
||||||
|
|
||||||
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:
|
||||||
self.current_cap.release()
|
self.current_cap.release()
|
||||||
|
|
||||||
if self.is_video(file_path):
|
if self.is_video(file_path):
|
||||||
self.current_cap = cv2.VideoCapture(str(file_path))
|
self.current_cap = cv2.VideoCapture(str(file_path))
|
||||||
if not self.current_cap.isOpened():
|
if not self.current_cap.isOpened():
|
||||||
@@ -98,19 +120,23 @@ class MediaGrader:
|
|||||||
self.current_cap = None
|
self.current_cap = None
|
||||||
self.total_frames = 1
|
self.total_frames = 1
|
||||||
self.current_frame = 0
|
self.current_frame = 0
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def display_media(self, file_path: Path) -> Optional[Tuple[bool, any]]:
|
def display_media(self, file_path: Path) -> Optional[Tuple[bool, any]]:
|
||||||
"""Display current media file"""
|
"""Display current media file"""
|
||||||
if self.is_video(file_path):
|
if self.is_video(file_path):
|
||||||
if not self.current_cap:
|
if not self.current_cap:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
ret, frame = self.current_cap.read()
|
# For high-speed playback, skip frames
|
||||||
if not ret:
|
frames_to_skip = self.calculate_frames_to_skip()
|
||||||
return False, None
|
|
||||||
|
for _ in range(frames_to_skip + 1): # +1 to read at least one frame
|
||||||
|
ret, frame = self.current_cap.read()
|
||||||
|
if not ret:
|
||||||
|
return False, None
|
||||||
|
|
||||||
self.current_frame = int(self.current_cap.get(cv2.CAP_PROP_POS_FRAMES))
|
self.current_frame = int(self.current_cap.get(cv2.CAP_PROP_POS_FRAMES))
|
||||||
return True, frame
|
return True, frame
|
||||||
else:
|
else:
|
||||||
@@ -119,78 +145,110 @@ class MediaGrader:
|
|||||||
if frame is None:
|
if frame is None:
|
||||||
return False, None
|
return False, None
|
||||||
return True, frame
|
return True, frame
|
||||||
|
|
||||||
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"""
|
||||||
height, width = frame.shape[:2]
|
height, width = frame.shape[:2]
|
||||||
|
|
||||||
# Calculate scaling factor to fit within max dimensions
|
# Calculate scaling factor to fit within max dimensions
|
||||||
scale_w = self.WINDOW_MAX_WIDTH / width if width > self.WINDOW_MAX_WIDTH else 1.0
|
scale_w = (
|
||||||
scale_h = self.WINDOW_MAX_HEIGHT / height if height > self.WINDOW_MAX_HEIGHT else 1.0
|
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)
|
scale = min(scale_w, scale_h)
|
||||||
|
|
||||||
# Don't scale up small images too much
|
# Don't scale up small images too much
|
||||||
if scale > self.WINDOW_MAX_SCALE_UP:
|
if scale > self.WINDOW_MAX_SCALE_UP:
|
||||||
scale = self.WINDOW_MAX_SCALE_UP
|
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)
|
||||||
|
|
||||||
cv2.resizeWindow('Media Grader', new_width, new_height)
|
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]
|
||||||
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
new_frame = max(0, min(self.current_frame + frames_delta, self.total_frames - 1))
|
new_frame = max(
|
||||||
|
0, min(self.current_frame + frames_delta, self.total_frames - 1)
|
||||||
|
)
|
||||||
|
|
||||||
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 % 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
|
self.current_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:
|
||||||
"""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 key and timing
|
# Determine seek amount based on key and timing
|
||||||
seek_amount = 0
|
seek_amount = 0
|
||||||
|
is_arrow_key = False
|
||||||
|
|
||||||
# Try different arrow key detection methods
|
# Try different arrow key detection methods
|
||||||
if key == 2424832 or key == 81: # Left arrow (different systems)
|
if key == ord("a"): # Left arrow (various systems)
|
||||||
if self.last_key == key and (current_time - self.last_key_time) < self.KEY_REPEAT_THRESHOLD_SEC:
|
is_arrow_key = True
|
||||||
seek_amount = -self.fast_seek_frames
|
direction = -1
|
||||||
else:
|
elif key == ord("d"): # Right arrow (various systems)
|
||||||
seek_amount = -self.coarse_seek_frames
|
is_arrow_key = True
|
||||||
elif key == 2555904 or key == 83: # Right arrow (different systems)
|
direction = 1
|
||||||
if self.last_key == key and (current_time - self.last_key_time) < self.KEY_REPEAT_THRESHOLD_SEC:
|
elif key == ord(","): # Comma - fine seek backward
|
||||||
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
|
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
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.seek_video(seek_amount)
|
if is_arrow_key:
|
||||||
self.last_key = key
|
# Track key press timing for fast seek detection
|
||||||
self.last_key_time = current_time
|
if self.last_key != key:
|
||||||
return True
|
# New key press
|
||||||
|
self.key_first_press_time = current_time
|
||||||
|
self.last_key = key
|
||||||
|
seek_amount = direction * self.coarse_seek_frames
|
||||||
|
else:
|
||||||
|
# Repeated key press
|
||||||
|
time_held = current_time - self.key_first_press_time
|
||||||
|
time_since_last = current_time - self.last_key_time
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"Key held for {time_held:.2f}s, since last: {time_since_last:.2f}s"
|
||||||
|
)
|
||||||
|
|
||||||
|
if time_held > self.FAST_SEEK_ACTIVATION_TIME:
|
||||||
|
# Fast seek mode
|
||||||
|
seek_amount = direction * self.fast_seek_frames
|
||||||
|
print(f"FAST SEEK: {seek_amount} frames")
|
||||||
|
else:
|
||||||
|
# Normal seek
|
||||||
|
seek_amount = direction * self.coarse_seek_frames
|
||||||
|
|
||||||
|
if seek_amount != 0:
|
||||||
|
self.seek_video(seek_amount)
|
||||||
|
self.last_key_time = current_time
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
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:
|
||||||
return
|
return
|
||||||
|
|
||||||
current_file = self.media_files[self.current_index]
|
current_file = self.media_files[self.current_index]
|
||||||
grade_dir = self.directory / str(grade)
|
grade_dir = self.directory / str(grade)
|
||||||
destination = grade_dir / current_file.name
|
destination = grade_dir / current_file.name
|
||||||
|
|
||||||
# Handle name conflicts
|
# Handle name conflicts
|
||||||
counter = 1
|
counter = 1
|
||||||
while destination.exists():
|
while destination.exists():
|
||||||
@@ -198,140 +256,215 @@ class MediaGrader:
|
|||||||
suffix = current_file.suffix
|
suffix = current_file.suffix
|
||||||
destination = grade_dir / f"{stem}_{counter}{suffix}"
|
destination = grade_dir / f"{stem}_{counter}{suffix}"
|
||||||
counter += 1
|
counter += 1
|
||||||
|
|
||||||
try:
|
try:
|
||||||
shutil.move(str(current_file), str(destination))
|
shutil.move(str(current_file), str(destination))
|
||||||
print(f"Moved {current_file.name} to grade {grade}")
|
print(f"Moved {current_file.name} to grade {grade}")
|
||||||
|
|
||||||
# Remove from current list
|
# Remove from current list
|
||||||
self.media_files.pop(self.current_index)
|
self.media_files.pop(self.current_index)
|
||||||
|
|
||||||
# Adjust current index
|
# Adjust current index
|
||||||
if self.current_index >= len(self.media_files):
|
if self.current_index >= len(self.media_files):
|
||||||
self.current_index = 0
|
self.current_index = 0
|
||||||
|
|
||||||
if not self.media_files:
|
if not self.media_files:
|
||||||
print("No more media files to grade!")
|
print("No more media files to grade!")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error moving file: {e}")
|
print(f"Error moving file: {e}")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""Main grading loop"""
|
"""Main grading loop"""
|
||||||
self.media_files = self.find_media_files()
|
self.media_files = self.find_media_files()
|
||||||
|
|
||||||
if not self.media_files:
|
if not self.media_files:
|
||||||
print("No media files found in directory!")
|
print("No media files found in directory!")
|
||||||
return
|
return
|
||||||
|
|
||||||
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 (accelerates on repeat)")
|
print(" Left/Right: 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(" 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")
|
||||||
print(" P: Previous file")
|
print(" P: Previous file")
|
||||||
print(" Q/ESC: Quit")
|
print(" Q/ESC: Quit")
|
||||||
|
|
||||||
cv2.namedWindow('Media Grader', cv2.WINDOW_NORMAL)
|
cv2.namedWindow("Media Grader", cv2.WINDOW_NORMAL)
|
||||||
|
|
||||||
while self.media_files and self.current_index < len(self.media_files):
|
while self.media_files and self.current_index < len(self.media_files):
|
||||||
current_file = self.media_files[self.current_index]
|
current_file = self.media_files[self.current_index]
|
||||||
|
|
||||||
if not self.load_media(current_file):
|
if not self.load_media(current_file):
|
||||||
print(f"Could not load {current_file}")
|
print(f"Could not load {current_file}")
|
||||||
self.current_index += 1
|
self.current_index += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
window_resized = False
|
window_resized = False
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
# Only advance frame if playing (for videos)
|
# Always try to get and display a frame (for seeking while paused)
|
||||||
if self.is_playing or not self.is_video(current_file):
|
result = self.display_media(current_file)
|
||||||
result = self.display_media(current_file)
|
if result is None or not result[0]:
|
||||||
if result is None or not result[0]:
|
break
|
||||||
break
|
|
||||||
|
ret, frame = result
|
||||||
ret, frame = result
|
|
||||||
|
# Auto-resize window on first frame
|
||||||
# Auto-resize window on first frame
|
if not window_resized:
|
||||||
if not window_resized:
|
self.auto_resize_window(frame)
|
||||||
self.auto_resize_window(frame)
|
window_resized = True
|
||||||
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: ←→ (hold=FAST) ,. (fine) | A/D speed | 1-5 grade | Space pause | Q quit"
|
||||||
help_text = "Seek: ←→ (accel) ,. (fine) | A/D speed | 1-5 grade | Space pause | Q quit"
|
|
||||||
|
# White background for text visibility
|
||||||
# White background for text visibility
|
cv2.putText(
|
||||||
cv2.putText(frame, info_text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
|
frame,
|
||||||
cv2.putText(frame, info_text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 0), 1)
|
info_text,
|
||||||
cv2.putText(frame, help_text, (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2)
|
(10, 30),
|
||||||
cv2.putText(frame, help_text, (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)
|
cv2.FONT_HERSHEY_SIMPLEX,
|
||||||
|
0.7,
|
||||||
cv2.imshow('Media Grader', frame)
|
(255, 255, 255),
|
||||||
|
2,
|
||||||
# Calculate appropriate delay
|
)
|
||||||
delay = self.calculate_frame_delay() if self.is_video(current_file) else self.IMAGE_DISPLAY_DELAY_MS
|
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 - shorter for paused videos to enable seeking
|
||||||
|
if self.is_video(current_file):
|
||||||
|
if self.is_playing:
|
||||||
|
delay = self.calculate_frame_delay()
|
||||||
|
else:
|
||||||
|
delay = (
|
||||||
|
30 # Short delay when paused to enable responsive seeking
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
delay = self.IMAGE_DISPLAY_DELAY_MS
|
||||||
|
|
||||||
key = cv2.waitKey(delay) & 0xFF
|
key = cv2.waitKey(delay) & 0xFF
|
||||||
|
|
||||||
# Debug: print key codes to help with arrow key detection
|
# Debug: print key codes to help with arrow key detection
|
||||||
if key != 255: # 255 means no key pressed
|
if key != 255: # 255 means no key pressed
|
||||||
print(f"Key pressed: {key}")
|
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
|
||||||
elif key == ord('a'): # A - decrease speed
|
print(f"{'Playing' if self.is_playing else 'Paused'}")
|
||||||
self.playback_speed = max(self.MIN_PLAYBACK_SPEED, self.playback_speed - self.SPEED_INCREMENT)
|
elif key == ord("w"): # A - decrease speed
|
||||||
elif key == ord('d'): # D - increase speed
|
self.playback_speed = max(
|
||||||
self.playback_speed = min(self.MAX_PLAYBACK_SPEED, self.playback_speed + self.SPEED_INCREMENT)
|
self.MIN_PLAYBACK_SPEED,
|
||||||
|
self.playback_speed - self.SPEED_INCREMENT,
|
||||||
|
)
|
||||||
|
print(f"Speed: {self.playback_speed:.1f}x")
|
||||||
|
elif key == ord("s"): # D - increase speed
|
||||||
|
self.playback_speed = min(
|
||||||
|
self.MAX_PLAYBACK_SPEED,
|
||||||
|
self.playback_speed + self.SPEED_INCREMENT,
|
||||||
|
)
|
||||||
|
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
|
||||||
pass
|
pass
|
||||||
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
|
||||||
self.current_index = max(0, self.current_index - 1)
|
self.current_index = max(0, self.current_index - 1)
|
||||||
break
|
break
|
||||||
elif key in [ord('1'), ord('2'), ord('3'), ord('4'), ord('5')]: # Grade
|
elif key in [ord("1"), ord("2"), ord("3"), ord("4"), ord("5")]: # Grade
|
||||||
grade = int(chr(key))
|
grade = int(chr(key))
|
||||||
if not self.grade_media(grade):
|
if not self.grade_media(grade):
|
||||||
return
|
return
|
||||||
break
|
break
|
||||||
|
elif key == 255: # No key pressed
|
||||||
if key not in [ord('p')]: # Don't increment for previous
|
# Reset key tracking if no key is pressed
|
||||||
|
if self.last_key is not None:
|
||||||
|
self.last_key = None
|
||||||
|
print("Key released")
|
||||||
|
|
||||||
|
# Only advance to next frame if playing AND it's a video
|
||||||
|
if not self.is_playing and self.is_video(current_file):
|
||||||
|
# When paused, seek back one frame to stay on current frame
|
||||||
|
# (since display_media already advanced us)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if key not in [ord("p")]: # Don't increment for previous
|
||||||
self.current_index += 1
|
self.current_index += 1
|
||||||
|
|
||||||
if self.current_cap:
|
if self.current_cap:
|
||||||
self.current_cap.release()
|
self.current_cap.release()
|
||||||
cv2.destroyAllWindows()
|
cv2.destroyAllWindows()
|
||||||
|
|
||||||
print("Grading session complete!")
|
print("Grading session complete!")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description='Media Grader - Grade media files by moving them to numbered folders')
|
parser = argparse.ArgumentParser(
|
||||||
parser.add_argument('directory', nargs='?', default='.', help='Directory to scan for media files (default: current directory)')
|
description="Media Grader - Grade media files by moving them to numbered folders"
|
||||||
parser.add_argument('--seek-frames', type=int, default=30, help='Number of frames to seek when using arrow keys (default: 30)')
|
)
|
||||||
parser.add_argument('--snap-to-iframe', action='store_true', help='Snap to I-frames when seeking backward for better performance')
|
parser.add_argument(
|
||||||
|
"directory",
|
||||||
|
nargs="?",
|
||||||
|
default=".",
|
||||||
|
help="Directory to scan for media files (default: current directory)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--seek-frames",
|
||||||
|
type=int,
|
||||||
|
default=30,
|
||||||
|
help="Number of frames to seek when using arrow keys (default: 30)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--snap-to-iframe",
|
||||||
|
action="store_true",
|
||||||
|
help="Snap to I-frames when seeking backward for better performance",
|
||||||
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if not os.path.isdir(args.directory):
|
if not os.path.isdir(args.directory):
|
||||||
print(f"Error: {args.directory} is not a valid directory")
|
print(f"Error: {args.directory} is not a valid directory")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
grader = MediaGrader(args.directory, args.seek_frames, args.snap_to_iframe)
|
grader = MediaGrader(args.directory, args.seek_frames, args.snap_to_iframe)
|
||||||
try:
|
try:
|
||||||
grader.run()
|
grader.run()
|
||||||
|
Reference in New Issue
Block a user