@weirdfingers/baseboards 0.2.1 → 0.3.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.
Files changed (51) hide show
  1. package/README.md +10 -0
  2. package/dist/index.js +9 -0
  3. package/dist/index.js.map +1 -1
  4. package/package.json +1 -1
  5. package/templates/api/ARTIFACT_RESOLUTION_GUIDE.md +148 -0
  6. package/templates/api/README.md +138 -6
  7. package/templates/api/config/generators.yaml +41 -7
  8. package/templates/api/docs/TESTING_LIVE_APIS.md +417 -0
  9. package/templates/api/pyproject.toml +49 -9
  10. package/templates/api/src/boards/__init__.py +1 -1
  11. package/templates/api/src/boards/generators/__init__.py +2 -2
  12. package/templates/api/src/boards/generators/artifact_resolution.py +380 -0
  13. package/templates/api/src/boards/generators/base.py +4 -0
  14. package/templates/api/src/boards/generators/implementations/__init__.py +4 -2
  15. package/templates/api/src/boards/generators/implementations/fal/__init__.py +25 -0
  16. package/templates/api/src/boards/generators/implementations/fal/audio/__init__.py +4 -0
  17. package/templates/api/src/boards/generators/implementations/fal/audio/minimax_music_v2.py +173 -0
  18. package/templates/api/src/boards/generators/implementations/fal/audio/minimax_speech_2_6_turbo.py +221 -0
  19. package/templates/api/src/boards/generators/implementations/fal/image/__init__.py +17 -0
  20. package/templates/api/src/boards/generators/implementations/fal/image/flux_pro_kontext.py +216 -0
  21. package/templates/api/src/boards/generators/implementations/fal/image/flux_pro_ultra.py +197 -0
  22. package/templates/api/src/boards/generators/implementations/fal/image/imagen4_preview.py +191 -0
  23. package/templates/api/src/boards/generators/implementations/fal/image/imagen4_preview_fast.py +179 -0
  24. package/templates/api/src/boards/generators/implementations/fal/image/nano_banana.py +183 -0
  25. package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_edit.py +212 -0
  26. package/templates/api/src/boards/generators/implementations/fal/utils.py +61 -0
  27. package/templates/api/src/boards/generators/implementations/fal/video/__init__.py +13 -0
  28. package/templates/api/src/boards/generators/implementations/fal/video/kling_video_v2_5_turbo_pro_text_to_video.py +168 -0
  29. package/templates/api/src/boards/generators/implementations/fal/video/sync_lipsync_v2.py +167 -0
  30. package/templates/api/src/boards/generators/implementations/fal/video/veo31_first_last_frame_to_video.py +180 -0
  31. package/templates/api/src/boards/generators/implementations/openai/__init__.py +1 -0
  32. package/templates/api/src/boards/generators/implementations/openai/audio/__init__.py +1 -0
  33. package/templates/api/src/boards/generators/implementations/{audio → openai/audio}/whisper.py +9 -6
  34. package/templates/api/src/boards/generators/implementations/openai/image/__init__.py +1 -0
  35. package/templates/api/src/boards/generators/implementations/{image → openai/image}/dalle3.py +8 -5
  36. package/templates/api/src/boards/generators/implementations/replicate/__init__.py +1 -0
  37. package/templates/api/src/boards/generators/implementations/replicate/image/__init__.py +1 -0
  38. package/templates/api/src/boards/generators/implementations/{image → replicate/image}/flux_pro.py +8 -5
  39. package/templates/api/src/boards/generators/implementations/replicate/video/__init__.py +1 -0
  40. package/templates/api/src/boards/generators/implementations/{video → replicate/video}/lipsync.py +9 -6
  41. package/templates/api/src/boards/generators/resolution.py +72 -12
  42. package/templates/api/src/boards/jobs/repository.py +49 -0
  43. package/templates/api/src/boards/storage/factory.py +16 -6
  44. package/templates/api/src/boards/workers/actors.py +69 -5
  45. package/templates/api/src/boards/workers/context.py +173 -17
  46. package/templates/web/package.json +1 -1
  47. package/templates/web/src/components/boards/GenerationInput.tsx +154 -52
  48. package/templates/web/src/components/boards/GeneratorSelector.tsx +2 -1
  49. package/templates/api/src/boards/generators/implementations/audio/__init__.py +0 -3
  50. package/templates/api/src/boards/generators/implementations/image/__init__.py +0 -3
  51. package/templates/api/src/boards/generators/implementations/video/__init__.py +0 -3
