AI Video Generation Implementation

This commit is contained in:
ajaysi
2025-11-17 17:38:23 +05:30
parent 4901b7eb72
commit bf7493c366
132 changed files with 6200 additions and 19475 deletions

View File

@@ -694,6 +694,44 @@ class LimitValidator:
total_images = projected_images
# Check video generation limits
elif provider == APIProvider.VIDEO:
video_limit = limits.get('video_calls', 0) or 0
total_video_calls = usage.video_calls or 0
projected_video_calls = total_video_calls + 1
if video_limit > 0 and projected_video_calls > video_limit:
error_info = {
'current_calls': total_video_calls,
'limit': video_limit,
'provider': 'video',
'operation_type': operation_type,
'operation_index': op_idx
}
return False, f"Video generation limit would be exceeded. Would use {projected_video_calls} of {video_limit} videos this billing period.", {
'error_type': 'video_limit',
'usage_info': error_info
}
# Check image editing limits
elif provider == APIProvider.IMAGE_EDIT:
image_edit_limit = limits.get('image_edit_calls', 0) or 0
total_image_edit_calls = getattr(usage, 'image_edit_calls', 0) or 0
projected_image_edit_calls = total_image_edit_calls + 1
if image_edit_limit > 0 and projected_image_edit_calls > image_edit_limit:
error_info = {
'current_calls': total_image_edit_calls,
'limit': image_edit_limit,
'provider': 'image_edit',
'operation_type': operation_type,
'operation_index': op_idx
}
return False, f"Image editing limit would be exceeded. Would use {projected_image_edit_calls} of {image_edit_limit} image edits this billing period.", {
'error_type': 'image_edit_limit',
'usage_info': error_info
}
# Check other provider-specific limits
else:
provider_calls_key = f"{provider_name}_calls"

View File

@@ -299,3 +299,124 @@ def validate_image_generation_operations(
}
)
def validate_image_editing_operations(
pricing_service: PricingService,
user_id: str
) -> None:
"""
Validate image editing operation before making API calls.
Args:
pricing_service: PricingService instance
user_id: User ID for subscription checking
Returns:
None - raises HTTPException with 429 status if validation fails
"""
try:
operations_to_validate = [
{
'provider': APIProvider.IMAGE_EDIT,
'tokens_requested': 0,
'actual_provider_name': 'image_edit',
'operation_type': 'image_editing'
}
]
can_proceed, message, error_details = pricing_service.check_comprehensive_limits(
user_id=user_id,
operations=operations_to_validate
)
if not can_proceed:
logger.error(f"[Pre-flight Validator] Image editing blocked for user {user_id}: {message}")
usage_info = error_details.get('usage_info', {}) if error_details else {}
provider = usage_info.get('provider', 'image_edit') if usage_info else 'image_edit'
raise HTTPException(
status_code=429,
detail={
'error': message,
'message': message,
'provider': provider,
'usage_info': usage_info if usage_info else error_details
}
)
logger.info(f"[Pre-flight Validator] ✅ Image editing validated for user {user_id}")
# Validation passed - no return needed (function raises HTTPException if validation fails)
except HTTPException:
raise
except Exception as e:
logger.error(f"[Pre-flight Validator] Error validating image editing: {e}", exc_info=True)
raise HTTPException(
status_code=500,
detail={
'error': f"Failed to validate image editing: {str(e)}",
'message': f"Failed to validate image editing: {str(e)}"
}
)
def validate_video_generation_operations(
pricing_service: PricingService,
user_id: str
) -> None:
"""
Validate video generation operation before making API calls.
Args:
pricing_service: PricingService instance
user_id: User ID for subscription checking
Returns:
None - raises HTTPException with 429 status if validation fails
"""
try:
operations_to_validate = [
{
'provider': APIProvider.VIDEO,
'tokens_requested': 0,
'actual_provider_name': 'video',
'operation_type': 'video_generation'
}
]
can_proceed, message, error_details = pricing_service.check_comprehensive_limits(
user_id=user_id,
operations=operations_to_validate
)
if not can_proceed:
logger.error(f"[Pre-flight Validator] Video generation blocked for user {user_id}: {message}")
usage_info = error_details.get('usage_info', {}) if error_details else {}
provider = usage_info.get('provider', 'video') if usage_info else 'video'
raise HTTPException(
status_code=429,
detail={
'error': message,
'message': message,
'provider': provider,
'usage_info': usage_info if usage_info else error_details
}
)
logger.info(f"[Pre-flight Validator] ✅ Video generation validated for user {user_id}")
# Validation passed - no return needed (function raises HTTPException if validation fails)
except HTTPException:
raise
except Exception as e:
logger.error(f"[Pre-flight Validator] Error validating video generation: {e}", exc_info=True)
raise HTTPException(
status_code=500,
detail={
'error': f"Failed to validate video generation: {str(e)}",
'message': f"Failed to validate video generation: {str(e)}"
}
)

View File

