@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
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Artifact resolution utilities for converting Generation references to actual files.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
import base64
|
|
5
6
|
import os
|
|
6
7
|
import tempfile
|
|
7
8
|
import uuid
|
|
@@ -153,6 +154,59 @@ def _get_file_extension(
|
|
|
153
154
|
return format_ext
|
|
154
155
|
|
|
155
156
|
|
|
157
|
+
def _decode_data_url(data_url: str) -> bytes:
|
|
158
|
+
"""
|
|
159
|
+
Decode a data URL to bytes.
|
|
160
|
+
|
|
161
|
+
Supports data URLs in the format: data:[<mediatype>][;base64],<data>
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
data_url: Data URL string (e.g., "data:image/png;base64,iVBORw0KGgo...")
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
bytes: Decoded content
|
|
168
|
+
|
|
169
|
+
Raises:
|
|
170
|
+
ValueError: If data URL is malformed or empty
|
|
171
|
+
"""
|
|
172
|
+
if not data_url.startswith("data:"):
|
|
173
|
+
raise ValueError("Invalid data URL: must start with 'data:'")
|
|
174
|
+
|
|
175
|
+
# Split off the "data:" prefix
|
|
176
|
+
try:
|
|
177
|
+
# Format: data:[<mediatype>][;base64],<data>
|
|
178
|
+
header, data = data_url[5:].split(",", 1)
|
|
179
|
+
except ValueError as e:
|
|
180
|
+
raise ValueError("Invalid data URL format: missing comma separator") from e
|
|
181
|
+
|
|
182
|
+
if not data:
|
|
183
|
+
raise ValueError("Data URL contains no data after comma")
|
|
184
|
+
|
|
185
|
+
# Check if base64 encoded
|
|
186
|
+
is_base64 = ";base64" in header
|
|
187
|
+
|
|
188
|
+
if is_base64:
|
|
189
|
+
try:
|
|
190
|
+
decoded = base64.b64decode(data)
|
|
191
|
+
except Exception as e:
|
|
192
|
+
raise ValueError(f"Failed to decode base64 data: {e}") from e
|
|
193
|
+
else:
|
|
194
|
+
# URL-encoded data (rare for binary content)
|
|
195
|
+
from urllib.parse import unquote
|
|
196
|
+
|
|
197
|
+
decoded = unquote(data).encode("utf-8")
|
|
198
|
+
|
|
199
|
+
if len(decoded) == 0:
|
|
200
|
+
raise ValueError("Decoded data URL is empty")
|
|
201
|
+
|
|
202
|
+
logger.info(
|
|
203
|
+
"Successfully decoded data URL",
|
|
204
|
+
size_bytes=len(decoded),
|
|
205
|
+
is_base64=is_base64,
|
|
206
|
+
)
|
|
207
|
+
return decoded
|
|
208
|
+
|
|
209
|
+
|
|
156
210
|
async def download_from_url(url: str) -> bytes:
|
|
157
211
|
"""
|
|
158
212
|
Download content from a URL (typically a provider's temporary URL).
|
|
@@ -160,20 +214,26 @@ async def download_from_url(url: str) -> bytes:
|
|
|
160
214
|
This is used to download generated content from providers like Replicate, OpenAI, etc.
|
|
161
215
|
before uploading to our permanent storage.
|
|
162
216
|
|
|
217
|
+
Supports both HTTP(S) URLs and data URLs (data:mime/type;base64,...)
|
|
218
|
+
|
|
163
219
|
Note: For very large files, consider using streaming downloads directly to storage
|
|
164
220
|
instead of loading into memory.
|
|
165
221
|
|
|
166
222
|
Args:
|
|
167
|
-
url: URL to download from
|
|
223
|
+
url: URL to download from (HTTP(S) or data URL)
|
|
168
224
|
|
|
169
225
|
Returns:
|
|
170
226
|
bytes: Downloaded content
|
|
171
227
|
|
|
172
228
|
Raises:
|
|
173
229
|
httpx.HTTPError: If download fails
|
|
174
|
-
ValueError: If downloaded content is empty
|
|
230
|
+
ValueError: If downloaded content is empty or data URL is malformed
|
|
175
231
|
"""
|
|
176
|
-
logger.debug("Downloading content from URL", url=url)
|
|
232
|
+
logger.debug("Downloading content from URL", url=url[:50])
|
|
233
|
+
|
|
234
|
+
# Check if this is a data URL
|
|
235
|
+
if url.startswith("data:"):
|
|
236
|
+
return _decode_data_url(url)
|
|
177
237
|
|
|
178
238
|
# Stream download to avoid loading entire file into memory at once
|
|
179
239
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
@@ -244,8 +304,8 @@ async def store_image_result(
|
|
|
244
304
|
board_id: str,
|
|
245
305
|
storage_url: str,
|
|
246
306
|
format: str,
|
|
247
|
-
width: int,
|
|
248
|
-
height: int,
|
|
307
|
+
width: int | None = None,
|
|
308
|
+
height: int | None = None,
|
|
249
309
|
) -> ImageArtifact:
|
|
250
310
|
"""
|
|
251
311
|
Store an image result by downloading from provider URL and uploading to storage.
|
|
@@ -257,8 +317,8 @@ async def store_image_result(
|
|
|
257
317
|
board_id: Board ID for organization
|
|
258
318
|
storage_url: Provider's temporary URL to download from
|
|
259
319
|
format: Image format (png, jpg, etc.)
|
|
260
|
-
width: Image width in pixels
|
|
261
|
-
height: Image height in pixels
|
|
320
|
+
width: Image width in pixels (optional)
|
|
321
|
+
height: Image height in pixels (optional)
|
|
262
322
|
|
|
263
323
|
Returns:
|
|
264
324
|
ImageArtifact with permanent storage URL
|
|
@@ -270,7 +330,7 @@ async def store_image_result(
|
|
|
270
330
|
logger.info(
|
|
271
331
|
"Storing image result",
|
|
272
332
|
generation_id=generation_id,
|
|
273
|
-
provider_url=storage_url,
|
|
333
|
+
provider_url=storage_url[:50],
|
|
274
334
|
format=format,
|
|
275
335
|
)
|
|
276
336
|
|
|
@@ -294,7 +354,7 @@ async def store_image_result(
|
|
|
294
354
|
"Image stored successfully",
|
|
295
355
|
generation_id=generation_id,
|
|
296
356
|
storage_key=artifact_ref.storage_key,
|
|
297
|
-
storage_url=artifact_ref.storage_url,
|
|
357
|
+
storage_url=artifact_ref.storage_url[:50],
|
|
298
358
|
)
|
|
299
359
|
|
|
300
360
|
# Return artifact with our permanent storage URL
|
|
@@ -314,8 +374,8 @@ async def store_video_result(
|
|
|
314
374
|
board_id: str,
|
|
315
375
|
storage_url: str,
|
|
316
376
|
format: str,
|
|
317
|
-
width: int,
|
|
318
|
-
height: int,
|
|
377
|
+
width: int | None = None,
|
|
378
|
+
height: int | None = None,
|
|
319
379
|
duration: float | None = None,
|
|
320
380
|
fps: float | None = None,
|
|
321
381
|
) -> VideoArtifact:
|
|
@@ -329,8 +389,8 @@ async def store_video_result(
|
|
|
329
389
|
board_id: Board ID for organization
|
|
330
390
|
storage_url: Provider's temporary URL to download from
|
|
331
391
|
format: Video format (mp4, webm, etc.)
|
|
332
|
-
width: Video width in pixels
|
|
333
|
-
height: Video height in pixels
|
|
392
|
+
width: Video width in pixels (optional)
|
|
393
|
+
height: Video height in pixels (optional)
|
|
334
394
|
duration: Video duration in seconds (optional)
|
|
335
395
|
fps: Frames per second (optional)
|
|
336
396
|
|
|
@@ -344,7 +404,7 @@ async def store_video_result(
|
|
|
344
404
|
logger.info(
|
|
345
405
|
"Storing video result",
|
|
346
406
|
generation_id=generation_id,
|
|
347
|
-
provider_url=storage_url,
|
|
407
|
+
provider_url=storage_url[:50],
|
|
348
408
|
format=format,
|
|
349
409
|
)
|
|
350
410
|
|
|
@@ -368,7 +428,7 @@ async def store_video_result(
|
|
|
368
428
|
"Video stored successfully",
|
|
369
429
|
generation_id=generation_id,
|
|
370
430
|
storage_key=artifact_ref.storage_key,
|
|
371
|
-
storage_url=artifact_ref.storage_url,
|
|
431
|
+
storage_url=artifact_ref.storage_url[:50],
|
|
372
432
|
)
|
|
373
433
|
|
|
374
434
|
# Return artifact with our permanent storage URL
|
|
@@ -418,7 +478,7 @@ async def store_audio_result(
|
|
|
418
478
|
logger.info(
|
|
419
479
|
"Storing audio result",
|
|
420
480
|
generation_id=generation_id,
|
|
421
|
-
provider_url=storage_url,
|
|
481
|
+
provider_url=storage_url[:50],
|
|
422
482
|
format=format,
|
|
423
483
|
)
|
|
424
484
|
|
|
@@ -442,7 +502,7 @@ async def store_audio_result(
|
|
|
442
502
|
"Audio stored successfully",
|
|
443
503
|
generation_id=generation_id,
|
|
444
504
|
storage_key=artifact_ref.storage_key,
|
|
445
|
-
storage_url=artifact_ref.storage_url,
|
|
505
|
+
storage_url=artifact_ref.storage_url[:50],
|
|
446
506
|
)
|
|
447
507
|
|
|
448
508
|
# Return artifact with our permanent storage URL
|
|
@@ -485,7 +545,7 @@ async def store_text_result(
|
|
|
485
545
|
logger.info(
|
|
486
546
|
"Storing text result",
|
|
487
547
|
generation_id=generation_id,
|
|
488
|
-
content=content,
|
|
548
|
+
content=content[:50],
|
|
489
549
|
format=format,
|
|
490
550
|
)
|
|
491
551
|
|
|
@@ -503,13 +563,13 @@ async def store_text_result(
|
|
|
503
563
|
"Text stored successfully",
|
|
504
564
|
generation_id=generation_id,
|
|
505
565
|
storage_key=artifact_ref.storage_key,
|
|
506
|
-
storage_url=artifact_ref.storage_url,
|
|
566
|
+
storage_url=artifact_ref.storage_url[:50],
|
|
507
567
|
)
|
|
508
568
|
|
|
509
569
|
# Return artifact with our permanent storage URL
|
|
510
570
|
return TextArtifact(
|
|
511
571
|
generation_id=generation_id,
|
|
512
572
|
storage_url=artifact_ref.storage_url,
|
|
513
|
-
content=content,
|
|
573
|
+
content=content[:50],
|
|
514
574
|
format=format,
|
|
515
575
|
)
|
|
@@ -102,3 +102,52 @@ async def finalize_success(
|
|
|
102
102
|
)
|
|
103
103
|
)
|
|
104
104
|
await session.execute(stmt)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
async def create_batch_generation(
|
|
108
|
+
session: AsyncSession,
|
|
109
|
+
*,
|
|
110
|
+
tenant_id: UUID,
|
|
111
|
+
board_id: UUID,
|
|
112
|
+
user_id: UUID,
|
|
113
|
+
generator_name: str,
|
|
114
|
+
artifact_type: str,
|
|
115
|
+
input_params: dict,
|
|
116
|
+
batch_id: str,
|
|
117
|
+
batch_index: int,
|
|
118
|
+
) -> Generations:
|
|
119
|
+
"""Create a batch generation record for multi-output generators.
|
|
120
|
+
|
|
121
|
+
This creates a new generation record that is part of a batch, with
|
|
122
|
+
batch metadata stored in output_metadata.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
session: Database session
|
|
126
|
+
tenant_id: Tenant ID
|
|
127
|
+
board_id: Board ID
|
|
128
|
+
user_id: User ID
|
|
129
|
+
generator_name: Name of the generator
|
|
130
|
+
artifact_type: Type of artifact (image, video, etc.)
|
|
131
|
+
input_params: Input parameters (same as primary generation)
|
|
132
|
+
batch_id: Unique ID for this batch
|
|
133
|
+
batch_index: Index of this output in the batch
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Created generation record
|
|
137
|
+
"""
|
|
138
|
+
gen = Generations()
|
|
139
|
+
gen.tenant_id = tenant_id
|
|
140
|
+
gen.board_id = board_id
|
|
141
|
+
gen.user_id = user_id
|
|
142
|
+
gen.generator_name = generator_name
|
|
143
|
+
gen.artifact_type = artifact_type
|
|
144
|
+
gen.input_params = input_params
|
|
145
|
+
gen.status = "processing"
|
|
146
|
+
gen.progress = Decimal(0.0)
|
|
147
|
+
gen.output_metadata = {
|
|
148
|
+
"batch_id": batch_id,
|
|
149
|
+
"batch_index": batch_index,
|
|
150
|
+
}
|
|
151
|
+
session.add(gen)
|
|
152
|
+
await session.flush()
|
|
153
|
+
return gen
|
|
@@ -47,7 +47,10 @@ try:
|
|
|
47
47
|
except ImportError:
|
|
48
48
|
SupabaseStorageProvider = None
|
|
49
49
|
_supabase_available = False
|
|
50
|
-
logger.warning(
|
|
50
|
+
logger.warning(
|
|
51
|
+
"Supabase storage not available. "
|
|
52
|
+
"Install with: pip install weirdfingers-boards[storage-supabase]"
|
|
53
|
+
)
|
|
51
54
|
|
|
52
55
|
try:
|
|
53
56
|
from .implementations.s3 import S3StorageProvider
|
|
@@ -56,7 +59,9 @@ try:
|
|
|
56
59
|
except ImportError:
|
|
57
60
|
S3StorageProvider = None
|
|
58
61
|
_s3_available = False
|
|
59
|
-
logger.warning(
|
|
62
|
+
logger.warning(
|
|
63
|
+
"S3 storage not available. " "Install with: pip install weirdfingers-boards[storage-s3]"
|
|
64
|
+
)
|
|
60
65
|
|
|
61
66
|
try:
|
|
62
67
|
from .implementations.gcs import GCSStorageProvider
|
|
@@ -65,7 +70,9 @@ try:
|
|
|
65
70
|
except ImportError:
|
|
66
71
|
GCSStorageProvider = None
|
|
67
72
|
_gcs_available = False
|
|
68
|
-
logger.warning(
|
|
73
|
+
logger.warning(
|
|
74
|
+
"GCS storage not available. " "Install with: pip install weirdfingers-boards[storage-gcs]"
|
|
75
|
+
)
|
|
69
76
|
|
|
70
77
|
|
|
71
78
|
def create_storage_provider(provider_type: str, config: dict[str, Any]) -> StorageProvider:
|
|
@@ -88,19 +95,22 @@ def create_storage_provider(provider_type: str, config: dict[str, Any]) -> Stora
|
|
|
88
95
|
elif provider_type == "supabase":
|
|
89
96
|
if not _supabase_available:
|
|
90
97
|
raise ImportError(
|
|
91
|
-
"Supabase storage requires
|
|
98
|
+
"Supabase storage requires additional dependencies. "
|
|
99
|
+
"Install with: pip install weirdfingers-boards[storage-supabase]"
|
|
92
100
|
)
|
|
93
101
|
return _create_supabase_provider(config)
|
|
94
102
|
elif provider_type == "s3":
|
|
95
103
|
if not _s3_available:
|
|
96
104
|
raise ImportError(
|
|
97
|
-
"S3 storage requires
|
|
105
|
+
"S3 storage requires additional dependencies. "
|
|
106
|
+
"Install with: pip install weirdfingers-boards[storage-s3]"
|
|
98
107
|
)
|
|
99
108
|
return _create_s3_provider(config)
|
|
100
109
|
elif provider_type == "gcs":
|
|
101
110
|
if not _gcs_available:
|
|
102
111
|
raise ImportError(
|
|
103
|
-
"GCS storage
|
|
112
|
+
"GCS storage requires additional dependencies. "
|
|
113
|
+
"Install with: pip install weirdfingers-boards[storage-gcs]"
|
|
104
114
|
)
|
|
105
115
|
return _create_gcs_provider(config)
|
|
106
116
|
else:
|
|
@@ -75,6 +75,8 @@ async def process_generation(generation_id: str) -> None:
|
|
|
75
75
|
gen_id = gen.id
|
|
76
76
|
tenant_id = gen.tenant_id
|
|
77
77
|
board_id = gen.board_id
|
|
78
|
+
user_id = gen.user_id
|
|
79
|
+
artifact_type = gen.artifact_type
|
|
78
80
|
|
|
79
81
|
# Initialize storage manager
|
|
80
82
|
# This will use the default storage configuration from environment/config
|
|
@@ -88,18 +90,39 @@ async def process_generation(generation_id: str) -> None:
|
|
|
88
90
|
raise RuntimeError(f"Unknown generator: {generator_name}")
|
|
89
91
|
|
|
90
92
|
# Build and validate typed inputs
|
|
93
|
+
# First resolve any artifact fields (generation IDs -> artifact objects)
|
|
94
|
+
# This happens automatically via type introspection
|
|
91
95
|
try:
|
|
92
96
|
input_schema = generator.get_input_schema()
|
|
93
|
-
|
|
97
|
+
|
|
98
|
+
# Automatically resolve generation IDs to artifacts before validation
|
|
99
|
+
from ..generators.artifact_resolution import resolve_input_artifacts
|
|
100
|
+
|
|
101
|
+
async with get_async_session() as session:
|
|
102
|
+
resolved_params = await resolve_input_artifacts(
|
|
103
|
+
input_params,
|
|
104
|
+
input_schema, # Schema is introspected to find artifact fields
|
|
105
|
+
session,
|
|
106
|
+
tenant_id,
|
|
107
|
+
)
|
|
108
|
+
typed_inputs = input_schema.model_validate(resolved_params)
|
|
94
109
|
except Exception as e:
|
|
95
110
|
error_msg = "Invalid input parameters"
|
|
96
111
|
logger.error(error_msg, generation_id=generation_id, error=str(e))
|
|
97
112
|
raise ValueError(f"Invalid input parameters: {e}") from e
|
|
98
113
|
|
|
99
114
|
# Build context and run generator
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
115
|
+
context = GeneratorExecutionContext(
|
|
116
|
+
gen_id,
|
|
117
|
+
publisher,
|
|
118
|
+
storage_manager,
|
|
119
|
+
tenant_id,
|
|
120
|
+
board_id,
|
|
121
|
+
user_id,
|
|
122
|
+
generator_name,
|
|
123
|
+
artifact_type,
|
|
124
|
+
input_params,
|
|
125
|
+
)
|
|
103
126
|
|
|
104
127
|
await publisher.publish_progress(
|
|
105
128
|
generation_id,
|
|
@@ -125,9 +148,10 @@ async def process_generation(generation_id: str) -> None:
|
|
|
125
148
|
"Generator completed successfully",
|
|
126
149
|
generator_name=generator_name,
|
|
127
150
|
generation_id=generation_id,
|
|
151
|
+
artifact_count=len(output.outputs),
|
|
128
152
|
)
|
|
129
153
|
|
|
130
|
-
# Find the artifact with matching generation_id
|
|
154
|
+
# Find the artifact with matching generation_id (primary generation)
|
|
131
155
|
# Generators should return exactly one artifact with the matching generation_id
|
|
132
156
|
matching_artifacts = [art for art in output.outputs if art.generation_id == generation_id]
|
|
133
157
|
|
|
@@ -150,6 +174,18 @@ async def process_generation(generation_id: str) -> None:
|
|
|
150
174
|
storage_url = artifact.storage_url
|
|
151
175
|
output_metadata = artifact.model_dump()
|
|
152
176
|
|
|
177
|
+
# If this was a batch generation, add batch metadata to primary generation
|
|
178
|
+
if context._batch_id is not None:
|
|
179
|
+
output_metadata["batch_id"] = context._batch_id
|
|
180
|
+
output_metadata["batch_index"] = 0
|
|
181
|
+
output_metadata["batch_size"] = len(output.outputs)
|
|
182
|
+
logger.info(
|
|
183
|
+
"Primary generation is part of batch",
|
|
184
|
+
generation_id=generation_id,
|
|
185
|
+
batch_id=context._batch_id,
|
|
186
|
+
batch_size=len(output.outputs),
|
|
187
|
+
)
|
|
188
|
+
|
|
153
189
|
# Finalize DB with storage URL and output metadata
|
|
154
190
|
async with get_async_session() as session:
|
|
155
191
|
await jobs_repo.finalize_success(
|
|
@@ -159,6 +195,34 @@ async def process_generation(generation_id: str) -> None:
|
|
|
159
195
|
output_metadata=output_metadata,
|
|
160
196
|
)
|
|
161
197
|
|
|
198
|
+
# Finalize all batch generation records (if any)
|
|
199
|
+
if context._batch_id is not None:
|
|
200
|
+
batch_artifacts = [art for art in output.outputs if art.generation_id != generation_id]
|
|
201
|
+
logger.info(
|
|
202
|
+
"Finalizing batch generation records",
|
|
203
|
+
batch_id=context._batch_id,
|
|
204
|
+
batch_count=len(batch_artifacts),
|
|
205
|
+
)
|
|
206
|
+
for batch_artifact in batch_artifacts:
|
|
207
|
+
async with get_async_session() as session:
|
|
208
|
+
batch_metadata = batch_artifact.model_dump()
|
|
209
|
+
# Add batch metadata to each batch generation
|
|
210
|
+
batch_metadata["batch_id"] = context._batch_id
|
|
211
|
+
# batch_index was set during generation creation via create_batch_generation()
|
|
212
|
+
batch_metadata["batch_size"] = len(output.outputs)
|
|
213
|
+
|
|
214
|
+
await jobs_repo.finalize_success(
|
|
215
|
+
session,
|
|
216
|
+
batch_artifact.generation_id,
|
|
217
|
+
storage_url=batch_artifact.storage_url,
|
|
218
|
+
output_metadata=batch_metadata,
|
|
219
|
+
)
|
|
220
|
+
logger.info(
|
|
221
|
+
"Batch generation finalized",
|
|
222
|
+
batch_generation_id=batch_artifact.generation_id,
|
|
223
|
+
batch_id=context._batch_id,
|
|
224
|
+
)
|
|
225
|
+
|
|
162
226
|
logger.info("Job finalized successfully", generation_id=generation_id)
|
|
163
227
|
|
|
164
228
|
# Publish completion (DB already updated by finalize_success)
|