| name | speech-recognition |
| description | Transcribe speech to text using Apple's Speech framework. Use when implementing live microphone transcription with AVAudioEngine, recognizing recorded audio files, handling speech and microphone authorization, choosing on-device vs server-backed SFSpeechRecognizer behavior, or adopting SpeechAnalyzer, SpeechTranscriber, DictationTranscriber, AssetInventory, and async result streams on iOS 26+. |
Speech Recognition
Transcribe live and pre-recorded audio to text using Apple's Speech framework.
Covers SpeechAnalyzer / SpeechTranscriber (iOS 26+) and
SFSpeechRecognizer (iOS 10+) fallback guidance.
Scope boundary: Use this skill for speech-to-text recognition, speech
authorization, microphone capture plumbing, and result handling. Hand off text
analysis, language identification after transcription, sentiment, embeddings,
and translation to natural-language; hand off audio playback UI to avkit;
hand off summarization or generation over transcripts to apple-on-device-ai.
Contents
SpeechAnalyzer Strategy (iOS 26+)
Use SpeechAnalyzer for modern iOS 26+ speech analysis, especially long-form
recordings, live transcription, time-indexed transcripts, and fully on-device
flows. Keep SFSpeechRecognizer for iOS 10+ deployment targets, server-backed
locale coverage, or existing callback/delegate implementations.
Read SpeechAnalyzer patterns when
implementing an iOS 26+ transcription pipeline, model asset handling, volatile
results, or file/buffer examples.
SpeechAnalyzer setup checklist
- Choose the module:
SpeechTranscriber for the newer general-purpose on-device model.
DictationTranscriber when SpeechTranscriber is unavailable for the
current device or locale and dictation-compatible support is acceptable.
SpeechDetector only in conjunction with a transcriber when voice
activity detection is worth the accuracy/power tradeoff.
- Check support before creating the session:
SpeechTranscriber.isAvailable
SpeechTranscriber.supportedLocale(equivalentTo:)
SpeechTranscriber.installedLocales / supportedLocales when showing
language choices.
- Pick a documented preset:
.transcription for basic accurate transcription.
.progressiveTranscription for live UI updates.
.timeIndexedProgressiveTranscription when playback highlighting needs
audioTimeRange.
- Install required assets with
AssetInventory.assetInstallationRequest.
- Convert live audio buffers to
SpeechAnalyzer.bestAvailableAudioFormat(compatibleWith:) before yielding
AnalyzerInput.
- Consume module results from their
AsyncSequence in a separate task.
- Finish explicitly with
finalizeAndFinish(through:),
finalizeAndFinishThroughEndOfInput(), or cancelAndFinishNow().
Do not use an offlineTranscription preset; Apple does not document one.
Finishing an AsyncStream input sequence does not finish the analyzer session.
SFSpeechRecognizer Setup
Creating a recognizer with locale
import Speech
let recognizer = SFSpeechRecognizer()
let recognizer = SFSpeechRecognizer(locale: Locale(identifier: "en-US"))
guard let recognizer, recognizer.isAvailable else {
print("Speech recognition not available")
return
}
Monitoring availability changes
final class SpeechManager: NSObject, SFSpeechRecognizerDelegate {
private let recognizer = SFSpeechRecognizer()!
override init() {
super.init()
recognizer.delegate = self
}
func speechRecognizer(
_ speechRecognizer: SFSpeechRecognizer,
availabilityDidChange available: Bool
) {
}
}
Authorization
Request both speech recognition and microphone permissions before starting
live transcription. Add these keys to Info.plist:
NSSpeechRecognitionUsageDescription
NSMicrophoneUsageDescription
import Speech
import AVFoundation
func requestPermissions() async -> Bool {
let speechStatus = await withCheckedContinuation { continuation in
SFSpeechRecognizer.requestAuthorization { status in
continuation.resume(returning: status)
}
}
guard speechStatus == .authorized else { return false }
let micStatus: Bool
if #available(iOS 17, *) {
micStatus = await AVAudioApplication.requestRecordPermission()
} else {
micStatus = await withCheckedContinuation { continuation in
AVAudioSession.sharedInstance().requestRecordPermission { granted in
continuation.resume(returning: granted)
}
}
}
return micStatus
}
Live Microphone Transcription
The standard pattern: AVAudioEngine captures microphone audio → buffers are
appended to SFSpeechAudioBufferRecognitionRequest → results stream in.
import Speech
import AVFoundation
final class LiveTranscriber {
private let recognizer = SFSpeechRecognizer(locale: Locale(identifier: "en-US"))!
private let audioEngine = AVAudioEngine()
private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest?
private var recognitionTask: SFSpeechRecognitionTask?
func startTranscribing() throws {
recognitionTask?.cancel()
recognitionTask = nil
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(.record, mode: .measurement, options: .duckOthers)
try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
let request = SFSpeechAudioBufferRecognitionRequest()
request.shouldReportPartialResults = true
self.recognitionRequest = request
recognitionTask = recognizer.recognitionTask(with: request) { result, error
result {
text result.bestTranscription.formattedString
()
result.isFinal {
.stopTranscribing()
}
}
error {
()
.stopTranscribing()
}
}
inputNode audioEngine.inputNode
recordingFormat inputNode.outputFormat(forBus: )
inputNode.installTap(onBus: , bufferSize: , format: recordingFormat) {
buffer,
request.append(buffer)
}
audioEngine.prepare()
audioEngine.start()
}
() {
audioEngine.stop()
audioEngine.inputNode.removeTap(onBus: )
recognitionRequest.endAudio()
recognitionRequest
recognitionTask.cancel()
recognitionTask
}
}
Pre-Recorded Audio File Recognition
Use SFSpeechURLRecognitionRequest for audio files on disk:
func transcribeFile(at url: URL) async throws -> String {
guard let recognizer = SFSpeechRecognizer(), recognizer.isAvailable else {
throw SpeechError.unavailable
}
let request = SFSpeechURLRecognitionRequest(url: url)
request.shouldReportPartialResults = false
return try await withCheckedThrowingContinuation { continuation in
var didResume = false
recognizer.recognitionTask(with: request) { result, error in
guard !didResume else { return }
if let error {
didResume = true
continuation.resume(throwing: error)
} else if let result, result.isFinal {
didResume = true
continuation.resume(
returning: result.bestTranscription.formattedString
)
}
}
}
}
On-Device vs Server Recognition
SFSpeechRecognizer can use on-device recognition for supported locales on
iOS 13+. If supportsOnDeviceRecognition is false, the recognizer requires a
network connection. requiresOnDeviceRecognition only has effect when the
recognizer supports it.
let recognizer = SFSpeechRecognizer(locale: Locale(identifier: "en-US"))!
if recognizer.supportsOnDeviceRecognition {
let request = SFSpeechAudioBufferRecognitionRequest()
request.requiresOnDeviceRecognition = true
}
SFSpeechRecognizer requests may still be a poor fit for long-form capture.
Apple documents a roughly one-minute task limit for speech recognition and
other service limits. For long recordings on iOS 26+, prefer SpeechAnalyzer;
otherwise chunk or restart recognition before the limit and preserve transcript
state across tasks.
Handling Results
Partial vs final results
With shouldReportPartialResults, replace the displayed partial transcript until SFSpeechRecognitionResult.isFinal commits it. This is separate from SpeechTranscriber.Result.isFinal, whose volatile attributed range must be replaced by the final result for that range. The live example and references/speechanalyzer-patterns.md contain the canonical loops.
Accessing alternative transcriptions and confidence
recognizer.recognitionTask(with: request) { result, error in
guard let result else { return }
let best = result.bestTranscription
for transcription in result.transcriptions {
for segment in transcription.segments {
print("\(segment.substring): \(segment.confidence)")
}
}
}
Adding punctuation (iOS 16+)
let request = SFSpeechAudioBufferRecognitionRequest()
request.addsPunctuation = true
Contextual strings
Improve recognition of domain-specific terms:
let request = SFSpeechAudioBufferRecognitionRequest()
request.contextualStrings = ["SwiftUI", "Xcode", "CloudKit"]
Common Mistakes
| Mistake | Fix |
|---|
| Live audio requests only speech authorization | Require both speech and microphone permission. |
| Recognizer availability is checked once | Observe delegate availability changes and model loss of service. |
| Final/error/rollover paths perform different cleanup | Use one stopTranscribing() owner for engine stop, tap removal, endAudio, and task cancellation. |
| On-device mode is forced for every locale | Check supportsOnDeviceRecognition and provide a fallback. |
One SFSpeechRecognizer task is used for long-form capture | Prefer SpeechAnalyzer on iOS 26+ or roll bounded segments while preserving committed transcript. |
| Finishing analyzer input is treated as session completion | Explicitly finalize through the last sample or cancel-and-finish. |
| Volatile SpeechAnalyzer results are appended | Replace the volatile range until a final result commits it. |
| A second recognition task starts before the first ends | Cancel/finish the active task and complete cleanup before replacement. |
Load references/speechanalyzer-patterns.md for complete analyzer finalization and volatile-result code.
Review Checklist
References