videopython 0.22.0__tar.gz → 0.22.2__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 (51) hide show
  1. {videopython-0.22.0 → videopython-0.22.2}/PKG-INFO +1 -1
  2. {videopython-0.22.0 → videopython-0.22.2}/pyproject.toml +1 -1
  3. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/ai/understanding/audio.py +2 -2
  4. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/base/edit.py +63 -1
  5. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/base/text/transcription.py +13 -5
  6. {videopython-0.22.0 → videopython-0.22.2}/.gitignore +0 -0
  7. {videopython-0.22.0 → videopython-0.22.2}/LICENSE +0 -0
  8. {videopython-0.22.0 → videopython-0.22.2}/README.md +0 -0
  9. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/__init__.py +0 -0
  10. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/ai/__init__.py +0 -0
  11. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/ai/_device.py +0 -0
  12. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/ai/dubbing/__init__.py +0 -0
  13. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/ai/dubbing/dubber.py +0 -0
  14. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/ai/dubbing/models.py +0 -0
  15. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/ai/dubbing/pipeline.py +0 -0
  16. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/ai/dubbing/timing.py +0 -0
  17. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/ai/generation/__init__.py +0 -0
  18. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/ai/generation/audio.py +0 -0
  19. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/ai/generation/image.py +0 -0
  20. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/ai/generation/translation.py +0 -0
  21. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/ai/generation/video.py +0 -0
  22. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/ai/registry.py +0 -0
  23. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/ai/swapping/__init__.py +0 -0
  24. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/ai/swapping/inpainter.py +0 -0
  25. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/ai/swapping/models.py +0 -0
  26. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/ai/swapping/segmenter.py +0 -0
  27. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/ai/swapping/swapper.py +0 -0
  28. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/ai/transforms.py +0 -0
  29. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/ai/understanding/__init__.py +0 -0
  30. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/ai/understanding/image.py +0 -0
  31. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/ai/understanding/separation.py +0 -0
  32. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/ai/understanding/temporal.py +0 -0
  33. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/ai/video_analysis.py +0 -0
  34. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/base/__init__.py +0 -0
  35. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/base/audio/__init__.py +0 -0
  36. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/base/audio/analysis.py +0 -0
  37. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/base/audio/audio.py +0 -0
  38. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/base/combine.py +0 -0
  39. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/base/description.py +0 -0
  40. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/base/effects.py +0 -0
  41. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/base/exceptions.py +0 -0
  42. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/base/progress.py +0 -0
  43. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/base/registry.py +0 -0
  44. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/base/scene.py +0 -0
  45. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/base/text/__init__.py +0 -0
  46. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/base/text/overlay.py +0 -0
  47. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/base/transforms.py +0 -0
  48. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/base/transitions.py +0 -0
  49. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/base/utils.py +0 -0
  50. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/base/video.py +0 -0
  51. {videopython-0.22.0 → videopython-0.22.2}/src/videopython/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: videopython
3
- Version: 0.22.0
3
+ Version: 0.22.2
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.22.0"
3
+ version = "0.22.2"
4
4
  description = "Minimal video generation and processing library."
5
5
  authors = [
6
6
  { name = "Bartosz Wójtowicz", email = "bartoszwojtowicz@outlook.com" },
@@ -59,7 +59,7 @@ class AudioToText:
59
59
  )
60
60
  transcription_segments.append(transcription_segment)
61
61
 
62
- return Transcription(segments=transcription_segments)
62
+ return Transcription(segments=transcription_segments, language=transcription_result.get("language"))
63
63
 
64
64
  def _process_whisperx_result(self, whisperx_result: dict, audio_data) -> Transcription:
65
65
  """Process whisperx result with diarization."""
@@ -94,7 +94,7 @@ class AudioToText:
94
94
  )
95
95
  )
96
96
 
97
- return Transcription(words=words)
97
+ return Transcription(words=words, language=whisperx_result.get("language"))
98
98
 
99
99
  def _transcribe_local(self, audio: Audio) -> Transcription:
100
100
  """Transcribe using local Whisper model."""
@@ -272,11 +272,51 @@ class VideoEdit:
272
272
  return video
273
273
 
274
274
  def validate(self) -> VideoMetadata:
275
- """Validate the editing plan without loading video data."""
275
+ """Validate the editing plan without loading video data.
276
+
277
+ Requires source video files to be present on disk (uses ``VideoMetadata.from_path``).
278
+ For validation without file access, use :meth:`validate_with_metadata`.
279
+ """
276
280
  segment_metas: list[VideoMetadata] = []
277
281
  for i, segment in enumerate(self.segments):
278
282
  segment_metas.append(self._validate_segment(i, segment))
283
+ return self._validate_assembled(segment_metas)
284
+
285
+ def validate_with_metadata(
286
+ self,
287
+ source_metadata: VideoMetadata | dict[str, VideoMetadata],
288
+ ) -> VideoMetadata:
289
+ """Validate the editing plan using pre-built metadata instead of loading from file.
290
+
291
+ Same validation as validate() but accepts a VideoMetadata directly,
292
+ avoiding the need for the source video file to be on disk.
293
+
294
+ Args:
295
+ source_metadata: VideoMetadata for the source video (duration, dimensions, fps).
296
+ For multi-source plans, pass a dict mapping source paths to their metadata.
297
+
298
+ Returns:
299
+ Predicted output VideoMetadata after all operations.
279
300
 
301
+ Raises:
302
+ ValueError: If any validation check fails.
303
+ """
304
+ if isinstance(source_metadata, VideoMetadata):
305
+ meta_map: dict[str, VideoMetadata] = {str(seg.source_video): source_metadata for seg in self.segments}
306
+ else:
307
+ meta_map = source_metadata
308
+
309
+ segment_metas: list[VideoMetadata] = []
310
+ for i, segment in enumerate(self.segments):
311
+ source_key = str(segment.source_video)
312
+ if source_key not in meta_map:
313
+ raise ValueError(
314
+ f"Segment {i}: no metadata provided for source '{source_key}'. Available keys: {sorted(meta_map)}"
315
+ )
316
+ segment_metas.append(self._validate_segment_with_metadata(i, segment, meta_map[source_key]))
317
+ return self._validate_assembled(segment_metas)
318
+
319
+ def _validate_assembled(self, segment_metas: list[VideoMetadata]) -> VideoMetadata:
280
320
  if len(segment_metas) > 1:
