# io.github.tlq5l/agentic-video (pypi · agentic-video)

Local video tools for AI agents: contact sheets, transcripts, cuts, loudness and sync checks.

- Trust score: 77/100 (medium)
- Registry status: active
- Liveness: live
- Owner verified: no
- Last scored: 2026-11-05

## Components

- pypi · `agentic-video`: 77/100 (this document), [markdown](https://verifymcp.io/servers/tlq5l-agentic-video/agentic-video.md), [page](https://verifymcp.io/servers/tlq5l-agentic-video/agentic-video)

## Channel facts

- Registry: `pypi`
- Package: `agentic-video`
- Version: `0.3.4`
- Transport: `stdio`

## Trust breakdown

How this component scores in each security and reliability category. Every signal is checked automatically from public evidence about the published package, including repeated runs of it in an isolated sandbox, and we only credit what we can confirm. Scores are 0–100 per category. Scoring method: https://verifymcp.io/docs/scoring (what has changed: https://verifymcp.io/docs/scoring/changelog)

Scored 2026-11-05.

- **Supply Chain Security**: 100/100
  - No malware found by supply-chain analysis.
  - No known CVEs affecting this package version or its production dependencies.
  - Runs hatchling.build at install time, a recognised build step with no custom scripting around it.
  - 2 of 29 dependencies flagged as unhealthy.
- **Provenance & Transparency**: 97/100
  - Source repository is publicly reachable at the declared URL.
  - Cryptographically verified build provenance (signed, bound to tlq5l/agentic-video).
  - Clear OSI-approved license (MIT).
  - Actively maintained (last published 3 days ago).
  - Disclosure check failed: no security disclosure policy was found in the source repository.
- **Schema Quality & AI Usability**: 58/100
  - AI-judged instruction clarity (excellent).
  - Context-footprint check failed: tool/resource definitions use about 11897 tokens (~475/item across 25 items; 25 tools + 0 resources), over budget; trim descriptions and params.
  - Usage-examples check failed: none of the tools include examples.
- **Stability & Change Management**: 17/100
  - Stability observed for 5 of 30 days with no destabilising changes; credit accrues until the full window elapses.
- **Tool Coverage**: 98/100
  - 100% of tools have a non-trivial description (not blank, and not just the tool's name).
  - 93% of tool parameters carry a description.
  - Structured output schemas are declared (100% of tools); any adoption earns full credit.
- **Tool Safety**: 100/100
  - No prompt-injection markers were found in the server instructions, tool names or descriptions we captured.
  - We read all 25 captured tool definition(s), and no name or description among them implies an irreversible operation.
  - An AI judge read all 25 captured unit(s) of tool text and found none that tries to manipulate the model reading it.
- **Capabilities**: 20/100
  - Spec-recency check failed: implements MCP spec 2024-11-05; the latest is 2026-07-28.

## Install

### How do I install the io.github.tlq5l/agentic-video MCP server?

io.github.tlq5l/agentic-video runs locally as a PyPI package, launched with uvx agentic-video. Ready-made configuration for Claude, Cursor, VS Code, Codex and 5 more is on this page, copied from each client's own documentation.

### Claude

```bash
claude mcp add tlq5l-agentic-video -- uvx agentic-video
```

### Cursor

```json
{
  "mcpServers": {
    "tlq5l-agentic-video": {
      "command": "uvx",
      "args": [
        "agentic-video"
      ]
    }
  }
}
```

### VS Code

```json
{
  "servers": {
    "tlq5l-agentic-video": {
      "command": "uvx",
      "args": [
        "agentic-video"
      ]
    }
  }
}
```

### Codex

```bash
codex mcp add tlq5l-agentic-video -- uvx agentic-video
```

### opencode

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "tlq5l-agentic-video": {
      "type": "local",
      "command": [
        "uvx",
        "agentic-video"
      ],
      "enabled": true
    }
  }
}
```

### OpenClaw

```bash
openclaw mcp add tlq5l-agentic-video --command uvx --arg agentic-video
```

### Hermes

```yaml
mcp_servers:
  tlq5l-agentic-video:
    command: "uvx"
    args: ["agentic-video"]
