Skip to content

Commit 14fcde5

Browse files
authored
Add per-stream language hint for Qwen3-ASR (and tighten scaffold cleanup) (#3472)
1 parent 7ea8283 commit 14fcde5

6 files changed

Lines changed: 84 additions & 34 deletions

File tree

cxx-api-examples/qwen3-asr-cxx-api.cc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ int32_t main(int32_t argc, char *argv[]) {
7474
const auto begin = std::chrono::steady_clock::now();
7575

7676
OfflineStream stream = recognizer.CreateStream();
77+
// stream.SetOption("language", "Korean"); // optional: force transcription language
7778
stream.AcceptWaveform(wave.sample_rate, wave.samples.data(),
7879
wave.samples.size());
7980

python-api-examples/offline-qwen3-asr-decode-files.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,14 @@ def get_args():
7979
""",
8080
)
8181

82+
parser.add_argument(
83+
"--language",
84+
type=str,
85+
default="",
86+
required=False,
87+
help="Force transcription language, e.g. Korean, Chinese, English",
88+
)
89+
8290
parser.add_argument(
8391
"--max-total-len",
8492
type=int,
@@ -179,11 +187,15 @@ def create_recognizer(args) -> sherpa_onnx.OfflineRecognizer:
179187
)
180188

181189

182-
def decode_file(recognizer: sherpa_onnx.OfflineRecognizer, filename: str):
190+
def decode_file(
191+
recognizer: sherpa_onnx.OfflineRecognizer, filename: str, language: str = ""
192+
):
183193
audio, sample_rate = sf.read(filename, dtype="float32", always_2d=True)
184194
audio = audio[:, 0]
185195

186196
stream = recognizer.create_stream()
197+
if language:
198+
stream.set_option("language", language)
187199
stream.accept_waveform(sample_rate, audio)
188200
recognizer.decode_stream(stream)
189201
return stream.result
@@ -200,7 +212,7 @@ def main():
200212
if not Path(f).is_file():
201213
print(f"Skip missing file: {f}", file=sys.stderr)
202214
continue
203-
result = decode_file(recognizer, f)
215+
result = decode_file(recognizer, f, language=args.language)
204216
print(f"{f}\n text: {result.text}\n")
205217

206218

sherpa-onnx/csrc/offline-recognizer-qwen3-asr-impl.cc

Lines changed: 57 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -356,10 +356,17 @@ void OfflineRecognizerQwen3ASRImpl::InitPromptTemplateIds() {
356356
SHERPA_ONNX_LOGE("Failed to tokenize <|audio_pad|> for qwen3-asr prompt");
357357
SHERPA_ONNX_EXIT(-1);
358358
}
359+
360+
asr_text_token_id_ = tokenizer_->GetTokenId("<asr_text>");
361+
if (asr_text_token_id_ < 0) {
362+
SHERPA_ONNX_LOGE("Failed to locate <asr_text> token id for qwen3-asr");
363+
SHERPA_ONNX_EXIT(-1);
364+
}
359365
}
360366

361367
std::vector<int64_t> OfflineRecognizerQwen3ASRImpl::BuildSourceIds(
362-
const std::string &hotwords, int32_t audio_token_len, int32_t *before_len,
368+
const std::string &hotwords, const std::string &language,
369+
int32_t audio_token_len, int32_t *before_len,
363370
int32_t *fake_audio_token_len) const {
364371
const std::string before_utf8 = std::string(kQwen3SystemPromptPrefix) +
365372
hotwords + kQwen3SystemPromptSuffix;
@@ -372,11 +379,27 @@ std::vector<int64_t> OfflineRecognizerQwen3ASRImpl::BuildSourceIds(
372379
*fake_audio_token_len = audio_token_len;
373380
}
374381

382+
std::vector<int64_t> prompt_ids_after_with_language;
383+
const std::vector<int64_t> *ids_after = &prompt_ids_after_;
384+
if (!language.empty()) {
385+
auto language_ids = tokenizer_->Encode("language " + language);
386+
prompt_ids_after_with_language.reserve(prompt_ids_after_.size() +
387+
language_ids.size() + 1);
388+
prompt_ids_after_with_language.insert(prompt_ids_after_with_language.end(),
389+
prompt_ids_after_.begin(),
390+
prompt_ids_after_.end());
391+
prompt_ids_after_with_language.insert(prompt_ids_after_with_language.end(),
392+
language_ids.begin(),
393+
language_ids.end());
394+
prompt_ids_after_with_language.push_back(asr_text_token_id_);
395+
ids_after = &prompt_ids_after_with_language;
396+
}
397+
375398
std::vector<int64_t> source_ids;
376399
size_t estimated_size =
377400
prompt_ids_before.size() +
378401
static_cast<size_t>(audio_token_len) * audio_pad_ids_.size() +
379-
prompt_ids_after_.size();
402+
ids_after->size();
380403
source_ids.reserve(estimated_size);
381404
source_ids.insert(source_ids.end(), prompt_ids_before.begin(),
382405
prompt_ids_before.end());
@@ -386,8 +409,7 @@ std::vector<int64_t> OfflineRecognizerQwen3ASRImpl::BuildSourceIds(
386409
audio_pad_ids_.end());
387410
}
388411

389-
source_ids.insert(source_ids.end(), prompt_ids_after_.begin(),
390-
prompt_ids_after_.end());
412+
source_ids.insert(source_ids.end(), ids_after->begin(), ids_after->end());
391413

392414
return source_ids;
393415
}
@@ -689,10 +711,15 @@ OfflineRecognitionResult OfflineRecognizerQwen3ASRImpl::GenerateText(
689711
const std::string hotwords = Qwen3FormatHotwordsForPrompt(
690712
stream->HasOption("hotwords") ? stream->GetOption("hotwords") : "");
691713

714+
std::string language;
715+
if (stream->HasOption("language")) {
716+
language = stream->GetOption("language");
717+
}
718+
692719
int32_t before_len = 0;
693720
int32_t fake_audio_token_len = 0;
694721
std::vector<int64_t> source_ids = BuildSourceIds(
695-
hotwords, audio_token_len, &before_len, &fake_audio_token_len);
722+
hotwords, language, audio_token_len, &before_len, &fake_audio_token_len);
696723

697724
int32_t context_len = static_cast<int32_t>(source_ids.size());
698725
if (context_len == 0) {
@@ -951,21 +978,37 @@ OfflineRecognitionResult OfflineRecognizerQwen3ASRImpl::GenerateText(
951978
++cur_len;
952979
}
953980

954-
// drop the first 3 tokens which contain things like:
955-
// language None<asr_text>
956-
if (generated_ids.size() >= 3) {
957-
generated_ids.erase(generated_ids.begin(), generated_ids.begin() + 3);
981+
std::vector<int64_t> cleaned_ids = generated_ids;
982+
if (!generated_ids.empty()) {
983+
const size_t prefix_window = std::min<size_t>(16, generated_ids.size());
984+
auto asr_text_it = std::find(generated_ids.begin(),
985+
generated_ids.begin() + prefix_window,
986+
asr_text_token_id_);
987+
988+
// Only strip a leading scaffold prefix recognized by token ID.
989+
if (asr_text_it != generated_ids.begin() + prefix_window &&
990+
asr_text_it != generated_ids.begin()) {
991+
std::vector<int64_t> prefix_ids(generated_ids.begin(),
992+
std::next(asr_text_it));
993+
std::string prefix_text = tokenizer_->Decode(prefix_ids);
994+
if (prefix_text.rfind("language ", 0) == 0 &&
995+
prefix_text.size() >= 10 &&
996+
prefix_text.compare(prefix_text.size() - 10, 10, "<asr_text>") ==
997+
0) {
998+
cleaned_ids.assign(std::next(asr_text_it), generated_ids.end());
999+
}
1000+
}
9581001
}
9591002

960-
result.text = tokenizer_->Decode(generated_ids);
1003+
result.text = tokenizer_->Decode(cleaned_ids);
9611004
RemoveUtf8ReplacementChars(&result.text);
9621005

963-
if (!generated_ids.empty()) {
1006+
if (!cleaned_ids.empty()) {
9641007
std::vector<std::string> all_tokens;
965-
all_tokens.reserve(generated_ids.size());
1008+
all_tokens.reserve(cleaned_ids.size());
9661009
std::string pending_bytes;
9671010

968-
for (int64_t token_id : generated_ids) {
1011+
for (int64_t token_id : cleaned_ids) {
9691012
std::string s =
9701013
tokenizer_->GetTokenStringStreaming(token_id, &pending_bytes);
9711014
all_tokens.push_back(std::move(s));
@@ -975,25 +1018,7 @@ OfflineRecognitionResult OfflineRecognizerQwen3ASRImpl::GenerateText(
9751018
all_tokens.back().append("\xEF\xBF\xBD");
9761019
}
9771020

978-
std::string concat;
979-
size_t skip = 0;
980-
bool stripped = false;
981-
for (size_t i = 0; i < all_tokens.size(); ++i) {
982-
concat += all_tokens[i];
983-
if (concat.find("<asr_text>") != std::string::npos) {
984-
skip = i + 1;
985-
stripped = true;
986-
break;
987-
}
988-
}
989-
if (stripped && skip <= all_tokens.size()) {
990-
result.tokens.reserve(all_tokens.size() - skip);
991-
for (size_t i = skip; i < all_tokens.size(); ++i) {
992-
result.tokens.push_back(std::move(all_tokens[i]));
993-
}
994-
} else {
995-
result.tokens = std::move(all_tokens);
996-
}
1021+
result.tokens = std::move(all_tokens);
9971022
}
9981023

9991024
return result;

sherpa-onnx/csrc/offline-recognizer-qwen3-asr-impl.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class OfflineRecognizerQwen3ASRImpl : public OfflineRecognizerImpl {
3737
private:
3838
void InitPromptTemplateIds();
3939
std::vector<int64_t> BuildSourceIds(const std::string &hotwords,
40+
const std::string &language,
4041
int32_t audio_token_len,
4142
int32_t *before_len,
4243
int32_t *fake_audio_token_len) const;
@@ -62,6 +63,7 @@ class OfflineRecognizerQwen3ASRImpl : public OfflineRecognizerImpl {
6263
std::unique_ptr<QwenAsrTokenizer> tokenizer_;
6364
std::vector<int64_t> audio_pad_ids_;
6465
std::vector<int64_t> prompt_ids_after_;
66+
int64_t asr_text_token_id_ = -1;
6567
mutable std::mt19937 rng_;
6668
};
6769

sherpa-onnx/csrc/qwen-asr-tokenizer.cc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1243,6 +1243,15 @@ std::vector<int64_t> QwenAsrTokenizer::Encode(const std::string &text) {
12431243
return ans;
12441244
}
12451245

1246+
int64_t QwenAsrTokenizer::GetTokenId(const std::string &token) const {
1247+
auto it = token2id_.find(token);
1248+
if (it == token2id_.end()) {
1249+
return -1;
1250+
}
1251+
1252+
return it->second;
1253+
}
1254+
12461255
std::string QwenAsrTokenizer::Decode(const std::vector<int64_t> &token_ids) {
12471256
std::string ans;
12481257
std::string buffer;

sherpa-onnx/csrc/qwen-asr-tokenizer.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class QwenAsrTokenizer {
2525
std::string Decode(const std::vector<int64_t> &token_ids);
2626
std::string GetTokenStringStreaming(int64_t token_id,
2727
std::string *pending_bytes) const;
28+
int64_t GetTokenId(const std::string &token) const;
2829

2930
int64_t GetEosTokenId() const { return eos_token_id_; }
3031
int64_t GetPadTokenId() const { return pad_token_id_; }

0 commit comments

Comments
 (0)