runapi-seedream 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_seedream-0.1.0/PKG-INFO +78 -0
- runapi_seedream-0.1.0/README.md +65 -0
- runapi_seedream-0.1.0/pyproject.toml +33 -0
- runapi_seedream-0.1.0/src/runapi/seedream/__init__.py +24 -0
- runapi_seedream-0.1.0/src/runapi/seedream/client.py +29 -0
- runapi_seedream-0.1.0/src/runapi/seedream/contract_gen.py +117 -0
- runapi_seedream-0.1.0/src/runapi/seedream/py.typed +0 -0
- runapi_seedream-0.1.0/src/runapi/seedream/resources/__init__.py +4 -0
- runapi_seedream-0.1.0/src/runapi/seedream/resources/edit_image.py +92 -0
- runapi_seedream-0.1.0/src/runapi/seedream/resources/text_to_image.py +104 -0
- runapi_seedream-0.1.0/src/runapi/seedream/types.py +36 -0
- runapi_seedream-0.1.0/tests/test_client.py +145 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: runapi-seedream
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Seedream text-to-image and edit-image client for RunAPI
|
|
5
|
+
Project-URL: Homepage, https://runapi.ai/models/seedream
|
|
6
|
+
Project-URL: Documentation, https://runapi.ai/docs#sdk-seedream
|
|
7
|
+
Author-email: RunAPI <contact@runapi.ai>
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
Keywords: ai,edit-image,runapi,sdk,seedream,text-to-image
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Requires-Dist: runapi-core
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# Seedream API Python SDK for RunAPI
|
|
15
|
+
|
|
16
|
+
The seedream api Python SDK is the language-specific package for Seedream on RunAPI. Use this seedream api package for text-to-image, image editing, and creative production flows when your application needs JSON request bodies, task status lookup, and consistent RunAPI errors in Python.
|
|
17
|
+
|
|
18
|
+
This seedream api README is the Python package guide inside the public `seedream-sdk` repository. For the repository overview, start at `../README.md`; for model details, use https://runapi.ai/models/seedream; for API reference, use https://runapi.ai/docs#seedream; for SDK docs, use https://runapi.ai/docs#sdk-seedream.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install runapi-seedream
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from runapi.seedream import SeedreamClient
|
|
30
|
+
|
|
31
|
+
client = SeedreamClient() # reads RUNAPI_API_KEY, or pass api_key="sk-..."
|
|
32
|
+
|
|
33
|
+
task = client.text_to_image.create(
|
|
34
|
+
model="seedream-v4-text-to-image",
|
|
35
|
+
prompt="A precise product render of a glass teapot on white marble",
|
|
36
|
+
aspect_ratio="16:9",
|
|
37
|
+
output_resolution="2k",
|
|
38
|
+
output_count=3,
|
|
39
|
+
)
|
|
40
|
+
status = client.text_to_image.get(task.id)
|
|
41
|
+
|
|
42
|
+
edit = client.edit_image.create(
|
|
43
|
+
model="seedream-v4-edit",
|
|
44
|
+
prompt="Make it golden hour",
|
|
45
|
+
source_image_urls=["https://example.com/source.jpg"],
|
|
46
|
+
)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
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:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
result = client.text_to_image.run(
|
|
53
|
+
model="seedream-v4-text-to-image",
|
|
54
|
+
prompt="A serene mountain lake at dawn",
|
|
55
|
+
)
|
|
56
|
+
print(result.images[0].url)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
In web request handlers, prefer `create` plus webhook or later `get` polling so a worker is not held open.
|
|
60
|
+
|
|
61
|
+
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.
|
|
62
|
+
|
|
63
|
+
## Language notes
|
|
64
|
+
|
|
65
|
+
Pass parameters as keyword arguments and catch the `runapi.seedream` error classes when building image jobs or scripts. The available resources are `text_to_image` for text models and `edit_image` for editing models. Keep `RUNAPI_API_KEY` in the environment or your secret manager; never commit API keys or callback secrets.
|
|
66
|
+
|
|
67
|
+
## Links
|
|
68
|
+
|
|
69
|
+
- Model page: https://runapi.ai/models/seedream
|
|
70
|
+
- SDK docs: https://runapi.ai/docs#sdk-seedream
|
|
71
|
+
- Product docs: https://runapi.ai/docs#seedream
|
|
72
|
+
- Pricing and rate limits: https://runapi.ai/models/seedream/v4-text-to-image
|
|
73
|
+
- Full catalog: https://runapi.ai/models
|
|
74
|
+
- Repository: https://github.com/runapi-ai/seedream-sdk
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
Licensed under the Apache License, Version 2.0.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Seedream API Python SDK for RunAPI
|
|
2
|
+
|
|
3
|
+
The seedream api Python SDK is the language-specific package for Seedream on RunAPI. Use this seedream api package for text-to-image, image editing, and creative production flows when your application needs JSON request bodies, task status lookup, and consistent RunAPI errors in Python.
|
|
4
|
+
|
|
5
|
+
This seedream api README is the Python package guide inside the public `seedream-sdk` repository. For the repository overview, start at `../README.md`; for model details, use https://runapi.ai/models/seedream; for API reference, use https://runapi.ai/docs#seedream; for SDK docs, use https://runapi.ai/docs#sdk-seedream.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install runapi-seedream
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from runapi.seedream import SeedreamClient
|
|
17
|
+
|
|
18
|
+
client = SeedreamClient() # reads RUNAPI_API_KEY, or pass api_key="sk-..."
|
|
19
|
+
|
|
20
|
+
task = client.text_to_image.create(
|
|
21
|
+
model="seedream-v4-text-to-image",
|
|
22
|
+
prompt="A precise product render of a glass teapot on white marble",
|
|
23
|
+
aspect_ratio="16:9",
|
|
24
|
+
output_resolution="2k",
|
|
25
|
+
output_count=3,
|
|
26
|
+
)
|
|
27
|
+
status = client.text_to_image.get(task.id)
|
|
28
|
+
|
|
29
|
+
edit = client.edit_image.create(
|
|
30
|
+
model="seedream-v4-edit",
|
|
31
|
+
prompt="Make it golden hour",
|
|
32
|
+
source_image_urls=["https://example.com/source.jpg"],
|
|
33
|
+
)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
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:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
result = client.text_to_image.run(
|
|
40
|
+
model="seedream-v4-text-to-image",
|
|
41
|
+
prompt="A serene mountain lake at dawn",
|
|
42
|
+
)
|
|
43
|
+
print(result.images[0].url)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
In web request handlers, prefer `create` plus webhook or later `get` polling so a worker is not held open.
|
|
47
|
+
|
|
48
|
+
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.
|
|
49
|
+
|
|
50
|
+
## Language notes
|
|
51
|
+
|
|
52
|
+
Pass parameters as keyword arguments and catch the `runapi.seedream` error classes when building image jobs or scripts. The available resources are `text_to_image` for text models and `edit_image` for editing models. Keep `RUNAPI_API_KEY` in the environment or your secret manager; never commit API keys or callback secrets.
|
|
53
|
+
|
|
54
|
+
## Links
|
|
55
|
+
|
|
56
|
+
- Model page: https://runapi.ai/models/seedream
|
|
57
|
+
- SDK docs: https://runapi.ai/docs#sdk-seedream
|
|
58
|
+
- Product docs: https://runapi.ai/docs#seedream
|
|
59
|
+
- Pricing and rate limits: https://runapi.ai/models/seedream/v4-text-to-image
|
|
60
|
+
- Full catalog: https://runapi.ai/models
|
|
61
|
+
- Repository: https://github.com/runapi-ai/seedream-sdk
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
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-seedream"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Seedream text-to-image and edit-image 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", "seedream", "text-to-image", "edit-image", "ai", "sdk"]
|
|
14
|
+
dependencies = ["runapi-core"]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://runapi.ai/models/seedream"
|
|
18
|
+
Documentation = "https://runapi.ai/docs#sdk-seedream"
|
|
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 = "seedream"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Seedream 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 SeedreamClient
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"SeedreamClient",
|
|
17
|
+
"AuthenticationError",
|
|
18
|
+
"RateLimitError",
|
|
19
|
+
"InsufficientCreditsError",
|
|
20
|
+
"NotFoundError",
|
|
21
|
+
"ValidationError",
|
|
22
|
+
"TaskFailedError",
|
|
23
|
+
"TaskTimeoutError",
|
|
24
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Seedream 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.text_to_image import TextToImage
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SeedreamClient:
|
|
14
|
+
"""Seedream text-to-image and edit-image client.
|
|
15
|
+
|
|
16
|
+
Example::
|
|
17
|
+
|
|
18
|
+
client = SeedreamClient(api_key="sk-...")
|
|
19
|
+
result = client.text_to_image.run(
|
|
20
|
+
model="seedream-v4-text-to-image", prompt="A neon city street"
|
|
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.text_to_image = TextToImage(http)
|
|
29
|
+
self.edit_image = EditImage(http)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Code generated by SdkContractGenerator. DO NOT EDIT.
|
|
2
|
+
|
|
3
|
+
CONTRACT = {
|
|
4
|
+
"edit-image": {
|
|
5
|
+
"models": ["seedream-4.5-edit", "seedream-5-lite-edit", "seedream-v4-edit"],
|
|
6
|
+
"fields_by_model": {
|
|
7
|
+
"seedream-4.5-edit": {
|
|
8
|
+
"aspect_ratio": {
|
|
9
|
+
"enum": ["1:1", "4:3", "3:4", "16:9", "9:16", "2:3", "3:2", "21:9"],
|
|
10
|
+
"required": True
|
|
11
|
+
},
|
|
12
|
+
"output_count": {
|
|
13
|
+
"type": "integer"
|
|
14
|
+
},
|
|
15
|
+
"output_quality": {
|
|
16
|
+
"enum": ["basic", "high"],
|
|
17
|
+
"required": True
|
|
18
|
+
},
|
|
19
|
+
"seed": {
|
|
20
|
+
"type": "integer"
|
|
21
|
+
},
|
|
22
|
+
"source_image_urls": {
|
|
23
|
+
"required": True
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"seedream-5-lite-edit": {
|
|
27
|
+
"aspect_ratio": {
|
|
28
|
+
"enum": ["1:1", "4:3", "3:4", "16:9", "9:16", "2:3", "3:2", "21:9"],
|
|
29
|
+
"required": True
|
|
30
|
+
},
|
|
31
|
+
"output_count": {
|
|
32
|
+
"type": "integer"
|
|
33
|
+
},
|
|
34
|
+
"output_quality": {
|
|
35
|
+
"enum": ["basic", "high"],
|
|
36
|
+
"required": True
|
|
37
|
+
},
|
|
38
|
+
"seed": {
|
|
39
|
+
"type": "integer"
|
|
40
|
+
},
|
|
41
|
+
"source_image_urls": {
|
|
42
|
+
"required": True
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"seedream-v4-edit": {
|
|
46
|
+
"aspect_ratio": {
|
|
47
|
+
"enum": ["1:1", "4:3", "3:4", "3:2", "2:3", "16:9", "9:16", "21:9"]
|
|
48
|
+
},
|
|
49
|
+
"output_count": {
|
|
50
|
+
"enum": [1, 2, 3, 4, 5, 6],
|
|
51
|
+
"type": "integer"
|
|
52
|
+
},
|
|
53
|
+
"output_resolution": {
|
|
54
|
+
"enum": ["1k", "2k", "4k"]
|
|
55
|
+
},
|
|
56
|
+
"seed": {
|
|
57
|
+
"type": "integer"
|
|
58
|
+
},
|
|
59
|
+
"source_image_urls": {
|
|
60
|
+
"required": True
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"text-to-image": {
|
|
66
|
+
"models": ["seedream-4.5-text-to-image", "seedream-5-lite-text-to-image", "seedream-v4-text-to-image"],
|
|
67
|
+
"fields_by_model": {
|
|
68
|
+
"seedream-4.5-text-to-image": {
|
|
69
|
+
"aspect_ratio": {
|
|
70
|
+
"enum": ["1:1", "4:3", "3:4", "16:9", "9:16", "2:3", "3:2", "21:9"],
|
|
71
|
+
"required": True
|
|
72
|
+
},
|
|
73
|
+
"output_count": {
|
|
74
|
+
"type": "integer"
|
|
75
|
+
},
|
|
76
|
+
"output_quality": {
|
|
77
|
+
"enum": ["basic", "high"],
|
|
78
|
+
"required": True
|
|
79
|
+
},
|
|
80
|
+
"seed": {
|
|
81
|
+
"type": "integer"
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"seedream-5-lite-text-to-image": {
|
|
85
|
+
"aspect_ratio": {
|
|
86
|
+
"enum": ["1:1", "4:3", "3:4", "16:9", "9:16", "2:3", "3:2", "21:9"],
|
|
87
|
+
"required": True
|
|
88
|
+
},
|
|
89
|
+
"output_count": {
|
|
90
|
+
"type": "integer"
|
|
91
|
+
},
|
|
92
|
+
"output_quality": {
|
|
93
|
+
"enum": ["basic", "high"],
|
|
94
|
+
"required": True
|
|
95
|
+
},
|
|
96
|
+
"seed": {
|
|
97
|
+
"type": "integer"
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
"seedream-v4-text-to-image": {
|
|
101
|
+
"aspect_ratio": {
|
|
102
|
+
"enum": ["1:1", "4:3", "3:4", "3:2", "2:3", "16:9", "9:16", "21:9"]
|
|
103
|
+
},
|
|
104
|
+
"output_count": {
|
|
105
|
+
"enum": [1, 2, 3, 4, 5, 6],
|
|
106
|
+
"type": "integer"
|
|
107
|
+
},
|
|
108
|
+
"output_resolution": {
|
|
109
|
+
"enum": ["1k", "2k", "4k"]
|
|
110
|
+
},
|
|
111
|
+
"seed": {
|
|
112
|
+
"type": "integer"
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Seedream edit-image 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
|
+
LITE_MODELS,
|
|
12
|
+
V4_MODELS,
|
|
13
|
+
CompletedEditImageResponse,
|
|
14
|
+
EditImageResponse,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class EditImage(Resource):
|
|
19
|
+
"""Edit images from a prompt and source images with Seedream models."""
|
|
20
|
+
|
|
21
|
+
ENDPOINT = "/api/v1/seedream/edit_image"
|
|
22
|
+
|
|
23
|
+
RESPONSE_CLASS = EditImageResponse
|
|
24
|
+
COMPLETED_RESPONSE_CLASS = CompletedEditImageResponse
|
|
25
|
+
|
|
26
|
+
PROMPT_MAX_LENGTH = 3000
|
|
27
|
+
V4_PROMPT_MAX_LENGTH = 5000
|
|
28
|
+
PROMPT_MIN_LENGTH_LITE = 3
|
|
29
|
+
|
|
30
|
+
def run(self, **params: Any) -> Any:
|
|
31
|
+
"""Edit an image and poll until it completes.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
**params: image edit parameters (model, ...).
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
The completed (narrowed) image edit response.
|
|
38
|
+
"""
|
|
39
|
+
task = self.create(**params)
|
|
40
|
+
return self._poll_until_complete(lambda: self.get(task.id))
|
|
41
|
+
|
|
42
|
+
def create(self, **params: Any) -> Any:
|
|
43
|
+
"""Create an image editing task and return immediately with an id.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
**params: image edit parameters (model, ...).
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
The task creation result with an id.
|
|
50
|
+
"""
|
|
51
|
+
compacted = self._compact_params(params)
|
|
52
|
+
self._validate_params(compacted)
|
|
53
|
+
return self._request("post", self.ENDPOINT, body=compacted)
|
|
54
|
+
|
|
55
|
+
def get(self, id: str) -> Any:
|
|
56
|
+
"""Fetch the current status of an image editing task.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
id: The task id returned by ``create``.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
The current task status.
|
|
63
|
+
"""
|
|
64
|
+
return self._request("get", f"{self.ENDPOINT}/{id}")
|
|
65
|
+
|
|
66
|
+
def _validate_params(self, params: Dict[str, Any]) -> None:
|
|
67
|
+
self._validate_contract(CONTRACT["edit-image"], params)
|
|
68
|
+
|
|
69
|
+
model = params.get("model")
|
|
70
|
+
|
|
71
|
+
prompt = params.get("prompt")
|
|
72
|
+
if not (isinstance(prompt, str) and prompt):
|
|
73
|
+
raise ValidationError("prompt is required")
|
|
74
|
+
max_length = self.V4_PROMPT_MAX_LENGTH if model in V4_MODELS else self.PROMPT_MAX_LENGTH
|
|
75
|
+
if len(prompt) > max_length:
|
|
76
|
+
raise ValidationError(f"prompt must be at most {max_length} characters")
|
|
77
|
+
if model in LITE_MODELS and len(prompt) < self.PROMPT_MIN_LENGTH_LITE:
|
|
78
|
+
raise ValidationError(
|
|
79
|
+
f"prompt must be between {self.PROMPT_MIN_LENGTH_LITE} and {self.PROMPT_MAX_LENGTH} characters"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if model in V4_MODELS:
|
|
83
|
+
self._validate_integer(params, "seed")
|
|
84
|
+
|
|
85
|
+
@staticmethod
|
|
86
|
+
def _validate_integer(params: Dict[str, Any], key: str) -> None:
|
|
87
|
+
value = params.get(key)
|
|
88
|
+
if value is None:
|
|
89
|
+
return
|
|
90
|
+
if isinstance(value, int) and not isinstance(value, bool):
|
|
91
|
+
return
|
|
92
|
+
raise ValidationError(f"{key} must be an integer")
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Seedream text-to-image 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
|
+
LITE_MODELS,
|
|
12
|
+
V4_MODELS,
|
|
13
|
+
CompletedTextToImageResponse,
|
|
14
|
+
TextToImageResponse,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TextToImage(Resource):
|
|
19
|
+
"""Generate images from text prompts with Seedream models."""
|
|
20
|
+
|
|
21
|
+
ENDPOINT = "/api/v1/seedream/text_to_image"
|
|
22
|
+
|
|
23
|
+
RESPONSE_CLASS = TextToImageResponse
|
|
24
|
+
COMPLETED_RESPONSE_CLASS = CompletedTextToImageResponse
|
|
25
|
+
|
|
26
|
+
PROMPT_MAX_LENGTH = 3000
|
|
27
|
+
V4_PROMPT_MAX_LENGTH = 5000
|
|
28
|
+
PROMPT_MIN_LENGTH_LITE = 3
|
|
29
|
+
|
|
30
|
+
def run(self, **params: Any) -> Any:
|
|
31
|
+
"""Create a text-to-image task and poll until it completes.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
**params: text-to-image parameters (model, ...).
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
The completed (narrowed) text-to-image response.
|
|
38
|
+
"""
|
|
39
|
+
task = self.create(**params)
|
|
40
|
+
return self._poll_until_complete(lambda: self.get(task.id))
|
|
41
|
+
|
|
42
|
+
def create(self, **params: Any) -> Any:
|
|
43
|
+
"""Create a text-to-image task and return immediately with an id.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
**params: text-to-image parameters (model, ...).
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
The task creation result with an id.
|
|
50
|
+
"""
|
|
51
|
+
compacted = self._compact_params(params)
|
|
52
|
+
self._validate_params(compacted)
|
|
53
|
+
return self._request("post", self.ENDPOINT, body=compacted)
|
|
54
|
+
|
|
55
|
+
def get(self, id: str) -> Any:
|
|
56
|
+
"""Fetch the current status of a text-to-image task.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
id: The task id returned by ``create``.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
The current task status.
|
|
63
|
+
"""
|
|
64
|
+
return self._request("get", f"{self.ENDPOINT}/{id}")
|
|
65
|
+
|
|
66
|
+
def _validate_params(self, params: Dict[str, Any]) -> None:
|
|
67
|
+
self._validate_contract(CONTRACT["text-to-image"], params)
|
|
68
|
+
|
|
69
|
+
model = params.get("model")
|
|
70
|
+
|
|
71
|
+
prompt = params.get("prompt")
|
|
72
|
+
if not (isinstance(prompt, str) and prompt):
|
|
73
|
+
raise ValidationError("prompt is required")
|
|
74
|
+
max_length = self.V4_PROMPT_MAX_LENGTH if model in V4_MODELS else self.PROMPT_MAX_LENGTH
|
|
75
|
+
if len(prompt) > max_length:
|
|
76
|
+
raise ValidationError(f"prompt must be at most {max_length} characters")
|
|
77
|
+
if model in LITE_MODELS and len(prompt) < self.PROMPT_MIN_LENGTH_LITE:
|
|
78
|
+
raise ValidationError(
|
|
79
|
+
f"prompt must be between {self.PROMPT_MIN_LENGTH_LITE} and {self.PROMPT_MAX_LENGTH} characters"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if model in V4_MODELS:
|
|
83
|
+
self._validate_integer(params, "seed")
|
|
84
|
+
|
|
85
|
+
if self._field_present(params, "source_image_urls"):
|
|
86
|
+
raise ValidationError(f"source_image_urls is not supported for {model}")
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def _field_present(params: Dict[str, Any], key: str) -> bool:
|
|
90
|
+
value = params.get(key)
|
|
91
|
+
if value is None:
|
|
92
|
+
return False
|
|
93
|
+
if hasattr(value, "__len__"):
|
|
94
|
+
return len(value) > 0
|
|
95
|
+
return True
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def _validate_integer(params: Dict[str, Any], key: str) -> None:
|
|
99
|
+
value = params.get(key)
|
|
100
|
+
if value is None:
|
|
101
|
+
return
|
|
102
|
+
if isinstance(value, int) and not isinstance(value, bool):
|
|
103
|
+
return
|
|
104
|
+
raise ValidationError(f"{key} must be an integer")
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Seedream model lists, enums, and response models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from runapi.core import BaseModel, TaskResponse, optional, required
|
|
6
|
+
|
|
7
|
+
# Model groupings used by bespoke prompt-length selection (per-model rule the
|
|
8
|
+
# contract cannot express). Model membership and field enums are validated by
|
|
9
|
+
# the generated CONTRACT.
|
|
10
|
+
LITE_MODELS = ["seedream-5-lite-text-to-image", "seedream-5-lite-edit"]
|
|
11
|
+
V4_MODELS = ["seedream-v4-text-to-image", "seedream-v4-edit"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Image(BaseModel):
|
|
15
|
+
url = optional(str)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TextToImageResponse(TaskResponse):
|
|
19
|
+
"""Seedream image task status response."""
|
|
20
|
+
|
|
21
|
+
id = required(str)
|
|
22
|
+
status = optional(str, enum=lambda: TaskResponse.Status.ALL)
|
|
23
|
+
images = optional([lambda: Image])
|
|
24
|
+
error = optional(str)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
EditImageResponse = TextToImageResponse
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CompletedTextToImageResponse(TextToImageResponse):
|
|
31
|
+
"""Narrowed response from ``run()`` once polling observes completion."""
|
|
32
|
+
|
|
33
|
+
images = required([lambda: Image])
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
CompletedEditImageResponse = CompletedTextToImageResponse
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from runapi.core import config
|
|
4
|
+
from runapi.core.errors import AuthenticationError, ValidationError
|
|
5
|
+
from runapi.seedream import SeedreamClient
|
|
6
|
+
from runapi.seedream.resources.edit_image import EditImage
|
|
7
|
+
from runapi.seedream.resources.text_to_image import TextToImage
|
|
8
|
+
from runapi.seedream.types import CompletedTextToImageResponse, TextToImageResponse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FakeHttp:
|
|
12
|
+
def __init__(self, *responses):
|
|
13
|
+
self._responses = list(responses)
|
|
14
|
+
self.calls = []
|
|
15
|
+
|
|
16
|
+
def request(self, method, path, body=None, options=None):
|
|
17
|
+
self.calls.append((method, path, body))
|
|
18
|
+
if self._responses:
|
|
19
|
+
return self._responses.pop(0)
|
|
20
|
+
return {"id": "task_1", "status": "pending"}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture(autouse=True)
|
|
24
|
+
def reset_config(monkeypatch):
|
|
25
|
+
monkeypatch.delenv("RUNAPI_API_KEY", raising=False)
|
|
26
|
+
monkeypatch.setattr(config, "api_key", None)
|
|
27
|
+
yield
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# --- auth -----------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_accepts_api_key_parameter():
|
|
34
|
+
assert isinstance(SeedreamClient(api_key="k", http_client=FakeHttp()), SeedreamClient)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_falls_back_to_global(monkeypatch):
|
|
38
|
+
monkeypatch.setattr(config, "api_key", "global-key")
|
|
39
|
+
assert isinstance(SeedreamClient(http_client=FakeHttp()), SeedreamClient)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_falls_back_to_env(monkeypatch):
|
|
43
|
+
monkeypatch.setenv("RUNAPI_API_KEY", "env-key")
|
|
44
|
+
assert isinstance(SeedreamClient(http_client=FakeHttp()), SeedreamClient)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_raises_without_api_key():
|
|
48
|
+
with pytest.raises(AuthenticationError, match="API key is required"):
|
|
49
|
+
SeedreamClient()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# --- injection / accessors ------------------------------------------------
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_uses_injected_http_client():
|
|
56
|
+
fake = FakeHttp()
|
|
57
|
+
client = SeedreamClient(api_key="k", http_client=fake)
|
|
58
|
+
assert client.text_to_image._http is fake
|
|
59
|
+
assert client.edit_image._http is fake
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_exposes_resource_accessors():
|
|
63
|
+
client = SeedreamClient(api_key="k", http_client=FakeHttp())
|
|
64
|
+
assert isinstance(client.text_to_image, TextToImage)
|
|
65
|
+
assert isinstance(client.edit_image, EditImage)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# --- request shapes -------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_create_posts_compacted_body():
|
|
72
|
+
fake = FakeHttp({"id": "t1", "status": "pending"})
|
|
73
|
+
client = SeedreamClient(api_key="k", http_client=fake)
|
|
74
|
+
result = client.text_to_image.create(
|
|
75
|
+
model="seedream-v4-text-to-image", prompt="hello world", aspect_ratio="1:1", seed=None
|
|
76
|
+
)
|
|
77
|
+
assert fake.calls == [
|
|
78
|
+
("post", "/api/v1/seedream/text_to_image", {"model": "seedream-v4-text-to-image", "prompt": "hello world", "aspect_ratio": "1:1"}),
|
|
79
|
+
]
|
|
80
|
+
assert isinstance(result, TextToImageResponse)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_get_fetches_by_id():
|
|
84
|
+
fake = FakeHttp({"id": "t1", "status": "processing"})
|
|
85
|
+
client = SeedreamClient(api_key="k", http_client=fake)
|
|
86
|
+
client.text_to_image.get("t1")
|
|
87
|
+
assert fake.calls == [("get", "/api/v1/seedream/text_to_image/t1", None)]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_run_narrows_completed_type():
|
|
91
|
+
fake = FakeHttp(
|
|
92
|
+
{"id": "t1", "status": "pending"},
|
|
93
|
+
{"id": "t1", "status": "completed", "images": [{"url": "https://x/y.png"}]},
|
|
94
|
+
)
|
|
95
|
+
client = SeedreamClient(api_key="k", http_client=fake)
|
|
96
|
+
result = client.text_to_image.run(model="seedream-v4-text-to-image", prompt="a serene lake")
|
|
97
|
+
assert isinstance(result, CompletedTextToImageResponse)
|
|
98
|
+
assert result.images[0].url == "https://x/y.png"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# --- validation -----------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_rejects_unknown_model():
|
|
105
|
+
client = SeedreamClient(api_key="k", http_client=FakeHttp())
|
|
106
|
+
with pytest.raises(ValidationError, match="model must be one of:"):
|
|
107
|
+
client.text_to_image.create(model="nope", prompt="hi there")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_requires_prompt():
|
|
111
|
+
client = SeedreamClient(api_key="k", http_client=FakeHttp())
|
|
112
|
+
with pytest.raises(ValidationError, match="prompt is required"):
|
|
113
|
+
client.text_to_image.create(model="seedream-v4-text-to-image")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_v4_output_count_range():
|
|
117
|
+
client = SeedreamClient(api_key="k", http_client=FakeHttp())
|
|
118
|
+
with pytest.raises(ValidationError, match="output_count must be one of: 1, 2, 3, 4, 5, 6"):
|
|
119
|
+
client.text_to_image.create(model="seedream-v4-text-to-image", prompt="hi there", output_count=9)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_non_v4_requires_aspect_ratio_and_quality():
|
|
123
|
+
client = SeedreamClient(api_key="k", http_client=FakeHttp())
|
|
124
|
+
with pytest.raises(ValidationError, match="aspect_ratio is required"):
|
|
125
|
+
client.text_to_image.create(model="seedream-4.5-text-to-image", prompt="hi there")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_text_to_image_rejects_source_image_urls():
|
|
129
|
+
client = SeedreamClient(api_key="k", http_client=FakeHttp())
|
|
130
|
+
with pytest.raises(ValidationError, match="source_image_urls is not supported"):
|
|
131
|
+
client.text_to_image.create(
|
|
132
|
+
model="seedream-v4-text-to-image", prompt="hi there", source_image_urls=["https://x/a.png"]
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_edit_requires_source_image_urls():
|
|
137
|
+
client = SeedreamClient(api_key="k", http_client=FakeHttp())
|
|
138
|
+
with pytest.raises(ValidationError, match="source_image_urls is required"):
|
|
139
|
+
client.edit_image.create(model="seedream-v4-edit", prompt="make it pop")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_lite_prompt_min_length():
|
|
143
|
+
client = SeedreamClient(api_key="k", http_client=FakeHttp())
|
|
144
|
+
with pytest.raises(ValidationError, match="between 3 and 3000"):
|
|
145
|
+
client.text_to_image.create(model="seedream-5-lite-text-to-image", prompt="hi", aspect_ratio="1:1", output_quality="high")
|