MogensR commited on
Commit
d7530fc
·
1 Parent(s): 6b58990

Create audio_processor.py

Browse files
Files changed (1) hide show
  1. audio_processor.py +583 -0
audio_processor.py ADDED
@@ -0,0 +1,583 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Audio Processing Module
3
+ Handles audio extraction, processing, and integration with FFmpeg operations
4
+ """
5
+
6
+ import os
7
+ import subprocess
8
+ import tempfile
9
+ import logging
10
+ import time
11
+ from pathlib import Path
12
+ from typing import Optional, Dict, Any, List, Tuple
13
+ from exceptions import AudioProcessingError
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ class AudioProcessor:
18
+ """
19
+ Comprehensive audio processing for video background replacement
20
+ """
21
+
22
+ def __init__(self, temp_dir: Optional[str] = None):
23
+ self.temp_dir = temp_dir or tempfile.gettempdir()
24
+ self.ffmpeg_available = self._check_ffmpeg_availability()
25
+ self.ffprobe_available = self._check_ffprobe_availability()
26
+
27
+ # Audio processing statistics
28
+ self.stats = {
29
+ 'audio_extractions': 0,
30
+ 'audio_merges': 0,
31
+ 'total_processing_time': 0.0,
32
+ 'failed_operations': 0
33
+ }
34
+
35
+ if not self.ffmpeg_available:
36
+ logger.warning("FFmpeg not available - audio processing will be limited")
37
+
38
+ logger.info(f"AudioProcessor initialized (FFmpeg: {self.ffmpeg_available}, FFprobe: {self.ffprobe_available})")
39
+
40
+ def _check_ffmpeg_availability(self) -> bool:
41
+ """Check if FFmpeg is available on the system"""
42
+ try:
43
+ result = subprocess.run(
44
+ ['ffmpeg', '-version'],
45
+ capture_output=True,
46
+ text=True,
47
+ timeout=10
48
+ )
49
+ return result.returncode == 0
50
+ except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError):
51
+ return False
52
+
53
+ def _check_ffprobe_availability(self) -> bool:
54
+ """Check if FFprobe is available on the system"""
55
+ try:
56
+ result = subprocess.run(
57
+ ['ffprobe', '-version'],
58
+ capture_output=True,
59
+ text=True,
60
+ timeout=10
61
+ )
62
+ return result.returncode == 0
63
+ except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError):
64
+ return False
65
+
66
+ def get_audio_info(self, video_path: str) -> Dict[str, Any]:
67
+ """
68
+ Get comprehensive audio information from video file
69
+
70
+ Args:
71
+ video_path: Path to the video file
72
+
73
+ Returns:
74
+ Dictionary containing audio information
75
+ """
76
+ if not self.ffprobe_available:
77
+ return {'has_audio': False, 'error': 'FFprobe not available'}
78
+
79
+ try:
80
+ # Get audio stream information
81
+ result = subprocess.run([
82
+ 'ffprobe', '-v', 'quiet', '-select_streams', 'a:0',
83
+ '-show_entries', 'stream=codec_name,sample_rate,channels,duration,bit_rate',
84
+ '-of', 'csv=p=0', video_path
85
+ ], capture_output=True, text=True, timeout=30)
86
+
87
+ if result.returncode != 0:
88
+ return {
89
+ 'has_audio': False,
90
+ 'error': 'No audio stream found',
91
+ 'ffprobe_error': result.stderr
92
+ }
93
+
94
+ # Parse audio information
95
+ audio_data = result.stdout.strip().split(',')
96
+
97
+ if len(audio_data) >= 1 and audio_data[0]:
98
+ info = {
99
+ 'has_audio': True,
100
+ 'codec': audio_data[0] if len(audio_data) > 0 else 'unknown',
101
+ 'sample_rate': audio_data[1] if len(audio_data) > 1 else 'unknown',
102
+ 'channels': audio_data[2] if len(audio_data) > 2 else 'unknown',
103
+ 'duration': audio_data[3] if len(audio_data) > 3 else 'unknown',
104
+ 'bit_rate': audio_data[4] if len(audio_data) > 4 else 'unknown'
105
+ }
106
+
107
+ # Convert string values to appropriate types
108
+ try:
109
+ if info['sample_rate'] != 'unknown':
110
+ info['sample_rate'] = int(info['sample_rate'])
111
+ if info['channels'] != 'unknown':
112
+ info['channels'] = int(info['channels'])
113
+ if info['duration'] != 'unknown':
114
+ info['duration'] = float(info['duration'])
115
+ if info['bit_rate'] != 'unknown':
116
+ info['bit_rate'] = int(info['bit_rate'])
117
+ except ValueError:
118
+ pass # Keep as string if conversion fails
119
+
120
+ return info
121
+ else:
122
+ return {'has_audio': False, 'error': 'Audio stream data empty'}
123
+
124
+ except subprocess.TimeoutExpired:
125
+ return {'has_audio': False, 'error': 'FFprobe timeout'}
126
+ except Exception as e:
127
+ logger.error(f"Error getting audio info: {e}")
128
+ return {'has_audio': False, 'error': str(e)}
129
+
130
+ def extract_audio(self, video_path: str, output_path: Optional[str] = None,
131
+ audio_format: str = 'aac', quality: str = 'high') -> Optional[str]:
132
+ """
133
+ Extract audio from video file
134
+
135
+ Args:
136
+ video_path: Path to input video
137
+ output_path: Output path for audio (auto-generated if None)
138
+ audio_format: Output audio format (aac, mp3, wav)
139
+ quality: Audio quality (low, medium, high)
140
+
141
+ Returns:
142
+ Path to extracted audio file or None if failed
143
+ """
144
+ if not self.ffmpeg_available:
145
+ raise AudioProcessingError("extract", "FFmpeg not available", video_path)
146
+
147
+ start_time = time.time()
148
+
149
+ try:
150
+ # Check if input has audio
151
+ audio_info = self.get_audio_info(video_path)
152
+ if not audio_info.get('has_audio', False):
153
+ logger.info(f"No audio found in {video_path}")
154
+ return None
155
+
156
+ # Generate output path if not provided
157
+ if output_path is None:
158
+ timestamp = int(time.time())
159
+ output_path = os.path.join(
160
+ self.temp_dir,
161
+ f"extracted_audio_{timestamp}.{audio_format}"
162
+ )
163
+
164
+ # Quality settings
165
+ quality_settings = {
166
+ 'low': {'aac': ['-b:a', '96k'], 'mp3': ['-b:a', '128k'], 'wav': []},
167
+ 'medium': {'aac': ['-b:a', '192k'], 'mp3': ['-b:a', '192k'], 'wav': []},
168
+ 'high': {'aac': ['-b:a', '320k'], 'mp3': ['-b:a', '320k'], 'wav': []}
169
+ }
170
+
171
+ codec_settings = {
172
+ 'aac': ['-c:a', 'aac'],
173
+ 'mp3': ['-c:a', 'libmp3lame'],
174
+ 'wav': ['-c:a', 'pcm_s16le']
175
+ }
176
+
177
+ # Build FFmpeg command
178
+ cmd = ['ffmpeg', '-y', '-i', video_path]
179
+ cmd.extend(codec_settings.get(audio_format, ['-c:a', 'aac']))
180
+ cmd.extend(quality_settings.get(quality, {}).get(audio_format, []))
181
+ cmd.extend(['-vn', output_path]) # -vn excludes video
182
+
183
+ # Execute command
184
+ result = subprocess.run(
185
+ cmd,
186
+ capture_output=True,
187
+ text=True,
188
+ timeout=300 # 5 minute timeout
189
+ )
190
+
191
+ if result.returncode != 0:
192
+ raise AudioProcessingError(
193
+ "extract",
194
+ f"FFmpeg failed: {result.stderr}",
195
+ video_path,
196
+ output_path
197
+ )
198
+
199
+ if not os.path.exists(output_path):
200
+ raise AudioProcessingError(
201
+ "extract",
202
+ "Output audio file was not created",
203
+ video_path,
204
+ output_path
205
+ )
206
+
207
+ # Update statistics
208
+ processing_time = time.time() - start_time
209
+ self.stats['audio_extractions'] += 1
210
+ self.stats['total_processing_time'] += processing_time
211
+
212
+ logger.info(f"Audio extracted successfully in {processing_time:.1f}s: {output_path}")
213
+ return output_path
214
+
215
+ except subprocess.TimeoutExpired:
216
+ self.stats['failed_operations'] += 1
217
+ raise AudioProcessingError("extract", "FFmpeg timeout during extraction", video_path)
218
+ except Exception as e:
219
+ self.stats['failed_operations'] += 1
220
+ if isinstance(e, AudioProcessingError):
221
+ raise
222
+ else:
223
+ raise AudioProcessingError("extract", f"Unexpected error: {str(e)}", video_path)
224
+
225
+ def add_audio_to_video(self, original_video: str, processed_video: str,
226
+ output_path: Optional[str] = None,
227
+ audio_quality: str = 'high') -> str:
228
+ """
229
+ Add audio from original video to processed video
230
+
231
+ Args:
232
+ original_video: Path to original video with audio
233
+ processed_video: Path to processed video without audio
234
+ output_path: Output path (auto-generated if None)
235
+ audio_quality: Audio quality setting
236
+
237
+ Returns:
238
+ Path to final video with audio
239
+ """
240
+ if not self.ffmpeg_available:
241
+ logger.warning("FFmpeg not available - returning processed video without audio")
242
+ return processed_video
243
+
244
+ start_time = time.time()
245
+
246
+ try:
247
+ # Check if original video has audio
248
+ audio_info = self.get_audio_info(original_video)
249
+ if not audio_info.get('has_audio', False):
250
+ logger.info("Original video has no audio - returning processed video")
251
+ return processed_video
252
+
253
+ # Generate output path if not provided
254
+ if output_path is None:
255
+ timestamp = int(time.time())
256
+ output_path = os.path.join(
257
+ self.temp_dir,
258
+ f"final_with_audio_{timestamp}.mp4"
259
+ )
260
+
261
+ # Quality settings for audio encoding
262
+ quality_settings = {
263
+ 'low': ['-b:a', '96k'],
264
+ 'medium': ['-b:a', '192k'],
265
+ 'high': ['-b:a', '320k']
266
+ }
267
+
268
+ # Build FFmpeg command to combine video and audio
269
+ cmd = [
270
+ 'ffmpeg', '-y',
271
+ '-i', processed_video, # Video input
272
+ '-i', original_video, # Audio source
273
+ '-c:v', 'copy', # Copy video stream as-is
274
+ '-c:a', 'aac', # Encode audio as AAC
275
+ ]
276
+
277
+ # Add quality settings
278
+ cmd.extend(quality_settings.get(audio_quality, quality_settings['high']))
279
+
280
+ # Map streams and set duration
281
+ cmd.extend([
282
+ '-map', '0:v:0', # Video from first input
283
+ '-map', '1:a:0', # Audio from second input
284
+ '-shortest', # Match shortest stream duration
285
+ output_path
286
+ ])
287
+
288
+ # Execute command
289
+ result = subprocess.run(
290
+ cmd,
291
+ capture_output=True,
292
+ text=True,
293
+ timeout=600 # 10 minute timeout
294
+ )
295
+
296
+ if result.returncode != 0:
297
+ logger.warning(f"Audio merge failed: {result.stderr}")
298
+ logger.warning("Returning processed video without audio")
299
+ return processed_video
300
+
301
+ if not os.path.exists(output_path):
302
+ logger.warning("Output video with audio was not created")
303
+ return processed_video
304
+
305
+ # Verify the output file
306
+ if os.path.getsize(output_path) == 0:
307
+ logger.warning("Output video file is empty")
308
+ try:
309
+ os.remove(output_path)
310
+ except:
311
+ pass
312
+ return processed_video
313
+
314
+ # Clean up original processed video if successful
315
+ try:
316
+ if output_path != processed_video:
317
+ os.remove(processed_video)
318
+ logger.debug("Cleaned up intermediate processed video")
319
+ except Exception as e:
320
+ logger.warning(f"Could not clean up intermediate file: {e}")
321
+
322
+ # Update statistics
323
+ processing_time = time.time() - start_time
324
+ self.stats['audio_merges'] += 1
325
+ self.stats['total_processing_time'] += processing_time
326
+
327
+ logger.info(f"Audio merged successfully in {processing_time:.1f}s: {output_path}")
328
+ return output_path
329
+
330
+ except subprocess.TimeoutExpired:
331
+ self.stats['failed_operations'] += 1
332
+ logger.warning("Audio merge timeout - returning processed video without audio")
333
+ return processed_video
334
+ except Exception as e:
335
+ self.stats['failed_operations'] += 1
336
+ logger.warning(f"Audio merge error: {e} - returning processed video without audio")
337
+ return processed_video
338
+
339
+ def sync_audio_video(self, video_path: str, audio_path: str,
340
+ output_path: str, offset_ms: float = 0.0) -> bool:
341
+ """
342
+ Synchronize separate audio and video files
343
+
344
+ Args:
345
+ video_path: Path to video file
346
+ audio_path: Path to audio file
347
+ output_path: Output path for synchronized file
348
+ offset_ms: Audio offset in milliseconds (positive = delay audio)
349
+
350
+ Returns:
351
+ True if successful, False otherwise
352
+ """
353
+ if not self.ffmpeg_available:
354
+ raise AudioProcessingError("sync", "FFmpeg not available")
355
+
356
+ try:
357
+ cmd = ['ffmpeg', '-y', '-i', video_path, '-i', audio_path]
358
+
359
+ # Add audio offset if specified
360
+ if offset_ms != 0.0:
361
+ offset_seconds = offset_ms / 1000.0
362
+ cmd.extend(['-itsoffset', str(offset_seconds)])
363
+
364
+ cmd.extend([
365
+ '-c:v', 'copy', # Copy video as-is
366
+ '-c:a', 'aac', # Encode audio as AAC
367
+ '-b:a', '192k', # Audio bitrate
368
+ '-shortest', # Match shortest stream
369
+ output_path
370
+ ])
371
+
372
+ result = subprocess.run(
373
+ cmd,
374
+ capture_output=True,
375
+ text=True,
376
+ timeout=600
377
+ )
378
+
379
+ if result.returncode != 0:
380
+ raise AudioProcessingError(
381
+ "sync",
382
+ f"Synchronization failed: {result.stderr}",
383
+ video_path
384
+ )
385
+
386
+ return os.path.exists(output_path) and os.path.getsize(output_path) > 0
387
+
388
+ except subprocess.TimeoutExpired:
389
+ raise AudioProcessingError("sync", "Synchronization timeout", video_path)
390
+ except Exception as e:
391
+ if isinstance(e, AudioProcessingError):
392
+ raise
393
+ else:
394
+ raise AudioProcessingError("sync", f"Unexpected error: {str(e)}", video_path)
395
+
396
+ def adjust_audio_levels(self, input_path: str, output_path: str,
397
+ volume_factor: float = 1.0, normalize: bool = False) -> bool:
398
+ """
399
+ Adjust audio levels in a video file
400
+
401
+ Args:
402
+ input_path: Input video path
403
+ output_path: Output video path
404
+ volume_factor: Volume multiplication factor (1.0 = no change)
405
+ normalize: Whether to normalize audio levels
406
+
407
+ Returns:
408
+ True if successful, False otherwise
409
+ """
410
+ if not self.ffmpeg_available:
411
+ raise AudioProcessingError("adjust_levels", "FFmpeg not available")
412
+
413
+ try:
414
+ cmd = ['ffmpeg', '-y', '-i', input_path, '-c:v', 'copy']
415
+
416
+ # Build audio filter
417
+ audio_filters = []
418
+
419
+ if volume_factor != 1.0:
420
+ audio_filters.append(f"volume={volume_factor}")
421
+
422
+ if normalize:
423
+ audio_filters.append("loudnorm")
424
+
425
+ if audio_filters:
426
+ cmd.extend(['-af', ','.join(audio_filters)])
427
+
428
+ cmd.extend(['-c:a', 'aac', '-b:a', '192k', output_path])
429
+
430
+ result = subprocess.run(
431
+ cmd,
432
+ capture_output=True,
433
+ text=True,
434
+ timeout=600
435
+ )
436
+
437
+ if result.returncode != 0:
438
+ raise AudioProcessingError(
439
+ "adjust_levels",
440
+ f"Level adjustment failed: {result.stderr}",
441
+ input_path
442
+ )
443
+
444
+ return os.path.exists(output_path) and os.path.getsize(output_path) > 0
445
+
446
+ except Exception as e:
447
+ if isinstance(e, AudioProcessingError):
448
+ raise
449
+ else:
450
+ raise AudioProcessingError("adjust_levels", f"Unexpected error: {str(e)}", input_path)
451
+
452
+ def get_supported_formats(self) -> Dict[str, List[str]]:
453
+ """Get supported audio and video formats"""
454
+ if not self.ffmpeg_available:
455
+ return {'audio': [], 'video': []}
456
+
457
+ try:
458
+ # Get supported formats from FFmpeg
459
+ result = subprocess.run(
460
+ ['ffmpeg', '-formats'],
461
+ capture_output=True,
462
+ text=True,
463
+ timeout=30
464
+ )
465
+
466
+ if result.returncode != 0:
467
+ return {'audio': ['aac', 'mp3', 'wav'], 'video': ['mp4', 'avi', 'mov']}
468
+
469
+ # Parse output (simplified - could be more comprehensive)
470
+ lines = result.stdout.split('\n')
471
+ audio_formats = []
472
+ video_formats = []
473
+
474
+ for line in lines:
475
+ if 'aac' in line.lower():
476
+ audio_formats.append('aac')
477
+ elif 'mp3' in line.lower():
478
+ audio_formats.append('mp3')
479
+ elif 'wav' in line.lower():
480
+ audio_formats.append('wav')
481
+ elif 'mp4' in line.lower():
482
+ video_formats.append('mp4')
483
+ elif 'avi' in line.lower():
484
+ video_formats.append('avi')
485
+ elif 'mov' in line.lower():
486
+ video_formats.append('mov')
487
+
488
+ return {
489
+ 'audio': list(set(audio_formats)) or ['aac', 'mp3', 'wav'],
490
+ 'video': list(set(video_formats)) or ['mp4', 'avi', 'mov']
491
+ }
492
+
493
+ except Exception as e:
494
+ logger.warning(f"Could not get supported formats: {e}")
495
+ return {'audio': ['aac', 'mp3', 'wav'], 'video': ['mp4', 'avi', 'mov']}
496
+
497
+ def validate_audio_video_compatibility(self, video_path: str, audio_path: str) -> Dict[str, Any]:
498
+ """
499
+ Validate compatibility between video and audio files
500
+
501
+ Returns:
502
+ Dictionary with compatibility information
503
+ """
504
+ if not self.ffprobe_available:
505
+ return {'compatible': False, 'error': 'FFprobe not available'}
506
+
507
+ try:
508
+ # Get video info
509
+ video_result = subprocess.run([
510
+ 'ffprobe', '-v', 'quiet', '-select_streams', 'v:0',
511
+ '-show_entries', 'stream=duration', '-of', 'csv=p=0', video_path
512
+ ], capture_output=True, text=True, timeout=30)
513
+
514
+ # Get audio info
515
+ audio_result = subprocess.run([
516
+ 'ffprobe', '-v', 'quiet', '-select_streams', 'a:0',
517
+ '-show_entries', 'stream=duration', '-of', 'csv=p=0', audio_path
518
+ ], capture_output=True, text=True, timeout=30)
519
+
520
+ if video_result.returncode != 0 or audio_result.returncode != 0:
521
+ return {'compatible': False, 'error': 'Could not read file information'}
522
+
523
+ try:
524
+ video_duration = float(video_result.stdout.strip())
525
+ audio_duration = float(audio_result.stdout.strip())
526
+
527
+ duration_diff = abs(video_duration - audio_duration)
528
+ duration_diff_percent = (duration_diff / max(video_duration, audio_duration)) * 100
529
+
530
+ return {
531
+ 'compatible': duration_diff_percent < 5.0, # 5% tolerance
532
+ 'video_duration': video_duration,
533
+ 'audio_duration': audio_duration,
534
+ 'duration_difference': duration_diff,
535
+ 'duration_difference_percent': duration_diff_percent,
536
+ 'recommendation': (
537
+ 'Compatible' if duration_diff_percent < 5.0
538
+ else 'Duration mismatch - consider trimming/extending'
539
+ )
540
+ }
541
+
542
+ except ValueError:
543
+ return {'compatible': False, 'error': 'Invalid duration values'}
544
+
545
+ except Exception as e:
546
+ return {'compatible': False, 'error': str(e)}
547
+
548
+ def get_stats(self) -> Dict[str, Any]:
549
+ """Get audio processing statistics"""
550
+ return {
551
+ 'ffmpeg_available': self.ffmpeg_available,
552
+ 'ffprobe_available': self.ffprobe_available,
553
+ 'audio_extractions': self.stats['audio_extractions'],
554
+ 'audio_merges': self.stats['audio_merges'],
555
+ 'total_processing_time': self.stats['total_processing_time'],
556
+ 'failed_operations': self.stats['failed_operations'],
557
+ 'success_rate': (
558
+ (self.stats['audio_extractions'] + self.stats['audio_merges']) /
559
+ max(1, self.stats['audio_extractions'] + self.stats['audio_merges'] + self.stats['failed_operations'])
560
+ ) * 100
561
+ }
562
+
563
+ def cleanup_temp_files(self, max_age_hours: int = 24):
564
+ """Clean up temporary audio files older than specified age"""
565
+ try:
566
+ temp_path = Path(self.temp_dir)
567
+ current_time = time.time()
568
+ cutoff_time = current_time - (max_age_hours * 3600)
569
+
570
+ cleaned_files = 0
571
+ for file_path in temp_path.glob("*audio*.{aac,mp3,wav,mp4}"):
572
+ if file_path.stat().st_mtime < cutoff_time:
573
+ try:
574
+ file_path.unlink()
575
+ cleaned_files += 1
576
+ except Exception as e:
577
+ logger.warning(f"Could not delete temp file {file_path}: {e}")
578
+
579
+ if cleaned_files > 0:
580
+ logger.info(f"Cleaned up {cleaned_files} temporary audio files")
581
+
582
+ except Exception as e:
583
+ logger.warning(f"Error during temp file cleanup: {e}")