swarmauri_video_lipsync_synclabs 0.1.0.dev4__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.
@@ -0,0 +1,477 @@
1
+ """Sync Labs lip-sync provider implementation."""
2
+
3
+ import asyncio
4
+ import mimetypes
5
+ import time
6
+ from collections.abc import AsyncIterator, Iterator
7
+ from pathlib import Path
8
+ from typing import Any, Literal, cast
9
+ from urllib.parse import urlparse
10
+
11
+ import httpx
12
+ from pydantic import Field, SecretStr
13
+
14
+ from swarmauri_base.ComponentBase import ComponentBase
15
+ from swarmauri_base.video_lipsync.LipSyncBase import LipSyncBase
16
+ from swarmauri_core.video_lipsync.IQueuedLipSync import IQueuedLipSync
17
+ from swarmauri_core.video_lipsync.types import (
18
+ LipSyncJobEvent,
19
+ LipSyncJobStatus,
20
+ )
21
+
22
+ _TERMINAL_STATUSES = {"COMPLETED", "FAILED", "REJECTED"}
23
+ _STATUS_MAP: dict[str, LipSyncJobStatus] = {
24
+ "PENDING": "QUEUED",
25
+ "PROCESSING": "RUNNING",
26
+ "COMPLETED": "COMPLETED",
27
+ "FAILED": "FAILED",
28
+ "REJECTED": "REJECTED",
29
+ }
30
+
31
+
32
+ @ComponentBase.register_type(LipSyncBase, "SyncLabsLipSync")
33
+ class SyncLabsLipSync(LipSyncBase, IQueuedLipSync):
34
+ """Generate lip-synced videos with Sync Labs' queued API."""
35
+
36
+ allowed_models: list[str] = [
37
+ "sync-3",
38
+ "lipsync-2",
39
+ "lipsync-2-pro",
40
+ "lipsync-1.9.0-beta",
41
+ "react-1",
42
+ ]
43
+ api_key: SecretStr = Field(exclude=True, repr=False)
44
+ name: str = "lipsync-2"
45
+ type: Literal["SyncLabsLipSync"] = "SyncLabsLipSync"
46
+ base_url: str = "https://api.sync.so"
47
+ request_timeout: float = 60.0
48
+ poll_interval: float = 2.0
49
+ max_wait_time: float = 1800.0
50
+ max_retries: int = 3
51
+
52
+ def _headers(self) -> dict[str, str]:
53
+ return {
54
+ "accept": "application/json",
55
+ "content-type": "application/json",
56
+ "x-api-key": self.api_key.get_secret_value(),
57
+ }
58
+
59
+ def _create_api_client(self) -> httpx.Client:
60
+ return httpx.Client(
61
+ base_url=self.base_url,
62
+ headers=self._headers(),
63
+ timeout=self.request_timeout,
64
+ )
65
+
66
+ def _create_external_client(self) -> httpx.Client:
67
+ return httpx.Client(timeout=self.request_timeout)
68
+
69
+ def _create_async_api_client(self) -> httpx.AsyncClient:
70
+ return httpx.AsyncClient(
71
+ base_url=self.base_url,
72
+ headers=self._headers(),
73
+ timeout=self.request_timeout,
74
+ )
75
+
76
+ def _create_async_external_client(self) -> httpx.AsyncClient:
77
+ return httpx.AsyncClient(timeout=self.request_timeout)
78
+
79
+ @staticmethod
80
+ def _is_url(value: str) -> bool:
81
+ return urlparse(value).scheme in {"http", "https"}
82
+
83
+ @staticmethod
84
+ def _retry_delay(response: httpx.Response, attempt: int) -> float:
85
+ retry_after = response.headers.get("Retry-After")
86
+ if retry_after:
87
+ try:
88
+ return max(float(retry_after), 0.0)
89
+ except ValueError:
90
+ pass
91
+ return min(2**attempt, 8)
92
+
93
+ def _request(
94
+ self,
95
+ client: httpx.Client,
96
+ method: str,
97
+ url: str,
98
+ *,
99
+ retry_server_errors: bool = True,
100
+ **kwargs: Any,
101
+ ) -> httpx.Response:
102
+ for attempt in range(self.max_retries + 1):
103
+ response = client.request(method, url, **kwargs)
104
+ retryable = response.status_code == 429 or (
105
+ retry_server_errors
106
+ and response.status_code in {500, 502, 503, 504}
107
+ )
108
+ if not retryable or attempt == self.max_retries:
109
+ response.raise_for_status()
110
+ return response
111
+ time.sleep(self._retry_delay(response, attempt))
112
+ raise RuntimeError("unreachable retry state")
113
+
114
+ async def _arequest(
115
+ self,
116
+ client: httpx.AsyncClient,
117
+ method: str,
118
+ url: str,
119
+ *,
120
+ retry_server_errors: bool = True,
121
+ **kwargs: Any,
122
+ ) -> httpx.Response:
123
+ for attempt in range(self.max_retries + 1):
124
+ response = await client.request(method, url, **kwargs)
125
+ retryable = response.status_code == 429 or (
126
+ retry_server_errors
127
+ and response.status_code in {500, 502, 503, 504}
128
+ )
129
+ if not retryable or attempt == self.max_retries:
130
+ response.raise_for_status()
131
+ return response
132
+ await asyncio.sleep(self._retry_delay(response, attempt))
133
+ raise RuntimeError("unreachable retry state")
134
+
135
+ def _upload_local_asset(self, path_value: str, asset_type: str) -> str:
136
+ path = Path(path_value).expanduser().resolve(strict=True)
137
+ content_type = mimetypes.guess_type(path.name)[0]
138
+ if content_type is None:
139
+ content_type = (
140
+ "video/mp4" if asset_type == "VIDEO" else "audio/mpeg"
141
+ )
142
+
143
+ with self._create_api_client() as api_client:
144
+ upload = self._request(
145
+ api_client,
146
+ "POST",
147
+ "/v2/assets/upload",
148
+ json={
149
+ "fileName": path.name,
150
+ "contentType": content_type,
151
+ "size": path.stat().st_size,
152
+ },
153
+ ).json()
154
+
155
+ with (
156
+ path.open("rb") as media,
157
+ self._create_external_client() as external,
158
+ ):
159
+ response = external.put(
160
+ upload["uploadUrl"],
161
+ content=media,
162
+ headers={"Content-Type": content_type},
163
+ )
164
+ response.raise_for_status()
165
+
166
+ with self._create_api_client() as api_client:
167
+ asset = self._request(
168
+ api_client,
169
+ "POST",
170
+ "/v2/assets",
171
+ json={
172
+ "url": upload["url"],
173
+ "type": asset_type,
174
+ "name": path.name,
175
+ },
176
+ ).json()
177
+ return cast(str, asset["id"])
178
+
179
+ async def _aupload_local_asset(
180
+ self, path_value: str, asset_type: str
181
+ ) -> str:
182
+ path = Path(path_value).expanduser().resolve(strict=True)
183
+ content_type = mimetypes.guess_type(path.name)[0]
184
+ if content_type is None:
185
+ content_type = (
186
+ "video/mp4" if asset_type == "VIDEO" else "audio/mpeg"
187
+ )
188
+ size = path.stat().st_size
189
+
190
+ async with self._create_async_api_client() as api_client:
191
+ upload = (
192
+ await self._arequest(
193
+ api_client,
194
+ "POST",
195
+ "/v2/assets/upload",
196
+ json={
197
+ "fileName": path.name,
198
+ "contentType": content_type,
199
+ "size": size,
200
+ },
201
+ )
202
+ ).json()
203
+
204
+ async def chunks() -> AsyncIterator[bytes]:
205
+ with path.open("rb") as media:
206
+ while chunk := await asyncio.to_thread(
207
+ media.read, 1024 * 1024
208
+ ):
209
+ yield chunk
210
+
211
+ async with self._create_async_external_client() as external:
212
+ response = await external.put(
213
+ upload["uploadUrl"],
214
+ content=chunks(),
215
+ headers={"Content-Type": content_type},
216
+ )
217
+ response.raise_for_status()
218
+
219
+ async with self._create_async_api_client() as api_client:
220
+ asset = (
221
+ await self._arequest(
222
+ api_client,
223
+ "POST",
224
+ "/v2/assets",
225
+ json={
226
+ "url": upload["url"],
227
+ "type": asset_type,
228
+ "name": path.name,
229
+ },
230
+ )
231
+ ).json()
232
+ return cast(str, asset["id"])
233
+
234
+ def _input(self, value: str, media_type: str) -> dict[str, str]:
235
+ if self._is_url(value):
236
+ return {"type": media_type.lower(), "url": value}
237
+ asset_id = self._upload_local_asset(value, media_type)
238
+ return {"type": media_type.lower(), "assetId": asset_id}
239
+
240
+ async def _ainput(self, value: str, media_type: str) -> dict[str, str]:
241
+ if self._is_url(value):
242
+ return {"type": media_type.lower(), "url": value}
243
+ asset_id = await self._aupload_local_asset(value, media_type)
244
+ return {"type": media_type.lower(), "assetId": asset_id}
245
+
246
+ def _payload(
247
+ self,
248
+ video_input: dict[str, str],
249
+ audio_input: dict[str, str],
250
+ **kwargs: Any,
251
+ ) -> dict[str, Any]:
252
+ options = dict(kwargs.pop("options", {}))
253
+ for key in tuple(kwargs):
254
+ if key not in {"webhook_url", "output_file_name", "project_id"}:
255
+ options[key] = kwargs.pop(key)
256
+
257
+ payload: dict[str, Any] = {
258
+ "model": self.name,
259
+ "input": [video_input, audio_input],
260
+ }
261
+ if options:
262
+ payload["options"] = options
263
+ aliases = {
264
+ "webhook_url": "webhookUrl",
265
+ "output_file_name": "outputFileName",
266
+ "project_id": "projectId",
267
+ }
268
+ for source, target in aliases.items():
269
+ value = kwargs.get(source)
270
+ if value is not None:
271
+ payload[target] = value
272
+ return payload
273
+
274
+ def submit(self, video: str, audio: str, **kwargs: Any) -> str:
275
+ """Submit URL or local-file media to Sync Labs."""
276
+ payload = self._payload(
277
+ self._input(video, "VIDEO"),
278
+ self._input(audio, "AUDIO"),
279
+ **kwargs,
280
+ )
281
+ with self._create_api_client() as client:
282
+ response = self._request(
283
+ client,
284
+ "POST",
285
+ "/v2/generate",
286
+ retry_server_errors=False,
287
+ json=payload,
288
+ )
289
+ return cast(str, response.json()["id"])
290
+
291
+ async def _asubmit(self, video: str, audio: str, **kwargs: Any) -> str:
292
+ video_input, audio_input = await asyncio.gather(
293
+ self._ainput(video, "VIDEO"),
294
+ self._ainput(audio, "AUDIO"),
295
+ )
296
+ payload = self._payload(video_input, audio_input, **kwargs)
297
+ async with self._create_async_api_client() as client:
298
+ response = await self._arequest(
299
+ client,
300
+ "POST",
301
+ "/v2/generate",
302
+ retry_server_errors=False,
303
+ json=payload,
304
+ )
305
+ return cast(str, response.json()["id"])
306
+
307
+ @staticmethod
308
+ def _event(generation: dict[str, Any]) -> LipSyncJobEvent:
309
+ provider_status = str(generation["status"])
310
+ if provider_status not in _STATUS_MAP:
311
+ raise ValueError(f"Unknown Sync Labs status: {provider_status}")
312
+ event = LipSyncJobEvent(
313
+ job_id=str(generation["id"]),
314
+ status=_STATUS_MAP[provider_status],
315
+ )
316
+ progress = generation.get("progress_percent")
317
+ if progress is not None:
318
+ event["progress"] = float(progress) / 100.0
319
+ if generation.get("error"):
320
+ event["message"] = str(generation["error"])
321
+ if generation.get("errorCode"):
322
+ event["error_code"] = str(generation["errorCode"])
323
+ if generation.get("outputUrl"):
324
+ event["output_url"] = str(generation["outputUrl"])
325
+ return event
326
+
327
+ def get_status(self, job_id: str) -> LipSyncJobEvent:
328
+ """Fetch and normalize the current Sync Labs generation state."""
329
+ with self._create_api_client() as client:
330
+ response = self._request(
331
+ client,
332
+ "GET",
333
+ f"/v2/generate/{job_id}",
334
+ params={"include": "progress"},
335
+ )
336
+ return self._event(response.json())
337
+
338
+ async def _aget_status(self, job_id: str) -> LipSyncJobEvent:
339
+ async with self._create_async_api_client() as client:
340
+ response = await self._arequest(
341
+ client,
342
+ "GET",
343
+ f"/v2/generate/{job_id}",
344
+ params={"include": "progress"},
345
+ )
346
+ return self._event(response.json())
347
+
348
+ def iter_events(self, job_id: str) -> Iterator[LipSyncJobEvent]:
349
+ """Poll and yield only changed normalized job states."""
350
+ started = time.monotonic()
351
+ previous: LipSyncJobEvent | None = None
352
+ while time.monotonic() - started <= self.max_wait_time:
353
+ event = self.get_status(job_id)
354
+ if event != previous:
355
+ yield event
356
+ previous = event
357
+ if event["status"] in _TERMINAL_STATUSES:
358
+ return
359
+ time.sleep(self.poll_interval)
360
+ raise TimeoutError(f"Sync Labs job {job_id} did not finish in time")
361
+
362
+ async def aiter_events(
363
+ self, job_id: str
364
+ ) -> AsyncIterator[LipSyncJobEvent]:
365
+ """Asynchronously poll and yield only changed normalized states."""
366
+ loop = asyncio.get_running_loop()
367
+ started = loop.time()
368
+ previous: LipSyncJobEvent | None = None
369
+ while loop.time() - started <= self.max_wait_time:
370
+ event = await self._aget_status(job_id)
371
+ if event != previous:
372
+ yield event
373
+ previous = event
374
+ if event["status"] in _TERMINAL_STATUSES:
375
+ return
376
+ await asyncio.sleep(self.poll_interval)
377
+ raise TimeoutError(f"Sync Labs job {job_id} did not finish in time")
378
+
379
+ @staticmethod
380
+ def _completed_output(event: LipSyncJobEvent) -> str:
381
+ if event["status"] != "COMPLETED":
382
+ message = event.get(
383
+ "message", "provider returned no error message"
384
+ )
385
+ raise RuntimeError(
386
+ f"Lip-sync job ended with {event['status']}: {message}"
387
+ )
388
+ output_url = event.get("output_url")
389
+ if not output_url:
390
+ raise RuntimeError("Completed lip-sync job returned no output URL")
391
+ return output_url
392
+
393
+ def _download(self, output_url: str, output_path: str) -> str:
394
+ path = Path(output_path).expanduser().resolve()
395
+ path.parent.mkdir(parents=True, exist_ok=True)
396
+ with self._create_external_client() as client:
397
+ with client.stream("GET", output_url) as response:
398
+ response.raise_for_status()
399
+ with path.open("wb") as output:
400
+ for chunk in response.iter_bytes():
401
+ output.write(chunk)
402
+ return str(path)
403
+
404
+ async def _adownload(self, output_url: str, output_path: str) -> str:
405
+ path = Path(output_path).expanduser().resolve()
406
+ await asyncio.to_thread(path.parent.mkdir, parents=True, exist_ok=True)
407
+ async with self._create_async_external_client() as client:
408
+ async with client.stream("GET", output_url) as response:
409
+ response.raise_for_status()
410
+ with path.open("wb") as output:
411
+ async for chunk in response.aiter_bytes():
412
+ await asyncio.to_thread(output.write, chunk)
413
+ return str(path)
414
+
415
+ def predict(
416
+ self,
417
+ video: str,
418
+ audio: str,
419
+ output_path: str = "output.mp4",
420
+ **kwargs: Any,
421
+ ) -> str:
422
+ """Submit, await, and download a lip-synced video."""
423
+ job_id = self.submit(video, audio, **kwargs)
424
+ final_event: LipSyncJobEvent | None = None
425
+ for final_event in self.iter_events(job_id):
426
+ pass
427
+ if final_event is None:
428
+ raise RuntimeError(f"Sync Labs job {job_id} returned no state")
429
+ return self._download(
430
+ self._completed_output(final_event),
431
+ output_path,
432
+ )
433
+
434
+ async def apredict(
435
+ self,
436
+ video: str,
437
+ audio: str,
438
+ output_path: str = "output.mp4",
439
+ **kwargs: Any,
440
+ ) -> str:
441
+ """Asynchronously submit, await, and download a lip-synced video."""
442
+ job_id = await self._asubmit(video, audio, **kwargs)
443
+ final_event: LipSyncJobEvent | None = None
444
+ async for final_event in self.aiter_events(job_id):
445
+ pass
446
+ if final_event is None:
447
+ raise RuntimeError(f"Sync Labs job {job_id} returned no state")
448
+ return await self._adownload(
449
+ self._completed_output(final_event),
450
+ output_path,
451
+ )
452
+
453
+ def estimate_cost(self, video: str, audio: str, **kwargs: Any) -> float:
454
+ """Return the summed estimated generation cost in US dollars."""
455
+ payload = self._payload(
456
+ self._input(video, "VIDEO"),
457
+ self._input(audio, "AUDIO"),
458
+ **kwargs,
459
+ )
460
+ with self._create_api_client() as client:
461
+ estimates = self._request(
462
+ client,
463
+ "POST",
464
+ "/v2/generations/estimate",
465
+ json=payload,
466
+ ).json()
467
+ if isinstance(estimates, dict):
468
+ estimates = [estimates]
469
+ return sum(
470
+ float(
471
+ estimate.get(
472
+ "estimatedGenerationCost",
473
+ estimate.get("totalCost", 0.0),
474
+ )
475
+ )
476
+ for estimate in estimates
477
+ )
@@ -0,0 +1,5 @@
1
+ """Sync Labs lip-sync provider package."""
2
+
3
+ from swarmauri_video_lipsync_synclabs.SyncLabsLipSync import SyncLabsLipSync
4
+
5
+ __all__ = ["SyncLabsLipSync"]
@@ -0,0 +1,118 @@
1
+ Metadata-Version: 2.4
2
+ Name: swarmauri_video_lipsync_synclabs
3
+ Version: 0.1.0.dev4
4
+ Summary: Standalone Sync Labs queued lip-sync video provider for Swarmauri.
5
+ License-Expression: Apache-2.0
6
+ Keywords: swarmauri,sync labs,lip sync,video generation,video dubbing,media localization,queued api,multimodal ai
7
+ Author: Jacob Stewart
8
+ Author-email: jacob@swarmauri.com
9
+ Requires-Python: >=3.10,<3.13
10
+ Classifier: Development Status :: 1 - Planning
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: Apache Software License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Multimedia :: Video
21
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
+ Requires-Dist: httpx (>=0.27,<1)
23
+ Requires-Dist: pydantic (>=2.0)
24
+ Requires-Dist: swarmauri_base (>=0.11.0.dev5)
25
+ Requires-Dist: swarmauri_core (>=0.11.0.dev5)
26
+ Description-Content-Type: text/markdown
27
+
28
+ ![Swarmauri Logo](https://github.com/swarmauri/swarmauri-sdk/blob/3d4d1cfa949399d7019ae9d8f296afba773dfb7f/assets/swarmauri.brand.theme.svg)
29
+
30
+ # Swarmauri Sync Labs Lip Sync
31
+
32
+ `swarmauri_video_lipsync_synclabs` is the standalone Sync Labs provider for
33
+ Swarmauri lip-sync video generation. `SyncLabsLipSync` implements the common
34
+ `LipSyncBase` contract and exposes Sync Labs' queued job lifecycle without
35
+ misrepresenting progress events as streamed video.
36
+
37
+ [![PyPI Downloads](https://static.pepy.tech/badge/swarmauri_video_lipsync_synclabs)](https://pepy.tech/projects/swarmauri_video_lipsync_synclabs)
38
+ [![Hits](https://hits.sh/github.com/swarmauri/swarmauri-sdk/tree/master/pkgs/community/swarmauri_video_lipsync_synclabs.svg)](https://hits.sh/github.com/swarmauri/swarmauri-sdk/tree/master/pkgs/community/swarmauri_video_lipsync_synclabs/)
39
+ [![Python Versions](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12-blue)](https://pypi.org/project/swarmauri_video_lipsync_synclabs/)
40
+ [![License](https://img.shields.io/pypi/l/swarmauri_video_lipsync_synclabs)](https://pypi.org/project/swarmauri_video_lipsync_synclabs/)
41
+ [![PyPI Version](https://img.shields.io/pypi/v/swarmauri_video_lipsync_synclabs)](https://pypi.org/project/swarmauri_video_lipsync_synclabs/)
42
+
43
+ ## Features
44
+
45
+ - Common synchronous and asynchronous `predict` workflows.
46
+ - Observable queued jobs through `submit`, `get_status`, `iter_events`, and
47
+ `aiter_events`.
48
+ - Normalized queued, running, completed, failed, and rejected job events.
49
+ - Public URL inputs and local video/audio uploads through Sync Labs assets.
50
+ - Preflight cost estimation through Sync Labs' estimate endpoint.
51
+ - Retry handling for rate limits and safe transient requests.
52
+ - API credentials excluded from serialized component state and external media
53
+ requests.
54
+ - Current Sync Labs models: `sync-3`, `lipsync-2`, `lipsync-2-pro`,
55
+ `lipsync-1.9.0-beta`, and `react-1`.
56
+
57
+ ## Installation
58
+
59
+ ```bash
60
+ uv add swarmauri_video_lipsync_synclabs
61
+ ```
62
+
63
+ ```bash
64
+ pip install swarmauri_video_lipsync_synclabs
65
+ ```
66
+
67
+ Set `SYNC_API_KEY` before submitting billable work.
68
+
69
+ ## Usage
70
+
71
+ Generate from public media URLs:
72
+
73
+ ```python
74
+ import os
75
+
76
+ from swarmauri_video_lipsync_synclabs import SyncLabsLipSync
77
+
78
+ lip_sync = SyncLabsLipSync(api_key=os.environ["SYNC_API_KEY"])
79
+ result = lip_sync.predict(
80
+ "https://example.com/speaker.mp4",
81
+ "https://example.com/dialogue.wav",
82
+ "dubbed.mp4",
83
+ sync_mode="cut_off",
84
+ )
85
+ print(result)
86
+ ```
87
+
88
+ Local paths are uploaded as reusable Sync Labs assets before generation:
89
+
90
+ ```python
91
+ result = lip_sync.predict("speaker.mp4", "dialogue.wav", "dubbed.mp4")
92
+ ```
93
+
94
+ Observe a queued job without blocking for the final artifact:
95
+
96
+ ```python
97
+ job_id = lip_sync.submit(
98
+ "https://example.com/speaker.mp4",
99
+ "https://example.com/dialogue.wav",
100
+ )
101
+ for event in lip_sync.iter_events(job_id):
102
+ print(event)
103
+ ```
104
+
105
+ These iterators report job progress. Sync Labs returns a completed video
106
+ artifact rather than incremental video bytes, so this provider intentionally
107
+ does not expose `stream` or `astream`.
108
+
109
+ ## Contributing
110
+
111
+ This package is part of the
112
+ [Swarmauri SDK](https://github.com/swarmauri/swarmauri-sdk). Contributions
113
+ should follow the repository contribution and style guides.
114
+
115
+ ## License
116
+
117
+ Apache-2.0
118
+
@@ -0,0 +1,6 @@
1
+ swarmauri_video_lipsync_synclabs/SyncLabsLipSync.py,sha256=B39GNiWMVKGQn0--LfA5TBzV3rwR3djhvTtcQyx8tqE,17049
2
+ swarmauri_video_lipsync_synclabs/__init__.py,sha256=lxj1UHbAwMO3g6l40SzZYQVDecPeJ1KOJ7Lwwa7oDS4,152
3
+ swarmauri_video_lipsync_synclabs-0.1.0.dev4.dist-info/METADATA,sha256=WkCuhWeD7-hTmws8jb_gC1z6ZfzHSTotEjikhgEhUno,4455
4
+ swarmauri_video_lipsync_synclabs-0.1.0.dev4.dist-info/WHEEL,sha256=L-WvLdSBvJWHXv5zrQxDvNRnIgoBzLUpcbuiw19KCuQ,88
5
+ swarmauri_video_lipsync_synclabs-0.1.0.dev4.dist-info/entry_points.txt,sha256=rn2c-NQolmaeKNZMSVWBN9Pu6bgUxs5pElKDwK0aqAE,92
6
+ swarmauri_video_lipsync_synclabs-0.1.0.dev4.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.5.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [swarmauri.video_lipsync]
2
+ SyncLabsLipSync=swarmauri_video_lipsync_synclabs:SyncLabsLipSync
3
+