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
2671Get 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
3075Calculate 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
3479Cap 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
4085Extract 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
4388Count 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
4792Ensure 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
58103Now I'll help you find the best selfie frame using a tournament-style selection process.
59104
60105First, 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
64109I'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:
90135After identifying the best frame from Round 1, perform fine-grained refinement:
91136
92137Calculate 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
97142Determine 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
110155Calculate 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
115160Extract 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
131176After 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