runapi-luma 0.1.0__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_luma-0.1.0/.gitignore +29 -0
- runapi_luma-0.1.0/PKG-INFO +80 -0
- runapi_luma-0.1.0/README.md +67 -0
- runapi_luma-0.1.0/pyproject.toml +30 -0
- runapi_luma-0.1.0/src/runapi/luma/__init__.py +24 -0
- runapi_luma-0.1.0/src/runapi/luma/client.py +28 -0
- runapi_luma-0.1.0/src/runapi/luma/py.typed +0 -0
- runapi_luma-0.1.0/src/runapi/luma/resources/__init__.py +3 -0
- runapi_luma-0.1.0/src/runapi/luma/resources/modify_video.py +39 -0
- runapi_luma-0.1.0/src/runapi/luma/types.py +30 -0
- runapi_luma-0.1.0/tests/test_client.py +133 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Build artifacts
|
|
2
|
+
dist/
|
|
3
|
+
build/
|
|
4
|
+
*.egg-info/
|
|
5
|
+
*.egg
|
|
6
|
+
|
|
7
|
+
# Bytecode
|
|
8
|
+
__pycache__/
|
|
9
|
+
*.py[cod]
|
|
10
|
+
|
|
11
|
+
# Virtual environments
|
|
12
|
+
.venv/
|
|
13
|
+
venv/
|
|
14
|
+
|
|
15
|
+
# uv
|
|
16
|
+
uv.lock
|
|
17
|
+
|
|
18
|
+
# Test / type caches
|
|
19
|
+
.pytest_cache/
|
|
20
|
+
.mypy_cache/
|
|
21
|
+
.ruff_cache/
|
|
22
|
+
.coverage
|
|
23
|
+
htmlcov/
|
|
24
|
+
|
|
25
|
+
# IDE / OS
|
|
26
|
+
.idea/
|
|
27
|
+
.vscode/
|
|
28
|
+
*.swp
|
|
29
|
+
.DS_Store
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: runapi-luma
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Luma modify-video client for RunAPI
|
|
5
|
+
Project-URL: Homepage, https://runapi.ai/models/luma
|
|
6
|
+
Project-URL: Documentation, https://runapi.ai/docs#sdk-luma
|
|
7
|
+
Author-email: RunAPI <contact@runapi.ai>
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
Keywords: ai,luma,modify-video,runapi,sdk,video
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Requires-Dist: runapi-core
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# Luma Python SDK for RunAPI
|
|
15
|
+
|
|
16
|
+
The Luma Python SDK is the language-specific package for Luma on RunAPI. Use it
|
|
17
|
+
for video modification flows when your application needs JSON request bodies,
|
|
18
|
+
task status lookup, and consistent RunAPI errors in Python.
|
|
19
|
+
|
|
20
|
+
For model details, use https://runapi.ai/models/luma; for API reference, use
|
|
21
|
+
https://runapi.ai/docs#luma; for SDK docs, use https://runapi.ai/docs#sdk-luma.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install runapi-luma
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from runapi.luma import LumaClient
|
|
33
|
+
|
|
34
|
+
client = LumaClient() # reads RUNAPI_API_KEY, or pass api_key="sk-..."
|
|
35
|
+
|
|
36
|
+
task = client.modify_video.create(
|
|
37
|
+
model="ray-2-modify-video",
|
|
38
|
+
prompt="Make it cinematic with warm golden tones",
|
|
39
|
+
source_video_url="https://example.com/source.mp4",
|
|
40
|
+
)
|
|
41
|
+
status = client.modify_video.get(task.id)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Use `create` to submit a task and return quickly, `get` to fetch the latest task
|
|
45
|
+
state, and `run` to create and poll until completion:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
result = client.modify_video.run(
|
|
49
|
+
model="ray-2-modify-video",
|
|
50
|
+
prompt="Restyle as a hand-painted animation",
|
|
51
|
+
source_video_url="https://example.com/source.mp4",
|
|
52
|
+
)
|
|
53
|
+
print(result.videos[0].url)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
In web request handlers, prefer `create` plus webhook or later `get` polling so a
|
|
57
|
+
worker is not held open.
|
|
58
|
+
|
|
59
|
+
RunAPI-generated file URLs are temporary. Download and store generated videos in
|
|
60
|
+
your own durable storage within 7 days; do not treat returned URLs as long-term
|
|
61
|
+
assets.
|
|
62
|
+
|
|
63
|
+
## Language notes
|
|
64
|
+
|
|
65
|
+
Pass parameters as keyword arguments and catch the `runapi.luma` error classes
|
|
66
|
+
when building video jobs or scripts. The available resource is `modify_video`.
|
|
67
|
+
Keep `RUNAPI_API_KEY` in the environment or your secret manager; never commit API
|
|
68
|
+
keys or callback secrets.
|
|
69
|
+
|
|
70
|
+
## Links
|
|
71
|
+
|
|
72
|
+
- Model page: https://runapi.ai/models/luma
|
|
73
|
+
- SDK docs: https://runapi.ai/docs#sdk-luma
|
|
74
|
+
- Product docs: https://runapi.ai/docs#luma
|
|
75
|
+
- Pricing and rate limits: https://runapi.ai/models/luma
|
|
76
|
+
- Full catalog: https://runapi.ai/models
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
Licensed under the Apache License, Version 2.0.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Luma Python SDK for RunAPI
|
|
2
|
+
|
|
3
|
+
The Luma Python SDK is the language-specific package for Luma on RunAPI. Use it
|
|
4
|
+
for video modification flows when your application needs JSON request bodies,
|
|
5
|
+
task status lookup, and consistent RunAPI errors in Python.
|
|
6
|
+
|
|
7
|
+
For model details, use https://runapi.ai/models/luma; for API reference, use
|
|
8
|
+
https://runapi.ai/docs#luma; for SDK docs, use https://runapi.ai/docs#sdk-luma.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install runapi-luma
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from runapi.luma import LumaClient
|
|
20
|
+
|
|
21
|
+
client = LumaClient() # reads RUNAPI_API_KEY, or pass api_key="sk-..."
|
|
22
|
+
|
|
23
|
+
task = client.modify_video.create(
|
|
24
|
+
model="ray-2-modify-video",
|
|
25
|
+
prompt="Make it cinematic with warm golden tones",
|
|
26
|
+
source_video_url="https://example.com/source.mp4",
|
|
27
|
+
)
|
|
28
|
+
status = client.modify_video.get(task.id)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Use `create` to submit a task and return quickly, `get` to fetch the latest task
|
|
32
|
+
state, and `run` to create and poll until completion:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
result = client.modify_video.run(
|
|
36
|
+
model="ray-2-modify-video",
|
|
37
|
+
prompt="Restyle as a hand-painted animation",
|
|
38
|
+
source_video_url="https://example.com/source.mp4",
|
|
39
|
+
)
|
|
40
|
+
print(result.videos[0].url)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
In web request handlers, prefer `create` plus webhook or later `get` polling so a
|
|
44
|
+
worker is not held open.
|
|
45
|
+
|
|
46
|
+
RunAPI-generated file URLs are temporary. Download and store generated videos in
|
|
47
|
+
your own durable storage within 7 days; do not treat returned URLs as long-term
|
|
48
|
+
assets.
|
|
49
|
+
|
|
50
|
+
## Language notes
|
|
51
|
+
|
|
52
|
+
Pass parameters as keyword arguments and catch the `runapi.luma` error classes
|
|
53
|
+
when building video jobs or scripts. The available resource is `modify_video`.
|
|
54
|
+
Keep `RUNAPI_API_KEY` in the environment or your secret manager; never commit API
|
|
55
|
+
keys or callback secrets.
|
|
56
|
+
|
|
57
|
+
## Links
|
|
58
|
+
|
|
59
|
+
- Model page: https://runapi.ai/models/luma
|
|
60
|
+
- SDK docs: https://runapi.ai/docs#sdk-luma
|
|
61
|
+
- Product docs: https://runapi.ai/docs#luma
|
|
62
|
+
- Pricing and rate limits: https://runapi.ai/models/luma
|
|
63
|
+
- Full catalog: https://runapi.ai/models
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
Licensed under the Apache License, Version 2.0.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "runapi-luma"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Luma modify-video client for RunAPI"
|
|
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", "luma", "modify-video", "video", "ai", "sdk"]
|
|
14
|
+
dependencies = ["runapi-core"]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://runapi.ai/models/luma"
|
|
18
|
+
Documentation = "https://runapi.ai/docs#sdk-luma"
|
|
19
|
+
|
|
20
|
+
[tool.hatch.build.targets.wheel]
|
|
21
|
+
packages = ["src/runapi"]
|
|
22
|
+
|
|
23
|
+
[tool.uv]
|
|
24
|
+
package = true
|
|
25
|
+
|
|
26
|
+
[tool.uv.sources]
|
|
27
|
+
runapi-core = { path = "../runapi-core", editable = true }
|
|
28
|
+
|
|
29
|
+
[dependency-groups]
|
|
30
|
+
dev = ["pytest>=8"]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Luma 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 LumaClient
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"LumaClient",
|
|
17
|
+
"AuthenticationError",
|
|
18
|
+
"RateLimitError",
|
|
19
|
+
"InsufficientCreditsError",
|
|
20
|
+
"NotFoundError",
|
|
21
|
+
"ValidationError",
|
|
22
|
+
"TaskFailedError",
|
|
23
|
+
"TaskTimeoutError",
|
|
24
|
+
]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Luma 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.modify_video import ModifyVideo
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LumaClient:
|
|
13
|
+
"""Luma modify-video client.
|
|
14
|
+
|
|
15
|
+
Example::
|
|
16
|
+
|
|
17
|
+
client = LumaClient(api_key="sk-...")
|
|
18
|
+
result = client.modify_video.run(
|
|
19
|
+
model="...", prompt="Make it cinematic",
|
|
20
|
+
source_video_url="https://example.com/source.mp4",
|
|
21
|
+
)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, api_key: Optional[str] = None, **options: Any) -> None:
|
|
25
|
+
resolved_api_key = resolve_api_key(api_key)
|
|
26
|
+
client_options = ClientOptions(api_key=resolved_api_key, **options)
|
|
27
|
+
http = client_options.http_client or HttpClient(client_options)
|
|
28
|
+
self.modify_video = ModifyVideo(http)
|
|
File without changes
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Luma modify-video resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict
|
|
6
|
+
|
|
7
|
+
from runapi.core import Resource, ValidationError
|
|
8
|
+
|
|
9
|
+
from ..types import CompletedModifyVideoResponse, ModifyVideoResponse
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ModifyVideo(Resource):
|
|
13
|
+
"""Modify a source video with Luma."""
|
|
14
|
+
|
|
15
|
+
ENDPOINT = "/api/v1/luma/modify_video"
|
|
16
|
+
|
|
17
|
+
RESPONSE_CLASS = ModifyVideoResponse
|
|
18
|
+
COMPLETED_RESPONSE_CLASS = CompletedModifyVideoResponse
|
|
19
|
+
|
|
20
|
+
def run(self, **params: Any) -> Any:
|
|
21
|
+
"""Create a task and poll until it completes."""
|
|
22
|
+
task = self.create(**params)
|
|
23
|
+
return self._poll_until_complete(lambda: self.get(task.id))
|
|
24
|
+
|
|
25
|
+
def create(self, **params: Any) -> Any:
|
|
26
|
+
"""Create a modify-video task and return immediately with an ``id``."""
|
|
27
|
+
compacted = self._compact_params(params)
|
|
28
|
+
self._validate_params(compacted)
|
|
29
|
+
return self._request("post", self.ENDPOINT, body=compacted)
|
|
30
|
+
|
|
31
|
+
def get(self, id: str) -> Any:
|
|
32
|
+
"""Fetch the current status of a modify-video task."""
|
|
33
|
+
return self._request("get", f"{self.ENDPOINT}/{id}")
|
|
34
|
+
|
|
35
|
+
def _validate_params(self, params: Dict[str, Any]) -> None:
|
|
36
|
+
if not params.get("prompt"):
|
|
37
|
+
raise ValidationError("prompt is required")
|
|
38
|
+
if not params.get("source_video_url"):
|
|
39
|
+
raise ValidationError("source_video_url is required")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Luma 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 or source video reference."""
|
|
10
|
+
|
|
11
|
+
url = optional(str)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ModifyVideoResponse(TaskResponse):
|
|
15
|
+
"""Response for a modify-video task."""
|
|
16
|
+
|
|
17
|
+
id = required(str)
|
|
18
|
+
status = optional(str, enum=lambda: TaskResponse.Status.ALL)
|
|
19
|
+
videos = optional([lambda: Video])
|
|
20
|
+
sources = optional([lambda: Video])
|
|
21
|
+
error = optional(str)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CompletedModifyVideoResponse(ModifyVideoResponse):
|
|
25
|
+
"""Returned by ``modify_video.run()`` once polling observes completion.
|
|
26
|
+
|
|
27
|
+
``videos`` is required so callers never have to null-check it on success.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
videos = required([lambda: Video])
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from runapi.core import config
|
|
4
|
+
from runapi.core.errors import AuthenticationError, ValidationError
|
|
5
|
+
from runapi.luma import LumaClient
|
|
6
|
+
from runapi.luma.resources.modify_video import ModifyVideo
|
|
7
|
+
from runapi.luma.types import CompletedModifyVideoResponse, ModifyVideoResponse
|
|
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
|
+
# --- authentication -------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_accepts_api_key_parameter():
|
|
35
|
+
assert isinstance(LumaClient(api_key="param-key", http_client=FakeHttp()), LumaClient)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_falls_back_to_global(monkeypatch):
|
|
39
|
+
monkeypatch.setattr(config, "api_key", "global-key")
|
|
40
|
+
assert isinstance(LumaClient(http_client=FakeHttp()), LumaClient)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_falls_back_to_env(monkeypatch):
|
|
44
|
+
monkeypatch.setenv("RUNAPI_API_KEY", "env-key")
|
|
45
|
+
assert isinstance(LumaClient(http_client=FakeHttp()), LumaClient)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_raises_without_api_key():
|
|
49
|
+
with pytest.raises(AuthenticationError, match="API key is required"):
|
|
50
|
+
LumaClient()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# --- transport injection / accessors --------------------------------------
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_uses_injected_http_client():
|
|
57
|
+
fake = FakeHttp()
|
|
58
|
+
client = LumaClient(api_key="k", http_client=fake)
|
|
59
|
+
assert client.modify_video._http is fake
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_exposes_resource_accessors():
|
|
63
|
+
client = LumaClient(api_key="k", http_client=FakeHttp())
|
|
64
|
+
assert isinstance(client.modify_video, ModifyVideo)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# --- request shapes -------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_create_posts_compacted_body():
|
|
71
|
+
fake = FakeHttp({"id": "t1", "status": "pending"})
|
|
72
|
+
client = LumaClient(api_key="k", http_client=fake)
|
|
73
|
+
result = client.modify_video.create(
|
|
74
|
+
model="ray-2-modify-video",
|
|
75
|
+
prompt="hello",
|
|
76
|
+
source_video_url="https://example.com/source.mp4",
|
|
77
|
+
callback_url=None,
|
|
78
|
+
)
|
|
79
|
+
assert fake.calls == [
|
|
80
|
+
(
|
|
81
|
+
"post",
|
|
82
|
+
"/api/v1/luma/modify_video",
|
|
83
|
+
{
|
|
84
|
+
"model": "ray-2-modify-video",
|
|
85
|
+
"prompt": "hello",
|
|
86
|
+
"source_video_url": "https://example.com/source.mp4",
|
|
87
|
+
},
|
|
88
|
+
),
|
|
89
|
+
]
|
|
90
|
+
assert isinstance(result, ModifyVideoResponse)
|
|
91
|
+
assert result.id == "t1"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_get_fetches_by_id():
|
|
95
|
+
fake = FakeHttp({"id": "t1", "status": "processing"})
|
|
96
|
+
client = LumaClient(api_key="k", http_client=fake)
|
|
97
|
+
client.modify_video.get("t1")
|
|
98
|
+
assert fake.calls == [("get", "/api/v1/luma/modify_video/t1", None)]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_run_polls_and_narrows_completed_type():
|
|
102
|
+
fake = FakeHttp(
|
|
103
|
+
{"id": "t1", "status": "pending"},
|
|
104
|
+
{
|
|
105
|
+
"id": "t1",
|
|
106
|
+
"status": "completed",
|
|
107
|
+
"videos": [{"url": "https://x/y.mp4"}],
|
|
108
|
+
},
|
|
109
|
+
)
|
|
110
|
+
client = LumaClient(api_key="k", http_client=fake)
|
|
111
|
+
result = client.modify_video.run(
|
|
112
|
+
prompt="hi",
|
|
113
|
+
source_video_url="https://example.com/source.mp4",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
assert isinstance(result, CompletedModifyVideoResponse)
|
|
117
|
+
assert result.videos[0].url == "https://x/y.mp4"
|
|
118
|
+
assert [call[0] for call in fake.calls] == ["post", "get"]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# --- validation -----------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_create_requires_prompt():
|
|
125
|
+
client = LumaClient(api_key="k", http_client=FakeHttp())
|
|
126
|
+
with pytest.raises(ValidationError, match="prompt is required"):
|
|
127
|
+
client.modify_video.create(source_video_url="https://example.com/source.mp4")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_create_requires_source_video_url():
|
|
131
|
+
client = LumaClient(api_key="k", http_client=FakeHttp())
|
|
132
|
+
with pytest.raises(ValidationError, match="source_video_url is required"):
|
|
133
|
+
client.modify_video.create(prompt="hi")
|