Skip to content

Commit 9f80c5e

Browse files
authored
Merge pull request #131 from solidDoWant/feat/qwen3-asr-language-key-1
Support OpenAI-standard `language` key for Qwen3-ASR models
2 parents 68ab888 + b3d82a5 commit 9f80c5e

4 files changed

Lines changed: 125 additions & 3 deletions

File tree

src/engine/openvino/qwen3_asr/qwen3_asr.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@
3030
MAX_ASR_INPUT_SECONDS,
3131
merge_languages,
3232
normalize_audios,
33-
normalize_language_name,
3433
parse_asr_output,
34+
resolve_language_name,
3535
split_audio_into_chunks,
3636
validate_language,
3737
)
@@ -377,7 +377,7 @@ async def transcribe(self, gen_config: OV_Qwen3ASRGenConfig) -> AsyncIterator[Un
377377
audio_array = (await asyncio.to_thread(normalize_audios, audio_input))[0]
378378
language: Optional[str] = None
379379
if gen_config.language:
380-
language = normalize_language_name(gen_config.language)
380+
language = resolve_language_name(gen_config.language)
381381
validate_language(language)
382382

383383
audio_seconds = len(audio_array) / SAMPLE_RATE

src/engine/openvino/qwen3_asr/qwen3_asr_utils.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,42 @@
6666
"Hungarian",
6767
"Macedonian"
6868
]
69+
# ISO-639-1 codes (with a few ISO-639-2/3 codes for languages that lack a
70+
# 639-1 code) mapped to the canonical full names Qwen3-ASR expects. Used to
71+
# accept Whisper-style language codes (e.g. "en") in addition to full names.
72+
LANGUAGE_CODE_TO_NAME: dict = {
73+
"zh": "Chinese",
74+
"en": "English",
75+
"yue": "Cantonese", # no ISO-639-1 code exists for Cantonese
76+
"ar": "Arabic",
77+
"de": "German",
78+
"fr": "French",
79+
"es": "Spanish",
80+
"pt": "Portuguese",
81+
"id": "Indonesian",
82+
"it": "Italian",
83+
"ko": "Korean",
84+
"ru": "Russian",
85+
"th": "Thai",
86+
"vi": "Vietnamese",
87+
"ja": "Japanese",
88+
"tr": "Turkish",
89+
"hi": "Hindi",
90+
"ms": "Malay",
91+
"nl": "Dutch",
92+
"sv": "Swedish",
93+
"da": "Danish",
94+
"fi": "Finnish",
95+
"pl": "Polish",
96+
"cs": "Czech",
97+
"tl": "Filipino", # Tagalog; Filipino has no distinct ISO-639-1 code
98+
"fil": "Filipino",
99+
"fa": "Persian",
100+
"el": "Greek",
101+
"ro": "Romanian",
102+
"hu": "Hungarian",
103+
"mk": "Macedonian",
104+
}
69105
_ASR_TEXT_TAG = "<asr_text>"
70106
_LANG_PREFIX = "language "
71107

@@ -92,6 +128,35 @@ def normalize_language_name(language: str) -> str:
92128
return s[:1].upper() + s[1:].lower()
93129

94130

