Repository navigation
feat: add Qwen3-ASR speech-to-text - #1554
msluszniak wants to merge 5 commits into
Conversation
|
@barhanc I forgot that I need to rebuild libs for this one to cover all backends, will let you know when this will be ready since I want to batch this with Szymon's changes to MIGAN. |
Audio tower on Core ML, decoder on MLX. A variant key naming two backends resolves only when both are linked.
Fixes the Qwen3-ASR docs anchor and lets the feature-map test read backends from file names.
e315b13 to
e2fc471
Compare
| ## Qwen3-ASR | ||
|
|
||
| [Qwen3-ASR](https://huggingface.co/Qwen/Qwen3-ASR-0.6B) works with the same hook and the same `transcribe` / `stream` API. It detects the language by itself; pass `language` (one of [`QWEN3_ASR_LANGUAGES`](../../06-api-reference/variables/QWEN3_ASR_LANGUAGES.md)) only to force one: | ||
|
|
||
| ```typescript | ||
| const stt = useSpeechToText(models.speechToText.QWEN3_ASR.SIZE_0_6B.DEFAULT); | ||
|
|
||
| const transcript = await stt.transcribe(audioData); // auto-detect | ||
| const french = await stt.transcribe(audioData, { language: 'fr' }); | ||
| ``` | ||
|
|
There was a problem hiding this comment.
Not sure a separate subsection in docs for a model is needed.
| ): { | ||
| /** Whether the model is loaded and ready to transcribe. */ | ||
| isReady: boolean; | ||
| /** The download or load error, if any. */ | ||
| error: Error | undefined; | ||
| /** Download progress across every asset, in percent. */ | ||
| downloadProgress: number; | ||
| /** The config with every remote URL resolved to a local path. */ | ||
| resource: WhisperSttModel<L> | Qwen3AsrSttModel | undefined; | ||
| transcribe: (WhisperSpeechToText<L> | Qwen3AsrSpeechToText)['transcribe'] | undefined; | ||
| transcribeWorklet: | ||
| | (WhisperSpeechToText<L> | Qwen3AsrSpeechToText)['transcribeWorklet'] | ||
| | undefined; | ||
| transcribeStop: (WhisperSpeechToText<L> | Qwen3AsrSpeechToText)['transcribeStop'] | undefined; | ||
| stream: (WhisperSpeechToText<L> | Qwen3AsrSpeechToText)['stream'] | undefined; | ||
| streamInsert: (WhisperSpeechToText<L> | Qwen3AsrSpeechToText)['streamInsert'] | undefined; | ||
| streamStop: (WhisperSpeechToText<L> | Qwen3AsrSpeechToText)['streamStop'] | undefined; | ||
| }; |
There was a problem hiding this comment.
This is not needed, see TTS example.
| 'name' in sttConfig && sttConfig.name === 'qwen3-asr' | ||
| ? createQwen3AsrSpeechToText(sttConfig) | ||
| : createWhisperSpeechToText(sttConfig as WhisperSttModel<L>); |
There was a problem hiding this comment.
Use the same pattern as TTS example, adding name field to existing Whisper config is a non-destructive change.
There was a problem hiding this comment.
I'm not sure about the name qwen3AsrSpeechToText as ASR means the same as speech to text, so it reads a bit weird maybe qwen3SpeechToText or qwen3AutomaticSpeechRecognition / Recognizer would be better.
| resource: WhisperSttModel<L> | undefined; | ||
| transcribe: WhisperSpeechToText<L>['transcribe'] | undefined; | ||
| transcribeWorklet: WhisperSpeechToText<L>['transcribeWorklet'] | undefined; | ||
| transcribeStop: WhisperSpeechToText<L>['transcribeStop'] | undefined; | ||
| stream: WhisperSpeechToText<L>['stream'] | undefined; | ||
| streamInsert: WhisperSpeechToText<L>['streamInsert'] | undefined; | ||
| streamStop: WhisperSpeechToText<L>['streamStop'] | undefined; |
There was a problem hiding this comment.
| resource: WhisperSttModel<L> | undefined; | |
| transcribe: WhisperSpeechToText<L>['transcribe'] | undefined; | |
| transcribeWorklet: WhisperSpeechToText<L>['transcribeWorklet'] | undefined; | |
| transcribeStop: WhisperSpeechToText<L>['transcribeStop'] | undefined; | |
| stream: WhisperSpeechToText<L>['stream'] | undefined; | |
| streamInsert: WhisperSpeechToText<L>['streamInsert'] | undefined; | |
| streamStop: WhisperSpeechToText<L>['streamStop'] | undefined; | |
| resource: WhisperSttModel<L> | undefined; | |
| // Offline transcription methods | |
| transcribe: WhisperSpeechToText<L>['transcribe'] | undefined; | |
| transcribeStop: WhisperSpeechToText<L>['transcribeStop'] | undefined; | |
| transcribeWorklet: WhisperSpeechToText<L>['transcribeWorklet'] | undefined; | |
| // Live-streaming transcription methods | |
| stream: WhisperSpeechToText<L>['stream'] | undefined; | |
| streamStop: WhisperSpeechToText<L>['streamStop'] | undefined; | |
| streamInsert: WhisperSpeechToText<L>['streamInsert'] | undefined; |
| requireRegisteredBackends(modelPath_, methodName, backends_[methodName]); | ||
| auto loadCtx = std::format("Load method '{}' failed.\n" |
There was a problem hiding this comment.
I'm wondering if this is needed if the next line describes the first action on error is to check if required backends are registered.
| ...method('preprocess', [f32(Dyn('Samples'))], [f32(1, MEL_BINS, Dyn('Frames'))]), | ||
| ...method( | ||
| 'audio_encoder', // One 8 s attention window: whole 100-frame chunks, plus the valid token count. | ||
| [f32(1, MEL_BINS, melFrames), i64(1)], | ||
| [f32(1, audioTokens, 'Dim')] | ||
| ), | ||
| ...method('token_embedding', [i64(1, Dyn('Tokens'))], [f32(1, Dyn('Tokens'), 'Dim')]), | ||
| ...method('text_decoder', [f32(1, Dyn('PrefillLen'), 'Dim'), i64(1)], [f32(1, 'VocabSize')]), |
There was a problem hiding this comment.
Please add explicit new lines via // prettier-ignore so that the spec is easier to read.
| const withTensors = <R>(created: Tensor[], fn: () => R): R => { | ||
| 'worklet'; | ||
| try { | ||
| return fn(); | ||
| } finally { | ||
| created.forEach((t) => t.dispose()); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Explicit try finally is imo more clear.
| return !inTranscript && transcript.startsWith('language ') ? '' : transcript; | ||
| }; | ||
|
|
||
| const resolveLanguage = (options?: Qwen3AsrSttOptions): string | undefined => { |
There was a problem hiding this comment.
If this takes options explicitly, can't it be a top level helper?
| const language = resolveLanguage(options); | ||
|
|
||
| const parts: string[] = []; | ||
| for (const [start, end] of splitAtQuietPoints(audio, maxSamples)) { |
There was a problem hiding this comment.
Why not use here the VAD instead of heuristic?
Description
createQwen3AsrSpeechToTexttask for Qwen3-ASR 0.6B, sametranscribe/streamAPI as Whisper; language is auto-detected or forced viaQWEN3_ASR_LANGUAGES.useSpeechToTextaccepts Qwen3-ASR models; registry entrymodels.speechToText.QWEN3_ASR.SIZE_0_6B(XNNPACK 8da4w; iOS default Core ML audio tower + MLX decoder; HFreact-native-executorch-qwen-3-asr@ v0.11.0).MLX_COREML_INT4) is the default only when both are linked; loading a method whose backend is not linked now names it.Introduces a breaking change?
Type of change
Tested on
Testing instructions
apps/speech: pick "Qwen3-ASR 0.6B (CPU)" on the audio file or microphone screen and transcribe.Screenshots
Related issues
#769
Checklist
Additional notes