From b410ece4ca497ab92a97dd4ebc413b84b74a728d Mon Sep 17 00:00:00 2001 From: ajaysi Date: Tue, 10 Mar 2026 17:17:04 +0530 Subject: [PATCH] Commit_remaining_local_changes_after_PR_407_merge --- backend/api/podcast/handlers/video.py | 24 +----- backend/api/video_studio/handlers/avatar.py | 22 +---- .../wavespeed/generators/video/generation.py | 70 ++++------------ backend/utils/error_normalization.py | 80 +++++++++++++++++++ 4 files changed, 98 insertions(+), 98 deletions(-) create mode 100644 backend/utils/error_normalization.py diff --git a/backend/api/podcast/handlers/video.py b/backend/api/podcast/handlers/video.py index b7cc69af..c1ffad13 100644 --- a/backend/api/podcast/handlers/video.py +++ b/backend/api/podcast/handlers/video.py @@ -18,6 +18,7 @@ from services.database import get_session_for_user from middleware.auth_middleware import get_current_user, get_current_user_with_query_token from api.story_writer.utils.auth import require_authenticated_user from services.wavespeed.infinitetalk import animate_scene_with_voiceover +from utils.error_normalization import extract_error_metadata from services.podcast.video_combination_service import PodcastVideoCombinationService from services.llm_providers.main_video_generation import track_video_usage from services.subscription import PricingService @@ -92,27 +93,6 @@ def _extract_error_message(exc: Exception) -> str: return error_str -def _extract_error_metadata(exc: Exception) -> Dict[str, Any]: - """Extract structured error metadata for task polling clients.""" - if isinstance(exc, HTTPException): - detail = exc.detail - if isinstance(detail, dict): - return { - "error_status": exc.status_code, - "error_data": detail, - } - if isinstance(detail, str): - return { - "error_status": exc.status_code, - "error_data": { - "error": detail, - "message": detail, - }, - } - - return {} - - def _execute_podcast_video_task( task_id: str, request: PodcastVideoGenerationRequest, @@ -256,7 +236,7 @@ def _execute_podcast_video_task( # Extract user-friendly error message from exception error_msg = _extract_error_message(exc) - error_meta = _extract_error_metadata(exc) + error_meta = extract_error_metadata(exc) task_manager.update_task_status( task_id, diff --git a/backend/api/video_studio/handlers/avatar.py b/backend/api/video_studio/handlers/avatar.py index 555a95a4..7a37a06d 100644 --- a/backend/api/video_studio/handlers/avatar.py +++ b/backend/api/video_studio/handlers/avatar.py @@ -11,6 +11,7 @@ from loguru import logger from services.database import get_engine_for_user from sqlalchemy.orm import sessionmaker from utils.asset_tracker import save_asset_to_library +from utils.error_normalization import extract_error_metadata router = APIRouter() @@ -19,25 +20,6 @@ UPLOAD_DIR = Path("backend/data/video_studio/uploads") UPLOAD_DIR.mkdir(parents=True, exist_ok=True) -def _extract_error_metadata(exc: Exception) -> Dict[str, Any]: - """Extract structured HTTP error metadata for polling clients.""" - if isinstance(exc, HTTPException): - detail = exc.detail - if isinstance(detail, dict): - return { - "error_status": exc.status_code, - "error_data": detail, - } - if isinstance(detail, str): - return { - "error_status": exc.status_code, - "error_data": { - "error": detail, - "message": detail, - }, - } - return {} - def _process_avatar_generation(task_id: str, image_path: Path, audio_path: Path, user_id: str, resolution: str, model: str): """ Background task to process avatar generation using shared InfiniteTalk service. @@ -114,7 +96,7 @@ def _process_avatar_generation(task_id: str, image_path: Path, audio_path: Path, except Exception as e: logger.error(f"[VideoStudio] Avatar generation failed for task {task_id}: {e}", exc_info=True) - error_meta = _extract_error_metadata(e) + error_meta = extract_error_metadata(e) task_manager.update_task( task_id, "failed", diff --git a/backend/services/wavespeed/generators/video/generation.py b/backend/services/wavespeed/generators/video/generation.py index 9c353e42..fe6cdebc 100644 --- a/backend/services/wavespeed/generators/video/generation.py +++ b/backend/services/wavespeed/generators/video/generation.py @@ -3,29 +3,20 @@ Video generation operations (text-to-video and image-to-video). """ import requests -import json from typing import Any, Dict, Optional from fastapi import HTTPException +from utils.error_normalization import ( + build_wavespeed_topup_detail, + extract_response_message, + is_insufficient_credits_message, +) from utils.logger_utils import get_service_logger from .base import VideoBase logger = get_service_logger("wavespeed.generators.video.generation") -def _extract_wavespeed_message(response_text: str) -> str: - """Best-effort extraction of WaveSpeed error message from response payload.""" - if not response_text: - return "" - try: - parsed = json.loads(response_text) - if isinstance(parsed, dict): - return str(parsed.get("message") or parsed.get("error") or "") - except (json.JSONDecodeError, TypeError, ValueError): - return "" - return "" - - class VideoGeneration(VideoBase): """Video generation operations.""" @@ -46,22 +37,11 @@ class VideoGeneration(VideoBase): if response.status_code != 200: logger.error(f"[WaveSpeed] Submission failed: {response.status_code} {response.text}") - error_message = _extract_wavespeed_message(response.text) - if "insufficient credits" in error_message.lower() or "credit" in error_message.lower(): + error_message = extract_response_message(response.text) + if is_insufficient_credits_message(error_message): raise HTTPException( status_code=429, - detail={ - "error": "Insufficient WaveSpeed credits", - "message": "Insufficient credits. Please top up to continue video generation.", - "provider": "wavespeed", - "usage_info": { - "provider": "wavespeed", - "type": "credits", - "limit_type": "provider_credits", - "operation_type": "scene_animation", - "action_required": "top_up", - }, - }, + detail=build_wavespeed_topup_detail(operation_type="scene_animation"), ) raise HTTPException( @@ -109,22 +89,11 @@ class VideoGeneration(VideoBase): if response.status_code != 200: logger.error(f"[WaveSpeed] Text-to-video submission failed: {response.status_code} {response.text}") - error_message = _extract_wavespeed_message(response.text) - if "insufficient credits" in error_message.lower() or "credit" in error_message.lower(): + error_message = extract_response_message(response.text) + if is_insufficient_credits_message(error_message): raise HTTPException( status_code=429, - detail={ - "error": "Insufficient WaveSpeed credits", - "message": "Insufficient credits. Please top up to continue video generation.", - "provider": "wavespeed", - "usage_info": { - "provider": "wavespeed", - "type": "credits", - "limit_type": "provider_credits", - "operation_type": "video_generation", - "action_required": "top_up", - }, - }, + detail=build_wavespeed_topup_detail(operation_type="video_generation"), ) raise HTTPException( @@ -227,22 +196,11 @@ class VideoGeneration(VideoBase): if response.status_code != 200: logger.error(f"[WaveSpeed] Text-to-video submission failed: {response.status_code} {response.text}") - error_message = _extract_wavespeed_message(response.text) - if "insufficient credits" in error_message.lower() or "credit" in error_message.lower(): + error_message = extract_response_message(response.text) + if is_insufficient_credits_message(error_message): raise HTTPException( status_code=429, - detail={ - "error": "Insufficient WaveSpeed credits", - "message": "Insufficient credits. Please top up to continue video generation.", - "provider": "wavespeed", - "usage_info": { - "provider": "wavespeed", - "type": "credits", - "limit_type": "provider_credits", - "operation_type": "video_generation", - "action_required": "top_up", - }, - }, + detail=build_wavespeed_topup_detail(operation_type="video_generation"), ) raise HTTPException( diff --git a/backend/utils/error_normalization.py b/backend/utils/error_normalization.py new file mode 100644 index 00000000..f16bf169 --- /dev/null +++ b/backend/utils/error_normalization.py @@ -0,0 +1,80 @@ +"""Shared error normalization helpers for backend API/service layers.""" + +from __future__ import annotations + +import json +from typing import Any, Dict + +from fastapi import HTTPException + + +def extract_error_metadata(exc: Exception) -> Dict[str, Any]: + """Extract structured HTTP error metadata for polling clients.""" + if isinstance(exc, HTTPException): + detail = exc.detail + if isinstance(detail, dict): + return { + "error_status": exc.status_code, + "error_data": detail, + } + if isinstance(detail, str): + return { + "error_status": exc.status_code, + "error_data": { + "error": detail, + "message": detail, + }, + } + return {} + + +def extract_response_message(response_text: str) -> str: + """Best-effort extraction of provider message from a JSON response string.""" + if not response_text: + return "" + try: + parsed = json.loads(response_text) + if isinstance(parsed, dict): + return str(parsed.get("message") or parsed.get("error") or "") + except (json.JSONDecodeError, TypeError, ValueError): + return "" + return "" + + +def is_insufficient_credits_message(message: str) -> bool: + """Detect provider top-up/credit exhaustion messages.""" + lowered = (message or "").lower() + return "insufficient credits" in lowered or "credit" in lowered + + +def build_wavespeed_topup_detail(operation_type: str) -> Dict[str, Any]: + """Build unified WaveSpeed top-up payload for frontend subscription modal flows.""" + return { + "error": "Insufficient WaveSpeed credits", + "message": "Insufficient credits. Please top up to continue video generation.", + "provider": "wavespeed", + "usage_info": { + "provider": "wavespeed", + "type": "credits", + "limit_type": "provider_credits", + "operation_type": operation_type, + "action_required": "top_up", + }, + } + + +def normalize_wavespeed_topup_http_exception(exc: HTTPException, operation_type: str) -> HTTPException: + """Convert nested WaveSpeed credit errors into unified HTTP 429 contract.""" + detail = exc.detail if isinstance(exc.detail, dict) else {} + provider_message = "" + + if isinstance(detail, dict): + response_text = str(detail.get("response") or "") + provider_message = extract_response_message(response_text) + if not provider_message: + provider_message = str(detail.get("message") or detail.get("error") or "") + + if is_insufficient_credits_message(provider_message): + return HTTPException(status_code=429, detail=build_wavespeed_topup_detail(operation_type)) + + return exc