everypixel-cli 0.1.0__py3-none-any.whl
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.
- everypixel_cli/__init__.py +1 -0
- everypixel_cli/__main__.py +5 -0
- everypixel_cli/application/__init__.py +10 -0
- everypixel_cli/application/models.py +59 -0
- everypixel_cli/application/serialization.py +20 -0
- everypixel_cli/application/services.py +1376 -0
- everypixel_cli/cli.py +1520 -0
- everypixel_cli/client.py +159 -0
- everypixel_cli/config.py +221 -0
- everypixel_cli/errors.py +262 -0
- everypixel_cli/files.py +298 -0
- everypixel_cli/mcp_server.py +828 -0
- everypixel_cli/openapi.py +338 -0
- everypixel_cli/output.py +159 -0
- everypixel_cli/resources/__init__.py +1 -0
- everypixel_cli/resources/openapi.json +5402 -0
- everypixel_cli/schemas.py +846 -0
- everypixel_cli-0.1.0.dist-info/METADATA +454 -0
- everypixel_cli-0.1.0.dist-info/RECORD +22 -0
- everypixel_cli-0.1.0.dist-info/WHEEL +4 -0
- everypixel_cli-0.1.0.dist-info/entry_points.txt +3 -0
- everypixel_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,846 @@
|
|
|
1
|
+
"""Pydantic payload schemas for common generation commands.
|
|
2
|
+
|
|
3
|
+
These models validate CLI parameters before a request is sent to the API:
|
|
4
|
+
media sources, model limits, enum values, and numeric ranges.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Annotated, Any, Literal
|
|
10
|
+
|
|
11
|
+
from pydantic import (
|
|
12
|
+
BaseModel,
|
|
13
|
+
ConfigDict,
|
|
14
|
+
Discriminator,
|
|
15
|
+
Field,
|
|
16
|
+
model_validator,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
ImageSize = Literal[
|
|
21
|
+
"square",
|
|
22
|
+
"portrait_3_2",
|
|
23
|
+
"portrait_4_3",
|
|
24
|
+
"portrait_16_9",
|
|
25
|
+
"landscape_3_2",
|
|
26
|
+
"landscape_4_3",
|
|
27
|
+
"landscape_16_9",
|
|
28
|
+
]
|
|
29
|
+
ImageStyle = Literal[
|
|
30
|
+
"instagram",
|
|
31
|
+
"transparent",
|
|
32
|
+
"replication",
|
|
33
|
+
"basic",
|
|
34
|
+
]
|
|
35
|
+
ImageResolution = Literal["1k", "2k", "3k", "4k"]
|
|
36
|
+
ImageGenerateModel = Literal[
|
|
37
|
+
"zimage",
|
|
38
|
+
"wan22",
|
|
39
|
+
"wan2.7",
|
|
40
|
+
"wan2.7-pro",
|
|
41
|
+
"flux2",
|
|
42
|
+
"grok",
|
|
43
|
+
"grok_quality",
|
|
44
|
+
"gemini-3.1-flash",
|
|
45
|
+
"gemini-3-pro",
|
|
46
|
+
"seedream-5-pro",
|
|
47
|
+
"seedream-5",
|
|
48
|
+
"gpt-image-2-low",
|
|
49
|
+
"gpt-image-2-medium",
|
|
50
|
+
"gpt-image-2-high",
|
|
51
|
+
]
|
|
52
|
+
ImageEditModel = Literal[
|
|
53
|
+
"flux2",
|
|
54
|
+
"qwen",
|
|
55
|
+
"wan2.7",
|
|
56
|
+
"wan2.7-pro",
|
|
57
|
+
"grok",
|
|
58
|
+
"grok_quality",
|
|
59
|
+
"gemini-3.1-flash",
|
|
60
|
+
"gemini-3-pro",
|
|
61
|
+
"seedream-5-pro",
|
|
62
|
+
"seedream-5",
|
|
63
|
+
"gpt-image-2-low",
|
|
64
|
+
"gpt-image-2-medium",
|
|
65
|
+
"gpt-image-2-high",
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
_GEMINI_IMAGE_MODELS = {"gemini-3.1-flash", "gemini-3-pro"}
|
|
69
|
+
_WAN_IMAGE_MODELS = {"wan2.7", "wan2.7-pro"}
|
|
70
|
+
_SEEDREAM_IMAGE_MODELS = {"seedream-5-pro", "seedream-5"}
|
|
71
|
+
_GPT_IMAGE_2_MODELS = {
|
|
72
|
+
"gpt-image-2-low",
|
|
73
|
+
"gpt-image-2-medium",
|
|
74
|
+
"gpt-image-2-high",
|
|
75
|
+
}
|
|
76
|
+
_IMAGE_EDIT_MODEL_MAX_IMAGES = {
|
|
77
|
+
"flux2": 5,
|
|
78
|
+
"qwen": 3,
|
|
79
|
+
"wan2.7": 9,
|
|
80
|
+
"wan2.7-pro": 9,
|
|
81
|
+
"grok": 3,
|
|
82
|
+
"grok_quality": 3,
|
|
83
|
+
"gemini-3.1-flash": 5,
|
|
84
|
+
"gemini-3-pro": 5,
|
|
85
|
+
"seedream-5-pro": 10,
|
|
86
|
+
"seedream-5": 14,
|
|
87
|
+
"gpt-image-2-low": 5,
|
|
88
|
+
"gpt-image-2-medium": 5,
|
|
89
|
+
"gpt-image-2-high": 5,
|
|
90
|
+
}
|
|
91
|
+
ImageEditMedia = Annotated[
|
|
92
|
+
list[str],
|
|
93
|
+
Field(min_length=1, max_length=max(_IMAGE_EDIT_MODEL_MAX_IMAGES.values())),
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class TaskResponse(BaseModel):
|
|
98
|
+
"""Standard API response when an async task is created."""
|
|
99
|
+
|
|
100
|
+
task_id: str
|
|
101
|
+
status: str = "PENDING"
|
|
102
|
+
result: Any | None = None
|
|
103
|
+
queue: int | None = None
|
|
104
|
+
error: str | None = None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ImageGeneratePayload(BaseModel):
|
|
108
|
+
"""Payload for image generation."""
|
|
109
|
+
|
|
110
|
+
prompt: str
|
|
111
|
+
model: ImageGenerateModel = "zimage"
|
|
112
|
+
image_size: ImageSize = "square"
|
|
113
|
+
style: ImageStyle | None = None
|
|
114
|
+
resolution: ImageResolution = "1k"
|
|
115
|
+
seed: int = -1
|
|
116
|
+
image_url: str | None = None
|
|
117
|
+
callback_url: str | None = None
|
|
118
|
+
|
|
119
|
+
@model_validator(mode="after")
|
|
120
|
+
def validate_model_constraints(self) -> "ImageGeneratePayload":
|
|
121
|
+
"""Validate provider-specific styles, inputs, and resolutions."""
|
|
122
|
+
|
|
123
|
+
style = self.style
|
|
124
|
+
if style is not None:
|
|
125
|
+
expected_models = {
|
|
126
|
+
"basic": "wan22",
|
|
127
|
+
"instagram": "wan22",
|
|
128
|
+
"replication": "wan22",
|
|
129
|
+
"transparent": "flux2",
|
|
130
|
+
}
|
|
131
|
+
if self.model != expected_models[style]:
|
|
132
|
+
raise ValueError(
|
|
133
|
+
f"Model {self.model} is not compatible with style {style}"
|
|
134
|
+
)
|
|
135
|
+
if style == "replication" and not self.image_url:
|
|
136
|
+
raise ValueError(f"{style} style requires --image")
|
|
137
|
+
|
|
138
|
+
if self.model in {"grok", "grok_quality"} and self.resolution not in {
|
|
139
|
+
"1k",
|
|
140
|
+
"2k",
|
|
141
|
+
}:
|
|
142
|
+
raise ValueError(
|
|
143
|
+
f"resolution {self.resolution} is not supported by model {self.model}"
|
|
144
|
+
)
|
|
145
|
+
if self.model in _WAN_IMAGE_MODELS:
|
|
146
|
+
if not 1 <= len(self.prompt) <= 5000:
|
|
147
|
+
raise ValueError(
|
|
148
|
+
"Wan 2.7 prompt must contain between 1 and 5000 characters"
|
|
149
|
+
)
|
|
150
|
+
if not -1 <= self.seed <= 2_147_483_647:
|
|
151
|
+
raise ValueError("Wan 2.7 seed must be -1 or between 0 and 2147483647")
|
|
152
|
+
if self.image_url is not None:
|
|
153
|
+
raise ValueError(
|
|
154
|
+
"image is not supported for Wan 2.7 image generate; use image edit"
|
|
155
|
+
)
|
|
156
|
+
if "resolution" not in self.model_fields_set:
|
|
157
|
+
self.resolution = "2k"
|
|
158
|
+
supported_resolutions = {"1k", "2k"}
|
|
159
|
+
if self.model == "wan2.7-pro":
|
|
160
|
+
supported_resolutions.add("4k")
|
|
161
|
+
if self.resolution not in supported_resolutions:
|
|
162
|
+
raise ValueError(
|
|
163
|
+
f"resolution {self.resolution} is not supported by model "
|
|
164
|
+
f"{self.model}"
|
|
165
|
+
)
|
|
166
|
+
if self.model in _GEMINI_IMAGE_MODELS and self.resolution not in {
|
|
167
|
+
"1k",
|
|
168
|
+
"2k",
|
|
169
|
+
"4k",
|
|
170
|
+
}:
|
|
171
|
+
raise ValueError(
|
|
172
|
+
f"resolution {self.resolution} is not supported by model {self.model}"
|
|
173
|
+
)
|
|
174
|
+
if self.model in _GPT_IMAGE_2_MODELS:
|
|
175
|
+
if self.image_url is not None:
|
|
176
|
+
raise ValueError(
|
|
177
|
+
"image is not supported for GPT Image 2 image generate; "
|
|
178
|
+
"use image edit"
|
|
179
|
+
)
|
|
180
|
+
if self.resolution not in {"1k", "2k", "3k"}:
|
|
181
|
+
raise ValueError(
|
|
182
|
+
f"resolution {self.resolution} is not supported by model "
|
|
183
|
+
f"{self.model}"
|
|
184
|
+
)
|
|
185
|
+
if self.model in _SEEDREAM_IMAGE_MODELS:
|
|
186
|
+
if self.image_url is not None:
|
|
187
|
+
raise ValueError(
|
|
188
|
+
"image is not supported for Seedream image generate; use image edit"
|
|
189
|
+
)
|
|
190
|
+
if "resolution" not in self.model_fields_set:
|
|
191
|
+
self.resolution = "2k"
|
|
192
|
+
supported_resolutions_by_model = {
|
|
193
|
+
"seedream-5-pro": {"1k", "2k"},
|
|
194
|
+
"seedream-5": {"2k", "3k", "4k"},
|
|
195
|
+
}
|
|
196
|
+
if self.resolution not in supported_resolutions_by_model[self.model]:
|
|
197
|
+
raise ValueError(
|
|
198
|
+
f"resolution {self.resolution} is not supported by model "
|
|
199
|
+
f"{self.model}"
|
|
200
|
+
)
|
|
201
|
+
return self
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class ImageEditPayload(BaseModel):
|
|
205
|
+
"""Payload for image editing."""
|
|
206
|
+
|
|
207
|
+
prompt: str
|
|
208
|
+
image_urls: ImageEditMedia
|
|
209
|
+
model: ImageEditModel = "flux2"
|
|
210
|
+
image_size: ImageSize | None = None
|
|
211
|
+
resolution: ImageResolution = "1k"
|
|
212
|
+
seed: int = -1
|
|
213
|
+
callback_url: str | None = None
|
|
214
|
+
|
|
215
|
+
@model_validator(mode="after")
|
|
216
|
+
def validate_model_limits(self) -> "ImageEditPayload":
|
|
217
|
+
"""Validate image count limits for the selected model."""
|
|
218
|
+
|
|
219
|
+
max_images = _IMAGE_EDIT_MODEL_MAX_IMAGES.get(self.model)
|
|
220
|
+
if max_images is None:
|
|
221
|
+
raise ValueError(f"image edit model {self.model} has no configured limit")
|
|
222
|
+
if len(self.image_urls) > max_images:
|
|
223
|
+
raise ValueError(f"{self.model} supports a maximum of {max_images} images")
|
|
224
|
+
|
|
225
|
+
if self.model in {"grok", "grok_quality"} and self.resolution not in {
|
|
226
|
+
"1k",
|
|
227
|
+
"2k",
|
|
228
|
+
}:
|
|
229
|
+
raise ValueError(
|
|
230
|
+
f"resolution {self.resolution} is not supported by model {self.model}"
|
|
231
|
+
)
|
|
232
|
+
if self.model in _WAN_IMAGE_MODELS:
|
|
233
|
+
if not 1 <= len(self.prompt) <= 5000:
|
|
234
|
+
raise ValueError(
|
|
235
|
+
"Wan 2.7 prompt must contain between 1 and 5000 characters"
|
|
236
|
+
)
|
|
237
|
+
if not -1 <= self.seed <= 2_147_483_647:
|
|
238
|
+
raise ValueError("Wan 2.7 seed must be -1 or between 0 and 2147483647")
|
|
239
|
+
if "resolution" not in self.model_fields_set:
|
|
240
|
+
self.resolution = "2k"
|
|
241
|
+
if self.resolution not in {"1k", "2k"}:
|
|
242
|
+
raise ValueError(
|
|
243
|
+
f"resolution {self.resolution} is not supported by model "
|
|
244
|
+
f"{self.model}"
|
|
245
|
+
)
|
|
246
|
+
if self.model in _GEMINI_IMAGE_MODELS and self.resolution not in {
|
|
247
|
+
"1k",
|
|
248
|
+
"2k",
|
|
249
|
+
"4k",
|
|
250
|
+
}:
|
|
251
|
+
raise ValueError(
|
|
252
|
+
f"resolution {self.resolution} is not supported by model {self.model}"
|
|
253
|
+
)
|
|
254
|
+
if self.model in _SEEDREAM_IMAGE_MODELS:
|
|
255
|
+
if "resolution" not in self.model_fields_set:
|
|
256
|
+
self.resolution = "2k"
|
|
257
|
+
supported = {
|
|
258
|
+
"seedream-5-pro": {"1k", "2k"},
|
|
259
|
+
"seedream-5": {"2k", "3k", "4k"},
|
|
260
|
+
}
|
|
261
|
+
if self.resolution not in supported[self.model]:
|
|
262
|
+
raise ValueError(
|
|
263
|
+
f"resolution {self.resolution} is not supported by model "
|
|
264
|
+
f"{self.model}"
|
|
265
|
+
)
|
|
266
|
+
if self.model in _GPT_IMAGE_2_MODELS:
|
|
267
|
+
if self.resolution not in {"1k", "2k", "3k"}:
|
|
268
|
+
raise ValueError(
|
|
269
|
+
f"resolution {self.resolution} is not supported by model "
|
|
270
|
+
f"{self.model}"
|
|
271
|
+
)
|
|
272
|
+
if self.image_size is None:
|
|
273
|
+
self.image_size = "square"
|
|
274
|
+
return self
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
CommonVideoResolution = Literal["360p", "480p", "720p"]
|
|
278
|
+
CommonVideoAspectRatio = Literal["16:9", "9:16", "1:1"]
|
|
279
|
+
Seedance2VideoResolution = Literal["480p", "720p", "1080p", "4k"]
|
|
280
|
+
Seedance2VideoAspectRatio = Literal["16:9", "4:3", "1:1", "3:4", "9:16", "21:9"]
|
|
281
|
+
Wan27VideoResolution = Literal["720p", "1080p"]
|
|
282
|
+
Wan27VideoAspectRatio = Literal["16:9", "9:16", "1:1", "4:3", "3:4"]
|
|
283
|
+
VeoVideoResolution = Literal["720p", "1080p", "4k"]
|
|
284
|
+
VeoVideoAspectRatio = Literal["16:9", "9:16"]
|
|
285
|
+
Aleph2AspectRatio = Literal["16:9", "4:3", "3:2", "1:1", "2:3", "3:4", "9:16", "21:9"]
|
|
286
|
+
VideoGenerateModel = Literal[
|
|
287
|
+
"wan22",
|
|
288
|
+
"ltx23",
|
|
289
|
+
"grok",
|
|
290
|
+
"grok15",
|
|
291
|
+
"veo-3.1",
|
|
292
|
+
"veo-3.1-fast",
|
|
293
|
+
"seedance2",
|
|
294
|
+
"seedance2-mini",
|
|
295
|
+
"kling-2.6",
|
|
296
|
+
"kling-3",
|
|
297
|
+
"kling-3-turbo",
|
|
298
|
+
"kling-3-omni",
|
|
299
|
+
"wan2.7",
|
|
300
|
+
]
|
|
301
|
+
VideoEditModel = Literal[
|
|
302
|
+
"seedance2",
|
|
303
|
+
"seedance2-mini",
|
|
304
|
+
"kling-3-omni",
|
|
305
|
+
"wan2.7",
|
|
306
|
+
"aleph2",
|
|
307
|
+
]
|
|
308
|
+
VideoGenerateResolution = Literal["360p", "480p", "720p", "1080p", "4k"]
|
|
309
|
+
VideoEditResolution = Literal["480p", "720p", "1080p", "4k"]
|
|
310
|
+
VideoGenerateAspectRatio = Literal[
|
|
311
|
+
"16:9",
|
|
312
|
+
"4:3",
|
|
313
|
+
"1:1",
|
|
314
|
+
"3:4",
|
|
315
|
+
"9:16",
|
|
316
|
+
"21:9",
|
|
317
|
+
]
|
|
318
|
+
VideoEditAspectRatio = Aleph2AspectRatio
|
|
319
|
+
VideoGenerateDuration = Annotated[int, Field(ge=1, le=15)]
|
|
320
|
+
VideoEditDuration = Annotated[int, Field(ge=2, le=15)]
|
|
321
|
+
PublicFigureThreshold = Literal["auto", "low"]
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class _StrictVideoPayload(BaseModel):
|
|
325
|
+
"""Reject fields that do not belong to the selected API model."""
|
|
326
|
+
|
|
327
|
+
model_config = ConfigDict(extra="forbid")
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
class WAN22VideoGenRequest(_StrictVideoPayload):
|
|
331
|
+
prompt: str
|
|
332
|
+
model: Literal["wan22"] = "wan22"
|
|
333
|
+
duration: int = Field(ge=1, le=10)
|
|
334
|
+
resolution: Literal["360p", "480p"] = "480p"
|
|
335
|
+
aspect_ratio: CommonVideoAspectRatio = "16:9"
|
|
336
|
+
seed: int = -1
|
|
337
|
+
lora_high_url: str | None = None
|
|
338
|
+
lora_low_url: str | None = None
|
|
339
|
+
callback_url: str | None = None
|
|
340
|
+
|
|
341
|
+
@model_validator(mode="after")
|
|
342
|
+
def validate_lora_urls(self) -> "WAN22VideoGenRequest":
|
|
343
|
+
if bool(self.lora_high_url) != bool(self.lora_low_url):
|
|
344
|
+
raise ValueError(
|
|
345
|
+
"Both lora_high_url and lora_low_url must be provided together"
|
|
346
|
+
)
|
|
347
|
+
return self
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class LTX23VideoGenRequest(_StrictVideoPayload):
|
|
351
|
+
prompt: str
|
|
352
|
+
model: Literal["ltx23"] = "ltx23"
|
|
353
|
+
duration: int = Field(ge=1, le=10)
|
|
354
|
+
resolution: CommonVideoResolution = "720p"
|
|
355
|
+
aspect_ratio: CommonVideoAspectRatio = "16:9"
|
|
356
|
+
image_url: str | None = None
|
|
357
|
+
image_last_url: str | None = None
|
|
358
|
+
seed: int = -1
|
|
359
|
+
callback_url: str | None = None
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
class GrokVideoGenRequest(_StrictVideoPayload):
|
|
363
|
+
prompt: str
|
|
364
|
+
model: Literal["grok"] = "grok"
|
|
365
|
+
duration: int = Field(ge=1, le=15)
|
|
366
|
+
resolution: Literal["480p", "720p"] = "480p"
|
|
367
|
+
aspect_ratio: CommonVideoAspectRatio = "16:9"
|
|
368
|
+
image_url: str | None = None
|
|
369
|
+
callback_url: str | None = None
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
class Grok15VideoGenRequest(_StrictVideoPayload):
|
|
373
|
+
prompt: str
|
|
374
|
+
model: Literal["grok15"] = "grok15"
|
|
375
|
+
duration: int = Field(ge=1, le=15)
|
|
376
|
+
resolution: Literal["480p", "720p"] = "480p"
|
|
377
|
+
aspect_ratio: CommonVideoAspectRatio = "16:9"
|
|
378
|
+
image_url: str
|
|
379
|
+
callback_url: str | None = None
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
class VeoVideoGenRequest(_StrictVideoPayload):
|
|
383
|
+
prompt: str = Field(min_length=1)
|
|
384
|
+
model: Literal["veo-3.1", "veo-3.1-fast"]
|
|
385
|
+
duration: Literal[4, 6, 8] = 8
|
|
386
|
+
resolution: VeoVideoResolution = "720p"
|
|
387
|
+
aspect_ratio: VeoVideoAspectRatio = "16:9"
|
|
388
|
+
image_url: str | None = None
|
|
389
|
+
image_last_url: str | None = None
|
|
390
|
+
reference_image_urls: list[str] = Field(default_factory=list, max_length=3)
|
|
391
|
+
callback_url: str | None = None
|
|
392
|
+
|
|
393
|
+
@model_validator(mode="after")
|
|
394
|
+
def validate_media_and_duration(self) -> "VeoVideoGenRequest":
|
|
395
|
+
for value in (self.image_url, self.image_last_url):
|
|
396
|
+
if value is not None and not value.startswith(
|
|
397
|
+
("http://", "https://", "data:image/")
|
|
398
|
+
):
|
|
399
|
+
raise ValueError(
|
|
400
|
+
"Veo frame input must be an HTTP(S) URL or image data URI"
|
|
401
|
+
)
|
|
402
|
+
for value in self.reference_image_urls:
|
|
403
|
+
if not value.startswith(("http://", "https://", "data:image/")):
|
|
404
|
+
raise ValueError(
|
|
405
|
+
"Veo reference image must be an HTTP(S) URL or image data URI"
|
|
406
|
+
)
|
|
407
|
+
if self.resolution != "720p" and self.duration != 8:
|
|
408
|
+
raise ValueError("Veo 1080p and 4k generation requires duration 8")
|
|
409
|
+
if self.image_last_url and not self.image_url:
|
|
410
|
+
raise ValueError("Veo image_last_url requires image_url")
|
|
411
|
+
if self.reference_image_urls and (self.image_url or self.image_last_url):
|
|
412
|
+
raise ValueError(
|
|
413
|
+
"Veo reference_image_urls cannot be combined with frame images"
|
|
414
|
+
)
|
|
415
|
+
if self.reference_image_urls and self.duration != 8:
|
|
416
|
+
raise ValueError("Veo reference images require duration 8")
|
|
417
|
+
return self
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
class Seedance2VideoGenRequest(_StrictVideoPayload):
|
|
421
|
+
prompt: str
|
|
422
|
+
model: Literal["seedance2", "seedance2-mini"] = "seedance2"
|
|
423
|
+
duration: int = Field(default=5, ge=4, le=15)
|
|
424
|
+
resolution: Seedance2VideoResolution = "720p"
|
|
425
|
+
aspect_ratio: Seedance2VideoAspectRatio = "16:9"
|
|
426
|
+
generate_audio: bool = True
|
|
427
|
+
callback_url: str | None = None
|
|
428
|
+
|
|
429
|
+
@model_validator(mode="after")
|
|
430
|
+
def validate_model_resolution(self) -> "Seedance2VideoGenRequest":
|
|
431
|
+
if self.model == "seedance2-mini" and self.resolution not in {
|
|
432
|
+
"480p",
|
|
433
|
+
"720p",
|
|
434
|
+
}:
|
|
435
|
+
raise ValueError("seedance2-mini supports only 480p and 720p")
|
|
436
|
+
return self
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
class KlingV26VideoGenRequest(_StrictVideoPayload):
|
|
440
|
+
prompt: str
|
|
441
|
+
model: Literal["kling-2.6"] = "kling-2.6"
|
|
442
|
+
duration: Literal[5, 10] = 5
|
|
443
|
+
resolution: Literal["720p", "1080p"] = "1080p"
|
|
444
|
+
aspect_ratio: CommonVideoAspectRatio = "16:9"
|
|
445
|
+
generate_audio: bool = True
|
|
446
|
+
callback_url: str | None = None
|
|
447
|
+
|
|
448
|
+
@model_validator(mode="after")
|
|
449
|
+
def validate_audio_resolution(self) -> "KlingV26VideoGenRequest":
|
|
450
|
+
if self.generate_audio and self.resolution != "1080p":
|
|
451
|
+
raise ValueError("kling-2.6 audio generation is supported only at 1080p")
|
|
452
|
+
return self
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
class KlingV3VideoGenRequest(_StrictVideoPayload):
|
|
456
|
+
prompt: str
|
|
457
|
+
model: Literal["kling-3"] = "kling-3"
|
|
458
|
+
duration: int = Field(default=5, ge=3, le=15)
|
|
459
|
+
resolution: Literal["720p", "1080p", "4k"] = "1080p"
|
|
460
|
+
aspect_ratio: CommonVideoAspectRatio = "16:9"
|
|
461
|
+
generate_audio: bool = True
|
|
462
|
+
callback_url: str | None = None
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
class KlingV3TurboVideoGenRequest(_StrictVideoPayload):
|
|
466
|
+
prompt: str
|
|
467
|
+
model: Literal["kling-3-turbo"] = "kling-3-turbo"
|
|
468
|
+
duration: int = Field(default=5, ge=3, le=15)
|
|
469
|
+
resolution: Literal["720p", "1080p"] = "720p"
|
|
470
|
+
aspect_ratio: CommonVideoAspectRatio = "16:9"
|
|
471
|
+
generate_audio: Literal[False] = False
|
|
472
|
+
callback_url: str | None = None
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
class KlingV3OmniVideoGenRequest(_StrictVideoPayload):
|
|
476
|
+
prompt: str
|
|
477
|
+
model: Literal["kling-3-omni"] = "kling-3-omni"
|
|
478
|
+
duration: int = Field(default=5, ge=3, le=15)
|
|
479
|
+
resolution: Literal["720p", "1080p", "4k"] = "1080p"
|
|
480
|
+
aspect_ratio: CommonVideoAspectRatio = "16:9"
|
|
481
|
+
generate_audio: bool = True
|
|
482
|
+
callback_url: str | None = None
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
class Wan27VideoGenRequest(_StrictVideoPayload):
|
|
486
|
+
prompt: str = Field(min_length=1, max_length=5000)
|
|
487
|
+
model: Literal["wan2.7"] = "wan2.7"
|
|
488
|
+
duration: int = Field(default=5, ge=2, le=15)
|
|
489
|
+
resolution: Wan27VideoResolution = "720p"
|
|
490
|
+
aspect_ratio: Wan27VideoAspectRatio = "16:9"
|
|
491
|
+
image_url: str | None = None
|
|
492
|
+
image_last_url: str | None = None
|
|
493
|
+
reference_image_urls: list[str] = Field(default_factory=list, max_length=5)
|
|
494
|
+
reference_video_urls: list[str] = Field(default_factory=list, max_length=3)
|
|
495
|
+
seed: int = Field(default=-1, ge=-1, le=2_147_483_647)
|
|
496
|
+
callback_url: str | None = None
|
|
497
|
+
|
|
498
|
+
@model_validator(mode="after")
|
|
499
|
+
def validate_media(self) -> "Wan27VideoGenRequest":
|
|
500
|
+
for value in (self.image_url, self.image_last_url):
|
|
501
|
+
if value is not None and not value.startswith(
|
|
502
|
+
("http://", "https://", "data:image/")
|
|
503
|
+
):
|
|
504
|
+
raise ValueError(
|
|
505
|
+
"Wan 2.7 frame input must be an HTTP(S) URL or image data URI"
|
|
506
|
+
)
|
|
507
|
+
for value in self.reference_image_urls:
|
|
508
|
+
if not value.startswith(("http://", "https://", "data:image/")):
|
|
509
|
+
raise ValueError(
|
|
510
|
+
"Wan 2.7 reference image must be an HTTP(S) URL or image data URI"
|
|
511
|
+
)
|
|
512
|
+
for value in self.reference_video_urls:
|
|
513
|
+
if not value.startswith(
|
|
514
|
+
("http://", "https://", "data:video/mp4;", "data:video/quicktime;")
|
|
515
|
+
):
|
|
516
|
+
raise ValueError(
|
|
517
|
+
"Wan 2.7 reference video must be an HTTP(S) URL or MP4/MOV data URI"
|
|
518
|
+
)
|
|
519
|
+
if self.image_last_url and not self.image_url:
|
|
520
|
+
raise ValueError("image_last_url requires image_url")
|
|
521
|
+
if (self.image_url or self.image_last_url) and (
|
|
522
|
+
self.reference_image_urls or self.reference_video_urls
|
|
523
|
+
):
|
|
524
|
+
raise ValueError(
|
|
525
|
+
"Wan 2.7 frame inputs cannot be combined with reference inputs"
|
|
526
|
+
)
|
|
527
|
+
reference_count = len(self.reference_image_urls) + len(
|
|
528
|
+
self.reference_video_urls
|
|
529
|
+
)
|
|
530
|
+
if reference_count > 5:
|
|
531
|
+
raise ValueError(
|
|
532
|
+
"Wan 2.7 supports at most 5 reference images and videos in total"
|
|
533
|
+
)
|
|
534
|
+
if self.reference_video_urls and self.duration > 10:
|
|
535
|
+
raise ValueError(
|
|
536
|
+
"Wan 2.7 requests with reference videos support up to 10 seconds"
|
|
537
|
+
)
|
|
538
|
+
return self
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
VideoGenerateRequest = Annotated[
|
|
542
|
+
WAN22VideoGenRequest
|
|
543
|
+
| LTX23VideoGenRequest
|
|
544
|
+
| GrokVideoGenRequest
|
|
545
|
+
| Grok15VideoGenRequest
|
|
546
|
+
| VeoVideoGenRequest
|
|
547
|
+
| Seedance2VideoGenRequest
|
|
548
|
+
| KlingV26VideoGenRequest
|
|
549
|
+
| KlingV3VideoGenRequest
|
|
550
|
+
| KlingV3TurboVideoGenRequest
|
|
551
|
+
| KlingV3OmniVideoGenRequest
|
|
552
|
+
| Wan27VideoGenRequest,
|
|
553
|
+
Discriminator("model"),
|
|
554
|
+
]
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
class VideoEditPayload(_StrictVideoPayload):
|
|
558
|
+
"""JSON request body for Seedance ``video_edit`` operations."""
|
|
559
|
+
|
|
560
|
+
prompt: str
|
|
561
|
+
model: Literal["seedance2", "seedance2-mini"] = "seedance2"
|
|
562
|
+
duration: int = Field(default=5, ge=4, le=15)
|
|
563
|
+
resolution: Literal["480p", "720p", "1080p", "4k"] = "720p"
|
|
564
|
+
aspect_ratio: Literal["16:9", "4:3", "1:1", "3:4", "9:16", "21:9"] = "16:9"
|
|
565
|
+
generate_audio: bool = True
|
|
566
|
+
callback_url: str | None = None
|
|
567
|
+
content: list[dict[str, Any]] = Field(min_length=1)
|
|
568
|
+
|
|
569
|
+
@model_validator(mode="after")
|
|
570
|
+
def validate_model_resolution(self) -> "VideoEditPayload":
|
|
571
|
+
if self.model == "seedance2-mini" and self.resolution not in {
|
|
572
|
+
"480p",
|
|
573
|
+
"720p",
|
|
574
|
+
}:
|
|
575
|
+
raise ValueError("seedance2-mini supports only 480p and 720p")
|
|
576
|
+
return self
|
|
577
|
+
|
|
578
|
+
@model_validator(mode="after")
|
|
579
|
+
def validate_content(self) -> "VideoEditPayload":
|
|
580
|
+
"""Mirror the documented structural validation before making a request."""
|
|
581
|
+
|
|
582
|
+
video_count = 0
|
|
583
|
+
has_visual = False
|
|
584
|
+
has_audio = False
|
|
585
|
+
for item in self.content:
|
|
586
|
+
item_type = item.get("type")
|
|
587
|
+
if item_type == "text":
|
|
588
|
+
if not isinstance(item.get("text"), str) or not item["text"]:
|
|
589
|
+
raise ValueError("text content item requires text")
|
|
590
|
+
continue
|
|
591
|
+
if item_type not in {"image_url", "video_url", "audio_url"}:
|
|
592
|
+
raise ValueError("content item type is not supported")
|
|
593
|
+
field = item_type
|
|
594
|
+
media = item.get(field)
|
|
595
|
+
if not isinstance(media, dict) or not isinstance(media.get("url"), str):
|
|
596
|
+
raise ValueError(f"{field} content item requires a URL")
|
|
597
|
+
url = media["url"]
|
|
598
|
+
expected_role = f"reference_{item_type.removesuffix('_url')}"
|
|
599
|
+
if item.get("role") != expected_role:
|
|
600
|
+
raise ValueError(f"{field} content item has an invalid role")
|
|
601
|
+
if item_type == "image_url" and not url.startswith(
|
|
602
|
+
("http://", "https://", "data:image/")
|
|
603
|
+
):
|
|
604
|
+
raise ValueError("image_url must be an HTTP(S) URL or image data URI")
|
|
605
|
+
if item_type == "video_url":
|
|
606
|
+
if not url.startswith(
|
|
607
|
+
("http://", "https://", "data:video/mp4;", "data:video/quicktime;")
|
|
608
|
+
):
|
|
609
|
+
raise ValueError(
|
|
610
|
+
"video_url must be an HTTP(S) URL or MP4/MOV data URI"
|
|
611
|
+
)
|
|
612
|
+
video_count += 1
|
|
613
|
+
has_visual = True
|
|
614
|
+
if item_type == "audio_url":
|
|
615
|
+
if not url.startswith(("http://", "https://", "data:audio/")):
|
|
616
|
+
raise ValueError(
|
|
617
|
+
"audio_url must be an HTTP(S) URL or audio data URI"
|
|
618
|
+
)
|
|
619
|
+
has_audio = True
|
|
620
|
+
if item_type == "image_url":
|
|
621
|
+
has_visual = True
|
|
622
|
+
if video_count > 1:
|
|
623
|
+
raise ValueError("content supports at most one video_url item")
|
|
624
|
+
if not has_visual and not has_audio:
|
|
625
|
+
raise ValueError(
|
|
626
|
+
"video_edit requires at least one image_url, video_url, "
|
|
627
|
+
"or audio_url item"
|
|
628
|
+
)
|
|
629
|
+
if has_audio and not has_visual:
|
|
630
|
+
raise ValueError(
|
|
631
|
+
"audio_url requires at least one image_url or video_url item"
|
|
632
|
+
)
|
|
633
|
+
return self
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
class KlingV3OmniVideoEditPayload(_StrictVideoPayload):
|
|
637
|
+
"""JSON request body for Kling 3 Omni video-to-video editing."""
|
|
638
|
+
|
|
639
|
+
prompt: str
|
|
640
|
+
model: Literal["kling-3-omni"] = "kling-3-omni"
|
|
641
|
+
duration: int = Field(default=5, ge=3, le=10)
|
|
642
|
+
resolution: Literal["720p", "1080p", "4k"] = "1080p"
|
|
643
|
+
aspect_ratio: CommonVideoAspectRatio = "16:9"
|
|
644
|
+
generate_audio: Literal[False] = False
|
|
645
|
+
callback_url: str | None = None
|
|
646
|
+
content: list[dict[str, Any]] = Field(min_length=1)
|
|
647
|
+
|
|
648
|
+
@model_validator(mode="after")
|
|
649
|
+
def validate_content(self) -> "KlingV3OmniVideoEditPayload":
|
|
650
|
+
"""Require the provider's single video reference and content shapes."""
|
|
651
|
+
|
|
652
|
+
video_count = 0
|
|
653
|
+
for item in self.content:
|
|
654
|
+
item_type = item.get("type")
|
|
655
|
+
if item_type == "text":
|
|
656
|
+
if not isinstance(item.get("text"), str) or not item["text"]:
|
|
657
|
+
raise ValueError("text content item requires text")
|
|
658
|
+
continue
|
|
659
|
+
if item_type not in {"image_url", "video_url"}:
|
|
660
|
+
raise ValueError("content item type is not supported")
|
|
661
|
+
media = item.get(item_type)
|
|
662
|
+
if not isinstance(media, dict) or not isinstance(media.get("url"), str):
|
|
663
|
+
raise ValueError(f"{item_type} content item requires a URL")
|
|
664
|
+
url = media["url"]
|
|
665
|
+
if item_type == "image_url" and not url.startswith(
|
|
666
|
+
("http://", "https://", "data:image/")
|
|
667
|
+
):
|
|
668
|
+
raise ValueError("image_url must be an HTTP(S) URL or image data URI")
|
|
669
|
+
if item_type == "video_url":
|
|
670
|
+
if not url.startswith(
|
|
671
|
+
("http://", "https://", "data:video/mp4;", "data:video/quicktime;")
|
|
672
|
+
):
|
|
673
|
+
raise ValueError(
|
|
674
|
+
"video_url must be an HTTP(S) URL or MP4/MOV data URI"
|
|
675
|
+
)
|
|
676
|
+
video_count += 1
|
|
677
|
+
if video_count != 1:
|
|
678
|
+
raise ValueError(
|
|
679
|
+
"kling-3-omni video edit requires exactly one video reference"
|
|
680
|
+
)
|
|
681
|
+
if self.resolution == "4k":
|
|
682
|
+
raise ValueError(
|
|
683
|
+
"kling-3-omni video edit does not support 4k with video input"
|
|
684
|
+
)
|
|
685
|
+
return self
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
class Wan27VideoEditPayload(_StrictVideoPayload):
|
|
689
|
+
"""JSON request body for Wan 2.7 video editing."""
|
|
690
|
+
|
|
691
|
+
prompt: str = Field(min_length=1, max_length=5000)
|
|
692
|
+
model: Literal["wan2.7"] = "wan2.7"
|
|
693
|
+
duration: int = Field(default=5, ge=2, le=10)
|
|
694
|
+
resolution: Wan27VideoResolution = "720p"
|
|
695
|
+
aspect_ratio: Wan27VideoAspectRatio = "16:9"
|
|
696
|
+
video_url: str
|
|
697
|
+
reference_image_urls: list[str] = Field(default_factory=list, max_length=3)
|
|
698
|
+
seed: int = Field(default=-1, ge=-1, le=2_147_483_647)
|
|
699
|
+
callback_url: str | None = None
|
|
700
|
+
|
|
701
|
+
@model_validator(mode="after")
|
|
702
|
+
def validate_media(self) -> "Wan27VideoEditPayload":
|
|
703
|
+
if not self.video_url.startswith(
|
|
704
|
+
("http://", "https://", "data:video/mp4;", "data:video/quicktime;")
|
|
705
|
+
):
|
|
706
|
+
raise ValueError("Wan 2.7 video must be an HTTP(S) URL or MP4/MOV data URI")
|
|
707
|
+
for value in self.reference_image_urls:
|
|
708
|
+
if not value.startswith(("http://", "https://", "data:image/")):
|
|
709
|
+
raise ValueError(
|
|
710
|
+
"Wan 2.7 reference image must be an HTTP(S) URL or image data URI"
|
|
711
|
+
)
|
|
712
|
+
return self
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
class Aleph2KeyframeRange(BaseModel):
|
|
716
|
+
start_seconds: int = Field(ge=0)
|
|
717
|
+
end_seconds: int = Field(gt=0)
|
|
718
|
+
|
|
719
|
+
@model_validator(mode="after")
|
|
720
|
+
def validate_range(self) -> "Aleph2KeyframeRange":
|
|
721
|
+
if self.end_seconds <= self.start_seconds:
|
|
722
|
+
raise ValueError("end_seconds must be greater than start_seconds")
|
|
723
|
+
return self
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
class Aleph2Keyframe(BaseModel):
|
|
727
|
+
image_url: str
|
|
728
|
+
seconds: float | None = Field(default=None, ge=0, le=30)
|
|
729
|
+
at: float | None = Field(default=None, ge=0, le=1)
|
|
730
|
+
range: Aleph2KeyframeRange | None = None
|
|
731
|
+
|
|
732
|
+
@model_validator(mode="after")
|
|
733
|
+
def validate_keyframe(self) -> "Aleph2Keyframe":
|
|
734
|
+
if not self.image_url.startswith(("https://", "data:image/")):
|
|
735
|
+
raise ValueError(
|
|
736
|
+
"Aleph 2 keyframe image_url must use HTTPS or an image data URI"
|
|
737
|
+
)
|
|
738
|
+
if self.image_url.startswith("data:") and len(self.image_url) > 5 * 1024 * 1024:
|
|
739
|
+
raise ValueError("Aleph 2 keyframe data URI exceeds 5 MB")
|
|
740
|
+
if len(self.image_url) > 2048 and not self.image_url.startswith("data:"):
|
|
741
|
+
raise ValueError("Aleph 2 keyframe image_url exceeds 2048 characters")
|
|
742
|
+
if (self.seconds is None) == (self.at is None):
|
|
743
|
+
raise ValueError("Aleph 2 keyframe requires exactly one of seconds or at")
|
|
744
|
+
return self
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
class Aleph2VideoEditPayload(_StrictVideoPayload):
|
|
748
|
+
model: Literal["aleph2"] = "aleph2"
|
|
749
|
+
video_url: str
|
|
750
|
+
prompt: str | None = Field(default=None, min_length=1)
|
|
751
|
+
keyframes: list[Aleph2Keyframe] = Field(default_factory=list, max_length=5)
|
|
752
|
+
seed: int = Field(default=-1, ge=-1, le=4_294_967_295)
|
|
753
|
+
target_aspect_ratio: Aleph2AspectRatio | None = None
|
|
754
|
+
public_figure_threshold: PublicFigureThreshold | None = None
|
|
755
|
+
callback_url: str | None = None
|
|
756
|
+
|
|
757
|
+
@model_validator(mode="after")
|
|
758
|
+
def validate_request(self) -> "Aleph2VideoEditPayload":
|
|
759
|
+
if not self.video_url.startswith(
|
|
760
|
+
("http://", "https://", "data:video/mp4;", "data:video/quicktime;")
|
|
761
|
+
):
|
|
762
|
+
raise ValueError("Aleph 2 video must be an HTTP(S) URL or MP4/MOV data URI")
|
|
763
|
+
if self.prompt is not None:
|
|
764
|
+
code_units = len(self.prompt.encode("utf-16-le")) // 2
|
|
765
|
+
if code_units > 1000:
|
|
766
|
+
raise ValueError(
|
|
767
|
+
"Aleph 2 prompt must not exceed 1000 UTF-16 code units"
|
|
768
|
+
)
|
|
769
|
+
ranged = [keyframe.range is not None for keyframe in self.keyframes]
|
|
770
|
+
if ranged and any(ranged) and not all(ranged):
|
|
771
|
+
raise ValueError("All Aleph 2 keyframes must either set range or omit it")
|
|
772
|
+
return self
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
VideoEditRequest = Annotated[
|
|
776
|
+
VideoEditPayload
|
|
777
|
+
| KlingV3OmniVideoEditPayload
|
|
778
|
+
| Wan27VideoEditPayload
|
|
779
|
+
| Aleph2VideoEditPayload,
|
|
780
|
+
Discriminator("model"),
|
|
781
|
+
]
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
class ImageUpscalePayload(BaseModel):
|
|
785
|
+
"""Payload for image upscale from URL or another task result."""
|
|
786
|
+
|
|
787
|
+
image_url: str | None = None
|
|
788
|
+
image_from_task_id: str | None = None
|
|
789
|
+
model: str = "seedvr2"
|
|
790
|
+
callback_url: str | None = None
|
|
791
|
+
|
|
792
|
+
@model_validator(mode="after")
|
|
793
|
+
def validate_source(self) -> "ImageUpscalePayload":
|
|
794
|
+
"""Require exactly one source: --image or --task-id."""
|
|
795
|
+
|
|
796
|
+
if bool(self.image_url) == bool(self.image_from_task_id):
|
|
797
|
+
raise ValueError("Provide exactly one of --image or --task-id")
|
|
798
|
+
return self
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
VideoUpscaleResolution = Literal["720p", "1080p", "1440p"]
|
|
802
|
+
LipsyncVideoResolution = Literal["360p", "480p"]
|
|
803
|
+
LipsyncImageModel = Literal["inftalk", "ltx23"]
|
|
804
|
+
LipsyncImageResolution = Literal["360p", "480p", "720p"]
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
class VideoUpscalePayload(BaseModel):
|
|
808
|
+
"""Payload for video upscale from URL or another task result."""
|
|
809
|
+
|
|
810
|
+
video_url: str | None = None
|
|
811
|
+
video_from_task_id: str | None = None
|
|
812
|
+
resolution: VideoUpscaleResolution = Field(
|
|
813
|
+
default="1080p",
|
|
814
|
+
description="Output resolution; 1440p accepts videos up to 20 seconds.",
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
@model_validator(mode="after")
|
|
818
|
+
def validate_source(self) -> "VideoUpscalePayload":
|
|
819
|
+
"""Require exactly one source: --video or --task-id."""
|
|
820
|
+
|
|
821
|
+
if bool(self.video_url) == bool(self.video_from_task_id):
|
|
822
|
+
raise ValueError("Provide exactly one of --video or --task-id")
|
|
823
|
+
return self
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
class LipsyncVideoPayload(BaseModel):
|
|
827
|
+
"""Payload for lipsync from source video and audio."""
|
|
828
|
+
|
|
829
|
+
video_url: str
|
|
830
|
+
audio_url: str
|
|
831
|
+
resolution: LipsyncVideoResolution = "480p"
|
|
832
|
+
prompt: str | None = None
|
|
833
|
+
seed: int = -1
|
|
834
|
+
callback_url: str | None = None
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
class LipsyncImagePayload(BaseModel):
|
|
838
|
+
"""Payload for lipsync from a still image and audio."""
|
|
839
|
+
|
|
840
|
+
image_url: str
|
|
841
|
+
audio_url: str
|
|
842
|
+
model: LipsyncImageModel = "inftalk"
|
|
843
|
+
resolution: LipsyncImageResolution = "480p"
|
|
844
|
+
prompt: str | None = None
|
|
845
|
+
seed: int = -1
|
|
846
|
+
callback_url: str | None = None
|