@weirdfingers/baseboards 0.5.2 → 0.6.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 (76) hide show
  1. package/README.md +4 -1
  2. package/dist/index.js +131 -11
  3. package/dist/index.js.map +1 -1
  4. package/package.json +1 -1
  5. package/templates/api/alembic/env.py +9 -1
  6. package/templates/api/alembic/versions/20250101_000000_initial_schema.py +107 -49
  7. package/templates/api/alembic/versions/20251022_174729_remove_provider_name_from_generations.py +7 -3
  8. package/templates/api/alembic/versions/20251023_165852_switch_to_declarative_base_and_mapping.py +57 -1
  9. package/templates/api/alembic/versions/20251202_000000_add_artifact_lineage.py +134 -0
  10. package/templates/api/alembic/versions/2025925_62735_add_seed_data_for_default_tenant.py +8 -5
  11. package/templates/api/config/generators.yaml +111 -0
  12. package/templates/api/src/boards/__init__.py +1 -1
  13. package/templates/api/src/boards/api/app.py +2 -1
  14. package/templates/api/src/boards/api/endpoints/tenant_registration.py +1 -1
  15. package/templates/api/src/boards/api/endpoints/uploads.py +150 -0
  16. package/templates/api/src/boards/auth/factory.py +1 -1
  17. package/templates/api/src/boards/dbmodels/__init__.py +8 -22
  18. package/templates/api/src/boards/generators/artifact_resolution.py +45 -12
  19. package/templates/api/src/boards/generators/implementations/fal/audio/__init__.py +16 -1
  20. package/templates/api/src/boards/generators/implementations/fal/audio/beatoven_music_generation.py +171 -0
  21. package/templates/api/src/boards/generators/implementations/fal/audio/beatoven_sound_effect_generation.py +167 -0
  22. package/templates/api/src/boards/generators/implementations/fal/audio/elevenlabs_sound_effects_v2.py +194 -0
  23. package/templates/api/src/boards/generators/implementations/fal/audio/elevenlabs_tts_eleven_v3.py +209 -0
  24. package/templates/api/src/boards/generators/implementations/fal/audio/fal_elevenlabs_tts_turbo_v2_5.py +206 -0
  25. package/templates/api/src/boards/generators/implementations/fal/audio/fal_minimax_speech_26_hd.py +237 -0
  26. package/templates/api/src/boards/generators/implementations/fal/audio/minimax_speech_2_6_turbo.py +1 -1
  27. package/templates/api/src/boards/generators/implementations/fal/image/__init__.py +30 -0
  28. package/templates/api/src/boards/generators/implementations/fal/image/clarity_upscaler.py +220 -0
  29. package/templates/api/src/boards/generators/implementations/fal/image/crystal_upscaler.py +173 -0
  30. package/templates/api/src/boards/generators/implementations/fal/image/fal_ideogram_character.py +227 -0
  31. package/templates/api/src/boards/generators/implementations/fal/image/flux_2.py +203 -0
  32. package/templates/api/src/boards/generators/implementations/fal/image/flux_2_edit.py +230 -0
  33. package/templates/api/src/boards/generators/implementations/fal/image/flux_2_pro.py +204 -0
  34. package/templates/api/src/boards/generators/implementations/fal/image/flux_2_pro_edit.py +221 -0
  35. package/templates/api/src/boards/generators/implementations/fal/image/gemini_25_flash_image.py +177 -0
  36. package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_edit_image.py +182 -0
  37. package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_mini.py +167 -0
  38. package/templates/api/src/boards/generators/implementations/fal/image/ideogram_character_edit.py +299 -0
  39. package/templates/api/src/boards/generators/implementations/fal/image/ideogram_v2.py +190 -0
  40. package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_pro_edit.py +226 -0
  41. package/templates/api/src/boards/generators/implementations/fal/image/qwen_image.py +249 -0
  42. package/templates/api/src/boards/generators/implementations/fal/image/qwen_image_edit.py +244 -0
  43. package/templates/api/src/boards/generators/implementations/fal/video/__init__.py +42 -0
  44. package/templates/api/src/boards/generators/implementations/fal/video/bytedance_seedance_v1_pro_text_to_video.py +209 -0
  45. package/templates/api/src/boards/generators/implementations/fal/video/creatify_lipsync.py +161 -0
  46. package/templates/api/src/boards/generators/implementations/fal/video/fal_bytedance_seedance_v1_pro_image_to_video.py +222 -0
  47. package/templates/api/src/boards/generators/implementations/fal/video/fal_minimax_hailuo_02_standard_text_to_video.py +152 -0
  48. package/templates/api/src/boards/generators/implementations/fal/video/fal_pixverse_lipsync.py +197 -0
  49. package/templates/api/src/boards/generators/implementations/fal/video/fal_sora_2_text_to_video.py +173 -0
  50. package/templates/api/src/boards/generators/implementations/fal/video/infinitalk.py +221 -0
  51. package/templates/api/src/boards/generators/implementations/fal/video/kling_video_v2_5_turbo_pro_image_to_video.py +175 -0
  52. package/templates/api/src/boards/generators/implementations/fal/video/minimax_hailuo_2_3_pro_image_to_video.py +153 -0
  53. package/templates/api/src/boards/generators/implementations/fal/video/sora2_image_to_video.py +172 -0
  54. package/templates/api/src/boards/generators/implementations/fal/video/sora_2_image_to_video_pro.py +175 -0
  55. package/templates/api/src/boards/generators/implementations/fal/video/sora_2_text_to_video_pro.py +163 -0
  56. package/templates/api/src/boards/generators/implementations/fal/video/sync_lipsync_v2_pro.py +155 -0
  57. package/templates/api/src/boards/generators/implementations/fal/video/veed_lipsync.py +174 -0
  58. package/templates/api/src/boards/generators/implementations/fal/video/veo3.py +194 -0
  59. package/templates/api/src/boards/generators/implementations/fal/video/veo31_first_last_frame_to_video.py +1 -1
  60. package/templates/api/src/boards/generators/implementations/fal/video/wan_pro_image_to_video.py +158 -0
  61. package/templates/api/src/boards/graphql/access_control.py +1 -1
  62. package/templates/api/src/boards/graphql/mutations/root.py +16 -4
  63. package/templates/api/src/boards/graphql/resolvers/board.py +0 -2
  64. package/templates/api/src/boards/graphql/resolvers/generation.py +10 -233
  65. package/templates/api/src/boards/graphql/resolvers/lineage.py +381 -0
  66. package/templates/api/src/boards/graphql/resolvers/upload.py +463 -0
  67. package/templates/api/src/boards/graphql/types/generation.py +62 -26
  68. package/templates/api/src/boards/middleware.py +1 -1
  69. package/templates/api/src/boards/storage/factory.py +2 -2
  70. package/templates/api/src/boards/tenant_isolation.py +9 -9
  71. package/templates/api/src/boards/workers/actors.py +10 -1
  72. package/templates/web/package.json +1 -1
  73. package/templates/web/src/app/boards/[boardId]/page.tsx +14 -5
  74. package/templates/web/src/app/lineage/[generationId]/page.tsx +233 -0
  75. package/templates/web/src/components/boards/ArtifactPreview.tsx +20 -1
  76. package/templates/web/src/components/boards/UploadArtifact.tsx +253 -0
