chore(lint): fix all ruff errors — 0 warnings remaining
- B904 (55): add `from err` / `from None` to raise-in-except across 13 files - F821 (1): add missing HTTPException import in routes_language_qc.py - F841 (7): remove unused variable assignments (current_user, job_title, tts_provider, etc.) - W293 (13): strip trailing whitespace from blank lines - C416 (4): rewrite unnecessary dict comprehensions as dict() - C401 (1): rewrite unnecessary generator as set comprehension - E701 (4): split multi-statement lines in cost_tracker.py - E741 (1): rename ambiguous `l` to `lang` in cloud_run_dispatch.py - B007 (4): prefix unused loop variables with _ in tts.py, video_renderer.py - I001 (1): sort imports in tasks/__init__.py (move stdlib to top) - E402 (3): move threading/time/signals imports to top of tasks/__init__.py - UP042 (9): replace (str, Enum) with StrEnum in all model/schema enums Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
16000a8bd9
commit
ca312d48fa
33 changed files with 120 additions and 123 deletions
|
|
@ -253,7 +253,7 @@ async def update_user(
|
|||
action = AuditAction.USER_ROLE_CHANGE if user_update.role else AuditAction.USER_UPDATE
|
||||
await log_user_management(
|
||||
action, user_id, current_user, request,
|
||||
details={k: v for k, v in user_update.dict(exclude_none=True).items()},
|
||||
details=dict(user_update.dict(exclude_none=True).items()),
|
||||
)
|
||||
|
||||
return UserResponse(
|
||||
|
|
@ -439,7 +439,7 @@ async def detailed_health_check(
|
|||
try:
|
||||
from ...services.gcs import gcs_service
|
||||
# Simple check to see if bucket is accessible
|
||||
bucket_exists = await gcs_service.file_exists("health_check_dummy") # This will return False but won't error if bucket accessible
|
||||
await gcs_service.file_exists("health_check_dummy") # This will return False but won't error if bucket accessible
|
||||
health_status["components"]["gcs"] = {"status": "healthy"}
|
||||
except Exception as e:
|
||||
health_status["components"]["gcs"] = {"status": "unhealthy", "error": str(e)}
|
||||
|
|
|
|||
|
|
@ -143,13 +143,13 @@ async def microsoft_login(
|
|||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=f"Microsoft authentication failed: {str(e)}",
|
||||
)
|
||||
) from None
|
||||
except MicrosoftAuthError as e:
|
||||
await log_auth_failure("microsoft-sso", request, f"MS auth service error: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Microsoft authentication service error",
|
||||
)
|
||||
) from None
|
||||
|
||||
# Look up by Microsoft-derived ID first — handles email casing changes across logins
|
||||
ms_user_id = f"ms-{user_info.sub[:20]}"
|
||||
|
|
@ -287,7 +287,7 @@ async def refresh_token(
|
|||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid refresh token",
|
||||
)
|
||||
) from None
|
||||
|
||||
|
||||
@router.post("/logout", response_model=LogoutResponse)
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ async def update_client(
|
|||
db: AsyncIOMotorDatabase = Depends(get_database),
|
||||
):
|
||||
await _get_client_or_404(client_id, db)
|
||||
update: dict = {k: v for k, v in body.model_dump(exclude_none=True).items()}
|
||||
update: dict = dict(body.model_dump(exclude_none=True).items())
|
||||
if not update:
|
||||
raise HTTPException(status_code=422, detail="No fields to update")
|
||||
if "slug" in update and await db.clients.find_one({"slug": update["slug"], "_id": {"$ne": client_id}}):
|
||||
|
|
@ -296,7 +296,7 @@ async def update_team(
|
|||
await _get_client_or_404(client_id, db)
|
||||
await _assert_pm_or_admin(current_user, client_id, db)
|
||||
await _get_team_or_404(team_id, client_id, db)
|
||||
update = {k: v for k, v in body.model_dump(exclude_none=True).items()}
|
||||
update = dict(body.model_dump(exclude_none=True).items())
|
||||
if not update:
|
||||
raise HTTPException(status_code=422, detail="No fields to update")
|
||||
update["updated_at"] = _now()
|
||||
|
|
@ -440,7 +440,7 @@ async def update_project(
|
|||
await _get_client_or_404(client_id, db)
|
||||
await _assert_pm_or_admin(current_user, client_id, db)
|
||||
await _get_project_or_404(project_id, client_id, db)
|
||||
update = {k: v for k, v in body.model_dump(exclude_none=True).items()}
|
||||
update = dict(body.model_dump(exclude_none=True).items())
|
||||
if not update:
|
||||
raise HTTPException(status_code=422, detail="No fields to update")
|
||||
update["updated_at"] = _now()
|
||||
|
|
|
|||
|
|
@ -62,4 +62,4 @@ async def get_signed_upload_url(
|
|||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to generate signed upload URL: {str(e)}"
|
||||
)
|
||||
) from None
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ async def complete_chunked_upload(
|
|||
outputs_data = json.loads(json.dumps(payload.requested_outputs))
|
||||
outputs = RequestedOutputs(**outputs_data)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid requested_outputs format")
|
||||
raise HTTPException(status_code=400, detail="Invalid requested_outputs format") from None
|
||||
|
||||
organization_id: str | None = None
|
||||
brief_doc = None
|
||||
|
|
@ -196,7 +196,7 @@ async def complete_chunked_upload(
|
|||
logger.info("Dispatched ingest task for chunked-upload job %s", payload.job_id)
|
||||
except Exception as e:
|
||||
logger.error("Failed to dispatch ingest task for job %s: %s", payload.job_id, e)
|
||||
raise HTTPException(status_code=500, detail=f"Failed to start processing: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to start processing: {e}") from None
|
||||
|
||||
await log_job_action(
|
||||
AuditAction.JOB_CREATE, payload.job_id, current_user, request,
|
||||
|
|
@ -246,7 +246,7 @@ async def create_job(
|
|||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Invalid requested_outputs format"
|
||||
)
|
||||
) from None
|
||||
|
||||
# Resolve brief if provided — overrides some fields and sets organization_id
|
||||
brief_doc = None
|
||||
|
|
@ -330,7 +330,7 @@ async def create_job(
|
|||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to start processing: {e}",
|
||||
)
|
||||
) from None
|
||||
|
||||
await log_job_action(
|
||||
AuditAction.JOB_CREATE, job_id, current_user, request,
|
||||
|
|
@ -774,7 +774,6 @@ async def get_job(
|
|||
db: AsyncIOMotorDatabase = Depends(get_database),
|
||||
):
|
||||
job_doc = await get_job_or_403(job_id, ctx, db)
|
||||
current_user = ctx.user
|
||||
|
||||
# Check task status if task_id exists
|
||||
task_id = job_doc.get("task_id")
|
||||
|
|
@ -1586,7 +1585,7 @@ async def update_job_vtt_content(
|
|||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"You are not assigned to language '{target_language}'"
|
||||
)
|
||||
) from None
|
||||
|
||||
outputs = job_doc.get("outputs", {})
|
||||
lang_output = outputs.get(target_language, {})
|
||||
|
|
@ -2024,7 +2023,7 @@ async def adjust_vtt_timing(
|
|||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to adjust captions timing"
|
||||
)
|
||||
) from None
|
||||
|
||||
# Adjust audio description VTT if requested and exists
|
||||
if request.adjust_audio_description and "ad_vtt_gcs" in outputs:
|
||||
|
|
@ -2055,7 +2054,7 @@ async def adjust_vtt_timing(
|
|||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to adjust audio description timing"
|
||||
)
|
||||
) from None
|
||||
|
||||
if not update_operations:
|
||||
raise HTTPException(
|
||||
|
|
@ -2196,7 +2195,7 @@ async def delete_job(
|
|||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to delete job: {str(e)}"
|
||||
)
|
||||
) from None
|
||||
|
||||
|
||||
async def _delete_job_gcs_assets(job_id: str, job_doc: dict):
|
||||
|
|
@ -2322,7 +2321,7 @@ async def retry_tts(
|
|||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to start TTS retry"
|
||||
)
|
||||
) from None
|
||||
|
||||
return JobResponse(
|
||||
id=str(result["_id"]),
|
||||
|
|
@ -2460,7 +2459,7 @@ async def retry_job(
|
|||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to start retry task",
|
||||
)
|
||||
) from None
|
||||
|
||||
return JobResponse(
|
||||
id=str(result["_id"]),
|
||||
|
|
@ -2930,7 +2929,7 @@ async def trigger_accessible_video_rerender(
|
|||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"You are not assigned to language '{language}'"
|
||||
)
|
||||
) from None
|
||||
|
||||
# Get edit state
|
||||
lang_output = job_doc.get("outputs", {}).get(language)
|
||||
|
|
@ -2950,7 +2949,6 @@ async def trigger_accessible_video_rerender(
|
|||
]
|
||||
|
||||
# Update job status to RENDERING_QC — conditional to prevent concurrent render races
|
||||
job_title = job_doc.get("title", "Untitled Job")
|
||||
transition_result = await db.jobs.update_one(
|
||||
{"_id": job_id, "status": JobStatus.PENDING_QC.value}, # Only transition from PENDING_QC
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, Request
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
from motor.motor_asyncio import AsyncIOMotorDatabase
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
|
@ -205,7 +205,6 @@ async def bulk_assign_languages(
|
|||
"""Assign one linguist (and optionally one reviewer) to multiple languages in one call."""
|
||||
job_doc = await db["jobs"].find_one({"_id": job_id})
|
||||
if not job_doc:
|
||||
from fastapi import HTTPException
|
||||
raise HTTPException(status_code=404, detail="Job not found")
|
||||
|
||||
available = list((job_doc.get("outputs") or {}).keys())
|
||||
|
|
@ -352,10 +351,6 @@ async def mark_cue_reviewed(
|
|||
if not job_doc:
|
||||
raise HTTPException(status_code=404, detail="Job not found")
|
||||
|
||||
update: dict = {
|
||||
f"language_qc.{lang}.reviewed_cues": 1, # will use $inc below
|
||||
"updated_at": datetime.utcnow(),
|
||||
}
|
||||
inc_op: dict = {f"language_qc.{lang}.reviewed_cues": 1}
|
||||
set_op: dict = {"updated_at": datetime.utcnow()}
|
||||
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ async def restore_vtt_version(
|
|||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Version snapshot created (v{new_ver.version}) but live file update failed: {exc}",
|
||||
)
|
||||
) from None
|
||||
|
||||
# Update the GCS URI pointer in the job document
|
||||
gcs_uri_key = "captions_vtt_gcs" if kind == "captions" else "ad_vtt_gcs"
|
||||
|
|
|
|||
|
|
@ -58,4 +58,4 @@ def decode_token(token: str) -> dict[str, Any]:
|
|||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
)
|
||||
) from None
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ class RateLimiter:
|
|||
) -> tuple[bool, dict[str, int]]:
|
||||
"""
|
||||
Check if request is allowed under rate limit.
|
||||
|
||||
|
||||
Returns:
|
||||
Tuple of (is_allowed, rate_limit_info)
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -125,9 +125,9 @@ class RequestValidator:
|
|||
subtitle_extensions = {'vtt', 'srt', 'txt'}
|
||||
|
||||
if expected_type == "video" and ext not in video_extensions:
|
||||
raise ValidationError(f"Invalid video file extension: {ext}")
|
||||
raise ValidationError(f"Invalid video file extension: {ext}") from None
|
||||
elif expected_type == "subtitle" and ext not in subtitle_extensions:
|
||||
raise ValidationError(f"Invalid subtitle file extension: {ext}")
|
||||
raise ValidationError(f"Invalid subtitle file extension: {ext}") from None
|
||||
return
|
||||
|
||||
if expected_type == "video" and detected_type not in self.allowed_video_types:
|
||||
|
|
@ -186,7 +186,7 @@ class RequestValidator:
|
|||
return payload
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
raise ValidationError(f"Invalid JSON: {e}")
|
||||
raise ValidationError(f"Invalid JSON: {e}") from e
|
||||
|
||||
def _validate_json_values(self, obj: Any, path: str = "root") -> None:
|
||||
"""Recursively validate JSON values."""
|
||||
|
|
|
|||
|
|
@ -141,10 +141,10 @@ class MigrationManager:
|
|||
async def migrate_up(self, target_version: str | None = None) -> list[str]:
|
||||
"""
|
||||
Apply migrations up to the target version.
|
||||
|
||||
|
||||
Args:
|
||||
target_version: Version to migrate to. If None, applies all pending migrations.
|
||||
|
||||
|
||||
Returns:
|
||||
List of applied migration versions.
|
||||
"""
|
||||
|
|
@ -189,10 +189,10 @@ class MigrationManager:
|
|||
async def migrate_down(self, target_version: str) -> list[str]:
|
||||
"""
|
||||
Rollback migrations down to the target version.
|
||||
|
||||
|
||||
Args:
|
||||
target_version: Version to rollback to.
|
||||
|
||||
|
||||
Returns:
|
||||
List of rolled back migration versions.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"""Entry point for running migrations: python -m app.migrations.run"""
|
||||
import asyncio
|
||||
|
||||
from app.core.database import connect_to_mongo, close_mongo_connection
|
||||
from app.core.database import close_mongo_connection, connect_to_mongo
|
||||
from app.migrations.migrator import MigrationManager
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"""Audit log model for tracking sensitive operations."""
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
from typing import Any
|
||||
|
||||
from bson import ObjectId
|
||||
|
|
@ -10,7 +10,7 @@ from pydantic import BaseModel, Field
|
|||
from .user import PyObjectId
|
||||
|
||||
|
||||
class AuditAction(str, Enum):
|
||||
class AuditAction(StrEnum):
|
||||
"""Enumeration of auditable actions."""
|
||||
|
||||
# Authentication actions
|
||||
|
|
@ -84,7 +84,7 @@ class AuditAction(str, Enum):
|
|||
SUSPICIOUS_ACTIVITY = "security.suspicious.activity"
|
||||
|
||||
|
||||
class AuditLogSeverity(str, Enum):
|
||||
class AuditLogSeverity(StrEnum):
|
||||
"""Severity levels for audit events."""
|
||||
|
||||
INFO = "info" # Normal operations
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, Field, constr
|
||||
|
|
@ -7,7 +7,7 @@ from pydantic import BaseModel, Field, constr
|
|||
FailureStep = Literal["ingestion", "ai_processing", "translation", "tts", "render"]
|
||||
|
||||
|
||||
class JobStatus(str, Enum):
|
||||
class JobStatus(StrEnum):
|
||||
CREATED = "created"
|
||||
INGESTING = "ingesting"
|
||||
AI_PROCESSING = "ai_processing"
|
||||
|
|
@ -158,7 +158,7 @@ class Review(BaseModel):
|
|||
|
||||
# ── Per-language QC ───────────────────────────────────────────────────────────
|
||||
|
||||
class LanguageQCStatus(str, Enum):
|
||||
class LanguageQCStatus(StrEnum):
|
||||
PENDING = "pending"
|
||||
IN_PROGRESS = "in_progress" # linguist is working
|
||||
PENDING_REVIEW = "pending_review" # linguist submitted, awaiting reviewer
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
"""Job Brief model — pre-approved work order submitted before job creation."""
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from .job import RequestedOutputs
|
||||
|
||||
|
||||
class BriefStatus(str, Enum):
|
||||
class BriefStatus(StrEnum):
|
||||
DRAFT = "draft"
|
||||
SUBMITTED = "submitted"
|
||||
APPROVED = "approved"
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class OrgRole(str, Enum):
|
||||
class OrgRole(StrEnum):
|
||||
OWNER = "owner"
|
||||
ADMIN = "admin"
|
||||
MANAGER = "manager"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
from typing import Annotated
|
||||
|
||||
from bson import ObjectId
|
||||
|
|
@ -18,7 +18,7 @@ def validate_object_id(v) -> str:
|
|||
PyObjectId = Annotated[str, BeforeValidator(validate_object_id)]
|
||||
|
||||
|
||||
class UserRole(str, Enum):
|
||||
class UserRole(StrEnum):
|
||||
CLIENT = "client"
|
||||
REVIEWER = "reviewer"
|
||||
LINGUIST = "linguist"
|
||||
|
|
@ -27,7 +27,7 @@ class UserRole(str, Enum):
|
|||
ADMIN = "admin"
|
||||
|
||||
|
||||
class AuthProvider(str, Enum):
|
||||
class AuthProvider(StrEnum):
|
||||
LOCAL = "local"
|
||||
MICROSOFT = "microsoft"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
"""Schemas for accessible video generation with embedded audio descriptions."""
|
||||
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class AccessibleVideoMethod(str, Enum):
|
||||
class AccessibleVideoMethod(StrEnum):
|
||||
"""Method used for integrating audio descriptions into video."""
|
||||
OVERLAY = "overlay"
|
||||
PAUSE_INSERT = "pause_insert"
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ class AuditLogger:
|
|||
) -> str:
|
||||
"""
|
||||
Log an audit event.
|
||||
|
||||
|
||||
Returns:
|
||||
The ID of the created audit log entry.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ def _celery_fallback(task: str, job_id: str, **extra_args) -> str:
|
|||
from ..tasks.translate_and_synthesize import translate_and_synthesize_task
|
||||
_langs = extra_args.get("languages")
|
||||
if isinstance(_langs, str):
|
||||
_langs = [l for l in _langs.split(",") if l]
|
||||
_langs = [lang for lang in _langs.split(",") if lang]
|
||||
translate_and_synthesize_task.delay(job_id, languages=_langs or None)
|
||||
elif task == "render":
|
||||
from ..tasks.render_accessible_video import render_accessible_video_task
|
||||
|
|
|
|||
|
|
@ -75,8 +75,10 @@ def record(
|
|||
if chars is not None:
|
||||
units["char"] = chars
|
||||
else:
|
||||
if input_tokens: units["token_input"] = input_tokens
|
||||
if output_tokens: units["token_output"] = output_tokens
|
||||
if input_tokens:
|
||||
units["token_input"] = input_tokens
|
||||
if output_tokens:
|
||||
units["token_output"] = output_tokens
|
||||
|
||||
payload: dict = {
|
||||
"source_app": settings.cost_tracker_source_app,
|
||||
|
|
@ -87,8 +89,10 @@ def record(
|
|||
"latency_ms": latency_ms,
|
||||
"status": status,
|
||||
}
|
||||
if project_id: payload["project_external_id"] = project_id
|
||||
if job_external_id: payload["job_external_id"] = job_external_id
|
||||
if project_id:
|
||||
payload["project_external_id"] = project_id
|
||||
if job_external_id:
|
||||
payload["job_external_id"] = job_external_id
|
||||
|
||||
httpx.post(
|
||||
f"{settings.cost_tracker_base_url}/usage/record",
|
||||
|
|
|
|||
|
|
@ -273,7 +273,7 @@ async def run_ffmpeg(request: RunFFmpegRequest):
|
|||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"FFmpeg operation failed: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
raise HTTPException(status_code=500, detail=str(e)) from None
|
||||
|
||||
|
||||
@app.post("/probe", response_model=ProbeResponse)
|
||||
|
|
@ -328,7 +328,7 @@ async def probe_video(request: ProbeRequest):
|
|||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Probe failed: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
raise HTTPException(status_code=500, detail=str(e)) from None
|
||||
|
||||
|
||||
@app.post("/encode-segment", response_model=RunFFmpegResponse)
|
||||
|
|
@ -380,7 +380,7 @@ async def encode_segment(request: EncodeSegmentRequest):
|
|||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Encode segment failed: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
raise HTTPException(status_code=500, detail=str(e)) from None
|
||||
|
||||
|
||||
@app.post("/extract-frame", response_model=RunFFmpegResponse)
|
||||
|
|
@ -425,7 +425,7 @@ async def extract_frame(request: ExtractFrameRequest):
|
|||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Extract frame failed: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
raise HTTPException(status_code=500, detail=str(e)) from None
|
||||
|
||||
|
||||
@app.post("/create-freeze-segment", response_model=RunFFmpegResponse)
|
||||
|
|
@ -480,7 +480,7 @@ async def create_freeze_segment(request: CreateFreezeSegmentRequest):
|
|||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Create freeze segment failed: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
raise HTTPException(status_code=500, detail=str(e)) from None
|
||||
|
||||
|
||||
@app.post("/concatenate", response_model=RunFFmpegResponse)
|
||||
|
|
@ -534,4 +534,4 @@ async def concatenate_segments(request: ConcatenateRequest):
|
|||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Concatenate failed: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
raise HTTPException(status_code=500, detail=str(e)) from None
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ class GCSService:
|
|||
return await loop.run_in_executor(self.executor, _upload)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to upload file to GCS: {e}")
|
||||
raise HTTPException(status_code=500, detail="File upload failed")
|
||||
raise HTTPException(status_code=500, detail="File upload failed") from None
|
||||
|
||||
async def upload_text_to_gcs(
|
||||
self,
|
||||
|
|
@ -76,7 +76,7 @@ class GCSService:
|
|||
return await loop.run_in_executor(self.executor, _upload)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to upload text to GCS: {e}")
|
||||
raise HTTPException(status_code=500, detail="Text upload failed")
|
||||
raise HTTPException(status_code=500, detail="Text upload failed") from None
|
||||
|
||||
async def get_signed_url(
|
||||
self,
|
||||
|
|
@ -104,10 +104,10 @@ class GCSService:
|
|||
try:
|
||||
return await loop.run_in_executor(self.executor, _get_signed_url)
|
||||
except NotFound:
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
raise HTTPException(status_code=404, detail="File not found") from None
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to generate signed URL: {e}")
|
||||
raise HTTPException(status_code=500, detail="Failed to generate download URL")
|
||||
raise HTTPException(status_code=500, detail="Failed to generate download URL") from None
|
||||
|
||||
async def create_resumable_upload_session(self, blob_path: str, content_type: str) -> str:
|
||||
"""Create a GCS resumable upload session and return the session URI."""
|
||||
|
|
@ -123,7 +123,7 @@ class GCSService:
|
|||
return await loop.run_in_executor(self.executor, _create)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create resumable upload session: {e}")
|
||||
raise HTTPException(status_code=500, detail="Failed to initiate upload session")
|
||||
raise HTTPException(status_code=500, detail="Failed to initiate upload session") from None
|
||||
|
||||
async def delete_file(self, blob_path: str) -> bool:
|
||||
"""Delete a file from GCS"""
|
||||
|
|
@ -139,7 +139,7 @@ class GCSService:
|
|||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete file from GCS: {e}")
|
||||
raise HTTPException(status_code=500, detail="File deletion failed")
|
||||
raise HTTPException(status_code=500, detail="File deletion failed") from None
|
||||
|
||||
async def file_exists(self, blob_path: str) -> bool:
|
||||
"""Check if a file exists in GCS"""
|
||||
|
|
|
|||
|
|
@ -358,7 +358,7 @@ Fix the JSON and return it:
|
|||
|
||||
except Exception as e:
|
||||
logger.error(f"Self-heal attempt failed: {e}")
|
||||
raise ValueError("Failed to get valid JSON from Gemini after self-heal attempt")
|
||||
raise ValueError("Failed to get valid JSON from Gemini after self-heal attempt") from e
|
||||
|
||||
async def extract_accessibility_targeted(
|
||||
self,
|
||||
|
|
@ -567,7 +567,7 @@ Fix the JSON and return it:
|
|||
|
||||
except Exception as e:
|
||||
logger.error(f"Self-heal attempt failed for {target_language}: {e}")
|
||||
raise ValueError(f"Failed to get valid JSON from Gemini targeted extraction for {target_language}")
|
||||
raise ValueError(f"Failed to get valid JSON from Gemini targeted extraction for {target_language}") from e
|
||||
|
||||
def _attempt_json_fix(self, json_text: str) -> dict[str, Any] | None:
|
||||
"""Attempt to fix common JSON syntax issues"""
|
||||
|
|
@ -788,7 +788,7 @@ Fix the JSON and return it:
|
|||
|
||||
except Exception as e:
|
||||
logger.error(f"Self-heal attempt for accessible video analysis failed: {e}")
|
||||
raise ValueError("Failed to get valid JSON from accessible video analysis after self-heal")
|
||||
raise ValueError("Failed to get valid JSON from accessible video analysis after self-heal") from e
|
||||
|
||||
async def transcreate_content(
|
||||
self,
|
||||
|
|
@ -845,7 +845,7 @@ JSON:
|
|||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Failed to parse transcreation JSON response: {e}")
|
||||
raise ValueError("Invalid JSON response from transcreation")
|
||||
raise ValueError("Invalid JSON response from transcreation") from e
|
||||
except Exception as e:
|
||||
logger.error(f"Transcreation failed: {e}")
|
||||
raise
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ class MicrosoftAuthService:
|
|||
return response.json()
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"Failed to fetch OpenID configuration: {e}")
|
||||
raise MicrosoftAuthError("Failed to fetch Microsoft authentication configuration")
|
||||
raise MicrosoftAuthError("Failed to fetch Microsoft authentication configuration") from e
|
||||
|
||||
async def _get_jwks(self, force_refresh: bool = False) -> dict:
|
||||
"""Fetch JSON Web Key Set (JWKS) from Microsoft.
|
||||
|
|
@ -97,7 +97,7 @@ class MicrosoftAuthService:
|
|||
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"Failed to fetch JWKS: {e}")
|
||||
raise MicrosoftAuthError("Failed to fetch Microsoft public keys")
|
||||
raise MicrosoftAuthError("Failed to fetch Microsoft public keys") from e
|
||||
|
||||
async def validate_token(self, id_token: str) -> MicrosoftUserInfo:
|
||||
"""Validate Microsoft ID token and extract user information.
|
||||
|
|
@ -145,7 +145,7 @@ class MicrosoftAuthService:
|
|||
issuer=f"https://login.microsoftonline.com/{self.tenant_id}/v2.0"
|
||||
)
|
||||
except JWTError as e:
|
||||
raise MicrosoftTokenValidationError(f"Token validation failed: {str(e)}")
|
||||
raise MicrosoftTokenValidationError(f"Token validation failed: {str(e)}") from e
|
||||
|
||||
email = payload.get('email') or payload.get('preferred_username')
|
||||
if not email:
|
||||
|
|
@ -176,12 +176,12 @@ class MicrosoftAuthService:
|
|||
|
||||
except JWKError as e:
|
||||
logger.error(f"JWK error during token validation: {e}")
|
||||
raise MicrosoftTokenValidationError(f"Key processing error: {str(e)}")
|
||||
raise MicrosoftTokenValidationError(f"Key processing error: {str(e)}") from e
|
||||
except Exception as e:
|
||||
if isinstance(e, (MicrosoftAuthError, MicrosoftTokenValidationError)):
|
||||
raise
|
||||
logger.error(f"Unexpected error during token validation: {e}")
|
||||
raise MicrosoftTokenValidationError(f"Token validation failed: {str(e)}")
|
||||
raise MicrosoftTokenValidationError(f"Token validation failed: {str(e)}") from e
|
||||
|
||||
|
||||
# Singleton instance
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class SecretsManager:
|
|||
logger.info("Secret Manager client initialized")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize Secret Manager client: {e}")
|
||||
raise SecretManagerError(f"Failed to initialize Secret Manager: {e}")
|
||||
raise SecretManagerError(f"Failed to initialize Secret Manager: {e}") from e
|
||||
|
||||
return self.client
|
||||
|
||||
|
|
@ -44,14 +44,14 @@ class SecretsManager:
|
|||
async def get_secret(self, secret_name: str, version: str = "latest") -> str:
|
||||
"""
|
||||
Retrieve a secret from Google Cloud Secret Manager.
|
||||
|
||||
|
||||
Args:
|
||||
secret_name: Name of the secret
|
||||
version: Version of the secret (default: "latest")
|
||||
|
||||
|
||||
Returns:
|
||||
The secret value as a string
|
||||
|
||||
|
||||
Raises:
|
||||
SecretManagerError: If secret cannot be retrieved
|
||||
"""
|
||||
|
|
@ -89,26 +89,26 @@ class SecretsManager:
|
|||
except gcp_exceptions.NotFound:
|
||||
error_msg = f"Secret not found: {secret_name}"
|
||||
logger.error(error_msg)
|
||||
raise SecretManagerError(error_msg)
|
||||
raise SecretManagerError(error_msg) from None
|
||||
|
||||
except gcp_exceptions.PermissionDenied:
|
||||
error_msg = f"Permission denied accessing secret: {secret_name}"
|
||||
logger.error(error_msg)
|
||||
raise SecretManagerError(error_msg)
|
||||
raise SecretManagerError(error_msg) from None
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to retrieve secret {secret_name}: {e}"
|
||||
logger.error(error_msg)
|
||||
raise SecretManagerError(error_msg)
|
||||
raise SecretManagerError(error_msg) from e
|
||||
|
||||
@trace_async_operation("secrets_manager.get_secrets_batch")
|
||||
async def get_secrets_batch(self, secret_names: list[str]) -> dict[str, str]:
|
||||
"""
|
||||
Retrieve multiple secrets efficiently.
|
||||
|
||||
|
||||
Args:
|
||||
secret_names: List of secret names to retrieve
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary mapping secret names to their values
|
||||
"""
|
||||
|
|
@ -137,12 +137,12 @@ class SecretsManager:
|
|||
async def create_secret(self, secret_name: str, secret_value: str, labels: dict[str, str] | None = None) -> str:
|
||||
"""
|
||||
Create a new secret in Secret Manager.
|
||||
|
||||
|
||||
Args:
|
||||
secret_name: Name of the secret
|
||||
secret_value: Value to store
|
||||
labels: Optional labels for the secret
|
||||
|
||||
|
||||
Returns:
|
||||
The full secret resource name
|
||||
"""
|
||||
|
|
@ -186,12 +186,12 @@ class SecretsManager:
|
|||
except gcp_exceptions.AlreadyExists:
|
||||
error_msg = f"Secret already exists: {secret_name}"
|
||||
logger.error(error_msg)
|
||||
raise SecretManagerError(error_msg)
|
||||
raise SecretManagerError(error_msg) from None
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to create secret {secret_name}: {e}"
|
||||
logger.error(error_msg)
|
||||
raise SecretManagerError(error_msg)
|
||||
raise SecretManagerError(error_msg) from e
|
||||
|
||||
def clear_cache(self) -> None:
|
||||
"""Clear the secrets cache."""
|
||||
|
|
@ -217,7 +217,7 @@ async def get_database_url() -> str:
|
|||
# Fallback to environment variable
|
||||
url = os.getenv("MONGODB_URL")
|
||||
if not url:
|
||||
raise SecretManagerError("MongoDB URL not available in secrets or environment")
|
||||
raise SecretManagerError("MongoDB URL not available in secrets or environment") from None
|
||||
return url
|
||||
|
||||
|
||||
|
|
@ -229,7 +229,7 @@ async def get_redis_url() -> str:
|
|||
# Fallback to environment variable
|
||||
url = os.getenv("REDIS_URL")
|
||||
if not url:
|
||||
raise SecretManagerError("Redis URL not available in secrets or environment")
|
||||
raise SecretManagerError("Redis URL not available in secrets or environment") from None
|
||||
return url
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -232,7 +232,7 @@ class TTSService:
|
|||
audio_segments = []
|
||||
current_audio_position = 0.0 # Track actual audio timeline position
|
||||
|
||||
for i, cue in enumerate(cues):
|
||||
for _i, cue in enumerate(cues):
|
||||
# Calculate where this cue should start (anchored to VTT timing)
|
||||
target_start_time = cue["start_time"]
|
||||
|
||||
|
|
@ -298,7 +298,7 @@ class TTSService:
|
|||
audio_segments = []
|
||||
current_audio_position = 0.0 # Track actual audio timeline position
|
||||
|
||||
for i, cue in enumerate(cues):
|
||||
for _i, cue in enumerate(cues):
|
||||
# Calculate where this cue should start (anchored to VTT timing)
|
||||
target_start_time = cue["start_time"]
|
||||
|
||||
|
|
|
|||
|
|
@ -231,7 +231,7 @@ class VideoRendererService:
|
|||
error_detail = e.response.json().get("detail", str(e))
|
||||
except Exception:
|
||||
error_detail = str(e)
|
||||
raise FFmpegExecutionError(f"Cloud Run {endpoint} failed: {error_detail}")
|
||||
raise FFmpegExecutionError(f"Cloud Run {endpoint} failed: {error_detail}") from e
|
||||
|
||||
async def _dispatch_ffmpeg(self, cmd: list[str], timeout: int = 3600) -> dict[str, Any]:
|
||||
"""
|
||||
|
|
@ -391,8 +391,7 @@ class VideoRendererService:
|
|||
logger.info(f"Starting overlay render for {source_video_path}")
|
||||
placements = analysis.get("placements", [])
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_dir_path = Path(temp_dir)
|
||||
with tempfile.TemporaryDirectory() as _temp_dir:
|
||||
|
||||
# Get source video duration
|
||||
duration = await self._get_video_duration(source_video_path)
|
||||
|
|
@ -415,7 +414,7 @@ class VideoRendererService:
|
|||
filter_parts = []
|
||||
|
||||
# Add each AD segment as input
|
||||
for cue_index, mp3_path in ad_segments:
|
||||
for _cue_index, mp3_path in ad_segments:
|
||||
inputs.extend(["-i", mp3_path])
|
||||
|
||||
# Build complex filter
|
||||
|
|
@ -429,7 +428,7 @@ class VideoRendererService:
|
|||
|
||||
# Add delay to each AD segment and mix
|
||||
ad_labels = []
|
||||
for i, (cue_index, mp3_path) in enumerate(ad_segments):
|
||||
for i, (cue_index, _mp3_path) in enumerate(ad_segments):
|
||||
# Find the placement for this cue
|
||||
placement = next(
|
||||
(p for p in placements if p.get("ad_cue_index") == cue_index),
|
||||
|
|
@ -564,7 +563,7 @@ class VideoRendererService:
|
|||
logger.info(f"Source Properties: {video_props}, Duration: {source_duration:.2f}s")
|
||||
|
||||
# Create a mapping of cue_index to mp3_path
|
||||
cue_to_mp3 = {cue_index: mp3_path for cue_index, mp3_path in ad_segments}
|
||||
cue_to_mp3 = dict(ad_segments)
|
||||
|
||||
# Pre-process placements and validate
|
||||
valid_placements = []
|
||||
|
|
@ -884,9 +883,6 @@ class VideoRendererService:
|
|||
# Pause point is at the START of the freeze frame in the rendered timeline
|
||||
pause_ms = freeze_frame_starts.get(cue_index, p["pause_point"] * 1000)
|
||||
|
||||
# Find the freeze segment for this cue to get its end position
|
||||
freeze_seg = next((s for s in segment_metadata_list if s.is_freeze_frame and s.cue_index == cue_index), None)
|
||||
|
||||
# Compute min bound: end of previous AD segment (or 0 for first)
|
||||
if idx == 0:
|
||||
min_bound_ms = 0.0
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ async def transcribe(request: TranscribeRequest):
|
|||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Transcription failed: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
raise HTTPException(status_code=500, detail=str(e)) from None
|
||||
finally:
|
||||
# Clean up temp file
|
||||
if os.path.exists(tmp_path):
|
||||
|
|
@ -252,7 +252,7 @@ async def transcribe_with_gaps(request: TranscribeWithGapsRequest):
|
|||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Transcription with gaps failed: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
raise HTTPException(status_code=500, detail=str(e)) from None
|
||||
finally:
|
||||
# Clean up temp file
|
||||
if os.path.exists(tmp_path):
|
||||
|
|
@ -297,7 +297,7 @@ async def refine_pause_points(request: RefinePausePointsRequest):
|
|||
|
||||
except Exception as e:
|
||||
logger.error(f"Pause point refinement failed: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
raise HTTPException(status_code=500, detail=str(e)) from None
|
||||
|
||||
|
||||
# Startup event to pre-load Whisper model
|
||||
|
|
|
|||
|
|
@ -1,5 +1,15 @@
|
|||
import threading
|
||||
import time
|
||||
|
||||
from celery import Celery
|
||||
from celery.signals import task_failure, task_retry, task_success
|
||||
from celery.signals import (
|
||||
task_failure,
|
||||
task_prerun,
|
||||
task_received,
|
||||
task_retry,
|
||||
task_success,
|
||||
worker_ready,
|
||||
)
|
||||
|
||||
from ..core.config import settings
|
||||
from ..core.logging import get_logger
|
||||
|
|
@ -49,13 +59,6 @@ def test_task(message="test"):
|
|||
return f"Test task completed: {message}"
|
||||
|
||||
|
||||
# Add task received handler for debugging
|
||||
import threading
|
||||
import time
|
||||
|
||||
from celery.signals import task_prerun, task_received, worker_ready
|
||||
|
||||
|
||||
@worker_ready.connect
|
||||
def worker_ready_handler(sender=None, **kwargs):
|
||||
"""Log when worker is ready and start heartbeat"""
|
||||
|
|
|
|||
|
|
@ -272,7 +272,7 @@ async def _async_rerender_accessible_video(
|
|||
|
||||
# Validate VTT cue count matches MP3 count
|
||||
vtt_cues = VTTParser.parse(ad_vtt_content)
|
||||
downloaded_indices = set(idx for idx, _ in ad_segments)
|
||||
downloaded_indices = {idx for idx, _ in ad_segments}
|
||||
if len(vtt_cues) != len(ad_segments):
|
||||
missing_indices = set(range(len(vtt_cues))) - downloaded_indices
|
||||
logger.warning(
|
||||
|
|
|
|||
|
|
@ -255,7 +255,9 @@ async def _async_translate_and_synthesize(job_id: str, languages: list[str] | No
|
|||
lang_out["sdh_captions_vtt_gcs"] = sdh_gcs_uri
|
||||
|
||||
try:
|
||||
from ..services.descriptive_transcript import generate_descriptive_transcript
|
||||
from ..services.descriptive_transcript import (
|
||||
generate_descriptive_transcript,
|
||||
)
|
||||
transcript_text = generate_descriptive_transcript(translated_captions, translated_ad)
|
||||
if transcript_text:
|
||||
transcript_gcs_uri = await upload_vtt_to_gcs(
|
||||
|
|
@ -583,7 +585,6 @@ async def _generate_language_tts(job_id: str, language: str, lang_output: dict,
|
|||
)
|
||||
|
||||
# Preflight budget check before dispatching TTS
|
||||
tts_provider = tts_preferences.get("provider", "gemini")
|
||||
from .tts_synthesis import _TTS_MODEL_STRINGS
|
||||
tts_model_key = tts_preferences.get("model", "flash")
|
||||
await cost_tracker.aio_preflight(
|
||||
|
|
@ -646,7 +647,7 @@ async def _generate_language_tts(job_id: str, language: str, lang_output: dict,
|
|||
cue_index=0,
|
||||
cue_text="",
|
||||
api_response_info=str(e)
|
||||
)
|
||||
) from e
|
||||
|
||||
# Handle case where results contain exceptions (shouldn't happen with new task design, but safety net)
|
||||
processed_results = []
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ def synthesize_cue_task(
|
|||
f"Retrying TTS cue {cue_index} in {delay:.1f}s "
|
||||
f"(attempt {self.request.retries + 2}/{self.max_retries + 1})"
|
||||
)
|
||||
raise self.retry(exc=e, countdown=delay)
|
||||
raise self.retry(exc=e, countdown=delay) from e
|
||||
else:
|
||||
# Max retries exhausted - return failure result instead of raising
|
||||
logger.error(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue