yashvshetty commited on
Commit
be26c14
·
1 Parent(s): 2b19796

Google transformers commit + manual CTC decode + revision pin

Browse files
Files changed (2) hide show
  1. backend/models/medasr.py +69 -87
  2. requirements.txt +1 -1
backend/models/medasr.py CHANGED
@@ -2,14 +2,18 @@
2
 
3
  from __future__ import annotations
4
 
 
5
  from datetime import datetime, timezone
6
  from pathlib import Path
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,91 +22,62 @@ from backend.schemas import Transcript
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
  trust_remote_code=True,
 
86
  )
 
 
 
87
  except Exception as exc:
88
  raise ModelExecutionError(f"Failed to load MedASR model: {exc}") from exc
89
 
90
- self.model_manager.register_model("medasr", self._pipeline)
91
 
92
  def transcribe(self, audio_path: str) -> Transcript:
93
- """Transcribe audio input into a Transcript schema object.
94
-
95
- Args:
96
- audio_path (str): Path to 16kHz mono audio WAV.
97
-
98
- Returns:
99
- Transcript: Structured transcript result.
100
- """
101
  source = Path(audio_path)
102
  if not source.exists():
103
  raise ModelExecutionError(f"Audio path not found: {source}")
104
 
105
- if self._pipeline is None:
106
  self.load_model()
107
 
108
  if self.is_mock_mode:
@@ -114,30 +89,50 @@ class MedASRModel:
114
  duration_s = float(librosa.get_duration(y=waveform, sr=16000))
115
 
116
  try:
