typed-ffmpeg-compatible 3.5.1__py3-none-any.whl → 3.6__py3-none-any.whl

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 (71) hide show
  1. typed_ffmpeg/__init__.py +4 -1
  2. typed_ffmpeg/_version.py +2 -2
  3. typed_ffmpeg/base.py +4 -1
  4. typed_ffmpeg/codecs/__init__.py +2 -0
  5. typed_ffmpeg/codecs/decoders.py +1852 -1853
  6. typed_ffmpeg/codecs/encoders.py +2001 -1782
  7. typed_ffmpeg/codecs/schema.py +6 -12
  8. typed_ffmpeg/common/__init__.py +1 -0
  9. typed_ffmpeg/common/cache.py +9 -6
  10. typed_ffmpeg/common/schema.py +11 -0
  11. typed_ffmpeg/common/serialize.py +13 -7
  12. typed_ffmpeg/compile/__init__.py +1 -0
  13. typed_ffmpeg/compile/compile_cli.py +55 -8
  14. typed_ffmpeg/compile/compile_json.py +4 -0
  15. typed_ffmpeg/compile/compile_python.py +15 -0
  16. typed_ffmpeg/compile/context.py +15 -4
  17. typed_ffmpeg/compile/validate.py +9 -8
  18. typed_ffmpeg/dag/factory.py +2 -0
  19. typed_ffmpeg/dag/global_runnable/__init__.py +1 -0
  20. typed_ffmpeg/dag/global_runnable/global_args.py +2 -2
  21. typed_ffmpeg/dag/global_runnable/runnable.py +51 -11
  22. typed_ffmpeg/dag/io/__init__.py +1 -0
  23. typed_ffmpeg/dag/io/_input.py +20 -5
  24. typed_ffmpeg/dag/io/_output.py +24 -9
  25. typed_ffmpeg/dag/io/output_args.py +21 -7
  26. typed_ffmpeg/dag/nodes.py +20 -0
  27. typed_ffmpeg/dag/schema.py +19 -6
  28. typed_ffmpeg/dag/utils.py +2 -2
  29. typed_ffmpeg/exceptions.py +2 -1
  30. typed_ffmpeg/expressions.py +884 -0
  31. typed_ffmpeg/ffprobe/__init__.py +1 -0
  32. typed_ffmpeg/ffprobe/parse.py +7 -1
  33. typed_ffmpeg/ffprobe/probe.py +3 -1
  34. typed_ffmpeg/ffprobe/schema.py +83 -1
  35. typed_ffmpeg/ffprobe/xml2json.py +8 -2
  36. typed_ffmpeg/filters.py +540 -631
  37. typed_ffmpeg/formats/__init__.py +2 -0
  38. typed_ffmpeg/formats/demuxers.py +1869 -1921
  39. typed_ffmpeg/formats/muxers.py +1382 -1107
  40. typed_ffmpeg/formats/schema.py +6 -12
  41. typed_ffmpeg/info.py +8 -0
  42. typed_ffmpeg/options/__init__.py +15 -0
  43. typed_ffmpeg/options/codec.py +711 -0
  44. typed_ffmpeg/options/format.py +196 -0
  45. typed_ffmpeg/options/framesync.py +43 -0
  46. typed_ffmpeg/options/timeline.py +22 -0
  47. typed_ffmpeg/schema.py +15 -0
  48. typed_ffmpeg/sources.py +392 -381
  49. typed_ffmpeg/streams/__init__.py +2 -0
  50. typed_ffmpeg/streams/audio.py +1071 -882
  51. typed_ffmpeg/streams/av.py +9 -3
  52. typed_ffmpeg/streams/subtitle.py +3 -3
  53. typed_ffmpeg/streams/video.py +1873 -1725
  54. typed_ffmpeg/types.py +3 -2
  55. typed_ffmpeg/utils/__init__.py +1 -0
  56. typed_ffmpeg/utils/escaping.py +8 -4
  57. typed_ffmpeg/utils/frozendict.py +31 -1
  58. typed_ffmpeg/utils/lazy_eval/__init__.py +1 -0
  59. typed_ffmpeg/utils/lazy_eval/operator.py +75 -27
  60. typed_ffmpeg/utils/lazy_eval/schema.py +176 -4
  61. typed_ffmpeg/utils/run.py +2 -0
  62. typed_ffmpeg/utils/snapshot.py +3 -2
  63. typed_ffmpeg/utils/typing.py +2 -1
  64. typed_ffmpeg/utils/view.py +2 -1
  65. {typed_ffmpeg_compatible-3.5.1.dist-info → typed_ffmpeg_compatible-3.6.dist-info}/METADATA +1 -1
  66. typed_ffmpeg_compatible-3.6.dist-info/RECORD +73 -0
  67. typed_ffmpeg_compatible-3.5.1.dist-info/RECORD +0 -67
  68. {typed_ffmpeg_compatible-3.5.1.dist-info → typed_ffmpeg_compatible-3.6.dist-info}/WHEEL +0 -0
  69. {typed_ffmpeg_compatible-3.5.1.dist-info → typed_ffmpeg_compatible-3.6.dist-info}/entry_points.txt +0 -0
  70. {typed_ffmpeg_compatible-3.5.1.dist-info → typed_ffmpeg_compatible-3.6.dist-info}/licenses/LICENSE +0 -0
  71. {typed_ffmpeg_compatible-3.5.1.dist-info → typed_ffmpeg_compatible-3.6.dist-info}/top_level.txt +0 -0
