81581f0e5f
Two related cleanups the user asked for in one shot:
1. iPhone-only is now enforced at every target — Mac Catalyst and
visionOS were never configured in `project.yml`, but the
`OSGKeyboardShared` framework and the two test bundles were
still defaulting to `TARGETED_DEVICE_FAMILY = "1,2"` (iPhone +
iPad). All four targets now explicitly set `"1"`. SDK is
`iphoneos` for everyone, no `xros` / `macosx`.
2. Deployment target bumped from iOS 18.0 to iOS 26.0 across the
board (`project.yml` + the ext's per-target setting). With
iOS 26 as the floor, the iOS 18–25 SFSpeechRecognizer path
became dead code and several `#available` checks became
always-true. Removed:
- `AppleSpeechASR` (the entire SFSpeechRecognizer-based ASR
backend) and the `#available(iOS 26.0, *)` factory branch.
`ASRServiceFactory.make()` now returns `SpeechAnalyzerASR()`
directly. SpeechAnalyzer is always fully on-device, which
also made the `requiresOnDevice` flag meaningless.
- `requiresOnDevice` from the `ASRService.transcribe` protocol
signature, from `ProviderConfig`, `AppGroupStore`,
`KeyboardState`, `AppGroupPersistor`, and the ext's
`KeyboardViewController` (`state.requiresOnDevice`,
`state.setRequiresOnDevice`, `persistRequiresOnDevice`).
- `#available(iOS 17.0, *)` branch in
`PreviewASRController.requestMicrophonePermission` and the
ext's `PermissionManager.requestMicPermission` — both now
just call the iOS 17+ `AVAudioApplication` API directly.
- The `else` (iOS 18–25) branch in `SettingsView.asrEngineRow`
— the on-device-only toggle is gone, the row is a static
"SpeechAnalyzer active" badge. Same for the `else` branch
in `EnginePickerSection.localSubtitle`.
- `makeMicAuthHandler` (the iOS < 17 mic permission callback
wrapper) from `PreviewASRController`.
No `#available` / `@available` checks remain in the codebase
except for the SpeechAnalyzer class itself (now unnecessary
too, but kept for clarity — `AVAudioApplication` and
`SpeechAnalyzer` are both iOS 17+ / iOS 26+ respectively,
and the deployment target of 26 makes the explicit
`@available` redundant; I left the SpeechAnalyzer class
un-`@available` and removed the `@available(iOS 26.0, *)`
decoration since it's no longer needed).
The Keyboard ext's existing ASRService usage
(`asr.transcribe(stream:locale:)`) is unchanged at the
call-site level — just the third argument is gone.
3. Updated `info.plist` UISupportedInterfaceOrientations is
already `[UIInterfaceOrientationPortrait]` only, which is
correct for an iPhone-only app; no change needed.
Build: BUILD SUCCEEDED.
Tests: 22/22 pass.
Verified: `TARGETED_DEVICE_FAMILY = 1` on all four targets,
`SDKROOT = iphoneos` on all four.
🤖 Generated with Claude Code
124 lines
4.4 KiB
Swift
124 lines
4.4 KiB
Swift
// AppGroupStore.swift
|
|
// OSGKeyboard · Shared
|
|
//
|
|
// Convenience wrapper around App Group UserDefaults for non-Published reads.
|
|
// Used by the keyboard extension (no SwiftUI) to read config without
|
|
// instantiating an ObservableObject.
|
|
//
|
|
// `apiKey` is NOT read from UserDefaults — see `Keychain.swift`. We
|
|
// share access between the host app and the keyboard extension via a
|
|
// shared keychain-access-group declared in both targets' entitlements.
|
|
|
|
import Foundation
|
|
|
|
public struct AppGroupStore: @unchecked Sendable {
|
|
public let defaults: UserDefaults
|
|
|
|
public init(defaults: UserDefaults = AppGroup.defaults) {
|
|
self.defaults = defaults
|
|
}
|
|
|
|
// MARK: - Keys
|
|
|
|
private enum Key {
|
|
static let providerId = "config.providerId"
|
|
static let baseURL = "config.baseURL"
|
|
static let model = "config.model"
|
|
static let systemPrompt = "config.systemPrompt"
|
|
static let modeId = "config.modeId"
|
|
static let localeId = "config.localeId"
|
|
static let engineMode = "config.engineMode"
|
|
}
|
|
|
|
// MARK: - Reads
|
|
|
|
public var providerId: String {
|
|
defaults.string(forKey: Key.providerId) ?? "openai"
|
|
}
|
|
|
|
public var baseURL: String {
|
|
defaults.string(forKey: Key.baseURL) ?? LLMProvider.provider(id: providerId).defaultBaseURL
|
|
}
|
|
|
|
/// API key lives in the Keychain (cross-process, encrypted at rest).
|
|
/// Returns "" when nothing is stored so the LLMClient can surface a
|
|
/// `noAPIKey` error rather than firing off an obviously-bad request.
|
|
public var apiKey: String {
|
|
Keychain.apiKey() ?? ""
|
|
}
|
|
|
|
public var model: String {
|
|
defaults.string(forKey: Key.model) ?? LLMProvider.provider(id: providerId).defaultModel
|
|
}
|
|
|
|
public var systemPrompt: String {
|
|
defaults.string(forKey: Key.systemPrompt) ?? Self.defaultSystemPrompt(for: providerId)
|
|
}
|
|
|
|
public var modeId: String {
|
|
defaults.string(forKey: Key.modeId) ?? "polish"
|
|
}
|
|
|
|
public var localeId: String {
|
|
defaults.string(forKey: Key.localeId) ?? "auto"
|
|
}
|
|
|
|
/// "local" → on-device ASR only, no LLM polishing.
|
|
/// "cloud" → ASR + LLM polish (default behaviour).
|
|
public var engineMode: String {
|
|
defaults.string(forKey: Key.engineMode) ?? "cloud"
|
|
}
|
|
|
|
// MARK: - Writes
|
|
|
|
public func setModeId(_ id: String) {
|
|
defaults.set(id, forKey: Key.modeId)
|
|
}
|
|
|
|
public func setLocaleId(_ id: String) {
|
|
defaults.set(id, forKey: Key.localeId)
|
|
}
|
|
|
|
public func setEngineMode(_ mode: String) {
|
|
defaults.set(mode, forKey: Key.engineMode)
|
|
}
|
|
|
|
// MARK: - Client
|
|
|
|
public func makeClient() -> LLMClient {
|
|
OpenAICompatibleClient(
|
|
baseURL: baseURL,
|
|
apiKey: apiKey,
|
|
model: model
|
|
)
|
|
}
|
|
|
|
// MARK: - Defaults
|
|
|
|
/// Per-provider default system prompt. We bias the prompt by the
|
|
/// provider's *primary* language so Chinese LLMs naturally return
|
|
/// Chinese for Chinese input, and English LLMs stay terse.
|
|
public static func defaultSystemPrompt(for providerId: String) -> String {
|
|
switch providerId {
|
|
case "zhipu", "moonshot", "qwen", "deepseek":
|
|
return """
|
|
你是一位语音输入润色助手。请将用户的口述改写为干净的中文(或英文)书面文字:
|
|
1) 保留原意,不编造事实;保持输入语言。
|
|
2) 添加恰当的标点、大小写、段落。
|
|
3) 当用户枚举"第一…第二…第三…"时,使用 markdown 列表。
|
|
4) 简洁,不超出原长 1.5 倍;可去掉无意义的口头禅(嗯、啊、那个)。
|
|
5) 只输出润色后的正文,不要解释、不要加引号。
|
|
"""
|
|
default:
|
|
return """
|
|
You are a voice-input polishing assistant. The user has spoken informally; rewrite their dictation as clean written text:
|
|
1) Preserve the user's original intent and meaning; do not invent facts.
|
|
2) Add proper punctuation, capitalization, and paragraph breaks.
|
|
3) When the user enumerates items ("first ... second ... third"), output a markdown list.
|
|
4) Keep the output concise — do not exceed 1.5x the spoken length. Drop filler words (um, uh, like).
|
|
5) Output in the same language as the input. No quotes, no explanation, no preamble.
|
|
"""
|
|
}
|
|
}
|
|
}
|