Cat Feeder  1.0.0
The Cat feeder project
Loading...
Searching...
No Matches
audio_to_audio.py
Go to the documentation of this file.
1r"""
2# +==== BEGIN CatFeeder =================+
3# LOGO:
4# ..............(..../\
5# ...............)..(.')
6# ..............(../..)
7# ...............\‍(__)|
8# Inspired by Joan Stark
9# source https://www.asciiart.eu/
10# animals/cats
11# /STOP
12# PROJECT: CatFeeder
13# FILE: image_to_image.py
14# CREATION DATE: 15-01-2026
15# LAST Modified: 1:32:3 17-01-2026
16# DESCRIPTION:
17# This is the backend server in charge of making the actual website work.
18# /STOP
19# COPYRIGHT: (c) Cat Feeder
20# PURPOSE: The file containing the code for converting bytes to a base of the user's choice.
21# // AR
22# +==== END CatFeeder =================+
23"""
24
25from io import BytesIO
26from typing import Optional
27
28from display_tty import Disp, initialise_logger
29
30# Audio processing library
31from pydub import AudioSegment
32
33from .aliases import AUDIO_FORMAT_ALIASES
34from . import converters_constants as CONV_CONST
35
36from ..http_constants import DataTypes, MEDIA_TYPES
37
38from ...core import FinalClass
39from ...utils import CONST
40from ...fffamily import FFMPEGDownloader
41
42
43class AudioToAudio(metaclass=FinalClass):
44 """Class used to convert audio from one format to another using pydub/ffmpeg.
45
46 Optimized to minimize I/O costs:
47 - All conversions done in-memory using BytesIO (zero disk I/O)
48 - pydub handles format detection and conversion via ffmpeg
49 """
50
51 disp: Disp = initialise_logger(__qualname__, CONST.DEBUG)
52
53 _instance: Optional["AudioToAudio"] = None
54
55 _ffmpeg_ensured: bool = False
56
57 def __new__(cls) -> "AudioToAudio":
58 if cls._instance is None:
59 cls._instance = super(AudioToAudio, cls).__new__(cls)
60 return cls._instance
61
62 def __init__(self) -> None:
63 self.disp.log_debug("Initialising...")
64 self.disp.log_debug("Ensuring FFMPEG is available...")
65 self._ensure_ffmpeg()
66 self.disp.log_debug("FFMPEG is available.")
67 self.disp.log_debug("Initialised.")
68
69 def __call__(self, data: bytes, source_format: DataTypes) -> CONV_CONST.ConversionResult:
70 return self.audio_to_audio(data, source_format)
71
72 def _ensure_ffmpeg(self) -> None:
73 """Ensure that FFMPEG is downloaded and available."""
74 if self._ffmpeg_ensured:
75 return
76
77 self.disp.log_debug("Ensuring FFMPEG is available...")
78 FDI = FFMPEGDownloader(
79 cwd=str(CONV_CONST.FF_FAMILY_PATH),
80 success=CONV_CONST.SUCCESS,
81 error=CONV_CONST.ERROR,
82 debug=CONV_CONST.DEBUG
83 )
84 try:
85 FDI.main()
86 self._ffmpeg_ensured = True
87 except Exception as e:
88 self.disp.log_error(f"FFMPEG is not available: {e}")
89 raise RuntimeError(
90 "FFMPEG is required for audio conversion but could not be ensured."
91 ) from e
92 self.disp.log_debug("FFMPEG is available.")
93
94 def get_audio_extension(self, data_type: DataTypes) -> Optional[str]:
95 """
96 Get the file extension for a given audio DataType.
97
98 Args:
99 data_type: The DataType to get extension for
100
101 Returns:
102 Extension string or None
103 """
104 return AUDIO_FORMAT_ALIASES.get(data_type)
105
107 self,
108 data: bytes,
109 source_format: DataTypes
110 ) -> tuple[Optional[DataTypes], Optional[str], Optional[str]]:
111 """
112 Validate conversion parameters and get destination format and extensions.
113
114 Args:
115 data: The audio data to validate
116 source_format: The source audio format
117
118 Returns:
119 Tuple of (destination_format, source_ext, dest_ext)
120 Returns (None, None, None) if validation fails
121 """
122 try:
123 destination_format = MEDIA_TYPES.get_conversion_target(
124 source_format)
125 except (AttributeError, NameError):
126 destination_format = None
127
128 if destination_format is None:
129 self.disp.log_debug(f"No conversion target for {source_format}")
130 return None, None, None
131
132 if source_format == destination_format:
133 self.disp.log_debug(
134 f"Source and destination formats are the same: {source_format}"
135 )
136 return destination_format, None, None
137
138 source_ext = self.get_audio_extension(source_format)
139 dest_ext = self.get_audio_extension(destination_format)
140
141 if not source_ext or not dest_ext:
142 self.disp.log_warning(
143 f"Unknown audio extension for {source_format} -> {destination_format}"
144 )
145 return destination_format, None, None
146
147 return destination_format, source_ext, dest_ext
148
150 self,
151 data: bytes,
152 source_format: DataTypes,
153 destination_format: DataTypes,
154 source_ext: str,
155 dest_ext: str
156 ) -> Optional[bytes]:
157 """
158 Perform in-memory audio conversion using pydub.
159 ZERO I/O COST - everything happens in RAM via BytesIO.
160
161 Args:
162 data: Source audio data
163 source_format: Source audio format
164 destination_format: Destination audio format
165 source_ext: Source file extension
166 dest_ext: Destination file extension
167
168 Returns:
169 Converted audio data as bytes, or None if conversion failed
170 """
171 self.disp.log_debug(
172 f"Converting in-memory (ZERO I/O): {source_format} -> {destination_format}"
173 )
174
175 try:
176 # Load audio from bytes into pydub AudioSegment
177 input_buffer = BytesIO(data)
178 audio = AudioSegment.from_file(input_buffer, format=source_ext)
179
180 # Export to destination format in memory
181 output_buffer = BytesIO()
182 audio.export(
183 output_buffer,
184 format=dest_ext
185 )
186
187 # Get the converted bytes
188 converted_data = output_buffer.getvalue()
189
190 self.disp.log_debug(
191 f"In-memory audio conversion successful (ZERO I/O cost)"
192 )
193 return converted_data
194
195 except Exception as e:
196 self.disp.log_error(f"In-memory audio conversion failed: {e}")
197 return None
198
200 self,
201 data: bytes,
202 source_format: DataTypes,
203 destination_format: DataTypes,
204 result: Optional[bytes] = None
205 ) -> CONV_CONST.ConversionResult:
206 """Create a failed conversion result."""
207 return CONV_CONST.ConversionResult(
208 data=data,
209 converted=False,
210 from_type=source_format,
211 to_type=destination_format,
212 result=result
213 )
214
216 self,
217 data: bytes,
218 source_format: DataTypes,
219 destination_format: DataTypes,
220 converted_data: bytes
221 ) -> CONV_CONST.ConversionResult:
222 """Create a successful conversion result."""
223 return CONV_CONST.ConversionResult(
224 data=data,
225 converted=True,
226 from_type=source_format,
227 to_type=destination_format,
228 result=converted_data
229 )
230
232 self,
233 data: bytes,
234 source_format: DataTypes,
235 destination_format: Optional[DataTypes],
236 source_ext: Optional[str],
237 dest_ext: Optional[str]
238 ) -> Optional[CONV_CONST.ConversionResult]:
239 """Handle validation failures and return appropriate result."""
240 if destination_format is None:
241 return self._create_failed_result(
242 data, source_format, source_format, None
243 )
244
245 if source_ext is None or dest_ext is None:
246 result_data = data if source_format == destination_format else data
247 return self._create_failed_result(
248 data, source_format, destination_format, result_data
249 )
250
251 return None
252
253 def audio_to_audio(self, data: bytes, source_format: DataTypes) -> CONV_CONST.ConversionResult:
254 """
255 Convert audio data from one format to another using pydub/ffmpeg.
256 All operations done in-memory (ZERO I/O cost).
257
258 Args:
259 data (bytes): The audio data to convert.
260 source_format (DataTypes): The original audio format.
261
262 Returns:
263 ConversionResult: The converted audio data in a contained dataclass.
264 """
265 # Validate conversion parameters
266 destination_format, source_ext, dest_ext = self._validate_conversion_params(
267 data, source_format
268 )
269
270 # Handle validation failures
271 validation_result = self._handle_validation_failure(
272 data, source_format, destination_format, source_ext, dest_ext
273 )
274 if validation_result is not None:
275 return validation_result
276
277 # Type guard: at this point we know these are not None due to validation
278 if destination_format is None or source_ext is None or dest_ext is None:
279 # This should never happen after validation, but satisfies type checker
280 return self._create_failed_result(
281 data, source_format, source_format, None
282 )
283
284 # Perform in-memory conversion (ZERO I/O cost)
285 converted_data = self._convert_in_memory(
286 data, source_format, destination_format, source_ext, dest_ext
287 )
288
289 if converted_data is None:
290 return self._create_failed_result(
291 data, source_format, destination_format, None
292 )
293
294 return self._create_success_result(
295 data, source_format, destination_format, converted_data
296 )
CONV_CONST.ConversionResult _create_failed_result(self, bytes data, DataTypes source_format, DataTypes destination_format, Optional[bytes] result=None)
CONV_CONST.ConversionResult audio_to_audio(self, bytes data, DataTypes source_format)
Optional[CONV_CONST.ConversionResult] _handle_validation_failure(self, bytes data, DataTypes source_format, Optional[DataTypes] destination_format, Optional[str] source_ext, Optional[str] dest_ext)
CONV_CONST.ConversionResult _create_success_result(self, bytes data, DataTypes source_format, DataTypes destination_format, bytes converted_data)
Optional[bytes] _convert_in_memory(self, bytes data, DataTypes source_format, DataTypes destination_format, str source_ext, str dest_ext)
Optional[str] get_audio_extension(self, DataTypes data_type)
CONV_CONST.ConversionResult __call__(self, bytes data, DataTypes source_format)
tuple[Optional[DataTypes], Optional[str], Optional[str]] _validate_conversion_params(self, bytes data, DataTypes source_format)