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.

Files changed (36) hide show
  1. pixeltable/_version.py +1 -1
  2. pixeltable/catalog/catalog.py +76 -50
  3. pixeltable/catalog/column.py +29 -16
  4. pixeltable/catalog/insertable_table.py +2 -2
  5. pixeltable/catalog/path.py +4 -10
  6. pixeltable/catalog/table.py +51 -0
  7. pixeltable/catalog/table_version.py +40 -7
  8. pixeltable/catalog/view.py +2 -2
  9. pixeltable/config.py +1 -0
  10. pixeltable/env.py +2 -0
  11. pixeltable/exprs/column_ref.py +2 -1
  12. pixeltable/functions/__init__.py +1 -0
  13. pixeltable/functions/image.py +2 -8
  14. pixeltable/functions/reve.py +250 -0
  15. pixeltable/functions/video.py +534 -1
  16. pixeltable/globals.py +2 -1
  17. pixeltable/index/base.py +5 -18
  18. pixeltable/index/btree.py +6 -2
  19. pixeltable/index/embedding_index.py +4 -4
  20. pixeltable/metadata/schema.py +7 -32
  21. pixeltable/share/__init__.py +1 -1
  22. pixeltable/share/packager.py +22 -18
  23. pixeltable/share/protocol/__init__.py +34 -0
  24. pixeltable/share/protocol/common.py +170 -0
  25. pixeltable/share/protocol/operation_types.py +33 -0
  26. pixeltable/share/protocol/replica.py +109 -0
  27. pixeltable/share/publish.py +90 -56
  28. pixeltable/store.py +11 -15
  29. pixeltable/utils/av.py +87 -1
  30. pixeltable/utils/dbms.py +15 -11
  31. pixeltable/utils/image.py +10 -0
  32. {pixeltable-0.4.19.dist-info → pixeltable-0.4.20.dist-info}/METADATA +2 -1
  33. {pixeltable-0.4.19.dist-info → pixeltable-0.4.20.dist-info}/RECORD +36 -31
  34. {pixeltable-0.4.19.dist-info → pixeltable-0.4.20.dist-info}/WHEEL +0 -0
  35. {pixeltable-0.4.19.dist-info → pixeltable-0.4.20.dist-info}/entry_points.txt +0 -0
  36. {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__