yashvshetty commited on
Commit
a58d6dd
·
1 Parent(s): 4e6fa5a

Pin MedASR to Dec 22 revision + transformers==4.47.1

Browse files
Files changed (2) hide show
  1. backend/models/medasr.py +89 -43
  2. requirements.txt +1 -1
backend/models/medasr.py CHANGED
@@ -7,12 +7,9 @@ from pathlib import Path
7
 
8
  import librosa
9
  try:
10
- import torch
11
- from transformers import AutoProcessor, AutoModelForCTC
12
- except ModuleNotFoundError:
13
- torch = None
14
- AutoProcessor = None
15
- AutoModelForCTC = None
16
 
17
  from backend.config import get_settings
18
  from backend.errors import ModelExecutionError
@@ -21,54 +18,90 @@ from backend.schemas import Transcript
21
 
22
 
23
  class MedASRModel:
24
- """Load and run MedASR speech recognition or a deterministic mock implementation."""
 
 
 
 
 
 
 
25
 
26
  def __init__(self, model_manager: ModelManager | None = None) -> None:
 
 
 
 
 
 
 
 
27
  self.settings = get_settings()
28
  self.model_manager = model_manager or ModelManager()
29
- self._model = None
30
- self._processor = None
31
- self._device = "cpu"
32
 
33
  @property
34
  def is_mock_mode(self) -> bool:
 
 
 
 
 
 
 
 
35
  return self.settings.MEDASR_MODEL_ID.lower() == "mock"
36
 
37
  def load_model(self) -> None:
 
 
 
 
 
 
 
 
38
  if self.is_mock_mode:
39
- self._model = "mock"
40
- self.model_manager.register_model("medasr", self._model)
41
  return
42
 
43
- if self._model is not None:
44
  return
45
 
46
- if AutoModelForCTC is None:
47
  raise ModelExecutionError("transformers is required for non-mock MedASR mode")
48
 
49
  device = "cuda:0"
50
  if self.model_manager.check_gpu()["vram_total_bytes"] == 0:
51
  device = "cpu"
52
 
53
- model_id = self.settings.MEDASR_MODEL_ID
54
-
55
  try:
56
- self._processor = AutoProcessor.from_pretrained(model_id, trust_remote_code=True)
57
- self._model = AutoModelForCTC.from_pretrained(model_id, trust_remote_code=True)
58
- self._model = self._model.to(device)
59
- self._model.eval()
60
- self._device = device
 
61
  except Exception as exc:
62
  raise ModelExecutionError(f"Failed to load MedASR model: {exc}") from exc
63
 
64
- self.model_manager.register_model("medasr", self._model)
65
 
66
  def transcribe(self, audio_path: str) -> Transcript:
 
 
 
 
 
 
 
 
67
  source = Path(audio_path)
68
  if not source.exists():
69
  raise ModelExecutionError(f"Audio path not found: {source}")
70
 
71
- if self._model is None:
72
  self.load_model()
73
 
74
  if self.is_mock_mode:
@@ -80,30 +113,30 @@ class MedASRModel:
80
  duration_s = float(librosa.get_duration(y=waveform, sr=16000))
81
 
82
  try:
83
- inputs = self._processor(
84
  waveform,
85
- sampling_rate=16000,
86
- return_tensors="pt",
87
- padding=True,
 
88
  )
89
- inputs = inputs.to(self._device)
90
-
91
- with torch.no_grad():
92
- outputs = self._model.generate(**inputs)
93
- transcript_text = self._processor.batch_decode(outputs, skip_special_tokens=True)[0]
94
-
95
- # Clean up special tokens that may remain
96
- import re
97
- transcript_text = transcript_text.replace("<epsilon>", "")
98
- transcript_text = transcript_text.replace("</s>", "").replace("<s>", "")
99
- transcript_text = re.sub(r'\s+', ' ', transcript_text).strip()
100
-
101
  except Exception as exc:
102
  raise ModelExecutionError(f"MedASR inference failed: {exc}") from exc
103
 
 
104
  return self._make_transcript(source, transcript_text, duration_s)
