@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
@@ -0,0 +1,381 @@
1
+ """
2
+ Lineage resolvers for ancestry and descendants tracking
3
+ """
4
+
5
+ from uuid import UUID
6
+
7
+ import strawberry
8
+ from sqlalchemy import select, text
9
+
10
+ from ...database.connection import get_async_session
11
+ from ...dbmodels import Boards, Generations
12
+ from ...logging import get_logger
13
+ from ..access_control import can_access_board, get_auth_context_from_info
14
+ from ..types.generation import (
15
+ AncestryNode,
16
+ ArtifactLineage,
17
+ ArtifactType,
18
+ DescendantNode,
19
+ Generation,
20
+ GenerationStatus,
21
+ )
22
+
23
+ logger = get_logger(__name__)
24
+
25
+
26
+ def convert_db_to_graphql_generation(gen: Generations) -> Generation:
27
+ """Convert a database Generation model to GraphQL Generation type."""
28
+ return Generation(
29
+ id=gen.id,
30
+ tenant_id=gen.tenant_id,
31
+ board_id=gen.board_id,
32
+ user_id=gen.user_id,
33
+ generator_name=gen.generator_name,
34
+ artifact_type=ArtifactType(gen.artifact_type),
35
+ storage_url=gen.storage_url,
36
+ thumbnail_url=gen.thumbnail_url,
37
+ additional_files=gen.additional_files or [],
38
+ input_params=gen.input_params or {},
39
+ output_metadata=gen.output_metadata or {},
40
+ external_job_id=gen.external_job_id,
41
+ status=GenerationStatus(gen.status),
42
+ progress=float(gen.progress or 0.0),
43
+ error_message=gen.error_message,
44
+ started_at=gen.started_at,
45
+ completed_at=gen.completed_at,
46
+ created_at=gen.created_at,
47
+ updated_at=gen.updated_at,
48
+ )
49
+
50
+
51
+ async def resolve_input_artifacts(
52
+ generation: Generation, info: strawberry.Info
53
+ ) -> list[ArtifactLineage]:
54
+ """Resolve input artifacts with role metadata."""
55
+ async with get_async_session() as session:
56
+ # Query the generation to get input_artifacts
57
+ stmt = select(Generations).where(Generations.id == generation.id)
58
+ result = await session.execute(stmt)
59
+ gen = result.scalar_one_or_none()
60
+
61
+ if not gen or not gen.input_artifacts:
62
+ return []
63
+
64
+ # Build ArtifactLineage objects
65
+ lineages = []
66
+ for artifact_data in gen.input_artifacts:
67
+ lineages.append(
68
+ ArtifactLineage(
69
+ generation_id=UUID(artifact_data["generation_id"]),
70
+ role=artifact_data["role"],
71
+ artifact_type=ArtifactType(artifact_data["artifact_type"]),
72
+ )
73
+ )
74
+
75
+ return lineages
76
+
77
+
78
+ async def resolve_generation_by_id(info: strawberry.Info, generation_id: UUID) -> Generation | None:
79
+ """Helper to resolve a generation by ID with access control."""
80
+ auth_context = await get_auth_context_from_info(info)
81
+ if auth_context is None:
82
+ return None
83
+
84
+ async with get_async_session() as session:
85
+ # Query generation
86
+ stmt = select(Generations).where(Generations.id == generation_id)
87
+ result = await session.execute(stmt)
88
+ gen = result.scalar_one_or_none()
89
+
90
+ if not gen:
91
+ return None
92
+
93
+ # Check board access
94
+ board_stmt = select(Boards).where(Boards.id == gen.board_id)
95
+ board_result = await session.execute(board_stmt)
96
+ board = board_result.scalar_one_or_none()
97
+
98
+ if not board or not can_access_board(board, auth_context):
99
+ return None
100
+
101
+ return convert_db_to_graphql_generation(gen)
102
+
103
+
104
+ async def resolve_ancestry(
105
+ generation: Generation, info: strawberry.Info, max_depth: int = 25
106
+ ) -> AncestryNode:
107
+ """Build recursive ancestry tree using recursive CTE."""
108
+ auth_context = await get_auth_context_from_info(info)
109
+
110
+ if not auth_context:
111
+ # Return empty tree if not authenticated
112
+ return AncestryNode(generation=generation, depth=0, role=None, parents=[])
113
+
114
+ async with get_async_session() as session:
115
+ # Use recursive CTE to fetch entire ancestry tree in one query
116
+ cte_query = text("""
117
+ WITH RECURSIVE ancestry_tree AS (
118
+ -- Base case: starting generation
119
+ SELECT
120
+ id,
121
+ tenant_id,
122
+ board_id,
123
+ user_id,
124
+ generator_name,
125
+ artifact_type,
126
+ storage_url,
127
+ thumbnail_url,
128
+ additional_files,
129
+ input_params,
130
+ output_metadata,
131
+ external_job_id,
132
+ status,
133
+ progress,
134
+ error_message,
135
+ started_at,
136
+ completed_at,
137
+ created_at,
138
+ updated_at,
139
+ input_artifacts,
140
+ 0 as depth,
141
+ NULL::text as role,
142
+ ARRAY[id] as path -- cycle detection
143
+ FROM generations
144
+ WHERE id = :gen_id AND tenant_id = :tenant_id
145
+
146
+ UNION ALL
147
+
148
+ -- Recursive case: parent generations
149
+ SELECT
150
+ g.id,
151
+ g.tenant_id,
152
+ g.board_id,
153
+ g.user_id,
154
+ g.generator_name,
155
+ g.artifact_type,
156
+ g.storage_url,
157
+ g.thumbnail_url,
158
+ g.additional_files,
159
+ g.input_params,
160
+ g.output_metadata,
161
+ g.external_job_id,
162
+ g.status,
163
+ g.progress,
164
+ g.error_message,
165
+ g.started_at,
166
+ g.completed_at,
167
+ g.created_at,
168
+ g.updated_at,
169
+ g.input_artifacts,
170
+ at.depth + 1,
171
+ artifact->>'role' as role,
172
+ at.path || g.id
173
+ FROM ancestry_tree at
174
+ CROSS JOIN LATERAL jsonb_array_elements(at.input_artifacts) AS artifact
175
+ JOIN generations g ON
176
+ g.id = (artifact->>'generation_id')::uuid
177
+ AND NOT (g.id = ANY(at.path)) -- prevent cycles
178
+ AND at.depth < :max_depth
179
+ WHERE g.tenant_id = :tenant_id
180
+ )
181
+ SELECT * FROM ancestry_tree ORDER BY depth, id
182
+ """)
183
+
184
+ result = await session.execute(
185
+ cte_query,
186
+ {
187
+ "gen_id": generation.id,
188
+ "tenant_id": auth_context.tenant_id,
189
+ "max_depth": max_depth,
190
+ },
191
+ )
192
+ rows = result.fetchall()
193
+
194
+ if not rows:
195
+ # Return root node with no parents
196
+ return AncestryNode(generation=generation, depth=0, role=None, parents=[])
197
+
198
+ # Build tree from flat results
199
+ nodes_by_id: dict[UUID, tuple[Generations, int, str | None]] = {}
200
+ for row in rows:
201
+ gen_obj = Generations()
202
+ gen_obj.id = row.id
203
+ gen_obj.tenant_id = row.tenant_id
204
+ gen_obj.board_id = row.board_id
205
+ gen_obj.user_id = row.user_id
206
+ gen_obj.generator_name = row.generator_name
207
+ gen_obj.artifact_type = row.artifact_type
208
+ gen_obj.storage_url = row.storage_url
209
+ gen_obj.thumbnail_url = row.thumbnail_url
210
+ gen_obj.additional_files = row.additional_files
211
+ gen_obj.input_params = row.input_params
212
+ gen_obj.output_metadata = row.output_metadata
213
+ gen_obj.external_job_id = row.external_job_id
214
+ gen_obj.status = row.status
215
+ gen_obj.progress = row.progress
216
+ gen_obj.error_message = row.error_message
217
+ gen_obj.started_at = row.started_at
218
+ gen_obj.completed_at = row.completed_at
219
+ gen_obj.created_at = row.created_at
220
+ gen_obj.updated_at = row.updated_at
221
+ gen_obj.input_artifacts = row.input_artifacts
222
+ nodes_by_id[row.id] = (gen_obj, row.depth, row.role)
223
+
224
+ # Build tree structure recursively
225
+ def build_node(gen_id: UUID) -> AncestryNode:
226
+ gen_obj, depth, role = nodes_by_id[gen_id]
227
+ gen_graphql = convert_db_to_graphql_generation(gen_obj)
228
+
229
+ # Find parent nodes
230
+ parent_nodes = []
231
+ if gen_obj.input_artifacts:
232
+ for artifact_data in gen_obj.input_artifacts:
233
+ parent_id = UUID(artifact_data["generation_id"])
234
+ if parent_id in nodes_by_id:
235
+ parent_nodes.append(build_node(parent_id))
236
+
237
+ return AncestryNode(
238
+ generation=gen_graphql, depth=depth, role=role, parents=parent_nodes
239
+ )
240
+
241
+ return build_node(generation.id)
242
+
243
+
244
+ async def resolve_descendants(
245
+ generation: Generation, info: strawberry.Info, max_depth: int = 25
246
+ ) -> DescendantNode:
247
+ """Build recursive descendants tree using recursive CTE."""
248
+ auth_context = await get_auth_context_from_info(info)
249
+
250
+ if not auth_context:
251
+ # Return empty tree if not authenticated
252
+ return DescendantNode(generation=generation, depth=0, role=None, children=[])
253
+
254
+ async with get_async_session() as session:
255
+ # Use recursive CTE to fetch entire descendants tree in one query
256
+ cte_query = text("""
257
+ WITH RECURSIVE descendants_tree AS (
258
+ -- Base case: starting generation
259
+ SELECT
260
+ id,
261
+ tenant_id,
262
+ board_id,
263
+ user_id,
264
+ generator_name,
265
+ artifact_type,
266
+ storage_url,
267
+ thumbnail_url,
268
+ additional_files,
269
+ input_params,
270
+ output_metadata,
271
+ external_job_id,
272
+ status,
273
+ progress,
274
+ error_message,
275
+ started_at,
276
+ completed_at,
277
+ created_at,
278
+ updated_at,
279
+ input_artifacts,
280
+ 0 as depth,
281
+ NULL::text as role,
282
+ NULL::uuid as parent_id,
283
+ ARRAY[id] as path -- cycle detection
284
+ FROM generations
285
+ WHERE id = :gen_id AND tenant_id = :tenant_id
286
+
287
+ UNION ALL
288
+
289
+ -- Recursive case: child generations
290
+ SELECT
291
+ g.id,
292
+ g.tenant_id,
293
+ g.board_id,
294
+ g.user_id,
295
+ g.generator_name,
296
+ g.artifact_type,
297
+ g.storage_url,
298
+ g.thumbnail_url,
299
+ g.additional_files,
300
+ g.input_params,
301
+ g.output_metadata,
302
+ g.external_job_id,
303
+ g.status,
304
+ g.progress,
305
+ g.error_message,
306
+ g.started_at,
307
+ g.completed_at,
308
+ g.created_at,
309
+ g.updated_at,
310
+ g.input_artifacts,
311
+ dt.depth + 1,
312
+ artifact->>'role' as role,
313
+ dt.id as parent_id,
314
+ dt.path || g.id
315
+ FROM generations g
316
+ CROSS JOIN LATERAL jsonb_array_elements(g.input_artifacts) AS artifact
317
+ JOIN descendants_tree dt ON
318
+ dt.id = (artifact->>'generation_id')::uuid
319
+ AND NOT (g.id = ANY(dt.path)) -- prevent cycles
320
+ AND dt.depth < :max_depth
321
+ WHERE g.tenant_id = :tenant_id
322
+ )
323
+ SELECT * FROM descendants_tree ORDER BY depth, id
324
+ """)
325
+
326
+ result = await session.execute(
327
+ cte_query,
328
+ {
329
+ "gen_id": generation.id,
330
+ "tenant_id": auth_context.tenant_id,
331
+ "max_depth": max_depth,
332
+ },
333
+ )
334
+ rows = result.fetchall()
335
+
336
+ if not rows:
337
+ # Return root node with no children
338
+ return DescendantNode(generation=generation, depth=0, role=None, children=[])
339
+
340
+ # Build tree from flat results
341
+ nodes_by_id: dict[UUID, tuple[Generations, int, str | None, UUID | None]] = {}
342
+ for row in rows:
343
+ gen_obj = Generations()
344
+ gen_obj.id = row.id
345
+ gen_obj.tenant_id = row.tenant_id
346
+ gen_obj.board_id = row.board_id
347
+ gen_obj.user_id = row.user_id
348
+ gen_obj.generator_name = row.generator_name
349
+ gen_obj.artifact_type = row.artifact_type
350
+ gen_obj.storage_url = row.storage_url
351
+ gen_obj.thumbnail_url = row.thumbnail_url
352
+ gen_obj.additional_files = row.additional_files
353
+ gen_obj.input_params = row.input_params
354
+ gen_obj.output_metadata = row.output_metadata
355
+ gen_obj.external_job_id = row.external_job_id
356
+ gen_obj.status = row.status
357
+ gen_obj.progress = row.progress
358
+ gen_obj.error_message = row.error_message
359
+ gen_obj.started_at = row.started_at
360
+ gen_obj.completed_at = row.completed_at
361
+ gen_obj.created_at = row.created_at
362
+ gen_obj.updated_at = row.updated_at
363
+ gen_obj.input_artifacts = row.input_artifacts
364
+ nodes_by_id[row.id] = (gen_obj, row.depth, row.role, row.parent_id)
365
+
366
+ # Build tree structure recursively
367
+ def build_node(gen_id: UUID) -> DescendantNode:
368
+ gen_obj, depth, role, _ = nodes_by_id[gen_id]
369
+ gen_graphql = convert_db_to_graphql_generation(gen_obj)
370
+
371
+ # Find child nodes (nodes that have this as parent_id)
372
+ child_nodes = []
373
+ for child_id, (_, _, _, parent_id) in nodes_by_id.items():
374
+ if parent_id == gen_id:
375
+ child_nodes.append(build_node(child_id))
376
+
377
+ return DescendantNode(
378
+ generation=gen_graphql, depth=depth, role=role, children=child_nodes
379
+ )
380
+
381
+ return build_node(generation.id)