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.
Files changed (87) hide show
  1. {videopython-0.50.0 → videopython-0.50.1}/PKG-INFO +1 -1
  2. {videopython-0.50.0 → videopython-0.50.1}/pyproject.toml +1 -1
  3. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/_ollama.py +15 -0
  4. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/video_analysis/models.py +12 -0
  5. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/mcp/server.py +58 -22
  6. {videopython-0.50.0 → videopython-0.50.1}/.gitignore +0 -0
  7. {videopython-0.50.0 → videopython-0.50.1}/LICENSE +0 -0
  8. {videopython-0.50.0 → videopython-0.50.1}/README.md +0 -0
  9. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/__init__.py +0 -0
  10. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/__init__.py +0 -0
  11. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/_device.py +0 -0
  12. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/_optional.py +0 -0
  13. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/_predictor.py +0 -0
  14. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/_revisions.py +0 -0
  15. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/auto_edit/__init__.py +0 -0
  16. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/auto_edit/backend.py +0 -0
  17. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/auto_edit/catalog.py +0 -0
  18. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/auto_edit/editor.py +0 -0
  19. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/auto_edit/local.py +0 -0
  20. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/auto_edit/models.py +0 -0
  21. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/auto_edit/resolve.py +0 -0
  22. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/dubbing/__init__.py +0 -0
  23. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/dubbing/config.py +0 -0
  24. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/dubbing/dubber.py +0 -0
  25. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/dubbing/expressiveness.py +0 -0
  26. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/dubbing/loudness.py +0 -0
  27. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/dubbing/models.py +0 -0
  28. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/dubbing/pipeline.py +0 -0
  29. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/dubbing/quality.py +0 -0
  30. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/dubbing/remux.py +0 -0
  31. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/dubbing/timing.py +0 -0
  32. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/dubbing/voice_sample.py +0 -0
  33. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/effects.py +0 -0
  34. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/generation/__init__.py +0 -0
  35. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/generation/_tts_backend.py +0 -0
  36. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/generation/audio.py +0 -0
  37. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/generation/image.py +0 -0
  38. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/generation/translation.py +0 -0
  39. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/generation/video.py +0 -0
  40. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/transforms.py +0 -0
  41. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/understanding/__init__.py +0 -0
  42. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/understanding/audio.py +0 -0
  43. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/understanding/faces.py +0 -0
  44. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/understanding/image.py +0 -0
  45. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/understanding/objects.py +0 -0
  46. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/understanding/separation.py +0 -0
  47. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/understanding/temporal.py +0 -0
  48. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/video_analysis/__init__.py +0 -0
  49. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/video_analysis/analyzer.py +0 -0
  50. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/video_analysis/sampling.py +0 -0
  51. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/ai/video_analysis/stages.py +0 -0
  52. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/audio/__init__.py +0 -0
  53. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/audio/analysis.py +0 -0
  54. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/audio/audio.py +0 -0
  55. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/__init__.py +0 -0
  56. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/_dimensions.py +0 -0
  57. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/_ffmpeg.py +0 -0
  58. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/_video_io.py +0 -0
  59. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/description.py +0 -0
  60. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/draw_detections.py +0 -0
  61. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/exceptions.py +0 -0
  62. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/fonts/Anton-OFL.txt +0 -0
  63. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/fonts/Anton-Regular.ttf +0 -0
  64. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/fonts/BebasNeue-OFL.txt +0 -0
  65. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/fonts/BebasNeue-Regular.ttf +0 -0
  66. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/fonts/DejaVuSans.ttf +0 -0
  67. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/fonts/LICENSE_DEJAVU +0 -0
  68. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/fonts/Lato-Bold.ttf +0 -0
  69. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/fonts/Lato-OFL.txt +0 -0
  70. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/fonts/Poppins-Bold.ttf +0 -0
  71. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/fonts/Poppins-OFL.txt +0 -0
  72. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/fonts/__init__.py +0 -0
  73. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/transcription.py +0 -0
  74. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/base/video.py +0 -0
  75. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/editing/__init__.py +0 -0
  76. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/editing/_ass.py +0 -0
  77. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/editing/_easing.py +0 -0
  78. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/editing/_schema.py +0 -0
  79. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/editing/audio_ops.py +0 -0
  80. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/editing/effects.py +0 -0
  81. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/editing/operation.py +0 -0
  82. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/editing/streaming.py +0 -0
  83. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/editing/transcription_overlay.py +0 -0
  84. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/editing/transforms.py +0 -0
  85. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/editing/video_edit.py +0 -0
  86. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/mcp/__init__.py +0 -0
  87. {videopython-0.50.0 → videopython-0.50.1}/src/videopython/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: videopython
3
- Version: 0.50.0
3
+ Version: 0.50.1
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/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "videopython"
3
- version = "0.50.0"
3
+ version = "0.50.1"
4
4
  description = "Minimal video generation and processing library."
5
5
  authors = [
6
6
  { name = "Bartosz Wójtowicz", email = "bartoszwojtowicz@outlook.com" },
@@ -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
- _analyzer: VideoAnalyzer | None = None
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
- Returns a short summary; call this once per source, then build_catalog.
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
- 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
+ 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
- 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"))
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
- global _analyzer
151
- if _analyzer is None:
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
- _analyzer = VideoAnalyzer()
155
- return _analyzer
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