105
 
106
  def _make_transcript(self, audio_path: Path, text: str, duration_s: float) -> Transcript:
 
 
 
 
 
 
 
 
 
 
107
  now = datetime.now(tz=timezone.utc).isoformat()
108
  consultation_id = audio_path.stem
109
  return Transcript(
@@ -116,20 +149,33 @@ class MedASRModel:
116
 
117
  @staticmethod
118
  def _duration(audio_path: Path) -> float:
 
 
 
 
 
 
 
 
119
  waveform, sample_rate = librosa.load(audio_path, sr=16000, mono=True)
120
  return float(librosa.get_duration(y=waveform, sr=sample_rate))
121
 
122
  @staticmethod
123
  def _get_mock_text(audio_path: Path) -> str:
 
 
 
 
 
 
 
 
124
  transcript_map = {
125
  "mrs_thompson": Path("data/demo/mrs_thompson_transcript.txt"),
126
  "mr_okafor": Path("data/demo/mr_okafor_transcript.txt"),
127
  "ms_patel": Path("data/demo/ms_patel_transcript.txt"),
128
- "mr_williams": Path("data/demo/mr_williams_transcript.txt"),
129
- "mrs_khan": Path("data/demo/mrs_khan_transcript.txt"),
130
  }
131
  for key, transcript_path in transcript_map.items():
132
  if key in audio_path.stem:
133
- if transcript_path.exists():
134
- return transcript_path.read_text(encoding="utf-8").strip()
135
  return "Mock transcript placeholder for non-demo audio input."
 
7
 
8
  import librosa
9
  try:
10
+ from transformers import pipeline
11
+ except ModuleNotFoundError: # pragma: no cover - mock mode support
12
+ pipeline = None
 
 
 
13
 
14
  from backend.config import get_settings
15
  from backend.errors import ModelExecutionError
 
18
 
19
 
20
  class MedASRModel:
21
+ """Load and run MedASR speech recognition or a deterministic mock implementation.
22
+
23
+ Args:
24
+ model_manager (ModelManager | None): Optional shared model registry manager.
25
+
26
+ Returns:
27
+ None: Initialised model wrapper with lazy-loaded pipeline.
28
+ """
29
 
30
  def __init__(self, model_manager: ModelManager | None = None) -> None:
31
+ """Initialise MedASR wrapper.
32
+
33
+ Args:
34
+ model_manager (ModelManager | None): Optional model manager instance.
35
+
36
+ Returns:
37
+ None: Sets internal settings and model state.
38
+ """
39
  self.settings = get_settings()
40
  self.model_manager = model_manager or ModelManager()
41
+ self._pipeline = None
 
 
42
 
43
  @property
44
  def is_mock_mode(self) -> bool:
45
+ """Return whether MedASR should operate in deterministic mock mode.
46
+
47
+ Args:
48
+ None: Reads current settings.
49
+
50
+ Returns:
51
+ bool: True when configured model id is "mock".
52
+ """
53
  return self.settings.MEDASR_MODEL_ID.lower() == "mock"
54
 
55
  def load_model(self) -> None:
56
+ """Load the MedASR transformer pipeline unless running in mock mode.
57
+
58
+ Args:
59
+ None: Uses settings for model id and device selection.
60
+
61
+ Returns:
62
+ None: Caches loaded pipeline instance.
63
+ """
64
  if self.is_mock_mode:
65
+ self._pipeline = "mock"
66
+ self.model_manager.register_model("medasr", self._pipeline)
67
  return
68
 
69
+ if self._pipeline is not None:
70
  return
71
 
72
+ if pipeline is None:
73
  raise ModelExecutionError("transformers is required for non-mock MedASR mode")
74
 
75
  device = "cuda:0"
76
  if self.model_manager.check_gpu()["vram_total_bytes"] == 0:
77
  device = "cpu"
78
 
 
 
79
  try:
80
+ self._pipeline = pipeline(
81
+ "automatic-speech-recognition",
82
+ model=self.settings.MEDASR_MODEL_ID,
83
+ revision="2625be4f1377ac544b451c6938eaf955c19a9c38",
84
+ device=device,
85
+ )
86
  except Exception as exc:
87
  raise ModelExecutionError(f"Failed to load MedASR model: {exc}") from exc
88
 
89
+ self.model_manager.register_model("medasr", self._pipeline)
90
 
91
  def transcribe(self, audio_path: str) -> Transcript:
92
+ """Transcribe audio input into a Transcript schema object.
93
+
94
+ Args:
95
+ audio_path (str): Path to 16kHz mono audio WAV.
96
+
97
+ Returns:
98
+ Transcript: Structured transcript result.
99
+ """
100
  source = Path(audio_path)
101
  if not source.exists():
102
  raise ModelExecutionError(f"Audio path not found: {source}")
103
 
104
+ if self._pipeline is None:
105
  self.load_model()
106
 
107
  if self.is_mock_mode:
 
113
  duration_s = float(librosa.get_duration(y=waveform, sr=16000))
114
 
115
  try:
116
+ result = self._pipeline(
117
  waveform,
118
+ chunk_length_s=20,
119
+ stride_length_s=(4, 2),
120
+ return_timestamps=True,
121
+ generate_kwargs={"language": "en", "task": "transcribe"},
122
  )
 
 
 
 
 
 
 
 
 
 
 
 
123
  except Exception as exc:
124
  raise ModelExecutionError(f"MedASR inference failed: {exc}") from exc
125
 
126
+ transcript_text = str(result.get("text", "")).strip()
127
  return self._make_transcript(source, transcript_text, duration_s)
128
 
129
  def _make_transcript(self, audio_path: Path, text: str, duration_s: float) -> Transcript:
130
+ """Build a Transcript object from model output values.
131
+
132
+ Args:
133
+ audio_path (Path): Source audio path.
134
+ text (str): Transcript text.
135
+ duration_s (float): Audio duration seconds.
136
+
137
+ Returns:
138
+ Transcript: Pydantic transcript model.
139
+ """
140
  now = datetime.now(tz=timezone.utc).isoformat()
141
  consultation_id = audio_path.stem
142
  return Transcript(
 
149
 
150
  @staticmethod
151
  def _duration(audio_path: Path) -> float:
152
+ """Compute audio duration in seconds using librosa.
153
+
154
+ Args:
155
+ audio_path (Path): Audio file path.
156
+
157
+ Returns:
158
+ float: Duration in seconds.
159
+ """
160
  waveform, sample_rate = librosa.load(audio_path, sr=16000, mono=True)
161
  return float(librosa.get_duration(y=waveform, sr=sample_rate))
162
 
163
  @staticmethod
164
  def _get_mock_text(audio_path: Path) -> str:
165
+ """Return ground-truth transcript for known demo files in mock mode.
166
+
167
+ Args:
168
+ audio_path (Path): Audio file path used for lookup.
169
+
170
+ Returns:
171
+ str: Transcript text from fixture file or fallback placeholder.
172
+ """
173
  transcript_map = {
174
  "mrs_thompson": Path("data/demo/mrs_thompson_transcript.txt"),
175
  "mr_okafor": Path("data/demo/mr_okafor_transcript.txt"),
176
  "ms_patel": Path("data/demo/ms_patel_transcript.txt"),
 
 
177
  }
178
  for key, transcript_path in transcript_map.items():
179
  if key in audio_path.stem:
180
+ return transcript_path.read_text(encoding="utf-8").strip()
 
181
  return "Mock transcript placeholder for non-demo audio input."
requirements.txt CHANGED
@@ -1,5 +1,5 @@
1
  torch==2.4.1
2
- transformers @ git+https://github.com/huggingface/transformers.git@65dc261512cbdb1ee72b88ae5b222f2605aad8e5
3
  bitsandbytes>=0.46.1
4
  accelerate>=1.2.1
5
  gradio>=5.10.0
 
1
  torch==2.4.1
2
+ transformers==4.47.1
3
  bitsandbytes>=0.46.1
4
  accelerate>=1.2.1
5
  gradio>=5.10.0