[P0-②] On-device ASR three-piece set

- Add NSSpeechRecognitionUsageDescription to both target Info.plists
- KeyboardViewController.requestSpeechPermission() wraps
  SFSpeechRecognizer.requestAuthorization
- pressBegan() now requests Speech permission right after mic permission
- ASRService emits a DEBUG warning when recognizer.supportsOnDeviceRecognition
  is false so cloud fallback is visible during dev
- CHANGELOG: move iOS 26 SpeechAnalyzer out of Unreleased into [0.2.0] Planned,
  so the iOS 18 SFSpeechRecognizer path is the only ASR shipped in v0.1

xcodebuild iOS Simulator: SUCCEEDED
This commit is contained in:
Zhongshu
2026-06-18 10:41:20 +08:00
parent e0b92ff35a
commit e93bab50b4
5 changed files with 68 additions and 15 deletions
+35 -2
View File
@@ -20,6 +20,7 @@
import UIKit
import SwiftUI
import AVFoundation
import Speech
import OSGKeyboardShared
@objc(KeyboardViewController)
@@ -199,12 +200,26 @@ public final class KeyboardViewController: UIInputViewController {
// user already feels the press registered.
Task { @MainActor [weak self] in
guard let self else { return }
let granted = await self.requestMicPermission()
guard granted else {
let micGranted = await self.requestMicPermission()
guard micGranted else {
self.state.phase = .error("麦克风被拒绝,请到「设置」中允许")
self.scheduleAutoClearError()
return
}
// iOS 18 SFSpeechRecognizer path: we explicitly ask for Speech
// recognition permission. Without this call + the
// NSSpeechRecognitionUsageDescription key in Info.plist the
// recogniser silently returns .denied and the user hears
// nothing back.
// iOS 26 SpeechAnalyzer path (planned for the next release)
// does not expose an explicit request API the framework
// prompts via the same plist key on first use.
let speechGranted = await self.requestSpeechPermission()
guard speechGranted else {
self.state.phase = .error("语音识别被拒绝,请到「设置」中允许")
self.scheduleAutoClearError()
return
}
self.startPipeline()
}
}
@@ -348,6 +363,24 @@ public final class KeyboardViewController: UIInputViewController {
}
}
/// Request Speech Recognition permission. Returns true if granted
/// (or already authorised). For the iOS 18 SFSpeechRecognizer path
/// this is required before recognition can begin; for the iOS 26
/// SpeechAnalyzer path the framework prompts on first use.
private func requestSpeechPermission() async -> Bool {
await withCheckedContinuation { (cont: CheckedContinuation<Bool, Never>) in
SFSpeechRecognizer.requestAuthorization { status in
switch status {
case .authorized: cont.resume(returning: true)
case .denied, .restricted, .notDetermined:
cont.resume(returning: false)
@unknown default:
cont.resume(returning: false)
}
}
}
}
// MARK: - Open host app
private func openHostApp() {