@@ -295,10 +295,22 @@ class PricingService:
"model_name": "exa-search",
"cost_per_request": 0.005, # $0.005 per search (1-25 results)
"description": "Exa Neural Search API"
},
{
"provider": APIProvider.VIDEO,
"model_name": "tencent/HunyuanVideo",
"cost_per_request": 0.10, # $0.10 per video generation (estimated)
"description": "HuggingFace AI Video Generation (HunyuanVideo)"
},
{
"provider": APIProvider.VIDEO,
"model_name": "default",
"cost_per_request": 0.10, # $0.10 per video generation (estimated)
"description": "AI Video Generation default pricing"
}
]
# Combine all pricing data
# Combine all pricing data (include video pricing in search_pricing list)
all_pricing = gemini_pricing + openai_pricing + anthropic_pricing + mistral_pricing + search_pricing
# Insert or update pricing data
@@ -344,6 +356,8 @@ class PricingService:
"firecrawl_calls_limit": 10,
"stability_calls_limit": 5,
"exa_calls_limit": 100,
"video_calls_limit": 0, # No video generation for free tier
"image_edit_calls_limit": 10, # 10 AI image editing calls/month
"gemini_tokens_limit": 100000,
"monthly_cost_limit": 0.0,
"features": ["basic_content_generation", "limited_research"],
@@ -365,6 +379,8 @@ class PricingService:
"firecrawl_calls_limit": 100,
"stability_calls_limit": 5,
"exa_calls_limit": 500,
"video_calls_limit": 20, # 20 videos/month for basic plan
"image_edit_calls_limit": 30, # 30 AI image editing calls/month
"gemini_tokens_limit": 20000, # Increased from 5000 for better stability
"openai_tokens_limit": 20000, # Increased from 5000 for better stability
"anthropic_tokens_limit": 20000, # Increased from 5000 for better stability
@@ -388,6 +404,8 @@ class PricingService:
"firecrawl_calls_limit": 500,
"stability_calls_limit": 200,
"exa_calls_limit": 2000,
"video_calls_limit": 50, # 50 videos/month for pro plan
"image_edit_calls_limit": 100, # 100 AI image editing calls/month
"gemini_tokens_limit": 5000000,
"openai_tokens_limit": 2500000,
"anthropic_tokens_limit": 1000000,
@@ -411,6 +429,8 @@ class PricingService:
"firecrawl_calls_limit": 0,
"stability_calls_limit": 0,
"exa_calls_limit": 0, # Unlimited
"video_calls_limit": 0, # Unlimited for enterprise
"image_edit_calls_limit": 0, # Unlimited image editing for enterprise
"gemini_tokens_limit": 0,
"openai_tokens_limit": 0,
"anthropic_tokens_limit": 0,
@@ -429,6 +449,20 @@ class PricingService:
if not existing:
plan = SubscriptionPlan(**plan_data)
self.db.add(plan)
else:
# Update existing plan with new limits (e.g., image_edit_calls_limit)
# This ensures existing plans get new columns like image_edit_calls_limit
for key, value in plan_data.items():
if key not in ["name", "tier"]: # Don't overwrite name/tier
try:
# Try to set the attribute (works even if column was just added)
setattr(existing, key, value)
except (AttributeError, Exception) as e:
# If attribute doesn't exist yet (column not migrated), skip it
# Schema migration will add it, then this will update it on next run
logger.debug(f"Could not set {key} on plan {existing.name}: {e}")
existing.updated_at = datetime.utcnow()
logger.debug(f"Updated existing plan: {existing.name}")
self.db.commit()
logger.debug("Default subscription plans initialized")
@@ -615,6 +649,8 @@ class PricingService:
'metaphor_calls': plan.metaphor_calls_limit,
'firecrawl_calls': plan.firecrawl_calls_limit,
'stability_calls': plan.stability_calls_limit,
'video_calls': getattr(plan, 'video_calls_limit', 0), # Support missing column
'image_edit_calls': getattr(plan, 'image_edit_calls_limit', 0), # Support missing column
# Token limits
'gemini_tokens': plan.gemini_tokens_limit,
'openai_tokens': plan.openai_tokens_limit,

View File

@@ -29,6 +29,8 @@ def ensure_subscription_plan_columns(db: Session) -> None:
# Columns we may reference in models but might be missing in older DBs
required_columns = {
"exa_calls_limit": "INTEGER DEFAULT 0",
"video_calls_limit": "INTEGER DEFAULT 0",
"image_edit_calls_limit": "INTEGER DEFAULT 0",
}
for col_name, ddl in required_columns.items():
@@ -78,6 +80,10 @@ def ensure_usage_summaries_columns(db: Session) -> None:
required_columns = {
"exa_calls": "INTEGER DEFAULT 0",
"exa_cost": "REAL DEFAULT 0.0",
"video_calls": "INTEGER DEFAULT 0",
"video_cost": "REAL DEFAULT 0.0",
"image_edit_calls": "INTEGER DEFAULT 0",
"image_edit_cost": "REAL DEFAULT 0.0",
}
for col_name, ddl in required_columns.items():

View File

@@ -608,6 +608,12 @@ class UsageTrackingService:
# Reset image generation counters
summary.stability_calls = 0
# Reset video generation counters
summary.video_calls = 0
# Reset image editing counters
summary.image_edit_calls = 0
# Reset cost counters
summary.gemini_cost = 0.0
summary.openai_cost = 0.0
@@ -618,6 +624,9 @@ class UsageTrackingService:
summary.metaphor_cost = 0.0
summary.firecrawl_cost = 0.0
summary.stability_cost = 0.0
summary.exa_cost = 0.0
summary.video_cost = 0.0
summary.image_edit_cost = 0.0
# Reset totals
summary.total_calls = 0