pixeltable 0.2.26__py3-none-any.whl → 0.5.7__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 (245) hide show
  1. pixeltable/__init__.py +83 -19
  2. pixeltable/_query.py +1444 -0
  3. pixeltable/_version.py +1 -0
  4. pixeltable/catalog/__init__.py +7 -4
  5. pixeltable/catalog/catalog.py +2394 -119
  6. pixeltable/catalog/column.py +225 -104
  7. pixeltable/catalog/dir.py +38 -9
  8. pixeltable/catalog/globals.py +53 -34
  9. pixeltable/catalog/insertable_table.py +265 -115
  10. pixeltable/catalog/path.py +80 -17
  11. pixeltable/catalog/schema_object.py +28 -43
  12. pixeltable/catalog/table.py +1270 -677
  13. pixeltable/catalog/table_metadata.py +103 -0
  14. pixeltable/catalog/table_version.py +1270 -751
  15. pixeltable/catalog/table_version_handle.py +109 -0
  16. pixeltable/catalog/table_version_path.py +137 -42
  17. pixeltable/catalog/tbl_ops.py +53 -0
  18. pixeltable/catalog/update_status.py +191 -0
  19. pixeltable/catalog/view.py +251 -134
  20. pixeltable/config.py +215 -0
  21. pixeltable/env.py +736 -285
  22. pixeltable/exceptions.py +26 -2
  23. pixeltable/exec/__init__.py +7 -2
  24. pixeltable/exec/aggregation_node.py +39 -21
  25. pixeltable/exec/cache_prefetch_node.py +87 -109
  26. pixeltable/exec/cell_materialization_node.py +268 -0
  27. pixeltable/exec/cell_reconstruction_node.py +168 -0
  28. pixeltable/exec/component_iteration_node.py +25 -28
  29. pixeltable/exec/data_row_batch.py +11 -46
  30. pixeltable/exec/exec_context.py +26 -11
  31. pixeltable/exec/exec_node.py +35 -27
  32. pixeltable/exec/expr_eval/__init__.py +3 -0
  33. pixeltable/exec/expr_eval/evaluators.py +365 -0
  34. pixeltable/exec/expr_eval/expr_eval_node.py +413 -0
  35. pixeltable/exec/expr_eval/globals.py +200 -0
  36. pixeltable/exec/expr_eval/row_buffer.py +74 -0
  37. pixeltable/exec/expr_eval/schedulers.py +413 -0
  38. pixeltable/exec/globals.py +35 -0
  39. pixeltable/exec/in_memory_data_node.py +35 -27
  40. pixeltable/exec/object_store_save_node.py +293 -0
  41. pixeltable/exec/row_update_node.py +44 -29
  42. pixeltable/exec/sql_node.py +414 -115
  43. pixeltable/exprs/__init__.py +8 -5
  44. pixeltable/exprs/arithmetic_expr.py +79 -45
  45. pixeltable/exprs/array_slice.py +5 -5
  46. pixeltable/exprs/column_property_ref.py +40 -26
  47. pixeltable/exprs/column_ref.py +254 -61
  48. pixeltable/exprs/comparison.py +14 -9
  49. pixeltable/exprs/compound_predicate.py +9 -10
  50. pixeltable/exprs/data_row.py +213 -72
  51. pixeltable/exprs/expr.py +270 -104
  52. pixeltable/exprs/expr_dict.py +6 -5
  53. pixeltable/exprs/expr_set.py +20 -11
  54. pixeltable/exprs/function_call.py +383 -284
  55. pixeltable/exprs/globals.py +18 -5
  56. pixeltable/exprs/in_predicate.py +7 -7
  57. pixeltable/exprs/inline_expr.py +37 -37
  58. pixeltable/exprs/is_null.py +8 -4
  59. pixeltable/exprs/json_mapper.py +120 -54
  60. pixeltable/exprs/json_path.py +90 -60
  61. pixeltable/exprs/literal.py +61 -16
  62. pixeltable/exprs/method_ref.py +7 -6
  63. pixeltable/exprs/object_ref.py +19 -8
  64. pixeltable/exprs/row_builder.py +238 -75
  65. pixeltable/exprs/rowid_ref.py +53 -15
  66. pixeltable/exprs/similarity_expr.py +65 -50
  67. pixeltable/exprs/sql_element_cache.py +5 -5
  68. pixeltable/exprs/string_op.py +107 -0
  69. pixeltable/exprs/type_cast.py +25 -13
  70. pixeltable/exprs/variable.py +2 -2
  71. pixeltable/func/__init__.py +9 -5
  72. pixeltable/func/aggregate_function.py +197 -92
  73. pixeltable/func/callable_function.py +119 -35
  74. pixeltable/func/expr_template_function.py +101 -48
  75. pixeltable/func/function.py +375 -62
  76. pixeltable/func/function_registry.py +20 -19
  77. pixeltable/func/globals.py +6 -5
  78. pixeltable/func/mcp.py +74 -0
  79. pixeltable/func/query_template_function.py +151 -35
  80. pixeltable/func/signature.py +178 -49
  81. pixeltable/func/tools.py +164 -0
  82. pixeltable/func/udf.py +176 -53
  83. pixeltable/functions/__init__.py +44 -4
  84. pixeltable/functions/anthropic.py +226 -47
  85. pixeltable/functions/audio.py +148 -11
  86. pixeltable/functions/bedrock.py +137 -0
  87. pixeltable/functions/date.py +188 -0
  88. pixeltable/functions/deepseek.py +113 -0
  89. pixeltable/functions/document.py +81 -0
  90. pixeltable/functions/fal.py +76 -0
  91. pixeltable/functions/fireworks.py +72 -20
  92. pixeltable/functions/gemini.py +249 -0
  93. pixeltable/functions/globals.py +208 -53
  94. pixeltable/functions/groq.py +108 -0
  95. pixeltable/functions/huggingface.py +1088 -95
  96. pixeltable/functions/image.py +155 -84
  97. pixeltable/functions/json.py +8 -11
  98. pixeltable/functions/llama_cpp.py +31 -19
  99. pixeltable/functions/math.py +169 -0
  100. pixeltable/functions/mistralai.py +50 -75
  101. pixeltable/functions/net.py +70 -0
  102. pixeltable/functions/ollama.py +29 -36
  103. pixeltable/functions/openai.py +548 -160
  104. pixeltable/functions/openrouter.py +143 -0
  105. pixeltable/functions/replicate.py +15 -14
  106. pixeltable/functions/reve.py +250 -0
  107. pixeltable/functions/string.py +310 -85
  108. pixeltable/functions/timestamp.py +37 -19
  109. pixeltable/functions/together.py +77 -120
  110. pixeltable/functions/twelvelabs.py +188 -0
  111. pixeltable/functions/util.py +7 -2
  112. pixeltable/functions/uuid.py +30 -0
  113. pixeltable/functions/video.py +1528 -117
  114. pixeltable/functions/vision.py +26 -26
  115. pixeltable/functions/voyageai.py +289 -0
  116. pixeltable/functions/whisper.py +19 -10
  117. pixeltable/functions/whisperx.py +179 -0
  118. pixeltable/functions/yolox.py +112 -0
  119. pixeltable/globals.py +716 -236
  120. pixeltable/index/__init__.py +3 -1
  121. pixeltable/index/base.py +17 -21
  122. pixeltable/index/btree.py +32 -22
  123. pixeltable/index/embedding_index.py +155 -92
  124. pixeltable/io/__init__.py +12 -7
  125. pixeltable/io/datarows.py +140 -0
  126. pixeltable/io/external_store.py +83 -125
  127. pixeltable/io/fiftyone.py +24 -33
  128. pixeltable/io/globals.py +47 -182
  129. pixeltable/io/hf_datasets.py +96 -127
  130. pixeltable/io/label_studio.py +171 -156
  131. pixeltable/io/lancedb.py +3 -0
  132. pixeltable/io/pandas.py +136 -115
  133. pixeltable/io/parquet.py +40 -153
  134. pixeltable/io/table_data_conduit.py +702 -0
  135. pixeltable/io/utils.py +100 -0
  136. pixeltable/iterators/__init__.py +8 -4
  137. pixeltable/iterators/audio.py +207 -0
  138. pixeltable/iterators/base.py +9 -3
  139. pixeltable/iterators/document.py +144 -87
  140. pixeltable/iterators/image.py +17 -38
  141. pixeltable/iterators/string.py +15 -12
  142. pixeltable/iterators/video.py +523 -127
  143. pixeltable/metadata/__init__.py +33 -8
  144. pixeltable/metadata/converters/convert_10.py +2 -3
  145. pixeltable/metadata/converters/convert_13.py +2 -2
  146. pixeltable/metadata/converters/convert_15.py +15 -11
  147. pixeltable/metadata/converters/convert_16.py +4 -5
  148. pixeltable/metadata/converters/convert_17.py +4 -5
  149. pixeltable/metadata/converters/convert_18.py +4 -6
  150. pixeltable/metadata/converters/convert_19.py +6 -9
  151. pixeltable/metadata/converters/convert_20.py +3 -6
  152. pixeltable/metadata/converters/convert_21.py +6 -8
  153. pixeltable/metadata/converters/convert_22.py +3 -2
  154. pixeltable/metadata/converters/convert_23.py +33 -0
  155. pixeltable/metadata/converters/convert_24.py +55 -0
  156. pixeltable/metadata/converters/convert_25.py +19 -0
  157. pixeltable/metadata/converters/convert_26.py +23 -0
  158. pixeltable/metadata/converters/convert_27.py +29 -0
  159. pixeltable/metadata/converters/convert_28.py +13 -0
  160. pixeltable/metadata/converters/convert_29.py +110 -0
  161. pixeltable/metadata/converters/convert_30.py +63 -0
  162. pixeltable/metadata/converters/convert_31.py +11 -0
  163. pixeltable/metadata/converters/convert_32.py +15 -0
  164. pixeltable/metadata/converters/convert_33.py +17 -0
  165. pixeltable/metadata/converters/convert_34.py +21 -0
  166. pixeltable/metadata/converters/convert_35.py +9 -0
  167. pixeltable/metadata/converters/convert_36.py +38 -0
  168. pixeltable/metadata/converters/convert_37.py +15 -0
  169. pixeltable/metadata/converters/convert_38.py +39 -0
  170. pixeltable/metadata/converters/convert_39.py +124 -0
  171. pixeltable/metadata/converters/convert_40.py +73 -0
  172. pixeltable/metadata/converters/convert_41.py +12 -0
  173. pixeltable/metadata/converters/convert_42.py +9 -0
  174. pixeltable/metadata/converters/convert_43.py +44 -0
  175. pixeltable/metadata/converters/util.py +44 -18
  176. pixeltable/metadata/notes.py +21 -0
  177. pixeltable/metadata/schema.py +185 -42
  178. pixeltable/metadata/utils.py +74 -0
  179. pixeltable/mypy/__init__.py +3 -0
  180. pixeltable/mypy/mypy_plugin.py +123 -0
  181. pixeltable/plan.py +616 -225
  182. pixeltable/share/__init__.py +3 -0
  183. pixeltable/share/packager.py +797 -0
  184. pixeltable/share/protocol/__init__.py +33 -0
  185. pixeltable/share/protocol/common.py +165 -0
  186. pixeltable/share/protocol/operation_types.py +33 -0
  187. pixeltable/share/protocol/replica.py +119 -0
  188. pixeltable/share/publish.py +349 -0
  189. pixeltable/store.py +398 -232
  190. pixeltable/type_system.py +730 -267
  191. pixeltable/utils/__init__.py +40 -0
  192. pixeltable/utils/arrow.py +201 -29
  193. pixeltable/utils/av.py +298 -0
  194. pixeltable/utils/azure_store.py +346 -0
  195. pixeltable/utils/coco.py +26 -27
  196. pixeltable/utils/code.py +4 -4
  197. pixeltable/utils/console_output.py +46 -0
  198. pixeltable/utils/coroutine.py +24 -0
  199. pixeltable/utils/dbms.py +92 -0
  200. pixeltable/utils/description_helper.py +11 -12
  201. pixeltable/utils/documents.py +60 -61
  202. pixeltable/utils/exception_handler.py +36 -0
  203. pixeltable/utils/filecache.py +38 -22
  204. pixeltable/utils/formatter.py +88 -51
  205. pixeltable/utils/gcs_store.py +295 -0
  206. pixeltable/utils/http.py +133 -0
  207. pixeltable/utils/http_server.py +14 -13
  208. pixeltable/utils/iceberg.py +13 -0
  209. pixeltable/utils/image.py +17 -0
  210. pixeltable/utils/lancedb.py +90 -0
  211. pixeltable/utils/local_store.py +322 -0
  212. pixeltable/utils/misc.py +5 -0
  213. pixeltable/utils/object_stores.py +573 -0
  214. pixeltable/utils/pydantic.py +60 -0
  215. pixeltable/utils/pytorch.py +20 -20
  216. pixeltable/utils/s3_store.py +527 -0
  217. pixeltable/utils/sql.py +32 -5
  218. pixeltable/utils/system.py +30 -0
  219. pixeltable/utils/transactional_directory.py +4 -3
  220. pixeltable-0.5.7.dist-info/METADATA +579 -0
  221. pixeltable-0.5.7.dist-info/RECORD +227 -0
  222. {pixeltable-0.2.26.dist-info → pixeltable-0.5.7.dist-info}/WHEEL +1 -1
  223. pixeltable-0.5.7.dist-info/entry_points.txt +2 -0
  224. pixeltable/__version__.py +0 -3
  225. pixeltable/catalog/named_function.py +0 -36
  226. pixeltable/catalog/path_dict.py +0 -141
  227. pixeltable/dataframe.py +0 -894
  228. pixeltable/exec/expr_eval_node.py +0 -232
  229. pixeltable/ext/__init__.py +0 -14
  230. pixeltable/ext/functions/__init__.py +0 -8
  231. pixeltable/ext/functions/whisperx.py +0 -77
  232. pixeltable/ext/functions/yolox.py +0 -157
  233. pixeltable/tool/create_test_db_dump.py +0 -311
  234. pixeltable/tool/create_test_video.py +0 -81
  235. pixeltable/tool/doc_plugins/griffe.py +0 -50
  236. pixeltable/tool/doc_plugins/mkdocstrings.py +0 -6
  237. pixeltable/tool/doc_plugins/templates/material/udf.html.jinja +0 -135
  238. pixeltable/tool/embed_udf.py +0 -9
  239. pixeltable/tool/mypy_plugin.py +0 -55
  240. pixeltable/utils/media_store.py +0 -76
  241. pixeltable/utils/s3.py +0 -16
  242. pixeltable-0.2.26.dist-info/METADATA +0 -400
  243. pixeltable-0.2.26.dist-info/RECORD +0 -156
  244. pixeltable-0.2.26.dist-info/entry_points.txt +0 -3
  245. {pixeltable-0.2.26.dist-info → pixeltable-0.5.7.dist-info/licenses}/LICENSE +0 -0
