Spaces:
Running
Running
| 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 execute, output_path, require_inputs, video_codecs | |
| from app.services.ffmpeg_service import FFmpegService | |
| async def rotate_video( | |
| ffmpeg: FFmpegService, inputs: Sequence[InputMedia], params: dict[str, Any], output_dir: Path | |
| ) -> OperationResult: | |
| require_inputs(inputs) | |
| try: | |
| angle = int(params.get("angle", 90)) % 360 | |
| except (TypeError, ValueError) as exc: | |
| raise InputError("'angle' must be an integer") from exc | |
| filters = {90: "transpose=1", 180: "hflip,vflip", 270: "transpose=2", 0: "null"} | |
| if angle not in filters: | |
| raise InputError("Rotation angle must be 0, 90, 180, or 270 degrees") | |
| output = output_path(output_dir, "rotated", "mp4") | |
| return await execute( | |
| ffmpeg, | |
| [ | |
| "-i", | |
| inputs[0].temp_path, | |
| "-vf", | |
| filters[angle], | |
| "-metadata:s:v:0", | |
| "rotate=0", | |
| *video_codecs("mp4"), | |
| output, | |
| ], | |
| output, | |
| "video.rotate", | |
| {"angle": angle}, | |
| ) | |
| async def reverse_video( | |
| ffmpeg: FFmpegService, inputs: Sequence[InputMedia], params: dict[str, Any], output_dir: Path | |
| ) -> OperationResult: | |
| require_inputs(inputs) | |
| output = output_path(output_dir, "reversed", "mp4") | |
| has_audio = bool(inputs[0].metadata.get("audio_streams", True)) | |
| args: list[str | Path] = ["-i", inputs[0].temp_path, "-vf", "reverse"] | |
| if has_audio: | |
| args += ["-af", "areverse"] | |
| args += [*video_codecs("mp4"), output] | |
| return await execute(ffmpeg, args, output, "video.reverse") | |
| async def change_speed( | |
| ffmpeg: FFmpegService, inputs: Sequence[InputMedia], params: dict[str, Any], output_dir: Path | |
| ) -> OperationResult: | |
| from app.operations.common import atempo_chain | |
| require_inputs(inputs) | |
| try: | |
| factor = float(params.get("factor", 2.0)) | |
| except (TypeError, ValueError) as exc: | |
| raise InputError("'factor' must be a number") from exc | |
| if not 0.125 <= factor <= 8: | |
| raise InputError("Speed factor must be between 0.125 and 8") | |
| output = output_path(output_dir, "speed", "mp4") | |
| has_audio = bool(inputs[0].metadata.get("audio_streams", True)) | |
| args: list[str | Path] = ["-i", inputs[0].temp_path, "-vf", f"setpts=PTS/{factor:.8g}"] | |
| if has_audio: | |
| args += ["-af", atempo_chain(factor)] | |
| args += [*video_codecs("mp4"), output] | |
| return await execute(ffmpeg, args, output, "video.speed", {"factor": factor}) | |
| async def slow_motion( | |
| ffmpeg: FFmpegService, | |
| inputs: Sequence[InputMedia], | |
| params: dict[str, Any], | |
| output_dir: Path, | |
| ) -> OperationResult: | |
| result = await change_speed( | |
| ffmpeg, inputs, {"factor": params.get("factor", 0.5), **params}, output_dir | |
| ) | |
| result.metadata["operation"] = "video.slow_motion" | |
| return result | |
| async def change_fps( | |
| ffmpeg: FFmpegService, inputs: Sequence[InputMedia], params: dict[str, Any], output_dir: Path | |
| ) -> OperationResult: | |
| require_inputs(inputs) | |
| try: | |
| fps = float(params.get("fps", 30)) | |
| except (TypeError, ValueError) as exc: | |
| raise InputError("'fps' must be a number") from exc | |
| if not 1 <= fps <= 240: | |
| raise InputError("FPS must be between 1 and 240") | |
| output = output_path(output_dir, "fps", "mp4") | |
| return await execute( | |
| ffmpeg, | |
| ["-i", inputs[0].temp_path, "-vf", f"fps={fps:.6g}", *video_codecs("mp4"), output], | |
| output, | |
| "video.fps", | |
| {"fps": fps}, | |
| ) | |
| async def change_bitrate( | |
| ffmpeg: FFmpegService, inputs: Sequence[InputMedia], params: dict[str, Any], output_dir: Path | |
| ) -> OperationResult: | |
| require_inputs(inputs) | |
| bitrate = str(params.get("bitrate", "1500k")) | |
| normalized = bitrate.lower().removesuffix("k").removesuffix("m") | |
| if not normalized.replace(".", "", 1).isdigit(): | |
| raise InputError("Invalid bitrate; examples: 1500k or 2M") | |
| output = output_path(output_dir, "bitrate", "mp4") | |
| args = [ | |
| "-i", | |
| inputs[0].temp_path, | |
| "-c:v", | |
| "libx264", | |
| "-b:v", | |
| bitrate, | |
| "-maxrate", | |
| bitrate, | |
| "-bufsize", | |
| bitrate, | |
| "-c:a", | |
| "aac", | |
| "-b:a", | |
| "128k", | |
| "-movflags", | |
| "+faststart", | |
| output, | |
| ] | |
| return await execute(ffmpeg, args, output, "video.bitrate", {"bitrate": bitrate}) | |