@weirdfingers/baseboards 0.5.3 → 0.6.1

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 (74) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/templates/api/alembic/env.py +9 -1
  4. package/templates/api/alembic/versions/20250101_000000_initial_schema.py +107 -49
  5. package/templates/api/alembic/versions/20251022_174729_remove_provider_name_from_generations.py +7 -3
  6. package/templates/api/alembic/versions/20251023_165852_switch_to_declarative_base_and_mapping.py +57 -1
  7. package/templates/api/alembic/versions/20251202_000000_add_artifact_lineage.py +134 -0
  8. package/templates/api/alembic/versions/2025925_62735_add_seed_data_for_default_tenant.py +8 -5
  9. package/templates/api/config/generators.yaml +111 -0
  10. package/templates/api/src/boards/__init__.py +1 -1
  11. package/templates/api/src/boards/api/app.py +2 -1
  12. package/templates/api/src/boards/api/endpoints/tenant_registration.py +1 -1
  13. package/templates/api/src/boards/api/endpoints/uploads.py +150 -0
  14. package/templates/api/src/boards/auth/factory.py +1 -1
  15. package/templates/api/src/boards/dbmodels/__init__.py +8 -22
  16. package/templates/api/src/boards/generators/artifact_resolution.py +45 -12
  17. package/templates/api/src/boards/generators/implementations/fal/audio/__init__.py +16 -1
  18. package/templates/api/src/boards/generators/implementations/fal/audio/beatoven_music_generation.py +171 -0
  19. package/templates/api/src/boards/generators/implementations/fal/audio/beatoven_sound_effect_generation.py +167 -0
  20. package/templates/api/src/boards/generators/implementations/fal/audio/elevenlabs_sound_effects_v2.py +194 -0
  21. package/templates/api/src/boards/generators/implementations/fal/audio/elevenlabs_tts_eleven_v3.py +209 -0
  22. package/templates/api/src/boards/generators/implementations/fal/audio/fal_elevenlabs_tts_turbo_v2_5.py +206 -0
  23. package/templates/api/src/boards/generators/implementations/fal/audio/fal_minimax_speech_26_hd.py +237 -0
  24. package/templates/api/src/boards/generators/implementations/fal/audio/minimax_speech_2_6_turbo.py +1 -1
  25. package/templates/api/src/boards/generators/implementations/fal/image/__init__.py +30 -0
  26. package/templates/api/src/boards/generators/implementations/fal/image/clarity_upscaler.py +220 -0
  27. package/templates/api/src/boards/generators/implementations/fal/image/crystal_upscaler.py +173 -0
  28. package/templates/api/src/boards/generators/implementations/fal/image/fal_ideogram_character.py +227 -0
  29. package/templates/api/src/boards/generators/implementations/fal/image/flux_2.py +203 -0
  30. package/templates/api/src/boards/generators/implementations/fal/image/flux_2_edit.py +230 -0
  31. package/templates/api/src/boards/generators/implementations/fal/image/flux_2_pro.py +204 -0
  32. package/templates/api/src/boards/generators/implementations/fal/image/flux_2_pro_edit.py +221 -0
  33. package/templates/api/src/boards/generators/implementations/fal/image/gemini_25_flash_image.py +177 -0
  34. package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_edit_image.py +182 -0
  35. package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_mini.py +167 -0
  36. package/templates/api/src/boards/generators/implementations/fal/image/ideogram_character_edit.py +299 -0
  37. package/templates/api/src/boards/generators/implementations/fal/image/ideogram_v2.py +190 -0
  38. package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_pro_edit.py +226 -0
  39. package/templates/api/src/boards/generators/implementations/fal/image/qwen_image.py +249 -0
  40. package/templates/api/src/boards/generators/implementations/fal/image/qwen_image_edit.py +244 -0
  41. package/templates/api/src/boards/generators/implementations/fal/video/__init__.py +42 -0
  42. package/templates/api/src/boards/generators/implementations/fal/video/bytedance_seedance_v1_pro_text_to_video.py +209 -0
  43. package/templates/api/src/boards/generators/implementations/fal/video/creatify_lipsync.py +161 -0
  44. package/templates/api/src/boards/generators/implementations/fal/video/fal_bytedance_seedance_v1_pro_image_to_video.py +222 -0
  45. package/templates/api/src/boards/generators/implementations/fal/video/fal_minimax_hailuo_02_standard_text_to_video.py +152 -0
  46. package/templates/api/src/boards/generators/implementations/fal/video/fal_pixverse_lipsync.py +197 -0
  47. package/templates/api/src/boards/generators/implementations/fal/video/fal_sora_2_text_to_video.py +173 -0
  48. package/templates/api/src/boards/generators/implementations/fal/video/infinitalk.py +221 -0
  49. package/templates/api/src/boards/generators/implementations/fal/video/kling_video_v2_5_turbo_pro_image_to_video.py +175 -0
  50. package/templates/api/src/boards/generators/implementations/fal/video/minimax_hailuo_2_3_pro_image_to_video.py +153 -0
  51. package/templates/api/src/boards/generators/implementations/fal/video/sora2_image_to_video.py +172 -0
  52. package/templates/api/src/boards/generators/implementations/fal/video/sora_2_image_to_video_pro.py +175 -0
  53. package/templates/api/src/boards/generators/implementations/fal/video/sora_2_text_to_video_pro.py +163 -0
  54. package/templates/api/src/boards/generators/implementations/fal/video/sync_lipsync_v2_pro.py +155 -0
  55. package/templates/api/src/boards/generators/implementations/fal/video/veed_lipsync.py +174 -0
  56. package/templates/api/src/boards/generators/implementations/fal/video/veo3.py +194 -0
  57. package/templates/api/src/boards/generators/implementations/fal/video/veo31_first_last_frame_to_video.py +1 -1
  58. package/templates/api/src/boards/generators/implementations/fal/video/wan_pro_image_to_video.py +158 -0
  59. package/templates/api/src/boards/graphql/access_control.py +1 -1
  60. package/templates/api/src/boards/graphql/mutations/root.py +16 -4
  61. package/templates/api/src/boards/graphql/resolvers/board.py +0 -2
  62. package/templates/api/src/boards/graphql/resolvers/generation.py +10 -233
  63. package/templates/api/src/boards/graphql/resolvers/lineage.py +381 -0
  64. package/templates/api/src/boards/graphql/resolvers/upload.py +463 -0
  65. package/templates/api/src/boards/graphql/types/generation.py +62 -26
  66. package/templates/api/src/boards/middleware.py +1 -1
  67. package/templates/api/src/boards/storage/factory.py +2 -2
  68. package/templates/api/src/boards/tenant_isolation.py +9 -9
  69. package/templates/api/src/boards/workers/actors.py +10 -1
  70. package/templates/web/package.json +1 -1
  71. package/templates/web/src/app/boards/[boardId]/page.tsx +14 -5
  72. package/templates/web/src/app/lineage/[generationId]/page.tsx +233 -0
  73. package/templates/web/src/components/boards/ArtifactPreview.tsx +20 -1
  74. package/templates/web/src/components/boards/UploadArtifact.tsx +253 -0
