runapi-seedream 0.3.0__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.
@@ -1,18 +1,18 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: runapi-seedream
3
- Version: 0.3.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.3.0
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>=0.8.0
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.3.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>=0.8.0"]
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.3.0"
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"]
@@ -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, Dict, Optional
5
+ from typing import Any, Optional
6
6
 
7
- from runapi.core import Resource, ValidationError, RequestOptions
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,18 +1,9 @@
1
- """Seedream model groups and responses; request enums come from CONTRACT."""
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)
@@ -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", "z_index": 1}],
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
- # --- validation -----------------------------------------------------------
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 test_v4_output_count_range():
193
- client = SeedreamClient(api_key="k", http_client=FakeHttp())
194
- with pytest.raises(ValidationError, match="output_count must be one of: 1, 2, 3, 4, 5, 6"):
195
- client.text_to_image.create(model="seedream-v4-text-to-image", prompt="hi there", output_count=9)
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
- def test_edit_requires_source_image_urls():
213
- client = SeedreamClient(api_key="k", http_client=FakeHttp())
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
- def test_pro_edit_rejects_more_than_ten_source_images():
235
- client = SeedreamClient(api_key="k", http_client=FakeHttp())
236
- with pytest.raises(ValidationError, match="source_image_urls must contain between 1 and 10 items"):
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")