281
321
  first = segment_metas[0]
282
322
  for j, other in enumerate(segment_metas[1:], start=1):
@@ -343,6 +383,28 @@ class VideoEdit:
343
383
  _validate_effect_bounds(record, meta.total_seconds, context=ctx)
344
384
  return meta
345
385
 
386
+ def _validate_segment_with_metadata(
387
+ self, index: int, segment: SegmentConfig, source_meta: VideoMetadata
388
+ ) -> VideoMetadata:
389
+ ctx = f"Segment {index}"
390
+ if segment.start_second < 0:
391
+ raise ValueError(f"{ctx}: start_second ({segment.start_second}) must be >= 0")
392
+ if segment.end_second <= segment.start_second:
393
+ raise ValueError(
394
+ f"{ctx}: end_second ({segment.end_second}) must be > start_second ({segment.start_second})"
395
+ )
396
+ if segment.end_second > source_meta.total_seconds:
397
+ raise ValueError(
398
+ f"{ctx}: end_second ({segment.end_second}) exceeds source duration ({source_meta.total_seconds}s)"
399
+ )
400
+ meta = source_meta.cut(segment.start_second, segment.end_second)
401
+
402
+ for record in segment.transform_records:
403
+ meta = _predict_transform_metadata(meta, record.op_id, record.args, context=f"{ctx} ({record.op_id})")
404
+ for record in segment.effect_records:
405
+ _validate_effect_bounds(record, meta.total_seconds, context=ctx)
406
+ return meta
407
+
346
408
  def _assemble_segments(self, context: dict[str, Any] | None = None) -> Video:
347
409
  result: Video | None = None
348
410
  for segment in self.segments:
@@ -67,12 +67,14 @@ class Transcription:
67
67
  self,
68
68
  segments: list[TranscriptionSegment] | None = None,
69
69
  words: list[TranscriptionWord] | None = None,
70
+ language: str | None = None,
70
71
  ):
71
72
  """Initialize Transcription from either segments or words.
72
73
 
73
74
  Args:
74
75
  segments: Pre-constructed segments (backward compatible)
75
76
  words: Words to group into segments by speaker (for diarization)
77
+ language: ISO 639-1 language code detected during transcription (e.g. "en", "pl")
76
78
 
77
79
  Raises:
78
80
  ValueError: If both or neither arguments are provided
@@ -80,6 +82,8 @@ class Transcription:
80
82
  if (segments is None) == (words is None):
81
83
  raise ValueError("Exactly one of 'segments' or 'words' must be provided")
82
84
 
85
+ self.language = language
86
+
83
87
  if segments is not None:
84
88
  self.segments = segments
85
89
  self.speakers = {s.speaker for s in segments if s.speaker is not None}
@@ -185,7 +189,7 @@ class Transcription:
185
189
  )
186
190
  )
187
191
 
188
- return Transcription(segments=offset_segments)
192
+ return Transcription(segments=offset_segments, language=self.language)
189
193
 
190
194
  def standardize_segments(self, *, time: float | None = None, num_words: int | None = None) -> Transcription:
191
195
  """Return a new Transcription with standardized segments.
@@ -212,7 +216,7 @@ class Transcription:
212
216
  all_words.extend(segment.words)
213
217
 
214
218
  if not all_words:
215
- return Transcription(segments=[])
219
+ return Transcription(segments=[], language=self.language)
216
220
 
217
221
  standardized_segments = []
218
222
 
@@ -266,7 +270,7 @@ class Transcription:
266
270
  )
267
271
  )
268
272
 
269
- return Transcription(segments=standardized_segments)
273
+ return Transcription(segments=standardized_segments, language=self.language)
270
274
 
271
275
  def slice(self, start: float, end: float) -> Transcription | None:
272
276
  """Return a new Transcription containing only words within the time range.
@@ -334,15 +338,19 @@ class Transcription:
334
338
  )
335
339
  )
336
340
 
337
- return Transcription(segments=sliced_segments)
341
+ return Transcription(segments=sliced_segments, language=self.language)
338
342
 
339
343
  def to_dict(self) -> dict:
340
344
  """Convert to dictionary for JSON serialization."""
341
345
  return {
342
346
  "segments": [s.to_dict() for s in self.segments],
347
+ "language": self.language,
343
348
  }
344
349
 
345
350
  @classmethod
346
351
  def from_dict(cls, data: dict) -> Transcription:
347
352
  """Create Transcription from dictionary."""
348
- return cls(segments=[TranscriptionSegment.from_dict(s) for s in data["segments"]])
353
+ return cls(
354
+ segments=[TranscriptionSegment.from_dict(s) for s in data["segments"]],
355
+ language=data.get("language"),
356
+ )
File without changes
File without changes
File without changes