@@ -286,8 +286,8 @@ actor ExpoSpeechRecognizer: ObservableObject {
286
286
)
287
287
}
288
288
289
- // Don't run any timers if the audio source is from a file
290
- let continuous = options. continuous || isSourcedFromFile
289
+ // Run timers on non-continuous mode, as long as the audio source is the mic
290
+ let shouldRunTimers = ! options. continuous && ! isSourcedFromFile
291
291
let audioEngine = self . audioEngine
292
292
293
293
self . task = recognizer. recognitionTask (
@@ -300,18 +300,20 @@ actor ExpoSpeechRecognizer: ObservableObject {
300
300
}
301
301
}
302
302
303
- // Result handler
303
+ // Handle the result
304
304
self ? . recognitionHandler (
305
305
audioEngine: audioEngine,
306
306
result: result,
307
307
error: error,
308
308
resultHandler: resultHandler,
309
309
errorHandler: errorHandler,
310
- continuous: continuous
310
+ continuous: options. continuous,
311
+ shouldRunTimers: shouldRunTimers,
312
+ canEmitInterimResults: options. interimResults
311
313
)
312
314
} )
313
315
314
- if !continuous {
316
+ if shouldRunTimers {
315
317
invalidateAndScheduleTimer ( )
316
318
}
317
319
@@ -449,7 +451,10 @@ actor ExpoSpeechRecognizer: ObservableObject {
449
451
request = SFSpeechAudioBufferRecognitionRequest ( )
450
452
}
451
453
452
- request. shouldReportPartialResults = options. interimResults
454
+ // We also force-enable partial results on non-continuous mode,
455
+ // which will allow us to re-schedule timers when text is detected
456
+ // These won't get emitted to the user, however
457
+ request. shouldReportPartialResults = options. interimResults || options. continuous
453
458
454
459
if recognizer. supportsOnDeviceRecognition {
455
460
request. requiresOnDeviceRecognition = options. requiresOnDeviceRecognition
@@ -613,12 +618,25 @@ actor ExpoSpeechRecognizer: ObservableObject {
613
618
error: Error ? ,
614
619
resultHandler: @escaping ( SFSpeechRecognitionResult ) -> Void ,
615
620
errorHandler: @escaping ( Error ) -> Void ,
616
- continuous: Bool
621
+ continuous: Bool ,
622
+ shouldRunTimers: Bool ,
623
+ canEmitInterimResults: Bool
617
624
) {
625
+ // When a final result is returned, we should expect the task to be idle or stopping
618
626
let receivedFinalResult = result? . isFinal ?? false
619
627
let receivedError = error != nil
620
628
621
- if let result: SFSpeechRecognitionResult {
629
+ // Hack for iOS 18 to detect final results
630
+ // See: https://forums.developer.apple.com/forums/thread/762952 for more info
631
+ // This can be emitted multiple times during a continuous session, unlike `result.isFinal` which is only emitted once
632
+ var receivedFinalLikeResult : Bool = receivedFinalResult
633
+ if #available( iOS 18 . 0 , * ) , !receivedFinalLikeResult {
634
+ receivedFinalLikeResult = result? . speechRecognitionMetadata? . speechDuration ?? 0 > 0
635
+ }
636
+
637
+ let shouldEmitResult = receivedFinalResult || canEmitInterimResults || receivedFinalLikeResult
638
+
639
+ if let result: SFSpeechRecognitionResult , shouldEmitResult {
622
640
Task { @MainActor in
623
641
let taskState = await task? . state
624
642
// Make sure the task is running before emitting the result
@@ -638,15 +656,16 @@ actor ExpoSpeechRecognizer: ObservableObject {
638
656
}
639
657
}
640
658
641
- if receivedFinalResult || receivedError {
659
+ if ( receivedFinalLikeResult && !continuous ) || receivedError || receivedFinalResult {
642
660
Task { @MainActor in
643
661
await reset ( )
644
662
}
663
+ return
645
664
}
646
665
647
666
// Non-continuous speech recognition
648
667
// Stop the speech recognizer if the timer fires after not receiving a result for 3 seconds
649
- if !continuous && !receivedError {
668
+ if shouldRunTimers && !receivedError {
650
669
invalidateAndScheduleTimer ( )
651
670
}
652
671
}
0 commit comments