agentic-video 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentic_video-0.1.0.dist-info/METADATA +219 -0
- agentic_video-0.1.0.dist-info/RECORD +41 -0
- agentic_video-0.1.0.dist-info/WHEEL +4 -0
- agentic_video-0.1.0.dist-info/entry_points.txt +3 -0
- agentic_video-0.1.0.dist-info/licenses/LICENSE +21 -0
- src/adapters/mcp_definitions.legacy.ts +367 -0
- src/adapters/mcp_definitions.ts +295 -0
- src/adapters/mcp_schemas.json +2745 -0
- src/adapters/model_adapter.py +132 -0
- src/core/__init__.py +43 -0
- src/core/cache.py +240 -0
- src/core/config.py +56 -0
- src/core/errors.py +46 -0
- src/core/types.py +167 -0
- src/executor/mcp_server.py +1007 -0
- src/executor/types.d.ts +1247 -0
- src/jev.py +103 -0
- src/loop.py +1886 -0
- src/media/__init__.py +23 -0
- src/media/audio.py +3715 -0
- src/media/contact_sheet.py +368 -0
- src/media/fetch.py +186 -0
- src/media/frames.py +252 -0
- src/media/overlay.py +93 -0
- src/media/probe.py +169 -0
- src/media/sampling.py +380 -0
- src/media/scene.py +782 -0
- src/media/seeking.py +140 -0
- src/media/timing.py +170 -0
- src/media/visual_timing.py +885 -0
- src/retrieval/__init__.py +4 -0
- src/retrieval/engine.py +1269 -0
- src/retrieval/occurrence.py +835 -0
- src/retrieval/ocr.py +74 -0
- src/retrieval/periodicity.py +164 -0
- src/retrieval/repetition.py +590 -0
- src/retrieval/visual.py +459 -0
- src/transcript/__init__.py +9 -0
- src/transcript/asr.py +1454 -0
- src/transcript/repeats.py +40 -0
- src/transcript/search.py +113 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: agentic-video
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Local video tools for AI agents that read text and images but not video: contact sheets, transcripts, cut, loudness and sync checks, served over MCP.
|
|
5
|
+
Project-URL: Homepage, https://github.com/tlq5l/agentic-video
|
|
6
|
+
Project-URL: Repository, https://github.com/tlq5l/agentic-video
|
|
7
|
+
Project-URL: Issues, https://github.com/tlq5l/agentic-video/issues
|
|
8
|
+
Author: tlq5l
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agents,contact-sheet,ffmpeg,mcp,video,whisper
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Multimedia :: Video
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: av<19
|
|
18
|
+
Requires-Dist: faster-whisper>=1.0
|
|
19
|
+
Requires-Dist: numpy>=1.21
|
|
20
|
+
Requires-Dist: pillow>=10
|
|
21
|
+
Provides-Extra: visual
|
|
22
|
+
Requires-Dist: pillow>=10; extra == 'visual'
|
|
23
|
+
Requires-Dist: pyobjc-framework-vision>=10; (sys_platform == 'darwin') and extra == 'visual'
|
|
24
|
+
Requires-Dist: torch<3,>=2.6; extra == 'visual'
|
|
25
|
+
Requires-Dist: transformers<5,>=4.45; extra == 'visual'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# agentic-video
|
|
29
|
+
|
|
30
|
+
agentic-video lets an AI agent that reads text and images, but not video, work with video files on your own machine. It is a Python library plus a stdio MCP server. The agent asks small, targeted questions ("show me 12 frames from 0:40 to 1:10", "when is 'launch' spoken?") and gets back measurements, transcripts and contact sheets: grids of frames, each with its timestamp burned into the pixels. Nothing is uploaded; everything runs locally with FFmpeg, Whisper and optional local models.
|
|
31
|
+
|
|
32
|
+
**Install:** `brew install ffmpeg`, then add it to Claude Code with `claude mcp add -s user agentic-video -- uvx agentic-video` (or `pipx install agentic-video`). Other clients: [Run the MCP server](#run-the-mcp-server).
|
|
33
|
+
|
|
34
|
+

|
|
35
|
+
|
|
36
|
+
*One `contact_sheet` call on a 5-second clip: six frames, each stamped with its time, as a single image the agent can read.*
|
|
37
|
+
|
|
38
|
+
In 2 minutes (macOS) your agent can answer "what happens in this video?" and "when is X said?" for a local video file. You get the length and size, a contact sheet image with timestamps, the cuts, a loudness summary and searchable speech. Nothing leaves your machine. Setup for Claude Code:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
brew install ffmpeg uv
|
|
42
|
+
git clone https://github.com/tlq5l/agentic-video.git && cd agentic-video
|
|
43
|
+
uv venv --python 3.12 .venv && uv pip install --python .venv/bin/python -r requirements.txt
|
|
44
|
+
claude mcp add -s user agentic-video -e PYTHONPATH="$PWD" -- "$PWD/.venv/bin/python" -m src.executor.mcp_server
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Then ask: "Use agentic-video to review `/path/to/video.mp4`." Other clients and details: [Run the MCP server](#run-the-mcp-server).
|
|
48
|
+
|
|
49
|
+
**Status:** experimental. The author uses it every day; support is best-effort. Issues and pull requests are welcome, but expect slow replies.
|
|
50
|
+
|
|
51
|
+
## What it gives an agent
|
|
52
|
+
|
|
53
|
+
The MCP server exposes 22 tools (full schemas in `src/adapters/mcp_schemas.json`):
|
|
54
|
+
|
|
55
|
+
- **Look:** `review_video` (first look in one call: metadata, contact sheet, cuts and a loudness summary), `probe_video` (duration, fps, size, codecs, audio), `contact_sheet`, `overview_chunks` (consecutive sheets across a long video), `inspect_window` / `inspect_windows` (dense frames for short ranges), `inspect_moments` (frames around given instants), `refine_temporal_beam` (zoom into candidate windows).
|
|
56
|
+
- **Listen:** `transcribe` (timestamped speech, cached), `search_transcript` (find when words are spoken).
|
|
57
|
+
- **Find:** `detect_cuts` (hard cuts and animated scene changes), `check_frame_cadence` (stretches where frames repeat so the picture updates less often than the frame rate, such as 30 fps padded to 60), `build_visual_index` + `search_visual` (local CLIP search for things that are visible, optional).
|
|
58
|
+
- **Count and track:** `count_occurrences` (separate appearances of an object or on-screen text), `count_repetitions` (cycles of a repeated action), `track_region_changes` / `track_region_events` (changes inside one fixed rectangle).
|
|
59
|
+
- **Audio and sync:** `audio_onsets`, `loudness_timeline`, `spectrogram_window`, `sync_check` (cuts and sound cues against measured audio).
|
|
60
|
+
|
|
61
|
+
There is also a command-line checker for rendered videos: `python -m src.loop check <video>` measures sync, loudness, onsets, cuts and some picture faults, then writes `report.md` plus evidence images. `python -m src.loop cadence <video>` prints the `check_frame_cadence` result as JSON.
|
|
62
|
+
|
|
63
|
+
`AGENTS.md` is a guide for agents on which tool to use when. `FEATURES.md` maps each feature to its code and tests.
|
|
64
|
+
|
|
65
|
+
## Install (macOS)
|
|
66
|
+
|
|
67
|
+
You need Homebrew, FFmpeg, Git and [uv](https://docs.astral.sh/uv/). Tested with Python 3.12 on Apple silicon.
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
brew install ffmpeg uv
|
|
71
|
+
git clone https://github.com/tlq5l/agentic-video.git
|
|
72
|
+
cd agentic-video
|
|
73
|
+
uv venv --python 3.12 .venv
|
|
74
|
+
uv pip install --python .venv/bin/python -r requirements.txt pytest
|
|
75
|
+
.venv/bin/python -m pytest -q # optional: about 3 minutes
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The test run is optional for users.
|
|
79
|
+
|
|
80
|
+
To lint before every commit (`uvx ruff check .`, the core rules in `ruff.toml`), turn the hook on once per clone: `git config core.hooksPath .githooks`.
|
|
81
|
+
|
|
82
|
+
What is macOS-only:
|
|
83
|
+
|
|
84
|
+
- On-screen text matching in `count_occurrences` (`text=`) uses Apple Vision through pyobjc.
|
|
85
|
+
- The bench-v1 harness uses `sandbox-exec`; some tests and generators use `say`. Tests that need a missing tool skip themselves.
|
|
86
|
+
|
|
87
|
+
The core tools should also work on Linux with FFmpeg installed, but the author has not tested that.
|
|
88
|
+
|
|
89
|
+
### Optional: faster, more accurate speech (whisper.cpp turbo)
|
|
90
|
+
|
|
91
|
+
By default `transcribe` uses faster-whisper on the CPU (`tiny`, `base` or `small`; weights are downloaded on first use). On Apple silicon, whisper.cpp with the large-v3-turbo model is much faster and more accurate:
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
brew install whisper-cpp
|
|
95
|
+
mkdir -p ~/.cache/whisper.cpp
|
|
96
|
+
curl -L -o ~/.cache/whisper.cpp/ggml-large-v3-turbo-q5_0.bin \
|
|
97
|
+
https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3-turbo-q5_0.bin
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Set `AGENTIC_VIDEO_WHISPER_CPP_MODEL` if you keep the model somewhere else. When it is installed, `turbo` becomes the default model.
|
|
101
|
+
|
|
102
|
+
### Optional: visual search (CLIP) and on-screen text
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
uv pip install --python .venv/bin/python -r requirements-visual.txt
|
|
106
|
+
.venv/bin/python -c 'from huggingface_hub import snapshot_download; snapshot_download(repo_id="openai/clip-vit-base-patch32", revision="3d74acf9a28c67741b2f4f2ea7635f0aaf6f0268", allow_patterns=["*.json", "*.txt", "pytorch_model.bin"])'
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
This installs PyTorch, Transformers and (on macOS) the Apple Vision bindings, and downloads about 605 MB of CLIP weights. The server only loads local files and never calls a paid API. Without these, `search_visual` and `build_visual_index` return an error that explains the setup.
|
|
110
|
+
|
|
111
|
+
## Run the MCP server
|
|
112
|
+
|
|
113
|
+
The server speaks MCP over stdio, one JSON message per line:
|
|
114
|
+
|
|
115
|
+
```sh
|
|
116
|
+
.venv/bin/python -m src.executor.mcp_server
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
To add it to an MCP client (Claude Code, Claude Desktop, Cursor and others), use the venv's Python and put the repository on `PYTHONPATH`, so the server starts from any folder:
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"mcpServers": {
|
|
124
|
+
"agentic-video": {
|
|
125
|
+
"command": "/absolute/path/to/agentic-video/.venv/bin/python",
|
|
126
|
+
"args": ["-m", "src.executor.mcp_server"],
|
|
127
|
+
"env": { "PYTHONPATH": "/absolute/path/to/agentic-video" }
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Give tools absolute video paths. Results are cached under `~/.cache/agentic-video` (change it with `AGENTIC_VIDEO_CACHE_DIR`).
|
|
134
|
+
|
|
135
|
+
### Add it to Claude Code (one command)
|
|
136
|
+
|
|
137
|
+
Run this from the cloned folder (it uses the current folder's path). `-s user` makes the tool available in every project, not only this folder:
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
claude mcp add -s user agentic-video -e PYTHONPATH="$PWD" -- "$PWD/.venv/bin/python" -m src.executor.mcp_server
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
By default results stay small enough for Claude Code: images are saved as JPEG files and the result gives their `file_path` (the agent opens the file), and a long `transcribe` result lists the first part of the transcript plus a `transcript_path` file for the rest. A client that renders images from the result itself can add `-e AGENTIC_VIDEO_INLINE_IMAGES=1`; results then carry base64 images and can pass 1 MB. Changed: earlier versions included base64 images by default; if you relied on that, set `AGENTIC_VIDEO_INLINE_IMAGES=1` to restore it.
|
|
144
|
+
|
|
145
|
+
### Tell your agent to use it (paste into your AGENTS.md or CLAUDE.md)
|
|
146
|
+
|
|
147
|
+
An always-loaded note works better than hoping the agent finds the tool:
|
|
148
|
+
|
|
149
|
+
```md
|
|
150
|
+
## Video files (agentic-video MCP)
|
|
151
|
+
You cannot watch video. For any .mp4/.mov/.webm question, use the agentic-video MCP tools; never guess from the filename.
|
|
152
|
+
- First look: `review_video` with an absolute path. It returns metadata, a contact sheet, cuts and loudness. Open the JPEG at `file_path` to see the frames.
|
|
153
|
+
- Spoken words: `search_transcript` (when is X said) or `transcribe`. Do this before looking at frames.
|
|
154
|
+
- A short range: `inspect_window` (start_sec, end_sec). Counting: `count_occurrences`. Sync and audio faults: `sync_check`, `loudness_timeline`.
|
|
155
|
+
- Sampling can miss brief events and a missing hit is not proof. Quote the burned-in timestamps. Full guide: AGENTS.md in the agentic-video repo.
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Five-line example
|
|
159
|
+
|
|
160
|
+
From the repository folder: a handshake, the video's metadata, and a 6-frame contact sheet (its JPEG path is in `file_path`):
|
|
161
|
+
|
|
162
|
+
```sh
|
|
163
|
+
printf '%s\n' \
|
|
164
|
+
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \
|
|
165
|
+
'{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"probe_video","arguments":{"source":"fixtures/reference_videos/flower.mp4"}}}' \
|
|
166
|
+
'{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"contact_sheet","arguments":{"source":"fixtures/reference_videos/flower.mp4","samples":6,"cols":3}}}' \
|
|
167
|
+
| .venv/bin/python -m src.executor.mcp_server
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
The same from Python:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from src.retrieval.engine import AgenticVideoEngine
|
|
174
|
+
|
|
175
|
+
engine = AgenticVideoEngine("fixtures/reference_videos/flower.mp4")
|
|
176
|
+
print(engine.metadata.duration_sec) # 5.005
|
|
177
|
+
print(engine.overview(samples=6, cols=3).file_path) # contact sheet JPEG
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Results so far
|
|
181
|
+
|
|
182
|
+
These are the author's own small tests, not independent benchmarks. The raw runs are not part of this repository.
|
|
183
|
+
|
|
184
|
+
- **Planted faults:** in promo videos with 20 deliberately planted faults, the checker (`python -m src.loop check`) found 14 of 20, with 2 false alarms. Gemini in agentic video mode found 4 of 20.
|
|
185
|
+
- **bench-v1 (`benchmarks/bench_v1`):** on the author's small benchmark, Claude using these tools beat Gemini's agentic video mode at counting, finding moments and long-video search.
|
|
186
|
+
- **LVBench (public long-video benchmark):** it scored about 86%. Gemini's published score is 88.6%, so it does not beat Gemini there.
|
|
187
|
+
|
|
188
|
+
It does not beat Gemini's video understanding in general.
|
|
189
|
+
|
|
190
|
+
## Limits
|
|
191
|
+
|
|
192
|
+
- **Images come back as files.** Image tools return `file_path`, not an MCP image content block; the base64 copy in `structuredContent` is off by default (set `AGENTIC_VIDEO_INLINE_IMAGES=1` to include it). Many clients show the agent only the JSON text, so the agent has to open the JPEG at `file_path` itself (for example with its file-reading tool).
|
|
193
|
+
- **Sampling can miss things.** Contact sheets and visual search look at sampled frames; something brief between samples can be missed, and a missing hit does not prove absence. Visual search runs CLIP on the CPU and is slow on long videos.
|
|
194
|
+
- **Transcripts can invent lines.** Whisper (especially turbo) can invent lines over music or silence. A line repeated 3 or more times in a row is flagged `suspect`; a single invented line is not.
|
|
195
|
+
- **Cuts can be missed.** A hard cut between two similar pictures that scores under the 0.25 threshold can be missed.
|
|
196
|
+
- **Counting and sync use heuristics** tuned on the author's videos. Treat their output as evidence to check, not as ground truth.
|
|
197
|
+
- **The fix loop needs extra tools.** `python -m src.loop run` calls the Codex CLI as its fixer (default model ladder in `src/loop.py`). The optional `--judge jev` needs `TYPESAFE_API_KEY` or `OPENROUTER_API_KEY` in the environment.
|
|
198
|
+
- **bench-v1 needs accounts.** Running it needs the Codex CLI, `GEMINI_API_KEY` in the environment, Node.js and macOS `sandbox-exec`; see `benchmarks/bench_v1/README.md`.
|
|
199
|
+
- **No stable API.** Tool names and outputs may change between commits.
|
|
200
|
+
|
|
201
|
+
## Repository layout
|
|
202
|
+
|
|
203
|
+
- `src/`: the library (`media/`, `transcript/`, `retrieval/`, `core/`), the MCP server (`executor/mcp_server.py`), the checker (`loop.py`) and tool schemas (`adapters/`).
|
|
204
|
+
- `tests/`: the pytest suite. `evals/adversarial_runner.py` holds the failure-injection checks that `tests/test_adversarial_failures.py` runs.
|
|
205
|
+
- `benchmarks/bench_v1/`: the blind three-contestant benchmark harness. Its data and answer key live outside the repo.
|
|
206
|
+
- `benchmarks/dev/count_dev/`: a small development set for the counting tools.
|
|
207
|
+
- `scripts/generate_synthetic_benchmark.py`: regenerates the synthetic fixture video.
|
|
208
|
+
- `fixtures/`: small test videos (see Media credits).
|
|
209
|
+
- `docs/`: architecture notes and per-feature pages.
|
|
210
|
+
|
|
211
|
+
## Media credits
|
|
212
|
+
|
|
213
|
+
- `fixtures/reference_videos/sample_10s.mp4`: a 10-second excerpt, with no audio, of *Big Buck Bunny* (Sunflower version). (c) 2008 Blender Foundation, www.bigbuckbunny.org / peach.blender.org; Sunflower version by Janus Bager Kristensen, 2013 (bbb3d.renderfarming.net). Licensed under [Creative Commons Attribution 3.0](https://creativecommons.org/licenses/by/3.0/).
|
|
214
|
+
- `fixtures/reference_videos/flower.mp4`: a sample video from MDN Web Docs, released under CC0 (public domain).
|
|
215
|
+
- `fixtures/synthetic_bench_60s.mp4` with `.srt`, `.vtt` and `fixtures/synthetic_ground_truth.json`: generated by this repository's `scripts/generate_synthetic_benchmark.py`. Same license as the code.
|
|
216
|
+
|
|
217
|
+
## License
|
|
218
|
+
|
|
219
|
+
MIT. See `LICENSE`.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
src/jev.py,sha256=uQItdgwOZRkOfKnL-9xWI_3jddUSeH9_z-ZnY_5GzIw,4031
|
|
2
|
+
src/loop.py,sha256=hXCUhJ87E78HrnniFwM5oT-_6IwZ28vEesCrWa1xLuE,94735
|
|
3
|
+
src/adapters/mcp_definitions.legacy.ts,sha256=qJEcAMQJN1LzntfQ9ar2UHePwL6Qe4jUIlfqcpYVJIQ,13987
|
|
4
|
+
src/adapters/mcp_definitions.ts,sha256=13UkweYGeHZ3u877Q2uKd2PEsIpg_ARQrfuEYSBKT8c,37777
|
|
5
|
+
src/adapters/mcp_schemas.json,sha256=bdYiKdm8atIh2hJy3bxX8zBkQnUKoxZERzNvKpXntQk,148385
|
|
6
|
+
src/adapters/model_adapter.py,sha256=O9VN9t_WgQkV7xDrOyivPB3nfqPr2eN-n8nb_v5ncK8,6176
|
|
7
|
+
src/core/__init__.py,sha256=wgvx4R1WEXEPJEvcNBG21RHKa3CEtLEFhdK0vjX4bEU,932
|
|
8
|
+
src/core/cache.py,sha256=PX4RRnhVbFUGfbFCPy71bVDldo15fMjjIIwd_EXYSD0,10701
|
|
9
|
+
src/core/config.py,sha256=P5kRqN7jIHvG3XxEcyyKjb7p0gD_KjZZOozXabMzyi8,2051
|
|
10
|
+
src/core/errors.py,sha256=7pPTRKcZUBEAvL6oGs2Ura92DqIAeIbJwCrnGgZ1CxQ,1556
|
|
11
|
+
src/core/types.py,sha256=XHw-4weD6pYSVpCOrm7ZLwFIoaeRcG0wiwSkMPkaWZo,5414
|
|
12
|
+
src/executor/mcp_server.py,sha256=-gZwNQVjqdkvsSzNPlsqppT5dYZiesWzfBxQ7kz22Ps,48244
|
|
13
|
+
src/executor/types.d.ts,sha256=MRfYpKzO5LzdJlma4fAiMhLd0syGjFQDEYnqb1pz1Q0,46925
|
|
14
|
+
src/media/__init__.py,sha256=8joiX4SrVjbNf0-FUTKAL5XF7FjhFw_tihvwIgT1Ab8,886
|
|
15
|
+
src/media/audio.py,sha256=g1Ngwxgl6Z3NVGnWFZFdN0jZ9SnpcDMjk6YSMYOnoUU,188946
|
|
16
|
+
src/media/contact_sheet.py,sha256=Z64QuvvaBDTq16Xvt498HIyci2raMwRo_9RNTRErTtM,14303
|
|
17
|
+
src/media/fetch.py,sha256=MnCrNjOq1a8uq2vV43_cUVRaJqpECLlPnLqbJa18TYY,7449
|
|
18
|
+
src/media/frames.py,sha256=NZC9_QqAI4RVyroG6hpvSJGvKgYkAtRVMJzghS3MUzU,9777
|
|
19
|
+
src/media/overlay.py,sha256=dG3Pwe8S5b5JuIAG47w8mjibAtLij6vnwt4w7bsJJzc,3132
|
|
20
|
+
src/media/probe.py,sha256=vcPxN1yTnABlwFyTbkpsotAxxxvKRr_7CRfLDe_LSZk,5452
|
|
21
|
+
src/media/sampling.py,sha256=r5GTDofJWHMAyG7woZnIKEoQklLFikBw_I3b8OTKjtk,14716
|
|
22
|
+
src/media/scene.py,sha256=3AEoJXeH46yXKnmb7znRhsbiOtkYsEaWLyVkpxBKMow,35799
|
|
23
|
+
src/media/seeking.py,sha256=qgJR5fuaOmypHQAxJhswgVo8Xbv5bD06VeacfvJyWCM,4797
|
|
24
|
+
src/media/timing.py,sha256=cycLhpuBXDT0bl9Nk_CpeUK4ah60__nfr9XvFacQ27o,5638
|
|
25
|
+
src/media/visual_timing.py,sha256=PVeA9DI02F4gFbAXh7WQlTQN6F3xMSnGJZpCDY9JWl8,40808
|
|
26
|
+
src/retrieval/__init__.py,sha256=apeTz8Z2dpd57hkuNEt5COUrbOXSX_9MyGLDytH7ARY,118
|
|
27
|
+
src/retrieval/engine.py,sha256=tpQwcBejHHfrPJM1rB6RGZnng8Iop2g41Wuth4c4jXc,62208
|
|
28
|
+
src/retrieval/occurrence.py,sha256=tRfzrbaYCx2J8Xzl1i5Wm3Cg6i_tDl05WJtelo-dhoI,43948
|
|
29
|
+
src/retrieval/ocr.py,sha256=FjSu5vdXOEL6YiyznWp4VZ54zYOxo1z7_U3K0PPjlRs,3474
|
|
30
|
+
src/retrieval/periodicity.py,sha256=TSrOosXxkVRpybfZkalfb40bpB2O-zNGtKWjpsSarKY,6468
|
|
31
|
+
src/retrieval/repetition.py,sha256=ml7gbb4NnuGKtXax-TS6LnYvoxW0Pmnf1sMueJE1fvI,29698
|
|
32
|
+
src/retrieval/visual.py,sha256=x_H_nsewLBiSqg2EyxjxCLsVhz_Wbg3X1jtqt9Sw1Us,23128
|
|
33
|
+
src/transcript/__init__.py,sha256=wJamJxwr1x8q-TLOGjpVT59A6LkRT9DaCvvtN5hkOzI,240
|
|
34
|
+
src/transcript/asr.py,sha256=ndhUZsh9jZWOk5MEXQ4T9IA3xLng1EGsfSUOw2poz7U,61014
|
|
35
|
+
src/transcript/repeats.py,sha256=Ttd08KMq085nUj3E1xELaL4eWBpEPfh1C72JQDOsR0Q,1538
|
|
36
|
+
src/transcript/search.py,sha256=3-vxAhIVhgoGOFxfWwsytzXAh3dTrxtjeWpU4mfFLoo,4297
|
|
37
|
+
agentic_video-0.1.0.dist-info/METADATA,sha256=kEd6RYg1x8tGQ5Hfok9csD0IkOdToBBnOLhhhj_8RhQ,14023
|
|
38
|
+
agentic_video-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
39
|
+
agentic_video-0.1.0.dist-info/entry_points.txt,sha256=5pttM5Y0TvEs7SylnauflOISeP2epIoocNfHbdOOQB8,99
|
|
40
|
+
agentic_video-0.1.0.dist-info/licenses/LICENSE,sha256=7HK_UGM9zcw6lyZb4XXn33ixSEneHGxZeKRUy5AuG90,1062
|
|
41
|
+
agentic_video-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tlq5l
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical six-tool adapter. The JSON contract in mcp_schemas.json is the
|
|
3
|
+
* language-neutral source of truth; this Zod surface mirrors only the live
|
|
4
|
+
* Python MCP server and intentionally does not advertise legacy tools.
|
|
5
|
+
*/
|
|
6
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
|
|
9
|
+
export const ProbeVideoInputSchema = {
|
|
10
|
+
source: z.string().trim().min(1),
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const TranscribeInputSchema = {
|
|
14
|
+
source: z.string().trim().min(1),
|
|
15
|
+
model_size: z.enum(["tiny", "base", "small"]).optional(),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const SearchTranscriptInputSchema = {
|
|
19
|
+
source: z.string().trim().min(1),
|
|
20
|
+
query: z.string().trim().min(1),
|
|
21
|
+
top_k: z.number().int().min(1).max(50).optional(),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const ContactSheetInputSchema = {
|
|
25
|
+
source: z.string().trim().min(1),
|
|
26
|
+
samples: z.number().int().min(1).max(150).optional(),
|
|
27
|
+
cols: z.number().int().min(1).optional(),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const InspectWindowInputSchema = {
|
|
31
|
+
source: z.string().trim().min(1),
|
|
32
|
+
start_sec: z.number().min(0),
|
|
33
|
+
end_sec: z.number().min(0),
|
|
34
|
+
fps: z.number().min(0.01).max(30).optional(),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const DetectCutsInputSchema = {
|
|
38
|
+
source: z.string().trim().min(1),
|
|
39
|
+
threshold: z.number().min(0).max(1).optional(),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export interface AgenticVideoHandlers {
|
|
43
|
+
probe_video: (input: any) => Promise<any>;
|
|
44
|
+
transcribe: (input: any) => Promise<any>;
|
|
45
|
+
search_transcript: (input: any) => Promise<any>;
|
|
46
|
+
contact_sheet: (input: any) => Promise<any>;
|
|
47
|
+
inspect_window: (input: any) => Promise<any>;
|
|
48
|
+
detect_cuts: (input: any) => Promise<any>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function registerAgenticVideoTools(server: McpServer, handlers: AgenticVideoHandlers): void {
|
|
52
|
+
const tools: Array<[string, Record<string, unknown>, (input: any) => Promise<any>]> = [
|
|
53
|
+
["probe_video", { description: "Extract container metadata, duration, fps, resolution, codecs, and audio presence.", inputSchema: ProbeVideoInputSchema }, handlers.probe_video],
|
|
54
|
+
["transcribe", { description: "Transcribe speech audio into timestamped segments using faster-whisper or sidecar subtitles.", inputSchema: TranscribeInputSchema }, handlers.transcribe],
|
|
55
|
+
["search_transcript", { description: "Search timestamped transcript for query terms, returning matching segments and timecodes.", inputSchema: SearchTranscriptInputSchema }, handlers.search_transcript],
|
|
56
|
+
["contact_sheet", { description: "Generate a visual overview contact sheet with burned timestamp badges.", inputSchema: ContactSheetInputSchema }, handlers.contact_sheet],
|
|
57
|
+
["inspect_window", { description: "Extract dense frames in [start_sec, end_sec] with timestamp overlay.", inputSchema: InspectWindowInputSchema }, handlers.inspect_window],
|
|
58
|
+
["detect_cuts", { description: "Detect visual scene cuts and transitions using FFmpeg packet analysis.", inputSchema: DetectCutsInputSchema }, handlers.detect_cuts],
|
|
59
|
+
];
|
|
60
|
+
for (const [name, definition, handler] of tools) {
|
|
61
|
+
server.registerTool(name, definition as any, async (args) => {
|
|
62
|
+
const data = await handler(args);
|
|
63
|
+
return {
|
|
64
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
65
|
+
structuredContent: data,
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* The previous seven-tool implementation is retained below as a recoverable
|
|
72
|
+
* historical reference only; it is not part of the compiled/public surface.
|
|
73
|
+
*/
|
|
74
|
+
/*
|
|
75
|
+
* MCP Server Tool Registration and Definitions for Agentic Video.
|
|
76
|
+
*
|
|
77
|
+
* Implements the standard Model Context Protocol (MCP) tool contract using
|
|
78
|
+
* `@modelcontextprotocol/sdk` and `zod`.
|
|
79
|
+
*
|
|
80
|
+
* Can be run standalone via stdio (for Claude, Cursor, Grok, etc.) or
|
|
81
|
+
* connected directly as an Executor integration via `@executor-js/plugin-mcp`.
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
85
|
+
import { z } from "zod";
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Zod Schemas for Tool Inputs
|
|
89
|
+
*/
|
|
90
|
+
export const ProbeVideoInputSchema = {
|
|
91
|
+
source: z.string().trim().min(1).describe("Local filesystem path or accessible URI to video file."),
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const TranscribeInputSchema = {
|
|
95
|
+
source: z.string().trim().min(1).describe("Video or audio file path."),
|
|
96
|
+
options: z
|
|
97
|
+
.object({
|
|
98
|
+
model_size: z
|
|
99
|
+
.enum(["tiny", "base", "small", "medium", "large-v3"])
|
|
100
|
+
.default("base")
|
|
101
|
+
.describe("Whisper model size for transcription accuracy vs speed tradeoff."),
|
|
102
|
+
language: z
|
|
103
|
+
.string()
|
|
104
|
+
.optional()
|
|
105
|
+
.describe("ISO 639-1 language code (e.g. 'en', 'es') or 'auto' for auto-detection."),
|
|
106
|
+
vad_filter: z
|
|
107
|
+
.boolean()
|
|
108
|
+
.default(true)
|
|
109
|
+
.describe("Apply Voice Activity Detection filter to strip silence and noise."),
|
|
110
|
+
word_timestamps: z
|
|
111
|
+
.boolean()
|
|
112
|
+
.default(false)
|
|
113
|
+
.describe("Include start and end timestamps for each individual word."),
|
|
114
|
+
time_range: z
|
|
115
|
+
.object({
|
|
116
|
+
start: z.number().min(0).optional().describe("Start time offset in seconds."),
|
|
117
|
+
end: z.number().min(0).optional().describe("End time offset in seconds."),
|
|
118
|
+
})
|
|
119
|
+
.optional()
|
|
120
|
+
.describe("Optional temporal window restriction."),
|
|
121
|
+
})
|
|
122
|
+
.optional(),
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export const SearchTranscriptInputSchema = {
|
|
126
|
+
source: z.string().trim().min(1).describe("Video source path."),
|
|
127
|
+
query: z.string().trim().min(1).describe("Natural language query or keywords to search in transcript."),
|
|
128
|
+
top_k: z.number().int().min(1).max(50).default(5).describe("Maximum number of matched segments to return."),
|
|
129
|
+
time_range: z
|
|
130
|
+
.object({
|
|
131
|
+
start: z.number().min(0).optional(),
|
|
132
|
+
end: z.number().min(0).optional(),
|
|
133
|
+
})
|
|
134
|
+
.optional()
|
|
135
|
+
.describe("Optional bounding temporal window."),
|
|
136
|
+
search_mode: z
|
|
137
|
+
.enum(["hybrid", "semantic", "lexical"])
|
|
138
|
+
.default("hybrid")
|
|
139
|
+
.describe("Retrieval algorithm: 'hybrid' (vector + BM25), 'semantic' (embeddings), or 'lexical' (BM25/fuzzy)."),
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const SampleFramesInputSchema = {
|
|
143
|
+
source: z.string().trim().min(1).describe("Video source path."),
|
|
144
|
+
start: z.number().min(0).default(0).describe("Start timestamp in seconds."),
|
|
145
|
+
end: z.number().min(0).optional().describe("End timestamp in seconds (defaults to video duration)."),
|
|
146
|
+
fps: z.number().positive().max(30).optional().describe("Sample rate in frames per second (e.g. 0.5 = 1 frame every 2s)."),
|
|
147
|
+
count: z.number().int().positive().max(500).optional().describe("Exact count of evenly spaced frames across window (overrides fps)."),
|
|
148
|
+
width: z.number().int().positive().default(720).describe("Thumbnail width in pixels preserving aspect ratio."),
|
|
149
|
+
quality: z.number().int().min(1).max(100).default(85).describe("JPEG compression quality (1-100)."),
|
|
150
|
+
output_format: z
|
|
151
|
+
.enum(["path", "base64", "tool_file"])
|
|
152
|
+
.default("path")
|
|
153
|
+
.describe("Payload format: disk file paths ('path'), base64 strings ('base64'), or Executor ToolFile ('tool_file')."),
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export const InspectWindowInputSchema = {
|
|
157
|
+
source: z.string().trim().min(1).describe("Video source path."),
|
|
158
|
+
start: z.number().min(0).describe("Window start timestamp in seconds."),
|
|
159
|
+
end: z.number().min(0).describe("Window end timestamp in seconds (max recommended: 30s)."),
|
|
160
|
+
fps: z.number().positive().max(10).default(2.0).describe("Dense sampling frame rate (default: 2 fps)."),
|
|
161
|
+
width: z.number().int().positive().default(720).describe("Frame image width in pixels."),
|
|
162
|
+
include_motion: z.boolean().default(false).describe("Compute optical flow / motion delta score per frame."),
|
|
163
|
+
output_format: z.enum(["path", "base64", "tool_file"]).default("path"),
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export const ContactSheetInputSchema = {
|
|
167
|
+
source: z.string().trim().min(1).describe("Video source path."),
|
|
168
|
+
start: z.number().min(0).default(0).describe("Start timestamp in seconds."),
|
|
169
|
+
end: z.number().min(0).optional().describe("End timestamp in seconds."),
|
|
170
|
+
samples: z.number().int().min(4).max(64).default(16).describe("Number of thumbnail tiles in contact sheet grid."),
|
|
171
|
+
cols: z.number().int().min(2).max(8).default(4).describe("Number of grid columns."),
|
|
172
|
+
tile_width: z.number().int().positive().default(320).describe("Pixel width of each thumbnail tile."),
|
|
173
|
+
show_timestamps: z.boolean().default(true).describe("Burn timestamp badges onto thumbnail tiles."),
|
|
174
|
+
output_format: z.enum(["path", "base64", "tool_file"]).default("path"),
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export const GetClipInputSchema = {
|
|
178
|
+
source: z.string().trim().min(1).describe("Video source path."),
|
|
179
|
+
start: z.number().min(0).describe("Clip start timestamp in seconds."),
|
|
180
|
+
end: z.number().min(0).describe("Clip end timestamp in seconds."),
|
|
181
|
+
re_encode: z
|
|
182
|
+
.boolean()
|
|
183
|
+
.default(false)
|
|
184
|
+
.describe("If false, fast stream copy on keyframes; if true, re-encodes for frame-exact trimming."),
|
|
185
|
+
output_format: z.enum(["mp4", "webm"]).default("mp4").describe("Container format for extracted subclip."),
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Tool Handler Interface
|
|
190
|
+
*/
|
|
191
|
+
export interface AgenticVideoHandlers {
|
|
192
|
+
probe_video: (input: z.infer<z.ZodObject<typeof ProbeVideoInputSchema>>) => Promise<any>;
|
|
193
|
+
transcribe: (input: z.infer<z.ZodObject<typeof TranscribeInputSchema>>) => Promise<any>;
|
|
194
|
+
search_transcript: (input: z.infer<z.ZodObject<typeof SearchTranscriptInputSchema>>) => Promise<any>;
|
|
195
|
+
sample_frames: (input: z.infer<z.ZodObject<typeof SampleFramesInputSchema>>) => Promise<any>;
|
|
196
|
+
inspect_window: (input: z.infer<z.ZodObject<typeof InspectWindowInputSchema>>) => Promise<any>;
|
|
197
|
+
contact_sheet: (input: z.infer<z.ZodObject<typeof ContactSheetInputSchema>>) => Promise<any>;
|
|
198
|
+
get_clip: (input: z.infer<z.ZodObject<typeof GetClipInputSchema>>) => Promise<any>;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Register all agentic_video tools on an MCP Server instance.
|
|
203
|
+
*/
|
|
204
|
+
export function registerAgenticVideoTools(server: McpServer, handlers: AgenticVideoHandlers): void {
|
|
205
|
+
server.registerTool(
|
|
206
|
+
"probe_video",
|
|
207
|
+
{
|
|
208
|
+
description:
|
|
209
|
+
"Extract container and stream metadata including duration, resolution, frame rate, codecs, and audio channel presence.",
|
|
210
|
+
inputSchema: ProbeVideoInputSchema,
|
|
211
|
+
},
|
|
212
|
+
async (args) => {
|
|
213
|
+
const data = await handlers.probe_video(args);
|
|
214
|
+
return {
|
|
215
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
216
|
+
structuredContent: data,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
server.registerTool(
|
|
222
|
+
"transcribe",
|
|
223
|
+
{
|
|
224
|
+
description:
|
|
225
|
+
"Transcribe spoken audio from the video into time-aligned speech segments with start/end timestamps.",
|
|
226
|
+
inputSchema: TranscribeInputSchema,
|
|
227
|
+
},
|
|
228
|
+
async (args) => {
|
|
229
|
+
const data = await handlers.transcribe(args);
|
|
230
|
+
return {
|
|
231
|
+
content: [
|
|
232
|
+
{
|
|
233
|
+
type: "text",
|
|
234
|
+
text: `Transcribed ${data.segments.length} segments (${data.duration.toFixed(1)}s, lang: ${data.language}).`,
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
structuredContent: data,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
server.registerTool(
|
|
243
|
+
"search_transcript",
|
|
244
|
+
{
|
|
245
|
+
description:
|
|
246
|
+
"Search video speech transcript using lexical/semantic retrieval to pinpoint timestamps of interest.",
|
|
247
|
+
inputSchema: SearchTranscriptInputSchema,
|
|
248
|
+
},
|
|
249
|
+
async (args) => {
|
|
250
|
+
const data = await handlers.search_transcript(args);
|
|
251
|
+
return {
|
|
252
|
+
content: [
|
|
253
|
+
{
|
|
254
|
+
type: "text",
|
|
255
|
+
text: `Found ${data.matches.length} matching segment(s) for query: "${data.query}".`,
|
|
256
|
+
},
|
|
257
|
+
],
|
|
258
|
+
structuredContent: data,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
server.registerTool(
|
|
264
|
+
"sample_frames",
|
|
265
|
+
{
|
|
266
|
+
description:
|
|
267
|
+
"Sample video frames at uniform intervals or target count, returning file paths with exact timestamps and dimensions.",
|
|
268
|
+
inputSchema: SampleFramesInputSchema,
|
|
269
|
+
},
|
|
270
|
+
async (args) => {
|
|
271
|
+
const data = await handlers.sample_frames(args);
|
|
272
|
+
const content: any[] = [
|
|
273
|
+
{
|
|
274
|
+
type: "text",
|
|
275
|
+
text: `Sampled ${data.frames.length} frames from ${args.source}.`,
|
|
276
|
+
},
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
// If base64 is present or tool_file format, attach image content block
|
|
280
|
+
for (const frame of data.frames) {
|
|
281
|
+
if (frame.base64_data) {
|
|
282
|
+
content.push({
|
|
283
|
+
type: "image",
|
|
284
|
+
data: frame.base64_data,
|
|
285
|
+
mimeType: "image/jpeg",
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
content,
|
|
292
|
+
structuredContent: data,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
server.registerTool(
|
|
298
|
+
"inspect_window",
|
|
299
|
+
{
|
|
300
|
+
description:
|
|
301
|
+
"Densely sample frames within a narrow temporal window (e.g. 1-15s) for fine-grained action or visual inspection.",
|
|
302
|
+
inputSchema: InspectWindowInputSchema,
|
|
303
|
+
},
|
|
304
|
+
async (args) => {
|
|
305
|
+
const data = await handlers.inspect_window(args);
|
|
306
|
+
return {
|
|
307
|
+
content: [
|
|
308
|
+
{
|
|
309
|
+
type: "text",
|
|
310
|
+
text: `Inspected window [${data.window_start.toFixed(2)}s - ${data.window_end.toFixed(2)}s]: ${data.frames.length} frames extracted.`,
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
structuredContent: data,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
server.registerTool(
|
|
319
|
+
"contact_sheet",
|
|
320
|
+
{
|
|
321
|
+
description:
|
|
322
|
+
"Render a composite grid image (contact sheet) with burned-in timestamps for rapid visual timeline scanning.",
|
|
323
|
+
inputSchema: ContactSheetInputSchema,
|
|
324
|
+
},
|
|
325
|
+
async (args) => {
|
|
326
|
+
const data = await handlers.contact_sheet(args);
|
|
327
|
+
const content: any[] = [
|
|
328
|
+
{
|
|
329
|
+
type: "text",
|
|
330
|
+
text: `Rendered ${data.tile_count}-tile contact sheet (${data.cols}x${data.rows}) saved to: ${data.grid_image_path}`,
|
|
331
|
+
},
|
|
332
|
+
];
|
|
333
|
+
if (data.base64_data) {
|
|
334
|
+
content.push({
|
|
335
|
+
type: "image",
|
|
336
|
+
data: data.base64_data,
|
|
337
|
+
mimeType: "image/jpeg",
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
content,
|
|
342
|
+
structuredContent: data,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
server.registerTool(
|
|
348
|
+
"get_clip",
|
|
349
|
+
{
|
|
350
|
+
description: "Extract a temporal video subclip between start and end timestamps.",
|
|
351
|
+
inputSchema: GetClipInputSchema,
|
|
352
|
+
},
|
|
353
|
+
async (args) => {
|
|
354
|
+
const data = await handlers.get_clip(args);
|
|
355
|
+
return {
|
|
356
|
+
content: [
|
|
357
|
+
{
|
|
358
|
+
type: "text",
|
|
359
|
+
text: `Extracted clip (${data.duration.toFixed(2)}s) saved to: ${data.clip_path}`,
|
|
360
|
+
},
|
|
361
|
+
],
|
|
362
|
+
structuredContent: data,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
*/
|