pixeltable/io/utils.py ADDED
@@ -0,0 +1,100 @@
1
+ from keyword import iskeyword as is_python_keyword
2
+ from typing import Any
3
+
4
+ import pixeltable as pxt
5
+ import pixeltable.exceptions as excs
6
+ from pixeltable.catalog.globals import is_system_column_name
7
+
8
+
9
+ def normalize_pxt_col_name(name: str) -> str:
10
+ """
11
+ Normalizes an arbitrary column name into a valid Pixeltable identifier by:
12
+ - replacing any non-ascii or non-alphanumeric characters with an underscore _
13
+ - prefixing the result with the letter 'c' if it starts with an underscore or a number
14
+ """
15
+ id = ''.join(ch if ch.isascii() and ch.isalnum() else '_' for ch in name)
16
+ if id[0].isnumeric():
17
+ id = f'c_{id}'
18
+ elif id[0] == '_':
19
+ id = f'c{id}'
20
+ assert pxt.catalog.is_valid_identifier(id), id
21
+ return id
22
+
23
+
24
+ def normalize_primary_key_parameter(primary_key: str | list[str] | None = None) -> list[str]:
25
+ if primary_key is None:
26
+ primary_key = []
27
+ elif isinstance(primary_key, str):
28
+ primary_key = [primary_key]
29
+ elif not isinstance(primary_key, list) or not all(isinstance(pk, str) for pk in primary_key):
30
+ raise excs.Error('primary_key must be a single column name or a list of column names')
31
+ return primary_key
32
+
33
+
34
+ def _is_usable_as_column_name(name: str, destination_schema: dict[str, Any]) -> bool:
35
+ return not (is_system_column_name(name) or is_python_keyword(name) or name in destination_schema)
36
+
37
+
38
+ def normalize_schema_names(
39
+ in_schema: dict[str, Any],
40
+ primary_key: list[str],
41
+ schema_overrides: dict[str, Any],
42
+ require_valid_pxt_column_names: bool = False,
43
+ ) -> tuple[dict[str, Any], list[str], dict[str, str] | None]:
44
+ """
45
+ Convert all names in the input schema from source names to valid Pixeltable identifiers
46
+ - Ensure that all names are unique.
47
+ - Report an error if any types are missing
48
+ - If "require_valid_pxt_column_names", report an error if any column names are not valid Pixeltable column names
49
+ - Report an error if any primary key columns are missing
50
+ Returns
51
+ - A new schema with normalized column names
52
+ - The primary key columns, mapped to the normalized names
53
+ - A mapping from the original names to the normalized names.
54
+ """
55
+
56
+ # Report any untyped columns as an error
57
+ untyped_cols = [in_name for in_name, column_type in in_schema.items() if column_type is None]
58
+ if len(untyped_cols) > 0:
59
+ raise excs.Error(f'Could not infer pixeltable type for column(s): {", ".join(untyped_cols)}')
60
+
61
+ # Report any columns in `schema_overrides` that are not in the source
62
+ extraneous_overrides = schema_overrides.keys() - in_schema.keys()
63
+ if len(extraneous_overrides) > 0:
64
+ raise excs.Error(
65
+ f'Some column(s) specified in `schema_overrides` are not present '
66
+ f'in the source: {", ".join(extraneous_overrides)}'
67
+ )
68
+
69
+ schema: dict[str, Any] = {}
70
+ col_mapping: dict[str, str] = {} # Maps column names to Pixeltable column names if needed
71
+ for in_name, pxt_type in in_schema.items():
72
+ pxt_name = normalize_pxt_col_name(in_name)
73
+ # Ensure that column names are unique by appending a distinguishing suffix
74
+ # to any collisions
75
+ pxt_fname = pxt_name
76
+ n = 1
77
+ while not _is_usable_as_column_name(pxt_fname, schema):
78
+ pxt_fname = f'{pxt_name}_{n}'
79
+ n += 1
80
+ schema[pxt_fname] = pxt_type
81
+ col_mapping[in_name] = pxt_fname
82
+
83
+ # Determine if the col_mapping is the identity mapping
84
+ non_identity_keys = [k for k, v in col_mapping.items() if k != v]
85
+ if len(non_identity_keys) > 0:
86
+ if require_valid_pxt_column_names:
87
+ raise excs.Error(
88
+ f'Column names must be valid pixeltable identifiers. Invalid names: {", ".join(non_identity_keys)}'
89
+ )
90
+ else:
91
+ col_mapping = None
92
+
93
+ # Report any primary key columns that are not in the source as an error
94
+ missing_pk = [pk for pk in primary_key if pk not in in_schema]
95
+ if len(missing_pk) > 0:
96
+ raise excs.Error(f'Primary key column(s) are not found in the source: {", ".join(missing_pk)}')
97
+
98
+ pxt_pk = [col_mapping[pk] for pk in primary_key] if col_mapping is not None else primary_key
99
+
100
+ return schema, pxt_pk, col_mapping
@@ -1,13 +1,17 @@
1
+ """Iterators for splitting media and documents into components."""
2
+ # ruff: noqa: F401
3
+
4
+ from .audio import AudioSplitter
1
5
  from .base import ComponentIterator
