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
This commit is contained in:
ajaysi
2026-05-09 17:18:16 +05:30
parent 93a1985d9f
commit 7385100017
3 changed files with 56 additions and 110 deletions

View File

@@ -13,7 +13,7 @@ from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from loguru import logger 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 utils.asset_tracker import save_asset_to_library
from services.database import SessionLocal from services.database import SessionLocal
from fastapi import HTTPException from fastapi import HTTPException
@@ -113,12 +113,7 @@ class ProductImageService:
def __init__(self): def __init__(self):
"""Initialize Product Image Service.""" """Initialize Product Image Service."""
try: logger.info("[Product Image Service] Initialized")
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
def validate_request(self, request: ProductImageRequest) -> None: def validate_request(self, request: ProductImageRequest) -> None:
""" """
@@ -260,77 +255,7 @@ class ProductImageService:
return full_prompt return full_prompt
def _generate_image_with_retry( def generate_product_image(
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(
self, self,
request: ProductImageRequest, request: ProductImageRequest,
user_id: str, user_id: str,
@@ -374,15 +299,18 @@ class ProductImageService:
# Generate image using WaveSpeed with retry logic # Generate image using WaveSpeed with retry logic
try: try:
image_bytes = self._generate_image_with_retry( result = generate_image(
model=model,
prompt=prompt, prompt=prompt,
width=width, options={
height=height, "provider": "wavespeed",
max_retries=3, "model": model,
retry_delay=2.0 "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)}") logger.error(f"[Product Image Service] Image generation failed: {str(e)}")
generation_time = time.time() - start_time generation_time = time.time() - start_time
return ProductImageResult( return ProductImageResult(

View File

@@ -1,6 +1,15 @@
import { useState, useCallback } from 'react'; import { useState, useCallback } from 'react';
import { aiApiClient } from '../api/client'; 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 { export interface CampaignCreateRequest {
campaign_name: string; campaign_name: string;
goal: string; goal: string;
@@ -211,7 +220,7 @@ export const useCampaignCreator = () => {
setBlueprint(response.data); setBlueprint(response.data);
return response.data; return response.data;
} catch (err: any) { } 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); setError(errorMessage);
throw new Error(errorMessage); throw new Error(errorMessage);
} finally { } finally {
@@ -236,7 +245,7 @@ export const useCampaignCreator = () => {
setProposals(response.data); setProposals(response.data);
return response.data; return response.data;
} catch (err: any) { } 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); setError(errorMessage);
throw new Error(errorMessage); throw new Error(errorMessage);
} finally { } finally {
@@ -258,7 +267,7 @@ export const useCampaignCreator = () => {
setGeneratedAsset(response.data); setGeneratedAsset(response.data);
return response.data; return response.data;
} catch (err: any) { } 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); setError(errorMessage);
throw new Error(errorMessage); throw new Error(errorMessage);
} finally { } finally {
@@ -276,7 +285,7 @@ export const useCampaignCreator = () => {
setBrandDNA(response.data.brand_dna); setBrandDNA(response.data.brand_dna);
return response.data.brand_dna; return response.data.brand_dna;
} catch (err: any) { } 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); setError(errorMessage);
throw new Error(errorMessage); throw new Error(errorMessage);
} finally { } finally {
@@ -294,7 +303,7 @@ export const useCampaignCreator = () => {
); );
return response.data.brand_dna; return response.data.brand_dna;
} catch (err: any) { } 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); setError(errorMessage);
throw new Error(errorMessage); throw new Error(errorMessage);
} finally { } finally {
@@ -315,7 +324,7 @@ export const useCampaignCreator = () => {
setChannelPack(response.data); setChannelPack(response.data);
return response.data; return response.data;
} catch (err: any) { } 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); setError(errorMessage);
throw new Error(errorMessage); throw new Error(errorMessage);
} finally { } finally {
@@ -337,7 +346,7 @@ export const useCampaignCreator = () => {
setAuditResult(response.data); setAuditResult(response.data);
return response.data; return response.data;
} catch (err: any) { } 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); setError(errorMessage);
throw new Error(errorMessage); throw new Error(errorMessage);
} finally { } finally {
@@ -356,7 +365,7 @@ export const useCampaignCreator = () => {
setCampaigns(response.data.campaigns); setCampaigns(response.data.campaigns);
return response.data.campaigns; return response.data.campaigns;
} catch (err: any) { } 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); setError(errorMessage);
throw new Error(errorMessage); throw new Error(errorMessage);
} finally { } finally {
@@ -370,7 +379,7 @@ export const useCampaignCreator = () => {
const response = await aiApiClient.get<CampaignBlueprint>(`/api/campaign-creator/campaigns/${campaignId}`); const response = await aiApiClient.get<CampaignBlueprint>(`/api/campaign-creator/campaigns/${campaignId}`);
return response.data; return response.data;
} catch (err: any) { } 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); setError(errorMessage);
throw new Error(errorMessage); throw new Error(errorMessage);
} }
@@ -382,7 +391,7 @@ export const useCampaignCreator = () => {
const response = await aiApiClient.get<AssetProposalsResponse>(`/api/campaign-creator/campaigns/${campaignId}/proposals`); const response = await aiApiClient.get<AssetProposalsResponse>(`/api/campaign-creator/campaigns/${campaignId}/proposals`);
return response.data; return response.data;
} catch (err: any) { } 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); setError(errorMessage);
throw new Error(errorMessage); throw new Error(errorMessage);
} }
@@ -400,7 +409,7 @@ export const useCampaignCreator = () => {
setPreflightResult(response.data); setPreflightResult(response.data);
return response.data; return response.data;
} catch (err: any) { } 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); setError(errorMessage);
const errorResult: PreflightValidationResult = { const errorResult: PreflightValidationResult = {
can_proceed: false, can_proceed: false,
@@ -452,7 +461,7 @@ export const useCampaignCreator = () => {
const response = await aiApiClient.get(`/api/product-marketing/personalization/defaults/${formType}`); const response = await aiApiClient.get(`/api/product-marketing/personalization/defaults/${formType}`);
return response.data.defaults; return response.data.defaults;
} catch (err: any) { } 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); setError(errorMessage);
throw new Error(errorMessage); throw new Error(errorMessage);
} }
@@ -468,7 +477,7 @@ export const useCampaignCreator = () => {
setRecommendations(response.data.recommendations); setRecommendations(response.data.recommendations);
return response.data.recommendations; return response.data.recommendations;
} catch (err: any) { } 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); setError(errorMessage);
throw new Error(errorMessage); throw new Error(errorMessage);
} finally { } finally {

View File

@@ -1,6 +1,15 @@
import { useState, useCallback } from 'react'; import { useState, useCallback } from 'react';
import { aiApiClient } from '../api/client'; 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 * useProductMarketing Hook
* *
@@ -37,7 +46,7 @@ export const useProductMarketing = () => {
setGeneratedProductImage(response.data); setGeneratedProductImage(response.data);
return response.data; return response.data;
} catch (err: any) { } 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); setProductImageError(errorMessage);
throw new Error(errorMessage); throw new Error(errorMessage);
} finally { } finally {
@@ -70,7 +79,7 @@ export const useProductMarketing = () => {
setGeneratedAnimation(response.data); setGeneratedAnimation(response.data);
return response.data; return response.data;
} catch (err: any) { } 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); setAnimationError(errorMessage);
throw new Error(errorMessage); throw new Error(errorMessage);
} finally { } finally {
@@ -102,7 +111,7 @@ export const useProductMarketing = () => {
setGeneratedVideo(response.data); setGeneratedVideo(response.data);
return response.data; return response.data;
} catch (err: any) { } 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); setVideoError(errorMessage);
throw new Error(errorMessage); throw new Error(errorMessage);
} finally { } finally {
@@ -137,7 +146,7 @@ export const useProductMarketing = () => {
setGeneratedAvatar(response.data); setGeneratedAvatar(response.data);
return response.data; return response.data;
} catch (err: any) { } 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); setAvatarError(errorMessage);
throw new Error(errorMessage); throw new Error(errorMessage);
} finally { } finally {
@@ -174,7 +183,7 @@ export const useProductMarketing = () => {
setInferredConfig(response.data.configuration); setInferredConfig(response.data.configuration);
return response.data.configuration; return response.data.configuration;
} catch (err: any) { } 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); setInferenceError(errorMessage);
throw new Error(errorMessage); throw new Error(errorMessage);
} finally { } finally {
@@ -194,8 +203,8 @@ export const useProductMarketing = () => {
setBrandDNA(response.data.brand_dna); setBrandDNA(response.data.brand_dna);
return response.data.brand_dna; return response.data.brand_dna;
} catch (err: any) { } 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); setError(errorMessage);
throw new Error(errorMessage); throw new Error(errorMessage);
} finally { } finally {
setIsLoadingBrandDNA(false); setIsLoadingBrandDNA(false);
@@ -210,8 +219,8 @@ export const useProductMarketing = () => {
setUserPreferences(response.data.preferences); setUserPreferences(response.data.preferences);
return response.data.preferences; return response.data.preferences;
} catch (err: any) { } catch (err: any) {
const errorMessage = err.response?.data?.detail || err.message || 'Failed to get user preferences'; const errorMessage = formatError(err, 'Failed to get user preferences');
setError(errorMessage); setError(errorMessage);
throw new Error(errorMessage); throw new Error(errorMessage);
} finally { } finally {
setIsLoadingPreferences(false); setIsLoadingPreferences(false);
@@ -225,7 +234,7 @@ export const useProductMarketing = () => {
const response = await aiApiClient.get(`/api/product-marketing/personalization/defaults/${formType}`); const response = await aiApiClient.get(`/api/product-marketing/personalization/defaults/${formType}`);
return response.data.defaults; return response.data.defaults;
} catch (err: any) { } 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); setError(errorMessage);
throw new Error(errorMessage); throw new Error(errorMessage);
} }
@@ -241,8 +250,8 @@ export const useProductMarketing = () => {
setRecommendations(response.data.recommendations); setRecommendations(response.data.recommendations);
return response.data.recommendations; return response.data.recommendations;
} catch (err: any) { } 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); setError(errorMessage);
throw new Error(errorMessage); throw new Error(errorMessage);
} finally { } finally {
setIsLoadingRecommendations(false); setIsLoadingRecommendations(false);