@@ -11,24 +11,99 @@ allow_unlisted: false
11
11
  # NOTE: Please keep generators in alphabetical order by class name for easier maintenance
12
12
  generators:
13
13
  # Fal.ai generators
14
+ - class: "boards.generators.implementations.fal.audio.beatoven_music_generation.FalBeatovenMusicGenerationGenerator"
15
+ enabled: true
16
+
17
+ - class: "boards.generators.implementations.fal.audio.beatoven_sound_effect_generation.FalBeatovenSoundEffectGenerationGenerator"
18
+ enabled: true
19
+
20
+ - class: "boards.generators.implementations.fal.image.clarity_upscaler.FalClarityUpscalerGenerator"
21
+ enabled: true
22
+
23
+ - class: "boards.generators.implementations.fal.image.crystal_upscaler.FalCrystalUpscalerGenerator"
24
+ enabled: true
25
+
26
+ - class: "boards.generators.implementations.fal.video.creatify_lipsync.FalCreatifyLipsyncGenerator"
27
+ enabled: true
28
+
29
+ - class: "boards.generators.implementations.fal.video.fal_bytedance_seedance_v1_pro_image_to_video.FalBytedanceSeedanceV1ProImageToVideoGenerator"
30
+ enabled: true
31
+
32
+ - class: "boards.generators.implementations.fal.video.bytedance_seedance_v1_pro_text_to_video.FalBytedanceSeedanceV1ProTextToVideoGenerator"
33
+ enabled: true
34
+
35
+ - class: "boards.generators.implementations.fal.audio.elevenlabs_tts_eleven_v3.FalElevenlabsTtsElevenV3Generator"
36
+ enabled: true
37
+
38
+ - class: "boards.generators.implementations.fal.audio.fal_elevenlabs_tts_turbo_v2_5.FalElevenlabsTtsTurboV25Generator"
39
+ enabled: true
40
+
41
+ - class: "boards.generators.implementations.fal.image.flux_2.FalFlux2Generator"
42
+ enabled: true
43
+
44
+ - class: "boards.generators.implementations.fal.image.flux_2_edit.FalFlux2EditGenerator"
45
+ enabled: true
46
+
47
+ - class: "boards.generators.implementations.fal.image.flux_2_pro.FalFlux2ProGenerator"
48
+ enabled: true
49
+
50
+ - class: "boards.generators.implementations.fal.image.flux_2_pro_edit.FalFlux2ProEditGenerator"
51
+ enabled: true
52
+
14
53
  - class: "boards.generators.implementations.fal.image.flux_pro_kontext.FalFluxProKontextGenerator"
