Skip to content

Commit 2b81d87

Browse files
committed
feat: add batch processing support for multiple videos
Extends extract-best-frame procedure to handle multiple videos in a single invocation. Videos are processed sequentially with unique output directories for each. **New Capabilities:** - Accept newline-separated list of video paths - Process videos sequentially (one after another) - Create unique output directories per video (FRAMES_DIR/video_name/) - Track and report results for all processed videos - Batch summary with success/failure status for each video **Invocation Format:** Single video (backward compatible): extract-best-frame /path/to/video.mp4 Multiple videos (newline-separated): extract-best-frame /path/to/video1.mp4 /path/to/video2.mp4 /path/to/video3.mp4 **Future Enhancement:** - Documented TODO for concurrent processing - Videos are independent and could be processed in parallel - Current sequential implementation prioritizes simplicity **Structure Changes:** - Added Step 0: Parse and Initialize Batch Processing - Wrapped Steps 1-6 in for-loop over all videos - Added Step 7: Batch Summary reporting - All paths now use video-specific directories (VIDEO_FRAMES_DIR, VIDEO_OUTPUT_DIR)
1 parent 129e455 commit 2b81d87

1 file changed

Lines changed: 131 additions & 55 deletions

File tree

knowledge/procedures/extract-best-frame-procedure.md

Lines changed: 131 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -10,56 +10,101 @@
1010
# - Round 2: Adapts window size based on video length
1111
# - Videos <10s: ±0.5s window at 20 fps for tight precision
1212
# - Videos ≥10s: ±1.0s window at 10 fps for standard refinement
13+
#
14+
# BATCH PROCESSING:
15+
# - Supports single or multiple videos (newline-separated paths)
16+
# - Processes videos sequentially (one after another)
17+
# - Creates unique output directories per video
18+
# - TODO: Future enhancement for concurrent processing (videos are independent)
1319

1420
## Invocation
15-
- Primary command: "extract-best-frame <video_path> [<frames_dir>] [<output_dir>]"
16-
- Alternative formats: "best-frame <video_path>"
21+
- Primary command: "extract-best-frame <video_path(s)> [<frames_dir>] [<output_dir>]"
22+
- Alternative formats: "best-frame <video_path(s)>"
23+
- Single video: "extract-best-frame /path/to/video.mp4"
24+
- Multiple videos (newline-separated):
25+
```
26+
extract-best-frame /path/to/video1.mp4
27+
/path/to/video2.mp4
28+
/path/to/video3.mp4
29+
```
1730
- Optional selection criteria: Any trailing text after the arguments should be treated as guidance (preferences, qualities to optimize for) and incorporated with graceful flexibility.
1831

