feat(main.py): implement video sampling and watch tracking features

This commit is contained in:
2025-08-19 08:30:00 +02:00
parent 554b4ffc26
commit 20b5739dbe

169
main.py
View File

@@ -79,6 +79,11 @@ 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
def find_media_files(self) -> List[Path]:
"""Find all media files recursively in the directory"""
media_files = []
@@ -138,6 +143,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 +168,158 @@ 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)
# 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 display_current_frame(self):
"""Display the current cached frame with overlays"""
if self.current_display_frame is None:
@@ -282,6 +443,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 +623,7 @@ 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(" Q/ESC: Quit")
cv2.namedWindow("Media Grader", cv2.WINDOW_NORMAL)
@@ -512,6 +678,9 @@ 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 in [ord("1"), ord("2"), ord("3"), ord("4"), ord("5")]:
grade = int(chr(key))
if not self.grade_media(grade):