Automatic Speech Recognition
Safetensors
Chinese
whisper
gpric024 commited on
Commit
a4e4eb4
·
verified ·
1 Parent(s): b1ef1ed

fix: allow endpoint callers to select transcription language

Browse files
Files changed (3) hide show
  1. README.md +164 -133
  2. handler.py +213 -192
  3. tests/test_handler.py +83 -0
README.md CHANGED
@@ -1,137 +1,168 @@
1
- ---
2
- license: apache-2.0
3
- datasets:
4
- - AImpower/MandarinStutteredSpeech
5
- language:
6
- - zh
7
- metrics:
8
- - cer
9
- base_model:
10
- - openai/whisper-large-v2
11
- pipeline_tag: automatic-speech-recognition
12
- ---
13
- # Model Card: AImpower/StutteredSpeechASR
14
-
15
- This model is a version of OpenAI's `whisper-large-v2` fine-tuned on the **AImpower/MandarinStutteredSpeech** dataset, a grassroots-collected corpus of Mandarin Chinese speech from people who stutter (PWS).
16
-
17
- ## Model Details
18
-
19
- * **Base Model:** `openai/whisper-large-v2`
20
- * **Language:** Mandarin Chinese
21
- * **Fine-tuning Dataset:** [AImpower/MandarinStutteredSpeech](https://huggingface.co/datasets/AImpower/MandarinStutteredSpeech)
22
- * **Fine-tuning Method:** The model was fine-tuned using the LoRA adapter (AdaLora) methodology to preserve speech disfluencies in its transcriptions.
23
- * **Paper:** [Our Collective Voices: The Social and Technical Values of a Grassroots Chinese Stuttered Speech Dataset](https://doi.org/10.1145/3715275.3732179)
24
-
25
- ## Model Description
26
-
27
- This model is specifically adapted to provide more accurate and authentic transcriptions for Mandarin-speaking PWS.
28
- Standard Automatic Speech Recognition (ASR) models often exhibit "fluency bias," where they "smoothen" out or delete stuttered speech patterns like repetitions and interjections.
29
- This model was fine-tuned on literal transcriptions that intentionally preserve these disfluencies.
30
-
31
- The primary goal is to create a more inclusive ASR system that recognizes and respects the natural speech patterns of PWS, reducing deletion errors and improving overall accuracy.
32
-
33
- ## Intended Uses & Limitations
34
-
35
- ### Intended Use
36
-
37
- This model is intended for transcribing conversational Mandarin Chinese speech from individuals who stutter. It's particularly useful for:
38
- * Improving accessibility in speech-to-text applications.
39
- * Linguistic research on stuttered speech.
40
- * Developing more inclusive voice-enabled technologies.
41
-
42
- ### Limitations
43
-
44
- * **Language Specificity:** The model is fine-tuned exclusively on Mandarin Chinese and is not intended for other languages.
45
- * **Data Specificity:** Performance is optimized for speech patterns present in the AImpower/MandarinStutteredSpeech dataset. It may not perform as well on other types of atypical speech or in environments with significant background noise.
46
- * **Variability:** Stuttering is highly variable. While the model shows significant improvements across severity levels, accuracy may still vary between individuals and contexts.
47
-
48
- ---
49
-
50
- ## How to Use
51
-
52
- You can use the model with the `transformers` library. Ensure you have `torch`, `transformers`, and `librosa` installed.
53
-
54
- ```python
55
- from transformers import AutoModelForSpeechSeq2Seq, AutoProcessor
56
- import torch
57
- import librosa
58
-
59
- # Load the fine-tuned model and processor
60
- model_path = "AImpower/StutteredSpeechASR"
61
- model = AutoModelForSpeechSeq2Seq.from_pretrained(model_path)
62
- processor = AutoProcessor.from_pretrained(model_path)
63
-
64
- device = "cuda" if torch.cuda.is_available() else "cpu"
65
- model.to(device)
66
-
67
- # Load an example audio file (replace with your audio file)
68
- audio_input_name = "example_stuttered_speech.wav"
69
- waveform, sampling_rate = librosa.load(audio_input_name, sr=16000)
70
-
71
- # Process the audio and generate transcription
72
- input_features = processor(waveform, sampling_rate=sampling_rate, return_tensors="pt").input_features
73
- input_features = input_features.to(device)
74
-
75
- predicted_ids = model.generate(input_features)
76
- transcription = processor.batch_decode(predicted_ids, skip_special_tokens=True)[0]
77
-
78
- print(f"Transcription: {transcription}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  ```
80
 
81
- -----
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
  ## Training Data
84
-
85
- The model was fine-tuned on the **[AImpower/MandarinStutteredSpeech](https://huggingface.co/datasets/AImpower/MandarinStutteredSpeech)** dataset.
86
- This dataset was created through a community-led, grassroots effort with StammerTalk, an online community for Chinese-speaking PWS.
87
-
88
- * **Size:** The dataset contains nearly 50 hours of speech from 72 adults who stutter.
89
- * **Content:** It includes both unscripted, spontaneous conversations between two PWS and the dictation of 200 voice commands.
90
- * **Transcription:** The training was performed on verbatim (literal) transcriptions that include disfluencies such as word repetitions and interjections, which was a deliberate choice by the community to ensure their speech was represented authentically.
91
-
92
- ## Training Procedure
93
-
94
- * **Data Split:** A three-fold cross-validation approach was used, with data split by participant to ensure robustness. Each fold had a roughly 65:10:25 split for train/dev/test sets, with a balanced representation of mild, moderate, and severe stuttering levels. This model card represents the best-performing fold.
95
- * **Hyperparameters:**
96
- * **Epochs:** 3
97
- * **Learning Rate:** 0.001
98
- * **Optimizer:** AdamW
99
- * **Batch Size:** 16
100
- * **Fine-tuning Method:** AdaLora
101
- * **GPU:** Four NVIDIA A100 80G GPUs
102
- -----
103
-
104
- ## Evaluation Results
105
-
106
- The fine-tuned model demonstrates a substantial improvement in transcription accuracy across all stuttering severity levels compared to the baseline `whisper-large-v2` model.
107
- The key metric used is Character Error Rate (CER), evaluated on literal transcriptions to measure the model's ability to preserve disfluencies.
108
-
109
- | Stuttering Severity | Baseline Whisper CER | Fine-tuned Model CER |
110
- | :------------------ | :------------------- | :------------------- |
111
- | Mild | 16.34% | **5.80%** |
112
- | Moderate | 21.72% | **9.03%** |
113
- | Severe | 49.24% | **20.46%** |
114
-
115
- *(Results from Figure 3 of the paper)*
116
-
117
- Notably, the model achieved a significant reduction in **deletion errors (DEL)**, especially for severe speech (from 26.56% to 2.29%), indicating that it is much more effective at preserving repeated words and phrases instead of omitting them.
118
-
119
- ## Citation
120
-
121
- If you use this model, please cite the original paper:
122
-
123
- ```bibtex
124
- @inproceedings{li2025collective,
125
- author = {Li, Jingjin and Li, Qisheng and Gong, Rong and Wang, Lezhi and Wu, Shaomei},
126
- title = {Our Collective Voices: The Social and Technical Values of a Grassroots Chinese Stuttered Speech Dataset},
127
- year = {2025},
128
- isbn = {9798400714825},
129
- publisher = {Association for Computing Machinery},
130
- address = {New York, NY, USA},
131
- url = {https://doi.org/10.1145/3715275.3732179},
132
- booktitle = {The 2025 ACM Conference on Fairness, Accountability, and Transparency},
133
- pages = {2768–2783},
134
- location = {Athens, Greece},
135
- series = {FAccT '25}
136
- }
137
- ```
 
1
+ ---
2
+ license: apache-2.0
3
+ datasets:
4
+ - AImpower/MandarinStutteredSpeech
5
+ language:
6
+ - zh
7
+ metrics:
8
+ - cer
9
+ base_model:
10
+ - openai/whisper-large-v2
11
+ pipeline_tag: automatic-speech-recognition
12
+ ---
13
+ # Model Card: AImpower/StutteredSpeechASR
14
+
15
+ This model is a version of OpenAI's `whisper-large-v2` fine-tuned on the **AImpower/MandarinStutteredSpeech** dataset, a grassroots-collected corpus of Mandarin Chinese speech from people who stutter (PWS).
16
+
17
+ ## Model Details
18
+
19
+ * **Base Model:** `openai/whisper-large-v2`
20
+ * **Language:** Mandarin Chinese
21
+ * **Fine-tuning Dataset:** [AImpower/MandarinStutteredSpeech](https://huggingface.co/datasets/AImpower/MandarinStutteredSpeech)
22
+ * **Fine-tuning Method:** The model was fine-tuned using the LoRA adapter (AdaLora) methodology to preserve speech disfluencies in its transcriptions.
23
+ * **Paper:** [Our Collective Voices: The Social and Technical Values of a Grassroots Chinese Stuttered Speech Dataset](https://doi.org/10.1145/3715275.3732179)
24
+
25
+ ## Model Description
26
+
27
+ This model is specifically adapted to provide more accurate and authentic transcriptions for Mandarin-speaking PWS.
28
+ Standard Automatic Speech Recognition (ASR) models often exhibit "fluency bias," where they "smoothen" out or delete stuttered speech patterns like repetitions and interjections.
29
+ This model was fine-tuned on literal transcriptions that intentionally preserve these disfluencies.
30
+
31
+ The primary goal is to create a more inclusive ASR system that recognizes and respects the natural speech patterns of PWS, reducing deletion errors and improving overall accuracy.
32
+
33
+ ## Intended Uses & Limitations
34
+
35
+ ### Intended Use
36
+
37
+ This model is intended for transcribing conversational Mandarin Chinese speech from individuals who stutter. It's particularly useful for:
38
+ * Improving accessibility in speech-to-text applications.
39
+ * Linguistic research on stuttered speech.
40
+ * Developing more inclusive voice-enabled technologies.
41
+
42
+ ### Limitations
43
+
44
+ * **Language Specificity:** The model is fine-tuned exclusively on Mandarin Chinese and is not intended for other languages.
45
+ * **Data Specificity:** Performance is optimized for speech patterns present in the AImpower/MandarinStutteredSpeech dataset. It may not perform as well on other types of atypical speech or in environments with significant background noise.
46
+ * **Variability:** Stuttering is highly variable. While the model shows significant improvements across severity levels, accuracy may still vary between individuals and contexts.
47
+
48
+ ---
49
+
50
+ ## How to Use
51
+
52
+ You can use the model with the `transformers` library. Ensure you have `torch`, `transformers`, and `librosa` installed.
53
+
54
+ ```python
55
+ from transformers import AutoModelForSpeechSeq2Seq, AutoProcessor
56
+ import torch
57
+ import librosa
58
+
59
+ # Load the fine-tuned model and processor
60
+ model_path = "AImpower/StutteredSpeechASR"
61
+ model = AutoModelForSpeechSeq2Seq.from_pretrained(model_path)
62
+ processor = AutoProcessor.from_pretrained(model_path)
63
+
64
+ device = "cuda" if torch.cuda.is_available() else "cpu"
65
+ model.to(device)
66
+
67
+ # Load an example audio file (replace with your audio file)
68
+ audio_input_name = "example_stuttered_speech.wav"
69
+ waveform, sampling_rate = librosa.load(audio_input_name, sr=16000)
70
+
71
+ # Process the audio and generate transcription
72
+ input_features = processor(waveform, sampling_rate=sampling_rate, return_tensors="pt").input_features
73
+ input_features = input_features.to(device)
74
+
75
+ predicted_ids = model.generate(input_features)
76
+ transcription = processor.batch_decode(predicted_ids, skip_special_tokens=True)[0]
77
+
78
+ print(f"Transcription: {transcription}")
79
+ ```
80
+
81
+ -----
82
+
83
+ ### Hosted inference endpoint: selecting a language
84
+
85
+ For an endpoint deployed with `handler.py`, send a JSON request with the
86
+ language in `parameters.language`:
87
+
88
+ ```json
89
+ {
90
+ "inputs": "<base64-encoded audio file>",
91
+ "parameters": {"language": "en"}
92
+ }
93
  ```
94
 
95
+ Whisper accepts language codes, names, or language tokens, for example `"en"`,
96
+ `"english"`, or `"<|en|>"` for English and `"zh"` for Mandarin Chinese.
97
+ The endpoint passes the selected language to Whisper with `task="transcribe"`
98
+ so the output stays in the source language rather than translating it.
99
+ The same `parameters` object works with `{"inputs": {"audio": "..."}}`
100
+ and `{"audio": "..."}` requests. Use JSON to select a language; raw binary
101
+ requests do not carry parameters.
102
+
103
+ Omitting `language` or setting it to `null` preserves the model's default
104
+ generation behavior. The checked-in generation configuration leaves the
105
+ language unspecified, allowing Whisper to detect it automatically; the handler
106
+ does not force Mandarin. A language override applies only to its own request.
107
+ Unsupported language strings are rejected by Whisper using the handler's
108
+ existing error response format.
109
+
110
+ Language selection enables cross-language benchmarking, but this model was
111
+ fine-tuned on Mandarin and improvements in other languages have not been
112
+ established. Redeploy the endpoint after merging handler changes to use this option.
113
 
114
  ## Training Data
115
+
116
+ The model was fine-tuned on the **[AImpower/MandarinStutteredSpeech](https://huggingface.co/datasets/AImpower/MandarinStutteredSpeech)** dataset.
117
+ This dataset was created through a community-led, grassroots effort with StammerTalk, an online community for Chinese-speaking PWS.
118
+
119
+ * **Size:** The dataset contains nearly 50 hours of speech from 72 adults who stutter.
120
+ * **Content:** It includes both unscripted, spontaneous conversations between two PWS and the dictation of 200 voice commands.
121
+ * **Transcription:** The training was performed on verbatim (literal) transcriptions that include disfluencies such as word repetitions and interjections, which was a deliberate choice by the community to ensure their speech was represented authentically.
122
+
123
+ ## Training Procedure
124
+
125
+ * **Data Split:** A three-fold cross-validation approach was used, with data split by participant to ensure robustness. Each fold had a roughly 65:10:25 split for train/dev/test sets, with a balanced representation of mild, moderate, and severe stuttering levels. This model card represents the best-performing fold.
126
+ * **Hyperparameters:**
127
+ * **Epochs:** 3
128
+ * **Learning Rate:** 0.001
129
+ * **Optimizer:** AdamW
130
+ * **Batch Size:** 16
131
+ * **Fine-tuning Method:** AdaLora
132
+ * **GPU:** Four NVIDIA A100 80G GPUs
133
+ -----
134
+
135
+ ## Evaluation Results
136
+
137
+ The fine-tuned model demonstrates a substantial improvement in transcription accuracy across all stuttering severity levels compared to the baseline `whisper-large-v2` model.
138
+ The key metric used is Character Error Rate (CER), evaluated on literal transcriptions to measure the model's ability to preserve disfluencies.
139
+
140
+ | Stuttering Severity | Baseline Whisper CER | Fine-tuned Model CER |
141
+ | :------------------ | :------------------- | :------------------- |
142
+ | Mild | 16.34% | **5.80%** |
143
+ | Moderate | 21.72% | **9.03%** |
144
+ | Severe | 49.24% | **20.46%** |
145
+
146
+ *(Results from Figure 3 of the paper)*
147
+
148
+ Notably, the model achieved a significant reduction in **deletion errors (DEL)**, especially for severe speech (from 26.56% to 2.29%), indicating that it is much more effective at preserving repeated words and phrases instead of omitting them.
149
+
150
+ ## Citation
151
+
152
+ If you use this model, please cite the original paper:
153
+
154
+ ```bibtex
155
+ @inproceedings{li2025collective,
156
+ author = {Li, Jingjin and Li, Qisheng and Gong, Rong and Wang, Lezhi and Wu, Shaomei},
157
+ title = {Our Collective Voices: The Social and Technical Values of a Grassroots Chinese Stuttered Speech Dataset},
158
+ year = {2025},
159
+ isbn = {9798400714825},
160
+ publisher = {Association for Computing Machinery},
161
+ address = {New York, NY, USA},
162
+ url = {https://doi.org/10.1145/3715275.3732179},
163
+ booktitle = {The 2025 ACM Conference on Fairness, Accountability, and Transparency},
164
+ pages = {2768–2783},
165
+ location = {Athens, Greece},
166
+ series = {FAccT '25}
167
+ }
168
+ ```
handler.py CHANGED
@@ -1,197 +1,218 @@
1
- """
2
- Custom Inference Handler for StutteredSpeechASR Model
3
- Handles audio input and returns transcriptions for stuttered speech.
4
- """
5
-
6
- import torch
7
- import librosa
8
- import numpy as np
9
- import base64
10
- import io
11
  import logging
12
- from typing import Dict, Any
13
- from transformers import AutoModelForSpeechSeq2Seq, AutoProcessor
14
-
15
- # Configure logging
16
- logging.basicConfig(level=logging.INFO)
17
- logger = logging.getLogger(__name__)
18
-
19
-
20
- class EndpointHandler:
21
- """
22
- Custom handler for StutteredSpeechASR inference endpoint.
23
-
24
- This handler processes audio inputs and returns transcriptions
25
- using the fine-tuned Whisper model for stuttered Mandarin speech.
26
- """
27
-
28
- def __init__(self, path: str = ""):
29
- """
30
- Initialize the handler by loading the model and processor.
31
-
32
- Args:
33
- path: Path to the model directory (provided by Inference Endpoints)
34
- """
35
- logger.info("Initializing StutteredSpeechASR handler...")
36
-
37
- # Determine device and dtype
38
- self.device = "cuda" if torch.cuda.is_available() else "cpu"
39
- self.torch_dtype = torch.float16 if torch.cuda.is_available() else torch.float32
40
-
41
- logger.info(f"Using device: {self.device}")
42
- logger.info(f"Using dtype: {self.torch_dtype}")
43
-
44
- # Load model and processor
45
- try:
46
- self.model = AutoModelForSpeechSeq2Seq.from_pretrained(
47
- path,
48
- torch_dtype=self.torch_dtype
49
- )
50
- self.processor = AutoProcessor.from_pretrained(path)
51
- self.model.to(self.device)
52
- self.model.eval() # Set to evaluation mode
53
-
54
- logger.info("Model and processor loaded successfully!")
55
- except Exception as e:
56
- logger.error(f"Error loading model: {e}")
57
- raise
58
-
59
- def _load_audio_from_bytes(self, audio_bytes: bytes) -> np.ndarray:
60
- """
61
- Load audio from bytes and resample to 16kHz.
62
-
63
- Args:
64
- audio_bytes: Raw audio bytes
65
-
66
- Returns:
67
- Audio waveform as numpy array
68
- """
69
- try:
70
- # Load audio from bytes using librosa
71
- audio_buffer = io.BytesIO(audio_bytes)
72
- waveform, _ = librosa.load(audio_buffer, sr=16000, mono=True)
73
- return waveform
74
- except Exception as e:
75
- logger.error(f"Error loading audio from bytes: {e}")
76
- raise ValueError(f"Failed to load audio: {e}")
77
-
78
- def _load_audio_from_base64(self, base64_string: str) -> np.ndarray:
79
- """
80
- Load audio from base64-encoded string.
81
-
82
- Args:
83
- base64_string: Base64-encoded audio data
84
-
85
- Returns:
86
- Audio waveform as numpy array
87
- """
88
- try:
89
- # Decode base64 string
90
- audio_bytes = base64.b64decode(base64_string)
91
- return self._load_audio_from_bytes(audio_bytes)
92
- except Exception as e:
93
- logger.error(f"Error decoding base64 audio: {e}")
94
- raise ValueError(f"Failed to decode base64 audio: {e}")
95
-
96
- def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]:
97
- """
98
- Process incoming requests and return transcriptions.
99
-
100
- Expected input formats:
101
- 1. {"inputs": "base64_encoded_audio_string"}
102
- 2. {"inputs": {"audio": "base64_encoded_audio_string"}}
 
103
  3. Binary audio data in request body
104
-
105
- Args:
106
- data: Input data dictionary
107
-
108
- Returns:
109
- Dictionary containing transcription and metadata
110
- """
111
- try:
 
 
 
112
  logger.info("Processing inference request...")
113
-
114
- # Extract audio data from various input formats
115
- waveform = None
116
-
117
  if isinstance(data, dict):
118
- # Format 1: {"inputs": "base64_string"}
119
- if "inputs" in data:
120
- inputs = data["inputs"]
121
-
122
- if isinstance(inputs, str):
123
- # Base64-encoded audio
124
- waveform = self._load_audio_from_base64(inputs)
125
-
126
- elif isinstance(inputs, dict):
127
- # Format 2: {"inputs": {"audio": "base64_string"}}
128
- if "audio" in inputs:
129
- waveform = self._load_audio_from_base64(inputs["audio"])
130
- else:
131
- raise ValueError("Missing 'audio' field in inputs dictionary")
132
-
133
- elif isinstance(inputs, bytes):
134
- # Binary audio data
135
- waveform = self._load_audio_from_bytes(inputs)
136
-
137
- else:
138
- raise ValueError(f"Unsupported input type: {type(inputs)}")
139
-
140
- # Direct audio field
141
- elif "audio" in data:
142
- audio_data = data["audio"]
143
- if isinstance(audio_data, str):
144
- waveform = self._load_audio_from_base64(audio_data)
145
- elif isinstance(audio_data, bytes):
146
- waveform = self._load_audio_from_bytes(audio_data)
147
-
148
- else:
149
- raise ValueError("No valid audio data found in request. Expected 'inputs' or 'audio' field.")
150
-
151
- elif isinstance(data, (bytes, bytearray)):
152
- # Format 3: Direct binary data
153
- waveform = self._load_audio_from_bytes(bytes(data))
154
-
155
- else:
156
- raise ValueError(f"Unsupported data type: {type(data)}")
157
-
158
- if waveform is None:
159
- raise ValueError("Failed to extract audio from request")
160
-
161
- logger.info(f"Audio loaded: {len(waveform)} samples at 16kHz")
162
-
163
- # Process audio with the processor
164
- input_features = self.processor(
165
- waveform,
166
- sampling_rate=16000,
167
- return_tensors="pt"
168
- ).input_features
169
-
170
- # Move to device
171
- input_features = input_features.to(self.device, dtype=self.torch_dtype)
172
-
173
- # Run inference with forced Mandarin Chinese language
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  with torch.no_grad():
175
- predicted_ids = self.model.generate(input_features)
176
-
177
- # Decode transcription
178
- transcription = self.processor.batch_decode(
179
- predicted_ids,
180
- skip_special_tokens=True
181
- )[0]
182
-
183
- logger.info(f"Transcription complete: {transcription[:100]}...")
184
-
185
- # Return result
186
- return {
187
- "transcription": transcription.strip(),
188
- "audio_duration_seconds": float(len(waveform) / 16000),
189
- "model": "AImpower/StutteredSpeechASR"
190
- }
191
-
192
- except Exception as e:
193
- logger.error(f"Error during inference: {e}", exc_info=True)
194
- return {
195
- "error": str(e),
196
- "transcription": None
197
- }
 
1
+ """
2
+ Custom Inference Handler for StutteredSpeechASR Model
3
+ Handles audio input and returns transcriptions for stuttered speech.
4
+ """
5
+
6
+ import torch
7
+ import librosa
8
+ import numpy as np
9
+ import base64
10
+ import io
11
  import logging
12
+ from copy import deepcopy
13
+ from typing import Dict, Any
14
+ from transformers import AutoModelForSpeechSeq2Seq, AutoProcessor
15
+
16
+ # Configure logging
17
+ logging.basicConfig(level=logging.INFO)
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class EndpointHandler:
22
+ """
23
+ Custom handler for StutteredSpeechASR inference endpoint.
24
+
25
+ This handler processes audio inputs and returns transcriptions
26
+ using the fine-tuned Whisper model for stuttered Mandarin speech.
27
+ """
28
+
29
+ def __init__(self, path: str = ""):
30
+ """
31
+ Initialize the handler by loading the model and processor.
32
+
33
+ Args:
34
+ path: Path to the model directory (provided by Inference Endpoints)
35
+ """
36
+ logger.info("Initializing StutteredSpeechASR handler...")
37
+
38
+ # Determine device and dtype
39
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
40
+ self.torch_dtype = torch.float16 if torch.cuda.is_available() else torch.float32
41
+
42
+ logger.info(f"Using device: {self.device}")
43
+ logger.info(f"Using dtype: {self.torch_dtype}")
44
+
45
+ # Load model and processor
46
+ try:
47
+ self.model = AutoModelForSpeechSeq2Seq.from_pretrained(
48
+ path,
49
+ torch_dtype=self.torch_dtype
50
+ )
51
+ self.processor = AutoProcessor.from_pretrained(path)
52
+ self.model.to(self.device)
53
+ self.model.eval() # Set to evaluation mode
54
+
55
+ logger.info("Model and processor loaded successfully!")
56
+ except Exception as e:
57
+ logger.error(f"Error loading model: {e}")
58
+ raise
59
+
60
+ def _load_audio_from_bytes(self, audio_bytes: bytes) -> np.ndarray:
61
+ """
62
+ Load audio from bytes and resample to 16kHz.
63
+
64
+ Args:
65
+ audio_bytes: Raw audio bytes
66
+
67
+ Returns:
68
+ Audio waveform as numpy array
69
+ """
70
+ try:
71
+ # Load audio from bytes using librosa
72
+ audio_buffer = io.BytesIO(audio_bytes)
73
+ waveform, _ = librosa.load(audio_buffer, sr=16000, mono=True)
74
+ return waveform
75
+ except Exception as e:
76
+ logger.error(f"Error loading audio from bytes: {e}")
77
+ raise ValueError(f"Failed to load audio: {e}")
78
+
79
+ def _load_audio_from_base64(self, base64_string: str) -> np.ndarray:
80
+ """
81
+ Load audio from base64-encoded string.
82
+
83
+ Args:
84
+ base64_string: Base64-encoded audio data
85
+
86
+ Returns:
87
+ Audio waveform as numpy array
88
+ """
89
+ try:
90
+ # Decode base64 string
91
+ audio_bytes = base64.b64decode(base64_string)
92
+ return self._load_audio_from_bytes(audio_bytes)
93
+ except Exception as e:
94
+ logger.error(f"Error decoding base64 audio: {e}")
95
+ raise ValueError(f"Failed to decode base64 audio: {e}")
96
+
97
+ def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]:
98
+ """
99
+ Process incoming requests and return transcriptions.
100
+
101
+ Expected input formats:
102
+ 1. {"inputs": "base64_encoded_audio_string"}
103
+ 2. {"inputs": {"audio": "base64_encoded_audio_string"}}
104
  3. Binary audio data in request body
105
+
106
+ Dictionary requests may include {"parameters": {"language": "en"}}.
107
+ Omit language (or use null) to keep the model's default behavior.
108
+
109
+ Args:
110
+ data: Input data dictionary
111
+
112
+ Returns:
113
+ Dictionary containing transcription and metadata
114
+ """
115
+ try:
116
  logger.info("Processing inference request...")
117
+
118
+ generate_kwargs = {}
 
 
119
  if isinstance(data, dict):
120
+ parameters = data.get("parameters", {})
121
+ if not isinstance(parameters, dict):
122
+ raise ValueError("'parameters' must be a dictionary")
123
+ language = parameters.get("language")
124
+ if language is not None:
125
+ if not isinstance(language, str) or not language.strip():
126
+ raise ValueError("'language' must be a non-empty string or null")
127
+ # Keep language overrides local to this request, including on
128
+ # Transformers versions that modify the generation config.
129
+ generate_kwargs = {
130
+ "generation_config": deepcopy(self.model.generation_config),
131
+ "language": language.strip(),
132
+ "task": "transcribe",
133
+ }
134
+
135
+ # Extract audio data from various input formats
136
+ waveform = None
137
+
138
+ if isinstance(data, dict):
139
+ # Format 1: {"inputs": "base64_string"}
140
+ if "inputs" in data:
141
+ inputs = data["inputs"]
142
+
143
+ if isinstance(inputs, str):
144
+ # Base64-encoded audio
145
+ waveform = self._load_audio_from_base64(inputs)
146
+
147
+ elif isinstance(inputs, dict):
148
+ # Format 2: {"inputs": {"audio": "base64_string"}}
149
+ if "audio" in inputs:
150
+ waveform = self._load_audio_from_base64(inputs["audio"])
151
+ else:
152
+ raise ValueError("Missing 'audio' field in inputs dictionary")
153
+
154
+ elif isinstance(inputs, bytes):
155
+ # Binary audio data
156
+ waveform = self._load_audio_from_bytes(inputs)
157
+
158
+ else:
159
+ raise ValueError(f"Unsupported input type: {type(inputs)}")
160
+
161
+ # Direct audio field
162
+ elif "audio" in data:
163
+ audio_data = data["audio"]
164
+ if isinstance(audio_data, str):
165
+ waveform = self._load_audio_from_base64(audio_data)
166
+ elif isinstance(audio_data, bytes):
167
+ waveform = self._load_audio_from_bytes(audio_data)
168
+
169
+ else:
170
+ raise ValueError("No valid audio data found in request. Expected 'inputs' or 'audio' field.")
171
+
172
+ elif isinstance(data, (bytes, bytearray)):
173
+ # Format 3: Direct binary data
174
+ waveform = self._load_audio_from_bytes(bytes(data))
175
+
176
+ else:
177
+ raise ValueError(f"Unsupported data type: {type(data)}")
178
+
179
+ if waveform is None:
180
+ raise ValueError("Failed to extract audio from request")
181
+
182
+ logger.info(f"Audio loaded: {len(waveform)} samples at 16kHz")
183
+
184
+ # Process audio with the processor
185
+ input_features = self.processor(
186
+ waveform,
187
+ sampling_rate=16000,
188
+ return_tensors="pt"
189
+ ).input_features
190
+
191
+ # Move to device
192
+ input_features = input_features.to(self.device, dtype=self.torch_dtype)
193
+
194
+ # Use the requested language, or the model's defaults when omitted.
195
  with torch.no_grad():
196
+ predicted_ids = self.model.generate(input_features, **generate_kwargs)
197
+
198
+ # Decode transcription
199
+ transcription = self.processor.batch_decode(
200
+ predicted_ids,
201
+ skip_special_tokens=True
202
+ )[0]
203
+
204
+ logger.info(f"Transcription complete: {transcription[:100]}...")
205
+
206
+ # Return result
207
+ return {
208
+ "transcription": transcription.strip(),
209
+ "audio_duration_seconds": float(len(waveform) / 16000),
210
+ "model": "AImpower/StutteredSpeechASR"
211
+ }
212
+
213
+ except Exception as e:
214
+ logger.error(f"Error during inference: {e}", exc_info=True)
215
+ return {
216
+ "error": str(e),
217
+ "transcription": None
218
+ }
tests/test_handler.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Handler regression tests; no model weights or endpoint required."""
2
+
3
+ import unittest
4
+ from types import SimpleNamespace
5
+ from unittest.mock import Mock
6
+
7
+ from handler import EndpointHandler
8
+
9
+
10
+ class LanguageTests(unittest.TestCase):
11
+ def setUp(self):
12
+ self.handler = EndpointHandler.__new__(EndpointHandler)
13
+ self.handler.device = "cpu"
14
+ self.handler.torch_dtype = None
15
+ self.handler.model = Mock()
16
+ self.handler.model.generation_config = SimpleNamespace(language=None)
17
+ self.features = Mock()
18
+ self.features.to.return_value = self.features
19
+ self.handler.processor = Mock(return_value=SimpleNamespace(input_features=self.features))
20
+ self.handler.processor.batch_decode.return_value = [" test transcription "]
21
+ self.handler._load_audio_from_base64 = Mock(return_value=[0.0] * 16000)
22
+ self.handler._load_audio_from_bytes = Mock(return_value=[0.0] * 16000)
23
+
24
+ def assert_success(self, request):
25
+ self.assertEqual(self.handler(request), {
26
+ "transcription": "test transcription",
27
+ "audio_duration_seconds": 1.0,
28
+ "model": "AImpower/StutteredSpeechASR",
29
+ })
30
+
31
+ def test_languages_and_json_audio_formats(self):
32
+ for language in ("en", "english", "<|en|>", "zh", " en "):
33
+ for audio in ({"inputs": "audio"}, {"inputs": {"audio": "audio"}},
34
+ {"audio": "audio"}, {"inputs": b"audio"}):
35
+ with self.subTest(language=language, audio=audio):
36
+ self.assert_success({**audio, "parameters": {"language": language}})
37
+ args, kwargs = self.handler.model.generate.call_args
38
+ self.assertEqual(args, (self.features,))
39
+ self.assertEqual(kwargs["language"], language.strip())
40
+ self.assertEqual(kwargs["task"], "transcribe")
41
+ self.assertIsNot(kwargs["generation_config"], self.handler.model.generation_config)
42
+
43
+ def test_default_requests_keep_existing_generation_call(self):
44
+ for request in ({"inputs": "audio"}, {"inputs": "audio", "parameters": {}},
45
+ {"inputs": "audio", "parameters": {"language": None}},
46
+ b"audio", bytearray(b"audio")):
47
+ with self.subTest(request=request):
48
+ self.assert_success(request)
49
+ self.handler.model.generate.assert_called_with(self.features)
50
+
51
+ def test_language_does_not_leak_between_requests(self):
52
+ def generate(features, **kwargs):
53
+ if "generation_config" in kwargs:
54
+ kwargs["generation_config"].language = kwargs["language"]
55
+ return [[1]]
56
+ self.handler.model.generate.side_effect = generate
57
+ for language in ("en", "zh"):
58
+ self.assert_success({"inputs": "audio", "parameters": {"language": language}})
59
+ self.assertIsNone(self.handler.model.generation_config.language)
60
+ self.assert_success({"inputs": "audio"})
61
+ self.handler.model.generate.assert_called_with(self.features)
62
+
63
+ def test_invalid_parameter_shapes_return_existing_error_format(self):
64
+ for parameters in (None, [], "en", {"language": ""}, {"language": " "},
65
+ {"language": 123}, {"language": ["en"]}):
66
+ with self.subTest(parameters=parameters):
67
+ result = self.handler({"inputs": "audio", "parameters": parameters})
68
+ self.assertIsNone(result["transcription"])
69
+ self.assertIn("error", result)
70
+ self.handler.model.generate.assert_not_called()
71
+
72
+ def test_unsupported_language_error_is_returned(self):
73
+ self.handler.model.generate.side_effect = ValueError("Unsupported language: invalid")
74
+ result = self.handler({"inputs": "audio", "parameters": {"language": "invalid"}})
75
+ self.assertEqual(result, {"error": "Unsupported language: invalid", "transcription": None})
76
+
77
+ def test_unrelated_parameters_are_not_forwarded(self):
78
+ self.assert_success({"inputs": "audio", "parameters": {"max_new_tokens": 1}})
79
+ self.handler.model.generate.assert_called_with(self.features)
80
+
81
+
82
+ if __name__ == "__main__":
83
+ unittest.main()