@weirdfingers/baseboards 0.2.1 → 0.4.0
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.
- package/README.md +14 -4
- package/dist/index.js +13 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/api/ARTIFACT_RESOLUTION_GUIDE.md +148 -0
- package/templates/api/Dockerfile +2 -2
- package/templates/api/README.md +138 -6
- package/templates/api/config/generators.yaml +41 -7
- package/templates/api/docs/TESTING_LIVE_APIS.md +417 -0
- package/templates/api/pyproject.toml +49 -9
- package/templates/api/src/boards/__init__.py +1 -1
- package/templates/api/src/boards/auth/adapters/__init__.py +9 -2
- package/templates/api/src/boards/auth/factory.py +16 -2
- package/templates/api/src/boards/generators/__init__.py +2 -2
- package/templates/api/src/boards/generators/artifact_resolution.py +372 -0
- package/templates/api/src/boards/generators/artifacts.py +4 -4
- package/templates/api/src/boards/generators/base.py +8 -4
- package/templates/api/src/boards/generators/implementations/__init__.py +4 -2
- package/templates/api/src/boards/generators/implementations/fal/__init__.py +25 -0
- package/templates/api/src/boards/generators/implementations/fal/audio/__init__.py +4 -0
- package/templates/api/src/boards/generators/implementations/fal/audio/minimax_music_v2.py +173 -0
- package/templates/api/src/boards/generators/implementations/fal/audio/minimax_speech_2_6_turbo.py +221 -0
- package/templates/api/src/boards/generators/implementations/fal/image/__init__.py +17 -0
- package/templates/api/src/boards/generators/implementations/fal/image/flux_pro_kontext.py +216 -0
- package/templates/api/src/boards/generators/implementations/fal/image/flux_pro_ultra.py +197 -0
- package/templates/api/src/boards/generators/implementations/fal/image/imagen4_preview.py +191 -0
- package/templates/api/src/boards/generators/implementations/fal/image/imagen4_preview_fast.py +179 -0
- package/templates/api/src/boards/generators/implementations/fal/image/nano_banana.py +183 -0
- package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_edit.py +212 -0
- package/templates/api/src/boards/generators/implementations/fal/utils.py +61 -0
- package/templates/api/src/boards/generators/implementations/fal/video/__init__.py +13 -0
- package/templates/api/src/boards/generators/implementations/fal/video/kling_video_v2_5_turbo_pro_text_to_video.py +168 -0
- package/templates/api/src/boards/generators/implementations/fal/video/sync_lipsync_v2.py +167 -0
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_first_last_frame_to_video.py +180 -0
- package/templates/api/src/boards/generators/implementations/openai/__init__.py +1 -0
- package/templates/api/src/boards/generators/implementations/openai/audio/__init__.py +1 -0
- package/templates/api/src/boards/generators/implementations/{audio → openai/audio}/whisper.py +9 -6
- package/templates/api/src/boards/generators/implementations/openai/image/__init__.py +1 -0
- package/templates/api/src/boards/generators/implementations/{image → openai/image}/dalle3.py +8 -5
- package/templates/api/src/boards/generators/implementations/replicate/__init__.py +1 -0
- package/templates/api/src/boards/generators/implementations/replicate/image/__init__.py +1 -0
- package/templates/api/src/boards/generators/implementations/{image → replicate/image}/flux_pro.py +8 -5
- package/templates/api/src/boards/generators/implementations/replicate/video/__init__.py +1 -0
- package/templates/api/src/boards/generators/implementations/{video → replicate/video}/lipsync.py +9 -6
- package/templates/api/src/boards/generators/resolution.py +80 -20
- package/templates/api/src/boards/jobs/repository.py +49 -0
- package/templates/api/src/boards/storage/factory.py +16 -6
- package/templates/api/src/boards/workers/actors.py +69 -5
- package/templates/api/src/boards/workers/context.py +177 -21
- package/templates/web/package.json +2 -1
- package/templates/web/src/components/boards/GenerationInput.tsx +154 -52
- package/templates/web/src/components/boards/GeneratorSelector.tsx +57 -59
- package/templates/web/src/components/ui/dropdown-menu.tsx +200 -0
- package/templates/api/src/boards/generators/implementations/audio/__init__.py +0 -3
- package/templates/api/src/boards/generators/implementations/image/__init__.py +0 -3
- package/templates/api/src/boards/generators/implementations/video/__init__.py +0 -3
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""
|
|
2
|
+
fal.ai FLUX1.1 [pro] ultra text-to-image generator.
|
|
3
|
+
|
|
4
|
+
High-quality image generation using fal.ai's FLUX1.1 [pro] ultra model with support for
|
|
5
|
+
batch outputs and advanced controls.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from typing import Literal
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
from ....base import BaseGenerator, GeneratorExecutionContext, GeneratorResult
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FluxProUltraInput(BaseModel):
|
|
17
|
+
"""Input schema for FLUX1.1 [pro] ultra image generation."""
|
|
18
|
+
|
|
19
|
+
prompt: str = Field(description="Text prompt for image generation")
|
|
20
|
+
aspect_ratio: Literal[
|
|
21
|
+
"21:9",
|
|
22
|
+
"16:9",
|
|
23
|
+
"4:3",
|
|
24
|
+
"3:2",
|
|
25
|
+
"1:1",
|
|
26
|
+
"2:3",
|
|
27
|
+
"3:4",
|
|
28
|
+
"9:16",
|
|
29
|
+
"9:21",
|
|
30
|
+
] = Field(
|
|
31
|
+
default="16:9",
|
|
32
|
+
description="Image aspect ratio",
|
|
33
|
+
)
|
|
34
|
+
num_images: int = Field(
|
|
35
|
+
default=1,
|
|
36
|
+
ge=1,
|
|
37
|
+
le=4,
|
|
38
|
+
description="Number of images to generate in batch (max 4)",
|
|
39
|
+
)
|
|
40
|
+
enable_safety_checker: bool = Field(
|
|
41
|
+
default=True,
|
|
42
|
+
description="Enable safety checker to filter unsafe content",
|
|
43
|
+
)
|
|
44
|
+
safety_tolerance: int = Field(
|
|
45
|
+
default=2,
|
|
46
|
+
ge=1,
|
|
47
|
+
le=6,
|
|
48
|
+
description="Safety tolerance level (1 = most strict, 6 = most permissive)",
|
|
49
|
+
)
|
|
50
|
+
seed: int | None = Field(
|
|
51
|
+
default=None,
|
|
52
|
+
description="Random seed for reproducibility (optional)",
|
|
53
|
+
)
|
|
54
|
+
output_format: Literal["jpeg", "png"] = Field(
|
|
55
|
+
default="jpeg",
|
|
56
|
+
description="Output image format",
|
|
57
|
+
)
|
|
58
|
+
enhance_prompt: bool = Field(
|
|
59
|
+
default=False,
|
|
60
|
+
description="Whether to enhance the prompt for better results",
|
|
61
|
+
)
|
|
62
|
+
raw: bool = Field(
|
|
63
|
+
default=False,
|
|
64
|
+
description="Generate less processed, more natural-looking images",
|
|
65
|
+
)
|
|
66
|
+
sync_mode: bool = Field(
|
|
67
|
+
default=True,
|
|
68
|
+
description="Use synchronous mode (wait for completion)",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class FalFluxProUltraGenerator(BaseGenerator):
|
|
73
|
+
"""FLUX1.1 [pro] ultra image generator using fal.ai."""
|
|
74
|
+
|
|
75
|
+
name = "fal-flux-pro-ultra"
|
|
76
|
+
artifact_type = "image"
|
|
77
|
+
description = (
|
|
78
|
+
"Fal: FLUX1.1 [pro] ultra - high-quality text-to-image generation with advanced controls"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def get_input_schema(self) -> type[FluxProUltraInput]:
|
|
82
|
+
return FluxProUltraInput
|
|
83
|
+
|
|
84
|
+
async def generate(
|
|
85
|
+
self, inputs: FluxProUltraInput, context: GeneratorExecutionContext
|
|
86
|
+
) -> GeneratorResult:
|
|
87
|
+
"""Generate images using fal.ai FLUX1.1 [pro] ultra model."""
|
|
88
|
+
# Check for API key (fal-client uses FAL_KEY environment variable)
|
|
89
|
+
if not os.getenv("FAL_KEY"):
|
|
90
|
+
raise ValueError("API configuration invalid. Missing FAL_KEY environment variable")
|
|
91
|
+
|
|
92
|
+
# Import fal_client
|
|
93
|
+
try:
|
|
94
|
+
import fal_client
|
|
95
|
+
except ImportError as e:
|
|
96
|
+
raise ImportError(
|
|
97
|
+
"fal.ai SDK is required for FalFluxProUltraGenerator. "
|
|
98
|
+
"Install with: pip install weirdfingers-boards[generators-fal]"
|
|
99
|
+
) from e
|
|
100
|
+
|
|
101
|
+
# Prepare arguments for fal.ai API
|
|
102
|
+
arguments = {
|
|
103
|
+
"prompt": inputs.prompt,
|
|
104
|
+
"aspect_ratio": inputs.aspect_ratio,
|
|
105
|
+
"num_images": inputs.num_images,
|
|
106
|
+
"enable_safety_checker": inputs.enable_safety_checker,
|
|
107
|
+
"safety_tolerance": inputs.safety_tolerance,
|
|
108
|
+
"output_format": inputs.output_format,
|
|
109
|
+
"enhance_prompt": inputs.enhance_prompt,
|
|
110
|
+
"raw": inputs.raw,
|
|
111
|
+
"sync_mode": inputs.sync_mode,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# Add seed if provided
|
|
115
|
+
if inputs.seed is not None:
|
|
116
|
+
arguments["seed"] = inputs.seed
|
|
117
|
+
|
|
118
|
+
# Submit async job and get handler
|
|
119
|
+
handler = await fal_client.submit_async(
|
|
120
|
+
"fal-ai/flux-pro/v1.1-ultra",
|
|
121
|
+
arguments=arguments,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Store the external job ID for tracking
|
|
125
|
+
await context.set_external_job_id(handler.request_id)
|
|
126
|
+
|
|
127
|
+
# Stream progress updates (sample every 3rd event to avoid spam)
|
|
128
|
+
from .....progress.models import ProgressUpdate
|
|
129
|
+
|
|
130
|
+
event_count = 0
|
|
131
|
+
async for event in handler.iter_events(with_logs=True):
|
|
132
|
+
event_count += 1
|
|
133
|
+
|
|
134
|
+
# Process every 3rd event to provide feedback without overwhelming
|
|
135
|
+
if event_count % 3 == 0:
|
|
136
|
+
# Extract logs if available
|
|
137
|
+
logs = getattr(event, "logs", None)
|
|
138
|
+
if logs:
|
|
139
|
+
# Join log entries into a single message
|
|
140
|
+
if isinstance(logs, list):
|
|
141
|
+
message = " | ".join(str(log) for log in logs if log)
|
|
142
|
+
else:
|
|
143
|
+
message = str(logs)
|
|
144
|
+
|
|
145
|
+
if message:
|
|
146
|
+
await context.publish_progress(
|
|
147
|
+
ProgressUpdate(
|
|
148
|
+
job_id=handler.request_id,
|
|
149
|
+
status="processing",
|
|
150
|
+
progress=50.0, # Approximate mid-point progress
|
|
151
|
+
phase="processing",
|
|
152
|
+
message=message,
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Get final result
|
|
157
|
+
result = await handler.get()
|
|
158
|
+
|
|
159
|
+
# Extract image URLs from result
|
|
160
|
+
# fal.ai returns: {"images": [{"url": "...", "width": ..., "height": ...}, ...]}
|
|
161
|
+
images = result.get("images", [])
|
|
162
|
+
if not images:
|
|
163
|
+
raise ValueError("No images returned from fal.ai API")
|
|
164
|
+
|
|
165
|
+
# Store each image using output_index
|
|
166
|
+
artifacts = []
|
|
167
|
+
for idx, image_data in enumerate(images):
|
|
168
|
+
image_url = image_data.get("url")
|
|
169
|
+
width = image_data.get("width", 1024)
|
|
170
|
+
height = image_data.get("height", 1024)
|
|
171
|
+
|
|
172
|
+
if not image_url:
|
|
173
|
+
raise ValueError(f"Image {idx} missing URL in fal.ai response")
|
|
174
|
+
|
|
175
|
+
# Store with appropriate output_index
|
|
176
|
+
artifact = await context.store_image_result(
|
|
177
|
+
storage_url=image_url,
|
|
178
|
+
format=inputs.output_format,
|
|
179
|
+
width=width,
|
|
180
|
+
height=height,
|
|
181
|
+
output_index=idx,
|
|
182
|
+
)
|
|
183
|
+
artifacts.append(artifact)
|
|
184
|
+
|
|
185
|
+
return GeneratorResult(outputs=artifacts)
|
|
186
|
+
|
|
187
|
+
async def estimate_cost(self, inputs: FluxProUltraInput) -> float:
|
|
188
|
+
"""Estimate cost for FLUX1.1 [pro] ultra generation.
|
|
189
|
+
|
|
190
|
+
FLUX1.1 [pro] ultra billing is based on megapixels (rounded up).
|
|
191
|
+
The aspect ratios map to different resolutions, all roughly in the 2-4MP range.
|
|
192
|
+
Estimated at approximately $0.04 per image based on typical resolutions.
|
|
193
|
+
"""
|
|
194
|
+
# Approximate cost per image (varies slightly by resolution/megapixels)
|
|
195
|
+
# Most aspect ratios result in 2-4 megapixels
|
|
196
|
+
cost_per_image = 0.04
|
|
197
|
+
return cost_per_image * inputs.num_images
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""
|
|
2
|
+
fal.ai Imagen 4 Preview text-to-image generator.
|
|
3
|
+
|
|
4
|
+
Google's highest quality image generation model with support for multiple aspect ratios
|
|
5
|
+
and resolutions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from typing import Literal
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
from ....base import BaseGenerator, GeneratorExecutionContext, GeneratorResult
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Imagen4PreviewInput(BaseModel):
|
|
17
|
+
"""Input schema for Imagen 4 Preview image generation."""
|
|
18
|
+
|
|
19
|
+
prompt: str = Field(description="Text description of desired image")
|
|
20
|
+
aspect_ratio: Literal["1:1", "16:9", "9:16", "3:4", "4:3"] = Field(
|
|
21
|
+
default="1:1",
|
|
22
|
+
description="Image aspect ratio",
|
|
23
|
+
)
|
|
24
|
+
resolution: Literal["1K", "2K"] = Field(
|
|
25
|
+
default="1K",
|
|
26
|
+
description="Image resolution (1K or 2K)",
|
|
27
|
+
)
|
|
28
|
+
num_images: int = Field(
|
|
29
|
+
default=1,
|
|
30
|
+
ge=1,
|
|
31
|
+
le=4,
|
|
32
|
+
description="Number of images to generate (max 4)",
|
|
33
|
+
)
|
|
34
|
+
seed: int | None = Field(
|
|
35
|
+
default=None,
|
|
36
|
+
description="Random seed for reproducibility (optional)",
|
|
37
|
+
)
|
|
38
|
+
negative_prompt: str = Field(
|
|
39
|
+
default="",
|
|
40
|
+
description="Content to exclude from generation",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class FalImagen4PreviewGenerator(BaseGenerator):
|
|
45
|
+
"""Imagen 4 Preview image generator using fal.ai."""
|
|
46
|
+
|
|
47
|
+
name = "fal-imagen4-preview"
|
|
48
|
+
artifact_type = "image"
|
|
49
|
+
description = "Fal: Imagen 4 - Google's highest quality text-to-image generation model"
|
|
50
|
+
|
|
51
|
+
def get_input_schema(self) -> type[Imagen4PreviewInput]:
|
|
52
|
+
return Imagen4PreviewInput
|
|
53
|
+
|
|
54
|
+
def _calculate_dimensions(self, aspect_ratio: str, resolution: str) -> tuple[int, int]:
|
|
55
|
+
"""Calculate image dimensions based on aspect ratio and resolution.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
aspect_ratio: Image aspect ratio (e.g., "16:9")
|
|
59
|
+
resolution: Image resolution ("1K" or "2K")
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Tuple of (width, height)
|
|
63
|
+
"""
|
|
64
|
+
# Base size for each resolution
|
|
65
|
+
base_size = 1024 if resolution == "1K" else 2048
|
|
66
|
+
|
|
67
|
+
# Dimension mapping for each aspect ratio
|
|
68
|
+
dimensions = {
|
|
69
|
+
"1:1": (base_size, base_size),
|
|
70
|
+
"16:9": (base_size, int(base_size * 9 / 16)),
|
|
71
|
+
"9:16": (int(base_size * 9 / 16), base_size),
|
|
72
|
+
"4:3": (base_size, int(base_size * 3 / 4)),
|
|
73
|
+
"3:4": (int(base_size * 3 / 4), base_size),
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return dimensions.get(aspect_ratio, (base_size, base_size))
|
|
77
|
+
|
|
78
|
+
async def generate(
|
|
79
|
+
self, inputs: Imagen4PreviewInput, context: GeneratorExecutionContext
|
|
80
|
+
) -> GeneratorResult:
|
|
81
|
+
"""Generate images using fal.ai Imagen 4 Preview model."""
|
|
82
|
+
# Check for API key (fal-client uses FAL_KEY environment variable)
|
|
83
|
+
if not os.getenv("FAL_KEY"):
|
|
84
|
+
raise ValueError("API configuration invalid. Missing FAL_KEY environment variable")
|
|
85
|
+
|
|
86
|
+
# Import fal_client
|
|
87
|
+
try:
|
|
88
|
+
import fal_client
|
|
89
|
+
except ImportError as e:
|
|
90
|
+
raise ImportError(
|
|
91
|
+
"fal.ai SDK is required for FalImagen4PreviewGenerator. "
|
|
92
|
+
"Install with: pip install weirdfingers-boards[generators-fal]"
|
|
93
|
+
) from e
|
|
94
|
+
|
|
95
|
+
# Prepare arguments for fal.ai API
|
|
96
|
+
arguments = {
|
|
97
|
+
"prompt": inputs.prompt,
|
|
98
|
+
"aspect_ratio": inputs.aspect_ratio,
|
|
99
|
+
"resolution": inputs.resolution,
|
|
100
|
+
"num_images": inputs.num_images,
|
|
101
|
+
"negative_prompt": inputs.negative_prompt,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# Add seed if provided
|
|
105
|
+
if inputs.seed is not None:
|
|
106
|
+
arguments["seed"] = inputs.seed
|
|
107
|
+
|
|
108
|
+
# Submit async job and get handler
|
|
109
|
+
handler = await fal_client.submit_async(
|
|
110
|
+
"fal-ai/imagen4/preview",
|
|
111
|
+
arguments=arguments,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Store the external job ID for tracking
|
|
115
|
+
await context.set_external_job_id(handler.request_id)
|
|
116
|
+
|
|
117
|
+
# Stream progress updates (sample every 3rd event to avoid spam)
|
|
118
|
+
from .....progress.models import ProgressUpdate
|
|
119
|
+
|
|
120
|
+
event_count = 0
|
|
121
|
+
async for event in handler.iter_events(with_logs=True):
|
|
122
|
+
event_count += 1
|
|
123
|
+
|
|
124
|
+
# Process every 3rd event to provide feedback without overwhelming
|
|
125
|
+
if event_count % 3 == 0:
|
|
126
|
+
# Extract logs if available
|
|
127
|
+
logs = getattr(event, "logs", None)
|
|
128
|
+
if logs:
|
|
129
|
+
# Join log entries into a single message
|
|
130
|
+
if isinstance(logs, list):
|
|
131
|
+
message = " | ".join(str(log) for log in logs if log)
|
|
132
|
+
else:
|
|
133
|
+
message = str(logs)
|
|
134
|
+
|
|
135
|
+
if message:
|
|
136
|
+
await context.publish_progress(
|
|
137
|
+
ProgressUpdate(
|
|
138
|
+
job_id=handler.request_id,
|
|
139
|
+
status="processing",
|
|
140
|
+
progress=50.0, # Approximate mid-point progress
|
|
141
|
+
phase="processing",
|
|
142
|
+
message=message,
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Get final result
|
|
147
|
+
result = await handler.get()
|
|
148
|
+
|
|
149
|
+
# Extract image URLs from result
|
|
150
|
+
# fal.ai returns: {"images": [{"url": "...", "content_type": "...", ...}, ...]}
|
|
151
|
+
images = result.get("images", [])
|
|
152
|
+
if not images:
|
|
153
|
+
raise ValueError("No images returned from fal.ai API")
|
|
154
|
+
|
|
155
|
+
# Calculate dimensions based on inputs
|
|
156
|
+
width, height = self._calculate_dimensions(inputs.aspect_ratio, inputs.resolution)
|
|
157
|
+
|
|
158
|
+
# Store each image using output_index
|
|
159
|
+
artifacts = []
|
|
160
|
+
for idx, image_data in enumerate(images):
|
|
161
|
+
image_url = image_data.get("url")
|
|
162
|
+
|
|
163
|
+
if not image_url:
|
|
164
|
+
raise ValueError(f"Image {idx} missing URL in fal.ai response")
|
|
165
|
+
|
|
166
|
+
# Detect format from content_type or URL
|
|
167
|
+
content_type = image_data.get("content_type", "")
|
|
168
|
+
if "png" in content_type.lower():
|
|
169
|
+
format = "png"
|
|
170
|
+
else:
|
|
171
|
+
format = "jpeg"
|
|
172
|
+
|
|
173
|
+
# Store with appropriate output_index
|
|
174
|
+
artifact = await context.store_image_result(
|
|
175
|
+
storage_url=image_url,
|
|
176
|
+
format=format,
|
|
177
|
+
width=width,
|
|
178
|
+
height=height,
|
|
179
|
+
output_index=idx,
|
|
180
|
+
)
|
|
181
|
+
artifacts.append(artifact)
|
|
182
|
+
|
|
183
|
+
return GeneratorResult(outputs=artifacts)
|
|
184
|
+
|
|
185
|
+
async def estimate_cost(self, inputs: Imagen4PreviewInput) -> float:
|
|
186
|
+
"""Estimate cost for Imagen 4 Preview generation.
|
|
187
|
+
|
|
188
|
+
Imagen 4 Preview costs $0.04 per image.
|
|
189
|
+
"""
|
|
190
|
+
cost_per_image = 0.04
|
|
191
|
+
return cost_per_image * inputs.num_images
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Google Imagen 4 fast text-to-image generator.
|
|
3
|
+
|
|
4
|
+
Google's highest quality image generation model with support for multiple aspect ratios
|
|
5
|
+
and batch generation.
|
|
6
|
+
|
|
7
|
+
Based on Fal AI's fal-ai/imagen4/preview/fast model.
|
|
8
|
+
See: https://fal.ai/models/fal-ai/imagen4/preview/fast
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from typing import Literal
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel, Field
|
|
15
|
+
|
|
16
|
+
from ....base import BaseGenerator, GeneratorExecutionContext, GeneratorResult
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Imagen4PreviewFastInput(BaseModel):
|
|
20
|
+
"""Input schema for Google Imagen 4 fast image generation.
|
|
21
|
+
|
|
22
|
+
All parameters are simple types - no artifact inputs needed.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
prompt: str = Field(description="The text prompt describing what you want to see")
|
|
26
|
+
negative_prompt: str = Field(
|
|
27
|
+
default="",
|
|
28
|
+
description="Discourage specific elements in generated images",
|
|
29
|
+
)
|
|
30
|
+
aspect_ratio: Literal["1:1", "16:9", "9:16", "3:4", "4:3"] = Field(
|
|
31
|
+
default="1:1",
|
|
32
|
+
description="Image aspect ratio",
|
|
33
|
+
)
|
|
34
|
+
num_images: int = Field(
|
|
35
|
+
default=1,
|
|
36
|
+
ge=1,
|
|
37
|
+
le=4,
|
|
38
|
+
description="Number of images to generate (1-4)",
|
|
39
|
+
)
|
|
40
|
+
seed: int | None = Field(
|
|
41
|
+
default=None,
|
|
42
|
+
description="Random seed for reproducible generation (optional)",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class FalImagen4PreviewFastGenerator(BaseGenerator):
|
|
47
|
+
"""Google Imagen 4 fast image generator using fal.ai."""
|
|
48
|
+
|
|
49
|
+
name = "fal-imagen4-preview-fast"
|
|
50
|
+
artifact_type = "image"
|
|
51
|
+
description = "Fal: Google Imagen 4 - highest quality text-to-image generation"
|
|
52
|
+
|
|
53
|
+
def get_input_schema(self) -> type[Imagen4PreviewFastInput]:
|
|
54
|
+
return Imagen4PreviewFastInput
|
|
55
|
+
|
|
56
|
+
async def generate(
|
|
57
|
+
self, inputs: Imagen4PreviewFastInput, context: GeneratorExecutionContext
|
|
58
|
+
) -> GeneratorResult:
|
|
59
|
+
"""Generate images using Google Imagen 4 via fal.ai."""
|
|
60
|
+
# Check for API key (fal-client uses FAL_KEY environment variable)
|
|
61
|
+
if not os.getenv("FAL_KEY"):
|
|
62
|
+
raise ValueError("API configuration invalid. Missing FAL_KEY environment variable")
|
|
63
|
+
|
|
64
|
+
# Import fal_client
|
|
65
|
+
try:
|
|
66
|
+
import fal_client
|
|
67
|
+
except ImportError as e:
|
|
68
|
+
raise ImportError(
|
|
69
|
+
"fal.ai SDK is required for FalImagen4PreviewFastGenerator. "
|
|
70
|
+
"Install with: pip install weirdfingers-boards[generators-fal]"
|
|
71
|
+
) from e
|
|
72
|
+
|
|
73
|
+
# Prepare arguments for fal.ai API
|
|
74
|
+
arguments = {
|
|
75
|
+
"prompt": inputs.prompt,
|
|
76
|
+
"negative_prompt": inputs.negative_prompt,
|
|
77
|
+
"aspect_ratio": inputs.aspect_ratio,
|
|
78
|
+
"num_images": inputs.num_images,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Add seed if provided
|
|
82
|
+
if inputs.seed is not None:
|
|
83
|
+
arguments["seed"] = inputs.seed
|
|
84
|
+
|
|
85
|
+
# Submit async job and get handler
|
|
86
|
+
handler = await fal_client.submit_async(
|
|
87
|
+
"fal-ai/imagen4/preview/fast",
|
|
88
|
+
arguments=arguments,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Store the external job ID for tracking
|
|
92
|
+
await context.set_external_job_id(handler.request_id)
|
|
93
|
+
|
|
94
|
+
# Stream progress updates (sample every 3rd event to avoid spam)
|
|
95
|
+
from .....progress.models import ProgressUpdate
|
|
96
|
+
|
|
97
|
+
event_count = 0
|
|
98
|
+
async for event in handler.iter_events(with_logs=True):
|
|
99
|
+
event_count += 1
|
|
100
|
+
|
|
101
|
+
# Process every 3rd event to provide feedback without overwhelming
|
|
102
|
+
if event_count % 3 == 0:
|
|
103
|
+
# Extract logs if available
|
|
104
|
+
logs = getattr(event, "logs", None)
|
|
105
|
+
if logs:
|
|
106
|
+
# Join log entries into a single message
|
|
107
|
+
if isinstance(logs, list):
|
|
108
|
+
message = " | ".join(str(log) for log in logs if log)
|
|
109
|
+
else:
|
|
110
|
+
message = str(logs)
|
|
111
|
+
|
|
112
|
+
if message:
|
|
113
|
+
await context.publish_progress(
|
|
114
|
+
ProgressUpdate(
|
|
115
|
+
job_id=handler.request_id,
|
|
116
|
+
status="processing",
|
|
117
|
+
progress=50.0, # Approximate mid-point progress
|
|
118
|
+
phase="processing",
|
|
119
|
+
message=message,
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Get final result
|
|
124
|
+
result = await handler.get()
|
|
125
|
+
|
|
126
|
+
# Extract image URLs from result
|
|
127
|
+
# fal.ai returns: {"images": [{"url": "...", "content_type": "...", ...}, ...], "seed": ...}
|
|
128
|
+
images = result.get("images", [])
|
|
129
|
+
if not images:
|
|
130
|
+
raise ValueError("No images returned from fal.ai API")
|
|
131
|
+
|
|
132
|
+
# Store each image using output_index
|
|
133
|
+
artifacts = []
|
|
134
|
+
for idx, image_data in enumerate(images):
|
|
135
|
+
image_url = image_data.get("url")
|
|
136
|
+
|
|
137
|
+
if not image_url:
|
|
138
|
+
raise ValueError(f"Image {idx} missing URL in fal.ai response")
|
|
139
|
+
|
|
140
|
+
# Imagen 4 returns PNG images
|
|
141
|
+
# We don't have width/height in the response, so use defaults based on aspect ratio
|
|
142
|
+
# This is a simplification - actual dimensions may vary
|
|
143
|
+
width, height = self._get_dimensions_for_aspect_ratio(inputs.aspect_ratio)
|
|
144
|
+
|
|
145
|
+
# Store with appropriate output_index
|
|
146
|
+
artifact = await context.store_image_result(
|
|
147
|
+
storage_url=image_url,
|
|
148
|
+
format="png",
|
|
149
|
+
width=width,
|
|
150
|
+
height=height,
|
|
151
|
+
output_index=idx,
|
|
152
|
+
)
|
|
153
|
+
artifacts.append(artifact)
|
|
154
|
+
|
|
155
|
+
return GeneratorResult(outputs=artifacts)
|
|
156
|
+
|
|
157
|
+
def _get_dimensions_for_aspect_ratio(self, aspect_ratio: str) -> tuple[int, int]:
|
|
158
|
+
"""Get approximate dimensions for a given aspect ratio.
|
|
159
|
+
|
|
160
|
+
Returns (width, height) tuple. Uses 1024 as base for 1:1 ratio.
|
|
161
|
+
"""
|
|
162
|
+
aspect_map = {
|
|
163
|
+
"1:1": (1024, 1024),
|
|
164
|
+
"16:9": (1360, 768),
|
|
165
|
+
"9:16": (768, 1360),
|
|
166
|
+
"3:4": (896, 1152),
|
|
167
|
+
"4:3": (1152, 896),
|
|
168
|
+
}
|
|
169
|
+
return aspect_map.get(aspect_ratio, (1024, 1024))
|
|
170
|
+
|
|
171
|
+
async def estimate_cost(self, inputs: Imagen4PreviewFastInput) -> float:
|
|
172
|
+
"""Estimate cost for Google Imagen 4 generation.
|
|
173
|
+
|
|
174
|
+
Pricing information was not available in the documentation.
|
|
175
|
+
Using estimated cost of $0.04 per image based on similar high-quality models.
|
|
176
|
+
"""
|
|
177
|
+
# Estimated cost per image (actual pricing may vary)
|
|
178
|
+
cost_per_image = 0.04
|
|
179
|
+
return cost_per_image * inputs.num_images
|