15
54
  enabled: true
16
55
 
17
56
  - class: "boards.generators.implementations.fal.image.flux_pro_ultra.FalFluxProUltraGenerator"
18
57
  enabled: true
19
58
 
59
+ - class: "boards.generators.implementations.fal.image.gemini_25_flash_image.FalGemini25FlashImageGenerator"
60
+ enabled: true
61
+
62
+ - class: "boards.generators.implementations.fal.image.gpt_image_1_edit_image.FalGptImage1EditImageGenerator"
63
+ enabled: true
64
+
65
+ - class: "boards.generators.implementations.fal.image.gpt_image_1_mini.FalGptImage1MiniGenerator"
66
+ enabled: true
67
+
68
+ - class: "boards.generators.implementations.fal.image.fal_ideogram_character.FalIdeogramCharacterGenerator"
69
+ enabled: true
70
+
71
+ - class: "boards.generators.implementations.fal.image.ideogram_character_edit.FalIdeogramCharacterEditGenerator"
72
+ enabled: true
73
+
74
+ - class: "boards.generators.implementations.fal.image.ideogram_v2.FalIdeogramV2Generator"
75
+ enabled: true
76
+
20
77
  - class: "boards.generators.implementations.fal.image.imagen4_preview_fast.FalImagen4PreviewFastGenerator"
21
78
  enabled: true
22
79
 
23
80
  - class: "boards.generators.implementations.fal.image.imagen4_preview.FalImagen4PreviewGenerator"
24
81
  enabled: true
25
82
 
83
+ - class: "boards.generators.implementations.fal.audio.elevenlabs_sound_effects_v2.FalElevenlabsSoundEffectsV2Generator"
84
+ enabled: true
85
+
86
+ - class: "boards.generators.implementations.fal.video.infinitalk.FalInfinitalkGenerator"
87
+ enabled: true
88
+
89
+ - class: "boards.generators.implementations.fal.video.kling_video_v2_5_turbo_pro_image_to_video.FalKlingVideoV25TurboProImageToVideoGenerator"
90
+ enabled: true
91
+
26
92
  - class: "boards.generators.implementations.fal.video.kling_video_v2_5_turbo_pro_text_to_video.FalKlingVideoV25TurboProTextToVideoGenerator"
27
93
  enabled: true
28
94
 
95
+ - class: "boards.generators.implementations.fal.video.fal_minimax_hailuo_02_standard_text_to_video.FalMinimaxHailuo02StandardTextToVideoGenerator"
96
+ enabled: true
97
+
98
+ - class: "boards.generators.implementations.fal.video.minimax_hailuo_2_3_pro_image_to_video.FalMinimaxHailuo23ProImageToVideoGenerator"
99
+ enabled: true
100
+
29
101
  - class: "boards.generators.implementations.fal.audio.minimax_music_v2.FalMinimaxMusicV2Generator"
30
102
  enabled: true
31
103
 
104
+ - class: "boards.generators.implementations.fal.audio.fal_minimax_speech_26_hd.FalMinimaxSpeech26HdGenerator"
105
+ enabled: true
106
+
32
107
  - class: "boards.generators.implementations.fal.audio.minimax_speech_2_6_turbo.FalMinimaxSpeech26TurboGenerator"
33
108
  enabled: true
34
109
 
