How Auto-FPS Is Calculated Based on Video Length and Budget Caps in Claude-Video

The claude-video repository automatically determines frame rates by mapping video duration to preset target frame counts, then clamping the result against a user-defined max_frames budget and a global MAX_FPS limit of 2.0.

The auto-fps calculation is a core feature of the bradautomates/claude-video project, designed to balance visual coverage with downstream LLM token costs. In skills/watch/scripts/frames.py, the system implements duration-aware heuristics that prioritize dense sampling for short videos while respecting strict upper bounds on total frames extracted.

Core Constants and Constraints

The auto-fps system operates within hard limits defined at the module level in [skills/watch/scripts/frames.py](https://github.com/bradautomates/claude-video/blob/main/skills/watch/scripts/frames.py):

  • MAX_FPS = 2.0 — the absolute frame rate ceiling regardless of video length
  • MAX_READ_DIMENSION — constrains resolution during extraction

These constants ensure predictable performance and prevent excessive frame generation that would overwhelm vision-language models.

The _clamp_fps Helper Function

All fps calculations flow through _clamp_fps(), which enforces the budget cap. This function:

  1. Receives a proposed fps, video duration, and max_frames budget
  2. Clamps the fps to MAX_FPS if exceeded
  3. Computes target frames as fps × duration
  4. Returns the lesser of calculated frames or max_frames, with a minimum of 1

# From skills/watch/scripts/frames.py, lines 49-52

def _clamp_fps(fps, duration_seconds, max_frames):
    fps = min(fps, MAX_FPS)
    target = int(fps * duration_seconds)
    return fps, min(target, max_frames)

Full-Video Auto-FPS: auto_fps()

For processing entire videos, auto_fps(duration_seconds, max_frames) selects target frames based on duration tiers:

Duration Target Frames (before budget cap)
≤ 30 seconds max(12, round(duration))
≤ 60 seconds 40
≤ 3 minutes 60
≤ 10 minutes 80
> 10 minutes max_frames (full budget)

# Example: 2-minute video with 100-frame budget

from skills.watch.scripts.frames import auto_fps

fps, target = auto_fps(duration_seconds=120, max_frames=100)

# Returns: fps=0.67, target=80

For invalid durations (≤ 0), the function falls back to 1 fps and 1 frame.

Focused-Range Auto-FPS: auto_fps_focus()

When users specify time ranges (e.g., 00:01:00-00:01:30), the system applies denser sampling via auto_fps_focus():

Duration Target Frames (before budget cap)
≤ 5 seconds max(10, round(duration × 6))
≤ 15 seconds max(30, round(duration × 4))
≤ 30 seconds 60
≤ 60 seconds 80
> 60 seconds max_frames (full budget)

This prioritizes granular detail for brief segments that users explicitly flag as important.


# Example: 8-second focused clip with 100-frame budget

from skills.watch.scripts.frames import auto_fps_focus

fps, target = auto_fps_focus(duration_seconds=8, max_frames=100)

# Returns fps=6.0 initially, but _clamp_fps reduces to MAX_FPS=2.0

# target becomes min(16, 100) = 16 frames

End-to-End Execution Flow

The auto-fps calculation integrates into the video processing pipeline as follows:

  1. Metadata extractionget_metadata() calls ffprobe to obtain duration_seconds
  2. Function selectionscripts/watch.py chooses auto_fps or auto_fps_focus based on whether a time range was specified
  3. Rate calculation — The selected function returns (fps, target_frames)
  4. Frame extractionextract() invokes ffmpeg with -vf fps={fps}, writing at most target_frames frames

# Command-line usage with explicit budget

python -m skills.watch.scripts.watch path/to/video.mp4 --budget 50

Budget Cap Behavior in Practice

The max_frames parameter acts as a hard ceiling. Consider a 30-minute video with default settings:

fps, target = auto_fps(duration_seconds=1800, max_frames=100)

# Duration > 600s triggers "else" branch: target = max_frames = 100

# fps = min(100/1800, MAX_FPS) = 0.056 → effectively sampling every ~18 seconds

Without the budget cap, long videos would generate prohibitive frame counts. The tiered heuristics ensure short videos receive adequate coverage while scaling gracefully to arbitrarily long content.

Summary

  • MAX_FPS = 2.0 caps absolute frame rate regardless of duration
  • auto_fps() applies tiered targets for full videos: 12–40–60–80–budget
  • auto_fps_focus() applies denser sampling for user-specified ranges: 10–30–60–80–budget
  • _clamp_fps() enforces both the fps ceiling and frame budget in all cases
  • Duration ≤ 0 triggers safe fallback to 1 fps, 1 frame
  • All logic is deterministic and located in skills/watch/scripts/frames.py

Frequently Asked Questions

What happens if my video is shorter than 1 second?

The auto_fps function detects duration_seconds ≤ 0 and falls back to 1 fps with 1 frame output. This prevents division errors and ensures at least one representative frame is extracted.

Why is MAX_FPS capped at 2.0 instead of higher values?

According to the claude-video source code, the 2 fps limit balances temporal resolution against vision-language model token costs. Higher rates rarely improve understanding for most video content while significantly increasing processing overhead.

How do I force a specific frame rate instead of auto-fps?

The current implementation does not expose manual fps override in the CLI. You would need to modify skills/watch/scripts/frames.py to bypass auto_fps/auto_fps_focus and pass a hardcoded value directly to extract().

Does the budget cap affect video quality or resolution?

No. The max_frames budget and MAX_FPS limit control quantity of frames only. Resolution constraints are handled separately via MAX_READ_DIMENSION during the ffmpeg extraction phase.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →