Compare commits
3 Commits
554b4ffc26
...
7464efbd95
Author | SHA1 | Date | |
---|---|---|---|
7464efbd95 | |||
21408779ab | |||
20b5739dbe |
311
main.py
311
main.py
@@ -79,6 +79,17 @@ class MediaGrader:
|
||||
# Undo functionality
|
||||
self.undo_history = [] # List of (source_path, destination_path, original_index) tuples
|
||||
|
||||
# Watch tracking for "good look" feature
|
||||
self.watched_regions = {} # Dict[file_path: List[Tuple[start_frame, end_frame]]]
|
||||
self.current_watch_start = None # Frame where current viewing session started
|
||||
self.last_frame_position = 0 # Track last known frame position
|
||||
|
||||
# Bisection navigation tracking
|
||||
self.last_jump_position = {} # Dict[file_path: last_frame] for bisection reference
|
||||
|
||||
# Jump history for H key (undo jump)
|
||||
self.jump_history = {} # Dict[file_path: List[frame_positions]] for jump undo
|
||||
|
||||
def find_media_files(self) -> List[Path]:
|
||||
"""Find all media files recursively in the directory"""
|
||||
media_files = []
|
||||
@@ -138,6 +149,10 @@ class MediaGrader:
|
||||
|
||||
# Load initial frame
|
||||
self.load_current_frame()
|
||||
|
||||
# Start watch tracking session for videos
|
||||
self.start_watch_session()
|
||||
|
||||
return True
|
||||
|
||||
def load_current_frame(self):
|
||||
@@ -159,6 +174,285 @@ class MediaGrader:
|
||||
return True
|
||||
return False
|
||||
|
||||
def start_watch_session(self):
|
||||
"""Start tracking a new viewing session"""
|
||||
if self.is_video(self.media_files[self.current_index]):
|
||||
self.current_watch_start = self.current_frame
|
||||
self.last_frame_position = self.current_frame
|
||||
|
||||
def update_watch_tracking(self):
|
||||
"""Update watch tracking based on current frame position"""
|
||||
if not self.is_video(self.media_files[self.current_index]) or self.current_watch_start is None:
|
||||
return
|
||||
|
||||
current_file = str(self.media_files[self.current_index])
|
||||
|
||||
# If we've moved more than a few frames from last position, record the watched region
|
||||
if abs(self.current_frame - self.last_frame_position) > 5 or \
|
||||
abs(self.current_frame - self.current_watch_start) > 30:
|
||||
|
||||
# Record the region we just watched
|
||||
start_frame = min(self.current_watch_start, self.last_frame_position)
|
||||
end_frame = max(self.current_watch_start, self.last_frame_position)
|
||||
|
||||
if current_file not in self.watched_regions:
|
||||
self.watched_regions[current_file] = []
|
||||
|
||||
# Merge with existing regions if they overlap
|
||||
self.add_watched_region(current_file, start_frame, end_frame)
|
||||
|
||||
# Start new session from current position
|
||||
self.current_watch_start = self.current_frame
|
||||
|
||||
self.last_frame_position = self.current_frame
|
||||
|
||||
def add_watched_region(self, file_path, start_frame, end_frame):
|
||||
"""Add a watched region, merging with existing overlapping regions"""
|
||||
if file_path not in self.watched_regions:
|
||||
self.watched_regions[file_path] = []
|
||||
|
||||
regions = self.watched_regions[file_path]
|
||||
new_region = [start_frame, end_frame]
|
||||
|
||||
# Merge overlapping regions
|
||||
merged = []
|
||||
for region in regions:
|
||||
if new_region[1] < region[0] or new_region[0] > region[1]:
|
||||
# No overlap
|
||||
merged.append(region)
|
||||
else:
|
||||
# Overlap, merge
|
||||
new_region[0] = min(new_region[0], region[0])
|
||||
new_region[1] = max(new_region[1], region[1])
|
||||
|
||||
merged.append(tuple(new_region))
|
||||
self.watched_regions[file_path] = merged
|
||||
|
||||
def find_largest_unwatched_region(self):
|
||||
"""Find the largest unwatched region in the current video"""
|
||||
if not self.is_video(self.media_files[self.current_index]):
|
||||
return None
|
||||
|
||||
current_file = str(self.media_files[self.current_index])
|
||||
watched = self.watched_regions.get(current_file, [])
|
||||
|
||||
if not watched:
|
||||
# No regions watched yet, return the beginning
|
||||
return (0, self.total_frames // 4)
|
||||
|
||||
# Sort watched regions by start frame
|
||||
watched.sort(key=lambda x: x[0])
|
||||
|
||||
# Find gaps between watched regions
|
||||
gaps = []
|
||||
|
||||
# Gap before first watched region
|
||||
if watched[0][0] > 0:
|
||||
gaps.append((0, watched[0][0]))
|
||||
|
||||
# Gaps between watched regions
|
||||
for i in range(len(watched) - 1):
|
||||
gap_start = watched[i][1]
|
||||
gap_end = watched[i + 1][0]
|
||||
if gap_end > gap_start:
|
||||
gaps.append((gap_start, gap_end))
|
||||
|
||||
# Gap after last watched region
|
||||
if watched[-1][1] < self.total_frames:
|
||||
gaps.append((watched[-1][1], self.total_frames))
|
||||
|
||||
if not gaps:
|
||||
# Everything watched, return None
|
||||
return None
|
||||
|
||||
# Return the largest gap
|
||||
largest_gap = max(gaps, key=lambda x: x[1] - x[0])
|
||||
return largest_gap
|
||||
|
||||
def jump_to_unwatched_region(self):
|
||||
"""Jump to the next unwatched region of the video"""
|
||||
if not self.is_video(self.media_files[self.current_index]):
|
||||
return False
|
||||
|
||||
current_file = str(self.media_files[self.current_index])
|
||||
|
||||
# Get or initialize jump counter for this file
|
||||
if not hasattr(self, 'jump_counters'):
|
||||
self.jump_counters = {}
|
||||
|
||||
if current_file not in self.jump_counters:
|
||||
self.jump_counters[current_file] = 0
|
||||
|
||||
# Define sampling strategy: divide video into segments and sample them
|
||||
segments = 8 # Divide video into 8 segments for sampling
|
||||
segment_size = self.total_frames // segments
|
||||
|
||||
if segment_size == 0:
|
||||
print("Video too short for sampling")
|
||||
return False
|
||||
|
||||
# Jump to different segments in a smart order
|
||||
# Start with 1/4, 1/2, 3/4, then fill in the gaps
|
||||
sample_points = [
|
||||
segment_size * 2, # 1/4 through
|
||||
segment_size * 4, # 1/2 through
|
||||
segment_size * 6, # 3/4 through
|
||||
segment_size * 1, # 1/8 through
|
||||
segment_size * 3, # 3/8 through
|
||||
segment_size * 5, # 5/8 through
|
||||
segment_size * 7, # 7/8 through
|
||||
0 # Beginning
|
||||
]
|
||||
|
||||
current_jump = self.jump_counters[current_file]
|
||||
|
||||
if current_jump >= len(sample_points):
|
||||
print("All sample points visited! Video fully sampled.")
|
||||
return False
|
||||
|
||||
target_frame = sample_points[current_jump]
|
||||
target_frame = min(target_frame, self.total_frames - 1)
|
||||
|
||||
# Track last position for bisection
|
||||
self.last_jump_position[current_file] = self.current_frame
|
||||
|
||||
# Track jump history for H key undo
|
||||
if current_file not in self.jump_history:
|
||||
self.jump_history[current_file] = []
|
||||
self.jump_history[current_file].append(self.current_frame)
|
||||
|
||||
# Jump to the target frame
|
||||
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, target_frame)
|
||||
self.load_current_frame()
|
||||
|
||||
# Increment jump counter
|
||||
self.jump_counters[current_file] += 1
|
||||
|
||||
# Calculate percentage through video
|
||||
percentage = (target_frame / self.total_frames) * 100
|
||||
|
||||
print(f"Sample {current_jump + 1}/{len(sample_points)}: jumped to frame {target_frame} ({percentage:.1f}% through video)")
|
||||
return True
|
||||
|
||||
def bisect_backwards(self):
|
||||
"""Bisect backwards between last position and current position"""
|
||||
if not self.is_video(self.media_files[self.current_index]):
|
||||
return False
|
||||
|
||||
current_file = str(self.media_files[self.current_index])
|
||||
|
||||
if current_file not in self.last_jump_position:
|
||||
print("No previous position to bisect from. Use L first to establish a reference point.")
|
||||
return False
|
||||
|
||||
last_pos = self.last_jump_position[current_file]
|
||||
current_pos = self.current_frame
|
||||
|
||||
if last_pos == current_pos:
|
||||
print("Already at the same position as last jump.")
|
||||
return False
|
||||
|
||||
# Calculate midpoint
|
||||
if last_pos < current_pos:
|
||||
midpoint = (last_pos + current_pos) // 2
|
||||
else:
|
||||
midpoint = (current_pos + last_pos) // 2
|
||||
|
||||
# Update last position for further bisection
|
||||
self.last_jump_position[current_file] = current_pos
|
||||
|
||||
# Jump to midpoint
|
||||
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, midpoint)
|
||||
self.load_current_frame()
|
||||
|
||||
percentage = (midpoint / self.total_frames) * 100
|
||||
print(f"Bisected backwards to frame {midpoint} ({percentage:.1f}% through video)")
|
||||
return True
|
||||
|
||||
def bisect_forwards(self):
|
||||
"""Bisect forwards between current position and next sample point"""
|
||||
if not self.is_video(self.media_files[self.current_index]):
|
||||
return False
|
||||
|
||||
current_file = str(self.media_files[self.current_index])
|
||||
|
||||
# Get next sample point
|
||||
if not hasattr(self, 'jump_counters') or current_file not in self.jump_counters:
|
||||
print("No sampling started yet. Use L first to establish sample points.")
|
||||
return False
|
||||
|
||||
# Define same sampling strategy as L key
|
||||
segments = 8
|
||||
segment_size = self.total_frames // segments
|
||||
sample_points = [
|
||||
segment_size * 2, # 1/4 through
|
||||
segment_size * 4, # 1/2 through
|
||||
segment_size * 6, # 3/4 through
|
||||
segment_size * 1, # 1/8 through
|
||||
segment_size * 3, # 3/8 through
|
||||
segment_size * 5, # 5/8 through
|
||||
segment_size * 7, # 7/8 through
|
||||
0 # Beginning
|
||||
]
|
||||
|
||||
current_jump = self.jump_counters[current_file]
|
||||
|
||||
if current_jump >= len(sample_points):
|
||||
print("All sample points visited. No forward reference point.")
|
||||
return False
|
||||
|
||||
next_sample = sample_points[current_jump]
|
||||
next_sample = min(next_sample, self.total_frames - 1)
|
||||
current_pos = self.current_frame
|
||||
|
||||
# Calculate midpoint between current and next sample
|
||||
midpoint = (current_pos + next_sample) // 2
|
||||
|
||||
if midpoint == current_pos:
|
||||
print("Already at or very close to next sample point.")
|
||||
return False
|
||||
|
||||
# Update last position for further bisection
|
||||
self.last_jump_position[current_file] = current_pos
|
||||
|
||||
# Jump to midpoint
|
||||
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, midpoint)
|
||||
self.load_current_frame()
|
||||
|
||||
percentage = (midpoint / self.total_frames) * 100
|
||||
print(f"Bisected forwards to frame {midpoint} ({percentage:.1f}% through video)")
|
||||
return True
|
||||
|
||||
def undo_jump(self):
|
||||
"""Undo the last L jump by returning to previous position"""
|
||||
if not self.is_video(self.media_files[self.current_index]):
|
||||
return False
|
||||
|
||||
current_file = str(self.media_files[self.current_index])
|
||||
|
||||
if current_file not in self.jump_history or not self.jump_history[current_file]:
|
||||
print("No jump history to undo. Use L first to establish jump points.")
|
||||
return False
|
||||
|
||||
# Get the last position before the most recent jump
|
||||
if len(self.jump_history[current_file]) < 1:
|
||||
print("No previous position to return to.")
|
||||
return False
|
||||
|
||||
# Remove the current position from history and get the previous one
|
||||
previous_position = self.jump_history[current_file].pop()
|
||||
|
||||
# Jump back to previous position
|
||||
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, previous_position)
|
||||
self.load_current_frame()
|
||||
|
||||
# Update last jump position for bisection reference
|
||||
self.last_jump_position[current_file] = previous_position
|
||||
|
||||
percentage = (previous_position / self.total_frames) * 100
|
||||
print(f"Undid jump: returned to frame {previous_position} ({percentage:.1f}% through video)")
|
||||
return True
|
||||
|
||||
def display_current_frame(self):
|
||||
"""Display the current cached frame with overlays"""
|
||||
if self.current_display_frame is None:
|
||||
@@ -282,6 +576,10 @@ class MediaGrader:
|
||||
|
||||
self.current_display_frame = frame
|
||||
self.current_frame = int(self.current_cap.get(cv2.CAP_PROP_POS_FRAMES))
|
||||
|
||||
# Update watch tracking
|
||||
self.update_watch_tracking()
|
||||
|
||||
return True
|
||||
|
||||
def seek_video(self, frames_delta: int):
|
||||
@@ -458,6 +756,10 @@ class MediaGrader:
|
||||
print(" N: Next file")
|
||||
print(" P: Previous file")
|
||||
print(" U: Undo last grading action")
|
||||
print(" L: Sample video at key points (videos only)")
|
||||
print(" H: Undo last L jump (videos only)")
|
||||
print(" J: Bisect backwards from current position (videos only)")
|
||||
print(" K: Bisect forwards toward next sample (videos only)")
|
||||
print(" Q/ESC: Quit")
|
||||
|
||||
cv2.namedWindow("Media Grader", cv2.WINDOW_NORMAL)
|
||||
@@ -512,6 +814,15 @@ class MediaGrader:
|
||||
if self.undo_last_action():
|
||||
# File was restored, reload it
|
||||
break
|
||||
elif key == ord("l"):
|
||||
# Jump to largest unwatched region
|
||||
self.jump_to_unwatched_region()
|
||||
elif key == ord("j"):
|
||||
self.bisect_backwards()
|
||||
elif key == ord("k"):
|
||||
self.bisect_forwards()
|
||||
elif key == ord("h"): # Changed from "j" to "h" for undo jump
|
||||
self.undo_jump()
|
||||
elif key in [ord("1"), ord("2"), ord("3"), ord("4"), ord("5")]:
|
||||
grade = int(chr(key))
|
||||
if not self.grade_media(grade):
|
||||
|
Reference in New Issue
Block a user