2
6
  from .document import DocumentSplitter
3
7
  from .image import TileIterator
4
8
  from .string import StringSplitter
5
- from .video import FrameIterator
9
+ from .video import FrameIterator, VideoSplitter
6
10
 
7
- __default_dir = set(symbol for symbol in dir() if not symbol.startswith('_'))
11
+ __default_dir = {symbol for symbol in dir() if not symbol.startswith('_')}
8
12
  __removed_symbols = {'base', 'document', 'video'}
9
- __all__ = sorted(list(__default_dir - __removed_symbols))
13
+ __all__ = sorted(__default_dir - __removed_symbols)
10
14
 
11
15
 
12
- def __dir__():
16
+ def __dir__() -> list[str]:
13
17
  return __all__
@@ -0,0 +1,207 @@
1
+ import logging
2
+ from fractions import Fraction
3
+ from pathlib import Path
4
+ from typing import Any, ClassVar
5
+
6
+ import av
7
+ from deprecated import deprecated
8
+
9
+ from pixeltable import exceptions as excs, type_system as ts
10
+ from pixeltable.utils.local_store import TempStore
11
+
12
+ from .base import ComponentIterator
13
+
14
+ _logger = logging.getLogger('pixeltable')
15
+
16
+
17
+ class AudioSplitter(ComponentIterator):
18
+ # Input parameters
19
+ audio_path: Path
20
+ chunk_duration_sec: float
21
+ overlap_sec: float
22
+
23
+ # audio stream details
24
+ container: av.container.input.InputContainer
25
+ audio_time_base: Fraction # seconds per presentation time
26
+
27
+ # List of chunks to extract
28
+ # Each chunk is defined by start and end presentation timestamps in audio file (int)
29
+ chunks_to_extract_in_pts: list[tuple[int, int]] | None
30
+ # next chunk to extract
31
+ next_pos: int
32
+
33
+ __codec_map: ClassVar[dict[str, str]] = {
34
+ 'mp3': 'mp3', # MP3 decoder -> mp3/libmp3lame encoder
35
+ 'mp3float': 'mp3', # MP3float decoder -> mp3 encoder
36
+ 'aac': 'aac', # AAC decoder -> AAC encoder
37
+ 'vorbis': 'libvorbis', # Vorbis decoder -> libvorbis encoder
38
+ 'opus': 'libopus', # Opus decoder -> libopus encoder
39
+ 'flac': 'flac', # FLAC decoder -> FLAC encoder
40
+ 'wavpack': 'wavpack', # WavPack decoder -> WavPack encoder
41
+ 'alac': 'alac', # ALAC decoder -> ALAC encoder
42
+ }
43
+
44
+ def __init__(
45
+ self, audio: str, chunk_duration_sec: float, *, overlap_sec: float = 0.0, min_chunk_duration_sec: float = 0.0
46
+ ):
47
+ assert chunk_duration_sec > 0.0
48
+ assert chunk_duration_sec >= min_chunk_duration_sec
49
+ assert overlap_sec < chunk_duration_sec
50
+ audio_path = Path(audio)
51
+ assert audio_path.exists() and audio_path.is_file()
52
+ self.audio_path = audio_path
53
+ self.next_pos = 0
54
+ self.container = av.open(str(audio_path))
55
+ if len(self.container.streams.audio) == 0:
56
+ # No audio stream
57
+ return
58
+ self.chunk_duration_sec = chunk_duration_sec
59
+ self.overlap_sec = overlap_sec
60
+ self.min_chunk_duration_sec = min_chunk_duration_sec
61
+ self.audio_time_base = self.container.streams.audio[0].time_base
62
+
63
+ audio_start_time_pts = self.container.streams.audio[0].start_time or 0
64
+ audio_start_time_sec = float(audio_start_time_pts * self.audio_time_base)
65
+ total_audio_duration_pts = self.container.streams.audio[0].duration or 0
66
+ total_audio_duration_sec = float(total_audio_duration_pts * self.audio_time_base)
67
+
68
+ self.chunks_to_extract_in_pts = [
69
+ (round(start / self.audio_time_base), round(end / self.audio_time_base))
70
+ for (start, end) in self.build_chunks(
71
+ audio_start_time_sec, total_audio_duration_sec, chunk_duration_sec, overlap_sec, min_chunk_duration_sec
72
+ )
73
+ ]
74
+ _logger.debug(
75
+ f'AudioIterator: path={self.audio_path} total_audio_duration_pts={total_audio_duration_pts} '
76
+ f'chunks_to_extract_in_pts={self.chunks_to_extract_in_pts}'
77
+ )
78
+
79
+ @classmethod
80
+ def build_chunks(
81
+ cls,
82
+ start_time_sec: float,
83
+ total_duration_sec: float,
84
+ chunk_duration_sec: float,
85
+ overlap_sec: float,
86
+ min_chunk_duration_sec: float,
87
+ ) -> list[tuple[float, float]]:
88
+ chunks_to_extract_in_sec: list[tuple[float, float]] = []
89
+ current_pos = start_time_sec
90
+ end_time = start_time_sec + total_duration_sec
91
+ while current_pos < end_time:
92
+ chunk_start = current_pos
93
+ chunk_end = min(chunk_start + chunk_duration_sec, end_time)
94
+ chunks_to_extract_in_sec.append((chunk_start, chunk_end))
95
+ if chunk_end >= end_time:
96
+ break
97
+ current_pos = chunk_end - overlap_sec
98
+ # If the last chunk is smaller than min_chunk_duration_sec then drop the last chunk from the list
99
+ if (
100
+ len(chunks_to_extract_in_sec) > 0
101
+ and (chunks_to_extract_in_sec[-1][1] - chunks_to_extract_in_sec[-1][0]) < min_chunk_duration_sec
102
+ ):
103
+ return chunks_to_extract_in_sec[:-1] # return all but the last chunk
104
+ return chunks_to_extract_in_sec
105
+
106
+ @classmethod
107
+ def input_schema(cls) -> dict[str, ts.ColumnType]:
108
+ return {
109
+ 'audio': ts.AudioType(nullable=False),
110
+ 'chunk_duration_sec': ts.FloatType(nullable=True),
111
+ 'overlap_sec': ts.FloatType(nullable=True),
112
+ 'min_chunk_duration_sec': ts.FloatType(nullable=True),
113
+ }
114
+
115
+ @classmethod
116
+ def output_schema(cls, *args: Any, **kwargs: Any) -> tuple[dict[str, ts.ColumnType], list[str]]:
117
+ param_names = ['chunk_duration_sec', 'min_chunk_duration_sec', 'overlap_sec']
118
+ params = dict(zip(param_names, args))
119
+ params.update(kwargs)
120
+
121
+ chunk_duration_sec = params['chunk_duration_sec']
122
+ min_chunk_duration_sec = params.get('min_chunk_duration_sec', 0.0)
123
+ overlap_sec = params.get('overlap_sec', 0.0)
124
+ if chunk_duration_sec <= 0.0:
125
+ raise excs.Error('chunk_duration_sec must be a positive number')
126
+ if chunk_duration_sec < min_chunk_duration_sec:
127
+ raise excs.Error('chunk_duration_sec must be at least min_chunk_duration_sec')
128
+ if overlap_sec >= chunk_duration_sec:
129
+ raise excs.Error('overlap_sec must be less than chunk_duration_sec')
130
+ return {
131
+ 'start_time_sec': ts.FloatType(),
132
+ 'end_time_sec': ts.FloatType(),
133
+ 'audio_chunk': ts.AudioType(nullable=True),
134
+ }, []
135
+
136
+ def __next__(self) -> dict[str, Any]:
137
+ if self.next_pos >= len(self.chunks_to_extract_in_pts):
138
+ raise StopIteration
139
+ target_chunk_start, target_chunk_end = self.chunks_to_extract_in_pts[self.next_pos]
140
+ chunk_start_pts = 0
141
+ chunk_end_pts = 0
142
+ chunk_file = str(TempStore.create_path(extension=self.audio_path.suffix))
143
+ output_container = av.open(chunk_file, mode='w')
144
+ input_stream = self.container.streams.audio[0]
145
+ codec_name = AudioSplitter.__codec_map.get(input_stream.codec_context.name, input_stream.codec_context.name)
146
+ output_stream = output_container.add_stream(codec_name, rate=input_stream.codec_context.sample_rate)
147
+ assert isinstance(output_stream, av.audio.stream.AudioStream)
148
+ frame_count = 0
149
+ # Since frames don't align with chunk boundaries, we may have read an extra frame in previous iteration
150
+ # Seek to the nearest frame in stream at current chunk start time
151
+ self.container.seek(target_chunk_start, backward=True, stream=self.container.streams.audio[0])
152
+ while True:
153
+ try:
154
+ frame = next(self.container.decode(audio=0))
155
+ except EOFError as e:
156
+ raise excs.Error(f"Failed to read audio file '{self.audio_path}': {e}") from e
157
+ except StopIteration:
158
+ # no more frames to scan
159
+ break
160
+ if frame.pts < target_chunk_start:
161
+ # Current frame is behind chunk's start time, always get frame next to chunk's start time
162
+ continue
163
+ if frame.pts >= target_chunk_end:
164
+ # Frame has crossed the chunk boundary, it should be picked up by next chunk, throw away
165
+ # the current frame
166
+ break
167
+ frame_end = frame.pts + frame.samples
168
+ if frame_count == 0:
169
+ # Record start of the first frame
170
+ chunk_start_pts = frame.pts
171
+ # Write frame to output container
172
+ frame_count += 1
173
+ # If encode returns packets, write them to output container. Some encoders will buffer the frames.
174
+ output_container.mux(output_stream.encode(frame))
175
+ # record this frame's end as chunks end
176
+ chunk_end_pts = frame_end
177
+ # Check if frame's end has crossed the chunk boundary
178
+ if frame_end >= target_chunk_end:
179
+ break
180
+
181
+ # record result
182
+ if frame_count > 0:
183
+ # flush encoder
184
+ output_container.mux(output_stream.encode(None))
185
+ output_container.close()
186
+ result = {
187
+ 'start_time_sec': round(float(chunk_start_pts * self.audio_time_base), 4),
188
+ 'end_time_sec': round(float(chunk_end_pts * self.audio_time_base), 4),
189
+ 'audio_chunk': chunk_file if frame_count > 0 else None,
190
+ }
191
+ _logger.debug('audio chunk result: %s', result)
192
+ self.next_pos += 1
193
+ return result
194
+ else:
195
+ # It's possible that there are no frames in the range of the last chunk, stop the iterator in this case.
196
+ # Note that start_time points at the first frame so case applies only for the last chunk
197
+ assert self.next_pos == len(self.chunks_to_extract_in_pts) - 1
198
+ self.next_pos += 1
199
+ raise StopIteration
200
+
201
+ def close(self) -> None:
202
+ self.container.close()
203
+
204
+ @classmethod
205
+ @deprecated('create() is deprecated; use `pixeltable.functions.audio.audio_splitter` instead', version='0.5.6')
206
+ def create(cls, **kwargs: Any) -> tuple[type[ComponentIterator], dict[str, Any]]:
207
+ return super()._create(**kwargs)
@@ -43,11 +43,17 @@ class ComponentIterator(ABC):
43
43
  """Close the iterator and release all resources"""
44
44
  raise NotImplementedError
45
45
 
46
- @abstractmethod
47
- def set_pos(self, pos: int) -> None:
46
+ def set_pos(self, pos: int, **kwargs: Any) -> None:
48
47
  """Set the iterator position to pos"""
49
- raise NotImplementedError
48
+ pass
50
49
 
51
50
  @classmethod
52
51
  def create(cls, **kwargs: Any) -> tuple[type[ComponentIterator], dict[str, Any]]:
52
+ # TODO: This is still needed for compatibility with existing user-defined iterators; it will become deprecated
53
+ # when the new decorator pattern is introduced for iterators
54
+ return cls._create(**kwargs)
55
+
56
+ @classmethod
57
+ def _create(cls, **kwargs: Any) -> tuple[type[ComponentIterator], dict[str, Any]]:
58
+ # create() variant that can be called by subclasses without generating a deprecation warning.
53
59
  return cls, kwargs