Skip to content

Commit 17efe9c

Browse files
committed
Add heartbeat mechanism and visual progress indicator for content generation
- Add SSE heartbeat events every 15 seconds to prevent 504 gateway timeouts - Improve SSE response headers (Connection: keep-alive, Cache-Control) - Add generationStatus state to track content generation progress - Add styled progress indicator in ChatPanel with stage-based messages - Update AgentResponse type to include heartbeat type - Add ProductReview component - Update WebApp.Dockerfile for container deployment - Truncate DALL-E prompts to prevent 'prompt too long' errors
1 parent 68ba13e commit 17efe9c

15 files changed

Lines changed: 880 additions & 49 deletions

File tree

content-gen/scripts/load_sample_data.py

Lines changed: 192 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,25 @@
22
Sample data loader for Content Generation Solution Accelerator.
33
44
This script loads sample product data (Contoso Paints Catalog) into CosmosDB.
5+
It also generates detailed image descriptions using GPT-4 Vision by analyzing
6+
the product images stored in Azure Blob Storage.
57
"""
68

79
import asyncio
10+
import base64
811
import os
912
import sys
1013

1114
# Add the src directory to the path
1215
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
1316

17+
from azure.identity.aio import DefaultAzureCredential, ManagedIdentityCredential
18+
from azure.storage.blob.aio import BlobServiceClient
19+
from openai import AsyncAzureOpenAI
20+
1421
from backend.services.cosmos_service import get_cosmos_service
1522
from backend.models import Product
23+
from backend.settings import app_settings
1624

1725

1826
# Contoso Paints Catalog - Paint colors with descriptions, tags, and prices
@@ -153,6 +161,129 @@
153161
]
154162

155163

164+
async def get_openai_client():
165+
"""Get an authenticated Azure OpenAI client."""
166+
client_id = app_settings.base_settings.azure_client_id
167+
if client_id:
168+
credential = ManagedIdentityCredential(client_id=client_id)
169+
else:
170+
credential = DefaultAzureCredential()
171+
172+
token = await credential.get_token("https://cognitiveservices.azure.com/.default")
173+
174+
client = AsyncAzureOpenAI(
175+
azure_endpoint=app_settings.azure_openai.endpoint,
176+
azure_ad_token=token.token,
177+
api_version=app_settings.azure_openai.api_version,
178+
)
179+
180+
return client
181+
182+
183+
async def get_image_from_blob(image_url: str) -> bytes:
184+
"""Download an image from Azure Blob Storage."""
185+
# Try connection string first (from environment), then fall back to DefaultAzureCredential
186+
connection_string = os.environ.get("AZURE_STORAGE_CONNECTION_STRING")
187+
188+
# Parse the blob URL
189+
# Format: https://{account}.blob.core.windows.net/{container}/{blob_name}
190+
parts = image_url.replace("https://", "").split("/")
191+
account_url = f"https://{parts[0]}"
192+
container_name = parts[1]
193+
blob_name = "/".join(parts[2:])
194+
195+
if connection_string:
196+
blob_service = BlobServiceClient.from_connection_string(connection_string)
197+
else:
198+
credential = DefaultAzureCredential()
199+
blob_service = BlobServiceClient(account_url=account_url, credential=credential)
200+
201+
async with blob_service:
202+
blob_client = blob_service.get_blob_client(container=container_name, blob=blob_name)
203+
download = await blob_client.download_blob()
204+
return await download.readall()
205+
206+
207+
async def generate_image_description(client: AsyncAzureOpenAI, image_bytes: bytes, product_name: str, product_description: str) -> str:
208+
"""
209+
Generate a detailed 2000-character description of a product image using GPT-4 Vision.
210+
211+
Args:
212+
client: Azure OpenAI client
213+
image_bytes: Raw image bytes
214+
product_name: Name of the product
215+
product_description: Marketing description of the product
216+
217+
Returns:
218+
Detailed image description (approximately 2000 characters)
219+
"""
220+
# Convert image to base64
221+
image_base64 = base64.b64encode(image_bytes).decode('utf-8')
222+
223+
prompt = f"""Analyze this paint color swatch image for "{product_name}" and provide a detailed visual description that could be used to recreate this exact color and presentation in an AI image generator like DALL-E.
224+
225+
Product Context: {product_description}
226+
227+
Your description should be approximately 2000 characters and include:
228+
229+
1. **Exact Color Analysis**: Describe the precise hue, saturation, and brightness. Include RGB-style descriptions (e.g., "a soft blue-gray with hints of lavender"). Note any gradients, variations, or undertones visible in the swatch.
230+
231+
2. **Visual Texture & Finish**: Describe the paint's apparent finish (matte, satin, eggshell, glossy). Note any visible texture, brush strokes, or surface quality.
232+
233+
3. **Lighting & Shadows**: Describe how light interacts with the color - any highlights, shadows, or reflective qualities visible in the swatch.
234+
235+
4. **Color Relationships**: Describe what colors this would complement. Note warm vs cool tones, and how it might appear in different lighting conditions (daylight, incandescent, etc.).
236+
237+
5. **Mood & Atmosphere**: Describe the emotional quality and atmosphere this color evokes - is it calming, energizing, sophisticated, cozy?
238+
239+
6. **Room Application Suggestions**: Describe how this color might look on walls, in different room types, and what design styles it suits.
240+
241+
7. **Technical Color Details**: If possible, estimate the color in terms that could guide image generation - dominant wavelength, approximate hex range, comparison to well-known colors.
242+
243+
Write in a descriptive, visual style that would help an AI image generator accurately reproduce this paint color in marketing images. Be specific and evocative."""
244+
245+
try:
246+
response = await client.chat.completions.create(
247+
model=app_settings.azure_openai.gpt_model,
248+
messages=[
249+
{
250+
"role": "user",
251+
"content": [
252+
{
253+
"type": "text",
254+
"text": prompt
255+
},
256+
{
257+
"type": "image_url",
258+
"image_url": {
259+
"url": f"data:image/png;base64,{image_base64}",
260+
"detail": "high"
261+
}
262+
}
263+
]
264+
}
265+
],
266+
max_completion_tokens=1000,
267+
temperature=0.7
268+
)
269+
270+
description = response.choices[0].message.content
271+
272+
# Ensure description is around 2000 characters (truncate if too long)
273+
if len(description) > 2200:
274+
# Find a good break point near 2000 chars
275+
description = description[:2000]
276+
last_period = description.rfind('.')
277+
if last_period > 1500:
278+
description = description[:last_period + 1]
279+
280+
return description
281+
282+
except Exception as e:
283+
print(f" Warning: Failed to generate image description: {e}")
284+
return None
285+
286+
156287
async def delete_existing_products():
157288
"""Delete all existing products from CosmosDB."""
158289
print("Deleting existing products...")
@@ -164,15 +295,45 @@ async def delete_existing_products():
164295
return deleted_count
165296

166297

167-
async def load_sample_data():
168-
"""Load sample products into CosmosDB."""
298+
async def load_sample_data(generate_descriptions: bool = True):
299+
"""Load sample products into CosmosDB with optional image description generation."""
169300
print("\nLoading Contoso Paints Catalog...")
170301

171302
cosmos_service = await get_cosmos_service()
303+
openai_client = None
304+
305+
if generate_descriptions:
306+
print(" Initializing GPT-4 Vision for image description generation...")
307+
try:
308+
openai_client = await get_openai_client()
309+
print(" ✓ GPT-4 Vision client initialized")
310+
except Exception as e:
311+
print(f" ✗ Failed to initialize GPT-4 Vision: {e}")
312+
print(" Proceeding without image descriptions...")
313+
generate_descriptions = False
172314

173315
loaded_count = 0
174316
for product_data in SAMPLE_PRODUCTS:
175317
try:
318+
product_name = product_data.get('product_name', 'unknown')
319+
320+
# Generate image description if enabled
321+
if generate_descriptions and openai_client and product_data.get('image_url'):
322+
print(f" Generating description for {product_name}...")
323+
try:
324+
image_bytes = await get_image_from_blob(product_data['image_url'])
325+
description = await generate_image_description(
326+
client=openai_client,
327+
image_bytes=image_bytes,
328+
product_name=product_name,
329+
product_description=product_data.get('description', '')
330+
)
331+
if description:
332+
product_data['image_description'] = description
333+
print(f" ✓ Generated {len(description)} character description")
334+
except Exception as e:
335+
print(f" ✗ Failed to generate description: {e}")
336+
176337
product = Product(**product_data)
177338
await cosmos_service.upsert_product(product)
178339
print(f" ✓ Loaded: {product.product_name} ({product.sku})")
@@ -183,18 +344,43 @@ async def load_sample_data():
183344
print(f"\nLoaded {loaded_count} products from Contoso Paints Catalog.")
184345

185346

186-
async def main():
347+
async def main(generate_descriptions: bool = True):
187348
"""Main entry point."""
188349
try:
189350
# Delete existing products first
190351
await delete_existing_products()
191352

192-
# Load new products
193-
await load_sample_data()
353+
# Load new products with optional image description generation
354+
await load_sample_data(generate_descriptions=generate_descriptions)
194355
except Exception as e:
195356
print(f"Error loading sample data: {e}")
196357
raise
197358

198359

199360
if __name__ == "__main__":
200-
asyncio.run(main())
361+
import argparse
362+
363+
parser = argparse.ArgumentParser(description="Load sample product data into CosmosDB")
364+
parser.add_argument(
365+
"--skip-descriptions",
366+
action="store_true",
367+
help="Skip generating image descriptions using GPT-4 Vision"
368+
)
369+
parser.add_argument(
370+
"--generate-descriptions",
371+
action="store_true",
372+
default=True,
373+
help="Generate detailed image descriptions using GPT-4 Vision (default: True)"
374+
)
375+
376+
args = parser.parse_args()
377+
378+
generate_descriptions = not args.skip_descriptions
379+
380+
print("=" * 60)
381+
print("Content Generation Solution Accelerator - Sample Data Loader")
382+
print("=" * 60)
383+
print(f"Image Description Generation: {'ENABLED' if generate_descriptions else 'DISABLED'}")
384+
print()
385+
386+
asyncio.run(main(generate_descriptions=generate_descriptions))

0 commit comments

Comments
 (0)