# SpeechAnalyzer patterns (iOS 26+)

Companion to the `speech-recognition` skill ([../SKILL.md](../SKILL.md)).
Every API here needs iOS 26 or later.

## Picking modules

```swift
import Speech

func makeModule(for locale: Locale) async -> (any SpeechModule)? {
    if SpeechTranscriber.isAvailable,
       let matched = await SpeechTranscriber.supportedLocale(equivalentTo: locale) {
        return SpeechTranscriber(locale: matched, preset: .progressiveTranscription)
    }
    if let matched = await DictationTranscriber.supportedLocale(equivalentTo: locale) {
        return DictationTranscriber(locale: matched, preset: .progressiveLongDictation)
    }
    return nil        // disable the feature for this device and locale
}
```

### SpeechTranscriber presets

| Preset | Use it for |
|---|---|
| `.transcription` | Plain, accurate text |
| `.transcriptionWithAlternatives` | Editing UI that offers other candidate words |
| `.timeIndexedTranscriptionWithAlternatives` | Alternatives plus timing for each span |
| `.progressiveTranscription` | Low-latency live captions |
| `.timeIndexedProgressiveTranscription` | Live captions that also carry time ranges |

`SpeechDetector` never works alone; attach it next to a transcriber, and only
when skipping silence saves enough to justify occasionally dropping audio
that sounded like noise.

## Model assets

Speech models are not in your bundle. The system downloads and shares them
between apps, and your app asks for the ones its configuration needs:

```swift
func ensureAssets(for transcriber: SpeechTranscriber) async throws {
    if let install = try await AssetInventory.assetInstallationRequest(supporting: [transcriber]) {
        try await install.downloadAndInstall()
    }
}
```

For a language picker, combine `await SpeechTranscriber.installedLocales`,
`await SpeechTranscriber.supportedLocales` and
`AssetInventory.status(forModules:)`, which reports `.installed`,
`.downloading`, `.supported` (can be downloaded) or `.unsupported`.

An app can reserve only a limited number of locales. Give one back with
`AssetInventory.release(reservedLocale:)` when the user drops a language.

## Transcribing a file

The analyzer converts the file's audio format on its own.

```swift
import AVFoundation
import Speech

func transcript(of recording: URL, locale: Locale) async throws -> AttributedString {
    guard let matched = await SpeechTranscriber.supportedLocale(equivalentTo: locale) else {
        throw CocoaError(.featureUnsupported)
    }
    let transcriber = SpeechTranscriber(locale: matched, preset: .transcription)
    try await ensureAssets(for: transcriber)

    let analyzer = SpeechAnalyzer(modules: [transcriber])
    async let collected = transcriber.results.reduce(into: AttributedString()) { text, piece in
        text.append(piece.text)
    }

    let audio = try AVAudioFile(forReading: recording)
    if let lastSample = try await analyzer.analyzeSequence(from: audio) {
        try await analyzer.finalizeAndFinish(through: lastSample)
    } else {
        await analyzer.cancelAndFinishNow()
    }
    return try await collected
}
```

## Live audio

```swift
import AVFoundation
import Speech

@MainActor
final class LiveTranscript {
    private(set) var settled = AttributedString()
    private(set) var pending = AttributedString()
    private var analyzer: SpeechAnalyzer?
    private var feed: AsyncStream<AnalyzerInput>.Continuation?
    private var reader: Task<Void, Never>?

    func start(locale: Locale) async throws -> AVAudioFormat? {
        let transcriber = SpeechTranscriber(locale: locale, preset: .timeIndexedProgressiveTranscription)
        try await ensureAssets(for: transcriber)
        let analyzer = SpeechAnalyzer(modules: [transcriber])
        let format = await SpeechAnalyzer.bestAvailableAudioFormat(compatibleWith: [transcriber])

        let (inputs, feed) = AsyncStream.makeStream(of: AnalyzerInput.self)
        self.feed = feed
        self.analyzer = analyzer

        reader = Task { [weak self] in
            do {
                for try await piece in transcriber.results {
                    self?.absorb(piece)
                }
            } catch {
                // surface the error in the UI
            }
        }
        try await analyzer.start(inputSequence: inputs)
        return format        // convert microphone buffers to this before yielding
    }

    func push(_ converted: AVAudioPCMBuffer) {
        feed?.yield(AnalyzerInput(buffer: converted))
    }

    func stop() async throws {
        feed?.finish()
        try await analyzer?.finalizeAndFinishThroughEndOfInput()
        await reader?.value
    }

    private func absorb(_ piece: SpeechTranscriber.Result) {
        if piece.isFinal {
            settled.append(piece.text)
            pending = AttributedString()
        } else {
            pending = piece.text
        }
    }
}
```

Convert tap buffers with `AVAudioConverter` (or an audio pipeline you
already have) into the format returned by `bestAvailableAudioFormat`. Do not
feed raw input-node buffers unless their format already matches.

## Reading results

`SpeechTranscriber.Result.text` is an `AttributedString`. With a
time-indexed preset its runs carry audio time-range attributes, which is how
playback can highlight the word being spoken. Progressive presets deliver
volatile results that a later final result replaces, so keep the volatile
text in its own state, as `pending` does above.

## Ending a session

- An analyzer processes one input sequence at a time, and finishing your
  input stream does not end the session.
- After `analyzeSequence(_:)` returns the last sample time, call
  `finalizeAndFinish(through:)`.
- After starting with `start(inputSequence:)`, call
  `finalizeAndFinishThroughEndOfInput()` once input is complete.
- `cancelAndFinishNow()` stops immediately and discards pending work.

Once finished, the module result streams end and the analyzer refuses most
further calls. Create a new analyzer for the next session.

## Further reading

- https://developer.apple.com/documentation/speech/speechanalyzer
- https://developer.apple.com/documentation/speech/speechtranscriber
- https://developer.apple.com/documentation/speech/speechtranscriber/preset
- https://developer.apple.com/documentation/speech/dictationtranscriber
- https://developer.apple.com/documentation/speech/speechdetector
- https://developer.apple.com/documentation/speech/assetinventory
- WWDC25 session 277 on SpeechAnalyzer