@@ -1,17 +1,11 @@
1
- from dataclasses import dataclass
1
+ """FFmpeg codec schema definitions."""
2
2
 
3
- from ..common.serialize import Serializable
4
- from ..utils.frozendict import FrozenDict
3
+ from ..schema import FFMpegOptionGroup
5
4
 
6
5
 
7
- @dataclass(frozen=True, kw_only=True)
8
- class FFMpegCodecOption(Serializable):
9
- kwargs: FrozenDict[str, str | int | float | bool] = FrozenDict({})
6
+ class FFMpegEncoderOption(FFMpegOptionGroup):
7
+ """FFmpeg encoder option group."""
10
8
 
11
9
 
12
- @dataclass(frozen=True, kw_only=True)
13
- class FFMpegEncoderOption(FFMpegCodecOption): ...
14
-
15
-
16
- @dataclass(frozen=True, kw_only=True)
17
- class FFMpegDecoderOption(FFMpegCodecOption): ...
10
+ class FFMpegDecoderOption(FFMpegOptionGroup):
11
+ """FFmpeg decoder option group."""
@@ -0,0 +1 @@
1
+ """FFmpeg common utilities package."""
@@ -1,3 +1,5 @@
1
+ """Cache utilities for FFmpeg operations."""
2
+
1
3
  from pathlib import Path
2
4
  from typing import TypeVar
3
5
 
@@ -11,7 +13,7 @@ cache_path.mkdir(exist_ok=True)
11
13
 
12
14
  def load(cls: type[T], id: str) -> T:
13
15
  """
14
- Load an object from the cache
16
+ Load an object from the cache.
15
17
 
16
18
  Args:
17
19
  cls: The class of the object
@@ -19,6 +21,7 @@ def load(cls: type[T], id: str) -> T:
19
21
 
20
22
  Returns:
21
23
  The loaded object
24
+
22
25
  """
23
26
  path = cache_path / f"{cls.__name__}/{id}.json"
24
27
 
@@ -29,11 +32,12 @@ def load(cls: type[T], id: str) -> T:
29
32
 
30
33
  def save(obj: T, id: str) -> None:
31
34
  """
32
- Save an object to the cache
35
+ Save an object to the cache.
33
36
 
34
37
  Args:
35
38
  obj: The object to save
36
39
  id: The id of the object
40
+
37
41
  """
38
42
  schema_path = cache_path / f"{obj.__class__.__name__}"
39
43
  schema_path.mkdir(exist_ok=True)
@@ -44,13 +48,14 @@ def save(obj: T, id: str) -> None:
44
48
 
45
49
  def list_all(cls: type[T]) -> list[T]:
46
50
  """
47
- List all objects of a class in the cache
51
+ List all objects of a class in the cache.
48
52
 
49
53
  Args:
50
54
  cls: The class of the objects
51
55
 
52
56
  Returns:
53
57
  A list of all objects of the class in the cache
58
+
54
59
  """