@@ -129,7 +129,7 @@ def ensure_preloaded(obj, attr_name: str, error_msg: str | None = None) -> None:
129
129
  except Exception as e:
130
130
  if "was not loaded" in str(e) or "lazy loading" in str(e):
131
131
  msg = error_msg or (
132
- f"Relationship '{attr_name}' was not preloaded. " "Use selectinload() in the query."
132
+ f"Relationship '{attr_name}' was not preloaded. Use selectinload() in the query."
133
133
  )
134
134
  raise RuntimeError(msg) from e
135
135
  # Re-raise other exceptions
@@ -7,7 +7,7 @@ from uuid import UUID
7
7
  import strawberry
8
8
 
9
9
  from ..types.board import Board, BoardRole
10
- from ..types.generation import ArtifactType, Generation
10
+ from ..types.generation import ArtifactType, Generation, UploadArtifactInput
11
11
 
12
12
 
13
13
  # Input types for mutations
@@ -43,14 +43,17 @@ class AddBoardMemberInput:
43
43
 
44
44
  @strawberry.input
45
45
  class CreateGenerationInput:
46
- """Input for creating a new generation."""
46
+ """Input for creating a new generation.
47
+
48
+ Note: Lineage is now captured automatically via artifact resolution.
49
+ Generator input parameters that reference other generations (as artifact types)
50
+ will be automatically resolved and lineage will be tracked.
51
+ """
47
52
 
