Spaces:
Running
Running
| from __future__ import annotations | |
| import re | |
| 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 audio_codec, execute, output_path, require_inputs | |
| from app.services.ffmpeg_service import FFmpegService | |
| from app.services.validator import bounded_number | |
| async def extract_audio( | |
| ffmpeg: FFmpegService, inputs: Sequence[InputMedia], params: dict[str, Any], output_dir: Path | |
| ) -> OperationResult: | |
| require_inputs(inputs) | |
| extension = str(params.get("format", "mp3")).lower().lstrip(".") | |
| if extension not in {"mp3", "wav", "aac", "m4a", "flac", "ogg", "opus"}: | |
| raise InputError("Unsupported extracted audio format") | |
| output = output_path(output_dir, "audio", extension) | |
| return await execute( | |
| ffmpeg, | |
| ["-i", inputs[0].temp_path, "-vn", *audio_codec(extension), output], | |
| output, | |
| "audio.extract", | |
| {"format": extension}, | |
| ) | |
| async def replace_audio( | |
| ffmpeg: FFmpegService, inputs: Sequence[InputMedia], params: dict[str, Any], output_dir: Path | |
| ) -> OperationResult: | |
| require_inputs(inputs, 2) | |
| output = output_path(output_dir, "replaced-audio", "mp4") | |
| args = [ | |
| "-i", | |
| inputs[0].temp_path, | |
| "-i", | |
| inputs[1].temp_path, | |
| "-map", | |
| "0:v:0", | |
| "-map", | |
| "1:a:0", | |
| "-c:v", | |
| "copy", | |
| "-c:a", | |
| "aac", | |
| "-shortest", | |
| "-movflags", | |
| "+faststart", | |
| output, | |
| ] | |
| return await execute(ffmpeg, args, output, "video.replace_audio") | |
| async def remove_audio( | |
| ffmpeg: FFmpegService, inputs: Sequence[InputMedia], params: dict[str, Any], output_dir: Path | |
| ) -> OperationResult: | |
| require_inputs(inputs) | |
| output = output_path(output_dir, "muted", "mp4") | |
| return await execute( | |
| ffmpeg, | |
| ["-i", inputs[0].temp_path, "-an", "-c:v", "copy", output], | |
| output, | |
| "video.remove_audio", | |
| ) | |
| async def mute_video( | |
| ffmpeg: FFmpegService, inputs: Sequence[InputMedia], params: dict[str, Any], output_dir: Path | |
| ) -> OperationResult: | |
| result = await remove_audio(ffmpeg, inputs, params, output_dir) | |
| result.metadata["operation"] = "video.mute" | |
| return result | |
| async def fade_audio( | |
| ffmpeg: FFmpegService, inputs: Sequence[InputMedia], params: dict[str, Any], output_dir: Path | |
| ) -> OperationResult: | |
| require_inputs(inputs) | |
| start = bounded_number(params, "start", 0, 0, 86_400) | |
| duration = bounded_number(params, "duration", 3, 0.01, 86_400) | |
| fade_type = str(params.get("type", "in")) | |
| if fade_type not in {"in", "out"}: | |
| raise InputError("Fade requires type in/out, non-negative start, and positive duration") | |
| output = output_path(output_dir, "faded", "mp3") | |
| filter_name = "afade=t=in" if fade_type == "in" else "afade=t=out" | |
| return await execute( | |
| ffmpeg, | |
| [ | |
| "-i", | |
| inputs[0].temp_path, | |
| "-af", | |
| f"{filter_name}:st={start}:d={duration}", | |
| *audio_codec("mp3"), | |
| output, | |
| ], | |
| output, | |
| "audio.fade", | |
| ) | |
| async def set_volume( | |
| ffmpeg: FFmpegService, inputs: Sequence[InputMedia], params: dict[str, Any], output_dir: Path | |
| ) -> OperationResult: | |
| require_inputs(inputs) | |
| volume = bounded_number(params, "volume", 1, 0, 10) | |
| output = output_path(output_dir, "volume", "mp3") | |
| return await execute( | |
| ffmpeg, | |
| ["-i", inputs[0].temp_path, "-af", f"volume={volume}", *audio_codec("mp3"), output], | |
| output, | |
| "audio.volume", | |
| {"volume": volume}, | |
| ) | |
| async def remove_silence( | |
| ffmpeg: FFmpegService, inputs: Sequence[InputMedia], params: dict[str, Any], output_dir: Path | |
| ) -> OperationResult: | |
| require_inputs(inputs) | |
| threshold = str(params.get("threshold", "-45dB")) | |
| if not re.fullmatch(r"-?\d+(?:\.\d+)?dB", threshold): | |
| raise InputError("threshold must be a decibel value such as -45dB") | |
| output = output_path(output_dir, "no-silence", "mp3") | |
| return await execute( | |
| ffmpeg, | |
| [ | |
| "-i", | |
| inputs[0].temp_path, | |
| "-af", | |
| f"silenceremove=start_periods=1:start_duration=0.2:start_threshold={threshold}:stop_periods=-1:stop_duration=0.2:stop_threshold={threshold}", | |
| *audio_codec("mp3"), | |
| output, | |
| ], | |
| output, | |
| "audio.remove_silence", | |
| ) | |
| async def noise_reduction( | |
| ffmpeg: FFmpegService, inputs: Sequence[InputMedia], params: dict[str, Any], output_dir: Path | |
| ) -> OperationResult: | |
| require_inputs(inputs) | |
| output = output_path(output_dir, "denoised", "mp3") | |
| return await execute( | |
| ffmpeg, | |
| ["-i", inputs[0].temp_path, "-af", "afftdn", *audio_codec("mp3"), output], | |
| output, | |
| "audio.noise_reduction", | |
| ) | |