videopython 0.38.0__tar.gz → 0.40.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.38.0 → videopython-0.40.0}/PKG-INFO +1 -1
- {videopython-0.38.0 → videopython-0.40.0}/pyproject.toml +1 -1
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/effects.py +2 -2
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/exceptions.py +6 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/editing/__init__.py +5 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/editing/effects.py +19 -19
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/editing/operation.py +14 -7
- videopython-0.40.0/src/videopython/editing/streaming.py +489 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/editing/transcription_overlay.py +74 -35
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/editing/video_edit.py +150 -17
- videopython-0.38.0/src/videopython/editing/streaming.py +0 -275
- {videopython-0.38.0 → videopython-0.40.0}/.gitignore +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/LICENSE +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/README.md +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/__init__.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/__init__.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/_device.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/dubbing/__init__.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/dubbing/config.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/dubbing/dubber.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/dubbing/expressiveness.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/dubbing/loudness.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/dubbing/models.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/dubbing/pipeline.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/dubbing/quality.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/dubbing/remux.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/dubbing/timing.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/dubbing/voice_sample.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/generation/__init__.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/generation/audio.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/generation/image.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/generation/qwen3.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/generation/translation.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/generation/video.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/transforms.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/understanding/__init__.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/understanding/audio.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/understanding/faces.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/understanding/image.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/understanding/objects.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/understanding/separation.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/understanding/temporal.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/video_analysis/__init__.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/video_analysis/analyzer.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/video_analysis/models.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/video_analysis/sampling.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/video_analysis/stages.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/audio/__init__.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/audio/analysis.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/audio/audio.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/__init__.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/_dimensions.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/_ffmpeg.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/_video_io.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/description.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/draw_detections.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/fonts/Anton-OFL.txt +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/fonts/Anton-Regular.ttf +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/fonts/BebasNeue-OFL.txt +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/fonts/BebasNeue-Regular.ttf +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/fonts/DejaVuSans.ttf +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/fonts/LICENSE_DEJAVU +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/fonts/Lato-Bold.ttf +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/fonts/Lato-OFL.txt +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/fonts/Poppins-Bold.ttf +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/fonts/Poppins-OFL.txt +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/fonts/__init__.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/image_text.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/transcription.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/video.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/editing/_easing.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/editing/transforms.py +0 -0
- {videopython-0.38.0 → videopython-0.40.0}/src/videopython/py.typed +0 -0
|
@@ -9,7 +9,7 @@ AI-free renderer in :mod:`videopython.base.draw_detections`.
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
-
from typing import ClassVar, Literal
|
|
12
|
+
from typing import Any, ClassVar, Literal
|
|
13
13
|
|
|
14
14
|
import numpy as np
|
|
15
15
|
from pydantic import Field, PrivateAttr
|
|
@@ -97,7 +97,7 @@ class ObjectDetectionOverlay(Effect):
|
|
|
97
97
|
backend=self.backend,
|
|
98
98
|
)
|
|
99
99
|
|
|
100
|
-
def streaming_init(self, total_frames: int, fps: float, width: int, height: int) -> None:
|
|
100
|
+
def streaming_init(self, total_frames: int, fps: float, width: int, height: int, **_context: Any) -> None:
|
|
101
101
|
self._last = []
|
|
102
102
|
self._init_detector()
|
|
103
103
|
|
|
@@ -105,6 +105,8 @@ class PlanErrorCode(str, Enum):
|
|
|
105
105
|
CONCAT_MISMATCH = "concat_mismatch"
|
|
106
106
|
SUBTITLE_UNFITTABLE = "subtitle_unfittable"
|
|
107
107
|
POST_OP_REQUIRES_CONTEXT = "post_op_requires_context"
|
|
108
|
+
# Streaming strictness (opt-in via strict_streaming).
|
|
109
|
+
STREAMING_FALLBACK = "streaming_fallback"
|
|
108
110
|
|
|
109
111
|
|
|
110
112
|
@dataclass
|
|
@@ -113,6 +115,9 @@ class PlanError:
|
|
|
113
115
|
|
|
114
116
|
``location`` is a path into the plan (e.g. ``'segments[1].operations[0]'``);
|
|
115
117
|
the remaining fields are populated when meaningful for the ``code``.
|
|
118
|
+
``detail`` carries a short human-readable cause when the code alone is not
|
|
119
|
+
actionable (e.g. *why* an op forces the eager fallback) -- prose meant for
|
|
120
|
+
LLM refine-loop feedback, not for branching.
|
|
116
121
|
"""
|
|
117
122
|
|
|
118
123
|
code: PlanErrorCode
|
|
@@ -122,6 +127,7 @@ class PlanError:
|
|
|
122
127
|
value: float | None = None
|
|
123
128
|
limit: float | None = None
|
|
124
129
|
predicted_duration: float | None = None
|
|
130
|
+
detail: str | None = None
|
|
125
131
|
|
|
126
132
|
|
|
127
133
|
@dataclass
|
|
@@ -22,6 +22,7 @@ from .effects import (
|
|
|
22
22
|
Zoom,
|
|
23
23
|
)
|
|
24
24
|
from .operation import FilterCtx, OpCategory, Operation, TimeRange
|
|
25
|
+
from .streaming import OpStreamability, StreamabilityReport, StreamingClass
|
|
25
26
|
from .transcription_overlay import SubtitleRegion, SubtitleStyle, TranscriptionOverlay
|
|
26
27
|
from .transforms import (
|
|
27
28
|
Crop,
|
|
@@ -82,4 +83,8 @@ __all__ = [
|
|
|
82
83
|
# Plan runner
|
|
83
84
|
"VideoEdit",
|
|
84
85
|
"SegmentConfig",
|
|
86
|
+
# Streamability report
|
|
87
|
+
"StreamabilityReport",
|
|
88
|
+
"OpStreamability",
|
|
89
|
+
"StreamingClass",
|
|
85
90
|
]
|
|
@@ -109,7 +109,7 @@ class FullImageOverlay(Effect):
|
|
|
109
109
|
img_pil.paste(overlay_pil, (0, 0), overlay_pil)
|
|
110
110
|
return np.array(img_pil)
|
|
111
111
|
|
|
112
|
-
def streaming_init(self, total_frames: int, fps: float, width: int, height: int) -> None:
|
|
112
|
+
def streaming_init(self, total_frames: int, fps: float, width: int, height: int, **_context: Any) -> None:
|
|
113
113
|
self._load_overlay()
|
|
114
114
|
self._stream_total = total_frames
|
|
115
115
|
self._stream_fade_frames = round(self.fade_time * fps) if self.fade_time > 0 else 0
|
|
@@ -121,7 +121,7 @@ class FullImageOverlay(Effect):
|
|
|
121
121
|
fade_alpha = 1.0 if dist_from_end >= self._stream_fade_frames else dist_from_end / self._stream_fade_frames
|
|
122
122
|
return self._overlay_frame(frame, fade_alpha)
|
|
123
123
|
|
|
124
|
-
def _apply(self, video: Video) -> Video:
|
|
124
|
+
def _apply(self, video: Video, **_context: Any) -> Video:
|
|
125
125
|
overlay = self._load_overlay()
|
|
126
126
|
if video.frame_shape != overlay[:, :, :3].shape:
|
|
127
127
|
raise ValueError(f"Mismatch of overlay shape `{overlay.shape}` with video shape: `{video.frame_shape}`!")
|
|
@@ -181,7 +181,7 @@ class Blur(Effect):
|
|
|
181
181
|
ratios = np.linspace(1.0, 1 / n_frames, n_frames)
|
|
182
182
|
return base_sigma * np.sqrt(np.maximum(1, np.round(ratios * self.iterations)))
|
|
183
183
|
|
|
184
|
-
def streaming_init(self, total_frames: int, fps: float, width: int, height: int) -> None:
|
|
184
|
+
def streaming_init(self, total_frames: int, fps: float, width: int, height: int, **_context: Any) -> None:
|
|
185
185
|
self._stream_sigmas = self._compute_sigmas(total_frames)
|
|
186
186
|
|
|
187
187
|
def process_frame(self, frame: np.ndarray, frame_index: int) -> np.ndarray:
|
|
@@ -215,7 +215,7 @@ class Zoom(Effect):
|
|
|
215
215
|
crop_w, crop_h = crop_w[::-1], crop_h[::-1]
|
|
216
216
|
return np.stack([crop_w, crop_h], axis=1)
|
|
217
217
|
|
|
218
|
-
def streaming_init(self, total_frames: int, fps: float, width: int, height: int) -> None:
|
|
218
|
+
def streaming_init(self, total_frames: int, fps: float, width: int, height: int, **_context: Any) -> None:
|
|
219
219
|
self._stream_crops = self._crop_sizes(total_frames, width, height)
|
|
220
220
|
self._stream_width = width
|
|
221
221
|
self._stream_height = height
|
|
@@ -317,7 +317,7 @@ class Vignette(Effect):
|
|
|
317
317
|
mask = 1.0 - np.clip(distance - 0.5, 0, 1) * 2 * self.strength
|
|
318
318
|
return mask.astype(np.float32)
|
|
319
319
|
|
|
320
|
-
def streaming_init(self, total_frames: int, fps: float, width: int, height: int) -> None:
|
|
320
|
+
def streaming_init(self, total_frames: int, fps: float, width: int, height: int, **_context: Any) -> None:
|
|
321
321
|
if self._mask is None or self._mask.shape != (height, width):
|
|
322
322
|
self._mask = self._create_mask(height, width)
|
|
323
323
|
self._stream_mask_3d = self._mask[:, :, np.newaxis]
|
|
@@ -326,7 +326,7 @@ class Vignette(Effect):
|
|
|
326
326
|
assert self._stream_mask_3d is not None
|
|
327
327
|
return (frame.astype(np.float32) * self._stream_mask_3d).astype(np.uint8)
|
|
328
328
|
|
|
329
|
-
def _apply(self, video: Video) -> Video:
|
|
329
|
+
def _apply(self, video: Video, **_context: Any) -> Video:
|
|
330
330
|
logger.info("Applying vignette effect...")
|
|
331
331
|
height, width = video.frame_shape[:2]
|
|
332
332
|
if self._mask is None or self._mask.shape != (height, width):
|
|
@@ -404,7 +404,7 @@ class KenBurns(Effect):
|
|
|
404
404
|
regions[i] = (x, y, crop_w, crop_h)
|
|
405
405
|
return regions
|
|
406
406
|
|
|
407
|
-
def streaming_init(self, total_frames: int, fps: float, width: int, height: int) -> None:
|
|
407
|
+
def streaming_init(self, total_frames: int, fps: float, width: int, height: int, **_context: Any) -> None:
|
|
408
408
|
self._stream_regions = self._precompute_regions(total_frames, width, height)
|
|
409
409
|
self._stream_target_w = width
|
|
410
410
|
self._stream_target_h = height
|
|
@@ -461,7 +461,7 @@ class Fade(Effect):
|
|
|
461
461
|
alpha[-ramp:] = np.minimum(alpha[-ramp:], _compute_curve(t, self.curve))
|
|
462
462
|
return alpha
|
|
463
463
|
|
|
464
|
-
def streaming_init(self, total_frames: int, fps: float, width: int, height: int) -> None:
|
|
464
|
+
def streaming_init(self, total_frames: int, fps: float, width: int, height: int, **_context: Any) -> None:
|
|
465
465
|
self._stream_alpha = self._fade_envelope(total_frames, fps)
|
|
466
466
|
|
|
467
467
|
def process_frame(self, frame: np.ndarray, frame_index: int) -> np.ndarray:
|
|
@@ -649,7 +649,7 @@ class _AnchoredOverlay(Effect):
|
|
|
649
649
|
rgb = region[:, :, :3].astype(np.float32)
|
|
650
650
|
return alpha, rgb, (dst_y, dst_x, paste_h, paste_w)
|
|
651
651
|
|
|
652
|
-
def streaming_init(self, total_frames: int, fps: float, width: int, height: int) -> None:
|
|
652
|
+
def streaming_init(self, total_frames: int, fps: float, width: int, height: int, **_context: Any) -> None:
|
|
653
653
|
params = self._blend_params(width, height)
|
|
654
654
|
if params is None:
|
|
655
655
|
self._stream_noop = True
|
|
@@ -966,7 +966,7 @@ class Shake(Effect):
|
|
|
966
966
|
M = np.array([[1.0, 0.0, dx], [0.0, 1.0, dy]], dtype=np.float32)
|
|
967
967
|
return cv2.warpAffine(frame, M, (w, h), borderMode=cv2.BORDER_REFLECT)
|
|
968
968
|
|
|
969
|
-
def streaming_init(self, total_frames: int, fps: float, width: int, height: int) -> None:
|
|
969
|
+
def streaming_init(self, total_frames: int, fps: float, width: int, height: int, **_context: Any) -> None:
|
|
970
970
|
self._stream_offsets = self._compute_offsets(total_frames, fps)
|
|
971
971
|
|
|
972
972
|
def process_frame(self, frame: np.ndarray, frame_index: int) -> np.ndarray:
|
|
@@ -1028,7 +1028,7 @@ class PunchIn(Effect):
|
|
|
1028
1028
|
cropped = frame[y : y + crop_h, x : x + crop_w]
|
|
1029
1029
|
return cv2.resize(cropped, (width, height), interpolation=cv2.INTER_LINEAR)
|
|
1030
1030
|
|
|
1031
|
-
def streaming_init(self, total_frames: int, fps: float, width: int, height: int) -> None:
|
|
1031
|
+
def streaming_init(self, total_frames: int, fps: float, width: int, height: int, **_context: Any) -> None:
|
|
1032
1032
|
self._stream_zooms = self._zoom_envelope(total_frames)
|
|
1033
1033
|
self._stream_width = width
|
|
1034
1034
|
self._stream_height = height
|
|
@@ -1091,7 +1091,7 @@ class Flash(Effect):
|
|
|
1091
1091
|
alpha[:] = self.peak_alpha
|
|
1092
1092
|
return alpha
|
|
1093
1093
|
|
|
1094
|
-
def streaming_init(self, total_frames: int, fps: float, width: int, height: int) -> None:
|
|
1094
|
+
def streaming_init(self, total_frames: int, fps: float, width: int, height: int, **_context: Any) -> None:
|
|
1095
1095
|
self._stream_alpha = self._alpha_envelope(total_frames)
|
|
1096
1096
|
self._stream_color = np.array(self.color, dtype=np.float32)
|
|
1097
1097
|
|
|
@@ -1163,7 +1163,7 @@ class ChromaticAberration(Effect):
|
|
|
1163
1163
|
out[:, :, 2] = cv2.remap(b, b_map_x, b_map_y, cv2.INTER_LINEAR, borderMode=cv2.BORDER_REPLICATE)
|
|
1164
1164
|
return out
|
|
1165
1165
|
|
|
1166
|
-
def streaming_init(self, total_frames: int, fps: float, width: int, height: int) -> None:
|
|
1166
|
+
def streaming_init(self, total_frames: int, fps: float, width: int, height: int, **_context: Any) -> None:
|
|
1167
1167
|
if self.mode == "radial":
|
|
1168
1168
|
self._stream_maps = self._build_radial_maps(width, height)
|
|
1169
1169
|
|
|
@@ -1226,7 +1226,7 @@ class Glitch(Effect):
|
|
|
1226
1226
|
|
|
1227
1227
|
return out
|
|
1228
1228
|
|
|
1229
|
-
def streaming_init(self, total_frames: int, fps: float, width: int, height: int) -> None:
|
|
1229
|
+
def streaming_init(self, total_frames: int, fps: float, width: int, height: int, **_context: Any) -> None:
|
|
1230
1230
|
return None
|
|
1231
1231
|
|
|
1232
1232
|
def process_frame(self, frame: np.ndarray, frame_index: int) -> np.ndarray:
|
|
@@ -1266,7 +1266,7 @@ class FilmGrain(Effect):
|
|
|
1266
1266
|
noise = rng.standard_normal((h, w, 3), dtype=np.float32) * amp
|
|
1267
1267
|
return np.clip(frame.astype(np.float32) + noise, 0, 255).astype(np.uint8)
|
|
1268
1268
|
|
|
1269
|
-
def streaming_init(self, total_frames: int, fps: float, width: int, height: int) -> None:
|
|
1269
|
+
def streaming_init(self, total_frames: int, fps: float, width: int, height: int, **_context: Any) -> None:
|
|
1270
1270
|
return None
|
|
1271
1271
|
|
|
1272
1272
|
def process_frame(self, frame: np.ndarray, frame_index: int) -> np.ndarray:
|
|
@@ -1308,7 +1308,7 @@ class Sharpen(Effect):
|
|
|
1308
1308
|
sharpened = cv2.addWeighted(frame, 1.0 + self.amount, blurred, -self.amount, 0)
|
|
1309
1309
|
return sharpened
|
|
1310
1310
|
|
|
1311
|
-
def streaming_init(self, total_frames: int, fps: float, width: int, height: int) -> None:
|
|
1311
|
+
def streaming_init(self, total_frames: int, fps: float, width: int, height: int, **_context: Any) -> None:
|
|
1312
1312
|
return None
|
|
1313
1313
|
|
|
1314
1314
|
def process_frame(self, frame: np.ndarray, frame_index: int) -> np.ndarray:
|
|
@@ -1355,7 +1355,7 @@ class Pixelate(Effect):
|
|
|
1355
1355
|
frame[y : y + h, x : x + w] = big
|
|
1356
1356
|
return frame
|
|
1357
1357
|
|
|
1358
|
-
def streaming_init(self, total_frames: int, fps: float, width: int, height: int) -> None:
|
|
1358
|
+
def streaming_init(self, total_frames: int, fps: float, width: int, height: int, **_context: Any) -> None:
|
|
1359
1359
|
self._stream_region_px = self._resolve_region(width, height)
|
|
1360
1360
|
|
|
1361
1361
|
def process_frame(self, frame: np.ndarray, frame_index: int) -> np.ndarray:
|
|
@@ -1410,7 +1410,7 @@ class MirrorFlip(Effect):
|
|
|
1410
1410
|
out[:half, :] = out[h - half :, :][::-1, :]
|
|
1411
1411
|
return out
|
|
1412
1412
|
|
|
1413
|
-
def streaming_init(self, total_frames: int, fps: float, width: int, height: int) -> None:
|
|
1413
|
+
def streaming_init(self, total_frames: int, fps: float, width: int, height: int, **_context: Any) -> None:
|
|
1414
1414
|
return None
|
|
1415
1415
|
|
|
1416
1416
|
def process_frame(self, frame: np.ndarray, frame_index: int) -> np.ndarray:
|
|
@@ -1470,7 +1470,7 @@ class Kaleidoscope(Effect):
|
|
|
1470
1470
|
borderMode=cv2.BORDER_REFLECT,
|
|
1471
1471
|
)
|
|
1472
1472
|
|
|
1473
|
-
def streaming_init(self, total_frames: int, fps: float, width: int, height: int) -> None:
|
|
1473
|
+
def streaming_init(self, total_frames: int, fps: float, width: int, height: int, **_context: Any) -> None:
|
|
1474
1474
|
self._stream_map_x, self._stream_map_y = self._build_maps(width, height)
|
|
1475
1475
|
|
|
1476
1476
|
def process_frame(self, frame: np.ndarray, frame_index: int) -> np.ndarray:
|
|
@@ -397,12 +397,12 @@ class Effect(Operation):
|
|
|
397
397
|
original_shape = video.video_shape
|
|
398
398
|
|
|
399
399
|
if self.window is None or (self.window.start is None and self.window.stop is None):
|
|
400
|
-
result = self._apply(video)
|
|
400
|
+
result = self._apply(video, **context)
|
|
401
401
|
else:
|
|
402
402
|
start_s, stop_s = self._resolved_window(video.total_seconds)
|
|
403
403
|
start_f = round(start_s * video.fps)
|
|
404
404
|
end_f = round(stop_s * video.fps)
|
|
405
|
-
inner = self._apply(video[start_f:end_f])
|
|
405
|
+
inner = self._apply(video[start_f:end_f], **context)
|
|
406
406
|
old_audio = video.audio
|
|
407
407
|
result = _Video.from_frames(
|
|
408
408
|
np.r_["0,2", video.frames[:start_f], inner.frames, video.frames[end_f:]],
|
|
@@ -436,23 +436,30 @@ class Effect(Operation):
|
|
|
436
436
|
raise ValueError(f"Effect stop ({stop_s}) must be >= start ({start_s})")
|
|
437
437
|
return start_s, stop_s
|
|
438
438
|
|
|
439
|
-
def _apply(self, video: Video) -> Video:
|
|
439
|
+
def _apply(self, video: Video, **context: Any) -> Video:
|
|
440
440
|
"""Apply the effect to ``video`` in memory by replaying the streaming path.
|
|
441
441
|
|
|
442
442
|
Runs :meth:`streaming_init` once, then :meth:`process_frame` over every
|
|
443
443
|
frame in order -- the same logic streaming uses, so eager and streaming
|
|
444
|
-
cannot drift.
|
|
445
|
-
|
|
444
|
+
cannot drift. ``context`` carries resolved ``requires`` values through
|
|
445
|
+
to :meth:`streaming_init`, mirroring the streaming scheduler.
|
|
446
|
+
Subclasses that need a genuinely different eager path (extra
|
|
447
|
+
validation, batched vectorisation) override this.
|
|
446
448
|
"""
|
|
447
449
|
height, width = video.frame_shape[:2]
|
|
448
|
-
self.streaming_init(len(video.frames), video.fps, width, height)
|
|
450
|
+
self.streaming_init(len(video.frames), video.fps, width, height, **context)
|
|
449
451
|
for i in tqdm(range(len(video.frames)), desc=type(self).__name__):
|
|
450
452
|
video.frames[i] = self.process_frame(video.frames[i], i)
|
|
451
453
|
return video
|
|
452
454
|
|
|
453
|
-
def streaming_init(self, total_frames: int, fps: float, width: int, height: int) -> None:
|
|
455
|
+
def streaming_init(self, total_frames: int, fps: float, width: int, height: int, **_context: Any) -> None:
|
|
454
456
|
"""Hook for per-stream precomputation (per-frame alphas, sigma curves...).
|
|
455
457
|
|
|
458
|
+
``_context`` carries resolved ``requires`` values for context-aware
|
|
459
|
+
effects (e.g. ``transcription=...`` for ``TranscriptionOverlay``),
|
|
460
|
+
already re-based onto the local timeline by the runner. Effects that
|
|
461
|
+
declare no ``requires`` are always called without context kwargs.
|
|
462
|
+
|
|
456
463
|
Default: no-op. Override in subclasses that need it.
|
|
457
464
|
"""
|
|
458
465
|
|