@weirdfingers/baseboards 0.9.5 → 0.9.7

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 (237) hide show
  1. package/dist/index.js +561 -469
  2. package/dist/index.js.map +1 -1
  3. package/package.json +2 -5
  4. package/templates/README.md +0 -122
  5. package/templates/api/.env.example +0 -65
  6. package/templates/api/ARTIFACT_RESOLUTION_GUIDE.md +0 -148
  7. package/templates/api/Dockerfile +0 -32
  8. package/templates/api/README.md +0 -264
  9. package/templates/api/alembic/env.py +0 -114
  10. package/templates/api/alembic/script.py.mako +0 -28
  11. package/templates/api/alembic/versions/20250101_000000_initial_schema.py +0 -506
  12. package/templates/api/alembic/versions/20251022_174729_remove_provider_name_from_generations.py +0 -75
  13. package/templates/api/alembic/versions/20251023_165852_switch_to_declarative_base_and_mapping.py +0 -467
  14. package/templates/api/alembic/versions/20251202_000000_add_artifact_lineage.py +0 -134
  15. package/templates/api/alembic/versions/2025925_62735_add_seed_data_for_default_tenant.py +0 -88
  16. package/templates/api/alembic.ini +0 -36
  17. package/templates/api/config/generators.yaml +0 -237
  18. package/templates/api/config/storage_config.yaml +0 -26
  19. package/templates/api/docs/ADDING_GENERATORS.md +0 -409
  20. package/templates/api/docs/GENERATORS_API.md +0 -502
  21. package/templates/api/docs/MIGRATIONS.md +0 -472
  22. package/templates/api/docs/TESTING_LIVE_APIS.md +0 -417
  23. package/templates/api/docs/storage_providers.md +0 -337
  24. package/templates/api/pyproject.toml +0 -205
  25. package/templates/api/src/boards/__init__.py +0 -10
  26. package/templates/api/src/boards/api/app.py +0 -172
  27. package/templates/api/src/boards/api/auth.py +0 -75
  28. package/templates/api/src/boards/api/endpoints/__init__.py +0 -3
  29. package/templates/api/src/boards/api/endpoints/jobs.py +0 -76
  30. package/templates/api/src/boards/api/endpoints/setup.py +0 -505
  31. package/templates/api/src/boards/api/endpoints/sse.py +0 -129
  32. package/templates/api/src/boards/api/endpoints/storage.py +0 -155
  33. package/templates/api/src/boards/api/endpoints/tenant_registration.py +0 -296
  34. package/templates/api/src/boards/api/endpoints/uploads.py +0 -149
  35. package/templates/api/src/boards/api/endpoints/webhooks.py +0 -13
  36. package/templates/api/src/boards/auth/__init__.py +0 -15
  37. package/templates/api/src/boards/auth/adapters/__init__.py +0 -27
  38. package/templates/api/src/boards/auth/adapters/auth0.py +0 -220
  39. package/templates/api/src/boards/auth/adapters/base.py +0 -73
  40. package/templates/api/src/boards/auth/adapters/clerk.py +0 -172
  41. package/templates/api/src/boards/auth/adapters/jwt.py +0 -122
  42. package/templates/api/src/boards/auth/adapters/none.py +0 -102
  43. package/templates/api/src/boards/auth/adapters/oidc.py +0 -284
  44. package/templates/api/src/boards/auth/adapters/supabase.py +0 -110
  45. package/templates/api/src/boards/auth/context.py +0 -35
  46. package/templates/api/src/boards/auth/factory.py +0 -129
  47. package/templates/api/src/boards/auth/middleware.py +0 -221
  48. package/templates/api/src/boards/auth/provisioning.py +0 -129
  49. package/templates/api/src/boards/auth/tenant_extraction.py +0 -278
  50. package/templates/api/src/boards/cli.py +0 -354
  51. package/templates/api/src/boards/config.py +0 -131
  52. package/templates/api/src/boards/database/__init__.py +0 -7
  53. package/templates/api/src/boards/database/cli.py +0 -110
  54. package/templates/api/src/boards/database/connection.py +0 -292
  55. package/templates/api/src/boards/database/models.py +0 -19
  56. package/templates/api/src/boards/database/seed_data.py +0 -182
  57. package/templates/api/src/boards/dbmodels/__init__.py +0 -441
  58. package/templates/api/src/boards/generators/__init__.py +0 -57
  59. package/templates/api/src/boards/generators/artifact_resolution.py +0 -405
  60. package/templates/api/src/boards/generators/artifacts.py +0 -53
  61. package/templates/api/src/boards/generators/base.py +0 -144
  62. package/templates/api/src/boards/generators/implementations/__init__.py +0 -14
  63. package/templates/api/src/boards/generators/implementations/fal/__init__.py +0 -25
  64. package/templates/api/src/boards/generators/implementations/fal/audio/__init__.py +0 -23
  65. package/templates/api/src/boards/generators/implementations/fal/audio/beatoven_music_generation.py +0 -171
  66. package/templates/api/src/boards/generators/implementations/fal/audio/beatoven_sound_effect_generation.py +0 -167
  67. package/templates/api/src/boards/generators/implementations/fal/audio/chatterbox_text_to_speech.py +0 -176
  68. package/templates/api/src/boards/generators/implementations/fal/audio/chatterbox_tts_turbo.py +0 -195
  69. package/templates/api/src/boards/generators/implementations/fal/audio/elevenlabs_sound_effects_v2.py +0 -194
  70. package/templates/api/src/boards/generators/implementations/fal/audio/elevenlabs_tts_eleven_v3.py +0 -209
  71. package/templates/api/src/boards/generators/implementations/fal/audio/fal_elevenlabs_tts_turbo_v2_5.py +0 -206
  72. package/templates/api/src/boards/generators/implementations/fal/audio/fal_minimax_speech_26_hd.py +0 -237
  73. package/templates/api/src/boards/generators/implementations/fal/audio/minimax_music_v2.py +0 -173
  74. package/templates/api/src/boards/generators/implementations/fal/audio/minimax_speech_2_6_turbo.py +0 -221
  75. package/templates/api/src/boards/generators/implementations/fal/image/__init__.py +0 -63
  76. package/templates/api/src/boards/generators/implementations/fal/image/bytedance_seedream_v45_edit.py +0 -219
  77. package/templates/api/src/boards/generators/implementations/fal/image/clarity_upscaler.py +0 -220
  78. package/templates/api/src/boards/generators/implementations/fal/image/crystal_upscaler.py +0 -173
  79. package/templates/api/src/boards/generators/implementations/fal/image/fal_ideogram_character.py +0 -227
  80. package/templates/api/src/boards/generators/implementations/fal/image/flux_2.py +0 -203
  81. package/templates/api/src/boards/generators/implementations/fal/image/flux_2_edit.py +0 -230
  82. package/templates/api/src/boards/generators/implementations/fal/image/flux_2_pro.py +0 -204
  83. package/templates/api/src/boards/generators/implementations/fal/image/flux_2_pro_edit.py +0 -221
  84. package/templates/api/src/boards/generators/implementations/fal/image/flux_pro_kontext.py +0 -216
  85. package/templates/api/src/boards/generators/implementations/fal/image/flux_pro_ultra.py +0 -197
  86. package/templates/api/src/boards/generators/implementations/fal/image/gemini_25_flash_image.py +0 -177
  87. package/templates/api/src/boards/generators/implementations/fal/image/gemini_25_flash_image_edit.py +0 -208
  88. package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_15_edit.py +0 -216
  89. package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_5.py +0 -177
  90. package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_edit_image.py +0 -182
  91. package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_mini.py +0 -167
  92. package/templates/api/src/boards/generators/implementations/fal/image/ideogram_character_edit.py +0 -299
  93. package/templates/api/src/boards/generators/implementations/fal/image/ideogram_v2.py +0 -190
  94. package/templates/api/src/boards/generators/implementations/fal/image/imagen4_preview.py +0 -191
  95. package/templates/api/src/boards/generators/implementations/fal/image/imagen4_preview_fast.py +0 -179
  96. package/templates/api/src/boards/generators/implementations/fal/image/nano_banana.py +0 -183
  97. package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_edit.py +0 -212
  98. package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_pro.py +0 -179
  99. package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_pro_edit.py +0 -226
  100. package/templates/api/src/boards/generators/implementations/fal/image/qwen_image.py +0 -249
  101. package/templates/api/src/boards/generators/implementations/fal/image/qwen_image_edit.py +0 -244
  102. package/templates/api/src/boards/generators/implementations/fal/image/reve_edit.py +0 -178
  103. package/templates/api/src/boards/generators/implementations/fal/image/reve_text_to_image.py +0 -155
  104. package/templates/api/src/boards/generators/implementations/fal/image/seedream_v45_text_to_image.py +0 -180
  105. package/templates/api/src/boards/generators/implementations/fal/utils.py +0 -61
  106. package/templates/api/src/boards/generators/implementations/fal/video/__init__.py +0 -77
  107. package/templates/api/src/boards/generators/implementations/fal/video/bytedance_seedance_v1_pro_text_to_video.py +0 -209
  108. package/templates/api/src/boards/generators/implementations/fal/video/creatify_lipsync.py +0 -161
  109. package/templates/api/src/boards/generators/implementations/fal/video/fal_bytedance_seedance_v1_pro_image_to_video.py +0 -222
  110. package/templates/api/src/boards/generators/implementations/fal/video/fal_minimax_hailuo_02_standard_text_to_video.py +0 -152
  111. package/templates/api/src/boards/generators/implementations/fal/video/fal_pixverse_lipsync.py +0 -197
  112. package/templates/api/src/boards/generators/implementations/fal/video/fal_sora_2_text_to_video.py +0 -173
  113. package/templates/api/src/boards/generators/implementations/fal/video/infinitalk.py +0 -221
  114. package/templates/api/src/boards/generators/implementations/fal/video/kling_video_ai_avatar_v2_pro.py +0 -168
  115. package/templates/api/src/boards/generators/implementations/fal/video/kling_video_ai_avatar_v2_standard.py +0 -159
  116. package/templates/api/src/boards/generators/implementations/fal/video/kling_video_v2_5_turbo_pro_image_to_video.py +0 -175
  117. package/templates/api/src/boards/generators/implementations/fal/video/kling_video_v2_5_turbo_pro_text_to_video.py +0 -168
  118. package/templates/api/src/boards/generators/implementations/fal/video/minimax_hailuo_2_3_pro_image_to_video.py +0 -153
  119. package/templates/api/src/boards/generators/implementations/fal/video/sora2_image_to_video.py +0 -172
  120. package/templates/api/src/boards/generators/implementations/fal/video/sora_2_image_to_video_pro.py +0 -175
  121. package/templates/api/src/boards/generators/implementations/fal/video/sora_2_text_to_video_pro.py +0 -163
  122. package/templates/api/src/boards/generators/implementations/fal/video/sync_lipsync_v2.py +0 -167
  123. package/templates/api/src/boards/generators/implementations/fal/video/sync_lipsync_v2_pro.py +0 -155
  124. package/templates/api/src/boards/generators/implementations/fal/video/veed_fabric_1_0.py +0 -180
  125. package/templates/api/src/boards/generators/implementations/fal/video/veed_lipsync.py +0 -174
  126. package/templates/api/src/boards/generators/implementations/fal/video/veo3.py +0 -194
  127. package/templates/api/src/boards/generators/implementations/fal/video/veo31.py +0 -190
  128. package/templates/api/src/boards/generators/implementations/fal/video/veo31_fast.py +0 -190
  129. package/templates/api/src/boards/generators/implementations/fal/video/veo31_fast_image_to_video.py +0 -191
  130. package/templates/api/src/boards/generators/implementations/fal/video/veo31_first_last_frame_to_video.py +0 -187
  131. package/templates/api/src/boards/generators/implementations/fal/video/veo31_image_to_video.py +0 -183
  132. package/templates/api/src/boards/generators/implementations/fal/video/veo31_reference_to_video.py +0 -172
  133. package/templates/api/src/boards/generators/implementations/fal/video/wan_25_preview_image_to_video.py +0 -212
  134. package/templates/api/src/boards/generators/implementations/fal/video/wan_25_preview_text_to_video.py +0 -208
  135. package/templates/api/src/boards/generators/implementations/fal/video/wan_pro_image_to_video.py +0 -158
  136. package/templates/api/src/boards/generators/implementations/kie/__init__.py +0 -11
  137. package/templates/api/src/boards/generators/implementations/kie/base.py +0 -316
  138. package/templates/api/src/boards/generators/implementations/kie/image/__init__.py +0 -3
  139. package/templates/api/src/boards/generators/implementations/kie/image/nano_banana_edit.py +0 -190
  140. package/templates/api/src/boards/generators/implementations/kie/utils.py +0 -98
  141. package/templates/api/src/boards/generators/implementations/kie/video/__init__.py +0 -8
  142. package/templates/api/src/boards/generators/implementations/kie/video/veo3.py +0 -161
  143. package/templates/api/src/boards/generators/implementations/openai/__init__.py +0 -1
  144. package/templates/api/src/boards/generators/implementations/openai/audio/__init__.py +0 -1
  145. package/templates/api/src/boards/generators/implementations/openai/audio/whisper.py +0 -69
  146. package/templates/api/src/boards/generators/implementations/openai/image/__init__.py +0 -1
  147. package/templates/api/src/boards/generators/implementations/openai/image/dalle3.py +0 -96
  148. package/templates/api/src/boards/generators/implementations/replicate/__init__.py +0 -1
  149. package/templates/api/src/boards/generators/implementations/replicate/image/__init__.py +0 -1
  150. package/templates/api/src/boards/generators/implementations/replicate/image/flux_pro.py +0 -88
  151. package/templates/api/src/boards/generators/implementations/replicate/video/__init__.py +0 -1
  152. package/templates/api/src/boards/generators/implementations/replicate/video/lipsync.py +0 -73
  153. package/templates/api/src/boards/generators/loader.py +0 -253
  154. package/templates/api/src/boards/generators/registry.py +0 -114
  155. package/templates/api/src/boards/generators/resolution.py +0 -632
  156. package/templates/api/src/boards/generators/testmods/class_gen.py +0 -34
  157. package/templates/api/src/boards/generators/testmods/import_side_effect.py +0 -35
  158. package/templates/api/src/boards/graphql/__init__.py +0 -7
  159. package/templates/api/src/boards/graphql/access_control.py +0 -136
  160. package/templates/api/src/boards/graphql/mutations/root.py +0 -148
  161. package/templates/api/src/boards/graphql/queries/root.py +0 -116
  162. package/templates/api/src/boards/graphql/resolvers/__init__.py +0 -8
  163. package/templates/api/src/boards/graphql/resolvers/auth.py +0 -12
  164. package/templates/api/src/boards/graphql/resolvers/board.py +0 -1053
  165. package/templates/api/src/boards/graphql/resolvers/generation.py +0 -666
  166. package/templates/api/src/boards/graphql/resolvers/generator.py +0 -50
  167. package/templates/api/src/boards/graphql/resolvers/lineage.py +0 -381
  168. package/templates/api/src/boards/graphql/resolvers/upload.py +0 -463
  169. package/templates/api/src/boards/graphql/resolvers/user.py +0 -25
  170. package/templates/api/src/boards/graphql/schema.py +0 -81
  171. package/templates/api/src/boards/graphql/types/board.py +0 -102
  172. package/templates/api/src/boards/graphql/types/generation.py +0 -166
  173. package/templates/api/src/boards/graphql/types/generator.py +0 -17
  174. package/templates/api/src/boards/graphql/types/user.py +0 -47
  175. package/templates/api/src/boards/jobs/repository.py +0 -153
  176. package/templates/api/src/boards/logging.py +0 -195
  177. package/templates/api/src/boards/middleware.py +0 -339
  178. package/templates/api/src/boards/progress/__init__.py +0 -4
  179. package/templates/api/src/boards/progress/models.py +0 -25
  180. package/templates/api/src/boards/progress/publisher.py +0 -64
  181. package/templates/api/src/boards/py.typed +0 -0
  182. package/templates/api/src/boards/redis_pool.py +0 -118
  183. package/templates/api/src/boards/storage/__init__.py +0 -52
  184. package/templates/api/src/boards/storage/base.py +0 -363
  185. package/templates/api/src/boards/storage/config.py +0 -187
  186. package/templates/api/src/boards/storage/factory.py +0 -288
  187. package/templates/api/src/boards/storage/implementations/__init__.py +0 -27
  188. package/templates/api/src/boards/storage/implementations/gcs.py +0 -340
  189. package/templates/api/src/boards/storage/implementations/local.py +0 -201
  190. package/templates/api/src/boards/storage/implementations/s3.py +0 -294
  191. package/templates/api/src/boards/storage/implementations/supabase.py +0 -218
  192. package/templates/api/src/boards/tenant_isolation.py +0 -446
  193. package/templates/api/src/boards/validation.py +0 -262
  194. package/templates/api/src/boards/workers/__init__.py +0 -1
  195. package/templates/api/src/boards/workers/actors.py +0 -274
  196. package/templates/api/src/boards/workers/cli.py +0 -125
  197. package/templates/api/src/boards/workers/context.py +0 -348
  198. package/templates/api/src/boards/workers/middleware.py +0 -58
  199. package/templates/api/src/py.typed +0 -0
  200. package/templates/compose.web.yaml +0 -35
  201. package/templates/compose.yaml +0 -116
  202. package/templates/docker/env.example +0 -23
  203. package/templates/web/.env.example +0 -28
  204. package/templates/web/Dockerfile +0 -51
  205. package/templates/web/components.json +0 -22
  206. package/templates/web/imageLoader.js +0 -18
  207. package/templates/web/next-env.d.ts +0 -5
  208. package/templates/web/next.config.js +0 -36
  209. package/templates/web/package.json +0 -41
  210. package/templates/web/postcss.config.mjs +0 -7
  211. package/templates/web/public/favicon.ico +0 -0
  212. package/templates/web/src/app/boards/[boardId]/page.tsx +0 -353
  213. package/templates/web/src/app/globals.css +0 -123
  214. package/templates/web/src/app/layout.tsx +0 -31
  215. package/templates/web/src/app/lineage/[generationId]/page.tsx +0 -235
  216. package/templates/web/src/app/page.tsx +0 -35
  217. package/templates/web/src/app/providers.tsx +0 -18
  218. package/templates/web/src/components/boards/ArtifactInputSlots.tsx +0 -206
  219. package/templates/web/src/components/boards/ArtifactPreview.tsx +0 -466
  220. package/templates/web/src/components/boards/GenerationGrid.tsx +0 -282
  221. package/templates/web/src/components/boards/GenerationInput.tsx +0 -370
  222. package/templates/web/src/components/boards/GeneratorSelector.tsx +0 -272
  223. package/templates/web/src/components/boards/UploadArtifact.tsx +0 -563
  224. package/templates/web/src/components/header.tsx +0 -32
  225. package/templates/web/src/components/theme-provider.tsx +0 -10
  226. package/templates/web/src/components/theme-toggle.tsx +0 -75
  227. package/templates/web/src/components/ui/alert-dialog.tsx +0 -157
  228. package/templates/web/src/components/ui/button.tsx +0 -58
  229. package/templates/web/src/components/ui/card.tsx +0 -92
  230. package/templates/web/src/components/ui/dropdown-menu.tsx +0 -200
  231. package/templates/web/src/components/ui/navigation-menu.tsx +0 -168
  232. package/templates/web/src/components/ui/toast.tsx +0 -128
  233. package/templates/web/src/components/ui/toaster.tsx +0 -35
  234. package/templates/web/src/components/ui/use-toast.ts +0 -187
  235. package/templates/web/src/hooks/useGeneratorMRU.ts +0 -57
  236. package/templates/web/src/lib/utils.ts +0 -6
  237. package/templates/web/tsconfig.json +0 -41
