videopython 0.50.1__tar.gz → 0.52.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {videopython-0.50.1 → videopython-0.52.0}/PKG-INFO +6 -4
- {videopython-0.50.1 → videopython-0.52.0}/README.md +5 -3
- {videopython-0.50.1 → videopython-0.52.0}/pyproject.toml +1 -1
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/ai/__init__.py +14 -12
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/ai/_ollama.py +5 -27
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/ai/_optional.py +13 -15
- videopython-0.52.0/src/videopython/ai/_predictor.py +70 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/ai/_revisions.py +1 -1
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/ai/auto_edit/__init__.py +10 -5
- videopython-0.52.0/src/videopython/ai/auto_edit/backend.py +30 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/ai/auto_edit/editor.py +16 -10
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/ai/auto_edit/local.py +12 -5
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/ai/auto_edit/resolve.py +2 -1
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/ai/dubbing/__init__.py +0 -2
- {videopython-0.50.1/src/videopython/ai/generation → videopython-0.52.0/src/videopython/ai/dubbing}/_tts_backend.py +7 -8
- videopython-0.50.1/src/videopython/ai/dubbing/loudness.py → videopython-0.52.0/src/videopython/ai/dubbing/audio_ops.py +47 -2
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/ai/dubbing/config.py +16 -7
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/ai/dubbing/dubber.py +8 -9
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/ai/dubbing/models.py +22 -3
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/ai/dubbing/pipeline.py +19 -22
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/ai/dubbing/quality.py +3 -1
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/ai/dubbing/remux.py +8 -72
- {videopython-0.50.1/src/videopython/ai/understanding → videopython-0.52.0/src/videopython/ai/dubbing}/separation.py +4 -12
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/ai/dubbing/timing.py +1 -62
- {videopython-0.50.1/src/videopython/ai/generation → videopython-0.52.0/src/videopython/ai/dubbing}/translation.py +1 -48
- videopython-0.52.0/src/videopython/ai/errors.py +13 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/ai/generation/audio.py +5 -19
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/ai/generation/image.py +4 -7
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/ai/generation/video.py +7 -13
- videopython-0.52.0/src/videopython/ai/keyframe.py +43 -0
- videopython-0.52.0/src/videopython/ai/ops.py +23 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/ai/transforms.py +2 -2
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/ai/understanding/__init__.py +9 -7
- videopython-0.52.0/src/videopython/ai/understanding/_yolo.py +111 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/ai/understanding/audio.py +6 -201
- videopython-0.52.0/src/videopython/ai/understanding/classification.py +196 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/ai/understanding/faces.py +95 -109
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/ai/understanding/objects.py +14 -68
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/ai/understanding/temporal.py +2 -7
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/ai/video_analysis/__init__.py +7 -1
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/ai/video_analysis/analyzer.py +41 -141
- videopython-0.50.1/src/videopython/ai/video_analysis/stages.py → videopython-0.52.0/src/videopython/ai/video_analysis/detectors.py +2 -2
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/ai/video_analysis/models.py +0 -5
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/ai/video_analysis/sampling.py +1 -1
- videopython-0.52.0/src/videopython/ai/video_analysis/source_metadata.py +133 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/mcp/server.py +2 -2
- videopython-0.50.1/src/videopython/ai/_predictor.py +0 -54
- videopython-0.50.1/src/videopython/ai/auto_edit/backend.py +0 -37
- videopython-0.50.1/src/videopython/ai/dubbing/expressiveness.py +0 -47
- {videopython-0.50.1 → videopython-0.52.0}/.gitignore +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/LICENSE +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/__init__.py +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/ai/_device.py +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/ai/auto_edit/catalog.py +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/ai/auto_edit/models.py +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/ai/dubbing/voice_sample.py +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/ai/effects.py +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/ai/generation/__init__.py +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/ai/understanding/image.py +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/audio/__init__.py +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/audio/analysis.py +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/audio/audio.py +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/base/__init__.py +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/base/_dimensions.py +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/base/_ffmpeg.py +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/base/_video_io.py +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/base/description.py +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/base/draw_detections.py +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/base/exceptions.py +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/base/fonts/Anton-OFL.txt +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/base/fonts/Anton-Regular.ttf +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/base/fonts/BebasNeue-OFL.txt +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/base/fonts/BebasNeue-Regular.ttf +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/base/fonts/DejaVuSans.ttf +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/base/fonts/LICENSE_DEJAVU +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/base/fonts/Lato-Bold.ttf +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/base/fonts/Lato-OFL.txt +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/base/fonts/Poppins-Bold.ttf +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/base/fonts/Poppins-OFL.txt +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/base/fonts/__init__.py +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/base/transcription.py +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/base/video.py +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/editing/__init__.py +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/editing/_ass.py +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/editing/_easing.py +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/editing/_schema.py +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/editing/audio_ops.py +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/editing/effects.py +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/editing/operation.py +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/editing/streaming.py +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/editing/transcription_overlay.py +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/editing/transforms.py +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/editing/video_edit.py +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/mcp/__init__.py +0 -0
- {videopython-0.50.1 → videopython-0.52.0}/src/videopython/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: videopython
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.52.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/
|
|
@@ -128,11 +128,13 @@ Putting an LLM in the loop works three ways:
|
|
|
128
128
|
2. **`AutoEditor`** — a local Ollama vision model is the planner (see [Automatic editing](#automatic-editing-local-llm) above).
|
|
129
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
130
|
|
|
131
|
-
|
|
131
|
+
**Mode 1** in brief: every operation is a Pydantic model whose fields *are* the JSON wire format, so `VideoEdit.json_schema()` hands your model a ready-made tool schema — a discriminated union over every LLM-exposed op (pass `strict=True` for provider grammar modes). Plans parse permissively and own their numeric bounds at validation, so a refine loop converges fast:
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
- **`edit.check(meta)`** — collect *every* structured error in one pass, not just the first
|
|
134
|
+
- **`edit.repair(meta)`** — auto-clamp mechanical violations (overruns, negatives) with a changelog
|
|
135
|
+
- **`edit.normalize_dimensions(meta, target)`** — make heterogeneous segments concat-compatible
|
|
134
136
|
|
|
135
|
-
See the [LLM Integration Guide](https://videopython.com/guides/llm-integration/) for end-to-end examples, the
|
|
137
|
+
See the [LLM Integration Guide](https://videopython.com/guides/llm-integration/) for end-to-end examples (Anthropic / OpenAI tool use), the refine loop, and operation discovery.
|
|
136
138
|
|
|
137
139
|
## Features
|
|
138
140
|
|
|
@@ -84,11 +84,13 @@ Putting an LLM in the loop works three ways:
|
|
|
84
84
|
2. **`AutoEditor`** — a local Ollama vision model is the planner (see [Automatic editing](#automatic-editing-local-llm) above).
|
|
85
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
86
|
|
|
87
|
-
|
|
87
|
+
**Mode 1** in brief: every operation is a Pydantic model whose fields *are* the JSON wire format, so `VideoEdit.json_schema()` hands your model a ready-made tool schema — a discriminated union over every LLM-exposed op (pass `strict=True` for provider grammar modes). Plans parse permissively and own their numeric bounds at validation, so a refine loop converges fast:
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
- **`edit.check(meta)`** — collect *every* structured error in one pass, not just the first
|
|
90
|
+
- **`edit.repair(meta)`** — auto-clamp mechanical violations (overruns, negatives) with a changelog
|
|
91
|
+
- **`edit.normalize_dimensions(meta, target)`** — make heterogeneous segments concat-compatible
|
|
90
92
|
|
|
91
|
-
See the [LLM Integration Guide](https://videopython.com/guides/llm-integration/) for end-to-end examples, the
|
|
93
|
+
See the [LLM Integration Guide](https://videopython.com/guides/llm-integration/) for end-to-end examples (Anthropic / OpenAI tool use), the refine loop, and operation discovery.
|
|
92
94
|
|
|
93
95
|
## Features
|
|
94
96
|
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Every public symbol is re-exported lazily via PEP 562 ``__getattr__`` so that
|
|
4
4
|
``import videopython.ai`` (or ``from videopython.ai import X``) loads ONLY the
|
|
5
|
-
leaf module backing the requested symbol — not every sibling.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
``[
|
|
10
|
-
|
|
5
|
+
leaf module backing the requested symbol — not every sibling. All AI capabilities
|
|
6
|
+
ship in the single ``[ai]`` extra; the laziness keeps ``import videopython`` (and
|
|
7
|
+
importing one leaf class) light by deferring the heavy ML imports (torch /
|
|
8
|
+
transformers / diffusers / ultralytics) until a symbol is actually used. When
|
|
9
|
+
``[ai]`` is not installed, touching a symbol raises a clear
|
|
10
|
+
``pip install 'videopython[ai]'``-pointing ``ImportError`` at attribute access
|
|
11
|
+
instead of failing the whole package import.
|
|
11
12
|
|
|
12
13
|
The ``TYPE_CHECKING`` block below keeps the eager imports visible to mypy and
|
|
13
14
|
IDEs (static autocompletion / re-export checking) without executing them at
|
|
@@ -28,13 +29,12 @@ if TYPE_CHECKING:
|
|
|
28
29
|
from .auto_edit import AutoEditor as AutoEditor
|
|
29
30
|
from .auto_edit import EditCatalog as EditCatalog
|
|
30
31
|
from .auto_edit import EditPlan as EditPlan
|
|
31
|
-
from .auto_edit import ImagePart as ImagePart
|
|
32
32
|
from .auto_edit import OllamaVisionLLM as OllamaVisionLLM
|
|
33
33
|
from .auto_edit import PlannerError as PlannerError
|
|
34
34
|
from .auto_edit import StructuredVisionLLM as StructuredVisionLLM
|
|
35
|
-
from .auto_edit import TextPart as TextPart
|
|
36
35
|
from .auto_edit import build_catalog as build_catalog
|
|
37
36
|
from .effects import ObjectDetectionOverlay as ObjectDetectionOverlay
|
|
37
|
+
from .errors import AiError as AiError
|
|
38
38
|
from .generation import ImageToVideo as ImageToVideo
|
|
39
39
|
from .generation import TextToImage as TextToImage
|
|
40
40
|
from .generation import TextToMusic as TextToMusic
|
|
@@ -43,7 +43,8 @@ if TYPE_CHECKING:
|
|
|
43
43
|
from .transforms import FaceTrackingCrop as FaceTrackingCrop
|
|
44
44
|
from .understanding import AudioClassifier as AudioClassifier
|
|
45
45
|
from .understanding import AudioToText as AudioToText
|
|
46
|
-
from .understanding import
|
|
46
|
+
from .understanding import FaceShotTracker as FaceShotTracker
|
|
47
|
+
from .understanding import FaceSmoothingTracker as FaceSmoothingTracker
|
|
47
48
|
from .understanding import ObjectDetector as ObjectDetector
|
|
48
49
|
from .understanding import SceneVLM as SceneVLM
|
|
49
50
|
from .understanding import SemanticSceneDetector as SemanticSceneDetector
|
|
@@ -64,7 +65,8 @@ _exports: dict[str, str] = {
|
|
|
64
65
|
# Understanding
|
|
65
66
|
"AudioToText": ".understanding",
|
|
66
67
|
"AudioClassifier": ".understanding",
|
|
67
|
-
"
|
|
68
|
+
"FaceShotTracker": ".understanding",
|
|
69
|
+
"FaceSmoothingTracker": ".understanding",
|
|
68
70
|
"ObjectDetector": ".understanding",
|
|
69
71
|
"SceneVLM": ".understanding",
|
|
70
72
|
"SemanticSceneDetector": ".understanding",
|
|
@@ -76,14 +78,14 @@ _exports: dict[str, str] = {
|
|
|
76
78
|
"VideoAnalysis": ".video_analysis",
|
|
77
79
|
"VideoAnalysisConfig": ".video_analysis",
|
|
78
80
|
"VideoAnalyzer": ".video_analysis",
|
|
81
|
+
# Errors
|
|
82
|
+
"AiError": ".errors",
|
|
79
83
|
# Auto-editing (LLM-authored edits)
|
|
80
84
|
"AutoEditor": ".auto_edit",
|
|
81
85
|
"AutoEditError": ".auto_edit",
|
|
82
86
|
"OllamaVisionLLM": ".auto_edit",
|
|
83
87
|
"StructuredVisionLLM": ".auto_edit",
|
|
84
88
|
"PlannerError": ".auto_edit",
|
|
85
|
-
"TextPart": ".auto_edit",
|
|
86
|
-
"ImagePart": ".auto_edit",
|
|
87
89
|
"EditCatalog": ".auto_edit",
|
|
88
90
|
"EditPlan": ".auto_edit",
|
|
89
91
|
"build_catalog": ".auto_edit",
|
|
@@ -2,19 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import base64
|
|
6
|
-
import io
|
|
7
5
|
import json
|
|
8
6
|
from typing import Any
|
|
9
7
|
|
|
10
|
-
import cv2
|
|
11
8
|
import numpy as np
|
|
12
|
-
from PIL import Image
|
|
13
9
|
|
|
14
10
|
from videopython.ai._optional import require
|
|
11
|
+
from videopython.ai.errors import AiError
|
|
12
|
+
from videopython.ai.keyframe import encode_png_b64
|
|
15
13
|
|
|
16
14
|
|
|
17
|
-
class OllamaError(RuntimeError):
|
|
15
|
+
class OllamaError(AiError, RuntimeError):
|
|
18
16
|
"""Ollama returned unusable output (non-JSON or an unexpected shape)."""
|
|
19
17
|
|
|
20
18
|
|
|
@@ -35,7 +33,7 @@ class OllamaStructuredClient:
|
|
|
35
33
|
|
|
36
34
|
def _get_client(self) -> Any:
|
|
37
35
|
if self._client is None:
|
|
38
|
-
ollama = require("ollama",
|
|
36
|
+
ollama = require("ollama", feature="Ollama")
|
|
39
37
|
self._client = ollama.Client(host=self.host)
|
|
40
38
|
return self._client
|
|
41
39
|
|
|
@@ -50,7 +48,7 @@ class OllamaStructuredClient:
|
|
|
50
48
|
"""Return the parsed JSON object Ollama generates under ``schema``."""
|
|
51
49
|
user: dict[str, Any] = {"role": "user", "content": text}
|
|
52
50
|
if images:
|
|
53
|
-
user["images"] = [
|
|
51
|
+
user["images"] = [encode_png_b64(image) for image in images]
|
|
54
52
|
messages = [{"role": "system", "content": system}, user]
|
|
55
53
|
response = self._get_client().chat(model=self.model, messages=messages, format=schema, options=self.options)
|
|
56
54
|
content = response.message.content
|
|
@@ -64,23 +62,3 @@ class OllamaStructuredClient:
|
|
|
64
62
|
|
|
65
63
|
def unload(self) -> None:
|
|
66
64
|
self._client = None
|
|
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
|
-
|
|
83
|
-
def _encode_png_b64(frame: np.ndarray) -> str:
|
|
84
|
-
buffer = io.BytesIO()
|
|
85
|
-
Image.fromarray(frame).save(buffer, format="PNG")
|
|
86
|
-
return base64.b64encode(buffer.getvalue()).decode("ascii")
|
|
@@ -6,12 +6,11 @@ method that needs it (there are no top-level ``import torch``/``transformers``/
|
|
|
6
6
|
importable on a slim install: importing ``ai.understanding.objects`` only needs
|
|
7
7
|
core deps until a detector is actually constructed.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
``pip install 'videopython[<extra>]'``.
|
|
9
|
+
All those heavy deps ship in the single ``[ai]`` extra. When it isn't installed
|
|
10
|
+
and a code path reaches one of them, the bare ``import`` would raise a stock
|
|
11
|
+
``ModuleNotFoundError`` with no hint about how to fix it. :func:`require` wraps
|
|
12
|
+
the import and turns that into an actionable ``ImportError`` pointing at
|
|
13
|
+
``pip install 'videopython[ai]'``.
|
|
15
14
|
"""
|
|
16
15
|
|
|
17
16
|
from __future__ import annotations
|
|
@@ -21,15 +20,13 @@ from types import ModuleType
|
|
|
21
20
|
from typing import Callable
|
|
22
21
|
|
|
23
22
|
|
|
24
|
-
def require(module: str,
|
|
25
|
-
"""Import ``module`` or raise a clear,
|
|
23
|
+
def require(module: str, *, feature: str | None = None) -> ModuleType:
|
|
24
|
+
"""Import ``module`` or raise a clear, ``[ai]``-pointing ``ImportError``.
|
|
26
25
|
|
|
27
26
|
Args:
|
|
28
27
|
module: Dotted module name to import (e.g. ``"chatterbox.mtl_tts"``,
|
|
29
28
|
``"torch"``). The fully-imported module object is returned so the
|
|
30
29
|
call site can bind the symbols it needs.
|
|
31
|
-
extra: The videopython extra that ships ``module`` (e.g. ``"tts"``,
|
|
32
|
-
``"asr"``). Surfaced verbatim in the install hint.
|
|
33
30
|
feature: Optional human-readable feature name for the message head. When
|
|
34
31
|
omitted the top-level package of ``module`` is used.
|
|
35
32
|
|
|
@@ -38,14 +35,14 @@ def require(module: str, extra: str, *, feature: str | None = None) -> ModuleTyp
|
|
|
38
35
|
|
|
39
36
|
Raises:
|
|
40
37
|
ImportError: If ``module`` cannot be imported. The message always
|
|
41
|
-
contains
|
|
42
|
-
|
|
38
|
+
contains a ``pip install 'videopython[ai]'`` hint so the caller
|
|
39
|
+
knows exactly how to fix it.
|
|
43
40
|
"""
|
|
44
41
|
try:
|
|
45
42
|
return importlib.import_module(module)
|
|
46
43
|
except ImportError as exc:
|
|
47
44
|
label = feature or module.split(".")[0]
|
|
48
|
-
raise ImportError(f"{label} requires the '
|
|
45
|
+
raise ImportError(f"{label} requires the 'ai' extra: pip install 'videopython[ai]'") from exc
|
|
49
46
|
|
|
50
47
|
|
|
51
48
|
def lazy_exports(package: str, exports: dict[str, str]) -> tuple[Callable[[str], object], Callable[[], list[str]]]:
|
|
@@ -53,8 +50,9 @@ def lazy_exports(package: str, exports: dict[str, str]) -> tuple[Callable[[str],
|
|
|
53
50
|
|
|
54
51
|
Re-exports a set of public symbols lazily: the submodule backing a symbol is
|
|
55
52
|
imported only on first attribute access, so importing the package does not
|
|
56
|
-
pull in any sibling leaf module
|
|
57
|
-
|
|
53
|
+
pull in any sibling leaf module. This keeps ``import videopython`` (and
|
|
54
|
+
importing a single leaf class) light by deferring the heavy ML imports
|
|
55
|
+
(torch / transformers / diffusers / ultralytics) until a symbol is used.
|
|
58
56
|
|
|
59
57
|
Args:
|
|
60
58
|
package: The ``__name__`` of the package defining the re-exports. Used
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Context-manager base with a default VRAM-releasing ``unload()``.
|
|
2
|
+
|
|
3
|
+
Every predictor under ``videopython.ai`` holds a lazily-loaded model (and
|
|
4
|
+
sometimes a processor / pipeline / VAD field) on the selected device, and needs
|
|
5
|
+
to drop those references and free the allocator cache when done. That teardown
|
|
6
|
+
is mechanical and identical across predictors: set the model field(s) to
|
|
7
|
+
``None`` and call :func:`videopython.ai._device.release_device_memory`.
|
|
8
|
+
|
|
9
|
+
:class:`ManagedPredictor` provides that as a default ``unload()`` driven by two
|
|
10
|
+
class attributes -- ``_model_attrs`` (the fields holding model state) and
|
|
11
|
+
``_device_attr`` (the field holding the device) -- and turns any predictor into
|
|
12
|
+
a context manager::
|
|
13
|
+
|
|
14
|
+
with SceneVLM(...) as vlm:
|
|
15
|
+
... # use vlm
|
|
16
|
+
# vlm.unload() has fired here, releasing VRAM
|
|
17
|
+
|
|
18
|
+
Subclasses with non-default model fields just declare ``_model_attrs``; those
|
|
19
|
+
whose teardown isn't "null the fields + release" (e.g. delegating to a client's
|
|
20
|
+
own ``unload()``) override ``unload()`` instead.
|
|
21
|
+
|
|
22
|
+
The base imports no torch / transformers / ultralytics (``release_device_memory``
|
|
23
|
+
defers its torch import), so it stays safe to mix into any predictor regardless
|
|
24
|
+
of how it is constructed.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from types import TracebackType
|
|
30
|
+
from typing import TYPE_CHECKING, Literal
|
|
31
|
+
|
|
32
|
+
from videopython.ai._device import release_device_memory
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from typing_extensions import Self
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ManagedPredictor:
|
|
39
|
+
"""Adds a default ``unload()`` plus ``with``-statement support.
|
|
40
|
+
|
|
41
|
+
``unload()`` clears each attribute named in ``_model_attrs`` to ``None`` and
|
|
42
|
+
releases the cache for the device named by ``_device_attr``. It is idempotent
|
|
43
|
+
(safe before the model is loaded and on repeated calls). ``__exit__`` always
|
|
44
|
+
returns ``False`` so exceptions inside the ``with`` block propagate --
|
|
45
|
+
``unload()`` runs on both the success and failure paths.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
# Attributes holding model state, cleared to None on unload. Override per
|
|
49
|
+
# predictor (e.g. ("_model", "_processor")).
|
|
50
|
+
_model_attrs: tuple[str, ...] = ("_model",)
|
|
51
|
+
# Attribute holding the resolved device passed to release_device_memory.
|
|
52
|
+
_device_attr: str = "device"
|
|
53
|
+
|
|
54
|
+
def unload(self) -> None:
|
|
55
|
+
"""Drop the model reference(s) and release device memory. Idempotent."""
|
|
56
|
+
for attr in self._model_attrs:
|
|
57
|
+
setattr(self, attr, None)
|
|
58
|
+
release_device_memory(getattr(self, self._device_attr, None))
|
|
59
|
+
|
|
60
|
+
def __enter__(self) -> Self:
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
def __exit__(
|
|
64
|
+
self,
|
|
65
|
+
exc_type: type[BaseException] | None,
|
|
66
|
+
exc: BaseException | None,
|
|
67
|
+
tb: TracebackType | None,
|
|
68
|
+
) -> Literal[False]:
|
|
69
|
+
self.unload()
|
|
70
|
+
return False
|
|
@@ -65,7 +65,7 @@ MODEL_REVISIONS: dict[str, str] = {
|
|
|
65
65
|
# Speaker diarization (ai/understanding/audio.py: PYANNOTE_DIARIZATION_MODEL).
|
|
66
66
|
# Gated repo (auto-approved); from_pretrained needs an accepted HF token.
|
|
67
67
|
"pyannote/speaker-diarization-community-1": "3533c8cf8e369892e6b79ff1bf80f7b0286a54ee",
|
|
68
|
-
# Audio event classifier (ai/understanding/
|
|
68
|
+
# Audio event classifier (ai/understanding/classification.py: AudioClassifier)
|
|
69
69
|
"MIT/ast-finetuned-audioset-10-10-0.4593": "f826b80d28226b62986cc218e5cec390b1096902",
|
|
70
70
|
# Face detection checkpoint (ai/understanding/faces.py: hf_hub_download)
|
|
71
71
|
"arnabdhar/YOLOv8-Face-Detection": "52fa54977207fa4f021de949b515fb19dcab4488",
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
"""LLM-authored editing: build a scene catalog and plan a VideoEdit from it.
|
|
1
|
+
"""LLM-authored editing: build a scene catalog and plan a VideoEdit from it.
|
|
2
|
+
|
|
3
|
+
Uses eager imports (unlike the leaf packages): it is a thin orchestration
|
|
4
|
+
package whose submodules load together, and it MUST eagerly import
|
|
5
|
+
``videopython.ai.ops`` so the AI editing ops are registered before any
|
|
6
|
+
``EditPlan.json_schema()`` call. Heavy ML deps it pulls in stay lazy.
|
|
7
|
+
"""
|
|
2
8
|
|
|
3
9
|
from __future__ import annotations
|
|
4
10
|
|
|
5
|
-
|
|
11
|
+
import videopython.ai.ops # noqa: F401 -- registers the AI ops (face_crop, object_detection_overlay)
|
|
12
|
+
|
|
13
|
+
from .backend import PlannerError, StructuredVisionLLM
|
|
6
14
|
from .catalog import build_catalog
|
|
7
15
|
from .editor import AutoEditError, AutoEditor
|
|
8
16
|
from .local import OllamaVisionLLM
|
|
@@ -16,13 +24,10 @@ __all__ = [
|
|
|
16
24
|
"CatalogScene",
|
|
17
25
|
"EditCatalog",
|
|
18
26
|
"EditPlan",
|
|
19
|
-
"ImagePart",
|
|
20
27
|
"OllamaVisionLLM",
|
|
21
|
-
"Part",
|
|
22
28
|
"PlanSegment",
|
|
23
29
|
"PlannerError",
|
|
24
30
|
"StructuredVisionLLM",
|
|
25
|
-
"TextPart",
|
|
26
31
|
"UnknownSceneIdsError",
|
|
27
32
|
"build_catalog",
|
|
28
33
|
"resolve_plan",
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""The SDK-free seam between the editor and a structured-vision model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
from videopython.ai.errors import AiError
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PlannerError(AiError, RuntimeError):
|
|
14
|
+
"""A backend produced unusable output; the editor retries (infra errors should propagate instead)."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@runtime_checkable
|
|
18
|
+
class StructuredVisionLLM(Protocol):
|
|
19
|
+
"""Returns schema-shaped JSON from a system prompt + text + optional keyframes.
|
|
20
|
+
|
|
21
|
+
The signature mirrors
|
|
22
|
+
:meth:`videopython.ai._ollama.OllamaStructuredClient.generate_json`, so any
|
|
23
|
+
structured-generation client satisfies it structurally. Implementations
|
|
24
|
+
raise :class:`PlannerError` on unusable output (the editor retries those);
|
|
25
|
+
infra errors should propagate so they are not silently retried.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def generate_json(
|
|
29
|
+
self, *, system: str, text: str, images: list[np.ndarray] | None, schema: dict[str, Any]
|
|
30
|
+
) -> dict[str, Any]: ...
|
|
@@ -8,12 +8,16 @@ from typing import TYPE_CHECKING, Any, Literal
|
|
|
8
8
|
|
|
9
9
|
from pydantic import ValidationError
|
|
10
10
|
|
|
11
|
-
from .
|
|
11
|
+
from videopython.ai.errors import AiError
|
|
12
|
+
|
|
13
|
+
from .backend import PlannerError, StructuredVisionLLM
|
|
12
14
|
from .catalog import build_catalog
|
|
13
15
|
from .models import CatalogBundle, CatalogScene, EditPlan
|
|
14
16
|
from .resolve import UnknownSceneIdsError, resolve_plan
|
|
15
17
|
|
|
16
18
|
if TYPE_CHECKING:
|
|
19
|
+
import numpy as np
|
|
20
|
+
|
|
17
21
|
from videopython.ai.video_analysis import VideoAnalysis, VideoAnalyzer
|
|
18
22
|
from videopython.base.video import VideoMetadata
|
|
19
23
|
from videopython.editing import VideoEdit
|
|
@@ -28,7 +32,7 @@ _SYSTEM_PROMPT = (
|
|
|
28
32
|
_NormalizeTarget = tuple[int, int] | Literal["first", "largest", "match"]
|
|
29
33
|
|
|
30
34
|
|
|
31
|
-
class AutoEditError(RuntimeError):
|
|
35
|
+
class AutoEditError(AiError, RuntimeError):
|
|
32
36
|
"""The planner could not produce a valid edit within the retry budget."""
|
|
33
37
|
|
|
34
38
|
|
|
@@ -61,13 +65,13 @@ class AutoEditor:
|
|
|
61
65
|
metadata = _metadata_by_source(analyses)
|
|
62
66
|
run_context = _merge_context(analyses, context)
|
|
63
67
|
schema = EditPlan.json_schema(strict=True)
|
|
64
|
-
|
|
68
|
+
base_text, images = _build_prompt(brief, bundle)
|
|
65
69
|
|
|
66
70
|
feedback: str | None = None
|
|
67
71
|
for _ in range(self.max_rounds):
|
|
68
|
-
|
|
72
|
+
text = base_text if feedback is None else f"{base_text}\n\n{feedback}"
|
|
69
73
|
try:
|
|
70
|
-
raw = self.planner.generate_json(system=_SYSTEM_PROMPT,
|
|
74
|
+
raw = self.planner.generate_json(system=_SYSTEM_PROMPT, text=text, images=images or None, schema=schema)
|
|
71
75
|
edit = resolve_plan(EditPlan.model_validate(raw), bundle.catalog)
|
|
72
76
|
except (PlannerError, ValidationError, UnknownSceneIdsError) as exc:
|
|
73
77
|
feedback = _shape_feedback(exc)
|
|
@@ -89,14 +93,16 @@ class AutoEditor:
|
|
|
89
93
|
return self._analyzer
|
|
90
94
|
|
|
91
95
|
|
|
92
|
-
def
|
|
93
|
-
|
|
96
|
+
def _build_prompt(brief: str, bundle: CatalogBundle) -> tuple[str, list[np.ndarray]]:
|
|
97
|
+
"""The planner prompt: scene-line text plus the keyframes, in catalog order."""
|
|
98
|
+
lines = [f"Brief: {brief}\n\nCandidate scenes:"]
|
|
99
|
+
images: list[np.ndarray] = []
|
|
94
100
|
for scene in bundle.catalog.scenes:
|
|
95
|
-
|
|
101
|
+
lines.append(_scene_line(scene))
|
|
96
102
|
frame = bundle.keyframes.get(scene.id)
|
|
97
103
|
if frame is not None:
|
|
98
|
-
|
|
99
|
-
return
|
|
104
|
+
images.append(frame)
|
|
105
|
+
return "\n\n".join(lines), images
|
|
100
106
|
|
|
101
107
|
|
|
102
108
|
def _scene_line(scene: CatalogScene) -> str:
|
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from typing import Any
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
6
|
|
|
7
7
|
from videopython.ai._ollama import OllamaError, OllamaStructuredClient
|
|
8
8
|
|
|
9
|
-
from .backend import
|
|
9
|
+
from .backend import PlannerError
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
import numpy as np
|
|
10
13
|
|
|
11
14
|
DEFAULT_OLLAMA_MODEL = "gemma3:27b"
|
|
12
15
|
|
|
@@ -19,6 +22,10 @@ class OllamaVisionLLM:
|
|
|
19
22
|
every model supports schema conditioning -- ``gemma3:27b`` is verified working;
|
|
20
23
|
some builds (e.g. certain MLX ones) fail it. ``ollama pull <model>`` first;
|
|
21
24
|
``options`` are extra generation options merged over ``temperature=0``.
|
|
25
|
+
|
|
26
|
+
Thin wrapper over the shared :class:`OllamaStructuredClient`: its only job is
|
|
27
|
+
to translate :class:`OllamaError` into the :class:`PlannerError` the editor
|
|
28
|
+
retries on.
|
|
22
29
|
"""
|
|
23
30
|
|
|
24
31
|
def __init__(
|
|
@@ -30,9 +37,9 @@ class OllamaVisionLLM:
|
|
|
30
37
|
) -> None:
|
|
31
38
|
self._client = OllamaStructuredClient(model=model, host=host, options=options)
|
|
32
39
|
|
|
33
|
-
def generate_json(
|
|
34
|
-
|
|
35
|
-
|
|
40
|
+
def generate_json(
|
|
41
|
+
self, *, system: str, text: str, images: list[np.ndarray] | None, schema: dict[str, Any]
|
|
42
|
+
) -> dict[str, Any]:
|
|
36
43
|
try:
|
|
37
44
|
return self._client.generate_json(system=system, text=text, schema=schema, images=images or None)
|
|
38
45
|
except OllamaError as exc:
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from videopython.ai.errors import AiError
|
|
5
6
|
from videopython.editing import SegmentConfig, VideoEdit
|
|
6
7
|
|
|
7
8
|
from .models import EditCatalog, EditPlan
|
|
8
9
|
|
|
9
10
|
|
|
10
|
-
class UnknownSceneIdsError(ValueError):
|
|
11
|
+
class UnknownSceneIdsError(AiError, ValueError):
|
|
11
12
|
"""An EditPlan referenced scene ids absent from the catalog."""
|
|
12
13
|
|
|
13
14
|
def __init__(self, ids: list[str]) -> None:
|
|
@@ -29,7 +29,6 @@ if TYPE_CHECKING:
|
|
|
29
29
|
from videopython.ai.dubbing.quality import TranscriptQuality as TranscriptQuality
|
|
30
30
|
from videopython.ai.dubbing.quality import assess_transcript as assess_transcript
|
|
31
31
|
from videopython.ai.dubbing.timing import TimingSynchronizer as TimingSynchronizer
|
|
32
|
-
from videopython.ai.generation.translation import UnsupportedLanguageError as UnsupportedLanguageError
|
|
33
32
|
|
|
34
33
|
# Public symbol -> fully-qualified module that defines it.
|
|
35
34
|
_exports: dict[str, str] = {
|
|
@@ -45,7 +44,6 @@ _exports: dict[str, str] = {
|
|
|
45
44
|
"GarbageTranscriptError": "videopython.ai.dubbing.quality",
|
|
46
45
|
"TranscriptQuality": "videopython.ai.dubbing.quality",
|
|
47
46
|
"assess_transcript": "videopython.ai.dubbing.quality",
|
|
48
|
-
"UnsupportedLanguageError": "videopython.ai.generation.translation",
|
|
49
47
|
}
|
|
50
48
|
|
|
51
49
|
__all__ = list(_exports)
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
"""Pluggable speech-synthesis backend protocol for the dubbing pipeline.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
dependency-free, ``runtime_checkable`` Protocol that the dubbing pipeline
|
|
3
|
+
A dependency-free, ``runtime_checkable`` Protocol that the dubbing pipeline
|
|
5
4
|
depends on instead of binding directly to the local
|
|
6
5
|
:class:`videopython.ai.generation.audio.TextToSpeech` implementation.
|
|
7
6
|
|
|
8
|
-
This is the seam that lets
|
|
9
|
-
local ``TextToSpeech`` (which pulls ``chatterbox-tts``
|
|
7
|
+
This is the seam that lets a consumer run speech synthesis out-of-process. The
|
|
8
|
+
local ``TextToSpeech`` (which pulls ``chatterbox-tts`` from the ``[ai]`` extra)
|
|
10
9
|
satisfies this protocol structurally — no changes needed there. A consumer that
|
|
11
|
-
can't or won't
|
|
10
|
+
can't or won't load chatterbox in-process (e.g. a service running synthesis in a
|
|
12
11
|
separate process or on a remote/Modal function) supplies its own object
|
|
13
|
-
implementing :meth:`SpeechBackend.generate_audio` and injects it; the pipeline
|
|
14
|
-
imports chatterbox in that case. videopython ships ONLY this protocol plus
|
|
15
|
-
local backend — no reference remote/HTTP backend.
|
|
12
|
+
implementing :meth:`SpeechBackend.generate_audio` and injects it; the pipeline
|
|
13
|
+
never imports chatterbox in that case. videopython ships ONLY this protocol plus
|
|
14
|
+
the local backend — no reference remote/HTTP backend.
|
|
16
15
|
"""
|
|
17
16
|
|
|
18
17
|
from __future__ import annotations
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Audio DSP helpers for the dubbing pipeline.
|
|
2
|
+
|
|
3
|
+
Merges the former ``expressiveness`` and ``loudness`` leaf modules: RMS,
|
|
4
|
+
source-prosody -> Chatterbox expressiveness mapping, and LUFS/peak loudness
|
|
5
|
+
matching. These are the small numpy/DSP helpers the pipeline composes; keeping
|
|
6
|
+
them in one file (rather than two ~50-line files) makes the audio-side toolkit
|
|
7
|
+
legible.
|
|
8
|
+
"""
|
|
2
9
|
|
|
3
10
|
from __future__ import annotations
|
|
4
11
|
|
|
@@ -6,10 +13,48 @@ from typing import TYPE_CHECKING
|
|
|
6
13
|
|
|
7
14
|
import numpy as np
|
|
8
15
|
|
|
16
|
+
from videopython.ai.dubbing.models import Expressiveness
|
|
17
|
+
|
|
9
18
|
if TYPE_CHECKING:
|
|
10
19
|
from videopython.audio import Audio
|
|
11
20
|
|
|
12
21
|
|
|
22
|
+
def rms(data: np.ndarray) -> float:
|
|
23
|
+
"""RMS over samples; ``0.0`` for empty input. float64 reduction so a
|
|
24
|
+
long slice can't overflow the squared accumulator."""
|
|
25
|
+
if data.size == 0:
|
|
26
|
+
return 0.0
|
|
27
|
+
return float(np.sqrt(np.mean(np.square(data, dtype=np.float64))))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# --- Source-prosody-driven expressiveness (Chatterbox TTS knobs) -----------
|
|
31
|
+
# Source-segment RMS / whole-vocals RMS below CALM lands in the calm bucket;
|
|
32
|
+
# above DRAMATIC in the dramatic bucket; in between gets Chatterbox's defaults.
|
|
33
|
+
# Knob values picked by-ear on cam1_1min.mp4 -- see RELEASE_NOTES 0.29.0.
|
|
34
|
+
CALM_RATIO_THRESHOLD = 0.7
|
|
35
|
+
DRAMATIC_RATIO_THRESHOLD = 1.3
|
|
36
|
+
_CALM = Expressiveness(exaggeration=0.3, cfg_weight=0.7)
|
|
37
|
+
_DRAMATIC = Expressiveness(exaggeration=0.85, cfg_weight=0.35)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def expressiveness_for(source_slice: Audio, baseline_rms: float) -> Expressiveness:
|
|
41
|
+
"""Map a source vocals slice to a Chatterbox expressiveness profile
|
|
42
|
+
by RMS ratio. Falls back to the no-knobs default for empty or silent
|
|
43
|
+
inputs."""
|
|
44
|
+
if baseline_rms <= 0.0:
|
|
45
|
+
return Expressiveness()
|
|
46
|
+
segment_rms = rms(source_slice.data)
|
|
47
|
+
if segment_rms <= 0.0:
|
|
48
|
+
return Expressiveness()
|
|
49
|
+
ratio = segment_rms / baseline_rms
|
|
50
|
+
if ratio < CALM_RATIO_THRESHOLD:
|
|
51
|
+
return _CALM
|
|
52
|
+
if ratio > DRAMATIC_RATIO_THRESHOLD:
|
|
53
|
+
return _DRAMATIC
|
|
54
|
+
return Expressiveness()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# --- LUFS / peak loudness matching -----------------------------------------
|
|
13
58
|
# BS.1770 integrated-loudness measurement requires at least 400 ms of audio
|
|
14
59
|
# (one gating block). Below this, fall back to peak match -- pyloudnorm
|
|
15
60
|
# returns -inf or warns, neither of which gives a usable gain.
|
|
@@ -64,7 +109,7 @@ def loudness_match(target: Audio, reference: Audio) -> Audio:
|
|
|
64
109
|
|
|
65
110
|
from videopython.ai._optional import require
|
|
66
111
|
|
|
67
|
-
pyloudnorm = require("pyloudnorm",
|
|
112
|
+
pyloudnorm = require("pyloudnorm", feature="loudness matching")
|
|
68
113
|
|
|
69
114
|
target_lufs = pyloudnorm.Meter(target.metadata.sample_rate).integrated_loudness(target.data)
|
|
70
115
|
reference_lufs = pyloudnorm.Meter(reference.metadata.sample_rate).integrated_loudness(reference.data)
|