Skip to content

Commit 8c8a2ab

Browse files
authored
feat(blocks): Add Bannerbear API block for text overlay on images (#10768)
## Summary - Implemented a new Bannerbear API block that enables adding text overlays to images using template designs - Block supports customizable text styling (color, font, size, weight, alignment) - Always uses synchronous API mode for immediate image generation results [agent_ead942d9-58a2-4be6-bdb3-99010c489466.json](https://github.com/user-attachments/files/22027352/agent_ead942d9-58a2-4be6-bdb3-99010c489466.json) <img width="140" height="572" alt="Screenshot 2025-08-28 at 16 28 35" src="https://github.com/user-attachments/assets/096b532b-31dc-4ca6-bd68-c00b7594426c" /> ## Features - **Text overlay capabilities**: Add multiple text layers to images using Bannerbear templates - **Customizable styling**: Support for color, font family, font size, font weight, and text alignment - **Image support**: Optional ability to add images to templates - **Smart field handling**: Only sends non-empty optional parameters to the API - **Webhook & metadata**: Advanced options for webhook notifications and custom metadata ## Implementation Details - Created provider configuration with API key authentication - Implemented `BannerbearTextOverlayBlock` with proper input/output schemas - Extracted API calls to private method `_make_api_request()` for test mocking support - Follows SDK guide patterns and integrates with AutoGPT platform ## Use Case This block will be used in the Ad generator agent for creating dynamic marketing materials and social media graphics with text overlays. ## Test plan - [x] Block imports successfully - [x] Block instantiates with unique ID - [x] Code passes linting and formatting checks - [x] Manual testing with actual Bannerbear API key - [x] Integration testing with Ad generator agent
1 parent 4041e1f commit 8c8a2ab

File tree

3 files changed

+250
-0
lines changed

3 files changed

+250
-0
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .text_overlay import BannerbearTextOverlayBlock
2+
3+
__all__ = ["BannerbearTextOverlayBlock"]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from backend.sdk import BlockCostType, ProviderBuilder
2+
3+
bannerbear = (
4+
ProviderBuilder("bannerbear")
5+
.with_api_key("BANNERBEAR_API_KEY", "Bannerbear API Key")
6+
.with_base_cost(1, BlockCostType.RUN)
7+
.build()
8+
)
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
import uuid
2+
from typing import TYPE_CHECKING, Any, Dict, List
3+
4+
if TYPE_CHECKING:
5+
pass
6+
7+
from pydantic import SecretStr
8+
9+
from backend.sdk import (
10+
APIKeyCredentials,
11+
Block,
12+
BlockCategory,
13+
BlockOutput,
14+
BlockSchema,
15+
CredentialsMetaInput,
16+
Requests,
17+
SchemaField,
18+
)
19+
20+
from ._config import bannerbear
21+
22+
TEST_CREDENTIALS = APIKeyCredentials(
23+
id="01234567-89ab-cdef-0123-456789abcdef",
24+
provider="bannerbear",
25+
api_key=SecretStr("mock-bannerbear-api-key"),
26+
title="Mock Bannerbear API Key",
27+
)
28+
29+
30+
class TextModification(BlockSchema):
31+
name: str = SchemaField(
32+
description="The name of the layer to modify in the template"
33+
)
34+
text: str = SchemaField(description="The text content to add to this layer")
35+
color: str = SchemaField(
36+
description="Hex color code for the text (e.g., '#FF0000')",
37+
default="",
38+
advanced=True,
39+
)
40+
font_family: str = SchemaField(
41+
description="Font family to use for the text",
42+
default="",
43+
advanced=True,
44+
)
45+
font_size: int = SchemaField(
46+
description="Font size in pixels",
47+
default=0,
48+
advanced=True,
49+
)
50+
font_weight: str = SchemaField(
51+
description="Font weight (e.g., 'bold', 'normal')",
52+
default="",
53+
advanced=True,
54+
)
55+
text_align: str = SchemaField(
56+
description="Text alignment (left, center, right)",
57+
default="",
58+
advanced=True,
59+
)
60+
61+
62+
class BannerbearTextOverlayBlock(Block):
63+
class Input(BlockSchema):
64+
credentials: CredentialsMetaInput = bannerbear.credentials_field(
65+
description="API credentials for Bannerbear"
66+
)
67+
template_id: str = SchemaField(
68+
description="The unique ID of your Bannerbear template"
69+
)
70+
project_id: str = SchemaField(
71+
description="Optional: Project ID (required when using Master API Key)",
72+
default="",
73+
advanced=True,
74+
)
75+
text_modifications: List[TextModification] = SchemaField(
76+
description="List of text layers to modify in the template"
77+
)
78+
image_url: str = SchemaField(
79+
description="Optional: URL of an image to use in the template",
80+
default="",
81+
advanced=True,
82+
)
83+
image_layer_name: str = SchemaField(
84+
description="Optional: Name of the image layer in the template",
85+
default="photo",
86+
advanced=True,
87+
)
88+
webhook_url: str = SchemaField(
89+
description="Optional: URL to receive webhook notification when image is ready",
90+
default="",
91+
advanced=True,
92+
)
93+
metadata: str = SchemaField(
94+
description="Optional: Custom metadata to attach to the image",
95+
default="",
96+
advanced=True,
97+
)
98+
99+
class Output(BlockSchema):
100+
success: bool = SchemaField(
101+
description="Whether the image generation was successfully initiated"
102+
)
103+
image_url: str = SchemaField(
104+
description="URL of the generated image (if synchronous) or placeholder"
105+
)
106+
uid: str = SchemaField(description="Unique identifier for the generated image")
107+
status: str = SchemaField(description="Status of the image generation")
108+
error: str = SchemaField(description="Error message if the operation failed")
109+
110+
def __init__(self):
111+
super().__init__(
112+
id="c7d3a5c2-05fc-450e-8dce-3b0e04626009",
113+
description="Add text overlay to images using Bannerbear templates. Perfect for creating social media graphics, marketing materials, and dynamic image content.",
114+
categories={BlockCategory.PRODUCTIVITY, BlockCategory.AI},
115+
input_schema=self.Input,
116+
output_schema=self.Output,
117+
test_input={
118+
"template_id": "jJWBKNELpQPvbX5R93Gk",
119+
"text_modifications": [
120+
{
121+
"name": "headline",
122+
"text": "Amazing Product Launch!",
123+
"color": "#FF0000",
124+
},
125+
{
126+
"name": "subtitle",
127+
"text": "50% OFF Today Only",
128+
},
129+
],
130+
"credentials": {
131+
"provider": "bannerbear",
132+
"id": str(uuid.uuid4()),
133+
"type": "api_key",
134+
},
135+
},
136+
test_output=[
137+
("success", True),
138+
("image_url", "https://cdn.bannerbear.com/test-image.jpg"),
139+
("uid", "test-uid-123"),
140+
("status", "completed"),
141+
],
142+
test_mock={
143+
"_make_api_request": lambda *args, **kwargs: {
144+
"uid": "test-uid-123",
145+
"status": "completed",
146+
"image_url": "https://cdn.bannerbear.com/test-image.jpg",
147+
}
148+
},
149+
test_credentials=TEST_CREDENTIALS,
150+
)
151+
152+
async def _make_api_request(self, payload: dict, api_key: str) -> dict:
153+
"""Make the actual API request to Bannerbear. This is separated for easy mocking in tests."""
154+
headers = {
155+
"Authorization": f"Bearer {api_key}",
156+
"Content-Type": "application/json",
157+
}
158+
159+
response = await Requests().post(
160+
"https://sync.api.bannerbear.com/v2/images",
161+
headers=headers,
162+
json=payload,
163+
)
164+
165+
if response.status in [200, 201, 202]:
166+
return response.json()
167+
else:
168+
error_msg = f"API request failed with status {response.status}"
169+
if response.text:
170+
try:
171+
error_data = response.json()
172+
error_msg = (
173+
f"{error_msg}: {error_data.get('message', response.text)}"
174+
)
175+
except Exception:
176+
error_msg = f"{error_msg}: {response.text}"
177+
raise Exception(error_msg)
178+
179+
async def run(
180+
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
181+
) -> BlockOutput:
182+
# Build the modifications array
183+
modifications = []
184+
185+
# Add text modifications
186+
for text_mod in input_data.text_modifications:
187+
mod_data: Dict[str, Any] = {
188+
"name": text_mod.name,
189+
"text": text_mod.text,
190+
}
191+
192+
# Add optional text styling parameters only if they have values
193+
if text_mod.color and text_mod.color.strip():
194+
mod_data["color"] = text_mod.color
195+
if text_mod.font_family and text_mod.font_family.strip():
196+
mod_data["font_family"] = text_mod.font_family
197+
if text_mod.font_size and text_mod.font_size > 0:
198+
mod_data["font_size"] = text_mod.font_size
199+
if text_mod.font_weight and text_mod.font_weight.strip():
200+
mod_data["font_weight"] = text_mod.font_weight
201+
if text_mod.text_align and text_mod.text_align.strip():
202+
mod_data["text_align"] = text_mod.text_align
203+
204+
modifications.append(mod_data)
205+
206+
# Add image modification if provided and not empty
207+
if input_data.image_url and input_data.image_url.strip():
208+
modifications.append(
209+
{
210+
"name": input_data.image_layer_name,
211+
"image_url": input_data.image_url,
212+
}
213+
)
214+
215+
# Build the request payload - only include non-empty optional fields
216+
payload = {
217+
"template": input_data.template_id,
218+
"modifications": modifications,
219+
}
220+
221+
# Add project_id if provided (required for Master API keys)
222+
if input_data.project_id and input_data.project_id.strip():
223+
payload["project_id"] = input_data.project_id
224+
225+
if input_data.webhook_url and input_data.webhook_url.strip():
226+
payload["webhook_url"] = input_data.webhook_url
227+
if input_data.metadata and input_data.metadata.strip():
228+
payload["metadata"] = input_data.metadata
229+
230+
# Make the API request using the private method
231+
data = await self._make_api_request(
232+
payload, credentials.api_key.get_secret_value()
233+
)
234+
235+
# Synchronous request - image should be ready
236+
yield "success", True
237+
yield "image_url", data.get("image_url", "")
238+
yield "uid", data.get("uid", "")
239+
yield "status", data.get("status", "completed")

0 commit comments

Comments
 (0)