diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4b764e4..d831b18 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Mac MLX streaming ASR**: local dictation uses Qwen3-ASR via mlx-audio-swift with overlay partial preview, tail drain, vocabulary prompt, and polish-before-insert. / **Mac MLX 流式 ASR**:本地听写改用 mlx-audio-swift 的 Qwen3-ASR,支持浮层 partial 预览、尾部截断、词库 prompt 与润色后再插入。
### Changed
+- **Privacy-safe Flow timeout**: new sessions default to a 5-minute inactivity window, with new 1-minute and 5-minute options in Settings; previous 30-/10-minute product defaults migrate once. / **更安全的 Flow 超时**:新会话默认在无活动 5 分钟后结束,设置中新增 1 分钟与 5 分钟选项;旧的 30/10 分钟产品默认会一次性迁移到 5 分钟。
+- **Cloud ASR privacy disclosure**: permission prompts and the bundled privacy policy now explain that audio stays on-device by default and is sent to the configured speech provider only after cloud recognition is enabled. / **云端 ASR 隐私说明**:权限弹窗与内置隐私政策现明确说明音频默认在设备端处理,仅在启用云端识别后发送至用户配置的语音服务商。
- **Mac local ASR engine**: removed Sherpa offline CLI; default model is Qwen3 MLX 0.6B 4-bit (1.7B optional download). / **Mac 本地 ASR 引擎**:移除 Sherpa offline CLI;默认模型改为 Qwen3 MLX 0.6B 4-bit(1.7B 可选下载)。
- **Landing competitor matrix**: compare Typeless / Superwhisper / Openless / OSGKeyboard on open source, pricing, on-device ASR, BYOK, and platforms (incl. honest Windows gap). / **落地页竞品对照**:对比 Typeless / Superwhisper / Openless / OSGKeyboard 的开源、付费、本地识别、BYOK 与平台(含暂无 Windows)。
- **Voluntary support tip**: Settings (top of the page) includes an optional ¥28 Consumable in-app tip (StoreKit 2). All features stay free — no paywall or unlock. / **自愿打赏**:设置页顶部新增可选 ¥28 消耗型应用内打赏(StoreKit 2)。全功能仍免费,无付费墙或功能解锁。
@@ -25,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Landing hero polish**: replace CSS device frames with the marketing composite; full-bleed pale-green hero wash (no side gaps / no radial gradient); Mac story shots sit on transparent chrome. / **落地页 Hero 抛光**:设备框改为营销合成图;首屏淡绿单色通栏(无两侧留白 / 无径向渐变);Mac 故事截图去卡片底。
### Fixed
+- **Permission pre-prompt CTA**: onboarding / Home buttons use Next instead of Allow before the system microphone and speech dialogs (App Store 5.1.1(iv)). / **权限引导按钮**:系统麦克风与语音识别弹窗前的引导/首页按钮改为「下一步」,不再使用「允许」(App Store 5.1.1(iv))。
- **Model download for China networks**: add the `hf-mirror.com` mirror as the mainland-China-first source (bypasses the flaky HF Xet backend), drop the dead ModelScope link, and correct the model file list; add automatic resume-on-drop retry with cross-mirror fallback and a manual "Download source" picker in Settings. / **国内网络模型下载**:新增 `hf-mirror.com` 镜像并在中国大陆优先(绕开不稳定的 HF Xet 后端),删除失效的 ModelScope 链接并修正模型文件清单;下载中断自动断点续传重试并在镜像间回退,设置里新增手动「下载源」选择。
- **iPad sidebar brand mark**: use the template `OSGLogoWide` mark with accent tint so the logo stays visible in the split-view sidebar. / **iPad 侧栏品牌标**:改用可着色的 `OSGLogoWide`,保证分栏侧栏始终显示 logo。
diff --git a/OSGKeyboard/Info.plist b/OSGKeyboard/Info.plist
index a373c36..44cacfb 100644
--- a/OSGKeyboard/Info.plist
+++ b/OSGKeyboard/Info.plist
@@ -94,9 +94,9 @@
NSMicrophoneUsageDescription
- OSGKeyboard uses the microphone for voice dictation and keeps a background audio session active while a voice session is running.
+ OSGKeyboard uses the microphone for voice dictation, including active Flow sessions while you type in other apps.
NSSpeechRecognitionUsageDescription
- OSGKeyboard uses on-device speech recognition to transcribe your voice. Audio is processed on your device and is not uploaded for transcription.
+ OSGKeyboard uses speech recognition to transcribe your voice. Audio is processed on-device by default, or sent to your configured speech provider only when you enable cloud recognition.
NSSupportsLiveActivities
UIApplicationSceneManifest
diff --git a/OSGKeyboard/PrivacyInfo.xcprivacy b/OSGKeyboard/PrivacyInfo.xcprivacy
index 20edd9b..900a72e 100644
--- a/OSGKeyboard/PrivacyInfo.xcprivacy
+++ b/OSGKeyboard/PrivacyInfo.xcprivacy
@@ -15,7 +15,7 @@
NSPrivacyCollectedDataType
NSPrivacyCollectedDataTypeAudioData
NSPrivacyCollectedDataTypeLinked
-
+
NSPrivacyCollectedDataTypeTracking
NSPrivacyCollectedDataTypePurposes
@@ -27,7 +27,7 @@
NSPrivacyCollectedDataType
NSPrivacyCollectedDataTypeOtherUserContent
NSPrivacyCollectedDataTypeLinked
-
+
NSPrivacyCollectedDataTypeTracking
NSPrivacyCollectedDataTypePurposes
diff --git a/OSGKeyboard/Resources/PrivacyPolicy.html b/OSGKeyboard/Resources/PrivacyPolicy.html
index bb9e15a..dd74e84 100644
--- a/OSGKeyboard/Resources/PrivacyPolicy.html
+++ b/OSGKeyboard/Resources/PrivacyPolicy.html
@@ -22,12 +22,12 @@
OSGKeyboard Privacy Policy
-
Last updated: July 8, 2026
+
Last updated: July 23, 2026
OSGKeyboard is a custom iOS keyboard that turns your voice into text. This policy explains what data the app processes and how it is used.
What we collect
- - Voice audio — captured only while you actively record. On-device mode transcribes locally with Apple’s speech APIs; raw audio is not uploaded by OSGKeyboard.
+ - Voice audio — captured only while you actively record. The default on-device mode transcribes locally with Apple’s speech APIs and does not upload raw audio. If you explicitly enable cloud recognition, recordings are sent to the speech provider you configure for transcription; that provider’s privacy policy applies. OSGKeyboard does not store or proxy the audio on its own servers.
- Transcribed text — in Cloud polish mode, the final text (not audio) may be sent to the LLM provider you configure (e.g. OpenAI) for punctuation and formatting.
- API credentials — stored in the iOS Keychain and shared between the main app and keyboard extension. When iCloud settings sync is enabled, keys replicate through iCloud Keychain (not iCloud KVS JSON).
- App preferences — engine mode, language, and keyboard settings stored in App Group UserDefaults. Optional iCloud sync mirrors preferences, usage statistics, and voice history through your private iCloud account.
@@ -48,7 +48,7 @@
Third parties
-
When you choose Cloud polish mode, transcribed text is sent to the API endpoint you configure. That provider’s privacy policy applies to those requests.
+
When you enable cloud recognition, recordings are sent directly to the speech provider you configure. When you choose Cloud polish mode, transcribed text is sent to the API endpoint you configure. Those providers’ privacy policies apply to the requests.
Data retention
Settings remain on your device until you delete the app or reset settings. With iCloud sync enabled, API keys use iCloud Keychain; preferences, statistics, and history may sync via your private iCloud account.
@@ -60,12 +60,12 @@
OSGKeyboard 隐私政策
-
更新日期:2026 年 7 月 8 日
+
更新日期:2026 年 7 月 23 日
OSGKeyboard 是一款 iOS 自定义键盘,可将语音转为文字。本政策说明应用处理哪些数据及用途。
我们处理的数据
- - 语音音频 — 仅在你主动录音时采集。本地模式在设备端通过 Apple 语音识别转写,OSGKeyboard 不会上传原始录音。
+ - 语音音频 — 仅在你主动录音时采集。默认本地模式通过 Apple 语音能力在设备端转写,不会上传原始录音。若你主动启用云端识别,录音会发送到你配置的语音服务商完成转写,并适用该服务商的隐私政策。OSGKeyboard 自身不会存储或中转音频。
- 转写文字 — 云端润色模式下,最终文字(非音频)可能发送到你配置的 LLM 服务商以整理标点和格式。
- API 凭证 — 保存在设备 Keychain,在主 App 与键盘扩展间共享。开启 iCloud 设置同步后,经 iCloud 钥匙串同步(非 iCloud KVS JSON)。
- 应用偏好 — 引擎、语言等设置保存在 App Group。可选 iCloud 同步经私有 iCloud 账户镜像偏好、统计与语音历史。
@@ -86,7 +86,7 @@
第三方
-
选择云端润色时,转写文字会发往你配置的 API,该服务商的隐私政策适用于相关请求。
+
启用云端识别时,录音会直接发送到你配置的语音服务商;选择云端润色时,转写文字会发送到你配置的 API。相关请求适用对应服务商的隐私政策。
数据保留
设置保留在设备上,直至卸载或重置。开启 iCloud 同步后,API 密钥走 iCloud 钥匙串;偏好、统计与历史可能经私有 iCloud 账户同步。
diff --git a/OSGKeyboard/Views/OnboardingView.swift b/OSGKeyboard/Views/OnboardingView.swift
index 01e7d61..1dcac82 100644
--- a/OSGKeyboard/Views/OnboardingView.swift
+++ b/OSGKeyboard/Views/OnboardingView.swift
@@ -460,7 +460,8 @@ private struct MicPermissionPage: View {
switch status {
case .granted: return "onboarding.permission.status.granted"
case .denied: return "onboarding.permission.openSettings"
- case .undetermined: return "onboarding.permission.mic.allow"
+ // App Store 5.1.1(iv): pre-permission CTA must not say "Allow".
+ case .undetermined: return "common.next"
}
}
@@ -526,7 +527,8 @@ private struct SpeechPermissionPage: View {
switch status {
case .granted: return "onboarding.permission.status.granted"
case .denied, .restricted: return "onboarding.permission.openSettings"
- case .undetermined: return "onboarding.permission.speech.allow"
+ // App Store 5.1.1(iv): pre-permission CTA must not say "Allow".
+ case .undetermined: return "common.next"
}
}
diff --git a/OSGKeyboard/en.lproj/InfoPlist.strings b/OSGKeyboard/en.lproj/InfoPlist.strings
index d4670f3..75841fd 100644
--- a/OSGKeyboard/en.lproj/InfoPlist.strings
+++ b/OSGKeyboard/en.lproj/InfoPlist.strings
@@ -1,4 +1,4 @@
/* Permission prompts — English */
-"NSMicrophoneUsageDescription" = "OSGKeyboard uses the microphone for voice dictation and keeps a background audio session active while a voice session is running.";
+"NSMicrophoneUsageDescription" = "OSGKeyboard uses the microphone for voice dictation, including active Flow sessions while you type in other apps.";
"NSSpeechRecognitionUsageDescription" = "OSGKeyboard transcribes your voice with on-device speech recognition by default. If you explicitly switch to a cloud engine in Settings, recordings are sent to the ASR provider you configure.";
diff --git a/OSGKeyboard/en.lproj/Localizable.strings b/OSGKeyboard/en.lproj/Localizable.strings
index 5070a22..7a01e5b 100644
--- a/OSGKeyboard/en.lproj/Localizable.strings
+++ b/OSGKeyboard/en.lproj/Localizable.strings
@@ -5,14 +5,15 @@
/* Onboarding */
"onboarding.progress" = "Step %1$d of %2$d";
"onboarding.welcome.tagline" = "Speak it. It’s typed.";
-"onboarding.permission.preface" = "iOS will ask for two permissions — please allow both.";
+"onboarding.permission.preface" = "iOS will ask for microphone and speech recognition next.";
"onboarding.permission.mic.title" = "Microphone";
"onboarding.permission.mic.body" = "Records your voice.";
-"onboarding.permission.mic.allow" = "Allow";
+/* Pre-system prompt CTA — use Next/Continue, never Allow (App Store 5.1.1(iv)). */
+"onboarding.permission.mic.allow" = "Next";
"onboarding.permission.mic.deniedHint" = "Open Settings to enable, or tap Next to set up later.";
"onboarding.permission.speech.title" = "Speech Recognition";
"onboarding.permission.speech.body" = "Turns your voice into text.";
-"onboarding.permission.speech.allow" = "Allow";
+"onboarding.permission.speech.allow" = "Next";
"onboarding.permission.speech.deniedHint" = "Open Settings to enable, or tap Next to set up later.";
"onboarding.permission.openSettings" = "Open Settings";
"onboarding.permission.status.undetermined" = "Not requested yet";
@@ -327,7 +328,7 @@
"home.setup.permission.mic" = "Microphone access is off — voice input won't work.";
"home.setup.permission.speech" = "Speech recognition is off — voice input won't work.";
"home.setup.permission.both" = "Microphone and speech recognition are off — voice input won't work.";
-"home.setup.permission.request" = "Grant access";
+"home.setup.permission.request" = "Next";
"home.setup.cloudIncomplete" = "Cloud engine needs an API key. Open the Settings tab.";
"home.setup.keyboardHint" = "Don't see OSGKeyboard? Add it in iOS Settings.";
"home.setup.keyboardHint.dismiss" = "Got it";
@@ -420,6 +421,8 @@
"settings.flow.skipAppSwitch.title" = "Skip app switch";
"settings.flow.skipAppSwitch.subtitle" = "After a cold start, try to return to the app you came from.";
"settings.flow.inactivity.title" = "End session after inactivity";
+"settings.flow.inactivity.1m" = "1 minute";
+"settings.flow.inactivity.5m" = "5 minutes";
"settings.flow.inactivity.10m" = "10 minutes";
"settings.flow.inactivity.30m" = "30 minutes";
"settings.flow.inactivity.3h" = "3 hours";
diff --git a/OSGKeyboard/zh-Hans.lproj/InfoPlist.strings b/OSGKeyboard/zh-Hans.lproj/InfoPlist.strings
index a70affe..73c78cc 100644
--- a/OSGKeyboard/zh-Hans.lproj/InfoPlist.strings
+++ b/OSGKeyboard/zh-Hans.lproj/InfoPlist.strings
@@ -1,4 +1,4 @@
/* 权限说明 — 简体中文 */
-"NSMicrophoneUsageDescription" = "OSGKeyboard 使用麦克风进行语音听写,并在语音会话运行期间保持后台音频会话。";
+"NSMicrophoneUsageDescription" = "OSGKeyboard 使用麦克风进行语音听写,包括你在其他 App 中输入时保持启用的 Flow 会话。";
"NSSpeechRecognitionUsageDescription" = "OSGKeyboard 默认使用设备端语音识别将你的语音转为文字。若你在设置中主动切换到云端引擎,录音会发送到你配置的识别服务商。";
diff --git a/OSGKeyboard/zh-Hans.lproj/Localizable.strings b/OSGKeyboard/zh-Hans.lproj/Localizable.strings
index 35079cf..80cc767 100644
--- a/OSGKeyboard/zh-Hans.lproj/Localizable.strings
+++ b/OSGKeyboard/zh-Hans.lproj/Localizable.strings
@@ -5,14 +5,15 @@
/* Onboarding */
"onboarding.progress" = "第 %1$d / %2$d 步";
"onboarding.welcome.tagline" = "开口即文字。";
-"onboarding.permission.preface" = "系统会弹出两次授权,请「允许」权限申请。";
+"onboarding.permission.preface" = "接下来系统会分别请求麦克风与语音识别权限。";
"onboarding.permission.mic.title" = "麦克风";
"onboarding.permission.mic.body" = "用于录制语音。";
-"onboarding.permission.mic.allow" = "允许";
+/* 系统弹窗前的引导按钮须用「下一步/继续」,勿用「允许」(App Store 5.1.1(iv))。 */
+"onboarding.permission.mic.allow" = "下一步";
"onboarding.permission.mic.deniedHint" = "去设置打开,或先点「下一步」。";
"onboarding.permission.speech.title" = "语音识别";
"onboarding.permission.speech.body" = "把你说的话转成文字。";
-"onboarding.permission.speech.allow" = "允许";
+"onboarding.permission.speech.allow" = "下一步";
"onboarding.permission.speech.deniedHint" = "去设置打开,或先点「下一步」。";
"onboarding.permission.openSettings" = "打开设置";
"onboarding.permission.status.undetermined" = "尚未请求";
@@ -326,7 +327,7 @@
"home.setup.permission.mic" = "麦克风还没授权,语音输入用不了。";
"home.setup.permission.speech" = "语音识别还没授权,语音输入用不了。";
"home.setup.permission.both" = "麦克风和语音识别还没授权,语音输入用不了。";
-"home.setup.permission.request" = "去授权";
+"home.setup.permission.request" = "下一步";
"home.setup.cloudIncomplete" = "选了云端引擎,先去「设置」填 API Key。";
"home.setup.keyboardHint" = "列表里没有?去系统设置里添加键盘。";
"home.setup.keyboardHint.dismiss" = "知道了";
@@ -419,6 +420,8 @@
"settings.flow.skipAppSwitch.title" = "跳过应用切换";
"settings.flow.skipAppSwitch.subtitle" = "冷启动完成后,尝试自动返回来源 App。";
"settings.flow.inactivity.title" = "无活动后结束会话";
+"settings.flow.inactivity.1m" = "1 分钟";
+"settings.flow.inactivity.5m" = "5 分钟";
"settings.flow.inactivity.10m" = "10 分钟";
"settings.flow.inactivity.30m" = "30 分钟";
"settings.flow.inactivity.3h" = "3 小时";
diff --git a/OSGKeyboardExt/Info.plist b/OSGKeyboardExt/Info.plist
index 03a1fda..007214a 100644
--- a/OSGKeyboardExt/Info.plist
+++ b/OSGKeyboardExt/Info.plist
@@ -44,8 +44,8 @@
$(PRODUCT_MODULE_NAME).KeyboardViewController
NSMicrophoneUsageDescription
-
OSGKeyboard uses the microphone for voice dictation and keeps a background audio session active while a voice session is running.
+
OSGKeyboard uses the microphone for voice dictation, including active Flow sessions while you type in other apps.
NSSpeechRecognitionUsageDescription
-
OSGKeyboard uses on-device speech recognition to transcribe your voice. Audio is processed on your device and is not uploaded for transcription.
+
OSGKeyboard uses speech recognition to transcribe your voice. Audio is processed on-device by default, or sent to your configured speech provider only when you enable cloud recognition.
diff --git a/OSGKeyboardShared/Models/AppGroupConfiguration.swift b/OSGKeyboardShared/Models/AppGroupConfiguration.swift
index 9887f74..825dcf0 100644
--- a/OSGKeyboardShared/Models/AppGroupConfiguration.swift
+++ b/OSGKeyboardShared/Models/AppGroupConfiguration.swift
@@ -52,6 +52,9 @@ public struct AppGroupConfiguration: Sendable, Equatable {
public static let flowSkipAppSwitch = "config.flowSkipAppSwitch"
/// Raw `FlowInactivityDuration` value; session expires after this idle window.
public static let flowInactivityDuration = "config.flowInactivityDuration"
+ /// One-shot: remap previous product defaults (30m / 10m) → 5m.
+ public static let flowInactivityMigratedToFiveMinuteDefault =
+ "config.flowInactivityDuration.migratedToFiveMinuteDefault"
/// Diagnostic switch: when false, local ASR skips the custom language model.
public static let localASRCustomLanguageModelEnabled = "config.localASR.customLanguageModelEnabled"
}
@@ -310,14 +313,9 @@ public struct AppGroupConfiguration: Sendable, Equatable {
preferICloudSync: config.settingsICloudSyncEnabled
)
- // One-shot default migration for installs that predate an explicit
- // stored value. The privacy-safe defaults ("local", 30 min TTL) are
- // for NEW installs only — an existing user who ran on the old
- // defaults must keep their behavior, both because silently changing
- // engines under someone is wrong, and because iCloud settings sync
- // would stamp the flip as a fresh "edit" and propagate it to every
- // other device, overriding choices made there. Persisting the
- // resolved value makes the decision stable and sync-invisible.
+ // One-shot defaults for installs that predate explicit settings.
+ // Preserve the legacy engine choice, but use the current privacy-safe
+ // inactivity duration when the user has never selected one.
let isExistingInstall = defaults.bool(forKey: Keys.hasCompletedOnboarding)
if defaults.string(forKey: Keys.engineMode) == nil {
let resolved = isExistingInstall ? "cloud" : "local"
@@ -325,9 +323,23 @@ public struct AppGroupConfiguration: Sendable, Equatable {
defaults.set(resolved, forKey: Keys.engineMode)
}
if defaults.string(forKey: Keys.flowInactivityDuration) == nil {
- let resolved: FlowInactivityDuration = isExistingInstall ? .twelveHours : .default
+ let resolved = FlowInactivityDuration.default
config.flowInactivityDuration = resolved
defaults.set(resolved.rawValue, forKey: Keys.flowInactivityDuration)
+ defaults.set(true, forKey: Keys.flowInactivityMigratedToFiveMinuteDefault)
+ } else if !defaults.bool(forKey: Keys.flowInactivityMigratedToFiveMinuteDefault) {
+ // Previous product defaults were 30m then briefly 10m. Remap those
+ // once so existing installs pick up the new 5-minute default; users
+ // who later choose 30m / 10m again keep that choice.
+ let previousDefaults: Set
= [
+ FlowInactivityDuration.thirtyMinutes.rawValue,
+ FlowInactivityDuration.tenMinutes.rawValue,
+ ]
+ if previousDefaults.contains(config.flowInactivityDuration.rawValue) {
+ config.flowInactivityDuration = .default
+ defaults.set(FlowInactivityDuration.default.rawValue, forKey: Keys.flowInactivityDuration)
+ }
+ defaults.set(true, forKey: Keys.flowInactivityMigratedToFiveMinuteDefault)
}
// Cloud no longer exposes off/transcribe; migrate legacy values.
diff --git a/OSGKeyboardShared/Models/FlowInactivityDuration.swift b/OSGKeyboardShared/Models/FlowInactivityDuration.swift
index d62269d..56d90db 100644
--- a/OSGKeyboardShared/Models/FlowInactivityDuration.swift
+++ b/OSGKeyboardShared/Models/FlowInactivityDuration.swift
@@ -7,6 +7,8 @@
import Foundation
public enum FlowInactivityDuration: String, CaseIterable, Identifiable, Sendable, Codable {
+ case oneMinute = "1m"
+ case fiveMinutes = "5m"
case tenMinutes = "10m"
case thirtyMinutes = "30m"
case threeHours = "3h"
@@ -15,15 +17,13 @@ public enum FlowInactivityDuration: String, CaseIterable, Identifiable, Sendable
public var id: String { rawValue }
- /// 30 minutes, not hours: competitors cap sessions at 5–60 min for a
- /// reason — a very long TTL keeps advertising "session active" long after
- /// the host process is likely suspended or dead, amplifying every stale-
- /// state bug into hours of confusing UI. Users can still opt into longer
- /// windows explicitly.
- public static let `default`: FlowInactivityDuration = .thirtyMinutes
+ /// Privacy-safe default: users can choose a longer window explicitly.
+ public static let `default`: FlowInactivityDuration = .fiveMinutes
public var timeInterval: TimeInterval {
switch self {
+ case .oneMinute: return 60
+ case .fiveMinutes: return 5 * 60
case .tenMinutes: return 10 * 60
case .thirtyMinutes: return 30 * 60
case .threeHours: return 3 * 60 * 60
@@ -34,6 +34,8 @@ public enum FlowInactivityDuration: String, CaseIterable, Identifiable, Sendable
public var labelKey: String {
switch self {
+ case .oneMinute: return "settings.flow.inactivity.1m"
+ case .fiveMinutes: return "settings.flow.inactivity.5m"
case .tenMinutes: return "settings.flow.inactivity.10m"
case .thirtyMinutes: return "settings.flow.inactivity.30m"
case .threeHours: return "settings.flow.inactivity.3h"
diff --git a/OSGKeyboardTests/AppGroupConfigurationTests.swift b/OSGKeyboardTests/AppGroupConfigurationTests.swift
index aa75a85..fcb40d5 100644
--- a/OSGKeyboardTests/AppGroupConfigurationTests.swift
+++ b/OSGKeyboardTests/AppGroupConfigurationTests.swift
@@ -33,7 +33,7 @@ final class AppGroupConfigurationTests: XCTestCase {
XCTAssertEqual(config.polishIntensity, .default)
XCTAssertTrue(config.personalDictionary.entries.isEmpty)
XCTAssertTrue(config.flowSkipAppSwitch)
- XCTAssertEqual(config.flowInactivityDuration, .thirtyMinutes)
+ XCTAssertEqual(config.flowInactivityDuration, .fiveMinutes)
}
func testSaveAndLoadRoundTrip() {
@@ -84,33 +84,50 @@ final class AppGroupConfigurationTests: XCTestCase {
XCTAssertEqual(loaded.flowInactivityDuration, .threeHours)
}
- /// Existing installs (onboarding completed, no explicit engineMode key)
- /// ran on the old "cloud"/12h defaults — a silent flip to the new
- /// privacy defaults would change their engine under them AND propagate
- /// through settings sync as a fake fresh edit to their other devices.
- func testDefaultMigrationPreservesExistingInstallBehavior() {
+ /// Existing installs retain their legacy engine, but an unset inactivity
+ /// duration adopts the current privacy-safe default.
+ func testDefaultMigrationUsesPrivacySafeInactivityDuration() {
let defaults = makeDefaults()
defaults.set(true, forKey: AppGroupConfiguration.Keys.hasCompletedOnboarding)
let config = AppGroupConfiguration.load(fromAvailable: defaults)
XCTAssertEqual(config.engineMode, "cloud", "pre-picker installs stay on their old default")
- XCTAssertEqual(config.flowInactivityDuration, .twelveHours)
+ XCTAssertEqual(config.flowInactivityDuration, .fiveMinutes)
// The resolution is persisted so it is stable and sync-invisible.
XCTAssertEqual(defaults.string(forKey: AppGroupConfiguration.Keys.engineMode), "cloud")
XCTAssertEqual(
defaults.string(forKey: AppGroupConfiguration.Keys.flowInactivityDuration),
- FlowInactivityDuration.twelveHours.rawValue
+ FlowInactivityDuration.fiveMinutes.rawValue
)
}
+ func testPreviousDefaultInactivityIsMigratedToFiveMinutesOnce() {
+ let defaults = makeDefaults()
+ defaults.set(FlowInactivityDuration.thirtyMinutes.rawValue,
+ forKey: AppGroupConfiguration.Keys.flowInactivityDuration)
+
+ let first = AppGroupConfiguration.load(fromAvailable: defaults)
+ XCTAssertEqual(first.flowInactivityDuration, .fiveMinutes)
+ XCTAssertTrue(defaults.bool(
+ forKey: AppGroupConfiguration.Keys.flowInactivityMigratedToFiveMinuteDefault
+ ))
+
+ // After migration, an explicit 30-minute choice sticks.
+ var updated = first
+ updated.flowInactivityDuration = .thirtyMinutes
+ updated.save(to: defaults)
+ let second = AppGroupConfiguration.load(fromAvailable: defaults)
+ XCTAssertEqual(second.flowInactivityDuration, .thirtyMinutes)
+ }
+
func testDefaultMigrationGivesFreshInstallPrivacyDefaults() {
let defaults = makeDefaults()
let config = AppGroupConfiguration.load(fromAvailable: defaults)
XCTAssertEqual(config.engineMode, "local")
- XCTAssertEqual(config.flowInactivityDuration, .thirtyMinutes)
+ XCTAssertEqual(config.flowInactivityDuration, .fiveMinutes)
XCTAssertEqual(defaults.string(forKey: AppGroupConfiguration.Keys.engineMode), "local")
}
diff --git a/OSGKeyboardTests/FlowSessionPolicyTests.swift b/OSGKeyboardTests/FlowSessionPolicyTests.swift
index 4aca356..ab46941 100644
--- a/OSGKeyboardTests/FlowSessionPolicyTests.swift
+++ b/OSGKeyboardTests/FlowSessionPolicyTests.swift
@@ -17,10 +17,16 @@ final class FlowSessionPolicyTests: XCTestCase {
XCTAssertTrue(FlowSessionPolicy.skipAppSwitch(defaults: defaults))
}
- func testInactivityDurationDefaultsToThirtyMinutes() {
+ func testInactivityDurationDefaultsToFiveMinutes() {
let defaults = makeDefaults()
- XCTAssertEqual(FlowSessionPolicy.inactivityDuration(defaults: defaults), .thirtyMinutes)
- XCTAssertEqual(FlowSessionPolicy.sessionDuration(defaults: defaults), 30 * 60)
+ XCTAssertEqual(FlowSessionPolicy.inactivityDuration(defaults: defaults), .fiveMinutes)
+ XCTAssertEqual(FlowSessionPolicy.sessionDuration(defaults: defaults), 5 * 60)
+ }
+
+ func testShortInactivityDurations() {
+ XCTAssertEqual(FlowInactivityDuration.oneMinute.timeInterval, 60)
+ XCTAssertEqual(FlowInactivityDuration.fiveMinutes.timeInterval, 5 * 60)
+ XCTAssertEqual(FlowInactivityDuration.tenMinutes.timeInterval, 10 * 60)
}
func testTouchLastActivityExtendsExpiry() {
diff --git a/docs/APPSTORE_METADATA.md b/docs/APPSTORE_METADATA.md
index 5eb06cd..3990b2c 100644
--- a/docs/APPSTORE_METADATA.md
+++ b/docs/APPSTORE_METADATA.md
@@ -209,22 +209,16 @@ We'd love your feedback — open an issue on GitHub or rate this app.
## App Privacy (App Store Connect "Privacy" section)
-Choose **"Data Not Collected"** in the first question.
+Choose **"Yes, we collect data from this app"** because optional cloud
+recognition sends audio and cloud polish/translation sends user text to
+the provider selected by the user. Declare **Audio Data** and **Other
+User Content** for **App Functionality**, linked to the user, and not
+used for tracking. The exact answers are listed under
+[App Privacy answers](#app-privacy-answers).
-The OSGKeyboard app and keyboard extension collect **no data** from
-you. All processing happens on-device or through the LLM endpoint you
-explicitly configure. The app does not embed any analytics, crash
-report, or tracking SDK.
-
-| Question | Answer |
-|---|---|
-| Data collected from this app? | **No** |
-| Data used to track you? | **No** |
-| Data linked to your identity? | **No** |
-
-The `PrivacyInfo.xcprivacy` files in both `OSGKeyboard/` and
-`OSGKeyboardExt/` declare `NSPrivacyTracking: false` and
-`NSPrivacyCollectedDataTypes: []` to match.
+On-device recognition remains the default and does not upload audio.
+The app does not embed analytics, crash-reporting, advertising, or
+tracking SDKs.
---
@@ -265,9 +259,11 @@ to end, please:
through this, but you can also tap it in the keyboard settings).
Full Access is required for the continuous-capture flow session
(network access for the LLM polish step + shared App Group
- container with the main app). The mic is captured on-device; the
- network call only sends the final text transcript to the LLM
- endpoint configured in Settings.
+ container with the main app). On-device recognition is the default
+ and does not upload audio. If the user explicitly enables cloud
+ recognition, recordings are sent to the speech provider configured
+ in Settings. Transcribed text may also be sent to the configured
+ LLM endpoint for polish or translation.
3. In any app, switch to OSGKeyboard (globe key), then hold the
purple mic key, speak, and release.
4. For the LLM polish demo: open OSGKeyboard's main app, Settings,
@@ -288,6 +284,56 @@ Source code: https://github.com/hkgood/OSGKeyboard
---
+## App Privacy answers
+
+Use these conservative disclosures in App Store Connect → App Privacy.
+They cover optional cloud recognition and cloud text polish even though
+on-device recognition remains the default.
+
+### Data types collected
+
+#### User Content → Audio Data
+
+- **Collected:** Yes
+- **Purpose:** App Functionality
+- **Linked to the user:** Yes
+- **Used for tracking:** No
+
+Audio is sent off-device only after the user explicitly enables cloud
+recognition. The configured provider may associate requests with the
+user's provider account/API credential, so the conservative answer is
+"linked".
+
+#### User Content → Other User Content
+
+- **Collected:** Yes
+- **Purpose:** App Functionality
+- **Linked to the user:** Yes
+- **Used for tracking:** No
+
+This covers transcripts, polish prompts, optional translation text, and
+personal-dictionary terms included in those requests. A configured
+provider may associate requests with the user's provider account/API
+credential.
+
+### Do not select
+
+- Contact Info, Financial Info, Location, Contacts, Photos or Videos
+- Browsing History, Search History, Purchases, Identifiers
+- Usage Data or Diagnostics (stored locally/private iCloud only)
+- Third-Party Advertising, Developer Advertising or Marketing,
+ Analytics, Product Personalization, or Other Purposes
+- Tracking
+
+### URLs
+
+- **Privacy Policy URL:** `https://hkgood.github.io/OSGKeyboard/privacy/`
+- **User Privacy Choices URL:** leave blank (optional); users can disable
+ cloud recognition/polish, clear history, reset settings, or delete the
+ app as described in the policy.
+
+---
+
## Submission checklist
- [ ] All 10 screenshots replaced with real Simulator captures
@@ -296,7 +342,8 @@ Source code: https://github.com/hkgood/OSGKeyboard
Store Connect → Upload
- [ ] Select the new build under "Builds" in the version
- [ ] Fill in metadata from this document
-- [ ] Privacy: "Data Not Collected"
+- [ ] Privacy: Audio Data + Other User Content; App Functionality;
+ linked to user; not used for tracking
- [ ] Encryption: skip (auto-skipped via Info.plist key)
- [ ] Add for review
- [ ] Submit
diff --git a/docs/privacy.html b/docs/privacy.html
index 2354de5..d537001 100644
--- a/docs/privacy.html
+++ b/docs/privacy.html
@@ -15,7 +15,7 @@
中文
OSGKeyboard Privacy Policy
- Last updated: July 8, 2026 · v0.5.x
+ Last updated: July 23, 2026 · v1.0
OSGKeyboard is a custom iOS keyboard that turns your voice into text. It runs as a Custom Keyboard Extension on iOS 26 and later, and uses Apple's on-device SpeechAnalyzer + DictationTranscriber for transcription by default. An optional cloud ASR engine (explicit opt-in) uploads recordings to the provider you configure. After transcription, text may be polished or translated via a cloud LLM. This policy explains what data the app processes and how it is used.
What we collect
@@ -67,7 +67,7 @@
OSGKeyboard 隐私政策
- 更新日期:2026 年 7 月 8 日 · v0.5.x
+ 更新日期:2026 年 7 月 23 日 · v1.0
OSGKeyboard 是一款 iOS 自定义键盘,可将语音转为文字。它以自定义键盘扩展的形式运行,需要 iOS 26 及以上系统,默认使用 Apple 端侧的 SpeechAnalyzer + DictationTranscriber 转写;可选的云端识别引擎(需显式二次确认开启)会把录音上传到你配置的服务商。转写完成后,文字可能经云端 LLM 润色或翻译。本政策说明应用处理哪些数据及用途。
我们处理的数据
diff --git a/docs/privacy/index.html b/docs/privacy/index.html
index 2ef0a5d..b667c5a 100644
--- a/docs/privacy/index.html
+++ b/docs/privacy/index.html
@@ -16,7 +16,7 @@
中文
OSGKeyboard Privacy Policy
- Last updated: July 9, 2026 · v0.5.x
+ Last updated: July 23, 2026 · v1.0
OSGKeyboard is a custom iOS keyboard that turns your voice into text. It runs as a Custom Keyboard Extension on iOS 26 and later, and uses Apple's on-device SpeechAnalyzer + DictationTranscriber for transcription by default. An optional cloud ASR engine (explicit opt-in) uploads recordings to the provider you configure. After transcription, text may be polished or translated via a cloud LLM. This policy explains what data the app processes and how it is used.
What we collect
@@ -26,7 +26,7 @@
API credentials — your cloud-engine LLM API key is stored in the iOS Keychain on your device and is read only when an LLM request is made. It is shared with the main app through a shared Keychain group, never through UserDefaults.
App preferences — engine mode, recognition language, polish intensity, translation target, handedness, cursor-navigation toggle, and keyboard settings are stored in App Group UserDefaults on your device so the main app and keyboard extension stay in sync.
Personal dictionary — terms and aliases you add in the Dictionary tab are stored locally on your device. They are included in LLM polish prompts so your vocabulary is preserved; dictionary data is not uploaded to a separate server.
- Voice history — the host app may keep a local list of recent successful transcripts in its History tab, on-device only, capped at 500 entries.
+ Voice history — the host app may keep a list of recent successful transcripts in its History tab. History is capped at 300 entries. When iCloud settings sync is enabled, history may also sync through your private iCloud Key-Value Store.
Usage statistics — cumulative dictation time, dictation characters, translation characters, and dictionary entry count are computed and stored locally on the home screen stats card.
@@ -52,7 +52,7 @@
After on-device ASR, transcribed text is sent for polish and optional translation. On the local engine this goes to a built-in DeepSeek endpoint. On the cloud engine it goes to the OpenAI-compatible API endpoint you configured in Settings. That provider's privacy policy applies to those requests. OSGKeyboard does not proxy, log, or aggregate your requests.
Data retention
- Settings and API keys remain on your device until you delete the app or reset settings. Transcription results are passed to the host app you are typing in and are not stored long-term by OSGKeyboard. Voice history is kept locally (up to 500 entries) and is never uploaded; you can clear it at any time from the History tab or by resetting settings.
+ Settings remain on your device until you delete the app or reset settings. When iCloud settings sync is enabled, API keys replicate through iCloud Keychain and preferences, statistics, and history may sync through your private iCloud account. Transcription results are passed to the host app you are typing in and are not stored long-term by OSGKeyboard. Voice history is capped at 300 entries; you can clear it from the History tab or by resetting settings.
Children's privacy
OSGKeyboard is not directed to children under 13 and does not knowingly collect personal data from children.
@@ -68,7 +68,7 @@
OSGKeyboard 隐私政策
- 更新日期:2026 年 7 月 9 日 · v0.5.x
+ 更新日期:2026 年 7 月 23 日 · v1.0
OSGKeyboard 是一款 iOS 自定义键盘,可将语音转为文字。它以自定义键盘扩展的形式运行,需要 iOS 26 及以上系统,默认使用 Apple 端侧的 SpeechAnalyzer + DictationTranscriber 转写;可选的云端识别引擎(需显式二次确认开启)会把录音上传到你配置的服务商。转写完成后,文字可能经云端 LLM 润色或翻译。本政策说明应用处理哪些数据及用途。
我们处理的数据
@@ -104,7 +104,7 @@
端侧 ASR 完成后,转写文字会发送润色与可选翻译。本地引擎发送至内置 DeepSeek 端点;云端引擎发送至你在设置中配置的 OpenAI 兼容 API 端点。该服务商的隐私政策适用于相关请求。OSGKeyboard 不代理、不记录、不聚合这些请求。
数据保留
- 设置与 API Key 保留在设备上,直至卸载或重置。识别结果写入你正在使用的宿主 App,OSGKeyboard 不会长期存储。语音历史最多 500 条本机保存,不会上传,可随时在「历史」页清空或通过重置设置清除。
+ 设置保留在设备上,直至卸载或重置。开启 iCloud 设置同步后,API 密钥经 iCloud 钥匙串同步,偏好、统计与历史可能经私有 iCloud 账户同步。识别结果写入你正在使用的宿主 App,OSGKeyboard 不会长期存储。语音历史上限 300 条,可随时在「历史」页清空或通过重置设置清除。
儿童隐私
OSGKeyboard 不面向 13 岁以下儿童,亦不会明知地从儿童处收集个人信息。
diff --git a/project.yml b/project.yml
index 78f2b1c..3ec9326 100644
--- a/project.yml
+++ b/project.yml
@@ -48,8 +48,8 @@ settings:
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS: YES
STRING_CATALOG_GENERATE_SYMBOLS: YES
CLANG_CXX_LANGUAGE_STANDARD: c++17
- MARKETING_VERSION: "0.5.4"
- CURRENT_PROJECT_VERSION: "23"
+ MARKETING_VERSION: "1.0"
+ CURRENT_PROJECT_VERSION: "26"
# 签名配置来自 Signing.local.xcconfig(gitignored,不会被覆盖)
# 项目级签名 xcconfig,适用于所有 target
@@ -129,8 +129,8 @@ targets:
- UIInterfaceOrientationLandscapeRight
UIApplicationSceneManifest:
UIApplicationSupportsMultipleScenes: false
- NSMicrophoneUsageDescription: "OSGKeyboard uses the microphone for voice dictation and keeps a background audio session active while a voice session is running."
- NSSpeechRecognitionUsageDescription: "OSGKeyboard uses on-device speech recognition to transcribe your voice. Audio is processed on your device and is not uploaded for transcription."
+ NSMicrophoneUsageDescription: "OSGKeyboard uses the microphone for voice dictation, including active Flow sessions while you type in other apps."
+ NSSpeechRecognitionUsageDescription: "OSGKeyboard uses speech recognition to transcribe your voice. Audio is processed on-device by default, or sent to your configured speech provider only when you enable cloud recognition."
UIBackgroundModes:
- audio
NSSupportsLiveActivities: true
@@ -257,8 +257,8 @@ targets:
CFBundleLocalizations:
- en
- zh-Hans
- NSMicrophoneUsageDescription: "OSGKeyboard uses the microphone for voice dictation and keeps a background audio session active while a voice session is running."
- NSSpeechRecognitionUsageDescription: "OSGKeyboard uses on-device speech recognition to transcribe your voice. Audio is processed on your device and is not uploaded for transcription."
+ NSMicrophoneUsageDescription: "OSGKeyboard uses the microphone for voice dictation, including active Flow sessions while you type in other apps."
+ NSSpeechRecognitionUsageDescription: "OSGKeyboard uses speech recognition to transcribe your voice. Audio is processed on-device by default, or sent to your configured speech provider only when you enable cloud recognition."
NSExtension:
NSExtensionAttributes:
IsASCIICapable: false