131+
def resolve_language_name(language: str) -> str:
132+
"""
133+
Resolve a user-provided language to the canonical full name used by
134+
Qwen3-ASR, accepting either an ISO-639-1 code (e.g. 'en', 'zh') or a full
135+
name (e.g. 'English', 'Chinese').
136+
137+
Codes are matched case-insensitively against LANGUAGE_CODE_TO_NAME; anything
138+
else falls back to normalize_language_name.
139+
140+
Args:
141+
language (str): ISO-639-1 code or language name.
142+
143+
Returns:
144+
str: Canonical language name.
145+
146+
Raises:
147+
ValueError: If language is empty.
148+
"""
149+
if language is None:
150+
raise ValueError("language is None")
151+
s = str(language).strip()
152+
if not s:
153+
raise ValueError("language is empty")
154+
mapped = LANGUAGE_CODE_TO_NAME.get(s.lower())
155+
if mapped is not None:
156+
return mapped
157+
return normalize_language_name(s)
158+
159+
95160
def validate_language(language: str) -> None:
96161
"""
97162
Validate the language is supported.

src/server/routes/openai.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,14 @@ async def event_stream() -> AsyncIterator[bytes]:
487487
async def openai_audio_transcriptions(
488488
file: UploadFile = File(..., description="The audio file to transcribe"),
489489
model: str = Form(..., description="ID of the model to use"),
490+
language: Optional[str] = Form(
491+
None,
492+
description=(
493+
"Language of the input audio as an ISO-639-1 code (e.g. 'en') or "
494+
"full name (e.g. 'English'). When set, overrides "
495+
"openarc_asr.qwen3_asr.language; when unset, that value is used."
496+
),
497+
),
490498
response_format: Optional[str] = Form("json", description="Format of output"),
491499
openarc_asr: Optional[str] = Form(
492500
None, description="JSON: OpenArcASRConfig with qwen3_asr params"
@@ -516,7 +524,12 @@ async def openai_audio_transcriptions(
516524
payload["qwen3_asr"] = {}
517525

518526
cfg = OpenArcASRConfig.model_validate(payload)
519-
gen_config = cfg.qwen3_asr.model_copy(update={"audio_base64": audio_base64})
527+
update = {"audio_base64": audio_base64}
528+
if language:
529+
# Whisper-style top-level `language` takes precedence; otherwise
530+
# fall back to openarc_asr.qwen3_asr.language (current behavior).
531+
update["language"] = language
532+
gen_config = cfg.qwen3_asr.model_copy(update=update)
520533
result = await _workers.transcribe_qwen3_asr(model, gen_config)
521534
else:
522535
gen_config = OVGenAI_WhisperGenConfig(audio_base64=audio_base64)

src/tests/test_qwen3_asr_unit.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import pytest # type: ignore[import]
2+
3+
from src.engine.openvino.qwen3_asr.qwen3_asr_utils import (
4+
LANGUAGE_CODE_TO_NAME,
5+
SUPPORTED_LANGUAGES,
6+
resolve_language_name,
7+
validate_language,
8+
)
9+
10+
11+
def test_resolve_language_name_maps_iso_639_1_code() -> None:
12+
assert resolve_language_name("en") == "English"
13+
assert resolve_language_name("zh") == "Chinese"
14+
15+
16+
def test_resolve_language_name_code_is_case_insensitive_and_trimmed() -> None:
17+
assert resolve_language_name("ZH") == "Chinese"
18+
assert resolve_language_name(" ja ") == "Japanese"
19+
20+
21+
def test_resolve_language_name_passes_through_full_names() -> None:
22+
assert resolve_language_name("english") == "English"
23+
assert resolve_language_name("cHINese") == "Chinese"
24+
25+
26+
def test_resolve_language_name_handles_non_iso_639_1_special_cases() -> None:
27+
# Cantonese has no ISO-639-1 code; Filipino accepts both 639-1 (tl) and 639-3 (fil).
28+
assert resolve_language_name("yue") == "Cantonese"
29+
assert resolve_language_name("tl") == "Filipino"
30+
assert resolve_language_name("fil") == "Filipino"
31+
32+
33+
@pytest.mark.parametrize("value", [None, "", " "])
34+
def test_resolve_language_name_rejects_empty(value) -> None:
35+
with pytest.raises(ValueError):
36+
resolve_language_name(value)
37+
38+
39+
def test_every_mapped_code_resolves_to_a_supported_language() -> None:
40+
# Guards against drift if SUPPORTED_LANGUAGES is edited without updating the map.
41+
for code, name in LANGUAGE_CODE_TO_NAME.items():
42+
assert name in SUPPORTED_LANGUAGES, f"{code} -> {name} not in SUPPORTED_LANGUAGES"
43+
# Resolved canonical names must pass validation.
44+
validate_language(resolve_language_name(code))

0 commit comments

Comments
 (0)