117
- result = self._pipeline(
118
  waveform,
119
- chunk_length_s=20,
120
- stride_length_s=(4, 2),
121
- return_timestamps=True,
122
- generate_kwargs={"language": "en", "task": "transcribe"},
123
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  except Exception as exc:
125
  raise ModelExecutionError(f"MedASR inference failed: {exc}") from exc
126
 
127
- transcript_text = str(result.get("text", "")).strip()
128
  return self._make_transcript(source, transcript_text, duration_s)
129
 
130
  def _make_transcript(self, audio_path: Path, text: str, duration_s: float) -> Transcript:
131
- """Build a Transcript object from model output values.
132
-
133
- Args:
134
- audio_path (Path): Source audio path.
135
- text (str): Transcript text.
136
- duration_s (float): Audio duration seconds.
137
-
138
- Returns:
139
- Transcript: Pydantic transcript model.
140
- """
141
  now = datetime.now(tz=timezone.utc).isoformat()
142
  consultation_id = audio_path.stem
143
  return Transcript(
@@ -150,33 +145,20 @@ class MedASRModel:
150
 
151
  @staticmethod
152
  def _duration(audio_path: Path) -> float:
153
- """Compute audio duration in seconds using librosa.
154
-
155
- Args:
156
- audio_path (Path): Audio file path.
157
-
158
- Returns:
159
- float: Duration in seconds.
160
- """
161
  waveform, sample_rate = librosa.load(audio_path, sr=16000, mono=True)
162
  return float(librosa.get_duration(y=waveform, sr=sample_rate))
163
 
164
  @staticmethod
165
  def _get_mock_text(audio_path: Path) -> str:
166
- """Return ground-truth transcript for known demo files in mock mode.
167
-
168
- Args:
169
- audio_path (Path): Audio file path used for lookup.
170
-
171
- Returns:
172
- str: Transcript text from fixture file or fallback placeholder.
173
- """
174
  transcript_map = {
175
  "mrs_thompson": Path("data/demo/mrs_thompson_transcript.txt"),
176
  "mr_okafor": Path("data/demo/mr_okafor_transcript.txt"),
177
  "ms_patel": Path("data/demo/ms_patel_transcript.txt"),
 
 
178
  }
179
  for key, transcript_path in transcript_map.items():
180
  if key in audio_path.stem:
181
- return transcript_path.read_text(encoding="utf-8").strip()
 
182
  return "Mock transcript placeholder for non-demo audio input."
 
2
 
3
  from __future__ import annotations
4
 
5
+ import re
6
  from datetime import datetime, timezone
7
  from pathlib import Path
8
 
9
  import librosa
10
  try:
11
+ import torch
12
+ from transformers import AutoProcessor, AutoModelForCTC
13
+ except ModuleNotFoundError:
14
+ torch = None
15
+ AutoProcessor = None
16
+ AutoModelForCTC = None
17
 
18
  from backend.config import get_settings
19
  from backend.errors import ModelExecutionError
 
22
 
23
 
24
  class MedASRModel:
25
+ """Load and run MedASR speech recognition or a deterministic mock implementation."""
 
 
 
 
 
 
 
26
 
27
  def __init__(self, model_manager: ModelManager | None = None) -> None:
 
 
 
 
 
 
 
 
28
  self.settings = get_settings()
29
  self.model_manager = model_manager or ModelManager()
30
+ self._model = None
31
+ self._processor = None
32
+ self._device = "cpu"
33
 
34
  @property
35
  def is_mock_mode(self) -> bool:
 
 
 
 
 
 
 
 
36
  return self.settings.MEDASR_MODEL_ID.lower() == "mock"
37
 
38
  def load_model(self) -> None:
 
 
 
 
 
 
 
 
39
  if self.is_mock_mode:
40
+ self._model = "mock"
41
+ self.model_manager.register_model("medasr", self._model)
42
  return
43
 
44
+ if self._model is not None:
45
  return
46
 
47
+ if AutoModelForCTC is None:
48
  raise ModelExecutionError("transformers is required for non-mock MedASR mode")
49
 
50
  device = "cuda:0"
51
  if self.model_manager.check_gpu()["vram_total_bytes"] == 0:
52
  device = "cpu"
53
 
54
+ model_id = self.settings.MEDASR_MODEL_ID
55
+
56
  try:
57
+ self._processor = AutoProcessor.from_pretrained(
58
+ model_id,
59
+ trust_remote_code=True,
60
  revision="2625be4f1377ac544b451c6938eaf955c19a9c38",
61
+ )
62
+ self._model = AutoModelForCTC.from_pretrained(
63
+ model_id,
64
  trust_remote_code=True,
65
+ revision="2625be4f1377ac544b451c6938eaf955c19a9c38",
66
  )
67
+ self._model = self._model.to(device)
68
+ self._model.eval()
69
+ self._device = device
70
  except Exception as exc:
71
  raise ModelExecutionError(f"Failed to load MedASR model: {exc}") from exc
72
 
73
+ self.model_manager.register_model("medasr", self._model)
74
 
75
  def transcribe(self, audio_path: str) -> Transcript:
 
 
 
 
 
 
 
 
76
  source = Path(audio_path)
77
  if not source.exists():
78
  raise ModelExecutionError(f"Audio path not found: {source}")
79
 
80
+ if self._model is None:
81
  self.load_model()
82
 
83
  if self.is_mock_mode:
 
89
  duration_s = float(librosa.get_duration(y=waveform, sr=16000))
90
 
91
  try:
92
+ inputs = self._processor(
93
  waveform,
94
+ sampling_rate=16000,
95
+ return_tensors="pt",
96
+ padding=True,
 
97
  )
98
+ inputs = inputs.to(self._device)
99
+
100
+ with torch.no_grad():
101
+ output = self._model(**inputs)
102
+ logits = output.logits
103
+
104
+ # Greedy CTC decoding
105
+ predicted_ids = torch.argmax(logits, dim=-1)[0].tolist()
106
+
107
+ # CTC collapse: remove consecutive duplicates
108
+ collapsed = []
109
+ prev = None
110
+ for token_id in predicted_ids:
111
+ if token_id != prev:
112
+ collapsed.append(token_id)
113
+ prev = token_id
114
+
115
+ # Remove blank token
116
+ blank_id = getattr(self._model.config, 'ctc_blank_id', 0)
117
+ collapsed = [t for t in collapsed if t != blank_id]
118
+
119
+ if collapsed:
120
+ collapsed_tensor = torch.tensor([collapsed], dtype=torch.long)
121
+ transcript_text = self._processor.batch_decode(collapsed_tensor)[0]
122
+ else:
123
+ transcript_text = ""
124
+
125
+ # Clean up
126
+ transcript_text = transcript_text.replace("<epsilon>", "")
127
+ transcript_text = transcript_text.replace("</s>", "").replace("<s>", "")
128
+ transcript_text = re.sub(r'\s+', ' ', transcript_text).strip()
129
+
130
  except Exception as exc:
131
  raise ModelExecutionError(f"MedASR inference failed: {exc}") from exc
132
 
 
133
  return self._make_transcript(source, transcript_text, duration_s)
134
 
135
  def _make_transcript(self, audio_path: Path, text: str, duration_s: float) -> Transcript:
 
 
 
 
 
 
 
 
 
 
136
  now = datetime.now(tz=timezone.utc).isoformat()
137
  consultation_id = audio_path.stem
138
  return Transcript(
 
145
 
146
  @staticmethod
147
  def _duration(audio_path: Path) -> float:
 
 
 
 
 
 
 
 
148
  waveform, sample_rate = librosa.load(audio_path, sr=16000, mono=True)
149
  return float(librosa.get_duration(y=waveform, sr=sample_rate))
150
 
151
  @staticmethod
152
  def _get_mock_text(audio_path: Path) -> str:
 
 
 
 
 
 
 
 
153
  transcript_map = {
154
  "mrs_thompson": Path("data/demo/mrs_thompson_transcript.txt"),
155
  "mr_okafor": Path("data/demo/mr_okafor_transcript.txt"),
156
  "ms_patel": Path("data/demo/ms_patel_transcript.txt"),
157
+ "mr_williams": Path("data/demo/mr_williams_transcript.txt"),
158
+ "mrs_khan": Path("data/demo/mrs_khan_transcript.txt"),
159
  }
160
  for key, transcript_path in transcript_map.items():
161
  if key in audio_path.stem:
162
+ if transcript_path.exists():
163
+ return transcript_path.read_text(encoding="utf-8").strip()
164
  return "Mock transcript placeholder for non-demo audio input."
requirements.txt CHANGED
@@ -1,5 +1,5 @@
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
 
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