Skip to content

Commit bfaea64

Browse files
committed
fix(ios): non-continuous recognition timers
1 parent 09f510a commit bfaea64

File tree

1 file changed

+29
-10
lines changed

1 file changed

+29
-10
lines changed

ios/ExpoSpeechRecognizer.swift

+29-10
Original file line numberDiff line numberDiff line change
@@ -286,8 +286,8 @@ actor ExpoSpeechRecognizer: ObservableObject {
286286
)
287287
}
288288

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
291291
let audioEngine = self.audioEngine
292292

293293
self.task = recognizer.recognitionTask(
@@ -300,18 +300,20 @@ actor ExpoSpeechRecognizer: ObservableObject {
300300
}
301301
}
302302

303-
// Result handler
303+
// Handle the result
304304
self?.recognitionHandler(
305305
audioEngine: audioEngine,
306306
result: result,
307307
error: error,
308308
resultHandler: resultHandler,
309309
errorHandler: errorHandler,
310-
continuous: continuous
310+
continuous: options.continuous,
311+
shouldRunTimers: shouldRunTimers,
312+
canEmitInterimResults: options.interimResults
311313
)
312314
})
313315

314-
if !continuous {
316+
if shouldRunTimers {
315317
invalidateAndScheduleTimer()
316318
}
317319

@@ -449,7 +451,10 @@ actor ExpoSpeechRecognizer: ObservableObject {
449451
request = SFSpeechAudioBufferRecognitionRequest()
450452
}
451453

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
453458

454459
if recognizer.supportsOnDeviceRecognition {
455460
request.requiresOnDeviceRecognition = options.requiresOnDeviceRecognition
@@ -613,12 +618,25 @@ actor ExpoSpeechRecognizer: ObservableObject {
613618
error: Error?,
614619
resultHandler: @escaping (SFSpeechRecognitionResult) -> Void,
615620
errorHandler: @escaping (Error) -> Void,
616-
continuous: Bool
621+
continuous: Bool,
622+
shouldRunTimers: Bool,
623+
canEmitInterimResults: Bool
617624
) {
625+
// When a final result is returned, we should expect the task to be idle or stopping
618626
let receivedFinalResult = result?.isFinal ?? false
619627
let receivedError = error != nil
620628

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 {
622640
Task { @MainActor in
623641
let taskState = await task?.state
624642
// Make sure the task is running before emitting the result
@@ -638,15 +656,16 @@ actor ExpoSpeechRecognizer: ObservableObject {
638656
}
639657
}
640658

641-
if receivedFinalResult || receivedError {
659+
if (receivedFinalLikeResult && !continuous) || receivedError || receivedFinalResult {
642660
Task { @MainActor in
643661
await reset()
644662
}
663+
return
645664
}
646665

647666
// Non-continuous speech recognition
648667
// Stop the speech recognizer if the timer fires after not receiving a result for 3 seconds
649-
if !continuous && !receivedError {
668+
if shouldRunTimers && !receivedError {
650669
invalidateAndScheduleTimer()
651670
}
652671
}

0 commit comments

Comments
 (0)