Commit_all_local_changes_after_PR_406_merge

This commit is contained in:
ajaysi
2026-03-10 17:01:36 +05:30
parent f78b5f1e04
commit 8c2d88efb9
17 changed files with 936 additions and 412 deletions

View File

@@ -45,11 +45,17 @@ def _extract_error_message(exc: Exception) -> str:
"""
Extract user-friendly error message from exception.
Handles HTTPException with nested error details from WaveSpeed API.
Preserves subscription modal flags for frontend.
"""
if isinstance(exc, HTTPException):
detail = exc.detail
# If detail is a dict (from WaveSpeed client)
if isinstance(detail, dict):
# Check if this is a subscription/credit error
if detail.get("error_type") == "insufficient_credits" or detail.get("show_subscription_modal"):
# Return the error message with subscription modal flag
return detail.get("message", "Insufficient credits. Please top up your account.")
# Try to extract message from nested response JSON
response_str = detail.get("response", "")
if response_str:
@@ -86,6 +92,27 @@ 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,
@@ -229,9 +256,15 @@ def _execute_podcast_video_task(
# Extract user-friendly error message from exception
error_msg = _extract_error_message(exc)
error_meta = _extract_error_metadata(exc)
task_manager.update_task_status(
task_id, "failed", error=error_msg, message=f"Video generation failed: {error_msg}"
task_id,
"failed",
error=error_msg,
message=f"Video generation failed: {error_msg}",
error_status=error_meta.get("error_status"),
error_data=error_meta.get("error_data"),
)
@@ -257,7 +290,7 @@ async def generate_podcast_video(
try:
if hasattr(request, 'headers') and hasattr(request.headers, 'get'):
auth_header = request.headers.get("Authorization")
except:
except Exception:
pass
if auth_header and auth_header.startswith("Bearer "):

View File

@@ -76,6 +76,10 @@ class TaskManager:
if task["status"] == "failed" and task.get("error"):
response["error"] = task["error"]
if task.get("error_status") is not None:
response["error_status"] = task["error_status"]
if task.get("error_data") is not None:
response["error_data"] = task["error_data"]
return response
@@ -86,7 +90,9 @@ class TaskManager:
progress: Optional[float] = None,
message: Optional[str] = None,
result: Optional[Dict[str, Any]] = None,
error: Optional[str] = None
error: Optional[str] = None,
error_status: Optional[int] = None,
error_data: Optional[Dict[str, Any]] = None,
):
"""Update the status of a task."""
if task_id not in self.task_storage:
@@ -112,6 +118,10 @@ class TaskManager:
if error is not None:
task["error"] = error
logger.error(f"[StoryWriter] Task {task_id} error: {error}")
if error_status is not None:
task["error_status"] = error_status
if error_data is not None:
task["error_data"] = error_data
async def execute_story_generation_task(
self,

View File

@@ -11,7 +11,7 @@ from sqlalchemy.exc import SQLAlchemyError
from middleware.auth_middleware import get_current_user
from services.database import get_db
from services.today_workflow_service import get_or_create_daily_workflow_plan, update_task_status
from services.today_workflow_service import get_or_create_daily_workflow_plan, update_task_status, _today_date_str
from models.daily_workflow_models import DailyWorkflowPlan, DailyWorkflowTask
import asyncio
from services.intelligence.txtai_service import TxtaiIntelligenceService
@@ -81,26 +81,7 @@ async def _index_tasks_to_sif(user_id: str, date: str, tasks: list[dict], label:
logger.debug(f"Background indexing failed for user {user_id}: {e}")
@router.get("")
async def get_today_workflow(
date: Optional[str] = None,
current_user: dict = Depends(get_current_user),
db: Session = Depends(get_db),
) -> Dict[str, Any]:
from starlette.concurrency import run_in_threadpool
user_id = str(current_user.get("id"))
plan, created = await get_or_create_daily_workflow_plan(db, user_id, date=date)
def _fetch_tasks():
return (
db.query(DailyWorkflowTask)
.filter(DailyWorkflowTask.plan_id == plan.id, DailyWorkflowTask.user_id == user_id)
.order_by(DailyWorkflowTask.created_at.asc())
.all()
)
tasks = await run_in_threadpool(_fetch_tasks)
def _build_workflow_payload(user_id: str, plan: DailyWorkflowPlan, tasks: list[DailyWorkflowTask]) -> Dict[str, Any]:
response_tasks = []
for t in tasks:
response_tasks.append(
@@ -136,8 +117,156 @@ async def get_today_workflow(
workflow_status = "completed"
total_estimated = int(sum(int(t.get("estimatedTime") or 0) for t in response_tasks))
plan_json = plan.plan_json or {}
return {
"workflow": {
"id": f"daily-{user_id}-{plan.date}",
"date": plan.date,
"userId": user_id,
"tasks": response_tasks,
"currentTaskIndex": current_index,
"completedTasks": completed,
"totalTasks": total,
"workflowStatus": workflow_status,
"totalEstimatedTime": total_estimated,
"actualTimeSpent": 0,
},
"plan": {
"id": plan.id,
"date": plan.date,
"source": plan.source,
"generation_mode": plan.generation_mode,
"committee_agent_count": plan.committee_agent_count,
"fallback_used": bool(plan.fallback_used),
"quality_status": plan_json.get("quality_status", "contextual"),
"contextuality_validation": plan_json.get("contextuality_validation"),
"provenance_summary": {
"generationMode": plan.generation_mode,
"committeeAgentCount": plan.committee_agent_count,
"fallbackUsed": bool(plan.fallback_used),
"taskSourceBreakdown": {},
},
"created_at": plan.created_at.isoformat() if plan.created_at else None,
"updated_at": plan.updated_at.isoformat() if plan.updated_at else None,
},
"schedule_status": {
"date": plan.date,
"generated": True,
"scheduled_run_completed": plan.source == "scheduled",
"source": plan.source,
"created_at": plan.created_at.isoformat() if plan.created_at else None,
},
}
@router.get("")
async def get_today_workflow(
date: Optional[str] = None,
current_user: dict = Depends(get_current_user),
db: Session = Depends(get_db),
) -> Dict[str, Any]:
"""Get existing daily workflow for the specified date.
Returns 404 if no workflow exists for the date.
Workflow should only be created via explicit user action or scheduled job.
"""
from starlette.concurrency import run_in_threadpool
user_id = str(current_user.get("id"))
date_str = date or _today_date_str()
def _get_existing():
return (
db.query(DailyWorkflowPlan)
.filter(DailyWorkflowPlan.user_id == user_id, DailyWorkflowPlan.date == date_str)
.first()
)
plan = await run_in_threadpool(_get_existing)
if not plan:
raise HTTPException(
status_code=404,
detail=f"No workflow found for date {date_str}. Workflow should be generated via explicit user action or scheduled job."
)
def _fetch_tasks():
return (
db.query(DailyWorkflowTask)
.filter(DailyWorkflowTask.plan_id == plan.id, DailyWorkflowTask.user_id == user_id)
.order_by(DailyWorkflowTask.created_at.asc())
.all()
)
tasks = await run_in_threadpool(_fetch_tasks)
return {
"success": True,
"data": _build_workflow_payload(user_id, plan, tasks),
"timestamp": datetime.utcnow().isoformat(),
"user_id": user_id,
}
@router.get("/status")
async def get_today_workflow_status(
date: Optional[str] = None,
current_user: dict = Depends(get_current_user),
db: Session = Depends(get_db),
) -> Dict[str, Any]:
from starlette.concurrency import run_in_threadpool
user_id = str(current_user.get("id"))
date_str = date or _today_date_str()
def _get_existing():
return (
db.query(DailyWorkflowPlan)
.filter(DailyWorkflowPlan.user_id == user_id, DailyWorkflowPlan.date == date_str)
.first()
)
plan = await run_in_threadpool(_get_existing)
return {
"success": True,
"data": {
"date": date_str,
"generated": plan is not None,
"scheduled_run_completed": bool(plan and plan.source == "scheduled"),
"source": plan.source if plan else None,
"created_at": plan.created_at.isoformat() if plan and plan.created_at else None,
},
"timestamp": datetime.utcnow().isoformat(),
"user_id": user_id,
}
@router.post("/generate")
async def generate_workflow(
date: Optional[str] = None,
current_user: dict = Depends(get_current_user),
db: Session = Depends(get_db),
) -> Dict[str, Any]:
"""Explicitly generate a new daily workflow for the specified date.
This should only be called when the user explicitly requests workflow generation
or via a scheduled job at night.
"""
from starlette.concurrency import run_in_threadpool
user_id = str(current_user.get("id"))
plan, created = await get_or_create_daily_workflow_plan(db, user_id, date=date, creation_source="manual")
def _fetch_tasks():
return (
db.query(DailyWorkflowTask)
.filter(DailyWorkflowTask.plan_id == plan.id, DailyWorkflowTask.user_id == user_id)
.order_by(DailyWorkflowTask.created_at.asc())
.all()
)
tasks = await run_in_threadpool(_fetch_tasks)
if created:
response_tasks = _build_workflow_payload(user_id, plan, tasks)["workflow"]["tasks"]
asyncio.create_task(_index_tasks_to_sif(user_id, plan.date, response_tasks, label="today"))
from datetime import date as date_type, timedelta
@@ -200,29 +329,7 @@ async def get_today_workflow(
return {
"success": True,
"data": {
"workflow": {
"id": f"daily-{user_id}-{plan.date}",
"date": plan.date,
"userId": user_id,
"tasks": response_tasks,
"currentTaskIndex": current_index,
"completedTasks": completed,
"totalTasks": total,
"workflowStatus": workflow_status,
"totalEstimatedTime": total_estimated,
"actualTimeSpent": 0,
},
"plan": {
"id": plan.id,
"date": plan.date,
"source": plan.source,
"quality_status": (plan.plan_json or {}).get("quality_status", "contextual"),
"contextuality_validation": (plan.plan_json or {}).get("contextuality_validation"),
"created_at": plan.created_at.isoformat() if plan.created_at else None,
"updated_at": plan.updated_at.isoformat() if plan.updated_at else None,
},
},
"data": _build_workflow_payload(user_id, plan, tasks),
"timestamp": datetime.utcnow().isoformat(),
"user_id": user_id,
}

View File

@@ -18,6 +18,26 @@ router = APIRouter()
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.
@@ -94,7 +114,15 @@ 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)
task_manager.update_task(task_id, "failed", error=str(e), user_id=user_id)
error_meta = _extract_error_metadata(e)
task_manager.update_task(
task_id,
"failed",
error=str(e),
user_id=user_id,
error_status=error_meta.get("error_status"),
error_data=error_meta.get("error_data"),
)
finally:
# Cleanup temp upload files
try:

View File

@@ -39,7 +39,18 @@ class TaskManager:
logger.error(f"[VideoStudio] Failed to create task: {e}")
raise
def update_task(self, task_id: str, status: str, result: Optional[Dict] = None, error: Optional[str] = None, user_id: str = None, progress: float = None, message: str = None):
def update_task(
self,
task_id: str,
status: str,
result: Optional[Dict] = None,
error: Optional[str] = None,
user_id: str = None,
progress: float = None,
message: str = None,
error_status: Optional[int] = None,
error_data: Optional[Dict[str, Any]] = None,
):
"""Update an existing task."""
if not user_id:
logger.error(f"[VideoStudio] Cannot update task {task_id} without user_id")
@@ -74,6 +85,13 @@ class TaskManager:
task.result = result
if error:
task.error = error
if error_status is not None or error_data is not None:
result_payload = task.result if isinstance(task.result, dict) else {}
if error_status is not None:
result_payload["error_status"] = error_status
if error_data is not None:
result_payload["error_data"] = error_data
task.result = result_payload
if progress is not None:
task.progress = progress
if message:
@@ -107,7 +125,7 @@ class TaskManager:
if status_val == "processing":
status_val = "running"
return {
response = {
"task_id": task.task_id,
"status": status_val,
"result": task.result,
@@ -117,6 +135,12 @@ class TaskManager:
"created_at": task.created_at,
"updated_at": task.updated_at
}
if isinstance(task.result, dict):
if task.result.get("error_status") is not None:
response["error_status"] = task.result.get("error_status")
if task.result.get("error_data") is not None:
response["error_data"] = task.result.get("error_data")
return response
finally:
db.close()
except Exception as e: