Skip to content

Commit 8e634d7

Browse files
authored
feat(store): Generate AI images for store submissions (Significant-Gravitas#9090)
Allow generating ai images for store submissions
1 parent d028f5b commit 8e634d7

File tree

6 files changed

+275
-25
lines changed

6 files changed

+275
-25
lines changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import io
2+
import logging
3+
from enum import Enum
4+
5+
import replicate
6+
import replicate.exceptions
7+
import requests
8+
from replicate.helpers import FileOutput
9+
10+
from backend.data.graph import Graph
11+
from backend.util.settings import Settings
12+
13+
logger = logging.getLogger(__name__)
14+
15+
16+
class ImageSize(str, Enum):
17+
LANDSCAPE = "1024x768"
18+
19+
20+
class ImageStyle(str, Enum):
21+
DIGITAL_ART = "digital art"
22+
23+
24+
async def generate_agent_image(agent: Graph) -> io.BytesIO:
25+
"""
26+
Generate an image for an agent using Flux model via Replicate API.
27+
28+
Args:
29+
agent (Graph): The agent to generate an image for
30+
31+
Returns:
32+
io.BytesIO: The generated image as bytes
33+
"""
34+
try:
35+
settings = Settings()
36+
37+
if not settings.secrets.replicate_api_key:
38+
raise ValueError("Missing Replicate API key in settings")
39+
40+
# Construct prompt from agent details
41+
prompt = f"App store image for AI agent that gives a cool visual representation of what the agent does: - {agent.name} - {agent.description}"
42+
43+
# Set up Replicate client
44+
client = replicate.Client(api_token=settings.secrets.replicate_api_key)
45+
46+
# Model parameters
47+
input_data = {
48+
"prompt": prompt,
49+
"width": 1024,
50+
"height": 768,
51+
"aspect_ratio": "4:3",
52+
"output_format": "jpg",
53+
"output_quality": 90,
54+
"num_inference_steps": 30,
55+
"guidance": 3.5,
56+
"negative_prompt": "blurry, low quality, distorted, deformed",
57+
"disable_safety_checker": True,
58+
}
59+
60+
try:
61+
# Run model
62+
output = client.run("black-forest-labs/flux-pro", input=input_data)
63+
64+
# Depending on the model output, extract the image URL or bytes
65+
# If the output is a list of FileOutput or URLs
66+
if isinstance(output, list) and output:
67+
if isinstance(output[0], FileOutput):
68+
image_bytes = output[0].read()
69+
else:
70+
# If it's a URL string, fetch the image bytes
71+
result_url = output[0]
72+
response = requests.get(result_url)
73+
response.raise_for_status()
74+
image_bytes = response.content
75+
elif isinstance(output, FileOutput):
76+
image_bytes = output.read()
77+
elif isinstance(output, str):
78+
# Output is a URL
79+
response = requests.get(output)
80+
response.raise_for_status()
81+
image_bytes = response.content
82+
else:
83+
raise RuntimeError("Unexpected output format from the model.")
84+
85+
return io.BytesIO(image_bytes)
86+
87+
except replicate.exceptions.ReplicateError as e:
88+
if e.status == 401:
89+
raise RuntimeError("Invalid Replicate API token") from e
90+
raise RuntimeError(f"Replicate API error: {str(e)}") from e
91+
92+
except Exception as e:
93+
logger.exception("Failed to generate agent image")
94+
raise RuntimeError(f"Image generation failed: {str(e)}")

autogpt_platform/backend/backend/server/v2/store/media.py

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,45 @@
1515
MAX_FILE_SIZE = 50 * 1024 * 1024 # 50MB
1616

1717

18-
async def upload_media(user_id: str, file: fastapi.UploadFile) -> str:
18+
async def check_media_exists(user_id: str, filename: str) -> str | None:
19+
"""
20+
Check if a media file exists in storage for the given user.
21+
Tries both images and videos directories.
22+
23+
Args:
24+
user_id (str): ID of the user who uploaded the file
25+
filename (str): Name of the file to check
26+
27+
Returns:
28+
str | None: URL of the blob if it exists, None otherwise
29+
"""
30+
try:
31+
settings = Settings()
32+
storage_client = storage.Client()
33+
bucket = storage_client.bucket(settings.config.media_gcs_bucket_name)
34+
35+
# Check images
36+
image_path = f"users/{user_id}/images/{filename}"
37+
image_blob = bucket.blob(image_path)
38+
if image_blob.exists():
39+
return image_blob.public_url
40+
41+
# Check videos
42+
video_path = f"users/{user_id}/videos/{filename}"
43+
44+
video_blob = bucket.blob(video_path)
45+
if video_blob.exists():
46+
return video_blob.public_url
47+
48+
return None
49+
except Exception as e:
50+
logger.error(f"Error checking if media file exists: {str(e)}")
51+
return None
52+
53+
54+
async def upload_media(
55+
user_id: str, file: fastapi.UploadFile, use_file_name: bool = False
56+
) -> str:
1957

2058
# Get file content for deeper validation
2159
try:
@@ -84,6 +122,9 @@ async def upload_media(user_id: str, file: fastapi.UploadFile) -> str:
84122
try:
85123
# Validate file type
86124
content_type = file.content_type
125+
if content_type is None:
126+
content_type = "image/jpeg"
127+
87128
if (
88129
content_type not in ALLOWED_IMAGE_TYPES
89130
and content_type not in ALLOWED_VIDEO_TYPES
@@ -119,7 +160,10 @@ async def upload_media(user_id: str, file: fastapi.UploadFile) -> str:
119160
# Generate unique filename
120161
filename = file.filename or ""
121162
file_ext = os.path.splitext(filename)[1].lower()
122-
unique_filename = f"{uuid.uuid4()}{file_ext}"
163+
if use_file_name:
164+
unique_filename = filename
165+
else:
166+
unique_filename = f"{uuid.uuid4()}{file_ext}"
123167

124168
# Construct storage path
125169
media_type = "images" if content_type in ALLOWED_IMAGE_TYPES else "videos"

autogpt_platform/backend/backend/server/v2/store/routes.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
import fastapi
77
import fastapi.responses
88

9+
import backend.data.graph
910
import backend.server.v2.store.db
11+
import backend.server.v2.store.image_gen
1012
import backend.server.v2.store.media
1113
import backend.server.v2.store.model
1214

@@ -439,3 +441,63 @@ async def upload_submission_media(
439441
raise fastapi.HTTPException(
440442
status_code=500, detail=f"Failed to upload media file: {str(e)}"
441443
)
444+
445+
446+
@router.post(
447+
"/submissions/generate_image",
448+
tags=["store", "private"],
449+
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
450+
)
451+
async def generate_image(
452+
agent_id: str,
453+
user_id: typing.Annotated[
454+
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
455+
],
456+
) -> fastapi.responses.Response:
457+
"""
458+
Generate an image for a store listing submission.
459+
460+
Args:
461+
agent_id (str): ID of the agent to generate an image for
462+
user_id (str): ID of the authenticated user
463+
464+
Returns:
465+
JSONResponse: JSON containing the URL of the generated image
466+
"""
467+
try:
468+
agent = await backend.data.graph.get_graph(agent_id, user_id=user_id)
469+
470+
if not agent:
471+
raise fastapi.HTTPException(
472+
status_code=404, detail=f"Agent with ID {agent_id} not found"
473+
)
474+
# Use .jpeg here since we are generating JPEG images
475+
filename = f"agent_{agent_id}.jpeg"
476+
477+
existing_url = await backend.server.v2.store.media.check_media_exists(
478+
user_id, filename
479+
)
480+
if existing_url:
481+
logger.info(f"Using existing image for agent {agent_id}")
482+
return fastapi.responses.JSONResponse(content={"image_url": existing_url})
483+
# Generate agent image as JPEG
484+
image = await backend.server.v2.store.image_gen.generate_agent_image(
485+
agent=agent
486+
)
487+
488+
# Create UploadFile with the correct filename and content_type
489+
image_file = fastapi.UploadFile(
490+
file=image,
491+
filename=filename,
492+
)
493+
494+
image_url = await backend.server.v2.store.media.upload_media(
495+
user_id=user_id, file=image_file, use_file_name=True
496+
)
497+
498+
return fastapi.responses.JSONResponse(content={"image_url": image_url})
499+
except Exception as e:
500+
logger.exception("Exception occurred whilst generating submission image")
501+
raise fastapi.HTTPException(
502+
status_code=500, detail=f"Failed to generate image: {str(e)}"
503+
)

autogpt_platform/frontend/src/components/agptui/PublishAgentSelectInfo.tsx

Lines changed: 62 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ interface PublishAgentInfoProps {
1919
) => void;
2020
onClose: () => void;
2121
initialData?: {
22+
agent_id: string;
2223
title: string;
2324
subheader: string;
2425
slug: string;
@@ -36,6 +37,7 @@ export const PublishAgentInfo: React.FC<PublishAgentInfoProps> = ({
3637
onClose,
3738
initialData,
3839
}) => {
40+
const [agentId, setAgentId] = React.useState<string | null>(null);
3941
const [images, setImages] = React.useState<string[]>(
4042
initialData?.additionalImages
4143
? [initialData.thumbnailSrc, ...initialData.additionalImages]
@@ -59,10 +61,10 @@ export const PublishAgentInfo: React.FC<PublishAgentInfoProps> = ({
5961
);
6062
const [slug, setSlug] = React.useState(initialData?.slug || "");
6163
const thumbnailsContainerRef = React.useRef<HTMLDivElement | null>(null);
62-
6364
React.useEffect(() => {
6465
if (initialData) {
65-
setImages(initialData.additionalImages || []);
66+
setAgentId(initialData.agent_id);
67+
setImagesWithValidation(initialData.additionalImages || []);
6668
setSelectedImage(initialData.thumbnailSrc || null);
6769
setTitle(initialData.title);
6870
setSubheader(initialData.subheader);
@@ -73,10 +75,18 @@ export const PublishAgentInfo: React.FC<PublishAgentInfoProps> = ({
7375
}
7476
}, [initialData]);
7577

78+
const setImagesWithValidation = (newImages: string[]) => {
79+
// Remove duplicates
80+
const uniqueImages = Array.from(new Set(newImages));
81+
// Keep only first 5 images
82+
const limitedImages = uniqueImages.slice(0, 5);
83+
setImages(limitedImages);
84+
};
85+
7686
const handleRemoveImage = (indexToRemove: number) => {
7787
const newImages = [...images];
7888
newImages.splice(indexToRemove, 1);
79-
setImages(newImages);
89+
setImagesWithValidation(newImages);
8090
if (newImages[indexToRemove] === selectedImage) {
8191
setSelectedImage(newImages[0] || null);
8292
}
@@ -88,6 +98,8 @@ export const PublishAgentInfo: React.FC<PublishAgentInfoProps> = ({
8898
};
8999

90100
const handleAddImage = async () => {
101+
if (images.length >= 5) return;
102+
91103
const input = document.createElement("input");
92104
input.type = "file";
93105
input.accept = "image/*";
@@ -115,11 +127,7 @@ export const PublishAgentInfo: React.FC<PublishAgentInfoProps> = ({
115127
"$1",
116128
);
117129

118-
setImages((prev) => {
119-
const newImages = [...prev, imageUrl];
120-
console.log("Added image. Images now:", newImages);
121-
return newImages;
122-
});
130+
setImagesWithValidation([...images, imageUrl]);
123131
if (!selectedImage) {
124132
setSelectedImage(imageUrl);
125133
}
@@ -128,6 +136,27 @@ export const PublishAgentInfo: React.FC<PublishAgentInfoProps> = ({
128136
}
129137
};
130138

139+
const [isGenerating, setIsGenerating] = React.useState(false);
140+
141+
const handleGenerateImage = async () => {
142+
if (isGenerating || images.length >= 5) return;
143+
144+
setIsGenerating(true);
145+
try {
146+
const api = new BackendAPI();
147+
if (!agentId) {
148+
throw new Error("Agent ID is required");
149+
}
150+
const { image_url } = await api.generateStoreSubmissionImage(agentId);
151+
console.log("image_url", image_url);
152+
setImagesWithValidation([...images, image_url]);
153+
} catch (error) {
154+
console.error("Failed to generate image:", error);
155+
} finally {
156+
setIsGenerating(false);
157+
}
158+
};
159+
131160
const handleSubmit = (e: React.MouseEvent<HTMLButtonElement>) => {
132161
e.preventDefault();
133162
onSubmit(title, subheader, slug, description, images, youtubeLink, [
@@ -284,19 +313,21 @@ export const PublishAgentInfo: React.FC<PublishAgentInfoProps> = ({
284313
</button>
285314
</div>
286315
))}
287-
<Button
288-
onClick={handleAddImage}
289-
variant="ghost"
290-
className="flex h-[70px] w-[100px] flex-col items-center justify-center rounded-md bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-700 dark:hover:bg-neutral-600"
291-
>
292-
<IconPlus
293-
size="lg"
294-
className="text-neutral-600 dark:text-neutral-300"
295-
/>
296-
<span className="mt-1 font-['Geist'] text-xs font-normal text-neutral-600 dark:text-neutral-300">
297-
Add image
298-
</span>
299-
</Button>
316+
{images.length < 5 && (
317+
<Button
318+
onClick={handleAddImage}
319+
variant="ghost"
320+
className="flex h-[70px] w-[100px] flex-col items-center justify-center rounded-md bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-700 dark:hover:bg-neutral-600"
321+
>
322+
<IconPlus
323+
size="lg"
324+
className="text-neutral-600 dark:text-neutral-300"
325+
/>
326+
<span className="mt-1 font-['Geist'] text-xs font-normal text-neutral-600 dark:text-neutral-300">
327+
Add image
328+
</span>
329+
</Button>
330+
)}
300331
</>
301332
)}
302333
</div>
@@ -313,9 +344,17 @@ export const PublishAgentInfo: React.FC<PublishAgentInfoProps> = ({
313344
<Button
314345
variant="default"
315346
size="sm"
316-
className="bg-neutral-800 text-white hover:bg-neutral-900 dark:bg-neutral-600 dark:hover:bg-neutral-500"
347+
className={`bg-neutral-800 text-white hover:bg-neutral-900 dark:bg-neutral-600 dark:hover:bg-neutral-500 ${
348+
images.length >= 5 ? "cursor-not-allowed opacity-50" : ""
349+
}`}
350+
onClick={handleGenerateImage}
351+
disabled={isGenerating || images.length >= 5}
317352
>
318-
Generate
353+
{isGenerating
354+
? "Generating..."
355+
: images.length >= 5
356+
? "Max images reached"
357+
: "Generate"}
319358
</Button>
320359
</div>
321360
</div>

0 commit comments

Comments
 (0)