```

### Netclaw

```json
{
  "McpServers": {
    "tlq5l-agentic-video": {
      "Transport": "stdio",
      "Command": "uvx",
      "Arguments": [
        "agentic-video"
      ]
    }
  }
}
```

### Vellum

```bash
assistant mcp add tlq5l-agentic-video -t stdio -c uvx -a agentic-video
```

### Other

```json
{
  "mcpServers": {
    "tlq5l-agentic-video": {
      "command": "uvx",
      "args": [
        "agentic-video"
      ]
    }
  }
}
```

## Changelog

Every change recorded for this component, newest first. Days that predate change tracking, or that we cannot explain, say so: "we were watching and nothing happened" and "we were not watching" are different claims.

### 2026-11-05 (score 77, +1)

No change was recorded against any check on this day. Stability & Change Management went from 13 to 17. That category is still filling its 30-day observation window: 4 days of observed history at the previous scan, 5 at this one. The score rises as the window fills, whether or not the server changes.

### 2026-11-03 (score 76, +1)

No change was recorded against any check on this day. Stability & Change Management went from 7 to 10. That category is still filling its 30-day observation window: 2 days of observed history at the previous scan, 3 at this one. The score rises as the window fills, whether or not the server changes.

### 2026-11-01 (score 75, +16)

- [security regression] Tool safety: pass → unverified
- [security improvement] Malware scan: unverified → pass
- [security] Stability: Stability not yet verified: we do not have a sandbox capture of the MCP schema this version of the package serves yet.
- [functional regression] Capabilities: fail → unverified
- [functional regression] Tool coverage: 100 → unverified
- [functional improvement] Stability: unverified → 0.03
- [functional] First check of Schema quality: unverified
- [functional] Package version: 0.3.3 → 0.3.4

### 2026-10-31 (score 59)

First indexed and scored.

## MCP tools (25)

### `count_repetitions` (~670 tokens)

Count completed cycles of a repeated visual action (jumps, claps, bounces, swings) inside one window of at most 180 seconds. Give `region` (fractions of the frame: x, y, w, h) to isolate the moving thing. Repeat the same call while status=in_progress; verify peak badges and ambiguous edges with inspect_window. For separate appearances of a specific symbol or exact on-screen text use count_occurrences; to locate one moment use search_visual. For a longer clip call once per window, cutting at pauses between cycles so none straddles an edge. The count is a rough first pass and can be far off on real footage. Before answering, count cycles by eye with inspect_window or contact_sheet over the window; if your count differs, trust your own.

Input parameters:

- `action_hint` (string): Short phrase naming the action being counted, e.g. 'jumping jacks'.
- `end_sec` (number|string|null): End of the window in seconds; defaults to the end of the video. Give both bounds for a video longer than 180 s. Also accepts a timecode string in MM:SS.mmm form, e.g. 00:14.250.
- `fps` (number): Sampling rate, default 10. Larger values than 15 are clamped to 15 and the result says so.
- `max_period_sec` (number|string): Longest expected cycle in seconds (default 3); must exceed min_period_sec. Bounds the amplitude counter only; the periodicity check ignores it. Also accepts a timecode string in MM:SS.mmm form, e.g.…
- `method` (string): auto (default) picks between motion, embedding and a periodicity check; motion or embedding forces that amplitude counter alone.
- `min_period_sec` (number|string): Shortest expected cycle in seconds (default 0.4). Bounds the amplitude counter only; the periodicity check ignores it and searches from 0.25 s. Also accepts a timecode string in MM:SS.mmm form, e.g.…
- `region`: Rectangle that contains the repeating motion, as fractions of the frame: x, y (top-left) and w, h (size), with x+w <= 1 and y+h <= 1. Uses w/h; width/height with the same meaning is also accepted. Fo…
- `source` (string, required): Local video file path, or a video URL (X/Twitter post, YouTube link, other sites yt-dlp supports, direct .mp4 link). A URL is downloaded once and cached; while it downloads the result is status=in_pr…
- `start_sec` (number|string): Start of the window in seconds (default 0). The window may be at most 180 s long. Also accepts a timecode string in MM:SS.mmm form, e.g. 00:14.250.

Output parameters:

- `confidence` (number)
- `contact_sheet` (object|null)
- `count` (integer)
- `cycles` (array)
- `embedding_count` (integer|null)
- `local_path` (string): Cached local copy of `source_url`; pass it to other tools or ffmpeg to skip the URL lookup.
- `median_period_sec` (number|null)
- `message` (string)
- `method_used` (string)
- `motion_count` (integer)
- `note` (string)
- `period_sec` (number|null)
- `periodic_count` (integer|null)
- `regularity` (number)
- `retry_after_seconds` (number)
- `signal_quality` (string)
- `source_url` (string): The video URL you passed as `source`; only present when `source` was a URL.
- `status` (string)

### `count_occurrences` (~1189 tokens)

Count how many separate times a specific symbol/object or exact on-screen text appears. Use for questions like 'how many separate times does the yellow outlined circle appear (ignore other colours)', 'how many times does the exact word RELEASE appear (not RELEASED)', 'how many times does the green diamond cross the screen'. Give `description` (colour and shape words are enforced, so look-alikes in another colour or shape do not count) and/or `text` (whole-word match by offline OCR). Returns `count` plus one span per continuous appearance (start_sec, end_sec, representative_sec, region, travel, weak); gaps of min_gap_sec or more separate spans. Verify a span with inspect_window at its representative_sec. Not for periodic reps or cycles (use count_repetitions) and not for locating one moment (use search_visual, which ranks frames, is capped at top_k 20 and cannot count). No visual index needed. Repeat the same call while status=in_progress. Lists at most max_spans spans (default 20, maximum 100; larger values are clamped and the result says so); `count` includes any omitted. If 100 or more spans match, the description is too broad: `count` is null, `capped` is true and `spans_found` says how many matched, so narrow the description or window and rerun instead of reporting a number. A min_gap_sec above 0.3 can join separate appearances; then `gaps_merged` is set, and `count_without_merges` too when no `direction` is passed (with a direction, rerun with the default min_gap_sec to see the separate appearances). The count is a rough first pass: it can merge separate appearances that are split by a scene cut, split one appearance into many spans, and count appearances rather than crossings; `direction` (e.g. left_to_right) narrows the count to appearances that travel that way, plus spans too brief to show motion (travel unknown), which count under any direction: check those with inspect_window. The result also carries `contact_sheet`, one labeled image with a tile per list…

Input parameters:

- `case_sensitive` (boolean): Require the same letter case for `text`.
- `description` (string): Visible thing to count, e.g. 'yellow outlined circle', 'green diamond', 'red star'. Name its colour and shape; text on screen goes in `text`.
- `direction` (string): Count the appearances whose net motion across the frame is this way, e.g. left_to_right for 'how many times did it cross left to right'. Motion is estimated from each span's first and last positions,…
- `end_sec` (number|string|null): Defaults to the end of the video. At most 2400 frames are analyzed per call, so fps is lowered automatically on long windows. Also accepts a timecode string in MM:SS.mmm form, e.g. 00:14.250.
- `fps` (number): Sampling rate, default 5. Appearances shorter than 1/fps can be missed; raise it for brief flashes. Larger values than 15 are clamped to 15 and the result says so.
- `max_spans` (integer): Maximum spans listed (default 20, maximum 100; larger values are clamped to 100 and the result says so). `count` always includes every span.
- `min_gap_sec` (number|string): Absence of at least this long starts a new span; shorter dropouts stay inside one span. Raising it above the default 0.3 JOINS separate appearances whose gap is shorter than it (0.8 merges appearance…
- `source` (string, required): Local video file path, or a video URL (X/Twitter post, YouTube link, other sites yt-dlp supports, direct .mp4 link). A URL is downloaded once and cached; while it downloads the result is status=in_pr…
- `start_sec` (number|string): Seconds, or a timecode string in MM:SS.mmm form, e.g. 00:14.250.
- `text` (string): Exact on-screen phrase, matched as whole words (RELEASE matches 'RELEASE' and 'RELEASE NOTES', not 'RELEASED' or 'PRE-RELEASE').

Output parameters:

- `capped` (boolean): True when 100 or more spans matched: the description is too broad and count is null.
- `checks` (object)
- `contact_sheet` (object|null): One image (file_path, tool_file) with a labeled tile per listed span, in span order, up to 24: a zoomed frame of the span's best detection with the match boxed. `labels` lists the tile labels, e.g. '…
- `count` (integer|null): Number of separate appearances. When `direction` was passed: those travelling that way plus spans too brief to show motion (travel unknown), which count under any direction; check those with inspect_…
- `count_without_merges` (integer): The count if those joined pairs were kept apart. Set only when no direction was passed; with a direction, rerun with the default min_gap_sec instead.
- `fps_used` (number)
- `frames_analyzed` (integer)
- `frames_sampled` (integer)
- `gaps_merged` (integer): Appearance pairs joined because min_gap_sec is above the default 0.3; only set when it joined some.
- `local_path` (string): Cached local copy of `source_url`; pass it to other tools or ffmpeg to skip the URL lookup.
- `message` (string)
- `note` (string)
- `retry_after_seconds` (number)
- `source_url` (string): The video URL you passed as `source`; only present when `source` was a URL.
- `spans` (array)
- `spans_found` (integer): How many spans matched; only set when capped.
- `spans_omitted` (integer)
- `spans_returned` (integer)
- `status` (string)
- `travel_counts` (object): Net motion of moving appearances across the frame, by direction (left_to_right, right_to_left, top_to_bottom, bottom_to_top); only set when some move. Each value is an estimate from a span's first an…
- `verify` (string)
- `window` (object)

### `probe_video` (~283 tokens)

Read a video's duration, frame rate, resolution, codecs, and whether it has an audio track and a video track. Call this first: duration_sec bounds every start_sec/end_sec you pass to other tools. Audio-only files (.m4a, .mp3, .wav) are accepted: has_video is false and width, height and fps are 0. has_audio tells you whether transcribe, search_transcript and the audio tools (audio_onsets, loudness_timeline, spectrogram_window) can work; the visual tools and sync_check need has_video true. `source` is a local file path or a video URL and is the same in every tool: an x.com / Twitter post with a video, a YouTube link, another site yt-dlp supports, or a direct .mp4 link. A URL is downloaded once (up to 1080p) and cached; the result gives source_url and local_path, and while the download runs it is status=in_progress, so call again.

Input parameters:

- `source` (string, required): Local video file path, or a video URL (X/Twitter post, YouTube link, other sites yt-dlp supports, direct .mp4 link). A URL is downloaded once and cached; while it downloads the result is status=in_pr…

Output parameters:

- `audio_codec` (string|null)
- `duration_sec` (number)
- `fingerprint` (string)
- `format_name` (string)
- `fps` (number)
- `has_audio` (boolean)
- `has_video` (boolean): False for audio-only files: width, height and fps are 0, and the visual tools refuse the file.
- `height` (integer)
- `local_path` (string): Cached local copy of `source_url`; pass it to other tools or ffmpeg to skip the URL lookup.
- `note` (string): Plain-English remark about the call: a capped argument, omitted items, or an ignored unknown argument.
- `retry_after_seconds` (integer)
- `size_bytes` (integer)
- `source_path` (string)
- `source_url` (string): The video URL you passed as `source`; only present when `source` was a URL.
- `status` (string)
- `video_codec` (string|null)
- `width` (integer)

### `transcribe` (~320 tokens)

Get the full speech transcript as timestamped segments (cached after the first run). Audio-only files (.m4a, .mp3, .wav) work too. To find where a specific word or phrase is spoken, use search_transcript instead of reading it all. If the requested model is not cached, returns another cached transcript with cached_model_size and requested_model_cached=false while one background job runs; with no transcript cached, returns status=in_progress and retry_after_seconds, so repeat the same call after that delay. model_size: tiny, base or small (more accurate, slower on the CPU), or turbo (Whisper large-v3-turbo on the GPU via whisper.cpp: the most accurate and, where installed, the fastest, about 1 minute per hour of audio). Omit model_size to get turbo where it is installed, else tiny. Long audio is decoded in 60-second chunks. Lines repeated 3 or more times in a row carry `suspect` and `notice` names them: speech models often invent such lines over music or silence, though a real chant can repeat too. A line without `suspect` is not verified.

Input parameters:

- `model_size` (string)
- `source` (string, required): Local video file path, or a video URL (X/Twitter post, YouTube link, other sites yt-dlp supports, direct .mp4 link). A URL is downloaded once and cached; while it downloads the result is status=in_pr…

Output parameters:

- `cached_model_size` (string)
- `confidence` (number|null)
- `duration_sec` (number)
- `elapsed_sec` (integer): While status=in_progress: seconds since the background transcription started.
- `estimated_percent` (integer): While status=in_progress: elapsed / estimated total, capped at 99. An estimate, not a measurement.
- `estimated_total_sec` (integer): While status=in_progress: rough estimate of the whole run, from the audio length and model speed.
- `has_speech` (boolean)
- `language` (string)
- `local_path` (string): Cached local copy of `source_url`; pass it to other tools or ffmpeg to skip the URL lookup.
- `model_substitution` (string): Only when the requested model is not installed: names the installed model used instead.
- `note` (string): Plain-English remark about the call: a capped argument, omitted items, or an ignored unknown argument.
- `notice` (string)
- `requested_model_cached` (boolean)
- `requested_model_size` (string)
- `retry_after_seconds` (integer)
- `segment_count` (integer)
- `segments` (array)
- `segments_omitted` (integer): Only with transcript_path: how many segments are left out of `segments`.
- `source_path` (string)
- `source_url` (string): The video URL you passed as `source`; only present when `source` was a URL.
- `status` (string)
- `transcript_path` (string): Only when the transcript was too long to list in full: path of a JSON file with every segment.
- `transcription_message` (string)
- `transcription_status` (string)
- `uncertainty` (string)

### `search_transcript` (~449 tokens)

Find when a word or phrase is SPOKEN. Audio-only files (.m4a, .mp3, .wav) work too. Default mode=hybrid lists exact keyword matches first (match=exact: the phrase; match=all_terms: every content word in one segment), then passages that match by meaning (match=semantic: a 20 s window, found on this Mac with EmbeddingGemma 2; nothing is sent to the cloud), so a reworded question still finds the moment. mode=lexical gives keyword matches only. When no hit is exact or all_terms, the words were not found in the speech: semantic hits are the closest passages, not proof, so read their text before answering. If semantic search is not set up or not ready, hits are keyword-only and `note` says why. Returns up to top_k timestamped hits (default 5, maximum 50; larger values are clamped and the result says so; `note` says when more keyword matches exist). For text shown on screen use count_occurrences, and for visible things use search_visual. If ASR is needed, starts or joins background transcription and returns status=in_progress with retry_after_seconds instead of waiting; repeat the same call after that delay. A hit on a flagged line carries `suspect` (see transcribe).

Input parameters:

- `mode` (string): hybrid (default): keyword matches first, then matches by meaning. lexical: keyword matches only. For example: "mode": "lexical".
- `query` (string, required): Words or a phrase to find in the speech, e.g. 'launch date'. Punctuation and case are ignored.
- `source` (string, required): Local video file path, or a video URL (X/Twitter post, YouTube link, other sites yt-dlp supports, direct .mp4 link). A URL is downloaded once and cached; while it downloads the result is status=in_pr…
- `top_k` (integer): Maximum hits (default 5, maximum 50; larger values are clamped to 50 and the result says so).

Output parameters:

- `cached_model_size` (string)
- `elapsed_sec` (integer): While status=in_progress: seconds since the background transcription started.
- `estimated_percent` (integer): While status=in_progress: elapsed / estimated total, capped at 99. An estimate, not a measurement.
- `estimated_total_sec` (integer): While status=in_progress: rough estimate of the whole run, from the audio length and model speed.
- `hits` (array)
- `local_path` (string): Cached local copy of `source_url`; pass it to other tools or ffmpeg to skip the URL lookup.
- `model_substitution` (string): Only when the requested model is not installed: names the installed model used instead.
- `note` (string): Plain-English remark about the call: a capped argument, omitted items, or an ignored unknown argument.
- `requested_model_cached` (boolean)
- `requested_model_size` (string)
- `retry_after_seconds` (integer)
- `source_url` (string): The video URL you passed as `source`; only present when `source` was a URL.
- `status` (string)
- `top_k_clamped_from` (number): The value you requested for top_k before it was lowered to its cap.
- `total` (integer)
- `transcription_message` (string)
- `transcription_status` (string)

### `build_visual_index` (~143 tokens)

Start or poll construction of the local visual frame index that search_visual needs. Call it once, then repeat the same call (or search_visual, which reports status=index_required or partial) until the status is complete; frames are processed in bounded chunks. count_occurrences, contact_sheet and the inspect tools do not need the index.

Input parameters:

- `source` (string, required): Local video file path, or a video URL (X/Twitter post, YouTube link, other sites yt-dlp supports, direct .mp4 link). A URL is downloaded once and cached; while it downloads the result is status=in_pr…

### `search_visual` (~504 tokens)

Find WHEN something visible happens: an object, person, action or scene (semantic match between the query and sampled frames). It matches meaning, not letters, so it cannot find on-screen words or numbers (names, scores, jersey or bib numbers): find those with count_occurrences `text`, which reads text in frames, one window of up to 2400 s per call. Returns up to top_k ranked frame hits (default 5, maximum 20; larger values are clamped and the result says so), merged intervals (the one holding rank 1 grows from its best frames only over neighbouring frames that score close to the best, up to 8 seconds, so it can be shorter; a span already longer than 8 seconds, such as a held frame or hits merged together, is kept whole, so it can be longer and can include lower-scoring hits; intervals that overlap it are merged into it), and a contact sheet to verify them. It ranks frames, so it cannot count: to count separate appearances of a symbol or exact on-screen text use count_occurrences, for a repeated action use count_repetitions, for spoken words use search_transcript. Build the local visual index first (build_visual_index), then repeat the same query to poll until complete. Use start_sec/end_sec to search one range at a time.

Input parameters:

- `end_sec` (number|string|null): Exclusive end of the range to search, in seconds; defaults to the end of the video. Also accepts a timecode string in MM:SS.mmm form, e.g. 00:14.250.
- `query` (string, required): Natural-language description of visible content.
- `source` (string, required): Local video file path, or a video URL (X/Twitter post, YouTube link, other sites yt-dlp supports, direct .mp4 link). A URL is downloaded once and cached; while it downloads the result is status=in_pr…
- `start_sec` (number|string): Inclusive start of the range to search, in seconds. Also accepts a timecode string in MM:SS.mmm form, e.g. 00:14.250.
- `top_k` (integer): Maximum frame hits (default 5, maximum 20; larger values are clamped to 20 and the result says so). This ranks frames and cannot count: use count_occurrences for counts.

### `contact_sheet` (~301 tokens)

Get ONE grid image of evenly spaced frames, with burned timestamp badges, across the whole video or a start_sec/end_sec range: the cheapest way to see what a video contains before searching. Default 12 samples, at most 150 (larger values are clamped and the result says so). Use it to survey; use search_visual to find a specific thing, overview_chunks to survey a long video as ordered sheets, and inspect_window or inspect_windows for dense consecutive frames of a short span.

Input parameters:

- `cols` (integer): Grid columns (default 4).
- `end_sec` (number|string): Optional exclusive end of the overview range in absolute video seconds. Also accepts a timecode string in MM:SS.mmm form, e.g. 00:14.250.
- `samples` (integer): Requested number of thumbnails; values above 150 are accepted and clamped to 150.
- `source` (string, required): Local video file path, or a video URL (X/Twitter post, YouTube link, other sites yt-dlp supports, direct .mp4 link). A URL is downloaded once and cached; while it downloads the result is status=in_pr…
- `start_sec` (number|string): Optional inclusive start of the overview range in absolute video seconds. Also accepts a timecode string in MM:SS.mmm form, e.g. 00:14.250.

Output parameters:

- `cols` (integer)
- `end_sec` (number)
- `file_path` (string)
- `frame_height` (integer)
- `frame_width` (integer)
- `local_path` (string): Cached local copy of `source_url`; pass it to other tools or ffmpeg to skip the URL lookup.
- `note` (string)
- `retry_after_seconds` (integer)
- `rows` (integer)
- `sample_count` (integer)
- `samples_clamped_from` (integer)
- `source_url` (string): The video URL you passed as `source`; only present when `source` was a URL.
- `start_sec` (number)
- `status` (string)
- `timestamps` (array)
- `tool_file` (object)

### `review_video` (~374 tokens)

First look at a video in ONE call: container metadata (duration, size, fps, codecs, has_audio), a contact sheet image with burned timestamp badges (default 24 samples; `samples` up to 150, larger values are clamped and the result says so), every cut and transition from detect_cuts, every near-black stretch of at least 0.2 s (`black_ranges`: start, end and duration; at most 50 are listed and `black_total` counts them all), and a loudness summary (average and peak level, silence gaps, the strongest band-energy outliers with their times). It runs probe_video, contact_sheet, detect_cuts, the black scan and loudness_timeline for you, so start here instead of calling them one by one. A first call on a 10-minute 1080p clip takes about 35 s (the cuts scan and the black scan share one decode of the file); repeat calls on the same file return from cache. `loudness` is null when the video has no audio track. Use the times it returns to zoom in with inspect_window or spectrogram_window; for a different threshold, window or time range call detect_cuts or loudness_timeline directly. Any step failing fails the whole call with that step's error message.

Input parameters:

- `samples` (integer): Contact sheet thumbnails (default 24; values above 150 are clamped to 150 and the result says so).
- `source` (string, required): Local video file path, or a video URL (X/Twitter post, YouTube link, other sites yt-dlp supports, direct .mp4 link). A URL is downloaded once and cached; while it downloads the result is status=in_pr…

Output parameters:

- `black_ranges` (array): Stretches of at least 0.2 s where the picture is near-black, in time order. At most 50 are listed; black_total is the full count.
- `black_ranges_omitted` (integer): How many black_ranges entries were left out of the list.
- `black_total` (integer): How many near-black stretches were found in all.
- `contact_sheet` (object): Same fields as contact_sheet: file_path, sample_count, rows, cols, start_sec, end_sec, timestamps, frame_width, frame_height, tool_file (the image), and samples_clamped_from when samples was lowered.
- `cuts` (array)
- `cuts_omitted` (integer): How many cuts entries were left out of the list.
- `cuts_total` (integer): How many cuts and transitions were found in all.
- `local_path` (string): Cached local copy of `source_url`; pass it to other tools or ffmpeg to skip the URL lookup.
- `loudness` (object|null): Null when the video has no audio track.
- `metadata` (object): Same fields as probe_video: source_path, duration_sec, width, height, fps, format_name, size_bytes, has_audio, has_video, audio_codec, video_codec, fingerprint.
- `note` (string): Plain-English remark about the call: a capped argument, omitted items, or an ignored unknown argument.
- `retry_after_seconds` (integer)
- `source_url` (string): The video URL you passed as `source`; only present when `source` was a URL.
- `status` (string)

### `overview_chunks` (~392 tokens)

Survey a long video as consecutive contact sheets, one per chunk_sec (default 60 s), so every stretch is covered in order. At most max_chunks sheets per call (default and maximum 32): for a longer video raise chunk_sec or page through it with start_sec/end_sec; a call over budget fails and says what chunk_sec would fit. samples_per_chunk defaults to 8 (maximum 150, larger values are clamped). Use contact_sheet instead when one sheet is enough.

Input parameters:

- `chunk_sec` (number|string): Seconds covered by each sheet (default 60, maximum 600). Also accepts a timecode string in MM:SS.mmm form, e.g. 00:14.250.
- `cols` (integer)
- `end_sec` (number|string): Optional exclusive end in absolute video seconds; defaults to the end of the video. Also accepts a timecode string in MM:SS.mmm form, e.g. 00:14.250.
- `max_chunks` (integer): Most sheets in one call (default and maximum 32; larger values are clamped). The call fails if the range needs more chunks: raise chunk_sec or narrow the range.
- `samples_per_chunk` (integer): Thumbnails per sheet (default 8; values above 150 are clamped to 150 and the result says so).
- `source` (string, required): Local video file path, or a video URL (X/Twitter post, YouTube link, other sites yt-dlp supports, direct .mp4 link). A URL is downloaded once and cached; while it downloads the result is status=in_pr…
- `start_sec` (number|string): Optional inclusive start in absolute video seconds. Also accepts a timecode string in MM:SS.mmm form, e.g. 00:14.250.

Output parameters:

- `chunk_sec` (number)
- `chunks` (array)
- `end_sec` (number)
- `local_path` (string): Cached local copy of `source_url`; pass it to other tools or ffmpeg to skip the URL lookup.
- `max_chunks_clamped_from` (number): The value you requested for max_chunks before it was lowered to its cap.
- `note` (string): Plain-English remark about the call: a capped argument, omitted items, or an ignored unknown argument.
- `retry_after_seconds` (integer)
- `samples_per_chunk_clamped_from` (number): The value you requested for samples_per_chunk before it was lowered to its cap.
- `source_url` (string): The video URL you passed as `source`; only present when `source` was a URL.
- `start_sec` (number)
- `status` (string)
- `total` (integer)

### `refine_temporal_beam` (~399 tokens)

Zoom into candidate time windows: splits each window into `fanout` contiguous child ranges and returns one timestamped contact sheet per child, without ranking relevance. Use it after contact_sheet or search_visual has narrowed the answer to a few windows and you want to localize inside them; use inspect_windows for dense consecutive frames instead. Budget: windows x fanout x samples_per_child must be at most max_total_samples (default 96, maximum 300); a call over budget fails and says which of the three to lower.

Input parameters:

- `align_to_cuts` (boolean)
- `cols` (integer)
- `compact_parent_sheet` (boolean)
- `context_sec` (number|string): Seconds of padding added to each side of every window (default 0). Also accepts a timecode string in MM:SS.mmm form, e.g. 00:14.250.
- `fanout` (integer): Child ranges per window (default 4, maximum 8; larger values are clamped and the result says so).
- `max_total_samples` (integer): Frame budget: windows x fanout x samples_per_child must not exceed it (default 96, maximum 300; larger values are clamped and the result says so).
- `samples_per_child` (integer): Thumbnails per child range (default 3, maximum 12; larger values are clamped and the result says so).
- `source` (string, required): Local video file path, or a video URL (X/Twitter post, YouTube link, other sites yt-dlp supports, direct .mp4 link). A URL is downloaded once and cached; while it downloads the result is status=in_pr…
- `windows` (array, required): Absolute time ranges to zoom into, e.g. [{"start_sec": 10, "end_sec": 40}], each within the video.

Output parameters:

- `estimated_vision_tokens` (integer)
- `fanout_clamped_from` (number): The value you requested for fanout before it was lowered to its cap.
- `local_path` (string): Cached local copy of `source_url`; pass it to other tools or ffmpeg to skip the URL lookup.
- `max_total_samples_clamped_from` (number): The value you requested for max_total_samples before it was lowered to its cap.
- `note` (string): Plain-English remark about the call: a capped argument, omitted items, or an ignored unknown argument.
- `parents` (array)
- `retry_after_seconds` (integer)
- `samples_per_child_clamped_from` (number): The value you requested for samples_per_child before it was lowered to its cap.
- `source_url` (string): The video URL you passed as `source`; only present when `source` was a URL.
- `status` (string)
- `total_samples` (integer)

### `track_region_changes` (~629 tokens)

Watch one fixed rectangle of the frame (region: x, y, width, height as fractions 0-1) from start_sec to end_sec (both required) and rank the moments its pixels change most, as candidates to verify with inspect_moments or inspect_window. Use for a scoreboard, counter, indicator light or door. Slow continuous motion ranks no candidates: read steady_change.detected for that. Returns ranked candidates, not a count: for a bounded count of state transitions use track_region_events. Budget: about (end_sec - start_sec) x sample_fps frames, at most max_total_frames (default 600, maximum 2000); a call over budget fails and says the sample_fps that fits. This tool's region uses width/height (count_repetitions uses w/h).

Input parameters:

- `end_sec` (number|string, required): End of the range in seconds; must be within the video. Also accepts a timecode string in MM:SS.mmm form, e.g. 00:14.250.
- `frame_width` (integer): Decoded frame width in pixels (default 640, maximum 1920; larger values are clamped).
- `max_candidates` (integer): Most candidates returned (default 32, maximum 128; larger values are clamped). Raise min_change_score to keep only stronger ones.
- `max_total_frames` (integer): Frame budget for the whole range (default 600, maximum 2000; larger values are clamped). The call fails when (end_sec - start_sec) x sample_fps exceeds it.
- `min_change_score` (number)
- `min_separation_sec` (number|string): Seconds, or a timecode string in MM:SS.mmm form, e.g. 00:14.250.
- `region` (required): Rectangle to watch, as fractions of the frame: x, y (top-left) and width, height, with x+width <= 1 and y+height <= 1. Uses width/height, not w/h; w/h with the same meaning is also accepted. For exam…
- `robust_sigma` (number)
- `sample_fps` (number): Sampling rate (default 0.25, maximum 10; larger values are clamped and the result says so). Changes shorter than 1/sample_fps can be missed.
- `source` (string, required): Local video file path, or a video URL (X/Twitter post, YouTube link, other sites yt-dlp supports, direct .mp4 link). A URL is downloaded once and cached; while it downloads the result is status=in_pr…
- `start_sec` (number|string, required): Start of the range in seconds. Also accepts a timecode string in MM:SS.mmm form, e.g. 00:14.250.

Output parameters:

- `baseline_mad` (number)
- `baseline_median` (number)
- `candidates` (array)
- `end_sec` (number)
- `frame_width_clamped_from` (number): The value you requested for frame_width before it was lowered to its cap.
- `local_path` (string): Cached local copy of `source_url`; pass it to other tools or ffmpeg to skip the URL lookup.
- `max_candidates_clamped_from` (number): The value you requested for max_candidates before it was lowered to its cap.
- `max_total_frames_clamped_from` (number): The value you requested for max_total_frames before it was lowered to its cap.
- `note` (string): Plain-English remark about the call: a capped argument, omitted items, or an ignored unknown argument.
- `region` (object)
- `retry_after_seconds` (integer)
- `sample_count` (integer)
- `sample_fps` (number)
- `sample_fps_clamped_from` (number): The value you requested for sample_fps before it was lowered to its cap.
- `samples` (array)
- `scan_segments` (array)
- `source_url` (string): The video URL you passed as `source`; only present when `source` was a URL.
- `start_sec` (number)
- `status` (string)
- `steady_change` (object)
- `temporal_resolution_sec` (number)
- `threshold` (number)

### `track_region_events` (~634 tokens)

Count discrete state changes (transitions) inside one fixed rectangle of the frame (region: x, y, width, height as fractions 0-1) between start_sec and end_sec (both required). Returns event_count plus before/after evidence moments to verify. Use for 'how many times did the light turn on' or 'how often does the counter change'. For ranked change candidates without a count use track_region_changes; for a periodic action use count_repetitions; for a symbol appearing anywhere on screen use count_occurrences. Budget: about (end_sec - start_sec) x sample_fps frames, at most max_total_frames (default 600, maximum 2000); a call over budget fails and says the sample_fps that fits. This tool's region uses width/height (count_repetitions uses w/h).

Input parameters:

- `ambiguity_margin` (number)
- `change_threshold` (number)
- `end_sec` (number|string, required): End of the range in seconds; must be within the video. Also accepts a timecode string in MM:SS.mmm form, e.g. 00:14.250.
- `frame_width` (integer): Decoded frame width in pixels (default 640, maximum 1920; larger values are clamped).
- `max_events` (integer): Most events returned (default 32, maximum 128; larger values are clamped). Raise change_threshold to keep only stronger ones.
- `max_total_frames` (integer): Frame budget for the whole range (default 600, maximum 2000; larger values are clamped). The call fails when (end_sec - start_sec) x sample_fps exceeds it.
- `min_separation_sec` (number|string): Seconds, or a timecode string in MM:SS.mmm form, e.g. 00:14.250.
- `region` (required): Rectangle to watch, as fractions of the frame: x, y (top-left) and width, height, with x+width <= 1 and y+height <= 1. Uses width/height, not w/h; w/h with the same meaning is also accepted. For exam…
- `sample_fps` (number): Sampling rate (default 0.25, maximum 10; larger values are clamped and the result says so). Transitions shorter than 1/sample_fps can be missed.
- `source` (string, required): Local video file path, or a video URL (X/Twitter post, YouTube link, other sites yt-dlp supports, direct .mp4 link). A URL is downloaded once and cached; while it downloads the result is status=in_pr…
- `start_sec` (number|string, required): Start of the range in seconds. Also accepts a timecode string in MM:SS.mmm form, e.g. 00:14.250.

Output parameters:

- `end_sec` (number)
- `event_count` (integer|null)
- `frame_width_clamped_from` (number): The value you requested for frame_width before it was lowered to its cap.
- `incomplete` (boolean)
- `local_path` (string): Cached local copy of `source_url`; pass it to other tools or ffmpeg to skip the URL lookup.
- `max_events_clamped_from` (number): The value you requested for max_events before it was lowered to its cap.
- `max_total_frames_clamped_from` (number): The value you requested for max_total_frames before it was lowered to its cap.
- `note` (string): Plain-English remark about the call: a capped argument, omitted items, or an ignored unknown argument.
- `region` (object)
- `retry_after_seconds` (integer)
- `sample_count` (integer)
- `sample_fps` (number)
- `sample_fps_clamped_from` (number): The value you requested for sample_fps before it was lowered to its cap.
- `scan_segments` (array)
- `source_url` (string): The video URL you passed as `source`; only present when `source` was a URL.
- `start_sec` (number)
- `status` (string)
- `temporal_resolution_sec` (number)
- `transitions` (array)
- `uncertain` (boolean)
- `uncertainty_reason` (string|null)

### `inspect_moments` (~425 tokens)

Look closely around specific instants: for each moment (timestamp_sec, optional label) returns a contact sheet of frames before, at and after it (default 4 s before and 4 s after, samples_per_moment 9). Use it to verify a hit from search_visual, search_transcript, detect_cuts, audio_onsets or track_region_changes. At most 16 moments per call and max_total_samples frames in all (default 144, maximum 400); a call over budget fails and says what to lower. For one continuous span of frames use inspect_window.

Input parameters:

- `after_sec` (number|string): Seconds of context after each moment (default 4). Also accepts a timecode string in MM:SS.mmm form, e.g. 00:14.250.
- `before_sec` (number|string): Seconds of context before each moment (default 4). Also accepts a timecode string in MM:SS.mmm form, e.g. 00:14.250.
- `cols` (integer)
- `max_moments` (integer): Most moments accepted (default and maximum 16; larger values are clamped).
- `max_total_samples` (integer): Frame budget: moments x samples_per_moment must not exceed it (default 144, maximum 400; larger values are clamped).
- `moments` (array, required): Instants to inspect, e.g. [{"timestamp_sec": 12.5, "label": "door opens"}], at most 16, each within the video.
- `samples_per_moment` (integer): Frames per moment, an odd number 3-25 (default 9; larger values are clamped to 25).
- `source` (string, required): Local video file path, or a video URL (X/Twitter post, YouTube link, other sites yt-dlp supports, direct .mp4 link). A URL is downloaded once and cached; while it downloads the result is status=in_pr…

Output parameters:

- `estimated_vision_tokens` (integer)
- `local_path` (string): Cached local copy of `source_url`; pass it to other tools or ffmpeg to skip the URL lookup.
- `max_moments_clamped_from` (number): The value you requested for max_moments before it was lowered to its cap.
- `max_total_samples_clamped_from` (number): The value you requested for max_total_samples before it was lowered to its cap.
- `moments` (array)
- `note` (string): Plain-English remark about the call: a capped argument, omitted items, or an ignored unknown argument.
- `retry_after_seconds` (integer)
- `samples_per_moment_clamped_from` (number): The value you requested for samples_per_moment before it was lowered to its cap.
- `source_url` (string): The video URL you passed as `source`; only present when `source` was a URL.
- `status` (string)
- `total` (integer)
- `total_samples` (integer)

### `inspect_windows` (~353 tokens)

Dense consecutive frames (default 4 fps) for several time ranges in one call, with optional context_sec padding and merging of overlapping windows. At most 16 windows and max_total_frames frames in all (default 150, maximum 500); a call over budget fails and says to lower fps or shorten the windows. For one range use inspect_window, for a coarse survey use contact_sheet, for frames around single instants use inspect_moments.

Input parameters:

- `context_sec` (number|string): Seconds of padding added to each side of every window (default 0). Also accepts a timecode string in MM:SS.mmm form, e.g. 00:14.250.
- `fps` (number): Sampling rate (default 4, maximum 30; larger values are clamped).
- `max_total_frames` (integer): Frame budget for all windows together (default 150, maximum 500; larger values are clamped). Lower fps or shorten the windows to fit.
- `max_windows` (integer): Most windows accepted (default and maximum 16; larger values are clamped).
- `merge_overlaps` (boolean): Merge overlapping windows into one (default true).
- `source` (string, required): Local video file path, or a video URL (X/Twitter post, YouTube link, other sites yt-dlp supports, direct .mp4 link). A URL is downloaded once and cached; while it downloads the result is status=in_pr…
- `windows` (array, required): Time ranges to inspect, e.g. [{"start_sec": 10, "end_sec": 14}], at most 16, each within the video.

Output parameters:

- `estimated_vision_tokens` (integer)
- `fps_clamped_from` (number): The value you requested for fps before it was lowered to its cap.
- `local_path` (string): Cached local copy of `source_url`; pass it to other tools or ffmpeg to skip the URL lookup.
- `max_total_frames_clamped_from` (number): The value you requested for max_total_frames before it was lowered to its cap.
- `max_windows_clamped_from` (number): The value you requested for max_windows before it was lowered to its cap.
- `note` (string): Plain-English remark about the call: a capped argument, omitted items, or an ignored unknown argument.
- `retry_after_seconds` (integer)
- `source_url` (string): The video URL you passed as `source`; only present when `source` was a URL.
- `status` (string)
- `total` (integer)
- `total_frames` (integer)
- `windows` (array)

### `inspect_window` (~428 tokens)

Dense consecutive frames of ONE short window [start_sec, end_sec] (both required) with timestamp overlays, as a contact sheet plus frame list. Default 4 fps, maximum 30 (larger values are clamped). At most about 150 frames and 600 seconds per call: frames = fps x duration, so a 10 s window allows at most 15 fps; a call over budget fails and says the fps that fits. Use it to verify a moment or a count. For a wide overview use contact_sheet; for several ranges use inspect_windows. With read_text true each frame also lists the on-screen text read from it (Apple Vision, macOS only).

Input parameters:

- `end_sec` (number|string, required): End of the window in seconds; must be within the video. Also accepts a timecode string in MM:SS.mmm form, e.g. 00:14.250.
- `fps` (number): Sampling rate (default 4, maximum 30; larger values are clamped). Frames = fps x duration and must stay within about 150.
- `read_text` (boolean): Also read the on-screen text in each returned frame with Apple Vision OCR (macOS only). Each frame then lists text_lines: the text, confidence and box (x, y, width, height; 0 to 1, top-left origin) o…
- `source` (string, required): Local video file path, or a video URL (X/Twitter post, YouTube link, other sites yt-dlp supports, direct .mp4 link). A URL is downloaded once and cached; while it downloads the result is status=in_pr…
- `start_sec` (number|string, required): Start of the window in seconds. Also accepts a timecode string in MM:SS.mmm form, e.g. 00:14.250.

Output parameters:

- `contact_sheet_path` (string|null)
- `end_sec` (number)
- `estimated_vision_tokens` (integer)
- `fps` (number)
- `fps_clamped_from` (number): The value you requested for fps before it was lowered to its cap.
- `frames` (array)
- `local_path` (string): Cached local copy of `source_url`; pass it to other tools or ffmpeg to skip the URL lookup.
- `note` (string): Plain-English remark about the call: a capped argument, omitted items, or an ignored unknown argument.
- `retry_after_seconds` (integer)
- `source_path` (string)
- `source_url` (string): The video URL you passed as `source`; only present when `source` was a URL.
- `start_sec` (number)
- `status` (string)
- `tool_file` (object)

### `frame_at` (~516 tokens)

Use this when you need one exact frame at full size, or a crop to read small text; for many frames use inspect_window or contact_sheet. Returns one PNG of the frame shown at time_sec, at the source's native resolution, with no timestamp drawn on it. An optional crop (x, y, width, height in source pixels) cuts a rectangle out of that frame, and scale (1 to 4, default 1) enlarges the crop with Lanczos so small text is easier to read. frame_time_sec is the start time of the frame actually decoded: the last frame starting at or before time_sec, so on variable-frame-rate video it can be well before time_sec. A repeat call with the same source, time_sec, crop and scale returns the saved file. With read_text true it also returns the on-screen text read from that frame (Apple Vision, macOS only).

Input parameters:

- `crop` (object): Optional rectangle in source pixels: x and y (top-left corner, >= 0) and width and height (>= 1). It must lie inside the frame as displayed (rotation applied): x + width <= source width and y + heigh…
- `read_text` (boolean): Also read the on-screen text in each returned frame with Apple Vision OCR (macOS only). Each frame then lists text_lines: the text, confidence and box (x, y, width, height; 0 to 1, top-left origin) o…
- `scale` (number): Enlargement of the (cropped) frame, 1 to 4. The default 1 leaves the pixels as they are. Use 2 to 4 to read small text.
- `source` (string, required): Local video file path, or a video URL (X/Twitter post, YouTube link, other sites yt-dlp supports, direct .mp4 link). A URL is downloaded once and cached; while it downloads the result is status=in_pr…
- `time_sec` (number|string, required): Moment in seconds, from 0 to the media duration. Returns the frame shown at that moment. Also accepts a timecode string in MM:SS.mmm form, e.g. 00:14.250.

Output parameters:

- `crop` (object|null): The crop applied, in source pixels; null when no crop was given.
- `file_path` (string): The PNG on disk; open this for the full-size pixels.
- `frame_time_sec` (number|null): Start time of the frame shown at time_sec, from the decoder; it can be earlier than time_sec. Null when the decoder reports no time.
- `height` (integer): Height of the saved PNG in pixels (the crop height times scale).
- `local_path` (string): Cached local copy of `source_url`; pass it to other tools or ffmpeg to skip the URL lookup.
- `note` (string): Plain-English remark about the call: a capped argument, omitted items, or an ignored unknown argument.
- `retry_after_seconds` (integer)
- `scale` (number): The enlargement applied (1 when none was asked for).
- `source_height` (integer): Height of the source video as displayed (rotation applied), in pixels.
- `source_url` (string): The video URL you passed as `source`; only present when `source` was a URL.
- `source_width` (integer): Width of the source video as displayed (rotation applied), in pixels.
- `status` (string)
- `text_lines` (array): Lines of on-screen text read from this frame by Apple Vision, top to bottom then left to right. Present only when read_text was true. An empty list means OCR found no text in the frame.
- `text_lines_note` (string): Present when the frame had more than 40 text lines: lists the first 40 and says how many there were.
- `tool_file` (object)
- `width` (integer): Width of the saved PNG in pixels (the crop width times scale).

### `detect_cuts` (~442 tokens)

List scene changes (timestamp_sec, score, kind) in the whole video or a start_sec/end_sec range. Hard cuts and animated changes (fades, dissolves, slides, wipes, zooms) are both returned; `kind` tells them apart: "cut" or "transition". A transition's timestamp is its middle. Scores of the two kinds are not comparable. threshold 0-1 (default 0.25) applies to hard cuts only: raise it to keep only stronger cuts. A hard cut between two similar pictures is found by a second check and listed as a cut with a score below the default threshold; any threshold above 0.25 leaves these out. A change that is over within about 4 frames (a very fast wipe) is listed as a cut, and a few similar-picture cuts still come back as a transition. Film footage with heavy motion can still give false transitions; check a doubtful one with inspect_moments. At most 500 cuts are listed; `total` is the full count and the result says how to see the rest. Use it to segment a video into shots or find edit points; use audio_onsets for sounds and sync_check to compare both.

Input parameters:

- `end_sec` (number|string): Optional exclusive end of the cut-search range in absolute video seconds. Also accepts a timecode string in MM:SS.mmm form, e.g. 00:14.250.
- `source` (string, required): Local video file path, or a video URL (X/Twitter post, YouTube link, other sites yt-dlp supports, direct .mp4 link). A URL is downloaded once and cached; while it downloads the result is status=in_pr…
- `start_sec` (number|string): Optional inclusive start of the cut-search range in absolute video seconds. Also accepts a timecode string in MM:SS.mmm form, e.g. 00:14.250.
- `threshold` (number): Cut strength cutoff 0-1 (default 0.25). Raise it to keep only stronger cuts.

Output parameters:

- `cuts` (array)
- `cuts_omitted` (integer): How many cuts entries were left out of the list.
- `local_path` (string): Cached local copy of `source_url`; pass it to other tools or ffmpeg to skip the URL lookup.
- `note` (string): Plain-English remark about the call: a capped argument, omitted items, or an ignored unknown argument.
- `retry_after_seconds` (integer)
- `source_url` (string): The video URL you passed as `source`; only present when `source` was a URL.
- `status` (string)
- `total` (integer)

### `check_frame_cadence` (~348 tokens)

Find stretches where the picture updates less often than the file's frame rate, e.g. a 30 fps capture padded to 60 fps so every frame is doubled (judder). It decodes every frame at 160x90 grey and calls a frame a repeat when it is near-identical to the one before; a range is a stretch of at least 0.5 s where repeats fill at least a quarter of every half second while other frames still change. Each range gives start_sec, end_sec, frames, duplicate_frames and effective_fps (unique frames per second; compare with frame_rate). Runs of repeats longer than 0.5 s are holds (still shots, title cards, stalls) and are not ranges; held_frames counts them (use detect_cuts and the frozen-picture check in `python -m src.loop check` for those). It does not judge whether a range is intended: a 24 fps film in a 60 fps file is one range covering the whole file, and a slow, smooth pan can look like repeats. Look at a range with inspect_window to see what moves there. At most 500 ranges are listed; `total` is the full count. Reads the whole file: about 1 s per 700 frames of 1080p; there are no range arguments.

Input parameters:

- `source` (string, required): Local video file path, or a video URL (X/Twitter post, YouTube link, other sites yt-dlp supports, direct .mp4 link). A URL is downloaded once and cached; while it downloads the result is status=in_pr…

Output parameters:

- `frame_rate` (number): Frame rate in frames per second (the stream's r_frame_rate; a variable-rate file is decoded at it, gaps filled with copies).
- `frames` (integer): Frames decoded.
- `held_frames` (integer): Frames inside holds (runs of repeats longer than 0.5 s), left out of the ranges.
- `local_path` (string): Cached local copy of `source_url`; pass it to other tools or ffmpeg to skip the URL lookup.
- `note` (string): Plain-English remark about the call: omitted items or an ignored unknown argument.
- `ranges` (array)
- `ranges_omitted` (integer): How many ranges entries were left out of the list.
- `retry_after_seconds` (integer)
- `source_url` (string): The video URL you passed as `source`; only present when `source` was a URL.
- `status` (string)
- `total` (integer): Number of ranges found.

### `audio_onsets` (~547 tokens)

List non-speech sound attacks (hits, claps, beats) with millisecond timestamps and five-band energy profiles, optionally restricted to a frequency band (band_hz: sub, low_mid, mid, high, air, or [low_hz, high_hz]) and compared with another source (compare_to). min_strength 0-1 (default 0.12): raise it for only strong onsets. min_gap_sec (default 0.035 s) merges onsets within that gap of a group's first onset into one onset at the first time with the group's strongest strength; 0 keeps every raw onset. At most 500 onsets are listed; `total` is the full count and the result says how to see the rest. Needs an audio track (probe_video has_audio); audio-only files (.m4a, .mp3, .wav) work. For level over time use loudness_timeline; for speech use transcribe. To name what a sound is, use label_sounds (cloud).

Input parameters:

- `band_hz`: Restrict to a named band (sub, low_mid, mid, high, air) or a [low_hz, high_hz] range.
- `compare_to` (string): Path or video URL of another media file whose in-band energy is compared at each onset.
- `end_sec` (number|string): Optional exclusive end in seconds; omit to analyze to the end. Also accepts a timecode string in MM:SS.mmm form, e.g. 00:14.250.
- `min_gap_sec` (number|string): Onsets closer than this many seconds to a group's first onset are merged into it: the group keeps the first onset's time and the strongest strength. 0 keeps every raw onset (default 0.035). Also acce…
- `min_strength` (number): Onset strength cutoff 0-1 (default 0.12). Raise it to keep only stronger onsets.
- `source` (string, required): Local video file path, or a video URL (X/Twitter post, YouTube link, other sites yt-dlp supports, direct .mp4 link). A URL is downloaded once and cached; while it downloads the result is status=in_pr…
- `start_sec` (number|string): Optional inclusive start in seconds; omit to analyze from the beginning. Also accepts a timecode string in MM:SS.mmm form, e.g. 00:14.250.

Output parameters:

- `band_hz`
- `compare_min_delta_db` (number|null)
- `compare_to` (string|null)
- `end_sec` (number)
- `hop_samples` (integer)
- `local_path` (string): Cached local copy of `source_url`; pass it to other tools or ffmpeg to skip the URL lookup.
- `merged_count` (integer): How many detected onsets were merged into a neighbouring onset by min_gap_sec.
- `min_strength` (number)
- `note` (string): Plain-English remark about the call: a capped argument, omitted items, or an ignored unknown argument.
- `onsets` (array)
- `onsets_omitted` (integer): How many onsets entries were left out of the list.
- `retry_after_seconds` (integer)
- `sample_rate` (integer)
- `source_path` (string)
- `source_url` (string): The video URL you passed as `source`; only present when `source` was a URL.
- `start_sec` (number)
- `status` (string)
- `time_resolution_ms` (number)
- `total` (integer)

### `loudness_timeline` (~533 tokens)

Measure loudness over time: RMS, peak, band energies, spectral centroid, silence gaps, and band-energy outliers classified as steps, ramps or transients, in fixed windows (window_sec 0.01-600, default 0.25) or beat-length windows (bpm 20-300; give one or the other). At most 500 windows are listed in `windows` (`total` is the full count; `outliers` and `silence_gaps` always cover the whole range); use a larger window_sec or a start_sec/end_sec range for the rest. Ranges needing over 50,000 windows are rejected with the window_sec that fits. Needs an audio track (audio-only files work too). For discrete hits use audio_onsets. `peak_dbfs` (top level, with `peak_sec`, and per window) is the loudest sample on any channel, as ffmpeg reports sample peak; `rms_dbfs` and `band_db` are measured on the mono mix (L+R)/1.414, so on centred stereo they read up to 3 dB above a per-channel meter.

Input parameters:

- `bpm` (number): Use one-beat windows at this tempo, 20-300. Give this or window_sec, not both.
- `end_sec` (number|string): Optional exclusive end in seconds; omit to analyze to the end. Also accepts a timecode string in MM:SS.mmm form, e.g. 00:14.250.
- `ignore_ranges` (array): Ranges with a reason (e.g. an intentional silence) that are excluded from outlier detection.
- `source` (string, required): Local video file path, or a video URL (X/Twitter post, YouTube link, other sites yt-dlp supports, direct .mp4 link). A URL is downloaded once and cached; while it downloads the result is status=in_pr…
- `start_sec` (number|string): Optional inclusive start in seconds; omit to analyze from the beginning. Also accepts a timecode string in MM:SS.mmm form, e.g. 00:14.250.
- `window_sec` (number|string): Fixed window length in seconds, 0.01-600 (default 0.25). Larger values than 600 are clamped to 600. Give this or bpm, not both. Also accepts a timecode string in MM:SS.mmm form, e.g. 00:14.250.

Output parameters:

- `bpm` (number|null)
- `end_sec` (number)
- `ignore_ranges` (array)
- `local_path` (string): Cached local copy of `source_url`; pass it to other tools or ffmpeg to skip the URL lookup.
- `note` (string): Plain-English remark about the call: a capped argument, omitted items, or an ignored unknown argument.
- `outliers` (array)
- `peak_dbfs` (number): Loudest sample on any channel in the range (sample peak, as ffmpeg astats/ebur128 report it).
- `peak_sec` (number): Time of that loudest sample.
- `retry_after_seconds` (integer)
- `silence_gaps` (array)
- `silence_threshold_dbfs` (number)
- `source_path` (string)
- `source_url` (string): The video URL you passed as `source`; only present when `source` was a URL.
- `start_sec` (number)
- `status` (string)
- `total` (integer)
- `window_sec` (number)
- `window_sec_clamped_from` (number): The value you requested for window_sec before it was lowered to its cap.
- `windows` (array)
- `windows_omitted` (integer): How many windows entries were left out of the list.

### `spectrogram_window` (~281 tokens)

Render a log-frequency dB spectrogram PNG for [start_sec, end_sec] (both required, at most 600 s) with absolute timecode badges and optional event marks (time_sec + label). Use a short window (a few seconds) to look at one sound; find candidate times with audio_onsets or loudness_timeline first. Needs an audio track (audio-only files work too).

Input parameters:

- `end_sec` (number|string, required): End of the window in seconds; the window may be at most 600 s long. Also accepts a timecode string in MM:SS.mmm form, e.g. 00:14.250.
- `marks` (array): Optional event markers drawn on the image, e.g. [{"time_sec": 12.5, "label": "kick"}].
- `source` (string, required): Local video file path, or a video URL (X/Twitter post, YouTube link, other sites yt-dlp supports, direct .mp4 link). A URL is downloaded once and cached; while it downloads the result is status=in_pr…
- `start_sec` (number|string, required): Start of the window in seconds. Also accepts a timecode string in MM:SS.mmm form, e.g. 00:14.250.

Output parameters:

- `end_sec` (number)
- `file_path` (string)
- `frequency_ticks` (array)
- `height` (integer)
- `hop_samples` (integer)
- `local_path` (string): Cached local copy of `source_url`; pass it to other tools or ffmpeg to skip the URL lookup.
- `marks` (array)
- `note` (string): Plain-English remark about the call: a capped argument, omitted items, or an ignored unknown argument.
- `retry_after_seconds` (integer)
- `sample_rate` (integer)
- `source_path` (string)
- `source_url` (string): The video URL you passed as `source`; only present when `source` was a URL.
- `start_sec` (number)
- `status` (string)
- `timecode_badges` (array)
- `tool_file` (object)
- `width` (integer)

### `sync_check` (~377 tokens)

Check audio/video sync: compares visual cuts with measured audio onsets, and optionally with intended sound cues (time_sec, label, optional band_hz), within tolerance_ms (0-5000, default 80). Returns per-cut and per-cue matches and unmatched strong onsets (the cuts and unmatched_strong_onsets lists show at most 500 items each; the cues list is never cut short; total_cuts and total_cues are the full counts). Each cue reports candidate_onsets_count, the number of onsets near it; verbose true reports that cue's candidate_onsets list instead of the count. Needs an audio track. To look at the two signals separately use detect_cuts and audio_onsets.

Input parameters:

- `bpm` (number): Tempo of the music, 20-300. Compares each cue with uncued moments at the same beat phase, so steady backing percussion does not hide a missing sound.
- `cues` (array): Intended sound cues to check, e.g. [{"time_sec": 12.5, "label": "hit"}]. Omit to compare cuts with onsets only.
- `source` (string, required): Local video file path, or a video URL (X/Twitter post, YouTube link, other sites yt-dlp supports, direct .mp4 link). A URL is downloaded once and cached; while it downloads the result is status=in_pr…
- `tolerance_ms` (number): Match window in milliseconds (default 80, maximum 5000; larger values are clamped).
- `verbose` (boolean): false (default) gives each cue candidate_onsets_count; true gives each cue candidate_onsets (the onsets near it with time_sec, offset_ms and strength) instead of the count.

Output parameters:

- `beats_per_bar` (integer|null): Beats per bar used with bpm; null without bpm.
- `bpm` (number|null): The tempo passed in, or null.
- `cue_recovery_window_ms` (number)
- `cue_signatures` (object): Learned sound signature for each cue group, keyed by group name.
- `cues` (array)
- `cuts` (array)
- `cuts_omitted` (integer): How many cuts entries were left out of the list.
- `local_path` (string): Cached local copy of `source_url`; pass it to other tools or ffmpeg to skip the URL lookup.
- `note` (string): Plain-English remark about the call: a capped argument, omitted items, or an ignored unknown argument.
- `retry_after_seconds` (integer)
- `search_window_ms` (number): The window used to pair cues with onsets, in ms.
- `source_path` (string)
- `source_url` (string): The video URL you passed as `source`; only present when `source` was a URL.
- `status` (string)
- `strong_onset_threshold` (number)
- `systematic_lags` (array)
- `tolerance_ms` (number)
- `tolerance_ms_clamped_from` (number): The value you requested for tolerance_ms before it was lowered to its cap.
- `total_cues` (integer)
- `total_cuts` (integer)
- `uncued_sounds` (array): Strong sounds with no cue, found from a cue group's repeated sound.
- `unmatched_strong_onsets` (array)
- `unmatched_strong_onsets_omitted` (integer): How many unmatched_strong_onsets entries were left out of the list.

### `label_sounds` (~596 tokens)

Name the sounds in a stretch of a video's audio: cuts [start_sec, end_sec] (both required) into windows of window_sec (default 5 s, at most 60 windows per call) and returns, per window, the chance of each label plus none_chance, from a cloud model (Clef-omni on Cloudflare Workers AI). THIS SENDS THE AUDIO TO CLOUDFLARE: never use it on personal recordings. The server needs CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN (or AGENTIC_VIDEO_CF_TOKEN_COMMAND, a command that prints the token) and the call fails with a message naming what is missing. `labels` (default: 20 common sounds such as dog barking, siren, glass breaking, church bells) is the closed list to choose from, so a sound outside the list is forced onto the nearest label or none: list every sound that may be present. Steady musical chords can be named church bells, so treat top_chance under 0.5 as weak and check a hit with spectrogram_window. top_label is null when none_chance beats every label. It does not search: find candidate times with audio_onsets or loudness_timeline first, then label those ranges. For speech use transcribe or search_transcript. Needs an audio track. No window starts after about 40 s and the call answers within about 50 s: a longer range returns the windows finished in time with complete=false and next_start_sec to continue from.

Input parameters:

- `end_sec` (number|string, required): End of the range in seconds; at least 1 s after start_sec, and the range may hold at most 60 windows. Also accepts a timecode string in MM:SS.mmm form, e.g. 00:14.250.
- `labels` (array): Sound names to choose from, e.g. ["dog barking", "siren"] (at most 50, distinct). Default: 20 common sounds.
- `source` (string, required): Local video file path, or a video URL (X/Twitter post, YouTube link, other sites yt-dlp supports, direct .mp4 link). A URL is downloaded once and cached; while it downloads the result is status=in_pr…
- `start_sec` (number|string, required): Start of the range in seconds. Also accepts a timecode string in MM:SS.mmm form, e.g. 00:14.250.
- `window_sec` (number|string): Length of each labelled window in seconds (default 5, from 1 to 30). The last window may be shorter; a remainder under 1 s is not labelled. Also accepts a timecode string in MM:SS.mmm form, e.g. 00:1…

Output parameters:

- `complete` (boolean): false when the call stopped early (the time limit or a failed window); the windows returned are the finished ones in order.
- `end_sec` (number)
- `input_tokens` (integer)
- `labels` (array)
- `local_path` (string)
- `model` (string)
- `next_start_sec` (number|null): Where to start the next call so the unfinished windows are covered; null when complete.
- `note` (string): Plain-English remark about the call: a capped argument, omitted items, or an ignored unknown argument.
- `retry_after_seconds` (number)
- `source_path` (string)
- `source_url` (string)
- `start_sec` (number)
- `status` (string)
- `window_sec` (number)
- `windows` (array)

### `watch_for` (~764 tokens)

Watch a stretch of a video for yes/no conditions with a cloud model (Clef on Cloudflare Workers AI) and return the moments each one started to hold. One frame every interval_sec (default 5 s, 1 to 600) from [start_sec, end_sec] is asked every condition (1 to 8 yes/no questions about ONE frame, e.g. "Is a person visible?"); a condition wakes at the sample where its chance of yes has been at least cut (default 0.5) on `consecutive` samples in a row (default 2), at most once per cooldown_sec (default 30) per condition. THIS SENDS VIDEO FRAMES TO CLOUDFLARE: never use it on personal recordings. The server needs CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN (or AGENTIC_VIDEO_CF_TOKEN_COMMAND, a command that prints the token) and the call fails naming what is missing, before anything is sent. Only the questions you write can wake: a condition nobody asked about is never reported, so list every event you care about. An event must last at least consecutive x interval_sec to be caught (two samples 5 s apart need about 10 s); lower interval_sec or consecutive for shorter ones, which costs more calls. Accuracy on real footage is not yet measured. At most 720 samples per call; the call answers within about 50 s even when Cloudflare is slow: a longer range, or a sample that keeps failing after its retry, returns the finished samples with complete=false and next_start_sec to continue from (the cooldown does not carry over). This is for finding when something starts to be true; to inspect what happened use inspect_window, for scene changes use detect_cuts, and for sounds use audio_onsets. Results are not cached.

Input parameters:

- `conditions` (array, required): 1 to 8 distinct yes/no questions about one frame, e.g. ["Is a person visible?", "Is a red rectangle visible?"]. Name what to look for; vague questions give vague chances.
- `consecutive` (integer): Samples in a row that must count as yes before a wake (1 to 5, default 2).
- `cooldown_sec` (number|string): Seconds after a wake during which the same condition wakes no more (0 to 3600, default 30). Also accepts a timecode string in MM:SS.mmm form, e.g. 00:14.250.
- `cut` (number): Chance of yes (above 0, up to 1) at which a sample counts as yes (default 0.5).
- `end_sec` (number|string): End of the range in seconds (default: the end of the video). Also accepts a timecode string in MM:SS.mmm form, e.g. 00:14.250.
- `interval_sec` (number|string): Seconds between samples (default 5, from 1 to 600). The range may hold at most 720 samples. Also accepts a timecode string in MM:SS.mmm form, e.g. 00:14.250.
- `source` (string, required): Local video file path, or a video URL (X/Twitter post, YouTube link, other sites yt-dlp supports, direct .mp4 link). A URL is downloaded once and cached; while it downloads the result is status=in_pr…
- `start_sec` (number|string): Start of the range in seconds (default 0). Also accepts a timecode string in MM:SS.mmm form, e.g. 00:14.250.

Output parameters:

- `complete` (boolean)
- `conditions` (array)
- `consecutive` (integer)
- `cooldown_sec` (number)
- `cut` (number)
- `input_tokens` (integer)
- `interval_sec` (number)
- `local_path` (string)
- `model` (string)
- `next_start_sec` (number|null): Where to continue when complete is false; null when complete.
- `note` (string): Plain-English remark about the call: a capped argument, omitted items, or an ignored unknown argument.
- `retry_after_seconds` (number)
- `samples` (integer)
- `scanned_to_sec` (number): Time of the last sample looked at.
- `source_path` (string)
- `source_url` (string)
- `start_sec` (number)
- `status` (string)
- `wakes` (array)

## Diagnostics

Captured diagnostic sections: Provenance, Install scripts, Dependencies. The full working is on the page: https://verifymcp.io/servers/tlq5l-agentic-video/agentic-video#diagnostics

## Score history

- 2026-11-05: 77
- 2026-11-04: 76
- 2026-11-03: 76
- 2026-11-02: 75
- 2026-11-01: 75
- 2026-10-31: 59

## Common questions

### What is the io.github.tlq5l/agentic-video MCP server?

io.github.tlq5l/agentic-video is an MCP server listed in the public MCP registry as io.github.tlq5l/agentic-video. Local video tools for AI agents: contact sheets, transcripts, cuts, loudness and sync checks. This page covers its PyPI package (agentic-video).

### Is the io.github.tlq5l/agentic-video MCP server safe to use?

io.github.tlq5l/agentic-video scores 77 out of 100 on VerifyMCP. We found no known CVEs affecting it as of 5 November 2026. Its build provenance is signed and verified. That is a record of what we were able to check automatically, not an endorsement. The category breakdown on this page shows every signal behind the number, including the ones we could not confirm.

### What tools does the io.github.tlq5l/agentic-video MCP server expose?

io.github.tlq5l/agentic-video exposes 25 tools: count_repetitions, count_occurrences, probe_video, transcribe, search_transcript, and 20 more. Their descriptions and schemas cost roughly 11,897 tokens of context every time the server is loaded.

### Is the io.github.tlq5l/agentic-video MCP server still maintained?

io.github.tlq5l/agentic-video is still listed as active in the MCP registry. We last reached this channel on 5 November 2026. Those dates come from our own scans of the registry and the channel itself, not from anything the publisher announced.

### What licence is the io.github.tlq5l/agentic-video MCP server under?

io.github.tlq5l/agentic-video declares the MIT licence, which is OSI-approved. That covers the source only, and says nothing about the cost of any service it calls.

### What did VerifyMCP test on the io.github.tlq5l/agentic-video MCP server?

VerifyMCP ran 16 checks on io.github.tlq5l/agentic-video, covering Malware scan, Known CVEs, Install scripts, Dependency health and 12 more. Endpoint Security and Transport & Reachability do not apply to a PyPI package, so they were not tested. The score does not measure everything, and the scoring documentation lists what it leaves out.

## Links

- PyPI project: https://pypi.org/project/agentic-video/
- Socket report: https://socket.dev/pypi/package/agentic-video
- Repository: https://github.com/tlq5l/agentic-video
- Changelog RSS feed: https://verifymcp.io/servers/tlq5l-agentic-video/agentic-video.xml
- Changelog JSON feed: https://verifymcp.io/servers/tlq5l-agentic-video/agentic-video/changelog.json
- Trust report (JSON): https://verifymcp.io/servers/tlq5l-agentic-video/agentic-video.json
- HTML version of this page: https://verifymcp.io/servers/tlq5l-agentic-video/agentic-video
