runapi-volcengine-lip-sync 0.1.1__tar.gz
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.
- runapi_volcengine_lip_sync-0.1.1/PKG-INFO +53 -0
- runapi_volcengine_lip_sync-0.1.1/README.md +38 -0
- runapi_volcengine_lip_sync-0.1.1/pyproject.toml +32 -0
- runapi_volcengine_lip_sync-0.1.1/src/runapi/volcengine_lip_sync/__init__.py +24 -0
- runapi_volcengine_lip_sync-0.1.1/src/runapi/volcengine_lip_sync/client.py +19 -0
- runapi_volcengine_lip_sync-0.1.1/src/runapi/volcengine_lip_sync/contract_gen.py +38 -0
- runapi_volcengine_lip_sync-0.1.1/src/runapi/volcengine_lip_sync/py.typed +1 -0
- runapi_volcengine_lip_sync-0.1.1/src/runapi/volcengine_lip_sync/resources/__init__.py +3 -0
- runapi_volcengine_lip_sync-0.1.1/src/runapi/volcengine_lip_sync/resources/lip_sync_video.py +34 -0
- runapi_volcengine_lip_sync-0.1.1/src/runapi/volcengine_lip_sync/types.py +26 -0
- runapi_volcengine_lip_sync-0.1.1/tests/test_client.py +135 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: runapi-volcengine-lip-sync
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: RunAPI Volcengine Lip Sync SDK for audio-driven video-to-video lip sync workflows in JavaScript, Python, Ruby, Go, Java, and PHP
|
|
5
|
+
Project-URL: Homepage, https://runapi.ai/models/volcengine-lip-sync
|
|
6
|
+
Project-URL: Documentation, https://runapi.ai/docs#sdk-volcengine-lip-sync
|
|
7
|
+
Project-URL: Source, https://github.com/runapi-ai/volcengine-lip-sync-sdk
|
|
8
|
+
Project-URL: Issues, https://github.com/runapi-ai/volcengine-lip-sync-sdk/issues
|
|
9
|
+
Author-email: RunAPI <contact@runapi.ai>
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
Keywords: api,golang,gradle,java,lip-sync,lip-sync-api,maven,python,ruby,runapi,runapi-ai,sdk,typescript,video-api,video-generation,volcengine
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Requires-Dist: runapi-core>=0.1.4
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# Volcengine Lip Sync API Python SDK for RunAPI
|
|
17
|
+
|
|
18
|
+
The Volcengine Lip Sync Python SDK submits lip-sync video tasks and retrieves task results through RunAPI.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install runapi-volcengine-lip-sync
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from runapi.volcengine_lip_sync import VolcengineLipSyncClient
|
|
30
|
+
|
|
31
|
+
client = VolcengineLipSyncClient()
|
|
32
|
+
task = client.lip_sync_video.create(
|
|
33
|
+
model="volcengine-lip-sync",
|
|
34
|
+
mode="lite",
|
|
35
|
+
source_video_url="https://cdn.runapi.ai/public/samples/volcengine-lip-sync-source.mp4",
|
|
36
|
+
source_audio_url="https://cdn.runapi.ai/public/samples/volcengine-lip-sync-voice-adam.mp3",
|
|
37
|
+
)
|
|
38
|
+
status = client.lip_sync_video.get(task.id)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Use `create`, `get`, and `run` for async workflows. Keep `RUNAPI_API_KEY` in the environment or your secret manager.
|
|
42
|
+
|
|
43
|
+
## Links
|
|
44
|
+
|
|
45
|
+
- Model page: https://runapi.ai/models/volcengine-lip-sync
|
|
46
|
+
- SDK docs: https://runapi.ai/docs#sdk-volcengine-lip-sync
|
|
47
|
+
- Product docs: https://runapi.ai/docs#volcengine-lip-sync
|
|
48
|
+
- Pricing and rate limits: https://runapi.ai/models/volcengine-lip-sync
|
|
49
|
+
- Repository: https://github.com/runapi-ai/volcengine-lip-sync-sdk
|
|
50
|
+
|
|
51
|
+
## License
|
|
52
|
+
|
|
53
|
+
Licensed under the Apache License, Version 2.0.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Volcengine Lip Sync API Python SDK for RunAPI
|
|
2
|
+
|
|
3
|
+
The Volcengine Lip Sync Python SDK submits lip-sync video tasks and retrieves task results through RunAPI.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install runapi-volcengine-lip-sync
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from runapi.volcengine_lip_sync import VolcengineLipSyncClient
|
|
15
|
+
|
|
16
|
+
client = VolcengineLipSyncClient()
|
|
17
|
+
task = client.lip_sync_video.create(
|
|
18
|
+
model="volcengine-lip-sync",
|
|
19
|
+
mode="lite",
|
|
20
|
+
source_video_url="https://cdn.runapi.ai/public/samples/volcengine-lip-sync-source.mp4",
|
|
21
|
+
source_audio_url="https://cdn.runapi.ai/public/samples/volcengine-lip-sync-voice-adam.mp3",
|
|
22
|
+
)
|
|
23
|
+
status = client.lip_sync_video.get(task.id)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Use `create`, `get`, and `run` for async workflows. Keep `RUNAPI_API_KEY` in the environment or your secret manager.
|
|
27
|
+
|
|
28
|
+
## Links
|
|
29
|
+
|
|
30
|
+
- Model page: https://runapi.ai/models/volcengine-lip-sync
|
|
31
|
+
- SDK docs: https://runapi.ai/docs#sdk-volcengine-lip-sync
|
|
32
|
+
- Product docs: https://runapi.ai/docs#volcengine-lip-sync
|
|
33
|
+
- Pricing and rate limits: https://runapi.ai/models/volcengine-lip-sync
|
|
34
|
+
- Repository: https://github.com/runapi-ai/volcengine-lip-sync-sdk
|
|
35
|
+
|
|
36
|
+
## License
|
|
37
|
+
|
|
38
|
+
Licensed under the Apache License, Version 2.0.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "runapi-volcengine-lip-sync"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "RunAPI Volcengine Lip Sync SDK for audio-driven video-to-video lip sync workflows in JavaScript, Python, Ruby, Go, Java, and PHP"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
authors = [{ name = "RunAPI", email = "contact@runapi.ai" }]
|
|
13
|
+
keywords = ["runapi", "runapi-ai", "volcengine", "lip-sync", "api", "sdk", "typescript", "python", "ruby", "golang", "java", "maven", "gradle", "video-generation", "video-api", "lip-sync-api"]
|
|
14
|
+
dependencies = ["runapi-core>=0.1.4"]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://runapi.ai/models/volcengine-lip-sync"
|
|
18
|
+
Documentation = "https://runapi.ai/docs#sdk-volcengine-lip-sync"
|
|
19
|
+
Source = "https://github.com/runapi-ai/volcengine-lip-sync-sdk"
|
|
20
|
+
Issues = "https://github.com/runapi-ai/volcengine-lip-sync-sdk/issues"
|
|
21
|
+
|
|
22
|
+
[tool.hatch.build.targets.wheel]
|
|
23
|
+
packages = ["src/runapi"]
|
|
24
|
+
|
|
25
|
+
[tool.uv]
|
|
26
|
+
package = true
|
|
27
|
+
|
|
28
|
+
[dependency-groups]
|
|
29
|
+
dev = ["pytest>=8"]
|
|
30
|
+
|
|
31
|
+
[tool.runapi]
|
|
32
|
+
slug = "volcengine-lip-sync"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Volcengine Lip Sync client for RunAPI."""
|
|
2
|
+
|
|
3
|
+
from runapi.core import (
|
|
4
|
+
AuthenticationError,
|
|
5
|
+
InsufficientCreditsError,
|
|
6
|
+
NotFoundError,
|
|
7
|
+
RateLimitError,
|
|
8
|
+
TaskFailedError,
|
|
9
|
+
TaskTimeoutError,
|
|
10
|
+
ValidationError,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from .client import VolcengineLipSyncClient
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"VolcengineLipSyncClient",
|
|
17
|
+
"AuthenticationError",
|
|
18
|
+
"RateLimitError",
|
|
19
|
+
"InsufficientCreditsError",
|
|
20
|
+
"NotFoundError",
|
|
21
|
+
"ValidationError",
|
|
22
|
+
"TaskFailedError",
|
|
23
|
+
"TaskTimeoutError",
|
|
24
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Volcengine Lip Sync client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from runapi.core import ClientOptions, HttpClient, resolve_api_key
|
|
8
|
+
|
|
9
|
+
from .resources.lip_sync_video import LipSyncVideo
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class VolcengineLipSyncClient:
|
|
13
|
+
"""Volcengine Lip Sync client."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, api_key: Optional[str] = None, **options: Any) -> None:
|
|
16
|
+
resolved_api_key = resolve_api_key(api_key)
|
|
17
|
+
client_options = ClientOptions(api_key=resolved_api_key, **options)
|
|
18
|
+
http = client_options.http_client or HttpClient(client_options)
|
|
19
|
+
self.lip_sync_video = LipSyncVideo(http)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
CONTRACT = {
|
|
2
|
+
"lip-sync-video": {
|
|
3
|
+
"models": ["volcengine-lip-sync"],
|
|
4
|
+
"fields_by_model": {
|
|
5
|
+
"volcengine-lip-sync": {
|
|
6
|
+
"mode": {
|
|
7
|
+
"enum": ["lite", "basic"],
|
|
8
|
+
"required": True
|
|
9
|
+
},
|
|
10
|
+
"source_audio_url": {
|
|
11
|
+
"required": True
|
|
12
|
+
},
|
|
13
|
+
"source_video_url": {
|
|
14
|
+
"required": True
|
|
15
|
+
},
|
|
16
|
+
"template_start_seconds": {
|
|
17
|
+
"min": 0
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"rules": [{
|
|
22
|
+
"when": {
|
|
23
|
+
"mode": "lite"
|
|
24
|
+
},
|
|
25
|
+
"forbidden": ["enable_scene_detection"]
|
|
26
|
+
}, {
|
|
27
|
+
"when": {
|
|
28
|
+
"mode": "basic"
|
|
29
|
+
},
|
|
30
|
+
"forbidden": ["align_audio", "align_audio_reverse", "template_start_seconds"]
|
|
31
|
+
}, {
|
|
32
|
+
"when": {
|
|
33
|
+
"align_audio_reverse": True
|
|
34
|
+
},
|
|
35
|
+
"required": ["align_audio"]
|
|
36
|
+
}]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Volcengine Lip Sync video resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from runapi.core import Resource, RequestOptions
|
|
8
|
+
|
|
9
|
+
from ..contract_gen import CONTRACT
|
|
10
|
+
from ..types import CompletedLipSyncVideoResponse, LipSyncVideoResponse
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LipSyncVideo(Resource):
|
|
14
|
+
"""Drive a source video's lip movement from an audio track."""
|
|
15
|
+
|
|
16
|
+
ENDPOINT = "/api/v1/volcengine_lip_sync/lip_sync_video"
|
|
17
|
+
|
|
18
|
+
RESPONSE_CLASS = LipSyncVideoResponse
|
|
19
|
+
COMPLETED_RESPONSE_CLASS = CompletedLipSyncVideoResponse
|
|
20
|
+
|
|
21
|
+
def run(self, options: Optional[RequestOptions] = None, **params: Any) -> Any:
|
|
22
|
+
"""Create a task and poll until it completes."""
|
|
23
|
+
task = self.create(options=options, **params)
|
|
24
|
+
return self._poll_until_complete(lambda: self.get(task.id, options=options))
|
|
25
|
+
|
|
26
|
+
def create(self, options: Optional[RequestOptions] = None, **params: Any) -> Any:
|
|
27
|
+
"""Create a lip-sync video task and return immediately with an ``id``."""
|
|
28
|
+
compacted = self._compact_params(params)
|
|
29
|
+
self._validate_contract(CONTRACT["lip-sync-video"], compacted)
|
|
30
|
+
return self._request("post", self.ENDPOINT, body=compacted, options=options)
|
|
31
|
+
|
|
32
|
+
def get(self, id: str, options: Optional[RequestOptions] = None) -> Any:
|
|
33
|
+
"""Fetch the current status of a lip-sync video task."""
|
|
34
|
+
return self._request("get", f"{self.ENDPOINT}/{id}", options=options)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Volcengine Lip Sync response models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from runapi.core import BaseModel, TaskResponse, optional, required
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Video(BaseModel):
|
|
9
|
+
"""A generated video reference."""
|
|
10
|
+
|
|
11
|
+
url = optional(str)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LipSyncVideoResponse(TaskResponse):
|
|
15
|
+
"""Response for a lip-sync video task."""
|
|
16
|
+
|
|
17
|
+
id = required(str)
|
|
18
|
+
status = optional(str, enum=lambda: TaskResponse.Status.ALL)
|
|
19
|
+
videos = optional([lambda: Video])
|
|
20
|
+
error = optional(str)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CompletedLipSyncVideoResponse(LipSyncVideoResponse):
|
|
24
|
+
"""Returned by ``lip_sync_video.run()`` once polling observes completion."""
|
|
25
|
+
|
|
26
|
+
videos = required([lambda: Video])
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from runapi.core import config
|
|
4
|
+
from runapi.core.errors import AuthenticationError, ValidationError
|
|
5
|
+
from runapi.volcengine_lip_sync import VolcengineLipSyncClient
|
|
6
|
+
from runapi.volcengine_lip_sync.resources.lip_sync_video import LipSyncVideo
|
|
7
|
+
from runapi.volcengine_lip_sync.types import CompletedLipSyncVideoResponse, LipSyncVideoResponse
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FakeHttp:
|
|
11
|
+
"""Records (method, path, body) and replays preset responses by call order."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, *responses):
|
|
14
|
+
self._responses = list(responses)
|
|
15
|
+
self.calls = []
|
|
16
|
+
|
|
17
|
+
def request(self, method, path, body=None, options=None):
|
|
18
|
+
self.calls.append((method, path, body))
|
|
19
|
+
if self._responses:
|
|
20
|
+
return self._responses.pop(0)
|
|
21
|
+
return {"id": "task_1", "status": "pending"}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.fixture(autouse=True)
|
|
25
|
+
def reset_config(monkeypatch):
|
|
26
|
+
monkeypatch.delenv("RUNAPI_API_KEY", raising=False)
|
|
27
|
+
monkeypatch.setattr(config, "api_key", None)
|
|
28
|
+
yield
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_accepts_api_key_parameter():
|
|
32
|
+
assert isinstance(VolcengineLipSyncClient(api_key="param-key", http_client=FakeHttp()), VolcengineLipSyncClient)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_falls_back_to_global(monkeypatch):
|
|
36
|
+
monkeypatch.setattr(config, "api_key", "global-key")
|
|
37
|
+
assert isinstance(VolcengineLipSyncClient(http_client=FakeHttp()), VolcengineLipSyncClient)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_falls_back_to_env(monkeypatch):
|
|
41
|
+
monkeypatch.setenv("RUNAPI_API_KEY", "env-key")
|
|
42
|
+
assert isinstance(VolcengineLipSyncClient(http_client=FakeHttp()), VolcengineLipSyncClient)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_raises_without_api_key():
|
|
46
|
+
with pytest.raises(AuthenticationError, match="API key is required"):
|
|
47
|
+
VolcengineLipSyncClient()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_uses_injected_http_client():
|
|
51
|
+
fake = FakeHttp()
|
|
52
|
+
client = VolcengineLipSyncClient(api_key="k", http_client=fake)
|
|
53
|
+
assert client.lip_sync_video._http is fake
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_exposes_resource_accessors():
|
|
57
|
+
client = VolcengineLipSyncClient(api_key="k", http_client=FakeHttp())
|
|
58
|
+
assert isinstance(client.lip_sync_video, LipSyncVideo)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_create_posts_compacted_body():
|
|
62
|
+
fake = FakeHttp({"id": "t1", "status": "pending"})
|
|
63
|
+
client = VolcengineLipSyncClient(api_key="k", http_client=fake)
|
|
64
|
+
result = client.lip_sync_video.create(
|
|
65
|
+
model="volcengine-lip-sync",
|
|
66
|
+
mode="lite",
|
|
67
|
+
source_video_url="https://cdn.runapi.ai/public/samples/volcengine-lip-sync-source.mp4",
|
|
68
|
+
source_audio_url="https://cdn.runapi.ai/public/samples/volcengine-lip-sync-voice-adam.mp3",
|
|
69
|
+
callback_url=None,
|
|
70
|
+
enable_vocal_separation=False,
|
|
71
|
+
)
|
|
72
|
+
assert fake.calls == [
|
|
73
|
+
(
|
|
74
|
+
"post",
|
|
75
|
+
"/api/v1/volcengine_lip_sync/lip_sync_video",
|
|
76
|
+
{
|
|
77
|
+
"model": "volcengine-lip-sync",
|
|
78
|
+
"mode": "lite",
|
|
79
|
+
"source_video_url": "https://cdn.runapi.ai/public/samples/volcengine-lip-sync-source.mp4",
|
|
80
|
+
"source_audio_url": "https://cdn.runapi.ai/public/samples/volcengine-lip-sync-voice-adam.mp3",
|
|
81
|
+
"enable_vocal_separation": False,
|
|
82
|
+
},
|
|
83
|
+
),
|
|
84
|
+
]
|
|
85
|
+
assert isinstance(result, LipSyncVideoResponse)
|
|
86
|
+
assert result.id == "t1"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_get_fetches_by_id():
|
|
90
|
+
fake = FakeHttp({"id": "t1", "status": "processing"})
|
|
91
|
+
client = VolcengineLipSyncClient(api_key="k", http_client=fake)
|
|
92
|
+
client.lip_sync_video.get("t1")
|
|
93
|
+
assert fake.calls == [("get", "/api/v1/volcengine_lip_sync/lip_sync_video/t1", None)]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_run_polls_and_narrows_completed_type():
|
|
97
|
+
fake = FakeHttp(
|
|
98
|
+
{"id": "t1", "status": "pending"},
|
|
99
|
+
{
|
|
100
|
+
"id": "t1",
|
|
101
|
+
"status": "completed",
|
|
102
|
+
"videos": [{"url": "https://cdn.runapi.ai/public/samples/volcengine-lip-sync-result-adam.mp4"}],
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
client = VolcengineLipSyncClient(api_key="k", http_client=fake)
|
|
106
|
+
result = client.lip_sync_video.run(
|
|
107
|
+
model="volcengine-lip-sync",
|
|
108
|
+
mode="lite",
|
|
109
|
+
source_video_url="https://cdn.runapi.ai/public/samples/volcengine-lip-sync-source.mp4",
|
|
110
|
+
source_audio_url="https://cdn.runapi.ai/public/samples/volcengine-lip-sync-voice-adam.mp3",
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
assert isinstance(result, CompletedLipSyncVideoResponse)
|
|
114
|
+
assert result.videos[0].url == "https://cdn.runapi.ai/public/samples/volcengine-lip-sync-result-adam.mp4"
|
|
115
|
+
assert [call[0] for call in fake.calls] == ["post", "get"]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_create_requires_model():
|
|
119
|
+
client = VolcengineLipSyncClient(api_key="k", http_client=FakeHttp())
|
|
120
|
+
with pytest.raises(ValidationError, match="model must be one of: volcengine-lip-sync"):
|
|
121
|
+
client.lip_sync_video.create(
|
|
122
|
+
mode="lite",
|
|
123
|
+
source_video_url="https://cdn.runapi.ai/public/samples/volcengine-lip-sync-source.mp4",
|
|
124
|
+
source_audio_url="https://cdn.runapi.ai/public/samples/volcengine-lip-sync-voice-adam.mp3",
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_create_requires_source_audio_url():
|
|
129
|
+
client = VolcengineLipSyncClient(api_key="k", http_client=FakeHttp())
|
|
130
|
+
with pytest.raises(ValidationError, match="source_audio_url is required"):
|
|
131
|
+
client.lip_sync_video.create(
|
|
132
|
+
model="volcengine-lip-sync",
|
|
133
|
+
mode="lite",
|
|
134
|
+
source_video_url="https://cdn.runapi.ai/public/samples/volcengine-lip-sync-source.mp4",
|
|
135
|
+
)
|