audio_worker.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. """
  2. Audio Worker Module
  3. Worker thread for async audio processing and ripeness classification.
  4. """
  5. from typing import Optional
  6. import logging
  7. from PyQt5.QtCore import pyqtSignal
  8. from PyQt5.QtGui import QPixmap
  9. from workers.base_worker import BaseWorker, WorkerSignals
  10. from models.audio_model import AudioModel
  11. logger = logging.getLogger(__name__)
  12. class AudioWorkerSignals(WorkerSignals):
  13. """
  14. Signals specific to audio processing.
  15. Signals:
  16. result_ready: Emitted when prediction is complete
  17. (waveform: QPixmap, spectrogram: QPixmap, class_name: str,
  18. confidence: float, probabilities: dict, knock_count: int)
  19. """
  20. result_ready = pyqtSignal(QPixmap, QPixmap, str, float, dict, int)
  21. class AudioWorker(BaseWorker):
  22. """
  23. Worker for processing audio files and predicting ripeness.
  24. Runs AudioModel inference in a background thread without blocking UI.
  25. Supports multiple audio formats (WAV, MP3, FLAC, OGG, M4A, AAC, WMA).
  26. Attributes:
  27. audio_path: Path to audio file (multiple formats supported)
  28. model: AudioModel instance
  29. signals: AudioWorkerSignals for emitting results
  30. """
  31. def __init__(self, audio_path: str, model: Optional[AudioModel] = None):
  32. """
  33. Initialize the audio worker.
  34. Args:
  35. audio_path: Path to audio file to process (supports multiple formats)
  36. model: AudioModel instance (if None, creates new one)
  37. """
  38. super().__init__()
  39. self.audio_path = audio_path
  40. self.model = model
  41. # Replace base signals with audio-specific signals
  42. self.signals = AudioWorkerSignals()
  43. # If no model provided, create and load one
  44. if self.model is None:
  45. self.model = AudioModel(device='cpu') # TensorFlow works best on CPU
  46. self.model.load()
  47. logger.info(f"AudioWorker created for: {audio_path}")
  48. def process(self):
  49. """
  50. Process the audio file and predict ripeness.
  51. Emits result_ready signal with prediction results.
  52. """
  53. if self.is_cancelled():
  54. logger.info("AudioWorker cancelled before processing")
  55. return
  56. # Update progress
  57. self.emit_progress(10, "Loading audio file...")
  58. if not self.model.is_loaded:
  59. logger.warning("⚠️ Model not loaded, loading now...")
  60. self.emit_progress(30, "Loading model...")
  61. if not self.model.load():
  62. logger.error("❌ Failed to load audio model")
  63. raise RuntimeError("Failed to load audio model")
  64. logger.info("✓ Model loaded successfully")
  65. # Process audio
  66. self.emit_progress(50, "Detecting knocks and generating visualizations...")
  67. logger.info(f"Processing audio: {self.audio_path}")
  68. result = self.model.predict(self.audio_path)
  69. logger.info(f"Prediction result success: {result.get('success')}")
  70. if self.is_cancelled():
  71. logger.info("AudioWorker cancelled during processing")
  72. return
  73. if not result['success']:
  74. logger.error(f"❌ Prediction failed: {result.get('error')}")
  75. raise RuntimeError(result['error'])
  76. logger.info(f"✓ Prediction successful: {result.get('class_name')} ({result.get('confidence'):.2%})")
  77. self.emit_progress(90, "Finalizing results...")
  78. # Emit results (handle None images gracefully with placeholders)
  79. waveform_image = result.get('waveform_image')
  80. spectrogram_image = result.get('spectrogram_image')
  81. if waveform_image is None:
  82. # Create a placeholder pixmap for missing waveform
  83. from PyQt5.QtGui import QPixmap, QColor
  84. waveform_image = QPixmap(400, 200)
  85. waveform_image.fill(QColor("#2c3e50"))
  86. if spectrogram_image is None:
  87. # Create a placeholder pixmap for missing spectrogram
  88. from PyQt5.QtGui import QPixmap, QColor
  89. spectrogram_image = QPixmap(400, 200)
  90. spectrogram_image.fill(QColor("#2c3e50"))
  91. self.signals.result_ready.emit(
  92. waveform_image,
  93. spectrogram_image,
  94. result['class_name'],
  95. result['confidence'],
  96. result['probabilities'],
  97. result.get('knock_count', 0)
  98. )
  99. self.emit_progress(100, "Complete!")
  100. logger.info(f"AudioWorker completed: {result['class_name']} ({result['confidence']:.2%}) - {result.get('knock_count', 0)} knocks")