@@ -36,7 +36,6 @@ classifiers = [
36
36
  ]
37
37
  dependencies = [
38
38
  "sqlalchemy>=2.0.0",
39
- "supabase>=2.0.0",
40
39
  "pydantic>=2.0.0",
41
40
  "pydantic-settings>=2.0.0",
42
41
  "httpx>=0.24.0",
@@ -60,6 +59,52 @@ dependencies = [
60
59
  ]
61
60
 
62
61
  [project.optional-dependencies]
62
+ # Generator providers (per-provider)
63
+ generators-replicate = ["replicate>=1.0.4"]
64
+ generators-openai = ["openai>=1.60.1"]
65
+ generators-fal = ["fal-client>=0.5.0"]
66
+ generators-anthropic = ["anthropic>=0.25.0"]
67
+ generators-together = ["together>=1.0.0"]
68
+
69
+ # Convenience: all generators
70
+ generators-all = [
71
+ "replicate>=1.0.4",
72
+ "openai>=1.60.1",
73
+ "fal-client>=0.5.0",
74
+ "anthropic>=0.25.0",
75
+ "together>=1.0.0",
76
+ ]
77
+
78
+ # Storage providers (per-provider)
79
+ storage-supabase = ["supabase>=2.0.0"]
80
+ storage-s3 = ["boto3>=1.34.0", "aioboto3>=12.0.0"]
81
+ storage-gcs = ["google-cloud-storage>=2.10.0"]
82
+
83
+ # Convenience: all storage backends
84
+ storage-all = [
85
+ "supabase>=2.0.0",
86
+ "boto3>=1.34.0",
87
+ "aioboto3>=12.0.0",
88
+ "google-cloud-storage>=2.10.0",
89
+ ]
90
+
91
+ # Auth providers (only Supabase needs extra deps beyond core httpx/pyjwt)
92
+ auth-supabase = ["supabase>=2.0.0"]
93
+
94
+ # Convenience: everything
95
+ all = [
96
+ "replicate>=1.0.4",
97
+ "openai>=1.60.1",
98
+ "fal-client>=0.5.0",
99
+ "anthropic>=0.25.0",
100
+ "together>=1.0.0",
101
+ "supabase>=2.0.0",
102
+ "boto3>=1.34.0",
103
+ "aioboto3>=12.0.0",
104
+ "google-cloud-storage>=2.10.0",
105
+ ]
106
+
107
+ # Development dependencies
63
108
  dev = [
64
109
  "pytest>=7.0.0",
65
110
  "pytest-asyncio>=0.21.0",
@@ -74,21 +119,15 @@ dev = [
74
119
  "anthropic>=0.25.0",
75
120
  "replicate>=1.0.4",
76
121
  "together>=1.0.0",
122
+ "fal-client>=0.5.0",
77
123
  # Include all storage deps for typecheck
124
+ "supabase>=2.0.0",
78
125
  "boto3>=1.34.0",
79
126
  "aioboto3>=12.0.0",
80
127
  "google-cloud-storage>=2.10.0",
81
128
  # Type stubs
82
129
  "types-redis>=4.6.0",
83
130
  ]
84
- providers = [
85
- "openai>=1.60.1",
86
- "anthropic>=0.25.0",
87
- "replicate>=1.0.4",
88
- "together>=1.0.0",
89
- ]
90
- storage-s3 = ["boto3>=1.34.0", "aioboto3>=12.0.0"]
91
- storage-gcs = ["google-cloud-storage>=2.10.0"]
92
131
 
93
132
  [project.urls]
94
133
  Homepage = "https://github.com/weirdfingers/boards"
@@ -156,6 +195,7 @@ dev-dependencies = [
156
195
  "anthropic>=0.25.0",
157
196
  "replicate>=1.0.4",
158
197
  "together>=1.0.0",
198
+ "fal-client>=0.5.0",
159
199
  # Include all storage deps for typecheck
160
200
  "boto3>=1.34.0",
161
201
  "aioboto3>=12.0.0",
@@ -3,7 +3,7 @@ Boards Backend SDK
3
3
  Open-source creative toolkit for AI-generated content
4
4
  """
5
5
 
6
- __version__ = "0.2.1"
6
+ __version__ = "0.3.0"
7
7
 
8
8
  from .config import settings
9
9
 
@@ -12,13 +12,13 @@ Key components:
12
12
 
13
13
  Example usage:
14
14
  from boards.generators import registry
15
- from boards.generators.implementations.image.flux_pro import FluxProGenerator
15
+ from boards.generators.implementations.replicate.image.flux_pro import ReplicateFluxProGenerator
16
16
 
17
17
  # Get available generators
18
18
  image_generators = registry.list_by_artifact_type("image")
19
19
 
20
20
  # Use a specific generator
21
- flux = registry.get("flux-pro")
21
+ flux = registry.get("replicate-flux-pro")
22
22
  result = await flux.generate(inputs)
23
23
  """
24
24
 
@@ -0,0 +1,380 @@
1
+ """
2
+ Utilities for resolving generation IDs to artifact objects.
3
+
4
+ This module provides utilities for converting generation ID strings (UUIDs)
5
+ into typed artifact objects (ImageArtifact, AudioArtifact, etc.) with proper
6
+ validation of ownership and completion status.
7
+
8
+ The main approach is to automatically detect artifact fields via type introspection
9
+ and resolve them BEFORE Pydantic validation.
10
+ """
11
+
12
+ from typing import Any, TypeVar, get_args, get_origin
13
+ from uuid import UUID
14
+
15
+ from pydantic import BaseModel
16
+ from sqlalchemy.ext.asyncio import AsyncSession
17
+
18
+ from ..dbmodels import Generations
19
+ from ..jobs import repository as jobs_repo
20
+ from ..logging import get_logger
21
+ from .artifacts import AudioArtifact, ImageArtifact, TextArtifact, VideoArtifact
22
+
23
+ logger = get_logger(__name__)
24
+
25
+ # Type variable for artifact types
26
+ TArtifact = TypeVar("TArtifact", ImageArtifact, VideoArtifact, AudioArtifact, TextArtifact)
27
+
28
+ # Set of all artifact types for quick checking
29
+ ARTIFACT_TYPES = {ImageArtifact, VideoArtifact, AudioArtifact, TextArtifact}
30
+
31
+
32
+ def _extract_artifact_type(annotation: Any) -> type[TArtifact] | None:
33
+ """Extract artifact type from a field annotation.
34
+
35
+ Handles both single artifacts (ImageArtifact) and lists (list[ImageArtifact]).
36
+
37
+ Args:
38
+ annotation: The type annotation from a Pydantic field
39
+
40
+ Returns:
41
+ The artifact class if this is an artifact field, None otherwise
42
+
43
+ Examples:
44
+ _extract_artifact_type(ImageArtifact) -> ImageArtifact
45
+ _extract_artifact_type(list[ImageArtifact]) -> ImageArtifact
46
+ _extract_artifact_type(str) -> None
47
+ """
48
+ # Direct artifact type (e.g., ImageArtifact)
49
+ if annotation in ARTIFACT_TYPES:
50
+ return annotation # type: ignore[return-value]
51
+
52
+ # List of artifacts (e.g., list[ImageArtifact])
53
+ origin = get_origin(annotation)
54
+ if origin is list:
55
+ args = get_args(annotation)
56
+ if args and args[0] in ARTIFACT_TYPES:
57
+ return args[0] # type: ignore[return-value]
58
+
59
+ return None
60
+
61
+
62
+ def extract_artifact_fields(schema: type[BaseModel]) -> dict[str, tuple[type[TArtifact], bool]]:
63
+ """Automatically extract artifact fields from a Pydantic schema.
64
+
65
+ Inspects the schema's field annotations and returns a mapping of field names
66
+ to their artifact types and whether they expect a list.
67
+
68
+ Args:
69
+ schema: Pydantic model class to inspect
70
+
71
+ Returns:
72
+ Dictionary mapping field names to (artifact_type, is_list) tuples
73
+
74
+ Example:
75
+ class MyInput(BaseModel):
76
+ prompt: str
77
+ image_source: ImageArtifact
78
+ video_sources: list[VideoArtifact]
79
+
80
+ extract_artifact_fields(MyInput)
81
+ # Returns: {"image_source": (ImageArtifact, False), "video_sources": (VideoArtifact, True)}
82
+ """
83
+ artifact_fields: dict[str, tuple[type[TArtifact], bool]] = {}
84
+
85
+ for field_name, field_info in schema.model_fields.items():
86
+ artifact_type = _extract_artifact_type(field_info.annotation)
87
+ if artifact_type is not None:
88
+ # Check if the field is a list type
89
+ origin = get_origin(field_info.annotation)
90
+ is_list = origin is list
91
+ artifact_fields[field_name] = (artifact_type, is_list)
92
+
93
+ return artifact_fields
94
+
95
+
96
+ def _get_artifact_type_name[T: (ImageArtifact, VideoArtifact, AudioArtifact, TextArtifact)](
97
+ artifact_class: type[T],
98
+ ) -> str:
99
+ """Get the database artifact_type string for an artifact class."""
100
+ type_map = {
101
+ ImageArtifact: "image",
102
+ VideoArtifact: "video",
103
+ AudioArtifact: "audio",
104
+ TextArtifact: "text",
105
+ }
106
+ artifact_type = type_map.get(artifact_class)
107
+ if artifact_type is None:
108
+ raise ValueError(f"Unsupported artifact class: {artifact_class}")
109
+ return artifact_type
110
+
111
+
112
+ def _generation_to_artifact[T: (ImageArtifact, VideoArtifact, AudioArtifact, TextArtifact)](
113
+ generation: Generations, artifact_class: type[T]
114
+ ) -> T:
115
+ """Convert a Generations database record to an artifact object.
116
+
117
+ Args:
118
+ generation: Database generation record
119
+ artifact_class: Target artifact class (ImageArtifact, VideoArtifact, etc.)
120
+
121
+ Returns:
122
+ Artifact object populated from the generation record
123
+
124
+ Raises:
125
+ ValueError: If generation is missing required fields or data
126
+ """
127
+ if not generation.storage_url:
128
+ raise ValueError(
129
+ f"Generation {generation.id} has no storage_url - generation may not be completed"
130
+ )
131
+
132
+ # Get output metadata
133
+ metadata = generation.output_metadata or {}
134
+
135
+ # Build artifact based on type
136
+ if artifact_class == ImageArtifact:
137
+ width = metadata.get("width")
138
+ height = metadata.get("height")
139
+ if width is None or height is None:
140
+ raise ValueError(
141
+ f"Generation {generation.id} missing image dimensions in output_metadata"
142
+ )
143
+ return ImageArtifact(
144
+ generation_id=str(generation.id),
145
+ storage_url=generation.storage_url,
146
+ format=metadata.get("format", "png"),
147
+ width=width,
148
+ height=height,
149
+ ) # type: ignore[return-value]
150
+
151
+ elif artifact_class == VideoArtifact:
152
+ width = metadata.get("width")
153
+ height = metadata.get("height")
154
+ if width is None or height is None:
155
+ raise ValueError(
156
+ f"Generation {generation.id} missing video dimensions in output_metadata"
157
+ )
158
+ return VideoArtifact(
159
+ generation_id=str(generation.id),
160
+ storage_url=generation.storage_url,
161
+ format=metadata.get("format", "mp4"),
162
+ width=width,
163
+ height=height,
164
+ duration=metadata.get("duration"),
165
+ fps=metadata.get("fps"),
166
+ ) # type: ignore[return-value]
167
+
168
+ elif artifact_class == AudioArtifact:
169
+ return AudioArtifact(
170
+ generation_id=str(generation.id),
171
+ storage_url=generation.storage_url,
172
+ format=metadata.get("format", "mp3"),
173
+ duration=metadata.get("duration"),
174
+ sample_rate=metadata.get("sample_rate"),
175
+ channels=metadata.get("channels"),
176
+ ) # type: ignore[return-value]
177
+
178
+ elif artifact_class == TextArtifact:
179
+ content = metadata.get("content")
180
+ if content is None:
181
+ raise ValueError(f"Generation {generation.id} missing text content in output_metadata")
182
+ return TextArtifact(
183
+ generation_id=str(generation.id),
184
+ storage_url=generation.storage_url,
185
+ format=metadata.get("format", "plain"),
186
+ content=content,
187
+ ) # type: ignore[return-value]
188
+
189
+ else:
190
+ raise ValueError(f"Unsupported artifact class: {artifact_class}")
191
+
192
+
193
+ async def resolve_generation_ids_to_artifacts[
194
+ T: (ImageArtifact, VideoArtifact, AudioArtifact, TextArtifact)
195
+ ](
196
+ generation_ids: list[str | UUID],
197
+ artifact_class: type[T],
198
+ session: AsyncSession,
199
+ tenant_id: UUID,
200
+ ) -> list[T]:
201
+ """Convert a list of generation IDs to typed artifact objects.
202
+
203
+ This function:
204
+ 1. Queries the database for each generation ID
205
+ 2. Validates the generation is completed
206
+ 3. Validates the artifact type matches
207
+ 4. Validates the user has access (tenant_id matches)
208
+ 5. Converts to the appropriate artifact object
209
+
210
+ Args:
211
+ generation_ids: List of generation IDs (as strings or UUIDs)
212
+ artifact_class: Target artifact class (ImageArtifact, VideoArtifact, etc.)
213
+ session: Database session for queries
214
+ tenant_id: Tenant ID for access validation
215
+
216
+ Returns:
217
+ List of artifact objects
218
+
219
+ Raises:
220
+ ValueError: If any generation is not found, not completed, wrong type, or access denied
221
+ """
222
+ expected_artifact_type = _get_artifact_type_name(artifact_class)
223
+ artifacts: list[T] = []
224
+
225
+ for gen_id in generation_ids:
226
+ # Query generation from database
227
+ try:
228
+ generation = await jobs_repo.get_generation(session, gen_id)
229
+ except Exception as e:
230
+ raise ValueError(f"Generation {gen_id} not found") from e
231
+
232
+ # Validate tenant access
233
+ if generation.tenant_id != tenant_id:
234
+ raise ValueError(f"Access denied to generation {gen_id} - tenant mismatch")
235
+
236
+ # Validate completion status
237
+ if generation.status != "completed":
238
+ raise ValueError(f"Generation {gen_id} is not completed (status: {generation.status})")
239
+
240
+ # Validate artifact type
241
+ if generation.artifact_type != expected_artifact_type:
242
+ raise ValueError(
243
+ f"Generation {gen_id} has wrong artifact type: "
244
+ f"expected {expected_artifact_type}, got {generation.artifact_type}"
245
+ )
246
+
247
+ # Convert to artifact object
248
+ try:
249
+ artifact = _generation_to_artifact(generation, artifact_class)
250
+ artifacts.append(artifact)
251
+ except ValueError as e:
252
+ raise ValueError(f"Failed to convert generation {gen_id} to artifact: {e}") from e
253
+
254
+ return artifacts
255
+
256
+
257
+ async def resolve_input_artifacts(
258
+ input_params: dict[str, Any],
259
+ schema: type[BaseModel],
260
+ session: AsyncSession,
261
+ tenant_id: UUID,
262
+ ) -> dict[str, Any]:
263
+ """Resolve generation IDs to artifact objects in input parameters.
264
+
265
+ This function automatically detects artifact fields from the Pydantic schema
266
+ via type introspection, then resolves generation ID strings to typed artifact
267
+ objects before Pydantic validation.
268
+
269
+ The function respects the field's type annotation:
270
+ - If field is `ImageArtifact`, input can be a single ID string → returns single artifact
271
+ - If field is `list[ImageArtifact]`, input can be a list of IDs → returns list of artifacts
272
+ (always a list, even if only one ID is provided)
273
+
274
+ Usage in generator input schema (no special declarations needed!):
275
+ class MyGeneratorInput(BaseModel):
276
+ prompt: str = Field(...)
277
+ image_source: ImageArtifact = Field(...) # Automatically detected
278
+ video_sources: list[VideoArtifact] = Field(...) # Also detected
279
+
280
+ Usage in actors.py:
281
+ # Artifacts are automatically detected and resolved
282
+ resolved_params = await resolve_input_artifacts(
283
+ input_params,
284
+ MyGeneratorInput, # Just pass the schema class
285
+ session,
286
+ tenant_id,
287
+ )
288
+ # Now validate with resolved artifacts
289
+ typed_inputs = MyGeneratorInput.model_validate(resolved_params)
290
+
291
+ Args:
292
+ input_params: Raw input parameters dictionary (may contain generation IDs)
293
+ schema: Pydantic model class to inspect for artifact fields
294
+ session: Database session for queries
295
+ tenant_id: Tenant ID for access validation
296
+
297
+ Returns:
298
+ Updated input_params dictionary with generation IDs resolved to artifacts
299
+
300
+ Raises:
301
+ ValueError: If any generation ID cannot be resolved or validated
302
+ """
303
+ # Automatically extract artifact fields from schema
304
+ artifact_field_map = extract_artifact_fields(schema)
305
+
306
+ # If no artifact fields, just return original params
307
+ if not artifact_field_map:
308
+ return input_params
309
+
310
+ # Create a new dict with resolved artifacts
311
+ # Use dict constructor to avoid shallow copy issues
312
+ resolved_params = dict(input_params)
313
+
314
+ for field_name, (artifact_class, expects_list) in artifact_field_map.items():
315
+ field_value = resolved_params.get(field_name)
316
+
317
+ # Skip if field is not present
318
+ if field_value is None:
319
+ continue
320
+
321
+ # Skip if already artifacts (already resolved)
322
+ if isinstance(field_value, list) and all(
323
+ isinstance(item, artifact_class) for item in field_value
324
+ ):
325
+ continue
326
+
327
+ # Also check for single artifact (only if field expects a single artifact)
328
+ if not expects_list and isinstance(field_value, artifact_class):
329
+ continue
330
+
331
+ # Convert field value to list of UUIDs
332
+ generation_ids: list[str | UUID]
333
+ if isinstance(field_value, str):
334
+ # Single generation ID - convert to list for processing
335
+ generation_ids = [field_value]
336
+ elif isinstance(field_value, list):
337
+ # List of generation IDs - ensure all are strings
338
+ # Convert each item to str to ensure type consistency
339
+ generation_ids = [str(item) for item in field_value]
340
+ else:
341
+ raise ValueError(
342
+ f"Field '{field_name}' must be a generation ID (UUID string) "
343
+ f"or list of generation IDs, got: {type(field_value)}"
344
+ )
345
+
346
+ # Resolve to artifacts
347
+ try:
348
+ artifacts = await resolve_generation_ids_to_artifacts(
349
+ generation_ids, artifact_class, session, tenant_id
350
+ )
351
+ except ValueError as e:
352
+ raise ValueError(f"Failed to resolve field '{field_name}': {e}") from e
353
+
354
+ # Update params with resolved artifacts
355
+ # If field expects a single artifact (not a list), unwrap the first artifact
356
+ # Otherwise, keep as a list (even if there's only one artifact)
357
+ if expects_list:
358
+ resolved_value = artifacts
359
+ else:
360
+ # Field expects a single artifact, so unwrap
361
+ if len(artifacts) != 1:
362
+ raise ValueError(
363
+ f"Field '{field_name}' expects a single artifact, "
364
+ f"but got {len(artifacts)} generation IDs"
365
+ )
366
+ resolved_value = artifacts[0]
367
+
368
+ # Debug logging
369
+ logger.debug(
370
+ "Resolved artifact field",
371
+ field_name=field_name,
372
+ expects_list=expects_list,
373
+ artifacts_type=type(artifacts),
374
+ artifacts_len=len(artifacts),
375
+ resolved_value_type=type(resolved_value),
376
+ )
377
+
378
+ resolved_params[field_name] = resolved_value
379
+
380
+ return resolved_params
@@ -96,6 +96,7 @@ class GeneratorExecutionContext(Protocol):
96
96
  format: str,
97
97
  width: int,
98
98
  height: int,
99
+ output_index: int = 0,
99
100
  ) -> ImageArtifact:
100
101
  """Store an image result to permanent storage."""
101
102
  ...
@@ -108,6 +109,7 @@ class GeneratorExecutionContext(Protocol):
108
109
  height: int,
109
110
  duration: float | None = None,
110
111
  fps: float | None = None,
112
+ output_index: int = 0,
111
113
  ) -> VideoArtifact:
112
114
  """Store a video result to permanent storage."""
113
115
  ...
@@ -119,6 +121,7 @@ class GeneratorExecutionContext(Protocol):
119
121
  duration: float | None = None,
120
122
  sample_rate: int | None = None,
121
123
  channels: int | None = None,
124
+ output_index: int = 0,
122
125
  ) -> AudioArtifact:
