|
| 1 | +#!/usr/bin/env python3 |
| 2 | +""" |
| 3 | +Sample Content Generation Script |
| 4 | +
|
| 5 | +This script demonstrates how to use the ContentOrchestrator to generate |
| 6 | +complete marketing content packages including text and images. |
| 7 | +
|
| 8 | +Prerequisites: |
| 9 | +1. Set up environment variables (or use a .env file): |
| 10 | + - AZURE_OPENAI_ENDPOINT: Your Azure OpenAI endpoint |
| 11 | + - AZURE_OPENAI_GPT_MODEL: GPT model deployment name |
| 12 | + - AZURE_OPENAI_GPT_IMAGE_ENDPOINT: (Optional) Endpoint for images |
| 13 | + - AZURE_OPENAI_IMAGE_MODEL: Image model name (e.g., gpt-image-1) |
| 14 | + - AZURE_COSMOS_ENDPOINT: Your CosmosDB endpoint |
| 15 | + - AZURE_COSMOS_DATABASE_NAME: content-generation |
| 16 | + - AZURE_COSMOS_CONVERSATIONS_CONTAINER: conversations |
| 17 | + |
| 18 | +2. Ensure you have RBAC access: |
| 19 | + - "Cognitive Services OpenAI User" role on the Azure OpenAI resource |
| 20 | + - "Cosmos DB Built-in Data Contributor" on CosmosDB (if using products) |
| 21 | +
|
| 22 | +Usage: |
| 23 | + python sample_content_generation.py |
| 24 | + python sample_content_generation.py --no-images |
| 25 | + python sample_content_generation.py --output results.json |
| 26 | +""" |
| 27 | + |
| 28 | +import asyncio |
| 29 | +import argparse |
| 30 | +import json |
| 31 | +import os |
| 32 | +import sys |
| 33 | +from datetime import datetime |
| 34 | +from pathlib import Path |
| 35 | + |
| 36 | +# Add the backend directory to the path |
| 37 | +backend_path = Path(__file__).parent.parent / "src" / "backend" |
| 38 | +sys.path.insert(0, str(backend_path)) |
| 39 | + |
| 40 | +# Now import the orchestrator and models |
| 41 | +from orchestrator import ContentOrchestrator |
| 42 | +from models import CreativeBrief |
| 43 | + |
| 44 | + |
| 45 | +def create_sample_brief() -> CreativeBrief: |
| 46 | + """Create a sample creative brief for testing.""" |
| 47 | + return CreativeBrief( |
| 48 | + overview="Spring home refresh campaign promoting interior paint colors", |
| 49 | + objectives="Drive awareness and consideration for spring paint collection, increase website traffic by 20%", |
| 50 | + target_audience="Homeowners aged 30-55, interested in DIY home improvement and interior design", |
| 51 | + key_message="Transform your home this spring with our fresh, on-trend paint colors that bring warmth and style to any room", |
| 52 | + tone_and_style="Inspiring, approachable, and aspirational. Use warm, inviting language that speaks to the joy of home improvement", |
| 53 | + deliverable="Social media carousel post (3-5 images) with captions for Instagram and Facebook", |
| 54 | + timelines="Launch by March 15th for spring campaign", |
| 55 | + visual_guidelines="Bright, airy rooms with natural lighting. Show before/after transformations. Feature paint swatches in context. Modern, clean aesthetic", |
| 56 | + cta="Shop the Spring Collection - Visit our website for color inspiration", |
| 57 | + raw_input="Sample brief for testing content generation" |
| 58 | + ) |
| 59 | + |
| 60 | + |
| 61 | +def create_sample_products() -> list: |
| 62 | + """Create sample product data for testing.""" |
| 63 | + return [ |
| 64 | + { |
| 65 | + "id": "sample-1", |
| 66 | + "product_name": "Morning Mist", |
| 67 | + "description": "A soft, ethereal blue-gray that evokes early morning calm. Perfect for bedrooms and living spaces.", |
| 68 | + "tags": "blue, gray, soft, calming, bedroom, living room", |
| 69 | + "price": 45.99, |
| 70 | + "image_url": "https://example.com/morning-mist.jpg" |
| 71 | + }, |
| 72 | + { |
| 73 | + "id": "sample-2", |
| 74 | + "product_name": "Sunlit Meadow", |
| 75 | + "description": "A warm, golden yellow that brings sunshine indoors. Ideal for kitchens and breakfast nooks.", |
| 76 | + "tags": "yellow, warm, sunny, kitchen, cheerful", |
| 77 | + "price": 42.99, |
| 78 | + "image_url": "https://example.com/sunlit-meadow.jpg" |
| 79 | + }, |
| 80 | + { |
| 81 | + "id": "sample-3", |
| 82 | + "product_name": "Forest Haven", |
| 83 | + "description": "A rich, deep green inspired by lush forest canopies. Creates a sophisticated, grounding atmosphere.", |
| 84 | + "tags": "green, deep, sophisticated, nature, accent wall", |
| 85 | + "price": 48.99, |
| 86 | + "image_url": "https://example.com/forest-haven.jpg" |
| 87 | + } |
| 88 | + ] |
| 89 | + |
| 90 | + |
| 91 | +async def generate_content_sample( |
| 92 | + brief: CreativeBrief = None, |
| 93 | + products: list = None, |
| 94 | + generate_images: bool = True, |
| 95 | + output_path: str = None |
| 96 | +) -> dict: |
| 97 | + """ |
| 98 | + Generate a complete content package using the orchestrator. |
| 99 | + |
| 100 | + Args: |
| 101 | + brief: Creative brief (uses sample if not provided) |
| 102 | + products: Products to feature (uses samples if not provided) |
| 103 | + generate_images: Whether to generate images |
| 104 | + output_path: Path to save results JSON (optional) |
| 105 | + |
| 106 | + Returns: |
| 107 | + Dictionary with generation results |
| 108 | + """ |
| 109 | + # Use defaults if not provided |
| 110 | + brief = brief or create_sample_brief() |
| 111 | + products = products or create_sample_products() |
| 112 | + |
| 113 | + print(f"\n{'='*70}") |
| 114 | + print("CONTENT GENERATION SAMPLE") |
| 115 | + print(f"{'='*70}") |
| 116 | + print(f"\nCreative Brief Overview: {brief.overview}") |
| 117 | + print(f"Target Audience: {brief.target_audience}") |
| 118 | + print(f"Deliverable: {brief.deliverable}") |
| 119 | + print(f"Products: {len(products)} items") |
| 120 | + print(f"Generate Images: {generate_images}") |
| 121 | + |
| 122 | + print(f"\n{'='*70}") |
| 123 | + print("Initializing Content Orchestrator...") |
| 124 | + print(f"{'='*70}\n") |
| 125 | + |
| 126 | + # Create and initialize the orchestrator |
| 127 | + orchestrator = ContentOrchestrator() |
| 128 | + orchestrator.initialize() |
| 129 | + |
| 130 | + print("Orchestrator initialized successfully!") |
| 131 | + print("\nGenerating content...\n") |
| 132 | + |
| 133 | + # Generate the content |
| 134 | + start_time = datetime.now() |
| 135 | + |
| 136 | + results = await orchestrator.generate_content( |
| 137 | + brief=brief, |
| 138 | + products=products, |
| 139 | + generate_images=generate_images |
| 140 | + ) |
| 141 | + |
| 142 | + elapsed = (datetime.now() - start_time).total_seconds() |
| 143 | + |
| 144 | + # Display results |
| 145 | + print(f"\n{'='*70}") |
| 146 | + print("GENERATION RESULTS") |
| 147 | + print(f"{'='*70}") |
| 148 | + print(f"\nTime elapsed: {elapsed:.1f} seconds") |
| 149 | + |
| 150 | + # Text content |
| 151 | + if results.get("text_content"): |
| 152 | + print(f"\n{'─'*40}") |
| 153 | + print("TEXT CONTENT:") |
| 154 | + print(f"{'─'*40}") |
| 155 | + text_content = results["text_content"] |
| 156 | + # Show first 1000 chars |
| 157 | + if len(text_content) > 1000: |
| 158 | + print(text_content[:1000] + "\n\n[...truncated...]") |
| 159 | + else: |
| 160 | + print(text_content) |
| 161 | + |
| 162 | + # Image prompt |
| 163 | + if results.get("image_prompt"): |
| 164 | + print(f"\n{'─'*40}") |
| 165 | + print("IMAGE PROMPT:") |
| 166 | + print(f"{'─'*40}") |
| 167 | + print(results["image_prompt"][:500] + "..." if len(results.get("image_prompt", "")) > 500 else results["image_prompt"]) |
| 168 | + |
| 169 | + # Generated image |
| 170 | + if results.get("generated_image"): |
| 171 | + print(f"\n{'─'*40}") |
| 172 | + print("GENERATED IMAGE:") |
| 173 | + print(f"{'─'*40}") |
| 174 | + img = results["generated_image"] |
| 175 | + if img.get("success"): |
| 176 | + print(f"✅ Image generated successfully") |
| 177 | + print(f" Model: {img.get('model', 'unknown')}") |
| 178 | + if img.get("image_base64"): |
| 179 | + print(f" Data size: {len(img['image_base64']) / 1024:.1f} KB (base64)") |
| 180 | + else: |
| 181 | + print(f"❌ Image generation failed: {img.get('error', 'unknown error')}") |
| 182 | + |
| 183 | + # Compliance |
| 184 | + if results.get("compliance"): |
| 185 | + print(f"\n{'─'*40}") |
| 186 | + print("COMPLIANCE CHECK:") |
| 187 | + print(f"{'─'*40}") |
| 188 | + print(results["compliance"]) |
| 189 | + |
| 190 | + if results.get("violations"): |
| 191 | + print(f"\n⚠️ Violations found: {len(results['violations'])}") |
| 192 | + for v in results["violations"]: |
| 193 | + print(f" - {v}") |
| 194 | + |
| 195 | + if results.get("requires_modification"): |
| 196 | + print("\n⚠️ Content requires modification before publishing") |
| 197 | + |
| 198 | + # Save results if output path provided |
| 199 | + if output_path: |
| 200 | + # Prepare results for JSON serialization |
| 201 | + output_data = { |
| 202 | + "timestamp": datetime.now().isoformat(), |
| 203 | + "elapsed_seconds": elapsed, |
| 204 | + "brief": brief.model_dump(), |
| 205 | + "products": products, |
| 206 | + "generate_images": generate_images, |
| 207 | + "results": { |
| 208 | + "text_content": results.get("text_content"), |
| 209 | + "image_prompt": results.get("image_prompt"), |
| 210 | + "compliance": results.get("compliance"), |
| 211 | + "violations": results.get("violations"), |
| 212 | + "requires_modification": results.get("requires_modification"), |
| 213 | + } |
| 214 | + } |
| 215 | + |
| 216 | + # Handle generated image separately (base64 can be large) |
| 217 | + if results.get("generated_image"): |
| 218 | + img = results["generated_image"] |
| 219 | + output_data["results"]["generated_image"] = { |
| 220 | + "success": img.get("success"), |
| 221 | + "model": img.get("model"), |
| 222 | + "error": img.get("error"), |
| 223 | + "revised_prompt": img.get("revised_prompt"), |
| 224 | + # Store base64 in separate file to keep JSON readable |
| 225 | + "has_image_data": bool(img.get("image_base64")) |
| 226 | + } |
| 227 | + |
| 228 | + # Save image to separate file if successful |
| 229 | + if img.get("success") and img.get("image_base64"): |
| 230 | + import base64 |
| 231 | + image_path = output_path.replace(".json", "_image.png") |
| 232 | + image_data = base64.b64decode(img["image_base64"]) |
| 233 | + with open(image_path, "wb") as f: |
| 234 | + f.write(image_data) |
| 235 | + output_data["results"]["generated_image"]["image_file"] = image_path |
| 236 | + print(f"\n📁 Image saved to: {image_path}") |
| 237 | + |
| 238 | + with open(output_path, "w") as f: |
| 239 | + json.dump(output_data, f, indent=2) |
| 240 | + print(f"📁 Results saved to: {output_path}") |
| 241 | + |
| 242 | + return results |
| 243 | + |
| 244 | + |
| 245 | +async def main(): |
| 246 | + """Main entry point for the sample script.""" |
| 247 | + parser = argparse.ArgumentParser( |
| 248 | + description="Generate marketing content using the Content Orchestrator" |
| 249 | + ) |
| 250 | + parser.add_argument( |
| 251 | + "--no-images", |
| 252 | + action="store_true", |
| 253 | + help="Skip image generation" |
| 254 | + ) |
| 255 | + parser.add_argument( |
| 256 | + "--output", "-o", |
| 257 | + type=str, |
| 258 | + default=None, |
| 259 | + help="Output file path for results JSON" |
| 260 | + ) |
| 261 | + parser.add_argument( |
| 262 | + "--brief-file", |
| 263 | + type=str, |
| 264 | + default=None, |
| 265 | + help="Path to JSON file containing creative brief" |
| 266 | + ) |
| 267 | + parser.add_argument( |
| 268 | + "--products-file", |
| 269 | + type=str, |
| 270 | + default=None, |
| 271 | + help="Path to JSON file containing products list" |
| 272 | + ) |
| 273 | + |
| 274 | + args = parser.parse_args() |
| 275 | + |
| 276 | + # Load brief from file if provided |
| 277 | + brief = None |
| 278 | + if args.brief_file: |
| 279 | + with open(args.brief_file, "r") as f: |
| 280 | + brief_data = json.load(f) |
| 281 | + brief = CreativeBrief(**brief_data) |
| 282 | + |
| 283 | + # Load products from file if provided |
| 284 | + products = None |
| 285 | + if args.products_file: |
| 286 | + with open(args.products_file, "r") as f: |
| 287 | + products = json.load(f) |
| 288 | + |
| 289 | + # Generate content |
| 290 | + try: |
| 291 | + result = await generate_content_sample( |
| 292 | + brief=brief, |
| 293 | + products=products, |
| 294 | + generate_images=not args.no_images, |
| 295 | + output_path=args.output |
| 296 | + ) |
| 297 | + |
| 298 | + print(f"\n{'='*70}") |
| 299 | + print("✅ Content generation completed!") |
| 300 | + print(f"{'='*70}\n") |
| 301 | + |
| 302 | + sys.exit(0) |
| 303 | + |
| 304 | + except Exception as e: |
| 305 | + print(f"\n❌ Error during content generation: {e}") |
| 306 | + import traceback |
| 307 | + traceback.print_exc() |
| 308 | + sys.exit(1) |
| 309 | + |
| 310 | + |
| 311 | +# Example: Custom brief and products |
| 312 | +async def custom_example(): |
| 313 | + """Example with custom brief and products.""" |
| 314 | + |
| 315 | + # Custom brief for a summer promotion |
| 316 | + brief = CreativeBrief( |
| 317 | + overview="Summer outdoor living promotion for patio furniture", |
| 318 | + objectives="Increase summer patio sales by 30%, drive foot traffic to showrooms", |
| 319 | + target_audience="Suburban homeowners, 35-60, with outdoor entertaining spaces", |
| 320 | + key_message="Create your perfect outdoor oasis with our premium patio collection", |
| 321 | + tone_and_style="Relaxed, aspirational, lifestyle-focused. Evoke summer gatherings and outdoor relaxation", |
| 322 | + deliverable="Email banner and 2 social media posts", |
| 323 | + timelines="Launch Memorial Day weekend", |
| 324 | + visual_guidelines="Outdoor settings at golden hour, families enjoying patios, lush greenery, modern outdoor furniture", |
| 325 | + cta="Explore the Summer Collection - 20% off this weekend only" |
| 326 | + ) |
| 327 | + |
| 328 | + # Custom products |
| 329 | + products = [ |
| 330 | + { |
| 331 | + "id": "patio-1", |
| 332 | + "product_name": "Sunset Lounger", |
| 333 | + "description": "Premium outdoor chaise lounge with weather-resistant cushions in coastal blue", |
| 334 | + "tags": "patio, lounge, outdoor, blue, relaxation", |
| 335 | + "price": 599.99 |
| 336 | + }, |
| 337 | + { |
| 338 | + "id": "patio-2", |
| 339 | + "product_name": "Garden Dining Set", |
| 340 | + "description": "6-piece aluminum dining set with tempered glass table, perfect for outdoor entertaining", |
| 341 | + "tags": "patio, dining, aluminum, entertaining, family", |
| 342 | + "price": 1299.99 |
| 343 | + } |
| 344 | + ] |
| 345 | + |
| 346 | + results = await generate_content_sample( |
| 347 | + brief=brief, |
| 348 | + products=products, |
| 349 | + generate_images=True, |
| 350 | + output_path="custom_content_results.json" |
| 351 | + ) |
| 352 | + |
| 353 | + return results |
| 354 | + |
| 355 | + |
| 356 | +if __name__ == "__main__": |
| 357 | + # Run the main function |
| 358 | + asyncio.run(main()) |
| 359 | + |
| 360 | + # Uncomment below to run custom example instead: |
| 361 | + # asyncio.run(custom_example()) |
0 commit comments