Files
2026-03-17 18:11:23 +03:00

130 lines
4.4 KiB
Python

from __future__ import annotations
import uuid
import httpx
from sqlalchemy.ext.asyncio import AsyncSession
from cpv3.infrastructure.settings import get_settings
from cpv3.modules.captions.models import CaptionPreset
from cpv3.modules.captions.repository import CaptionPresetRepository
from cpv3.modules.captions.schemas import (
CaptionPresetCreate,
CaptionPresetUpdate,
CaptionStyleConfig,
)
from cpv3.modules.transcription.schemas import Document
ERROR_PRESET_NOT_FOUND = "Пресет субтитров не найден"
ERROR_PRESET_FORBIDDEN = "Нельзя изменять чужой или системный пресет"
# ---------------------------------------------------------------------------
# Caption rendering (calls Remotion service)
# ---------------------------------------------------------------------------
async def generate_captions(
*,
video_s3_path: str,
folder: str,
transcription: Document,
style_config: dict | None = None,
callback_url: str | None = None,
render_id: str | None = None,
) -> str:
"""Generate captions for a video using the Remotion service.
Returns render_id (async mode with callback_url) or S3 output path (sync fallback).
"""
settings = get_settings()
payload: dict = {
"folder": folder,
"videoSrc": video_s3_path,
"transcription": transcription.model_dump(),
}
if style_config is not None:
payload["styleConfig"] = style_config
if callback_url is not None:
payload["callbackUrl"] = callback_url
if render_id is not None:
payload["renderId"] = render_id
timeout = 30.0 if callback_url else 300.0
async with httpx.AsyncClient(timeout=timeout) as client:
resp = await client.post(f"{settings.remotion_service_url}/api/render", json=payload)
resp.raise_for_status()
data = resp.json()
# Async mode: Remotion returns { renderId, status: "queued" }
if callback_url and "renderId" in data:
return str(data["renderId"])
# Sync fallback: Remotion returns { output: "s3/path" }
if isinstance(data, dict) and "output" in data:
return str(data["output"])
raise RuntimeError("Unexpected response from remotion service")
# ---------------------------------------------------------------------------
# Preset service
# ---------------------------------------------------------------------------
class CaptionPresetService:
"""Business logic for caption preset CRUD with ownership checks."""
def __init__(self, session: AsyncSession) -> None:
self._repo = CaptionPresetRepository(session)
async def list_presets(self, *, user_id: uuid.UUID) -> list[CaptionPreset]:
return await self._repo.list_all_for_user(user_id)
async def get_preset(self, *, preset_id: uuid.UUID) -> CaptionPreset:
preset = await self._repo.get_by_id(preset_id)
if preset is None:
raise ValueError(ERROR_PRESET_NOT_FOUND)
return preset
async def create_preset(
self, *, user_id: uuid.UUID, data: CaptionPresetCreate
) -> CaptionPreset:
return await self._repo.create(user_id=user_id, data=data)
async def update_preset(
self,
*,
preset_id: uuid.UUID,
user_id: uuid.UUID,
data: CaptionPresetUpdate,
) -> CaptionPreset:
preset = await self.get_preset(preset_id=preset_id)
if preset.is_system or preset.user_id != user_id:
raise PermissionError(ERROR_PRESET_FORBIDDEN)
return await self._repo.update(preset, data)
async def delete_preset(self, *, preset_id: uuid.UUID, user_id: uuid.UUID) -> None:
preset = await self.get_preset(preset_id=preset_id)
if preset.is_system or preset.user_id != user_id:
raise PermissionError(ERROR_PRESET_FORBIDDEN)
await self._repo.deactivate(preset)
async def resolve_style_config(
self,
*,
preset_id: uuid.UUID | None = None,
inline_config: dict | None = None,
) -> dict | None:
"""Resolve a style config from preset_id or inline override."""
if inline_config is not None:
# Validate by parsing, then return as dict
cfg = CaptionStyleConfig.model_validate(inline_config)
return cfg.model_dump(mode="json")
if preset_id is not None:
preset = await self.get_preset(preset_id=preset_id)
return preset.style_config
return None