Getting Started with Generators
This guide will walk you through creating your first generator, from setup to testing. We'll build a simple text-to-image generator using Replicate's API.
Prerequisites
Before you start, make sure you have:
- Boards development environment set up
- A Replicate API token (get one here)
- Basic familiarity with Python and Pydantic
Your First Generator
Let's create a generator that uses Stable Diffusion to generate images from text prompts.
Step 1: Environment Setup
First, set your API key:
export REPLICATE_API_TOKEN="r8_your_token_here"
Or add it to your .env file:
REPLICATE_API_TOKEN=r8_your_token_here
Step 2: Create the Generator File
Create a new file at packages/backend/src/boards/generators/implementations/image/stable_diffusion.py:
"""
Stable Diffusion generator using Replicate API.
"""
from typing import Type
from pydantic import BaseModel, Field
from ...base import BaseGenerator
from ...artifacts import ImageArtifact
from ...resolution import store_image_result
from ...registry import registry
class StableDiffusionInput(BaseModel):
"""Input schema for Stable Diffusion generation."""
prompt: str = Field(
description="Text description of the image to generate",
min_length=1,
max_length=500
)
negative_prompt: str = Field(
default="",
description="What to avoid in the generated image"
)
width: int = Field(
default=512,
description="Image width in pixels",
ge=64,
le=2048
)
height: int = Field(
default=512,
description="Image height in pixels",
ge=64,
le=2048
)
num_inference_steps: int = Field(
default=50,
description="Number of denoising steps",
ge=1,
le=500
)
guidance_scale: float = Field(
default=7.5,
description="How closely to follow the prompt",
ge=1.0,
le=20.0
)
class StableDiffusionOutput(BaseModel):
"""Output schema for Stable Diffusion generation."""
image: ImageArtifact
class StableDiffusionGenerator(BaseGenerator):
"""Stable Diffusion image generator using Replicate."""
name = "stable-diffusion"
artifact_type = "image"
description = "Stable Diffusion 1.5 - text-to-image generation with fine control"
def get_input_schema(self) -> Type[StableDiffusionInput]:
return StableDiffusionInput
def get_output_schema(self) -> Type[StableDiffusionOutput]:
return StableDiffusionOutput
async def generate(self, inputs: StableDiffusionInput) -> StableDiffusionOutput:
"""Generate image using Stable Diffusion via Replicate."""
import os
import replicate
# Check for API key
if not os.getenv("REPLICATE_API_TOKEN"):
raise ValueError(
"REPLICATE_API_TOKEN environment variable is required. "
"Get your token from https://replicate.com/account/api-tokens"
)
# Use Replicate SDK directly
output = await replicate.async_run(
"stability-ai/stable-diffusion:db21e45d3f7023abc2a46ee38a23973f6dce16bb082a930b0c49861f96d1e5bf",
input={
"prompt": inputs.prompt,
"negative_prompt": inputs.negative_prompt,
"width": inputs.width,
"height": inputs.height,
"num_inference_steps": inputs.num_inference_steps,
"guidance_scale": inputs.guidance_scale,
}
)
# Replicate returns a list of URLs
image_url = output[0] if isinstance(output, list) else output
# Store result and create artifact
image_artifact = await store_image_result(
storage_url=image_url,
format="png",
generation_id="temp_gen_id", # TODO: Get from generation context
width=inputs.width,
height=inputs.height
)
return StableDiffusionOutput(image=image_artifact)
async def estimate_cost(self, inputs: StableDiffusionInput) -> float:
"""Estimate cost for Stable Diffusion generation."""
# Stable Diffusion on Replicate costs about $0.0023 per image
# Cost increases with more inference steps
base_cost = 0.0023
step_multiplier = inputs.num_inference_steps / 50 # 50 is the default
return base_cost * step_multiplier
# Register the generator
registry.register(StableDiffusionGenerator())
Step 3: Test Your Generator
Create a test file packages/backend/tests/generators/implementations/test_stable_diffusion.py:
"""
Tests for StableDiffusionGenerator.
"""
import pytest
import os
from unittest.mock import patch
from boards.generators.implementations.image.stable_diffusion import (
StableDiffusionGenerator,
StableDiffusionInput,
StableDiffusionOutput,
)
class TestStableDiffusionGenerator:
def setup_method(self):
self.generator = StableDiffusionGenerator()
def test_generator_metadata(self):
"""Test generator has correct metadata."""
assert self.generator.name == "stable-diffusion"
assert self.generator.artifact_type == "image"
assert "Stable Diffusion" in self.generator.description
def test_input_validation(self):
"""Test input schema validation."""
# Valid input
valid_input = StableDiffusionInput(
prompt="A beautiful sunset over mountains",
width=768,
height=768
)
assert valid_input.prompt == "A beautiful sunset over mountains"
assert valid_input.width == 768
# Invalid input (too large)
with pytest.raises(ValueError):
StableDiffusionInput(
prompt="Test",
width=3000 # Too large
)
@pytest.mark.asyncio
async def test_generate_success(self):
"""Test successful generation."""
inputs = StableDiffusionInput(
prompt="A red apple",
width=512,
height=512
)
fake_output_url = "https://replicate.delivery/fake-image.png"
with patch.dict(os.environ, {"REPLICATE_API_TOKEN": "fake-token"}):
with patch('replicate.async_run') as mock_run:
with patch('boards.generators.implementations.image.stable_diffusion.store_image_result') as mock_store:
# Mock Replicate response
mock_run.return_value = [fake_output_url]
# Mock storage
from boards.generators.artifacts import ImageArtifact
mock_artifact = ImageArtifact(
generation_id="test",
storage_url=fake_output_url,
width=512,
height=512,
format="png"
)
mock_store.return_value = mock_artifact
# Test generation
result = await self.generator.generate(inputs)
assert isinstance(result, StableDiffusionOutput)
assert result.image == mock_artifact
# Verify correct API call
mock_run.assert_called_once()
call_args = mock_run.call_args
assert "stability-ai/stable-diffusion" in call_args[0][0]
assert call_args[1]["input"]["prompt"] == "A red apple"
@pytest.mark.asyncio
async def test_cost_estimation(self):
"""Test cost estimation."""
inputs = StableDiffusionInput(prompt="Test", num_inference_steps=100)
cost = await self.generator.estimate_cost(inputs)
# Should be roughly double the base cost due to double the steps
assert cost > 0.004 # More than base cost
assert cost < 0.01 # But not too much
Step 4: Run the Tests
cd packages/backend
python -m pytest tests/generators/implementations/test_stable_diffusion.py -v
Step 5: Import Your Generator
To make your generator available to the system, import it in the image generators init file:
# packages/backend/src/boards/generators/implementations/image/__init__.py
from . import flux_pro
from . import dalle3
from . import stable_diffusion # Add this line
Step 6: Verify Registration
You can verify your generator is registered:
from boards.generators.registry import registry
# List all generators
print("All generators:", registry.list_names())
# Get your specific generator
sd_gen = registry.get("stable-diffusion")
print("Generator:", sd_gen.name, sd_gen.description)
# List image generators
image_gens = registry.list_by_artifact_type("image")
print(f"Image generators: {[g.name for g in image_gens]}")
Understanding the Code
Let's break down what we built:
Input Schema (StableDiffusionInput)
- Validation: Pydantic validates all inputs automatically
- Constraints:
min_length,ge(greater/equal),le(less/equal) provide bounds - Descriptions: Used for API documentation and UI generation
- Defaults: Fields can have sensible defaults
Generator Class
- Metadata: Name, type, and description identify the generator
- Schema Methods: Define what inputs and outputs look like
- Generation Logic: Uses provider SDK directly, no wrappers
- Cost Estimation: Provides budget planning functionality
Error Handling
- Environment Checks: Validates API keys early
- Clear Messages: Provides actionable error information
- Provider Errors: Could be enhanced to catch and translate provider-specific errors
Frontend Integration
Once your generator is registered, the frontend automatically gets:
- TypeScript Types: Generated from your Pydantic schemas
- Form UI: Text inputs, number sliders, dropdowns based on field types
- Validation: Client-side validation matching your schema
- Cost Display: Shows estimated cost before generation
The form would look something like:
// Auto-generated TypeScript interface
interface StableDiffusionInput {
prompt: string;
negative_prompt: string;
width: number; // Slider from 64 to 2048
height: number; // Slider from 64 to 2048
num_inference_steps: number; // Slider from 1 to 500
guidance_scale: number; // Slider from 1.0 to 20.0
}
Next Steps
Now that you have a working generator:
- Add More Validation: Use Pydantic validators for complex rules
- Handle More Providers: Try OpenAI, Fal.ai, or other services
- Create Artifact Chains: Build generators that use other artifacts as inputs
- Add Comprehensive Tests: Test edge cases, error conditions, and cost calculations
- Contribute Examples: Share your generators with the community
Common Patterns
Environment Variable Validation
def check_api_key(self, env_var: str, provider_name: str):
if not os.getenv(env_var):
raise ValueError(
f"{env_var} environment variable is required. "
f"Get your API key from {provider_name}"
)
Provider Error Translation
try:
result = await provider.generate(inputs)
except provider.AuthError:
raise ValueError("Invalid API key")
except provider.RateLimitError as e:
raise ValueError(f"Rate limited - retry in {e.retry_after}s")
Dynamic Cost Calculation
async def estimate_cost(self, inputs):
base_cost = 0.001
# More steps = higher cost
step_factor = inputs.num_inference_steps / 50
# Larger images = higher cost
size_factor = (inputs.width * inputs.height) / (512 * 512)
return base_cost * step_factor * size_factor
You now have a fully functional generator integrated into the Boards system!