@@ -41,9 +116,42 @@ generators:
41
116
  - class: "boards.generators.implementations.fal.image.nano_banana_pro.FalNanoBananaProGenerator"
42
117
  enabled: true
43
118
 
119
+ - class: "boards.generators.implementations.fal.image.nano_banana_pro_edit.FalNanoBananaProEditGenerator"
120
+ enabled: true
121
+
122
+ - class: "boards.generators.implementations.fal.video.fal_pixverse_lipsync.FalPixverseLipsyncGenerator"
123
+ enabled: true
124
+
125
+ - class: "boards.generators.implementations.fal.image.qwen_image_edit.FalQwenImageEditGenerator"
126
+ enabled: true
127
+
128
+ - class: "boards.generators.implementations.fal.image.qwen_image.FalQwenImageGenerator"
129
+ enabled: true
130
+
131
+ - class: "boards.generators.implementations.fal.video.sora_2_text_to_video_pro.FalSora2TextToVideoProGenerator"
132
+ enabled: true
133
+
134
+ - class: "boards.generators.implementations.fal.video.fal_sora_2_text_to_video.FalSora2TextToVideoGenerator"
135
+ enabled: true
136
+
137
+ - class: "boards.generators.implementations.fal.video.sora2_image_to_video.FalSora2ImageToVideoGenerator"
138
+ enabled: true
139
+
140
+ - class: "boards.generators.implementations.fal.video.sora_2_image_to_video_pro.FalSora2ImageToVideoProGenerator"
141
+ enabled: true
142
+
44
143
  - class: "boards.generators.implementations.fal.video.sync_lipsync_v2.FalSyncLipsyncV2Generator"
45
144
  enabled: true
46
145
 
146
+ - class: "boards.generators.implementations.fal.video.veed_lipsync.FalVeedLipsyncGenerator"
147
+ enabled: true
148
+
149
+ - class: "boards.generators.implementations.fal.video.sync_lipsync_v2_pro.FalSyncLipsyncV2ProGenerator"
150
+ enabled: true
151
+
152
+ - class: "boards.generators.implementations.fal.video.veo3.FalVeo3Generator"
153
+ enabled: true
154
+
47
155
  - class: "boards.generators.implementations.fal.video.veo31_first_last_frame_to_video.FalVeo31FirstLastFrameToVideoGenerator"
48
156
  enabled: true
49
157
 
@@ -53,6 +161,9 @@ generators:
53
161
  - class: "boards.generators.implementations.fal.video.veo31_reference_to_video.FalVeo31ReferenceToVideoGenerator"
54
162
  enabled: true
55
163
 
164
+ - class: "boards.generators.implementations.fal.video.wan_pro_image_to_video.FalWanProImageToVideoGenerator"
165
+ enabled: true
166
+
56
167
  # OpenAI generators
57
168
  - class: "boards.generators.implementations.openai.image.dalle3.OpenAIDallE3Generator"
58
169
  enabled: true
@@ -3,7 +3,7 @@ Boards Backend SDK
3
3
  Open-source creative toolkit for AI-generated content
