diff --git a/.gitignore b/.gitignore index 773a29b..987dd7d 100644 --- a/.gitignore +++ b/.gitignore @@ -112,3 +112,18 @@ CHANGES.md .ai-skills-layout-shots/ .workbuddy-ai/ reports/ + +# Internal planning / audit docs kept locally only (zero in-repo or +# GitHub-Pages references; never re-add these — they were removed +# in 2026-08 to keep the public docs/ limited to GitHub Pages assets). +/docs/ACCOUNT_MANAGED_GATEWAY_TESTING.md +/docs/APPSTORE_METADATA.md +/docs/STOREKIT_TESTING.md +/docs/TYPING_KEYBOARD_BASELINE.md +/docs/ios-pip-voice-session-plan.md +/docs/keyboard-accuracy-plan.md +/docs/keyboard-memory-budget.md +/docs/local-asr-architecture.md +/docs/mac-parity-audit-2026-08-23.md +/docs/personal-dictionary-icloud-kvs-checklist.md +/docs/screenshots/README.md diff --git a/OSGKeyboard/Assets.xcassets/anthropic.imageset/Contents.json b/OSGKeyboard/Assets.xcassets/anthropic.imageset/Contents.json deleted file mode 100644 index 9f10dd1..0000000 --- a/OSGKeyboard/Assets.xcassets/anthropic.imageset/Contents.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "images" : [ - { - "filename" : "anthropic.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "template" - } -} diff --git a/OSGKeyboard/Assets.xcassets/anthropic.imageset/anthropic.svg b/OSGKeyboard/Assets.xcassets/anthropic.imageset/anthropic.svg deleted file mode 100644 index 5b81844..0000000 --- a/OSGKeyboard/Assets.xcassets/anthropic.imageset/anthropic.svg +++ /dev/null @@ -1 +0,0 @@ -Anthropic \ No newline at end of file diff --git a/OSGKeyboard/Assets.xcassets/apple.imageset/Contents.json b/OSGKeyboard/Assets.xcassets/apple.imageset/Contents.json deleted file mode 100644 index bc991b6..0000000 --- a/OSGKeyboard/Assets.xcassets/apple.imageset/Contents.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "images" : [ - { - "filename" : "apple.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "template" - } -} diff --git a/OSGKeyboard/Assets.xcassets/apple.imageset/apple.svg b/OSGKeyboard/Assets.xcassets/apple.imageset/apple.svg deleted file mode 100644 index e41c537..0000000 --- a/OSGKeyboard/Assets.xcassets/apple.imageset/apple.svg +++ /dev/null @@ -1 +0,0 @@ -Apple \ No newline at end of file diff --git a/OSGKeyboard/Assets.xcassets/ark.imageset/Contents.json b/OSGKeyboard/Assets.xcassets/ark.imageset/Contents.json deleted file mode 100644 index 6f122ce..0000000 --- a/OSGKeyboard/Assets.xcassets/ark.imageset/Contents.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "images" : [ - { - "filename" : "ark.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "template" - } -} diff --git a/OSGKeyboard/Assets.xcassets/ark.imageset/ark.svg b/OSGKeyboard/Assets.xcassets/ark.imageset/ark.svg deleted file mode 100644 index 28556c3..0000000 --- a/OSGKeyboard/Assets.xcassets/ark.imageset/ark.svg +++ /dev/null @@ -1 +0,0 @@ -Volcengine \ No newline at end of file diff --git a/OSGKeyboard/Assets.xcassets/codingplanx.imageset/Contents.json b/OSGKeyboard/Assets.xcassets/codingplanx.imageset/Contents.json deleted file mode 100644 index 5b51e01..0000000 --- a/OSGKeyboard/Assets.xcassets/codingplanx.imageset/Contents.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "images" : [ - { - "filename" : "codingplanx.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "template" - } -} diff --git a/OSGKeyboard/Assets.xcassets/codingplanx.imageset/codingplanx.svg b/OSGKeyboard/Assets.xcassets/codingplanx.imageset/codingplanx.svg deleted file mode 100644 index d80b55c..0000000 --- a/OSGKeyboard/Assets.xcassets/codingplanx.imageset/codingplanx.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/OSGKeyboard/Assets.xcassets/cometapi.imageset/Contents.json b/OSGKeyboard/Assets.xcassets/cometapi.imageset/Contents.json deleted file mode 100644 index 39305f7..0000000 --- a/OSGKeyboard/Assets.xcassets/cometapi.imageset/Contents.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "images" : [ - { - "filename" : "cometapi.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "template" - } -} diff --git a/OSGKeyboard/Assets.xcassets/cometapi.imageset/cometapi.svg b/OSGKeyboard/Assets.xcassets/cometapi.imageset/cometapi.svg deleted file mode 100644 index efe9317..0000000 --- a/OSGKeyboard/Assets.xcassets/cometapi.imageset/cometapi.svg +++ /dev/null @@ -1 +0,0 @@ -CometAPI \ No newline at end of file diff --git a/OSGKeyboard/Assets.xcassets/custom.imageset/Contents.json b/OSGKeyboard/Assets.xcassets/custom.imageset/Contents.json deleted file mode 100644 index 998fdfc..0000000 --- a/OSGKeyboard/Assets.xcassets/custom.imageset/Contents.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "images" : [ - { - "filename" : "custom.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "template" - } -} diff --git a/OSGKeyboard/Assets.xcassets/custom.imageset/custom.svg b/OSGKeyboard/Assets.xcassets/custom.imageset/custom.svg deleted file mode 100644 index b9ba5cb..0000000 --- a/OSGKeyboard/Assets.xcassets/custom.imageset/custom.svg +++ /dev/null @@ -1 +0,0 @@ -Custom diff --git a/OSGKeyboard/Assets.xcassets/deepseek.imageset/Contents.json b/OSGKeyboard/Assets.xcassets/deepseek.imageset/Contents.json deleted file mode 100644 index 2076164..0000000 --- a/OSGKeyboard/Assets.xcassets/deepseek.imageset/Contents.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "images" : [ - { - "filename" : "deepseek.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "template" - } -} diff --git a/OSGKeyboard/Assets.xcassets/deepseek.imageset/deepseek.svg b/OSGKeyboard/Assets.xcassets/deepseek.imageset/deepseek.svg deleted file mode 100644 index dc224e4..0000000 --- a/OSGKeyboard/Assets.xcassets/deepseek.imageset/deepseek.svg +++ /dev/null @@ -1 +0,0 @@ -DeepSeek \ No newline at end of file diff --git a/OSGKeyboard/Assets.xcassets/gemini.imageset/Contents.json b/OSGKeyboard/Assets.xcassets/gemini.imageset/Contents.json deleted file mode 100644 index ca57c6b..0000000 --- a/OSGKeyboard/Assets.xcassets/gemini.imageset/Contents.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "images" : [ - { - "filename" : "gemini.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "template" - } -} diff --git a/OSGKeyboard/Assets.xcassets/gemini.imageset/gemini.svg b/OSGKeyboard/Assets.xcassets/gemini.imageset/gemini.svg deleted file mode 100644 index 87736bb..0000000 --- a/OSGKeyboard/Assets.xcassets/gemini.imageset/gemini.svg +++ /dev/null @@ -1 +0,0 @@ -Gemini \ No newline at end of file diff --git a/OSGKeyboard/Assets.xcassets/groq.imageset/Contents.json b/OSGKeyboard/Assets.xcassets/groq.imageset/Contents.json deleted file mode 100644 index 3874e19..0000000 --- a/OSGKeyboard/Assets.xcassets/groq.imageset/Contents.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "images" : [ - { - "filename" : "groq.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "template" - } -} diff --git a/OSGKeyboard/Assets.xcassets/groq.imageset/groq.svg b/OSGKeyboard/Assets.xcassets/groq.imageset/groq.svg deleted file mode 100644 index 7294646..0000000 --- a/OSGKeyboard/Assets.xcassets/groq.imageset/groq.svg +++ /dev/null @@ -1 +0,0 @@ -Groq \ No newline at end of file diff --git a/OSGKeyboard/Assets.xcassets/mimo.imageset/Contents.json b/OSGKeyboard/Assets.xcassets/mimo.imageset/Contents.json deleted file mode 100644 index 1f42ccf..0000000 --- a/OSGKeyboard/Assets.xcassets/mimo.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "mimo.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" - } -} diff --git a/OSGKeyboard/Assets.xcassets/mimo.imageset/mimo.png b/OSGKeyboard/Assets.xcassets/mimo.imageset/mimo.png deleted file mode 100644 index 499bb95..0000000 Binary files a/OSGKeyboard/Assets.xcassets/mimo.imageset/mimo.png and /dev/null differ diff --git a/OSGKeyboard/Assets.xcassets/minimax.imageset/Contents.json b/OSGKeyboard/Assets.xcassets/minimax.imageset/Contents.json deleted file mode 100644 index ac7e6a2..0000000 --- a/OSGKeyboard/Assets.xcassets/minimax.imageset/Contents.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "images" : [ - { - "filename" : "minimax.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "template" - } -} diff --git a/OSGKeyboard/Assets.xcassets/minimax.imageset/minimax.svg b/OSGKeyboard/Assets.xcassets/minimax.imageset/minimax.svg deleted file mode 100644 index 1d32449..0000000 --- a/OSGKeyboard/Assets.xcassets/minimax.imageset/minimax.svg +++ /dev/null @@ -1 +0,0 @@ -Minimax \ No newline at end of file diff --git a/OSGKeyboard/Assets.xcassets/mistral.imageset/Contents.json b/OSGKeyboard/Assets.xcassets/mistral.imageset/Contents.json deleted file mode 100644 index f28b753..0000000 --- a/OSGKeyboard/Assets.xcassets/mistral.imageset/Contents.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "images" : [ - { - "filename" : "mistral.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "template" - } -} diff --git a/OSGKeyboard/Assets.xcassets/mistral.imageset/mistral.svg b/OSGKeyboard/Assets.xcassets/mistral.imageset/mistral.svg deleted file mode 100644 index 32c6cbd..0000000 --- a/OSGKeyboard/Assets.xcassets/mistral.imageset/mistral.svg +++ /dev/null @@ -1 +0,0 @@ -Mistral \ No newline at end of file diff --git a/OSGKeyboard/Assets.xcassets/moonshot.imageset/Contents.json b/OSGKeyboard/Assets.xcassets/moonshot.imageset/Contents.json deleted file mode 100644 index 3dc959a..0000000 --- a/OSGKeyboard/Assets.xcassets/moonshot.imageset/Contents.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "images" : [ - { - "filename" : "moonshot.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "template" - } -} diff --git a/OSGKeyboard/Assets.xcassets/moonshot.imageset/moonshot.svg b/OSGKeyboard/Assets.xcassets/moonshot.imageset/moonshot.svg deleted file mode 100644 index fb56ac1..0000000 --- a/OSGKeyboard/Assets.xcassets/moonshot.imageset/moonshot.svg +++ /dev/null @@ -1 +0,0 @@ -MoonshotAI \ No newline at end of file diff --git a/OSGKeyboard/Assets.xcassets/openai.imageset/Contents.json b/OSGKeyboard/Assets.xcassets/openai.imageset/Contents.json deleted file mode 100644 index 7f508a4..0000000 --- a/OSGKeyboard/Assets.xcassets/openai.imageset/Contents.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "images" : [ - { - "filename" : "openai.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "template" - } -} diff --git a/OSGKeyboard/Assets.xcassets/openai.imageset/openai.svg b/OSGKeyboard/Assets.xcassets/openai.imageset/openai.svg deleted file mode 100644 index 78caf4f..0000000 --- a/OSGKeyboard/Assets.xcassets/openai.imageset/openai.svg +++ /dev/null @@ -1 +0,0 @@ -OpenAI \ No newline at end of file diff --git a/OSGKeyboard/Assets.xcassets/openrouter.imageset/Contents.json b/OSGKeyboard/Assets.xcassets/openrouter.imageset/Contents.json deleted file mode 100644 index 1ba973b..0000000 --- a/OSGKeyboard/Assets.xcassets/openrouter.imageset/Contents.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "images" : [ - { - "filename" : "openrouter.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "template" - } -} diff --git a/OSGKeyboard/Assets.xcassets/openrouter.imageset/openrouter.svg b/OSGKeyboard/Assets.xcassets/openrouter.imageset/openrouter.svg deleted file mode 100644 index 31fe130..0000000 --- a/OSGKeyboard/Assets.xcassets/openrouter.imageset/openrouter.svg +++ /dev/null @@ -1 +0,0 @@ -OpenRouter \ No newline at end of file diff --git a/OSGKeyboard/Assets.xcassets/qwen.imageset/Contents.json b/OSGKeyboard/Assets.xcassets/qwen.imageset/Contents.json deleted file mode 100644 index 4ea4e9b..0000000 --- a/OSGKeyboard/Assets.xcassets/qwen.imageset/Contents.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "images" : [ - { - "filename" : "qwen.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "template" - } -} diff --git a/OSGKeyboard/Assets.xcassets/qwen.imageset/qwen.svg b/OSGKeyboard/Assets.xcassets/qwen.imageset/qwen.svg deleted file mode 100644 index a4bb382..0000000 --- a/OSGKeyboard/Assets.xcassets/qwen.imageset/qwen.svg +++ /dev/null @@ -1 +0,0 @@ -Qwen \ No newline at end of file diff --git a/OSGKeyboard/Assets.xcassets/siliconflow.imageset/Contents.json b/OSGKeyboard/Assets.xcassets/siliconflow.imageset/Contents.json deleted file mode 100644 index 2386a84..0000000 --- a/OSGKeyboard/Assets.xcassets/siliconflow.imageset/Contents.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "images" : [ - { - "filename" : "siliconflow.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "template" - } -} diff --git a/OSGKeyboard/Assets.xcassets/siliconflow.imageset/siliconflow.svg b/OSGKeyboard/Assets.xcassets/siliconflow.imageset/siliconflow.svg deleted file mode 100644 index f06093b..0000000 --- a/OSGKeyboard/Assets.xcassets/siliconflow.imageset/siliconflow.svg +++ /dev/null @@ -1 +0,0 @@ -SiliconCloud \ No newline at end of file diff --git a/OSGKeyboard/Assets.xcassets/xai.imageset/Contents.json b/OSGKeyboard/Assets.xcassets/xai.imageset/Contents.json deleted file mode 100644 index a0cd740..0000000 --- a/OSGKeyboard/Assets.xcassets/xai.imageset/Contents.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "images" : [ - { - "filename" : "xai.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "template" - } -} diff --git a/OSGKeyboard/Assets.xcassets/xai.imageset/xai.svg b/OSGKeyboard/Assets.xcassets/xai.imageset/xai.svg deleted file mode 100644 index 536e713..0000000 --- a/OSGKeyboard/Assets.xcassets/xai.imageset/xai.svg +++ /dev/null @@ -1 +0,0 @@ -Grok \ No newline at end of file diff --git a/OSGKeyboard/Assets.xcassets/zhipu.imageset/Contents.json b/OSGKeyboard/Assets.xcassets/zhipu.imageset/Contents.json deleted file mode 100644 index bcf670c..0000000 --- a/OSGKeyboard/Assets.xcassets/zhipu.imageset/Contents.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "images" : [ - { - "filename" : "zhipu.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "template" - } -} diff --git a/OSGKeyboard/Assets.xcassets/zhipu.imageset/zhipu.svg b/OSGKeyboard/Assets.xcassets/zhipu.imageset/zhipu.svg deleted file mode 100644 index f7b1252..0000000 --- a/OSGKeyboard/Assets.xcassets/zhipu.imageset/zhipu.svg +++ /dev/null @@ -1 +0,0 @@ -Zhipu \ No newline at end of file diff --git a/OSGKeyboard/Views/MainTabView.swift b/OSGKeyboard/Views/MainTabView.swift index 824114a..8d040c5 100644 --- a/OSGKeyboard/Views/MainTabView.swift +++ b/OSGKeyboard/Views/MainTabView.swift @@ -8,7 +8,19 @@ struct MainTabView: View { @Environment(\.themePalette) private var palette: ThemePalette @Environment(\.horizontalSizeClass) private var horizontalSizeClass - @State private var tab: AppTab = .keyboard + @State private var tab: AppTab = { + #if DEBUG + if let raw = ProcessInfo.processInfo.arguments + .first(where: { $0.hasPrefix("-osgStartTab=") })? + .split(separator: "=").last, + let value = Int(raw), + let initial = AppTab(rawValue: value) + { + return initial + } + #endif + return .keyboard + }() private var usesSplitLayout: Bool { horizontalSizeClass == .regular diff --git a/OSGKeyboardShared/Models/ProviderLogo.swift b/OSGKeyboardShared/Models/ProviderLogo.swift deleted file mode 100644 index b763c09..0000000 --- a/OSGKeyboardShared/Models/ProviderLogo.swift +++ /dev/null @@ -1,36 +0,0 @@ -// ProviderLogo.swift -// OSGKeyboard · Shared -// -// Maps a provider id to its asset-catalog logo name. Shared by the iOS -// app and the macOS menu-bar app so both show identical brand marks. - -import Foundation - -public enum ProviderLogo { - /// Asset name for the provider's logo, or `nil` when there is no bundled logo. - public static func assetName(for providerId: String) -> String? { - switch providerId { - case "openai", "whisper": return "openai" - case "deepseek": return "deepseek" - case "qwen", "bailian", "alibabaCoding": return "qwen" - case "moonshot": return "moonshot" - case "zhipu": return "zhipu" - case "mimo": return "mimo" - case "ark", "volcengine": return "ark" - case "siliconflow": return "siliconflow" - case "groq": return "groq" - case "minimax": return "minimax" - case "openrouter": return "openrouter" - case "gemini": return "gemini" - case "anthropic": return "anthropic" - case "xai": return "xai" - case "mistral": return "mistral" - case "cometapi": return "cometapi" - case "codingPlanX": return "codingplanx" - case "codex_oauth": return "openai" - case "apple": return "apple" - case "custom": return "custom" - default: return nil - } - } -} diff --git a/docs/ACCOUNT_MANAGED_GATEWAY_TESTING.md b/docs/ACCOUNT_MANAGED_GATEWAY_TESTING.md deleted file mode 100644 index ebb60df..0000000 --- a/docs/ACCOUNT_MANAGED_GATEWAY_TESTING.md +++ /dev/null @@ -1,127 +0,0 @@ -# Account and managed gateway verification - -This checklist validates the optional OSG account path without changing the -existing local or BYOK defaults. Never record tokens, Apple identifiers, audio, -prompts, transcripts, or model output while running these checks. - -## Automated gate - -```bash -./Scripts/run-tests.sh validate -./Scripts/run-tests.sh pr -swiftlint lint --quiet --strict -xcodebuild \ - -project OSGKeyboard.xcodeproj \ - -scheme OSGKeyboardUITests \ - -destination 'platform=iOS Simulator,name=iPhone 17' \ - -only-testing:OSGKeyboardUITests/AccountCenterUITests \ - test -xcodebuild \ - -project OSGKeyboard.xcodeproj \ - -scheme OSGKeyboard \ - -destination 'generic/platform=iOS Simulator' \ - -configuration Release \ - -onlyUsePackageVersionsFromResolvedFile \ - CODE_SIGNING_ALLOWED=NO \ - build -``` - -The account and managed gateway tests must remain hermetic. They use injected -Apple integrity adapters, URL loading stubs, and WebSocket fakes rather than -production credentials. - -## Physical-device prerequisites - -- A development build signed for `com.osgkeyboard.ios`. -- Sign in with Apple, App Attest, DeviceCheck, Associated Domains, and both - Keychain groups enabled in the provisioning profile. -- `https://osglab.com/.well-known/apple-app-site-association` returns HTTP 200 - without a redirect and includes `X329MZU23S.com.osgkeyboard.ios` for `/i/*`. -- The production account service is ready at `https://account.osglab.com`. -- The production service temporarily enables `ALLOW_DEVELOPMENT_APP_ATTEST=true` - for the test window; disable it again after physical-device testing. -- OSGKeyboard is installed and enabled with Full Access for managed requests. -- The test account has enough non-production credits for the requested checks. - -## Identity and account - -1. Sign in with Apple and verify that nickname, balance, and referral state load - after a cold launch. -2. Confirm the raw nonce is never persisted and an App Attest assertion is - accepted. Repeat after an access-token expiry to exercise one refresh. -3. Open `https://osglab.com/i/{test-code}` while signed out. Sign in, then - verify the pending code is redeemed exactly once. -4. Force-quit and reopen the app. Verify session recovery without another Apple - prompt and confirm the keyboard extension cannot read the account session. -5. Sign out and verify account tokens plus shared gateway grants are removed. -6. Sign in again, choose Delete Account, complete Apple reauthentication, and - verify local and BYOK features still work afterward. - -For destructive verification, use a disposable Apple sandbox identity: - -1. Set a nickname, generate an invitation code, and select **Use Credits**. -2. Delete the account after both confirmations and fresh Apple authorization. -3. Confirm the app returns to signed-out/BYOK state and no account, grant, - profile, or purchase state remains visible. -4. Confirm old access and refresh tokens receive `401`; an Apple revoke outage - must not restore the locally deleted account. -5. Sign in again and confirm a new local App Attest key state is registered. - -Only pseudonymous immutable ledger, StoreKit audit, and time-limited anti-abuse -records remain where required for replay and abuse prevention. - -## Managed DeepSeek - -1. Select **Use Credits** for the first time. Verify the managed-cloud data - disclosure appears, Cancel leaves BYOK selected, and Agree enables credits. - Switch away and back again to confirm the disclosure is not repeated. -2. Verify the runtime uses managed Volcengine ASR and managed polishing together. -3. Run one polish request and one AI request. Verify actionable behavior for - insufficient balance, expired grant, timeout, and cancellation. -4. In the server ledger, verify one reservation and one settlement per request. - Retrying the same transport request must not create a second charge. - -## Managed Volcengine ASR - -1. Record approximately ten seconds of Mandarin PCM16LE at 16 kHz. -2. Verify partial and final results, then run translate-and-polish. -3. Cancel one recording mid-stream and verify the WebSocket closes without a - stuck reservation. -4. Verify session-open fallback, idle timeout, empty result, insufficient - balance, and concurrency-limit behavior. -5. Confirm the ledger settles successful sessions and releases failed or - cancelled reservations. - -Managed ASR currently does not send hotwords. Treat this as an explicit product -difference until the server request schema supports them. - -## Regression gate - -- Signed-out use remains valid. -- Local ASR never requires an account. -- Existing BYOK LLM and ASR credentials still use their direct providers. -- iCloud settings sync never contains account or gateway tokens. -- Flow, keyboard typing, and keyboard-extension memory-budget tests pass. - -## StoreKit credits - -The existing `ByRockyACoffee` product remains a voluntary consumable and never -grants credits. Configure `500tks` for 500 credits at USD 0.99, `1500tks` for -1,500 credits at USD 1.99 / CNY 18, and `3000tks` for 3,000 credits at -USD 2.99 / CNY 28. - -1. Use a Sandbox Apple account and sign in to the same OSG account before - purchasing. -2. Confirm the purchase supplies the OSG account UUID as `appAccountToken`. -3. Buy each product and verify the server grants exactly 500, 1,500, or 3,000 - credits and appends one `STOREKIT_PURCHASE` ledger entry before the app - finishes the transaction. -4. Submit the same signed transaction again and verify the response is marked - as replayed without changing the balance. -5. Interrupt the network after App Store success but before server - acknowledgement. Relaunch and verify the unfinished transaction reconciles - once. -6. Sign in to another OSG account and verify the first account's transaction is - rejected. -7. Confirm there is no Restore Purchases action for credit packs and that the - voluntary tip still changes only the local support count. diff --git a/docs/APPSTORE_METADATA.md b/docs/APPSTORE_METADATA.md deleted file mode 100644 index c0e7797..0000000 --- a/docs/APPSTORE_METADATA.md +++ /dev/null @@ -1,296 +0,0 @@ -# App Store Connect — OSGKeyboard 2.0.3 (build 94) - -> Current metadata baseline for the iOS/iPadOS App Store build. Version and build -> numbers come from `project.yml`. The repository also contains a separate -> macOS 15+ Developer ID target; it is not this App Store listing. - -## App information - -| Field | Value | Notes | -|---|---|---| -| App name | `OSGKeyboard` | ≤ 30 characters | -| Subtitle | `Voice input, everywhere` | ≤ 30 characters | -| Bundle ID | `com.osgkeyboard.ios` | iOS host target | -| Version / build | `2.0.3` / `94` | `MARKETING_VERSION` / `CURRENT_PROJECT_VERSION` | -| Minimum system | iOS/iPadOS 26 | iPhone and iPad | -| Primary locale | `en-US` | Simplified Chinese is also bundled | -| Primary category | Utilities | | -| Secondary category | Productivity | Optional | -| Age rating | 4+ | No objectionable content | - -## URLs - -| Field | Value | -|---|---| -| Support URL | `https://github.com/hkgood/OSGKeyboard/issues` | -| Marketing URL | `https://hkgood.github.io/OSGKeyboard/` | -| Privacy Policy URL | `https://hkgood.github.io/OSGKeyboard/privacy/` | -| EULA | Leave blank; use Apple's standard EULA | - -## Pricing and availability - -| Field | Value | -|---|---| -| Price | Free | -| In-App Purchases | Optional consumables: tip `ByRockyACoffee`; managed-credit packs `500tks`, `1500tks`, `3000tks` | -| Availability | All configured App Store territories | -| Pre-order | No | - -## Description (≤ 4000 characters) - -```text -OSGKeyboard is a voice and typing keyboard for iPhone and iPad. Speak in -any app and insert the transcript at the cursor, or switch to Chinese and -English typing without leaving the keyboard. - -VOICE INPUT - -• On-device by default. iOS 26 SpeechAnalyzer and DictationTranscriber - transcribe locally. -• Optional cloud recognition. Use your own provider credentials, or - sign in with Apple and choose managed credits. -• Optional AI polish and translation. Use your own provider API key or - managed credits; without either, recognized text can still be inserted. -• AI keyboard mode. Ask a spoken question, review the generated answer, - then explicitly insert or send it. -• Edit the last verified OSGKeyboard insertion by voice before replacing - or appending the result. - -TYPING - -• Chinese full pinyin, Microsoft double pinyin, and Sogou double pinyin, - with optional fuzzy-pinyin pairs. -• English autocomplete, autocorrect, and next-word prediction from - offline resources. -• Personal dictionary terms can participate in Chinese candidates, - English suggestions, ASR correction, and polish protection. -• iPhone and iPad layouts, including iPad globe and editing controls. -• Optional clipboard history is off by default and keeps up to 15 text - items from this device or Universal Clipboard in this device's App Group. - Turning it off keeps existing history; clearing is a separate confirmed action. - -PRIVACY - -• Limited first-party product analytics; no third-party analytics, - advertising, tracking SDKs, ATT, or IDFA. -• Product analytics never includes keyboard input, audio, transcripts, - prompts, model output, credentials, or personal identifiers. It can be - disabled in Settings, which deletes queued events. -• Local recognition does not upload audio. -• User-configured cloud requests go directly to that provider. Managed-credit - requests go through OSGKeyboard's managed gateway to the managed provider. -• Provider keys are stored in Keychain. -• Clipboard history stays device-local, does not iCloud-sync, and is not - sent to AI automatically. Text you insert may later be included when you - actively invoke polish with your configured provider. -• Core use requires no OSGKeyboard account. - -OSGKeyboard's own code is source available for audit and personal, -non-commercial local use. It is not MIT-licensed or open source; see the -repository LICENSE for redistribution and commercial-use restrictions. - -Requires iOS or iPadOS 26 or later. - -https://github.com/hkgood/OSGKeyboard -``` - -## Promotional text (≤ 170 characters) - -```text -Voice input anywhere, with on-device recognition by default. Use your own AI key or optional managed credits for cloud speech, polish, translation, and AI answers. -``` - -## Keywords (≤ 100 characters) - -```text -keyboard,voice,dictation,speech,transcribe,AI,pinyin,Chinese,English,polish,typing,productivity -``` - -## What's new in 2.0.3 - -```text -NEW -• Home can now suggest repeated rare English terms alongside Chinese terms - for explicit review before adding them to the Personal Dictionary. - -CHANGED -• Cards, settings typography, native selection menus, account rows, and - primary-to-detail navigation transitions now follow one visual system. - -FIXED -• Apple account restoration now retries transient secure-session failures and - keeps sign-in loading feedback consistent. -• English and Chinese candidates now recover reliably after switching apps - without carrying state from the previous input field. -``` - -## App Review information - -| Field | Value | -|---|---| -| Sign-in required | No | -| Demo account | Not applicable | -| Contact info | Maintainer's Apple Developer account details | - -### Notes to App Review - -```text -OSGKeyboard is a custom keyboard for iOS/iPadOS 26. - -1. Add the keyboard: - Settings → General → Keyboard → Keyboards → Add New Keyboard → - OSGKeyboard. -2. Enable Full Access. It is required for App Group communication between - the keyboard and host app and for optional provider network requests. -3. Complete onboarding in the OSGKeyboard host app. -4. In any editable field, switch to OSGKeyboard and tap the microphone. - The default local engine uses on-device Apple speech recognition. -5. Local dictation and user-configured providers require no OSGKeyboard - account. The Account tab offers optional Sign in with Apple. -6. After signing in, Settings → AI Service → Use Credits enables the managed - cloud path. The consumable products are `500tks`, `1500tks`, and `3000tks`. - Purchased credits are verified by the account service before StoreKit - transactions are finished. -7. AI polish and AI mode can use either managed credits or a user-owned - provider key. Without either, local dictation still inserts recognized text. -8. Optional tip `ByRockyACoffee` remains a consumable support purchase and - does not grant managed credits or unlock features. -9. Clipboard history is off by default. To test it, open Settings → - Clipboard, enable History, copy text on this device or through Universal - Clipboard, then return to the keyboard. Secure fields hide the clipboard - entry point. Turning History off preserves saved items; use the separate - confirmed clear action to delete them. -10. First-party Product Analytics is enabled by default under Settings → - About → Privacy. Turning it off deletes queued events. It does not collect - keyboard input, audio, transcripts, prompts, model output, or credentials. - -Privacy policy: -https://hkgood.github.io/OSGKeyboard/privacy/ - -Source and license: -https://github.com/hkgood/OSGKeyboard -``` - -## App Privacy answers - -Use conservative disclosures that cover optional cloud recognition, cloud -polish/translation, and AI mode even though local recognition is the default. - -### User Content → Audio Data - -- Collected: Yes -- Purpose: App Functionality -- Linked to the user: Yes -- Used for tracking: No - -Audio is sent off-device only when the user enables cloud recognition. The -configured provider may associate requests with the user's credential. In -managed-credit mode, audio is linked to the OSGKeyboard account for service -authorization and credit accounting. - -### User Content → Other User Content - -- Collected: Yes -- Purpose: App Functionality -- Linked to the user: Yes -- Used for tracking: No - -This covers transcripts and nearby cursor context used for polish/translation, -AI questions and skill prompts, optional provider search requests, dictionary -terms included in provider prompts, and clipboard text only after the user -actively invokes a clipboard skill, names the clipboard in AI mode, or inserts -it and requests polish. Skill results may also be handed on-device to an Apple -Shortcut, while navigation addresses may be opened in the selected map app. -Device-local clipboard history and typing-learning data by themselves are not -collected by the developer. - -### Contact Info → Name - -- Collected: Yes -- Purpose: App Functionality -- Linked to the user: Yes -- Used for tracking: No - -The display name supplied by Sign in with Apple is optional and is used only -for the optional OSGKeyboard account profile. - -### Purchases → Purchase History - -- Collected: Yes -- Purpose: App Functionality -- Linked to the user: Yes -- Used for tracking: No - -StoreKit transaction identifiers, product identifiers, and granted-credit -results are processed to verify consumable managed-credit purchases, prevent -replay, and maintain the account credit ledger. - -### Identifiers → User ID - -- Collected: Yes -- Purpose: App Functionality -- Linked to the user: Yes -- Used for tracking: No - -This covers the pseudonymous OSGKeyboard account identifier and scoped -managed-service grant identifiers. Core use remains available without an -OSGKeyboard account. - -### Identifiers → Device ID - -- Collected: Yes -- Purpose: Analytics -- Linked to the user: Yes -- Used for tracking: No - -This is an app-scoped random installation identifier. It rotates when analytics -is re-enabled, after account deletion, or when a different account signs in. It -is not IDFA and is not used across apps. - -### Usage Data → Product Interaction - -- Collected: Yes -- Purpose: Analytics -- Linked to the user: Yes -- Used for tracking: No - -This covers fixed event names for app and keyboard sessions, purchase-page -interactions, and invitation actions. It contains no free-form properties. - -### Usage Data → Other Usage Data - -- Collected: Yes -- Purpose: Analytics -- Linked to the user: Yes -- Used for tracking: No - -This covers fixed AI feature categories, execution modes, outcome categories, -and coarse duration buckets. It does not include prompts, transcripts, model -output, audio, or keyboard content. - -### Do not select - -- Advertising, marketing, product personalization, or tracking -- Email address, phone number, physical address, location, contacts, photos, - browsing history, or search history -- Usage data or diagnostics stored only locally or in the user's private iCloud - -## Encryption - -`Info.plist` declares `ITSAppUsesNonExemptEncryption = false`. Network calls use -standard HTTPS. Re-evaluate this answer if non-exempt cryptography is added. - -## Submission checklist - -- [ ] Confirm `project.yml` still reads version 2.0.3 / build 94 -- [ ] Open the existing Xcode project (do not regenerate unless needed) -- [ ] Run the release build and test suites on macOS with Xcode 26 -- [ ] Replace screenshots with captures from the submitted build -- [ ] Verify the privacy answers against the submitted provider features -- [ ] In App Store Connect, add Device ID, Product Interaction, and Other - Usage Data for Analytics; linked to the user, not used for tracking -- [ ] Confirm `500tks`, `1500tks`, and `3000tks` are approved, consumable, - and mapped to the server credit catalog -- [ ] Confirm `ByRockyACoffee` remains an optional consumable tip and unlocks - no feature -- [ ] Upload, select build 94, add review notes, and submit diff --git a/docs/STOREKIT_TESTING.md b/docs/STOREKIT_TESTING.md deleted file mode 100644 index b239f37..0000000 --- a/docs/STOREKIT_TESTING.md +++ /dev/null @@ -1,122 +0,0 @@ -# StoreKit — Voluntary Support Tip - -OSGKeyboard offers a single **Consumable** in-app purchase: - -| Field | Value | -|---|---| -| **Type** | Consumable | -| **Product ID** | `ByRockyACoffee` | -| **Reference name (Connect)** | 给老刘买杯咖啡 | -| **Display name (en)** | Buy me a cup of coffee | -| **Display name (zh-Hans)** | 给老刘买杯冰美式 | -| **Price (China)** | ¥28 (nearest available App Store tier) | -| **Price (US, suggested)** | $3.99 equivalent tier | - -**Important:** this tip does **not** unlock translation, dictionary limits, -long Flow sessions, or any other capability. The app remains fully free. - -Consumable purchases **cannot be restored** (Apple policy). Settings copy -explains this to users. - ---- - -## App Store Connect setup - -1. Open **App Store Connect → OSGKeyboard → In-App Purchases**. -2. Create **Consumable** with Product ID `ByRockyACoffee`. -3. Add localizations (en + zh-Hans) using the strings in - `OSGKeyboardShared/*/Shared.strings` (`tip.*` keys) and - `docs/APPSTORE_METADATA.md`. -4. Set pricing: **China ¥28** (App Store only offers fixed tiers — ¥28 is - the nearest to ¥30); pick equivalent tiers for other territories. -5. Submit the IAP for review **with** the app version that includes the - Settings → Support the Developer entry. - -### Review screenshot (required — fixes「元数据丢失」) - -App Store Connect → IAP **ByRockyACoffee** → **审核信息** → **截屏**: - -1. Run the app (Simulator or device) with Settings open at the top - **支持开发者** card showing the green **打赏 ¥28.00** button. -2. Capture that screen (⌘S in Simulator, or device screenshot). -3. Upload to **截屏 → 选取文件**. -4. Optional **审核备注**: - -``` -Optional voluntary tip only (Consumable IAP ByRockyACoffee). -All features free before and after purchase. Settings tab → top of page. -Consumable — cannot restore (stated in UI). -``` - -Save — status should become **准备提交**. - ---- - -## Local testing (StoreKit Test — no Connect / sandbox account) - -Uses [`OSGKeyboard.storekit`](../OSGKeyboard.storekit). The **OSGKeyboard** -scheme already references it in `project.yml` (`storeKitConfiguration`). - -1. `xcodegen generate && open OSGKeyboard.xcodeproj` -2. **Product → Scheme → Edit Scheme → Run → Options** - - Confirm **StoreKit Configuration** = `OSGKeyboard.storekit` -3. Run **OSGKeyboard** on Simulator (e.g. iPhone 17) or a plugged-in device -4. Open **Settings** tab → top card **支持开发者** -5. Tap **打赏 ¥28.00** → StoreKit Test purchase sheet appears -6. **Buy** → thank-you alert; **Cancel** → no error -7. Repeat buy once (Consumable allows multiple) - -**Debug menu (optional):** Xcode → **Debug → StoreKit → Manage Transactions** -to view / delete test purchases. - ---- - -## Sandbox testing (real App Store sandbox — after Connect IAP is 准备提交) - -1. App Store Connect → **用户和访问** → **沙盒** → create a **Sandbox Tester** -2. On device: **设置 → App Store → 沙盒账户** → sign in (not your real Apple ID) -3. Install via **TestFlight** or **Debug run without** `.storekit`: - - To hit Connect products: Edit Scheme → Run → Options → set StoreKit - Configuration to **None**, then run on device -4. Settings → **支持开发者** → purchase with sandbox account -5. Sandbox charges are free; receipt is real sandbox flow - ---- - -## Submit to App Review (after local + sandbox pass) - -Yes — **wait until testing looks good**, then: - -1. IAP **ByRockyACoffee** status = **准备提交** (pricing + screenshot + localizations) -2. Bump app version in `project.yml` / `CHANGELOG.md` if needed -3. **Archive** → upload build to App Store Connect -4. Open the new **App Store version** page → **App 内购买项目** → **+** → select **ByRockyACoffee** -5. Fill metadata, attach build, submit **version + IAP together** (first IAP rule) - -### Sandbox checklist - -- [ ] Product loads and shows localized price -- [ ] Successful purchase shows thank-you alert -- [ ] User cancel returns to idle (no error spam) -- [ ] Repeat purchase works (Consumable allows multiple) -- [ ] No Restore button for this product (Consumable) -- [ ] Translation, Flow, dictionary, BYOK unchanged after tipping - -### macOS note - -The menu-bar Mac build (`com.osgkeyboard.mac`) ships via Developer ID outside -the Mac App Store today. StoreKit products load only for App Store builds. -The Mac Settings UI is present for parity; tip IAP requires an App Store -distribution if Mac tipping is enabled later. - ---- - -## Files - -| File | Role | -|---|---| -| `OSGKeyboardShared/Services/Tip/TipProduct.swift` | Product ID constants | -| `OSGKeyboardShared/Services/Tip/TipPurchaseManager.swift` | StoreKit 2 purchase flow | -| `OSGKeyboardShared/DesignSystem/SupportDeveloperSection.swift` | iOS Settings UI | -| `OSGKeyboardMac/MacSupportDeveloperTipRows.swift` | macOS Settings UI | -| `OSGKeyboard.storekit` | Local StoreKit Test catalog | diff --git a/docs/TYPING_KEYBOARD_BASELINE.md b/docs/TYPING_KEYBOARD_BASELINE.md deleted file mode 100644 index dc232df..0000000 --- a/docs/TYPING_KEYBOARD_BASELINE.md +++ /dev/null @@ -1,55 +0,0 @@ -# Typing Keyboard Baseline (Phase 0) - -Frozen reference for NanoMouse × OSGKeyboard fusion. - -## NanoMouse reference - -| Field | Value | -|-------|--------| -| Repo | https://github.com/xjwhnxjwhn/nanomouse | -| Frozen commit | `a6177d898a01662ce551b43a01cf82a9f84ca54c` (2026-07-29 tip at clone) | -| Local clone (gitignored) | `.refs/nanomouse/` | - -## KeyboardKit / key shell - -NanoMouse does **not** SPM-pin upstream KeyboardKit Pro. It vendors an MIT KeyboardKit tree inside: - -`ios/Packages/HamsterKeyboardKit/Sources/KeyboardKit/` (see that folder’s `LICENSE`). - -OSG ships a **lean SwiftUI key shell** in-repo (`OSGKeyboardExt/Typing/`) -behind `TypingLayoutProviding`; KeyboardKit is not linked. - -**KeyboardKit Pro is not used.** - -## Chinese schema / lexicon - -| Choice | Detail | -|--------|--------| -| Product intent | Full pinyin + Microsoft/Sogou double pinyin + opt-in fuzzy pairs | -| Engine | librime 1.17.0 via static XCFramework (BSD-3-Clause) | -| Binary package | `ghostflyby/librime-xcframework` `1.17.0-pack.1`, checksum `0f0fc13b…1164` | -| Baseline | rime-pinyin-simp (Apache-2.0) | -| Modern words | Jieba frequencies + phrase-pinyin-data + pinyin-data (MIT) | -| Generated lexicon | `Resources/Typing/Rime/osg_pinyin.dict.yaml` (~365K entries) | -| User learning | librime userdb in App Group | -| Explicitly excluded | rime-ice / rime-double-pinyin (GPL), Luna / Essay (LGPL) | - -The dictionary is rebuilt deterministically by -`Scripts/typing/build_rime_dictionary.py`; `manifest.json` pins every source -commit and SHA-256. - -## Memory / height budget - -| Mode | Target height | Memory notes | -|------|---------------|--------------| -| Voice | 281 pt (`KeyboardRootView.totalHeight`) | Matches typing height; no typing engine loaded | -| Typing | 281 pt (`TypingRootView.totalHeight`) | Host-prebuilt Rime data; extension opens one session | -| RSS goal | Typing peak < 50 MB | Session closes on voice switch / memory warning | - -## Success criteria (Phase 1) - -- Top-right tab switches voice ↔ typing; recording/processing locks voice. -- English QWERTY + 123 / basic symbols. -- Full pinyin and Microsoft/Sogou double pinyin produce phrase candidates. -- Fuzzy pairs default off and are enabled individually in Settings. -- No GPL/LGPL input data in the app; NOTICE lists exact source licenses. diff --git a/docs/assets/badges/appstore-en.svg b/docs/assets/badges/appstore-en.svg deleted file mode 100644 index 072b425..0000000 --- a/docs/assets/badges/appstore-en.svg +++ /dev/null @@ -1,46 +0,0 @@ - - Download_on_the_App_Store_Badge_US-UK_RGB_blk_4SVG_092917 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/assets/badges/appstore-zh.svg b/docs/assets/badges/appstore-zh.svg deleted file mode 100644 index e3f29cd..0000000 --- a/docs/assets/badges/appstore-zh.svg +++ /dev/null @@ -1,29 +0,0 @@ - - Download_on_the_App_Store_Badge_CNSC_RGB_blk_092917 - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/ios-pip-voice-session-plan.md b/docs/ios-pip-voice-session-plan.md deleted file mode 100644 index 228612a..0000000 --- a/docs/ios-pip-voice-session-plan.md +++ /dev/null @@ -1,385 +0,0 @@ -# iOS PiP 语音会话保活规划 - -> **文档状态**:产品与架构规划(待验证,未进入实现) -> **适用范围**:iOS 主 App + 键盘扩展 + Live Activity -> **目标版本**:待产品验证后确定 -> **创建日期**:2026-07-26 - ---- - -## 1. Executive Summary - -### 1.1 目标 - -在不要求 OSGKeyboard 长时间占用麦克风的前提下,尽量保持主 App 可响应键盘扩展的听写指令,降低用户在宿主 App 与 OSGKeyboard 之间反复切换的频率。 - -核心方向是将两个当前耦合的能力拆开: - -1. **会话可用性**:主 App 仍可接收键盘命令。 -2. **麦克风采集**:仅在用户明确开始听写时启用,完成后立即释放。 - -PiP(画中画)只承担系统可见的多任务会话载体,不绕过麦克风授权,也不应使用静音音频循环伪造后台活动。 - -### 1.2 核心结论 - -| 决策 | 规划选择 | -|------|----------| -| 产品定位 | 将 PiP 作为可选的「免切换模式」,不替代普通 Flow | -| 麦克风策略 | PiP 空闲时关闭;键盘点按听写后按需激活 | -| 默认策略 | 保留当前隐私友好的 5 分钟 Flow;PiP 由用户主动开启 | -| 降级路径 | PiP 不可用或失效时回落到现有 `startflow` 冷启动流程 | -| 状态展示 | PiP 显示有意义的语音会话状态;Live Activity 继续负责锁屏与灵动岛 | -| 禁止方案 | 不播放静音文件保活,不使用定位或 VoIP 等无关后台模式 | -| 上线方式 | 先做真机技术验证和 TestFlight 审核验证,再决定正式产品化 | - -### 1.3 非目标 - -- 不让键盘扩展直接访问麦克风;这是 iOS 平台限制。 -- 不承诺 App 被用户强制退出后仍可免切换听写。 -- 不承诺电话、Siri、相机或其他录音 App 抢占音频设备时继续录音。 -- 不用 PiP 绕过麦克风权限、隐私提示或系统音频策略。 -- 第一阶段不重写 ASR、润色、App Group 或 Darwin 通知管线。 - ---- - -## 2. 问题定义 - -### 2.1 平台约束 - -iOS 键盘扩展无法直接申请或使用麦克风。系统级语音键盘因此必须采用: - -```text -键盘扩展 - → 发送开始/停止命令 - → 主 App 采集并转写 - → App Group 返回结果 - → 键盘插入文本 -``` - -当主 App 被系统挂起或终止时,键盘无法即时启动录音,只能打开主 App 重新建立会话。当前 Flow 通过持续运行 `AVAudioEngine` 输入链路换取后台可用性,但会带来麦克风长期占用、橙色隐私指示、电量消耗和音频冲突。 - -### 2.2 用户问题 - -| 用户感知 | 当前根因 | 目标变化 | -|----------|----------|----------| -| 频繁跳转主 App | 后台主进程不可响应 | PiP 有效时直接响应键盘命令 | -| 麦克风指示长时间亮起 | Flow 会话级连续采集 | 空闲时释放麦克风 | -| 耗电或发热 | 音频引擎持续采样和处理 | 仅听写期间采样 | -| 其他 App 无法使用麦克风 | OSGKeyboard 持有输入设备 | 听写结束后主动释放 | -| 不知道会话是否可用 | Flow、麦克风和进程状态混为一体 | 分开展示「免切换已就绪」和「正在录音」 | - -### 2.3 成功定义 - -PiP 模式下,用户应能: - -1. 在 OSGKeyboard 主 App 中主动开启免切换模式。 -2. 将 PiP 小窗收纳到屏幕边缘。 -3. 回到微信、邮件等宿主 App。 -4. 点击键盘麦克风后直接开始听写。 -5. 停止听写后收到文本,同时麦克风在短时间内释放。 -6. PiP 失效时收到明确提示,并能通过现有冷启动路径恢复。 - ---- - -## 3. 竞品与行业模式 - -### 3.1 Typeless - -Typeless iOS 1.9.0 将该能力命名为 Picture in picture / Skip app switching: - -- 用户先在主 App 中主动开启。 -- PiP 可拖到屏幕边缘收纳。 -- 用户在其他 App 的 Typeless 键盘中开始说话。 -- 官方产品说明强调麦克风空闲时关闭,以降低电量消耗。 - -其公开资料无法证明具体内部实现,因此本规划只借鉴产品模型,不假定其私有代码结构。 - -### 3.2 Wispr Flow、TypeWhisper 与同类开源项目 - -常见架构是主 App 持有 `AVAudioEngine`,键盘通过 App Group 与 Darwin 通知控制句子开始和停止。优点是首字延迟低,缺点是会话期间通常持续占用音频输入。 - -OSGKeyboard 当前 Flow 已属于此模式,并已具备: - -- 主 App 会话所有权; -- 键盘与主 App IPC; -- 连续采集与 utterance gate; -- App Group 结果回传; -- Live Activity; -- 冷启动与恢复流程。 - -因此 PiP 应作为会话生命周期的新载体,而不是重建整条语音管线。 - -### 3.3 SuperWhisper / App Intents 路线 - -更保守的方案是不做长期后台保活,使用 App Intents、Action Button、快捷指令或显式 App 切换启动录音。该方案最符合系统预期,但无法完全满足键盘内即时听写。 - -OSGKeyboard 应保留这类入口作为稳定降级,而不是依赖 PiP 达到 100% 可用。 - -### 3.4 合规边界 - -以下方式不应采用: - -- 循环播放静音音频以防止挂起; -- 声明与产品无关的定位、VoIP 后台能力; -- 使用不可见或无实际产品意义的伪视频,仅为延长进程生命; -- 在用户未明确开启会话时自动恢复麦克风。 - -PiP 内容需要能被解释为真实的语音会话控制面,例如展示: - -- 「免切换已就绪」; -- 「正在聆听」及音量反馈; -- 「正在转写」; -- 暂停、结束或返回 App 操作。 - ---- - -## 4. 目标产品模型 - -### 4.1 三层可用性 - -```text -层级 0:冷启动 - 主 App 不可用 - → 键盘打开 startflow - → 主 App 建立语音会话 - -层级 1:短时 Flow - AVAudioEngine 会话保持 - → 最低首字延迟 - → 默认 5 分钟无活动后结束 - -层级 2:PiP 免切换模式 - PiP 保持用户可见的多任务会话 - → 空闲时麦克风关闭 - → 键盘命令触发按需开麦 -``` - -三个层级必须共用同一份 `FlowSessionBridge` 状态合约,键盘不应根据实现细节分别写三套逻辑。 - -### 4.2 用户入口 - -建议在首页提供独立状态卡,而不是继续扩张设置开关: - -- 未开启:`开启免切换模式` -- 启动中:`正在准备画中画` -- 已就绪:`免切换已就绪 · 麦克风未使用` -- 录音中:`正在聆听` -- 失效:`会话已断开,点击恢复` - -首次开启时应明确说明: - -1. 屏幕上会出现可收纳的 PiP 小窗。 -2. 空闲时不会使用麦克风。 -3. 用户关闭 PiP、强制退出 App 或系统回收进程后,需要重新开启。 - -### 4.3 键盘状态 - -键盘麦克风状态应从「主 App 是否活着」升级为明确能力状态: - -| 状态 | 表现 | 点击结果 | -|------|------|----------| -| 不可用 | 灰色 | 引导权限或 Full Access | -| 需恢复 | 橙色 | 打开主 App 恢复会话 | -| PiP 就绪、麦克风关闭 | 绿色 | 请求主 App 按需开麦 | -| 正在激活麦克风 | 绿色加载态 | 等待真实音频 proof | -| 正在录音 | 红色/波形 | 发送停止命令 | -| 正在转写 | 处理中 | 等待结果 | - ---- - -## 5. 目标架构 - -### 5.1 组件边界 - -```text -Keyboard Extension - └─ FlowSessionBridge / Darwin command - ↓ -Host App - ├─ VoiceSessionCoordinator - │ ├─ FlowSessionManager - │ ├─ PiPVoiceSessionController - │ └─ AudioCaptureLifecycle - ├─ FlowContinuousCapture - ├─ ASR + Polish pipeline - └─ Live Activity -``` - -规划职责: - -- `PiPVoiceSessionController`:只管理 PiP 生命周期和展示状态。 -- `AudioCaptureLifecycle`:管理按需激活、音频 proof、停止及释放。 -- `FlowSessionManager`:继续负责命令、ASR、润色和结果回传。 -- `FlowSessionBridge`:发布跨进程能力快照,不让键盘猜测主 App 状态。 - -### 5.2 状态机 - -```text -inactive - → preparingPiP - → pipReadyMicOff - → activatingMic - → recording - → processing - → releasingMic - → pipReadyMicOff - -任意状态 - → interrupted - → recovering 或 inactive -``` - -重要不变量: - -1. `pipReadyMicOff` 必须确认音频输入已停止并释放。 -2. 键盘只有在收到 `recording` 和真实 audio proof 后才显示正在录音。 -3. PiP 存活不能等价于麦克风可用。 -4. 电话/Siri 中断后不得静默恢复录音。 -5. 任何超时都要回收麦克风并写入明确错误。 - -### 5.3 PiP 内容方案 - -技术验证阶段应比较两类 Apple 官方能力: - -1. 基于 `AVPlayerLayer` 的媒体 PiP; -2. 基于 `AVSampleBufferDisplayLayer` / 视频通话内容源的实时 PiP。 - -选择标准不是「哪种最容易保活」,而是: - -- 是否符合 OSGKeyboard 的真实产品用途; -- 能否展示动态语音会话状态; -- 麦克风激活/释放是否稳定; -- 收纳、锁屏、音频中断行为是否可预测; -- App Review 是否能清楚理解其用途。 - -在完成真机和审核验证前,不冻结具体 AVKit 实现。 - ---- - -## 6. 实施阶段 - -### Phase 0:技术与审核可行性验证 - -目标:证明「PiP 存活 + 闲时关麦 + 键盘触发按需开麦」在目标 iOS 版本可行。 - -验证项: - -- PiP 启动、收纳、恢复与关闭; -- 空闲 30 分钟后主 App 是否仍能响应; -- 空闲期间系统麦克风指示是否消失; -- 键盘命令到首个有效音频帧的延迟; -- 连续 20 次开始/停止是否稳定; -- 电话、Siri、蓝牙切换、锁屏、低电量模式; -- 用户关闭 PiP 后的降级行为; -- TestFlight / App Review 说明是否被接受。 - -退出标准: - -- 空闲时没有麦克风占用; -- P95 命令到有效音频帧小于 1 秒; -- 20 次连续听写无僵尸录音或失联状态; -- 失败后都能回到冷启动路径; -- 没有使用静音循环或无关后台能力。 - -### Phase 1:内部可用版本 - -- 新增 PiP 会话控制器; -- 将持续采集改造成可重复激活/释放; -- 扩展跨进程状态快照; -- 键盘增加激活中、PiP 就绪和失效状态; -- 复用现有 ASR、润色、结果回传和 Live Activity; -- 添加状态机与 IPC 单元测试。 - -### Phase 2:产品化 - -- 首页免切换状态卡; -- 首次开启说明与 PiP 收纳引导; -- 中英文文案与隐私说明; -- 诊断页增加 PiP、音频会话和最近中断原因; -- 增加遥测指标,但不采集音频内容。 - -### Phase 3:灰度与决策 - -- TestFlight 小流量开启; -- 比较 PiP 与普通 Flow 的成功率、首字延迟和耗电; -- 根据审核反馈决定默认入口和长期支持范围; -- 若 PiP 不稳定或审核风险不可接受,保留为实验功能或停止上线。 - ---- - -## 7. 测试矩阵 - -### 7.1 功能场景 - -| 场景 | 预期 | -|------|------| -| PiP 空闲 | 主 App 可响应,麦克风未占用 | -| 键盘开始听写 | 按需激活并获得真实音频帧 | -| 停止听写 | 完成转写并及时释放麦克风 | -| 连续多句 | 每句均重新激活成功,无第二句无音频 | -| PiP 被关闭 | 键盘切为需恢复,不显示假就绪 | -| App 被强退 | 清除旧 generation 和僵尸状态 | -| 电话/Siri 中断 | 当前句失败并提示,不自动偷录 | -| 蓝牙设备变化 | 音频格式重建,不崩溃 | -| 网络失败 | 本地 ASR 保留;润色按现有策略降级 | - -### 7.2 设备与系统 - -- 最低支持 iOS 版本、当前稳定版和最新 beta; -- 刘海机、灵动岛机型、iPad; -- AirPods、普通蓝牙耳机、车载音频、有线设备; -- 微信、信息、邮件、Slack、Notes 及自定义文本输入控件; -- 锁屏、横竖屏、多窗口、低电量和后台刷新关闭状态。 - ---- - -## 8. 指标与验收 - -### 8.1 核心指标 - -| 指标 | 定义 | 目标 | -|------|------|------| -| 免切换成功率 | PiP 就绪时无需打开主 App完成听写 | ≥ 98% | -| 麦克风空闲占用 | 非录音期间仍占麦的时长比例 | 接近 0 | -| 首帧延迟 P95 | 键盘点击到真实音频 proof | < 1 秒 | -| 结果回传成功率 | 停止后键盘收到最终结果 | ≥ 99% | -| 僵尸状态率 | 键盘显示可用但主 App无法响应 | < 0.5% | -| 恢复成功率 | 失效后通过冷启动恢复 | ≥ 99% | - -### 8.2 观察指标 - -- 每日 PiP 开启人数与启用留存; -- PiP 被用户主动关闭的比例; -- 每小时耗电和温升相对普通 Flow 的变化; -- 音频中断类型分布; -- 用户因橙色麦克风指示或隐私产生的反馈; -- App Review 反馈和政策变化。 - ---- - -## 9. 风险与应对 - -| 风险 | 等级 | 应对 | -|------|------|------| -| PiP 被认定与媒体用途不匹配 | 高 | 提供真实会话 UI、明确审核说明;先 TestFlight/审核验证 | -| 系统版本改变 PiP 行为 | 高 | 保持冷启动降级;按系统版本做兼容验证 | -| 按需开麦首字丢失 | 中 | 激活态 + audio proof + 预录缓冲,不提前向键盘宣告录音 | -| 频繁激活导致音频路由异常 | 中 | 串行状态机、格式重建、媒体服务重置恢复 | -| PiP 与 Live Activity 状态冲突 | 中 | 单一 coordinator 发布状态,两个 UI 只消费 | -| 用户误解 PiP 仍在监听 | 中 | 空闲状态明确写「麦克风未使用」,隐私说明可验证 | -| 其他 App 抢占麦克风 | 中 | 显式中断提示,不承诺并发录音 | - ---- - -## 10. 产品决策门 - -进入代码实现前,需要确认: - -1. 是否接受 PiP 作为用户主动开启、系统可见且可收纳的产品形态; -2. 是否优先「空闲关麦」而接受约数百毫秒的重新激活延迟; -3. PiP 中展示哪些真实功能,确保它不是纯保活黑窗; -4. 是否将现有「跳过 App 切换」重命名并拆为普通 Flow / PiP 两种模式; -5. 最低支持系统和目标测试设备; -6. 技术验证失败或审核风险过高时,是否接受回退到短 Flow + App Intents。 - -在上述决策和 Phase 0 证据完成前,不建议直接进入正式实现。 diff --git a/docs/keyboard-accuracy-plan.md b/docs/keyboard-accuracy-plan.md deleted file mode 100644 index 38247d6..0000000 --- a/docs/keyboard-accuracy-plan.md +++ /dev/null @@ -1,350 +0,0 @@ -# 打字键盘输入准确率提升计划 - -> **文档状态**:产品与工程规划(**实现中**:Phase 1~4 已落地,待真机试打) -> **适用范围**:iOS 键盘扩展打字面(`OSGKeyboardExt` 打字键网格)+ 共享布局度量(`OSGKeyboardShared`) -> **关联基线**:`docs/TYPING_KEYBOARD_BASELINE.md` -> **实现分支**:`feat/keyboard-touch-accuracy` -> **创建日期**:2026-08-03 -> **目标版本**:待 Phase 0 基线试打后确定发布节奏 - ---- - -## 1. Executive Summary - -### 1.1 目标 - -在拼音打字主路径上,将点按准确率与按键契约提升到**接近系统键盘的日常体感**: - -- 漏触(按了没反应)接近 0 -- 邻键误触明显下降 -- 「不准」不再成为用户槽点 - -**成功定义(产品语言)**:同设备、同姿势对比系统键盘时,内部试打主观分接近;键缝无空响应;字母键支持预览与滑动改选。 - -### 1.2 核心结论(冻结) - -| 决策 | 选择 | -|------|------| -| 准确率策略 | 对齐系统**触控契约 + 分层纠错**,不复刻私有算法 | -| 字母 / 数字 / 符号 | 按下高亮 → 可滑动改选 → **松手确认** | -| 删除 | **按下即删** + 长按连删(与现状及系统一致) | -| 空格 / 回车 | 与字母一致:**松手确认** | -| 键缝 | 视觉保留间距;触控**不允许无响应**(最近键中心) | -| 滑出键平面再松手 | **取消本次输入**(不出字) | -| 视觉 | 键位外观不因热区/智能变形(热区隐形) | -| 智能边界 | 只微调;用户明确落点或滑动可覆盖 | -| 语言策略 | 中文拼音过程可偏心;英文先求稳 | -| 反馈最低标准 | Phase 1~2:**高亮 + 按键音**;气泡为加分项 | -| 实施顺序 | 无死区 → 按键契约 → 触点微调 → 拼音偏心 → 气泡等 | - -### 1.3 非目标(本期不做) - -- 绝对准确率 100% 追平系统键盘 -- QuickPath / 滑行输入 -- 依赖 `UIKeyboardLayoutStar` 等系统私有 hit API -- 完整双 strike zone 个性化、键盘整体漂移重定位(re-spotting) -- 英文词级 predictive hit 放大(后置评估) -- 重写 Rime / 抛光引擎(触控层与词级兜底分工,不互相替代) - -### 1.4 现实预期 - -| 目标 | 是否现实 | -|------|----------| -| 漏触接近系统 | ✅ Phase 1~2 可达 | -| 邻键误触主观接近系统 | ✅ Phase 2~4 可达 | -| 中文拼音连打体感不输系统 | ✅ 相对最有机会(拼音偏心) | -| 全场景绝对准确率 = 系统 | ❌ 不作为本期 KPI | - ---- - -## 2. 背景与现状差距 - -### 2.1 问题定义(第一性原理) - -用户打字时只关心两件事: - -1. **我以为我点了谁**(视觉反馈) -2. **机器最后认了谁**(实际输入) - -系统键盘准,是因为整条链路在缩小二者差距,并在不一致时提供改选与词级兜底。OSG 当前布局已对齐系统键位,但触控模型仍接近「普通按钮网格」。 - -### 2.2 系统键盘能力分层(参考模型) - -系统做法是多层流水线(专利 + 私有头文件命名 + 可观察行为),不是单一「热区 +N pt」: - -| 层 | 含义 | 本期态度 | -|----|------|----------| -| ① 触点估计 | 触点中心 ≠ 意图点,可做偏移/加权 | Phase 3 轻量做 | -| ② 几何热区 | 视觉 ≠ hit;缝可命中 | Phase 1 必做 | -| ③ 动态偏心 | 下一键更可能则更好点 | Phase 4 拼音合法优先 | -| ④ 触摸生命周期 | Down 反馈 / Move 改选 / Up 提交 | Phase 2 必做 | -| ⑤ 个性化双区 | 按节奏/习惯自适应 | 后置 | -| ⑥ 词级兜底 | 自动更正 / 候选 | 已有 Rime + 抛光,需保住 | - -### 2.3 现状(代码事实) - -| 区域 | 现状 | 影响 | -|------|------|------| -| 字符键 | SwiftUI `Button`,视觉 frame ≈ hit | 键间 6~8pt 为死区;松手且需仍在键内 | -| 删除 / 部分工具栏 | `DragGesture(minimumDistance: 0)` 按下即响 | 已接近系统删除契约 | -| 主动扩大 hit | 基本仅候选栏展开按钮(34 视觉 / 44 hit) | 字母区无 fat-finger 扩展 | -| 滑动改选 | 无行级/网格级跟踪 | 邻键误触难在松手前纠正 | -| 点击音 | 字符键未统一走 `KeyboardSoundFeedback` | 反馈弱于系统 | -| 词级 | Rime 候选 + 抛光 | 后段较强;前段触控不稳会浪费兜底 | - -主要相关位置: - -- `OSGKeyboardExt/Typing/TypingRootView.swift`(键网格、间距、`keyButton`) -- `OSGKeyboardExt/Views/ToolbarActionButtons.swift`(按下即响模板) -- `OSGKeyboardExt/Utilities/KeyboardSoundFeedback.swift` -- `OSGKeyboardShared/Typing/TypingLayoutProviding.swift`(键位布局) - -### 2.4 用户失败模式 - -| 体感 | 主因 | 主要靠哪一层治 | -|------|------|----------------| -| 点到缝没反应 | 间距死区 | Phase 1 | -| 经常打成邻键 | 无改选窗口 + 无触点/上下文加权 | Phase 2~4 | -| 滑出键外丢输入 | per-key Button 生命周期 | Phase 2 | - ---- - -## 3. 产品宪法(全程遵守) - -1. **字母 / 数字 / 符号**:按下高亮 → 可滑动改选 → **松手确认** -2. **删除**:**按下即删**,长按连删 -3. **键平面内不允许无响应**(缝必命中最近键中心) -4. **视觉键位不因智能变形**(热区与偏心均隐形) -5. **智能只微调**;用户滑动或明确落点可覆盖 -6. **中文拼音过程可以更聪明;英文先求稳** - -对内沟通口径(Phase 2): - -> 不是变慢,是把确认点从按下挪到松开,换来改选能力——和系统键盘一样。 - ---- - -## 4. 分阶段计划 - -### Phase 0 — 基线与验收设计 - -**目的**:能量化「准不准」,避免凭感觉争论。 - -| 事项 | 说明 | -|------|------| -| 记录现状问题 | 漏触 / 邻键错 / 滑出丢键的典型场景 | -| 定试打脚本 | 至少 3 类:缝点击、邻键偏触、常见拼音连打(如 `zhongguo`、`pingguo`) | -| 定对比方式 | 同设备、同姿势;系统键盘 vs OSG;主观 1~5 分 + 错字/漏触次数 | -| 定反馈最低标准 | Phase 1~2:高亮 + 按键音;气泡单列加分 | - -**工期**:0.5~1 天 -**产出**:试打清单 + 打分表(可放飞书/Notion;关键结论可回写本文「附录」) - -**出口**:脚本固定、打分表可用,再进入开发。 - ---- - -### Phase 1 — 无死区热区(P0) - -**产品一句话**:缝也算键,别点空。 - -**范围** - -- 字母 / 数字 / 符号区:视觉间距保留,触控无空洞 -- 缝内命中:**最近键中心** -- 边缘键(如 Q/P、A/L)外侧略扩,降低边缘难点 -- 不改变键的视觉大小与间距观感 - -**工期**:可与 Phase 2 合并开发,但**验收分开** -**建议**:约短迭代内完成几何命中;若与 Phase 2 同 PR,须分 commit 或分验收项 - -**验收** - -- [ ] 故意点键缝 20 次,0 次无响应 -- [ ] 视觉观感与改前一致 -- [ ] 无明显「看在 A 上却出 S」的跳键 - ---- - -### Phase 2 — 系统同款按键契约(P0) - -**产品一句话**:先告诉你按了谁,松手再算数。 - -**范围** - -| 键类 | 行为 | -|------|------| -| 字母 / 数字 / 符号 | Down 高亮 → Move 改选 → Up 提交 | -| 删除 | Down 即删 + 长按连删(保持) | -| 空格 / 回车 | 松手确认 | -| 滑出键平面再松手 | 取消本次输入 | -| 反馈 | 高亮 + `KeyboardSoundFeedback.keyClick()` | - -**本阶段不做**:按键气泡、长按附加符号、滑行输入。 - -**工期**:约 1 个迭代(含内部试打调参) - -**验收** - -- [ ] 按下 F 滑到 G 松开 → 只出 G,高亮跟随 -- [ ] 按下后滑出键盘再松 → 不出字 -- [ ] 删除仍为按下即删、可连删 -- [ ] 内部试打:「手感接近系统」主观分显著高于改前 - -**工程注意(规划级)** - -- 宜收敛为网格/行级触摸跟踪,而非继续堆 per-key `Button` -- 可复用 `RepeatingPressButton` 与现有 UIKit 触控跟踪经验,但字母键生命周期以 Up 确认为准 -- 需单列与候选栏、Shift、123 切换的手势边界用例 - ---- - -### Phase 3 — 触点微调(P1) - -**产品一句话**:按手指意图认键,不只认触点中心。 - -**范围** - -- 对命中点做轻微、稳定的向上偏移(或按触点大小微调) -- 规则保持可预期;偏移量建议可配置便于试打 -- 中英文均可生效(与语言无关的几何修正) - -**工期**:0.5~1 天(可挂在 Phase 2 尾声) - -**验收** - -- [ ] 拇指自然打字时,偏下误触主观减少 -- [ ] 不引入新的「明显认错键」投诉 -- [ ] 偏移可调,便于内测收敛 - ---- - -### Phase 4 — 拼音下一键偏心(P1) - -**产品一句话**:拼到一半时,更可能的下一家更好点。 - -**范围(档位 1:合法字母优先)** - -- 仅拼音组词过程中启用 -- 依据当前拼音/Rime 状态:合法后续字母热区略增,非法略减 -- **偏心有上限**(微调,不霸道) -- 用户滑动改选 / 明确落点可覆盖 -- 英文模式:关闭或明显减弱 -- 上屏或开始新音节:偏心状态重置 - -**后置**:英文词预测式放大、重度概率模型、个性化学习。 - -**工期**:约 1 个迭代 -**依赖**:必须在 Phase 2 稳定之后(无改选窗口时,偏心误判会被感知为「自作主张」) - -**验收** - -- [ ] 常见拼音连打,邻键误触下降 -- [ ] 故意打冷门 / 非法后续字母,仍能打上 -- [ ] 视觉上看不出键变大;体感更顺 -- [ ] 可用试打脚本对比「偏心开 / 关」 - ---- - -### Phase 5 — 体验加分(P2,不阻塞主目标) - -| 项 | 说明 | 优先级 | -|----|------|--------| -| 按键气泡预览 | 强化所见即所得 | 高(体验) | -| 候选 / 抛光联动检查 | 触控改善后词级兜底不回退 | 高(质量) | -| 长按附加符号 | 系统能力对齐,非准确率核心 | 中 | -| 个性化热区 | 长期数据,后置 | 低 | - ---- - -## 5. 推荐排期 - -```text -Week 0 Phase 0 基线试打脚本 + 打分 -Week 1 Phase 1+2 无死区 + 松手确认/滑动改选(可合并开发,分开验收) -Week 1 末 Phase 3 触点微调 -Week 2 内部试打 / 调参 / 边界修复(滑出取消、宽键、Shift、候选冲突) -Week 3 Phase 4 拼音合法下一键偏心 -Week 3~4 Phase 5 气泡等加分项(可选) -``` - -**底线**:Week 1 必须交出 Phase 1 + 2;没有这两项,不宣称「接近系统准确率」。 - ---- - -## 6. 范围边界 - -| 做 | 不做(本项目) | -|----|----------------| -| 点按准确率与系统同款契约 | QuickPath | -| 拼音过程热区偏心(合法优先) | 系统私有 hit API | -| 高亮 + 按键音 | 完整自动更正引擎重写 | -| 与现有 Rime / 候选协同 | 键盘整体自动平移 / 缩放 | -| 可配置调参(偏移、偏心强度) | 为准确率牺牲视觉键位一致性 | - ---- - -## 7. 风险与对策 - -| 风险 | 对策 | -|------|------| -| 「松手确认」被觉得变钝 | 对内对齐系统口径;用试打错字率对比说话 | -| 热区重叠导致跳键 | 最近中心 + 偏心上限;内测可关偏心 | -| 拼音偏心误伤冷门输入 | 合法优先 + 可覆盖;可选调试开关 | -| 与删除 / 空格 / 候选手势冲突 | Phase 2 单列边界用例;滑出取消 | -| 扩展内难自动化 UI 测 | Phase 0 人工脚本为主;命中几何/偏心纯逻辑补单测 | -| 改动面集中在打字面 | 语音面工具栏契约保持;避免一次改两套手感却无验收 | - ---- - -## 8. 角色与出口标准 - -| 角色 | 职责 | -|------|------| -| 产品 | 守宪法、主观验收、决定气泡是否进本期 | -| 设计 | 高亮态;(可选)气泡视觉 | -| 工程 | Phase 1~4 实现;关键常量可配置 | -| 内测 | 每 Phase 用同一试打脚本回归 | - -**每阶段出口**:试打清单通过 + 无 P0「无响应 / 乱出字」再进入下一阶段。 - ---- - -## 9. 与版本发布的关系 - -- 本能力属用户可感知的打字体验提升,合并 `main` 并准备发布时: - - 按 `AGENTS.md` 用 Conventional Commits 评估版本 bump(预期含 `feat` → **MINOR**) - - `CHANGELOG.md` 在 `[Unreleased]` 下补双语条目(实现时再写,不在本文提前编造版本号) -- 功能分支开发期间**不**提前 bump `project.yml` 版本号 - ---- - -## 10. 一句话执行顺序 - -> **先量基线 → 无死区 + 系统按键契约 → 触点微调 → 拼音偏心 → 气泡等加分。** - ---- - -## 附录 A. 试打脚本草案(Phase 0 细化) - -| 编号 | 场景 | 操作 | 记录 | -|------|------|------|------| -| T1 | 键缝 | 在同行相邻键缝点击 20 次 | 无响应次数 | -| T2 | 邻键偏触 | 瞄准某键但故意偏向邻键 20 次 | 错键次数;有/无滑动改选 | -| T3 | 滑动改选 | 按下 A 滑到 S 松开(Phase 2+) | 是否只出 S | -| T4 | 滑出取消 | 按下后滑出键盘松开(Phase 2+) | 是否不出字 | -| T5 | 删除契约 | 点按 / 长按删除 | 是否按下即删、可连删 | -| T6 | 拼音连打 | `zhongguo` / `pingguo` 等各 5 遍 | 错字次数、主观 1~5 | -| T7 | 偏心开关 | Phase 4 后 T6 对比开/关 | 错字差、误伤冷门键与否 | - -对比时尽量:同一 iPhone、竖屏、单手拇指、系统键盘与 OSG 各跑一轮。 - ---- - -## 附录 B. 修订记录 - -| 日期 | 说明 | -|------|------| -| 2026-08-03 | 初稿:基于系统键盘分层分析与产品讨论冻结 Phase 0~5 | -| 2026-08-05 | 开分支 `feat/keyboard-touch-accuracy`;落地 Phase 1(无死区)+ Phase 2(网格 Down/Move/Up)+ Phase 3(触点上偏);纯逻辑见 `KeyHitTesting` / `TypingKeyLayout`,触控见 `TypingKeyTouchPad` | -| 2026-08-05 | Phase 4:全拼合法下一键偏心(`PinyinNextKeyResolver` + `rawInput`);双拼/英文保持中性;歧义最近键加权,单键明确命中不受偏置 | diff --git a/docs/keyboard-memory-budget.md b/docs/keyboard-memory-budget.md deleted file mode 100644 index 3207e73..0000000 --- a/docs/keyboard-memory-budget.md +++ /dev/null @@ -1,81 +0,0 @@ -# Keyboard memory budget — acceptance (OSGDiag) - -Phase 0 / 2 memory work is validated on a **physical iPhone** with Console -filtering for `OSGDiag`. Unit tests and `xcodebuild` cover compile-time -wiring; jetsam behavior is device-only. - -## Hybrid Flow (product default) - -- Foreground host is **light by default**: orphan Live Activity cleanup only — - **no** auto `startSession` / continuous capture on appear. -- Capture starts only on explicit Start / `osgkeyboard://startflow` / mic press. -- Idle background capture is stopped so a parked host does not jetsam the keyboard. -- **Do not** stack CLM + Rime + ASR in the same second after onboarding. -- ASR warmup runs on **first mic press** (`beginUtterance`), gated by - `HostMemoryBudget` (~260 MB RSS). `hostHeavy` is set only while heavy work - actually runs, then cleared. A sticky `hostHeavy` (host died mid-work) - expires after `hostHeavyMaxAge` (~120 s) and is cleared on - `clearFlowState` / host-launch reconciliation so typing 中文/EN is not - permanently blocked. - -## Console checklist - -Filter Console by process separately: host `OSGKeyboard` vs extension -`com.osgkeyboard.ios.keyboard`. Host-only filter will never show `KVC.*`. - -| Scenario | Expect | -|----------|--------| -| **Force-quit host**, open Notes, switch to OSG | First `dyld.constructor`, then `KVC.init` → `viewDidLoad` → `viewDidAppear` | -| Host foreground right after launch | `skip capture` + `postOnboardingWarmup scheduled … delay=45s` (no immediate Rime/CLM) | -| ~45 s later, host still active | Serial `rime.installIfNeeded` then `clm.prepare` | -| First mic press | `scheduleASRWarmup`; `hostHeavy` only while work runs | -| Switch to typing | Single `rime.prepare` / `englishPrepare` | - -If neither `dyld.constructor` nor `KVC.init` appears after force-quitting the host, -the extension is dying in dyld (Shared≈9.4 MB + librime) — next lever is splitting -Rime out of Shared. - -## RSS comparison (optional) - -Record `OSGDiag` `rss=` tags for: - -1. Cold voice surface only -2. Cold typing after prepare -3. Host foreground + extension - -Target: typing peak below the old “voice + eager Librime construct” baseline. - -## Extension physical-footprint budget - -`phys_footprint` is the primary extension metric because device jetsam follows -it more closely than RSS. `KeyboardExtensionMemoryTelemetry` records structured -`[OSGDiag/memory] extMemory` lines at lifecycle and heavy-resource milestones, -plus 50 ms samples during the first four seconds: - -- **Normal:** below 36 MiB -- **Warning:** 36–40 MiB -- **High:** 40–48 MiB; 40 MiB is the internal safe peak -- **Critical:** 48 MiB or above - -The approximate 60 MiB device boundary is not a public Apple contract. The -40 MiB target deliberately reserves room for transient SwiftUI, Rime, and -system-framework pages. Telemetry is observation-only: crossing a band logs -`crossed=1` but does not change the selected surface or unload resources. - -Each record includes the current and peak footprint, delta from process start, -elapsed startup time, surface/language, Full Access, clipboard state, and -operation-specific context. Filter Console by `OSGDiag/memory` and compare: - -1. `KVC.viewDidLoad.afterInstallServices` -2. `KVC.viewDidLoad.afterInstallSwiftUI` -3. `typing.englishPrepare.done` -4. `typing.rimePrepare.done` -5. `clipboard.reload.done` -6. `KVC.viewDidAppear.done` - -## Structural split - -- Extension links **OSGKeyboardShared** only (no Charts / StoreKit / Speech / HostSupport). -- Host embeds **OSGKeyboardHostSupport** (ASR, CLM, CloudASR, tip/charts UI). -- Heavy assets (`osg_pinyin.dict.yaml`, CLM bin, licenses, local-asr catalog) - ship in the **host app** bundle; extension reads Rime from App Group after deploy. diff --git a/docs/local-asr-architecture.md b/docs/local-asr-architecture.md deleted file mode 100644 index 1175ce9..0000000 --- a/docs/local-asr-architecture.md +++ /dev/null @@ -1,259 +0,0 @@ -# OSGKeyboard 本地 ASR 技术架构 - -> **文档状态**:1.7.0 代码事实 + 后续评测方向 -> **适用范围**:macOS 15+ 本地听写;iOS 26+ 使用 Apple SpeechAnalyzer。 -> **当前结论**:Mac 默认安装目录模型为 -> `qwen3-mlx-0.6b-4bit`,通过 `mlx-audio-swift` 做 Qwen3 MLX 真流式识别; -> 模型不可用时走 Apple Speech fallback。Sherpa 已不在当前 catalog 或运行路径中。 - -## 1. 架构摘要 - -本地 ASR 的专有名词质量由三层共同完成: - -1. **ASR bias**:`PersonalDictionary` 与内置技术词经 - `LocalASRBiasAdapter` 生成 Qwen3 `promptBias`。 -2. **确定性纠错**:识别后按个人词库 `aliases → term` 做边界受控替换。 -3. **Polish 保真**:把内置词参考作为补充上下文交给润色层;无 API Key 或润色失败时 - 仍返回本地识别与纠错结果。 - -这三层已经接入 Mac 的 live 与 batch 路径。“Mac 本地路径不消费词库”不再是当前事实。 - -## 2. 当前端到端数据流 - -```mermaid -flowchart LR - Audio["MacAudioRecorder · 16 kHz samples"] --> Pipeline["MacDictationPipeline"] - Dict["PersonalDictionary"] --> Bias["LocalASRBiasAdapter"] - Lexicon["phrases.tsv · BuiltinLexiconIndex"] --> Bias - App["Front app + locale"] --> Bias - Bias --> Prompt["promptBias"] - Bias --> Pairs["correctionPairs"] - Bias --> PolishTerms["polishFragment"] - - Pipeline -->|local + installed MLX| Live["MacMLXLiveCapture"] - Pipeline -->|batch/recovery| Local["MacLocalASRService"] - Live --> Qwen["MacMLXStreamingASRProvider"] - Local --> Qwen - Local -->|model unavailable / Apple selected| Apple["MacSpeechLocalASR"] - Prompt --> Qwen - Qwen --> Raw["Raw transcript"] - Apple --> Raw - Raw --> Correct["LocalASRTranscriptCorrector"] - Pairs --> Correct - Correct --> Polish["PolishingService"] - PolishTerms --> Polish - Polish --> Insert["MacTextInsertionService"] -``` - -### 2.1 引擎选择与回退 - -| 项目 | 当前事实 | -|---|---| -| 默认模型 ID | `qwen3-mlx-0.6b-4bit` | -| 可选 MLX 模型 | 0.6B 4-bit、1.7B 4-bit | -| Catalog | `OSGKeyboard/Resources/LocalASR/local-asr-catalog.json` | -| 下载源 | `hf-mirror.com` 与 Hugging Face repository files | -| 推理 | `MLXAudioSTT.Qwen3ASRModel` | -| Live partial | 100 ms 音频 feed;流式 session 定期 decode | -| Batch | 同一 MLX 模型的 `generate(audio:context:language:)` | -| Fallback | Apple Speech 的本地 `SFSpeechURLRecognitionRequest` | - -旧 Sherpa model ID 只在偏好迁移逻辑中映射到当前 MLX 默认值,不代表 Sherpa -backend 仍可运行。当前 catalog 的 `runtimes` 为空。 - -### 2.2 词库接线 - -`MacDictationPipeline.resolveLocalBias` 与 `MacMLXLiveCapture.resolveBias` 都读取 -`store.personalDictionary`,再调用: - -```text -LocalASRBiasAdapter.adapt( - dictionary + locale + frontAppBundleId + backend capabilities -) -``` - -适配器当前输出: - -| 输出 | 当前消费者 | -|---|---| -| `promptBias` | Qwen3 MLX streaming `StreamingConfig.context` 与 batch `generate(context:)` | -| `correctionPairs` | `LocalASRTranscriptCorrector`,在 polish 前修正 aliases | -| `polishFragment` | `PolishingService` 的 `dictionarySupplement` | -| `diagnostics` | `LocalASRBiasDiagnosticsStore` | -| `hardHotwords` | 为具备 hard-hotword capability 的 backend 保留;当前 Qwen3 MLX 不使用 | - -个人词优先;`BuiltinLexiconIndex` 从 `phrases.tsv` 选择 `weight >= 4` 的 Top-N, -代码编辑器/终端前台场景优先 `computer_terms`。默认最多考虑 300 个内置 ASR 词, -Qwen3 soft prompt 最长 800 字符,润色补充最多 40 个内置词。 - -### 2.3 Apple Speech fallback - -Apple fallback 强制 `requiresOnDeviceRecognition = true`,系统缺少对应语言模型时会 -明确失败,不会静默切云。中文路径会准备打包的 Apple Custom Language Model; -个人词库仍参与识别后的 alias 纠错与 polish 保真。 - -当前 `LocalASRCapabilities.appleSpeech` 声明 `hotwordMode = none`,因此不要把 -`contextualStrings` 描述为已经由个人词库动态注入。相关 API 虽有适配入口,但当前 -capability 不生成 hard hotwords。 - -## 3. 代码索引 - -| 主题 | 当前路径 | -|---|---| -| Mac 听写编排 | `OSGKeyboardMac/MacDictationPipeline.swift` | -| 本地引擎选择 / fallback | `OSGKeyboardMac/MacLocalASRService.swift` | -| MLX live capture | `OSGKeyboardMac/MacMLXLiveCapture.swift` | -| MLX provider | `OSGKeyboardMac/MacMLXStreamingASRProvider.swift` | -| MLX streaming session | `OSGKeyboardMac/MacMLXStreamingSession.swift` | -| Apple Speech fallback | `OSGKeyboardMac/MacSpeechLocalASR.swift` | -| Bias payload / capability | `OSGKeyboardShared/Models/LocalASRBiasPayload.swift`, `LocalASRCapabilities.swift` | -| Bias 构建 | `OSGKeyboardShared/Services/LocalASRBiasAdapter.swift` | -| 内置词索引 | `OSGKeyboardShared/Services/BuiltinLexiconIndex.swift` | -| 用户词库 | `OSGKeyboardShared/Models/PersonalDictionary.swift` | -| 云 ASR bias | `OSGKeyboardShared/Models/PersonalDictionary+ASRBias.swift` | -| Apple CLM | `OSGKeyboardHostSupport/Services/CustomLanguageModelManager.swift` | -| 内置 TSV | `OSGKeyboard/Resources/CustomLanguageModel/v1/phrases.tsv` | - -## 4. 设计目标与边界 - -### 4.1 目标 - -- Mac 本地听写离线可用,音频默认不离开设备。 -- iOS 与 Mac 复用 `PersonalDictionary` 和 `phrases.tsv` 的源数据。 -- 每个 backend 如实声明 soft prompt、hard hotword、streaming 与 reload 成本。 -- 模型下载、校验、安装和选择显式可管理。 -- 质量、延迟、内存与误触发均可量化评测。 -- 本地失败不静默回退云端。 - -### 4.2 非目标 - -- 不把一万词全量塞进 ASR prompt。 -- 不把 polish 当作唯一专名纠错层。 -- 不把尚未进入 catalog/runtime 的 Sherpa 或 SenseVoice 描述成当前能力。 -- 不承诺未经同一测试集验证的模型质量。 -- 不把 iOS 生成的 CLM `.bin` 直接喂给 MLX 或其他 backend。 - -## 5. 能力模型 - -`LocalASRCapabilities` 区分: - -| 字段 | 含义 | -|---|---| -| `hotwordMode` | `none` / `promptOnly` / `perRequest` / `recognizerScoped` / `cloudVocabulary` | -| `maxPromptCharacters` | soft prompt 上限 | -| `maxHotwordCount` | hard hotword 上限 | -| `supportsStreaming` | 是否提供真流式 partial | -| `hotwordReloadCost` | `none` / `recognizerReload` / `modelReload` | - -当前实际运行矩阵: - -| Backend | 当前角色 | Bias | Streaming | -|---|---|---|---| -| Qwen3 MLX | Mac 默认 | `promptOnly`,800 字符 | 是 | -| Apple Speech | Mac fallback / 显式选择 | ASR 层 `none`;后处理与 polish 仍接词库 | 否 | -| iOS SpeechAnalyzer | iOS 主路径 | Apple CLM + 本地纠错 | 渐进结果 | -| Cloud ASR | 用户显式选择 | 按 provider 使用个人词库 | 按 provider | - -## 6. 竞品研究中仍有效的结论 - -基于 2026-03 的公开源码快照,保留以下架构结论,不把它们当作 OSG 当前实现: - -| 项目 | 观察 | 对 OSG 的启示 | -|---|---|---| -| OpenLess | 多 provider;本地 Qwen3 词典接线有限 | README 不能把统一词库接口等同于 backend 已消费 | -| Typeflux | 词库限额、动态排序、项目词学习 | Top-N 与上下文排序有价值;自动学习必须可确认 | -| SayIt | Sherpa Qwen3 recognizer-scoped hotwords | 若未来重做 Sherpa POC,词库变化需计入 recognizer 重建成本 | -| VoiceSnap | SenseVoice 离线与静音处理 | 可作为速度/资源基线,不代表具备个性词库 | -| OpenBroca | 显式 model selection 与 manifest | 禁止扫描目录后任取第一个模型 | - -这些研究只支撑未来实验。当前发布架构仍是 Qwen3 MLX + Apple Speech fallback。 - -## 7. 模型管理 - -当前安装由 catalog 描述 repository files、模型布局与体积,安装状态由 manifest -和必要文件校验决定。设计原则保持: - -- Catalog 与 runtime 分离。 -- `selectedModelId` 显式保存;旧 ID 有确定迁移规则。 -- 下载进入 staging,经校验后原子发布。 -- 无效或未安装模型不能假装可用。 -- 中国大陆镜像与官方 Hugging Face 可按偏好选择。 - -ModelScope、自定义企业镜像与额外 backend 可以作为后续 catalog 扩展,但不是 -1.7.0 当前下载路径。 - -## 8. 评测方法 - -### 8.1 测试集 - -| 类别 | 内容 | 目的 | -|---|---|---| -| A 普通中文 | 日常口语 50 句 | CER / 误触发基线 | -| B 技术术语 | SwiftUI、Cursor、Qwen3-ASR 等 50 句 | 专名召回 | -| C 用户词典 | 20 个个人词,每词多句 | bias 与 alias 纠错 | -| D 长句 | 30 秒以上口语 | streaming 稳定性与 finalize | -| E 噪声 / 短句 | 低 SNR、少于 2 秒 | 幻觉与热词污染 | -| F 中英混合 | 技术会议和代码口述 | 语言提示与专名保真 | - -### 8.2 当前对照矩阵 - -| 配置 | 说明 | -|---|---| -| Baseline | Qwen3 MLX 0.6B,不传 bias | -| B1 | 0.6B + `promptBias` | -| B2 | B1 + alias correction + polish supplement | -| Quality | Qwen3 MLX 1.7B + B2 | -| Fallback | Apple Speech + correction / polish | -| Reference | 用户所选 Cloud ASR + `PersonalDictionary` | - -未来 Sherpa/SenseVoice 只能作为新增 POC 行,不能替换当前 baseline 名称。 - -### 8.3 指标 - -- Raw CER/WER。 -- 用户词 raw/final hotword recall。 -- False hotword rate。 -- 录音结束到最终插入的延迟,以及 live partial 首次可见延迟。 -- 8 GB Apple-silicon Mac 上的峰值内存、CPU 与模型加载时间。 -- 无网络完成率、模型安装成功率、fallback 成功率。 - -若未来评估 hard-hotword backend,建议门槛: - -- 用户词召回相对当前 Qwen3 MLX + B2 提升至少 20%。 -- False hotword rate 不高于 2%。 -- 30 秒音频端到端延迟不超过当前基线 1.5 倍。 -- 分发体积、签名、公证与 8 GB 设备内存均可接受。 - -## 9. 回退与风险 - -| 条件 | 当前/要求行为 | -|---|---| -| MLX 模型未安装 | 使用 Apple Speech fallback | -| MLX live 失败 | 返回 batch recovery 信号 | -| Apple 本地语言模型缺失 | 明确报错并提示系统下载,不切云 | -| Polish 缺 Key、超时或失败 | 返回本地识别/纠错文本 | -| 用户禁用云 | 不静默切换云 ASR | -| 热词 dump / 近静音幻觉 | 丢弃可疑 live 结果并尝试 batch | - -主要后续风险: - -1. 800 字符 soft prompt 对低频专名的提升有限。 -2. 内置 Top-N 过多会污染短句或近静音输入。 -3. alias 替换必须保持整词/高置信边界。 -4. 1.7B 模型在低配 Mac 上的内存与首载延迟需持续量化。 -5. 新 backend 必须先证明收益,再承担下载、签名和维护成本。 - -## 10. 后续方向 - -1. 用固定测试集持续比较 0.6B / 1.7B、无 bias / 分层 bias。 -2. 将 diagnostics 与实际命中/截断数据用于调节 Top-N 和 prompt 上限。 -3. 验证 Apple Speech fallback 的个人词动态提示能力后,再决定是否修改 capability。 -4. 只有 hard-hotword 收益达到阈值时,才恢复 Sherpa Qwen3 POC。 -5. 自动词库学习若实验,必须默认关闭、本地处理、用户确认后写入,并可审计/清空。 - -## 11. 修订记录 - -| 日期 | 说明 | -|---|---| -| 2026-08-11 | 按 1.7.0 代码重写:Qwen3 MLX streaming 默认、Apple Speech fallback、LocalASRBiasAdapter/PersonalDictionary 已接线;移除 Sherpa 当前路径叙述 | -| 2026-03-31 | 初版竞品研究与评测框架 | diff --git a/docs/mac-parity-audit-2026-08-23.md b/docs/mac-parity-audit-2026-08-23.md deleted file mode 100644 index babc1bc..0000000 --- a/docs/mac-parity-audit-2026-08-23.md +++ /dev/null @@ -1,818 +0,0 @@ -# OSGKeyboard · iOS 完整检查 + macOS 差距与移植建议报告 - -> **报告日期**:2026-08-23 -> **当前版本**:2.0.1(build 90)— `project.yml:51-52` -> **审查范围**:iOS Host (`OSGKeyboard/`, 88 .swift) + iOS HostSupport (`OSGKeyboardHostSupport/`, 29 .swift) + iOS Extension (`OSGKeyboardExt/`, 27 .swift) + Shared (`OSGKeyboardShared/`, 230 .swift) + macOS (`OSGKeyboardMac/`, 39 .swift) + macOS Tests (3 .swift) -> **方法**:只读代码考古,证据来自 `project.yml` 共享 / 排除清单 + 文件结构 + 关键源码摘录 + `CHANGELOG.md` - ---- - -## TL;DR - -| 维度 | iOS | macOS | 结论 | -|---|---|---|---| -| 产品形态 | 宿主 App + 自定义键盘扩展 | 单窗口菜单栏 App + 浮动 HUD | **不同形态**——Mac 不是键盘,是"按住 Option 说话→⌘V 注入" | -| 核心流水线 | Flow 跨进程会话 + SpeechAnalyzer | 单一进程 + MLX Audio Qwen3 流式 | **架构不可同构** | -| LLM 润色 | ✅ 完整(`PolishingService`) | ✅ 完整(**直接复用** iOS `PolishingService`) | **完全对齐** | -| 本地 ASR | iOS 26 `SpeechAnalyzer + DictationTranscriber` | **MLX Qwen3-ASR 0.6B/1.7B 4-bit** + Apple Speech 兜底 | **完全不同的技术栈** | -| 云 ASR | 7 家供应商 + 流式 | ✅ **完全对齐**(同 `CloudASRClientFactory`) | **完全对齐** | -| 个人词典 | ✅ | ✅ | **完全对齐** | -| 历史 / 使用统计 | ✅ | ✅(iCloud KVS 共用) | **完全对齐** | -| 润色样式 + 学习 | ✅ | ✅ | **完全对齐** | -| iCloud 同步 | ✅(5 路:设置/词典/历史/使用/样式) | ✅(**5 路全部对齐**) | **完全对齐** | -| 提示页 (Tip) | ✅ | ✅ | **完全对齐** | -| 自定义语言模型 CLM | ✅ UI + 后台 | ⚠️ 后台调用了,**无 UI** | **部分缺失** | -| 剪贴板历史 + AI 技能 | ✅ | ❌ 共享代码已编译但**无 Mac UI** | **完全缺失** | -| AI 提示轮播 / 上下文技能 | ✅ | ❌ 共享代码已编译但**无 Mac UI** | **完全缺失** | -| Flow 跨进程会话 / PiP | ✅ | ❌ | **Mac 形态不需要** | -| 自定义键盘 (Rime/拼音/英文) | ✅ | ❌(`Typing/**` 全排除) | **Mac 形态不需要** | -| OSG 账户 (Sign in with Apple + 积分 + 推荐) | ✅ | ❌ | **完全缺失** | -| App Attest | ✅ | ❌(无对应 entitlement) | **完全缺失** | -| 一方分析 (Analytics) | ✅(108 测试覆盖) | ❌(共享代码已编译但**无任何调用**) | **完全缺失** | -| 助手指令 (Shortcuts) | ✅ | ❌ | **完全缺失** | -| 屏幕唤醒 / 锁 / Live Activity | ✅ | ❌ | **Mac 形态不需要** | - -**最关键的发现**: - -1. **macOS 复用策略 = 源码级 include**。Mac 不 `import OSGKeyboardShared`——`project.yml:570-602` 把 Shared + HostSupport 的 `.swift` 文件直接编进 Mac target,再用 `excludes:` 列表过滤 iOS-only 文件。**整个框架里没有 `#if os(macOS)` 条件编译**(Mac target 内部有几处 `import AppKit` 守卫,是防御性的)。 -2. **LLM 润色、云 ASR、iCloud 同步、本地 ASR 模型管理、模型目录、个人词典、润色样式、提示页、Onboarding——这 9 大块是 iOS / Mac 完全对齐的**。差异主要在"宿主 App 才有"的系统集成(Account、App Attest、Analytics、Shortcuts、Flow 跨进程会话)。 -3. **"共享代码已编进 Mac 但零调用"的浪费**:`OSGKeyboardShared/Features/Account/`、`Features/ManagedGateway/`、`Features/Analytics/`、`Services/AIClipboard*.swift`、`Services/AIHint*.swift`、`Services/AIUserSkill*.swift` —— 这些文件编译进了 Mac 二进制但没有任何 Mac 文件引用。**编译器开销 + 二进制体积 + 维护面全白付**。要么真正接进 Mac,要么从 Mac target 排除。 -4. **Mac 真正独占的体验是 MLX 流式 ASR**。这是 iOS 受限于 ARM NEON + iOS 26 平台绑定做不到的能力,恰好把 Mac 的 Apple Silicon 算力用足。 -5. **移植优先级不应是"把 iOS 全搬到 Mac"**。Mac 是"按住说话→润色→注入"工具,自然形态完全不同。**真正应该补的是:剪贴板 AI、CLM 管理、Account 体系、Analytics。**其他(Flow、自定义键盘)不应该硬塞进 Mac。 - ---- - -## 1. iOS 端完整状态 - -### 1.1 Target 拓扑 - -| Target | 平台 | 类型 | .swift 数 | 备注 | -|---|---|---|---|---| -| `OSGKeyboard` | iOS 26 | App | 88 | 宿主 App,沙盒、StoreKit、iCloud、App Attest | -| `OSGKeyboardExt` | iOS 26 | App Extension (键盘服务) | 27 | `RequestsOpenAccess: true` | -| `OSGKeyboardShared` | iOS 26 | Framework | 230 | 跨进程共用 + 复用到 Mac | -| `OSGKeyboardHostSupport` | iOS 26 | Framework | 29 | 宿主专用(Speech/AVFoundation/Charts/StoreKit) | -| `OSGKeyboardMac` | macOS 15+ | App | 39 | 菜单栏单窗口,**沙盒关闭**(Accessibility 需要) | -| `OSGKeyboardMacTests` | macOS 15+ | Unit Test | 3 | 挂在 Mac App bundle 内 | -| `OSGKeyboardTests` | iOS 26 | Unit Test | 108 | 覆盖 host + shared + hostsupport | -| `OSGKeyboardExtTests` | iOS 26 | Unit Test | 16 | 覆盖 ext + shared | -| `OSGKeyboardUITests` | iOS 26 | UI Test | — | TestFlight 截图自动化 | - -### 1.2 iOS Host App (`OSGKeyboard/`) — 用户可见的所有功能 - -**导航**:4 Tab (Keyboard / Skills / Styles / Settings) on iPhone;iPad 自动切换到 `NavigationSplitView` + 240pt 侧栏 + `WideStatusFooter`。 - -**7 大用户场景**: - -1. **Onboarding**(`Views/OnboardingExperienceView.swift`)—— 7 步:欢迎 → 权限 → 键盘安装 → 切换引导 → 4-feature 实践(语音/翻译/回复/AI)→ 登录奖励 → 完成。匿名实践用 `OOBEClientInfrastructure` 拿服务器发放的体验金。 -2. **Home 仪表盘**(`Views/HomeView.swift:122`)—— 7 天柱状图 + 指标瓦片 + Flow 连接卡 + 词典/历史卡 + 中英文高频词条 chip。 -3. **Settings**(`Views/SettingsView.swift:30`)—— 7 个子面板:账户、AI 代理、剪贴板、Locale、润色强度、翻译目标、语音识别、文本润色、通用、关于。NavigationStack + `SettingsRoute` 枚举。 -4. **Account Center**(`Views/Account/AccountCenterView.swift:11`)—— 已登录摘要、积分 + 等级、StoreKit 商品列表、购买历史链接、登出 / 删号 (reauth)、推荐链接。下拉刷新。 -5. **Polish Styles**(`Views/PolishStylesView.swift:11`)—— 内置/社区/自定义样式网格 + 编辑器 sheet + "从历史学习"按钮。 -6. **AI Agent Skills**(`Views/AIAgentSkillsView.swift:12`)—— 已装/未装技能卡,安装调起 `shortcuts://` 或 bundled `.shortcut` 文件。 -7. **Legal / 隐私**—— `PrivacyPolicyView` / `OpenSourceLicensesView` / 远程 web view。 - -**宿主端 Services(`OSGKeyboard/Services/`)** 关键件: - -| 文件 | 职责 | LOC 估算 | -|---|---|---| -| `FlowSessionManager.swift` | **唯一拥有者**——AVAudioSession 激活、连续 `.playAndRecord` 捕获、ASR 选择、后处理、`PolishingService` 调用、AI 模式、App Group 写、utterance token、launch reconciliation、start/stop/end session | ~3500 | -| `FlowPictureInPictureController.swift` | PiP keep-alive(`AVPictureInPictureVideoCallViewController` 0.1pt content,no audio session) | — | -| `FlowASRPostProcessor.swift` | 本地引擎转写后处理(`LocalASRTranscriptCorrector.apply`) | — | -| `FlowTerminationCoordinator.swift` | 进程退出时同步释放 mic、结束 LiveActivity | — | -| `FlowAnalyticsOperationRegistry.swift` | 会话级分析去重,订阅 `FlowSessionManager` 事件 | — | -| `RimeDeploymentController.swift` | Rime 资源部署(host-owned) | — | -| `AIAgentShortcutInstaller.swift` | 调起 `shortcuts://` 安装 iCloud 分享的技能 / bundled `.shortcut` | — | -| `OOBEClientInfrastructure.swift` | 匿名 OOBE 体验金客户端 | — | -| `OfficialSkillCatalogRefreshService.swift` | 15 分钟 ETag-aware 刷新 `https://account.osglab.com/v1/content/skills` | — | -| `AIHintRefreshService.swift` | 12 小时静默刷新远程提示包 | — | -| `AnalyticsHostService.swift` | `BGTaskScheduler` 注册 (`com.osgkeyboard.ios.analytics-sync`) + `NWPathMonitor` + 后台 drain | — | -| `HostReturnService.swift` | 解析 `FlowSessionBridge.pendingHostBundleId()` 通过 `HostAppURLRegistry` 重开原 App | — | -| `AppURLHandler.swift` | `UIApplicationDelegate` + `UISceneDelegate`:`osgkeyboard://startflow` / `https://osglab.com/i/...` 通用链接 / `didBecomeActiveNotification` 反弹保留 `sourceApplication` | — | -| `AppPermissions.swift` | `AVAudioApplication.requestRecordPermission` (iOS 17+) + `SFSpeechRecognizer` + `PasteAccessResult` 一次性 `pasteAccessVerified` | — | - -**宿主端 Models 关键点**: -- `AppGroupConfiguration` 完整 Codable blob —— 镜像到 App Group + iCloud KVS -- `ProviderConfig` 8 家供应商预设(openai/ark/deepseek/qwen/zhipu/moonshot/siliconflow/groq) - -**关键 iOS-only 调用**: -- `UIOpenURLContext.options.sourceApplication` (iOS 26 only) —— 保留源 bundle id(host-return 白名单唯一路径) -- `AVAudioApplication.requestRecordPermission` (iOS 17+) -- `SpeechAnalyzer + DictationTranscriber` (iOS 26 only) -- `OSAllocatedUnfairLock` (iOS 16+) -- iOS 26 Icon Composer (`OSGKeyboard/AppIcon.icon`) -- `UIBackgroundModes: audio` + `BGTaskScheduler` -- `applinks:osglab.com` Universal Links -- `LSApplicationQueriesSchemes` 白名单 50+ 国外 App scheme -- `SKIncludeConsumableInAppPurchaseHistory=true` —— StoreKit 2 一次性商品进历史 - -**iOS-only entitlements**(在 `OSGKeyboard.entitlements`): -- App Group `group.com.osgkeyboard.shared`(与扩展共享) -- 两个 keychain-access-groups(shared + iOS-only) -- `aps-environment: $(APP_ATTEST_ENVIRONMENT)` ← xcconfig 注入(debug=development / release=production) -- Sign in with Apple `Default` -- iCloud KVS `$(TeamIdentifierPrefix)com.osgkeyboard.ios`(**单字符串**,非数组 —— 数组会破坏自动签名) -- 音频输入 -- 关联域名 `applinks:osglab.com` - -**StoreKit 配置**(`OSGKeyboard.storekit`):4 个消耗型 —— 自愿打赏 `ByRockyACoffee` (¥28) + 3 档账户积分 `500tks/1500tks/3000tks` (¥8/¥18/¥28)。**无订阅**。 - -### 1.3 iOS HostSupport 框架 (`OSGKeyboardHostSupport/`) - -设计原则:**唯一**链接 `Speech / AVFoundation / Charts / StoreKit` 的 iOS 框架——键盘扩展**不**链接它,把 jetsam 预算留给 Shared。 - -**子目录**: - -| 路径 | 内容 | -|---|---| -| `Services/` | ASR + 云 ASR + Flow 捕获 + Dictation + Tip + `CloudASR/` + `Tip/` | -| `Models/` | `AudioBufferSnapshot+AVFoundation.swift` —— `AVAudioPCMBuffer` ↔ 共享 `AudioBufferSnapshot` 适配 | -| `DesignSystem/` | `SevenDayUsageChart` + `UsageStatsCluster` + `SupportDeveloperSection` | -| `Features/Account/` | `AccountAPIClient` / `SignInWithApple` / `DeviceIntegrity` (App Attest) / `HostPrivateAccountKeychain` / `AccountModels` / `OOBEGrantProvisioningCoordinator` | - -**关键 Services**: - -- **`ASRService.swift:1`** —— 公共协议 `transcribe(stream:locale:)` 返回 `AsyncStream` (`.capability/.partial/.final/.error`);`transcribeChunk(samples:locale:)` 用于流水线 Flow 路径。`ASRServiceFactory.make(store:)` 返回 `SpeechAnalyzerASR`(本地)或 `CloudASRService`(云)。`SpeechAnalyzerASR` 用 **iOS 26 `SpeechAnalyzer + DictationTranscriber` + bundled `SFCustomLanguageModelData`**,锁用 `OSAllocatedUnfairLock`,支持流水线 chunk 复用。 -- **`CloudASRService.swift:1`** —— 云 ASR 前门。`CloudASRClientFactory.make(...)` 返回 8 个客户端:`ZhipuCloudASRClient` / `AlibabaFunASRClient` / `BailianRealtimeASRClient` / `VolcengineCloudASRClient` / `OpenAIRealtimeASRClient` / `PromptCloudASRClient` / `ManagedVolcengineASRClient` / `UnsupportedCloudASRClient`。 -- **`LiveDictationController.swift:1`** —— `@MainActor ObservableObject`,阶段机 `idle/recording/processing/denied/error`,自管 `AVAudioEngine + AVAudioSession`。生产键盘**不**用它,**生产用 `FlowSessionManager + FlowContinuousCapture`**。 -- **`FlowAudioSessionCoordinator.swift:1`** —— 进程级 `AVAudioSession` + Flow 的 `AVAudioEngine` 拥有者。`.voiceChat` 模式。 -- **`CustomLanguageModelManager.swift`** —— 单例,准备 bundled `OSGKeyboardCLM.bin` 写进 App Group。指数退避 30s/120s/600s。**重到 `MainAppRoot.scheduleCLMWarmup` 延后 45s 启动,Flow 忙时拒绝运行**。 - -**HostSupport 持久化**: -- CLM 编译产物 → App Group container -- Account 令牌 → host-only keychain(**不**走 App Group) -- Managed AI 凭证 → 单独 `GatewayGrantKeychainStore`;**只**镜像"会话可用"布尔进 App Group -- Tip 计数 → 标准 `UserDefaults` - -### 1.4 iOS Keyboard Extension (`OSGKeyboardExt/`) - -**入口**:`KeyboardViewController.swift:33-38` —— `@objc(KeyboardViewController) @MainActor` `UIInputViewController`。**单一** `KeyboardState` ObservableObject。 - -**生命周期(`KVC.swift`)**: -- `init` → 启动 `KeyboardExtensionMemoryTelemetry`(warning 36MB / safe 40MB / critical 48MB) -- `viewDidLoad` (`:148-207`) → 设 `showsSystemGlobeKey = isPad`、`primaryLanguage = "mis"`(隐藏"English"副标题误导),读 `TypingInputConfiguration.preferredSurfaceOnOpen()`,调 `refreshLayoutMode() + installKeyboardHeight() + configureDictationBehavior() + installServices() + installSwiftUI()` -- `viewWillAppear` (`:256-293`) → 标记 `KeyboardSetupBridge.markExtensionAppearance(hasFullAccess:)`、刷新 Flow/config、同步 onboarding、准备 `KeyboardHapticFeedback` generators -- `viewIsAppearing/viewDidAppear` (`:295-356`) → 锁高度 `lockPresentedKeyboardHeight()`,禁系统手势延迟,记录 analytics -- `viewWillDisappear` (`:209-254`) → 通知 `AnalyticsExtensionService.keyboardWillDisappear()`、取消 `assistantFieldActionRefreshTask`、使 `editHintScheduler` 失效、持久化最后 surface -- `textDidChange/selectionDidChange` → `refreshReturnKeyRole`、同步 English 文档上下文 -- `didReceiveMemoryWarning` (`:390-404`) → **取消流水线、退出 typing 模式、强制回 voice surface** —— typing 引擎是最大内存消费者 - -**`installServices` (`:419-547`)** 安装的 8 个 coordinator: -- `EditHintScheduler`(mic 上方一句话提示) -- `KeyboardTextInserter`(插入 Flow 转写 / undo / redo / 粘贴剪贴板) -- `KeyboardConfigSync`(App Group 配置 + Darwin observers + onboarding 镜像) -- `KeyboardFlowCoordinator`(Flow start/stop + session monitor + watchdogs) -- `LastInputEditCoordinator`(长按 → 编辑上次输入) -- `AIKeyboardCoordinator`(长按 → AI 模式) -- `ClipboardCaptureCoordinator`(`changeCount` 轮询,secure-field 抑制,建议条) -- `AnalyticsExtensionService` - -**`installStateActions` (`:551-655`)** 装的 30+ action:`beginRecording/endRecording/tapMic/cancelVoiceInput/beginEditLastInput/.../setMode/setLocale/setEngineMode/setTranslationTargetLocaleId/insertNewline/insertSpace/deleteBackward/undoLastInsertion/redoLastInsertion/copySelection/cutSelection/setSurface` - -**手势模型**: -- 长按 push-to-talk → `RecordButtonGesturePolicy` 阈值 `longPressDuration = 0.45s` -- 点击 mic → 切换(`RecordButton.tapAction`) -- 重复删除 → `RepeatingPressButton` + `RepeatingDeleteTiming`(80→50→30→15 ms 加速) -- Shift 长按 → `TypingSessionController.shiftHeld`,双击 capsLock -- 语言切换 → 3-tab `KeyboardInputTab`(assistant / 中文 / 英文) -- 触觉 → `KeyboardHapticFeedback`(5 个 Taptic generator + 4 个按键角色 + `KeyboardHapticIntensity` off/light/strong) -- 声音 → `UIDevice.playInputClick()` + 系统音 1155 (delete),通过 `@retroactive UIInputView: UIInputViewAudioFeedback` 启用点击声 -- 主题 → 始终深色(`Palette.dark` 是 canonical) - -**iOS-only 扩展调用**:`UIInputViewController` / `UIInputView` / `UITextDocumentProxy` / `NSExtensionContext` / `UIPasteboard.general` / `UIImpactFeedbackGenerator` / `UIDevice.playInputClick()` / `AudioServicesPlaySystemSound(1155)` / `UIButton` / `UICollectionView` + `UICollectionViewCompositionalLayout` / `UIHostingController` - -**Host/Extension 边界数据**(`AppGroupPersistor.load(into:)` 读取): -- `providerId` / `baseURL` / `apiKey`(`Keychain.apiKeyOutcome` 解析 shared keychain `$(AppIdentifierPrefix)com.osgkeyboard.shared`) -- `model` / `modeId`(恒为 `polish`)/ `localeId` / `engineMode`(local/cloud) -- `translationTargetLocaleId` / `handednessPreference`(左/右手 delete↔space 互换)/ `clipboardHistoryEnabled` / `clipboardCandidateBarEnabled` -- `keyboardHapticIntensity` / `clipboardSkillSnapshot`(启用技能 ID)/ `apiKeyAvailability` - -**`KeyboardConfigSync` (`KeyboardConfigSync.swift:24-62`)** 安装 4 个 `FlowSessionDarwinObserver`:`session.changed` / `command.changed` / `transcription.changed` / `host.ready.changed` - -### 1.5 iOS Shared Framework (`OSGKeyboardShared/`) - -**目录**: - -| 路径 | 内容 | -|---|---| -| `Constants/AppGroup.swift` | App Group ID 唯一源 | -| `Core/Configuration/` | `ConfigurationStore` 协议 + iOS 实现 `AppGroupStore+ConfigurationStore.swift` + `LiveConfigurationStore` | -| `DesignSystem/` (9 文件) | `Theme` / `RecordButton` / `RecordButtonGesturePolicy` / `WaveformView` / `EditTextPager` / `CardPageLayout` / `SonicParticleField` / `ThemedRoot` / `UsageStatCard` / `UsageSurfaceCard` | -| `Features/Analytics/` (10 文件) | `AnalyticsClient` / `AnalyticsRepository` (SQLite) / `AnalyticsUploadCoordinator` / `KeyboardUsageRepository` (跨进程 SQLite WAL) / ... | -| `Features/ManagedGateway/` (8 文件) | `ManagedLLMClient` / `GatewayGrantCoordinator` (actor) / `GatewayGrantCredentialStore` / `ManagedGatewayAccountAccessPolicy` / `ManagedGatewayScopePolicy` / `ManagedGatewayQuestionRouter` / `ManagedGatewayModels` / `OOBEGatewayGrantCoordinator` | -| `Localization/SharedL10n.swift` | `NSLocalizedString` against `Shared.strings`;解析 `AppUILanguage` | -| `Models/` (~50 文件) | 见下 | -| `Resources/` | `ClipboardSemantics/{*.mlmodel,*.json}` (6 CoreML) / `PolishStyles/{manifest.json,builtin.*.json}` (10 内置样式) / `Typing/English/{english_lexicon.{bin,tsv},english_bigrams.tsv}` | -| `Services/` (~80 文件) | 见下 | -| `Typing/` (24 文件) | librime + 拼音 + 英文 autocorrect + 触屏 + 布局 | -| `Utilities/` (21 文件) | 日志、错误模型、keychain、locale、Han 脚本、PCM/WAV、内存预算等 | - -**关键 Services 详解**: - -- **`PolishingService.swift:34-105`** —— `public actor`。`polish(_ raw:mode:systemPrompt:context:) -> PolishOutcome(text:qualityDegraded:polishStyleID:polishStylePrompt:)`。**单一融合 LLM 调用**:T1 自纠 → T2 填料清理 → T3 同音纠正 → T4 标点 → T5 结构 → 应用样式。Fun personality 在 heavy 强度切到"仅格式"共享核心。 -- **`LLMClient` 协议** (`Services/LLMClient.swift:91-150`) —— `polish(_:systemPrompt:timeout:)` + `complete(messages:tools:timeout:options:)`。实现:`LLMClientFactory`(OpenAI 兼容)、`AnthropicLLMClient`(Anthropic Messages API)、`ResponsesAPILLMClient`(OpenAI Responses)、`ManagedLLMClient`(账户 grant 范围)、`AIModeSearchFallbackClient`(搜索→纯)。 -- **`Keychain.swift:15`** —— `kSecClassGenericPassword` 读写 + iCloud 同步变体 + `OnFirstUnlockThisDeviceOnly` 本地变体 + XCTest 内存回退。 -- **`AIClipboardSkill.swift:44`** —— 技能模型:`id` / `systemImage` / `titleKey` / `kind`(`.direct/.transform/.export`)/ `isDefault` / `shortcutName` + iCloud 分享 URL + bundled `.shortcut` 资源 / `thinkingEnabled` / `customName/Summary/Prompt` / `requiresShortcut` / `isUserCreated` / `isOfficial` / `managedGatewayTaskKind` / `supportsReplyStyle`。 -- **`ClipboardHistoryStore.swift:10-84`** —— App-Group-backed observable store。合并通过 `ClipboardHistoryPolicy`(去重、清洗、保留)。 -- **`ClipboardSemanticAnalyzer.swift:1-58`** —— 本地:`NLTokenizer/NLTagger` + 6 CoreML 分类器(`ClipboardSemantics/*.mlmodel`)产生 `language/dates/addresses/phoneNumbers/urls/personNames/organizationNames/sentiment/task/question/invitation/complaint/replyableMessage` 意图。 -- **`ClipboardSkillSemanticRanker.swift:11-66`** —— 用语义分析 + 偏好语言对完整目录排序,固定 generic Reply 兜底。Stateless / `Sendable`。 -- **`SpeechHistoryStore.swift:10-78`** —— iCloud-KVS 镜像 observable,prompt-fingerprint dedup。 -- **`LocalASRModelManager.swift:11`** —— macOS Qwen3-MLX 权重 Application-Support 安装管理器。状态持久化到 `installed-manifest.json`。**Mac 专用**(iOS 用系统 SpeechAnalyzer)。 -- **`LocalASRBiasAdapter.swift:9`** —— 分层偏差:`PersonalDictionary.effectiveEntries` + 内置 `BuiltinLexiconIndex.topTerms(...)`;按前台 App bundle id 选 `builtin-computer` vs `builtin-top`(Xcode/VSCode/Android Studio/IntelliJ/AppCode/Sublime/Terminal/iTerm2/Warp)。 -- **`LocalASRTranscriptCorrector.swift:9-65`** —— 确定式 alias→canonical 替换,最长匹配优先,ASCII whole-word 感知。 -- **`PolishStyleLearningService.swift:64`** —— `build(from:)` 从 `SpeechHistoryEntry` 历史生成 `PolishStyleLearningCorpus`(5000 字符 `requiredEffectiveCharacterCount`)。 -- **`AIQuestionService.swift:20`** —— `actor AIConversationStore` 保留 `retainedConversationRounds` 轮 / 对话。 -- **`AIEventExtraction` / `AIAddressExtraction` / `AITodoExtraction`** —— 解析 LLM 输出为 `start|end|title|location` / `origin|destination` / 列表。**全部 fail-closed**。 -- **`HostAppURLRegistry`** —— 白名单 deep-link 路径(`deployrime`、`settings/clipboard`、`skill/run` 等),host + ext 共用。 -- **`KeyboardSetupBridge`** —— `markExtensionAppearance(hasFullAccess:)` + OOBE 实践会话标志。 -- **`AppGroupConfigDarwin`** —— `CFNotificationCenter` 发 `com.osgkeyboard.config.changed`(host 写完后)。 -- **`FlowSessionBridge` + `FlowSessionKeys`** —— 跨进程 mailbox 全部 App-Group key:`flowHostReady` / `flowHostReadyAt` / `flowHeartbeat` / `flowSessionActive` / `flowSessionExpires` / `hostHeavy/At` / `hostGeneration` / `transcriptionResult` / `transcriptionPartial` / `transcriptionError{Kind,PolishWarning}` / `pendingKeyboardUtteranceId` / `lastPiPArmAttemptAt` / `flow.commandPayload.v1` / `flow.commandJournalPayload.v2` / `flow.resultPayload.v1` / `flow.ackPayload.v1` / `flow.startTransaction.v1` / `flow.readyPayload.v1`。 -- **`SettingsCloudSync` / `SpeechHistoryCloudSync` / `UsageStatisticsCloudSync` / `AppCloudSync`** —— KVS 镜像;`ICloudSyncPreferences` + `SyncDeviceID` 保留每设备身份。 -- **`AIHintStore` + `AIHintPool` + `AIHintKeywordExtractor` + `AIHintKeywordCompressor` + `AIHintLocalCatalog`** —— 闲置热词轮播:App-Group-pack loader、合并远端 + 本地卡片、确定性 10 或 22 字符关键词抽取(zh/en)。 -- **`TranscriptionPolishFallback.swift`** —— LLM 润色失败或跳过时,交付原始转写 + 软警告。 -- **`WhatsNewDemoScenario`** —— "What's New" 时间轴 driver,host(peek/consume)和 ext(DEBUG)共用。 - -**Polish 子系统详细**: -- `PolishContext` (`Models/PolishContext.swift`):appContext + precedingText + followingText + fieldHints + dictionarySupplement + maxPrecedingChars=600 + maxFollowingChars=200 -- `AppContext` 5 种(code/email/chat/document/unknown) -- `AppContextDetector` (`:30-116`):3-fallback 链——`textDocumentProxy.documentContextBeforeInput` 启发式(iPad 沙盒安全;键盘读不到前台 bundle id)→ 30 分钟缓存 → 环境 blend -- `PolishStylePack` (`Models/PolishStylePack.swift:9-87`):`Codable, Equatable, Identifiable, Sendable`;`id/name/prompt/allowsAddedEmoji/kind(.builtin/.user)/createdAt/updatedAt` -- `PolishStyleLimits`:`maximumUserPacks = 8` / `maximumPromptCharacters = 6_000` -- 内置样式分两组:`.practical`(light/structured/formal/chat)+ `.fun`(dating/flex/corp/diba/xhs) - -**Typing 子系统**(`Typing/`): -- `LibrimeEngine.swift` —— `@MainActor` 中文 IME,部署 3 套 schema:`osg_pinyin` / `osg_double_pinyin_mspy` / `osg_double_pinyin_sogou` -- `EnglishSuggestionEngine.swift` —— 3 槽 QuickType(verbatim / correction / completion),`inVocabularyFrequencyGap = 250` -- `EnglishLexicon.swift` —— mmap 40k 词 `english_lexicon.bin` -- `EnglishSystemLexicon.swift` —— `#if canImport(UIKit)` 守卫包 `UITextChecker + UILexicon` -- `EnglishQWERTYProximity.swift` —— 空间编辑代价 -- `TypingSessionController` —— 拥有 typing surface:language/page/shift state、English 引擎、librime 引擎、拼音/英文消歧、autocap、句号快捷、shadow preceding text、`supplementaryWords`、hot reload -- `TypingSurfaceMetrics` (`Models/TypingSurfaceMetrics.swift`) + `KeyboardChromeLayout` —— 单一源:键行指标、底部动作行分数、总高 281pt,iPad narrow/wide(narrow 54pt / wide 76pt),`wideIPadWidthThreshold = 1100` -- `TypingInputConfiguration` —— `TypingInputSchema` (3 case) + `DefaultInputMode` (voice/pinyin/english) -- `PeriodShortcut.swift` —— iOS 句号快捷:letter→number 后第二个空格 0.45s 内变 ". " + arm sentence Shift -- `TypingAutocapitalization` —— 镜像 `UITextAutocapitalizationType` - -**Models (~50 文件) 关键**: -- `AppGroupConfiguration` —— 完整 Codable blob -- `SyncedAppSettingsV2` —— per-field `SyncedField` LWW + broken-clock containment (6h skew horizon) -- `LLMProvider` —— 8 家云预设 -- `AIUserSkill` / `AIAgentSkillLayout` / `OfficialSkillCatalog` / `OfficialSkillDefinition` —— 技能体系 -- `PolishStylePack` / `PolishStyleCatalog` / `PolishStyleLearningCorpus` -- `PersonalDictionary` / `+Merging` / `+ASRBias` —— 跨设备词典,iCloud 合并;提供 `hotwords` / `asrPromptBias` / `alibabaHotwordEntries` / `vocabularySyncFingerprint` -- `ClipboardHistoryEntry` / `SpeechHistoryEntry` / `SyncedSpeechHistory` -- `PolishContext` / `FieldHints` / `AppContext` / `AppUILanguage` -- `ProviderConfig` / `CloudProviderRole` / `CloudASRModels` / `LocalASRModelCatalog` / `LocalASRCapabilities` / `LocalASRBiasPayload` -- `TypingInputConfiguration` / `TypingSurfaceMetrics` / `KeyboardChromeLayout` -- `HandednessPreference` / `KeyboardHapticIntensity` / `PolishIntensity` / `AIResponseLength` -- `FlowUtteranceRequest` / `FlowUtteranceMode` / `FlowUtteranceChunkConfig` / `FlowHandoffPolicy` / `FlowAck` / `FlowResult` / `FlowCommand` / `FlowReadySnapshot` / `FlowStartTransaction` / `FlowTranscriptionError` / `FlowFieldContext` -- `MicVoiceAvailability` / `+Keyboard` —— mic 状态 + 派生 -- `EditableInputReference` —— 跨进程最后插入引用(10 min TTL、≤1200 graphemes、schema v1) -- `EditSessionState` —— 关闭状态机 `.inactive/.preparing/.listening/.processing/.review/.applying/.appending/.failed` -- `AISessionState` —— `.inactive/.idle/.preparing/.listening/.recognizing/.generating/.ready/.awaitingSend/.inserted/.sent/.failed` - -**Utilities (21 文件)**: -- `OSGLog.swift` —— subsystem + 类别(`flow/clm/config/asr/keyboardExt`) -- `OSGDiag.swift` —— NSLog + 内存快照(`task_info`) -- `FlowTrace.swift` —— `[trace] stage=...` -- `FlowPipelineDiagnostics.swift` / `FlowCaptureTailDrain.swift` / `FlowUtteranceEndCoordinator.swift` / `FlowUtterancePCMStore.swift` -- `UtteranceStreamChunker.swift` / `UtteranceBatchFallbackPolicy.swift` / `UtteranceTranscriptGuard.swift` / `UtteranceTranscriptStitcher.swift` / `TranscriptOverlapUtilities.swift` / `TranscriptLanguageDetector.swift` -- `ProgressiveDictationTranscriptAccumulator.swift`(**Mac 排除**) -- `DictationTextComposer.swift` / `FinalChunkRecovery.swift` / `PCMSampleWavEncoder.swift`(mono Float32 @16kHz → WAV) -- `PromptXMLEscaping.swift` / `HanScript.swift`(BMP 汉字谓词) -- `HostMemoryBudget.swift` / `KeyboardExtensionMemoryTelemetry.swift`(**Mac 排除**) -- `ProviderDisplayName.swift` / `AppVersionDisplay.swift` - -**iOS-only 排除(`project.yml:570-586`)**: -``` -DesignSystem/WaveformView.swift -DesignSystem/RecordButton.swift -DesignSystem/RecordButtonGesturePolicy.swift -Services/KeyboardState.swift -Services/KeyboardOpenSurfacePolicy.swift -Models/MicVoiceAvailability+Keyboard.swift -Models/TypingInputConfiguration.swift -Models/TypingSurfaceMetrics.swift -Typing/** -Utilities/ProgressiveDictationTranscriptAccumulator.swift -``` - -### 1.6 iOS 测试覆盖(108 + 16 文件) - -**`OSGKeyboardTests/` (108 文件)** —— host + shared + hostsupport 端到端: -- **ASR/云 ASR**:`CloudASRServiceTests` / `CloudASRHTTPClientTests` / `CloudASRStreamingHelpersTests` / `CloudASRStreamingEventParsingTests` / `CloudASRTests` / `ASRConversionTests` / `LocalASRModelCatalogTests` / `LocalASRDownloadSourceSorterTests` / `LocalASRBiasAdapterTests` / `AlibabaVocabularySyncTests` / `FrequentTermStoreTests` / `PreviewASRControllerStateTests` -- **语音流水线**:`VoicePipelinePerformanceTests` / `FlowPhysicalAudioStressTests` / `FlowReliabilityTests` / `FlowASRPostProcessorTests` / `ChunkedUtterancePipelineTests` / `FlowBudgetAndMergeTests` / `FlowCaptureTailDrainTests` / `FlowUtteranceEndCoordinatorTests` / `FlowUtterancePCMStoreTests` / `UtteranceBatchFallbackPolicyTests` / `UtteranceStreamChunkerTests` / `UtteranceTranscriptGuardTests` / `UtteranceTranscriptStitcherTests` / `ProgressiveDictationTranscriptAccumulatorTests` / `FlowSessionBridgeTests` / `FlowSessionManagerAnalyticsTests` / `FlowSessionPolicyTests` / `FlowHandoffPolicyTests` / `FlowPiPRecoveryPolicyTests` / `FlowStartTransactionPolicyTests` / `FlowHomePiPStatusPolicyTests` / `FlowKeyboardPoliciesTests` / `KeyboardExtensionMemoryBudgetTests` -- **Polish/LLM**:`PolishStylePackTests` / `PolishStyleLearningServiceTests` / `PolishPromptComposerQuestionTests` / `PolishOutputValidatorTests` / `IntelligentPolishTests` / `LLMClientTests` / `AIModeLLMClientTests` -- **AI 特性**:`AIQuestionServiceTests` / `AIEventExtractionTests` / `AIAddressExtractionTests` / `AINoteExportTests` / `AIHintKeywordExtractorTests` / `AIHintPoolTests` / `AIUserSkillTests` / `AIUserSkillStoreTests` / `AIAgentSkillLayoutTests` / `AIHistoryAndUsageTests` / `AssistantFieldActionTests` / `AISessionStateTests` / `AIClipboardPromptTests` / `AnalyticsAIOperationTests` / `AppleNaturalLanguageCapabilityTests` / `SpeechHistoryRevisionTests` / `SpeechHistoryDayDeletionTests` / `SpeechHistoryCloudSyncTests` / `PublicContentRefreshServiceTests` -- **剪贴板/工具**:`ClipboardSkillSemanticRankerTests` / `ClipboardSemanticAnalyzerTests` / `ClipboardHistoryStoreTests` / `ClipboardHistoryPolicyTests` / `AccountCenterViewModelTests` / `AccountSnapshotLoaderTests` / `AccountAPIClientTests` / `AccountSignInCoordinatorTests` / `AccountCreditPurchaseManagerTests` / `AccountSecurityPrimitiveTests` / `ReferralProfileTests` / `EditTransactionStoreTests` / `EditableInputReferenceTests` / `EditLastInputPromptTests` -- **iCloud 同步**:`PersonalDictionaryCloudSyncTests` / `PersonalDictionaryMergeTests` / `SettingsCloudSyncTests` / `UsageStatisticsCloudSyncTests` / `KeyboardUsageRepositoryTests` / `KeyboardUsageModelTests` / `KeyboardUsageUploadCoordinatorTests` / `AnalyticsUploadCoordinatorTests` / `AnalyticsUploadSchedulingTests` / `AnalyticsRepositoryTests` / `AnalyticsModelTests` / `AnalyticsAttributionTests` -- **本地 ASR + 内存**:`LocalASRModelCatalogTests` / `KeyboardExtensionMemoryBudgetTests` -- **设置 + 工具**:`AppGroupConfigurationTests` / `AppGroupOnboardingStoreTests` / `ConfigurationStoreTests` / `HostAppURLRegistryTests` / `KeyboardTranslationConfigProtectionTests` / `KeychainTests` / `DeviceIntegrityTests` / `MicVoiceAvailabilityTests` / `OpenSourceLicenseCatalogTests` / `TipProductTests` / `EnglishTypingOnDeviceTests` / `TranscriptLanguageDetectorTests` -- **账户/Managed gateway**:`AccountAPIClientTests` / `AccountSignInCoordinatorTests` / `AccountCenterViewModelTests` / `AccountCreditPurchaseManagerTests` / `AccountSecurityPrimitiveTests` / `AccountSnapshotLoaderTests` -- **测试支持**:`AnalyticsTestSupport` / `KeyboardUsageTestSupport` / `FakeUbiquitousKeyValueStore` - -**`OSGKeyboardExtTests/` (16 文件)**: -- `EnglishTypingTests` / `RimePersonalDictionaryExporterTests` / `KeyHitTestingTests` / `CandidatePanelExpandTests` / `KeyboardUsageTypingTests` / `LibrimeIntegrationTests` / `ManagedGatewayTests` / `RimeSchemaGeneratorTests` / `KeyboardSurfaceStateTests` / `AnalyticsExtensionPrivacyTests` / `PinyinNextKeyResolverTests` / `TypingTouchTrackerTests` / `FinalChunkRecoveryTests` / `KeyboardStateTests` / `EditHintSchedulerTests` / `ClipboardSuggestionLifecycleTests` - ---- - -## 2. macOS 端完整状态 - -### 2.1 目标基本事实 - -- Bundle ID: `com.osgkeyboard.mac`(Developer ID + 公证,**沙盒关闭**) -- 最低系统:macOS 15.0。Swift 6 / strict concurrency -- 装包名:`OSGKeyboard.app`(target 名 `OSGKeyboardMac` 改 `PRODUCT_NAME`) -- 本地 ASR:**MLX Audio Qwen3-ASR 0.6B/1.7B 4-bit**(`ThirdParty/mlx-audio-swift/` SPM) -- 复用 iOS AppIcon (`OSGKeyboard/AppIcon.icon`) + iOS 资产目录(除 `AppIcon.appiconset`) - -### 2.2 macOS App 目录结构(`OSGKeyboardMac/`,39 Swift 文件,无子目录) - -**入口与生命周期**: -- `OSGKeyboardMacApp.swift:1` —— `@main struct OSGKeyboardMacApp: App`。**单一** `Window`(`.windowStyle(.hiddenTitleBar)`、`.defaultSize(width: 860, height: 600)`)+ `MacAppDelegate: NSApplicationDelegate + NSPopoverDelegate` 拥有 `NSStatusItem + NSPopover`(340×420 transient)+ 浮动 dictation overlay -- **不**用 `MenuBarExtra`(`OSGKeyboardMacApp.swift:104-105` 注释:与主 Window 同存时图标会消失) -- 订阅 `Notification.Name`(`.settingsDidSyncFromCloud` / `.personalDictionaryDidSyncFromCloud` / `.usageStatisticsDidSyncFromCloud` / `.speechHistoryDidSyncFromCloud`)iCloud pull 刷新 -- `onOpenURL` 接 `osgkeyboard://seed-demo`(DEBUG `DemoDataSeeder`) - -**侧栏/Shell**: -- `MacRootView.swift` —— `NavigationSplitView(.balanced)`,侧栏:`OSGLogoWide` 品牌头 + 5 个 `MacSection` 行(`.dashboard/.history/.dictionary/.styles/.settings`)+ "Devices" 页脚;detail 切换 `DashboardView/MacHistoryView/MacDictionaryView/MacPolishStylesView/MacSettingsView`;底部 `MacStatusFooter` -- `MacDictationViewModel.swift:12` —— `MacSection` enum -- `MacTheme.swift:114` —— `MacSystemPalette` 双模式(light 暖白 + dark stepped systemGray6→4),通过 `\.themePalette` 注入,颜色方案变更重渲染 -- `MacAppearance.swift` —— `MacAppearancePreference`(system/light/dark)存 `mac.appearancePreference`,`applyToApp` 推到 `NSApp.appearance` 和每个 window - -**菜单栏 UI**(Status Item + Popover): -- `OSGKeyboardMacApp.swift:140` —— variable-length `NSStatusItem` 模板 NSImage,target/action = `togglePopover(_:)` -- Popover 内容:`NSHostingController(rootView: MacMenuBarPopover())` (`:214`)。未完成 onboarding → "open the main window" 提示;否则 `MacContentView`(品牌 + record 按钮 + 热键提示 + 状态文本 + 滚动转写 ≤120pt + 模式/翻译/连接状态 + footer) -- Popover 打开:`prepareForPopoverPresentation()` snapshot 前台 App,**OSGKeyboard 成为 key 时不丢失粘贴目标** - -**录制 overlay**: -- `MacDictationOverlayController.swift` —— 一个 borderless non-activating `NSPanel`(level `floatingWindow + 1`、`.canJoinAllSpaces + .fullScreenAuxiliary + .stationary`) -- 状态机驱动 show/hide(`viewModel.$isRecording/$isPreparingToRecord/$isProcessing` CombineLatest3) -- 位置 user-draggable(双击重置底部居中),持久化到 UserDefaults(`mac.overlay.hasCustomPosition / centerX / originY`) -- `MacDictationOverlayView.swift` —— SwiftUI pill(500pt 内容宽)+ 状态点 + 一行转写 + 实时徽章 + `MiniWaveform` + stop 按钮 -- `MiniWaveform` 复用 `OSGKeyboardShared/DesignSystem` - -**主窗口 5 大页**: -- `DashboardView.swift` —— `GeometryReader` 垂直布局:品牌头 + 7 天图(`UsageStatsCluster layout: .split`)+ `dictationStage` 卡片(`viewModel.homePreviewText` 累计预览)+ `BottomDictationBar`(翻译选择器 + record 按钮 + ready chip) -- `MacHistoryView.swift` —— 按天分组 `SpeechHistoryStore`,每行 copy/delete context menu,"Clear" 带确认 -- `MacDictionaryView.swift` —— `PersonalDictionary` 按 `Entry.Category` 分组,按使用次数 + 术语排序;搜索框过滤;`+` 弹 `MacDictionaryEntryEditor` sheet -- `MacPolishStylesView.swift` —— `PolishStylePack` 目录(内置 practical+fun + 用户),只读 prompt 详情 / 完整编辑器(`allowsAddedEmoji` 切换 + 2400 字符 prompt 限制);学习卡:语料进度 + "Generate learned style" → `PolishStyleLearningService.generateStyle` -- `MacSettingsView.swift` —— `NavigationStack + ScrollView` 7 段:support developer (Tip) / general (appearance / interface language / recognition language / iCloud sync) / recognition method (cloud vs local) / cloud ASR provider / local ASR model catalog / polish provider / input (hotkey / auto-paste / accessibility 状态) / legal (Privacy Policy + Third-Party Licenses + Restart Onboarding + version) - -**Onboarding**: -- `MacOnboardingView.swift:21` —— 6 步:`welcome → microphone → accessibility → engine → [.cloudAPI | .localModel]`,根据 `viewModel.config.engineMode` 分支 -- Mic 步调 `AVCaptureDevice.requestAccess(for: .audio)`;accessibility 步打开 `x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility` 并 1s 后重查 `AXIsProcessTrusted` -- Cloud 步收 LLM provider + key + model;local 步显示默认 Qwen3-MLX 0.6B + 下载按钮 + 进度 + "skip" 提示 -- 状态存 `mac.hasCompletedMacOnboarding` -- 注释 `MacOnboardingView.swift:7-8` —— "Sherpa identifiers and install records are retained only for migration compatibility":**近期从 Sherpa 换到 MLX** -- Settings → About 有 "Restart Onboarding" 按钮 - -**Legal / Support**: -- `MacLegalSettingsViews.swift` —— `MacPrivacyPolicyView`(`WKWebView` 加载 `OSGKeyboard/Resources/PrivacyPolicy.html`,按 `uiLanguage` 切滚动锚点)+ `MacOpenSourceLicensesView`(`OpenSourceLicenseCatalog.entries(for: .macOS)` 列表)→ `MacOpenSourceLicenseDetailView`(许可证名 / upstream 链接 / 用途 / 文本) -- `MacSupportDeveloperTipRows.swift` —— StoreKit 2 `TipPurchaseManager.shared` 消耗型打赏行 - -**DEBUG 工具**: -- `osgkeyboard://seed-demo` URL 触发 `DemoDataSeeder.seedRichPlaceholderData` -- `MacAudioRecorder.hasLiveSnapshotSink`(DEBUG seam,见 `MacAudioRecorder.swift:143`) - -### 2.3 全局热键 + 辅助功能 + 粘贴注入 - -**`MacHotkeyService.swift`**: -- 用**两个** `NSEvent.add*MonitorForEvents(matching: .flagsChanged)`(global + local) -- 区分左/右 Option(`NX_DEVICELALTKEYMASK = 0x20` / `NX_DEVICERALTKEYMASK = 0x40`);`MacHotkeyTrigger`(`rightOption / leftOption / eitherOption`) -- 150ms 持有防抖(`scheduleBegin` → `Task.sleep(150ms)` → fire `onPressBegan`),松开取消未发起的 begin -- `start()` 返回 Accessibility 状态(`globalFlagsMonitor == nil` ⇒ 大概率无权限),DEBUG 下 NSLog - -**`MacTextInsertionService.swift`** —— Accessibility 门控合成 ⌘V: -- `AXIsProcessTrustedWithOptions([kAXTrustedCheckOptionPrompt: true])` 按需 -- `insert(_:autoPaste:targetApp:)`:清 `NSPasteboard.general` → 写转写 → snapshot 原始 pasteboard(**所有 representation,不仅是 `.string`**)→ `CGEvent + kVK_ANSI_V + cghidEventTap` 合成 ⌘V → 等 500ms 后**仅当 change count 仍匹配 post-transcript write**才恢复 snapshot(用户/clipboard-manager 写入期间不覆盖) -- `FrontmostAppTracker`(`:177`)跟踪 `NSWorkspace.didActivateApplicationNotification`,**OSGKeyboard popover 激活时**保留原始 paste 目标 -- `activate(_:)` 重新激活目标 App,等最多 1s 成为 frontmost -- `shouldRestorePasteboard` + `restoreItems` 是单测 seam - -### 2.4 音频捕获 - -**`MacAudioRecorder.swift`**: -- `MacAudioRecording` 协议 -- **`AVAudioEngine.inputNode`** + 4096 帧 tap -- 输入格式 → 16kHz mono Float32 via `AVAudioConverter` -- 10 分钟 @16kHz 硬上限 + 30s trim hysteresis(卡住的热键不会无限增长 buffer) -- RMS 平滑(attack 0.5 / decay 0.15) -- 权限:`AVCaptureDevice.requestAccess(for: .audio)`;拒/受限抛 `microphoneAccessDenied` -- `makeSnapshotStream() -> AsyncStream` -- Sink 安装按 generation 追踪,避免前一 sink 延迟终止处理器卸载新 sink -- **无输入设备选择器**(`AVAudioEngine.inputNode` 用系统默认;iOS `AudioBufferSnapshot+AVFoundation.swift` 在 `project.yml:568` 排除) - -**`MacHallucinationFilter.swift`**: -- 剥 `` 脚手架、`language XX` 前缀、元数据噪声行(`language/emotion/event` 等) -- `silencePeakThreshold = 0.0005`(RMS 低于此跳喂 MLX stream) -- `shouldDiscardHotwordDump`(音频能量低 + 转写主要是词典热词 → 丢假触发) - -### 2.5 MLX / 本地 ASR 流水线 - -**关键文件**: -- `MacLocalASRService.swift` —— Mac 端公共面:`selectedModelDefinition()` / `isModelInstalled(_:)` / `usesMLXLiveStreaming()` / `transcribe(samples:locale:bias:)`。MLX 后端模型走 `MacMLXStreamingASRProvider`;`.appleSpeech` 后端或未装模型时落 `MacSpeechLocalASR`。Legacy Sherpa 标识符(`sherpaQwen3/sherpaSenseVoice/sherpaParaformer`)只接受以便抛 `qwen3ModelMissing` —— **明确迁移期处理** -- `MacMLXStreamingASRProvider.swift` —— `actor`,拥有单 `Qwen3ASRModel` 缓存(`cachedModelId / cachedModel / didWarmup`)。`loadModel` 从 `LocalASRModelInstallState.modelRootURL(definition)` 读并 `Qwen3ASRModel.fromModelDirectory(root)`。`warmupIfNeeded` 用 1s 静音预热 Metal kernel。`makeSession` 构建 `StreamingConfig`(0.5s decode interval、0.2s boundary、1s boundary boost、encoder window overlap 1s、max cached windows 8、`delayPreset: .realtime`、language hint 从 `MacQwen3LanguageHint.from(locale:)`、`context: bias?.promptBias`、`temperature: 0`、512 max tokens/pass、2/2 min-agreement passes、`finalizeCompletedWindows: true`) -- `MacMLXStreamingSession.swift` —— 包装 `mlx-audio-swift` 的 `StreamingInferenceSession`。订阅 `AsyncStream`(`displayUpdate/ended/provisional/confirmed/stats`);`MacHallucinationFilter` 过滤 `display/ended`;`onDisplayUpdate` 转发 `display`;`feed(samples:)` 通过 `FlowCaptureDrainTracker.rms(of:)` 跳过低于 `silencePeakThreshold` 的数据;`stop()` 通过 `CheckedContinuation` 返回最终 `String`;`peakAudioRMS()` 暴露 hotword-dump 守卫用最大 RMS -- `MacMLXLiveCapture.swift` —— Option 持有本地 ASR 编排:建 streaming session,`TaskGroup` 内 2 个并发任务:(1) 等首 `finishSignal`(`draining.withLock { $0 = true }` → `await FlowUtteranceEndCoordinator.awaitTailCapture(policy: .macMLX)`)——**显式 `break` 单信号**,永不结束的 stream 不会挂住 group;(2) 消费 `AudioBufferSnapshot` stream,100ms chunk(1600 samples @16kHz)喂 MLX,draining 时 `drainTracker.noteAudio`。然后喂余量,调 `session.stop()`,跑 `MacHallucinationFilter.shouldDiscardHotwordDump` 丢热词-only 转写 -- `MacSpeechLocalASR.swift` —— Apple Speech 兜底。16kHz PCM 写临时 WAV(`PCMSampleWavEncoder`),`SFSpeechURLRecognitionRequest` 强制 `requiresOnDeviceRecognition`(locale 无 on-device 模型时立即失败 → 清晰错误"去 系统设置 → 键盘 → 听写 下载")。Chinese locale 应用 `CustomLanguageModelManager.applyCustomLanguageModel(to:locale:bias:)`(与 iOS 共用)。2× 音频长硬超时 + 30s floor,`RecognitionSession` 锁保护单次 resume,识别回调和超时 task 正确竞速 -- `MacCloudASRChunkAdapter.swift` —— 薄 `ASRChunkTranscribing` 适配器,包装 `CloudASRClientFactory.make(store:)` 给 `MacDictationPipeline` 用 - -**模型目录**(`OSGKeyboard/Resources/LocalASR/local-asr-catalog.json`,2.0.1 状态): -- `schemaVersion: 1` -- `defaultModelId: "qwen3-mlx-0.6b-4bit"` -- `runtimes: []`(**空**——runtime 随 bundle 走,MLX 内置) -- 2 个模型: - 1. **`qwen3-mlx-0.6b-4bit`** —— Qwen3-ASR 0.6B,`backend: mlx`,`runtimePlatform: macos`,`sizeBytes: 730000000`(~0.7 GB),locale `zh-CN/en-US`,支持热词(`promptOnly`),`badgeKey: mac.localASR.badge.balanced`,路径 `models/qwen3-mlx-0.6b-4bit`。两个下载源(`hfmirror`/`huggingface`)都在 `mlx-community/Qwen3-ASR-0.6B-4bit`。文件:`config.json` / `generation_config.json` / `preprocessor_config.json` / `model.safetensors`(~708MB) / `model.safetensors.index.json` / `tokenizer_config.json` / `merges.txt` / `vocab.json` - 2. **`qwen3-mlx-1.7b-4bit`** —— Qwen3-ASR 1.7B,同上形状,`sizeBytes: 1700000000`(~1.7GB),`badgeKey: mac.localASR.badge.quality`。源在 `mlx-community/Qwen3-ASR-1.7B-4bit` - -**`local-asr-catalog.json` 中零 iOS 模型**。iOS app 运行时**不**用本目录——iOS 用 iOS 26 `SpeechAnalyzer + DictationTranscriber`,代码路径不同(`OSGKeyboardHostSupport/Services/ASRService.swift`,Mac 排除)。 - -**模型生命周期**: -- `loadModel(_:)` (`MacMLXStreamingASRProvider` 第 69-81 行) 缓存 loaded `Qwen3ASRModel` per model id。切模型失效缓存并 re-warm。`warmupIfNeeded` 每个冷模型跑一次 -- 卸载(`MacLocalASRModelSettingsViewModel.deleteModel`)→ 共享 `LocalASRModelManager.deleteModel(_:catalog:)` 删磁盘权重;actor 缓存**不**自动清——下次不同 id 的 `loadModel` 自然替换 - -**Mac 不支持 iOS 风格的"on-device + cloud hybrid"**——只有 engine-mode 开关(`local` → MLX 或 Apple Speech;`cloud` → 已配云 ASR)。两种模式共用同一 LLM 润色步。 - -### 2.6 LLM 润色 + 供应商集成 - -- `MacDictationPipeline.swift:254` —— 跑的是**同一份** `PolishingService(store: store).polishWithOutcome(...)`(iOS `OSGKeyboardShared/Services/PolishingService.swift`) -- `PolishContext` 用 `appContext`(从目标 App bundle id 通过 `MacAppContextService` 抓)+ `dictionarySupplement`(来自 local-bias `polishFragment`),**和 iOS 同款** -- `MacSettingsView.swift:111` (`polishProviderSection`) 用 `viewModel.polishSelectableProviders`(`LLMProvider.userSelectablePresets`——**完整集,与 iOS 相同**)。行:provider / API key / baseURL / model(`MacProviderModelRow` "拉模型列表" 动作) / thinking 切换 / `MacProviderToolsRow` "Test connection" / 翻译目标 -- `MacSettingsView.swift:173` (`asrProviderSection`) 用 `viewModel.asrSelectableProviders`(`LLMProvider.asrSelectablePresets`)。Volcengine 有自定义 auth-mode(API key vs App ID + Access Token)通过 `VolcengineASRFields.parse` / `updateMacVolcengine(...)`;其他供应商走通用 `baseURL/apiKey/model` 行 + "Test connection" -- **Mac 没有 Mac 专用 polish/ASR 供应商集**——供应商面和 iOS 一致(DeepSeek/OpenAI/Anthropic 兼容 LLM;Volcengine/OpenAI Realtime/Bailian/Alibaba 云 ASR) -- **MLX 供应商仅用于本地 ASR**——**无** Mac 端 MLX LLM 润色 - -### 2.7 持久化 - -| 类别 | 实现 | Key 前缀 / 位置 | -|---|---|---| -| UserDefaults | 标准 | `mac.*`(`MacHotkeyService:41` / `MacAppearance:56` / `MacDictationViewModel:103-105` / `MacDictationOverlayController:43-45`)—— 热键、auto-paste、appearance、onboarding、overlay 位置、MLX 模型选择、下载源、界面语言 | -| App Group | 共享 `AppGroupStore` | 与 iOS 同:`PersonalDictionary` / `PolishStyleCatalog+active id` / `translationTarget` / `detected appContext` / `speechHistory` / `usageStatistics` / `engineMode` / `localeId` / LLM API key & baseURL / 云 ASR provider id/apiKey/model/baseURL / iCloud-sync-enabled | -| Keychain | 共享 `Keychain` | `keychain-access-groups: [$(AppIdentifierPrefix)com.osgkeyboard.shared]`(entitlement)—— 存 LLM API key | -| iCloud KVS | 5 路全部接好 | `MacICloudSyncBootstrap.swift` 接 `AppCloudSync(makeStore:historyDefaults:)`,暴露 `settingsSync/dictionarySync/polishStyleSync/appCloudSync`,**entitlement 用 `$(TeamIdentifierPrefix)com.osgkeyboard.ios` 和 iOS 同桶** | -| 语音历史 | `SpeechHistoryStore.shared` + `SpeechHistoryStorage` | UserDefaults 后端 + iCloud `SpeechHistoryCloudSync` | -| 模型权重 | `LocalASRModelInstallState.rootDirectory()` | 由共享 `LocalASRModelManager` 管 | -| 临时文件 | `FileManager.default.temporaryDirectory` | Apple Speech WAV(`osg-mac-asr-.wav`,`defer` 删,`MacSpeechLocalASR:71`) | - -### 2.8 系统集成 - -- ✅ 菜单栏 status item + popover -- ✅ 全局 ⌥ 热键 + 合成 ⌘V -- ✅ URL scheme `osgkeyboard`(仅 DEBUG `osgkeyboard://seed-demo`) -- ❌ Login item / launchd(**无** `SMLoginItemSetEnabled`) -- ❌ 通知(`UNUserNotificationCenter`)—— HUD 本身就是唯一直达面 -- ❌ Touch Bar -- ❌ Services / Share extensions -- ❌ Dock badge -- ✅ `LSApplicationCategoryType: public.app-category.utilities` -- ✅ `ENABLE_HARDENED_RUNTIME: YES`(Developer ID + 公证必需) -- ❌ **沙盒关闭** `com.apple.security.app-sandbox: false`(`project.yml:628`)—— Accessibility + 全局事件 tap + ⌘V 注入都禁在沙盒里 -- 发行渠道:**Developer ID + 公证**,**不进** Mac App Store / TestFlight -- `applicationShouldTerminateAfterLastWindowClosed → false`(菜单栏项保活) -- `LSUIElement: false`(**不**设——启动时仍显 Dock 图标) - -### 2.9 macOS 独占(iOS 没有)的能力 - -1. **`NSStatusItem + NSPopover` 菜单栏 UI** —— SwiftUI `MenuBarExtra` 故意不用,AppKit 为源 -2. **全局 ⌥ 持有热键**(`NSEvent.add*MonitorForEvents(matching: .flagsChanged)`)区分左/右/任意 Option -3. **Accessibility 门控 ⌘V 注入**(`CGEvent + cghidEventTap + AXIsProcessTrusted`) -4. **前台 App 跟踪**(`FrontmostAppTracker`)让 popover 激活 OSGKeyboard 时不丢粘贴目标 -5. **MLX Audio Qwen3 流式 ASR**(Qwen3-ASR 0.6B/1.7B 4-bit)通过 `mlx-audio-swift` SPM -6. **系统原生 macOS light/dark 调色板**(暖白 light + stepped gray6→4 dark),从 AppKit semantic colors 解析,让 window chrome 跟随 SwiftUI 颜色方案 -7. **浮动 non-activating `NSPanel` HUD**(`floatingWindow + 1`),user-draggable 位置持久化 -8. **Bundle-ID → `AppContext` 映射**(`MacAppContextService.swift`)—— iOS 走 `AppContextDetector` 启发式(键盘读不到前台 bundle id) -9. **手动 MLX 模型下载/暂停/恢复**,per-file 字节进度 -10. **"Open Storage"** reveal-in-Finder MLX 模型根 -11. **Volcengine 云 ASR auth-mode 切换**(API key vs App ID + Access Token) -12. **同 `URL scheme: osgkeyboard`** 加 `osgkeyboard://seed-demo` DEBUG seed -13. **Apple Speech on-device 兜底**(仅当未装 MLX 模型或 backend == `.appleSpeech`) -14. **`NSApplicationDelegate`-driven AppKit 生命周期**:`applicationShouldTerminateAfterLastWindowClosed → false`、status-item 创建、overlay controller 启动、热键接续 -15. **`MacMLXStreamingASRProvider` actor + 模型缓存 + warmup** 保单 `Qwen3ASRModel` 驻留 -16. **两列 `MacInlinePicker`(供应商行)** vs iOS `.menu` `Picker` -17. **`MacProviderSettingRow` 显式 200pt label 列** 防长凭证输入挤 - -### 2.10 macOS 测试覆盖(`OSGKeyboardMacTests/`,3 文件,挂在 Mac App bundle 内) - -`project.yml:513-533` 覆盖 `@testable import OSGKeyboard`(host target 名 `OSGKeyboardMac` 但装包为 `OSGKeyboard.app`,所以 override `TEST_HOST`)。 - -1. **`MacDictationViewModelTests.swift`** —— `testCancellingButtonPreparationKeepsGateClosedUntilStartUnwinds`:替换 recorder 为 `SuspendedMacAudioRecorder`(`start()` 停 `CheckedContinuation`),验证第二次 `toggleRecording()`(取消)不重开 `isPreparingToRecord` 门 -2. **`MacAudioRecorderSnapshotStreamTests.swift`** —— `testReplacingSnapshotStreamDoesNotDeadlock` + `testReplacingSnapshotStreamKeepsTheNewSinkAttached`:回归守卫,防 Option 松开时 lock 重入冻结(`AsyncStream.Continuation.finish()` 在调用线程同步跑 `onTermination`,handler 重新拿 installer 持有的同 `NSLock` 死锁主线程)。第一个测试在 global queue 装 + 2s semaphore 超时,让回归失败而非挂住套件;第二个测读 `MacAudioRecorder.hasLiveSnapshotSink`(DEBUG seam)确认新 stream 顶替前辈 -3. **`MacTextInsertionServiceTests.swift`** —— `testRestoreRequiresTranscriptToStillOwnPasteboard`(pasteboard change-count 守卫)/ `testRestoringOriginallyEmptyPasteboardClearsTranscript` / `testCapturedBundleIdentifierDrivesPolishContext`(Xcode/WeChat/OSGKeyboard 自身经 `MacAppContextService.detectContext(bundleIdentifier:)`) - ---- - -## 3. macOS 落后 iOS 的功能(按缺口大小排序) - -### 3.1 完全缺失(共享代码已编进 Mac 但零调用) - -| 缺失功能 | iOS 路径 | 共享代码状态 | 影响 | -|---|---|---|---| -| **剪贴板历史 + AI 技能** | `OSGKeyboard/Views/ClipboardSettingsView`、`OSGKeyboardExt/Views/ClipboardKeyboardViews` | `ClipboardHistoryStore` / `ClipboardSemanticAnalyzer` / `ClipboardSkillSemanticRanker` / `AIClipboardSkill` / `AIClipboardSkillLayoutStore` / `AIClipboardPrompt` / `ClipboardHistoryPolicy` **全部编进 Mac 二进制,无任何 Mac 文件引用** | Mac 用户能说话能润色,但**没法**让"复制即智能" | -| **AI 提示轮播 / 上下文技能** | `OSGKeyboard/Services/AIHintRefreshService`、`OSGKeyboardExt/Views/AIKeyboardView` 内的 hint carousel | `AIHintStore` / `AIHintPool` / `AIHintKeywordExtractor` / `AIHintKeywordCompressor` / `AIHintLocalCatalog` / `AIHintModels` 全部已编,**无 Mac 调用** | Mac 没有 idle 屏的"想一句"灵感卡 | -| **OSG 账户(Sign in with Apple + 积分 + 推荐)** | `OSGKeyboard/Views/Account/AccountCenterView`、`AccountPurchaseHistoryView` | `OSGKeyboardHostSupport/Features/Account/` 全部编进 Mac(`AccountAPIClient` / `SignInWithApple` / `DeviceIntegrity` / `HostPrivateAccountKeychain` / `AccountModels` / `OOBEGrantProvisioningCoordinator`),**无 Mac UI 也无调用**,且 Mac entitlement 无 `com.apple.developer.applesignin` | Mac 只能用 tip 打赏;OSG 积分体系是 iOS 独占 | -| **App Attest** | `OSGKeyboardHostSupport/Features/Account/DeviceIntegrity.swift`(`SystemAppAttestProvider`)| 已编,但 Mac 无对应 entitlement(`com.apple.developer.devicecheck.appattest-environment`)也无调用 | Mac 不在 attestation 流程里 | -| **一方分析(Analytics)** | `OSGKeyboard/Services/AnalyticsHostService`、`OSGKeyboardExt/Services/AnalyticsExtensionService` | `OSGKeyboardShared/Features/Analytics/` 12+ 文件全部编进 Mac,**零 Mac 调用** | Mac 用户行为完全无遥测;产品迭代失去数据源 | -| **助手指令(Shortcuts)** | `OSGKeyboard/Services/AIAgentShortcutInstaller`、`AIAgentShortcutRunner`、3 个 bundled `.shortcut`(`OSGExtractTodos/OSGExtractEvents/OSGSaveToNotes`) | 共享 `AIAgentSkill*` / `AIShortcutShareLink` / `AIGenericSkillExport` / `AINoteExport` / `AIMapNavigation` / `AIPhoneNumberActions` 已编,无 Mac 调用;bundled `.shortcut` **Mac 不装**(`project.yml:97-105` 仅 iOS 装) | Mac 没法"说一句把当前文本加到 Notes / 提取 Todo" | -| **CLM 用户管理 UI** | iOS 后台用 `CustomLanguageModelManager` | Mac **用**了 CLM(`OSGKeyboardMacApp.swift:117` 调 `prepareInBackgroundIfNeeded`;`MacSpeechLocalASR:67-98` 调 `applyCustomLanguageModel`),**但**无 `MacCLMSettingsView` | Mac 用户不知道 CLM 在跑;没法改 phrase 库 | -| **最后输入编辑(Edit hint / Last-input edit)** | `OSGKeyboardExt/Views/LastInputEditView`、`EditHintScheduler`、`LastInputEditCoordinator` | 已编,无 Mac 调用 | Mac 没法长按"改刚才那句" | -| **Flow 跨进程会话** | `OSGKeyboard/Services/FlowSessionManager`、`FlowPictureInPictureController`、`FlowTerminationCoordinator`、`FlowAnalyticsOperationRegistry`、`FlowASRPostProcessor`、`FlowDiagnostics`、`OSGKeyboardExt/Services/KeyboardFlowCoordinator`、`OSGKeyboardShared/Services/FlowSessionBridge*` | 已编,Mac 仅借用 `FlowSessionKeys.cloudASRWaitTimeout` 一个常量(`MacDictationViewModel.swift:498`) | **架构性差异**——Mac 单进程、单用户、单次录制;不需要也不该硬塞 | -| **自定义键盘(Rime/拼音/英文)** | 整个 `OSGKeyboardExt/Typing/` + `OSGKeyboardShared/Typing/**` + `LibrimeEngine` + `RimeResourceInstaller` + `EnglishSuggestionEngine` + `Pinyin*` + `Rime*` | **Mac target 完全排除** `Typing/**`(`project.yml:585`) | **架构性差异**——Mac 不是键盘扩展;librime 是 iOS C/Objective-C++ 框架,Mac 结构性不兼容 | -| **后台任务** | iOS `UIBackgroundModes: audio` + `BGTaskSchedulerPermittedIdentifiers: [com.osgkeyboard.ios.analytics-sync]` | Mac Info.plist **无** `BG*` keys | Mac app 不能后台运行;iOS 的 analytics 同步、Flow PiP 都没 Mac 对应 | -| **Settings 子页面** | `TypingInputSettingsView` / `AIAgentSkillsView` / `Account/*` / `HelpFeedbackView` / `ReleaseNotesSheet` / `AppGroupErrorView` / `NewContactSheet` / `KeyboardPreviewSheet` / 各种 DEBUG demo | 无 Mac 对应 | 见上表 | -| **Universal Links 启动 handoff** | iOS `applinks:osglab.com` + `AppURLHandler` 保留 `sourceApplication` | Mac entitlement 共享同 `com.apple.developer.associated-domains` 但**无** Mac-side 解析 | Mac 不能从网页/邮件 deep-link 进 Onboarding 或 Skill | - -### 3.2 部分缺失 - -| 缺失 | iOS | macOS | 差异点 | -|---|---|---|---| -| **Onboarding 深度** | 7 步 + OOBE 4-feature 实践(用 `OOBEClientInfrastructure` 拿服务器体验金) | 6 步(welcome / mic / accessibility / engine / cloud\|local) | 无 Sign in with Apple、无 OOBE 实践 | -| **设置覆盖面** | 7 个子面板 + Account + AI Agent + Clipboard + Typing Input | 5 个段落(support / general / recognition / ASR / local ASR / polish / input / legal) | iOS 的 AI Agent / Clipboard / Typing 在 Mac 没对应;Mac 的 input 段含热键 + Accessibility 状态(iOS 没有) | -| **AI Hint / Shortcut 链接** | 提示页 + 3 bundled `.shortcut` | 仅 0(Mac 装包不含 `.shortcut`) | 整套 AI mode 体验 Mac 无 | -| **Provider 切换粒度** | iOS 通过 iOS `EnginePickerSection` + `ProviderPickerSection` 在 Settings 内 + 主页快捷 | Mac 整段 `recognitionSection` + `asrProviderSection` + `polishProviderSection` 拆开 | iOS 较紧,Mac 较松 | - -### 3.3 Mac 自身落后 iOS 之处("应该补"清单) - -按"代码复用难度 + 用户价值"排序: - -| 优先级 | 功能 | 现状 | 工作量 | 建议 | -|---|---|---|---|---| -| **P0** | **剪贴板历史 + AI 技能面板(Mac UI)** | 共享代码全到位,零 Mac UI | **小**——`MacClipboardHistoryView` + `MacAIClipboardSkillView`(仿 `MacHistoryView`)+ 改 `MacSettingsView` 加一个 Clipboard 段 | 立刻做。代码复用 100%,价值高 | -| **P0** | **CLM 设置页(Mac UI)** | 后台在跑(`OSGKeyboardMacApp:117`),无 UI | **小**——`MacCLMSettingsView`,仿 `MacLocalASRModelSettingsView` 模板 | 立刻做。让用户能编辑 phrase 库 | -| **P1** | **OSG 账户 + Sign in with Apple + 积分购买** | 共享代码编进 Mac,零 UI;Mac entitlement 缺 `com.apple.developer.applesignin` + 无 `SKIncludeConsumableInAppPurchaseHistory` | **中**——加 entitlement + 改 `MacICloudSyncBootstrap` 已涵盖大部分,加 `MacAccountCenterView`(仿 `AccountCenterView` 但精简)+ 改 Settings | 一周内可交付 | -| **P1** | **OSG credits 消耗型 IAP(500tks/1500tks/3000tks)** | 共享 `TipPurchaseManager` 已装;iOS 走 `AccountCreditPurchaseManager` | **中**——`MacAccountCenterView` 出来后一气呵成 | 同上 | -| **P2** | **一方分析(Mac)** | 共享 `AnalyticsClient` 等全编,零调用 | **中-大**——`MacAnalyticsHostService` + 隐私 / opt-in + 后台任务(Mac 上走 `NSProcessInfo.thermalState` 节流 + 用户同意时 `NSTask` 跑)| 半年窗口内 | -| **P2** | **AI 提示轮播 / 上下文技能(Mac)** | 共享代码全到位 | **中**——加 `MacAIHintPool` 屏 + `MacAIQuestionService` 入口 + Shortcuts bundle 装载 | 与 P0 剪贴板可联动 | -| **P3** | **助手指令集成** | 共享代码 + iOS bundled `.shortcut` | **中**——Mac 装 `.shortcut` + `AIAgentShortcutInstaller` 调起(`shortcuts://` URL 仍可用)| 与 P2 AI 提示联动 | -| **P3** | **最后输入编辑** | 共享代码全到位 | **大**——需要 Mac 端保留"刚才插入的引用"(`EditableInputReference`),但 Mac 走 Accessibility ⌘V 注入,**没有 `EditableInputReference` 的来源**——需要新协议 | 架构问题,先做 PoC | -| **P3** | **后台同步(Analytics 任务)** | 无 | **中**——`NSTask`/`SMAppService` LaunchAgent 拉 analytics;与 P2 联动 | | - -### 3.4 Mac 形态上"不应该补"的功能 - -| 功能 | 原因 | -|---|---| -| **Flow 跨进程会话** | Mac 单进程、单用户、单次录制——Flow 设计为键盘扩展 ↔ 宿主 App 的两进程 mailbox。强行移植会引入不必要的 IPC 开销。**Mac 用 `MacDictationPipeline` + `MacDictationViewModel` + `MacDictationOverlayController` 三件套已足够** | -| **PiP keep-alive** | macOS 无系统 PiP。`MacDictationOverlayController` 的 NSPanel 已占位 | -| **Rime / 拼音 / 英文 autocorrect** | Mac 不是键盘扩展,没输入面。`LibrimeEngine` 是 iOS C/Objective-C++ xcframework,**Mac 结构性不兼容**。即便 Mac 装键盘扩展(`Designed for iPad` 那种),librime 也需要重编译 | -| **`UIBackgroundModes: audio`** | Mac 没用——按住说话时 NSPanel 是 non-activating,不需要 audio session 后台保持 | -| **`UIOpenURLContext.options.sourceApplication`** | iOS 26 only API;Mac 走 `NSWorkspace.didActivateApplicationNotification` 已解决前台 App 跟踪 | -| **`AVAudioApplication.requestRecordPermission`** | iOS 17+ only;Mac 走 `AVCaptureDevice.requestAccess(for: .audio)` | -| **App Group 跨进程 mailbox** | Mac 单进程不需要。但 `AppGroup` + iCloud KVS 是用户态多设备同步用的,**保留** | -| **iOS 风格的 idle 屏** | Mac 主窗口即 dashboard,不需要 idle 屏的"AI hint 轮播";但 `MacAIHintPool` 屏可以放在 Dashboard 顶部,与 P2 联动 | -| **`LSApplicationQueriesSchemes`** | iOS only,Mac 无 `canOpenURL` 限制 | -| **`SKIncludeConsumableInAppPurchaseHistory` / Mac storekit 文件** | Mac App Store 与 iOS App Store 独立 SKU;目前 Mac 装包是 Developer ID 渠道,**不进 Mac App Store**,所以 storekit 不需要 | - ---- - -## 4. 适合在 Mac 上做开发和移植的功能 - -### 4.1 应该现在做的(P0) - -#### ① 剪贴板历史 + AI 技能面板 - -**为什么 Mac 适合**:共享代码 100% 到位(`ClipboardHistoryStore` / `ClipboardSemanticAnalyzer` / `ClipboardSkillSemanticRanker` / `AIClipboardSkill` / `AIClipboardPrompt`),iOS 8 个 UI 调试稳定,只需套 Mac 风格。 - -**具体工作**: -- 新增 `OSGKeyboardMac/Views/MacClipboardHistoryView.swift`(仿 `MacHistoryView`) -- 新增 `OSGKeyboardMac/Views/MacAIClipboardSkillView.swift`(仿 `AIAgentSkillsView`,精简为 Read/Edit/Disable) -- `MacSettingsView.swift` 加 `clipboardSection`(含 toggle 启用历史 / toggle 启用技能候选条 / "Manage Skills" 链接 / "Open History" 链接) -- `MacDictationViewModel` 订阅 `ClipboardHistoryStore.entries`(`@Published var clipboardEntries`) -- 复用 `ClipboardCaptureCoordinator` 不可(ext-only)——需要 Mac 端 `MacClipboardMonitor`(`NSPasteboard.general.changeCount` 轮询 + sanitize) - -**测试**:`MacClipboardMonitorTests`(用 in-memory pasteboard 模拟)/ `MacClipboardHistoryViewModelTests` - -**工作量**:1-2 周 - -#### ② CLM 设置页 - -**为什么 Mac 适合**:Mac 后台已经在跑 CLM(`OSGKeyboardMacApp:117`),只是用户管不到。 - -**具体工作**: -- 新增 `OSGKeyboardMac/Views/MacCLMSettingsView.swift`(仿 `MacLocalASRModelSettingsView`) -- 复用 `CustomLanguageModelManager.shared.state`(`@Published idle/preparing/ready/failed`) -- 复用 `OSGKeyboard/Resources/HostCLM/v1/OSGKeyboardCLM.bin`(已在 Mac 装包) -- 复用 `PersonalDictionary` 共享存储(用户输入的 phrase 库 + iCloud 同步) -- 提供 phrase 列表 / 编辑 / 触发"重编译"按钮 - -**测试**:`MacCLMSettingsViewModelTests`(验 `state` 状态机) - -**工作量**:1 周 - -### 4.2 应该中期做的(P1) - -#### ③ OSG 账户 + Sign in with Apple + 积分购买 - -**为什么 Mac 适合**:共享代码编进 Mac 都没报错,缺的是 entitlement + UI。 - -**先决条件**: -- `OSGKeyboardMac.entitlements` 加 `com.apple.developer.applesignin: [Default]` -- `OSGKeyboardMac/Info.plist` 加 `SKIncludeConsumableInAppPurchaseHistory: true` -- (选做)把 `OSGKeyboard.storekit` 内容 mirror 到 `OSGKeyboardMac.storekit` 或共享 - -**具体工作**: -- 加 `MacAccountCenterView`(仿 iOS `AccountCenterView` 但精简) -- 复用 `LiveAccountServices` / `AccountSessionCoordinator`(`@MainActor` state machine) -- 复用 `AccountCreditPurchaseManager`(StoreKit 2 product list → 服务器校验) -- Settings → 加 "Account" 段,含 sign-in / 积分余额 / 购买历史 / 登出 / 删号 - -**测试**:`MacAccountSessionCoordinatorTests`(in-memory `AccountAPIClient` fake) - -**工作量**:2-3 周 - -#### ④ Mac App Attest / DeviceCheck - -**为什么 Mac 适合**:iOS attestation 后端已经在用,Mac 端可以通过 `DCAppAttestService`(macOS 13+,需要 Mac Catalyst 但当前 `SUPPORTS_MACCATALYST: NO`)或者改用 DeviceCheck `DCDevice.generateToken`(macOS 13+ available without Catalyst)。 - -**先决条件**: -- `OSGKeyboardMac.entitlements` 加 `com.apple.developer.devicecheck.appattest-environment: $(APP_ATTEST_ENVIRONMENT)` -- 若走 App Attest,需要 `SUPPORTS_MACCATALYST: YES`(改大改)或改 DeviceCheck-only - -**具体工作**: -- `MacDeviceIntegrityCoordinator`(仿 iOS,但用 `DCDevice`) -- `AccountAPIClient` 复用,调 `/v1/integrity/attest` - -**工作量**:1-2 周(仅 Mac);+ Catalyst 决策时间 - -### 4.3 应该长期做的(P2) - -#### ⑤ 一方分析(Mac) - -**为什么 Mac 适合**:Mac 用户行为和 iOS 同样有价值(知道用户用不用 MLX 模式、热键触发频率、词条覆盖、润色样式流行度)。 - -**先决条件**: -- 共享 `AnalyticsClient` / `AnalyticsRepository` 已经完备 -- Mac 上加 `MacAnalyticsHostService`(仿 iOS):监听 `viewModel.$isRecording/$isPreparingToRecord/$isProcessing` 推送事件 -- 隐私 / opt-in:在 onboarding 末加一步 - -**挑战**: -- Mac 后台无 `BGTaskScheduler`——改用 `NSProcessInfo.thermalState` + `ProcessInfo.isLowPowerModeEnabled` 节流,**或** `SMAppService` 拉个 LaunchAgent -- `AnalyticsUploadCoordinator` 的移动策略(threshold 20 / 60s flush)要 Mac 化 - -**工作量**:3-4 周 - -#### ⑥ AI 提示轮播 / 上下文技能 - -**为什么 Mac 适合**:Mac dashboard 缺内容——除了使用统计,闲置时可推"想一句"灵感卡。 - -**具体工作**: -- `MacAIHintPool` 嵌在 `DashboardView` 顶部(仿 iOS `AIKeyboardView` 的 carousel 区域) -- 复用 `AIHintStore` / `AIHintPool` / `AIHintKeywordExtractor` / `AIHintLocalCatalog` -- 上下文技能入口放 `MacContentView`(popover 内 record 按钮旁的下拉) - -**工作量**:2-3 周 - -#### ⑦ 助手指令集成 - -**为什么 Mac 适合**:Mac 上有 Shortcuts.app(更成熟),bundled `.shortcut` 装入 Mac App bundle 即可。 - -**具体工作**: -- 装 `OSGKeyboard/Resources/Shortcuts/*.shortcut` 进 Mac bundle -- 复用 `AIAgentShortcutInstaller` / `AIAgentShortcutRun` -- popover / dashboard 加技能快捷入口 - -**工作量**:1-2 周 - -### 4.4 不应该在 Mac 做的(但代码可以清理) - -| 共享代码 | 为什么不该在 Mac 跑 | 处置 | -|---|---|---| -| `OSGKeyboardHostSupport/Features/Account/*`(Mac 已编) | App Attest 路径 iOS 专属;Mac 没 entitlement | 等 ④ 决策后从 Mac target 排除或接 UI | -| `OSGKeyboardShared/Features/Analytics/*`(Mac 已编) | 无 Mac 端调用 | 等 ⑤ 决策后从 Mac target 排除或接服务 | -| `OSGKeyboardShared/Features/ManagedGateway/*`(Mac 已编) | 是账户 grant 体系,与 ③ 绑定 | 等 ③ 决策后处理 | -| `OSGKeyboardShared/Services/AIClipboard*`(Mac 已编) | 与 ① 绑定 | ① 实现后从"无调用"变"有调用" | -| `OSGKeyboardShared/Services/AIHint*`(Mac 已编) | 与 ⑥ 绑定 | 同上 | -| `OSGKeyboardShared/Services/AIUserSkill*` / `AIAgentSkill*`(Mac 已编) | 与 ③⑦ 绑定 | 同上 | -| `OSGKeyboardShared/Services/Flow*`(Mac 已编) | Mac 架构不兼容 | **立刻**从 Mac target 排除(白付编译器开销) | -| `OSGKeyboardShared/Services/KeyboardState.swift`(Mac 已排除)| 已经是 Mac 排除 ✓ | 无 | -| `OSGKeyboardShared/Services/EditTransactionStore.swift` 等 | 与"最后输入编辑"绑定 | 见 P3 | - -### 4.5 立即可清的"白编译"清单 - -``` -OSGKeyboardShared/Services/FlowSessionBridge.swift + 所有 +* 文件 -OSGKeyboardShared/Services/FlowSessionKeys.swift -OSGKeyboardShared/Services/FlowSessionDarwin.swift -OSGKeyboardShared/Services/FlowSessionPolicy.swift -OSGKeyboardShared/Services/FlowStartTransactionPolicy.swift -OSGKeyboardShared/Services/FlowKeyboardPolicies.swift -OSGKeyboardShared/Services/FlowHandoffPolicy.swift -OSGKeyboardShared/Services/UtteranceStreamChunker.swift -OSGKeyboardShared/Services/UtteranceBatchFallbackPolicy.swift -OSGKeyboardShared/Services/UtteranceTranscriptGuard.swift -OSGKeyboardShared/Services/UtteranceTranscriptStitcher.swift -OSGKeyboardShared/Services/TranscriptOverlapUtilities.swift -OSGKeyboardShared/Services/ProgressiveDictationTranscriptAccumulator.swift # 已排除 -OSGKeyboardShared/Services/EditTransactionStore.swift -OSGKeyboardShared/Services/EditUsageMetricsStore.swift -OSGKeyboardShared/Services/EditLastInputPromptComposer.swift -OSGKeyboardShared/Services/EditOutputValidator.swift -OSGKeyboardShared/Models/Flow*.swift (全部) -OSGKeyboardShared/Models/EditableInputReference.swift -OSGKeyboardShared/Models/EditSessionState.swift -OSGKeyboardShared/Models/FlowUtterance*.swift -OSGKeyboardShared/Models/FlowInactivityDuration.swift -OSGKeyboardShared/Utilities/FlowCaptureTailDrain.swift -OSGKeyboardShared/Utilities/FlowUtteranceEndCoordinator.swift -OSGKeyboardShared/Utilities/FlowUtterancePCMStore.swift -OSGKeyboardShared/Utilities/FlowTrace.swift -OSGKeyboardShared/Utilities/FlowPipelineDiagnostics.swift -OSGKeyboardShared/Utilities/HostMemoryBudget.swift -OSGKeyboardShared/Utilities/KeyboardExtensionMemoryTelemetry.swift # 已排除 -OSGKeyboardShared/Features/Analytics/ # 全部 12+ 文件 -OSGKeyboardShared/Features/ManagedGateway/ # 全部 8+ 文件 -OSGKeyboardHostSupport/Features/Account/ # 全部 -OSGKeyboardHostSupport/Services/CloudASR/AlibabaVocabularySync.swift # iOS-only API -OSGKeyboardHostSupport/Services/ASRChunkTranscribing.swift # 需检查 -``` - -(**建议**):在 `project.yml:558-602` 给 Mac target 的 source list 加 `excludes:`,避免编译这些文件后被链接器裁掉造成的 dead-code 体积。 - ---- - -## 5. iOS 端需要"修一下"或注意的地方 - -报告主体是 macOS 差距,但 iOS 端顺手列出 5 个明显可改进点(不修不影响功能,但提升质量): - -1. **`OSGKeyboardHostSupport/Features/Account/*` 整块**虽然不通过 Mac UI 暴露,但通过 source include 编进 Mac 二进制——长期应从 Mac target 排除或加 `MacAccountServices` 实际使用 -2. **`OSGKeyboardShared/Features/Analytics/*`** 同上——Mac 编译进二进制但无任何调用 -3. **`OSGKeyboardShared/Features/ManagedGateway/*`** 同上 -4. **`project.yml:570-602`** 的 Mac target source list 很长且依赖手写排除,**建议**拆出 `OSGKeyboardMacExcludes.yml` 维护 -5. **`OSGKeyboardShared/Typing/EnglishSystemLexicon.swift:11`** 是**唯一**有 `import UIKit` 的 Shared 文件——是 iOS ext 独享的 QuickType 数据源。考虑改名/移动到 `OSGKeyboardExt/` 减面 -6. **iOS `FlowSessionManager` ~3500 LOC** 是单一 god object;建议把 `FlowPictureInPictureController` / `FlowTerminationCoordinator` / `FlowAnalyticsOperationRegistry` 三个生命周期拥有者抽离 -7. **iOS `appDelegate` 反弹 `.onOpenURL`** (`AppURLHandler.swift`) 注释里写 `UIOpenURLContext.options.sourceApplication` iOS 26 only——这个 API 在 iOS 26 还在吗?值得 verify(影响 deep-link host-return 白名单) -8. **`KeyboardViewController` `didReceiveMemoryWarning` (`:390-404`)** 强制回 voice surface——typing 引擎是最大内存消费者,但每次收到警告就退出 typing 体验略激进;考虑加 cooldown - ---- - -## 6. 实施建议 - -### 6.1 短期(未来 1-2 周) - -- **任务 A:从 Mac target 排除 `Flow*` / `Edit*` / `Analytics*` / `ManagedGateway*` / `Account*`(HostSupport)所有共享代码** - - 修改 `project.yml:570-602`,加 `excludes:` 列表 - - 验证 Mac target 仍能 build & test pass - - 节省 Mac 二进制 ~2-4 MB + 减少编译时间 ~10-15s -- **任务 B:Mac 剪贴板历史 + AI 技能面板**(P0 ①) -- **任务 C:Mac CLM 设置页**(P0 ②) - -### 6.2 中期(1-2 月) - -- **任务 D:OSG 账户 + Sign in with Apple + 积分**(P1 ③) -- **任务 E:App Attest 决策 + 实施**(P1 ④) - -### 6.3 长期(3-6 月) - -- **任务 F:Mac 一方分析**(P2 ⑤) -- **任务 G:AI 提示轮播 / 上下文技能**(P2 ⑥) -- **任务 H:助手指令集成**(P2 ⑦) - -### 6.4 决策项 - -| 决策 | 选项 | 影响 | -|---|---|---| -| **App Attest 路径** | (a) Catalyst + `DCAppAttestService`(macOS 13+)
(b) 仅 `DCDevice.generateToken`(无 attestation)
(c) 不做 | (a) 需开 `SUPPORTS_MACCATALYST: YES` 改大改;
(b) 安全性较弱但改动小;
(c) Mac 不在 attestation 内 | -| **Mac 走 Mac App Store 还是 Developer ID** | (a) 维持 Developer ID(现状)
(b) 走 Mac App Store | (a) 维持沙盒关闭,Accessibility 自由;
(b) 需重写热键 + 注入方案(沙盒内 Accessibility 拿不到) | -| **是否上 Rime 替代方案** | (a) 不做(Mac 不是键盘)
(b) 内部输入法,绕过 librime | (b) 工作量极大,且与产品形态不符 | -| **Mac 是否支持 iOS 风格的"on-device + cloud hybrid"** | (a) 仅 engine-mode toggle(现状)
(b) 支持每句 hybrid(local first,失败 fallback cloud) | (b) 需要重新设计 MLX streaming 端 | - ---- - -## 7. 关键引用 - -| 主题 | 文件 | 行号 | -|---|---|---| -| iOS 宿主入口 | `OSGKeyboard/OSGKeyboardApp.swift` | 1 | -| iOS Flow 单一拥有者 | `OSGKeyboard/Services/FlowSessionManager.swift` | 1 (~3500 LOC) | -| iOS 键盘入口 | `OSGKeyboardExt/KeyboardViewController.swift` | 33-38 | -| iOS 键盘生命周期 | `OSGKeyboardExt/KeyboardViewController.swift` | 148-655 | -| iOS 共享 ASR 协议 | `OSGKeyboardShared/Services/PolishingService.swift` | 34-105 | -| iOS HostSupport ASR 工厂 | `OSGKeyboardHostSupport/Services/ASRService.swift` | 1 | -| iOS HostSupport 云 ASR 工厂 | `OSGKeyboardHostSupport/Services/CloudASR/CloudASRClientFactory` | 44 | -| Mac 入口 | `OSGKeyboardMac/OSGKeyboardMacApp.swift` | 1 | -| Mac MLX 流式 provider | `OSGKeyboardMac/MacMLXStreamingASRProvider.swift` | 1 | -| Mac MLX 流式 session | `OSGKeyboardMac/MacMLXStreamingSession.swift` | 1 | -| Mac MLX 实时捕获编排 | `OSGKeyboardMac/MacMLXLiveCapture.swift` | 1 | -| Mac 热键 | `OSGKeyboardMac/MacHotkeyService.swift` | 1 | -| Mac 文本注入 | `OSGKeyboardMac/MacTextInsertionService.swift` | 1 | -| Mac 音频 | `OSGKeyboardMac/MacAudioRecorder.swift` | 1 | -| Mac 字幕过滤器 | `OSGKeyboardMac/MacHallucinationFilter.swift` | 1 | -| Mac 字典流水线 | `OSGKeyboardMac/MacDictationPipeline.swift` | 254 | -| Mac 模型目录 | `OSGKeyboard/Resources/LocalASR/local-asr-catalog.json` | 1-111 | -| Mac target 定义 | `project.yml` | 543-674 | -| Mac target 共享 source 排除 | `project.yml` | 558-602 | -| Mac entitlements | `OSGKeyboardMac/OSGKeyboardMac.entitlements` | 1 | - ---- - -## 8. 附录:版本与里程碑 - -| 版本 | 日期 | 关键 Mac 相关变更 | -|---|---|---| -| 2.0.1 (build 90) | 2026-08-21 | (无 Mac-specific changelog 条目) | -| 2.0.0 (build 85) | 2026-08-19 | "Optional OSG account"(iOS only,但相关 HostSupport 代码编进 Mac)| -| 1.8.0 (build 72) | 2026-08-14 | "Mac Styles / Settings follow the same pairing"(设计系统共用)+ "Mac model downloads"(**MLX 模型下装载入**)| -| Pre-1.8.0 | — | Sherpa → MLX 迁移(`MacOnboardingView.swift:7-8` 注释)| - ---- - -**报告完。** -下次更新窗口:2026-09-XX 复审 P0 ① ② 实施情况 + 决策项 6.4 落地。 diff --git a/docs/personal-dictionary-icloud-kvs-checklist.md b/docs/personal-dictionary-icloud-kvs-checklist.md deleted file mode 100644 index 74ef92a..0000000 --- a/docs/personal-dictionary-icloud-kvs-checklist.md +++ /dev/null @@ -1,28 +0,0 @@ -# Personal Dictionary iCloud KVS — Manual Verification Checklist - -Use this on **macOS with Xcode 16+** and at least two devices signed into the **same Apple ID** with iCloud Drive / iCloud enabled. - -## Prerequisites - -1. In Apple Developer Portal, enable **iCloud** → **Key-value storage** for `com.osgkeyboard.ios`. -2. Regenerate provisioning profiles after entitlements change. -3. Run `xcodegen generate` and install a fresh build on each device. - -## Scenarios - -| # | Steps | Expected | -|---|--------|----------| -| 1 | Device A: open Personal Dictionary, enable **Sync via iCloud**, add term `TestWordA` | Toggle stays on; term appears locally | -| 2 | Device B: open app → Personal Dictionary tab | `TestWordA` appears after pull (may take up to ~1 min) | -| 3 | Device B: add `TestWordB` | Device A eventually shows both terms | -| 4 | Both devices: edit same term offline, then go online | Newer edit wins; aliases union when terms match | -| 5 | Device A: delete a term | Term disappears on Device B after sync | -| 6 | Device A: disable iCloud sync | Local dictionary remains; Device B stops receiving new edits from A | -| 7 | Sign out of iCloud on one device | App keeps local dictionary; sync errors may surface in UI | -| 8 | Keyboard extension on Device A | Uses App Group cache immediately after main-app save — no iCloud wait | - -## Notes - -- KVS propagation is **eventual**; force-quit and reopen the app to speed up pulls. -- The keyboard extension never talks to iCloud directly; only the main app syncs. -- Payload limit is ~1 MB per key; very large dictionaries should show the “too large” error. diff --git a/docs/screenshots/6.1/01-keyboard-default.png b/docs/screenshots/6.1/01-keyboard-default.png deleted file mode 100644 index 7914cab..0000000 Binary files a/docs/screenshots/6.1/01-keyboard-default.png and /dev/null differ diff --git a/docs/screenshots/6.1/02-flow-session.png b/docs/screenshots/6.1/02-flow-session.png deleted file mode 100644 index 65a244b..0000000 Binary files a/docs/screenshots/6.1/02-flow-session.png and /dev/null differ diff --git a/docs/screenshots/6.1/03-on-device-asr.png b/docs/screenshots/6.1/03-on-device-asr.png deleted file mode 100644 index 582c438..0000000 Binary files a/docs/screenshots/6.1/03-on-device-asr.png and /dev/null differ diff --git a/docs/screenshots/6.1/04-llm-polish.png b/docs/screenshots/6.1/04-llm-polish.png deleted file mode 100644 index f31a8c6..0000000 Binary files a/docs/screenshots/6.1/04-llm-polish.png and /dev/null differ diff --git a/docs/screenshots/6.1/05-providers.png b/docs/screenshots/6.1/05-providers.png deleted file mode 100644 index 347026a..0000000 Binary files a/docs/screenshots/6.1/05-providers.png and /dev/null differ diff --git a/docs/screenshots/6.7/01-keyboard-default.png b/docs/screenshots/6.7/01-keyboard-default.png deleted file mode 100644 index cf787cd..0000000 Binary files a/docs/screenshots/6.7/01-keyboard-default.png and /dev/null differ diff --git a/docs/screenshots/6.7/02-flow-session.png b/docs/screenshots/6.7/02-flow-session.png deleted file mode 100644 index 7728ca4..0000000 Binary files a/docs/screenshots/6.7/02-flow-session.png and /dev/null differ diff --git a/docs/screenshots/6.7/03-on-device-asr.png b/docs/screenshots/6.7/03-on-device-asr.png deleted file mode 100644 index d4d4cbf..0000000 Binary files a/docs/screenshots/6.7/03-on-device-asr.png and /dev/null differ diff --git a/docs/screenshots/6.7/04-llm-polish.png b/docs/screenshots/6.7/04-llm-polish.png deleted file mode 100644 index 6e5f2e4..0000000 Binary files a/docs/screenshots/6.7/04-llm-polish.png and /dev/null differ diff --git a/docs/screenshots/6.7/05-providers.png b/docs/screenshots/6.7/05-providers.png deleted file mode 100644 index caf375a..0000000 Binary files a/docs/screenshots/6.7/05-providers.png and /dev/null differ diff --git a/docs/screenshots/README.md b/docs/screenshots/README.md deleted file mode 100644 index 031cf4f..0000000 --- a/docs/screenshots/README.md +++ /dev/null @@ -1,73 +0,0 @@ -# App Store Screenshots - -> ⚠️ **PLACEHOLDERS.** The 10 PNGs in this directory are -> automatically generated blanks produced by -> `scripts/generate_screenshot_placeholders.py` and must be -> **replaced with real Simulator screenshots** before App Store -> submission. They use the correct dimensions (1290×2796 for -> 6.7", 1179×2556 for 6.1") so the upload validator will accept -> them, but they contain no real UI. - -## Required dimensions (2026) - -| Size | Devices | Dimensions | Apple requirement | -|------|---------|-----------|---| -| 6.7" | iPhone 17 Pro Max, 17, 16 Pro Max, 16 Plus, 15 Pro Max, 15 Plus | 1290 × 2796 px | **Required** (3-10 images) | -| 6.1" | iPhone 17 Pro, 17, 16 Pro, 16, 15 Pro, 15, 14 Pro, 14 | 1179 × 2556 px | **Required** (3-10 images) | -| 5.5" | iPhone 8 Plus (legacy) | 1242 × 2208 px | Optional since 2024 | - -iPad screenshots are not required because OSGKeyboard is iPhone-only -(`TARGETED_DEVICE_FAMILY = 1`). - -## Layout - -``` -docs/screenshots/ -├── 6.7/ ← 1290×2796 (iPhone 17 Pro Max / 17) -│ ├── 01-keyboard-default.png -│ ├── 02-flow-session.png -│ ├── 03-on-device-asr.png -│ ├── 04-llm-polish.png -│ └── 05-providers.png -└── 6.1/ ← 1179×2556 (iPhone 17 Pro / 17) - ├── 01-keyboard-default.png - ├── 02-flow-session.png - ├── 03-on-device-asr.png - ├── 04-llm-polish.png - └── 05-providers.png -``` - -## How to capture real screenshots - -1. Open `OSGKeyboard.xcodeproj` in Xcode 26+ -2. Run on **iPhone 17 Pro** simulator (6.1" set) and **iPhone 17 Pro Max** simulator (6.7" set) -3. For each scene: - ```bash - # Take a screenshot of the simulator window - xcrun simctl io booted screenshot ~/Desktop/shot.png - ``` -4. Process for App Store (Apple rejects frames containing the device bezel — full-screen content only): - ```bash - # The simulator screenshot already has a thin device frame. - # Open in Preview, crop to full screen (⌘+K with ⌥ for precision), - # export as PNG at 1290×2796 or 1179×2556. - sips -z 2796 1290 shot.png --out final-6.7.png - sips -z 2556 1179 shot.png --out final-6.1.png - ``` -5. Replace the placeholders with the real captures, keeping the - same filenames so the App Store Connect → Version → Uploads UI - auto-pairs by file. - -## Scenes to capture - -The 5 placeholders are intentional scene placeholders. Capture these -*exact* screens, in this order: - -1. **Keyboard at rest** — the iOS keyboard, OSGKeyboard mode, no recording -2. **Flow session active** — keyboard with the green/orange recording ring, - partial transcript visible in the host text field -3. **On-device ASR** — Settings view with the locale list, the on-device - indicator (iPhone icon) visible next to ≥ 3 supported locales -4. **LLM polish** — Settings view with the API provider card, a sample - "polish" transformation shown in the inline preview -5. **Providers** — API Settings card scrolled to show all 6 provider logos