runapi-veo-3-1 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_veo_3_1-0.1.0/PKG-INFO +81 -0
- runapi_veo_3_1-0.1.0/README.md +68 -0
- runapi_veo_3_1-0.1.0/pyproject.toml +33 -0
- runapi_veo_3_1-0.1.0/src/runapi/veo_3_1/__init__.py +24 -0
- runapi_veo_3_1-0.1.0/src/runapi/veo_3_1/client.py +31 -0
- runapi_veo_3_1-0.1.0/src/runapi/veo_3_1/contract_gen.py +65 -0
- runapi_veo_3_1-0.1.0/src/runapi/veo_3_1/py.typed +0 -0
- runapi_veo_3_1-0.1.0/src/runapi/veo_3_1/resources/__init__.py +5 -0
- runapi_veo_3_1-0.1.0/src/runapi/veo_3_1/resources/extend_video.py +60 -0
- runapi_veo_3_1-0.1.0/src/runapi/veo_3_1/resources/text_to_video.py +100 -0
- runapi_veo_3_1-0.1.0/src/runapi/veo_3_1/resources/upscale_video.py +65 -0
- runapi_veo_3_1-0.1.0/src/runapi/veo_3_1/types.py +65 -0
- runapi_veo_3_1-0.1.0/tests/test_client.py +252 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: runapi-veo-3-1
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Veo 3.1 video client for RunAPI
|
|
5
|
+
Project-URL: Homepage, https://runapi.ai/models/veo-3-1
|
|
6
|
+
Project-URL: Documentation, https://runapi.ai/docs#sdk-veo-3-1
|
|
7
|
+
Author-email: RunAPI <contact@runapi.ai>
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
Keywords: ai,runapi,sdk,text-to-video,veo,veo-3.1
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Requires-Dist: runapi-core
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# Veo API Python SDK for RunAPI
|
|
15
|
+
|
|
16
|
+
The veo api Python SDK is the language-specific package for Veo 3 on RunAPI. Use this veo api package for text-to-video, video extension, and video upscaling flows when your application needs JSON request bodies, task status lookup, and consistent RunAPI errors in Python.
|
|
17
|
+
|
|
18
|
+
This veo api README is the Python package guide inside the public `veo3-sdk` repository. For the repository overview, start at `../README.md`; for model details, use https://runapi.ai/models/veo-3.1; for API reference, use https://runapi.ai/docs#veo-3.1; for SDK docs, use https://runapi.ai/docs#sdk-veo-3.1.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install runapi-veo-3-1
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from runapi.veo_3_1 import Veo31Client
|
|
30
|
+
|
|
31
|
+
client = Veo31Client() # reads RUNAPI_API_KEY, or pass api_key="sk-..."
|
|
32
|
+
|
|
33
|
+
task = client.text_to_video.create(
|
|
34
|
+
model="veo-3.1-fast",
|
|
35
|
+
prompt="A drone shot over mountains at sunset",
|
|
36
|
+
duration_seconds=8,
|
|
37
|
+
)
|
|
38
|
+
status = client.text_to_video.get(task.id)
|
|
39
|
+
|
|
40
|
+
extension = client.extend_video.create(
|
|
41
|
+
source_task_id=task.id,
|
|
42
|
+
prompt="Continue the flight toward the coastline",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
upscale = client.upscale_video.create(
|
|
46
|
+
source_task_id=task.id,
|
|
47
|
+
output_resolution="4k",
|
|
48
|
+
)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Use `create` to submit a task and return quickly, `get` to fetch the latest task state, and `run` when a script should create and poll until completion:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
result = client.text_to_video.run(
|
|
55
|
+
model="veo-3.1-fast",
|
|
56
|
+
prompt="A serene mountain lake at dawn",
|
|
57
|
+
)
|
|
58
|
+
print(result.videos[0].url)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
In web request handlers, prefer `create` plus webhook or later `get` polling so a worker is not held open.
|
|
62
|
+
|
|
63
|
+
RunAPI-generated file URLs are temporary. Download and store generated images, videos, audio, or other files in your own durable storage within 7 days; do not treat returned URLs as long-term assets.
|
|
64
|
+
|
|
65
|
+
## Language notes
|
|
66
|
+
|
|
67
|
+
Pass parameters as keyword arguments and catch the `runapi.veo_3_1` error classes when building video jobs or scripts. Text-to-video request bodies can include `duration` with `4`, `6`, or `8` seconds. The available resources are `text_to_video`, `extend_video`, and `upscale_video`. Keep `RUNAPI_API_KEY` in the environment or your secret manager; never commit API keys or callback secrets.
|
|
68
|
+
|
|
69
|
+
## Links
|
|
70
|
+
|
|
71
|
+
- Model page: https://runapi.ai/models/veo-3.1
|
|
72
|
+
- SDK docs: https://runapi.ai/docs#sdk-veo-3.1
|
|
73
|
+
- Product docs: https://runapi.ai/docs#veo-3.1
|
|
74
|
+
- Pricing and rate limits: https://runapi.ai/models/veo-3.1/veo-3.1
|
|
75
|
+
- Provider comparison: https://runapi.ai/providers/google
|
|
76
|
+
- Full catalog: https://runapi.ai/models
|
|
77
|
+
- Repository: https://github.com/runapi-ai/veo3-sdk
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
Licensed under the Apache License, Version 2.0.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Veo API Python SDK for RunAPI
|
|
2
|
+
|
|
3
|
+
The veo api Python SDK is the language-specific package for Veo 3 on RunAPI. Use this veo api package for text-to-video, video extension, and video upscaling flows when your application needs JSON request bodies, task status lookup, and consistent RunAPI errors in Python.
|
|
4
|
+
|
|
5
|
+
This veo api README is the Python package guide inside the public `veo3-sdk` repository. For the repository overview, start at `../README.md`; for model details, use https://runapi.ai/models/veo-3.1; for API reference, use https://runapi.ai/docs#veo-3.1; for SDK docs, use https://runapi.ai/docs#sdk-veo-3.1.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install runapi-veo-3-1
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from runapi.veo_3_1 import Veo31Client
|
|
17
|
+
|
|
18
|
+
client = Veo31Client() # reads RUNAPI_API_KEY, or pass api_key="sk-..."
|
|
19
|
+
|
|
20
|
+
task = client.text_to_video.create(
|
|
21
|
+
model="veo-3.1-fast",
|
|
22
|
+
prompt="A drone shot over mountains at sunset",
|
|
23
|
+
duration_seconds=8,
|
|
24
|
+
)
|
|
25
|
+
status = client.text_to_video.get(task.id)
|
|
26
|
+
|
|
27
|
+
extension = client.extend_video.create(
|
|
28
|
+
source_task_id=task.id,
|
|
29
|
+
prompt="Continue the flight toward the coastline",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
upscale = client.upscale_video.create(
|
|
33
|
+
source_task_id=task.id,
|
|
34
|
+
output_resolution="4k",
|
|
35
|
+
)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Use `create` to submit a task and return quickly, `get` to fetch the latest task state, and `run` when a script should create and poll until completion:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
result = client.text_to_video.run(
|
|
42
|
+
model="veo-3.1-fast",
|
|
43
|
+
prompt="A serene mountain lake at dawn",
|
|
44
|
+
)
|
|
45
|
+
print(result.videos[0].url)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
In web request handlers, prefer `create` plus webhook or later `get` polling so a worker is not held open.
|
|
49
|
+
|
|
50
|
+
RunAPI-generated file URLs are temporary. Download and store generated images, videos, audio, or other files in your own durable storage within 7 days; do not treat returned URLs as long-term assets.
|
|
51
|
+
|
|
52
|
+
## Language notes
|
|
53
|
+
|
|
54
|
+
Pass parameters as keyword arguments and catch the `runapi.veo_3_1` error classes when building video jobs or scripts. Text-to-video request bodies can include `duration` with `4`, `6`, or `8` seconds. The available resources are `text_to_video`, `extend_video`, and `upscale_video`. Keep `RUNAPI_API_KEY` in the environment or your secret manager; never commit API keys or callback secrets.
|
|
55
|
+
|
|
56
|
+
## Links
|
|
57
|
+
|
|
58
|
+
- Model page: https://runapi.ai/models/veo-3.1
|
|
59
|
+
- SDK docs: https://runapi.ai/docs#sdk-veo-3.1
|
|
60
|
+
- Product docs: https://runapi.ai/docs#veo-3.1
|
|
61
|
+
- Pricing and rate limits: https://runapi.ai/models/veo-3.1/veo-3.1
|
|
62
|
+
- Provider comparison: https://runapi.ai/providers/google
|
|
63
|
+
- Full catalog: https://runapi.ai/models
|
|
64
|
+
- Repository: https://github.com/runapi-ai/veo3-sdk
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
|
|
68
|
+
Licensed under the Apache License, Version 2.0.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "runapi-veo-3-1"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Veo 3.1 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", "veo", "veo-3.1", "text-to-video", "ai", "sdk"]
|
|
14
|
+
dependencies = ["runapi-core"]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://runapi.ai/models/veo-3-1"
|
|
18
|
+
Documentation = "https://runapi.ai/docs#sdk-veo-3-1"
|
|
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 = { workspace = true }
|
|
28
|
+
|
|
29
|
+
[dependency-groups]
|
|
30
|
+
dev = ["pytest>=8"]
|
|
31
|
+
|
|
32
|
+
[tool.runapi]
|
|
33
|
+
slug = "veo-3.1"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Veo 3.1 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 Veo31Client
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"Veo31Client",
|
|
17
|
+
"AuthenticationError",
|
|
18
|
+
"RateLimitError",
|
|
19
|
+
"InsufficientCreditsError",
|
|
20
|
+
"NotFoundError",
|
|
21
|
+
"ValidationError",
|
|
22
|
+
"TaskFailedError",
|
|
23
|
+
"TaskTimeoutError",
|
|
24
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Veo 3.1 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.extend_video import ExtendVideo
|
|
10
|
+
from .resources.text_to_video import TextToVideo
|
|
11
|
+
from .resources.upscale_video import UpscaleVideo
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Veo31Client:
|
|
15
|
+
"""Veo 3.1 video API client.
|
|
16
|
+
|
|
17
|
+
Example::
|
|
18
|
+
|
|
19
|
+
client = Veo31Client(api_key="sk-...")
|
|
20
|
+
result = client.text_to_video.run(
|
|
21
|
+
model="veo-3.1-fast", prompt="A drone shot over mountains at sunset"
|
|
22
|
+
)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, api_key: Optional[str] = None, **options: Any) -> None:
|
|
26
|
+
resolved_api_key = resolve_api_key(api_key)
|
|
27
|
+
client_options = ClientOptions(api_key=resolved_api_key, **options)
|
|
28
|
+
http = client_options.http_client or HttpClient(client_options)
|
|
29
|
+
self.text_to_video = TextToVideo(http)
|
|
30
|
+
self.extend_video = ExtendVideo(http)
|
|
31
|
+
self.upscale_video = UpscaleVideo(http)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Code generated by SdkContractGenerator. DO NOT EDIT.
|
|
2
|
+
|
|
3
|
+
CONTRACT = {
|
|
4
|
+
"extend-video": {
|
|
5
|
+
"models": [],
|
|
6
|
+
"fields_by_model": {
|
|
7
|
+
"_": {
|
|
8
|
+
"seeds": {
|
|
9
|
+
"type": "integer"
|
|
10
|
+
},
|
|
11
|
+
"source_task_id": {
|
|
12
|
+
"required": True
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"text-to-video": {
|
|
18
|
+
"models": ["veo-3.1", "veo-3.1-fast"],
|
|
19
|
+
"fields_by_model": {
|
|
20
|
+
"veo-3.1": {
|
|
21
|
+
"aspect_ratio": {
|
|
22
|
+
"enum": ["16:9", "9:16", "auto"]
|
|
23
|
+
},
|
|
24
|
+
"duration_seconds": {
|
|
25
|
+
"enum": [4, 6, 8],
|
|
26
|
+
"type": "integer"
|
|
27
|
+
},
|
|
28
|
+
"input_mode": {
|
|
29
|
+
"enum": ["text", "first_and_last_frames", "reference"]
|
|
30
|
+
},
|
|
31
|
+
"seeds": {
|
|
32
|
+
"type": "integer"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"veo-3.1-fast": {
|
|
36
|
+
"aspect_ratio": {
|
|
37
|
+
"enum": ["16:9", "9:16", "auto"]
|
|
38
|
+
},
|
|
39
|
+
"duration_seconds": {
|
|
40
|
+
"enum": [4, 6, 8],
|
|
41
|
+
"type": "integer"
|
|
42
|
+
},
|
|
43
|
+
"input_mode": {
|
|
44
|
+
"enum": ["text", "first_and_last_frames", "reference"]
|
|
45
|
+
},
|
|
46
|
+
"seeds": {
|
|
47
|
+
"type": "integer"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"upscale-video": {
|
|
53
|
+
"models": [],
|
|
54
|
+
"fields_by_model": {
|
|
55
|
+
"_": {
|
|
56
|
+
"index": {
|
|
57
|
+
"type": "integer"
|
|
58
|
+
},
|
|
59
|
+
"output_resolution": {
|
|
60
|
+
"enum": ["1080p", "4k"]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Veo 3.1 video extension 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 CompletedExtendVideoResponse, ExtendVideoResponse
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ExtendVideo(Resource):
|
|
13
|
+
"""Extend a previously generated video by referencing its task id."""
|
|
14
|
+
|
|
15
|
+
ENDPOINT = "/api/v1/veo_3_1/extend_video"
|
|
16
|
+
|
|
17
|
+
RESPONSE_CLASS = ExtendVideoResponse
|
|
18
|
+
COMPLETED_RESPONSE_CLASS = CompletedExtendVideoResponse
|
|
19
|
+
|
|
20
|
+
def run(self, **params: Any) -> Any:
|
|
21
|
+
"""Extend a video and poll until it completes.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
**params: extend-video parameters (model, ...).
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
The completed (narrowed) extend-video response.
|
|
28
|
+
"""
|
|
29
|
+
task = self.create(**params)
|
|
30
|
+
return self._poll_until_complete(lambda: self.get(task.id))
|
|
31
|
+
|
|
32
|
+
def create(self, **params: Any) -> Any:
|
|
33
|
+
"""Create an extend-video task and return immediately with an id.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
**params: extend-video parameters (model, ...).
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
The task creation result with an id.
|
|
40
|
+
"""
|
|
41
|
+
compacted = self._compact_params(params)
|
|
42
|
+
self._validate_params(compacted)
|
|
43
|
+
return self._request("post", self.ENDPOINT, body=compacted)
|
|
44
|
+
|
|
45
|
+
def get(self, id: str) -> Any:
|
|
46
|
+
"""Fetch the current status of an extend-video task.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
id: The task id returned by ``create``.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
The current task status.
|
|
53
|
+
"""
|
|
54
|
+
return self._request("get", f"{self.ENDPOINT}/{id}")
|
|
55
|
+
|
|
56
|
+
def _validate_params(self, params: Dict[str, Any]) -> None:
|
|
57
|
+
if not params.get("source_task_id"):
|
|
58
|
+
raise ValidationError("source_task_id is required")
|
|
59
|
+
if not params.get("prompt"):
|
|
60
|
+
raise ValidationError("prompt is required")
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Veo 3.1 text-to-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 ..contract_gen import CONTRACT
|
|
10
|
+
from ..types import (
|
|
11
|
+
CompletedTextToVideoResponse,
|
|
12
|
+
TextToVideoResponse,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TextToVideo(Resource):
|
|
17
|
+
"""Generate videos from text, image, and reference-image prompts."""
|
|
18
|
+
|
|
19
|
+
ENDPOINT = "/api/v1/veo_3_1/text_to_video"
|
|
20
|
+
|
|
21
|
+
RESPONSE_CLASS = TextToVideoResponse
|
|
22
|
+
COMPLETED_RESPONSE_CLASS = CompletedTextToVideoResponse
|
|
23
|
+
|
|
24
|
+
def run(self, **params: Any) -> Any:
|
|
25
|
+
"""Generate a video and poll until it completes.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
**params: text-to-video parameters (model, ...).
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
The completed (narrowed) text-to-video response.
|
|
32
|
+
"""
|
|
33
|
+
task = self.create(**params)
|
|
34
|
+
return self._poll_until_complete(lambda: self.get(task.id))
|
|
35
|
+
|
|
36
|
+
def create(self, **params: Any) -> Any:
|
|
37
|
+
"""Create a text-to-video task and return immediately with an id.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
**params: text-to-video parameters (model, ...).
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
The task creation result with an id.
|
|
44
|
+
"""
|
|
45
|
+
compacted = self._compact_params(params)
|
|
46
|
+
self._validate_params(compacted)
|
|
47
|
+
return self._request("post", self.ENDPOINT, body=compacted)
|
|
48
|
+
|
|
49
|
+
def get(self, id: str) -> Any:
|
|
50
|
+
"""Fetch the current status of a text-to-video task.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
id: The task id returned by ``create``.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
The current task status.
|
|
57
|
+
"""
|
|
58
|
+
return self._request("get", f"{self.ENDPOINT}/{id}")
|
|
59
|
+
|
|
60
|
+
def _validate_params(self, params: Dict[str, Any]) -> None:
|
|
61
|
+
self._validate_contract(CONTRACT["text-to-video"], params)
|
|
62
|
+
|
|
63
|
+
if not params.get("prompt"):
|
|
64
|
+
raise ValidationError("prompt is required")
|
|
65
|
+
|
|
66
|
+
self._validate_input_mode(params)
|
|
67
|
+
|
|
68
|
+
def _validate_input_mode(self, params: Dict[str, Any]) -> None:
|
|
69
|
+
input_mode = params.get("input_mode")
|
|
70
|
+
if not input_mode:
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
if input_mode == "first_and_last_frames":
|
|
74
|
+
if not self._field_present(params, "first_frame_image_url"):
|
|
75
|
+
raise ValidationError("first_frame_image_url is required for first_and_last_frames")
|
|
76
|
+
if self._field_present(params, "reference_image_urls"):
|
|
77
|
+
raise ValidationError("reference_image_urls requires input_mode reference")
|
|
78
|
+
elif input_mode == "reference":
|
|
79
|
+
urls = params.get("reference_image_urls")
|
|
80
|
+
if not urls:
|
|
81
|
+
raise ValidationError("reference_image_urls is required for reference")
|
|
82
|
+
if not (isinstance(urls, list) and 1 <= len(urls) <= 3):
|
|
83
|
+
raise ValidationError("reference_image_urls must contain 1-3 items for reference")
|
|
84
|
+
model = params.get("model")
|
|
85
|
+
if model != "veo-3.1-fast":
|
|
86
|
+
raise ValidationError("reference requires model veo-3.1-fast")
|
|
87
|
+
ar = params.get("aspect_ratio")
|
|
88
|
+
if ar and ar != "16:9":
|
|
89
|
+
raise ValidationError("reference requires aspect_ratio 16:9")
|
|
90
|
+
if self._field_present(params, "first_frame_image_url") or self._field_present(params, "last_frame_image_url"):
|
|
91
|
+
raise ValidationError(
|
|
92
|
+
"first_frame_image_url and last_frame_image_url require input_mode first_and_last_frames"
|
|
93
|
+
)
|
|
94
|
+
else:
|
|
95
|
+
if self._field_present(params, "first_frame_image_url") or self._field_present(params, "last_frame_image_url"):
|
|
96
|
+
raise ValidationError(
|
|
97
|
+
"first_frame_image_url and last_frame_image_url require input_mode first_and_last_frames"
|
|
98
|
+
)
|
|
99
|
+
if self._field_present(params, "reference_image_urls"):
|
|
100
|
+
raise ValidationError("reference_image_urls requires input_mode reference")
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Veo 3.1 video upscaling 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 (
|
|
10
|
+
OUTPUT_RESOLUTIONS,
|
|
11
|
+
CompletedUpscaleVideoResponse,
|
|
12
|
+
UpscaleVideoResponse,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class UpscaleVideo(Resource):
|
|
17
|
+
"""Upscale a previously generated video by referencing its task id."""
|
|
18
|
+
|
|
19
|
+
ENDPOINT = "/api/v1/veo_3_1/upscale_video"
|
|
20
|
+
|
|
21
|
+
RESPONSE_CLASS = UpscaleVideoResponse
|
|
22
|
+
COMPLETED_RESPONSE_CLASS = CompletedUpscaleVideoResponse
|
|
23
|
+
|
|
24
|
+
def run(self, **params: Any) -> Any:
|
|
25
|
+
"""Upscale a video and poll until it completes.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
**params: video upscale parameters (model, ...).
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
The completed (narrowed) video upscale response.
|
|
32
|
+
"""
|
|
33
|
+
task = self.create(**params)
|
|
34
|
+
return self._poll_until_complete(lambda: self.get(task.id))
|
|
35
|
+
|
|
36
|
+
def create(self, **params: Any) -> Any:
|
|
37
|
+
"""Create a video upscale task and return immediately with an id.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
**params: video upscale parameters (model, ...).
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
The task creation result with an id.
|
|
44
|
+
"""
|
|
45
|
+
compacted = self._compact_params(params)
|
|
46
|
+
self._validate_params(compacted)
|
|
47
|
+
return self._request("post", self.ENDPOINT, body=compacted)
|
|
48
|
+
|
|
49
|
+
def get(self, id: str) -> Any:
|
|
50
|
+
"""Fetch the current status of a video upscale task.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
id: The task id returned by ``create``.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
The current task status.
|
|
57
|
+
"""
|
|
58
|
+
return self._request("get", f"{self.ENDPOINT}/{id}")
|
|
59
|
+
|
|
60
|
+
def _validate_params(self, params: Dict[str, Any]) -> None:
|
|
61
|
+
if not params.get("source_task_id"):
|
|
62
|
+
raise ValidationError("source_task_id is required")
|
|
63
|
+
output_resolution = params.get("output_resolution")
|
|
64
|
+
if output_resolution not in OUTPUT_RESOLUTIONS:
|
|
65
|
+
raise ValidationError(f"output_resolution must be one of: {', '.join(OUTPUT_RESOLUTIONS)}")
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Veo 3.1 model lists, enums, and response models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from runapi.core import BaseModel, TaskResponse, optional, required
|
|
6
|
+
|
|
7
|
+
OUTPUT_RESOLUTIONS = ["1080p", "4k"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Video(BaseModel):
|
|
11
|
+
url = optional(str)
|
|
12
|
+
resolution = optional(str)
|
|
13
|
+
has_audio = optional()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Source(BaseModel):
|
|
17
|
+
url = optional(str)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AsyncTaskResponse(TaskResponse):
|
|
21
|
+
"""Veo 3.1 async task status response."""
|
|
22
|
+
|
|
23
|
+
id = required(str)
|
|
24
|
+
status = optional(str, enum=lambda: TaskResponse.Status.ALL)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TextToVideoResponse(AsyncTaskResponse):
|
|
28
|
+
"""Veo 3.1 text-to-video task status response."""
|
|
29
|
+
|
|
30
|
+
videos = optional([lambda: Video])
|
|
31
|
+
sources = optional([lambda: Source])
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ExtendVideoResponse(AsyncTaskResponse):
|
|
35
|
+
"""Veo 3.1 extend-video task status response."""
|
|
36
|
+
|
|
37
|
+
videos = optional([lambda: Video])
|
|
38
|
+
sources = optional([lambda: Source])
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class UpscaleVideoResponse(AsyncTaskResponse):
|
|
42
|
+
"""Veo 3.1 video upscale task status response."""
|
|
43
|
+
|
|
44
|
+
source_task_id = optional(str)
|
|
45
|
+
videos = optional([lambda: Video])
|
|
46
|
+
sources = optional([lambda: Source])
|
|
47
|
+
media_ids = optional([str])
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class CompletedTextToVideoResponse(TextToVideoResponse):
|
|
51
|
+
"""Narrowed response from ``run()`` once polling observes completion."""
|
|
52
|
+
|
|
53
|
+
videos = required([lambda: Video])
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class CompletedExtendVideoResponse(ExtendVideoResponse):
|
|
57
|
+
"""Narrowed response from ``run()`` once polling observes completion."""
|
|
58
|
+
|
|
59
|
+
videos = required([lambda: Video])
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class CompletedUpscaleVideoResponse(UpscaleVideoResponse):
|
|
63
|
+
"""Narrowed response from ``run()`` once polling observes completion."""
|
|
64
|
+
|
|
65
|
+
videos = required([lambda: Video])
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from runapi.core import config
|
|
4
|
+
from runapi.core.errors import AuthenticationError, ValidationError
|
|
5
|
+
from runapi.veo_3_1 import Veo31Client
|
|
6
|
+
from runapi.veo_3_1.resources.extend_video import ExtendVideo
|
|
7
|
+
from runapi.veo_3_1.resources.text_to_video import TextToVideo
|
|
8
|
+
from runapi.veo_3_1.resources.upscale_video import UpscaleVideo
|
|
9
|
+
from runapi.veo_3_1.types import (
|
|
10
|
+
CompletedExtendVideoResponse,
|
|
11
|
+
CompletedTextToVideoResponse,
|
|
12
|
+
CompletedUpscaleVideoResponse,
|
|
13
|
+
ExtendVideoResponse,
|
|
14
|
+
TextToVideoResponse,
|
|
15
|
+
UpscaleVideoResponse,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FakeHttp:
|
|
20
|
+
def __init__(self, *responses):
|
|
21
|
+
self._responses = list(responses)
|
|
22
|
+
self.calls = []
|
|
23
|
+
|
|
24
|
+
def request(self, method, path, body=None, options=None):
|
|
25
|
+
self.calls.append((method, path, body))
|
|
26
|
+
if self._responses:
|
|
27
|
+
return self._responses.pop(0)
|
|
28
|
+
return {"id": "task_1", "status": "pending"}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.fixture(autouse=True)
|
|
32
|
+
def reset_config(monkeypatch):
|
|
33
|
+
monkeypatch.delenv("RUNAPI_API_KEY", raising=False)
|
|
34
|
+
monkeypatch.setattr(config, "api_key", None)
|
|
35
|
+
yield
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# --- auth -----------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_accepts_api_key_parameter():
|
|
42
|
+
assert isinstance(Veo31Client(api_key="k", http_client=FakeHttp()), Veo31Client)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_falls_back_to_global(monkeypatch):
|
|
46
|
+
monkeypatch.setattr(config, "api_key", "global-key")
|
|
47
|
+
assert isinstance(Veo31Client(http_client=FakeHttp()), Veo31Client)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_falls_back_to_env(monkeypatch):
|
|
51
|
+
monkeypatch.setenv("RUNAPI_API_KEY", "env-key")
|
|
52
|
+
assert isinstance(Veo31Client(http_client=FakeHttp()), Veo31Client)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_raises_without_api_key():
|
|
56
|
+
with pytest.raises(AuthenticationError, match="API key is required"):
|
|
57
|
+
Veo31Client()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# --- injection / accessors ------------------------------------------------
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_uses_injected_http_client():
|
|
64
|
+
fake = FakeHttp()
|
|
65
|
+
client = Veo31Client(api_key="k", http_client=fake)
|
|
66
|
+
assert client.text_to_video._http is fake
|
|
67
|
+
assert client.extend_video._http is fake
|
|
68
|
+
assert client.upscale_video._http is fake
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_exposes_resource_accessors():
|
|
72
|
+
client = Veo31Client(api_key="k", http_client=FakeHttp())
|
|
73
|
+
assert isinstance(client.text_to_video, TextToVideo)
|
|
74
|
+
assert isinstance(client.extend_video, ExtendVideo)
|
|
75
|
+
assert isinstance(client.upscale_video, UpscaleVideo)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# --- request shapes -------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_text_to_video_create_posts_compacted_body():
|
|
82
|
+
fake = FakeHttp({"id": "t1", "status": "pending"})
|
|
83
|
+
client = Veo31Client(api_key="k", http_client=fake)
|
|
84
|
+
result = client.text_to_video.create(
|
|
85
|
+
model="veo-3.1-fast", prompt="a kite", aspect_ratio="16:9", duration_seconds=None
|
|
86
|
+
)
|
|
87
|
+
assert fake.calls == [
|
|
88
|
+
("post", "/api/v1/veo_3_1/text_to_video", {"model": "veo-3.1-fast", "prompt": "a kite", "aspect_ratio": "16:9"}),
|
|
89
|
+
]
|
|
90
|
+
assert isinstance(result, TextToVideoResponse)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_text_to_video_get_fetches_by_id():
|
|
94
|
+
fake = FakeHttp({"id": "t1", "status": "processing"})
|
|
95
|
+
client = Veo31Client(api_key="k", http_client=fake)
|
|
96
|
+
client.text_to_video.get("t1")
|
|
97
|
+
assert fake.calls == [("get", "/api/v1/veo_3_1/text_to_video/t1", None)]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_extend_video_create_posts_source_task_id():
|
|
101
|
+
fake = FakeHttp({"id": "e1", "status": "pending"})
|
|
102
|
+
client = Veo31Client(api_key="k", http_client=fake)
|
|
103
|
+
result = client.extend_video.create(source_task_id="t1", prompt="keep going")
|
|
104
|
+
assert fake.calls == [
|
|
105
|
+
("post", "/api/v1/veo_3_1/extend_video", {"source_task_id": "t1", "prompt": "keep going"}),
|
|
106
|
+
]
|
|
107
|
+
assert isinstance(result, ExtendVideoResponse)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_extend_video_get_fetches_by_id():
|
|
111
|
+
fake = FakeHttp({"id": "e1", "status": "processing"})
|
|
112
|
+
client = Veo31Client(api_key="k", http_client=fake)
|
|
113
|
+
client.extend_video.get("e1")
|
|
114
|
+
assert fake.calls == [("get", "/api/v1/veo_3_1/extend_video/e1", None)]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_upscale_video_create_posts_source_task_id():
|
|
118
|
+
fake = FakeHttp({"id": "u1", "status": "pending"})
|
|
119
|
+
client = Veo31Client(api_key="k", http_client=fake)
|
|
120
|
+
result = client.upscale_video.create(source_task_id="t1", output_resolution="4k")
|
|
121
|
+
assert fake.calls == [
|
|
122
|
+
("post", "/api/v1/veo_3_1/upscale_video", {"source_task_id": "t1", "output_resolution": "4k"}),
|
|
123
|
+
]
|
|
124
|
+
assert isinstance(result, UpscaleVideoResponse)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_upscale_video_get_fetches_by_id():
|
|
128
|
+
fake = FakeHttp({"id": "u1", "status": "processing"})
|
|
129
|
+
client = Veo31Client(api_key="k", http_client=fake)
|
|
130
|
+
client.upscale_video.get("u1")
|
|
131
|
+
assert fake.calls == [("get", "/api/v1/veo_3_1/upscale_video/u1", None)]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# --- run narrowing --------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_text_to_video_run_narrows_completed_type():
|
|
138
|
+
fake = FakeHttp(
|
|
139
|
+
{"id": "t1", "status": "pending"},
|
|
140
|
+
{"id": "t1", "status": "completed", "videos": [{"url": "https://x/y.mp4"}]},
|
|
141
|
+
)
|
|
142
|
+
client = Veo31Client(api_key="k", http_client=fake)
|
|
143
|
+
result = client.text_to_video.run(model="veo-3.1-fast", prompt="a serene lake")
|
|
144
|
+
assert isinstance(result, CompletedTextToVideoResponse)
|
|
145
|
+
assert result.videos[0].url == "https://x/y.mp4"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_extend_video_run_narrows_completed_type():
|
|
149
|
+
fake = FakeHttp(
|
|
150
|
+
{"id": "e1", "status": "pending"},
|
|
151
|
+
{"id": "e1", "status": "completed", "videos": [{"url": "https://x/e.mp4"}]},
|
|
152
|
+
)
|
|
153
|
+
client = Veo31Client(api_key="k", http_client=fake)
|
|
154
|
+
result = client.extend_video.run(source_task_id="t1", prompt="continue")
|
|
155
|
+
assert isinstance(result, CompletedExtendVideoResponse)
|
|
156
|
+
assert result.videos[0].url == "https://x/e.mp4"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_upscale_video_run_narrows_completed_type():
|
|
160
|
+
fake = FakeHttp(
|
|
161
|
+
{"id": "u1", "status": "pending"},
|
|
162
|
+
{"id": "u1", "status": "completed", "videos": [{"url": "https://x/u.mp4"}]},
|
|
163
|
+
)
|
|
164
|
+
client = Veo31Client(api_key="k", http_client=fake)
|
|
165
|
+
result = client.upscale_video.run(source_task_id="t1", output_resolution="1080p")
|
|
166
|
+
assert isinstance(result, CompletedUpscaleVideoResponse)
|
|
167
|
+
assert result.videos[0].url == "https://x/u.mp4"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# --- validation -----------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_text_to_video_requires_model():
|
|
174
|
+
client = Veo31Client(api_key="k", http_client=FakeHttp())
|
|
175
|
+
with pytest.raises(ValidationError, match="model must be one of: veo-3.1, veo-3.1-fast"):
|
|
176
|
+
client.text_to_video.create(prompt="hi")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_text_to_video_requires_prompt():
|
|
180
|
+
client = Veo31Client(api_key="k", http_client=FakeHttp())
|
|
181
|
+
with pytest.raises(ValidationError, match="prompt is required"):
|
|
182
|
+
client.text_to_video.create(model="veo-3.1-fast")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_text_to_video_rejects_unknown_model():
|
|
186
|
+
client = Veo31Client(api_key="k", http_client=FakeHttp())
|
|
187
|
+
with pytest.raises(ValidationError, match="model must be one of: veo-3.1, veo-3.1-fast"):
|
|
188
|
+
client.text_to_video.create(model="nope", prompt="hi")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def test_text_to_video_rejects_invalid_aspect_ratio():
|
|
192
|
+
client = Veo31Client(api_key="k", http_client=FakeHttp())
|
|
193
|
+
with pytest.raises(ValidationError, match="aspect_ratio must be one of: 16:9, 9:16, auto"):
|
|
194
|
+
client.text_to_video.create(model="veo-3.1-fast", prompt="hi", aspect_ratio="4:3")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_text_to_video_rejects_invalid_duration():
|
|
198
|
+
client = Veo31Client(api_key="k", http_client=FakeHttp())
|
|
199
|
+
with pytest.raises(ValidationError, match="duration_seconds must be one of: 4, 6, 8"):
|
|
200
|
+
client.text_to_video.create(model="veo-3.1-fast", prompt="hi", duration_seconds=5)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def test_first_and_last_frames_requires_first_frame():
|
|
204
|
+
client = Veo31Client(api_key="k", http_client=FakeHttp())
|
|
205
|
+
with pytest.raises(ValidationError, match="first_frame_image_url is required for first_and_last_frames"):
|
|
206
|
+
client.text_to_video.create(model="veo-3.1-fast", prompt="hi", input_mode="first_and_last_frames")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def test_reference_requires_reference_image_urls():
|
|
210
|
+
client = Veo31Client(api_key="k", http_client=FakeHttp())
|
|
211
|
+
with pytest.raises(ValidationError, match="reference_image_urls is required for reference"):
|
|
212
|
+
client.text_to_video.create(model="veo-3.1-fast", prompt="hi", input_mode="reference")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_reference_requires_fast_model():
|
|
216
|
+
client = Veo31Client(api_key="k", http_client=FakeHttp())
|
|
217
|
+
with pytest.raises(ValidationError, match="reference requires model veo-3.1-fast"):
|
|
218
|
+
client.text_to_video.create(
|
|
219
|
+
model="veo-3.1", prompt="hi", input_mode="reference", reference_image_urls=["https://x/a.png"]
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def test_reference_image_urls_without_reference_mode_rejected():
|
|
224
|
+
client = Veo31Client(api_key="k", http_client=FakeHttp())
|
|
225
|
+
with pytest.raises(ValidationError, match="reference_image_urls requires input_mode reference"):
|
|
226
|
+
client.text_to_video.create(
|
|
227
|
+
model="veo-3.1-fast", prompt="hi", input_mode="text", reference_image_urls=["https://x/a.png"]
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def test_extend_video_requires_source_task_id():
|
|
232
|
+
client = Veo31Client(api_key="k", http_client=FakeHttp())
|
|
233
|
+
with pytest.raises(ValidationError, match="source_task_id is required"):
|
|
234
|
+
client.extend_video.create(prompt="continue")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def test_extend_video_requires_prompt():
|
|
238
|
+
client = Veo31Client(api_key="k", http_client=FakeHttp())
|
|
239
|
+
with pytest.raises(ValidationError, match="prompt is required"):
|
|
240
|
+
client.extend_video.create(source_task_id="t1")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def test_upscale_video_requires_source_task_id():
|
|
244
|
+
client = Veo31Client(api_key="k", http_client=FakeHttp())
|
|
245
|
+
with pytest.raises(ValidationError, match="source_task_id is required"):
|
|
246
|
+
client.upscale_video.create(output_resolution="4k")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def test_upscale_video_rejects_invalid_output_resolution():
|
|
250
|
+
client = Veo31Client(api_key="k", http_client=FakeHttp())
|
|
251
|
+
with pytest.raises(ValidationError, match="output_resolution must be one of"):
|
|
252
|
+
client.upscale_video.create(source_task_id="t1", output_resolution="720p")
|