123
126
  """Store an audio result to permanent storage."""
124
127
  ...
@@ -127,6 +130,7 @@ class GeneratorExecutionContext(Protocol):
127
130
  self,
128
131
  content: str,
129
132
  format: str,
133
+ output_index: int = 0,
130
134
  ) -> TextArtifact:
131
135
  """Store a text result to permanent storage."""
132
136
  ...
@@ -4,9 +4,11 @@ Built-in generator implementations for the Boards system.
4
4
  This package contains example generators that demonstrate how to integrate
5
5
  various AI services using their native SDKs.
6
6
 
7
+ Generators are organized by provider (Replicate, Fal, OpenAI, etc.).
8
+
7
9
  Import this module to automatically register all built-in generators:
8
10
  import boards.generators.implementations
9
11
  """
10
12
 
11
- # Import all generator modules to trigger registration
12
- from . import audio, image, video
13
+ # Import all provider modules to make them available
14
+ from . import fal, openai, replicate
@@ -0,0 +1,25 @@
1
+ """Fal.ai provider generators."""
2
+
3
+ from . import audio, image, video
4
+ from .image import (
5
+ FalFluxProUltraGenerator,
6
+ FalNanoBananaEditGenerator,
7
+ FalNanoBananaGenerator,
8
+ )
9
+ from .video import (
10
+ FalKlingVideoV25TurboProTextToVideoGenerator,
11
+ FalSyncLipsyncV2Generator,
12
+ FalVeo31FirstLastFrameToVideoGenerator,
13
+ )
14
+
15
+ # Maintain alphabetical order
16
+ __all__ = [
17
+ # Image generators
18
+ "FalFluxProUltraGenerator",
19
+ "FalNanoBananaEditGenerator",
20
+ "FalNanoBananaGenerator",
21
+ # Video generators
22
+ "FalKlingVideoV25TurboProTextToVideoGenerator",
23
+ "FalSyncLipsyncV2Generator",
24
+ "FalVeo31FirstLastFrameToVideoGenerator",
25
+ ]
@@ -0,0 +1,4 @@
1
+ from .minimax_music_v2 import FalMinimaxMusicV2Generator
2
+ from .minimax_speech_2_6_turbo import FalMinimaxSpeech26TurboGenerator
3
+
4
+ __all__ = ["FalMinimaxSpeech26TurboGenerator", "FalMinimaxMusicV2Generator"]