from __future__ import annotations from collections.abc import Sequence from pathlib import Path from typing import Any from app.core.exceptions import InputError from app.models.media import InputMedia, OperationResult from app.operations.common import ( IMAGE_FORMATS, execute, format_param, output_path, require_inputs, video_codecs, ) from app.services.ffmpeg_service import FFmpegService def _overlay(params: dict[str, Any]) -> tuple[str, dict[str, Any]]: position = str(params.get("position", "bottom-right")) positions = { "top-left": "10:10", "top-right": "W-w-10:10", "bottom-left": "10:H-h-10", "bottom-right": "W-w-10:H-h-10", "center": "(W-w)/2:(H-h)/2", } if position not in positions: raise InputError(f"position must be one of: {', '.join(positions)}") try: opacity = float(params.get("opacity", 1.0)) scale = float(params.get("watermark_scale", 1.0)) except (TypeError, ValueError) as exc: raise InputError("opacity and watermark_scale must be numbers") from exc if not 0 <= opacity <= 1 or not 0.01 <= scale <= 10: raise InputError("opacity must be 0..1 and watermark_scale must be 0.01..10") filter_graph = f"[1:v]format=rgba,colorchannelmixer=aa={opacity},scale=iw*{scale}:ih*{scale}[wm];[0:v][wm]overlay={positions[position]}[v]" return filter_graph, {"position": position, "opacity": opacity, "scale": scale} async def watermark_video( ffmpeg: FFmpegService, inputs: Sequence[InputMedia], params: dict[str, Any], output_dir: Path ) -> OperationResult: require_inputs(inputs, 2) graph, metadata = _overlay(params) output = output_path(output_dir, "watermarked", "mp4") args = [ "-i", inputs[0].temp_path, "-i", inputs[1].temp_path, "-filter_complex", graph, "-map", "[v]", "-map", "0:a?", *video_codecs("mp4"), output, ] return await execute(ffmpeg, args, output, "video.watermark", metadata) async def overlay_video( ffmpeg: FFmpegService, inputs: Sequence[InputMedia], params: dict[str, Any], output_dir: Path ) -> OperationResult: result = await watermark_video(ffmpeg, inputs, params, output_dir) result.metadata["operation"] = "video.overlay" return result async def watermark_image( ffmpeg: FFmpegService, inputs: Sequence[InputMedia], params: dict[str, Any], output_dir: Path ) -> OperationResult: require_inputs(inputs, 2) graph, metadata = _overlay(params) extension = format_param(params, "png", IMAGE_FORMATS) output = output_path(output_dir, "watermarked", extension) args = [ "-i", inputs[0].temp_path, "-i", inputs[1].temp_path, "-filter_complex", graph, "-map", "[v]", "-frames:v", "1", output, ] return await execute(ffmpeg, args, output, "image.watermark", metadata) async def overlay_image( ffmpeg: FFmpegService, inputs: Sequence[InputMedia], params: dict[str, Any], output_dir: Path ) -> OperationResult: result = await watermark_image(ffmpeg, inputs, params, output_dir) result.metadata["operation"] = "image.overlay" return result