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.
Files changed (87) hide show
  1. {videopython-0.49.0 → videopython-0.50.0}/PKG-INFO +34 -6
  2. {videopython-0.49.0 → videopython-0.50.0}/README.md +31 -5
  3. {videopython-0.49.0 → videopython-0.50.0}/pyproject.toml +7 -1
  4. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/video_analysis/models.py +1 -1
  5. videopython-0.50.0/src/videopython/mcp/__init__.py +6 -0
  6. videopython-0.50.0/src/videopython/mcp/server.py +221 -0
  7. {videopython-0.49.0 → videopython-0.50.0}/.gitignore +0 -0
  8. {videopython-0.49.0 → videopython-0.50.0}/LICENSE +0 -0
  9. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/__init__.py +0 -0
  10. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/__init__.py +0 -0
  11. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/_device.py +0 -0
  12. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/_ollama.py +0 -0
  13. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/_optional.py +0 -0
  14. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/_predictor.py +0 -0
  15. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/_revisions.py +0 -0
  16. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/auto_edit/__init__.py +0 -0
  17. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/auto_edit/backend.py +0 -0
  18. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/auto_edit/catalog.py +0 -0
  19. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/auto_edit/editor.py +0 -0
  20. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/auto_edit/local.py +0 -0
  21. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/auto_edit/models.py +0 -0
  22. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/auto_edit/resolve.py +0 -0
  23. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/dubbing/__init__.py +0 -0
  24. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/dubbing/config.py +0 -0
  25. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/dubbing/dubber.py +0 -0
  26. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/dubbing/expressiveness.py +0 -0
  27. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/dubbing/loudness.py +0 -0
  28. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/dubbing/models.py +0 -0
  29. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/dubbing/pipeline.py +0 -0
  30. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/dubbing/quality.py +0 -0
  31. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/dubbing/remux.py +0 -0
  32. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/dubbing/timing.py +0 -0
  33. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/dubbing/voice_sample.py +0 -0
  34. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/effects.py +0 -0
  35. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/generation/__init__.py +0 -0
  36. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/generation/_tts_backend.py +0 -0
  37. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/generation/audio.py +0 -0
  38. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/generation/image.py +0 -0
  39. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/generation/translation.py +0 -0
  40. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/generation/video.py +0 -0
  41. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/transforms.py +0 -0
  42. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/understanding/__init__.py +0 -0
  43. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/understanding/audio.py +0 -0
  44. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/understanding/faces.py +0 -0
  45. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/understanding/image.py +0 -0
  46. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/understanding/objects.py +0 -0
  47. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/understanding/separation.py +0 -0
  48. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/understanding/temporal.py +0 -0
  49. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/video_analysis/__init__.py +0 -0
  50. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/video_analysis/analyzer.py +0 -0
  51. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/video_analysis/sampling.py +0 -0
  52. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/ai/video_analysis/stages.py +0 -0
  53. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/audio/__init__.py +0 -0
  54. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/audio/analysis.py +0 -0
  55. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/audio/audio.py +0 -0
  56. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/__init__.py +0 -0
  57. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/_dimensions.py +0 -0
  58. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/_ffmpeg.py +0 -0
  59. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/_video_io.py +0 -0
  60. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/description.py +0 -0
  61. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/draw_detections.py +0 -0
  62. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/exceptions.py +0 -0
  63. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/fonts/Anton-OFL.txt +0 -0
  64. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/fonts/Anton-Regular.ttf +0 -0
  65. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/fonts/BebasNeue-OFL.txt +0 -0
  66. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/fonts/BebasNeue-Regular.ttf +0 -0
  67. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/fonts/DejaVuSans.ttf +0 -0
  68. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/fonts/LICENSE_DEJAVU +0 -0
  69. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/fonts/Lato-Bold.ttf +0 -0
  70. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/fonts/Lato-OFL.txt +0 -0
  71. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/fonts/Poppins-Bold.ttf +0 -0
  72. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/fonts/Poppins-OFL.txt +0 -0
  73. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/fonts/__init__.py +0 -0
  74. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/transcription.py +0 -0
  75. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/base/video.py +0 -0
  76. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/editing/__init__.py +0 -0
  77. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/editing/_ass.py +0 -0
  78. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/editing/_easing.py +0 -0
  79. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/editing/_schema.py +0 -0
  80. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/editing/audio_ops.py +0 -0
  81. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/editing/effects.py +0 -0
  82. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/editing/operation.py +0 -0
  83. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/editing/streaming.py +0 -0
  84. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/editing/transcription_overlay.py +0 -0
  85. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/editing/transforms.py +0 -0
  86. {videopython-0.49.0 → videopython-0.50.0}/src/videopython/editing/video_edit.py +0 -0
  87. {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.49.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 # core video/audio editing
60
- pip install "videopython[ai]" # + ALL local AI features (GPU recommended)
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
- 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.
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 # core video/audio editing
18
- pip install "videopython[ai]" # + ALL local AI features (GPU recommended)
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
- 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.
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.49.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
 
@@ -146,7 +146,7 @@ class VideoAnalysisConfig(BaseModel):
146
146
  VideoAnalysisConfig(
147
147
  analyzer_params={
148
148
  "audio_to_text": {"model_name": "large"},
149
- "scene_vlm": {"model": "llama3.2-vision"},
149
+ "scene_vlm": {"model": "gemma3:27b"},
150
150
  }
151
151
  )
152
152
  """
@@ -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