From 738510001788a6079ac407192e4f330a26a1027e Mon Sep 17 00:00:00 2001 From: ajaysi Date: Sat, 9 May 2026 17:18:16 +0530 Subject: [PATCH] fix(product-marketing): route image generation through unified subscription validation Backend: - product_image_service.py: Replaced direct wavespeed_client.generate_image() with generate_image() from main_image_generation (unified entry point) - This ensures subscription pre-flight validation (_validate_image_operation) and usage tracking (_track_image_operation_usage) are enforced - Removed _generate_image_with_retry method and WaveSpeedClient dependency - Animation/video/avatar services already route through ImageStudioManager - no changes needed Frontend: - useProductMarketing.ts: Added formatError() helper for 402/429 detection across all 8 API operations - useCampaignCreator.ts: Added formatError() helper for 402/429 detection across all 13 API operations - All error messages now surface subscription limits with upgrade prompts --- .../product_image_service.py | 98 +++---------------- frontend/src/hooks/useCampaignCreator.ts | 35 ++++--- frontend/src/hooks/useProductMarketing.ts | 33 ++++--- 3 files changed, 56 insertions(+), 110 deletions(-) diff --git a/backend/services/product_marketing/product_image_service.py b/backend/services/product_marketing/product_image_service.py index 13b15b42..5904e098 100644 --- a/backend/services/product_marketing/product_image_service.py +++ b/backend/services/product_marketing/product_image_service.py @@ -13,7 +13,7 @@ from dataclasses import dataclass from pathlib import Path from loguru import logger -from services.wavespeed.client import WaveSpeedClient +from services.llm_providers.main_image_generation import generate_image from utils.asset_tracker import save_asset_to_library from services.database import SessionLocal from fastapi import HTTPException @@ -113,12 +113,7 @@ class ProductImageService: def __init__(self): """Initialize Product Image Service.""" - try: - self.wavespeed_client = WaveSpeedClient() - logger.info("[Product Image Service] Initialized") - except Exception as e: - logger.error(f"[Product Image Service] Failed to initialize WaveSpeed client: {str(e)}") - raise ProductImageServiceError(f"Failed to initialize service: {str(e)}") from e + logger.info("[Product Image Service] Initialized") def validate_request(self, request: ProductImageRequest) -> None: """ @@ -260,77 +255,7 @@ class ProductImageService: return full_prompt - def _generate_image_with_retry( - self, - model: str, - prompt: str, - width: int, - height: int, - max_retries: int = 3, - retry_delay: float = 2.0 - ) -> bytes: - """ - Generate image with retry logic for transient failures. - - Args: - model: Model to use - prompt: Generation prompt - width: Image width - height: Image height - max_retries: Maximum number of retries - retry_delay: Delay between retries in seconds - - Returns: - Generated image bytes - - Raises: - ImageGenerationError: If generation fails after retries - """ - last_error = None - - for attempt in range(max_retries): - try: - logger.info(f"[Product Image Service] Image generation attempt {attempt + 1}/{max_retries}") - - image_bytes = self.wavespeed_client.generate_image( - model=model, - prompt=prompt, - width=width, - height=height, - enable_sync_mode=True, - timeout=120, - ) - - if not image_bytes: - raise ValueError("Image generation returned empty result") - - if len(image_bytes) < 100: # Sanity check: image should be at least 100 bytes - raise ValueError(f"Generated image too small: {len(image_bytes)} bytes") - - logger.info(f"[Product Image Service] ✅ Image generated successfully: {len(image_bytes)} bytes") - return image_bytes - - except Exception as e: - last_error = e - error_msg = str(e) - logger.warning(f"[Product Image Service] Attempt {attempt + 1} failed: {error_msg}") - - # Don't retry on validation errors or client errors (4xx) - if "4" in error_msg or "validation" in error_msg.lower() or "invalid" in error_msg.lower(): - logger.error(f"[Product Image Service] Non-retryable error: {error_msg}") - raise ImageGenerationError(f"Image generation failed: {error_msg}") from e - - # Retry on transient errors - if attempt < max_retries - 1: - logger.info(f"[Product Image Service] Retrying in {retry_delay} seconds...") - time.sleep(retry_delay) - retry_delay *= 1.5 # Exponential backoff - else: - logger.error(f"[Product Image Service] All retry attempts failed") - - raise ImageGenerationError(f"Image generation failed after {max_retries} attempts: {str(last_error)}") from last_error - - async def generate_product_image( + def generate_product_image( self, request: ProductImageRequest, user_id: str, @@ -374,15 +299,18 @@ class ProductImageService: # Generate image using WaveSpeed with retry logic try: - image_bytes = self._generate_image_with_retry( - model=model, + result = generate_image( prompt=prompt, - width=width, - height=height, - max_retries=3, - retry_delay=2.0 + options={ + "provider": "wavespeed", + "model": model, + "width": width, + "height": height, + }, + user_id=user_id, ) - except ImageGenerationError as e: + image_bytes = result.image_bytes + except Exception as e: logger.error(f"[Product Image Service] Image generation failed: {str(e)}") generation_time = time.time() - start_time return ProductImageResult( diff --git a/frontend/src/hooks/useCampaignCreator.ts b/frontend/src/hooks/useCampaignCreator.ts index a6327118..e94ceb23 100644 --- a/frontend/src/hooks/useCampaignCreator.ts +++ b/frontend/src/hooks/useCampaignCreator.ts @@ -1,6 +1,15 @@ import { useState, useCallback } from 'react'; import { aiApiClient } from '../api/client'; +const formatError = (err: any, fallback: string): string => { + if (err?.response?.status === 402 || err?.response?.status === 429) { + return 'Subscription limit reached. Upgrade your plan to continue using this feature.'; + } + if (err?.response?.data?.detail) return String(err.response.data.detail); + if (err?.message) return String(err.message); + return fallback; +}; + export interface CampaignCreateRequest { campaign_name: string; goal: string; @@ -211,7 +220,7 @@ export const useCampaignCreator = () => { setBlueprint(response.data); return response.data; } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Failed to create campaign blueprint'; + const errorMessage = formatError(err, 'Failed to create campaign blueprint'); setError(errorMessage); throw new Error(errorMessage); } finally { @@ -236,7 +245,7 @@ export const useCampaignCreator = () => { setProposals(response.data); return response.data; } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Failed to generate asset proposals'; + const errorMessage = formatError(err, 'Failed to generate asset proposals'); setError(errorMessage); throw new Error(errorMessage); } finally { @@ -258,7 +267,7 @@ export const useCampaignCreator = () => { setGeneratedAsset(response.data); return response.data; } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Failed to generate asset'; + const errorMessage = formatError(err, 'Failed to generate asset'); setError(errorMessage); throw new Error(errorMessage); } finally { @@ -276,7 +285,7 @@ export const useCampaignCreator = () => { setBrandDNA(response.data.brand_dna); return response.data.brand_dna; } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Failed to get brand DNA'; + const errorMessage = formatError(err, 'Failed to get brand DNA'); setError(errorMessage); throw new Error(errorMessage); } finally { @@ -294,7 +303,7 @@ export const useCampaignCreator = () => { ); return response.data.brand_dna; } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Failed to get channel brand DNA'; + const errorMessage = formatError(err, 'Failed to get channel brand DNA'); setError(errorMessage); throw new Error(errorMessage); } finally { @@ -315,7 +324,7 @@ export const useCampaignCreator = () => { setChannelPack(response.data); return response.data; } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Failed to get channel pack'; + const errorMessage = formatError(err, 'Failed to get channel pack'); setError(errorMessage); throw new Error(errorMessage); } finally { @@ -337,7 +346,7 @@ export const useCampaignCreator = () => { setAuditResult(response.data); return response.data; } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Failed to audit asset'; + const errorMessage = formatError(err, 'Failed to audit asset'); setError(errorMessage); throw new Error(errorMessage); } finally { @@ -356,7 +365,7 @@ export const useCampaignCreator = () => { setCampaigns(response.data.campaigns); return response.data.campaigns; } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Failed to list campaigns'; + const errorMessage = formatError(err, 'Failed to list campaigns'); setError(errorMessage); throw new Error(errorMessage); } finally { @@ -370,7 +379,7 @@ export const useCampaignCreator = () => { const response = await aiApiClient.get(`/api/campaign-creator/campaigns/${campaignId}`); return response.data; } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Failed to get campaign'; + const errorMessage = formatError(err, 'Failed to get campaign'); setError(errorMessage); throw new Error(errorMessage); } @@ -382,7 +391,7 @@ export const useCampaignCreator = () => { const response = await aiApiClient.get(`/api/campaign-creator/campaigns/${campaignId}/proposals`); return response.data; } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Failed to get proposals'; + const errorMessage = formatError(err, 'Failed to get proposals'); setError(errorMessage); throw new Error(errorMessage); } @@ -400,7 +409,7 @@ export const useCampaignCreator = () => { setPreflightResult(response.data); return response.data; } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Failed to validate campaign pre-flight'; + const errorMessage = formatError(err, 'Failed to validate campaign pre-flight'); setError(errorMessage); const errorResult: PreflightValidationResult = { can_proceed: false, @@ -452,7 +461,7 @@ export const useCampaignCreator = () => { const response = await aiApiClient.get(`/api/product-marketing/personalization/defaults/${formType}`); return response.data.defaults; } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Failed to get personalized defaults'; + const errorMessage = formatError(err, 'Failed to get personalized defaults'); setError(errorMessage); throw new Error(errorMessage); } @@ -468,7 +477,7 @@ export const useCampaignCreator = () => { setRecommendations(response.data.recommendations); return response.data.recommendations; } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Failed to get recommendations'; + const errorMessage = formatError(err, 'Failed to get recommendations'); setError(errorMessage); throw new Error(errorMessage); } finally { diff --git a/frontend/src/hooks/useProductMarketing.ts b/frontend/src/hooks/useProductMarketing.ts index a9b3f5e6..ed2d4c7b 100644 --- a/frontend/src/hooks/useProductMarketing.ts +++ b/frontend/src/hooks/useProductMarketing.ts @@ -1,6 +1,15 @@ import { useState, useCallback } from 'react'; import { aiApiClient } from '../api/client'; +const formatError = (err: any, fallback: string): string => { + if (err?.response?.status === 402 || err?.response?.status === 429) { + return 'Subscription limit reached. Upgrade your plan to continue using this feature.'; + } + if (err?.response?.data?.detail) return String(err.response.data.detail); + if (err?.message) return String(err.message); + return fallback; +}; + /** * useProductMarketing Hook * @@ -37,7 +46,7 @@ export const useProductMarketing = () => { setGeneratedProductImage(response.data); return response.data; } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Failed to generate product image'; + const errorMessage = formatError(err, 'Failed to generate product image'); setProductImageError(errorMessage); throw new Error(errorMessage); } finally { @@ -70,7 +79,7 @@ export const useProductMarketing = () => { setGeneratedAnimation(response.data); return response.data; } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Failed to generate product animation'; + const errorMessage = formatError(err, 'Failed to generate product animation'); setAnimationError(errorMessage); throw new Error(errorMessage); } finally { @@ -102,7 +111,7 @@ export const useProductMarketing = () => { setGeneratedVideo(response.data); return response.data; } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Failed to generate product video'; + const errorMessage = formatError(err, 'Failed to generate product video'); setVideoError(errorMessage); throw new Error(errorMessage); } finally { @@ -137,7 +146,7 @@ export const useProductMarketing = () => { setGeneratedAvatar(response.data); return response.data; } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Failed to generate product avatar'; + const errorMessage = formatError(err, 'Failed to generate product avatar'); setAvatarError(errorMessage); throw new Error(errorMessage); } finally { @@ -174,7 +183,7 @@ export const useProductMarketing = () => { setInferredConfig(response.data.configuration); return response.data.configuration; } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Failed to infer requirements'; + const errorMessage = formatError(err, 'Failed to infer requirements'); setInferenceError(errorMessage); throw new Error(errorMessage); } finally { @@ -194,8 +203,8 @@ export const useProductMarketing = () => { setBrandDNA(response.data.brand_dna); return response.data.brand_dna; } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Failed to get brand DNA'; - setError(errorMessage); + const errorMessage = formatError(err, 'Failed to get brand DNA'); + setError(errorMessage); throw new Error(errorMessage); } finally { setIsLoadingBrandDNA(false); @@ -210,8 +219,8 @@ export const useProductMarketing = () => { setUserPreferences(response.data.preferences); return response.data.preferences; } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Failed to get user preferences'; - setError(errorMessage); + const errorMessage = formatError(err, 'Failed to get user preferences'); + setError(errorMessage); throw new Error(errorMessage); } finally { setIsLoadingPreferences(false); @@ -225,7 +234,7 @@ export const useProductMarketing = () => { const response = await aiApiClient.get(`/api/product-marketing/personalization/defaults/${formType}`); return response.data.defaults; } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Failed to get personalized defaults'; + const errorMessage = formatError(err, 'Failed to get personalized defaults'); setError(errorMessage); throw new Error(errorMessage); } @@ -241,8 +250,8 @@ export const useProductMarketing = () => { setRecommendations(response.data.recommendations); return response.data.recommendations; } catch (err: any) { - const errorMessage = err.response?.data?.detail || err.message || 'Failed to get recommendations'; - setError(errorMessage); + const errorMessage = formatError(err, 'Failed to get recommendations'); + setError(errorMessage); throw new Error(errorMessage); } finally { setIsLoadingRecommendations(false);