48
53
  board_id: UUID
49
54
  generator_name: str
50
55
  artifact_type: ArtifactType
51
56
  input_params: strawberry.scalars.JSON # type: ignore[reportInvalidTypeForm]
52
- parent_generation_id: UUID | None = None
53
- input_generation_ids: list[UUID] | None = None
54
57
 
55
58
 
56
59
  @strawberry.type
@@ -134,3 +137,12 @@ class Mutation:
134
137
  from ..resolvers.generation import regenerate
135
138
 
136
139
  return await regenerate(info, id)
140
+
141
+ @strawberry.mutation(name="uploadArtifact")
142
+ async def upload_artifact(
143
+ self, info: strawberry.Info, input: UploadArtifactInput
144
+ ) -> Generation:
145
+ """Upload an artifact from URL (synchronous)."""
146
+ from ..resolvers.upload import upload_artifact_from_url
147
+
148
+ return await upload_artifact_from_url(info, input)
@@ -442,8 +442,6 @@ async def resolve_board_generations(
442
442
  additional_files=gen.additional_files or [],
443
443
  input_params=gen.input_params or {},
444
444
  output_metadata=gen.output_metadata or {},
445
- parent_generation_id=gen.parent_generation_id,
446
- input_generation_ids=gen.input_generation_ids or [],
447
445
  external_job_id=gen.external_job_id,
448
446
  status=GenerationStatus(gen.status),
449
447
  progress=float(gen.progress or 0.0),
@@ -81,8 +81,6 @@ async def resolve_generation_by_id(info: strawberry.Info, id: UUID) -> Generatio
81
81
  additional_files=gen.additional_files or [],
82
82
  input_params=gen.input_params or {},
83
83
  output_metadata=gen.output_metadata or {},
84
- parent_generation_id=gen.parent_generation_id,
85
- input_generation_ids=gen.input_generation_ids or [],
86
84
  external_job_id=gen.external_job_id,
87
85
  status=GenerationStatus(gen.status),
88
86
  progress=float(gen.progress or 0.0),
@@ -193,8 +191,6 @@ async def resolve_recent_generations(
193
191
  additional_files=gen.additional_files or [],
194
192
  input_params=gen.input_params or {},
195
193
  output_metadata=gen.output_metadata or {},
196
- parent_generation_id=gen.parent_generation_id,
197
- input_generation_ids=gen.input_generation_ids or [],
198
194
  external_job_id=gen.external_job_id,
199
195
  status=GenerationStatusEnum(gen.status),
200
196
  progress=float(gen.progress or 0.0),
@@ -275,187 +271,22 @@ async def resolve_generation_user(generation: Generation, info: strawberry.Info)
275
271
  async def resolve_generation_parent(
276
272
  generation: Generation, info: strawberry.Info
277
273
  ) -> Generation | None:
278
- """Resolve the parent generation if any."""
279
- if not generation.parent_generation_id:
280
- return None
281
-
282
- auth_context = await get_auth_context_from_info(info)
283
-
284
- async with get_async_session() as session:
285
- # Query parent generation
286
- stmt = select(Generations).where(Generations.id == generation.parent_generation_id)
287
- result = await session.execute(stmt)
288
- parent = result.scalar_one_or_none()
289
-
290
- if not parent:
291
- logger.warning(
292
- "Parent generation not found",
293
- parent_id=str(generation.parent_generation_id),
294
- )
295
- return None
296
-
297
- # Check access to parent's board
298
- board_stmt = (
299
- select(Boards)
300
- .where(Boards.id == parent.board_id)
301
- .options(selectinload(Boards.board_members))
302
- )
303
- board_result = await session.execute(board_stmt)
304
- board = board_result.scalar_one_or_none()
305
-
306
- if not board or not can_access_board(board, auth_context):
307
- logger.info(
308
- "Access denied to parent generation",
309
- parent_id=str(generation.parent_generation_id),
310
- )
311
- return None
312
-
313
- # Convert to GraphQL type
314
- from ..types.generation import ArtifactType, GenerationStatus
315
- from ..types.generation import Generation as GenerationType
316
-
317
- return GenerationType(
318
- id=parent.id,
319
- tenant_id=parent.tenant_id,
320
- board_id=parent.board_id,
321
- user_id=parent.user_id,
322
- generator_name=parent.generator_name,
323
- artifact_type=ArtifactType(parent.artifact_type),
324
- storage_url=parent.storage_url,
325
- thumbnail_url=parent.thumbnail_url,
326
- additional_files=parent.additional_files or [],
327
- input_params=parent.input_params or {},
328
- output_metadata=parent.output_metadata or {},
329
- parent_generation_id=parent.parent_generation_id,
330
- input_generation_ids=parent.input_generation_ids or [],
331
- external_job_id=parent.external_job_id,
332
- status=GenerationStatus(parent.status),
333
- progress=float(parent.progress or 0.0),
334
- error_message=parent.error_message,
335
- started_at=parent.started_at,
336
- completed_at=parent.completed_at,
337
- created_at=parent.created_at,
338
- updated_at=parent.updated_at,
339
- )
274
+ """DEPRECATED: Use ancestry field instead. This resolver is no longer used."""
275
+ return None
340
276
 
341
277
 
342
278
  async def resolve_generation_inputs(
343
279
  generation: Generation, info: strawberry.Info
344
280
  ) -> list[Generation]: # noqa: E501
345
- """Resolve input generations used for this generation."""
346
- if not generation.input_generation_ids:
347
- return []
348
-
349
- auth_context = await get_auth_context_from_info(info)
350
-
351
- async with get_async_session() as session:
352
- # Query input generations
353
- stmt = select(Generations).where(Generations.id.in_(generation.input_generation_ids))
354
- result = await session.execute(stmt)
355
- inputs = result.scalars().all()
356
-
357
- # Filter by board access
358
- accessible_inputs = []
359
- for input_gen in inputs:
360
- board_stmt = (
361
- select(Boards)
362
- .where(Boards.id == input_gen.board_id)
363
- .options(selectinload(Boards.board_members))
364
- )
365
- board_result = await session.execute(board_stmt)
366
- board = board_result.scalar_one_or_none()
367
-
368
- if board and can_access_board(board, auth_context):
369
- accessible_inputs.append(input_gen)
370
-
371
- # Convert to GraphQL types
372
- from ..types.generation import ArtifactType, GenerationStatus
373
- from ..types.generation import Generation as GenerationType
374
-
375
- return [
376
- GenerationType(
377
- id=gen.id,
378
- tenant_id=gen.tenant_id,
379
- board_id=gen.board_id,
380
- user_id=gen.user_id,
381
- generator_name=gen.generator_name,
382
- artifact_type=ArtifactType(gen.artifact_type),
383
- storage_url=gen.storage_url,
384
- thumbnail_url=gen.thumbnail_url,
385
- additional_files=gen.additional_files or [],
386
- input_params=gen.input_params or {},
387
- output_metadata=gen.output_metadata or {},
388
- parent_generation_id=gen.parent_generation_id,
389
- input_generation_ids=gen.input_generation_ids or [],
390
- external_job_id=gen.external_job_id,
391
- status=GenerationStatus(gen.status),
392
- progress=float(gen.progress or 0.0),
393
- error_message=gen.error_message,
394
- started_at=gen.started_at,
395
- completed_at=gen.completed_at,
396
- created_at=gen.created_at,
397
- updated_at=gen.updated_at,
398
- )
399
- for gen in accessible_inputs
400
- ]
281
+ """DEPRECATED: Use input_artifacts field instead. This resolver is no longer used."""
282
+ return []
401
283
 
402
284
 
403
285
  async def resolve_generation_children(
404
286
  generation: Generation, info: strawberry.Info
405
287
  ) -> list[Generation]: # noqa: E501
406
- """Resolve child generations derived from this one."""
407
- auth_context = await get_auth_context_from_info(info)
408
-
409
- async with get_async_session() as session:
410
- # Query child generations
411
- stmt = select(Generations).where(Generations.parent_generation_id == generation.id)
412
- result = await session.execute(stmt)
413
- children = result.scalars().all()
414
-
415
- # Filter by board access
416
- accessible_children = []
417
- for child_gen in children:
418
- board_stmt = (
419
- select(Boards)
420
- .where(Boards.id == child_gen.board_id)
421
- .options(selectinload(Boards.board_members))
422
- )
423
- board_result = await session.execute(board_stmt)
424
- board = board_result.scalar_one_or_none()
425
-
426
- if board and can_access_board(board, auth_context):
427
- accessible_children.append(child_gen)
428
-
429
- # Convert to GraphQL types
430
- from ..types.generation import ArtifactType, GenerationStatus
431
- from ..types.generation import Generation as GenerationType
432
-
433
- return [
434
- GenerationType(
435
- id=gen.id,
436
- tenant_id=gen.tenant_id,
437
- board_id=gen.board_id,
438
- user_id=gen.user_id,
439
- generator_name=gen.generator_name,
440
- artifact_type=ArtifactType(gen.artifact_type),
441
- storage_url=gen.storage_url,
442
- thumbnail_url=gen.thumbnail_url,
443
- additional_files=gen.additional_files or [],
444
- input_params=gen.input_params or {},
445
- output_metadata=gen.output_metadata or {},
446
- parent_generation_id=gen.parent_generation_id,
447
- input_generation_ids=gen.input_generation_ids or [],
448
- external_job_id=gen.external_job_id,
449
- status=GenerationStatus(gen.status),
450
- progress=float(gen.progress or 0.0),
451
- error_message=gen.error_message,
452
- started_at=gen.started_at,
453
- completed_at=gen.completed_at,
454
- created_at=gen.created_at,
455
- updated_at=gen.updated_at,
456
- )
457
- for gen in accessible_children
458
- ]
288
+ """DEPRECATED: Use descendants field instead. This resolver is no longer used."""
289
+ return []
459
290
 
460
291
 
461
292
  # Mutation resolvers
@@ -499,50 +330,9 @@ async def create_generation(info: strawberry.Info, input: CreateGenerationInput)
499
330
  if generator is None:
500
331
  raise RuntimeError(f"Unknown generator: {input.generator_name}")
501
332
 
502
- # Verify access to parent generation if provided
503
- if input.parent_generation_id:
504
- parent_stmt = select(Generations).where(Generations.id == input.parent_generation_id)
505
- parent_result = await session.execute(parent_stmt)
506
- parent_gen = parent_result.scalar_one_or_none()
507
-
508
- if not parent_gen:
509
- raise RuntimeError("Parent generation not found")
510
-
511
- # Check access to parent's board
512
- parent_board_stmt = (
513
- select(Boards)
514
- .where(Boards.id == parent_gen.board_id)
515
- .options(selectinload(Boards.board_members))
516
- )
517
- parent_board_result = await session.execute(parent_board_stmt)
518
- parent_board = parent_board_result.scalar_one_or_none()
519
-
520
- if not parent_board or not can_access_board(parent_board, auth_context):
521
- raise RuntimeError("Access denied to parent generation")
522
-
523
- # Verify access to input generations if provided
524
- if input.input_generation_ids:
525
- for input_gen_id in input.input_generation_ids:
526
- input_stmt = select(Generations).where(Generations.id == input_gen_id)
527
- input_result = await session.execute(input_stmt)
528
- input_gen = input_result.scalar_one_or_none()
529
-
530
- if not input_gen:
531
- raise RuntimeError(f"Input generation {input_gen_id} not found")
532
-
533
- # Check access to input's board
534
- input_board_stmt = (
535
- select(Boards)
536
- .where(Boards.id == input_gen.board_id)
537
- .options(selectinload(Boards.board_members))
538
- )
539
- input_board_result = await session.execute(input_board_stmt)
540
- input_board = input_board_result.scalar_one_or_none()
541
-
542
- if not input_board or not can_access_board(input_board, auth_context):
543
- raise RuntimeError(f"Access denied to input generation {input_gen_id}")
544
-
545
333
  # Create generation record
334
+ # Note: Lineage will be captured automatically during generation processing
335
+ # via artifact resolution in the worker (see actors.py)
546
336
  gen = await jobs_repo.create_generation(
547
337
  session,
548
338
  tenant_id=auth_context.tenant_id,
@@ -553,12 +343,6 @@ async def create_generation(info: strawberry.Info, input: CreateGenerationInput)
553
343
  input_params=input.input_params,
554
344
  )
555
345
 
556
- # Update parent and input relationships if provided
557
- if input.parent_generation_id:
558
- gen.parent_generation_id = input.parent_generation_id
559
- if input.input_generation_ids:
560
- gen.input_generation_ids = input.input_generation_ids
561
-
562
346
  await session.commit()
563
347
  await session.refresh(gen)
564
348
 
@@ -595,8 +379,6 @@ async def create_generation(info: strawberry.Info, input: CreateGenerationInput)
595
379
  additional_files=gen.additional_files or [],
596
380
  input_params=gen.input_params or {},
597
381
  output_metadata=gen.output_metadata or {},
598
- parent_generation_id=gen.parent_generation_id,
599
- input_generation_ids=gen.input_generation_ids or [],
600
382
  external_job_id=gen.external_job_id,
601
383
  status=GenerationStatus(gen.status),
602
384
  progress=float(gen.progress or 0.0),
@@ -695,8 +477,6 @@ async def cancel_generation(info: strawberry.Info, id: UUID) -> Generation:
695
477
  additional_files=gen.additional_files or [],
696
478
  input_params=gen.input_params or {},
697
479
  output_metadata=gen.output_metadata or {},
698
- parent_generation_id=gen.parent_generation_id,
699
- input_generation_ids=gen.input_generation_ids or [],
700
480
  external_job_id=gen.external_job_id,
701
481
  status=GenerationStatus(gen.status),
702
482
  progress=float(gen.progress or 0.0),
@@ -842,9 +622,8 @@ async def regenerate(info: strawberry.Info, id: UUID) -> Generation:
842
622
  input_params=original.input_params or {},
843
623
  )
844
624
 
845
- # Set parent and copy input relationships
846
- new_gen.parent_generation_id = original.id
847
- new_gen.input_generation_ids = original.input_generation_ids or []
625
+ # Note: Lineage will be captured automatically during generation processing
626
+ # via artifact resolution in the worker (see actors.py)
848
627
 
849
628
  await session.commit()
850
629
  await session.refresh(new_gen)
@@ -876,8 +655,6 @@ async def regenerate(info: strawberry.Info, id: UUID) -> Generation:
876
655
  additional_files=new_gen.additional_files or [],
877
656
  input_params=new_gen.input_params or {},
878
657
  output_metadata=new_gen.output_metadata or {},
879
- parent_generation_id=new_gen.parent_generation_id,
880
- input_generation_ids=new_gen.input_generation_ids or [],
881
658
  external_job_id=new_gen.external_job_id,
882
659
  status=GenerationStatus(new_gen.status),
883
660
  progress=float(new_gen.progress or 0.0),