pixeltable 0.2.24__py3-none-any.whl → 0.3.0__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.
Potentially problematic release.
This version of pixeltable might be problematic. Click here for more details.
- pixeltable/__init__.py +2 -2
- pixeltable/__version__.py +2 -2
- pixeltable/catalog/__init__.py +1 -1
- pixeltable/catalog/dir.py +6 -0
- pixeltable/catalog/globals.py +25 -0
- pixeltable/catalog/named_function.py +4 -0
- pixeltable/catalog/path_dict.py +37 -11
- pixeltable/catalog/schema_object.py +6 -0
- pixeltable/catalog/table.py +531 -251
- pixeltable/catalog/table_version.py +22 -8
- pixeltable/catalog/view.py +8 -7
- pixeltable/dataframe.py +439 -105
- pixeltable/env.py +19 -5
- pixeltable/exec/__init__.py +1 -1
- pixeltable/exec/exec_node.py +6 -7
- pixeltable/exec/expr_eval_node.py +1 -1
- pixeltable/exec/sql_node.py +92 -45
- pixeltable/exprs/__init__.py +1 -0
- pixeltable/exprs/arithmetic_expr.py +1 -1
- pixeltable/exprs/array_slice.py +1 -1
- pixeltable/exprs/column_property_ref.py +1 -1
- pixeltable/exprs/column_ref.py +29 -2
- pixeltable/exprs/comparison.py +1 -1
- pixeltable/exprs/compound_predicate.py +1 -1
- pixeltable/exprs/expr.py +12 -5
- pixeltable/exprs/expr_set.py +8 -0
- pixeltable/exprs/function_call.py +147 -39
- pixeltable/exprs/in_predicate.py +1 -1
- pixeltable/exprs/inline_expr.py +25 -5
- pixeltable/exprs/is_null.py +1 -1
- pixeltable/exprs/json_mapper.py +1 -1
- pixeltable/exprs/json_path.py +1 -1
- pixeltable/exprs/method_ref.py +1 -1
- pixeltable/exprs/row_builder.py +1 -1
- pixeltable/exprs/rowid_ref.py +1 -1
- pixeltable/exprs/similarity_expr.py +17 -7
- pixeltable/exprs/sql_element_cache.py +4 -0
- pixeltable/exprs/type_cast.py +2 -2
- pixeltable/exprs/variable.py +3 -0
- pixeltable/func/__init__.py +5 -4
- pixeltable/func/aggregate_function.py +151 -68
- pixeltable/func/callable_function.py +48 -16
- pixeltable/func/expr_template_function.py +64 -23
- pixeltable/func/function.py +227 -23
- pixeltable/func/function_registry.py +2 -1
- pixeltable/func/query_template_function.py +51 -9
- pixeltable/func/signature.py +65 -7
- pixeltable/func/tools.py +153 -0
- pixeltable/func/udf.py +57 -35
- pixeltable/functions/__init__.py +2 -2
- pixeltable/functions/anthropic.py +51 -4
- pixeltable/functions/gemini.py +85 -0
- pixeltable/functions/globals.py +54 -34
- pixeltable/functions/huggingface.py +10 -28
- pixeltable/functions/json.py +3 -8
- pixeltable/functions/math.py +67 -0
- pixeltable/functions/mistralai.py +0 -2
- pixeltable/functions/ollama.py +8 -8
- pixeltable/functions/openai.py +51 -4
- pixeltable/functions/timestamp.py +1 -1
- pixeltable/functions/video.py +3 -9
- pixeltable/functions/vision.py +1 -1
- pixeltable/globals.py +374 -89
- pixeltable/index/embedding_index.py +106 -29
- pixeltable/io/__init__.py +1 -1
- pixeltable/io/label_studio.py +1 -1
- pixeltable/io/parquet.py +39 -19
- pixeltable/iterators/__init__.py +1 -0
- pixeltable/iterators/document.py +12 -0
- pixeltable/iterators/image.py +100 -0
- pixeltable/iterators/video.py +7 -8
- pixeltable/metadata/__init__.py +1 -1
- pixeltable/metadata/converters/convert_16.py +2 -1
- pixeltable/metadata/converters/convert_17.py +2 -1
- pixeltable/metadata/converters/convert_22.py +17 -0
- pixeltable/metadata/converters/convert_23.py +35 -0
- pixeltable/metadata/converters/convert_24.py +56 -0
- pixeltable/metadata/converters/convert_25.py +19 -0
- pixeltable/metadata/converters/util.py +4 -2
- pixeltable/metadata/notes.py +4 -0
- pixeltable/metadata/schema.py +1 -0
- pixeltable/plan.py +129 -51
- pixeltable/store.py +1 -1
- pixeltable/type_system.py +196 -54
- pixeltable/utils/arrow.py +8 -3
- pixeltable/utils/description_helper.py +89 -0
- pixeltable/utils/documents.py +14 -0
- {pixeltable-0.2.24.dist-info → pixeltable-0.3.0.dist-info}/METADATA +32 -22
- pixeltable-0.3.0.dist-info/RECORD +155 -0
- {pixeltable-0.2.24.dist-info → pixeltable-0.3.0.dist-info}/WHEEL +1 -1
- pixeltable-0.3.0.dist-info/entry_points.txt +3 -0
- pixeltable/tool/create_test_db_dump.py +0 -308
- 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-0.2.24.dist-info/RECORD +0 -153
- pixeltable-0.2.24.dist-info/entry_points.txt +0 -3
- {pixeltable-0.2.24.dist-info → pixeltable-0.3.0.dist-info}/LICENSE +0 -0
|
@@ -37,30 +37,89 @@ class EmbeddingIndex(IndexBase):
|
|
|
37
37
|
Metric.L2: 'vector_l2_ops'
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
metric: Metric
|
|
41
|
+
value_expr: exprs.FunctionCall
|
|
42
|
+
string_embed: Optional[func.Function]
|
|
43
|
+
image_embed: Optional[func.Function]
|
|
44
|
+
string_embed_signature_idx: int
|
|
45
|
+
image_embed_signature_idx: int
|
|
46
|
+
index_col_type: pgvector.sqlalchemy.Vector
|
|
47
|
+
|
|
40
48
|
def __init__(
|
|
41
|
-
|
|
42
|
-
|
|
49
|
+
self,
|
|
50
|
+
c: catalog.Column,
|
|
51
|
+
metric: str,
|
|
52
|
+
embed: Optional[func.Function] = None,
|
|
53
|
+
string_embed: Optional[func.Function] = None,
|
|
54
|
+
image_embed: Optional[func.Function] = None,
|
|
55
|
+
):
|
|
56
|
+
if embed is None and string_embed is None and image_embed is None:
|
|
57
|
+
raise excs.Error('At least one of `embed`, `string_embed`, or `image_embed` must be specified')
|
|
43
58
|
metric_names = [m.name.lower() for m in self.Metric]
|
|
44
59
|
if metric.lower() not in metric_names:
|
|
45
60
|
raise excs.Error(f'Invalid metric {metric}, must be one of {metric_names}')
|
|
46
61
|
if not c.col_type.is_string_type() and not c.col_type.is_image_type():
|
|
47
62
|
raise excs.Error(f'Embedding index requires string or image column')
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
63
|
+
|
|
64
|
+
self.string_embed = None
|
|
65
|
+
self.image_embed = None
|
|
66
|
+
|
|
67
|
+
# Resolve the specific embedding functions corresponding to the user-provided `string_embed`, `image_embed`,
|
|
68
|
+
# and/or `embed`. For string embeddings, `string_embed` will be used if specified; otherwise, `embed` will
|
|
69
|
+
# be used as a fallback, if it has a matching signature. Likewise for image embeddings.
|
|
70
|
+
|
|
52
71
|
if string_embed is not None:
|
|
53
|
-
#
|
|
54
|
-
self.
|
|
72
|
+
# `string_embed` is specified; it MUST be valid.
|
|
73
|
+
self.string_embed = self._resolve_embedding_fn(string_embed, ts.ColumnType.Type.STRING)
|
|
74
|
+
if self.string_embed is None:
|
|
75
|
+
raise excs.Error(
|
|
76
|
+
f'The function `{string_embed.name}` is not a valid string embedding: '
|
|
77
|
+
'it must take a single string parameter'
|
|
78
|
+
)
|
|
79
|
+
elif embed is not None:
|
|
80
|
+
# `embed` is specified; see if it has a string signature.
|
|
81
|
+
self.string_embed = self._resolve_embedding_fn(embed, ts.ColumnType.Type.STRING)
|
|
82
|
+
|
|
55
83
|
if image_embed is not None:
|
|
56
|
-
#
|
|
57
|
-
self.
|
|
84
|
+
# `image_embed` is specified; it MUST be valid.
|
|
85
|
+
self.image_embed = self._resolve_embedding_fn(image_embed, ts.ColumnType.Type.IMAGE)
|
|
86
|
+
if self.image_embed is None:
|
|
87
|
+
raise excs.Error(
|
|
88
|
+
f'The function `{image_embed.name}` is not a valid image embedding: '
|
|
89
|
+
'it must take a single image parameter'
|
|
90
|
+
)
|
|
91
|
+
elif embed is not None:
|
|
92
|
+
# `embed` is specified; see if it has an image signature.
|
|
93
|
+
self.image_embed = self._resolve_embedding_fn(embed, ts.ColumnType.Type.IMAGE)
|
|
94
|
+
|
|
95
|
+
if self.string_embed is None and self.image_embed is None:
|
|
96
|
+
# No string OR image signature was found. This can only happen if `embed` was specified and
|
|
97
|
+
# contains no matching signatures.
|
|
98
|
+
assert embed is not None
|
|
99
|
+
raise excs.Error(
|
|
100
|
+
f'The function `{embed.name}` is not a valid embedding: '
|
|
101
|
+
'it must take a single string or image parameter'
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Now validate the return types of the embedding functions.
|
|
105
|
+
|
|
106
|
+
if self.string_embed is not None:
|
|
107
|
+
self._validate_embedding_fn(self.string_embed, ts.ColumnType.Type.STRING)
|
|
108
|
+
|
|
109
|
+
if self.image_embed is not None:
|
|
110
|
+
self._validate_embedding_fn(self.image_embed, ts.ColumnType.Type.IMAGE)
|
|
111
|
+
|
|
112
|
+
if c.col_type.is_string_type() and self.string_embed is None:
|
|
113
|
+
raise excs.Error(f"Text embedding function is required for column {c.name} (parameter 'string_embed')")
|
|
114
|
+
if c.col_type.is_image_type() and self.image_embed is None:
|
|
115
|
+
raise excs.Error(f"Image embedding function is required for column {c.name} (parameter 'image_embed')")
|
|
58
116
|
|
|
59
117
|
self.metric = self.Metric[metric.upper()]
|
|
60
|
-
self.value_expr =
|
|
118
|
+
self.value_expr = (
|
|
119
|
+
self.string_embed(exprs.ColumnRef(c)) if c.col_type.is_string_type()
|
|
120
|
+
else self.image_embed(exprs.ColumnRef(c))
|
|
121
|
+
)
|
|
61
122
|
assert isinstance(self.value_expr.col_type, ts.ArrayType)
|
|
62
|
-
self.string_embed = string_embed
|
|
63
|
-
self.image_embed = image_embed
|
|
64
123
|
vector_size = self.value_expr.col_type.shape[0]
|
|
65
124
|
assert vector_size is not None
|
|
66
125
|
self.index_col_type = pgvector.sqlalchemy.Vector(vector_size)
|
|
@@ -91,10 +150,10 @@ class EmbeddingIndex(IndexBase):
|
|
|
91
150
|
assert isinstance(item, (str, PIL.Image.Image))
|
|
92
151
|
if isinstance(item, str):
|
|
93
152
|
assert self.string_embed is not None
|
|
94
|
-
embedding = self.string_embed.exec(item)
|
|
153
|
+
embedding = self.string_embed.exec([item], {})
|
|
95
154
|
if isinstance(item, PIL.Image.Image):
|
|
96
155
|
assert self.image_embed is not None
|
|
97
|
-
embedding = self.image_embed.exec(item)
|
|
156
|
+
embedding = self.image_embed.exec([item], {})
|
|
98
157
|
|
|
99
158
|
if self.metric == self.Metric.COSINE:
|
|
100
159
|
return val_column.sa_col.cosine_distance(embedding) * -1 + 1
|
|
@@ -110,10 +169,10 @@ class EmbeddingIndex(IndexBase):
|
|
|
110
169
|
embedding: Optional[np.ndarray] = None
|
|
111
170
|
if isinstance(item, str):
|
|
112
171
|
assert self.string_embed is not None
|
|
113
|
-
embedding = self.string_embed.exec(item)
|
|
172
|
+
embedding = self.string_embed.exec([item], {})
|
|
114
173
|
if isinstance(item, PIL.Image.Image):
|
|
115
174
|
assert self.image_embed is not None
|
|
116
|
-
embedding = self.image_embed.exec(item)
|
|
175
|
+
embedding = self.image_embed.exec([item], {})
|
|
117
176
|
assert embedding is not None
|
|
118
177
|
|
|
119
178
|
if self.metric == self.Metric.COSINE:
|
|
@@ -132,29 +191,47 @@ class EmbeddingIndex(IndexBase):
|
|
|
132
191
|
return 'embedding'
|
|
133
192
|
|
|
134
193
|
@classmethod
|
|
135
|
-
def
|
|
136
|
-
"""
|
|
194
|
+
def _resolve_embedding_fn(cls, embed_fn: func.Function, expected_type: ts.ColumnType.Type) -> Optional[func.Function]:
|
|
195
|
+
"""Find an overload resolution for `embed_fn` that matches the given type."""
|
|
137
196
|
assert isinstance(embed_fn, func.Function)
|
|
197
|
+
for resolved_fn in embed_fn._resolved_fns:
|
|
198
|
+
# The embedding function must be a 1-ary function of the correct type. But it's ok if the function signature
|
|
199
|
+
# has more than one parameter, as long as it has at most one *required* parameter.
|
|
200
|
+
sig = resolved_fn.signature
|
|
201
|
+
if (len(sig.parameters) >= 1
|
|
202
|
+
and len(sig.required_parameters) <= 1
|
|
203
|
+
and sig.parameters_by_pos[0].col_type.type_enum == expected_type):
|
|
204
|
+
return resolved_fn
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
@classmethod
|
|
208
|
+
def _validate_embedding_fn(cls, embed_fn: func.Function, expected_type: ts.ColumnType.Type) -> None:
|
|
209
|
+
"""Validate the given embedding function."""
|
|
210
|
+
assert not embed_fn.is_polymorphic
|
|
138
211
|
sig = embed_fn.signature
|
|
139
|
-
if len(sig.parameters) != 1 or sig.parameters_by_pos[0].col_type.type_enum != expected_type:
|
|
140
|
-
raise excs.Error(
|
|
141
|
-
f'{name} must take a single {expected_type.name.lower()} parameter, but has signature {sig}')
|
|
142
212
|
|
|
143
213
|
# validate return type
|
|
144
214
|
param_name = sig.parameters_by_pos[0].name
|
|
145
215
|
if expected_type == ts.ColumnType.Type.STRING:
|
|
146
|
-
return_type = embed_fn.call_return_type({param_name: 'dummy'})
|
|
216
|
+
return_type = embed_fn.call_return_type([], {param_name: 'dummy'})
|
|
147
217
|
else:
|
|
148
218
|
assert expected_type == ts.ColumnType.Type.IMAGE
|
|
149
219
|
img = PIL.Image.new('RGB', (512, 512))
|
|
150
|
-
return_type = embed_fn.call_return_type({param_name: img})
|
|
220
|
+
return_type = embed_fn.call_return_type([], {param_name: img})
|
|
221
|
+
|
|
151
222
|
assert return_type is not None
|
|
152
223
|
if not isinstance(return_type, ts.ArrayType):
|
|
153
|
-
raise excs.Error(
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
224
|
+
raise excs.Error(
|
|
225
|
+
f'The function `{embed_fn.name}` is not a valid embedding: '
|
|
226
|
+
f'it must return an array, but returns {return_type}'
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
shape = return_type.shape
|
|
230
|
+
if len(shape) != 1 or shape[0] == None:
|
|
231
|
+
raise excs.Error(
|
|
232
|
+
f'The function `{embed_fn.name}` is not a valid embedding: '
|
|
233
|
+
f'it must return a 1-dimensional array of a specific length, but returns {return_type}'
|
|
234
|
+
)
|
|
158
235
|
|
|
159
236
|
def as_dict(self) -> dict:
|
|
160
237
|
return {
|
pixeltable/io/__init__.py
CHANGED
|
@@ -2,7 +2,7 @@ from .external_store import ExternalStore, SyncStatus
|
|
|
2
2
|
from .globals import create_label_studio_project, export_images_as_fo_dataset, import_json, import_rows
|
|
3
3
|
from .hf_datasets import import_huggingface_dataset
|
|
4
4
|
from .pandas import import_csv, import_excel, import_pandas
|
|
5
|
-
from .parquet import import_parquet
|
|
5
|
+
from .parquet import import_parquet, export_parquet
|
|
6
6
|
|
|
7
7
|
__default_dir = set(symbol for symbol in dir() if not symbol.startswith('_'))
|
|
8
8
|
__removed_symbols = {'globals', 'hf_datasets', 'pandas', 'parquet'}
|
pixeltable/io/label_studio.py
CHANGED
|
@@ -574,7 +574,7 @@ class LabelStudioProject(Project):
|
|
|
574
574
|
else:
|
|
575
575
|
local_annotations_column = next(k for k, v in col_mapping.items() if v == ANNOTATIONS_COLUMN)
|
|
576
576
|
if local_annotations_column not in t._schema.keys():
|
|
577
|
-
t
|
|
577
|
+
t.add_columns({local_annotations_column: pxt.JsonType(nullable=True)})
|
|
578
578
|
|
|
579
579
|
resolved_col_mapping = cls.validate_columns(
|
|
580
580
|
t, config.export_columns, {ANNOTATIONS_COLUMN: pxt.JsonType(nullable=True)}, col_mapping)
|
pixeltable/io/parquet.py
CHANGED
|
@@ -7,11 +7,14 @@ import random
|
|
|
7
7
|
import typing
|
|
8
8
|
from collections import deque
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import Any, Optional
|
|
10
|
+
from typing import Any, Optional, Union
|
|
11
11
|
|
|
12
12
|
import numpy as np
|
|
13
13
|
import PIL.Image
|
|
14
|
+
import datetime
|
|
14
15
|
|
|
16
|
+
import pixeltable as pxt
|
|
17
|
+
from pixeltable.env import Env
|
|
15
18
|
import pixeltable.exceptions as exc
|
|
16
19
|
import pixeltable.type_system as ts
|
|
17
20
|
from pixeltable.utils.transactional_directory import transactional_directory
|
|
@@ -39,28 +42,44 @@ def _write_batch(value_batch: dict[str, deque], schema: pa.Schema, output_path:
|
|
|
39
42
|
parquet.write_table(tab, str(output_path))
|
|
40
43
|
|
|
41
44
|
|
|
42
|
-
def
|
|
45
|
+
def export_parquet(
|
|
46
|
+
table_or_df: Union[pxt.Table, pxt.DataFrame],
|
|
47
|
+
parquet_path: Path,
|
|
48
|
+
partition_size_bytes: int = 100_000_000,
|
|
49
|
+
inline_images: bool = False
|
|
50
|
+
) -> None:
|
|
43
51
|
"""
|
|
44
|
-
|
|
45
|
-
Does not materialize the dataset to memory.
|
|
52
|
+
Exports a dataframe's data to one or more Parquet files. Requires pyarrow to be installed.
|
|
46
53
|
|
|
47
|
-
It
|
|
54
|
+
It additionally writes the pixeltable metadata in a json file, which would otherwise
|
|
48
55
|
not be available in the parquet format.
|
|
49
56
|
|
|
50
|
-
Images are stored inline in a compressed format in their parquet file.
|
|
51
|
-
|
|
52
57
|
Args:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
partition_size_bytes : maximum target size for each chunk. Default 100_000_000 bytes.
|
|
58
|
+
table_or_df : Table or Dataframe to export.
|
|
59
|
+
parquet_path : Path to directory to write the parquet files to.
|
|
60
|
+
partition_size_bytes : The maximum target size for each chunk. Default 100_000_000 bytes.
|
|
61
|
+
inline_images : If True, images are stored inline in the parquet file. This is useful
|
|
62
|
+
for small images, to be imported as pytorch dataset. But can be inefficient
|
|
63
|
+
for large images, and cannot be imported into pixeltable.
|
|
64
|
+
If False, will raise an error if the Dataframe has any image column.
|
|
65
|
+
Default False.
|
|
56
66
|
"""
|
|
57
67
|
from pixeltable.utils.arrow import to_arrow_schema
|
|
58
68
|
|
|
69
|
+
df: pxt.DataFrame
|
|
70
|
+
if isinstance(table_or_df, pxt.catalog.Table):
|
|
71
|
+
df = table_or_df._df()
|
|
72
|
+
else:
|
|
73
|
+
df = table_or_df
|
|
74
|
+
|
|
59
75
|
type_dict = {k: v.as_dict() for k, v in df.schema.items()}
|
|
60
76
|
arrow_schema = to_arrow_schema(df.schema)
|
|
61
77
|
|
|
78
|
+
if not inline_images and any(col_type.is_image_type() for col_type in df.schema.values()):
|
|
79
|
+
raise exc.Error('Cannot export Dataframe with image columns when inline_images is False')
|
|
80
|
+
|
|
62
81
|
# store the changes atomically
|
|
63
|
-
with transactional_directory(
|
|
82
|
+
with transactional_directory(parquet_path) as temp_path:
|
|
64
83
|
# dump metadata json file so we can inspect what was the source of the parquet file later on.
|
|
65
84
|
json.dump(df.as_dict(), (temp_path / '.pixeltable.json').open('w'))
|
|
66
85
|
json.dump(type_dict, (temp_path / '.pixeltable.column_types.json').open('w')) # keep type metadata
|
|
@@ -111,6 +130,7 @@ def save_parquet(df: pxt.DataFrame, dest_path: Path, partition_size_bytes: int =
|
|
|
111
130
|
elif col_type.is_bool_type():
|
|
112
131
|
length = 1
|
|
113
132
|
elif col_type.is_timestamp_type():
|
|
133
|
+
val = val.astimezone(datetime.timezone.utc)
|
|
114
134
|
length = 8
|
|
115
135
|
else:
|
|
116
136
|
assert False, f'unknown type {col_type} for {col_name}'
|
|
@@ -139,7 +159,7 @@ def parquet_schema_to_pixeltable_schema(parquet_path: str) -> dict[str, Optional
|
|
|
139
159
|
|
|
140
160
|
|
|
141
161
|
def import_parquet(
|
|
142
|
-
|
|
162
|
+
table: str,
|
|
143
163
|
*,
|
|
144
164
|
parquet_path: str,
|
|
145
165
|
schema_overrides: Optional[dict[str, ts.ColumnType]] = None,
|
|
@@ -148,7 +168,7 @@ def import_parquet(
|
|
|
148
168
|
"""Creates a new base table from a Parquet file or set of files. Requires pyarrow to be installed.
|
|
149
169
|
|
|
150
170
|
Args:
|
|
151
|
-
|
|
171
|
+
table: Fully qualified name of the table to import the data into.
|
|
152
172
|
parquet_path: Path to an individual Parquet file or directory of Parquet files.
|
|
153
173
|
schema_overrides: If specified, then for each (name, type) pair in `schema_overrides`, the column with
|
|
154
174
|
name `name` will be given type `type`, instead of being inferred from the Parquet dataset. The keys in
|
|
@@ -157,7 +177,7 @@ def import_parquet(
|
|
|
157
177
|
kwargs: Additional arguments to pass to `create_table`.
|
|
158
178
|
|
|
159
179
|
Returns:
|
|
160
|
-
A handle to the newly created
|
|
180
|
+
A handle to the newly created table.
|
|
161
181
|
"""
|
|
162
182
|
from pyarrow import parquet
|
|
163
183
|
|
|
@@ -176,11 +196,11 @@ def import_parquet(
|
|
|
176
196
|
if v is None:
|
|
177
197
|
raise exc.Error(f'Could not infer pixeltable type for column {k} from parquet file')
|
|
178
198
|
|
|
179
|
-
if
|
|
180
|
-
raise exc.Error(f'Table {
|
|
199
|
+
if table in pxt.list_tables():
|
|
200
|
+
raise exc.Error(f'Table {table} already exists')
|
|
181
201
|
|
|
182
202
|
try:
|
|
183
|
-
tmp_name = f'{
|
|
203
|
+
tmp_name = f'{table}_tmp_{random.randint(0, 100000000)}'
|
|
184
204
|
tab = pxt.create_table(tmp_name, schema, **kwargs)
|
|
185
205
|
for fragment in parquet_dataset.fragments: # type: ignore[attr-defined]
|
|
186
206
|
for batch in fragment.to_batches():
|
|
@@ -190,5 +210,5 @@ def import_parquet(
|
|
|
190
210
|
_logger.error(f'Error while inserting Parquet file into table: {e}')
|
|
191
211
|
raise e
|
|
192
212
|
|
|
193
|
-
pxt.move(tmp_name,
|
|
194
|
-
return pxt.get_table(
|
|
213
|
+
pxt.move(tmp_name, table)
|
|
214
|
+
return pxt.get_table(table)
|
pixeltable/iterators/__init__.py
CHANGED
pixeltable/iterators/document.py
CHANGED
|
@@ -151,6 +151,9 @@ class DocumentSplitter(ComponentIterator):
|
|
|
151
151
|
elif self._doc_handle.format == DocumentType.DocumentFormat.PDF:
|
|
152
152
|
assert self._doc_handle.pdf_doc is not None
|
|
153
153
|
self._sections = self._pdf_sections()
|
|
154
|
+
elif self._doc_handle.format == DocumentType.DocumentFormat.TXT:
|
|
155
|
+
assert self._doc_handle.txt_doc is not None
|
|
156
|
+
self._sections = self._txt_sections()
|
|
154
157
|
else:
|
|
155
158
|
assert False, f'Unsupported document format: {self._doc_handle.format}'
|
|
156
159
|
|
|
@@ -389,6 +392,15 @@ class DocumentSplitter(ComponentIterator):
|
|
|
389
392
|
if accumulated_text and not emit_on_page:
|
|
390
393
|
yield DocumentSection(text=_emit_text(), metadata=DocumentSectionMetadata())
|
|
391
394
|
|
|
395
|
+
def _txt_sections(self) -> Iterator[DocumentSection]:
|
|
396
|
+
"""Create DocumentSections for text files.
|
|
397
|
+
|
|
398
|
+
Currently, it returns the entire text as a single section.
|
|
399
|
+
TODO: Add support for paragraphs.
|
|
400
|
+
"""
|
|
401
|
+
assert self._doc_handle.txt_doc is not None
|
|
402
|
+
yield DocumentSection(text=ftfy.fix_text(self._doc_handle.txt_doc), metadata=DocumentSectionMetadata())
|
|
403
|
+
|
|
392
404
|
def _sentence_sections(self, input_sections: Iterable[DocumentSection]) -> Iterator[DocumentSection]:
|
|
393
405
|
"""Split the input sections into sentences"""
|
|
394
406
|
for section in input_sections:
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from typing import Any, Sequence
|
|
2
|
+
|
|
3
|
+
import PIL.Image
|
|
4
|
+
|
|
5
|
+
import pixeltable.exceptions as excs
|
|
6
|
+
import pixeltable.type_system as ts
|
|
7
|
+
from pixeltable.iterators.base import ComponentIterator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TileIterator(ComponentIterator):
|
|
11
|
+
"""
|
|
12
|
+
Iterator over tiles of an image. Each image will be divided into tiles of size `tile_size`, and the tiles will be
|
|
13
|
+
iterated over in row-major order (left-to-right, then top-to-bottom). An optional `overlap` parameter may be
|
|
14
|
+
specified. If the tiles do not exactly cover the image, then the rightmost and bottommost tiles will be padded with
|
|
15
|
+
blackspace, so that the output images all have the exact size `tile_size`.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
image: Image to split into tiles.
|
|
19
|
+
tile_size: Size of each tile, as a pair of integers `[width, height]`.
|
|
20
|
+
overlap: Amount of overlap between adjacent tiles, as a pair of integers `[width, height]`.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
__image: PIL.Image.Image
|
|
24
|
+
__tile_size: Sequence[int]
|
|
25
|
+
__overlap: Sequence[int]
|
|
26
|
+
__width: int
|
|
27
|
+
__height: int
|
|
28
|
+
__xlen: int
|
|
29
|
+
__ylen: int
|
|
30
|
+
__i: int
|
|
31
|
+
__j: int
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
image: PIL.Image.Image,
|
|
36
|
+
*,
|
|
37
|
+
tile_size: tuple[int, int],
|
|
38
|
+
overlap: tuple[int, int] = (0, 0),
|
|
39
|
+
):
|
|
40
|
+
if overlap[0] >= tile_size[0] or overlap[1] >= tile_size[1]:
|
|
41
|
+
raise excs.Error(f"overlap dimensions {overlap} are not strictly smaller than tile size {tile_size}")
|
|
42
|
+
|
|
43
|
+
self.__image = image
|
|
44
|
+
self.__image.load()
|
|
45
|
+
self.__tile_size = tile_size
|
|
46
|
+
self.__overlap = overlap
|
|
47
|
+
self.__width, self.__height = image.size
|
|
48
|
+
# Justification for this formula: let t = tile_size[0], o = overlap[0]. Then the values of w (= width) that
|
|
49
|
+
# exactly accommodate an integer number of tiles are t, 2t - o, 3t - 2o, 4t - 3o, ...
|
|
50
|
+
# This formula ensures that t, 2t - o, 3t - 2o, ... result in an xlen of 1, 2, 3, ...
|
|
51
|
+
# but t + 1, 2t - o + 1, 3t - 2o + 1, ... result in an xlen of 2, 3, 4, ...
|
|
52
|
+
self.__xlen = (self.__width - overlap[0] - 1) // (tile_size[0] - overlap[0]) + 1
|
|
53
|
+
self.__ylen = (self.__height - overlap[1] - 1) // (tile_size[1] - overlap[1]) + 1
|
|
54
|
+
self.__i = 0
|
|
55
|
+
self.__j = 0
|
|
56
|
+
|
|
57
|
+
def __next__(self) -> dict[str, Any]:
|
|
58
|
+
if self.__j >= self.__ylen:
|
|
59
|
+
raise StopIteration
|
|
60
|
+
|
|
61
|
+
x1 = self.__i * (self.__tile_size[0] - self.__overlap[0])
|
|
62
|
+
y1 = self.__j * (self.__tile_size[1] - self.__overlap[1])
|
|
63
|
+
# If x2 > self.__width, PIL does the right thing and pads the image with blackspace
|
|
64
|
+
x2 = x1 + self.__tile_size[0]
|
|
65
|
+
y2 = y1 + self.__tile_size[1]
|
|
66
|
+
tile = self.__image.crop((x1, y1, x2, y2))
|
|
67
|
+
result = {
|
|
68
|
+
'tile': tile,
|
|
69
|
+
'tile_coord': [self.__i, self.__j],
|
|
70
|
+
'tile_box': [x1, y1, x2, y2]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
self.__i += 1
|
|
74
|
+
if self.__i >= self.__xlen:
|
|
75
|
+
self.__i = 0
|
|
76
|
+
self.__j += 1
|
|
77
|
+
return result
|
|
78
|
+
|
|
79
|
+
def close(self) -> None:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
def set_pos(self, pos: int) -> None:
|
|
83
|
+
self.__j = pos // self.__xlen
|
|
84
|
+
self.__i = pos % self.__xlen
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def input_schema(cls, *args: Any, **kwargs: Any) -> dict[str, ts.ColumnType]:
|
|
88
|
+
return {
|
|
89
|
+
'image': ts.ImageType(),
|
|
90
|
+
'tile_size': ts.JsonType(),
|
|
91
|
+
'overlap': ts.JsonType(),
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def output_schema(cls, *args: Any, **kwargs: Any) -> tuple[dict[str, ts.ColumnType], list[str]]:
|
|
96
|
+
return {
|
|
97
|
+
'tile': ts.ImageType(),
|
|
98
|
+
'tile_coord': ts.JsonType(),
|
|
99
|
+
'tile_box': ts.JsonType(),
|
|
100
|
+
}, ['tile']
|
pixeltable/iterators/video.py
CHANGED
|
@@ -23,13 +23,13 @@ class FrameIterator(ComponentIterator):
|
|
|
23
23
|
exact number of frames will be extracted. If neither is specified, then all frames will be extracted. The first
|
|
24
24
|
frame of the video will always be extracted, and the remaining frames will be spaced as evenly as possible.
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
Args:
|
|
27
|
+
video: URL or path of the video to use for frame extraction.
|
|
28
|
+
fps: Number of frames to extract per second of video. This may be a fractional value, such as 0.5.
|
|
29
|
+
If omitted or set to 0.0, then the native framerate of the video will be used (all frames will be
|
|
30
|
+
extracted). If `fps` is greater than the frame rate of the video, an error will be raised.
|
|
31
|
+
num_frames: Exact number of frames to extract. The frames will be spaced as evenly as possible. If
|
|
32
|
+
`num_frames` is greater than the number of frames in the video, all frames will be extracted.
|
|
33
33
|
"""
|
|
34
34
|
|
|
35
35
|
# Input parameters
|
|
@@ -180,7 +180,6 @@ class FrameIterator(ComponentIterator):
|
|
|
180
180
|
self.container.close()
|
|
181
181
|
|
|
182
182
|
def set_pos(self, pos: int) -> None:
|
|
183
|
-
"""Seek to frame idx"""
|
|
184
183
|
if pos == self.next_pos:
|
|
185
184
|
return # already there
|
|
186
185
|
|
pixeltable/metadata/__init__.py
CHANGED
|
@@ -10,7 +10,7 @@ import sqlalchemy.orm as orm
|
|
|
10
10
|
from .schema import SystemInfo, SystemInfoMd
|
|
11
11
|
|
|
12
12
|
# current version of the metadata; this is incremented whenever the metadata schema changes
|
|
13
|
-
VERSION =
|
|
13
|
+
VERSION = 26
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def create_system_info(engine: sql.engine.Engine) -> None:
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from uuid import UUID
|
|
1
2
|
import sqlalchemy as sql
|
|
2
3
|
|
|
3
4
|
from pixeltable.metadata import register_converter
|
|
@@ -12,7 +13,7 @@ def _(engine: sql.engine.Engine) -> None:
|
|
|
12
13
|
)
|
|
13
14
|
|
|
14
15
|
|
|
15
|
-
def __update_table_md(table_md: dict) -> None:
|
|
16
|
+
def __update_table_md(table_md: dict, table_id: UUID) -> None:
|
|
16
17
|
# External stores are not migratable; just drop them
|
|
17
18
|
del table_md['remotes']
|
|
18
19
|
table_md['external_stores'] = {}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from uuid import UUID
|
|
1
2
|
import sqlalchemy as sql
|
|
2
3
|
|
|
3
4
|
from pixeltable.metadata import register_converter
|
|
@@ -12,7 +13,7 @@ def _(engine: sql.engine.Engine) -> None:
|
|
|
12
13
|
)
|
|
13
14
|
|
|
14
15
|
|
|
15
|
-
def __update_table_md(table_md: dict) -> None:
|
|
16
|
+
def __update_table_md(table_md: dict, table_id: UUID) -> None:
|
|
16
17
|
# key changes in IndexMd.init_args: img_embed -> image_embed, txt_embed -> string_embed
|
|
17
18
|
if len(table_md['index_md']) == 0:
|
|
18
19
|
return
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
import sqlalchemy as sql
|
|
3
|
+
|
|
4
|
+
from pixeltable.metadata import register_converter
|
|
5
|
+
from pixeltable.metadata.converters.util import convert_table_md
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@register_converter(version=22)
|
|
9
|
+
def _(engine: sql.engine.Engine) -> None:
|
|
10
|
+
convert_table_md(engine, substitution_fn=__substitute_md)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def __substitute_md(k: Optional[str], v: Any) -> Optional[tuple[Optional[str], Any]]:
|
|
14
|
+
if isinstance(v, dict) and '_classname' in v and v['_classname'] == 'DataFrame':
|
|
15
|
+
v['from_clause'] = {'tbls': [v['tbl']], 'join_clauses': []}
|
|
16
|
+
return k, v
|
|
17
|
+
return None
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any, Optional
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
import sqlalchemy as sql
|
|
5
|
+
|
|
6
|
+
from pixeltable.metadata import register_converter
|
|
7
|
+
from pixeltable.metadata.converters.util import convert_table_md
|
|
8
|
+
from pixeltable.metadata.schema import Table
|
|
9
|
+
|
|
10
|
+
_logger = logging.getLogger('pixeltable')
|
|
11
|
+
|
|
12
|
+
@register_converter(version=23)
|
|
13
|
+
def _(engine: sql.engine.Engine) -> None:
|
|
14
|
+
convert_table_md(
|
|
15
|
+
engine,
|
|
16
|
+
table_md_updater=__update_table_md
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
def __update_table_md(table_md: dict, table_id: UUID) -> None:
|
|
20
|
+
"""update the index metadata to add indexed_col_tbl_id column if it is missing
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
table_md (dict): copy of the original table metadata. this gets updated in place.
|
|
24
|
+
table_id (UUID): the table id
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
if len(table_md['index_md']) == 0:
|
|
28
|
+
return
|
|
29
|
+
for idx_md in table_md['index_md'].values():
|
|
30
|
+
if 'indexed_col_tbl_id' not in idx_md:
|
|
31
|
+
# index metadata is missing indexed_col_tbl_id
|
|
32
|
+
# assume that the indexed column is in the same table
|
|
33
|
+
# and update the index metadata.
|
|
34
|
+
_logger.info(f'Updating index metadata for table: {table_id} index: {idx_md["id"]}')
|
|
35
|
+
idx_md['indexed_col_tbl_id'] = str(table_id)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
|
|
3
|
+
import sqlalchemy as sql
|
|
4
|
+
|
|
5
|
+
from pixeltable.metadata import register_converter
|
|
6
|
+
from pixeltable.metadata.converters.util import convert_table_md
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@register_converter(version=24)
|
|
10
|
+
def _(engine: sql.engine.Engine) -> None:
|
|
11
|
+
convert_table_md(engine, substitution_fn=__substitute_md)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def __substitute_md(k: Optional[str], v: Any) -> Optional[tuple[Optional[str], Any]]:
|
|
15
|
+
from pixeltable import func
|
|
16
|
+
from pixeltable.func.globals import resolve_symbol
|
|
17
|
+
|
|
18
|
+
if (isinstance(v, dict) and
|
|
19
|
+
'_classpath' in v and
|
|
20
|
+
v['_classpath'] in ['pixeltable.func.callable_function.CallableFunction',
|
|
21
|
+
'pixeltable.func.aggregate_function.AggregateFunction',
|
|
22
|
+
'pixeltable.func.expr_template_function.ExprTemplateFunction']):
|
|
23
|
+
if 'path' in v:
|
|
24
|
+
assert 'signature' not in v
|
|
25
|
+
f = resolve_symbol(__substitute_path(v['path']))
|
|
26
|
+
assert isinstance(f, func.Function)
|
|
27
|
+
v['signature'] = f.signatures[0].as_dict()
|
|
28
|
+
return k, v
|
|
29
|
+
|
|
30
|
+
if isinstance(v, dict) and '_classname' in v and v['_classname'] == 'FunctionCall':
|
|
31
|
+
# Correct an older serialization mechanism where Expr elements of FunctionCall args and
|
|
32
|
+
# kwargs were indicated with idx == -1 rather than None. This was fixed for InlineList
|
|
33
|
+
# and InlineDict back in convert_20, but not for FunctionCall.
|
|
34
|
+
assert 'args' in v and isinstance(v['args'], list)
|
|
35
|
+
assert 'kwargs' in v and isinstance(v['kwargs'], dict)
|
|
36
|
+
v['args'] = [
|
|
37
|
+
(None, arg) if idx == -1 else (idx, arg)
|
|
38
|
+
for idx, arg in v['args']
|
|
39
|
+
]
|
|
40
|
+
v['kwargs'] = {
|
|
41
|
+
k: (None, arg) if idx == -1 else (idx, arg)
|
|
42
|
+
for k, (idx, arg) in v['kwargs'].items()
|
|
43
|
+
}
|
|
44
|
+
return k, v
|
|
45
|
+
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def __substitute_path(path: str) -> str:
|
|
50
|
+
# Starting with version 25, function signatures are preserved in metadata. To migrate from older
|
|
51
|
+
# versions, it's necessary to resolve the function symbol to get the signature. The following
|
|
52
|
+
# adjustment is necessary for function names that are stored in db artifacts of version < 25, but
|
|
53
|
+
# have changed in some version > 25.
|
|
54
|
+
if path in ['pixeltable.functions.huggingface.clip_text', 'pixeltable.functions.huggingface.clip_image']:
|
|
55
|
+
return 'pixeltable.functions.huggingface.clip'
|
|
56
|
+
return path
|