Skip to content

Commit fa5e9d3

Browse files
Add --dry-run flag to extract-best-frame for safe preview (#1396)
* feat: add --dry-run and --json flags to extract-best-frame Closes #1394 * Update .agent-config.example.yml Co-authored-by: amazon-q-developer[bot] <208079219+amazon-q-developer[bot]@users.noreply.github.com> * Update commands/extract-best-frame.md Co-authored-by: amazon-q-developer[bot] <208079219+amazon-q-developer[bot]@users.noreply.github.com> * Update commands/extract-best-frame.md Co-authored-by: amazon-q-developer[bot] <208079219+amazon-q-developer[bot]@users.noreply.github.com> * Update commands/extract-best-frame.md Co-authored-by: amazon-q-developer[bot] <208079219+amazon-q-developer[bot]@users.noreply.github.com> * Update commands/extract-best-frame.md Co-authored-by: amazon-q-developer[bot] <208079219+amazon-q-developer[bot]@users.noreply.github.com> * Update commands/extract-best-frame.md Co-authored-by: amazon-q-developer[bot] <208079219+amazon-q-developer[bot]@users.noreply.github.com> --------- Co-authored-by: amazon-q-developer[bot] <208079219+amazon-q-developer[bot]@users.noreply.github.com>
1 parent a727ecb commit fa5e9d3

2 files changed

Lines changed: 201 additions & 1 deletion

File tree

.agent-config.example.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ agents:
4343
output_dir: "/tmp/extract-best-frame/output"
4444
keep_intermediate_frames: true
4545

46+
# Dry-run mode: Preview execution plan without running commands
47+
# Command-line --dry-run flag takes precedence over this setting
48+
# dry_run: false # Uncomment to enable dry-run mode by default
49+
4650
# Make Resume - Generate tailored resumes with GitHub PR proof
4751
make-resume:
4852
paths:

commands/extract-best-frame.md

Lines changed: 197 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

4564
Load 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

117136
If .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

121180
Parse 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

133329
For each video, process sequentially:
134330
!for VIDEO_PATH in "${VIDEO_PATHS[@]}"; do

0 commit comments

Comments
 (0)