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.
- pixeltable/__init__.py +83 -19
- pixeltable/_query.py +1444 -0
- pixeltable/_version.py +1 -0
- pixeltable/catalog/__init__.py +7 -4
- pixeltable/catalog/catalog.py +2394 -119
- pixeltable/catalog/column.py +225 -104
- pixeltable/catalog/dir.py +38 -9
- pixeltable/catalog/globals.py +53 -34
- pixeltable/catalog/insertable_table.py +265 -115
- pixeltable/catalog/path.py +80 -17
- pixeltable/catalog/schema_object.py +28 -43
- pixeltable/catalog/table.py +1270 -677
- pixeltable/catalog/table_metadata.py +103 -0
- pixeltable/catalog/table_version.py +1270 -751
- pixeltable/catalog/table_version_handle.py +109 -0
- pixeltable/catalog/table_version_path.py +137 -42
- pixeltable/catalog/tbl_ops.py +53 -0
- pixeltable/catalog/update_status.py +191 -0
- pixeltable/catalog/view.py +251 -134
- pixeltable/config.py +215 -0
- pixeltable/env.py +736 -285
- pixeltable/exceptions.py +26 -2
- pixeltable/exec/__init__.py +7 -2
- pixeltable/exec/aggregation_node.py +39 -21
- pixeltable/exec/cache_prefetch_node.py +87 -109
- pixeltable/exec/cell_materialization_node.py +268 -0
- pixeltable/exec/cell_reconstruction_node.py +168 -0
- pixeltable/exec/component_iteration_node.py +25 -28
- pixeltable/exec/data_row_batch.py +11 -46
- pixeltable/exec/exec_context.py +26 -11
- pixeltable/exec/exec_node.py +35 -27
- pixeltable/exec/expr_eval/__init__.py +3 -0
- pixeltable/exec/expr_eval/evaluators.py +365 -0
- pixeltable/exec/expr_eval/expr_eval_node.py +413 -0
- pixeltable/exec/expr_eval/globals.py +200 -0
- pixeltable/exec/expr_eval/row_buffer.py +74 -0
- pixeltable/exec/expr_eval/schedulers.py +413 -0
- pixeltable/exec/globals.py +35 -0
- pixeltable/exec/in_memory_data_node.py +35 -27
- pixeltable/exec/object_store_save_node.py +293 -0
- pixeltable/exec/row_update_node.py +44 -29
- pixeltable/exec/sql_node.py +414 -115
- pixeltable/exprs/__init__.py +8 -5
- pixeltable/exprs/arithmetic_expr.py +79 -45
- pixeltable/exprs/array_slice.py +5 -5
- pixeltable/exprs/column_property_ref.py +40 -26
- pixeltable/exprs/column_ref.py +254 -61
- pixeltable/exprs/comparison.py +14 -9
- pixeltable/exprs/compound_predicate.py +9 -10
- pixeltable/exprs/data_row.py +213 -72
- pixeltable/exprs/expr.py +270 -104
- pixeltable/exprs/expr_dict.py +6 -5
- pixeltable/exprs/expr_set.py +20 -11
- pixeltable/exprs/function_call.py +383 -284
- pixeltable/exprs/globals.py +18 -5
- pixeltable/exprs/in_predicate.py +7 -7
- pixeltable/exprs/inline_expr.py +37 -37
- pixeltable/exprs/is_null.py +8 -4
- pixeltable/exprs/json_mapper.py +120 -54
- pixeltable/exprs/json_path.py +90 -60
- pixeltable/exprs/literal.py +61 -16
- pixeltable/exprs/method_ref.py +7 -6
- pixeltable/exprs/object_ref.py +19 -8
- pixeltable/exprs/row_builder.py +238 -75
- pixeltable/exprs/rowid_ref.py +53 -15
- pixeltable/exprs/similarity_expr.py +65 -50
- pixeltable/exprs/sql_element_cache.py +5 -5
- pixeltable/exprs/string_op.py +107 -0
- pixeltable/exprs/type_cast.py +25 -13
- pixeltable/exprs/variable.py +2 -2
- pixeltable/func/__init__.py +9 -5
- pixeltable/func/aggregate_function.py +197 -92
- pixeltable/func/callable_function.py +119 -35
- pixeltable/func/expr_template_function.py +101 -48
- pixeltable/func/function.py +375 -62
- pixeltable/func/function_registry.py +20 -19
- pixeltable/func/globals.py +6 -5
- pixeltable/func/mcp.py +74 -0
- pixeltable/func/query_template_function.py +151 -35
- pixeltable/func/signature.py +178 -49
- pixeltable/func/tools.py +164 -0
- pixeltable/func/udf.py +176 -53
- pixeltable/functions/__init__.py +44 -4
- pixeltable/functions/anthropic.py +226 -47
- pixeltable/functions/audio.py +148 -11
- pixeltable/functions/bedrock.py +137 -0
- pixeltable/functions/date.py +188 -0
- pixeltable/functions/deepseek.py +113 -0
- pixeltable/functions/document.py +81 -0
- pixeltable/functions/fal.py +76 -0
- pixeltable/functions/fireworks.py +72 -20
- pixeltable/functions/gemini.py +249 -0
- pixeltable/functions/globals.py +208 -53
- pixeltable/functions/groq.py +108 -0
- pixeltable/functions/huggingface.py +1088 -95
- pixeltable/functions/image.py +155 -84
- pixeltable/functions/json.py +8 -11
- pixeltable/functions/llama_cpp.py +31 -19
- pixeltable/functions/math.py +169 -0
- pixeltable/functions/mistralai.py +50 -75
- pixeltable/functions/net.py +70 -0
- pixeltable/functions/ollama.py +29 -36
- pixeltable/functions/openai.py +548 -160
- pixeltable/functions/openrouter.py +143 -0
- pixeltable/functions/replicate.py +15 -14
- pixeltable/functions/reve.py +250 -0
- pixeltable/functions/string.py +310 -85
- pixeltable/functions/timestamp.py +37 -19
- pixeltable/functions/together.py +77 -120
- pixeltable/functions/twelvelabs.py +188 -0
- pixeltable/functions/util.py +7 -2
- pixeltable/functions/uuid.py +30 -0
- pixeltable/functions/video.py +1528 -117
- pixeltable/functions/vision.py +26 -26
- pixeltable/functions/voyageai.py +289 -0
- pixeltable/functions/whisper.py +19 -10
- pixeltable/functions/whisperx.py +179 -0
- pixeltable/functions/yolox.py +112 -0
- pixeltable/globals.py +716 -236
- pixeltable/index/__init__.py +3 -1
- pixeltable/index/base.py +17 -21
- pixeltable/index/btree.py +32 -22
- pixeltable/index/embedding_index.py +155 -92
- pixeltable/io/__init__.py +12 -7
- pixeltable/io/datarows.py +140 -0
- pixeltable/io/external_store.py +83 -125
- pixeltable/io/fiftyone.py +24 -33
- pixeltable/io/globals.py +47 -182
- pixeltable/io/hf_datasets.py +96 -127
- pixeltable/io/label_studio.py +171 -156
- pixeltable/io/lancedb.py +3 -0
- pixeltable/io/pandas.py +136 -115
- pixeltable/io/parquet.py +40 -153
- pixeltable/io/table_data_conduit.py +702 -0
- pixeltable/io/utils.py +100 -0
- pixeltable/iterators/__init__.py +8 -4
- pixeltable/iterators/audio.py +207 -0
- pixeltable/iterators/base.py +9 -3
- pixeltable/iterators/document.py +144 -87
- pixeltable/iterators/image.py +17 -38
- pixeltable/iterators/string.py +15 -12
- pixeltable/iterators/video.py +523 -127
- pixeltable/metadata/__init__.py +33 -8
- pixeltable/metadata/converters/convert_10.py +2 -3
- pixeltable/metadata/converters/convert_13.py +2 -2
- pixeltable/metadata/converters/convert_15.py +15 -11
- pixeltable/metadata/converters/convert_16.py +4 -5
- pixeltable/metadata/converters/convert_17.py +4 -5
- pixeltable/metadata/converters/convert_18.py +4 -6
- pixeltable/metadata/converters/convert_19.py +6 -9
- pixeltable/metadata/converters/convert_20.py +3 -6
- pixeltable/metadata/converters/convert_21.py +6 -8
- pixeltable/metadata/converters/convert_22.py +3 -2
- pixeltable/metadata/converters/convert_23.py +33 -0
- pixeltable/metadata/converters/convert_24.py +55 -0
- pixeltable/metadata/converters/convert_25.py +19 -0
- pixeltable/metadata/converters/convert_26.py +23 -0
- pixeltable/metadata/converters/convert_27.py +29 -0
- pixeltable/metadata/converters/convert_28.py +13 -0
- pixeltable/metadata/converters/convert_29.py +110 -0
- pixeltable/metadata/converters/convert_30.py +63 -0
- pixeltable/metadata/converters/convert_31.py +11 -0
- pixeltable/metadata/converters/convert_32.py +15 -0
- pixeltable/metadata/converters/convert_33.py +17 -0
- pixeltable/metadata/converters/convert_34.py +21 -0
- pixeltable/metadata/converters/convert_35.py +9 -0
- pixeltable/metadata/converters/convert_36.py +38 -0
- pixeltable/metadata/converters/convert_37.py +15 -0
- pixeltable/metadata/converters/convert_38.py +39 -0
- pixeltable/metadata/converters/convert_39.py +124 -0
- pixeltable/metadata/converters/convert_40.py +73 -0
- pixeltable/metadata/converters/convert_41.py +12 -0
- pixeltable/metadata/converters/convert_42.py +9 -0
- pixeltable/metadata/converters/convert_43.py +44 -0
- pixeltable/metadata/converters/util.py +44 -18
- pixeltable/metadata/notes.py +21 -0
- pixeltable/metadata/schema.py +185 -42
- pixeltable/metadata/utils.py +74 -0
- pixeltable/mypy/__init__.py +3 -0
- pixeltable/mypy/mypy_plugin.py +123 -0
- pixeltable/plan.py +616 -225
- pixeltable/share/__init__.py +3 -0
- pixeltable/share/packager.py +797 -0
- pixeltable/share/protocol/__init__.py +33 -0
- pixeltable/share/protocol/common.py +165 -0
- pixeltable/share/protocol/operation_types.py +33 -0
- pixeltable/share/protocol/replica.py +119 -0
- pixeltable/share/publish.py +349 -0
- pixeltable/store.py +398 -232
- pixeltable/type_system.py +730 -267
- pixeltable/utils/__init__.py +40 -0
- pixeltable/utils/arrow.py +201 -29
- pixeltable/utils/av.py +298 -0
- pixeltable/utils/azure_store.py +346 -0
- pixeltable/utils/coco.py +26 -27
- pixeltable/utils/code.py +4 -4
- pixeltable/utils/console_output.py +46 -0
- pixeltable/utils/coroutine.py +24 -0
- pixeltable/utils/dbms.py +92 -0
- pixeltable/utils/description_helper.py +11 -12
- pixeltable/utils/documents.py +60 -61
- pixeltable/utils/exception_handler.py +36 -0
- pixeltable/utils/filecache.py +38 -22
- pixeltable/utils/formatter.py +88 -51
- pixeltable/utils/gcs_store.py +295 -0
- pixeltable/utils/http.py +133 -0
- pixeltable/utils/http_server.py +14 -13
- pixeltable/utils/iceberg.py +13 -0
- pixeltable/utils/image.py +17 -0
- pixeltable/utils/lancedb.py +90 -0
- pixeltable/utils/local_store.py +322 -0
- pixeltable/utils/misc.py +5 -0
- pixeltable/utils/object_stores.py +573 -0
- pixeltable/utils/pydantic.py +60 -0
- pixeltable/utils/pytorch.py +20 -20
- pixeltable/utils/s3_store.py +527 -0
- pixeltable/utils/sql.py +32 -5
- pixeltable/utils/system.py +30 -0
- pixeltable/utils/transactional_directory.py +4 -3
- pixeltable-0.5.7.dist-info/METADATA +579 -0
- pixeltable-0.5.7.dist-info/RECORD +227 -0
- {pixeltable-0.2.26.dist-info → pixeltable-0.5.7.dist-info}/WHEEL +1 -1
- pixeltable-0.5.7.dist-info/entry_points.txt +2 -0
- pixeltable/__version__.py +0 -3
- pixeltable/catalog/named_function.py +0 -36
- pixeltable/catalog/path_dict.py +0 -141
- pixeltable/dataframe.py +0 -894
- pixeltable/exec/expr_eval_node.py +0 -232
- pixeltable/ext/__init__.py +0 -14
- pixeltable/ext/functions/__init__.py +0 -8
- pixeltable/ext/functions/whisperx.py +0 -77
- pixeltable/ext/functions/yolox.py +0 -157
- pixeltable/tool/create_test_db_dump.py +0 -311
- pixeltable/tool/create_test_video.py +0 -81
- pixeltable/tool/doc_plugins/griffe.py +0 -50
- pixeltable/tool/doc_plugins/mkdocstrings.py +0 -6
- pixeltable/tool/doc_plugins/templates/material/udf.html.jinja +0 -135
- pixeltable/tool/embed_udf.py +0 -9
- pixeltable/tool/mypy_plugin.py +0 -55
- pixeltable/utils/media_store.py +0 -76
- pixeltable/utils/s3.py +0 -16
- pixeltable-0.2.26.dist-info/METADATA +0 -400
- pixeltable-0.2.26.dist-info/RECORD +0 -156
- pixeltable-0.2.26.dist-info/entry_points.txt +0 -3
- {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
|
pixeltable/iterators/__init__.py
CHANGED
|
@@ -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 =
|
|
11
|
+
__default_dir = {symbol for symbol in dir() if not symbol.startswith('_')}
|
|
8
12
|
__removed_symbols = {'base', 'document', 'video'}
|
|
9
|
-
__all__ = sorted(
|
|
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)
|
pixeltable/iterators/base.py
CHANGED
|
@@ -43,11 +43,17 @@ class ComponentIterator(ABC):
|
|
|
43
43
|
"""Close the iterator and release all resources"""
|
|
44
44
|
raise NotImplementedError
|
|
45
45
|
|
|
46
|
-
|
|
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
|
-
|
|
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
|