videopython 0.50.0__tar.gz → 0.50.1__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.50.0 → videopython-0.50.1}/PKG-INFO +1 -1
- {videopython-0.50.0 → videopython-0.50.1}/pyproject.toml +1 -1
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/_ollama.py +15 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/video_analysis/models.py +12 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/mcp/server.py +58 -22
- {videopython-0.50.0 → videopython-0.50.1}/.gitignore +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/LICENSE +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/README.md +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/__init__.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/__init__.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/_device.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/_optional.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/_predictor.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/_revisions.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/auto_edit/__init__.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/auto_edit/backend.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/auto_edit/catalog.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/auto_edit/editor.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/auto_edit/local.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/auto_edit/models.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/auto_edit/resolve.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/dubbing/__init__.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/dubbing/config.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/dubbing/dubber.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/dubbing/expressiveness.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/dubbing/loudness.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/dubbing/models.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/dubbing/pipeline.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/dubbing/quality.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/dubbing/remux.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/dubbing/timing.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/dubbing/voice_sample.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/effects.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/generation/__init__.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/generation/_tts_backend.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/generation/audio.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/generation/image.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/generation/translation.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/generation/video.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/transforms.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/understanding/__init__.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/understanding/audio.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/understanding/faces.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/understanding/image.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/understanding/objects.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/understanding/separation.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/understanding/temporal.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/video_analysis/__init__.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/video_analysis/analyzer.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/video_analysis/sampling.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/video_analysis/stages.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/audio/__init__.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/audio/analysis.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/audio/audio.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/__init__.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/_dimensions.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/_ffmpeg.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/_video_io.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/description.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/draw_detections.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/exceptions.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/fonts/Anton-OFL.txt +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/fonts/Anton-Regular.ttf +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/fonts/BebasNeue-OFL.txt +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/fonts/BebasNeue-Regular.ttf +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/fonts/DejaVuSans.ttf +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/fonts/LICENSE_DEJAVU +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/fonts/Lato-Bold.ttf +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/fonts/Lato-OFL.txt +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/fonts/Poppins-Bold.ttf +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/fonts/Poppins-OFL.txt +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/fonts/__init__.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/transcription.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/video.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/editing/__init__.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/editing/_ass.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/editing/_easing.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/editing/_schema.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/editing/audio_ops.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/editing/effects.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/editing/operation.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/editing/streaming.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/editing/transcription_overlay.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/editing/transforms.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/editing/video_edit.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/mcp/__init__.py +0 -0
- {videopython-0.50.0 → videopython-0.50.1}/src/videopython/py.typed +0 -0
|
@@ -7,6 +7,7 @@ import io
|
|
|
7
7
|
import json
|
|
8
8
|
from typing import Any
|
|
9
9
|
|
|
10
|
+
import cv2
|
|
10
11
|
import numpy as np
|
|
11
12
|
from PIL import Image
|
|
12
13
|
|
|
@@ -65,6 +66,20 @@ class OllamaStructuredClient:
|
|
|
65
66
|
self._client = None
|
|
66
67
|
|
|
67
68
|
|
|
69
|
+
# Used only on the MCP keyframe path (videopython.mcp). SceneVLM captioning and the local
|
|
70
|
+
# planner deliberately encode full-resolution frames via _encode_png_b64.
|
|
71
|
+
KEYFRAME_MAX_DIM = 768 # bound a keyframe's longest side before PNG-encoding for the MCP payload
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _downscale(frame: np.ndarray, max_dim: int = KEYFRAME_MAX_DIM) -> np.ndarray:
|
|
75
|
+
"""Shrink an RGB frame so its longest side is at most ``max_dim`` (aspect preserved; never upscales)."""
|
|
76
|
+
h, w = frame.shape[:2]
|
|
77
|
+
scale = max_dim / max(h, w)
|
|
78
|
+
if scale >= 1.0:
|
|
79
|
+
return frame
|
|
80
|
+
return cv2.resize(frame, (max(1, round(w * scale)), max(1, round(h * scale))), interpolation=cv2.INTER_AREA)
|
|
81
|
+
|
|
82
|
+
|
|
68
83
|
def _encode_png_b64(frame: np.ndarray) -> str:
|
|
69
84
|
buffer = io.BytesIO()
|
|
70
85
|
Image.fromarray(frame).save(buffer, format="PNG")
|
|
@@ -173,6 +173,18 @@ class VideoAnalysisConfig(BaseModel):
|
|
|
173
173
|
"""Backward-compatible alias for the default config."""
|
|
174
174
|
return cls()
|
|
175
175
|
|
|
176
|
+
@classmethod
|
|
177
|
+
def for_profile(cls, profile: str, *, faces: bool = True) -> VideoAnalysisConfig:
|
|
178
|
+
"""Config for an analysis profile: 'full' (all analyzers) or 'editing' (catalog-only, no audio classifier)."""
|
|
179
|
+
if profile == "full":
|
|
180
|
+
return cls()
|
|
181
|
+
if profile == "editing":
|
|
182
|
+
enabled = {SEMANTIC_SCENE_DETECTOR, SCENE_VLM, AUDIO_TO_TEXT}
|
|
183
|
+
if faces:
|
|
184
|
+
enabled.add(FACE_TRACKER)
|
|
185
|
+
return cls(enabled_analyzers=enabled)
|
|
186
|
+
raise ValueError(f"Unknown profile: {profile!r} (expected 'full' or 'editing')")
|
|
187
|
+
|
|
176
188
|
|
|
177
189
|
class AudioAnalysisSection(BaseModel):
|
|
178
190
|
"""Audio understanding outputs."""
|
|
@@ -13,13 +13,13 @@ import json
|
|
|
13
13
|
import sys
|
|
14
14
|
from contextlib import redirect_stdout
|
|
15
15
|
from pathlib import Path
|
|
16
|
-
from typing import TYPE_CHECKING, Any
|
|
16
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
17
17
|
|
|
18
18
|
from mcp.server.fastmcp import FastMCP
|
|
19
19
|
from mcp.types import ImageContent, TextContent
|
|
20
20
|
from pydantic import ValidationError
|
|
21
21
|
|
|
22
|
-
from videopython.ai._ollama import _encode_png_b64
|
|
22
|
+
from videopython.ai._ollama import _downscale, _encode_png_b64
|
|
23
23
|
from videopython.ai.auto_edit import EditPlan, UnknownSceneIdsError, resolve_plan
|
|
24
24
|
from videopython.ai.auto_edit import build_catalog as _build_scene_catalog
|
|
25
25
|
from videopython.editing import VideoEdit
|
|
@@ -33,20 +33,24 @@ mcp = FastMCP("videopython")
|
|
|
33
33
|
|
|
34
34
|
# Session state for one stdio client (one process per client).
|
|
35
35
|
_analyses: dict[str, VideoAnalysis] = {}
|
|
36
|
-
|
|
36
|
+
_analyzers: dict[str, VideoAnalyzer] = {}
|
|
37
37
|
_bundle: CatalogBundle | None = None
|
|
38
38
|
|
|
39
|
+
_MAX_INLINE_KEYFRAMES = 12 # cap inlined keyframes in build_catalog; pull the rest with scene_keyframes
|
|
40
|
+
|
|
39
41
|
|
|
40
42
|
@mcp.tool()
|
|
41
|
-
def analyze_video(path: str) -> dict[str, Any]:
|
|
43
|
+
def analyze_video(path: str, profile: Literal["full", "editing"] = "full") -> dict[str, Any]:
|
|
42
44
|
"""Analyze a source video (scenes, transcript, captions) and cache it for build_catalog.
|
|
43
45
|
|
|
44
|
-
|
|
46
|
+
``profile="editing"`` skips audio classification (faster on long sources); the
|
|
47
|
+
catalog (captions + transcript + face flag) is unaffected. Returns a short
|
|
48
|
+
summary; call this once per source, then build_catalog.
|
|
45
49
|
"""
|
|
46
50
|
# Heavy analyzer deps (e.g. transnetv2-pytorch) bare-print to stdout, which here is
|
|
47
51
|
# the stdio JSON-RPC channel; send that to stderr so the transport stays clean.
|
|
48
52
|
with redirect_stdout(sys.stderr):
|
|
49
|
-
analysis = _get_analyzer().analyze_path(path)
|
|
53
|
+
analysis = _get_analyzer(profile).analyze_path(path)
|
|
50
54
|
_analyses[str(Path(path))] = analysis
|
|
51
55
|
src = analysis.source
|
|
52
56
|
return {
|
|
@@ -63,10 +67,11 @@ def analyze_video(path: str) -> dict[str, Any]:
|
|
|
63
67
|
def build_catalog(sources: list[str] | None = None) -> list[TextContent | ImageContent]:
|
|
64
68
|
"""Build the candidate-scene catalog from analyzed videos and cache it.
|
|
65
69
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
The first text block is the full catalog JSON (id/duration/shot_type/caption/
|
|
71
|
+
transcript per scene -- enough to shortlist from text alone). Up to
|
|
72
|
+
``_MAX_INLINE_KEYFRAMES`` downscaled keyframes follow; a final note names any
|
|
73
|
+
scene ids whose images were omitted (fetch them with scene_keyframes). Pass
|
|
74
|
+
``sources`` to restrict to specific analyzed paths, or omit for all.
|
|
70
75
|
"""
|
|
71
76
|
global _bundle
|
|
72
77
|
analyses = _selected_analyses(sources)
|
|
@@ -74,15 +79,36 @@ def build_catalog(sources: list[str] | None = None) -> list[TextContent | ImageC
|
|
|
74
79
|
raise ValueError("No analyzed videos cached; call analyze_video first.")
|
|
75
80
|
_bundle = _build_scene_catalog(analyses)
|
|
76
81
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
ids = [scene.id for scene in _bundle.catalog.scenes]
|
|
83
|
+
inlined, omitted = ids[:_MAX_INLINE_KEYFRAMES], ids[_MAX_INLINE_KEYFRAMES:]
|
|
84
|
+
blocks: list[TextContent | ImageContent] = [
|
|
85
|
+
TextContent(type="text", text=_bundle.catalog.model_dump_json()),
|
|
86
|
+
*_keyframe_blocks(inlined),
|
|
87
|
+
]
|
|
88
|
+
if omitted:
|
|
89
|
+
blocks.append(
|
|
90
|
+
TextContent(
|
|
91
|
+
type="text",
|
|
92
|
+
text=f"Keyframes omitted for {len(omitted)} scenes "
|
|
93
|
+
f"(call scene_keyframes for these ids): {', '.join(omitted)}",
|
|
94
|
+
)
|
|
95
|
+
)
|
|
83
96
|
return blocks
|
|
84
97
|
|
|
85
98
|
|
|
99
|
+
@mcp.tool(structured_output=False)
|
|
100
|
+
def scene_keyframes(scene_ids: list[str]) -> list[TextContent | ImageContent]:
|
|
101
|
+
"""Return downscaled keyframe images for specific catalog scene ids (use after build_catalog)."""
|
|
102
|
+
if _bundle is None:
|
|
103
|
+
raise ValueError("No catalog cached; call build_catalog first.")
|
|
104
|
+
known = _bundle.catalog.by_id()
|
|
105
|
+
unknown = sorted({sid for sid in scene_ids if sid not in known})
|
|
106
|
+
if unknown:
|
|
107
|
+
error = {"code": "unknown_scene_ids", "value": unknown, "message": f"Unknown scene ids: {unknown}"}
|
|
108
|
+
return [TextContent(type="text", text=json.dumps(error))]
|
|
109
|
+
return _keyframe_blocks(list(dict.fromkeys(scene_ids)))
|
|
110
|
+
|
|
111
|
+
|
|
86
112
|
@mcp.tool()
|
|
87
113
|
def validate_edit(plan: dict[str, Any]) -> dict[str, Any]:
|
|
88
114
|
"""Validate an edit plan (an EditPlan referencing catalog scene ids).
|
|
@@ -146,13 +172,12 @@ def edit_plan_schema() -> str:
|
|
|
146
172
|
return json.dumps(EditPlan.json_schema(strict=True))
|
|
147
173
|
|
|
148
174
|
|
|
149
|
-
def _get_analyzer() -> VideoAnalyzer:
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
from videopython.ai.video_analysis import VideoAnalyzer
|
|
175
|
+
def _get_analyzer(profile: str = "full") -> VideoAnalyzer:
|
|
176
|
+
if profile not in _analyzers:
|
|
177
|
+
from videopython.ai.video_analysis import VideoAnalysisConfig, VideoAnalyzer
|
|
153
178
|
|
|
154
|
-
|
|
155
|
-
return
|
|
179
|
+
_analyzers[profile] = VideoAnalyzer(config=VideoAnalysisConfig.for_profile(profile))
|
|
180
|
+
return _analyzers[profile]
|
|
156
181
|
|
|
157
182
|
|
|
158
183
|
def _selected_analyses(sources: list[str] | None) -> list[VideoAnalysis]:
|
|
@@ -161,6 +186,17 @@ def _selected_analyses(sources: list[str] | None) -> list[VideoAnalysis]:
|
|
|
161
186
|
return [_analyses[str(Path(s))] for s in sources if str(Path(s)) in _analyses]
|
|
162
187
|
|
|
163
188
|
|
|
189
|
+
def _keyframe_blocks(scene_ids: list[str]) -> list[TextContent | ImageContent]:
|
|
190
|
+
assert _bundle is not None # callers guard; narrows the module global for mypy
|
|
191
|
+
blocks: list[TextContent | ImageContent] = []
|
|
192
|
+
for sid in scene_ids:
|
|
193
|
+
frame = _bundle.keyframes.get(sid)
|
|
194
|
+
if frame is not None:
|
|
195
|
+
blocks.append(TextContent(type="text", text=f"scene {sid}:"))
|
|
196
|
+
blocks.append(ImageContent(type="image", data=_encode_png_b64(_downscale(frame)), mimeType="image/png"))
|
|
197
|
+
return blocks
|
|
198
|
+
|
|
199
|
+
|
|
164
200
|
def _resolve(plan: dict[str, Any]) -> tuple[VideoEdit | None, list[dict[str, Any]]]:
|
|
165
201
|
"""Resolve a by-id plan, or return (None, structured errors) for the agent's plan mistakes."""
|
|
166
202
|
if _bundle is None:
|
|
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
|
|
File without changes
|