Commit_remaining_local_changes_after_PR_407_merge
This commit is contained in:
@@ -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 middleware.auth_middleware import get_current_user, get_current_user_with_query_token
|
||||||
from api.story_writer.utils.auth import require_authenticated_user
|
from api.story_writer.utils.auth import require_authenticated_user
|
||||||
from services.wavespeed.infinitetalk import animate_scene_with_voiceover
|
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.podcast.video_combination_service import PodcastVideoCombinationService
|
||||||
from services.llm_providers.main_video_generation import track_video_usage
|
from services.llm_providers.main_video_generation import track_video_usage
|
||||||
from services.subscription import PricingService
|
from services.subscription import PricingService
|
||||||
@@ -92,27 +93,6 @@ def _extract_error_message(exc: Exception) -> str:
|
|||||||
return error_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(
|
def _execute_podcast_video_task(
|
||||||
task_id: str,
|
task_id: str,
|
||||||
request: PodcastVideoGenerationRequest,
|
request: PodcastVideoGenerationRequest,
|
||||||
@@ -256,7 +236,7 @@ def _execute_podcast_video_task(
|
|||||||
|
|
||||||
# Extract user-friendly error message from exception
|
# Extract user-friendly error message from exception
|
||||||
error_msg = _extract_error_message(exc)
|
error_msg = _extract_error_message(exc)
|
||||||
error_meta = _extract_error_metadata(exc)
|
error_meta = extract_error_metadata(exc)
|
||||||
|
|
||||||
task_manager.update_task_status(
|
task_manager.update_task_status(
|
||||||
task_id,
|
task_id,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from loguru import logger
|
|||||||
from services.database import get_engine_for_user
|
from services.database import get_engine_for_user
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
from utils.asset_tracker import save_asset_to_library
|
from utils.asset_tracker import save_asset_to_library
|
||||||
|
from utils.error_normalization import extract_error_metadata
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -19,25 +20,6 @@ UPLOAD_DIR = Path("backend/data/video_studio/uploads")
|
|||||||
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
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):
|
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.
|
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:
|
except Exception as e:
|
||||||
logger.error(f"[VideoStudio] Avatar generation failed for task {task_id}: {e}", exc_info=True)
|
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_manager.update_task(
|
||||||
task_id,
|
task_id,
|
||||||
"failed",
|
"failed",
|
||||||
|
|||||||
@@ -3,29 +3,20 @@ Video generation operations (text-to-video and image-to-video).
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import json
|
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
from fastapi import HTTPException
|
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 utils.logger_utils import get_service_logger
|
||||||
from .base import VideoBase
|
from .base import VideoBase
|
||||||
|
|
||||||
logger = get_service_logger("wavespeed.generators.video.generation")
|
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):
|
class VideoGeneration(VideoBase):
|
||||||
"""Video generation operations."""
|
"""Video generation operations."""
|
||||||
|
|
||||||
@@ -46,22 +37,11 @@ class VideoGeneration(VideoBase):
|
|||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
logger.error(f"[WaveSpeed] Submission failed: {response.status_code} {response.text}")
|
logger.error(f"[WaveSpeed] Submission failed: {response.status_code} {response.text}")
|
||||||
|
|
||||||
error_message = _extract_wavespeed_message(response.text)
|
error_message = extract_response_message(response.text)
|
||||||
if "insufficient credits" in error_message.lower() or "credit" in error_message.lower():
|
if is_insufficient_credits_message(error_message):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=429,
|
status_code=429,
|
||||||
detail={
|
detail=build_wavespeed_topup_detail(operation_type="scene_animation"),
|
||||||
"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",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -109,22 +89,11 @@ class VideoGeneration(VideoBase):
|
|||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
logger.error(f"[WaveSpeed] Text-to-video submission failed: {response.status_code} {response.text}")
|
logger.error(f"[WaveSpeed] Text-to-video submission failed: {response.status_code} {response.text}")
|
||||||
|
|
||||||
error_message = _extract_wavespeed_message(response.text)
|
error_message = extract_response_message(response.text)
|
||||||
if "insufficient credits" in error_message.lower() or "credit" in error_message.lower():
|
if is_insufficient_credits_message(error_message):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=429,
|
status_code=429,
|
||||||
detail={
|
detail=build_wavespeed_topup_detail(operation_type="video_generation"),
|
||||||
"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",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -227,22 +196,11 @@ class VideoGeneration(VideoBase):
|
|||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
logger.error(f"[WaveSpeed] Text-to-video submission failed: {response.status_code} {response.text}")
|
logger.error(f"[WaveSpeed] Text-to-video submission failed: {response.status_code} {response.text}")
|
||||||
|
|
||||||
error_message = _extract_wavespeed_message(response.text)
|
error_message = extract_response_message(response.text)
|
||||||
if "insufficient credits" in error_message.lower() or "credit" in error_message.lower():
|
if is_insufficient_credits_message(error_message):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=429,
|
status_code=429,
|
||||||
detail={
|
detail=build_wavespeed_topup_detail(operation_type="video_generation"),
|
||||||
"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",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|||||||
80
backend/utils/error_normalization.py
Normal file
80
backend/utils/error_normalization.py
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user