Spaces:
Sleeping
Sleeping
| 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, | |
| VIDEO_FORMATS, | |
| even, | |
| execute, | |
| format_param, | |
| output_path, | |
| require_inputs, | |
| video_codecs, | |
| ) | |
| from app.services.ffmpeg_service import FFmpegService | |
| from app.services.validator import positive_int | |
| def _scale(params: dict[str, Any]) -> tuple[int, int, str]: | |
| width = even(positive_int(params, "width", 1280)) | |
| height = even(positive_int(params, "height", 720)) | |
| if width < 2 or height < 2: | |
| raise InputError("Output dimensions must be at least 2x2") | |
| fit = str(params.get("fit", "contain")) | |
| if fit == "fill": | |
| expression = f"scale={width}:{height}" | |
| elif fit == "cover": | |
| expression = ( | |
| f"scale={width}:{height}:force_original_aspect_ratio=increase,crop={width}:{height}" | |
| ) | |
| elif fit == "contain": | |
| expression = f"scale={width}:{height}:force_original_aspect_ratio=decrease,pad={width}:{height}:(ow-iw)/2:(oh-ih)/2" | |
| else: | |
| raise InputError("'fit' must be contain, cover, or fill") | |
| return width, height, expression | |
| async def resize_video( | |
| ffmpeg: FFmpegService, inputs: Sequence[InputMedia], params: dict[str, Any], output_dir: Path | |
| ) -> OperationResult: | |
| require_inputs(inputs) | |
| width, height, expression = _scale(params) | |
| extension = format_param(params, "mp4", VIDEO_FORMATS) | |
| output = output_path(output_dir, "resized", extension) | |
| return await execute( | |
| ffmpeg, | |
| ["-i", inputs[0].temp_path, "-vf", expression, *video_codecs(extension), output], | |
| output, | |
| "video.resize", | |
| {"width": width, "height": height}, | |
| ) | |
| async def scale_video( | |
| ffmpeg: FFmpegService, inputs: Sequence[InputMedia], params: dict[str, Any], output_dir: Path | |
| ) -> OperationResult: | |
| copied = {**params, "fit": params.get("fit", "fill")} | |
| result = await resize_video(ffmpeg, inputs, copied, output_dir) | |
| result.metadata["operation"] = "video.scale" | |
| return result | |
| async def pad_video( | |
| ffmpeg: FFmpegService, inputs: Sequence[InputMedia], params: dict[str, Any], output_dir: Path | |
| ) -> OperationResult: | |
| require_inputs(inputs) | |
| width = even(positive_int(params, "width", 1920)) | |
| height = even(positive_int(params, "height", 1080)) | |
| color = str(params.get("color", "black")) | |
| if not color.replace("#", "").isalnum(): | |
| raise InputError("Invalid pad color") | |
| output = output_path(output_dir, "padded", "mp4") | |
| vf = f"scale={width}:{height}:force_original_aspect_ratio=decrease,pad={width}:{height}:(ow-iw)/2:(oh-ih)/2:{color}" | |
| return await execute( | |
| ffmpeg, | |
| ["-i", inputs[0].temp_path, "-vf", vf, *video_codecs("mp4"), output], | |
| output, | |
| "video.pad", | |
| {"width": width, "height": height}, | |
| ) | |
| async def resize_image( | |
| ffmpeg: FFmpegService, inputs: Sequence[InputMedia], params: dict[str, Any], output_dir: Path | |
| ) -> OperationResult: | |
| require_inputs(inputs) | |
| width, height, expression = _scale(params) | |
| extension = format_param(params, "png", IMAGE_FORMATS) | |
| output = output_path(output_dir, "resized", extension) | |
| return await execute( | |
| ffmpeg, | |
| ["-i", inputs[0].temp_path, "-vf", expression, "-frames:v", "1", output], | |
| output, | |
| "image.resize", | |
| {"width": width, "height": height}, | |
| ) | |