55
60
  path = cache_path / f"{cls.__name__}"
56
61
 
@@ -58,9 +63,7 @@ def list_all(cls: type[T]) -> list[T]:
58
63
 
59
64
 
60
65
  def clean(cls: type[T]) -> None:
61
- """
62
- Clean the cache for a class
63
- """
66
+ """Clean the cache for a class."""
64
67
  path = cache_path / f"{cls.__name__}"
65
68
  for i in path.glob("*.json"):
66
69
  i.unlink()
@@ -275,6 +275,7 @@ class FFMpegFilter(Serializable):
275
275
 
276
276
  Returns:
277
277
  A dictionary mapping parameter names to their values
278
+
278
279
  """
279
280
  return dict(self.pre)
280
281
 
@@ -288,6 +289,7 @@ class FFMpegFilter(Serializable):
288
289
 
289
290
  Returns:
290
291
  A simplified FFMpegFilterDef representation of this filter
292
+
291
293
  """
292
294
  return FFMpegFilterDef(
293
295
  name=self.name,
@@ -310,6 +312,7 @@ class FFMpegFilter(Serializable):
310
312
 
311
313
  Raises:
312
314
  AssertionError: If a dynamic input filter has no input formula
315
+
313
316
  """
314
317
  if self.is_filter_source:
315
318
  return set()
@@ -346,6 +349,7 @@ class FFMpegFilter(Serializable):
346
349
 
347
350
  Raises:
348
351
  AssertionError: If a dynamic output filter has no output formula
352
+
349
353
  """
350
354
  if self.is_filter_sink:
351
355
  return set()
@@ -386,6 +390,7 @@ class FFMpegFilter(Serializable):
386
390
  any known filter type
387
391
  AssertionError: If a sink filter has multiple input types or
388
392
  if a filter has no input types
393
+
389
394
  """
390
395
  if self.is_filter_sink:
391
396
  assert len(self.input_typings) == 1
@@ -423,6 +428,8 @@ class FFMpegFilter(Serializable):
423
428
 
424
429
  @serializable
425
430
  class FFMpegOptionFlag(int, Enum):
431
+ """FFmpeg option flags that define option behavior and characteristics."""
432
+
426
433
  OPT_FUNC_ARG = 1 << 0
427
434
  """
428
435
  The OPT_TYPE_FUNC option takes an argument.
@@ -567,6 +574,7 @@ class FFMpegOption(Serializable):
567
574
 
568
575
  Returns:
569
576
  True if this option is meant to be used with input files
577
+
570
578
  """
571
579
  return bool(self.flags & FFMpegOptionFlag.OPT_INPUT)
572
580
 
@@ -577,6 +585,7 @@ class FFMpegOption(Serializable):
577
585
 
578
586
  Returns:
579
587
  True if this option is meant to be used with output files
588
+
580
589
  """
581
590
  return bool(self.flags & FFMpegOptionFlag.OPT_OUTPUT)
582
591
 
@@ -588,6 +597,7 @@ class FFMpegOption(Serializable):
588
597
  Returns:
589
598
  True if this option is a global option that doesn't apply to
590
599
  specific input or output files
600
+
591
601
  """
592
602
  return (
593
603
  not self.is_input_option
@@ -605,5 +615,6 @@ class FFMpegOption(Serializable):
605
615
 
606
616
  Returns:
607
617
  True if this option can be used with stream specifiers
618
+
608
619
  """
609
620
  return bool(self.flags & FFMpegOptionFlag.OPT_SPEC)
@@ -37,6 +37,7 @@ def serializable(
37
37
 
38
38
  Returns:
39
39
  The class itself
40
+
40
41
  """
41
42
  assert cls.__name__ not in CLASS_REGISTRY, (
42
43
  f"Class {cls.__name__} already registered"
@@ -47,11 +48,10 @@ def serializable(
47
48
 
48
49
 
49
50
  class Serializable:
50
- """
51
- A base class for all serializable classes.
52
- """
51
+ """A base class for all serializable classes."""
53
52
 
54
53
  def __init_subclass__(cls, **kwargs: Any) -> None:
54
+ """Register the subclass in the serialization registry."""
55
55
  super().__init_subclass__(**kwargs)
56
56
  serializable(cls)
57
57
 
@@ -79,6 +79,7 @@ def load_class(name: str) -> type[Serializable] | type[Enum]:
79
79
  # Create an instance
80
80
  node = FilterNode(name='scale', ...)
81
81
  ```
82
+
82
83
  """
83
84
  assert name in CLASS_REGISTRY, f"Class {name} not registered"
84
85
  return CLASS_REGISTRY[name]
@@ -102,11 +103,13 @@ def frozen(v: Any) -> Any:
102
103
  Example:
103
104
  ```python
104
105
  # Convert a nested structure to immutable form
105
- frozen_data = frozen(
106
- {"options": ["option1", "option2"], "settings": {"key": "value"}}
107
- )
106
+ frozen_data = frozen({
107
+ "options": ["option1", "option2"],
108
+ "settings": {"key": "value"},
109
+ })
108
110
  # Result: FrozenDict with tuple instead of list and nested FrozenDict
109
111
  ```
112
+
110
113
  """
111
114
  if isinstance(v, list):
112
115
  return tuple(frozen(i) for i in v)
@@ -141,6 +144,7 @@ def object_hook(obj: Any) -> Any:
141
144
  }
142
145
  # Will be converted to a FilterNode instance
143
146
  ```
147
+
144
148
  """
145
149
  if isinstance(obj, dict):
146
150
  if obj.get("__class__"):
@@ -176,6 +180,7 @@ def loads(raw: str) -> Any:
176
180
  filter_node = loads(json_str)
177
181
  # filter_node is now a FilterNode instance
178
182
  ```
183
+
179
184
  """
180
185
  return json.loads(raw, object_hook=object_hook)
181
186
 
@@ -202,8 +207,8 @@ def to_dict_with_class_info(instance: Any) -> Any:
202
207
  serializable = to_dict_with_class_info(filter_node)
203
208
  # serializable now contains class information and all attributes
204
209
  ```
205
- """
206
210
 
211
+ """
207
212
  if isinstance(instance, dict | FrozenDict):
208
213
  return {k: to_dict_with_class_info(v) for k, v in instance.items()}
209
214
  elif isinstance(instance, list):
@@ -251,6 +256,7 @@ def dumps(instance: Any) -> str:
251
256
  # json_str can be saved to a file and later deserialized
252
257
  # with loads() to reconstruct the original object
253
258
  ```
259
+
254
260
  """
255
261
  obj = to_dict_with_class_info(instance)
256
262
  return json.dumps(obj, indent=2)
@@ -0,0 +1 @@
1
+ """FFmpeg compilation utilities package."""
@@ -16,8 +16,10 @@ filter graph syntax, and escaping of special characters in FFmpeg commands.
16
16
 
17
17
  from __future__ import annotations
18
18
 
19
+ import logging
19
20
  import re
20
21
  import shlex
22
+ import tempfile
21
23
  from collections import defaultdict
22
24
  from collections.abc import Mapping
23
25
  from dataclasses import replace
@@ -47,6 +49,8 @@ from ..utils.run import command_line
47
49
  from .context import DAGContext
48
50
  from .validate import validate
49
51
 
52
+ logger = logging.getLogger(__name__)
53
+
50
54
 
51
55
  def get_options_dict() -> dict[str, FFMpegOption]:
52
56
  """
@@ -54,6 +58,7 @@ def get_options_dict() -> dict[str, FFMpegOption]:
54
58
 
55
59
  Returns:
56
60
  Dictionary mapping option names to their FFMpegOption definitions
61
+
57
62
  """
58
63
  options = load(list[FFMpegOption], "options")
59
64
  return {option.name: option for option in options}
@@ -65,6 +70,7 @@ def get_filter_dict() -> dict[str, FFMpegFilter]:
65
70
 
66
71
  Returns:
67
72
  Dictionary mapping filter names to their FFMpegFilter definitions
73
+
68
74
  """
69
75
  filters = load(list[FFMpegFilter], "filters")
70
76
  return {filter.name: filter for filter in filters}
@@ -85,6 +91,7 @@ def parse_options(tokens: list[str]) -> dict[str, list[str | None | bool]]:
85
91
 
86
92
  Returns:
87
93
  Dictionary mapping option names to lists of their values
94
+
88
95
  """
89
96
  parsed_options: dict[str, list[str | None | bool]] = defaultdict(list)
90
97
 
@@ -130,6 +137,8 @@ def parse_stream_selector(
130
137
 
131
138
  Raises:
132
139
  AssertionError: If the stream label is not found in the mapping
140
+ FFMpegValueError: If the stream type is unknown
141
+
133
142
  """
134
143
  selector = selector.strip("[]")
135
144
 
@@ -178,6 +187,7 @@ def _is_filename(token: str) -> bool:
178
187
 
179
188
  Returns:
180
189
  True if the token is a filename, False otherwise
190
+
181
191
  """
182
192
  # not start with - and has ext
183
193
  return not token.startswith("-") and len(token.split(".")) > 1
@@ -201,6 +211,7 @@ def parse_output(
201
211
 
202
212
  Returns:
203
213
  List of OutputStream objects representing the output specifications
214
+
204
215
  """
205
216
  tokens = source.copy()
206
217
 
@@ -268,6 +279,7 @@ def parse_input(
268
279
 
269
280
  Returns:
270
281
  Dictionary mapping input indices to their FilterableStream objects
282
+
271
283
  """
272
284
  output: list[AVStream] = []
273
285
 
@@ -321,6 +333,10 @@ def parse_filter_complex(
321
333
 
322
334
  Returns:
323
335
  Updated stream mapping with new filter outputs added
336
+
337
+ Raises:
338
+ FFMpegValueError: If the stream type is unknown
339
+
324
340
  """
325
341
  # Use re.split with negative lookbehind to handle escaped semicolons
326
342
  filter_units = re.split(r"(?<!\\);", filter_complex)
@@ -421,6 +437,7 @@ def parse_global(
421
437
  Example:
422
438
  For tokens like ['-y', '-loglevel', 'quiet', '-i', 'input.mp4']:
423
439
  Returns ({'y': True, 'loglevel': 'quiet'}, ['-i', 'input.mp4'])
440
+
424
441
  """
425
442
  options = parse_options(tokens[: tokens.index("-i")])
426
443
  remaining_tokens = tokens[tokens.index("-i") :]
@@ -462,6 +479,7 @@ def parse(cli: str) -> Stream:
462
479
  "ffmpeg -i input.mp4 -filter_complex '[0:v]scale=1280:720[v]' -map '[v]' output.mp4"
463
480
  )
464
481
  ```
482
+
465
483
  """
466
484
  # ffmpeg [global_options] {[input_file_options] -i input_url} ... {[output_file_options] output_url} ...
467
485
  ffmpeg_options = get_options_dict()
@@ -502,7 +520,9 @@ def parse(cli: str) -> Stream:
502
520
  return result
503
521
 
504
522
 
505
- def compile(stream: Stream, auto_fix: bool = True) -> str:
523
+ def compile(
524
+ stream: Stream, auto_fix: bool = True, use_filter_complex_script: bool = False
525
+ ) -> str:
506
526
  """
507
527
  Compile a stream into a command-line string.
508
528
 
@@ -512,14 +532,21 @@ def compile(stream: Stream, auto_fix: bool = True) -> str:
512
532
  Args:
513
533
  stream: The Stream object to compile into a command-line string
514
534
  auto_fix: Whether to automatically fix issues in the stream
535
+ use_filter_complex_script: If True, use -filter_complex_script with a
536
+ temporary file instead of -filter_complex
515
537
 
516
538
  Returns:
517
539
  A command-line string that can be passed to FFmpeg
540
+
518
541
  """
519
- return "ffmpeg " + command_line(compile_as_list(stream, auto_fix))
542
+ return "ffmpeg " + command_line(
543
+ compile_as_list(stream, auto_fix, use_filter_complex_script)
544
+ )
520
545
 
521
546
 
522
- def compile_as_list(stream: Stream, auto_fix: bool = True) -> list[str]:
547
+ def compile_as_list(
548
+ stream: Stream, auto_fix: bool = True, use_filter_complex_script: bool = False
549
+ ) -> list[str]:
523
550
  """
524
551
  Compile a stream into a list of FFmpeg command-line arguments.
525
552
 
@@ -545,6 +572,8 @@ def compile_as_list(stream: Stream, auto_fix: bool = True) -> list[str]:
545
572
  - Properly labels all streams
546
573
  - Maintains correct filter chain order
547
574
  - Handles stream splitting and merging
575
+ - If use_filter_complex_script is True, creates a temporary file
576
+ with the filter complex content and uses -filter_complex_script
548
577
 
549
578
  5. Output Files: Processes destination files
550
579
  - File paths and output options
@@ -560,6 +589,8 @@ def compile_as_list(stream: Stream, auto_fix: bool = True) -> list[str]:
560
589
  stream: The Stream object to compile into arguments
561
590
  auto_fix: Whether to automatically fix issues in the stream
562
591
  (e.g., reconnecting disconnected nodes)
592
+ use_filter_complex_script: If True, use -filter_complex_script with a
593
+ temporary file instead of -filter_complex
563
594
 
564
595
  Returns:
565
596
  A list of strings representing FFmpeg command-line arguments
@@ -580,8 +611,8 @@ def compile_as_list(stream: Stream, auto_fix: bool = True) -> list[str]:
580
611
  args
581
612
  ) # ['ffmpeg', '-i', 'input.mp4', '-filter_complex', '...', 'output.mp4']
582
613
  ```
583
- """
584
614
 
615
+ """
585
616
  stream = validate(stream, auto_fix=auto_fix)
586
617
  node = stream.node
587
618
  context = DAGContext.build(node)
@@ -605,7 +636,19 @@ def compile_as_list(stream: Stream, auto_fix: bool = True) -> list[str]:
605
636
  vf_commands += ["".join(get_args(node, context))]
606
637
 
607
638
  if vf_commands:
608
- commands += ["-filter_complex", ";".join(vf_commands)]
639
+ filter_complex_content = ";".join(vf_commands)
640
+
641
+ if use_filter_complex_script:
642
+ # Create a temporary file with the filter complex content
643
+ with tempfile.NamedTemporaryFile(
644
+ mode="w", suffix=".txt", delete=False
645
+ ) as f:
646
+ f.write(filter_complex_content)
647
+ temp_filename = f.name
648
+
649
+ commands += ["-filter_complex_script", temp_filename]
650
+ else:
651
+ commands += ["-filter_complex", filter_complex_content]
609
652
 
610
653
  # compile the output nodes
611
654
  output_nodes = [node for node in context.all_nodes if isinstance(node, OutputNode)]
@@ -643,6 +686,7 @@ def get_stream_label(stream: Stream, context: DAGContext | None = None) -> str:
643
686
 
644
687
  Raises:
645
688
  FFMpegValueError: If the stream has an unknown type or node type
689
+
646
690
  """
647
691
  from ..streams.audio import AudioStream
648
692
  from ..streams.av import AVStream
@@ -713,8 +757,8 @@ def get_args_filter_node(node: FilterNode, context: DAGContext) -> list[str]:
713
757
  Example:
714
758
  For a scale filter with width=1280 and height=720, this might return:
715
759
  ['[0:v]', 'scale=', 'width=1280:height=720', '[s0]']
716
- """
717
760
 
761
+ """
718
762
  incoming_labels = "".join(f"[{get_stream_label(k, context)}]" for k in node.inputs)
719
763
  outputs = context.get_outgoing_streams(node)
720
764
 
@@ -766,6 +810,7 @@ def get_args_input_node(node: InputNode, context: DAGContext) -> list[str]:
766
810
  Example:
767
811
  For an input file "input.mp4" with options like seeking to 10 seconds:
768
812
  ['-ss', '10', '-i', 'input.mp4']
813
+
769
814
  """
770
815
  commands = []
771
816
  for key, value in node.kwargs.items():
@@ -799,6 +844,7 @@ def get_args_output_node(node: OutputNode, context: DAGContext) -> list[str]:
799
844
  Example:
800
845
  For an output file "output.mp4" with H.264 video codec:
801
846
  ['-map', '[v0]', '-c:v', 'libx264', 'output.mp4']
847
+
802
848
  """
803
849
  # !handle mapping
804
850
  commands = []
@@ -860,6 +906,7 @@ def get_args_global_node(node: GlobalNode, context: DAGContext) -> list[str]:
860
906
  Example:
861
907
  For global options like overwrite and quiet logging:
862
908
  ['-y', '-loglevel', 'quiet']
909
+
863
910
  """
864
911
  commands = []
865
912
  for key, value in node.kwargs.items():
@@ -891,8 +938,8 @@ def get_args(node: Node, context: DAGContext | None = None) -> list[str]:
891
938
 
892
939
  Raises:
893
940
  FFMpegValueError: If the node type is not recognized
894
- """
895
941
 
942
+ """
896
943
  context = context or DAGContext.build(node)
897
944
 
898
945
  match node:
@@ -927,8 +974,8 @@ def get_node_label(node: Node, context: DAGContext) -> str:
927
974
 
928
975
  Raises:
929
976
  AssertionError: If the node is not an InputNode or FilterNode
930
- """
931
977
 
978
+ """
932
979
  node_id = context.node_ids[node]
933
980
  match node:
934
981
  case InputNode():
@@ -1,3 +1,5 @@
1
+ """JSON compilation utilities for FFmpeg streams."""
2
+
1
3
  from ..common.serialize import dumps, loads
2
4
  from ..dag.schema import Stream
3
5
  from .validate import validate
@@ -16,6 +18,7 @@ def compile(stream: Stream, auto_fix: bool = True) -> str:
16
18
 
17
19
  Returns:
18
20
  A JSON string that can be passed to FFmpeg
21
+
19
22
  """
20
23
  stream = validate(stream, auto_fix=auto_fix)
21
24
 
@@ -34,5 +37,6 @@ def parse(json: str) -> Stream:
34
37
 
35
38
  Returns:
36
39
  A Stream object
40
+
37
41
  """
38
42
  return loads(json)
@@ -1,3 +1,5 @@
1
+ """Python code compilation utilities for FFmpeg streams."""
2
+
1
3
  from __future__ import annotations
2
4
 
3
5
  from collections.abc import Mapping
@@ -39,6 +41,7 @@ def filter_stream_typed_index(
39
41
 
40
42
  Returns:
41
43
  The index of the matched stream in the outgoing streams of the node.
44
+
42
45
  """
43
46
  matched_outgoing_streams = [
44
47
  k
@@ -69,6 +72,10 @@ def get_input_var_name(
69
72
 
70
73
  Returns:
71
74
  The input variable name for the stream.
75
+
76
+ Raises:
77
+ ValueError: If the stream type is unknown
78
+
72
79
  """
73
80
  match stream:
74
81
  case AVStream():
@@ -127,6 +134,10 @@ def get_output_var_name(node: Node, context: DAGContext) -> str:
127
134
 
128
135
  Returns:
129
136
  The output variable name for the node.
137
+
138
+ Raises:
139
+ ValueError: If the node type is unknown
140
+
130
141
  """
131
142
  match node:
132
143
  case InputNode():
@@ -153,6 +164,7 @@ def compile_kwargs(kwargs: Mapping[str, Any]) -> str:
153
164
 
154
165
  Returns:
155
166
  The compiled kwargs.
167
+
156
168
  """
157
169
  return ", ".join(f"{k}={repr(v)}" for k, v in kwargs.items())
158
170
 
@@ -169,6 +181,7 @@ def compile_fluent(code: list[str]) -> list[str]:
169
181
 
170
182
  Returns:
171
183
  The compiled code.
184
+
172
185
  """
173
186
  buffer = [k.split("=", 1)[:2] for k in code]
174
187
 
@@ -209,6 +222,7 @@ def compile(stream: Stream, auto_fix: bool = True, fluent: bool = True) -> str:
209
222
 
210
223
  Returns:
211
224
  The compiled python code.
225
+
212
226
  """
213
227
  stream = validate(stream, auto_fix=auto_fix)
214
228
  node = stream.node
@@ -314,6 +328,7 @@ def parse(code: str) -> Stream:
314
328
 
315
329
  Returns:
316
330
  The parsed stream.
331
+
317
332
  """
318
333
  local_vars: dict[str, Any] = {}
319
334
  exec(code, {}, local_vars)
@@ -35,6 +35,7 @@ def _remove_duplicates(seq: Iterable[T]) -> list[T]:
35
35
 
36
36
  Returns:
37
37
  A new list with duplicates removed, preserving the original order
38
+
38
39
  """
39
40
  seen = set()
40
41
  output: list[T] = []
@@ -62,6 +63,7 @@ def _collect(node: Node) -> tuple[list[Node], list[Stream]]:
62
63
  A tuple containing two lists:
63
64
  - A list of all nodes in the upstream path (including the starting node)
64
65
  - A list of all streams connecting these nodes
66
+
65
67
  """
66
68
  nodes: list[Node] = [node]
67
69
  streams: list[Stream] = list(node.inputs)
@@ -124,6 +126,7 @@ class DAGContext:
124
126
 
125
127
  Returns:
126
128
  A fully initialized DAGContext containing all nodes and streams in the graph
129
+
127
130
  """
128
131
  nodes, streams = _collect(node)
129
132
 
@@ -144,6 +147,7 @@ class DAGContext:
144
147
 
145
148
  Returns:
146
149
  A sorted list of all nodes in the graph
150
+
147
151
  """
148
152
  return sorted(self.nodes, key=lambda node: len(node.upstream_nodes))
149
153
 
@@ -159,6 +163,7 @@ class DAGContext:
159
163
 
160
164
  Returns:
161
165
  A sorted list of all streams in the graph
166
+
162
167
  """
163
168
  return sorted(
164
169
  self.streams,
@@ -177,6 +182,7 @@ class DAGContext:
177
182
 
178
183
  Returns:
179
184
  A dictionary mapping streams to their destination nodes and connection indices
185
+
180
186
  """
181
187
  outgoing_nodes: dict[Stream, list[tuple[Node, int]]] = defaultdict(list)
182
188
 
@@ -197,8 +203,8 @@ class DAGContext:
197
203
 
198
204
  Returns:
199
205
  A dictionary mapping nodes to their output streams
200
- """
201
206
 
207
+ """
202
208
  outgoing_streams: dict[Node, list[Stream]] = defaultdict(list)
203
209
 
204
210
  for stream in self.streams:
@@ -210,10 +216,13 @@ class DAGContext:
210
216
  def node_ids(self) -> dict[Node, int]:
211
217
  """
212
218
  Get a mapping of nodes to their unique integer IDs.
219
+
213
220
  This property assigns a unique integer ID to each node in the graph,
214
221
  based on the node type and its position in the processing chain.
222
+
215
223
  Returns:
216
- A dictionary mapping nodes to their unique integer IDs
224
+ A dictionary mapping nodes to their unique integer IDs.
225
+
217
226
  """
218
227
  node_index: dict[type[Node], int] = defaultdict(int)
219
228
  node_ids: dict[Node, int] = {}
@@ -239,8 +248,8 @@ class DAGContext:
239
248
 
240
249
  Returns:
241
250
  A dictionary mapping nodes to their string labels
242
- """
243
251
 
252
+ """
244
253
  input_node_index = 0
245
254
  filter_node_index = 0
246
255
  node_labels: dict[Node, str] = {}
@@ -271,6 +280,7 @@ class DAGContext:
271
280
 
272
281
  Returns:
273
282
  A list of (node, input_index) tuples for nodes that receive this stream
283
+
274
284
  """
275
285
  return self.outgoing_nodes[stream]
276
286
 
@@ -292,8 +302,8 @@ class DAGContext:
292
302
 
293
303
  Raises:
294
304
  AssertionError: If the node is not an InputNode or FilterNode
295
- """
296
305
 
306
+ """
297
307
  return self.node_labels[node]
298
308
 
299
309
  @override
@@ -311,5 +321,6 @@ class DAGContext:
311
321
 
312
322
  Returns:
313
323
  A list of streams that originate from this node
324
+
314
325
  """
315
326
  return self.outgoing_streams[node]