Skip to main content
Glama
audio_processor.py6.55 kB
"""Audio processing domain logic (pure business logic, no I/O).""" from pathlib import Path import anyio from pydub import AudioSegment # type: ignore from ..constants import DEFAULT_MAX_FILE_SIZE_MB, DEFAULT_TTS_SAMPLE_RATE, SupportedChatWithAudioFormat from ..exceptions import AudioCompressionError, AudioConversionError class AudioProcessor: """Domain logic for audio processing operations. This class contains pure business logic for audio manipulation without any I/O operations (those are handled by infrastructure layer). """ @staticmethod async def convert_audio_format( audio_data: AudioSegment, target_format: SupportedChatWithAudioFormat, output_path: Path, ) -> bytes: """Convert audio to target format. Args: ---- audio_data: Loaded AudioSegment object. target_format: Target format ('mp3' or 'wav'). output_path: Path for temporary export. Returns: ------- bytes: Converted audio data. Raises: ------ AudioConversionError: If conversion fails. """ try: # Export to temporary path await anyio.to_thread.run_sync( lambda: audio_data.export( str(output_path), format=target_format, parameters=["-ac", "2"], ) ) # Read the converted file with open(output_path, "rb") as f: return f.read() except Exception as e: raise AudioConversionError(f"Audio conversion to {target_format} failed: {e}") from e @staticmethod async def compress_mp3( audio_data: AudioSegment, output_path: Path, target_sample_rate: int = DEFAULT_TTS_SAMPLE_RATE, ) -> bytes: """Compress MP3 audio by downsampling. Args: ---- audio_data: Loaded AudioSegment object. output_path: Path for temporary export. target_sample_rate: Target sample rate for compression. Returns: ------- bytes: Compressed audio data. Raises: ------ AudioCompressionError: If compression fails. """ try: original_frame_rate = audio_data.frame_rate print(f"[Compression] Original frame rate: {original_frame_rate}, converting to {target_sample_rate}.") await anyio.to_thread.run_sync( lambda: audio_data.export( str(output_path), format="mp3", parameters=["-ar", str(target_sample_rate)], ) ) # Read the compressed file with open(output_path, "rb") as f: return f.read() except Exception as e: raise AudioCompressionError(f"MP3 compression failed: {e}") from e @staticmethod async def load_audio_from_path(file_path: Path) -> AudioSegment: """Load audio file into AudioSegment. Args: ---- file_path: Path to the audio file. Returns: ------- AudioSegment: Loaded audio segment. Raises: ------ AudioConversionError: If loading fails. """ try: format_str = file_path.suffix[1:] # Remove leading dot return await anyio.to_thread.run_sync(lambda: AudioSegment.from_file(str(file_path), format=format_str)) except Exception as e: raise AudioConversionError(f"Failed to load audio file {file_path}: {e}") from e @staticmethod def calculate_compression_needed( file_size_bytes: int, max_mb: int = DEFAULT_MAX_FILE_SIZE_MB, ) -> bool: """Determine if compression is needed based on file size. Args: ---- file_size_bytes: Current file size in bytes. max_mb: Maximum allowed size in megabytes. Returns: ------- bool: True if compression is needed, False otherwise. """ threshold_bytes = max_mb * 1024 * 1024 return file_size_bytes > threshold_bytes @staticmethod async def concatenate_audio_segments(audio_chunks: list[bytes], format: str = "mp3") -> bytes: """Concatenate multiple audio chunks into a single audio file. Args: ---- audio_chunks: List of audio data as bytes. format: Audio format (default: 'mp3'). Returns: ------- bytes: Concatenated audio data. Raises: ------ AudioProcessingError: If concatenation fails. """ try: from io import BytesIO combined = AudioSegment.empty() for chunk in audio_chunks: # Load each chunk chunk_io = BytesIO(chunk) if format == "mp3": segment = await anyio.to_thread.run_sync(lambda: AudioSegment.from_mp3(chunk_io)) elif format == "wav": segment = await anyio.to_thread.run_sync(lambda: AudioSegment.from_wav(chunk_io)) else: segment = await anyio.to_thread.run_sync(lambda: AudioSegment.from_file(chunk_io, format=format)) combined += segment # Export combined audio to bytes output = BytesIO() await anyio.to_thread.run_sync(lambda: combined.export(output, format=format)) return output.getvalue() except Exception as e: raise AudioConversionError(f"Audio concatenation failed: {e}") from e @staticmethod def generate_output_path( input_path: Path, output_path: Path | None, suffix: str, extension: str, ) -> Path: """Generate appropriate output path for processed audio. Args: ---- input_path: Original input file path. output_path: User-specified output path (if any). suffix: Suffix to add to filename (e.g., 'compressed'). extension: File extension (e.g., '.mp3'). Returns: ------- Path: Generated output path. """ if output_path is not None: return output_path if suffix: return input_path.parent / f"{suffix}_{input_path.stem}{extension}" else: return input_path.with_suffix(extension)

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/arcaputo3/mcp-server-whisper'

If you have feedback or need assistance with the MCP directory API, please join our Discord server