@@ -27,6 +27,12 @@ description: Extract best frame from video using AI
2727# - Processes videos sequentially (one after another)
2828# - Creates unique output directories per video
2929# - TODO: Future enhancement for concurrent processing (videos are independent)
30+ #
31+ # DRY-RUN MODE:
32+ # - Preview execution plan without running commands
33+ # - Shows resolved config, planned steps, would-be bash commands
34+ # - Supports both human-readable and machine-readable (--json) output
35+ # - Usage: "extract-best-frame video.mp4 --dry-run" or "extract-best-frame video.mp4 --dry-run --json"
3036
3137## Invocation
3238- Primary command: "extract-best-frame <video_path(s)> [ <frames_dir>] [ <output_dir>] "
@@ -38,8 +44,21 @@ description: Extract best frame from video using AI
3844 /path/to/video2.mp4
3945 /path/to/video3.mp4
4046 ```
47+ - Optional modifiers:
48+ - ` --dry-run ` : Preview execution plan without running commands (shows config, planned steps, would-be commands)
49+ - ` --json ` : Output in machine-readable JSON format (useful with --dry-run for tooling)
50+ - Parsing rules:
51+ - Detect flags anywhere in user input after command name
52+ - Flags can be combined: "extract-best-frame video.mp4 --dry-run --json"
53+ - Natural language variants accepted: "dry run", "preview", "show me what would happen"
4154- Optional selection criteria: Any trailing text after the arguments should be treated as guidance (preferences, qualities to optimize for) and incorporated with graceful flexibility.
4255
56+ Examples:
57+ - "extract-best-frame video.mp4" → Normal execution
58+ - "extract-best-frame video.mp4 --dry-run" → Preview without execution
59+ - "extract-best-frame video.mp4 --dry-run --json" → Machine-readable preview
60+ - "extract-best-frame video.mp4 dry run" → Natural language variant
61+
4362## Step 0: Load User Configuration
4463
4564Load selection preferences from .agent-config.yml (Config in Environment principle):
@@ -116,6 +135,46 @@ Load selection preferences from .agent-config.yml (Config in Environment princip
116135
117136If .agent-config.yml doesn't exist or PyYAML unavailable, gracefully falls back to defaults.
118137
138+ ## Step 0b: Parse Execution Modifiers
139+
140+ Detect dry-run and JSON output flags from user input:
141+
142+ !# Parse flags from the user's command invocation
143+ !# Supports --dry-run, --json, and natural language variants
144+ !DRY_RUN=false
145+ !JSON_OUTPUT=false
146+ !
147+ !# Get the full user input (this variable is provided by the LLM context)
148+ !USER_INPUT="${USER_INPUT:-$* }"
149+ !
150+ !# Check for dry-run flag variants
151+ if echo "$USER_INPUT" | grep -qiE '(--dry-run|dry[[ :space:]] +run|preview|show[[ :space:]] +me[[ :space:]] +what[[ :space:]] +would[[ :space:]] +happen)'; then
152+ ! DRY_RUN=true
153+ !fi
154+ !
155+ !# Check for JSON output flag
156+ !if echo "$USER_INPUT" | grep -qiE '(--json)'; then
157+ ! JSON_OUTPUT=true
158+ !fi
159+ !
160+ !# If dry-run mode detected, show banner
161+ !if [ "$DRY_RUN" = "true" ] ; then
162+ ! if [ "$JSON_OUTPUT" = "true" ] ; then
163+ ! echo '{"dry_run": true, "mode": "json"}'
164+ ! else
165+ ! echo "🧠 Dry Run Mode: Extract Best Frame"
166+ ! echo "──────────────────────────────────────"
167+ ! echo "Preview mode enabled - no commands will be executed"
168+ ! echo ""
169+ ! fi
170+ !fi
171+
172+ The dry-run mode will:
173+ - Show all resolved configuration values
174+ - Display planned execution steps with calculations
175+ - Preview would-be bash commands without executing them
176+ - Output either human-readable format (default) or JSON (with --json flag)
177+
119178## Step 1: Parse and Initialize Batch Processing
120179
121180Parse video paths (supports single or multiple newline-separated videos):
@@ -128,7 +187,144 @@ Initialize batch tracking:
128187!declare -a BATCH_RESULTS
129188!CURRENT_VIDEO=0
130189
131- ## Step 1: Batch Processing Loop
190+ ## Step 1b: Dry-Run Mode Output (If Enabled)
191+
192+ If dry-run mode is active, show the execution plan instead of running commands:
193+
194+ !if [ "$DRY_RUN" = "true" ] ; then
195+ ! # For each video, show what would happen
196+ ! for VIDEO_PATH in "${VIDEO_PATHS[ @] }"; do
197+ ! CURRENT_VIDEO=$((CURRENT_VIDEO + 1))
198+ !
199+ ! # Get video metadata (safe to run in dry-run - read-only operation)
200+ ! if [ -f "$VIDEO_PATH" ] ; then
201+ ! DURATION=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1: nokey =1 "$VIDEO_PATH" 2>/dev/null || echo "unknown")
202+ ! else
203+ ! DURATION="video not found"
204+ ! fi
205+ !
206+ ! VIDEO_NAME=$(basename "$VIDEO_PATH" | sed 's/\. [ ^ . ] * $//')
207+ !
208+ ! # Calculate planned values
209+ ! TARGET_FRAMES=30
210+ ! if [ "$DURATION" != "unknown" ] && [ "$DURATION" != "video not found" ] ; then
211+ # Check if bc is available for calculations
212+ if command -v bc >/dev/null 2>&1; then
213+ FPS=$(echo "scale=3; $TARGET_FRAMES / $DURATION" | bc)
214+ if (( $(echo "$FPS > 2.0" | bc -l) )); then FPS=2.0; fi
215+ if (( $(echo "$FPS < 0.1" | bc -l) )); then FPS=0.1; fi
216+ FRAME_INTERVAL=$(echo "scale=2; 1 / $FPS" | bc)
217+ else
218+ # Fallback: use awk for calculations if bc unavailable
219+ FPS=$(awk "BEGIN {fps = $TARGET_FRAMES / $DURATION; if (fps > 2.0) fps = 2.0; if (fps < 0.1) fps = 0.1; printf \" %.3f\" , fps}")
220+ FRAME_INTERVAL=$(awk "BEGIN {printf \" %.2f\" , 1 / $FPS}")
221+ fi
222+ !
223+ ! # Round 2 parameters
224+ ! if (( $(echo "$DURATION < 10" | bc -l) )); then
225+ ! WINDOW=0.5
226+ ! ROUND2_FPS=20
227+ ! else
228+ ! WINDOW=1.0
229+ ! ROUND2_FPS=10
230+ ! fi
231+ ! else
232+ ! FPS="N/A"
233+ ! FRAME_INTERVAL="N/A"
234+ ! WINDOW="N/A"
235+ ! ROUND2_FPS="N/A"
236+ ! fi
237+ !
238+ # Define directory paths (should match the main execution logic)
239+ FRAMES_DIR="${FRAMES_DIR:-/tmp/extract-best-frame/frames}"
240+ OUTPUT_DIR="${OUTPUT_DIR:-/tmp/extract-best-frame/output}"
241+
242+ VIDEO_FRAMES_DIR="${FRAMES_DIR}/${VIDEO_NAME}"
243+ VIDEO_OUTPUT_DIR="${OUTPUT_DIR}/${VIDEO_NAME}"
244+ !
245+ ! # Output based on mode
246+ ! if [ "$JSON_OUTPUT" = "true" ] ; then
247+ ! # JSON format output
248+ ! cat <<EOF
249+ !{
250+ ! "dry_run": true,
251+ ! "command": "extract-best-frame",
252+ ! "video_index": $CURRENT_VIDEO,
253+ ! "total_videos": $TOTAL_VIDEOS,
254+ ! "input": {
255+ ! "video_path": "$VIDEO_PATH",
256+ ! "video_exists": $([ -f "$VIDEO_PATH" ] && echo "true" || echo "false"),
257+ ! "video_duration": "$DURATION",
258+ ! "frames_dir": "$VIDEO_FRAMES_DIR",
259+ ! "output_dir": "$VIDEO_OUTPUT_DIR"
260+ ! },
261+ ! "config": {
262+ ! "optimize_for": "$CONFIG_OPTIMIZE_FOR",
263+ ! "target_person": "$CONFIG_TARGET_PERSON"
264+ ! },
265+ ! "planned_execution": {
266+ ! "round1": {
267+ ! "target_frames": $TARGET_FRAMES,
268+ ! "fps": "$FPS",
269+ ! "frame_interval": "$FRAME_INTERVAL",
270+ ! "command": "ffmpeg -i \" $VIDEO_PATH\" -vf \" fps=$FPS\" -q: v 2 \" $VIDEO_FRAMES_DIR/frame_ %04d.jpg\" -loglevel error"
271+ ! },
272+ ! "round2": {
273+ ! "window_seconds": "$WINDOW",
274+ ! "fps": "$ROUND2_FPS",
275+ "command": "ffmpeg -ss <WINNER_TIME-$WINDOW> -i \\ "$VIDEO_PATH\\ " -t <DURATION_R2> -vf \\ "fps=$ROUND2_FPS\\ " -q: v 2 \\ "$VIDEO_FRAMES_DIR/round2/refined_ %03d.jpg\\ " -loglevel error"
276+ ! },
277+ ! "final_output": {
278+ ! "path": "$VIDEO_OUTPUT_DIR/${VIDEO_NAME}_ best_frame.jpg",
279+ ! "command": "cp \" <BEST_FRAME>\" \" $VIDEO_OUTPUT_DIR/${VIDEO_NAME}_ best_frame.jpg\" "
280+ ! }
281+ ! },
282+ ! "execution_status": "skipped (dry-run mode)"
283+ !}
284+ !EOF
285+ ! else
286+ ! # Human-readable format
287+ ! echo ""
288+ ! echo "Video $CURRENT_VIDEO of $TOTAL_VIDEOS: $VIDEO_NAME"
289+ ! echo "──────────────────────────────────────"
290+ ! echo "Input Video: $VIDEO_PATH"
291+ ! [ -f "$VIDEO_PATH" ] && echo "Status: ✓ Found" || echo "Status: ✗ Not found"
292+ ! echo "Duration: ${DURATION}s"
293+ ! echo ""
294+ ! echo "Resolved Configuration:"
295+ ! echo " optimize_for: $CONFIG_OPTIMIZE_FOR"
296+ ! echo " target_person: $CONFIG_TARGET_PERSON"
297+ ! echo " frames_dir: $VIDEO_FRAMES_DIR"
298+ ! echo " output_dir: $VIDEO_OUTPUT_DIR"
299+ ! echo ""
300+ ! echo "Planned Execution Steps:"
301+ ! echo " 1. Calculate adaptive FPS: $TARGET_FRAMES frames / ${DURATION}s = $FPS fps"
302+ ! echo " 2. Extract ~ $TARGET_FRAMES frames (1 frame every ${FRAME_INTERVAL}s)"
303+ ! echo " 3. Run tournament selection (Round 1) - AI compares frames"
304+ ! echo " 4. Extract refined frames around winner (±${WINDOW}s at ${ROUND2_FPS} fps)"
305+ ! echo " 5. Select best frame from Round 2 - AI finds perfect moment"
306+ ! echo " 6. Copy to: $VIDEO_OUTPUT_DIR/${VIDEO_NAME}_ best_frame.jpg"
307+ ! echo ""
308+ ! echo "Would Execute Commands:"
309+ ! echo " \$ mkdir -p \" $VIDEO_FRAMES_DIR\" \" $VIDEO_OUTPUT_DIR\" "
310+ ! echo " \$ ffmpeg -i \" $VIDEO_PATH\" -vf \" fps=$FPS\" -q: v 2 \" $VIDEO_FRAMES_DIR/frame_ %04d.jpg\" -loglevel error"
311+ echo " \\ $ ffmpeg -ss <WINNER_TIME-$WINDOW> -i \\ "$VIDEO_PATH\\ " -t <DURATION_R2> -vf \\ "fps=$ROUND2_FPS\\ " -q: v 2 \\ "$VIDEO_FRAMES_DIR/round2/refined_ %03d.jpg\\ " -loglevel error"
312+ ! echo " \$ cp \" <BEST_FRAME>\" \" $VIDEO_OUTPUT_DIR/${VIDEO_NAME}_ best_frame.jpg\" "
313+ ! echo ""
314+ ! fi
315+ ! done
316+ !
317+ ! # Exit after dry-run preview
318+ ! if [ "$JSON_OUTPUT" != "true" ] ; then
319+ ! echo "──────────────────────────────────────"
320+ ! echo "(No actions executed - dry-run mode)"
321+ ! echo ""
322+ ! echo "To execute for real, run without --dry-run flag"
323+ ! fi
324+ ! exit 0
325+ !fi
326+
327+ ## Step 1c: Batch Processing Loop
132328
133329For each video, process sequentially:
134330!for VIDEO_PATH in "${VIDEO_PATHS[ @] }"; do
0 commit comments