diffio 0.1.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.
- diffio/__init__.py +59 -0
- diffio/client.py +1460 -0
- diffio/errors.py +15 -0
- diffio/testing.py +296 -0
- diffio/types.py +333 -0
- diffio-0.1.0.dist-info/METADATA +234 -0
- diffio-0.1.0.dist-info/RECORD +10 -0
- diffio-0.1.0.dist-info/WHEEL +5 -0
- diffio-0.1.0.dist-info/licenses/LICENSE +21 -0
- diffio-0.1.0.dist-info/top_level.txt +1 -0
diffio/client.py
ADDED
|
@@ -0,0 +1,1460 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import mimetypes
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
import time
|
|
6
|
+
import warnings
|
|
7
|
+
from urllib.parse import urlparse
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from .errors import DiffioApiError
|
|
12
|
+
from .types import (
|
|
13
|
+
AudioIsolationResult,
|
|
14
|
+
CreateGenerationResponse,
|
|
15
|
+
CreateProjectResponse,
|
|
16
|
+
DownloadType,
|
|
17
|
+
GenerationDownloadResponse,
|
|
18
|
+
GenerationProgressResponse,
|
|
19
|
+
ListProjectGenerationsResponse,
|
|
20
|
+
ListProjectsResponse,
|
|
21
|
+
ModelKey,
|
|
22
|
+
WebhookEventType,
|
|
23
|
+
WebhookMode,
|
|
24
|
+
WebhookPortalResponse,
|
|
25
|
+
WebhookTestEventResponse,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
DEFAULT_BASE_URL = "https://us-central1-diffioai.cloudfunctions.net"
|
|
29
|
+
API_PREFIX = "v1"
|
|
30
|
+
MODEL_ENDPOINTS = {
|
|
31
|
+
"diffio-2": "diffio-2.0-generation",
|
|
32
|
+
"diffio-2-flash": "diffio-2.0-flash-generation",
|
|
33
|
+
"diffio-3": "diffio-3.0-generation",
|
|
34
|
+
}
|
|
35
|
+
DEFAULT_RETRY_STATUS_CODES = [408, 429, 500, 502, 503, 504]
|
|
36
|
+
DEFAULT_RETRY_BACKOFF = 0.5
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class RequestOptions:
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
*,
|
|
43
|
+
headers=None,
|
|
44
|
+
timeout=None,
|
|
45
|
+
timeoutInSeconds=None,
|
|
46
|
+
maxRetries=None,
|
|
47
|
+
retryBackoff=None,
|
|
48
|
+
retryStatusCodes=None,
|
|
49
|
+
apiKey=None,
|
|
50
|
+
):
|
|
51
|
+
self.headers = headers or {}
|
|
52
|
+
self.timeout = timeoutInSeconds if timeoutInSeconds is not None else timeout
|
|
53
|
+
self.maxRetries = maxRetries
|
|
54
|
+
self.retryBackoff = retryBackoff
|
|
55
|
+
self.retryStatusCodes = retryStatusCodes
|
|
56
|
+
self.apiKey = apiKey
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _normalize_request_options(options):
|
|
60
|
+
if options is None:
|
|
61
|
+
return RequestOptions()
|
|
62
|
+
if isinstance(options, RequestOptions):
|
|
63
|
+
return options
|
|
64
|
+
if isinstance(options, dict):
|
|
65
|
+
timeout = options.get("timeout")
|
|
66
|
+
timeout_in_seconds = options.get("timeoutInSeconds")
|
|
67
|
+
if timeout_in_seconds is not None:
|
|
68
|
+
timeout = timeout_in_seconds
|
|
69
|
+
return RequestOptions(
|
|
70
|
+
headers=options.get("headers"),
|
|
71
|
+
timeout=timeout,
|
|
72
|
+
maxRetries=options.get("maxRetries"),
|
|
73
|
+
retryBackoff=options.get("retryBackoff"),
|
|
74
|
+
retryStatusCodes=options.get("retryStatusCodes"),
|
|
75
|
+
apiKey=options.get("apiKey"),
|
|
76
|
+
)
|
|
77
|
+
raise ValueError("requestOptions must be a RequestOptions or dict")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _merge_request_options(base_options, override_options):
|
|
81
|
+
if override_options is None:
|
|
82
|
+
return base_options
|
|
83
|
+
override = _normalize_request_options(override_options)
|
|
84
|
+
headers = {}
|
|
85
|
+
if base_options.headers:
|
|
86
|
+
headers.update(base_options.headers)
|
|
87
|
+
if override.headers:
|
|
88
|
+
headers.update(override.headers)
|
|
89
|
+
return RequestOptions(
|
|
90
|
+
headers=headers,
|
|
91
|
+
timeout=override.timeout if override.timeout is not None else base_options.timeout,
|
|
92
|
+
maxRetries=override.maxRetries if override.maxRetries is not None else base_options.maxRetries,
|
|
93
|
+
retryBackoff=override.retryBackoff if override.retryBackoff is not None else base_options.retryBackoff,
|
|
94
|
+
retryStatusCodes=(
|
|
95
|
+
override.retryStatusCodes
|
|
96
|
+
if override.retryStatusCodes is not None
|
|
97
|
+
else base_options.retryStatusCodes
|
|
98
|
+
),
|
|
99
|
+
apiKey=override.apiKey if override.apiKey is not None else base_options.apiKey,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _default_request_options():
|
|
104
|
+
return RequestOptions(
|
|
105
|
+
headers={},
|
|
106
|
+
timeout=None,
|
|
107
|
+
maxRetries=0,
|
|
108
|
+
retryBackoff=DEFAULT_RETRY_BACKOFF,
|
|
109
|
+
retryStatusCodes=list(DEFAULT_RETRY_STATUS_CODES),
|
|
110
|
+
apiKey=None,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class DiffioClient:
|
|
115
|
+
def __init__(
|
|
116
|
+
self,
|
|
117
|
+
*,
|
|
118
|
+
apiKey=None,
|
|
119
|
+
baseUrl=None,
|
|
120
|
+
timeout=60.0,
|
|
121
|
+
timeoutInSeconds=None,
|
|
122
|
+
httpClient=None,
|
|
123
|
+
requestOptions=None,
|
|
124
|
+
):
|
|
125
|
+
resolved_key = apiKey or os.environ.get("DIFFIO_API_KEY")
|
|
126
|
+
if not resolved_key:
|
|
127
|
+
raise ValueError("apiKey is required")
|
|
128
|
+
|
|
129
|
+
resolved_base = baseUrl or os.environ.get("DIFFIO_API_BASE_URL") or DEFAULT_BASE_URL
|
|
130
|
+
resolved_base = resolved_base.rstrip("/")
|
|
131
|
+
api_prefix = "" if resolved_base.endswith(f"/{API_PREFIX}") else API_PREFIX
|
|
132
|
+
|
|
133
|
+
self.apiKey = resolved_key
|
|
134
|
+
self.baseUrl = resolved_base
|
|
135
|
+
self._api_prefix = api_prefix
|
|
136
|
+
resolved_timeout = timeoutInSeconds if timeoutInSeconds is not None else timeout
|
|
137
|
+
if httpClient is None:
|
|
138
|
+
self._client = httpx.Client(base_url=resolved_base, timeout=resolved_timeout)
|
|
139
|
+
self._owns_client = True
|
|
140
|
+
else:
|
|
141
|
+
self._client = httpClient
|
|
142
|
+
self._owns_client = False
|
|
143
|
+
self._default_request_options = _merge_request_options(_default_request_options(), requestOptions)
|
|
144
|
+
|
|
145
|
+
self.audio_isolation = AudioIsolationClient(self)
|
|
146
|
+
self.generations = GenerationsClient(self)
|
|
147
|
+
self.projects = ProjectsClient(self)
|
|
148
|
+
self.webhooks = WebhooksClient(self)
|
|
149
|
+
|
|
150
|
+
def __enter__(self):
|
|
151
|
+
return self
|
|
152
|
+
|
|
153
|
+
def __exit__(self, exc_type, exc, tb):
|
|
154
|
+
self.close()
|
|
155
|
+
|
|
156
|
+
def close(self):
|
|
157
|
+
if self._owns_client:
|
|
158
|
+
self._client.close()
|
|
159
|
+
|
|
160
|
+
def create_project(
|
|
161
|
+
self,
|
|
162
|
+
*,
|
|
163
|
+
filePath,
|
|
164
|
+
contentType=None,
|
|
165
|
+
contentLength=None,
|
|
166
|
+
params=None,
|
|
167
|
+
fileFormat=None,
|
|
168
|
+
requestOptions=None,
|
|
169
|
+
):
|
|
170
|
+
payload = _build_create_project_payload(
|
|
171
|
+
filePath=filePath,
|
|
172
|
+
contentType=contentType,
|
|
173
|
+
contentLength=contentLength,
|
|
174
|
+
params=params,
|
|
175
|
+
fileFormat=fileFormat,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
response = self._request("POST", "create_project", json_payload=payload, requestOptions=requestOptions)
|
|
179
|
+
project = CreateProjectResponse.from_dict(response)
|
|
180
|
+
self._upload_file(
|
|
181
|
+
uploadUrl=project.uploadUrl,
|
|
182
|
+
uploadMethod=project.uploadMethod,
|
|
183
|
+
filePath=filePath,
|
|
184
|
+
contentType=payload["contentType"],
|
|
185
|
+
requestOptions=requestOptions,
|
|
186
|
+
)
|
|
187
|
+
return project
|
|
188
|
+
|
|
189
|
+
def _upload_file(
|
|
190
|
+
self,
|
|
191
|
+
*,
|
|
192
|
+
uploadUrl,
|
|
193
|
+
uploadMethod=None,
|
|
194
|
+
filePath=None,
|
|
195
|
+
data=None,
|
|
196
|
+
contentType=None,
|
|
197
|
+
requestOptions=None,
|
|
198
|
+
):
|
|
199
|
+
if (filePath is None) == (data is None):
|
|
200
|
+
raise ValueError("Provide filePath or data")
|
|
201
|
+
|
|
202
|
+
resolved_content_type = contentType
|
|
203
|
+
if resolved_content_type is None and filePath is not None:
|
|
204
|
+
resolved_content_type = _guess_content_type(filePath)
|
|
205
|
+
if resolved_content_type is None:
|
|
206
|
+
resolved_content_type = "application/octet-stream"
|
|
207
|
+
|
|
208
|
+
method = (uploadMethod or "PUT").upper()
|
|
209
|
+
merged_options = _merge_request_options(self._default_request_options, requestOptions)
|
|
210
|
+
headers = {"Content-Type": resolved_content_type}
|
|
211
|
+
if _is_storage_emulator_url(uploadUrl):
|
|
212
|
+
headers["Authorization"] = "Bearer owner"
|
|
213
|
+
headers = _merge_headers(headers, merged_options.headers)
|
|
214
|
+
timeout = merged_options.timeout
|
|
215
|
+
max_retries = merged_options.maxRetries if merged_options.maxRetries is not None else 0
|
|
216
|
+
retry_backoff = (
|
|
217
|
+
merged_options.retryBackoff if merged_options.retryBackoff is not None else DEFAULT_RETRY_BACKOFF
|
|
218
|
+
)
|
|
219
|
+
retry_statuses = (
|
|
220
|
+
merged_options.retryStatusCodes
|
|
221
|
+
if merged_options.retryStatusCodes is not None
|
|
222
|
+
else DEFAULT_RETRY_STATUS_CODES
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
attempt = 0
|
|
226
|
+
while True:
|
|
227
|
+
try:
|
|
228
|
+
if filePath is not None:
|
|
229
|
+
with open(filePath, "rb") as handle:
|
|
230
|
+
request_kwargs = {
|
|
231
|
+
"method": method,
|
|
232
|
+
"url": uploadUrl,
|
|
233
|
+
"headers": headers,
|
|
234
|
+
"content": handle,
|
|
235
|
+
}
|
|
236
|
+
if timeout is not None:
|
|
237
|
+
request_kwargs["timeout"] = timeout
|
|
238
|
+
response = self._client.request(**request_kwargs)
|
|
239
|
+
else:
|
|
240
|
+
request_kwargs = {
|
|
241
|
+
"method": method,
|
|
242
|
+
"url": uploadUrl,
|
|
243
|
+
"headers": headers,
|
|
244
|
+
"content": data,
|
|
245
|
+
}
|
|
246
|
+
if timeout is not None:
|
|
247
|
+
request_kwargs["timeout"] = timeout
|
|
248
|
+
response = self._client.request(**request_kwargs)
|
|
249
|
+
except httpx.RequestError:
|
|
250
|
+
if attempt >= max_retries:
|
|
251
|
+
raise
|
|
252
|
+
_sleep_retry(attempt, retry_backoff)
|
|
253
|
+
attempt += 1
|
|
254
|
+
continue
|
|
255
|
+
|
|
256
|
+
if retry_statuses and response.status_code in retry_statuses and attempt < max_retries:
|
|
257
|
+
response.close()
|
|
258
|
+
_sleep_retry(attempt, retry_backoff)
|
|
259
|
+
attempt += 1
|
|
260
|
+
continue
|
|
261
|
+
|
|
262
|
+
_raise_for_error(response)
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
def create_generation(
|
|
266
|
+
self,
|
|
267
|
+
*,
|
|
268
|
+
apiProjectId,
|
|
269
|
+
model="diffio-2",
|
|
270
|
+
sampling=None,
|
|
271
|
+
params=None,
|
|
272
|
+
requestOptions=None,
|
|
273
|
+
):
|
|
274
|
+
endpoint = MODEL_ENDPOINTS.get(model)
|
|
275
|
+
if not endpoint:
|
|
276
|
+
raise ValueError(f"Unsupported model: {model}")
|
|
277
|
+
|
|
278
|
+
payload = {"apiProjectId": apiProjectId}
|
|
279
|
+
if sampling is not None:
|
|
280
|
+
payload["sampling"] = sampling
|
|
281
|
+
if params:
|
|
282
|
+
payload["params"] = params
|
|
283
|
+
|
|
284
|
+
response = self._request("POST", endpoint, json_payload=payload, requestOptions=requestOptions)
|
|
285
|
+
return CreateGenerationResponse.from_dict(response)
|
|
286
|
+
|
|
287
|
+
def list_projects(self, *, requestOptions=None):
|
|
288
|
+
"""
|
|
289
|
+
Lists projects owned by the API key.
|
|
290
|
+
|
|
291
|
+
Returns
|
|
292
|
+
-------
|
|
293
|
+
ListProjectsResponse
|
|
294
|
+
Project summaries ordered by creation time.
|
|
295
|
+
|
|
296
|
+
Examples
|
|
297
|
+
--------
|
|
298
|
+
from diffio import DiffioClient
|
|
299
|
+
|
|
300
|
+
client = DiffioClient(apiKey="diffio_live_...")
|
|
301
|
+
projects = client.list_projects()
|
|
302
|
+
print(projects.projects[0].apiProjectId)
|
|
303
|
+
"""
|
|
304
|
+
response = self._request("POST", "list_projects", json_payload={}, requestOptions=requestOptions)
|
|
305
|
+
return ListProjectsResponse.from_dict(response)
|
|
306
|
+
|
|
307
|
+
def list_project_generations(
|
|
308
|
+
self,
|
|
309
|
+
*,
|
|
310
|
+
apiProjectId,
|
|
311
|
+
requestOptions=None,
|
|
312
|
+
):
|
|
313
|
+
"""
|
|
314
|
+
Lists generations for a project owned by the API key.
|
|
315
|
+
|
|
316
|
+
Parameters
|
|
317
|
+
----------
|
|
318
|
+
apiProjectId : str
|
|
319
|
+
The project id to list generations for.
|
|
320
|
+
|
|
321
|
+
Returns
|
|
322
|
+
-------
|
|
323
|
+
ListProjectGenerationsResponse
|
|
324
|
+
Generation summaries ordered by creation time.
|
|
325
|
+
|
|
326
|
+
Examples
|
|
327
|
+
--------
|
|
328
|
+
from diffio import DiffioClient
|
|
329
|
+
|
|
330
|
+
client = DiffioClient(apiKey="diffio_live_...")
|
|
331
|
+
generations = client.list_project_generations(apiProjectId="proj_123")
|
|
332
|
+
print(generations.generations[0].generationId)
|
|
333
|
+
"""
|
|
334
|
+
if not apiProjectId:
|
|
335
|
+
raise ValueError("apiProjectId is required")
|
|
336
|
+
response = self._request(
|
|
337
|
+
"POST",
|
|
338
|
+
"list_project_generations",
|
|
339
|
+
json_payload={"apiProjectId": apiProjectId},
|
|
340
|
+
requestOptions=requestOptions,
|
|
341
|
+
)
|
|
342
|
+
return ListProjectGenerationsResponse.from_dict(response)
|
|
343
|
+
|
|
344
|
+
def get_generation_progress(
|
|
345
|
+
self,
|
|
346
|
+
*,
|
|
347
|
+
generationId,
|
|
348
|
+
apiProjectId=None,
|
|
349
|
+
requestOptions=None,
|
|
350
|
+
):
|
|
351
|
+
"""
|
|
352
|
+
Gets progress for a generation.
|
|
353
|
+
|
|
354
|
+
Parameters
|
|
355
|
+
----------
|
|
356
|
+
generationId : str
|
|
357
|
+
The generation id to query.
|
|
358
|
+
apiProjectId : str, optional
|
|
359
|
+
Optional project id, used to speed up lookup.
|
|
360
|
+
|
|
361
|
+
Returns
|
|
362
|
+
-------
|
|
363
|
+
GenerationProgressResponse
|
|
364
|
+
Current generation progress.
|
|
365
|
+
|
|
366
|
+
Examples
|
|
367
|
+
--------
|
|
368
|
+
from diffio import DiffioClient
|
|
369
|
+
|
|
370
|
+
client = DiffioClient(apiKey="diffio_live_...")
|
|
371
|
+
progress = client.get_generation_progress(
|
|
372
|
+
generationId="gen_123",
|
|
373
|
+
apiProjectId="proj_123",
|
|
374
|
+
)
|
|
375
|
+
print(progress.status)
|
|
376
|
+
"""
|
|
377
|
+
payload = {"generationId": generationId}
|
|
378
|
+
if apiProjectId is not None:
|
|
379
|
+
payload["apiProjectId"] = apiProjectId
|
|
380
|
+
|
|
381
|
+
response = self._request(
|
|
382
|
+
"POST",
|
|
383
|
+
"get_generation_progress",
|
|
384
|
+
json_payload=payload,
|
|
385
|
+
requestOptions=requestOptions,
|
|
386
|
+
)
|
|
387
|
+
return GenerationProgressResponse.from_dict(response)
|
|
388
|
+
|
|
389
|
+
def wait_for_generation(
|
|
390
|
+
self,
|
|
391
|
+
*,
|
|
392
|
+
generationId,
|
|
393
|
+
apiProjectId=None,
|
|
394
|
+
pollInterval=2.0,
|
|
395
|
+
timeout=600.0,
|
|
396
|
+
onProgress=None,
|
|
397
|
+
showProgress=False,
|
|
398
|
+
requestOptions=None,
|
|
399
|
+
):
|
|
400
|
+
"""
|
|
401
|
+
Polls generation progress until completion or failure.
|
|
402
|
+
|
|
403
|
+
Parameters
|
|
404
|
+
----------
|
|
405
|
+
generationId : str
|
|
406
|
+
The generation id to wait for.
|
|
407
|
+
apiProjectId : str, optional
|
|
408
|
+
Optional project id, used to speed up lookup.
|
|
409
|
+
pollInterval : float
|
|
410
|
+
Seconds to wait between polls.
|
|
411
|
+
timeout : float
|
|
412
|
+
Maximum seconds to wait before timing out.
|
|
413
|
+
onProgress : callable, optional
|
|
414
|
+
Callback invoked with each GenerationProgressResponse.
|
|
415
|
+
showProgress : bool
|
|
416
|
+
If true, prints progress updates to stdout.
|
|
417
|
+
"""
|
|
418
|
+
deadline = time.monotonic() + timeout
|
|
419
|
+
last_progress = None
|
|
420
|
+
|
|
421
|
+
while time.monotonic() < deadline:
|
|
422
|
+
progress = self.get_generation_progress(
|
|
423
|
+
generationId=generationId,
|
|
424
|
+
apiProjectId=apiProjectId,
|
|
425
|
+
requestOptions=requestOptions,
|
|
426
|
+
)
|
|
427
|
+
last_progress = progress
|
|
428
|
+
_report_progress(progress, onProgress=onProgress, showProgress=showProgress)
|
|
429
|
+
|
|
430
|
+
if progress.status == "complete":
|
|
431
|
+
return progress
|
|
432
|
+
|
|
433
|
+
if progress.status == "failed":
|
|
434
|
+
raise RuntimeError(
|
|
435
|
+
"Generation failed"
|
|
436
|
+
f" (preProcessing={progress.preProcessing.status},"
|
|
437
|
+
f" inference={progress.inference.status},"
|
|
438
|
+
f" error={progress.error},"
|
|
439
|
+
f" details={progress.errorDetails})"
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
time.sleep(pollInterval)
|
|
443
|
+
|
|
444
|
+
raise RuntimeError(
|
|
445
|
+
"Timed out waiting for generation completion"
|
|
446
|
+
f" (lastStatus={getattr(last_progress, 'status', None)})"
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
def get_generation_download(
|
|
450
|
+
self,
|
|
451
|
+
*,
|
|
452
|
+
generationId,
|
|
453
|
+
apiProjectId,
|
|
454
|
+
downloadType=None,
|
|
455
|
+
requestOptions=None,
|
|
456
|
+
):
|
|
457
|
+
"""
|
|
458
|
+
Gets a signed download URL for a generation.
|
|
459
|
+
|
|
460
|
+
Parameters
|
|
461
|
+
----------
|
|
462
|
+
generationId : str
|
|
463
|
+
The generation id to download.
|
|
464
|
+
apiProjectId : str
|
|
465
|
+
The project id that owns the generation.
|
|
466
|
+
downloadType : str, optional
|
|
467
|
+
Optional download type, audio, mp3, or video.
|
|
468
|
+
|
|
469
|
+
Returns
|
|
470
|
+
-------
|
|
471
|
+
GenerationDownloadResponse
|
|
472
|
+
Signed download URL and file metadata.
|
|
473
|
+
|
|
474
|
+
Examples
|
|
475
|
+
--------
|
|
476
|
+
from diffio import DiffioClient
|
|
477
|
+
|
|
478
|
+
client = DiffioClient(apiKey="diffio_live_...")
|
|
479
|
+
download = client.get_generation_download(
|
|
480
|
+
generationId="gen_123",
|
|
481
|
+
apiProjectId="proj_123",
|
|
482
|
+
downloadType="audio",
|
|
483
|
+
)
|
|
484
|
+
print(download.downloadUrl)
|
|
485
|
+
"""
|
|
486
|
+
payload = {"generationId": generationId, "apiProjectId": apiProjectId}
|
|
487
|
+
if downloadType is not None:
|
|
488
|
+
resolved_download_type, _ = _normalize_download_type(downloadType)
|
|
489
|
+
payload["downloadType"] = resolved_download_type
|
|
490
|
+
|
|
491
|
+
response = self._request(
|
|
492
|
+
"POST",
|
|
493
|
+
"get_generation_download",
|
|
494
|
+
json_payload=payload,
|
|
495
|
+
requestOptions=requestOptions,
|
|
496
|
+
)
|
|
497
|
+
return GenerationDownloadResponse.from_dict(response)
|
|
498
|
+
|
|
499
|
+
def get_webhooks_portal_access(
|
|
500
|
+
self,
|
|
501
|
+
*,
|
|
502
|
+
mode,
|
|
503
|
+
apiKeyId=None,
|
|
504
|
+
requestOptions=None,
|
|
505
|
+
):
|
|
506
|
+
"""
|
|
507
|
+
Gets the Svix App Portal URL for configuring webhooks.
|
|
508
|
+
|
|
509
|
+
Parameters
|
|
510
|
+
----------
|
|
511
|
+
mode : str
|
|
512
|
+
Webhook mode, test or live.
|
|
513
|
+
apiKeyId : str, optional
|
|
514
|
+
Optional API key id to validate access.
|
|
515
|
+
|
|
516
|
+
Returns
|
|
517
|
+
-------
|
|
518
|
+
WebhookPortalResponse
|
|
519
|
+
App portal URL and mode.
|
|
520
|
+
"""
|
|
521
|
+
if mode not in WebhookMode:
|
|
522
|
+
raise ValueError("mode must be test or live")
|
|
523
|
+
payload = {"mode": mode}
|
|
524
|
+
if apiKeyId is not None:
|
|
525
|
+
payload["apiKeyId"] = apiKeyId
|
|
526
|
+
|
|
527
|
+
response = self._request(
|
|
528
|
+
"POST",
|
|
529
|
+
"webhooks/app_portal_access",
|
|
530
|
+
json_payload=payload,
|
|
531
|
+
requestOptions=requestOptions,
|
|
532
|
+
)
|
|
533
|
+
return WebhookPortalResponse.from_dict(response)
|
|
534
|
+
|
|
535
|
+
def send_webhook_test_event(
|
|
536
|
+
self,
|
|
537
|
+
*,
|
|
538
|
+
eventType,
|
|
539
|
+
mode,
|
|
540
|
+
apiKeyId=None,
|
|
541
|
+
samplePayload=None,
|
|
542
|
+
requestOptions=None,
|
|
543
|
+
):
|
|
544
|
+
"""
|
|
545
|
+
Sends a test webhook event.
|
|
546
|
+
|
|
547
|
+
Parameters
|
|
548
|
+
----------
|
|
549
|
+
eventType : str
|
|
550
|
+
One of the supported generation webhook event types.
|
|
551
|
+
mode : str
|
|
552
|
+
Webhook mode, test or live.
|
|
553
|
+
apiKeyId : str, optional
|
|
554
|
+
Optional API key id to validate access.
|
|
555
|
+
samplePayload : dict, optional
|
|
556
|
+
Sample payload overrides. Must be a dict.
|
|
557
|
+
|
|
558
|
+
Returns
|
|
559
|
+
-------
|
|
560
|
+
WebhookTestEventResponse
|
|
561
|
+
Webhook message metadata.
|
|
562
|
+
"""
|
|
563
|
+
if eventType not in WebhookEventType:
|
|
564
|
+
raise ValueError("eventType is not supported")
|
|
565
|
+
if mode not in WebhookMode:
|
|
566
|
+
raise ValueError("mode must be test or live")
|
|
567
|
+
if samplePayload is not None and not isinstance(samplePayload, dict):
|
|
568
|
+
raise ValueError("samplePayload must be an object")
|
|
569
|
+
|
|
570
|
+
payload = {"eventType": eventType, "mode": mode}
|
|
571
|
+
if apiKeyId is not None:
|
|
572
|
+
payload["apiKeyId"] = apiKeyId
|
|
573
|
+
if samplePayload is not None:
|
|
574
|
+
payload["samplePayload"] = samplePayload
|
|
575
|
+
|
|
576
|
+
response = self._request(
|
|
577
|
+
"POST",
|
|
578
|
+
"webhooks/send_test_event",
|
|
579
|
+
json_payload=payload,
|
|
580
|
+
requestOptions=requestOptions,
|
|
581
|
+
)
|
|
582
|
+
return WebhookTestEventResponse.from_dict(response)
|
|
583
|
+
|
|
584
|
+
def restore_audio(
|
|
585
|
+
self,
|
|
586
|
+
*,
|
|
587
|
+
filePath,
|
|
588
|
+
contentType=None,
|
|
589
|
+
contentLength=None,
|
|
590
|
+
fileFormat=None,
|
|
591
|
+
model="diffio-2",
|
|
592
|
+
sampling=None,
|
|
593
|
+
projectParams=None,
|
|
594
|
+
generationParams=None,
|
|
595
|
+
downloadType="audio",
|
|
596
|
+
pollInterval=2.0,
|
|
597
|
+
timeout=600.0,
|
|
598
|
+
onProgress=None,
|
|
599
|
+
showProgress=False,
|
|
600
|
+
requestOptions=None,
|
|
601
|
+
progressRequestOptions=None,
|
|
602
|
+
downloadRequestOptions=None,
|
|
603
|
+
raiseOnError=False,
|
|
604
|
+
):
|
|
605
|
+
return self.audio_isolation.restore_audio(
|
|
606
|
+
filePath=filePath,
|
|
607
|
+
contentType=contentType,
|
|
608
|
+
contentLength=contentLength,
|
|
609
|
+
fileFormat=fileFormat,
|
|
610
|
+
model=model,
|
|
611
|
+
sampling=sampling,
|
|
612
|
+
projectParams=projectParams,
|
|
613
|
+
generationParams=generationParams,
|
|
614
|
+
downloadType=downloadType,
|
|
615
|
+
pollInterval=pollInterval,
|
|
616
|
+
timeout=timeout,
|
|
617
|
+
onProgress=onProgress,
|
|
618
|
+
showProgress=showProgress,
|
|
619
|
+
requestOptions=requestOptions,
|
|
620
|
+
progressRequestOptions=progressRequestOptions,
|
|
621
|
+
downloadRequestOptions=downloadRequestOptions,
|
|
622
|
+
raiseOnError=raiseOnError,
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
def _request(self, method, path, *, json_payload, requestOptions=None):
|
|
626
|
+
request_path = path.lstrip("/")
|
|
627
|
+
if self._api_prefix:
|
|
628
|
+
request_path = f"{self._api_prefix}/{request_path}"
|
|
629
|
+
merged_options = _merge_request_options(self._default_request_options, requestOptions)
|
|
630
|
+
api_key = merged_options.apiKey or self.apiKey
|
|
631
|
+
headers = _merge_headers({"Authorization": f"Bearer {api_key}"}, merged_options.headers)
|
|
632
|
+
timeout = merged_options.timeout
|
|
633
|
+
max_retries = merged_options.maxRetries if merged_options.maxRetries is not None else 0
|
|
634
|
+
retry_backoff = (
|
|
635
|
+
merged_options.retryBackoff if merged_options.retryBackoff is not None else DEFAULT_RETRY_BACKOFF
|
|
636
|
+
)
|
|
637
|
+
retry_statuses = (
|
|
638
|
+
merged_options.retryStatusCodes
|
|
639
|
+
if merged_options.retryStatusCodes is not None
|
|
640
|
+
else DEFAULT_RETRY_STATUS_CODES
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
attempt = 0
|
|
644
|
+
while True:
|
|
645
|
+
try:
|
|
646
|
+
request_kwargs = {
|
|
647
|
+
"method": method,
|
|
648
|
+
"url": request_path,
|
|
649
|
+
"headers": headers,
|
|
650
|
+
"json": json_payload,
|
|
651
|
+
}
|
|
652
|
+
if timeout is not None:
|
|
653
|
+
request_kwargs["timeout"] = timeout
|
|
654
|
+
response = self._client.request(**request_kwargs)
|
|
655
|
+
except httpx.RequestError:
|
|
656
|
+
if attempt >= max_retries:
|
|
657
|
+
raise
|
|
658
|
+
_sleep_retry(attempt, retry_backoff)
|
|
659
|
+
attempt += 1
|
|
660
|
+
continue
|
|
661
|
+
|
|
662
|
+
if retry_statuses and response.status_code in retry_statuses and attempt < max_retries:
|
|
663
|
+
response.close()
|
|
664
|
+
_sleep_retry(attempt, retry_backoff)
|
|
665
|
+
attempt += 1
|
|
666
|
+
continue
|
|
667
|
+
|
|
668
|
+
return _raise_for_error(response)
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
class GenerationsClient:
|
|
672
|
+
def __init__(self, parent):
|
|
673
|
+
self._parent = parent
|
|
674
|
+
|
|
675
|
+
def create(
|
|
676
|
+
self,
|
|
677
|
+
*,
|
|
678
|
+
apiProjectId,
|
|
679
|
+
model="diffio-2",
|
|
680
|
+
sampling=None,
|
|
681
|
+
params=None,
|
|
682
|
+
requestOptions=None,
|
|
683
|
+
):
|
|
684
|
+
return self._parent.create_generation(
|
|
685
|
+
apiProjectId=apiProjectId,
|
|
686
|
+
model=model,
|
|
687
|
+
sampling=sampling,
|
|
688
|
+
params=params,
|
|
689
|
+
requestOptions=requestOptions,
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
def get_progress(
|
|
693
|
+
self,
|
|
694
|
+
*,
|
|
695
|
+
generationId,
|
|
696
|
+
apiProjectId=None,
|
|
697
|
+
requestOptions=None,
|
|
698
|
+
):
|
|
699
|
+
return self._parent.get_generation_progress(
|
|
700
|
+
generationId=generationId,
|
|
701
|
+
apiProjectId=apiProjectId,
|
|
702
|
+
requestOptions=requestOptions,
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
def get_download(
|
|
706
|
+
self,
|
|
707
|
+
*,
|
|
708
|
+
generationId,
|
|
709
|
+
apiProjectId,
|
|
710
|
+
downloadType=None,
|
|
711
|
+
requestOptions=None,
|
|
712
|
+
):
|
|
713
|
+
return self._parent.get_generation_download(
|
|
714
|
+
generationId=generationId,
|
|
715
|
+
apiProjectId=apiProjectId,
|
|
716
|
+
downloadType=downloadType,
|
|
717
|
+
requestOptions=requestOptions,
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
def download(
|
|
721
|
+
self,
|
|
722
|
+
*,
|
|
723
|
+
generationId,
|
|
724
|
+
apiProjectId,
|
|
725
|
+
downloadFilePath,
|
|
726
|
+
downloadType=None,
|
|
727
|
+
requestOptions=None,
|
|
728
|
+
):
|
|
729
|
+
"""
|
|
730
|
+
Downloads a generation result directly to a file path.
|
|
731
|
+
|
|
732
|
+
Parameters
|
|
733
|
+
----------
|
|
734
|
+
generationId : str
|
|
735
|
+
The generation id to download.
|
|
736
|
+
apiProjectId : str
|
|
737
|
+
The project id that owns the generation.
|
|
738
|
+
downloadFilePath : str
|
|
739
|
+
Local file path to write the downloaded media to.
|
|
740
|
+
downloadType : str, optional
|
|
741
|
+
Optional download type, audio, mp3, or video.
|
|
742
|
+
|
|
743
|
+
Returns
|
|
744
|
+
-------
|
|
745
|
+
GenerationDownloadResponse
|
|
746
|
+
Download metadata for the saved file.
|
|
747
|
+
"""
|
|
748
|
+
resolved_path = os.fspath(downloadFilePath)
|
|
749
|
+
if not resolved_path:
|
|
750
|
+
raise ValueError("downloadFilePath is required")
|
|
751
|
+
if downloadType is not None:
|
|
752
|
+
resolved_download_type, _ = _normalize_download_type(downloadType)
|
|
753
|
+
else:
|
|
754
|
+
resolved_download_type = None
|
|
755
|
+
|
|
756
|
+
download = self._parent.get_generation_download(
|
|
757
|
+
generationId=generationId,
|
|
758
|
+
apiProjectId=apiProjectId,
|
|
759
|
+
downloadType=resolved_download_type,
|
|
760
|
+
requestOptions=requestOptions,
|
|
761
|
+
)
|
|
762
|
+
_warn_download_extension_mismatch(download, resolved_path)
|
|
763
|
+
_download_to_file(self._parent, download.downloadUrl, resolved_path, requestOptions=requestOptions)
|
|
764
|
+
return download
|
|
765
|
+
|
|
766
|
+
def wait_for_complete(
|
|
767
|
+
self,
|
|
768
|
+
*,
|
|
769
|
+
generationId,
|
|
770
|
+
apiProjectId=None,
|
|
771
|
+
pollInterval=2.0,
|
|
772
|
+
timeout=600.0,
|
|
773
|
+
onProgress=None,
|
|
774
|
+
showProgress=False,
|
|
775
|
+
requestOptions=None,
|
|
776
|
+
):
|
|
777
|
+
return self._parent.wait_for_generation(
|
|
778
|
+
generationId=generationId,
|
|
779
|
+
apiProjectId=apiProjectId,
|
|
780
|
+
pollInterval=pollInterval,
|
|
781
|
+
timeout=timeout,
|
|
782
|
+
onProgress=onProgress,
|
|
783
|
+
showProgress=showProgress,
|
|
784
|
+
requestOptions=requestOptions,
|
|
785
|
+
)
|
|
786
|
+
|
|
787
|
+
def create_and_wait(
|
|
788
|
+
self,
|
|
789
|
+
*,
|
|
790
|
+
apiProjectId,
|
|
791
|
+
model="diffio-2",
|
|
792
|
+
sampling=None,
|
|
793
|
+
params=None,
|
|
794
|
+
pollInterval=2.0,
|
|
795
|
+
timeout=600.0,
|
|
796
|
+
onProgress=None,
|
|
797
|
+
showProgress=False,
|
|
798
|
+
requestOptions=None,
|
|
799
|
+
progressRequestOptions=None,
|
|
800
|
+
):
|
|
801
|
+
resolved_progress_options = requestOptions if progressRequestOptions is None else progressRequestOptions
|
|
802
|
+
generation = self.create(
|
|
803
|
+
apiProjectId=apiProjectId,
|
|
804
|
+
model=model,
|
|
805
|
+
sampling=sampling,
|
|
806
|
+
params=params,
|
|
807
|
+
requestOptions=requestOptions,
|
|
808
|
+
)
|
|
809
|
+
progress = self.wait_for_complete(
|
|
810
|
+
generationId=generation.generationId,
|
|
811
|
+
apiProjectId=generation.apiProjectId,
|
|
812
|
+
pollInterval=pollInterval,
|
|
813
|
+
timeout=timeout,
|
|
814
|
+
onProgress=onProgress,
|
|
815
|
+
showProgress=showProgress,
|
|
816
|
+
requestOptions=resolved_progress_options,
|
|
817
|
+
)
|
|
818
|
+
return generation, progress
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
class ProjectsClient:
|
|
822
|
+
def __init__(self, parent):
|
|
823
|
+
self._parent = parent
|
|
824
|
+
|
|
825
|
+
def list(self, *, requestOptions=None):
|
|
826
|
+
return self._parent.list_projects(requestOptions=requestOptions)
|
|
827
|
+
|
|
828
|
+
def list_generations(
|
|
829
|
+
self,
|
|
830
|
+
*,
|
|
831
|
+
apiProjectId,
|
|
832
|
+
requestOptions=None,
|
|
833
|
+
):
|
|
834
|
+
return self._parent.list_project_generations(
|
|
835
|
+
apiProjectId=apiProjectId,
|
|
836
|
+
requestOptions=requestOptions,
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
class WebhooksClient:
|
|
841
|
+
def __init__(self, parent):
|
|
842
|
+
self._parent = parent
|
|
843
|
+
|
|
844
|
+
def get_portal_access(
|
|
845
|
+
self,
|
|
846
|
+
*,
|
|
847
|
+
mode,
|
|
848
|
+
apiKeyId=None,
|
|
849
|
+
requestOptions=None,
|
|
850
|
+
):
|
|
851
|
+
return self._parent.get_webhooks_portal_access(
|
|
852
|
+
mode=mode,
|
|
853
|
+
apiKeyId=apiKeyId,
|
|
854
|
+
requestOptions=requestOptions,
|
|
855
|
+
)
|
|
856
|
+
|
|
857
|
+
def send_test_event(
|
|
858
|
+
self,
|
|
859
|
+
*,
|
|
860
|
+
eventType,
|
|
861
|
+
mode,
|
|
862
|
+
apiKeyId=None,
|
|
863
|
+
samplePayload=None,
|
|
864
|
+
requestOptions=None,
|
|
865
|
+
):
|
|
866
|
+
return self._parent.send_webhook_test_event(
|
|
867
|
+
eventType=eventType,
|
|
868
|
+
mode=mode,
|
|
869
|
+
apiKeyId=apiKeyId,
|
|
870
|
+
samplePayload=samplePayload,
|
|
871
|
+
requestOptions=requestOptions,
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
class AudioIsolationClient:
|
|
876
|
+
def __init__(self, parent):
|
|
877
|
+
self._parent = parent
|
|
878
|
+
|
|
879
|
+
def convert(
|
|
880
|
+
self,
|
|
881
|
+
*,
|
|
882
|
+
filePath,
|
|
883
|
+
contentType=None,
|
|
884
|
+
contentLength=None,
|
|
885
|
+
fileFormat=None,
|
|
886
|
+
model="diffio-2",
|
|
887
|
+
sampling=None,
|
|
888
|
+
projectParams=None,
|
|
889
|
+
generationParams=None,
|
|
890
|
+
requestOptions=None,
|
|
891
|
+
):
|
|
892
|
+
return self.isolate(
|
|
893
|
+
filePath=filePath,
|
|
894
|
+
contentType=contentType,
|
|
895
|
+
contentLength=contentLength,
|
|
896
|
+
fileFormat=fileFormat,
|
|
897
|
+
model=model,
|
|
898
|
+
sampling=sampling,
|
|
899
|
+
projectParams=projectParams,
|
|
900
|
+
generationParams=generationParams,
|
|
901
|
+
requestOptions=requestOptions,
|
|
902
|
+
)
|
|
903
|
+
|
|
904
|
+
def isolate(
|
|
905
|
+
self,
|
|
906
|
+
*,
|
|
907
|
+
filePath,
|
|
908
|
+
contentType=None,
|
|
909
|
+
contentLength=None,
|
|
910
|
+
fileFormat=None,
|
|
911
|
+
model="diffio-2",
|
|
912
|
+
sampling=None,
|
|
913
|
+
projectParams=None,
|
|
914
|
+
generationParams=None,
|
|
915
|
+
requestOptions=None,
|
|
916
|
+
):
|
|
917
|
+
project = self._parent.create_project(
|
|
918
|
+
filePath=filePath,
|
|
919
|
+
contentType=contentType,
|
|
920
|
+
contentLength=contentLength,
|
|
921
|
+
params=projectParams,
|
|
922
|
+
fileFormat=fileFormat,
|
|
923
|
+
requestOptions=requestOptions,
|
|
924
|
+
)
|
|
925
|
+
|
|
926
|
+
generation = self._parent.create_generation(
|
|
927
|
+
apiProjectId=project.apiProjectId,
|
|
928
|
+
model=model,
|
|
929
|
+
sampling=sampling,
|
|
930
|
+
params=generationParams,
|
|
931
|
+
requestOptions=requestOptions,
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
return AudioIsolationResult(project=project, generation=generation)
|
|
935
|
+
|
|
936
|
+
def restore_audio(
|
|
937
|
+
self,
|
|
938
|
+
*,
|
|
939
|
+
filePath,
|
|
940
|
+
contentType=None,
|
|
941
|
+
contentLength=None,
|
|
942
|
+
fileFormat=None,
|
|
943
|
+
model="diffio-2",
|
|
944
|
+
sampling=None,
|
|
945
|
+
projectParams=None,
|
|
946
|
+
generationParams=None,
|
|
947
|
+
downloadType="audio",
|
|
948
|
+
pollInterval=2.0,
|
|
949
|
+
timeout=600.0,
|
|
950
|
+
onProgress=None,
|
|
951
|
+
showProgress=False,
|
|
952
|
+
requestOptions=None,
|
|
953
|
+
progressRequestOptions=None,
|
|
954
|
+
downloadRequestOptions=None,
|
|
955
|
+
raiseOnError=False,
|
|
956
|
+
):
|
|
957
|
+
metadata = _init_restore_metadata()
|
|
958
|
+
metadata["downloadType"] = downloadType
|
|
959
|
+
resolved_progress_options = requestOptions if progressRequestOptions is None else progressRequestOptions
|
|
960
|
+
resolved_download_options = requestOptions if downloadRequestOptions is None else downloadRequestOptions
|
|
961
|
+
|
|
962
|
+
try:
|
|
963
|
+
result = self.isolate(
|
|
964
|
+
filePath=filePath,
|
|
965
|
+
contentType=contentType,
|
|
966
|
+
contentLength=contentLength,
|
|
967
|
+
fileFormat=fileFormat,
|
|
968
|
+
model=model,
|
|
969
|
+
sampling=sampling,
|
|
970
|
+
projectParams=projectParams,
|
|
971
|
+
generationParams=generationParams,
|
|
972
|
+
requestOptions=requestOptions,
|
|
973
|
+
)
|
|
974
|
+
except Exception as exc:
|
|
975
|
+
metadata["stage"] = "isolate"
|
|
976
|
+
_set_restore_error(metadata, exc)
|
|
977
|
+
if raiseOnError:
|
|
978
|
+
_attach_restore_metadata(exc, metadata)
|
|
979
|
+
raise
|
|
980
|
+
return None, metadata
|
|
981
|
+
|
|
982
|
+
metadata["project"] = result.project
|
|
983
|
+
metadata["generation"] = result.generation
|
|
984
|
+
metadata["apiProjectId"] = result.project.apiProjectId
|
|
985
|
+
metadata["generationId"] = result.generation.generationId
|
|
986
|
+
metadata["stage"] = "generation"
|
|
987
|
+
|
|
988
|
+
try:
|
|
989
|
+
progress = self._parent.wait_for_generation(
|
|
990
|
+
generationId=result.generation.generationId,
|
|
991
|
+
apiProjectId=result.project.apiProjectId,
|
|
992
|
+
pollInterval=pollInterval,
|
|
993
|
+
timeout=timeout,
|
|
994
|
+
onProgress=onProgress,
|
|
995
|
+
showProgress=showProgress,
|
|
996
|
+
requestOptions=resolved_progress_options,
|
|
997
|
+
)
|
|
998
|
+
except Exception as exc:
|
|
999
|
+
metadata["stage"] = "progress"
|
|
1000
|
+
progress = None
|
|
1001
|
+
try:
|
|
1002
|
+
progress = self._parent.get_generation_progress(
|
|
1003
|
+
generationId=result.generation.generationId,
|
|
1004
|
+
apiProjectId=result.project.apiProjectId,
|
|
1005
|
+
requestOptions=resolved_progress_options,
|
|
1006
|
+
)
|
|
1007
|
+
except Exception:
|
|
1008
|
+
progress = None
|
|
1009
|
+
metadata["progress"] = progress
|
|
1010
|
+
metadata["status"] = getattr(progress, "status", None)
|
|
1011
|
+
_set_restore_error(metadata, exc)
|
|
1012
|
+
if progress is not None:
|
|
1013
|
+
metadata["error"] = progress.error or str(exc)
|
|
1014
|
+
metadata["errorDetails"] = progress.errorDetails
|
|
1015
|
+
if raiseOnError:
|
|
1016
|
+
_attach_restore_metadata(exc, metadata)
|
|
1017
|
+
raise
|
|
1018
|
+
return None, metadata
|
|
1019
|
+
|
|
1020
|
+
metadata["progress"] = progress
|
|
1021
|
+
metadata["status"] = progress.status
|
|
1022
|
+
metadata["error"] = progress.error
|
|
1023
|
+
metadata["errorDetails"] = progress.errorDetails
|
|
1024
|
+
|
|
1025
|
+
metadata["stage"] = "download_info"
|
|
1026
|
+
try:
|
|
1027
|
+
download = self._parent.get_generation_download(
|
|
1028
|
+
generationId=result.generation.generationId,
|
|
1029
|
+
apiProjectId=result.project.apiProjectId,
|
|
1030
|
+
downloadType=downloadType,
|
|
1031
|
+
requestOptions=resolved_download_options,
|
|
1032
|
+
)
|
|
1033
|
+
except Exception as exc:
|
|
1034
|
+
_set_restore_error(metadata, exc)
|
|
1035
|
+
if raiseOnError:
|
|
1036
|
+
_attach_restore_metadata(exc, metadata)
|
|
1037
|
+
raise
|
|
1038
|
+
return None, metadata
|
|
1039
|
+
|
|
1040
|
+
metadata["download"] = download
|
|
1041
|
+
metadata["downloadType"] = download.downloadType
|
|
1042
|
+
metadata["downloadUrl"] = download.downloadUrl
|
|
1043
|
+
metadata["fileName"] = download.fileName
|
|
1044
|
+
metadata["mimeType"] = download.mimeType
|
|
1045
|
+
|
|
1046
|
+
metadata["stage"] = "download"
|
|
1047
|
+
try:
|
|
1048
|
+
content = _download_binary(self._parent, download.downloadUrl, requestOptions=resolved_download_options)
|
|
1049
|
+
except Exception as exc:
|
|
1050
|
+
_set_restore_error(metadata, exc)
|
|
1051
|
+
if raiseOnError:
|
|
1052
|
+
_attach_restore_metadata(exc, metadata)
|
|
1053
|
+
raise
|
|
1054
|
+
return None, metadata
|
|
1055
|
+
|
|
1056
|
+
metadata["stage"] = "complete"
|
|
1057
|
+
metadata["ok"] = True
|
|
1058
|
+
return content, metadata
|
|
1059
|
+
|
|
1060
|
+
def restore(
|
|
1061
|
+
self,
|
|
1062
|
+
*,
|
|
1063
|
+
filePath,
|
|
1064
|
+
contentType=None,
|
|
1065
|
+
contentLength=None,
|
|
1066
|
+
fileFormat=None,
|
|
1067
|
+
model="diffio-2",
|
|
1068
|
+
sampling=None,
|
|
1069
|
+
projectParams=None,
|
|
1070
|
+
generationParams=None,
|
|
1071
|
+
downloadType="audio",
|
|
1072
|
+
pollInterval=2.0,
|
|
1073
|
+
timeout=600.0,
|
|
1074
|
+
onProgress=None,
|
|
1075
|
+
showProgress=False,
|
|
1076
|
+
requestOptions=None,
|
|
1077
|
+
progressRequestOptions=None,
|
|
1078
|
+
downloadRequestOptions=None,
|
|
1079
|
+
raiseOnError=False,
|
|
1080
|
+
):
|
|
1081
|
+
return self.restore_audio(
|
|
1082
|
+
filePath=filePath,
|
|
1083
|
+
contentType=contentType,
|
|
1084
|
+
contentLength=contentLength,
|
|
1085
|
+
fileFormat=fileFormat,
|
|
1086
|
+
model=model,
|
|
1087
|
+
sampling=sampling,
|
|
1088
|
+
projectParams=projectParams,
|
|
1089
|
+
generationParams=generationParams,
|
|
1090
|
+
downloadType=downloadType,
|
|
1091
|
+
pollInterval=pollInterval,
|
|
1092
|
+
timeout=timeout,
|
|
1093
|
+
onProgress=onProgress,
|
|
1094
|
+
showProgress=showProgress,
|
|
1095
|
+
requestOptions=requestOptions,
|
|
1096
|
+
progressRequestOptions=progressRequestOptions,
|
|
1097
|
+
downloadRequestOptions=downloadRequestOptions,
|
|
1098
|
+
raiseOnError=raiseOnError,
|
|
1099
|
+
)
|
|
1100
|
+
|
|
1101
|
+
|
|
1102
|
+
|
|
1103
|
+
def _guess_content_type(file_path):
|
|
1104
|
+
guessed, _ = mimetypes.guess_type(file_path)
|
|
1105
|
+
return guessed
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
def _build_create_project_payload(
|
|
1109
|
+
*,
|
|
1110
|
+
filePath=None,
|
|
1111
|
+
contentType=None,
|
|
1112
|
+
contentLength=None,
|
|
1113
|
+
params=None,
|
|
1114
|
+
fileFormat=None,
|
|
1115
|
+
):
|
|
1116
|
+
if filePath is None:
|
|
1117
|
+
raise ValueError("filePath is required")
|
|
1118
|
+
|
|
1119
|
+
resolved_path = os.fspath(filePath)
|
|
1120
|
+
if not resolved_path:
|
|
1121
|
+
raise ValueError("filePath is required")
|
|
1122
|
+
|
|
1123
|
+
resolved_file_name = os.path.basename(resolved_path)
|
|
1124
|
+
if not resolved_file_name:
|
|
1125
|
+
raise ValueError("filePath must include a file name")
|
|
1126
|
+
|
|
1127
|
+
resolved_content_type = contentType or _guess_content_type(resolved_path) or "application/octet-stream"
|
|
1128
|
+
resolved_content_length = contentLength
|
|
1129
|
+
if resolved_content_length is None:
|
|
1130
|
+
resolved_content_length = os.path.getsize(resolved_path)
|
|
1131
|
+
|
|
1132
|
+
payload = {
|
|
1133
|
+
"fileName": resolved_file_name,
|
|
1134
|
+
"contentType": resolved_content_type,
|
|
1135
|
+
"contentLength": int(resolved_content_length),
|
|
1136
|
+
}
|
|
1137
|
+
if params:
|
|
1138
|
+
payload["params"] = params
|
|
1139
|
+
if fileFormat is not None:
|
|
1140
|
+
payload["fileFormat"] = fileFormat
|
|
1141
|
+
|
|
1142
|
+
return payload
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
def _normalize_download_type(download_type):
|
|
1146
|
+
if download_type is None:
|
|
1147
|
+
return None, None
|
|
1148
|
+
if download_type == "mp3":
|
|
1149
|
+
return "audio", "mp3"
|
|
1150
|
+
if download_type in {"audio", "video"}:
|
|
1151
|
+
return download_type, download_type
|
|
1152
|
+
raise ValueError("downloadType must be audio, mp3, or video")
|
|
1153
|
+
|
|
1154
|
+
|
|
1155
|
+
def _extension_from_file_name(file_name):
|
|
1156
|
+
if not file_name:
|
|
1157
|
+
return None
|
|
1158
|
+
extension = os.path.splitext(file_name)[1]
|
|
1159
|
+
if extension:
|
|
1160
|
+
return extension
|
|
1161
|
+
return None
|
|
1162
|
+
|
|
1163
|
+
|
|
1164
|
+
def _extension_from_mime_type(mime_type):
|
|
1165
|
+
if not mime_type:
|
|
1166
|
+
return None
|
|
1167
|
+
extension = mimetypes.guess_extension(mime_type)
|
|
1168
|
+
if extension:
|
|
1169
|
+
return extension
|
|
1170
|
+
return None
|
|
1171
|
+
|
|
1172
|
+
|
|
1173
|
+
def _extension_from_url(download_url):
|
|
1174
|
+
if not download_url:
|
|
1175
|
+
return None
|
|
1176
|
+
try:
|
|
1177
|
+
parsed = urlparse(download_url)
|
|
1178
|
+
except Exception:
|
|
1179
|
+
return None
|
|
1180
|
+
extension = os.path.splitext(parsed.path or "")[1]
|
|
1181
|
+
if extension:
|
|
1182
|
+
return extension
|
|
1183
|
+
return None
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
def _expected_download_extension(download):
|
|
1187
|
+
if download.downloadType == "audio":
|
|
1188
|
+
return ".mp3"
|
|
1189
|
+
if download.downloadType == "video":
|
|
1190
|
+
return (
|
|
1191
|
+
_extension_from_file_name(download.fileName)
|
|
1192
|
+
or _extension_from_mime_type(download.mimeType)
|
|
1193
|
+
or _extension_from_url(download.downloadUrl)
|
|
1194
|
+
)
|
|
1195
|
+
return None
|
|
1196
|
+
|
|
1197
|
+
|
|
1198
|
+
def _warn_download_extension_mismatch(download, download_file_path):
|
|
1199
|
+
expected_extension = _expected_download_extension(download)
|
|
1200
|
+
if not expected_extension:
|
|
1201
|
+
return
|
|
1202
|
+
provided_extension = os.path.splitext(download_file_path)[1]
|
|
1203
|
+
if provided_extension.lower() != expected_extension.lower():
|
|
1204
|
+
warnings.warn(
|
|
1205
|
+
"downloadFilePath should end with "
|
|
1206
|
+
f"{expected_extension} for {download.downloadType} downloads. "
|
|
1207
|
+
f"Received {download_file_path}.",
|
|
1208
|
+
UserWarning,
|
|
1209
|
+
stacklevel=2,
|
|
1210
|
+
)
|
|
1211
|
+
|
|
1212
|
+
|
|
1213
|
+
def _is_storage_emulator_url(upload_url):
|
|
1214
|
+
try:
|
|
1215
|
+
parsed = urlparse(upload_url)
|
|
1216
|
+
except Exception:
|
|
1217
|
+
return False
|
|
1218
|
+
|
|
1219
|
+
host = (parsed.hostname or "").lower()
|
|
1220
|
+
port = parsed.port
|
|
1221
|
+
if host in {"127.0.0.1", "localhost", "0.0.0.0", "::1"} and (port == 9199 or port is None):
|
|
1222
|
+
return True
|
|
1223
|
+
|
|
1224
|
+
emulator_host = os.environ.get("STORAGE_EMULATOR_HOST") or os.environ.get("FIREBASE_STORAGE_EMULATOR_HOST")
|
|
1225
|
+
if not emulator_host:
|
|
1226
|
+
return False
|
|
1227
|
+
if not emulator_host.startswith(("http://", "https://")):
|
|
1228
|
+
emulator_host = f"http://{emulator_host}"
|
|
1229
|
+
try:
|
|
1230
|
+
emulator_parsed = urlparse(emulator_host)
|
|
1231
|
+
except Exception:
|
|
1232
|
+
return False
|
|
1233
|
+
if (parsed.hostname or "").lower() != (emulator_parsed.hostname or "").lower():
|
|
1234
|
+
return False
|
|
1235
|
+
|
|
1236
|
+
parsed_port = parsed.port or (443 if parsed.scheme == "https" else 80)
|
|
1237
|
+
emulator_port = emulator_parsed.port or (443 if emulator_parsed.scheme == "https" else 80)
|
|
1238
|
+
return parsed_port == emulator_port
|
|
1239
|
+
|
|
1240
|
+
|
|
1241
|
+
def _merge_headers(base_headers, override_headers):
|
|
1242
|
+
headers = {}
|
|
1243
|
+
if base_headers:
|
|
1244
|
+
headers.update(base_headers)
|
|
1245
|
+
if override_headers:
|
|
1246
|
+
headers.update(override_headers)
|
|
1247
|
+
return headers
|
|
1248
|
+
|
|
1249
|
+
|
|
1250
|
+
def _sleep_retry(attempt, retry_backoff):
|
|
1251
|
+
if retry_backoff is None:
|
|
1252
|
+
return
|
|
1253
|
+
delay = retry_backoff * (2 ** attempt)
|
|
1254
|
+
if delay > 0:
|
|
1255
|
+
time.sleep(delay)
|
|
1256
|
+
|
|
1257
|
+
|
|
1258
|
+
def _raise_for_error(response):
|
|
1259
|
+
if 200 <= response.status_code < 300:
|
|
1260
|
+
content_type = response.headers.get("Content-Type", "")
|
|
1261
|
+
if response.content and "application/json" in content_type:
|
|
1262
|
+
return response.json()
|
|
1263
|
+
return {}
|
|
1264
|
+
|
|
1265
|
+
message = f"Request failed with status {response.status_code}"
|
|
1266
|
+
body = None
|
|
1267
|
+
try:
|
|
1268
|
+
if response.content:
|
|
1269
|
+
body = response.json()
|
|
1270
|
+
if isinstance(body, dict) and body.get("error"):
|
|
1271
|
+
message = str(body.get("error"))
|
|
1272
|
+
else:
|
|
1273
|
+
body = None
|
|
1274
|
+
except json.JSONDecodeError:
|
|
1275
|
+
body = response.text
|
|
1276
|
+
|
|
1277
|
+
raise DiffioApiError(message, statusCode=response.status_code, responseBody=body)
|
|
1278
|
+
|
|
1279
|
+
|
|
1280
|
+
def _init_restore_metadata():
|
|
1281
|
+
return {
|
|
1282
|
+
"ok": False,
|
|
1283
|
+
"stage": "start",
|
|
1284
|
+
"apiProjectId": None,
|
|
1285
|
+
"generationId": None,
|
|
1286
|
+
"project": None,
|
|
1287
|
+
"generation": None,
|
|
1288
|
+
"progress": None,
|
|
1289
|
+
"download": None,
|
|
1290
|
+
"downloadType": None,
|
|
1291
|
+
"downloadUrl": None,
|
|
1292
|
+
"fileName": None,
|
|
1293
|
+
"mimeType": None,
|
|
1294
|
+
"status": None,
|
|
1295
|
+
"error": None,
|
|
1296
|
+
"errorDetails": None,
|
|
1297
|
+
"exceptionType": None,
|
|
1298
|
+
"exceptionMessage": None,
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
|
|
1302
|
+
def _set_restore_error(metadata, exc):
|
|
1303
|
+
metadata["error"] = str(exc)
|
|
1304
|
+
metadata["exceptionType"] = exc.__class__.__name__
|
|
1305
|
+
metadata["exceptionMessage"] = str(exc)
|
|
1306
|
+
|
|
1307
|
+
|
|
1308
|
+
def _attach_restore_metadata(exc, metadata):
|
|
1309
|
+
try:
|
|
1310
|
+
setattr(exc, "restoreInfo", metadata)
|
|
1311
|
+
except Exception:
|
|
1312
|
+
return
|
|
1313
|
+
|
|
1314
|
+
|
|
1315
|
+
def _download_binary(parent, download_url, *, requestOptions=None):
|
|
1316
|
+
merged_options = _merge_request_options(parent._default_request_options, requestOptions)
|
|
1317
|
+
headers = {}
|
|
1318
|
+
if _is_storage_emulator_url(download_url):
|
|
1319
|
+
headers["Authorization"] = "Bearer owner"
|
|
1320
|
+
headers = _merge_headers(headers, merged_options.headers)
|
|
1321
|
+
timeout = merged_options.timeout
|
|
1322
|
+
max_retries = merged_options.maxRetries if merged_options.maxRetries is not None else 0
|
|
1323
|
+
retry_backoff = (
|
|
1324
|
+
merged_options.retryBackoff if merged_options.retryBackoff is not None else DEFAULT_RETRY_BACKOFF
|
|
1325
|
+
)
|
|
1326
|
+
retry_statuses = (
|
|
1327
|
+
merged_options.retryStatusCodes
|
|
1328
|
+
if merged_options.retryStatusCodes is not None
|
|
1329
|
+
else DEFAULT_RETRY_STATUS_CODES
|
|
1330
|
+
)
|
|
1331
|
+
|
|
1332
|
+
attempt = 0
|
|
1333
|
+
while True:
|
|
1334
|
+
try:
|
|
1335
|
+
request_kwargs = {
|
|
1336
|
+
"method": "GET",
|
|
1337
|
+
"url": download_url,
|
|
1338
|
+
"headers": headers,
|
|
1339
|
+
}
|
|
1340
|
+
if timeout is not None:
|
|
1341
|
+
request_kwargs["timeout"] = timeout
|
|
1342
|
+
response = parent._client.request(**request_kwargs)
|
|
1343
|
+
except httpx.RequestError:
|
|
1344
|
+
if attempt >= max_retries:
|
|
1345
|
+
raise
|
|
1346
|
+
_sleep_retry(attempt, retry_backoff)
|
|
1347
|
+
attempt += 1
|
|
1348
|
+
continue
|
|
1349
|
+
|
|
1350
|
+
if retry_statuses and response.status_code in retry_statuses and attempt < max_retries:
|
|
1351
|
+
response.close()
|
|
1352
|
+
_sleep_retry(attempt, retry_backoff)
|
|
1353
|
+
attempt += 1
|
|
1354
|
+
continue
|
|
1355
|
+
|
|
1356
|
+
try:
|
|
1357
|
+
if 200 <= response.status_code < 300:
|
|
1358
|
+
return response.content
|
|
1359
|
+
_raise_for_error(response)
|
|
1360
|
+
finally:
|
|
1361
|
+
response.close()
|
|
1362
|
+
|
|
1363
|
+
|
|
1364
|
+
def _download_to_file(parent, download_url, file_path, *, requestOptions=None):
|
|
1365
|
+
merged_options = _merge_request_options(parent._default_request_options, requestOptions)
|
|
1366
|
+
headers = {}
|
|
1367
|
+
if _is_storage_emulator_url(download_url):
|
|
1368
|
+
headers["Authorization"] = "Bearer owner"
|
|
1369
|
+
headers = _merge_headers(headers, merged_options.headers)
|
|
1370
|
+
timeout = merged_options.timeout
|
|
1371
|
+
max_retries = merged_options.maxRetries if merged_options.maxRetries is not None else 0
|
|
1372
|
+
retry_backoff = (
|
|
1373
|
+
merged_options.retryBackoff if merged_options.retryBackoff is not None else DEFAULT_RETRY_BACKOFF
|
|
1374
|
+
)
|
|
1375
|
+
retry_statuses = (
|
|
1376
|
+
merged_options.retryStatusCodes
|
|
1377
|
+
if merged_options.retryStatusCodes is not None
|
|
1378
|
+
else DEFAULT_RETRY_STATUS_CODES
|
|
1379
|
+
)
|
|
1380
|
+
|
|
1381
|
+
resolved_path = os.fspath(file_path)
|
|
1382
|
+
directory = os.path.dirname(resolved_path) or "."
|
|
1383
|
+
temp_handle = None
|
|
1384
|
+
temp_path = None
|
|
1385
|
+
|
|
1386
|
+
try:
|
|
1387
|
+
attempt = 0
|
|
1388
|
+
while True:
|
|
1389
|
+
try:
|
|
1390
|
+
fd, temp_path = tempfile.mkstemp(prefix="diffio-download-", dir=directory)
|
|
1391
|
+
temp_handle = os.fdopen(fd, "wb")
|
|
1392
|
+
with parent._client.stream("GET", download_url, headers=headers, timeout=timeout) as response:
|
|
1393
|
+
if retry_statuses and response.status_code in retry_statuses and attempt < max_retries:
|
|
1394
|
+
temp_handle.close()
|
|
1395
|
+
try:
|
|
1396
|
+
os.remove(temp_path)
|
|
1397
|
+
except OSError:
|
|
1398
|
+
pass
|
|
1399
|
+
_sleep_retry(attempt, retry_backoff)
|
|
1400
|
+
attempt += 1
|
|
1401
|
+
continue
|
|
1402
|
+
if not (200 <= response.status_code < 300):
|
|
1403
|
+
temp_handle.close()
|
|
1404
|
+
try:
|
|
1405
|
+
os.remove(temp_path)
|
|
1406
|
+
except OSError:
|
|
1407
|
+
pass
|
|
1408
|
+
_raise_for_error(response)
|
|
1409
|
+
for chunk in response.iter_bytes():
|
|
1410
|
+
if chunk:
|
|
1411
|
+
temp_handle.write(chunk)
|
|
1412
|
+
temp_handle.close()
|
|
1413
|
+
temp_handle = None
|
|
1414
|
+
os.replace(temp_path, resolved_path)
|
|
1415
|
+
temp_path = None
|
|
1416
|
+
return
|
|
1417
|
+
except httpx.RequestError:
|
|
1418
|
+
if temp_handle is not None:
|
|
1419
|
+
temp_handle.close()
|
|
1420
|
+
temp_handle = None
|
|
1421
|
+
if temp_path is not None:
|
|
1422
|
+
try:
|
|
1423
|
+
os.remove(temp_path)
|
|
1424
|
+
except OSError:
|
|
1425
|
+
pass
|
|
1426
|
+
temp_path = None
|
|
1427
|
+
if attempt >= max_retries:
|
|
1428
|
+
raise
|
|
1429
|
+
_sleep_retry(attempt, retry_backoff)
|
|
1430
|
+
attempt += 1
|
|
1431
|
+
continue
|
|
1432
|
+
finally:
|
|
1433
|
+
if temp_handle is not None:
|
|
1434
|
+
temp_handle.close()
|
|
1435
|
+
if temp_path is not None:
|
|
1436
|
+
try:
|
|
1437
|
+
os.remove(temp_path)
|
|
1438
|
+
except OSError:
|
|
1439
|
+
pass
|
|
1440
|
+
|
|
1441
|
+
|
|
1442
|
+
def _format_progress(progress):
|
|
1443
|
+
parts = []
|
|
1444
|
+
if progress.preProcessing is not None:
|
|
1445
|
+
parts.append(f"pre={progress.preProcessing.status}:{progress.preProcessing.progress}%")
|
|
1446
|
+
if progress.inference is not None:
|
|
1447
|
+
parts.append(f"inf={progress.inference.status}:{progress.inference.progress}%")
|
|
1448
|
+
if getattr(progress, "restoredVideo", None) is not None:
|
|
1449
|
+
parts.append(f"vid={progress.restoredVideo.status}:{progress.restoredVideo.progress}%")
|
|
1450
|
+
joined = ", ".join(parts)
|
|
1451
|
+
if joined:
|
|
1452
|
+
return f"{progress.status} ({joined})"
|
|
1453
|
+
return f"{progress.status}"
|
|
1454
|
+
|
|
1455
|
+
|
|
1456
|
+
def _report_progress(progress, *, onProgress, showProgress):
|
|
1457
|
+
if onProgress:
|
|
1458
|
+
onProgress(progress)
|
|
1459
|
+
if showProgress:
|
|
1460
|
+
print(_format_progress(progress))
|