machgen-client 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.
- machgen/client/__init__.py +36 -0
- machgen/client/_models.py +138 -0
- machgen/client/_sse_executor.py +82 -0
- machgen/client/api.py +203 -0
- machgen/client/client.py +351 -0
- machgen/client/task_handle.py +274 -0
- machgen_client-0.1.0.dist-info/METADATA +74 -0
- machgen_client-0.1.0.dist-info/RECORD +11 -0
- machgen_client-0.1.0.dist-info/WHEEL +5 -0
- machgen_client-0.1.0.dist-info/licenses/LICENSE +201 -0
- machgen_client-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from machgen.client._models import (
|
|
2
|
+
GenerateResponse,
|
|
3
|
+
ModerationResult,
|
|
4
|
+
ModerationStage,
|
|
5
|
+
TaskMetadata,
|
|
6
|
+
TaskOutputType,
|
|
7
|
+
TaskStatusResponse,
|
|
8
|
+
UploadResponse,
|
|
9
|
+
)
|
|
10
|
+
from machgen.client.api import (
|
|
11
|
+
ImageConfig,
|
|
12
|
+
TaskInput,
|
|
13
|
+
TaskStatus,
|
|
14
|
+
TaskUpdate,
|
|
15
|
+
VideoConfig,
|
|
16
|
+
)
|
|
17
|
+
from machgen.client.client import MachGenClient, SseRetryConfig
|
|
18
|
+
from machgen.client.task_handle import TaskHandle
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"GenerateResponse",
|
|
22
|
+
"ImageConfig",
|
|
23
|
+
"MachGenClient",
|
|
24
|
+
"ModerationResult",
|
|
25
|
+
"ModerationStage",
|
|
26
|
+
"SseRetryConfig",
|
|
27
|
+
"TaskHandle",
|
|
28
|
+
"TaskInput",
|
|
29
|
+
"TaskMetadata",
|
|
30
|
+
"TaskOutputType",
|
|
31
|
+
"TaskStatus",
|
|
32
|
+
"TaskStatusResponse",
|
|
33
|
+
"TaskUpdate",
|
|
34
|
+
"UploadResponse",
|
|
35
|
+
"VideoConfig",
|
|
36
|
+
]
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Wire-level response models shared between client and server.
|
|
2
|
+
|
|
3
|
+
The server ([frontend/api_server.py](frontend/api_server.py)) and the external
|
|
4
|
+
client both import these so the JSON schema stays in lock-step.
|
|
5
|
+
|
|
6
|
+
**Schema evolution policy.** Every model here sets ``extra="ignore"`` so an
|
|
7
|
+
older peer (client *or* server) silently drops fields it doesn't know about.
|
|
8
|
+
That means new fields can be added without coordinating a release across both
|
|
9
|
+
sides — but it requires:
|
|
10
|
+
|
|
11
|
+
- New fields are always **optional** (carry a default).
|
|
12
|
+
- Existing required fields are **never removed or renamed**.
|
|
13
|
+
- Enum members are **never removed** (additions are tolerated by the receiver
|
|
14
|
+
only if the receiver doesn't dispatch on the enum value).
|
|
15
|
+
|
|
16
|
+
These rules are enforced by reviewer discipline, not by code; the test
|
|
17
|
+
[tests/api/test_wire_compat.py](tests/api/test_wire_compat.py) pins the
|
|
18
|
+
``extra="ignore"`` behavior so a regression flips a red light.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from enum import StrEnum
|
|
24
|
+
|
|
25
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
26
|
+
|
|
27
|
+
from machgen.client.api import TaskStatus
|
|
28
|
+
|
|
29
|
+
_WIRE_MODEL_CONFIG = ConfigDict(extra="ignore")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TaskMetadata(BaseModel):
|
|
33
|
+
model_config = _WIRE_MODEL_CONFIG
|
|
34
|
+
|
|
35
|
+
prompt: str = Field(description="Prompt the task was submitted with.")
|
|
36
|
+
model: str | None = Field(default=None, description="Resolved model id.")
|
|
37
|
+
task_type: str | None = Field(default=None, description="Resolved task type.")
|
|
38
|
+
seed: int | None = Field(default=None, description="Seed used for generation.")
|
|
39
|
+
fps: int | None = Field(default=None, description="Frames per second (video).")
|
|
40
|
+
height: int | None = Field(default=None, description="Output height in pixels.")
|
|
41
|
+
width: int | None = Field(default=None, description="Output width in pixels.")
|
|
42
|
+
aspect_ratio: str | None = Field(default=None, description="Output aspect ratio.")
|
|
43
|
+
duration_secs: int | None = Field(
|
|
44
|
+
default=None, description="Clip duration in seconds (video)."
|
|
45
|
+
)
|
|
46
|
+
src_image_urls: list[str] | None = Field(
|
|
47
|
+
default=None, description="Source / reference image refs."
|
|
48
|
+
)
|
|
49
|
+
src_video_url: str | None = Field(
|
|
50
|
+
default=None, description="Source video ref (video-to-video)."
|
|
51
|
+
)
|
|
52
|
+
acceleration: str | None = Field(
|
|
53
|
+
default=None, description="Acceleration profile applied by the backend."
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TaskOutputType(StrEnum):
|
|
58
|
+
"""A task may have 1 or more types of output."""
|
|
59
|
+
|
|
60
|
+
TEXT = "text"
|
|
61
|
+
VIDEO = "video"
|
|
62
|
+
IMAGE = "image"
|
|
63
|
+
AUDIO = "audio"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ModerationStage(StrEnum):
|
|
67
|
+
INPUT = "input"
|
|
68
|
+
OUTPUT = "output"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ModerationResult(BaseModel):
|
|
72
|
+
model_config = _WIRE_MODEL_CONFIG
|
|
73
|
+
|
|
74
|
+
flagged: bool = Field(
|
|
75
|
+
default=False, description="Whether moderation matched a block category."
|
|
76
|
+
)
|
|
77
|
+
stage: ModerationStage | None = Field(
|
|
78
|
+
default=None, description="Which stage flagged (input or output)."
|
|
79
|
+
)
|
|
80
|
+
surface: str | None = Field(
|
|
81
|
+
default=None, description='Surface that tripped: "text" or "image".'
|
|
82
|
+
)
|
|
83
|
+
categories: list[str] = Field(
|
|
84
|
+
default_factory=list, description="Matched block categories."
|
|
85
|
+
)
|
|
86
|
+
source: str | None = Field(
|
|
87
|
+
default=None,
|
|
88
|
+
description='Who produced the verdict: "machgen" (OpenAI) or "vendor".',
|
|
89
|
+
)
|
|
90
|
+
moderation_time_secs: float | None = Field(
|
|
91
|
+
default=None, description="Wall-clock time spent in the moderation call(s)."
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class GenerateResponse(BaseModel):
|
|
96
|
+
model_config = _WIRE_MODEL_CONFIG
|
|
97
|
+
|
|
98
|
+
task_id: str = Field(
|
|
99
|
+
description="Server-assigned id; poll status and download the asset with it."
|
|
100
|
+
)
|
|
101
|
+
metadata: TaskMetadata = Field(description="Echo of the resolved task parameters.")
|
|
102
|
+
task_output: dict[TaskOutputType, str] | None = Field(
|
|
103
|
+
default=None,
|
|
104
|
+
description="Per output-type asset locators, as they become available.",
|
|
105
|
+
)
|
|
106
|
+
moderation: ModerationResult | None = Field(
|
|
107
|
+
default=None,
|
|
108
|
+
description="Set when moderation rejected the request at admission. The task was admitted then immediately failed (refunded); no generation work was done.",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class TaskStatusResponse(GenerateResponse):
|
|
113
|
+
status: TaskStatus = Field(description="Current task status.")
|
|
114
|
+
error_msg: str | None = Field(
|
|
115
|
+
default=None, description="Failure reason, set when status is FAILED."
|
|
116
|
+
)
|
|
117
|
+
generation_time_secs: float | None = Field(
|
|
118
|
+
default=None, description="Time spent on actual generation."
|
|
119
|
+
)
|
|
120
|
+
upload_time_secs: float | None = Field(
|
|
121
|
+
default=None, description="Time spent uploading the asset after generation."
|
|
122
|
+
)
|
|
123
|
+
queue_time_secs: float | None = Field(
|
|
124
|
+
default=None,
|
|
125
|
+
description="Time spent waiting in the queue for generation to begin.",
|
|
126
|
+
)
|
|
127
|
+
moderation: ModerationResult | None = Field(
|
|
128
|
+
default=None,
|
|
129
|
+
description="Structured moderation verdict when the task failed or was blocked by moderation.",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class UploadResponse(BaseModel):
|
|
134
|
+
model_config = _WIRE_MODEL_CONFIG
|
|
135
|
+
|
|
136
|
+
artifact_path: str = Field(
|
|
137
|
+
description="Storage path of the uploaded input; reference it as @input/<artifact_path>."
|
|
138
|
+
)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Process-global thread pool for ``TaskHandle`` SSE consumers.
|
|
2
|
+
|
|
3
|
+
All handles across all ``MachGenClient`` instances submit their stream
|
|
4
|
+
consumption work to one ``ThreadPoolExecutor`` so the per-process worker
|
|
5
|
+
count stays bounded — the alternative (one ad-hoc daemon thread per
|
|
6
|
+
handle) blows up when callers spin up many clients with many handles.
|
|
7
|
+
|
|
8
|
+
Cap: ``MACHGEN_CLIENT_MAX_SSE_WORKERS`` env var (default 256). The pool
|
|
9
|
+
grows lazily up to that cap.
|
|
10
|
+
|
|
11
|
+
Atexit handling: at program shutdown we signal stop on every live stream
|
|
12
|
+
state before the pool's own atexit hook fires, so workers blocked on
|
|
13
|
+
``iter_sse()`` are unstuck and the pool's join doesn't hang. Atexit is
|
|
14
|
+
LIFO and the pool's hook is registered the first time a
|
|
15
|
+
``ThreadPoolExecutor`` is created — we register ours after that, so ours
|
|
16
|
+
runs first.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import atexit
|
|
22
|
+
import logging
|
|
23
|
+
import os
|
|
24
|
+
import threading
|
|
25
|
+
import weakref
|
|
26
|
+
from concurrent.futures import Future, ThreadPoolExecutor
|
|
27
|
+
from typing import TYPE_CHECKING, Callable
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from machgen.client.task_handle import _StreamState
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
_DEFAULT_MAX_WORKERS = int(os.environ.get("MACHGEN_CLIENT_MAX_SSE_WORKERS", "256"))
|
|
35
|
+
|
|
36
|
+
_executor: ThreadPoolExecutor | None = None
|
|
37
|
+
_executor_lock = threading.Lock()
|
|
38
|
+
_active_states: "weakref.WeakSet[_StreamState]" = weakref.WeakSet()
|
|
39
|
+
_active_lock = threading.Lock()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _ensure_executor() -> ThreadPoolExecutor:
|
|
43
|
+
global _executor
|
|
44
|
+
if _executor is not None:
|
|
45
|
+
return _executor
|
|
46
|
+
with _executor_lock:
|
|
47
|
+
if _executor is None:
|
|
48
|
+
_executor = ThreadPoolExecutor(
|
|
49
|
+
max_workers=_DEFAULT_MAX_WORKERS,
|
|
50
|
+
thread_name_prefix="machgen-sse",
|
|
51
|
+
)
|
|
52
|
+
atexit.register(_atexit_shutdown)
|
|
53
|
+
return _executor
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def submit(fn: Callable[..., None], *args, **kwargs) -> Future:
|
|
57
|
+
"""Submit an SSE consumer function to the shared pool."""
|
|
58
|
+
return _ensure_executor().submit(fn, *args, **kwargs)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def register_state(state: "_StreamState") -> None:
|
|
62
|
+
"""Track *state* so atexit can stop its worker before pool shutdown."""
|
|
63
|
+
with _active_lock:
|
|
64
|
+
_active_states.add(state)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _atexit_shutdown() -> None:
|
|
68
|
+
"""Signal stop on every live state, then shut down the pool.
|
|
69
|
+
|
|
70
|
+
Runs before ``concurrent.futures.thread._python_exit`` (LIFO atexit
|
|
71
|
+
order). ``signal_stop`` wakes any worker blocked on ``iter_sse()``;
|
|
72
|
+
the pool's subsequent join then proceeds promptly.
|
|
73
|
+
"""
|
|
74
|
+
with _active_lock:
|
|
75
|
+
states = list(_active_states)
|
|
76
|
+
for state in states:
|
|
77
|
+
try:
|
|
78
|
+
state.signal_stop(terminal_message="process exiting")
|
|
79
|
+
except Exception:
|
|
80
|
+
logger.warning("error stopping SSE worker at exit", exc_info=True)
|
|
81
|
+
if _executor is not None:
|
|
82
|
+
_executor.shutdown(wait=False)
|
machgen/client/api.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
6
|
+
|
|
7
|
+
_WIRE_MODEL_CONFIG = ConfigDict(extra="ignore", frozen=True)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TaskStatus(StrEnum):
|
|
11
|
+
PENDING = "PENDING"
|
|
12
|
+
RUNNING = "RUNNING"
|
|
13
|
+
# Generation + upload both done; task_output is populated. Terminal.
|
|
14
|
+
COMPLETED = "COMPLETED"
|
|
15
|
+
FAILED = "FAILED"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class VideoConfig(BaseModel):
|
|
19
|
+
model_config = _WIRE_MODEL_CONFIG
|
|
20
|
+
|
|
21
|
+
fps: int | None = Field(
|
|
22
|
+
default=None,
|
|
23
|
+
description="Video frames per second. If omitted the default FPS would be used based on the model.",
|
|
24
|
+
)
|
|
25
|
+
duration_secs: int = Field(
|
|
26
|
+
description="Video duration in seconds.",
|
|
27
|
+
)
|
|
28
|
+
height: int | None = Field(
|
|
29
|
+
default=None,
|
|
30
|
+
description="Output height in pixels",
|
|
31
|
+
)
|
|
32
|
+
width: int | None = Field(
|
|
33
|
+
default=None,
|
|
34
|
+
description="Output width in pixels.",
|
|
35
|
+
)
|
|
36
|
+
aspect_ratio: str | None = Field(
|
|
37
|
+
default=None,
|
|
38
|
+
description=(
|
|
39
|
+
"Output aspect ratio. "
|
|
40
|
+
"The default is 16:9 if omitted. "
|
|
41
|
+
"The width of the output will be updated to match the height based on the aspect ratio, "
|
|
42
|
+
"rounded up to the nearest integer. "
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
infer_steps: int | None = Field(
|
|
46
|
+
default=None,
|
|
47
|
+
description=(
|
|
48
|
+
"Number of denoising / inference steps. Higher values trade more "
|
|
49
|
+
"compute for potentially finer detail. This is a best-effort match: "
|
|
50
|
+
"if the model does not support it the model default "
|
|
51
|
+
"is used. "
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
audio: bool | None = Field(
|
|
55
|
+
default=None,
|
|
56
|
+
description=(
|
|
57
|
+
"Whether the video should include audio. "
|
|
58
|
+
"**Note:** some models do no support audio, "
|
|
59
|
+
"or the audio is always on (e.g. Veo 3.1). "
|
|
60
|
+
"In those cases this field has no effect."
|
|
61
|
+
),
|
|
62
|
+
)
|
|
63
|
+
guidance_scale: list[float] | None = Field(
|
|
64
|
+
default=None,
|
|
65
|
+
description=(
|
|
66
|
+
"Classifier-free guidance scale(s). Controls how strongly the output "
|
|
67
|
+
"adheres to the prompt: higher values follow the prompt more closely "
|
|
68
|
+
"at the cost of diversity. If the model supports multiple guidance "
|
|
69
|
+
"scales, these will be applied in a sequence (e.g. per stage or per "
|
|
70
|
+
"denoising phase). This is a best-effort match: if the model does "
|
|
71
|
+
"not support it, or does not support the number of scales provided, "
|
|
72
|
+
"the model default is used. "
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Accept the legacy scalar shape from tasks stored before guidance_scale
|
|
77
|
+
# became a list, so old rows keep loading after the schema change.
|
|
78
|
+
@field_validator("guidance_scale", mode="before")
|
|
79
|
+
@classmethod
|
|
80
|
+
def _coerce_scalar_guidance_scale(cls, v: object) -> object:
|
|
81
|
+
if isinstance(v, (int, float)) and not isinstance(v, bool):
|
|
82
|
+
return [float(v)]
|
|
83
|
+
return v
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class ImageConfig(BaseModel):
|
|
87
|
+
model_config = _WIRE_MODEL_CONFIG
|
|
88
|
+
|
|
89
|
+
height: int | None = Field(
|
|
90
|
+
default=None,
|
|
91
|
+
description="Output height in pixels",
|
|
92
|
+
)
|
|
93
|
+
width: int | None = Field(
|
|
94
|
+
default=None,
|
|
95
|
+
description="Output width in pixels.",
|
|
96
|
+
)
|
|
97
|
+
aspect_ratio: str | None = Field(
|
|
98
|
+
default=None,
|
|
99
|
+
description=(
|
|
100
|
+
"Output aspect ratio. "
|
|
101
|
+
"The default is 1:1 if omitted. "
|
|
102
|
+
"The width of the output will be updated to match the height based on the aspect ratio, "
|
|
103
|
+
"rounded up to the nearest integer. "
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
infer_steps: int | None = Field(
|
|
107
|
+
default=None,
|
|
108
|
+
description=(
|
|
109
|
+
"Number of denoising / inference steps. Higher values trade more "
|
|
110
|
+
"compute for potentially finer detail. This is a best-effort match: "
|
|
111
|
+
"if the model does not support it the model default "
|
|
112
|
+
"is used. "
|
|
113
|
+
),
|
|
114
|
+
)
|
|
115
|
+
guidance_scale: list[float] | None = Field(
|
|
116
|
+
default=None,
|
|
117
|
+
description=(
|
|
118
|
+
"Classifier-free guidance scale(s). Controls how strongly the output "
|
|
119
|
+
"adheres to the prompt: higher values follow the prompt more closely "
|
|
120
|
+
"at the cost of diversity. If the model supports multiple guidance "
|
|
121
|
+
"scales, these will be applied in a sequence (e.g. per stage or per "
|
|
122
|
+
"denoising phase). This is a best-effort match: if the model does "
|
|
123
|
+
"not support it, or does not support the number of scales provided, "
|
|
124
|
+
"the model default is used. "
|
|
125
|
+
),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Accept the legacy scalar shape from tasks stored before guidance_scale
|
|
129
|
+
# became a list, so old rows keep loading after the schema change.
|
|
130
|
+
@field_validator("guidance_scale", mode="before")
|
|
131
|
+
@classmethod
|
|
132
|
+
def _coerce_scalar_guidance_scale(cls, v: object) -> object:
|
|
133
|
+
if isinstance(v, (int, float)) and not isinstance(v, bool):
|
|
134
|
+
return [float(v)]
|
|
135
|
+
return v
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class TaskUpdate(BaseModel):
|
|
139
|
+
model_config = _WIRE_MODEL_CONFIG
|
|
140
|
+
|
|
141
|
+
status: TaskStatus
|
|
142
|
+
progress: float | None = None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class TaskInput(BaseModel):
|
|
146
|
+
"""
|
|
147
|
+
Public input model for :meth:`machgen.client.MachGenClient.submit_task`.
|
|
148
|
+
|
|
149
|
+
Model/task_type need to match the supported model list.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
model_config = _WIRE_MODEL_CONFIG
|
|
153
|
+
|
|
154
|
+
prompt: str = Field(description="Text prompt driving generation.")
|
|
155
|
+
multi_prompt: list[str] | None = Field(
|
|
156
|
+
default=None,
|
|
157
|
+
description=(
|
|
158
|
+
"Per-shot prompts for multi-shot passthrough vendors; length must "
|
|
159
|
+
"match src_image_urls when used as reference frames. "
|
|
160
|
+
"**This is only supported/needed for Kling-v3 R2V.**"
|
|
161
|
+
),
|
|
162
|
+
)
|
|
163
|
+
model: str = Field(description="Model id, e.g. 'Wan2.2-T2V-A14B', 'Kling-v3'.")
|
|
164
|
+
task_type: str = Field(description="one of T2I, I2I, T2V, I2V, R2V")
|
|
165
|
+
moderate: bool = Field(
|
|
166
|
+
default=True,
|
|
167
|
+
description=(
|
|
168
|
+
"Whether this request is screened by content moderation (the prompt "
|
|
169
|
+
"and any reference images, plus the generated image). Defaults to "
|
|
170
|
+
"true and most callers should leave it so. Setting it false is a "
|
|
171
|
+
"privileged option: it is honored only for accounts explicitly "
|
|
172
|
+
"allowlisted to bypass moderation; from any other account the "
|
|
173
|
+
"request is rejected with HTTP 403."
|
|
174
|
+
),
|
|
175
|
+
)
|
|
176
|
+
video_config: VideoConfig | None = Field(
|
|
177
|
+
default=None, description="Required for video task types."
|
|
178
|
+
)
|
|
179
|
+
image_config: ImageConfig | None = Field(
|
|
180
|
+
default=None, description="Required for image task types."
|
|
181
|
+
)
|
|
182
|
+
seed: int | None = Field(
|
|
183
|
+
default=None,
|
|
184
|
+
description="Seed for reproducible generation. If not specified, a random seed will be used.",
|
|
185
|
+
)
|
|
186
|
+
src_image_urls: list[str] | None = Field(
|
|
187
|
+
default=None,
|
|
188
|
+
description=(
|
|
189
|
+
"Source / reference image URLs. "
|
|
190
|
+
"Only needed for tasks that require input images like I2I, I2V, R2V "
|
|
191
|
+
"Refer to the API docs for concrete examples how to use this and what inputs are allowed. "
|
|
192
|
+
"For image to video tasks, this can optionally specify 1 (first frame) or 2 (first & last frames) input images. "
|
|
193
|
+
),
|
|
194
|
+
)
|
|
195
|
+
subject_to_image_ids: dict[str, list[int]] | None = Field(
|
|
196
|
+
default=None,
|
|
197
|
+
description=(
|
|
198
|
+
"R2V only: maps a subject name to the indices of its reference "
|
|
199
|
+
"images in src_image_urls, e.g. {'alice': [0, 1], 'bob': [2]}. The "
|
|
200
|
+
"prompt may address a subject via '@name'. Honored by vendors with "
|
|
201
|
+
"named subjects (Vidu reference2video)."
|
|
202
|
+
),
|
|
203
|
+
)
|