@weirdfingers/baseboards 0.2.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 (139) hide show
  1. package/README.md +191 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +887 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +64 -0
  6. package/templates/README.md +120 -0
  7. package/templates/api/.env.example +62 -0
  8. package/templates/api/Dockerfile +32 -0
  9. package/templates/api/README.md +132 -0
  10. package/templates/api/alembic/env.py +106 -0
  11. package/templates/api/alembic/script.py.mako +28 -0
  12. package/templates/api/alembic/versions/20250101_000000_initial_schema.py +448 -0
  13. package/templates/api/alembic/versions/20251022_174729_remove_provider_name_from_generations.py +71 -0
  14. package/templates/api/alembic/versions/20251023_165852_switch_to_declarative_base_and_mapping.py +411 -0
  15. package/templates/api/alembic/versions/2025925_62735_add_seed_data_for_default_tenant.py +85 -0
  16. package/templates/api/alembic.ini +36 -0
  17. package/templates/api/config/generators.yaml +25 -0
  18. package/templates/api/config/storage_config.yaml +26 -0
  19. package/templates/api/docs/ADDING_GENERATORS.md +409 -0
  20. package/templates/api/docs/GENERATORS_API.md +502 -0
  21. package/templates/api/docs/MIGRATIONS.md +472 -0
  22. package/templates/api/docs/storage_providers.md +337 -0
  23. package/templates/api/pyproject.toml +165 -0
  24. package/templates/api/src/boards/__init__.py +10 -0
  25. package/templates/api/src/boards/api/app.py +171 -0
  26. package/templates/api/src/boards/api/auth.py +75 -0
  27. package/templates/api/src/boards/api/endpoints/__init__.py +3 -0
  28. package/templates/api/src/boards/api/endpoints/jobs.py +76 -0
  29. package/templates/api/src/boards/api/endpoints/setup.py +505 -0
  30. package/templates/api/src/boards/api/endpoints/sse.py +129 -0
  31. package/templates/api/src/boards/api/endpoints/storage.py +74 -0
  32. package/templates/api/src/boards/api/endpoints/tenant_registration.py +296 -0
  33. package/templates/api/src/boards/api/endpoints/webhooks.py +13 -0
  34. package/templates/api/src/boards/auth/__init__.py +15 -0
  35. package/templates/api/src/boards/auth/adapters/__init__.py +20 -0
  36. package/templates/api/src/boards/auth/adapters/auth0.py +220 -0
  37. package/templates/api/src/boards/auth/adapters/base.py +73 -0
  38. package/templates/api/src/boards/auth/adapters/clerk.py +172 -0
  39. package/templates/api/src/boards/auth/adapters/jwt.py +122 -0
  40. package/templates/api/src/boards/auth/adapters/none.py +102 -0
  41. package/templates/api/src/boards/auth/adapters/oidc.py +284 -0
  42. package/templates/api/src/boards/auth/adapters/supabase.py +110 -0
  43. package/templates/api/src/boards/auth/context.py +35 -0
  44. package/templates/api/src/boards/auth/factory.py +115 -0
  45. package/templates/api/src/boards/auth/middleware.py +221 -0
  46. package/templates/api/src/boards/auth/provisioning.py +129 -0
  47. package/templates/api/src/boards/auth/tenant_extraction.py +278 -0
  48. package/templates/api/src/boards/cli.py +354 -0
  49. package/templates/api/src/boards/config.py +116 -0
  50. package/templates/api/src/boards/database/__init__.py +7 -0
  51. package/templates/api/src/boards/database/cli.py +110 -0
  52. package/templates/api/src/boards/database/connection.py +252 -0
  53. package/templates/api/src/boards/database/models.py +19 -0
  54. package/templates/api/src/boards/database/seed_data.py +182 -0
  55. package/templates/api/src/boards/dbmodels/__init__.py +455 -0
  56. package/templates/api/src/boards/generators/__init__.py +57 -0
  57. package/templates/api/src/boards/generators/artifacts.py +53 -0
  58. package/templates/api/src/boards/generators/base.py +140 -0
  59. package/templates/api/src/boards/generators/implementations/__init__.py +12 -0
  60. package/templates/api/src/boards/generators/implementations/audio/__init__.py +3 -0
  61. package/templates/api/src/boards/generators/implementations/audio/whisper.py +66 -0
  62. package/templates/api/src/boards/generators/implementations/image/__init__.py +3 -0
  63. package/templates/api/src/boards/generators/implementations/image/dalle3.py +93 -0
  64. package/templates/api/src/boards/generators/implementations/image/flux_pro.py +85 -0
  65. package/templates/api/src/boards/generators/implementations/video/__init__.py +3 -0
  66. package/templates/api/src/boards/generators/implementations/video/lipsync.py +70 -0
  67. package/templates/api/src/boards/generators/loader.py +253 -0
  68. package/templates/api/src/boards/generators/registry.py +114 -0
  69. package/templates/api/src/boards/generators/resolution.py +515 -0
  70. package/templates/api/src/boards/generators/testmods/class_gen.py +34 -0
  71. package/templates/api/src/boards/generators/testmods/import_side_effect.py +35 -0
  72. package/templates/api/src/boards/graphql/__init__.py +7 -0
  73. package/templates/api/src/boards/graphql/access_control.py +136 -0
  74. package/templates/api/src/boards/graphql/mutations/root.py +136 -0
  75. package/templates/api/src/boards/graphql/queries/root.py +116 -0
  76. package/templates/api/src/boards/graphql/resolvers/__init__.py +8 -0
  77. package/templates/api/src/boards/graphql/resolvers/auth.py +12 -0
  78. package/templates/api/src/boards/graphql/resolvers/board.py +1055 -0
  79. package/templates/api/src/boards/graphql/resolvers/generation.py +889 -0
  80. package/templates/api/src/boards/graphql/resolvers/generator.py +50 -0
  81. package/templates/api/src/boards/graphql/resolvers/user.py +25 -0
  82. package/templates/api/src/boards/graphql/schema.py +81 -0
  83. package/templates/api/src/boards/graphql/types/board.py +102 -0
  84. package/templates/api/src/boards/graphql/types/generation.py +130 -0
  85. package/templates/api/src/boards/graphql/types/generator.py +17 -0
  86. package/templates/api/src/boards/graphql/types/user.py +47 -0
  87. package/templates/api/src/boards/jobs/repository.py +104 -0
  88. package/templates/api/src/boards/logging.py +195 -0
  89. package/templates/api/src/boards/middleware.py +339 -0
  90. package/templates/api/src/boards/progress/__init__.py +4 -0
  91. package/templates/api/src/boards/progress/models.py +25 -0
  92. package/templates/api/src/boards/progress/publisher.py +64 -0
  93. package/templates/api/src/boards/py.typed +0 -0
  94. package/templates/api/src/boards/redis_pool.py +118 -0
  95. package/templates/api/src/boards/storage/__init__.py +52 -0
  96. package/templates/api/src/boards/storage/base.py +363 -0
  97. package/templates/api/src/boards/storage/config.py +187 -0
  98. package/templates/api/src/boards/storage/factory.py +278 -0
  99. package/templates/api/src/boards/storage/implementations/__init__.py +27 -0
  100. package/templates/api/src/boards/storage/implementations/gcs.py +340 -0
  101. package/templates/api/src/boards/storage/implementations/local.py +201 -0
  102. package/templates/api/src/boards/storage/implementations/s3.py +294 -0
  103. package/templates/api/src/boards/storage/implementations/supabase.py +218 -0
  104. package/templates/api/src/boards/tenant_isolation.py +446 -0
  105. package/templates/api/src/boards/validation.py +262 -0
  106. package/templates/api/src/boards/workers/__init__.py +1 -0
  107. package/templates/api/src/boards/workers/actors.py +201 -0
  108. package/templates/api/src/boards/workers/cli.py +125 -0
  109. package/templates/api/src/boards/workers/context.py +188 -0
  110. package/templates/api/src/boards/workers/middleware.py +58 -0
  111. package/templates/api/src/py.typed +0 -0
  112. package/templates/compose.dev.yaml +39 -0
  113. package/templates/compose.yaml +109 -0
  114. package/templates/docker/env.example +23 -0
  115. package/templates/web/.env.example +28 -0
  116. package/templates/web/Dockerfile +51 -0
  117. package/templates/web/components.json +22 -0
  118. package/templates/web/imageLoader.js +18 -0
  119. package/templates/web/next-env.d.ts +5 -0
  120. package/templates/web/next.config.js +36 -0
  121. package/templates/web/package.json +37 -0
  122. package/templates/web/postcss.config.mjs +7 -0
  123. package/templates/web/public/favicon.ico +0 -0
  124. package/templates/web/src/app/boards/[boardId]/page.tsx +232 -0
  125. package/templates/web/src/app/globals.css +120 -0
  126. package/templates/web/src/app/layout.tsx +21 -0
  127. package/templates/web/src/app/page.tsx +35 -0
  128. package/templates/web/src/app/providers.tsx +18 -0
  129. package/templates/web/src/components/boards/ArtifactInputSlots.tsx +142 -0
  130. package/templates/web/src/components/boards/ArtifactPreview.tsx +125 -0
  131. package/templates/web/src/components/boards/GenerationGrid.tsx +45 -0
  132. package/templates/web/src/components/boards/GenerationInput.tsx +251 -0
  133. package/templates/web/src/components/boards/GeneratorSelector.tsx +89 -0
  134. package/templates/web/src/components/header.tsx +30 -0
  135. package/templates/web/src/components/ui/button.tsx +58 -0
  136. package/templates/web/src/components/ui/card.tsx +92 -0
  137. package/templates/web/src/components/ui/navigation-menu.tsx +168 -0
  138. package/templates/web/src/lib/utils.ts +6 -0
  139. package/templates/web/tsconfig.json +47 -0
