videopython 0.49.0__tar.gz → 0.50.0__tar.gz
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.
- {videopython-0.49.0 → videopython-0.50.0}/PKG-INFO +34 -6
- {videopython-0.49.0 → videopython-0.50.0}/README.md +31 -5
- {videopython-0.49.0 → videopython-0.50.0}/pyproject.toml +7 -1
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/video_analysis/models.py +1 -1
- videopython-0.50.0/src/videopython/mcp/__init__.py +6 -0
- videopython-0.50.0/src/videopython/mcp/server.py +221 -0
- {videopython-0.49.0 → videopython-0.50.0}/.gitignore +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/LICENSE +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/__init__.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/__init__.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/_device.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/_ollama.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/_optional.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/_predictor.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/_revisions.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/auto_edit/__init__.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/auto_edit/backend.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/auto_edit/catalog.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/auto_edit/editor.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/auto_edit/local.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/auto_edit/models.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/auto_edit/resolve.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/dubbing/__init__.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/dubbing/config.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/dubbing/dubber.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/dubbing/expressiveness.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/dubbing/loudness.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/dubbing/models.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/dubbing/pipeline.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/dubbing/quality.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/dubbing/remux.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/dubbing/timing.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/dubbing/voice_sample.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/effects.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/generation/__init__.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/generation/_tts_backend.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/generation/audio.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/generation/image.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/generation/translation.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/generation/video.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/transforms.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/understanding/__init__.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/understanding/audio.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/understanding/faces.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/understanding/image.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/understanding/objects.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/understanding/separation.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/understanding/temporal.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/video_analysis/__init__.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/video_analysis/analyzer.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/video_analysis/sampling.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/video_analysis/stages.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/audio/__init__.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/audio/analysis.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/audio/audio.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/__init__.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/_dimensions.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/_ffmpeg.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/_video_io.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/description.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/draw_detections.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/exceptions.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/fonts/Anton-OFL.txt +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/fonts/Anton-Regular.ttf +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/fonts/BebasNeue-OFL.txt +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/fonts/BebasNeue-Regular.ttf +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/fonts/DejaVuSans.ttf +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/fonts/LICENSE_DEJAVU +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/fonts/Lato-Bold.ttf +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/fonts/Lato-OFL.txt +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/fonts/Poppins-Bold.ttf +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/fonts/Poppins-OFL.txt +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/fonts/__init__.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/transcription.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/video.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/editing/__init__.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/editing/_ass.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/editing/_easing.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/editing/_schema.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/editing/audio_ops.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/editing/effects.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/editing/operation.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/editing/streaming.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/editing/transcription_overlay.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/editing/transforms.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/editing/video_edit.py +0 -0
- {videopython-0.49.0 → videopython-0.50.0}/src/videopython/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: videopython
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.50.0
|
|
4
4
|
Summary: Minimal video generation and processing library.
|
|
5
5
|
Project-URL: Homepage, https://videopython.com
|
|
6
6
|
Project-URL: Repository, https://github.com/bartwojtowicz/videopython/
|
|
@@ -38,6 +38,8 @@ Requires-Dist: torchaudio>=2.8.0; extra == 'ai'
|
|
|
38
38
|
Requires-Dist: transformers>=5.2.0; extra == 'ai'
|
|
39
39
|
Requires-Dist: transnetv2-pytorch>=1.0.5; extra == 'ai'
|
|
40
40
|
Requires-Dist: ultralytics>=8.0.0; extra == 'ai'
|
|
41
|
+
Provides-Extra: mcp
|
|
42
|
+
Requires-Dist: mcp<2,>=1.27; extra == 'mcp'
|
|
41
43
|
Description-Content-Type: text/markdown
|
|
42
44
|
|
|
43
45
|
# videopython
|
|
@@ -56,11 +58,12 @@ Full documentation: [videopython.com](https://videopython.com)
|
|
|
56
58
|
|
|
57
59
|
```bash
|
|
58
60
|
# Install FFmpeg first (macOS: brew install ffmpeg | Debian: apt-get install ffmpeg)
|
|
59
|
-
pip install videopython
|
|
60
|
-
pip install "videopython[ai]"
|
|
61
|
+
pip install videopython # core video/audio editing
|
|
62
|
+
pip install "videopython[ai]" # + ALL local AI features (GPU recommended)
|
|
63
|
+
pip install "videopython[ai,mcp]" # + MCP server for agent-driven editing
|
|
61
64
|
```
|
|
62
65
|
|
|
63
|
-
Python `>=3.11, <3.14`. AI features run locally — no cloud API keys required, but model weights are downloaded on first use.
|
|
66
|
+
Python `>=3.11, <3.14`. AI features run locally — no cloud API keys required, but model weights are downloaded on first use. LLM-driven editing and scene captioning use a local [Ollama](https://ollama.com) server (`ollama pull gemma3:27b`).
|
|
64
67
|
|
|
65
68
|
## Quick Start
|
|
66
69
|
|
|
@@ -89,6 +92,23 @@ edit.run_to_file("output.mp4") # streams ffmpeg decode → effects → encode
|
|
|
89
92
|
|
|
90
93
|
`run_to_file()` streams ffmpeg decode → per-frame effects → encode, so memory stays bounded even for hour-long sources. If you need the frames back in memory, load the rendered file: `Video.from_path(str(edit.run_to_file("output.mp4")))`.
|
|
91
94
|
|
|
95
|
+
### Automatic editing (local LLM)
|
|
96
|
+
|
|
97
|
+
Give `AutoEditor` your clips and a brief; a local Ollama vision model selects and orders the shots, and you get back a runnable `VideoEdit`:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from videopython.ai import AutoEditor, OllamaVisionLLM
|
|
101
|
+
|
|
102
|
+
editor = AutoEditor(planner=OllamaVisionLLM(model="gemma3:27b")) # ollama pull gemma3:27b
|
|
103
|
+
edit = editor.edit(
|
|
104
|
+
["clip_a.mp4", "clip_b.mp4", "clip_c.mp4"],
|
|
105
|
+
brief="A punchy 15-second teaser; lead with the most dynamic shot.",
|
|
106
|
+
)
|
|
107
|
+
edit.run_to_file("teaser.mp4")
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The model picks scenes **by id** from a catalog built from scene detection + captions, so its temporal imprecision never reaches the render. See the [Automatic Editing Guide](https://videopython.com/guides/auto-editing/).
|
|
111
|
+
|
|
92
112
|
### AI generation
|
|
93
113
|
|
|
94
114
|
```python
|
|
@@ -102,7 +122,13 @@ video.add_audio(audio).save("ai_video.mp4")
|
|
|
102
122
|
|
|
103
123
|
## LLM & AI Agent Integration
|
|
104
124
|
|
|
105
|
-
|
|
125
|
+
Putting an LLM in the loop works three ways:
|
|
126
|
+
|
|
127
|
+
1. **Bring your own LLM** — videopython gives your model the JSON Schema and a structured refine loop; your model authors the plans (details below).
|
|
128
|
+
2. **`AutoEditor`** — a local Ollama vision model is the planner (see [Automatic editing](#automatic-editing-local-llm) above).
|
|
129
|
+
3. **MCP server** — `videopython-mcp` exposes the pipeline as [Model Context Protocol](https://modelcontextprotocol.io) tools, so an agent like Claude drives editing with its own model. Install `[ai,mcp]`, run `videopython-mcp`, and point your MCP client at it. See the [MCP Server Guide](https://videopython.com/guides/mcp/).
|
|
130
|
+
|
|
131
|
+
For mode 1: every operation is a Pydantic model whose fields ARE the JSON wire format. `VideoEdit.json_schema()` returns a JSON Schema with a discriminated union over every LLM-exposed `Operation` (server-only ops like `image_overlay` are excluded by default) — pass it straight to Anthropic tool use, OpenAI function calling, or any structured-output API. Pass `strict=True` for a provider strict-mode grammar that prevents simple bound violations at decode time.
|
|
106
132
|
|
|
107
133
|
The plan parses permissively (shape only) and owns numeric bounds at validation, so a refine loop converges fast: `edit.check(meta)` collects **every** structured `PlanError` in one pass, `edit.repair(meta)` auto-clamps the mechanical violations (window/timestamp overruns, negatives) with a reported changelog, and `edit.normalize_dimensions(meta, target)` makes heterogeneous segments concat-compatible by construction. `edit.validate()` still raises a typed `PlanValidationError` (a `ValueError` with structured `.errors`) for the single-error path.
|
|
108
134
|
|
|
@@ -113,8 +139,10 @@ See the [LLM Integration Guide](https://videopython.com/guides/llm-integration/)
|
|
|
113
139
|
- **`videopython.base`** — `Video`, `VideoMetadata`, `FrameIterator`, `Transcription`, and shared result types (`BoundingBox`, `FaceTrack`, `SceneBoundary`, ...). No AI dependencies.
|
|
114
140
|
- **`videopython.audio`** — `Audio` with overlay, concat, normalize, time-stretch, silence detection, segment classification.
|
|
115
141
|
- **`videopython.editing`** — `Operation`/`Effect` foundation, `VideoEdit` plan runner with JSON Schema + streaming execution. Transforms (resize, crop, fps, speed, freeze, silence removal; cutting is the segment's own start/end) and effects (blur, zoom, color grading, vignette, Ken Burns, fade, overlays, animated subtitles).
|
|
116
|
-
- **`videopython.ai`** *(install with `[ai]`)* — generation (`TextToVideo`, `ImageToVideo`, `TextToImage`, `TextToSpeech`, `TextToMusic`), understanding (`AudioToText`, `AudioClassifier`, `SceneVLM`, `FaceTracker`, `ObjectDetector`, `SemanticSceneDetector`), the `FaceTrackingCrop` transform, the `ObjectDetectionOverlay` effect (per-frame bounding boxes + labels), and the full-pipeline `VideoAnalyzer`.
|
|
142
|
+
- **`videopython.ai`** *(install with `[ai]`)* — generation (`TextToVideo`, `ImageToVideo`, `TextToImage`, `TextToSpeech`, `TextToMusic`), understanding (`AudioToText`, `AudioClassifier`, `SceneVLM`, `FaceTracker`, `ObjectDetector`, `SemanticSceneDetector`), the `FaceTrackingCrop` transform, the `ObjectDetectionOverlay` effect (per-frame bounding boxes + labels), and the full-pipeline `VideoAnalyzer`. Scene captioning and dub translation run on a local [Ollama](https://ollama.com) model.
|
|
143
|
+
- **`videopython.ai.auto_edit`** — `AutoEditor` + `OllamaVisionLLM`: plan and render an edit from sources + a one-line brief, with a local LLM selecting scenes by id from an auto-built catalog.
|
|
117
144
|
- **`videopython.ai.dubbing`** — `VideoDubber` for voice-cloned revoicing with timing sync.
|
|
145
|
+
- **`videopython.mcp`** *(install with `[mcp]`)* — `videopython-mcp`, an MCP stdio server exposing the auto-edit pipeline (analyze → catalog → validate/repair/run) so an agent drives editing.
|
|
118
146
|
|
|
119
147
|
## Examples
|
|
120
148
|
|
|
@@ -14,11 +14,12 @@ Full documentation: [videopython.com](https://videopython.com)
|
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
# Install FFmpeg first (macOS: brew install ffmpeg | Debian: apt-get install ffmpeg)
|
|
17
|
-
pip install videopython
|
|
18
|
-
pip install "videopython[ai]"
|
|
17
|
+
pip install videopython # core video/audio editing
|
|
18
|
+
pip install "videopython[ai]" # + ALL local AI features (GPU recommended)
|
|
19
|
+
pip install "videopython[ai,mcp]" # + MCP server for agent-driven editing
|
|
19
20
|
```
|
|
20
21
|
|
|
21
|
-
Python `>=3.11, <3.14`. AI features run locally — no cloud API keys required, but model weights are downloaded on first use.
|
|
22
|
+
Python `>=3.11, <3.14`. AI features run locally — no cloud API keys required, but model weights are downloaded on first use. LLM-driven editing and scene captioning use a local [Ollama](https://ollama.com) server (`ollama pull gemma3:27b`).
|
|
22
23
|
|
|
23
24
|
## Quick Start
|
|
24
25
|
|
|
@@ -47,6 +48,23 @@ edit.run_to_file("output.mp4") # streams ffmpeg decode → effects → encode
|
|
|
47
48
|
|
|
48
49
|
`run_to_file()` streams ffmpeg decode → per-frame effects → encode, so memory stays bounded even for hour-long sources. If you need the frames back in memory, load the rendered file: `Video.from_path(str(edit.run_to_file("output.mp4")))`.
|
|
49
50
|
|
|
51
|
+
### Automatic editing (local LLM)
|
|
52
|
+
|
|
53
|
+
Give `AutoEditor` your clips and a brief; a local Ollama vision model selects and orders the shots, and you get back a runnable `VideoEdit`:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from videopython.ai import AutoEditor, OllamaVisionLLM
|
|
57
|
+
|
|
58
|
+
editor = AutoEditor(planner=OllamaVisionLLM(model="gemma3:27b")) # ollama pull gemma3:27b
|
|
59
|
+
edit = editor.edit(
|
|
60
|
+
["clip_a.mp4", "clip_b.mp4", "clip_c.mp4"],
|
|
61
|
+
brief="A punchy 15-second teaser; lead with the most dynamic shot.",
|
|
62
|
+
)
|
|
63
|
+
edit.run_to_file("teaser.mp4")
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The model picks scenes **by id** from a catalog built from scene detection + captions, so its temporal imprecision never reaches the render. See the [Automatic Editing Guide](https://videopython.com/guides/auto-editing/).
|
|
67
|
+
|
|
50
68
|
### AI generation
|
|
51
69
|
|
|
52
70
|
```python
|
|
@@ -60,7 +78,13 @@ video.add_audio(audio).save("ai_video.mp4")
|
|
|
60
78
|
|
|
61
79
|
## LLM & AI Agent Integration
|
|
62
80
|
|
|
63
|
-
|
|
81
|
+
Putting an LLM in the loop works three ways:
|
|
82
|
+
|
|
83
|
+
1. **Bring your own LLM** — videopython gives your model the JSON Schema and a structured refine loop; your model authors the plans (details below).
|
|
84
|
+
2. **`AutoEditor`** — a local Ollama vision model is the planner (see [Automatic editing](#automatic-editing-local-llm) above).
|
|
85
|
+
3. **MCP server** — `videopython-mcp` exposes the pipeline as [Model Context Protocol](https://modelcontextprotocol.io) tools, so an agent like Claude drives editing with its own model. Install `[ai,mcp]`, run `videopython-mcp`, and point your MCP client at it. See the [MCP Server Guide](https://videopython.com/guides/mcp/).
|
|
86
|
+
|
|
87
|
+
For mode 1: every operation is a Pydantic model whose fields ARE the JSON wire format. `VideoEdit.json_schema()` returns a JSON Schema with a discriminated union over every LLM-exposed `Operation` (server-only ops like `image_overlay` are excluded by default) — pass it straight to Anthropic tool use, OpenAI function calling, or any structured-output API. Pass `strict=True` for a provider strict-mode grammar that prevents simple bound violations at decode time.
|
|
64
88
|
|
|
65
89
|
The plan parses permissively (shape only) and owns numeric bounds at validation, so a refine loop converges fast: `edit.check(meta)` collects **every** structured `PlanError` in one pass, `edit.repair(meta)` auto-clamps the mechanical violations (window/timestamp overruns, negatives) with a reported changelog, and `edit.normalize_dimensions(meta, target)` makes heterogeneous segments concat-compatible by construction. `edit.validate()` still raises a typed `PlanValidationError` (a `ValueError` with structured `.errors`) for the single-error path.
|
|
66
90
|
|
|
@@ -71,8 +95,10 @@ See the [LLM Integration Guide](https://videopython.com/guides/llm-integration/)
|
|
|
71
95
|
- **`videopython.base`** — `Video`, `VideoMetadata`, `FrameIterator`, `Transcription`, and shared result types (`BoundingBox`, `FaceTrack`, `SceneBoundary`, ...). No AI dependencies.
|
|
72
96
|
- **`videopython.audio`** — `Audio` with overlay, concat, normalize, time-stretch, silence detection, segment classification.
|
|
73
97
|
- **`videopython.editing`** — `Operation`/`Effect` foundation, `VideoEdit` plan runner with JSON Schema + streaming execution. Transforms (resize, crop, fps, speed, freeze, silence removal; cutting is the segment's own start/end) and effects (blur, zoom, color grading, vignette, Ken Burns, fade, overlays, animated subtitles).
|
|
74
|
-
- **`videopython.ai`** *(install with `[ai]`)* — generation (`TextToVideo`, `ImageToVideo`, `TextToImage`, `TextToSpeech`, `TextToMusic`), understanding (`AudioToText`, `AudioClassifier`, `SceneVLM`, `FaceTracker`, `ObjectDetector`, `SemanticSceneDetector`), the `FaceTrackingCrop` transform, the `ObjectDetectionOverlay` effect (per-frame bounding boxes + labels), and the full-pipeline `VideoAnalyzer`.
|
|
98
|
+
- **`videopython.ai`** *(install with `[ai]`)* — generation (`TextToVideo`, `ImageToVideo`, `TextToImage`, `TextToSpeech`, `TextToMusic`), understanding (`AudioToText`, `AudioClassifier`, `SceneVLM`, `FaceTracker`, `ObjectDetector`, `SemanticSceneDetector`), the `FaceTrackingCrop` transform, the `ObjectDetectionOverlay` effect (per-frame bounding boxes + labels), and the full-pipeline `VideoAnalyzer`. Scene captioning and dub translation run on a local [Ollama](https://ollama.com) model.
|
|
99
|
+
- **`videopython.ai.auto_edit`** — `AutoEditor` + `OllamaVisionLLM`: plan and render an edit from sources + a one-line brief, with a local LLM selecting scenes by id from an auto-built catalog.
|
|
75
100
|
- **`videopython.ai.dubbing`** — `VideoDubber` for voice-cloned revoicing with timing sync.
|
|
101
|
+
- **`videopython.mcp`** *(install with `[mcp]`)* — `videopython-mcp`, an MCP stdio server exposing the auto-edit pipeline (analyze → catalog → validate/repair/run) so an agent drives editing.
|
|
76
102
|
|
|
77
103
|
## Examples
|
|
78
104
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "videopython"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.50.0"
|
|
4
4
|
description = "Minimal video generation and processing library."
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Bartosz Wójtowicz", email = "bartoszwojtowicz@outlook.com" },
|
|
@@ -89,6 +89,11 @@ ai = [
|
|
|
89
89
|
"torch>=2.8.0",
|
|
90
90
|
"torchaudio>=2.8.0",
|
|
91
91
|
]
|
|
92
|
+
# MCP server (videopython/mcp/). Pin <2 — v2 is pre-release/breaking.
|
|
93
|
+
mcp = ["mcp>=1.27,<2"]
|
|
94
|
+
|
|
95
|
+
[project.scripts]
|
|
96
|
+
videopython-mcp = "videopython.mcp.server:main"
|
|
92
97
|
|
|
93
98
|
[project.urls]
|
|
94
99
|
Homepage = "https://videopython.com"
|
|
@@ -118,6 +123,7 @@ module = [
|
|
|
118
123
|
"cv2", "cv2.*",
|
|
119
124
|
"pyloudnorm", "pyloudnorm.*",
|
|
120
125
|
"imagehash", "imagehash.*",
|
|
126
|
+
"mcp", "mcp.*",
|
|
121
127
|
]
|
|
122
128
|
ignore_missing_imports = true
|
|
123
129
|
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""videopython MCP server: the auto-edit primitives exposed as MCP tools.
|
|
2
|
+
|
|
3
|
+
Run with the ``videopython-mcp`` console script (needs the ``[ai,mcp]`` extras).
|
|
4
|
+
The MCP client's own model is the planner; this server only exposes the steps
|
|
5
|
+
(analyze -> catalog -> validate/repair/run over an EditPlan by scene id).
|
|
6
|
+
"""
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""videopython MCP server (stdio): auto-edit primitives as MCP tools.
|
|
2
|
+
|
|
3
|
+
The MCP client's model is the planner. These tools expose the steps it drives:
|
|
4
|
+
analyze each source, build a keyframe catalog, then author an ``EditPlan`` that
|
|
5
|
+
references catalog scenes by ``scene_id`` and validate / repair / run it. The
|
|
6
|
+
server caches analyses + the catalog so the agent passes small payloads (scene
|
|
7
|
+
ids), not whole analysis blobs.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
from contextlib import redirect_stdout
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
17
|
+
|
|
18
|
+
from mcp.server.fastmcp import FastMCP
|
|
19
|
+
from mcp.types import ImageContent, TextContent
|
|
20
|
+
from pydantic import ValidationError
|
|
21
|
+
|
|
22
|
+
from videopython.ai._ollama import _encode_png_b64
|
|
23
|
+
from videopython.ai.auto_edit import EditPlan, UnknownSceneIdsError, resolve_plan
|
|
24
|
+
from videopython.ai.auto_edit import build_catalog as _build_scene_catalog
|
|
25
|
+
from videopython.editing import VideoEdit
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from videopython.ai.auto_edit import CatalogBundle
|
|
29
|
+
from videopython.ai.video_analysis import VideoAnalysis, VideoAnalyzer
|
|
30
|
+
from videopython.base import PlanError, PlanRepair
|
|
31
|
+
|
|
32
|
+
mcp = FastMCP("videopython")
|
|
33
|
+
|
|
34
|
+
# Session state for one stdio client (one process per client).
|
|
35
|
+
_analyses: dict[str, VideoAnalysis] = {}
|
|
36
|
+
_analyzer: VideoAnalyzer | None = None
|
|
37
|
+
_bundle: CatalogBundle | None = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@mcp.tool()
|
|
41
|
+
def analyze_video(path: str) -> dict[str, Any]:
|
|
42
|
+
"""Analyze a source video (scenes, transcript, captions) and cache it for build_catalog.
|
|
43
|
+
|
|
44
|
+
Returns a short summary; call this once per source, then build_catalog.
|
|
45
|
+
"""
|
|
46
|
+
# Heavy analyzer deps (e.g. transnetv2-pytorch) bare-print to stdout, which here is
|
|
47
|
+
# the stdio JSON-RPC channel; send that to stderr so the transport stays clean.
|
|
48
|
+
with redirect_stdout(sys.stderr):
|
|
49
|
+
analysis = _get_analyzer().analyze_path(path)
|
|
50
|
+
_analyses[str(Path(path))] = analysis
|
|
51
|
+
src = analysis.source
|
|
52
|
+
return {
|
|
53
|
+
"source": str(Path(path)),
|
|
54
|
+
"duration": src.duration,
|
|
55
|
+
"fps": src.fps,
|
|
56
|
+
"width": src.width,
|
|
57
|
+
"height": src.height,
|
|
58
|
+
"scenes": len(analysis.scenes.samples) if analysis.scenes else 0,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@mcp.tool(structured_output=False)
|
|
63
|
+
def build_catalog(sources: list[str] | None = None) -> list[TextContent | ImageContent]:
|
|
64
|
+
"""Build the candidate-scene catalog from analyzed videos and cache it.
|
|
65
|
+
|
|
66
|
+
Returns the catalog as JSON text plus one keyframe image per scene (each
|
|
67
|
+
preceded by its scene id), so the model can see the footage. Pass ``sources``
|
|
68
|
+
to restrict to specific analyzed paths, or omit for all. Author the edit by
|
|
69
|
+
referencing the returned ``id`` values via the edit-plan schema resource.
|
|
70
|
+
"""
|
|
71
|
+
global _bundle
|
|
72
|
+
analyses = _selected_analyses(sources)
|
|
73
|
+
if not analyses:
|
|
74
|
+
raise ValueError("No analyzed videos cached; call analyze_video first.")
|
|
75
|
+
_bundle = _build_scene_catalog(analyses)
|
|
76
|
+
|
|
77
|
+
blocks: list[TextContent | ImageContent] = [TextContent(type="text", text=_bundle.catalog.model_dump_json())]
|
|
78
|
+
for scene in _bundle.catalog.scenes:
|
|
79
|
+
frame = _bundle.keyframes.get(scene.id)
|
|
80
|
+
if frame is not None:
|
|
81
|
+
blocks.append(TextContent(type="text", text=f"scene {scene.id}:"))
|
|
82
|
+
blocks.append(ImageContent(type="image", data=_encode_png_b64(frame), mimeType="image/png"))
|
|
83
|
+
return blocks
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@mcp.tool()
|
|
87
|
+
def validate_edit(plan: dict[str, Any]) -> dict[str, Any]:
|
|
88
|
+
"""Validate an edit plan (an EditPlan referencing catalog scene ids).
|
|
89
|
+
|
|
90
|
+
Returns every problem at once as structured errors; ``valid`` is True when
|
|
91
|
+
the list is empty. A schema-invalid plan or an unknown scene id is reported
|
|
92
|
+
the same way (error ``code`` ``schema_invalid`` / ``unknown_scene_ids``).
|
|
93
|
+
"""
|
|
94
|
+
edit, errors = _resolve(plan)
|
|
95
|
+
if edit is None:
|
|
96
|
+
return {"valid": False, "errors": errors}
|
|
97
|
+
errors = [_error_dict(e) for e in edit.check(_source_metadata(edit), context=_context())]
|
|
98
|
+
return {"valid": not errors, "errors": errors}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@mcp.tool()
|
|
102
|
+
def repair_edit(plan: dict[str, Any]) -> dict[str, Any]:
|
|
103
|
+
"""Repair the mechanical issues in an edit plan and normalize segment dimensions.
|
|
104
|
+
|
|
105
|
+
Returns the resolved+repaired VideoEdit and a changelog, for inspection;
|
|
106
|
+
``edit`` is None with structured ``errors`` if the plan does not resolve. The
|
|
107
|
+
returned ``edit`` is a concrete VideoEdit, not an EditPlan -- keep refining by
|
|
108
|
+
adjusting the original by-id plan, not by resubmitting this output. run_edit
|
|
109
|
+
applies the same repair before rendering.
|
|
110
|
+
"""
|
|
111
|
+
edit, errors = _resolve(plan)
|
|
112
|
+
if edit is None:
|
|
113
|
+
return {"edit": None, "repairs": [], "errors": errors}
|
|
114
|
+
metadata = _source_metadata(edit)
|
|
115
|
+
context = _context()
|
|
116
|
+
edit, repairs = edit.repair(metadata, context=context, clamp_segment_end=True)
|
|
117
|
+
edit, dim_repairs = edit.normalize_dimensions(metadata, "largest", context=context)
|
|
118
|
+
return {"edit": edit.to_dict(), "repairs": [_repair_dict(r) for r in (*repairs, *dim_repairs)], "errors": []}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@mcp.tool()
|
|
122
|
+
def run_edit(plan: dict[str, Any], output_path: str) -> dict[str, Any]:
|
|
123
|
+
"""Render an edit plan to an MP4 file (the path suffix is normalized to .mp4).
|
|
124
|
+
|
|
125
|
+
Resolves scene ids, repairs + normalizes, validates, then renders. If the
|
|
126
|
+
plan does not resolve or validation still fails, returns structured ``errors``
|
|
127
|
+
and ``output_path`` None instead of rendering.
|
|
128
|
+
"""
|
|
129
|
+
edit, errors = _resolve(plan)
|
|
130
|
+
if edit is None:
|
|
131
|
+
return {"output_path": None, "errors": errors}
|
|
132
|
+
metadata = _source_metadata(edit)
|
|
133
|
+
context = _context()
|
|
134
|
+
edit, _ = edit.repair(metadata, context=context, clamp_segment_end=True)
|
|
135
|
+
edit, _ = edit.normalize_dimensions(metadata, "largest", context=context)
|
|
136
|
+
errors = [_error_dict(e) for e in edit.check(metadata, context=context)]
|
|
137
|
+
if errors:
|
|
138
|
+
return {"output_path": None, "errors": errors}
|
|
139
|
+
out = edit.run_to_file(output_path, context=context)
|
|
140
|
+
return {"output_path": str(out), "errors": []}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@mcp.resource("schema://videopython/edit-plan", mime_type="application/json")
|
|
144
|
+
def edit_plan_schema() -> str:
|
|
145
|
+
"""JSON Schema for the edit plan an agent authors (references catalog scene ids)."""
|
|
146
|
+
return json.dumps(EditPlan.json_schema(strict=True))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _get_analyzer() -> VideoAnalyzer:
|
|
150
|
+
global _analyzer
|
|
151
|
+
if _analyzer is None:
|
|
152
|
+
from videopython.ai.video_analysis import VideoAnalyzer
|
|
153
|
+
|
|
154
|
+
_analyzer = VideoAnalyzer()
|
|
155
|
+
return _analyzer
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _selected_analyses(sources: list[str] | None) -> list[VideoAnalysis]:
|
|
159
|
+
if sources is None:
|
|
160
|
+
return list(_analyses.values())
|
|
161
|
+
return [_analyses[str(Path(s))] for s in sources if str(Path(s)) in _analyses]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _resolve(plan: dict[str, Any]) -> tuple[VideoEdit | None, list[dict[str, Any]]]:
|
|
165
|
+
"""Resolve a by-id plan, or return (None, structured errors) for the agent's plan mistakes."""
|
|
166
|
+
if _bundle is None:
|
|
167
|
+
raise ValueError("No catalog cached; call build_catalog first.")
|
|
168
|
+
try:
|
|
169
|
+
edit = resolve_plan(EditPlan.model_validate(plan), _bundle.catalog)
|
|
170
|
+
except ValidationError as exc:
|
|
171
|
+
detail = [{"loc": list(e["loc"]), "msg": e["msg"], "type": e["type"]} for e in exc.errors()]
|
|
172
|
+
return None, [{"code": "schema_invalid", "detail": detail, "message": str(exc)}]
|
|
173
|
+
except UnknownSceneIdsError as exc:
|
|
174
|
+
return None, [{"code": "unknown_scene_ids", "value": sorted(set(exc.ids)), "message": str(exc)}]
|
|
175
|
+
return edit, []
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _source_metadata(edit: VideoEdit) -> dict[str, Any]:
|
|
179
|
+
from videopython.base.video import VideoMetadata
|
|
180
|
+
|
|
181
|
+
return {str(seg.source): VideoMetadata.from_path(seg.source) for seg in edit.segments}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _context() -> dict[str, Any] | None:
|
|
185
|
+
transcriptions: dict[str, Any] = {}
|
|
186
|
+
for analysis in _analyses.values():
|
|
187
|
+
src = analysis.source
|
|
188
|
+
if src.path is not None and analysis.audio is not None and analysis.audio.transcription is not None:
|
|
189
|
+
transcriptions[str(Path(src.path))] = analysis.audio.transcription
|
|
190
|
+
return {"transcription": transcriptions} if transcriptions else None
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _error_dict(error: PlanError) -> dict[str, Any]:
|
|
194
|
+
return {
|
|
195
|
+
"code": error.code.value,
|
|
196
|
+
"location": error.location,
|
|
197
|
+
"op": error.op,
|
|
198
|
+
"field": error.field,
|
|
199
|
+
"value": error.value,
|
|
200
|
+
"limit": error.limit,
|
|
201
|
+
"detail": error.detail,
|
|
202
|
+
"message": error.to_prompt_line(),
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _repair_dict(repair: PlanRepair) -> dict[str, Any]:
|
|
207
|
+
return {
|
|
208
|
+
"location": repair.location,
|
|
209
|
+
"field": repair.field,
|
|
210
|
+
"old": repair.old,
|
|
211
|
+
"new": repair.new,
|
|
212
|
+
"code": repair.code.value,
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def main() -> None:
|
|
217
|
+
mcp.run(transport="stdio")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
if __name__ == "__main__":
|
|
221
|
+
main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|