Bring advanced speech-to-text to your app with SpeechAnalyzer
Machine Learning & AI 高级 2m

用 SpeechAnalyzer 实现高级语音转文字

Bring advanced speech-to-text to your app with SpeechAnalyzer

2025年6月9日

在 Apple 官方观看视频

一句话判断

SFSpeechRecognizer 的继任者来了——SpeechAnalyzer 用 Swift Concurrency 和 AsyncSequence 重构了整套语音转文字 API,模型完全离线运行且不占用 App 内存空间,支持长音频、远距离拾音和 volatile 实时预览结果。

这场 Session 讲了什么

SpeechAnalyzer 取代 SFSpeechRecognizer 成为 iOS 26 上的语音转文字标准 API。核心架构是:SpeechAnalyzer 管理一个分析会话,你向它喂入音频 buffer(通过 AsyncSequence),它经过 SpeechTranscriber 模块处理后返回转写结果(也是 AsyncSequence)。所有操作基于音频时间轴上的 timecode 对齐,精确到单个采样点。

模型方面,SpeechTranscriber 搭载了 Apple 自研的新模型,相比 SFSpeechRecognizer 依赖的 Siri 模型有两个关键优势:支持长音频和远距离场景(会议、讲座),并且速度快得多。模型存储在系统级存储空间里,不增加 App 的下载体积和运行时内存。模型通过 AssetInventory API 下载和管理,支持的语言有中文、英文等,更多语言持续添加中。不支持的语言还可以走 DictationTranscriber 路径。

Session 用一个儿童睡前故事录音 App 做了完整 demo:录音 -> 实时转写 -> 播放时逐词高亮 -> 结束后用 Foundation Models 生成标题。整个流程展示了 volatile result(实时预览)、finalized result(最终结果)、AudioTimeRange 时间同步、音频格式转换、模型下载检查等全部关键 API。

值得深挖的点

Volatile Result 的延迟与精度权衡

Volatile result 是”几乎在说话后立即返回”的粗略转写结果,精度低但延迟极低。随着更多音频输入,模型会逐步改进结果,最终交付一个 finalized result。这个设计的本质是用 UI 上的”渐进式修正”换取感知延迟的降低——用户看到文字在实时出现,即使有些词会变。

是否启用 volatile result 取决于你的场景。如果做实时字幕或实时会议记录,volatile 几乎是必须的——用户需要即时反馈。如果是转写录音文件或语音备忘录,只用 finalized result 就够了,避免 UI 闪烁。Session 里的 Story App 同时用了两者:volatile 用浅色透明度显示,finalized 用正常样式,视觉上区分清晰。

音频格式的隐性要求

SpeechTranscriber 有 bestAvailableAudioFormat 属性,你的音频输入必须转换到这个格式。Session 里的做法是用 AVAudioConverter 把 AVAudioEngine 的输出格式转成 bestAvailableAudioFormat 再喂给 transcriber。如果你跳过这一步,大概率会遇到静默失败或乱码输出。

另一个容易忽略的点:必须在项目设置里启用麦克风权限,并且在启动录音前请求 AVAudioSession 的录音权限。Session 里这两步是串行做的。

异步流的生命周期管理

Session 用 Task、AsyncStream 和输入构建器串起了整个流程。关键的生命周期节点:创建 SpeechTranscriber 时拿到 inputBuilder,录音时往 inputBuilder 喂音频,停止时调用 analyzer.finalize() 确保所有 volatile result 被 finalize。如果不调 finalize,你可能会丢失最后几秒的转写结果。

代码片段

三步搭建实时转写

// Step 1: 配置 SpeechTranscriber
let transcriber = try SpeechTranscriber(
    locale: Locale(identifier: "zh-CN"),
    options: [.volatileResults, .audioTimeRange]
)

// Step 2: 确保模型可用
func ensureModel(for transcriber: SpeechTranscriber) async throws {
    guard transcriber.isLanguageAvailable else { return }
    let state = await AssetInventory.state(for: transcriber.locale)
    if state != .installed {
        let progress = AssetInventory.download(for: transcriber.locale)
        // 可以用 progress 显示下载进度
        await progress.complete()
    }
}

// Step 3: 创建 Analyzer 并处理结果
let analyzer = SpeechAnalyzer(modules: [transcriber])
let inputBuilder = try await analyzer.start()

// 后台接收结果
Task {
    for try await result in transcriber.results {
        if result.isFinal {
            finalizedTranscript.append(result.text)
            volatileTranscript = ""
        } else {
            volatileTranscript = result.text
        }
    }
}

坑:isLanguageAvailable 检查的是设备是否支持该语言的转写,和语言是否已下载是两回事——要分别检查。

音频输入与格式转换

// 配置 AVAudioEngine
let audioEngine = AVAudioEngine()
let inputNode = audioEngine.inputNode
let format = inputNode.outputFormat(forBus: 0)

// 创建 AsyncStream 接收音频
let (audioStream, continuation) = AsyncStream<AudioBuffer>.makeStream()

inputNode.installTap(onBus: 0, bufferSize: 1024, format: format) { buffer, _ in
    continuation.yield(buffer)
}

// 格式转换后喂给 transcriber
Task {
    let converter = AVAudioConverter(from: format,
                                      to: transcriber.bestAvailableAudioFormat)
    for await buffer in audioStream {
        // 转换格式...
        try await inputBuilder.append(convertedBuffer)
    }
}

坑:AudioBuffer 的 sampleRate 和 channelCount 必须和 bestAvailableAudioFormat 一致,否则输入会被忽略。

用 AttributedString 属性同步播放高亮

// 每个转写结果的 AttributedString 里自带 audioTimeRange
struct TranscriptView: View {
    let transcript: AttributedString
    let currentTime: CMTime

    var body: some View {
        Text(transcript.transformingAttributes(\.audioTimeRange) { range in
            if range.value.contains(currentTime) {
                range.value = .backgroundColor(.yellow)
            }
        })
    }
}

坑:audioTimeRange 是 CMTimeRange 类型,直接在 SwiftUI Text 里用 AttributedString 的 transformingAttributes 做条件高亮是最简洁的方案。

最佳实践

模型管理方面,不要在 App 启动时就下载所有语言的模型——设备存储有限,而且每个 App 同时只能持有有限数量的语言模型。按需下载,用户切换语言时再下载对应模型。如果超出语言数量限制,用 AssetInventory.deallocate 释放不常用的。

对于只需要转写录音文件(非实时)的场景,用最简单的单函数模式:创建 transcriber -> 创建 analyzer -> 用 analyzeSequence 读文件 -> reduce 拼接结果 -> 返回 AttributedString。不需要处理 volatile result,不需要管理音频流,代码量极少。

Foundation Models 配合 SpeechAnalyzer 是一个自然的组合——转写结果作为 LLM 的输入,可以做摘要、标题生成、情感分析等。Session 的 demo 用这个组合在转写完成后自动生成了故事标题,代码量只有一行。

还有什么值得关注

  • DictationTranscriber 作为备选方案,不需要用户去设置里手动开启语言支持——比 SFSpeechRecognizer 改善了一步。
  • 模型在系统存储空间里管理,Apple 会自动更新。你的 App 不需要处理模型版本管理。
  • analyzeSequence 方法可以直接从文件读取音频并执行转写,不需要手动创建音频流——适合批量处理场景。
  • SpeechAnalyzer + Apple Intelligence 可以实现通话摘要功能,Notes、Voice Memos、Journal 都已经在用了。
机器学习