calettippo commited on
Commit
65b0afc
·
1 Parent(s): 700a4c7

Add preprocessing

Browse files
Files changed (2) hide show
  1. app.py +177 -10
  2. requirements.txt +1 -0
app.py CHANGED
@@ -4,6 +4,7 @@ import tempfile
4
  import time
5
  import logging
6
  import gc
 
7
  from dataclasses import dataclass
8
  from typing import Optional, Tuple, List, Any, Dict
9
  from contextlib import contextmanager
@@ -12,11 +13,16 @@ import gradio as gr
12
  import torch
13
  import psutil
14
  from dotenv import load_dotenv
 
 
 
 
 
15
 
16
  load_dotenv()
17
 
18
- # Audio preprocessing not available in Hugging Face Spaces deployment
19
- PREPROCESSING_AVAILABLE = False
20
 
21
 
22
  def get_env_or_secret(key: str, default: Optional[str] = None) -> Optional[str]:
@@ -41,8 +47,143 @@ class PreprocessingConfig:
41
 
42
  normalize_format: bool = True
43
  normalize_volume: bool = True
44
- reduce_noise: bool = False
45
- remove_silence: bool = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
 
48
  def load_asr_pipeline(
@@ -208,6 +349,23 @@ def transcribe_local(
208
  if not os.path.exists(audio_path):
209
  raise FileNotFoundError(f"Audio file not found: {audio_path}")
210
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  # Load ASR pipeline with performance monitoring
212
  start_time = time.time()
213
 
@@ -262,10 +420,10 @@ def transcribe_local(
262
  try:
263
  # Primary inference attempt with safe parameters
264
  if asr_kwargs:
265
- result = asr(audio_path, **asr_kwargs)
266
  else:
267
  # Fallback to no parameters if all failed
268
- result = asr(audio_path)
269
 
270
  inference_time = time.time() - inference_start
271
  memory_after = psutil.Process().memory_info().rss / 1024 / 1024 # MB
@@ -295,7 +453,7 @@ def transcribe_local(
295
 
296
  try:
297
  inference_start = time.time()
298
- result = asr(audio_path) # No parameters at all
299
  inference_time = time.time() - inference_start
300
  memory_used = 0 # Reset memory tracking
301
 
@@ -313,6 +471,14 @@ def transcribe_local(
313
  torch.cuda.empty_cache()
314
  gc.collect()
315
 
 
 
 
 
 
 
 
 
316
  # Return results with performance metrics
317
  meta = {
318
  "device": device_str,
@@ -320,6 +486,7 @@ def transcribe_local(
320
  "inference_time": inference_time,
321
  "memory_used_mb": memory_used,
322
  "model_type": "original" if model_id == base_model_id else "fine-tuned",
 
323
  }
324
 
325
  return {"result": result, "meta": meta}
@@ -386,8 +553,8 @@ def transcribe_comparison(audio_file):
386
  error_msg = "❌ Modelli non configurati. Impostare HF_MODEL_ID e BASE_WHISPER_MODEL_ID nelle variabili d'ambiente"
387
  return error_msg, error_msg
388
 
389
- # Preprocessing sempre attivo (nascosto all'utente)
390
- # Non viene più utilizzato nel codice ma potrebbe servire per future implementazioni
391
 
392
  # Fixed settings optimized for medical transcription
393
  language = "it" # Always Italian for ScribeAId
@@ -540,7 +707,7 @@ def create_interface():
540
  - Modello originale: `{base_model_id}`
541
  - Modello fine-tuned: `{model_id}`
542
  - Lingua: Italiano (it)
543
- - Preprocessing audio: ottimizzato per registrazioni mediche
544
  """)
545
 
546
  gr.Markdown("---")
 
4
  import time
5
  import logging
6
  import gc
7
+ import io
8
  from dataclasses import dataclass
9
  from typing import Optional, Tuple, List, Any, Dict
10
  from contextlib import contextmanager
 
13
  import torch
14
  import psutil
15
  from dotenv import load_dotenv
16
+ import numpy as np
17
+ from pydub import AudioSegment
18
+ from pydub.silence import split_on_silence
19
+ import soundfile as sf
20
+ import noisereduce
21
 
22
  load_dotenv()
23
 
24
+ # Audio preprocessing available with required dependencies
25
+ PREPROCESSING_AVAILABLE = True
26
 
27
 
28
  def get_env_or_secret(key: str, default: Optional[str] = None) -> Optional[str]:
 
47
 
48
  normalize_format: bool = True
49
  normalize_volume: bool = True
50
+ reduce_noise: bool = True
51
+ remove_silence: bool = True
52
+
53
+
54
+ def normalize_audio(audio_bytes: bytes) -> bytes:
55
+ """
56
+ Converte un chunk audio in bytes nel formato standard per Whisper.
57
+ (16kHz, mono, WAV PCM)
58
+ """
59
+ # Carica i bytes in pydub usando un file in memoria (BytesIO)
60
+ audio_segment = AudioSegment.from_file(io.BytesIO(audio_bytes))
61
+
62
+ # 1. Imposta la frequenza di campionamento a 16kHz
63
+ audio_segment = audio_segment.set_frame_rate(16000)
64
+ # 2. Converte in mono
65
+ audio_segment = audio_segment.set_channels(1)
66
+ # 3. Assicura che il campione sia a 2 bytes (16-bit), standard per WAV
67
+ audio_segment = audio_segment.set_sample_width(2)
68
+
69
+ # Esporta i bytes processati in formato WAV
70
+ buffer = io.BytesIO()
71
+ audio_segment.export(buffer, format="wav")
72
+ return buffer.getvalue()
73
+
74
+
75
+ def normalize_volume(audio_bytes: bytes) -> bytes:
76
+ """
77
+ Normalizza il volume di un chunk audio WAV.
78
+ """
79
+ # Carica l'audio
80
+ audio_segment = AudioSegment.from_wav(io.BytesIO(audio_bytes))
81
+
82
+ # Normalizza l'audio. Porta il picco massimo a -1.0 dBFS
83
+ # Il valore di headroom è una buona pratica per evitare clipping
84
+ normalized_segment = audio_segment.normalize(headroom=0.1)
85
+
86
+ buffer = io.BytesIO()
87
+ normalized_segment.export(buffer, format="wav")
88
+ return buffer.getvalue()
89
+
90
+
91
+ def reduce_background_noise(audio_bytes: bytes) -> bytes:
92
+ """
93
+ Riduce il rumore di fondo da un chunk audio WAV.
94
+ """
95
+ # Leggi i dati audio dai bytes
96
+ buffer_read = io.BytesIO(audio_bytes)
97
+ rate, data = sf.read(buffer_read)
98
+
99
+ # Assicura che l'audio sia mono per la riduzione
100
+ if data.ndim > 1:
101
+ data = np.mean(data, axis=1)
102
+
103
+ # Esegui la riduzione del rumore
104
+ reduced_noise_data = noisereduce.reduce_noise(y=data, sr=rate)
105
+
106
+ # Scrivi i dati processati in un nuovo buffer di bytes
107
+ buffer_write = io.BytesIO()
108
+ sf.write(buffer_write, reduced_noise_data, rate, format="wav")
109
+ return buffer_write.getvalue()
110
+
111
+
112
+ def remove_silence(audio_bytes: bytes) -> bytes:
113
+ """
114
+ Rimuove i segmenti di silenzio da un chunk audio in formato WAV.
115
+ """
116
+
117
+ audio_segment = AudioSegment.from_wav(io.BytesIO(audio_bytes))
118
+
119
+ chunks = split_on_silence(
120
+ audio_segment,
121
+ min_silence_len=100,
122
+ silence_thresh=-35,
123
+ keep_silence=80, # Mantiene un piccolo silenzio tra i chunk
124
+ )
125
+
126
+ if not chunks:
127
+ # Se non trova parlato, restituisce bytes vuoti
128
+ return b""
129
+
130
+ # Unisce di nuovo i chunk in un unico segmento
131
+ processed_segment = sum(chunks, AudioSegment.empty())
132
+
133
+ buffer = io.BytesIO()
134
+ processed_segment.export(buffer, format="wav")
135
+ return buffer.getvalue()
136
+
137
+
138
+ def preprocess_audio_pipeline(audio_path: str) -> str:
139
+ """
140
+ Applica la pipeline completa di preprocessing audio.
141
+ Restituisce il path del file audio preprocessato.
142
+ """
143
+ logger = logging.getLogger(__name__)
144
+ logger.info("Avvio pipeline di preprocessing audio")
145
+
146
+ try:
147
+ # Leggi il file audio originale
148
+ with open(audio_path, "rb") as f:
149
+ audio_bytes = f.read()
150
+
151
+ # Applica tutte le fasi di preprocessing in sequenza
152
+ logger.info("1. Normalizzazione formato audio...")
153
+ audio_bytes = normalize_audio(audio_bytes)
154
+
155
+ logger.info("2. Normalizzazione volume...")
156
+ audio_bytes = normalize_volume(audio_bytes)
157
+
158
+ logger.info("3. Riduzione rumore di fondo...")
159
+ audio_bytes = reduce_background_noise(audio_bytes)
160
+
161
+ logger.info("4. Rimozione silenzi...")
162
+ audio_bytes = remove_silence(audio_bytes)
163
+
164
+ # Se l'audio è vuoto dopo la rimozione del silenzio, usa l'audio originale
165
+ if not audio_bytes:
166
+ logger.warning(
167
+ "Audio vuoto dopo rimozione silenzi, utilizzo audio originale"
168
+ )
169
+ with open(audio_path, "rb") as f:
170
+ audio_bytes = f.read()
171
+ # Applica solo normalizzazione formato e volume
172
+ audio_bytes = normalize_audio(audio_bytes)
173
+ audio_bytes = normalize_volume(audio_bytes)
174
+
175
+ # Salva l'audio preprocessato in un file temporaneo
176
+ with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as temp_file:
177
+ temp_file.write(audio_bytes)
178
+ preprocessed_path = temp_file.name
179
+
180
+ logger.info(f"Preprocessing completato: {preprocessed_path}")
181
+ return preprocessed_path
182
+
183
+ except Exception as e:
184
+ logger.error(f"Errore durante preprocessing: {e}")
185
+ logger.info("Utilizzo audio originale senza preprocessing")
186
+ return audio_path
187
 
188
 
189
  def load_asr_pipeline(
 
349
  if not os.path.exists(audio_path):
350
  raise FileNotFoundError(f"Audio file not found: {audio_path}")
351
 
352
+ # Apply audio preprocessing pipeline
353
+ preprocessed_audio_path = audio_path
354
+ if PREPROCESSING_AVAILABLE:
355
+ try:
356
+ logger.info("Applicazione preprocessing audio...")
357
+ preprocessed_audio_path = preprocess_audio_pipeline(audio_path)
358
+ logger.info(
359
+ f"Preprocessing completato. File processato: {os.path.basename(preprocessed_audio_path)}"
360
+ )
361
+ except Exception as e:
362
+ logger.warning(
363
+ f"Errore durante preprocessing, utilizzo audio originale: {e}"
364
+ )
365
+ preprocessed_audio_path = audio_path
366
+ else:
367
+ logger.info("Preprocessing audio non disponibile, utilizzo audio originale")
368
+
369
  # Load ASR pipeline with performance monitoring
370
  start_time = time.time()
371
 
 
420
  try:
421
  # Primary inference attempt with safe parameters
422
  if asr_kwargs:
423
+ result = asr(preprocessed_audio_path, **asr_kwargs)
424
  else:
425
  # Fallback to no parameters if all failed
426
+ result = asr(preprocessed_audio_path)
427
 
428
  inference_time = time.time() - inference_start
429
  memory_after = psutil.Process().memory_info().rss / 1024 / 1024 # MB
 
453
 
454
  try:
455
  inference_start = time.time()
456
+ result = asr(preprocessed_audio_path) # No parameters at all
457
  inference_time = time.time() - inference_start
458
  memory_used = 0 # Reset memory tracking
459
 
 
471
  torch.cuda.empty_cache()
472
  gc.collect()
473
 
474
+ # Cleanup temporary preprocessed file if it was created
475
+ if preprocessed_audio_path != audio_path:
476
+ try:
477
+ os.unlink(preprocessed_audio_path)
478
+ logger.info("File audio preprocessato temporaneo rimosso")
479
+ except Exception as e:
480
+ logger.warning(f"Errore rimozione file temporaneo: {e}")
481
+
482
  # Return results with performance metrics
483
  meta = {
484
  "device": device_str,
 
486
  "inference_time": inference_time,
487
  "memory_used_mb": memory_used,
488
  "model_type": "original" if model_id == base_model_id else "fine-tuned",
489
+ "preprocessing_applied": preprocessed_audio_path != audio_path,
490
  }
491
 
492
  return {"result": result, "meta": meta}
 
553
  error_msg = "❌ Modelli non configurati. Impostare HF_MODEL_ID e BASE_WHISPER_MODEL_ID nelle variabili d'ambiente"
554
  return error_msg, error_msg
555
 
556
+ # Preprocessing sempre attivo: normalizzazione formato, volume, riduzione rumore, rimozione silenzi
557
+ # Viene applicato automaticamente prima della trascrizione con entrambi i modelli
558
 
559
  # Fixed settings optimized for medical transcription
560
  language = "it" # Always Italian for ScribeAId
 
707
  - Modello originale: `{base_model_id}`
708
  - Modello fine-tuned: `{model_id}`
709
  - Lingua: Italiano (it)
710
+ - Preprocessing audio: **ATTIVO** (normalizzazione, riduzione rumore, rimozione silenzi)
711
  """)
712
 
713
  gr.Markdown("---")
requirements.txt CHANGED
@@ -12,3 +12,4 @@ psutil>=5.9.0
12
  python-dotenv>=1.0.0
13
  datasets>=2.14.0
14
  huggingface-hub>=0.17.0
 
 
12
  python-dotenv>=1.0.0
13
  datasets>=2.14.0
14
  huggingface-hub>=0.17.0
15
+ noisereduce>=3.0.0