4
4
  """
5
5
 
6
- __version__ = "0.5.2"
6
+ __version__ = "0.6.0"
7
7
 
8
8
  from .config import settings
9
9
 
@@ -142,13 +142,14 @@ def create_app() -> FastAPI:
142
142
  raise
143
143
 
144
144
  # REST API endpoints (for SSE, webhooks, etc.)
145
- from .endpoints import jobs, setup, sse, storage, tenant_registration, webhooks
145
+ from .endpoints import jobs, setup, sse, storage, tenant_registration, uploads, webhooks
146
146
 
147
147
  app.include_router(sse.router, prefix="/api/sse", tags=["SSE"])
148
148
  app.include_router(jobs.router, prefix="/api/jobs", tags=["Jobs"])
149
149
  app.include_router(webhooks.router, prefix="/api/webhooks", tags=["Webhooks"])
150
150
  app.include_router(storage.router, prefix="/api/storage", tags=["Storage"])
151
151
  app.include_router(setup.router, prefix="/api/setup", tags=["Setup"])
152
+ app.include_router(uploads.router, prefix="/api", tags=["Uploads"])
152
153
  app.include_router(
153
154
  tenant_registration.router, prefix="/api/tenants", tags=["Tenant Registration"]
154
155
  )
@@ -69,7 +69,7 @@ class TenantRegistrationRequest(BaseModel):
69
69
  if v is not None:
70
70
  valid_sizes = {"small", "medium", "large", "enterprise"}
71
71
  if v.lower() not in valid_sizes:
72
- raise ValueError(f'Organization size must be one of: {", ".join(valid_sizes)}')
72
+ raise ValueError(f"Organization size must be one of: {', '.join(valid_sizes)}")
73
73
  return v.lower() if v else None
74
74
 
75
75
 
@@ -0,0 +1,150 @@
1
+ """File upload endpoints for artifact uploads."""
2
+
3
+ import os
4
+ from typing import Annotated
5
+ from uuid import UUID
6
+
7
+ from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile
8
+
9
+ from ...auth import get_auth_context
10
+ from ...auth.context import AuthContext
11
+ from ...config import settings
12
+ from ...logging import get_logger
13
+
14
+ router = APIRouter(prefix="/uploads", tags=["uploads"])
15
+ logger = get_logger(__name__)
16
+
17
+
18
+ @router.post("/artifact")
19
+ async def upload_artifact_file(
20
+ board_id: Annotated[str, Form()],
21
+ artifact_type: Annotated[str, Form()], # image, video, audio, text
22
+ file: UploadFile = File(...),
23
+ user_description: Annotated[str | None, Form()] = None,
24
+ parent_generation_id: Annotated[str | None, Form()] = None,
25
+ auth_context: AuthContext = Depends(get_auth_context),
26
+ ) -> dict:
27
+ """
28
+ Upload artifact file (synchronous).
29
+
30
+ Args:
31
+ board_id: UUID of the board to upload to
32
+ artifact_type: Type of artifact (image, video, audio, text)
33
+ file: The file to upload
34
+ user_description: Optional description provided by user
35
+ parent_generation_id: Optional parent generation UUID
36
+ auth_context: Authentication context
37
+
38
+ Returns:
39
+ Generation object as JSON
40
+
41
+ Raises:
42
+ HTTPException: If validation fails or upload errors occur
43
+ """
44
+ from ...graphql.resolvers.upload import upload_artifact_from_file
45
+
46
+ # Validate authentication
47
+ if not auth_context.is_authenticated or not auth_context.user_id:
48
+ raise HTTPException(
49
+ status_code=401,
50
+ detail="Authentication required",
51
+ headers={"WWW-Authenticate": "Bearer"},
52
+ )
53
+
54
+ # Validate artifact type
55
+ valid_types = {"image", "video", "audio", "text"}
56
+ if artifact_type not in valid_types:
57
+ raise HTTPException(
58
+ status_code=400,
59
+ detail=f"Invalid artifact_type. Must be one of: {', '.join(valid_types)}",
60
+ )
61
+
62
+ # Read file content
63
+ try:
64
+ content = await file.read()
65
+ except Exception as e:
66
+ logger.error("Failed to read uploaded file", error=str(e), filename=file.filename)
67
+ raise HTTPException(
68
+ status_code=400,
69
+ detail="Failed to read uploaded file",
70
+ ) from e
71
+
72
+ # Validate file size
73
+ if len(content) > settings.max_upload_size:
74
+ raise HTTPException(
75
+ status_code=413,
76
+ detail=(
77
+ f"File size {len(content)} bytes exceeds maximum allowed size "
78
+ f"of {settings.max_upload_size} bytes"
79
+ ),
80
+ )
81
+
82
+ # Validate extension
83
+ file_ext = os.path.splitext(file.filename or "")[1].lower()
84
+ if file_ext and file_ext not in settings.allowed_upload_extensions:
85
+ allowed_exts = ", ".join(settings.allowed_upload_extensions)
86
+ raise HTTPException(
87
+ status_code=400,
88
+ detail=(
89
+ f"File extension '{file_ext}' is not allowed. "
90
+ f"Allowed extensions: {allowed_exts}"
91
+ ),
92
+ )
93
+
94
+ # Parse UUIDs
95
+ try:
96
+ board_uuid = UUID(board_id)
97
+ parent_uuid = UUID(parent_generation_id) if parent_generation_id else None
98
+ except ValueError as e:
99
+ logger.warning("Invalid UUID provided", board_id=board_id, error=str(e))
100
+ raise HTTPException(
101
+ status_code=400,
102
+ detail="Invalid board_id or parent_generation_id format",
103
+ ) from e
104
+
105
+ # Call resolver
106
+ try:
107
+ generation = await upload_artifact_from_file(
108
+ auth_context=auth_context,
109
+ board_id=board_uuid,
110
+ artifact_type=artifact_type,
111
+ file_content=content,
112
+ filename=file.filename,
113
+ content_type=file.content_type,
114
+ user_description=user_description,
115
+ parent_generation_id=parent_uuid,
116
+ )
117
+
118
+ logger.info(
119
+ "File upload successful",
120
+ generation_id=str(generation.id),
121
+ artifact_type=artifact_type,
122
+ file_size=len(content),
123
+ )
124
+
125
+ return {
126
+ "id": str(generation.id),
127
+ "status": generation.status.value,
128
+ "storageUrl": generation.storage_url,
129
+ "thumbnailUrl": generation.thumbnail_url,
130
+ "artifactType": generation.artifact_type.value,
131
+ "generatorName": generation.generator_name,
132
+ }
133
+
134
+ except RuntimeError as e:
135
+ # These are expected errors (permission denied, board not found, etc.)
136
+ # Pass through the message since these are safe, user-facing errors
137
+ logger.warning("Upload failed", error=str(e))
138
+ raise HTTPException(status_code=400, detail=str(e)) from e
139
+ except Exception as e:
140
+ # Unexpected errors - don't expose internal details
141
+ logger.error(
142
+ "Unexpected error during upload",
143
+ error=str(e),
144
+ board_id=board_id,
145
+ artifact_type=artifact_type,
146
+ )
147
+ raise HTTPException(
148
+ status_code=500,
149
+ detail="An unexpected error occurred during upload",
150
+ ) from e
@@ -75,7 +75,7 @@ def get_auth_adapter() -> AuthAdapter:
75
75
  secret_key = config.get("secret_key") or os.getenv("CLERK_SECRET_KEY")
76
76
  if not secret_key:
77
77
  raise ValueError(
78
- "Clerk secret key is required. " "Set CLERK_SECRET_KEY or provide in config."
78
+ "Clerk secret key is required. Set CLERK_SECRET_KEY or provide in config."
79
79
  )
80
80
 
81
81
  return ClerkAuthAdapter(
@@ -11,7 +11,6 @@ from typing import Any
11
11
  from uuid import UUID
12
12
 
13
13
  from sqlalchemy import (
14
- ARRAY,
15
14
  Boolean,
16
15
  CheckConstraint,
17
16
  Column,
@@ -43,7 +42,7 @@ naming_convention = {
43
42
  class Base(DeclarativeBase):
44
43
  """Base class for all database models with type checking support."""
45
44
 
46
- metadata = MetaData(naming_convention=naming_convention)
45
+ metadata = MetaData(naming_convention=naming_convention, schema="boards")
47
46
 
48
47
 
49
48
  class Tenants(Base):
@@ -322,11 +321,6 @@ class Generations(Base):
322
321
  ondelete="CASCADE",
323
322
  name="generations_board_id_fkey",
324
323
  ),
325
- ForeignKeyConstraint(
326
- ["parent_generation_id"],
327
- ["generations.id"],
328
- name="generations_parent_generation_id_fkey",
329
- ),
330
324
  ForeignKeyConstraint(
331
325
  ["tenant_id"],
332
326
  ["tenants.id"],
@@ -341,10 +335,14 @@ class Generations(Base):
341
335
  ),
342
336
  PrimaryKeyConstraint("id", name="generations_pkey"),
343
337
  Index("idx_generations_board", "board_id"),
344
- Index("idx_generations_lineage", "parent_generation_id"),
345
338
  Index("idx_generations_status", "status"),
346
339
  Index("idx_generations_tenant", "tenant_id"),
347
340
  Index("idx_generations_user", "user_id"),
341
+ Index(
342
+ "idx_generations_input_artifacts_gin",
343
+ "input_artifacts",
344
+ postgresql_using="gin",
345
+ ),
348
346
  )
349
347
 
350
348
  id: Mapped[UUID] = mapped_column(Uuid, server_default=text("uuid_generate_v4()"))
@@ -363,9 +361,8 @@ class Generations(Base):
363
361
  output_metadata: Mapped[dict[str, Any]] = mapped_column(
364
362
  JSONB, server_default=text("'{}'::jsonb")
365
363
  )
366
- parent_generation_id: Mapped[UUID | None] = mapped_column(Uuid)
367
- input_generation_ids: Mapped[list[UUID]] = mapped_column(
368
- ARRAY(Uuid()), server_default=text("'{}'::uuid[]")
364
+ input_artifacts: Mapped[list[dict[str, Any]]] = mapped_column(
365
+ JSONB, server_default=text("'[]'::jsonb")
369
366
  )
370
367
  external_job_id: Mapped[str | None] = mapped_column(String(255))
371
368
  progress: Mapped[Decimal] = mapped_column(Numeric(5, 2), server_default=text("0.0"))
@@ -380,17 +377,6 @@ class Generations(Base):
380
377
  )
381
378
 
382
379
  board: Mapped["Boards"] = relationship("Boards", back_populates="generations")
383
- parent_generation: Mapped["Generations | None"] = relationship(
384
- "Generations",
385
- remote_side="Generations.id",
386
- back_populates="parent_generation_reverse",
387
- )
388
- parent_generation_reverse: Mapped[list["Generations"]] = relationship(
389
- "Generations",
390
- uselist=True,
391
- remote_side="Generations.parent_generation_id",
392
- back_populates="parent_generation",
393
- )
394
380
  tenant: Mapped["Tenants"] = relationship("Tenants", back_populates="generations")
395
381
  user: Mapped["Users"] = relationship("Users", back_populates="generations")
396
382
  credit_transactions: Mapped[list["CreditTransactions"]] = relationship(
@@ -59,7 +59,9 @@ def _extract_artifact_type(annotation: Any) -> type[TArtifact] | None:
59
59
  return None
60
60
 
61
61
 
62
- def extract_artifact_fields(schema: type[BaseModel]) -> dict[str, tuple[type[TArtifact], bool]]:
62
+ def extract_artifact_fields(
63
+ schema: type[BaseModel],
64
+ ) -> dict[str, tuple[type[TArtifact], bool]]:
63
65
  """Automatically extract artifact fields from a Pydantic schema.
