From ca312d48faef7cc5d839d589e83ac0bc74685f3d Mon Sep 17 00:00:00 2001 From: Vadym Samoilenko Date: Wed, 13 May 2026 17:13:08 +0100 Subject: [PATCH] =?UTF-8?q?chore(lint):=20fix=20all=20ruff=20errors=20?= =?UTF-8?q?=E2=80=94=200=20warnings=20remaining?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- backend/app/api/v1/routes_admin.py | 4 +-- backend/app/api/v1/routes_auth.py | 6 ++-- backend/app/api/v1/routes_clients.py | 6 ++-- backend/app/api/v1/routes_files.py | 2 +- backend/app/api/v1/routes_jobs.py | 24 +++++++-------- backend/app/api/v1/routes_language_qc.py | 7 +---- backend/app/api/v1/routes_vtt_versions.py | 2 +- backend/app/core/security.py | 2 +- backend/app/middleware/rate_limiting.py | 2 +- backend/app/middleware/validation.py | 6 ++-- backend/app/migrations/migrator.py | 8 ++--- backend/app/migrations/run.py | 2 +- backend/app/models/audit_log.py | 6 ++-- backend/app/models/job.py | 6 ++-- backend/app/models/job_brief.py | 4 +-- backend/app/models/organization.py | 4 +-- backend/app/models/user.py | 6 ++-- backend/app/schemas/accessible_video.py | 4 +-- backend/app/services/audit_logger.py | 2 +- backend/app/services/cloud_run_dispatch.py | 2 +- backend/app/services/cost_tracker.py | 12 +++++--- backend/app/services/ffmpeg_http_service.py | 12 ++++---- backend/app/services/gcs.py | 12 ++++---- backend/app/services/gemini.py | 8 ++--- backend/app/services/microsoft_auth.py | 10 +++---- backend/app/services/secrets_manager.py | 30 +++++++++---------- backend/app/services/tts.py | 4 +-- backend/app/services/video_renderer.py | 14 ++++----- backend/app/services/whisper_http_service.py | 6 ++-- backend/app/tasks/__init__.py | 19 +++++++----- .../app/tasks/rerender_accessible_video.py | 2 +- backend/app/tasks/translate_and_synthesize.py | 7 +++-- backend/app/tasks/tts_synthesis.py | 2 +- 33 files changed, 120 insertions(+), 123 deletions(-) diff --git a/backend/app/api/v1/routes_admin.py b/backend/app/api/v1/routes_admin.py index 0c842da..7c802ce 100644 --- a/backend/app/api/v1/routes_admin.py +++ b/backend/app/api/v1/routes_admin.py @@ -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)} diff --git a/backend/app/api/v1/routes_auth.py b/backend/app/api/v1/routes_auth.py index 611f382..60e0d27 100644 --- a/backend/app/api/v1/routes_auth.py +++ b/backend/app/api/v1/routes_auth.py @@ -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) diff --git a/backend/app/api/v1/routes_clients.py b/backend/app/api/v1/routes_clients.py index 1cd91bc..75bb6d9 100644 --- a/backend/app/api/v1/routes_clients.py +++ b/backend/app/api/v1/routes_clients.py @@ -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() diff --git a/backend/app/api/v1/routes_files.py b/backend/app/api/v1/routes_files.py index 072717d..7ee5c53 100644 --- a/backend/app/api/v1/routes_files.py +++ b/backend/app/api/v1/routes_files.py @@ -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 diff --git a/backend/app/api/v1/routes_jobs.py b/backend/app/api/v1/routes_jobs.py index 2576c8b..27fad87 100644 --- a/backend/app/api/v1/routes_jobs.py +++ b/backend/app/api/v1/routes_jobs.py @@ -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 { diff --git a/backend/app/api/v1/routes_language_qc.py b/backend/app/api/v1/routes_language_qc.py index 537e0e0..e3c60c0 100644 --- a/backend/app/api/v1/routes_language_qc.py +++ b/backend/app/api/v1/routes_language_qc.py @@ -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()} diff --git a/backend/app/api/v1/routes_vtt_versions.py b/backend/app/api/v1/routes_vtt_versions.py index 0011df2..e01f96c 100644 --- a/backend/app/api/v1/routes_vtt_versions.py +++ b/backend/app/api/v1/routes_vtt_versions.py @@ -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" diff --git a/backend/app/core/security.py b/backend/app/core/security.py index cf44b0b..cf05aed 100644 --- a/backend/app/core/security.py +++ b/backend/app/core/security.py @@ -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 diff --git a/backend/app/middleware/rate_limiting.py b/backend/app/middleware/rate_limiting.py index fde3921..2b83189 100644 --- a/backend/app/middleware/rate_limiting.py +++ b/backend/app/middleware/rate_limiting.py @@ -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) """ diff --git a/backend/app/middleware/validation.py b/backend/app/middleware/validation.py index 04dc30f..657b824 100644 --- a/backend/app/middleware/validation.py +++ b/backend/app/middleware/validation.py @@ -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.""" diff --git a/backend/app/migrations/migrator.py b/backend/app/migrations/migrator.py index e1ab64d..6c3d506 100644 --- a/backend/app/migrations/migrator.py +++ b/backend/app/migrations/migrator.py @@ -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. """ diff --git a/backend/app/migrations/run.py b/backend/app/migrations/run.py index de16fc5..addd60a 100644 --- a/backend/app/migrations/run.py +++ b/backend/app/migrations/run.py @@ -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 diff --git a/backend/app/models/audit_log.py b/backend/app/models/audit_log.py index 8d7c150..7c7f6f6 100644 --- a/backend/app/models/audit_log.py +++ b/backend/app/models/audit_log.py @@ -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 diff --git a/backend/app/models/job.py b/backend/app/models/job.py index e8a1d10..57fe8d1 100644 --- a/backend/app/models/job.py +++ b/backend/app/models/job.py @@ -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 diff --git a/backend/app/models/job_brief.py b/backend/app/models/job_brief.py index 4a661a6..a2efcc6 100644 --- a/backend/app/models/job_brief.py +++ b/backend/app/models/job_brief.py @@ -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" diff --git a/backend/app/models/organization.py b/backend/app/models/organization.py index 5b39f32..e1de660 100644 --- a/backend/app/models/organization.py +++ b/backend/app/models/organization.py @@ -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" diff --git a/backend/app/models/user.py b/backend/app/models/user.py index 6490965..b3e5b19 100644 --- a/backend/app/models/user.py +++ b/backend/app/models/user.py @@ -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" diff --git a/backend/app/schemas/accessible_video.py b/backend/app/schemas/accessible_video.py index 18ad81d..c9c9783 100644 --- a/backend/app/schemas/accessible_video.py +++ b/backend/app/schemas/accessible_video.py @@ -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" diff --git a/backend/app/services/audit_logger.py b/backend/app/services/audit_logger.py index 81378de..1ca34c6 100644 --- a/backend/app/services/audit_logger.py +++ b/backend/app/services/audit_logger.py @@ -51,7 +51,7 @@ class AuditLogger: ) -> str: """ Log an audit event. - + Returns: The ID of the created audit log entry. """ diff --git a/backend/app/services/cloud_run_dispatch.py b/backend/app/services/cloud_run_dispatch.py index a7a4436..2668d45 100644 --- a/backend/app/services/cloud_run_dispatch.py +++ b/backend/app/services/cloud_run_dispatch.py @@ -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 diff --git a/backend/app/services/cost_tracker.py b/backend/app/services/cost_tracker.py index 7986ee3..a6a4385 100644 --- a/backend/app/services/cost_tracker.py +++ b/backend/app/services/cost_tracker.py @@ -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", diff --git a/backend/app/services/ffmpeg_http_service.py b/backend/app/services/ffmpeg_http_service.py index ffde5bf..2a8aba7 100644 --- a/backend/app/services/ffmpeg_http_service.py +++ b/backend/app/services/ffmpeg_http_service.py @@ -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 diff --git a/backend/app/services/gcs.py b/backend/app/services/gcs.py index 7594d80..db4ab33 100644 --- a/backend/app/services/gcs.py +++ b/backend/app/services/gcs.py @@ -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""" diff --git a/backend/app/services/gemini.py b/backend/app/services/gemini.py index ac3d878..75b6947 100644 --- a/backend/app/services/gemini.py +++ b/backend/app/services/gemini.py @@ -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 diff --git a/backend/app/services/microsoft_auth.py b/backend/app/services/microsoft_auth.py index 78139f8..07a862c 100644 --- a/backend/app/services/microsoft_auth.py +++ b/backend/app/services/microsoft_auth.py @@ -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 diff --git a/backend/app/services/secrets_manager.py b/backend/app/services/secrets_manager.py index f05e125..84bee71 100644 --- a/backend/app/services/secrets_manager.py +++ b/backend/app/services/secrets_manager.py @@ -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 diff --git a/backend/app/services/tts.py b/backend/app/services/tts.py index 200a878..1f0a71a 100644 --- a/backend/app/services/tts.py +++ b/backend/app/services/tts.py @@ -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"] diff --git a/backend/app/services/video_renderer.py b/backend/app/services/video_renderer.py index aeaa280..0a9622e 100644 --- a/backend/app/services/video_renderer.py +++ b/backend/app/services/video_renderer.py @@ -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 diff --git a/backend/app/services/whisper_http_service.py b/backend/app/services/whisper_http_service.py index 354c899..f8f2015 100644 --- a/backend/app/services/whisper_http_service.py +++ b/backend/app/services/whisper_http_service.py @@ -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 diff --git a/backend/app/tasks/__init__.py b/backend/app/tasks/__init__.py index 577f353..1c578ea 100644 --- a/backend/app/tasks/__init__.py +++ b/backend/app/tasks/__init__.py @@ -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""" diff --git a/backend/app/tasks/rerender_accessible_video.py b/backend/app/tasks/rerender_accessible_video.py index 5a35d11..b0dd9dc 100644 --- a/backend/app/tasks/rerender_accessible_video.py +++ b/backend/app/tasks/rerender_accessible_video.py @@ -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( diff --git a/backend/app/tasks/translate_and_synthesize.py b/backend/app/tasks/translate_and_synthesize.py index f94e903..5f68523 100644 --- a/backend/app/tasks/translate_and_synthesize.py +++ b/backend/app/tasks/translate_and_synthesize.py @@ -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 = [] diff --git a/backend/app/tasks/tts_synthesis.py b/backend/app/tasks/tts_synthesis.py index 658b4fc..53a28b5 100644 --- a/backend/app/tasks/tts_synthesis.py +++ b/backend/app/tasks/tts_synthesis.py @@ -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(