pixeltable 0.4.19__py3-none-any.whl → 0.4.20__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/_version.py +1 -1
- pixeltable/catalog/catalog.py +76 -50
- pixeltable/catalog/column.py +29 -16
- pixeltable/catalog/insertable_table.py +2 -2
- pixeltable/catalog/path.py +4 -10
- pixeltable/catalog/table.py +51 -0
- pixeltable/catalog/table_version.py +40 -7
- pixeltable/catalog/view.py +2 -2
- pixeltable/config.py +1 -0
- pixeltable/env.py +2 -0
- pixeltable/exprs/column_ref.py +2 -1
- pixeltable/functions/__init__.py +1 -0
- pixeltable/functions/image.py +2 -8
- pixeltable/functions/reve.py +250 -0
- pixeltable/functions/video.py +534 -1
- pixeltable/globals.py +2 -1
- pixeltable/index/base.py +5 -18
- pixeltable/index/btree.py +6 -2
- pixeltable/index/embedding_index.py +4 -4
- pixeltable/metadata/schema.py +7 -32
- pixeltable/share/__init__.py +1 -1
- pixeltable/share/packager.py +22 -18
- pixeltable/share/protocol/__init__.py +34 -0
- pixeltable/share/protocol/common.py +170 -0
- pixeltable/share/protocol/operation_types.py +33 -0
- pixeltable/share/protocol/replica.py +109 -0
- pixeltable/share/publish.py +90 -56
- pixeltable/store.py +11 -15
- pixeltable/utils/av.py +87 -1
- pixeltable/utils/dbms.py +15 -11
- pixeltable/utils/image.py +10 -0
- {pixeltable-0.4.19.dist-info → pixeltable-0.4.20.dist-info}/METADATA +2 -1
- {pixeltable-0.4.19.dist-info → pixeltable-0.4.20.dist-info}/RECORD +36 -31
- {pixeltable-0.4.19.dist-info → pixeltable-0.4.20.dist-info}/WHEEL +0 -0
- {pixeltable-0.4.19.dist-info → pixeltable-0.4.20.dist-info}/entry_points.txt +0 -0
- {pixeltable-0.4.19.dist-info → pixeltable-0.4.20.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pixeltable [UDFs](https://docs.pixeltable.com/datastore/custom-functions) that wrap [Reve](https://app.reve.com/) image
|
|
3
|
+
generation API. In order to use them, the API key must be specified either with `REVE_API_KEY` environment variable,
|
|
4
|
+
or as `api_key` in the `reve` section of the Pixeltable config file.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import atexit
|
|
9
|
+
import logging
|
|
10
|
+
import re
|
|
11
|
+
from io import BytesIO
|
|
12
|
+
|
|
13
|
+
import aiohttp
|
|
14
|
+
import PIL.Image
|
|
15
|
+
|
|
16
|
+
import pixeltable as pxt
|
|
17
|
+
from pixeltable.env import Env, register_client
|
|
18
|
+
from pixeltable.utils.code import local_public_names
|
|
19
|
+
from pixeltable.utils.image import to_base64
|
|
20
|
+
|
|
21
|
+
_logger = logging.getLogger('pixeltable')
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ReveRateLimitedError(Exception):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ReveContentViolationError(Exception):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ReveUnexpectedError(Exception):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _ReveClient:
|
|
37
|
+
"""
|
|
38
|
+
Client for interacting with the Reve API. Maintains a long-lived HTTP session to the service.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
api_key: str
|
|
42
|
+
session: aiohttp.ClientSession
|
|
43
|
+
|
|
44
|
+
def __init__(self, api_key: str):
|
|
45
|
+
self.api_key = api_key
|
|
46
|
+
self.session = Env.get().event_loop.run_until_complete(self._start_session())
|
|
47
|
+
atexit.register(lambda: asyncio.run(self.session.close()))
|
|
48
|
+
|
|
49
|
+
async def _start_session(self) -> aiohttp.ClientSession:
|
|
50
|
+
# Maintains a long-lived TPC connection. The default keepalive timeout is 15 seconds.
|
|
51
|
+
return aiohttp.ClientSession(base_url='https://api.reve.com')
|
|
52
|
+
|
|
53
|
+
async def _post(self, endpoint: str, *, payload: dict) -> PIL.Image.Image:
|
|
54
|
+
# Reve supports other formats as well, but we only use PNG for now
|
|
55
|
+
requested_content_type = 'image/png'
|
|
56
|
+
request_headers = {
|
|
57
|
+
'Authorization': f'Bearer {self.api_key}',
|
|
58
|
+
'Content-Type': 'application/json',
|
|
59
|
+
'Accept': requested_content_type,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async with self.session.post(endpoint, json=payload, headers=request_headers) as resp:
|
|
63
|
+
request_id = resp.headers.get('X-Reve-Request-Id')
|
|
64
|
+
error_code = resp.headers.get('X-Reve-Error-Code')
|
|
65
|
+
match resp.status:
|
|
66
|
+
case 200:
|
|
67
|
+
if error_code is not None:
|
|
68
|
+
raise ReveUnexpectedError(
|
|
69
|
+
f'Reve request {request_id} returned an unexpected error {error_code}'
|
|
70
|
+
)
|
|
71
|
+
content_violation = resp.headers.get('X-Reve-Content-Violation', 'false')
|
|
72
|
+
if content_violation.lower() != 'false':
|
|
73
|
+
raise ReveContentViolationError(
|
|
74
|
+
f'Reve request {request_id} resulted in a content violation error'
|
|
75
|
+
)
|
|
76
|
+
if resp.content_type != requested_content_type:
|
|
77
|
+
raise ReveUnexpectedError(
|
|
78
|
+
f'Reve request {request_id} expected content type {requested_content_type}, '
|
|
79
|
+
f'got {resp.content_type}'
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
img_data = await resp.read()
|
|
83
|
+
if len(img_data) == 0:
|
|
84
|
+
raise ReveUnexpectedError(f'Reve request {request_id} resulted in an empty image')
|
|
85
|
+
img = PIL.Image.open(BytesIO(img_data))
|
|
86
|
+
img.load()
|
|
87
|
+
_logger.debug(
|
|
88
|
+
f'Reve request {request_id} successful. Image bytes: {len(img_data)}, size: {img.size}'
|
|
89
|
+
f', format: {img.format}, mode: {img.mode}'
|
|
90
|
+
)
|
|
91
|
+
return img
|
|
92
|
+
case 429:
|
|
93
|
+
# Try to honor the server-provided Retry-After value if present
|
|
94
|
+
# Note: Retry-After value can also be given in the form of HTTP Date, which we don't currently
|
|
95
|
+
# handle
|
|
96
|
+
retry_after_seconds = None
|
|
97
|
+
retry_after_header = resp.headers.get('Retry-After')
|
|
98
|
+
if retry_after_header is not None and re.fullmatch(r'\d{1,2}', retry_after_header):
|
|
99
|
+
retry_after_seconds = int(retry_after_header)
|
|
100
|
+
_logger.info(
|
|
101
|
+
f'Reve request {request_id} failed due to rate limiting, retry after header value: '
|
|
102
|
+
f'{retry_after_header}'
|
|
103
|
+
)
|
|
104
|
+
# This error message is formatted specifically so that RequestRateScheduler can extract the retry
|
|
105
|
+
# delay from it
|
|
106
|
+
raise ReveRateLimitedError(
|
|
107
|
+
f'Reve request {request_id} failed due to rate limiting (429). retry-after:'
|
|
108
|
+
f'{retry_after_seconds}'
|
|
109
|
+
)
|
|
110
|
+
case _:
|
|
111
|
+
_logger.info(
|
|
112
|
+
f'Reve request {request_id} failed with status code {resp.status} and error code {error_code}'
|
|
113
|
+
)
|
|
114
|
+
raise ReveUnexpectedError(
|
|
115
|
+
f'Reve request failed with status code {resp.status} and error code {error_code}'
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@register_client('reve')
|
|
120
|
+
def _(api_key: str) -> _ReveClient:
|
|
121
|
+
return _ReveClient(api_key=api_key)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _client() -> _ReveClient:
|
|
125
|
+
return Env.get().get_client('reve')
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# TODO Regarding rate limiting: Reve appears to be going for a credits per minute rate limiting model, but does not
|
|
129
|
+
# currently communicate rate limit information in responses. Therefore neither of the currently implemented limiting
|
|
130
|
+
# strategies is a perfect match, but "request-rate" is the closest. Reve does not currently enforce the rate limits,
|
|
131
|
+
# but when it does, we can revisit this choice.
|
|
132
|
+
@pxt.udf(resource_pool='request-rate:reve')
|
|
133
|
+
async def create(prompt: str, *, aspect_ratio: str | None = None, version: str | None = None) -> PIL.Image.Image:
|
|
134
|
+
"""
|
|
135
|
+
Creates an image from a text prompt.
|
|
136
|
+
|
|
137
|
+
This UDF wraps the `https://api.reve.com/v1/image/create` endpoint. For more information, refer to the official
|
|
138
|
+
[API documentation](https://api.reve.com/console/docs/create).
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
prompt: prompt describing the desired image
|
|
142
|
+
aspect_ratio: desired image aspect ratio, e.g. '3:2', '16:9', '1:1', etc.
|
|
143
|
+
version: specific model version to use. Latest if not specified.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
A generated image
|
|
147
|
+
|
|
148
|
+
Examples:
|
|
149
|
+
Add a computed column with generated square images to a table with text prompts:
|
|
150
|
+
|
|
151
|
+
>>> t.add_computed_column(
|
|
152
|
+
... img=reve.create(t.prompt, aspect_ratio='1:1')
|
|
153
|
+
... )
|
|
154
|
+
"""
|
|
155
|
+
payload = {'prompt': prompt}
|
|
156
|
+
if aspect_ratio is not None:
|
|
157
|
+
payload['aspect_ratio'] = aspect_ratio
|
|
158
|
+
if version is not None:
|
|
159
|
+
payload['version'] = version
|
|
160
|
+
|
|
161
|
+
result = await _client()._post('/v1/image/create', payload=payload)
|
|
162
|
+
return result
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@pxt.udf(resource_pool='request-rate:reve')
|
|
166
|
+
async def edit(image: PIL.Image.Image, edit_instruction: str, *, version: str | None = None) -> PIL.Image.Image:
|
|
167
|
+
"""
|
|
168
|
+
Edits images based on a text prompt.
|
|
169
|
+
|
|
170
|
+
This UDF wraps the `https://api.reve.com/v1/image/edit` endpoint. For more information, refer to the official
|
|
171
|
+
[API documentation](https://api.reve.com/console/docs/edit)
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
image: image to edit
|
|
175
|
+
edit_instruction: text prompt describing the desired edit
|
|
176
|
+
version: specific model version to use. Latest if not specified.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
A generated image
|
|
180
|
+
|
|
181
|
+
Examples:
|
|
182
|
+
Add a computed column with catalog-ready images to the table with product pictures:
|
|
183
|
+
|
|
184
|
+
>>> t.add_computed_column(
|
|
185
|
+
... catalog_img=reve.edit(
|
|
186
|
+
... t.product_img,
|
|
187
|
+
... 'Remove background and distractions from the product picture, improve lighting.'
|
|
188
|
+
... )
|
|
189
|
+
... )
|
|
190
|
+
"""
|
|
191
|
+
payload = {'edit_instruction': edit_instruction, 'reference_image': to_base64(image)}
|
|
192
|
+
if version is not None:
|
|
193
|
+
payload['version'] = version
|
|
194
|
+
|
|
195
|
+
result = await _client()._post('/v1/image/edit', payload=payload)
|
|
196
|
+
return result
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@pxt.udf(resource_pool='request-rate:reve')
|
|
200
|
+
async def remix(
|
|
201
|
+
prompt: str, images: list[PIL.Image.Image], *, aspect_ratio: str | None = None, version: str | None = None
|
|
202
|
+
) -> PIL.Image.Image:
|
|
203
|
+
"""
|
|
204
|
+
Creates images based on a text prompt and reference images.
|
|
205
|
+
|
|
206
|
+
The prompt may include `<img>0</img>`, `<img>1</img>`, etc. tags to refer to the images in the `images` argument.
|
|
207
|
+
|
|
208
|
+
This UDF wraps the `https://api.reve.com/v1/image/remix` endpoint. For more information, refer to the official
|
|
209
|
+
[API documentation](https://api.reve.com/console/docs/remix)
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
prompt: prompt describing the desired image
|
|
213
|
+
images: list of reference images
|
|
214
|
+
aspect_ratio: desired image aspect ratio, e.g. '3:2', '16:9', '1:1', etc.
|
|
215
|
+
version: specific model version to use. Latest by default.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
A generated image
|
|
219
|
+
|
|
220
|
+
Examples:
|
|
221
|
+
Add a computed column with promotional collages to a table with original images:
|
|
222
|
+
|
|
223
|
+
>>> t.add_computed_column(
|
|
224
|
+
... promo_img=(
|
|
225
|
+
... reve.remix(
|
|
226
|
+
... 'Generate a product promotional image by combining the image of the product'
|
|
227
|
+
... ' from <img>0</img> with the landmark scene from <img>1</img>',
|
|
228
|
+
... images=[t.product_img, t.local_landmark_img],
|
|
229
|
+
... aspect_ratio='16:9',
|
|
230
|
+
... )
|
|
231
|
+
... )
|
|
232
|
+
... )
|
|
233
|
+
"""
|
|
234
|
+
if len(images) == 0:
|
|
235
|
+
raise pxt.Error('Must include at least 1 reference image')
|
|
236
|
+
|
|
237
|
+
payload = {'prompt': prompt, 'reference_images': [to_base64(img) for img in images]}
|
|
238
|
+
if version is not None:
|
|
239
|
+
payload['version'] = version
|
|
240
|
+
if aspect_ratio is not None:
|
|
241
|
+
payload['aspect_ratio'] = aspect_ratio
|
|
242
|
+
result = await _client()._post('/v1/image/remix', payload=payload)
|
|
243
|
+
return result
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
__all__ = local_public_names(__name__)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def __dir__() -> list[str]:
|
|
250
|
+
return __all__
|