runapi-seedream 0.2.3__tar.gz → 0.4.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.2.3 → runapi_seedream-0.4.0}/PKG-INFO +3 -3
- {runapi_seedream-0.2.3 → runapi_seedream-0.4.0}/pyproject.toml +3 -3
- {runapi_seedream-0.2.3 → runapi_seedream-0.4.0}/src/runapi/seedream/resources/decompose_layers.py +0 -2
- {runapi_seedream-0.2.3 → runapi_seedream-0.4.0}/src/runapi/seedream/resources/edit_image.py +2 -39
- runapi_seedream-0.4.0/src/runapi/seedream/resources/text_to_image.py +56 -0
- {runapi_seedream-0.2.3 → runapi_seedream-0.4.0}/src/runapi/seedream/types.py +20 -12
- {runapi_seedream-0.2.3 → runapi_seedream-0.4.0}/tests/test_client.py +31 -86
- runapi_seedream-0.2.3/src/runapi/seedream/contract_gen.py +0 -222
- runapi_seedream-0.2.3/src/runapi/seedream/resources/text_to_image.py +0 -105
- {runapi_seedream-0.2.3 → runapi_seedream-0.4.0}/README.md +0 -0
- {runapi_seedream-0.2.3 → runapi_seedream-0.4.0}/src/runapi/seedream/__init__.py +0 -0
- {runapi_seedream-0.2.3 → runapi_seedream-0.4.0}/src/runapi/seedream/client.py +0 -0
- {runapi_seedream-0.2.3 → runapi_seedream-0.4.0}/src/runapi/seedream/py.typed +0 -0
- {runapi_seedream-0.2.3 → runapi_seedream-0.4.0}/src/runapi/seedream/resources/__init__.py +0 -0
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: runapi-seedream
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: RunAPI Seedream SDK for text-to-image and edit-image workflows in JavaScript, Python, Ruby, Go, Java, and PHP
|
|
5
5
|
Project-URL: Homepage, https://runapi.ai/models/seedream
|
|
6
6
|
Project-URL: Documentation, https://runapi.ai/docs/resources/sdks
|
|
7
7
|
Project-URL: Source, https://github.com/runapi-ai/seedream-sdk
|
|
8
8
|
Project-URL: Issues, https://github.com/runapi-ai/seedream-sdk/issues
|
|
9
9
|
Project-URL: Changelog, https://github.com/runapi-ai/seedream-sdk/blob/main/CHANGELOG.md
|
|
10
|
-
Project-URL: Release Notes, https://github.com/runapi-ai/seedream-sdk/releases/tag/python%2Fv0.
|
|
10
|
+
Project-URL: Release Notes, https://github.com/runapi-ai/seedream-sdk/releases/tag/python%2Fv0.4.0
|
|
11
11
|
Author-email: RunAPI <contact@runapi.ai>
|
|
12
12
|
License-Expression: Apache-2.0
|
|
13
13
|
Keywords: api,golang,gradle,image-api,image-generation,java,maven,python,ruby,runapi,runapi-ai,sdk,seedream,seedream-api,text-to-image,typescript
|
|
14
14
|
Requires-Python: >=3.9
|
|
15
|
-
Requires-Dist: runapi-core
|
|
15
|
+
Requires-Dist: runapi-core~=0.10.0
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
|
|
18
18
|
# Seedream Python SDK for RunAPI
|
|
@@ -4,14 +4,14 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "runapi-seedream"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "RunAPI Seedream SDK for text-to-image and edit-image workflows in JavaScript, Python, Ruby, Go, Java, and PHP"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
11
11
|
license = "Apache-2.0"
|
|
12
12
|
authors = [{ name = "RunAPI", email = "contact@runapi.ai" }]
|
|
13
13
|
keywords = ["runapi", "runapi-ai", "seedream", "api", "sdk", "typescript", "python", "ruby", "golang", "java", "maven", "gradle", "image-generation", "text-to-image", "image-api", "seedream-api"]
|
|
14
|
-
dependencies = ["runapi-core
|
|
14
|
+
dependencies = ["runapi-core~=0.10.0"]
|
|
15
15
|
|
|
16
16
|
[project.urls]
|
|
17
17
|
Homepage = "https://runapi.ai/models/seedream"
|
|
@@ -19,7 +19,7 @@ Documentation = "https://runapi.ai/docs/resources/sdks"
|
|
|
19
19
|
Source = "https://github.com/runapi-ai/seedream-sdk"
|
|
20
20
|
Issues = "https://github.com/runapi-ai/seedream-sdk/issues"
|
|
21
21
|
Changelog = "https://github.com/runapi-ai/seedream-sdk/blob/main/CHANGELOG.md"
|
|
22
|
-
"Release Notes" = "https://github.com/runapi-ai/seedream-sdk/releases/tag/python%2Fv0.
|
|
22
|
+
"Release Notes" = "https://github.com/runapi-ai/seedream-sdk/releases/tag/python%2Fv0.4.0"
|
|
23
23
|
|
|
24
24
|
[tool.hatch.build.targets.wheel]
|
|
25
25
|
packages = ["src/runapi"]
|
{runapi_seedream-0.2.3 → runapi_seedream-0.4.0}/src/runapi/seedream/resources/decompose_layers.py
RENAMED
|
@@ -6,7 +6,6 @@ from typing import Any, Optional
|
|
|
6
6
|
|
|
7
7
|
from runapi.core import RequestOptions, Resource
|
|
8
8
|
|
|
9
|
-
from ..contract_gen import CONTRACT
|
|
10
9
|
from ..types import CompletedDecomposeLayersResponse, DecomposeLayersResponse
|
|
11
10
|
|
|
12
11
|
|
|
@@ -23,7 +22,6 @@ class DecomposeLayers(Resource):
|
|
|
23
22
|
|
|
24
23
|
def create(self, options: Optional[RequestOptions] = None, **params: Any) -> Any:
|
|
25
24
|
compacted = self._compact_params(params)
|
|
26
|
-
self._validate_contract(CONTRACT["decompose-layers"], compacted)
|
|
27
25
|
return self._request("post", self.ENDPOINT, body=compacted, options=options)
|
|
28
26
|
|
|
29
27
|
def get(self, id: str, options: Optional[RequestOptions] = None) -> Any:
|
|
@@ -2,15 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from typing import Any,
|
|
5
|
+
from typing import Any, Optional
|
|
6
6
|
|
|
7
|
-
from runapi.core import Resource,
|
|
7
|
+
from runapi.core import Resource, RequestOptions
|
|
8
8
|
|
|
9
|
-
from ..contract_gen import CONTRACT
|
|
10
9
|
from ..types import (
|
|
11
|
-
LONG_PROMPT_MODELS,
|
|
12
|
-
MINIMUM_THREE_PROMPT_MODELS,
|
|
13
|
-
V4_MODELS,
|
|
14
10
|
CompletedEditImageResponse,
|
|
15
11
|
EditImageResponse,
|
|
16
12
|
)
|
|
@@ -24,10 +20,6 @@ class EditImage(Resource):
|
|
|
24
20
|
RESPONSE_CLASS = EditImageResponse
|
|
25
21
|
COMPLETED_RESPONSE_CLASS = CompletedEditImageResponse
|
|
26
22
|
|
|
27
|
-
PROMPT_MAX_LENGTH = 3000
|
|
28
|
-
V4_PROMPT_MAX_LENGTH = 5000
|
|
29
|
-
PROMPT_MIN_LENGTH_LITE = 3
|
|
30
|
-
|
|
31
23
|
def run(self, options: Optional[RequestOptions] = None, **params: Any) -> Any:
|
|
32
24
|
"""Edit an image and poll until it completes.
|
|
33
25
|
|
|
@@ -50,7 +42,6 @@ class EditImage(Resource):
|
|
|
50
42
|
The task creation result with an id.
|
|
51
43
|
"""
|
|
52
44
|
compacted = self._compact_params(params)
|
|
53
|
-
self._validate_params(compacted)
|
|
54
45
|
return self._request("post", self.ENDPOINT, body=compacted, options=options)
|
|
55
46
|
|
|
56
47
|
def get(self, id: str, options: Optional[RequestOptions] = None) -> Any:
|
|
@@ -63,31 +54,3 @@ class EditImage(Resource):
|
|
|
63
54
|
The current task status.
|
|
64
55
|
"""
|
|
65
56
|
return self._request("get", f"{self.ENDPOINT}/{id}", options=options)
|
|
66
|
-
|
|
67
|
-
def _validate_params(self, params: Dict[str, Any]) -> None:
|
|
68
|
-
self._validate_contract(CONTRACT["edit-image"], params)
|
|
69
|
-
|
|
70
|
-
model = params.get("model")
|
|
71
|
-
|
|
72
|
-
prompt = params.get("prompt")
|
|
73
|
-
if not (isinstance(prompt, str) and prompt):
|
|
74
|
-
raise ValidationError("prompt is required")
|
|
75
|
-
max_length = self.V4_PROMPT_MAX_LENGTH if model in LONG_PROMPT_MODELS else self.PROMPT_MAX_LENGTH
|
|
76
|
-
if len(prompt) > max_length:
|
|
77
|
-
raise ValidationError(f"prompt must be at most {max_length} characters")
|
|
78
|
-
if model in MINIMUM_THREE_PROMPT_MODELS and len(prompt) < self.PROMPT_MIN_LENGTH_LITE:
|
|
79
|
-
raise ValidationError(
|
|
80
|
-
f"prompt must be between {self.PROMPT_MIN_LENGTH_LITE} and {max_length} characters"
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
if model in V4_MODELS:
|
|
84
|
-
self._validate_integer(params, "seed")
|
|
85
|
-
|
|
86
|
-
@staticmethod
|
|
87
|
-
def _validate_integer(params: Dict[str, Any], key: str) -> None:
|
|
88
|
-
value = params.get(key)
|
|
89
|
-
if value is None:
|
|
90
|
-
return
|
|
91
|
-
if isinstance(value, int) and not isinstance(value, bool):
|
|
92
|
-
return
|
|
93
|
-
raise ValidationError(f"{key} must be an integer")
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Seedream text-to-image resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from runapi.core import Resource, RequestOptions
|
|
8
|
+
|
|
9
|
+
from ..types import (
|
|
10
|
+
CompletedTextToImageResponse,
|
|
11
|
+
TextToImageResponse,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TextToImage(Resource):
|
|
16
|
+
"""Generate images from text prompts with Seedream models."""
|
|
17
|
+
|
|
18
|
+
ENDPOINT = "/api/v1/seedream/text_to_image"
|
|
19
|
+
|
|
20
|
+
RESPONSE_CLASS = TextToImageResponse
|
|
21
|
+
COMPLETED_RESPONSE_CLASS = CompletedTextToImageResponse
|
|
22
|
+
|
|
23
|
+
def run(self, options: Optional[RequestOptions] = None, **params: Any) -> Any:
|
|
24
|
+
"""Create a text-to-image task and poll until it completes.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
**params: text-to-image parameters (model, ...).
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
The completed (narrowed) text-to-image response.
|
|
31
|
+
"""
|
|
32
|
+
task = self.create(options=options, **params)
|
|
33
|
+
return self._poll_until_complete(lambda: self.get(task.id, options=options))
|
|
34
|
+
|
|
35
|
+
def create(self, options: Optional[RequestOptions] = None, **params: Any) -> Any:
|
|
36
|
+
"""Create a text-to-image task and return immediately with an id.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
**params: text-to-image parameters (model, ...).
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
The task creation result with an id.
|
|
43
|
+
"""
|
|
44
|
+
compacted = self._compact_params(params)
|
|
45
|
+
return self._request("post", self.ENDPOINT, body=compacted, options=options)
|
|
46
|
+
|
|
47
|
+
def get(self, id: str, options: Optional[RequestOptions] = None) -> Any:
|
|
48
|
+
"""Fetch the current status of a text-to-image task.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
id: The task id returned by ``create``.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
The current task status.
|
|
55
|
+
"""
|
|
56
|
+
return self._request("get", f"{self.ENDPOINT}/{id}", options=options)
|
|
@@ -1,23 +1,31 @@
|
|
|
1
|
-
"""Seedream
|
|
1
|
+
"""Seedream response models."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from runapi.core import BaseModel, TaskResponse, optional, required
|
|
6
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
|
-
PRO_MODELS = ["seedream-5-pro-text-to-image", "seedream-5-pro-edit"]
|
|
12
|
-
V4_MODELS = ["seedream-v4-text-to-image", "seedream-v4-edit"]
|
|
13
|
-
LONG_PROMPT_MODELS = [*PRO_MODELS, *V4_MODELS]
|
|
14
|
-
MINIMUM_THREE_PROMPT_MODELS = [*LITE_MODELS, *PRO_MODELS]
|
|
15
|
-
|
|
16
7
|
|
|
17
8
|
class Image(BaseModel):
|
|
18
9
|
url = optional(str)
|
|
19
10
|
|
|
20
11
|
|
|
12
|
+
class BoundingBox(BaseModel):
|
|
13
|
+
"""Bounding box describing a layer's position within the base image."""
|
|
14
|
+
|
|
15
|
+
absolute = optional([int])
|
|
16
|
+
normalized = optional([int])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Layer(BaseModel):
|
|
20
|
+
"""A decomposed layer with its position and metadata."""
|
|
21
|
+
|
|
22
|
+
url = required(str)
|
|
23
|
+
z_index = required(int)
|
|
24
|
+
bounding_box = optional(lambda: BoundingBox)
|
|
25
|
+
name = optional(str)
|
|
26
|
+
description = optional(str)
|
|
27
|
+
|
|
28
|
+
|
|
21
29
|
class TextToImageResponse(TaskResponse):
|
|
22
30
|
"""Seedream image task status response."""
|
|
23
31
|
|
|
@@ -45,7 +53,7 @@ class DecomposeLayersResponse(TaskResponse):
|
|
|
45
53
|
id = required(str)
|
|
46
54
|
status = optional(str, enum=lambda: TaskResponse.Status.ALL)
|
|
47
55
|
base_image = optional(lambda: Image)
|
|
48
|
-
layers = optional([lambda:
|
|
56
|
+
layers = optional([lambda: Layer])
|
|
49
57
|
error = optional(str)
|
|
50
58
|
|
|
51
59
|
|
|
@@ -53,4 +61,4 @@ class CompletedDecomposeLayersResponse(DecomposeLayersResponse):
|
|
|
53
61
|
"""Narrowed response once layer decomposition completes."""
|
|
54
62
|
|
|
55
63
|
base_image = required(lambda: Image)
|
|
56
|
-
layers = required([lambda:
|
|
64
|
+
layers = required([lambda: Layer])
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
1
4
|
import pytest
|
|
2
5
|
|
|
3
6
|
from runapi.core import config
|
|
4
7
|
from runapi.core.errors import AuthenticationError, ValidationError
|
|
8
|
+
from runapi.core.http_client import HttpClient
|
|
9
|
+
from runapi.core.options import ClientOptions
|
|
5
10
|
from runapi.seedream import SeedreamClient
|
|
6
11
|
from runapi.seedream.resources.edit_image import EditImage
|
|
7
12
|
from runapi.seedream.resources.decompose_layers import DecomposeLayers
|
|
@@ -73,10 +78,9 @@ def test_decompose_layers_create_get_and_completed_response():
|
|
|
73
78
|
{"id": "layers-1", "status": "processing"},
|
|
74
79
|
{
|
|
75
80
|
"id": "layers-1",
|
|
76
|
-
"status": "completed",
|
|
81
|
+
"status": "completed", "usage": {"cost": 0.05},
|
|
77
82
|
"base_image": {"url": "https://file.runapi.ai/base.jpeg"},
|
|
78
|
-
"layers": [{"url": "https://file.runapi.ai/layer.png"}],
|
|
79
|
-
},
|
|
83
|
+
"layers": [{"url": "https://file.runapi.ai/layer.png", "z_index": 1}]},
|
|
80
84
|
)
|
|
81
85
|
client = SeedreamClient(api_key="k", http_client=fake)
|
|
82
86
|
created = client.decompose_layers.create(
|
|
@@ -92,8 +96,7 @@ def test_decompose_layers_create_get_and_completed_response():
|
|
|
92
96
|
{
|
|
93
97
|
"model": "seedream-5-pro-layer-decomposition",
|
|
94
98
|
"image_url": "https://cdn.runapi.ai/public/samples/image.jpg",
|
|
95
|
-
"output_format": "jpeg",
|
|
96
|
-
},
|
|
99
|
+
"output_format": "jpeg"},
|
|
97
100
|
)
|
|
98
101
|
assert isinstance(result, DecomposeLayersResponse)
|
|
99
102
|
assert result.base_image.url == "https://file.runapi.ai/base.jpeg"
|
|
@@ -109,8 +112,7 @@ def test_create_posts_compacted_body():
|
|
|
109
112
|
model="seedream-v4-text-to-image", prompt="hello world", aspect_ratio="1:1", seed=None
|
|
110
113
|
)
|
|
111
114
|
assert fake.calls == [
|
|
112
|
-
("post", "/api/v1/seedream/text_to_image", {"model": "seedream-v4-text-to-image", "prompt": "hello world", "aspect_ratio": "1:1"})
|
|
113
|
-
]
|
|
115
|
+
("post", "/api/v1/seedream/text_to_image", {"model": "seedream-v4-text-to-image", "prompt": "hello world", "aspect_ratio": "1:1"})]
|
|
114
116
|
assert isinstance(result, TextToImageResponse)
|
|
115
117
|
|
|
116
118
|
|
|
@@ -135,10 +137,8 @@ def test_lite_output_format_is_forwarded():
|
|
|
135
137
|
"source_image_urls": ["https://cdn.runapi.ai/public/samples/image.jpg"],
|
|
136
138
|
"aspect_ratio": "1:1",
|
|
137
139
|
"output_quality": "high",
|
|
138
|
-
"output_format": "jpeg",
|
|
139
|
-
|
|
140
|
-
),
|
|
141
|
-
]
|
|
140
|
+
"output_format": "jpeg"},
|
|
141
|
+
)]
|
|
142
142
|
|
|
143
143
|
|
|
144
144
|
def test_pro_edit_is_forwarded():
|
|
@@ -166,7 +166,7 @@ def test_get_fetches_by_id():
|
|
|
166
166
|
def test_run_narrows_completed_type():
|
|
167
167
|
fake = FakeHttp(
|
|
168
168
|
{"id": "t1", "status": "pending"},
|
|
169
|
-
{"id": "t1", "status": "completed", "images": [{"url": "https://x/y.png"}]},
|
|
169
|
+
{"id": "t1", "status": "completed", "usage": {"cost": 0.05}, "images": [{"url": "https://x/y.png"}]},
|
|
170
170
|
)
|
|
171
171
|
client = SeedreamClient(api_key="k", http_client=fake)
|
|
172
172
|
result = client.text_to_image.run(model="seedream-v4-text-to-image", prompt="a serene lake")
|
|
@@ -174,82 +174,27 @@ def test_run_narrows_completed_type():
|
|
|
174
174
|
assert result.images[0].url == "https://x/y.png"
|
|
175
175
|
|
|
176
176
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
def test_rejects_unknown_model():
|
|
181
|
-
client = SeedreamClient(api_key="k", http_client=FakeHttp())
|
|
182
|
-
with pytest.raises(ValidationError, match="model must be one of:"):
|
|
183
|
-
client.text_to_image.create(model="nope", prompt="hi there")
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
def test_requires_prompt():
|
|
187
|
-
client = SeedreamClient(api_key="k", http_client=FakeHttp())
|
|
188
|
-
with pytest.raises(ValidationError, match="prompt is required"):
|
|
189
|
-
client.text_to_image.create(model="seedream-v4-text-to-image")
|
|
190
|
-
|
|
177
|
+
def test_server_decides_unknown_models_and_params():
|
|
178
|
+
sent = []
|
|
191
179
|
|
|
192
|
-
def
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
def test_non_v4_requires_aspect_ratio_and_quality():
|
|
199
|
-
client = SeedreamClient(api_key="k", http_client=FakeHttp())
|
|
200
|
-
with pytest.raises(ValidationError, match="aspect_ratio is required"):
|
|
201
|
-
client.text_to_image.create(model="seedream-4.5-text-to-image", prompt="hi there")
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
def test_text_to_image_rejects_source_image_urls():
|
|
205
|
-
client = SeedreamClient(api_key="k", http_client=FakeHttp())
|
|
206
|
-
with pytest.raises(ValidationError, match="source_image_urls is not supported"):
|
|
207
|
-
client.text_to_image.create(
|
|
208
|
-
model="seedream-v4-text-to-image", prompt="hi there", source_image_urls=["https://x/a.png"]
|
|
209
|
-
)
|
|
180
|
+
def handler(request):
|
|
181
|
+
body = json.loads(request.content)
|
|
182
|
+
sent.append(body)
|
|
183
|
+
if body["output_quality"] == "bad":
|
|
184
|
+
return httpx.Response(400, json={"error": "output_quality is not supported"})
|
|
185
|
+
return httpx.Response(200, json={"id": "task_1", "status": "processing"})
|
|
210
186
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
with pytest.raises(ValidationError, match="source_image_urls is required"):
|
|
215
|
-
client.edit_image.create(model="seedream-v4-edit", prompt="make it pop")
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
def test_lite_prompt_min_length():
|
|
219
|
-
client = SeedreamClient(api_key="k", http_client=FakeHttp())
|
|
220
|
-
with pytest.raises(ValidationError, match="between 3 and 3000"):
|
|
221
|
-
client.text_to_image.create(model="seedream-5-lite-text-to-image", prompt="hi", aspect_ratio="1:1", output_quality="high")
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
def test_pro_prompt_allows_5000_characters():
|
|
225
|
-
client = SeedreamClient(api_key="k", http_client=FakeHttp())
|
|
226
|
-
client.text_to_image.create(
|
|
227
|
-
model="seedream-5-pro-text-to-image",
|
|
228
|
-
prompt="x" * 5000,
|
|
229
|
-
aspect_ratio="1:1",
|
|
230
|
-
output_quality="high",
|
|
187
|
+
http = HttpClient(
|
|
188
|
+
ClientOptions(api_key="k", base_url="https://runapi.ai", max_retries=0),
|
|
189
|
+
transport=httpx.MockTransport(handler),
|
|
231
190
|
)
|
|
191
|
+
client = SeedreamClient(api_key="k", http_client=http)
|
|
192
|
+
params = {"model": "seedream-future-text-to-image", "prompt": "a lake", "future_setting": "on"}
|
|
232
193
|
|
|
194
|
+
result = client.text_to_image.create(**params, output_quality="ultra")
|
|
195
|
+
with pytest.raises(ValidationError) as error:
|
|
196
|
+
client.text_to_image.create(**params, output_quality="bad")
|
|
233
197
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
client.edit_image.create(
|
|
238
|
-
model="seedream-5-pro-edit",
|
|
239
|
-
prompt="restyle this image",
|
|
240
|
-
source_image_urls=["https://cdn.runapi.ai/public/samples/image.jpg"] * 11,
|
|
241
|
-
aspect_ratio="1:1",
|
|
242
|
-
output_quality="high",
|
|
243
|
-
)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
def test_lite_output_format_enum():
|
|
247
|
-
client = SeedreamClient(api_key="k", http_client=FakeHttp())
|
|
248
|
-
with pytest.raises(ValidationError, match="output_format must be one of: png, jpeg"):
|
|
249
|
-
client.text_to_image.create(
|
|
250
|
-
model="seedream-5-lite-text-to-image",
|
|
251
|
-
prompt="hi there",
|
|
252
|
-
aspect_ratio="1:1",
|
|
253
|
-
output_quality="high",
|
|
254
|
-
output_format="webp",
|
|
255
|
-
)
|
|
198
|
+
assert sent[0] == {**params, "output_quality": "ultra"}
|
|
199
|
+
assert result.id == "task_1"
|
|
200
|
+
assert error.value.message == "output_quality is not supported"
|
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
CONTRACT = {
|
|
2
|
-
"decompose-layers": {
|
|
3
|
-
"models": ["seedream-5-pro-layer-decomposition"],
|
|
4
|
-
"fields_by_model": {
|
|
5
|
-
"seedream-5-pro-layer-decomposition": {
|
|
6
|
-
"image_url": {
|
|
7
|
-
"required": True
|
|
8
|
-
},
|
|
9
|
-
"output_format": {
|
|
10
|
-
"enum": ["png", "jpeg"]
|
|
11
|
-
},
|
|
12
|
-
"prompt": {
|
|
13
|
-
"max": 5000,
|
|
14
|
-
"length": True
|
|
15
|
-
},
|
|
16
|
-
"size": {
|
|
17
|
-
"enum": ["auto", "1K", "1.5K", "2K"]
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
},
|
|
22
|
-
"edit-image": {
|
|
23
|
-
"models": ["seedream-4.5-edit", "seedream-5-lite-edit", "seedream-5-pro-edit", "seedream-v4-edit"],
|
|
24
|
-
"fields_by_model": {
|
|
25
|
-
"seedream-4.5-edit": {
|
|
26
|
-
"aspect_ratio": {
|
|
27
|
-
"enum": ["1:1", "4:3", "3:4", "16:9", "9:16", "2:3", "3:2", "21:9"],
|
|
28
|
-
"required": True
|
|
29
|
-
},
|
|
30
|
-
"output_count": {
|
|
31
|
-
"type": "integer"
|
|
32
|
-
},
|
|
33
|
-
"output_quality": {
|
|
34
|
-
"enum": ["basic", "high"],
|
|
35
|
-
"required": True
|
|
36
|
-
},
|
|
37
|
-
"seed": {
|
|
38
|
-
"type": "integer"
|
|
39
|
-
},
|
|
40
|
-
"source_image_urls": {
|
|
41
|
-
"required": True,
|
|
42
|
-
"min_items": 1,
|
|
43
|
-
"max_items": 14
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
"seedream-5-lite-edit": {
|
|
47
|
-
"aspect_ratio": {
|
|
48
|
-
"enum": ["1:1", "4:3", "3:4", "16:9", "9:16", "2:3", "3:2", "21:9"],
|
|
49
|
-
"required": True
|
|
50
|
-
},
|
|
51
|
-
"output_count": {
|
|
52
|
-
"type": "integer"
|
|
53
|
-
},
|
|
54
|
-
"output_format": {
|
|
55
|
-
"enum": ["png", "jpeg"]
|
|
56
|
-
},
|
|
57
|
-
"output_quality": {
|
|
58
|
-
"enum": ["basic", "high", "ultra"],
|
|
59
|
-
"required": True
|
|
60
|
-
},
|
|
61
|
-
"seed": {
|
|
62
|
-
"type": "integer"
|
|
63
|
-
},
|
|
64
|
-
"source_image_urls": {
|
|
65
|
-
"required": True,
|
|
66
|
-
"min_items": 1,
|
|
67
|
-
"max_items": 14
|
|
68
|
-
}
|
|
69
|
-
},
|
|
70
|
-
"seedream-5-pro-edit": {
|
|
71
|
-
"aspect_ratio": {
|
|
72
|
-
"enum": ["1:1", "4:3", "3:4", "16:9", "9:16", "2:3", "3:2", "21:9"],
|
|
73
|
-
"required": True
|
|
74
|
-
},
|
|
75
|
-
"output_format": {
|
|
76
|
-
"enum": ["png", "jpeg"]
|
|
77
|
-
},
|
|
78
|
-
"output_quality": {
|
|
79
|
-
"enum": ["basic", "high"],
|
|
80
|
-
"required": True
|
|
81
|
-
},
|
|
82
|
-
"prompt": {
|
|
83
|
-
"required": True,
|
|
84
|
-
"min": 3,
|
|
85
|
-
"max": 5000,
|
|
86
|
-
"length": True
|
|
87
|
-
},
|
|
88
|
-
"source_image_urls": {
|
|
89
|
-
"required": True,
|
|
90
|
-
"min_items": 1,
|
|
91
|
-
"max_items": 10
|
|
92
|
-
}
|
|
93
|
-
},
|
|
94
|
-
"seedream-v4-edit": {
|
|
95
|
-
"aspect_ratio": {
|
|
96
|
-
"enum": ["1:1", "4:3", "3:4", "3:2", "2:3", "16:9", "9:16", "21:9"]
|
|
97
|
-
},
|
|
98
|
-
"output_count": {
|
|
99
|
-
"enum": [1, 2, 3, 4, 5, 6],
|
|
100
|
-
"type": "integer"
|
|
101
|
-
},
|
|
102
|
-
"output_resolution": {
|
|
103
|
-
"enum": ["1k", "2k", "4k"]
|
|
104
|
-
},
|
|
105
|
-
"seed": {
|
|
106
|
-
"type": "integer"
|
|
107
|
-
},
|
|
108
|
-
"source_image_urls": {
|
|
109
|
-
"required": True,
|
|
110
|
-
"min_items": 1,
|
|
111
|
-
"max_items": 10
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
},
|
|
115
|
-
"rules": [{
|
|
116
|
-
"when": {
|
|
117
|
-
"model": "seedream-4.5-edit"
|
|
118
|
-
},
|
|
119
|
-
"forbidden": ["output_format"]
|
|
120
|
-
}, {
|
|
121
|
-
"when": {
|
|
122
|
-
"model": "seedream-5-pro-edit"
|
|
123
|
-
},
|
|
124
|
-
"forbidden": ["output_resolution", "output_count", "seed"]
|
|
125
|
-
}, {
|
|
126
|
-
"when": {
|
|
127
|
-
"model": "seedream-v4-edit"
|
|
128
|
-
},
|
|
129
|
-
"forbidden": ["output_format"]
|
|
130
|
-
}]
|
|
131
|
-
},
|
|
132
|
-
"text-to-image": {
|
|
133
|
-
"models": ["seedream-4.5-text-to-image", "seedream-5-lite-text-to-image", "seedream-5-pro-text-to-image", "seedream-v4-text-to-image"],
|
|
134
|
-
"fields_by_model": {
|
|
135
|
-
"seedream-4.5-text-to-image": {
|
|
136
|
-
"aspect_ratio": {
|
|
137
|
-
"enum": ["1:1", "4:3", "3:4", "16:9", "9:16", "2:3", "3:2", "21:9"],
|
|
138
|
-
"required": True
|
|
139
|
-
},
|
|
140
|
-
"output_count": {
|
|
141
|
-
"type": "integer"
|
|
142
|
-
},
|
|
143
|
-
"output_quality": {
|
|
144
|
-
"enum": ["basic", "high"],
|
|
145
|
-
"required": True
|
|
146
|
-
},
|
|
147
|
-
"seed": {
|
|
148
|
-
"type": "integer"
|
|
149
|
-
}
|
|
150
|
-
},
|
|
151
|
-
"seedream-5-lite-text-to-image": {
|
|
152
|
-
"aspect_ratio": {
|
|
153
|
-
"enum": ["1:1", "4:3", "3:4", "16:9", "9:16", "2:3", "3:2", "21:9"],
|
|
154
|
-
"required": True
|
|
155
|
-
},
|
|
156
|
-
"output_count": {
|
|
157
|
-
"type": "integer"
|
|
158
|
-
},
|
|
159
|
-
"output_format": {
|
|
160
|
-
"enum": ["png", "jpeg"]
|
|
161
|
-
},
|
|
162
|
-
"output_quality": {
|
|
163
|
-
"enum": ["basic", "high", "ultra"],
|
|
164
|
-
"required": True
|
|
165
|
-
},
|
|
166
|
-
"seed": {
|
|
167
|
-
"type": "integer"
|
|
168
|
-
}
|
|
169
|
-
},
|
|
170
|
-
"seedream-5-pro-text-to-image": {
|
|
171
|
-
"aspect_ratio": {
|
|
172
|
-
"enum": ["1:1", "4:3", "3:4", "16:9", "9:16", "2:3", "3:2", "21:9"],
|
|
173
|
-
"required": True
|
|
174
|
-
},
|
|
175
|
-
"output_format": {
|
|
176
|
-
"enum": ["png", "jpeg"]
|
|
177
|
-
},
|
|
178
|
-
"output_quality": {
|
|
179
|
-
"enum": ["basic", "high"],
|
|
180
|
-
"required": True
|
|
181
|
-
},
|
|
182
|
-
"prompt": {
|
|
183
|
-
"required": True,
|
|
184
|
-
"min": 3,
|
|
185
|
-
"max": 5000,
|
|
186
|
-
"length": True
|
|
187
|
-
}
|
|
188
|
-
},
|
|
189
|
-
"seedream-v4-text-to-image": {
|
|
190
|
-
"aspect_ratio": {
|
|
191
|
-
"enum": ["1:1", "4:3", "3:4", "3:2", "2:3", "16:9", "9:16", "21:9"]
|
|
192
|
-
},
|
|
193
|
-
"output_count": {
|
|
194
|
-
"enum": [1, 2, 3, 4, 5, 6],
|
|
195
|
-
"type": "integer"
|
|
196
|
-
},
|
|
197
|
-
"output_resolution": {
|
|
198
|
-
"enum": ["1k", "2k", "4k"]
|
|
199
|
-
},
|
|
200
|
-
"seed": {
|
|
201
|
-
"type": "integer"
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
},
|
|
205
|
-
"rules": [{
|
|
206
|
-
"when": {
|
|
207
|
-
"model": "seedream-4.5-text-to-image"
|
|
208
|
-
},
|
|
209
|
-
"forbidden": ["output_format"]
|
|
210
|
-
}, {
|
|
211
|
-
"when": {
|
|
212
|
-
"model": "seedream-5-pro-text-to-image"
|
|
213
|
-
},
|
|
214
|
-
"forbidden": ["output_resolution", "output_count", "seed"]
|
|
215
|
-
}, {
|
|
216
|
-
"when": {
|
|
217
|
-
"model": "seedream-v4-text-to-image"
|
|
218
|
-
},
|
|
219
|
-
"forbidden": ["output_format"]
|
|
220
|
-
}]
|
|
221
|
-
}
|
|
222
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
"""Seedream text-to-image resource."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from typing import Any, Dict, Optional
|
|
6
|
-
|
|
7
|
-
from runapi.core import Resource, ValidationError, RequestOptions
|
|
8
|
-
|
|
9
|
-
from ..contract_gen import CONTRACT
|
|
10
|
-
from ..types import (
|
|
11
|
-
LONG_PROMPT_MODELS,
|
|
12
|
-
MINIMUM_THREE_PROMPT_MODELS,
|
|
13
|
-
V4_MODELS,
|
|
14
|
-
CompletedTextToImageResponse,
|
|
15
|
-
TextToImageResponse,
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class TextToImage(Resource):
|
|
20
|
-
"""Generate images from text prompts with Seedream models."""
|
|
21
|
-
|
|
22
|
-
ENDPOINT = "/api/v1/seedream/text_to_image"
|
|
23
|
-
|
|
24
|
-
RESPONSE_CLASS = TextToImageResponse
|
|
25
|
-
COMPLETED_RESPONSE_CLASS = CompletedTextToImageResponse
|
|
26
|
-
|
|
27
|
-
PROMPT_MAX_LENGTH = 3000
|
|
28
|
-
V4_PROMPT_MAX_LENGTH = 5000
|
|
29
|
-
PROMPT_MIN_LENGTH_LITE = 3
|
|
30
|
-
|
|
31
|
-
def run(self, options: Optional[RequestOptions] = None, **params: Any) -> Any:
|
|
32
|
-
"""Create a text-to-image task and poll until it completes.
|
|
33
|
-
|
|
34
|
-
Args:
|
|
35
|
-
**params: text-to-image parameters (model, ...).
|
|
36
|
-
|
|
37
|
-
Returns:
|
|
38
|
-
The completed (narrowed) text-to-image response.
|
|
39
|
-
"""
|
|
40
|
-
task = self.create(options=options, **params)
|
|
41
|
-
return self._poll_until_complete(lambda: self.get(task.id, options=options))
|
|
42
|
-
|
|
43
|
-
def create(self, options: Optional[RequestOptions] = None, **params: Any) -> Any:
|
|
44
|
-
"""Create a text-to-image task and return immediately with an id.
|
|
45
|
-
|
|
46
|
-
Args:
|
|
47
|
-
**params: text-to-image parameters (model, ...).
|
|
48
|
-
|
|
49
|
-
Returns:
|
|
50
|
-
The task creation result with an id.
|
|
51
|
-
"""
|
|
52
|
-
compacted = self._compact_params(params)
|
|
53
|
-
self._validate_params(compacted)
|
|
54
|
-
return self._request("post", self.ENDPOINT, body=compacted, options=options)
|
|
55
|
-
|
|
56
|
-
def get(self, id: str, options: Optional[RequestOptions] = None) -> Any:
|
|
57
|
-
"""Fetch the current status of a text-to-image task.
|
|
58
|
-
|
|
59
|
-
Args:
|
|
60
|
-
id: The task id returned by ``create``.
|
|
61
|
-
|
|
62
|
-
Returns:
|
|
63
|
-
The current task status.
|
|
64
|
-
"""
|
|
65
|
-
return self._request("get", f"{self.ENDPOINT}/{id}", options=options)
|
|
66
|
-
|
|
67
|
-
def _validate_params(self, params: Dict[str, Any]) -> None:
|
|
68
|
-
self._validate_contract(CONTRACT["text-to-image"], params)
|
|
69
|
-
|
|
70
|
-
model = params.get("model")
|
|
71
|
-
|
|
72
|
-
prompt = params.get("prompt")
|
|
73
|
-
if not (isinstance(prompt, str) and prompt):
|
|
74
|
-
raise ValidationError("prompt is required")
|
|
75
|
-
max_length = self.V4_PROMPT_MAX_LENGTH if model in LONG_PROMPT_MODELS else self.PROMPT_MAX_LENGTH
|
|
76
|
-
if len(prompt) > max_length:
|
|
77
|
-
raise ValidationError(f"prompt must be at most {max_length} characters")
|
|
78
|
-
if model in MINIMUM_THREE_PROMPT_MODELS and len(prompt) < self.PROMPT_MIN_LENGTH_LITE:
|
|
79
|
-
raise ValidationError(
|
|
80
|
-
f"prompt must be between {self.PROMPT_MIN_LENGTH_LITE} and {max_length} characters"
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
if model in V4_MODELS:
|
|
84
|
-
self._validate_integer(params, "seed")
|
|
85
|
-
|
|
86
|
-
if self._field_present(params, "source_image_urls"):
|
|
87
|
-
raise ValidationError(f"source_image_urls is not supported for {model}")
|
|
88
|
-
|
|
89
|
-
@staticmethod
|
|
90
|
-
def _field_present(params: Dict[str, Any], key: str) -> bool:
|
|
91
|
-
value = params.get(key)
|
|
92
|
-
if value is None:
|
|
93
|
-
return False
|
|
94
|
-
if hasattr(value, "__len__"):
|
|
95
|
-
return len(value) > 0
|
|
96
|
-
return True
|
|
97
|
-
|
|
98
|
-
@staticmethod
|
|
99
|
-
def _validate_integer(params: Dict[str, Any], key: str) -> None:
|
|
100
|
-
value = params.get(key)
|
|
101
|
-
if value is None:
|
|
102
|
-
return
|
|
103
|
-
if isinstance(value, int) and not isinstance(value, bool):
|
|
104
|
-
return
|
|
105
|
-
raise ValidationError(f"{key} must be an integer")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|