Automatic Speech Recognition
Safetensors
Chinese
whisper

fix: allow endpoint callers to select transcription language

#3
Files changed (3) hide show
  1. README.md +32 -1
  2. handler.py +23 -2
  3. tests/test_handler.py +83 -0
README.md CHANGED
@@ -80,6 +80,37 @@ print(f"Transcription: {transcription}")
80
 
81
  -----
82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  ## Training Data
84
 
85
  The model was fine-tuned on the **[AImpower/MandarinStutteredSpeech](https://huggingface.co/datasets/AImpower/MandarinStutteredSpeech)** dataset.
@@ -134,4 +165,4 @@ If you use this model, please cite the original paper:
134
  location = {Athens, Greece},
135
  series = {FAccT '25}
136
  }
137
- ```
 
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.
 
165
  location = {Athens, Greece},
166
  series = {FAccT '25}
167
  }
168
+ ```
handler.py CHANGED
@@ -9,6 +9,7 @@ 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
 
@@ -101,6 +102,9 @@ class EndpointHandler:
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
@@ -110,6 +114,23 @@ class EndpointHandler:
110
  """
111
  try:
112
  logger.info("Processing inference request...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
 
114
  # Extract audio data from various input formats
115
  waveform = None
@@ -170,9 +191,9 @@ class EndpointHandler:
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(
 
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
 
 
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
 
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
 
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(
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()