Spaces:
Running
Running
| from __future__ import annotations | |
| import asyncio | |
| import shutil | |
| import zipfile | |
| from collections.abc import Sequence | |
| from pathlib import Path | |
| from typing import Any | |
| from app.core.exceptions import ProcessingError | |
| from app.models.media import InputMedia, OperationResult | |
| from app.operations.common import execute, output_path, require_inputs, video_codecs | |
| from app.services.ffmpeg_service import FFmpegService | |
| from app.services.validator import bounded_number, positive_int | |
| async def thumbnail( | |
| ffmpeg: FFmpegService, inputs: Sequence[InputMedia], params: dict[str, Any], output_dir: Path | |
| ) -> OperationResult: | |
| require_inputs(inputs) | |
| timestamp = bounded_number(params, "timestamp", 0, 0, 86_400) | |
| output = output_path(output_dir, "thumbnail", "jpg") | |
| quality = min(31, positive_int(params, "quality", 3)) | |
| return await execute( | |
| ffmpeg, | |
| [ | |
| "-ss", | |
| str(timestamp), | |
| "-i", | |
| inputs[0].temp_path, | |
| "-frames:v", | |
| "1", | |
| "-q:v", | |
| str(quality), | |
| output, | |
| ], | |
| output, | |
| "video.thumbnail", | |
| {"timestamp": timestamp}, | |
| ) | |
| async def extract_frames( | |
| ffmpeg: FFmpegService, inputs: Sequence[InputMedia], params: dict[str, Any], output_dir: Path | |
| ) -> OperationResult: | |
| require_inputs(inputs) | |
| fps = bounded_number(params, "fps", 1, 0.01, 60) | |
| frame_dir = output_dir / "frames" | |
| frame_dir.mkdir(parents=True, exist_ok=True) | |
| frame_pattern = frame_dir / "frame_%06d.jpg" | |
| args: list[str | Path] = ["-i", inputs[0].temp_path, "-vf", f"fps={fps}", "-q:v", "3"] | |
| maximum = params.get("max_frames") | |
| if maximum is not None: | |
| maximum_value = positive_int(params, "max_frames", 1) | |
| args += ["-frames:v", str(maximum_value)] | |
| args += [frame_pattern] | |
| await ffmpeg.run(args, operation="video.extract_frames") | |
| frames = sorted(frame_dir.glob("frame_*.jpg")) | |
| if not frames: | |
| raise ProcessingError("No frames were extracted") | |
| archive = output_path(output_dir, "frames", "zip") | |
| await asyncio.to_thread(_archive_frames, archive, frames) | |
| await asyncio.to_thread(shutil.rmtree, frame_dir, True) | |
| return OperationResult( | |
| path=archive, | |
| filename=archive.name, | |
| mime_type="application/zip", | |
| metadata={"frames": len(frames), "fps": fps}, | |
| ) | |
| def _archive_frames(archive: Path, frames: list[Path]) -> None: | |
| with zipfile.ZipFile(archive, "w", zipfile.ZIP_DEFLATED) as zip_file: | |
| for frame in frames: | |
| zip_file.write(frame, frame.name) | |
| async def generate_gif( | |
| ffmpeg: FFmpegService, inputs: Sequence[InputMedia], params: dict[str, Any], output_dir: Path | |
| ) -> OperationResult: | |
| require_inputs(inputs) | |
| fps = bounded_number(params, "fps", 10, 1, 30) | |
| width = positive_int(params, "width", 480) | |
| output = output_path(output_dir, "animation", "gif") | |
| palette = output_dir / "palette.png" | |
| await ffmpeg.run( | |
| [ | |
| "-i", | |
| inputs[0].temp_path, | |
| "-vf", | |
| f"fps={fps},scale={width}:-1:flags=lanczos,palettegen", | |
| palette, | |
| ], | |
| operation="video.gif.palette", | |
| ) | |
| await ffmpeg.run( | |
| [ | |
| "-i", | |
| inputs[0].temp_path, | |
| "-i", | |
| palette, | |
| "-lavfi", | |
| f"fps={fps},scale={width}:-1:flags=lanczos[x];[x][1:v]paletteuse", | |
| output, | |
| ], | |
| operation="video.gif", | |
| ) | |
| palette.unlink(missing_ok=True) | |
| if not output.is_file() or output.stat().st_size == 0: | |
| raise ProcessingError("GIF generation did not produce an output") | |
| return OperationResult( | |
| path=output, | |
| filename=output.name, | |
| mime_type="image/gif", | |
| metadata={"fps": fps, "width": width}, | |
| ) | |
| async def blur_video( | |
| ffmpeg: FFmpegService, inputs: Sequence[InputMedia], params: dict[str, Any], output_dir: Path | |
| ) -> OperationResult: | |
| require_inputs(inputs) | |
| strength = min(100, positive_int(params, "strength", 5)) | |
| output = output_path(output_dir, "blurred", "mp4") | |
| return await execute( | |
| ffmpeg, | |
| ["-i", inputs[0].temp_path, "-vf", f"boxblur={strength}:1", *video_codecs("mp4"), output], | |
| output, | |
| "video.blur", | |
| {"strength": strength}, | |
| ) | |
| async def sharpen_video( | |
| ffmpeg: FFmpegService, inputs: Sequence[InputMedia], params: dict[str, Any], output_dir: Path | |
| ) -> OperationResult: | |
| require_inputs(inputs) | |
| amount = bounded_number(params, "amount", 1.0, 0, 5) | |
| output = output_path(output_dir, "sharpened", "mp4") | |
| return await execute( | |
| ffmpeg, | |
| [ | |
| "-i", | |
| inputs[0].temp_path, | |
| "-vf", | |
| f"unsharp=5:5:{amount}:5:5:0", | |
| *video_codecs("mp4"), | |
| output, | |
| ], | |
| output, | |
| "video.sharpen", | |
| {"amount": amount}, | |
| ) | |
| async def denoise_video( | |
| ffmpeg: FFmpegService, inputs: Sequence[InputMedia], params: dict[str, Any], output_dir: Path | |
| ) -> OperationResult: | |
| require_inputs(inputs) | |
| output = output_path(output_dir, "denoised", "mp4") | |
| return await execute( | |
| ffmpeg, | |
| ["-i", inputs[0].temp_path, "-vf", "hqdn3d", *video_codecs("mp4"), output], | |
| output, | |
| "video.denoise", | |
| ) | |