runapi-grok-imagine 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_grok_imagine-0.1.0/.gitignore +29 -0
- runapi_grok_imagine-0.1.0/PKG-INFO +89 -0
- runapi_grok_imagine-0.1.0/README.md +76 -0
- runapi_grok_imagine-0.1.0/pyproject.toml +30 -0
- runapi_grok_imagine-0.1.0/src/runapi/grok_imagine/__init__.py +24 -0
- runapi_grok_imagine-0.1.0/src/runapi/grok_imagine/client.py +39 -0
- runapi_grok_imagine-0.1.0/src/runapi/grok_imagine/py.typed +0 -0
- runapi_grok_imagine-0.1.0/src/runapi/grok_imagine/resources/__init__.py +15 -0
- runapi_grok_imagine-0.1.0/src/runapi/grok_imagine/resources/edit_image.py +64 -0
- runapi_grok_imagine-0.1.0/src/runapi/grok_imagine/resources/extensions.py +78 -0
- runapi_grok_imagine-0.1.0/src/runapi/grok_imagine/resources/image_to_video.py +97 -0
- runapi_grok_imagine-0.1.0/src/runapi/grok_imagine/resources/text_to_image.py +66 -0
- runapi_grok_imagine-0.1.0/src/runapi/grok_imagine/resources/text_to_video.py +80 -0
- runapi_grok_imagine-0.1.0/src/runapi/grok_imagine/resources/upscales.py +64 -0
- runapi_grok_imagine-0.1.0/src/runapi/grok_imagine/types.py +50 -0
- runapi_grok_imagine-0.1.0/tests/test_client.py +352 -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,89 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: runapi-grok-imagine
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Grok-Imagine multimodal generation client for RunAPI
|
|
5
|
+
Project-URL: Homepage, https://runapi.ai/models/grok-imagine
|
|
6
|
+
Project-URL: Documentation, https://runapi.ai/docs#sdk-grok-imagine
|
|
7
|
+
Author-email: RunAPI <contact@runapi.ai>
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
Keywords: ai,grok-imagine,image-to-video,runapi,sdk,text-to-image,text-to-video
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Requires-Dist: runapi-core
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# Grok-Imagine Python SDK for RunAPI
|
|
15
|
+
|
|
16
|
+
The Grok-Imagine Python SDK is the language-specific package for Grok-Imagine on
|
|
17
|
+
RunAPI. Use it for text-to-video, image-to-video, text-to-image, and image
|
|
18
|
+
editing flows when your application needs JSON request bodies, task status
|
|
19
|
+
lookup, and consistent RunAPI errors in Python.
|
|
20
|
+
|
|
21
|
+
For model details, use https://runapi.ai/models/grok-imagine; for API reference,
|
|
22
|
+
use https://runapi.ai/docs#grok-imagine; for SDK docs, use
|
|
23
|
+
https://runapi.ai/docs#sdk-grok-imagine.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install runapi-grok-imagine
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick start
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from runapi.grok_imagine import GrokImagineClient
|
|
35
|
+
|
|
36
|
+
client = GrokImagineClient() # reads RUNAPI_API_KEY, or pass api_key="sk-..."
|
|
37
|
+
|
|
38
|
+
task = client.text_to_video.create(
|
|
39
|
+
model="grok-imagine-text-to-video",
|
|
40
|
+
prompt="A drone shot over a neon cityscape at night",
|
|
41
|
+
output_resolution="720p",
|
|
42
|
+
)
|
|
43
|
+
status = client.text_to_video.get(task.id)
|
|
44
|
+
|
|
45
|
+
image = client.text_to_image.create(
|
|
46
|
+
model="grok-imagine-text-to-image",
|
|
47
|
+
prompt="A watercolor fox in a snowy forest",
|
|
48
|
+
aspect_ratio="16:9",
|
|
49
|
+
)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Use `create` to submit a task and return quickly, `get` to fetch the latest task
|
|
53
|
+
state, and `run` to create and poll until completion:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
result = client.text_to_video.run(
|
|
57
|
+
model="grok-imagine-text-to-video",
|
|
58
|
+
prompt="A serene mountain lake at dawn",
|
|
59
|
+
)
|
|
60
|
+
print(result.videos[0].url)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
In web request handlers, prefer `create` plus webhook or later `get` polling so a
|
|
64
|
+
worker is not held open.
|
|
65
|
+
|
|
66
|
+
RunAPI-generated file URLs are temporary. Download and store generated media in
|
|
67
|
+
your own durable storage within 7 days; do not treat returned URLs as long-term
|
|
68
|
+
assets.
|
|
69
|
+
|
|
70
|
+
## Language notes
|
|
71
|
+
|
|
72
|
+
Pass parameters as keyword arguments and catch the `runapi.grok_imagine` error
|
|
73
|
+
classes when building media jobs or scripts. The available resources are
|
|
74
|
+
`text_to_video`, `image_to_video`, `text_to_image`, `edit_image`, `extensions`,
|
|
75
|
+
and `upscales`. The `extensions` and `upscales` resources take a prior task's
|
|
76
|
+
`source_task_id`. Keep `RUNAPI_API_KEY` in the environment or your secret
|
|
77
|
+
manager; never commit API keys or callback secrets.
|
|
78
|
+
|
|
79
|
+
## Links
|
|
80
|
+
|
|
81
|
+
- Model page: https://runapi.ai/models/grok-imagine
|
|
82
|
+
- SDK docs: https://runapi.ai/docs#sdk-grok-imagine
|
|
83
|
+
- Product docs: https://runapi.ai/docs#grok-imagine
|
|
84
|
+
- Pricing and rate limits: https://runapi.ai/models/grok-imagine
|
|
85
|
+
- Full catalog: https://runapi.ai/models
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
Licensed under the Apache License, Version 2.0.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Grok-Imagine Python SDK for RunAPI
|
|
2
|
+
|
|
3
|
+
The Grok-Imagine Python SDK is the language-specific package for Grok-Imagine on
|
|
4
|
+
RunAPI. Use it for text-to-video, image-to-video, text-to-image, and image
|
|
5
|
+
editing flows when your application needs JSON request bodies, task status
|
|
6
|
+
lookup, and consistent RunAPI errors in Python.
|
|
7
|
+
|
|
8
|
+
For model details, use https://runapi.ai/models/grok-imagine; for API reference,
|
|
9
|
+
use https://runapi.ai/docs#grok-imagine; for SDK docs, use
|
|
10
|
+
https://runapi.ai/docs#sdk-grok-imagine.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install runapi-grok-imagine
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from runapi.grok_imagine import GrokImagineClient
|
|
22
|
+
|
|
23
|
+
client = GrokImagineClient() # reads RUNAPI_API_KEY, or pass api_key="sk-..."
|
|
24
|
+
|
|
25
|
+
task = client.text_to_video.create(
|
|
26
|
+
model="grok-imagine-text-to-video",
|
|
27
|
+
prompt="A drone shot over a neon cityscape at night",
|
|
28
|
+
output_resolution="720p",
|
|
29
|
+
)
|
|
30
|
+
status = client.text_to_video.get(task.id)
|
|
31
|
+
|
|
32
|
+
image = client.text_to_image.create(
|
|
33
|
+
model="grok-imagine-text-to-image",
|
|
34
|
+
prompt="A watercolor fox in a snowy forest",
|
|
35
|
+
aspect_ratio="16:9",
|
|
36
|
+
)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Use `create` to submit a task and return quickly, `get` to fetch the latest task
|
|
40
|
+
state, and `run` to create and poll until completion:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
result = client.text_to_video.run(
|
|
44
|
+
model="grok-imagine-text-to-video",
|
|
45
|
+
prompt="A serene mountain lake at dawn",
|
|
46
|
+
)
|
|
47
|
+
print(result.videos[0].url)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
In web request handlers, prefer `create` plus webhook or later `get` polling so a
|
|
51
|
+
worker is not held open.
|
|
52
|
+
|
|
53
|
+
RunAPI-generated file URLs are temporary. Download and store generated media in
|
|
54
|
+
your own durable storage within 7 days; do not treat returned URLs as long-term
|
|
55
|
+
assets.
|
|
56
|
+
|
|
57
|
+
## Language notes
|
|
58
|
+
|
|
59
|
+
Pass parameters as keyword arguments and catch the `runapi.grok_imagine` error
|
|
60
|
+
classes when building media jobs or scripts. The available resources are
|
|
61
|
+
`text_to_video`, `image_to_video`, `text_to_image`, `edit_image`, `extensions`,
|
|
62
|
+
and `upscales`. The `extensions` and `upscales` resources take a prior task's
|
|
63
|
+
`source_task_id`. Keep `RUNAPI_API_KEY` in the environment or your secret
|
|
64
|
+
manager; never commit API keys or callback secrets.
|
|
65
|
+
|
|
66
|
+
## Links
|
|
67
|
+
|
|
68
|
+
- Model page: https://runapi.ai/models/grok-imagine
|
|
69
|
+
- SDK docs: https://runapi.ai/docs#sdk-grok-imagine
|
|
70
|
+
- Product docs: https://runapi.ai/docs#grok-imagine
|
|
71
|
+
- Pricing and rate limits: https://runapi.ai/models/grok-imagine
|
|
72
|
+
- Full catalog: https://runapi.ai/models
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
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-grok-imagine"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Grok-Imagine multimodal generation 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", "grok-imagine", "text-to-video", "image-to-video", "text-to-image", "ai", "sdk"]
|
|
14
|
+
dependencies = ["runapi-core"]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://runapi.ai/models/grok-imagine"
|
|
18
|
+
Documentation = "https://runapi.ai/docs#sdk-grok-imagine"
|
|
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
|
+
"""Grok-Imagine 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 GrokImagineClient
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"GrokImagineClient",
|
|
17
|
+
"AuthenticationError",
|
|
18
|
+
"RateLimitError",
|
|
19
|
+
"InsufficientCreditsError",
|
|
20
|
+
"NotFoundError",
|
|
21
|
+
"ValidationError",
|
|
22
|
+
"TaskFailedError",
|
|
23
|
+
"TaskTimeoutError",
|
|
24
|
+
]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Grok-Imagine 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.edit_image import EditImage
|
|
10
|
+
from .resources.extensions import Extensions
|
|
11
|
+
from .resources.image_to_video import ImageToVideo
|
|
12
|
+
from .resources.text_to_image import TextToImage
|
|
13
|
+
from .resources.text_to_video import TextToVideo
|
|
14
|
+
from .resources.upscales import Upscales
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class GrokImagineClient:
|
|
18
|
+
"""Grok-Imagine multimodal generation client.
|
|
19
|
+
|
|
20
|
+
Example::
|
|
21
|
+
|
|
22
|
+
client = GrokImagineClient(api_key="sk-...")
|
|
23
|
+
result = client.text_to_video.run(
|
|
24
|
+
model="grok-imagine-text-to-video",
|
|
25
|
+
prompt="A drone shot over a neon cityscape",
|
|
26
|
+
output_resolution="720p",
|
|
27
|
+
)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, api_key: Optional[str] = None, **options: Any) -> None:
|
|
31
|
+
resolved_api_key = resolve_api_key(api_key)
|
|
32
|
+
client_options = ClientOptions(api_key=resolved_api_key, **options)
|
|
33
|
+
http = client_options.http_client or HttpClient(client_options)
|
|
34
|
+
self.text_to_video = TextToVideo(http)
|
|
35
|
+
self.image_to_video = ImageToVideo(http)
|
|
36
|
+
self.text_to_image = TextToImage(http)
|
|
37
|
+
self.edit_image = EditImage(http)
|
|
38
|
+
self.extensions = Extensions(http)
|
|
39
|
+
self.upscales = Upscales(http)
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .edit_image import EditImage
|
|
2
|
+
from .extensions import Extensions
|
|
3
|
+
from .image_to_video import ImageToVideo
|
|
4
|
+
from .text_to_image import TextToImage
|
|
5
|
+
from .text_to_video import TextToVideo
|
|
6
|
+
from .upscales import Upscales
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"TextToVideo",
|
|
10
|
+
"ImageToVideo",
|
|
11
|
+
"TextToImage",
|
|
12
|
+
"EditImage",
|
|
13
|
+
"Extensions",
|
|
14
|
+
"Upscales",
|
|
15
|
+
]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Grok-Imagine prompt-guided image editing 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
|
+
EDIT_IMAGE_MODEL,
|
|
11
|
+
CompletedImageTaskResponse,
|
|
12
|
+
ImageTaskResponse,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class EditImage(Resource):
|
|
17
|
+
"""Edit an image with a text prompt using Grok-Imagine."""
|
|
18
|
+
|
|
19
|
+
ENDPOINT = "/api/v1/grok_imagine/edit_image"
|
|
20
|
+
|
|
21
|
+
RESPONSE_CLASS = ImageTaskResponse
|
|
22
|
+
COMPLETED_RESPONSE_CLASS = CompletedImageTaskResponse
|
|
23
|
+
|
|
24
|
+
def run(self, **params: Any) -> Any:
|
|
25
|
+
"""Create an edit-image task and poll until it completes.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
**params: Edit-image parameters (model, prompt, ...).
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
The completed edit-image 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 an edit-image task and return immediately with an id.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
**params: Edit-image parameters (model, prompt, ...).
|
|
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 an edit-image task.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
id: Task id.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
The current edit-image status.
|
|
57
|
+
"""
|
|
58
|
+
return self._request("get", f"{self.ENDPOINT}/{id}")
|
|
59
|
+
|
|
60
|
+
def _validate_params(self, params: Dict[str, Any]) -> None:
|
|
61
|
+
if params.get("model") != EDIT_IMAGE_MODEL:
|
|
62
|
+
raise ValidationError("model is required")
|
|
63
|
+
if not params.get("source_image_url"):
|
|
64
|
+
raise ValidationError("source_image_url is required")
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Grok-Imagine video extension resource.
|
|
2
|
+
|
|
3
|
+
Takes a prior grok-imagine video source_task_id and extends it.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any, Dict
|
|
9
|
+
|
|
10
|
+
from runapi.core import Resource, ValidationError
|
|
11
|
+
|
|
12
|
+
from ..types import (
|
|
13
|
+
EXTENSION_DURATION_SECONDS,
|
|
14
|
+
CompletedVideoTaskResponse,
|
|
15
|
+
VideoTaskResponse,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Extensions(Resource):
|
|
20
|
+
"""Extend a previously generated Grok-Imagine video."""
|
|
21
|
+
|
|
22
|
+
ENDPOINT = "/api/v1/grok_imagine/extend_video"
|
|
23
|
+
|
|
24
|
+
RESPONSE_CLASS = VideoTaskResponse
|
|
25
|
+
COMPLETED_RESPONSE_CLASS = CompletedVideoTaskResponse
|
|
26
|
+
|
|
27
|
+
def run(self, **params: Any) -> Any:
|
|
28
|
+
"""Create a video extension task and poll until it completes.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
**params: Video extension parameters (model, prompt, ...).
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
The completed video extension response.
|
|
35
|
+
"""
|
|
36
|
+
task = self.create(**params)
|
|
37
|
+
return self._poll_until_complete(lambda: self.get(task.id))
|
|
38
|
+
|
|
39
|
+
def create(self, **params: Any) -> Any:
|
|
40
|
+
"""Create a video extension task and return immediately with an id.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
**params: Video extension parameters (model, prompt, ...).
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
The task creation result with an id.
|
|
47
|
+
"""
|
|
48
|
+
compacted = self._compact_params(params)
|
|
49
|
+
self._validate_params(compacted)
|
|
50
|
+
return self._request("post", self.ENDPOINT, body=compacted)
|
|
51
|
+
|
|
52
|
+
def get(self, id: str) -> Any:
|
|
53
|
+
"""Fetch the current status of a video extension task.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
id: Task id.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
The current video extension status.
|
|
60
|
+
"""
|
|
61
|
+
return self._request("get", f"{self.ENDPOINT}/{id}")
|
|
62
|
+
|
|
63
|
+
def _validate_params(self, params: Dict[str, Any]) -> None:
|
|
64
|
+
if not params.get("source_task_id"):
|
|
65
|
+
raise ValidationError("source_task_id is required")
|
|
66
|
+
if not params.get("prompt"):
|
|
67
|
+
raise ValidationError("prompt is required")
|
|
68
|
+
# start_seconds is numeric and 0 (extend from the beginning) is valid, so
|
|
69
|
+
# check for absence explicitly rather than treating 0 as missing.
|
|
70
|
+
if params.get("start_seconds") is None:
|
|
71
|
+
raise ValidationError("start_seconds is required")
|
|
72
|
+
|
|
73
|
+
extension_duration_seconds = params.get("extension_duration_seconds")
|
|
74
|
+
if not extension_duration_seconds:
|
|
75
|
+
raise ValidationError("extension_duration_seconds is required")
|
|
76
|
+
if extension_duration_seconds not in EXTENSION_DURATION_SECONDS:
|
|
77
|
+
joined = ", ".join(str(value) for value in EXTENSION_DURATION_SECONDS)
|
|
78
|
+
raise ValidationError(f"extension_duration_seconds must be one of: {joined}")
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Grok-Imagine image-to-video generation resource.
|
|
2
|
+
|
|
3
|
+
Accepts either external source_image_urls or a prior text-to-image
|
|
4
|
+
source_task_id (+ index).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Dict
|
|
10
|
+
|
|
11
|
+
from runapi.core import Resource, ValidationError
|
|
12
|
+
|
|
13
|
+
from ..types import (
|
|
14
|
+
ASPECT_RATIOS,
|
|
15
|
+
IMAGE_TO_VIDEO_MODEL,
|
|
16
|
+
INDEX_RANGE,
|
|
17
|
+
MOTION_STYLES,
|
|
18
|
+
RESOLUTIONS,
|
|
19
|
+
CompletedVideoTaskResponse,
|
|
20
|
+
VideoTaskResponse,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ImageToVideo(Resource):
|
|
25
|
+
"""Generate videos from a source image with Grok-Imagine."""
|
|
26
|
+
|
|
27
|
+
ENDPOINT = "/api/v1/grok_imagine/image_to_video"
|
|
28
|
+
|
|
29
|
+
RESPONSE_CLASS = VideoTaskResponse
|
|
30
|
+
COMPLETED_RESPONSE_CLASS = CompletedVideoTaskResponse
|
|
31
|
+
|
|
32
|
+
def run(self, **params: Any) -> Any:
|
|
33
|
+
"""Create an image-to-video task and poll until it completes.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
**params: Image-to-video parameters (model, prompt, ...).
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
The completed image-to-video response.
|
|
40
|
+
"""
|
|
41
|
+
task = self.create(**params)
|
|
42
|
+
return self._poll_until_complete(lambda: self.get(task.id))
|
|
43
|
+
|
|
44
|
+
def create(self, **params: Any) -> Any:
|
|
45
|
+
"""Create an image-to-video task and return immediately with an id.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
**params: Image-to-video parameters (model, prompt, ...).
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
The task creation result with an id.
|
|
52
|
+
"""
|
|
53
|
+
compacted = self._compact_params(params)
|
|
54
|
+
self._validate_params(compacted)
|
|
55
|
+
return self._request("post", self.ENDPOINT, body=compacted)
|
|
56
|
+
|
|
57
|
+
def get(self, id: str) -> Any:
|
|
58
|
+
"""Fetch the current status of an image-to-video task.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
id: Task id.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
The current image-to-video status.
|
|
65
|
+
"""
|
|
66
|
+
return self._request("get", f"{self.ENDPOINT}/{id}")
|
|
67
|
+
|
|
68
|
+
def _validate_params(self, params: Dict[str, Any]) -> None:
|
|
69
|
+
if params.get("model") != IMAGE_TO_VIDEO_MODEL:
|
|
70
|
+
raise ValidationError("model is required")
|
|
71
|
+
|
|
72
|
+
source_image_urls = params.get("source_image_urls")
|
|
73
|
+
source_task_id = params.get("source_task_id")
|
|
74
|
+
|
|
75
|
+
if source_image_urls and len(source_image_urls) > 0 and source_task_id:
|
|
76
|
+
raise ValidationError("Provide either source_image_urls or source_task_id, not both")
|
|
77
|
+
if (not source_image_urls or len(source_image_urls) == 0) and not source_task_id:
|
|
78
|
+
raise ValidationError("One of source_image_urls or source_task_id is required")
|
|
79
|
+
if source_image_urls and len(source_image_urls) > 1:
|
|
80
|
+
raise ValidationError("source_image_urls supports at most 1 entry")
|
|
81
|
+
|
|
82
|
+
if source_task_id:
|
|
83
|
+
index = params.get("index")
|
|
84
|
+
if index is not None:
|
|
85
|
+
try:
|
|
86
|
+
value = None if isinstance(index, bool) else int(index)
|
|
87
|
+
except (TypeError, ValueError):
|
|
88
|
+
value = None
|
|
89
|
+
if value is None or value not in INDEX_RANGE:
|
|
90
|
+
raise ValidationError("index must be an integer between 0 and 5")
|
|
91
|
+
|
|
92
|
+
self._validate_optional(params, "aspect_ratio", ASPECT_RATIOS)
|
|
93
|
+
self._validate_optional(params, "motion_style", MOTION_STYLES)
|
|
94
|
+
self._validate_optional(params, "output_resolution", RESOLUTIONS)
|
|
95
|
+
|
|
96
|
+
if str(params.get("motion_style")) == "spicy" and source_image_urls and len(source_image_urls) > 0:
|
|
97
|
+
raise ValidationError("spicy motion_style requires a source_task_id source image.")
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Grok-Imagine text-to-image generation 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
|
+
ASPECT_RATIOS,
|
|
11
|
+
TEXT_TO_IMAGE_MODEL,
|
|
12
|
+
CompletedImageTaskResponse,
|
|
13
|
+
ImageTaskResponse,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TextToImage(Resource):
|
|
18
|
+
"""Generate images from text prompts with Grok-Imagine."""
|
|
19
|
+
|
|
20
|
+
ENDPOINT = "/api/v1/grok_imagine/text_to_image"
|
|
21
|
+
|
|
22
|
+
RESPONSE_CLASS = ImageTaskResponse
|
|
23
|
+
COMPLETED_RESPONSE_CLASS = CompletedImageTaskResponse
|
|
24
|
+
|
|
25
|
+
def run(self, **params: Any) -> Any:
|
|
26
|
+
"""Create a text-to-image task and poll until it completes.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
**params: Text-to-image parameters (model, prompt, ...).
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
The completed text-to-image response.
|
|
33
|
+
"""
|
|
34
|
+
task = self.create(**params)
|
|
35
|
+
return self._poll_until_complete(lambda: self.get(task.id))
|
|
36
|
+
|
|
37
|
+
def create(self, **params: Any) -> Any:
|
|
38
|
+
"""Create a text-to-image task and return immediately with an id.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
**params: Text-to-image parameters (model, prompt, ...).
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
The task creation result with an id.
|
|
45
|
+
"""
|
|
46
|
+
compacted = self._compact_params(params)
|
|
47
|
+
self._validate_params(compacted)
|
|
48
|
+
return self._request("post", self.ENDPOINT, body=compacted)
|
|
49
|
+
|
|
50
|
+
def get(self, id: str) -> Any:
|
|
51
|
+
"""Fetch the current status of a text-to-image task.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
id: Task id.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
The current text-to-image status.
|
|
58
|
+
"""
|
|
59
|
+
return self._request("get", f"{self.ENDPOINT}/{id}")
|
|
60
|
+
|
|
61
|
+
def _validate_params(self, params: Dict[str, Any]) -> None:
|
|
62
|
+
if params.get("model") != TEXT_TO_IMAGE_MODEL:
|
|
63
|
+
raise ValidationError("model is required")
|
|
64
|
+
if not params.get("prompt"):
|
|
65
|
+
raise ValidationError("prompt is required")
|
|
66
|
+
self._validate_optional(params, "aspect_ratio", ASPECT_RATIOS)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Grok-Imagine text-to-video generation 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
|
+
ASPECT_RATIOS,
|
|
11
|
+
DURATION_RANGE,
|
|
12
|
+
MOTION_STYLES,
|
|
13
|
+
RESOLUTIONS,
|
|
14
|
+
TEXT_TO_VIDEO_MODEL,
|
|
15
|
+
CompletedVideoTaskResponse,
|
|
16
|
+
VideoTaskResponse,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TextToVideo(Resource):
|
|
21
|
+
"""Generate videos from text prompts with Grok-Imagine."""
|
|
22
|
+
|
|
23
|
+
ENDPOINT = "/api/v1/grok_imagine/text_to_video"
|
|
24
|
+
|
|
25
|
+
RESPONSE_CLASS = VideoTaskResponse
|
|
26
|
+
COMPLETED_RESPONSE_CLASS = CompletedVideoTaskResponse
|
|
27
|
+
|
|
28
|
+
def run(self, **params: Any) -> Any:
|
|
29
|
+
"""Create a text-to-video task and poll until it completes.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
**params: Text-to-video parameters (model, prompt, ...).
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
The completed text-to-video response.
|
|
36
|
+
"""
|
|
37
|
+
task = self.create(**params)
|
|
38
|
+
return self._poll_until_complete(lambda: self.get(task.id))
|
|
39
|
+
|
|
40
|
+
def create(self, **params: Any) -> Any:
|
|
41
|
+
"""Create a text-to-video task and return immediately with an id.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
**params: Text-to-video parameters (model, prompt, ...).
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
The task creation result with an id.
|
|
48
|
+
"""
|
|
49
|
+
compacted = self._compact_params(params)
|
|
50
|
+
self._validate_params(compacted)
|
|
51
|
+
return self._request("post", self.ENDPOINT, body=compacted)
|
|
52
|
+
|
|
53
|
+
def get(self, id: str) -> Any:
|
|
54
|
+
"""Fetch the current status of a text-to-video task.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
id: Task id.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
The current text-to-video status.
|
|
61
|
+
"""
|
|
62
|
+
return self._request("get", f"{self.ENDPOINT}/{id}")
|
|
63
|
+
|
|
64
|
+
def _validate_params(self, params: Dict[str, Any]) -> None:
|
|
65
|
+
if params.get("model") != TEXT_TO_VIDEO_MODEL:
|
|
66
|
+
raise ValidationError("model is required")
|
|
67
|
+
if not params.get("prompt"):
|
|
68
|
+
raise ValidationError("prompt is required")
|
|
69
|
+
self._validate_optional(params, "aspect_ratio", ASPECT_RATIOS)
|
|
70
|
+
self._validate_optional(params, "motion_style", MOTION_STYLES)
|
|
71
|
+
self._validate_optional(params, "output_resolution", RESOLUTIONS)
|
|
72
|
+
|
|
73
|
+
duration_seconds = params.get("duration_seconds")
|
|
74
|
+
if duration_seconds:
|
|
75
|
+
try:
|
|
76
|
+
value = int(duration_seconds)
|
|
77
|
+
except (TypeError, ValueError):
|
|
78
|
+
value = None
|
|
79
|
+
if value is None or value not in DURATION_RANGE:
|
|
80
|
+
raise ValidationError("duration_seconds must be an integer between 6 and 30")
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Grok-Imagine video upscale resource.
|
|
2
|
+
|
|
3
|
+
Takes a prior grok-imagine video source_task_id and upscales it.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any, Dict
|
|
9
|
+
|
|
10
|
+
from runapi.core import Resource, ValidationError
|
|
11
|
+
|
|
12
|
+
from ..types import (
|
|
13
|
+
CompletedVideoTaskResponse,
|
|
14
|
+
VideoTaskResponse,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Upscales(Resource):
|
|
19
|
+
"""Upscale a previously generated Grok-Imagine video."""
|
|
20
|
+
|
|
21
|
+
ENDPOINT = "/api/v1/grok_imagine/upscale_image"
|
|
22
|
+
|
|
23
|
+
RESPONSE_CLASS = VideoTaskResponse
|
|
24
|
+
COMPLETED_RESPONSE_CLASS = CompletedVideoTaskResponse
|
|
25
|
+
|
|
26
|
+
def run(self, **params: Any) -> Any:
|
|
27
|
+
"""Create a video upscale task and poll until it completes.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
**params: Video upscale parameters (model, prompt, ...).
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
The completed video upscale response.
|
|
34
|
+
"""
|
|
35
|
+
task = self.create(**params)
|
|
36
|
+
return self._poll_until_complete(lambda: self.get(task.id))
|
|
37
|
+
|
|
38
|
+
def create(self, **params: Any) -> Any:
|
|
39
|
+
"""Create a video upscale task and return immediately with an id.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
**params: Video upscale parameters (model, prompt, ...).
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
The task creation result with an id.
|
|
46
|
+
"""
|
|
47
|
+
compacted = self._compact_params(params)
|
|
48
|
+
self._validate_params(compacted)
|
|
49
|
+
return self._request("post", self.ENDPOINT, body=compacted)
|
|
50
|
+
|
|
51
|
+
def get(self, id: str) -> Any:
|
|
52
|
+
"""Fetch the current status of a video upscale task.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
id: Task id.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
The current video upscale status.
|
|
59
|
+
"""
|
|
60
|
+
return self._request("get", f"{self.ENDPOINT}/{id}")
|
|
61
|
+
|
|
62
|
+
def _validate_params(self, params: Dict[str, Any]) -> None:
|
|
63
|
+
if not params.get("source_task_id"):
|
|
64
|
+
raise ValidationError("source_task_id is required")
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Grok-Imagine model identifiers, enums, and response models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from runapi.core import BaseModel, TaskResponse, optional, required
|
|
6
|
+
|
|
7
|
+
TEXT_TO_VIDEO_MODEL = "grok-imagine-text-to-video"
|
|
8
|
+
IMAGE_TO_VIDEO_MODEL = "grok-imagine-image-to-video"
|
|
9
|
+
TEXT_TO_IMAGE_MODEL = "grok-imagine-text-to-image"
|
|
10
|
+
EDIT_IMAGE_MODEL = "grok-imagine-edit-image"
|
|
11
|
+
|
|
12
|
+
ASPECT_RATIOS = ["2:3", "3:2", "1:1", "16:9", "9:16"]
|
|
13
|
+
MOTION_STYLES = ["fun", "normal", "spicy"]
|
|
14
|
+
RESOLUTIONS = ["480p", "720p"]
|
|
15
|
+
DURATION_RANGE = range(6, 31)
|
|
16
|
+
EXTENSION_DURATION_SECONDS = [6, 10]
|
|
17
|
+
INDEX_RANGE = range(0, 6)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MediaUrl(BaseModel):
|
|
21
|
+
url = optional(str)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AsyncTaskResponse(TaskResponse):
|
|
25
|
+
id = required(str)
|
|
26
|
+
status = optional(str, enum=lambda: TaskResponse.Status.ALL)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class VideoTaskResponse(AsyncTaskResponse):
|
|
30
|
+
"""Task status/result for Grok-Imagine video generation."""
|
|
31
|
+
videos = optional([lambda: MediaUrl])
|
|
32
|
+
error = optional(str)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CompletedVideoTaskResponse(VideoTaskResponse):
|
|
36
|
+
"""Narrowed video response from ``run()`` once polling observes completion."""
|
|
37
|
+
|
|
38
|
+
videos = required([lambda: MediaUrl])
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ImageTaskResponse(AsyncTaskResponse):
|
|
42
|
+
"""Task status/result for Grok-Imagine image generation."""
|
|
43
|
+
images = optional([lambda: MediaUrl])
|
|
44
|
+
error = optional(str)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class CompletedImageTaskResponse(ImageTaskResponse):
|
|
48
|
+
"""Narrowed image response from ``run()`` once polling observes completion."""
|
|
49
|
+
|
|
50
|
+
images = required([lambda: MediaUrl])
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from runapi.core import config
|
|
4
|
+
from runapi.core.errors import AuthenticationError, ValidationError
|
|
5
|
+
from runapi.grok_imagine import GrokImagineClient
|
|
6
|
+
from runapi.grok_imagine.resources.edit_image import EditImage
|
|
7
|
+
from runapi.grok_imagine.resources.extensions import Extensions
|
|
8
|
+
from runapi.grok_imagine.resources.image_to_video import ImageToVideo
|
|
9
|
+
from runapi.grok_imagine.resources.text_to_image import TextToImage
|
|
10
|
+
from runapi.grok_imagine.resources.text_to_video import TextToVideo
|
|
11
|
+
from runapi.grok_imagine.resources.upscales import Upscales
|
|
12
|
+
from runapi.grok_imagine.types import (
|
|
13
|
+
CompletedImageTaskResponse,
|
|
14
|
+
CompletedVideoTaskResponse,
|
|
15
|
+
ImageTaskResponse,
|
|
16
|
+
VideoTaskResponse,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FakeHttp:
|
|
21
|
+
def __init__(self, *responses):
|
|
22
|
+
self._responses = list(responses)
|
|
23
|
+
self.calls = []
|
|
24
|
+
|
|
25
|
+
def request(self, method, path, body=None, options=None):
|
|
26
|
+
self.calls.append((method, path, body))
|
|
27
|
+
if self._responses:
|
|
28
|
+
return self._responses.pop(0)
|
|
29
|
+
return {"id": "task_1", "status": "pending"}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.fixture(autouse=True)
|
|
33
|
+
def reset_config(monkeypatch):
|
|
34
|
+
monkeypatch.delenv("RUNAPI_API_KEY", raising=False)
|
|
35
|
+
monkeypatch.setattr(config, "api_key", None)
|
|
36
|
+
yield
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# --- auth -----------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_accepts_api_key_parameter():
|
|
43
|
+
assert isinstance(GrokImagineClient(api_key="k", http_client=FakeHttp()), GrokImagineClient)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_falls_back_to_global(monkeypatch):
|
|
47
|
+
monkeypatch.setattr(config, "api_key", "global-key")
|
|
48
|
+
assert isinstance(GrokImagineClient(http_client=FakeHttp()), GrokImagineClient)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_falls_back_to_env(monkeypatch):
|
|
52
|
+
monkeypatch.setenv("RUNAPI_API_KEY", "env-key")
|
|
53
|
+
assert isinstance(GrokImagineClient(http_client=FakeHttp()), GrokImagineClient)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_raises_without_api_key():
|
|
57
|
+
with pytest.raises(AuthenticationError, match="API key is required"):
|
|
58
|
+
GrokImagineClient()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# --- injection / accessors ------------------------------------------------
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_uses_injected_http_client():
|
|
65
|
+
fake = FakeHttp()
|
|
66
|
+
client = GrokImagineClient(api_key="k", http_client=fake)
|
|
67
|
+
assert client.text_to_video._http is fake
|
|
68
|
+
assert client.image_to_video._http is fake
|
|
69
|
+
assert client.text_to_image._http is fake
|
|
70
|
+
assert client.edit_image._http is fake
|
|
71
|
+
assert client.extensions._http is fake
|
|
72
|
+
assert client.upscales._http is fake
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_exposes_resource_accessors():
|
|
76
|
+
client = GrokImagineClient(api_key="k", http_client=FakeHttp())
|
|
77
|
+
assert isinstance(client.text_to_video, TextToVideo)
|
|
78
|
+
assert isinstance(client.image_to_video, ImageToVideo)
|
|
79
|
+
assert isinstance(client.text_to_image, TextToImage)
|
|
80
|
+
assert isinstance(client.edit_image, EditImage)
|
|
81
|
+
assert isinstance(client.extensions, Extensions)
|
|
82
|
+
assert isinstance(client.upscales, Upscales)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# --- request shapes -------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_text_to_video_create_posts_compacted_body():
|
|
89
|
+
fake = FakeHttp({"id": "t1", "status": "pending"})
|
|
90
|
+
client = GrokImagineClient(api_key="k", http_client=fake)
|
|
91
|
+
result = client.text_to_video.create(
|
|
92
|
+
model="grok-imagine-text-to-video",
|
|
93
|
+
prompt="a neon city",
|
|
94
|
+
output_resolution="720p",
|
|
95
|
+
motion_style=None,
|
|
96
|
+
)
|
|
97
|
+
assert fake.calls == [
|
|
98
|
+
(
|
|
99
|
+
"post",
|
|
100
|
+
"/api/v1/grok_imagine/text_to_video",
|
|
101
|
+
{"model": "grok-imagine-text-to-video", "prompt": "a neon city", "output_resolution": "720p"},
|
|
102
|
+
),
|
|
103
|
+
]
|
|
104
|
+
assert isinstance(result, VideoTaskResponse)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_text_to_video_get_fetches_by_id():
|
|
108
|
+
fake = FakeHttp({"id": "t1", "status": "processing"})
|
|
109
|
+
client = GrokImagineClient(api_key="k", http_client=fake)
|
|
110
|
+
client.text_to_video.get("t1")
|
|
111
|
+
assert fake.calls == [("get", "/api/v1/grok_imagine/text_to_video/t1", None)]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_text_to_image_create_shape():
|
|
115
|
+
fake = FakeHttp({"id": "t1", "status": "pending"})
|
|
116
|
+
client = GrokImagineClient(api_key="k", http_client=fake)
|
|
117
|
+
result = client.text_to_image.create(model="grok-imagine-text-to-image", prompt="a fox", aspect_ratio="1:1")
|
|
118
|
+
assert fake.calls == [
|
|
119
|
+
(
|
|
120
|
+
"post",
|
|
121
|
+
"/api/v1/grok_imagine/text_to_image",
|
|
122
|
+
{"model": "grok-imagine-text-to-image", "prompt": "a fox", "aspect_ratio": "1:1"},
|
|
123
|
+
),
|
|
124
|
+
]
|
|
125
|
+
assert isinstance(result, ImageTaskResponse)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_image_to_video_create_shape():
|
|
129
|
+
fake = FakeHttp({"id": "t1", "status": "pending"})
|
|
130
|
+
client = GrokImagineClient(api_key="k", http_client=fake)
|
|
131
|
+
client.image_to_video.create(
|
|
132
|
+
model="grok-imagine-image-to-video", source_image_urls=["https://x/a.png"]
|
|
133
|
+
)
|
|
134
|
+
assert fake.calls == [
|
|
135
|
+
(
|
|
136
|
+
"post",
|
|
137
|
+
"/api/v1/grok_imagine/image_to_video",
|
|
138
|
+
{"model": "grok-imagine-image-to-video", "source_image_urls": ["https://x/a.png"]},
|
|
139
|
+
),
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_edit_image_create_shape():
|
|
144
|
+
fake = FakeHttp({"id": "t1", "status": "pending"})
|
|
145
|
+
client = GrokImagineClient(api_key="k", http_client=fake)
|
|
146
|
+
client.edit_image.create(model="grok-imagine-edit-image", source_image_url="https://x/a.png")
|
|
147
|
+
assert fake.calls == [
|
|
148
|
+
(
|
|
149
|
+
"post",
|
|
150
|
+
"/api/v1/grok_imagine/edit_image",
|
|
151
|
+
{"model": "grok-imagine-edit-image", "source_image_url": "https://x/a.png"},
|
|
152
|
+
),
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_extensions_create_uses_task_id_shape():
|
|
157
|
+
fake = FakeHttp({"id": "t1", "status": "pending"})
|
|
158
|
+
client = GrokImagineClient(api_key="k", http_client=fake)
|
|
159
|
+
result = client.extensions.create(
|
|
160
|
+
source_task_id="src_1",
|
|
161
|
+
prompt="keep going",
|
|
162
|
+
start_seconds=4,
|
|
163
|
+
extension_duration_seconds=6,
|
|
164
|
+
)
|
|
165
|
+
assert fake.calls == [
|
|
166
|
+
(
|
|
167
|
+
"post",
|
|
168
|
+
"/api/v1/grok_imagine/extend_video",
|
|
169
|
+
{
|
|
170
|
+
"source_task_id": "src_1",
|
|
171
|
+
"prompt": "keep going",
|
|
172
|
+
"start_seconds": 4,
|
|
173
|
+
"extension_duration_seconds": 6,
|
|
174
|
+
},
|
|
175
|
+
),
|
|
176
|
+
]
|
|
177
|
+
assert isinstance(result, VideoTaskResponse)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def test_upscales_create_uses_task_id_shape():
|
|
181
|
+
fake = FakeHttp({"id": "t1", "status": "pending"})
|
|
182
|
+
client = GrokImagineClient(api_key="k", http_client=fake)
|
|
183
|
+
client.upscales.create(source_task_id="src_1")
|
|
184
|
+
assert fake.calls == [
|
|
185
|
+
("post", "/api/v1/grok_imagine/upscale_image", {"source_task_id": "src_1"}),
|
|
186
|
+
]
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def test_text_to_video_non_numeric_duration_raises_validation_error():
|
|
190
|
+
# Regression: a non-numeric duration must raise the SDK's ValidationError,
|
|
191
|
+
# not a bare ValueError from int(). Fails if int() is unguarded again.
|
|
192
|
+
client = GrokImagineClient(api_key="k", http_client=FakeHttp())
|
|
193
|
+
with pytest.raises(ValidationError, match="duration_seconds must be an integer between 6 and 30"):
|
|
194
|
+
client.text_to_video.create(
|
|
195
|
+
model="grok-imagine-text-to-video", prompt="a fox", duration_seconds="6s"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def test_image_to_video_non_numeric_index_raises_validation_error():
|
|
200
|
+
client = GrokImagineClient(api_key="k", http_client=FakeHttp())
|
|
201
|
+
with pytest.raises(ValidationError, match="index must be an integer between 0 and 5"):
|
|
202
|
+
client.image_to_video.create(
|
|
203
|
+
model="grok-imagine-image-to-video", source_task_id="src_1", index="abc"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_extensions_get_fetches_by_id():
|
|
208
|
+
fake = FakeHttp({"id": "t1", "status": "processing"})
|
|
209
|
+
client = GrokImagineClient(api_key="k", http_client=fake)
|
|
210
|
+
client.extensions.get("t1")
|
|
211
|
+
assert fake.calls == [("get", "/api/v1/grok_imagine/extend_video/t1", None)]
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# --- run narrowing --------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def test_run_narrows_completed_video_type():
|
|
218
|
+
fake = FakeHttp(
|
|
219
|
+
{"id": "t1", "status": "pending"},
|
|
220
|
+
{"id": "t1", "status": "completed", "videos": [{"url": "https://x/y.mp4"}]},
|
|
221
|
+
)
|
|
222
|
+
client = GrokImagineClient(api_key="k", http_client=fake)
|
|
223
|
+
result = client.text_to_video.run(model="grok-imagine-text-to-video", prompt="a lake")
|
|
224
|
+
assert isinstance(result, CompletedVideoTaskResponse)
|
|
225
|
+
assert result.videos[0].url == "https://x/y.mp4"
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def test_run_narrows_completed_image_type():
|
|
229
|
+
fake = FakeHttp(
|
|
230
|
+
{"id": "t1", "status": "pending"},
|
|
231
|
+
{"id": "t1", "status": "completed", "images": [{"url": "https://x/y.png"}]},
|
|
232
|
+
)
|
|
233
|
+
client = GrokImagineClient(api_key="k", http_client=fake)
|
|
234
|
+
result = client.text_to_image.run(model="grok-imagine-text-to-image", prompt="a fox")
|
|
235
|
+
assert isinstance(result, CompletedImageTaskResponse)
|
|
236
|
+
assert result.images[0].url == "https://x/y.png"
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# --- validation -----------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def test_text_to_video_requires_model():
|
|
243
|
+
client = GrokImagineClient(api_key="k", http_client=FakeHttp())
|
|
244
|
+
with pytest.raises(ValidationError, match="model is required"):
|
|
245
|
+
client.text_to_video.create(prompt="hi")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def test_text_to_video_requires_prompt():
|
|
249
|
+
client = GrokImagineClient(api_key="k", http_client=FakeHttp())
|
|
250
|
+
with pytest.raises(ValidationError, match="prompt is required"):
|
|
251
|
+
client.text_to_video.create(model="grok-imagine-text-to-video")
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def test_text_to_video_rejects_bad_aspect_ratio():
|
|
255
|
+
client = GrokImagineClient(api_key="k", http_client=FakeHttp())
|
|
256
|
+
with pytest.raises(ValidationError, match="Invalid aspect_ratio"):
|
|
257
|
+
client.text_to_video.create(model="grok-imagine-text-to-video", prompt="hi", aspect_ratio="4:5")
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def test_text_to_video_duration_range():
|
|
261
|
+
client = GrokImagineClient(api_key="k", http_client=FakeHttp())
|
|
262
|
+
with pytest.raises(ValidationError, match="duration_seconds must be an integer between 6 and 30"):
|
|
263
|
+
client.text_to_video.create(model="grok-imagine-text-to-video", prompt="hi", duration_seconds=99)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def test_image_to_video_requires_a_source():
|
|
267
|
+
client = GrokImagineClient(api_key="k", http_client=FakeHttp())
|
|
268
|
+
with pytest.raises(ValidationError, match="One of source_image_urls or source_task_id is required"):
|
|
269
|
+
client.image_to_video.create(model="grok-imagine-image-to-video")
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def test_image_to_video_rejects_both_sources():
|
|
273
|
+
client = GrokImagineClient(api_key="k", http_client=FakeHttp())
|
|
274
|
+
with pytest.raises(ValidationError, match="not both"):
|
|
275
|
+
client.image_to_video.create(
|
|
276
|
+
model="grok-imagine-image-to-video",
|
|
277
|
+
source_image_urls=["https://x/a.png"],
|
|
278
|
+
source_task_id="src_1",
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def test_image_to_video_rejects_multiple_urls():
|
|
283
|
+
client = GrokImagineClient(api_key="k", http_client=FakeHttp())
|
|
284
|
+
with pytest.raises(ValidationError, match="supports at most 1 entry"):
|
|
285
|
+
client.image_to_video.create(
|
|
286
|
+
model="grok-imagine-image-to-video",
|
|
287
|
+
source_image_urls=["https://x/a.png", "https://x/b.png"],
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def test_image_to_video_index_range():
|
|
292
|
+
client = GrokImagineClient(api_key="k", http_client=FakeHttp())
|
|
293
|
+
with pytest.raises(ValidationError, match="index must be an integer between 0 and 5"):
|
|
294
|
+
client.image_to_video.create(
|
|
295
|
+
model="grok-imagine-image-to-video", source_task_id="src_1", index=9
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def test_image_to_video_index_rejects_bool():
|
|
300
|
+
client = GrokImagineClient(api_key="k", http_client=FakeHttp())
|
|
301
|
+
with pytest.raises(ValidationError, match="index must be an integer between 0 and 5"):
|
|
302
|
+
client.image_to_video.create(
|
|
303
|
+
model="grok-imagine-image-to-video", source_task_id="src_1", index=True
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def test_image_to_video_spicy_requires_source_task_id():
|
|
308
|
+
client = GrokImagineClient(api_key="k", http_client=FakeHttp())
|
|
309
|
+
with pytest.raises(ValidationError, match="spicy motion_style requires a source_task_id source image."):
|
|
310
|
+
client.image_to_video.create(
|
|
311
|
+
model="grok-imagine-image-to-video",
|
|
312
|
+
source_image_urls=["https://x/a.png"],
|
|
313
|
+
motion_style="spicy",
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def test_edit_image_requires_source_image_url():
|
|
318
|
+
client = GrokImagineClient(api_key="k", http_client=FakeHttp())
|
|
319
|
+
with pytest.raises(ValidationError, match="source_image_url is required"):
|
|
320
|
+
client.edit_image.create(model="grok-imagine-edit-image")
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def test_extensions_requires_source_task_id():
|
|
324
|
+
client = GrokImagineClient(api_key="k", http_client=FakeHttp())
|
|
325
|
+
with pytest.raises(ValidationError, match="source_task_id is required"):
|
|
326
|
+
client.extensions.create(prompt="go", start_seconds=4, extension_duration_seconds=6)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def test_extensions_rejects_bad_duration():
|
|
330
|
+
client = GrokImagineClient(api_key="k", http_client=FakeHttp())
|
|
331
|
+
with pytest.raises(ValidationError, match="extension_duration_seconds must be one of: 6, 10"):
|
|
332
|
+
client.extensions.create(
|
|
333
|
+
source_task_id="src_1", prompt="go", start_seconds=4, extension_duration_seconds=8
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def test_upscales_requires_source_task_id():
|
|
338
|
+
client = GrokImagineClient(api_key="k", http_client=FakeHttp())
|
|
339
|
+
with pytest.raises(ValidationError, match="source_task_id is required"):
|
|
340
|
+
client.upscales.create()
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def test_extensions_accepts_zero_start_seconds():
|
|
344
|
+
# Regression: start_seconds=0 (extend from the beginning) is valid and must
|
|
345
|
+
# not be rejected as missing by a truthiness check.
|
|
346
|
+
fake = FakeHttp({"id": "t1", "status": "pending"})
|
|
347
|
+
client = GrokImagineClient(api_key="k", http_client=fake)
|
|
348
|
+
client.extensions.create(
|
|
349
|
+
source_task_id="src_1", prompt="go", start_seconds=0, extension_duration_seconds=6
|
|
350
|
+
)
|
|
351
|
+
_, _, body = fake.calls[0]
|
|
352
|
+
assert body["start_seconds"] == 0
|