@@ -1,632 +0,0 @@
1
- """
2
- Artifact resolution utilities for converting Generation references to actual files.
3
- """
4
-
5
- import base64
6
- import os
7
- import tempfile
8
- import uuid
9
- from urllib.parse import urlparse
10
-
11
- import aiofiles
12
- import httpx
13
-
14
- from ..logging import get_logger
15
- from ..storage.base import StorageManager
16
- from .artifacts import (
17
- AudioArtifact,
18
- ImageArtifact,
19
- LoRArtifact,
20
- TextArtifact,
21
- VideoArtifact,
22
- )
23
-
24
- logger = get_logger(__name__)
25
-
26
-
27
- def _rewrite_storage_url(storage_url: str) -> str:
28
- """
29
- Rewrite storage URL for Docker internal networking.
30
-
31
- Similar to the Next.js imageLoader, this rewrites public API URLs
32
- to internal Docker network URLs when running in containers.
33
-
34
- Args:
35
- storage_url: The original storage URL
36
-
37
- Returns:
38
- str: Rewritten URL if internal_api_url is configured, otherwise original URL
39
- """
40
- from ..config import settings
41
-
42
- logger.debug(
43
- "Checking URL rewriting configuration",
44
- internal_api_url=settings.internal_api_url,
45
- storage_url=storage_url[:100] if storage_url else None,
46
- )
47
-
48
- if not settings.internal_api_url:
49
- logger.debug("No internal_api_url configured, skipping URL rewrite")
50
- return storage_url
51
-
52
- # Common patterns to replace (localhost and 127.0.0.1 with various ports)
53
- # In Docker, the public URL is typically http://localhost:8800 or http://localhost:8088
54
- # We need to replace it with the internal URL (http://api:8800)
55
- replacements = [
56
- ("http://localhost:8800", settings.internal_api_url),
57
- ("http://127.0.0.1:8800", settings.internal_api_url),
58
- ("http://localhost:8088", settings.internal_api_url),
59
- ("http://127.0.0.1:8088", settings.internal_api_url),
60
- ]
61
-
62
- rewritten_url = storage_url
63
- for public_pattern, internal_url in replacements:
64
- if public_pattern in storage_url:
65
- rewritten_url = storage_url.replace(public_pattern, internal_url)
66
- logger.info(
67
- "Rewrote storage URL for internal Docker networking",
68
- original_url=storage_url,
69
- rewritten_url=rewritten_url,
70
- )
71
- break
72
-
73
- return rewritten_url
74
-
75
-
76
- async def resolve_artifact(
77
- artifact: AudioArtifact | VideoArtifact | ImageArtifact | LoRArtifact,
78
- ) -> str:
79
- """
80
- Resolve an artifact to a local file path that can be used by provider SDKs.
81
-
82
- This function downloads the artifact from storage if needed and returns
83
- a local file path that generators can pass to provider SDKs.
84
-
85
- Args:
86
- artifact: Artifact instance to resolve
87
-
88
- Returns:
89
- str: Local file path to the artifact content
90
-
91
- Raises:
92
- ValueError: If the artifact type is not supported for file resolution
93
- httpx.HTTPError: If downloading the artifact fails
94
- """
95
- if isinstance(artifact, TextArtifact):
96
- # Text artifacts don't need file resolution - they contain content directly
97
- raise ValueError(
98
- "TextArtifact cannot be resolved to a file path - use artifact.content directly"
99
- )
100
-
101
- # Validate that storage_url is actually a URL (not a local file path)
102
- # This prevents potential security issues with paths like /etc/passwd
103
- parsed = urlparse(artifact.storage_url)
104
-
105
- # Check if it's a valid URL with a scheme (http, https, s3, etc.)
106
- if parsed.scheme in ("http", "https", "s3", "gs"):
107
- # It's a remote URL, download it
108
- return await download_artifact_to_temp(artifact)
109
-
110
- # If no scheme, it might be a local file path
111
- # Only allow this if the file actually exists (for backward compatibility)
112
- if os.path.exists(artifact.storage_url):
113
- logger.debug(
114
- "Using local file path for artifact",
115
- storage_url=artifact.storage_url,
116
- )
117
- return artifact.storage_url
118
-
119
- # Download the file to a temporary location
120
- return await download_artifact_to_temp(artifact)
121
-
122
-
123
- async def download_artifact_to_temp(
124
- artifact: AudioArtifact | VideoArtifact | ImageArtifact | LoRArtifact,
125
- ) -> str:
126
- """
127
- Download an artifact from its storage URL to a temporary file.
128
-
129
- Args:
130
- artifact: Artifact to download
131
-
132
- Returns:
133
- str: Path to the temporary file containing the artifact content
134
-
135
- Raises:
136
- httpx.HTTPError: If downloading fails
137
- """
138
- # Determine file extension based on artifact type and format
139
- extension = _get_file_extension(artifact)
140
-
141
- # Create temporary file with appropriate extension (use random prefix for security)
142
- random_id = uuid.uuid4().hex[:8]
143
- temp_fd, temp_path = tempfile.mkstemp(suffix=extension, prefix=f"boards_artifact_{random_id}_")
144
-
145
- # Set restrictive file permissions (owner read/write only: 0o600)
146
- os.chmod(temp_path, 0o600)
147
-
148
- try:
149
- # Rewrite URL for Docker internal networking
150
- download_url = _rewrite_storage_url(artifact.storage_url)
151
-
152
- # Stream the download to avoid loading large files into memory
153
- async with httpx.AsyncClient(timeout=300.0) as client:
154
- logger.info(
155
- "Attempting to download artifact",
156
- original_url=artifact.storage_url,
157
- download_url=download_url,
158
- )
159
- async with client.stream("GET", download_url) as response:
160
- response.raise_for_status()
161
-
162
- # Close the file descriptor returned by mkstemp and use aiofiles
163
- os.close(temp_fd)
164
-
165
- # Stream content to file using async I/O
166
- total_bytes = 0
167
- async with aiofiles.open(temp_path, "wb") as temp_file:
168
- async for chunk in response.aiter_bytes(chunk_size=8192):
169
- await temp_file.write(chunk)
170
- total_bytes += len(chunk)
171
-
172
- # Validate that we downloaded something
173
- if total_bytes == 0:
174
- raise ValueError("Downloaded file is empty")
175
-
176
- logger.debug(
177
- "Successfully downloaded artifact to temp file",
178
- temp_path=temp_path,
179
- size_bytes=total_bytes,
180
- )
181
-
182
- return temp_path
183
-
184
- except Exception:
185
- # Clean up the temporary file if download failed
186
- try:
187
- os.unlink(temp_path)
188
- except FileNotFoundError:
189
- pass
190
- raise
191
-
192
-
193
- def _get_file_extension(
194
- artifact: AudioArtifact | VideoArtifact | ImageArtifact | LoRArtifact,
195
- ) -> str:
196
- """
197
- Get the appropriate file extension for an artifact based on its format.
198
-
199
- Args:
200
- artifact: Artifact to get extension for
201
-
202
- Returns:
203
- str: File extension including the dot (e.g., '.mp4', '.png')
204
- """
205
- format_ext = artifact.format.lower()
206
-
207
- # Add dot if not present
208
- if not format_ext.startswith("."):
209
- format_ext = f".{format_ext}"
210
-
211
- return format_ext
212
-
213
-
214
- def _decode_data_url(data_url: str) -> bytes:
215
- """
216
- Decode a data URL to bytes.
217
-
218
- Supports data URLs in the format: data:[<mediatype>][;base64],<data>
219
-
220
- Args:
221
- data_url: Data URL string (e.g., "data:image/png;base64,iVBORw0KGgo...")
222
-
223
- Returns:
224
- bytes: Decoded content
225
-
226
- Raises:
227
- ValueError: If data URL is malformed or empty
228
- """
229
- if not data_url.startswith("data:"):
230
- raise ValueError("Invalid data URL: must start with 'data:'")
231
-
232
- # Split off the "data:" prefix
233
- try:
234
- # Format: data:[<mediatype>][;base64],<data>
235
- header, data = data_url[5:].split(",", 1)
236
- except ValueError as e:
237
- raise ValueError("Invalid data URL format: missing comma separator") from e
238
-
239
- if not data:
240
- raise ValueError("Data URL contains no data after comma")
241
-
242
- # Check if base64 encoded
243
- is_base64 = ";base64" in header
244
-
245
- if is_base64:
246
- try:
247
- decoded = base64.b64decode(data)
248
- except Exception as e:
249
- raise ValueError(f"Failed to decode base64 data: {e}") from e
250
- else:
251
- # URL-encoded data (rare for binary content)
252
- from urllib.parse import unquote
253
-
254
- decoded = unquote(data).encode("utf-8")
255
-
256
- if len(decoded) == 0:
257
- raise ValueError("Decoded data URL is empty")
258
-
259
- logger.info(
260
- "Successfully decoded data URL",
261
- size_bytes=len(decoded),
262
- is_base64=is_base64,
263
- )
264
- return decoded
265
-
266
-
267
- async def download_from_url(url: str) -> bytes:
268
- """
269
- Download content from a URL (typically a provider's temporary URL).
270
-
271
- This is used to download generated content from providers like Replicate, OpenAI, etc.
272
- before uploading to our permanent storage.
273
-
274
- Supports both HTTP(S) URLs and data URLs (data:mime/type;base64,...)
275
-
276
- Note: For very large files, consider using streaming downloads directly to storage
277
- instead of loading into memory.
278
-
279
- Args:
280
- url: URL to download from (HTTP(S) or data URL)
281
-
282
- Returns:
283
- bytes: Downloaded content
284
-
285
- Raises:
286
- httpx.HTTPError: If download fails
287
- ValueError: If downloaded content is empty or data URL is malformed
288
- """
289
- logger.debug("Downloading content from URL", url=url[:50])
290
-
291
- # Check if this is a data URL
292
- if url.startswith("data:"):
293
- return _decode_data_url(url)
294
-
295
- # Stream download to avoid loading entire file into memory at once
296
- async with httpx.AsyncClient(timeout=60.0) as client:
297
- async with client.stream("GET", url) as response:
298
- response.raise_for_status()
299
-
300
- # Collect chunks
301
- chunks = []
302
- total_bytes = 0
303
- async for chunk in response.aiter_bytes(chunk_size=8192):
304
- chunks.append(chunk)
305
- total_bytes += len(chunk)
306
-
307
- # Validate content
308
- if total_bytes == 0:
309
- raise ValueError(f"Downloaded file from {url} is empty")
310
-
311
- logger.info(
312
- "Successfully downloaded content",
313
- url=url,
314
- size_bytes=total_bytes,
315
- )
316
- return b"".join(chunks)
317
-
318
-
319
- def _get_content_type_from_format(artifact_type: str, format: str) -> str:
320
- """
321
- Get MIME content type from artifact type and format.
322
-
323
- Args:
324
- artifact_type: Type of artifact ('image', 'video', 'audio')
325
- format: Format string (e.g., 'png', 'mp4', 'mp3')
326
-
327
- Returns:
328
- str: MIME content type
329
- """
330
- format_lower = format.lower()
331
-
332
- # Map common formats to content types
333
- content_type_map = {
334
- "image": {
335
- "png": "image/png",
336
- "jpg": "image/jpeg",
337
- "jpeg": "image/jpeg",
338
- "webp": "image/webp",
339
- "gif": "image/gif",
340
- },
341
- "video": {
342
- "mp4": "video/mp4",
343
- "webm": "video/webm",
344
- "mov": "video/quicktime",
345
- },
346
- "audio": {
347
- "mp3": "audio/mpeg",
348
- "wav": "audio/wav",
349
- "ogg": "audio/ogg",
350
- },
351
- }
352
-
353
- type_map = content_type_map.get(artifact_type, {})
354
- return type_map.get(format_lower, "application/octet-stream")
355
-
356
-
357
- async def store_image_result(
358
- storage_manager: StorageManager,
359
- generation_id: str,
360
- tenant_id: str,
361
- board_id: str,
362
- storage_url: str,
363
- format: str,
364
- width: int | None = None,
365
- height: int | None = None,
366
- ) -> ImageArtifact:
367
- """
368
- Store an image result by downloading from provider URL and uploading to storage.
369
-
370
- Args:
371
- storage_manager: Storage manager instance
372
- generation_id: ID of the generation
373
- tenant_id: Tenant ID for storage isolation
374
- board_id: Board ID for organization
375
- storage_url: Provider's temporary URL to download from
376
- format: Image format (png, jpg, etc.)
377
- width: Image width in pixels (optional)
378
- height: Image height in pixels (optional)
379
-
380
- Returns:
381
- ImageArtifact with permanent storage URL
382
-
383
- Raises:
384
- StorageException: If storage operation fails
385
- httpx.HTTPError: If download fails
386
- """
387
- logger.info(
388
- "Storing image result",
389
- generation_id=generation_id,
390
- provider_url=storage_url[:50],
391
- format=format,
392
- )
393
-
394
- # Download content from provider URL
395
- content = await download_from_url(storage_url)
396
-
397
- # Determine content type
398
- content_type = _get_content_type_from_format("image", format)
399
-
400
- # Upload to storage system
401
- artifact_ref = await storage_manager.store_artifact(
402
- artifact_id=generation_id,
403
- content=content,
404
- artifact_type="image",
405
- content_type=content_type,
406
- tenant_id=tenant_id,
407
- board_id=board_id,
408
- )
409
-
410
- logger.info(
411
- "Image stored successfully",
412
- generation_id=generation_id,
413
- storage_key=artifact_ref.storage_key,
414
- storage_url=artifact_ref.storage_url[:50],
415
- )
416
-
417
- # Return artifact with our permanent storage URL
418
- return ImageArtifact(
419
- generation_id=generation_id,
420
- storage_url=artifact_ref.storage_url,
421
- width=width,
422
- height=height,
423
- format=format,
424
- )
425
-
426
-
427
- async def store_video_result(
428
- storage_manager: StorageManager,
429
- generation_id: str,
430
- tenant_id: str,
431
- board_id: str,
432
- storage_url: str,
433
- format: str,
434
- width: int | None = None,
435
- height: int | None = None,
436
- duration: float | None = None,
437
- fps: float | None = None,
438
- ) -> VideoArtifact:
439
- """
440
- Store a video result by downloading from provider URL and uploading to storage.
441
-
442
- Args:
443
- storage_manager: Storage manager instance
444
- generation_id: ID of the generation
445
- tenant_id: Tenant ID for storage isolation
446
- board_id: Board ID for organization
447
- storage_url: Provider's temporary URL to download from
448
- format: Video format (mp4, webm, etc.)
449
- width: Video width in pixels (optional)
450
- height: Video height in pixels (optional)
451
- duration: Video duration in seconds (optional)
452
- fps: Frames per second (optional)
453
-
454
- Returns:
455
- VideoArtifact with permanent storage URL
456
-
457
- Raises:
458
- StorageException: If storage operation fails
459
- httpx.HTTPError: If download fails
460
- """
461
- logger.info(
462
- "Storing video result",
463
- generation_id=generation_id,
464
- provider_url=storage_url[:50],
465
- format=format,
466
- )
467
-
468
- # Download content from provider URL
469
- content = await download_from_url(storage_url)
470
-
471
- # Determine content type
472
- content_type = _get_content_type_from_format("video", format)
473
-
474
- # Upload to storage system
475
- artifact_ref = await storage_manager.store_artifact(
476
- artifact_id=generation_id,
477
- content=content,
478
- artifact_type="video",
479
- content_type=content_type,
480
- tenant_id=tenant_id,
481
- board_id=board_id,
482
- )
483
-
484
- logger.info(
485
- "Video stored successfully",
486
- generation_id=generation_id,
487
- storage_key=artifact_ref.storage_key,
488
- storage_url=artifact_ref.storage_url[:50],
489
- )
490
-
491
- # Return artifact with our permanent storage URL
492
- return VideoArtifact(
493
- generation_id=generation_id,
494
- storage_url=artifact_ref.storage_url,
495
- width=width,
496
- height=height,
497
- format=format,
498
- duration=duration,
499
- fps=fps,
500
- )
501
-
502
-
503
- async def store_audio_result(
504
- storage_manager: StorageManager,
505
- generation_id: str,
506
- tenant_id: str,
507
- board_id: str,
508
- storage_url: str,
509
- format: str,
510
- duration: float | None = None,
511
- sample_rate: int | None = None,
512
- channels: int | None = None,
513
- ) -> AudioArtifact:
514
- """
515
- Store an audio result by downloading from provider URL and uploading to storage.
516
-
517
- Args:
518
- storage_manager: Storage manager instance
519
- generation_id: ID of the generation
520
- tenant_id: Tenant ID for storage isolation
521
- board_id: Board ID for organization
522
- storage_url: Provider's temporary URL to download from
523
- format: Audio format (mp3, wav, etc.)
524
- duration: Audio duration in seconds (optional)
525
- sample_rate: Sample rate in Hz (optional)
526
- channels: Number of audio channels (optional)
527
-
528
- Returns:
529
- AudioArtifact with permanent storage URL
530
-
531
- Raises:
532
- StorageException: If storage operation fails
533
- httpx.HTTPError: If download fails
534
- """
535
- logger.info(
536
- "Storing audio result",
537
- generation_id=generation_id,
538
- provider_url=storage_url[:50],
539
- format=format,
540
- )
541
-
542
- # Download content from provider URL
543
- content = await download_from_url(storage_url)
544
-
545
- # Determine content type
546
- content_type = _get_content_type_from_format("audio", format)
547
-
548
- # Upload to storage system
549
- artifact_ref = await storage_manager.store_artifact(
550
- artifact_id=generation_id,
551
- content=content,
552
- artifact_type="audio",
553
- content_type=content_type,
554
- tenant_id=tenant_id,
555
- board_id=board_id,
556
- )
557
-
558
- logger.info(
559
- "Audio stored successfully",
560
- generation_id=generation_id,
561
- storage_key=artifact_ref.storage_key,
562
- storage_url=artifact_ref.storage_url[:50],
563
- )
564
-
565
- # Return artifact with our permanent storage URL
566
- return AudioArtifact(
567
- generation_id=generation_id,
568
- storage_url=artifact_ref.storage_url,
569
- format=format,
570
- duration=duration,
571
- sample_rate=sample_rate,
572
- channels=channels,
573
- )
574
-
575
-
576
- async def store_text_result(
577
- storage_manager: StorageManager,
578
- generation_id: str,
579
- tenant_id: str,
580
- board_id: str,
581
- content: str,
582
- format: str,
583
- ) -> TextArtifact:
584
- """
585
- Store a text result by uploading to storage.
586
-
587
- Args:
588
- storage_manager: Storage manager instance
589
- generation_id: ID of the generation
590
- tenant_id: Tenant ID for storage isolation
591
- board_id: Board ID for organization
592
- content: Text content to store
593
- format: Text format (plain, markdown, html, etc.)
594
-
595
- Returns:
596
- TextArtifact with permanent storage URL
597
-
598
- Raises:
599
- StorageException: If storage operation fails
600
- httpx.HTTPError: If upload fails
601
- """
602
- logger.info(
603
- "Storing text result",
604
- generation_id=generation_id,
605
- content=content[:50],
606
- format=format,
607
- )
608
-
609
- # Upload to storage system
610
- artifact_ref = await storage_manager.store_artifact(
611
- artifact_id=generation_id,
612
- content=content.encode("utf-8"),
613
- artifact_type="text",
614
- content_type="text/plain",
615
- tenant_id=tenant_id,
616
- board_id=board_id,
617
- )
618
-
619
- logger.info(
620
- "Text stored successfully",
621
- generation_id=generation_id,
622
- storage_key=artifact_ref.storage_key,
623
- storage_url=artifact_ref.storage_url[:50],
624
- )
625
-
626
- # Return artifact with our permanent storage URL
627
- return TextArtifact(
628
- generation_id=generation_id,
629
- storage_url=artifact_ref.storage_url,
630
- content=content[:50],
631
- format=format,
632
- )
@@ -1,34 +0,0 @@
1
- """Test helper generator class for loader unit tests (class-based)."""
2
-
3
- from pydantic import BaseModel
4
-
5
- from ..base import BaseGenerator
6
-
7
-
8
- class _Input(BaseModel):
9
- text: str = "hello"
10
-
11
-
12
- class _Output(BaseModel):
13
- text: str
14
-
15
-
16
- class ClassGen(BaseGenerator):
17
- name = "class-gen"
18
- artifact_type = "text"
19
- description = "Test class-based generator"
20
-
21
- def __init__(self, suffix: str | None = None):
22
- self.suffix = suffix or "!"
23
-
24
- def get_input_schema(self) -> type[_Input]:
25
- return _Input
26
-
27
- def get_output_schema(self) -> type[_Output]:
28
- return _Output
29
-
30
- async def generate(self, inputs: _Input, context) -> _Output: # type: ignore[override]
31
- return _Output(text=f"{inputs.text}{self.suffix}")
32
-
33
- async def estimate_cost(self, inputs: _Input) -> float: # type: ignore[override]
34
- return 0.0
@@ -1,35 +0,0 @@
1
- """Test helper module that registers on import (import-based)."""
2
-
3
- from pydantic import BaseModel
4
-
5
- from ..base import BaseGenerator
6
- from ..registry import registry
7
-
8
-
9
- class _Input(BaseModel):
10
- pass
11
-
12
-
13
- class _Output(BaseModel):
14
- pass
15
-
16
-
17
- class ImportGen(BaseGenerator):
18
- name = "import-gen"
19
- artifact_type = "text"
20
- description = "Test import-based generator"
21
-
22
- def get_input_schema(self) -> type[_Input]:
23
- return _Input
24
-
25
- def get_output_schema(self) -> type[_Output]:
26
- return _Output
27
-
28
- async def generate(self, inputs: _Input, context) -> _Output: # type: ignore[override]
29
- return _Output()
30
-
31
- async def estimate_cost(self, inputs: _Input) -> float: # type: ignore[override]
32
- return 0.0
33
-
34
-
35
- registry.register(ImportGen())
@@ -1,7 +0,0 @@
1
- """
2
- GraphQL API for Boards backend
3
- """
4
-
5
- from .schema import schema
6
-
7
- __all__ = ["schema"]