64
66
 
65
67
  Inspects the schema's field annotations and returns a mapping of field names
@@ -93,7 +95,14 @@ def extract_artifact_fields(schema: type[BaseModel]) -> dict[str, tuple[type[TAr
93
95
  return artifact_fields
94
96
 
95
97
 
96
- def _get_artifact_type_name[T: (ImageArtifact, VideoArtifact, AudioArtifact, TextArtifact)](
98
+ def _get_artifact_type_name[
99
+ T: (
100
+ ImageArtifact,
101
+ VideoArtifact,
102
+ AudioArtifact,
103
+ TextArtifact,
104
+ )
105
+ ](
97
106
  artifact_class: type[T],
98
107
  ) -> str:
99
108
  """Get the database artifact_type string for an artifact class."""
@@ -109,9 +118,14 @@ def _get_artifact_type_name[T: (ImageArtifact, VideoArtifact, AudioArtifact, Tex
109
118
  return artifact_type
110
119
 
111
120
 
112
- def _generation_to_artifact[T: (ImageArtifact, VideoArtifact, AudioArtifact, TextArtifact)](
113
- generation: Generations, artifact_class: type[T]
114
- ) -> T:
121
+ def _generation_to_artifact[
122
+ T: (
123
+ ImageArtifact,
124
+ VideoArtifact,
125
+ AudioArtifact,
126
+ TextArtifact,
127
+ )
128
+ ](generation: Generations, artifact_class: type[T]) -> T:
115
129
  """Convert a Generations database record to an artifact object.
116
130
 
117
131
  Args:
@@ -183,7 +197,12 @@ def _generation_to_artifact[T: (ImageArtifact, VideoArtifact, AudioArtifact, Tex
183
197
 
184
198
 
185
199
  async def resolve_generation_ids_to_artifacts[
186
- T: (ImageArtifact, VideoArtifact, AudioArtifact, TextArtifact)
200
+ T: (
201
+ ImageArtifact,
202
+ VideoArtifact,
203
+ AudioArtifact,
204
+ TextArtifact,
205
+ )
187
206
  ](
188
207
  generation_ids: list[str | UUID],
189
208
  artifact_class: type[T],
@@ -251,7 +270,7 @@ async def resolve_input_artifacts(
251
270
  schema: type[BaseModel],
252
271
  session: AsyncSession,
253
272
  tenant_id: UUID,
254
- ) -> dict[str, Any]:
273
+ ) -> tuple[dict[str, Any], list[dict[str, Any]]]:
255
274
  """Resolve generation IDs to artifact objects in input parameters.
256
275
 
257
276
  This function automatically detects artifact fields from the Pydantic schema
@@ -271,7 +290,7 @@ async def resolve_input_artifacts(
271
290
 
272
291
  Usage in actors.py:
273
292
  # Artifacts are automatically detected and resolved
274
- resolved_params = await resolve_input_artifacts(
293
+ resolved_params, lineage_metadata = await resolve_input_artifacts(
275
294
  input_params,
276
295
  MyGeneratorInput, # Just pass the schema class
277
296
  session,
@@ -287,7 +306,9 @@ async def resolve_input_artifacts(
287
306
  tenant_id: Tenant ID for access validation
288
307
 
289
308
  Returns:
290
- Updated input_params dictionary with generation IDs resolved to artifacts
309
+ Tuple of (resolved_params, lineage_metadata):
310
+ - resolved_params: Updated input_params dictionary with generation IDs resolved to artifacts
311
+ - lineage_metadata: List of dicts with generation_id, role, and artifact_type for lineage
291
312
 
292
313
  Raises:
293
314
  ValueError: If any generation ID cannot be resolved or validated
@@ -295,13 +316,14 @@ async def resolve_input_artifacts(
295
316
  # Automatically extract artifact fields from schema
296
317
  artifact_field_map = extract_artifact_fields(schema)
297
318
 
298
- # If no artifact fields, just return original params
319
+ # If no artifact fields, just return original params with empty lineage
299
320
  if not artifact_field_map:
300
- return input_params
321
+ return input_params, []
301
322
 
302
323
  # Create a new dict with resolved artifacts
303
324
  # Use dict constructor to avoid shallow copy issues
304
325
  resolved_params = dict(input_params)
326
+ lineage_metadata: list[dict[str, Any]] = []
305
327
 
306
328
  for field_name, (artifact_class, expects_list) in artifact_field_map.items():
307
329
  field_value = resolved_params.get(field_name)
@@ -343,6 +365,17 @@ async def resolve_input_artifacts(
343
365
  except ValueError as e:
344
366
  raise ValueError(f"Failed to resolve field '{field_name}': {e}") from e
345
367
 
368
+ # Capture lineage metadata for each artifact
369
+ artifact_type_name = _get_artifact_type_name(artifact_class)
370
+ for gen_id in generation_ids:
371
+ lineage_metadata.append(
372
+ {
373
+ "generation_id": str(gen_id),
374
+ "role": field_name, # Field name IS the role!
375
+ "artifact_type": artifact_type_name,
376
+ }
377
+ )
378
+
346
379
  # Update params with resolved artifacts
347
380
  # If field expects a single artifact (not a list), unwrap the first artifact
348
381
  # Otherwise, keep as a list (even if there's only one artifact)
@@ -369,4 +402,4 @@ async def resolve_input_artifacts(
369
402
 
370
403
  resolved_params[field_name] = resolved_value
371
404
 
372
- return resolved_params
405
+ return resolved_params, lineage_metadata
@@ -1,4 +1,19 @@
1
+ from .beatoven_music_generation import FalBeatovenMusicGenerationGenerator
2
+ from .beatoven_sound_effect_generation import FalBeatovenSoundEffectGenerationGenerator
3
+ from .elevenlabs_sound_effects_v2 import FalElevenlabsSoundEffectsV2Generator
4
+ from .elevenlabs_tts_eleven_v3 import FalElevenlabsTtsElevenV3Generator
5
+ from .fal_elevenlabs_tts_turbo_v2_5 import FalElevenlabsTtsTurboV25Generator
6
+ from .fal_minimax_speech_26_hd import FalMinimaxSpeech26HdGenerator
1
7
  from .minimax_music_v2 import FalMinimaxMusicV2Generator
2
8
  from .minimax_speech_2_6_turbo import FalMinimaxSpeech26TurboGenerator
3
9
 
4
- __all__ = ["FalMinimaxSpeech26TurboGenerator", "FalMinimaxMusicV2Generator"]
10
+ __all__ = [
11
+ "FalBeatovenMusicGenerationGenerator",
12
+ "FalBeatovenSoundEffectGenerationGenerator",
13
+ "FalElevenlabsSoundEffectsV2Generator",
14
+ "FalElevenlabsTtsElevenV3Generator",
15
+ "FalElevenlabsTtsTurboV25Generator",
16
+ "FalMinimaxMusicV2Generator",
17
+ "FalMinimaxSpeech26HdGenerator",
18
+ "FalMinimaxSpeech26TurboGenerator",
19
+ ]