diff --git a/CHANGELOG.md b/CHANGELOG.md index 75b562c..2fc9490 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ # Changelog +## 2.1.6 (2025-09-12) + +* fix: Fix format rate error on iOS (#104) ([d8fe8b0](https://github.com/riderodd/react-native-vosk/commit/d8fe8b0)), closes [#104](https://github.com/riderodd/react-native-vosk/issues/104) +* fix: ios record on simulator ([ff50aba](https://github.com/riderodd/react-native-vosk/commit/ff50aba)) +* fix: yarn lockfile (#155) ([507e67d](https://github.com/riderodd/react-native-vosk/commit/507e67d)), closes [#155](https://github.com/riderodd/react-native-vosk/issues/155) +* #67: replace file:// in name path, have a more descriptive log (#79) ([4e756da](https://github.com/riderodd/react-native-vosk/commit/4e756da)), closes [#67](https://github.com/riderodd/react-native-vosk/issues/67) [#79](https://github.com/riderodd/react-native-vosk/issues/79) +* Add Expo support for react-native-vosk (#151) ([fec6fb9](https://github.com/riderodd/react-native-vosk/commit/fec6fb9)), closes [#151](https://github.com/riderodd/react-native-vosk/issues/151) +* Create dependabot.yml (#71) ([8634eba](https://github.com/riderodd/react-native-vosk/commit/8634eba)), closes [#71](https://github.com/riderodd/react-native-vosk/issues/71) +* fix crashes (#143) ([cc9ea78](https://github.com/riderodd/react-native-vosk/commit/cc9ea78)), closes [#143](https://github.com/riderodd/react-native-vosk/issues/143) +* Fix expo ios model path (#156) ([e2877d9](https://github.com/riderodd/react-native-vosk/commit/e2877d9)), closes [#156](https://github.com/riderodd/react-native-vosk/issues/156) +* Fix Expo plugin configuration and dependencies (#152) ([3fa62f7](https://github.com/riderodd/react-native-vosk/commit/3fa62f7)), closes [#152](https://github.com/riderodd/react-native-vosk/issues/152) +* Fix ios pcm bitrate (#157) ([948534e](https://github.com/riderodd/react-native-vosk/commit/948534e)), closes [#157](https://github.com/riderodd/react-native-vosk/issues/157) +* Fix/expo ios (#154) ([cbdf9eb](https://github.com/riderodd/react-native-vosk/commit/cbdf9eb)), closes [#154](https://github.com/riderodd/react-native-vosk/issues/154) +* Fix/update version (#106) ([eeebd52](https://github.com/riderodd/react-native-vosk/commit/eeebd52)), closes [#106](https://github.com/riderodd/react-native-vosk/issues/106) +* Try to make expo plugin works (#153) ([b54f844](https://github.com/riderodd/react-native-vosk/commit/b54f844)), closes [#153](https://github.com/riderodd/react-native-vosk/issues/153) +* Update dependabot.yml (#72) ([4013cf3](https://github.com/riderodd/react-native-vosk/commit/4013cf3)), closes [#72](https://github.com/riderodd/react-native-vosk/issues/72) +* Update issue templates (#76) ([9e4ac88](https://github.com/riderodd/react-native-vosk/commit/9e4ac88)), closes [#76](https://github.com/riderodd/react-native-vosk/issues/76) +* Update to TurboModules with breaking changes (#150) ([4869ff0](https://github.com/riderodd/react-native-vosk/commit/4869ff0)), closes [#150](https://github.com/riderodd/react-native-vosk/issues/150) +* Update Vosk Android library and model loading method (#149) ([3248919](https://github.com/riderodd/react-native-vosk/commit/3248919)), closes [#149](https://github.com/riderodd/react-native-vosk/issues/149) +* docs: add Copilot Coding Agent onboarding instructions (#148) ([1881c34](https://github.com/riderodd/react-native-vosk/commit/1881c34)), closes [#148](https://github.com/riderodd/react-native-vosk/issues/148) +* chore: release 0.3.2 (#80) ([37ec42a](https://github.com/riderodd/react-native-vosk/commit/37ec42a)), closes [#80](https://github.com/riderodd/react-native-vosk/issues/80) +* chore: updated release it deps (#105) ([b73429c](https://github.com/riderodd/react-native-vosk/commit/b73429c)), closes [#105](https://github.com/riderodd/react-native-vosk/issues/105) +* chore(deps-dev): bump @react-native/metro-config from 0.76.1 to 0.79.2 (#118) ([a14e95a](https://github.com/riderodd/react-native-vosk/commit/a14e95a)), closes [#118](https://github.com/riderodd/react-native-vosk/issues/118) + + +### BREAKING CHANGE + +* the model is not searched in the android project by default anymore + +* chore: release 1.0.0 + ## 2.1.5 (2025-09-12) * fix: avoid crash on ios when multiple starts are thrown ([1aa44be](https://github.com/riderodd/react-native-vosk/commit/1aa44be)) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index a7bea9c..c635995 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1835,7 +1835,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - react-native-vosk (2.1.4): + - react-native-vosk (2.1.5): - boost - DoubleConversion - fast_float @@ -2643,7 +2643,7 @@ SPEC CHECKSUMS: React-Mapbuffer: 91e0eab42a6ae7f3e34091a126d70fc53bd3823e React-microtasksnativemodule: 1ead4fe154df3b1ba34b5a9e35ef3c4bdfa72ccb react-native-safe-area-context: c6e2edd1c1da07bdce287fa9d9e60c5f7b514616 - react-native-vosk: 25bd5e5a7b3b1197ebf56c65e783c9852cc04fa8 + react-native-vosk: 797d9009626d4bfa1b0522b7b1aa84def66c1f17 React-NativeModulesApple: eff2eba56030eb0d107b1642b8f853bc36a833ac React-oscompat: b12c633e9c00f1f99467b1e0e0b8038895dae436 React-perflogger: 58d12c4e5df1403030c97b9c621375c312cca454 @@ -2679,4 +2679,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 605b934b5eb489405fbad7204a4924dc73596edb -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/ios/Vosk.mm b/ios/Vosk.mm index f80a2b8..50e6762 100644 --- a/ios/Vosk.mm +++ b/ios/Vosk.mm @@ -22,6 +22,8 @@ @implementation Vosk { BOOL _isRunning; // protects use of recognizer after stop BOOL _isStarting; // prevents concurrent start BOOL _tapInstalled; // track tap installation + BOOL _pendingTap; // indicates we are retrying tap installation + int _tapRetryCount; // retry counter } RCT_EXPORT_MODULE() @@ -41,6 +43,8 @@ - (instancetype)init { _isRunning = NO; _isStarting = NO; _tapInstalled = NO; + _pendingTap = NO; + _tapRetryCount = 0; } return self; } @@ -87,6 +91,8 @@ - (void)start:(JS::NativeVosk::VoskOptions *_Nullable)options return; } _isStarting = YES; + _tapRetryCount = 0; + _pendingTap = NO; AVAudioSession *audioSession = [AVAudioSession sharedInstance]; // Extract options (grammar, timeout) from the codegen structure @@ -153,9 +159,9 @@ - (void)start:(JS::NativeVosk::VoskOptions *_Nullable)options } self->_formatInput = [self->_inputNode inputFormatForBus:0]; - const double sampleRate = (self->_formatInput.sampleRate > 0) ? self->_formatInput.sampleRate : 16000.0; - const AVAudioFrameCount bufferSize = (AVAudioFrameCount)(sampleRate / 10.0); - self->_isRunning = YES; + const double sampleRate = (self->_formatInput.sampleRate > 0) ? self->_formatInput.sampleRate : 16000.0; + const AVAudioFrameCount bufferSize = (AVAudioFrameCount)(sampleRate / 10.0); + self->_isRunning = YES; __weak __typeof(self) weakSelf = self; dispatch_async(self->_processingQueue, ^{ // recognizer init off main @@ -185,97 +191,19 @@ - (void)start:(JS::NativeVosk::VoskOptions *_Nullable)options return; // promise already resolved later below or not? -> we won't resolve now } - dispatch_async(dispatch_get_main_queue(), ^{ // install tap + start engine + dispatch_async(dispatch_get_main_queue(), ^{ // start engine first if (!self->_isRunning) { self->_isStarting = NO; return; } - if (self->_tapInstalled) { - NSLog(@"[Vosk] Tap already installed – skipping reinstall"); - self->_isStarting = NO; - resolve(nil); - return; - } - AVAudioFormat *tapFormat = [self->_inputNode inputFormatForBus:0]; - if (!tapFormat) { - [self emitOnError:@"tapFormat is nil; cannot install tap"]; - [self stopInternalWithoutEvents:YES]; - self->_isStarting = NO; - return; - } - BOOL invalidSR = tapFormat.sampleRate <= 0.0; - BOOL invalidCh = tapFormat.channelCount == 0; - if (invalidSR || invalidCh) { - NSLog(@"[Vosk] Invalid input format (sr=%.1f ch=%u) -> using nil format", tapFormat.sampleRate, (unsigned int)tapFormat.channelCount); - tapFormat = nil; - } - __weak __typeof(self) weakSelfTap = self; - @try { - [self->_inputNode installTapOnBus:0 bufferSize:bufferSize format:tapFormat block:^(AVAudioPCMBuffer *buffer, AVAudioTime *when) { - __strong __typeof(self) self = weakSelfTap; if (!self) return; if (!self->_isRunning) return; - dispatch_async(self->_processingQueue, ^{ - if (!self->_isRunning) return; VoskRecognizer *recognizer = self->_recognizer; if (!recognizer) return; - AVAudioFrameCount frames = buffer.frameLength; if (frames == 0) return; - int accepted = 0; - if (buffer.int16ChannelData && buffer.int16ChannelData[0]) { - int dataLen = (int)(frames * sizeof(int16_t)); - accepted = vosk_recognizer_accept_waveform(recognizer, (const char *)buffer.int16ChannelData[0], (int32_t)dataLen); - } else if (buffer.floatChannelData && buffer.floatChannelData[0]) { - float *const *floatChannels = buffer.floatChannelData; std::vector pcm16; pcm16.resize(frames); float *ch0 = floatChannels[0]; - for (AVAudioFrameCount i = 0; i < frames; ++i) { float s = ch0[i]; if (s > 1.f) s = 1.f; else if (s < -1.f) s = -1.f; pcm16[i] = (int16_t)lrintf(s * 32767.f); } - int dataLen = (int)(frames * sizeof(int16_t)); - accepted = vosk_recognizer_accept_waveform(recognizer, (const char *)pcm16.data(), (int32_t)dataLen); - } else { return; } - - const char *cstr = NULL; - BOOL isFinal = NO; - if (accepted) { // end of utterance recognized per Vosk API - cstr = vosk_recognizer_result(recognizer); - isFinal = YES; - } else { - cstr = vosk_recognizer_partial_result(recognizer); - isFinal = NO; - } - NSString *json = cstr ? [NSString stringWithUTF8String:cstr] : nil; - dispatch_async(dispatch_get_main_queue(), ^{ - if (!json) return; - NSData *data = [json dataUsingEncoding:NSUTF8StringEncoding]; - NSDictionary *parsed = data ? [NSJSONSerialization JSONObjectWithData:data options:0 error:nil] : nil; - if (![parsed isKindOfClass:[NSDictionary class]]) { - if (isFinal) { [self emitOnResult:json]; } else { [self emitOnPartialResult:json]; } - return; - } - NSString *text = parsed[@"text"]; NSString *partial = parsed[@"partial"]; - if (isFinal) { - if (text.length > 0) { [self emitOnResult:text]; } - self->_lastPartial = nil; // reset after final - } else { - if (partial.length > 0 && (!self->_lastPartial || ![self->_lastPartial isEqualToString:partial])) { - [self emitOnPartialResult:partial]; - } - self->_lastPartial = partial ?: self->_lastPartial; - } - }); - }); - }]; - self->_tapInstalled = YES; - } @catch (NSException *ex) { - [self emitOnError:[NSString stringWithFormat:@"Failed to install tap: %@", ex.reason ?: @"unknown"]]; - [self stopInternalWithoutEvents:YES]; - self->_isStarting = NO; - reject(@"start", @"Failed to install tap", nil); - return; - } [self->_audioEngine prepare]; NSError *startErr = nil; if (![self->_audioEngine startAndReturnError:&startErr]) { NSString *msg = [NSString stringWithFormat:@"Failed to start audio engine: %@", startErr.localizedDescription]; [self emitOnError:msg]; - // clean [self stopInternalWithoutEvents:YES]; - reject(@"start", msg, startErr); // still reject if not yet resolved self->_isStarting = NO; + reject(@"start", msg, startErr); return; } - - // Timeout timer after engine started + // Timeout timer if (timeoutMs >= 0) { self->_timeoutSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self->_processingQueue); if (self->_timeoutSource) { @@ -286,18 +214,69 @@ - (void)start:(JS::NativeVosk::VoskOptions *_Nullable)options dispatch_resume(self->_timeoutSource); } } - CFAbsoluteTime tEnd = CFAbsoluteTimeGetCurrent(); - NSLog(@"[Vosk] start() total after permission: %.2f ms", (tEnd - tStart) * 1000.0); - // Resolve promise (success path) - self->_isStarting = NO; + NSLog(@"[Vosk] start() engine running (tap pending): %.2f ms", (tEnd - tStart) * 1000.0); + // Resolve early; tap installation will be async via retry resolve(nil); + self->_isStarting = NO; + self->_pendingTap = YES; + self->_tapRetryCount = 0; + [self scheduleTapInstallationWithBufferSize:bufferSize]; }); }); }); }]; } +// Retry-based tap installer; called on main queue only +- (void)scheduleTapInstallationWithBufferSize:(AVAudioFrameCount)bufferSize { + if (!_isRunning) { _pendingTap = NO; return; } + if (_tapInstalled) { _pendingTap = NO; return; } + if (!_recognizer) { _pendingTap = NO; return; } + AVAudioFormat *fmt = [_inputNode inputFormatForBus:0]; + double sr = fmt ? fmt.sampleRate : 0.0; + AVAudioChannelCount ch = fmt ? fmt.channelCount : 0; + if (sr <= 0.0 || ch == 0) { + if (_tapRetryCount == 0) { + NSLog(@"[Vosk] Tap install waiting for valid format… (sr=%.1f ch=%u)", sr, (unsigned int)ch); + } + if (_tapRetryCount >= 25) { // ~2.5s at 100ms intervals + [self emitOnError:@"Unable to obtain valid audio input format (stabilization timeout)"]; _pendingTap = NO; return; } + _tapRetryCount++; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self scheduleTapInstallationWithBufferSize:bufferSize]; }); + return; + } + // Have a valid format now + NSLog(@"[Vosk] Tap install attempt with format sr=%.1f ch=%u retry=%d", sr, (unsigned int)ch, _tapRetryCount); + __weak __typeof(self) weakSelfTap = self; + @try { + [_inputNode installTapOnBus:0 bufferSize:bufferSize format:fmt block:^(AVAudioPCMBuffer *buffer, AVAudioTime *when) { + __strong __typeof(self) self = weakSelfTap; if (!self) return; if (!self->_isRunning) return; + dispatch_async(self->_processingQueue, ^{ + if (!self->_isRunning) return; VoskRecognizer *recognizer = self->_recognizer; if (!recognizer) return; + AVAudioFrameCount frames = buffer.frameLength; if (frames == 0) return; + int accepted = 0; + if (buffer.int16ChannelData && buffer.int16ChannelData[0]) { + int dataLen = (int)(frames * sizeof(int16_t)); + accepted = vosk_recognizer_accept_waveform(recognizer, (const char *)buffer.int16ChannelData[0], (int32_t)dataLen); + } else if (buffer.floatChannelData && buffer.floatChannelData[0]) { + float *const *floatChannels = buffer.floatChannelData; std::vector pcm16; pcm16.resize(frames); float *ch0 = floatChannels[0]; + for (AVAudioFrameCount i = 0; i < frames; ++i) { float s = ch0[i]; if (s > 1.f) s = 1.f; else if (s < -1.f) s = -1.f; pcm16[i] = (int16_t)lrintf(s * 32767.f); } + int dataLen = (int)(frames * sizeof(int16_t)); + accepted = vosk_recognizer_accept_waveform(recognizer, (const char *)pcm16.data(), (int32_t)dataLen); + } else { return; } + const char *cstr = NULL; BOOL isFinal = NO; if (accepted) { cstr = vosk_recognizer_result(recognizer); isFinal = YES; } else { cstr = vosk_recognizer_partial_result(recognizer); } + NSString *json = cstr ? [NSString stringWithUTF8String:cstr] : nil; + dispatch_async(dispatch_get_main_queue(), ^{ if (!json) return; NSData *data = [json dataUsingEncoding:NSUTF8StringEncoding]; NSDictionary *parsed = data ? [NSJSONSerialization JSONObjectWithData:data options:0 error:nil] : nil; if (![parsed isKindOfClass:[NSDictionary class]]) { if (isFinal) { [self emitOnResult:json]; } else { [self emitOnPartialResult:json]; } return; } NSString *text = parsed[@"text"]; NSString *partial = parsed[@"partial"]; if (isFinal) { if (text.length > 0) { [self emitOnResult:text]; } self->_lastPartial = nil; } else { if (partial.length > 0 && (!self->_lastPartial || ![self->_lastPartial isEqualToString:partial])) { [self emitOnPartialResult:partial]; } self->_lastPartial = partial ?: self->_lastPartial; } }); + }); + }]; + _tapInstalled = YES; _pendingTap = NO; NSLog(@"[Vosk] Tap installed successfully after %d retries.", _tapRetryCount); + } @catch (NSException *ex) { + [self emitOnError:[NSString stringWithFormat:@"Failed to install tap (retry phase): %@", ex.reason ?: @"unknown"]]; + _pendingTap = NO; [self stopInternalWithoutEvents:YES]; + } +} + - (void)stop { if (!_isRunning) return; // idempotent @@ -335,6 +314,7 @@ - (void)stopInternalWithoutEvents:(BOOL)withoutEvents { _isRunning = NO; // prevents new buffers from being processed _isStarting = NO; _tapInstalled = NO; + _pendingTap = NO; if (_audioEngine.isRunning) { [_audioEngine stop]; diff --git a/package.json b/package.json index cc31733..0108ce4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-vosk", - "version": "2.1.5", + "version": "2.1.6", "description": "Speech recognition module for react native using Vosk library", "types": "lib/typescript/src/index.d.ts", "main": "lib/commonjs/index.js",