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.
Files changed (73) hide show
  1. {videopython-0.38.0 → videopython-0.40.0}/PKG-INFO +1 -1
  2. {videopython-0.38.0 → videopython-0.40.0}/pyproject.toml +1 -1
  3. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/effects.py +2 -2
  4. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/exceptions.py +6 -0
  5. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/editing/__init__.py +5 -0
  6. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/editing/effects.py +19 -19
  7. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/editing/operation.py +14 -7
  8. videopython-0.40.0/src/videopython/editing/streaming.py +489 -0
  9. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/editing/transcription_overlay.py +74 -35
  10. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/editing/video_edit.py +150 -17
  11. videopython-0.38.0/src/videopython/editing/streaming.py +0 -275
  12. {videopython-0.38.0 → videopython-0.40.0}/.gitignore +0 -0
  13. {videopython-0.38.0 → videopython-0.40.0}/LICENSE +0 -0
  14. {videopython-0.38.0 → videopython-0.40.0}/README.md +0 -0
  15. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/__init__.py +0 -0
  16. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/__init__.py +0 -0
  17. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/_device.py +0 -0
  18. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/dubbing/__init__.py +0 -0
  19. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/dubbing/config.py +0 -0
  20. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/dubbing/dubber.py +0 -0
  21. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/dubbing/expressiveness.py +0 -0
  22. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/dubbing/loudness.py +0 -0
  23. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/dubbing/models.py +0 -0
  24. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/dubbing/pipeline.py +0 -0
  25. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/dubbing/quality.py +0 -0
  26. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/dubbing/remux.py +0 -0
  27. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/dubbing/timing.py +0 -0
  28. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/dubbing/voice_sample.py +0 -0
  29. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/generation/__init__.py +0 -0
  30. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/generation/audio.py +0 -0
  31. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/generation/image.py +0 -0
  32. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/generation/qwen3.py +0 -0
  33. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/generation/translation.py +0 -0
  34. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/generation/video.py +0 -0
  35. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/transforms.py +0 -0
  36. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/understanding/__init__.py +0 -0
  37. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/understanding/audio.py +0 -0
  38. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/understanding/faces.py +0 -0
  39. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/understanding/image.py +0 -0
  40. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/understanding/objects.py +0 -0
  41. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/understanding/separation.py +0 -0
  42. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/understanding/temporal.py +0 -0
  43. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/video_analysis/__init__.py +0 -0
  44. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/video_analysis/analyzer.py +0 -0
  45. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/video_analysis/models.py +0 -0
  46. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/video_analysis/sampling.py +0 -0
  47. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/ai/video_analysis/stages.py +0 -0
  48. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/audio/__init__.py +0 -0
  49. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/audio/analysis.py +0 -0
  50. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/audio/audio.py +0 -0
  51. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/__init__.py +0 -0
  52. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/_dimensions.py +0 -0
  53. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/_ffmpeg.py +0 -0
  54. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/_video_io.py +0 -0
  55. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/description.py +0 -0
  56. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/draw_detections.py +0 -0
  57. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/fonts/Anton-OFL.txt +0 -0
  58. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/fonts/Anton-Regular.ttf +0 -0
  59. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/fonts/BebasNeue-OFL.txt +0 -0
  60. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/fonts/BebasNeue-Regular.ttf +0 -0
  61. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/fonts/DejaVuSans.ttf +0 -0
  62. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/fonts/LICENSE_DEJAVU +0 -0
  63. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/fonts/Lato-Bold.ttf +0 -0
  64. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/fonts/Lato-OFL.txt +0 -0
  65. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/fonts/Poppins-Bold.ttf +0 -0
  66. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/fonts/Poppins-OFL.txt +0 -0
  67. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/fonts/__init__.py +0 -0
  68. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/image_text.py +0 -0
  69. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/transcription.py +0 -0
  70. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/base/video.py +0 -0
  71. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/editing/_easing.py +0 -0
  72. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/editing/transforms.py +0 -0
  73. {videopython-0.38.0 → videopython-0.40.0}/src/videopython/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: videopython
3
- Version: 0.38.0
3
+ Version: 0.40.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/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "videopython"
3
- version = "0.38.0"
3
+ version = "0.40.0"
4
4
  description = "Minimal video generation and processing library."
5
5
  authors = [
6
6
  { name = "Bartosz Wójtowicz", email = "bartoszwojtowicz@outlook.com" },
@@ -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. Subclasses that need a genuinely different eager path
445
- (extra validation, batched vectorisation) override this.
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