videopython 0.48.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 (91) hide show
  1. {videopython-0.48.0 → videopython-0.50.0}/PKG-INFO +34 -9
  2. {videopython-0.48.0 → videopython-0.50.0}/README.md +31 -5
  3. {videopython-0.48.0 → videopython-0.50.0}/pyproject.toml +7 -7
  4. videopython-0.50.0/src/videopython/ai/_ollama.py +71 -0
  5. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/_revisions.py +0 -11
  6. videopython-0.50.0/src/videopython/ai/auto_edit/local.py +39 -0
  7. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/dubbing/__init__.py +5 -6
  8. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/dubbing/config.py +7 -8
  9. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/dubbing/dubber.py +2 -2
  10. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/dubbing/models.py +3 -5
  11. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/dubbing/pipeline.py +6 -64
  12. videopython-0.50.0/src/videopython/ai/generation/translation.py +375 -0
  13. videopython-0.50.0/src/videopython/ai/understanding/image.py +79 -0
  14. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/video_analysis/analyzer.py +1 -1
  15. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/video_analysis/models.py +1 -1
  16. videopython-0.50.0/src/videopython/mcp/__init__.py +6 -0
  17. videopython-0.50.0/src/videopython/mcp/server.py +221 -0
  18. videopython-0.48.0/src/videopython/ai/auto_edit/local.py +0 -67
  19. videopython-0.48.0/src/videopython/ai/generation/qwen3.py +0 -504
  20. videopython-0.48.0/src/videopython/ai/generation/translation.py +0 -347
  21. videopython-0.48.0/src/videopython/ai/understanding/image.py +0 -400
  22. {videopython-0.48.0 → videopython-0.50.0}/.gitignore +0 -0
  23. {videopython-0.48.0 → videopython-0.50.0}/LICENSE +0 -0
  24. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/__init__.py +0 -0
  25. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/__init__.py +0 -0
  26. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/_device.py +0 -0
  27. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/_optional.py +0 -0
  28. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/_predictor.py +0 -0
  29. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/auto_edit/__init__.py +0 -0
  30. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/auto_edit/backend.py +0 -0
  31. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/auto_edit/catalog.py +0 -0
  32. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/auto_edit/editor.py +0 -0
  33. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/auto_edit/models.py +0 -0
  34. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/auto_edit/resolve.py +0 -0
  35. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/dubbing/expressiveness.py +0 -0
  36. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/dubbing/loudness.py +0 -0
  37. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/dubbing/quality.py +0 -0
  38. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/dubbing/remux.py +0 -0
  39. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/dubbing/timing.py +0 -0
  40. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/dubbing/voice_sample.py +0 -0
  41. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/effects.py +0 -0
  42. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/generation/__init__.py +0 -0
  43. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/generation/_tts_backend.py +0 -0
  44. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/generation/audio.py +0 -0
  45. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/generation/image.py +0 -0
  46. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/generation/video.py +0 -0
  47. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/transforms.py +0 -0
  48. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/understanding/__init__.py +0 -0
  49. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/understanding/audio.py +0 -0
  50. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/understanding/faces.py +0 -0
  51. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/understanding/objects.py +0 -0
  52. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/understanding/separation.py +0 -0
  53. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/understanding/temporal.py +0 -0
  54. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/video_analysis/__init__.py +0 -0
  55. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/video_analysis/sampling.py +0 -0
  56. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/ai/video_analysis/stages.py +0 -0
  57. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/audio/__init__.py +0 -0
  58. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/audio/analysis.py +0 -0
  59. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/audio/audio.py +0 -0
  60. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/base/__init__.py +0 -0
  61. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/base/_dimensions.py +0 -0
  62. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/base/_ffmpeg.py +0 -0
  63. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/base/_video_io.py +0 -0
  64. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/base/description.py +0 -0
  65. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/base/draw_detections.py +0 -0
  66. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/base/exceptions.py +0 -0
  67. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/base/fonts/Anton-OFL.txt +0 -0
  68. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/base/fonts/Anton-Regular.ttf +0 -0
  69. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/base/fonts/BebasNeue-OFL.txt +0 -0
  70. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/base/fonts/BebasNeue-Regular.ttf +0 -0
  71. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/base/fonts/DejaVuSans.ttf +0 -0
  72. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/base/fonts/LICENSE_DEJAVU +0 -0
  73. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/base/fonts/Lato-Bold.ttf +0 -0
  74. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/base/fonts/Lato-OFL.txt +0 -0
  75. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/base/fonts/Poppins-Bold.ttf +0 -0
  76. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/base/fonts/Poppins-OFL.txt +0 -0
  77. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/base/fonts/__init__.py +0 -0
  78. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/base/transcription.py +0 -0
  79. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/base/video.py +0 -0
  80. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/editing/__init__.py +0 -0
  81. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/editing/_ass.py +0 -0
  82. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/editing/_easing.py +0 -0
  83. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/editing/_schema.py +0 -0
  84. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/editing/audio_ops.py +0 -0
  85. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/editing/effects.py +0 -0
  86. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/editing/operation.py +0 -0
  87. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/editing/streaming.py +0 -0
  88. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/editing/transcription_overlay.py +0 -0
  89. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/editing/transforms.py +0 -0
  90. {videopython-0.48.0 → videopython-0.50.0}/src/videopython/editing/video_edit.py +0 -0
  91. {videopython-0.48.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.48.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/
@@ -28,19 +28,18 @@ Requires-Dist: chatterbox-tts>=0.1.7; extra == 'ai'
28
28
  Requires-Dist: demucs>=4.0.0; extra == 'ai'
29
29
  Requires-Dist: diffusers>=0.30.0; extra == 'ai'
30
30
  Requires-Dist: imagehash>=4.3; extra == 'ai'
31
- Requires-Dist: llama-cpp-python>=0.3.0; extra == 'ai'
32
31
  Requires-Dist: ollama>=0.5.0; extra == 'ai'
33
32
  Requires-Dist: openai-whisper>=20240930; extra == 'ai'
34
33
  Requires-Dist: pyannote-audio>=4.0.0; extra == 'ai'
35
34
  Requires-Dist: pyloudnorm>=0.1.1; extra == 'ai'
36
- Requires-Dist: qwen-vl-utils>=0.0.10; extra == 'ai'
37
- Requires-Dist: sentencepiece>=0.1.99; extra == 'ai'
38
35
  Requires-Dist: silero-vad>=5.1; extra == 'ai'
39
36
  Requires-Dist: torch>=2.8.0; extra == 'ai'
40
37
  Requires-Dist: torchaudio>=2.8.0; extra == 'ai'
41
38
  Requires-Dist: transformers>=5.2.0; extra == 'ai'
42
39
  Requires-Dist: transnetv2-pytorch>=1.0.5; extra == 'ai'
43
40
  Requires-Dist: ultralytics>=8.0.0; extra == 'ai'
41
+ Provides-Extra: mcp
42
+ Requires-Dist: mcp<2,>=1.27; extra == 'mcp'
44
43
  Description-Content-Type: text/markdown
45
44
 
46
45
  # videopython
@@ -59,11 +58,12 @@ Full documentation: [videopython.com](https://videopython.com)
59
58
 
60
59
  ```bash
61
60
  # Install FFmpeg first (macOS: brew install ffmpeg | Debian: apt-get install ffmpeg)
62
- pip install videopython # core video/audio editing
63
- 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
64
64
  ```
65
65
 
66
- 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`).
67
67
 
68
68
  ## Quick Start
69
69
 
@@ -92,6 +92,23 @@ edit.run_to_file("output.mp4") # streams ffmpeg decode → effects → encode
92
92
 
93
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")))`.
94
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
+
95
112
  ### AI generation
96
113
 
97
114
  ```python
@@ -105,7 +122,13 @@ video.add_audio(audio).save("ai_video.mp4")
105
122
 
106
123
  ## LLM & AI Agent Integration
107
124
 
108
- 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.
109
132
 
110
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.
111
134
 
@@ -116,8 +139,10 @@ See the [LLM Integration Guide](https://videopython.com/guides/llm-integration/)
116
139
  - **`videopython.base`** — `Video`, `VideoMetadata`, `FrameIterator`, `Transcription`, and shared result types (`BoundingBox`, `FaceTrack`, `SceneBoundary`, ...). No AI dependencies.
117
140
  - **`videopython.audio`** — `Audio` with overlay, concat, normalize, time-stretch, silence detection, segment classification.
118
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).
119
- - **`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.
120
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.
121
146
 
122
147
  ## Examples
123
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.48.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" },
@@ -72,13 +72,9 @@ ai = [
72
72
  "ultralytics>=8.0.0",
73
73
  "transnetv2-pytorch>=1.0.5",
74
74
  "transformers>=5.2.0",
75
- "qwen-vl-utils>=0.0.10",
76
75
  "imagehash>=4.3",
77
76
  # Audio source separation (understanding/separation.py)
78
77
  "demucs>=4.0.0",
79
- # Translation backends (generation/translation.py MarianMT, generation/qwen3.py GGUF)
80
- "sentencepiece>=0.1.99",
81
- "llama-cpp-python>=0.3.0",
82
78
  # Voice cloning TTS (generation/audio.py — Chatterbox); its strict pins drive
83
79
  # the [tool.uv].override-dependencies block below.
84
80
  "chatterbox-tts>=0.1.7",
@@ -93,6 +89,11 @@ ai = [
93
89
  "torch>=2.8.0",
94
90
  "torchaudio>=2.8.0",
95
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"
96
97
 
97
98
  [project.urls]
98
99
  Homepage = "https://videopython.com"
@@ -120,10 +121,9 @@ module = [
120
121
  "pyannote", "pyannote.*",
121
122
  "silero_vad", "silero_vad.*",
122
123
  "cv2", "cv2.*",
123
- "llama_cpp", "llama_cpp.*",
124
124
  "pyloudnorm", "pyloudnorm.*",
125
- "qwen_vl_utils", "qwen_vl_utils.*",
126
125
  "imagehash", "imagehash.*",
126
+ "mcp", "mcp.*",
127
127
  ]
128
128
  ignore_missing_imports = true
129
129
 
@@ -0,0 +1,71 @@
1
+ """Shared structured-generation client over a local Ollama server."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import io
7
+ import json
8
+ from typing import Any
9
+
10
+ import numpy as np
11
+ from PIL import Image
12
+
13
+ from videopython.ai._optional import require
14
+
15
+
16
+ class OllamaError(RuntimeError):
17
+ """Ollama returned unusable output (non-JSON or an unexpected shape)."""
18
+
19
+
20
+ class OllamaStructuredClient:
21
+ """Generate schema-constrained JSON from text + optional images via Ollama.
22
+
23
+ Shared by the auto-edit planner, scene captioner, and translator. The model
24
+ must be served by a local Ollama daemon and support structured-output
25
+ ``format`` (and vision, when images are passed); ``options`` are extra Ollama
26
+ generation options merged over ``temperature=0``.
27
+ """
28
+
29
+ def __init__(self, model: str, *, host: str | None = None, options: dict[str, Any] | None = None) -> None:
30
+ self.model = model
31
+ self.host = host
32
+ self.options: dict[str, Any] = {"temperature": 0.0, **(options or {})}
33
+ self._client: Any = None
34
+
35
+ def _get_client(self) -> Any:
36
+ if self._client is None:
37
+ ollama = require("ollama", "ai", feature="Ollama")
38
+ self._client = ollama.Client(host=self.host)
39
+ return self._client
40
+
41
+ def generate_json(
42
+ self,
43
+ *,
44
+ system: str,
45
+ text: str,
46
+ schema: dict[str, Any],
47
+ images: list[np.ndarray] | None = None,
48
+ ) -> dict[str, Any]:
49
+ """Return the parsed JSON object Ollama generates under ``schema``."""
50
+ user: dict[str, Any] = {"role": "user", "content": text}
51
+ if images:
52
+ user["images"] = [_encode_png_b64(image) for image in images]
53
+ messages = [{"role": "system", "content": system}, user]
54
+ response = self._get_client().chat(model=self.model, messages=messages, format=schema, options=self.options)
55
+ content = response.message.content
56
+ try:
57
+ data = json.loads(content)
58
+ except (ValueError, TypeError) as exc:
59
+ raise OllamaError(f"Ollama returned non-JSON output: {content!r}") from exc
60
+ if not isinstance(data, dict):
61
+ raise OllamaError(f"Ollama returned a non-object JSON value: {type(data).__name__}")
62
+ return data
63
+
64
+ def unload(self) -> None:
65
+ self._client = None
66
+
67
+
68
+ def _encode_png_b64(frame: np.ndarray) -> str:
69
+ buffer = io.BytesIO()
70
+ Image.fromarray(frame).save(buffer, format="PNG")
71
+ return base64.b64encode(buffer.getvalue()).decode("ascii")
@@ -41,11 +41,6 @@ Usage:
41
41
  # from the HF repo ``arnabdhar/YOLOv8-Face-Detection`` via
42
42
  # hf_hub_download, and that one IS pinned below.)
43
43
  #
44
- # * Dynamic Marian pairs -- ai/generation/translation.py builds the model
45
- # name per language pair at runtime (e.g. "Helsinki-NLP/opus-mt-en-de").
46
- # The set of repos is open-ended (one per direction), so pinning each pair
47
- # to a SHA is impractical and out of scope; left unpinned by design.
48
- #
49
44
  # * Chatterbox internal load -- ai/generation/audio.py calls
50
45
  # ``ChatterboxMultilingualTTS.from_pretrained(device=...)`` with no repo
51
46
  # argument. The repo id + revision are resolved internally by the
@@ -67,10 +62,6 @@ from __future__ import annotations
67
62
  # ``pinned(model_id)`` resolves with a plain dict lookup. SHAs captured from
68
63
  # the HuggingFace model API (see module docstring for refresh instructions).
69
64
  MODEL_REVISIONS: dict[str, str] = {
70
- # SceneVLM (ai/understanding/image.py: SCENE_VLM_MODEL_IDS)
71
- "Qwen/Qwen3.5-4B": "851bf6e806efd8d0a36b00ddf55e13ccb7b8cd0a",
72
- "Qwen/Qwen3.5-9B": "c202236235762e1c871ad0ccb60c8ee5ba337b9a",
73
- "Qwen/Qwen3.5-27B": "fc05daec18b0a78c049392ed2e771dde82bdf654",
74
65
  # Speaker diarization (ai/understanding/audio.py: PYANNOTE_DIARIZATION_MODEL).
75
66
  # Gated repo (auto-approved); from_pretrained needs an accepted HF token.
76
67
  "pyannote/speaker-diarization-community-1": "3533c8cf8e369892e6b79ff1bf80f7b0286a54ee",
@@ -78,8 +69,6 @@ MODEL_REVISIONS: dict[str, str] = {
78
69
  "MIT/ast-finetuned-audioset-10-10-0.4593": "f826b80d28226b62986cc218e5cec390b1096902",
79
70
  # Face detection checkpoint (ai/understanding/faces.py: hf_hub_download)
80
71
  "arnabdhar/YOLOv8-Face-Detection": "52fa54977207fa4f021de949b515fb19dcab4488",
81
- # Qwen3 translation GGUF (ai/generation/qwen3.py: DEFAULT_REPO_ID)
82
- "unsloth/Qwen3-4B-Instruct-2507-GGUF": "a06e946bb6b655725eafa393f4a9745d460374c9",
83
72
  # MusicGen (ai/generation/audio.py: TextToMusic)
84
73
  "facebook/musicgen-small": "4c8334b02c6ec4e8664a91979669a501ec497792",
85
74
  # SDXL (ai/generation/image.py: TextToImage)
@@ -0,0 +1,39 @@
1
+ """A local, model-agnostic planner backed by an Ollama server."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from videopython.ai._ollama import OllamaError, OllamaStructuredClient
8
+
9
+ from .backend import ImagePart, Part, PlannerError, TextPart
10
+
11
+ DEFAULT_OLLAMA_MODEL = "gemma3:27b"
12
+
13
+
14
+ class OllamaVisionLLM:
15
+ """A StructuredVisionLLM backed by a local Ollama server.
16
+
17
+ The model must be vision-capable (it is sent keyframes) AND support Ollama's
18
+ structured-output ``format`` (the EditPlan schema constrains the decode). Not
19
+ every model supports schema conditioning -- ``gemma3:27b`` is verified working;
20
+ some builds (e.g. certain MLX ones) fail it. ``ollama pull <model>`` first;
21
+ ``options`` are extra generation options merged over ``temperature=0``.
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ model: str = DEFAULT_OLLAMA_MODEL,
27
+ *,
28
+ host: str | None = None,
29
+ options: dict[str, Any] | None = None,
30
+ ) -> None:
31
+ self._client = OllamaStructuredClient(model=model, host=host, options=options)
32
+
33
+ def generate_json(self, *, system: str, parts: list[Part], schema: dict[str, Any]) -> dict[str, Any]:
34
+ text = "\n\n".join(part.text for part in parts if isinstance(part, TextPart))
35
+ images = [part.image for part in parts if isinstance(part, ImagePart)]
36
+ try:
37
+ return self._client.generate_json(system=system, text=text, schema=schema, images=images or None)
38
+ except OllamaError as exc:
39
+ raise PlannerError(str(exc)) from exc
@@ -1,11 +1,10 @@
1
1
  """Local video dubbing functionality, lazily re-exported (PEP 562).
2
2
 
3
- The pipeline's translation backends (``MarianTranslator``/``Qwen3Translator``)
4
- top-level import transformers/llama-cpp/torch, so eagerly chaining ``pipeline``
5
- here would drag the ``[dub]`` translation stack in just to read
6
- ``DubbingConfig`` (pydantic-only). Lazy re-exports keep the lightweight
7
- config/model symbols importable without that; only ``LocalDubbingPipeline`` /
8
- ``VideoDubber`` access loads the heavy chain. The ``TYPE_CHECKING`` block keeps
3
+ The pipeline pulls heavy stages (Whisper, Demucs, Chatterbox), so eagerly
4
+ chaining ``pipeline`` here would drag them in just to read ``DubbingConfig``
5
+ (pydantic-only). Lazy re-exports keep the lightweight config/model symbols
6
+ importable without that; only ``LocalDubbingPipeline`` / ``VideoDubber`` access
7
+ loads the heavy chain. The ``TYPE_CHECKING`` block keeps
9
8
  symbols visible to mypy and IDEs.
10
9
  """
11
10
 
@@ -6,7 +6,7 @@ from typing import Literal
6
6
 
7
7
  from pydantic import BaseModel, ConfigDict
8
8
 
9
- TranslatorChoice = Literal["auto", "marian", "qwen3"]
9
+ TranslatorChoice = Literal["auto", "ollama"]
10
10
  WhisperModel = Literal["tiny", "base", "small", "medium", "large", "turbo"]
11
11
 
12
12
 
@@ -18,7 +18,7 @@ class DubbingConfig(BaseModel):
18
18
 
19
19
  Attributes:
20
20
  device: Execution device (``cpu``, ``cuda``, ``mps``, or ``None`` for auto).
21
- low_memory: When True, each pipeline stage (Whisper, Demucs, MarianMT,
21
+ low_memory: When True, each pipeline stage (Whisper, Demucs, translation,
22
22
  Chatterbox TTS) is unloaded from memory after it runs, so only one
23
23
  model is resident at a time. Trades per-run latency (~10-30s of
24
24
  extra model loads) for a much lower memory ceiling. Recommended
@@ -47,12 +47,9 @@ class DubbingConfig(BaseModel):
47
47
  WARNING but processing continues. Either way the
48
48
  :class:`TranscriptQuality` is exposed on ``DubbingResult`` for
49
49
  inspection.
50
- translator: Translation backend to use. ``"auto"`` (default) picks
51
- Qwen3 on GPU, MarianMT on CPU; ``"marian"`` and ``"qwen3"`` force
52
- the named backend regardless of device. See
53
- :class:`videopython.ai.generation.qwen3.Qwen3Translator` for
54
- tradeoffs (Qwen3 is slower on CPU but produces context-aware,
55
- length-budgeted output).
50
+ translator: Translation backend; ``"auto"`` and ``"ollama"`` both use the
51
+ local Ollama text model. ``translator_model`` sets the Ollama tag and
52
+ ``translator_host`` the server URL.
56
53
  """
57
54
 
58
55
  model_config = ConfigDict(frozen=True)
@@ -66,6 +63,8 @@ class DubbingConfig(BaseModel):
66
63
  vocabulary: list[str] | None = None
67
64
  strict_quality: bool = False
68
65
  translator: TranslatorChoice = "auto"
66
+ translator_model: str | None = None
67
+ translator_host: str | None = None
69
68
 
70
69
  def init_log_fields(self) -> dict[str, object]:
71
70
  """Subset of fields surfaced in the init-log line.
@@ -247,6 +247,6 @@ class VideoDubber:
247
247
 
248
248
  @staticmethod
249
249
  def get_supported_languages() -> dict[str, str]:
250
- from videopython.ai.generation.translation import TextTranslator
250
+ from videopython.ai.generation.translation import OllamaTranslator
251
251
 
252
- return TextTranslator.get_supported_languages()
252
+ return OllamaTranslator.get_supported_languages()
@@ -218,11 +218,9 @@ class DubbingResult(BaseModel):
218
218
  timing_summary: Aggregate stats over per-segment timing adjustments.
219
219
  transcript_quality: Heuristic quality assessment of the transcription
220
220
  (None when the pipeline returned early on an empty transcription).
221
- translation_failures: Indices of segments where translation failed
222
- entirely. Used by Qwen3Translator when both the primary call and
223
- the per-segment Marian fallback fail; those segments are dubbed
224
- with empty text. Empty list under MarianTranslator (Marian has
225
- no failure mode that drops segments).
221
+ translation_failures: Indices of segments the translator could not
222
+ translate (missing after its parse-retry pass); those segments are
223
+ dubbed with empty text.
226
224
  """
227
225
 
228
226
  model_config = ConfigDict(arbitrary_types_allowed=True)
@@ -7,18 +7,12 @@ import tempfile
7
7
  from pathlib import Path
8
8
  from typing import TYPE_CHECKING, Any, Callable
9
9
 
10
- from videopython.ai._device import select_device
11
10
  from videopython.ai.dubbing import expressiveness, loudness, voice_sample
12
11
  from videopython.ai.dubbing.config import DubbingConfig
13
12
  from videopython.ai.dubbing.models import DubbingResult, Expressiveness, RevoiceResult, SeparatedAudio, TimingSummary
14
13
  from videopython.ai.dubbing.quality import GarbageTranscriptError, assess_transcript
15
14
  from videopython.ai.dubbing.timing import TimingSynchronizer
16
- from videopython.ai.generation.qwen3 import Qwen3Translator
17
- from videopython.ai.generation.translation import (
18
- MarianTranslator,
19
- TranslationBackend,
20
- UnsupportedLanguageError,
21
- )
15
+ from videopython.ai.generation.translation import DEFAULT_TRANSLATION_MODEL, OllamaTranslator
22
16
 
23
17
  if TYPE_CHECKING:
24
18
  from videopython.ai.dubbing.models import TranslatedSegment
@@ -205,63 +199,11 @@ class LocalDubbingPipeline:
205
199
  )
206
200
 
207
201
  def _init_translator(self, source_lang: str, target_lang: str) -> None:
208
- """Initialize the translation backend.
209
-
210
- Resolves the configured ``self.config.translator`` choice into a concrete
211
- backend. ``"auto"`` uses :meth:`_resolve_translator_auto`; explicit
212
- choices instantiate the named backend directly. Re-initialization
213
- is a no-op when ``self._translator`` is already a matching instance
214
- for the same language pair (handled at call sites via the existing
215
- ``self._translator is None`` gate).
216
- """
217
- if self.config.translator == "marian":
218
- self._translator = MarianTranslator(device=self.config.device)
219
- elif self.config.translator == "qwen3":
220
- self._translator = Qwen3Translator(device=self.config.device)
221
- else: # "auto"
222
- self._translator = self._resolve_translator_auto(source_lang, target_lang)
223
-
224
- def _resolve_translator_auto(self, source_lang: str, target_lang: str) -> TranslationBackend:
225
- """Pick a backend based on language coverage AND device.
226
-
227
- Qwen3-4B Q4_K_M on CPU is roughly 10-15x slower than MarianMT (M2.1
228
- spike on dreams_15min.mp4). The resolver picks Marian on CPU
229
- whenever it covers the language pair and only escalates to Qwen
230
- when a GPU is available or Marian doesn't cover the pair.
231
- """
232
- device = select_device(self.config.device, mps_allowed=True)
233
- has_gpu = device in ("cuda", "mps")
234
-
235
- # 1. GPU + Qwen covers the pair → Qwen wins (best quality).
236
- if has_gpu and Qwen3Translator.supports(source_lang, target_lang):
237
- logger.info(
238
- "translator: auto-selected qwen3 (device=%s, supports %s->%s)",
239
- device,
240
- source_lang,
241
- target_lang,
242
- )
243
- return Qwen3Translator(device=self.config.device)
244
-
245
- # 2. Marian covers the pair → Marian (fast).
246
- if MarianTranslator.has_model_for(source_lang, target_lang):
247
- if has_gpu:
248
- reason = f"Qwen does not cover {source_lang}->{target_lang}"
249
- else:
250
- reason = f"device={device} (Qwen would be ~10-15x slower; pass translator='qwen3' to override)"
251
- logger.info("translator: auto-selected marian (%s)", reason)
252
- return MarianTranslator(device=self.config.device)
253
-
254
- # 3. CPU + only Qwen covers it: warn loudly and use Qwen anyway.
255
- if Qwen3Translator.supports(source_lang, target_lang):
256
- logger.warning(
257
- "translator: auto-selected qwen3 on CPU (%s->%s not in Marian); "
258
- "translation will be slow (~10-15x MarianMT). Consider GPU.",
259
- source_lang,
260
- target_lang,
261
- )
262
- return Qwen3Translator(device=self.config.device)
263
-
264
- raise UnsupportedLanguageError(source_lang, target_lang)
202
+ """Initialize the Ollama translation backend."""
203
+ self._translator = OllamaTranslator(
204
+ model=self.config.translator_model or DEFAULT_TRANSLATION_MODEL,
205
+ host=self.config.translator_host,
206
+ )
265
207
 
266
208
  def _init_tts(self, language: str = "en") -> None:
267
209
  """Resolve the text-to-speech backend for ``language``.