@@ -0,0 +1,502 @@
1
+ # Generators API Reference
2
+
3
+ This document provides detailed API reference for the Boards generators system.
4
+
5
+ ## Core Classes
6
+
7
+ ### BaseGenerator
8
+
9
+ Abstract base class for all generators.
10
+
11
+ ```python
12
+ from boards.generators.base import BaseGenerator
13
+
14
+ class MyGenerator(BaseGenerator):
15
+ name: str # Unique identifier for the generator
16
+ artifact_type: str # Type of artifact produced ('image', 'video', 'audio', 'text', 'lora')
17
+ description: str # Human-readable description
18
+
19
+ def get_input_schema(self) -> Type[BaseModel]:
20
+ """Return Pydantic model class for input validation."""
21
+ pass
22
+
23
+ async def generate(self, inputs: BaseModel) -> BaseModel:
24
+ """Execute generation and return results."""
25
+ pass
26
+
27
+ async def estimate_cost(self, inputs: BaseModel) -> float:
28
+ """Estimate cost in USD for this generation."""
29
+ pass
30
+
31
+ def get_output_schema(self) -> Type[BaseModel]:
32
+ """Return Pydantic model class for output (optional override)."""
33
+ pass
34
+ ```
35
+
36
+ #### Required Attributes
37
+
38
+ - **`name`**: Unique string identifier (e.g., `"flux-pro"`, `"whisper"`)
39
+ - **`artifact_type`**: One of `"image"`, `"video"`, `"audio"`, `"text"`, `"lora"`
40
+ - **`description`**: Brief description of what the generator does
41
+
42
+ #### Required Methods
43
+
44
+ - **`get_input_schema()`**: Return the Pydantic model class that defines valid inputs
45
+ - **`generate(inputs)`**: Core generation logic that produces artifacts
46
+ - **`estimate_cost(inputs)`**: Return estimated cost in USD as a float
47
+
48
+ ## Artifact Types
49
+
50
+ ### ImageArtifact
51
+
52
+ Represents generated or input images.
53
+
54
+ ```python
55
+ from boards.generators.artifacts import ImageArtifact
56
+
57
+ artifact = ImageArtifact(
58
+ generation_id="gen_123", # ID of generation that created this
59
+ storage_url="https://...", # URL where image is stored
60
+ width=1024, # Image width in pixels
61
+ height=1024, # Image height in pixels
62
+ format="png" # Image format (png, jpg, webp, etc.)
63
+ )
64
+ ```
65
+
66
+ ### VideoArtifact
67
+
68
+ Represents generated or input videos.
69
+
70
+ ```python
71
+ from boards.generators.artifacts import VideoArtifact
72
+
73
+ artifact = VideoArtifact(
74
+ generation_id="gen_456", # Required: generation ID
75
+ storage_url="https://...", # Required: storage location
76
+ width=1920, # Required: video width
77
+ height=1080, # Required: video height
78
+ format="mp4", # Required: video format
79
+ duration=60.5, # Optional: duration in seconds
80
+ fps=30.0 # Optional: frames per second
81
+ )
82
+ ```
83
+
84
+ ### AudioArtifact
85
+
86
+ Represents generated or input audio.
87
+
88
+ ```python
89
+ from boards.generators.artifacts import AudioArtifact
90
+
91
+ artifact = AudioArtifact(
92
+ generation_id="gen_789", # Required: generation ID
93
+ storage_url="https://...", # Required: storage location
94
+ format="mp3", # Required: audio format
95
+ duration=120.0, # Optional: duration in seconds
96
+ sample_rate=44100, # Optional: sample rate in Hz
97
+ channels=2 # Optional: number of channels
98
+ )
99
+ ```
100
+
101
+ ### TextArtifact
102
+
103
+ Represents generated or input text.
104
+
105
+ ```python
106
+ from boards.generators.artifacts import TextArtifact
107
+
108
+ artifact = TextArtifact(
109
+ generation_id="gen_text", # Required: generation ID
110
+ content="Generated text...", # Required: the actual text content
111
+ format="plain" # Optional: format (plain, markdown, html)
112
+ )
113
+ ```
114
+
115
+ ### LoRArtifact
116
+
117
+ Represents LoRA (Low-Rank Adaptation) models.
118
+
119
+ ```python
120
+ from boards.generators.artifacts import LoRArtifact
121
+
122
+ artifact = LoRArtifact(
123
+ generation_id="gen_lora", # Required: generation ID
124
+ storage_url="https://...", # Required: storage location
125
+ base_model="sd-v1.5", # Required: base model name
126
+ format="safetensors", # Required: file format
127
+ trigger_words=["style1", "tag"] # Optional: trigger words list
128
+ )
129
+ ```
130
+
131
+ ## Generator Registry
132
+
133
+ ### Global Registry
134
+
135
+ The system provides a global registry for managing generators:
136
+
137
+ ```python
138
+ from boards.generators.registry import registry
139
+
140
+ # Register a generator
141
+ generator_instance = MyGenerator()
142
+ registry.register(generator_instance)
143
+
144
+ # Get a generator by name
145
+ generator = registry.get("my-generator")
146
+
147
+ # List all generators
148
+ all_generators = registry.list_all()
149
+
150
+ # List generators by artifact type
151
+ image_generators = registry.list_by_artifact_type("image")
152
+
153
+ # Check if generator exists
154
+ if "my-generator" in registry:
155
+ print("Generator is registered")
156
+
157
+ # Get count of registered generators
158
+ count = len(registry)
159
+ ```
160
+
161
+ ### GeneratorRegistry Methods
162
+
163
+ ```python
164
+ class GeneratorRegistry:
165
+ def register(self, generator: BaseGenerator) -> None:
166
+ """Register a generator instance."""
167
+
168
+ def get(self, name: str) -> Optional[BaseGenerator]:
169
+ """Get generator by name, returns None if not found."""
170
+
171
+ def list_all(self) -> List[BaseGenerator]:
172
+ """Return all registered generators."""
173
+
174
+ def list_by_artifact_type(self, artifact_type: str) -> List[BaseGenerator]:
175
+ """Return generators that produce the specified artifact type."""
176
+
177
+ def list_names(self) -> List[str]:
178
+ """Return list of all registered generator names."""
179
+
180
+ def unregister(self, name: str) -> bool:
181
+ """Remove generator by name. Returns True if found and removed."""
182
+
183
+ def clear(self) -> None:
184
+ """Remove all registered generators."""
185
+
186
+ def __contains__(self, name: str) -> bool:
187
+ """Check if generator with name is registered."""
188
+
189
+ def __len__(self) -> int:
190
+ """Return number of registered generators."""
191
+ ```
192
+
193
+ ## Artifact Resolution
194
+
195
+ ### resolve_artifact()
196
+
197
+ Converts artifact references to local file paths for use with provider SDKs:
198
+
199
+ ```python
200
+ from boards.generators.resolution import resolve_artifact
201
+
202
+ async def generate(self, inputs):
203
+ # Resolve artifacts to local file paths
204
+ audio_path = await resolve_artifact(inputs.audio_source)
205
+ video_path = await resolve_artifact(inputs.video_source)
206
+
207
+ # Now you can pass file paths to any provider SDK
208
+ result = await provider.process(audio=audio_path, video=video_path)
209
+ ```
210
+
211
+ **Supported artifacts**: `AudioArtifact`, `VideoArtifact`, `ImageArtifact`, `LoRArtifact`
212
+
213
+ **Not supported**: `TextArtifact` (use `.content` property directly)
214
+
215
+ ### Store Result Functions
216
+
217
+ Create artifact instances from generated content:
218
+
219
+ ```python
220
+ from boards.generators.resolution import (
221
+ store_image_result,
222
+ store_video_result,
223
+ store_audio_result
224
+ )
225
+
226
+ # Store generated image
227
+ image_artifact = await store_image_result(
228
+ storage_url="https://storage.com/image.png",
229
+ format="png",
230
+ generation_id="gen_123",
231
+ width=1024,
232
+ height=1024
233
+ )
234
+
235
+ # Store generated video
236
+ video_artifact = await store_video_result(
237
+ storage_url="https://storage.com/video.mp4",
238
+ format="mp4",
239
+ generation_id="gen_456",
240
+ width=1920,
241
+ height=1080,
242
+ duration=60.0, # Optional
243
+ fps=30.0 # Optional
244
+ )
245
+
246
+ # Store generated audio
247
+ audio_artifact = await store_audio_result(
248
+ storage_url="https://storage.com/audio.mp3",
249
+ format="mp3",
250
+ generation_id="gen_789",
251
+ duration=120.0, # Optional
252
+ sample_rate=44100, # Optional
253
+ channels=2 # Optional
254
+ )
255
+ ```
256
+
257
+ ## Pydantic Integration
258
+
259
+ ### Input Schema Patterns
260
+
261
+ #### Basic Inputs
262
+
263
+ ```python
264
+ from pydantic import BaseModel, Field
265
+
266
+ class BasicInput(BaseModel):
267
+ prompt: str = Field(description="Text prompt")
268
+ strength: float = Field(default=0.75, ge=0.0, le=1.0, description="Generation strength")
269
+ seed: Optional[int] = Field(None, description="Random seed")
270
+ ```
271
+
272
+ #### Artifact Inputs
273
+
274
+ ```python
275
+ class ArtifactInput(BaseModel):
276
+ image_source: ImageArtifact = Field(description="Input image")
277
+ audio_source: AudioArtifact = Field(description="Input audio")
278
+ reference_text: TextArtifact = Field(description="Reference text")
279
+ ```
280
+
281
+ #### Validation and Constraints
282
+
283
+ ```python
284
+ from pydantic import field_validator
285
+
286
+ class ValidatedInput(BaseModel):
287
+ prompt: str = Field(min_length=1, max_length=500)
288
+ quality: str = Field(pattern="^(low|medium|high)$")
289
+ dimensions: str = Field(pattern="^\\d+x\\d+$")
290
+
291
+ @field_validator('prompt')
292
+ @classmethod
293
+ def validate_prompt(cls, v):
294
+ if 'forbidden' in v.lower():
295
+ raise ValueError('Prompt contains forbidden content')
296
+ return v.strip()
297
+ ```
298
+
299
+ #### Conditional Fields
300
+
301
+ ```python
302
+ class ConditionalInput(BaseModel):
303
+ mode: str = Field(pattern="^(text|image)$")
304
+ text_prompt: Optional[str] = Field(None)
305
+ image_prompt: Optional[ImageArtifact] = Field(None)
306
+
307
+ @model_validator(mode='after')
308
+ def validate_conditional_fields(self):
309
+ if self.mode == 'text' and not self.text_prompt:
310
+ raise ValueError('text_prompt required when mode=text')
311
+ elif self.mode == 'image' and not self.image_prompt:
312
+ raise ValueError('image_prompt required when mode=image')
313
+ return self
314
+ ```
315
+
316
+ ### Output Schema Patterns
317
+
318
+ #### Simple Outputs
319
+
320
+ ```python
321
+ class SimpleOutput(BaseModel):
322
+ result: ImageArtifact
323
+ metadata: dict = Field(default_factory=dict)
324
+ ```
325
+
326
+ #### Multiple Artifacts
327
+
328
+ ```python
329
+ class MultiOutput(BaseModel):
330
+ images: List[ImageArtifact] = Field(description="Generated images")
331
+ preview: ImageArtifact = Field(description="Low-res preview")
332
+ generation_time: float = Field(description="Time taken in seconds")
333
+ ```
334
+
335
+ ## JSON Schema Generation
336
+
337
+ Pydantic models automatically generate JSON schemas for frontend integration:
338
+
339
+ ```python
340
+ # Get JSON schema for frontend type generation
341
+ schema = MyInputClass.model_json_schema()
342
+
343
+ # Example output:
344
+ {
345
+ "type": "object",
346
+ "properties": {
347
+ "prompt": {
348
+ "type": "string",
349
+ "description": "Text prompt",
350
+ "minLength": 1,
351
+ "maxLength": 500
352
+ },
353
+ "strength": {
354
+ "type": "number",
355
+ "minimum": 0.0,
356
+ "maximum": 1.0,
357
+ "default": 0.75,
358
+ "description": "Generation strength"
359
+ }
360
+ },
361
+ "required": ["prompt"]
362
+ }
363
+ ```
364
+
365
+ ## Error Handling
366
+
367
+ ### Common Error Patterns
368
+
369
+ ```python
370
+ async def generate(self, inputs):
371
+ # Check environment variables
372
+ api_key = os.getenv("PROVIDER_API_KEY")
373
+ if not api_key:
374
+ raise ValueError(
375
+ "PROVIDER_API_KEY environment variable required. "
376
+ "Get your key from https://provider.com/keys"
377
+ )
378
+
379
+ # Provider-specific error handling
380
+ try:
381
+ result = await provider.generate(inputs)
382
+ except provider.AuthenticationError:
383
+ raise ValueError("Invalid API key - check PROVIDER_API_KEY")
384
+ except provider.RateLimitError as e:
385
+ raise ValueError(f"Rate limited - retry after {e.retry_after} seconds")
386
+ except provider.ValidationError as e:
387
+ raise ValueError(f"Invalid input: {e.message}")
388
+ except Exception as e:
389
+ raise RuntimeError(f"Generation failed: {str(e)}")
390
+ ```
391
+
392
+ ### Best Practices
393
+
394
+ 1. **Specific Error Messages**: Provide actionable error messages
395
+ 2. **Environment Variable Checks**: Validate required configuration early
396
+ 3. **Provider Error Translation**: Convert provider errors to user-friendly messages
397
+ 4. **Resource Cleanup**: Clean up temporary files in finally blocks
398
+
399
+ ## Testing
400
+
401
+ ### Generator Testing Pattern
402
+
403
+ ```python
404
+ import pytest
405
+ from unittest.mock import patch, AsyncMock
406
+
407
+ class TestMyGenerator:
408
+ def setup_method(self):
409
+ self.generator = MyGenerator()
410
+
411
+ def test_metadata(self):
412
+ assert self.generator.name == "my-generator"
413
+ assert self.generator.artifact_type == "image"
414
+
415
+ def test_input_schema(self):
416
+ schema_class = self.generator.get_input_schema()
417
+ assert schema_class == MyInputClass
418
+
419
+ # Test schema validation
420
+ valid_input = schema_class(prompt="test")
421
+ assert valid_input.prompt == "test"
422
+
423
+ @pytest.mark.asyncio
424
+ async def test_generate_success(self):
425
+ inputs = MyInputClass(prompt="test prompt")
426
+
427
+ with patch.dict(os.environ, {"API_KEY": "fake-key"}):
428
+ with patch('provider.generate') as mock_generate:
429
+ mock_generate.return_value = "fake_result_url"
430
+
431
+ result = await self.generator.generate(inputs)
432
+
433
+ assert isinstance(result, MyOutputClass)
434
+ mock_generate.assert_called_once()
435
+
436
+ @pytest.mark.asyncio
437
+ async def test_generate_missing_api_key(self):
438
+ inputs = MyInputClass(prompt="test")
439
+
440
+ with patch.dict(os.environ, {}, clear=True):
441
+ with pytest.raises(ValueError, match="API_KEY.*required"):
442
+ await self.generator.generate(inputs)
443
+
444
+ @pytest.mark.asyncio
445
+ async def test_estimate_cost(self):
446
+ inputs = MyInputClass(prompt="short")
447
+ cost = await self.generator.estimate_cost(inputs)
448
+
449
+ assert isinstance(cost, float)
450
+ assert cost > 0
451
+ ```
452
+
453
+ ## Integration Points
454
+
455
+ ### Storage System Integration
456
+
457
+ Generators integrate with the Boards storage system through the resolution utilities:
458
+
459
+ ```python
460
+ # The store_*_result functions will be implemented to:
461
+ # 1. Upload content to configured storage backend (S3, local, etc.)
462
+ # 2. Return artifact with proper storage_url
463
+ # 3. Handle metadata and thumbnails
464
+ ```
465
+
466
+ ### Database Integration
467
+
468
+ Generated artifacts are automatically stored in the database with:
469
+
470
+ - Generation metadata (model, parameters, cost)
471
+ - Artifact metadata (dimensions, duration, format)
472
+ - Relationships to boards and users
473
+ - Audit trail information
474
+
475
+ ### GraphQL API Integration
476
+
477
+ Registered generators are automatically exposed via GraphQL:
478
+
479
+ ```graphql
480
+ query GetGenerators($artifactType: String) {
481
+ generators(artifactType: $artifactType) {
482
+ name
483
+ artifactType
484
+ description
485
+ inputSchema # JSON schema for frontend
486
+ }
487
+ }
488
+
489
+ mutation RunGeneration($generatorName: String!, $inputs: JSON!) {
490
+ runGeneration(generatorName: $generatorName, inputs: $inputs) {
491
+ id
492
+ status
493
+ result {
494
+ ... on ImageArtifact {
495
+ storageUrl
496
+ width
497
+ height
498
+ }
499
+ }
500
+ }
501
+ }
502
+ ```