19-
## Step 1: Validate Input
20-
21-
Ensure the video file exists:
22-
!test -f "$VIDEO_PATH" || { echo "Error: Video file not found: $VIDEO_PATH"; exit 1; }
32+
## Step 0: Parse and Initialize Batch Processing
33+
34+
Parse video paths (supports single or multiple newline-separated videos):
35+
!# Read all video paths into an array
36+
!mapfile -t VIDEO_PATHS < <(echo "$VIDEO_INPUT" | grep -v '^$' | grep '\.mp4$\|\.mov$\|\.avi$\|\.mkv$')
37+
!TOTAL_VIDEOS=${#VIDEO_PATHS[@]}
38+
!echo "Found $TOTAL_VIDEOS video(s) to process"
39+
40+
Initialize batch tracking:
41+
!declare -a BATCH_RESULTS
42+
!CURRENT_VIDEO=0
43+
44+
## Step 1: Batch Processing Loop
45+
46+
For each video, process sequentially:
47+
!for VIDEO_PATH in "${VIDEO_PATHS[@]}"; do
48+
! CURRENT_VIDEO=$((CURRENT_VIDEO + 1))
49+
! echo ""
50+
! echo "=========================================="
51+
! echo "Processing video $CURRENT_VIDEO of $TOTAL_VIDEOS"
52+
! echo "=========================================="
53+
! echo "Video: $VIDEO_PATH"
54+
55+
Validate video file exists:
56+
! if [ ! -f "$VIDEO_PATH" ]; then
57+
! echo "Error: Video file not found: $VIDEO_PATH"
58+
! BATCH_RESULTS+=("FAILED: $VIDEO_PATH (file not found)")
59+
! continue
60+
! fi
61+
62+
Extract video filename for unique output naming:
63+
! VIDEO_NAME=$(basename "$VIDEO_PATH" | sed 's/\.[^.]*$//')
64+
! VIDEO_FRAMES_DIR="${FRAMES_DIR}/${VIDEO_NAME}"
65+
! VIDEO_OUTPUT_DIR="${OUTPUT_DIR}/${VIDEO_NAME}"
66+
! mkdir -p "$VIDEO_FRAMES_DIR" "$VIDEO_OUTPUT_DIR"
67+
! echo "Output will be saved to: $VIDEO_OUTPUT_DIR"
2368

2469
## Step 2: Extract Frames (Adaptive)
2570

2671
Get video duration to calculate optimal frame extraction rate:
27-
!DURATION=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$VIDEO_PATH")
28-
!echo "Video duration: ${DURATION}s"
72+
! DURATION=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$VIDEO_PATH")
73+
! echo "Video duration: ${DURATION}s"
2974

3075
Calculate adaptive FPS targeting 20-50 frames for Round 1:
31-
!TARGET_FRAMES=30
32-
!FPS=$(echo "scale=3; $TARGET_FRAMES / $DURATION" | bc)
76+
! TARGET_FRAMES=30
77+
! FPS=$(echo "scale=3; $TARGET_FRAMES / $DURATION" | bc)
3378

3479
Cap FPS between reasonable bounds (0.1 to 2.0 fps):
35-
!if (( $(echo "$FPS > 2.0" | bc -l) )); then FPS=2.0; fi
36-
!if (( $(echo "$FPS < 0.1" | bc -l) )); then FPS=0.1; fi
37-
!FRAME_INTERVAL=$(echo "scale=2; 1 / $FPS" | bc)
38-
!echo "Using adaptive FPS: $FPS (1 frame every ${FRAME_INTERVAL}s)"
80+
! if (( $(echo "$FPS > 2.0" | bc -l) )); then FPS=2.0; fi
81+
! if (( $(echo "$FPS < 0.1" | bc -l) )); then FPS=0.1; fi
82+
! FRAME_INTERVAL=$(echo "scale=2; 1 / $FPS" | bc)
83+
! echo "Using adaptive FPS: $FPS (1 frame every ${FRAME_INTERVAL}s)"
3984

4085
Extract frames at adaptive intervals:
41-
!ffmpeg -i "$VIDEO_PATH" -vf "fps=$FPS" -q:v 2 "$FRAMES_DIR/frame_%04d.jpg" -loglevel error
86+
! ffmpeg -i "$VIDEO_PATH" -vf "fps=$FPS" -q:v 2 "$VIDEO_FRAMES_DIR/frame_%04d.jpg" -loglevel error
4287

4388
Count the extracted frames:
44-
!FRAME_COUNT=$(ls -1 "$FRAMES_DIR"/frame_*.jpg 2>/dev/null | wc -l)
45-
!echo "Extracted $FRAME_COUNT frames from video"
89+
! FRAME_COUNT=$(ls -1 "$VIDEO_FRAMES_DIR"/frame_*.jpg 2>/dev/null | wc -l)
90+
! echo "Extracted $FRAME_COUNT frames from video"
4691

4792
Ensure minimum frame coverage for very short videos:
48-
!MIN_FRAMES=10
49-
!if [ "$FRAME_COUNT" -lt "$MIN_FRAMES" ]; then
50-
! echo "Warning: Only $FRAME_COUNT frames extracted. Re-extracting with higher FPS for better coverage..."
51-
! ffmpeg -i "$VIDEO_PATH" -vf "fps=2.0" -q:v 2 "$FRAMES_DIR/frame_%04d.jpg" -loglevel error -y
52-
! FRAME_COUNT=$(ls -1 "$FRAMES_DIR"/frame_*.jpg 2>/dev/null | wc -l)
53-
! echo "Re-extracted $FRAME_COUNT frames for analysis"
54-
!fi
93+
! MIN_FRAMES=10
94+
! if [ "$FRAME_COUNT" -lt "$MIN_FRAMES" ]; then
95+
! echo "Warning: Only $FRAME_COUNT frames extracted. Re-extracting with higher FPS for better coverage..."
96+
! ffmpeg -i "$VIDEO_PATH" -vf "fps=2.0" -q:v 2 "$VIDEO_FRAMES_DIR/frame_%04d.jpg" -loglevel error -y
97+
! FRAME_COUNT=$(ls -1 "$VIDEO_FRAMES_DIR"/frame_*.jpg 2>/dev/null | wc -l)
98+
! echo "Re-extracted $FRAME_COUNT frames for analysis"
99+
! fi
55100

56101
## Step 3: Tournament Selection Using Claude
57102

58103
Now I'll help you find the best selfie frame using a tournament-style selection process.
59104

60105
First, let me see all the extracted frames to understand what we're working with:
61106

62-
!ls -1 "$FRAMES_DIR"/frame_*.jpg | head -20
107+
! ls -1 "$VIDEO_FRAMES_DIR"/frame_*.jpg | head -20
63108

64109
I'll now conduct a tournament where I compare pairs of frames to find the most flattering selfie.
65110

@@ -71,7 +116,7 @@ Let me start by comparing frames in pairs. For each pair, I'll select the more f
71116
- Overall pose and composition
72117
- Image clarity and lighting
73118

74-
!echo "Starting tournament selection..."
119+
! echo "Starting tournament selection..."
75120

76121
## Step 4: Claude's Visual Comparison
77122

@@ -90,31 +135,31 @@ The selection process:
90135
After identifying the best frame from Round 1, perform fine-grained refinement:
91136

92137
Calculate the timestamp of the Round 1 winner and extract refined frames:
93-
!WINNER_NUMBER=$(echo "$BEST_FRAME" | grep -o '[0-9]\+')
94-
!WINNER_TIME=$(echo "scale=2; $WINNER_NUMBER * $FRAME_INTERVAL" | bc)
95-
!echo "Round 1 winner is at approximately ${WINNER_TIME}s in the video"
138+
! WINNER_NUMBER=$(echo "$BEST_FRAME" | grep -o '[0-9]\+')
139+
! WINNER_TIME=$(echo "scale=2; $WINNER_NUMBER * $FRAME_INTERVAL" | bc)
140+
! echo "Round 1 winner is at approximately ${WINNER_TIME}s in the video"
96141

97142
Determine adaptive Round 2 parameters based on video duration:
98-
!if (( $(echo "$DURATION < 10" | bc -l) )); then
99-
! # Short videos: tighter window, higher precision
100-
! WINDOW=0.5
101-
! ROUND2_FPS=20
102-
! echo "Short video detected: Using ±${WINDOW}s window with ${ROUND2_FPS} fps"
103-
!else
104-
! # Longer videos: standard window
105-
! WINDOW=1.0
106-
! ROUND2_FPS=10
107-
! echo "Using standard ±${WINDOW}s window with ${ROUND2_FPS} fps"
108-
!fi
143+
! if (( $(echo "$DURATION < 10" | bc -l) )); then
144+
! # Short videos: tighter window, higher precision
145+
! WINDOW=0.5
146+
! ROUND2_FPS=20
147+
! echo "Short video detected: Using ±${WINDOW}s window with ${ROUND2_FPS} fps"
148+
! else
149+
! # Longer videos: standard window
150+
! WINDOW=1.0
151+
! ROUND2_FPS=10
152+
! echo "Using standard ±${WINDOW}s window with ${ROUND2_FPS} fps"
153+
! fi
109154

110155
Calculate Round 2 extraction window:
111-
!START_TIME=$(echo "scale=2; if ($WINNER_TIME - $WINDOW < 0) 0 else $WINNER_TIME - $WINDOW" | bc)
112-
!DURATION_R2=$(echo "scale=2; $WINDOW * 2" | bc)
113-
!mkdir -p "$FRAMES_DIR/round2"
156+
! START_TIME=$(echo "scale=2; if ($WINNER_TIME - $WINDOW < 0) 0 else $WINNER_TIME - $WINDOW" | bc)
157+
! DURATION_R2=$(echo "scale=2; $WINDOW * 2" | bc)
158+
! mkdir -p "$VIDEO_FRAMES_DIR/round2"
114159

115160
Extract refined frames around the winner:
116-
!ffmpeg -ss $START_TIME -i "$VIDEO_PATH" -t $DURATION_R2 -vf "fps=$ROUND2_FPS" -q:v 2 "$FRAMES_DIR/round2/refined_%03d.jpg" -loglevel error
117-
!echo "Extracted $(ls -1 "$FRAMES_DIR"/round2/*.jpg 2>/dev/null | wc -l) refined frames for Round 2"
161+
! ffmpeg -ss $START_TIME -i "$VIDEO_PATH" -t $DURATION_R2 -vf "fps=$ROUND2_FPS" -q:v 2 "$VIDEO_FRAMES_DIR/round2/refined_%03d.jpg" -loglevel error
162+
! echo "Extracted $(ls -1 "$VIDEO_FRAMES_DIR"/round2/*.jpg 2>/dev/null | wc -l) refined frames for Round 2"
118163

119164
[Claude will compare the refined frames to find the absolute best moment, capturing micro-expressions and perfect timing]
120165

@@ -124,24 +169,55 @@ The refined selection captures:
124169
- Perfect food/cooking action shots
125170
- Ideal environmental atmosphere
126171

127-
!echo "Round 2 complete: Found best frame with sub-second precision"
172+
! echo "Round 2 complete: Found best frame with sub-second precision"
128173

129174
## Step 5: Save the Best Frame
130175

131176
After the selection process, the winning frame will be copied to the output directory:
132-
!cp "$FRAMES_DIR/$BEST_FRAME" "$OUTPUT_DIR/${VIDEO_NAME}_best_frame.jpg"
133-
!echo "Best frame saved to: $OUTPUT_DIR/${VIDEO_NAME}_best_frame.jpg"
177+
! BEST_FRAME_OUTPUT="$VIDEO_OUTPUT_DIR/${VIDEO_NAME}_best_frame.jpg"
178+
! cp "$VIDEO_FRAMES_DIR/$BEST_FRAME" "$BEST_FRAME_OUTPUT"
179+
! echo "✓ Best frame saved to: $BEST_FRAME_OUTPUT"
180+
181+
Track result for batch summary:
182+
! BATCH_RESULTS+=("SUCCESS: $VIDEO_NAME → $BEST_FRAME_OUTPUT")
183+
184+
## Step 6: Cleanup (Per Video)
185+
186+
Keep frames for review (remove manually if not needed):
187+
! echo "Frames kept in $VIDEO_FRAMES_DIR for review"
134188

135-
## Step 6: Cleanup
189+
Close the batch processing loop:
190+
!done
136191

137-
Remove temporary frames directory (optional):
138-
!echo "Keeping frames in $FRAMES_DIR for review. Remove manually if not needed."
192+
## Step 7: Batch Summary
193+
194+
Display summary of all processed videos:
195+
!echo ""
196+
!echo "=========================================="
197+
!echo "BATCH PROCESSING COMPLETE"
198+
!echo "=========================================="
199+
!echo "Processed $TOTAL_VIDEOS video(s)"
200+
!echo ""
201+
!echo "Results:"
202+
!for result in "${BATCH_RESULTS[@]}"; do
203+
! echo " $result"
204+
!done
205+
!echo ""
139206

140207
{{ INJECT:principles/tracer-bullets.md }}
141208

142209
## Next Steps
143210

144-
The best selfie frame has been extracted! You can:
145-
1. View it at: `$OUTPUT_DIR/${VIDEO_NAME}_best_frame.jpg`
146-
2. Review all frames in: `$FRAMES_DIR`
147-
3. Run again with different videos
211+
The best frame extraction is complete!
212+
213+
**For single video:**
214+
- View the best frame at: `$OUTPUT_DIR/${VIDEO_NAME}_best_frame.jpg`
215+
- Review all extracted frames in: `$FRAMES_DIR/${VIDEO_NAME}`
216+
217+
**For batch processing:**
218+
- All best frames saved to their respective output directories
219+
- Check the batch summary above for individual file paths
220+
- Review frames for each video in: `$FRAMES_DIR/<video_name>/`
221+
222+
**Future Improvements:**
223+
- Concurrent processing: Since videos are independent, they could be processed in parallel for significant speed improvements (currently sequential)

0 commit comments

Comments
 (0)