中文全拼与双拼
+支持全拼、微软双拼、搜狗双拼、可选模糊音、简拼排序与最多约 80 条展开候选;词频学习只留在本机。
+Last updated: August 14, 2026 · v1.3
-OSGKeyboard provides an iOS custom keyboard and a macOS menu-bar dictation app. iOS uses Apple's on-device speech APIs by default; macOS can use a downloaded local Qwen3-ASR model. You may instead choose a cloud recognition engine in Settings. After transcription, text may be polished, translated, or used by AI Agent features through a provider you configure.
+Last updated: August 19, 2026 · v2.0
+OSGKeyboard provides an iOS custom keyboard and a macOS menu-bar dictation app. iOS uses Apple's on-device speech APIs by default; macOS can use a downloaded local Qwen3-ASR model. The core local and user-configured-provider features do not require an OSGKeyboard account. On iOS, you may optionally sign in with Apple and choose managed credits instead of supplying provider credentials. This policy explains both paths.
account.osglab.com to the managed ASR provider for transcription. OSGKeyboard does not keep raw recordings after the request is processed.account.osglab.com.account.osglab.com. When the selected service supports server-side web search, it may retrieve public web results for time-sensitive answers.key.osglab.com; no voice, transcript, clipboard body, API key, or ordinary keystroke content is included. Opening the update page requests public content from download.osglab.com. Those hosts may process ordinary connection metadata such as IP address and request time.Cloud ASR and LLM requests go directly to the provider you configure. AI skills may hand generated data to Apple Shortcuts or open a route in Apple Maps, Amap, or Baidu Maps. Public suggestions and updates use key.osglab.com and download.osglab.com. Optional Mac models are downloaded from Hugging Face. Each third party's privacy policy applies; OSGKeyboard does not proxy, log, or aggregate provider requests.
User-configured cloud ASR and LLM requests go directly to the provider you configure. Managed-credit requests go through account.osglab.com to the managed ASR or LLM provider. Apple processes Sign in with Apple and StoreKit purchases. AI skills may hand generated data to Apple Shortcuts or open a route in Apple Maps, Amap, or Baidu Maps. Public suggestions and updates use key.osglab.com and download.osglab.com. Optional Mac models are downloaded from Hugging Face. Each service's privacy policy applies.
Settings remain on your device until you delete the app or reset settings. With iCloud sync enabled, API keys use iCloud Keychain; eligible preferences, statistics, dictionary data, and voice history may sync through your private iCloud account.
Voice history — successful transcripts may be saved in the main app’s History tab (up to 300 entries). With iCloud settings sync enabled, history may also sync across your devices.
Clipboard history — stays in this device’s App Group, is capped at 15 entries, does not sync through iCloud, and has no fixed expiry. Turning the feature off or resetting settings keeps existing items. Use the separately confirmed clear action to delete them.
Typing learning and Mac models — Chinese and English learning data stays local and can be cleared from typing settings. Downloaded Mac speech models remain until you remove the model or delete its local files.
+Optional account — account profile, credit ledger, StoreKit audit records, and limited anti-abuse records are retained by the account service while the account is active. Account deletion removes the active profile and credentials; pseudonymous ledger, purchase-audit, and time-limited anti-abuse records may remain where required to prevent replay, fraud, or abuse.
Questions: open an issue at github.com/hkgood/OSGKeyboard.
@@ -69,17 +71,18 @@更新日期:2026 年 8 月 14 日 · v1.3
-OSGKeyboard 包含 iOS 自定义键盘与 macOS 菜单栏听写应用。iOS 默认使用 Apple 端侧语音能力;macOS 可使用下载到本机的 Qwen3-ASR 模型。你也可以在设置中主动选择云端识别引擎。转写后,文字可能通过你配置的服务商用于润色、翻译或 AI Agent 功能。
+更新日期:2026 年 8 月 19 日 · v2.0
+OSGKeyboard 包含 iOS 自定义键盘与 macOS 菜单栏听写应用。iOS 默认使用 Apple 端侧语音能力;macOS 可使用下载到本机的 Qwen3-ASR 模型。本地能力和用户自备服务商能力无需登录 OSGKeyboard 账号。iOS 用户也可以选择 Apple 登录并使用托管积分,无需自行填写服务商凭证。本政策同时说明这两条可选路径。
account.osglab.com 的 OSGKeyboard 托管网关发送到托管识别服务商。请求处理完成后,OSGKeyboard 不保留原始录音。account.osglab.com 处理。account.osglab.com 处理。所选服务支持联网搜索时,可能检索公开网页结果回答时效性问题。key.osglab.com 请求公开建议标题;请求不包含语音、转写、剪贴板正文、API Key 或普通击键内容。打开更新页时会从 download.osglab.com 请求公开内容。相关主机可能处理 IP 地址、请求时间等普通连接元数据。云端 ASR 与 LLM 请求直接发送到你配置的服务商。AI 技能也可能把生成数据交给 Apple 快捷指令,或在 Apple 地图、高德地图、百度地图中打开路线。公开建议与更新页面分别由 key.osglab.com、download.osglab.com 提供;可选 Mac 模型从 Hugging Face 下载。各第三方的隐私政策适用。
用户自备的云端 ASR 与 LLM 请求直接发送到你配置的服务商;托管积分请求经 account.osglab.com 发送到托管 ASR 或 LLM 服务商。Apple 处理 Apple 登录与 StoreKit 购买。AI 技能也可能把生成数据交给 Apple 快捷指令,或在 Apple 地图、高德地图、百度地图中打开路线。公开建议与更新页面分别由 key.osglab.com、download.osglab.com 提供;可选 Mac 模型从 Hugging Face 下载。各服务的隐私政策适用。
设置保留在设备上,直至卸载或重置。开启 iCloud 同步后,API 密钥走 iCloud 钥匙串;可同步的偏好、统计、词库与语音历史可能经私有 iCloud 账户同步。
语音历史 — 成功转写可保存在主 App「历史」页(最多 300 条)。开启 iCloud 设置同步后,历史也可能在多设备间同步。
剪贴板历史 — 仅保存在本机 App Group,上限 15 条,不经 iCloud 同步,也没有固定过期时间。关闭功能或重置设置会保留已有记录;需使用单独确认的清空操作才能删除。
输入学习与 Mac 模型 — 中英文输入学习数据仅留本机,可在输入设置中单独清除。下载的 Mac 语音模型会保留到你移除模型或删除本地文件。
+可选账号 — 账号有效期间,账号服务会保留资料、积分账本、StoreKit 审计记录及有限的反滥用记录。注销会删除有效资料与凭证;为防止交易重放、欺诈或滥用,匿名账本、购买审计及有时限的反滥用记录可能在必要范围内继续保留。
问题反馈:github.com/hkgood/OSGKeyboard
diff --git a/OSGKeyboard/Services/AIAgentShortcutInstaller.swift b/OSGKeyboard/Services/AIAgentShortcutInstaller.swift index ddbdea3..f65a712 100644 --- a/OSGKeyboard/Services/AIAgentShortcutInstaller.swift +++ b/OSGKeyboard/Services/AIAgentShortcutInstaller.swift @@ -6,8 +6,8 @@ // opens the Add sheet in Shortcuts directly. import Foundation -import UIKit import OSGKeyboardShared +import UIKit enum AIAgentShortcutInstaller { @MainActor diff --git a/OSGKeyboard/Services/AIAgentShortcutRunner.swift b/OSGKeyboard/Services/AIAgentShortcutRunner.swift index 260d93b..cfa8f47 100644 --- a/OSGKeyboard/Services/AIAgentShortcutRunner.swift +++ b/OSGKeyboard/Services/AIAgentShortcutRunner.swift @@ -4,8 +4,8 @@ // Consumes the keyboard's pending export-skill payload. Navigate opens a // map URL in the host. Other exports open the companion Shortcut. -import UIKit import OSGKeyboardShared +import UIKit enum AIAgentShortcutRunner { @MainActor diff --git a/OSGKeyboard/Services/AppURLHandler.swift b/OSGKeyboard/Services/AppURLHandler.swift index d0bfd38..6add671 100644 --- a/OSGKeyboard/Services/AppURLHandler.swift +++ b/OSGKeyboard/Services/AppURLHandler.swift @@ -7,8 +7,8 @@ // host-return whitelist) is `UIOpenURLContext.options.sourceApplication` from a // scene delegate — SwiftUI's `.onOpenURL` does not expose it. -import UIKit import OSGKeyboardShared +import UIKit /// Buffers launch/open URLs until the SwiftUI root registers a handler. /// @@ -77,6 +77,7 @@ final class AppSceneDelegate: NSObject, UIWindowSceneDelegate { options connectionOptions: UIScene.ConnectionOptions ) { handle(connectionOptions.urlContexts) + handle(connectionOptions.userActivities) } /// Warm open while the app is already running or suspended in memory. @@ -84,6 +85,11 @@ final class AppSceneDelegate: NSObject, UIWindowSceneDelegate { handle(URLContexts) } + /// Universal Links arrive as browsing-web user activities, not URL contexts. + func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { + handle([userActivity]) + } + private func handle(_ contexts: Set+ 开口即文字 · Speak it. It's typed. +
diff --git a/README.md b/README.md index eb9a685..dcc0fae 100644 --- a/README.md +++ b/README.md @@ -1,95 +1,139 @@ # OSGKeyboard -**开口即文字。** +**开口即文字。也可以直接打字、提问、编辑。** -在 iPhone、iPad 和 Mac 上,用说的代替打字。任意 App 里开口,润色好的文字直接落到光标处。 +OSGKeyboard 是面向 iPhone、iPad 与 Mac 的语音输入工具。iOS 键盘把端侧听写、中英打字、AI 助手、剪贴板技能与可选托管积分放在同一个输入界面;Mac 提供按住 Option 即可使用的全局听写。    - +  -[官网](https://hkgood.github.io/OSGKeyboard/) · [English](./README.en.md) · [隐私政策](https://hkgood.github.io/OSGKeyboard/privacy/) +[官网](https://hkgood.github.io/OSGKeyboard/) · [English](./README.en.md) · [隐私政策](https://hkgood.github.io/OSGKeyboard/privacy/) · [更新记录](./CHANGELOG.md) ---- +> Mac 公开 DMG 是已签名并公证的历史版本 1.1。仓库中的 macOS target 随整体源码演进到 2.0;最新源码能力可使用 Xcode 26 构建。 -## 为什么用它 +## 一个键盘,四种输入方式 -- **真的随处可用** — 微信、备忘录、Notion、Cursor、邮件……光标在哪,文字就落在哪 -- **说完就能用** — 点按(iOS)或按住 Option(Mac)开口,AI 自动补标点、整理结构,不用自己改稿 -- **中英都能打** — iOS 键盘内建全拼 / 双拼中文候选,以及英文补全、纠错与下一词预测 -- **默认不上传录音** — iOS 本地识别、Mac 可选本地模型;只有你主动开启云端引擎时,音频才会离开设备 -- **模型随你选** — 本地识别无需 API Key;润色与 AI 模式使用你配置的 DeepSeek、OpenAI、Anthropic、OpenRouter 等服务 -- **Mac 也能全局听写** — 菜单栏常驻,屏幕底部浮层实时反馈,说完自动插入当前 App +### 语音听写 ---- +- iOS 26 默认使用 Apple `SpeechAnalyzer` 与 `DictationTranscriber` 在设备端识别 +- 轻点麦克风开始或结束,转写结果直接插入当前 App 的光标位置 +- 可选 AI 润色、标点整理、结构重建与润色后翻译 +- 9 种内置润色风格、自定义风格、趣味风格轻度 / 重度档位与可选情绪 emoji +- Mac 菜单栏 App 支持按住 Option 全局听写;Apple Silicon 可下载 Qwen3 MLX 本地模型 -## 三步开始 +### 中英打字 -1. **安装并授权** — iOS 添加键盘并开启「完全访问」;Mac 授予麦克风与辅助功能 -2. **选引擎** — 本地识别可直接使用;需要润色或 AI 模式时再填入自己的 API Key -3. **开口说话** — 切换到 OSGKeyboard 键盘,或按住 Option 键,文字即出现 +- 中文:全拼、微软双拼、搜狗双拼、可选模糊音、简拼排序和展开候选面板 +- 英文:三格 QuickType、约 4 万词离线词表、补全、纠错与下一词建议 +- 输入体验:邻键纠错、叠指连打、长按连删、双空格句号、系统 Return 语义 +- 个性词库同时参与中文候选、英文建议、语音偏置与润色保护 -> iOS 首次打开会走 6 步引导:权限 → 键盘 → 识别引擎 → 润色模型,约 2 分钟完成。 +### 统一 AI 助手 ---- +- 轻点听写,长按向 AI 提问;语音与 AI 共用一个助手入口 +- 回答流式显示;仅当原输入框与光标上下文仍匹配时自动上屏 +- 支持口述修改最近一次可靠的 OSGKeyboard 输入,并选择替换或追加 +- 根据当前输入框显示发送、搜索、前往、完成、下一步或换行 +- 一次撤销覆盖听写、AI 回答、编辑与剪贴板粘贴 -## 核心能力 +### 剪贴板与技能 -| | iOS / iPadOS | macOS | +- 剪贴板历史默认关闭,开启后最多保存 15 条纯文本,仅存于本机 App Group +- 复制后短时间显示回复、总结、翻译等快捷技能 +- 内置待办、日程与备忘录可通过 Apple 快捷指令导出;地图导航直接打开高德、百度或 Apple 地图 +- 可创建自定义技能:名称、SF Symbol、提示词和可选 iCloud 快捷指令 +- 最多将 8 个技能放到键盘,支持长按拖动排序 + +## 三种服务路径 + +OSGKeyboard 不把云端服务绑定为唯一选择: + +1. **设备端** — 默认路径。iOS 本地听写无需账号或 API Key,原始录音不上传。 +2. **自备服务商(BYOK)** — 配置自己的云端 ASR / LLM 凭证,请求直接发送到所选服务商;凭证保存在 Keychain。 +3. **OSG 托管积分** — iOS / iPadOS 可选。Apple 登录后可使用托管语音与 AI,无需填写服务商凭证;首次启用前会明确说明离开设备的数据并征求同意。 + +本地听写和自备服务商始终可以独立使用,不要求 OSGKeyboard 账号。 + +## 可选账号与积分 + +iOS / iPadOS 账号中心支持: + +- Sign in with Apple、资料管理、退出登录与 App 内账号注销 +- 托管积分余额、App Store 消耗型积分包与邀请 +- 服务端核验购买结果并维护积分账本 +- 键盘扩展只接收短时、限权的托管服务凭证 + +自愿打赏 `ByRockyACoffee` 与积分包彼此独立,不解锁核心功能。 + +## 历史、统计与同步 + +- 首页集中展示近 7 天听写统计、最近历史与个性词库 +- 语音历史最多保留 300 条,可按天删除 +- 设置、个性词库、润色风格、历史与统计可按类型选择经私有 iCloud 同步 +- API Key 默认保存在 Keychain;仅在开启 iCloud 设置同步后经 iCloud 钥匙串复制。剪贴板历史、账号令牌与打字学习不会通过 iCloud 设置同步 + +## 平台能力 + +| 能力 | iOS / iPadOS 26+ | macOS 15+ | |---|:---:|:---:| -| 自定义键盘 / 全局热键 | ✅ | ✅ Option 按住说话 | -| 中文输入(全拼 / 微软双拼 / 搜狗双拼) | ✅ 可选模糊音 | — | -| 英文输入(补全 / 纠错 / 下一词) | ✅ 离线词表 + 个性词库加权 | — | -| 本地语音识别 | ✅ Apple SpeechAnalyzer | ✅ Qwen3 MLX 流式(默认 0.6B 4-bit)+ Apple Speech 回退 | -| AI 文本润色 | ✅ | ✅ | -| 语音 AI 问答(显式插入 / 发送) | ✅ | — | -| 语音编辑上次输入 | ✅ | — | -| 润色后翻译 | ✅ | ✅ | -| 个性词库 | ✅ iCloud 同步;保护润色并参与英文补全 | ✅ | -| 听写历史 | ✅ | ✅ | -| 听写浮层 | —(静默后台保活) | ✅ 底部胶囊浮层 | +| 系统键盘 / 全局热键 | 自定义键盘 | 按住 Option | +| 端侧语音识别 | Apple SpeechAnalyzer | Qwen3 MLX(Apple Silicon)+ Apple Speech 回退 | +| 中文全拼 / 双拼 | 支持 | — | +| 英文补全 / 纠错 | 支持 | — | +| AI 润色与翻译 | 支持 | 支持 | +| 统一助手与语音问答 | 支持 | — | +| 剪贴板历史与技能 | 支持 | — | +| 可选 OSG 账号与托管积分 | 支持 | — | +| 个性词库、历史与统计 | 支持 | 支持 | +| iCloud 同步 | 设置、词库、历史与统计等可选同步 | 共享兼容数据 | ---- +## 隐私原则 -## 隐私 +- **端侧优先**:使用本地识别时,原始录音不离开设备 +- **主动联网**:只有明确选择云端识别、润色、AI 或技能时,相关数据才会发送 +- **路径透明**:自备服务商请求直达所选服务商;托管积分请求经 `account.osglab.com` +- **击键不上传**:中文候选学习与英文建议偏好保存在本机,密码框不学习 +- **剪贴板可控**:历史默认关闭、最多 15 条、仅本机保存,不会自行发送给 AI +- **凭证隔离**:用户 API Key 保存在 Keychain;账号会话令牌只在主 App 私有 Keychain +- **无广告追踪**:不集成广告、分析或追踪 SDK,不出售个人数据 -- **默认本地识别** — 录音在设备上转写,不经过我们的服务器 -- **润色只发文字** — 发给 LLM 的是转写文本,以及用于衔接的少量光标附近文字,不是原始音频 -- **服务商由你选择** — 只有你在设置中主动选择云端识别、润色、AI 或技能时,相关音频或文字才会直接发给你配置的服务商 -- **剪贴板默认关闭** — 历史最多 15 条、仅保存在本机;只有你主动运行剪贴板技能、在 AI 提问中点名剪贴板或粘贴后润色时,正文才会离开设备 -- **不上传击键** — 中文候选学习与英文补全/纠错学习仅留在设备 App Group;密码框关闭英文建议与纠错,普通击键不会上传 -- **密钥与模型归你控制** — API Key 保存在 Keychain,可选经 iCloud 钥匙串同步;Mac 本地模型下载后保存在本机,端侧推理不上传录音 -- 详见 [隐私政策](https://hkgood.github.io/OSGKeyboard/privacy/) +完整数据类型、保留策略、账号删除与第三方服务说明见[隐私政策](https://hkgood.github.io/OSGKeyboard/privacy/)。 ---- +## 快速开始 -## 获取 +### iPhone / iPad -**iPhone / iPad(App Store)** +1. 从 App Store 安装并打开 OSGKeyboard。 +2. 按引导添加键盘、开启“完全访问”,并授予麦克风与语音识别权限。 +3. 选择本地识别、自备服务商或可选托管积分。 +4. 在任意输入框切换到 OSGKeyboard:轻点听写、长按问 AI,或切换到中文 / English 打字。 - -开口即文字 · Speak it. It's typed. diff --git a/Tests/suite-manifest.json b/Tests/suite-manifest.json index 98222db..ebf80f5 100644 --- a/Tests/suite-manifest.json +++ b/Tests/suite-manifest.json @@ -20,6 +20,26 @@ "OSGKeyboardTests/ProviderToolRequestCoordinatorTests" ] }, + "account": { + "description": "Host-private session, Apple integrity protocol, account center, and referral links", + "platform": "ios", + "tests": [ + "OSGKeyboardTests/AccountAPIClientTests", + "OSGKeyboardTests/AccountCenterViewModelTests", + "OSGKeyboardTests/AccountCreditPurchaseManagerTests", + "OSGKeyboardTests/AccountSecurityPrimitiveTests", + "OSGKeyboardTests/AccountSignInCoordinatorTests", + "OSGKeyboardTests/AccountSnapshotLoaderTests", + "OSGKeyboardTests/DeviceIntegrityTests" + ] + }, + "managed_gateway": { + "description": "Scope-limited grants and managed LLM transport", + "platform": "ios", + "tests": [ + "OSGKeyboardExtTests/ManagedGatewayTests" + ] + }, "sync": { "description": "iCloud KVS merge/sync for settings, local-only clipboard consent, dictionary, speech history, usage stats", "platform": "ios", @@ -185,6 +205,8 @@ "description": "Full iOS + Ext hermetic suite (includes host_misc + pipeline_perf; excludes mac and live_api)", "groups": [ "config", + "account", + "managed_gateway", "sync", "polish", "edit_last_input", @@ -201,6 +223,8 @@ "description": "Default CI / PR gate: critical path without host_misc, pipeline_perf, or mac", "groups": [ "config", + "account", + "managed_gateway", "sync", "polish", "edit_last_input", @@ -213,7 +237,15 @@ }, "api": { "description": "Online API contracts + polish/LLM stubs", - "groups": ["cloud_asr", "polish"] + "groups": ["cloud_asr", "polish", "managed_gateway"] + }, + "account": { + "description": "Account, Apple integrity, and referral-link contracts", + "groups": ["account"] + }, + "managed": { + "description": "Managed gateway grant and provider transports", + "groups": ["managed_gateway", "cloud_asr"] }, "asr": { "description": "Cloud + local ASR + utterance stitching", diff --git a/docs/ACCOUNT_MANAGED_GATEWAY_TESTING.md b/docs/ACCOUNT_MANAGED_GATEWAY_TESTING.md new file mode 100644 index 0000000..079bb37 --- /dev/null +++ b/docs/ACCOUNT_MANAGED_GATEWAY_TESTING.md @@ -0,0 +1,125 @@ +# 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`. +- 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 index 4035043..5553fed 100644 --- a/docs/APPSTORE_METADATA.md +++ b/docs/APPSTORE_METADATA.md @@ -1,4 +1,4 @@ -# App Store Connect — OSGKeyboard 1.8.0 (build 79) +# App Store Connect — OSGKeyboard 2.0.0 (build 83) > Current metadata baseline for the iOS/iPadOS App Store build. Version and build > numbers come from `project.yml`. The repository also contains a separate @@ -11,7 +11,7 @@ | App name | `OSGKeyboard` | ≤ 30 characters | | Subtitle | `Voice input, everywhere` | ≤ 30 characters | | Bundle ID | `com.osgkeyboard.ios` | iOS host target | -| Version / build | `1.8.0` / `79` | `MARKETING_VERSION` / `CURRENT_PROJECT_VERSION` | +| Version / build | `2.0.0` / `83` | `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 | | @@ -32,7 +32,7 @@ | Field | Value | |---|---| | Price | Free | -| In-App Purchases | Optional consumable tip `ByRockyACoffee`; unlocks no feature | +| In-App Purchases | Optional consumables: tip `ByRockyACoffee`; managed-credit packs `500tks`, `1500tks`, `3000tks` | | Availability | All configured App Store territories | | Pre-order | No | @@ -47,10 +47,10 @@ VOICE INPUT • On-device by default. iOS 26 SpeechAnalyzer and DictationTranscriber transcribe locally. -• Optional cloud recognition. Audio leaves the device only after you - enable a cloud ASR provider and configure its credentials. -• Optional AI polish and translation. Add your own provider API key; - without a key, recognized text can still be inserted. +• 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 @@ -73,7 +73,8 @@ PRIVACY • No advertising, analytics, or tracking SDKs. • Local recognition does not upload audio. -• Cloud ASR and LLM requests go directly to the provider you configure. +• 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 @@ -92,7 +93,7 @@ https://github.com/hkgood/OSGKeyboard ## Promotional text (≤ 170 characters) ```text -Voice input anywhere, with on-device recognition by default. Add your own AI key for polish, translation, and AI answers. Also types Chinese and English. +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) @@ -101,31 +102,23 @@ Voice input anywhere, with on-device recognition by default. Add your own AI key keyboard,voice,dictation,speech,transcribe,AI,pinyin,Chinese,English,polish,typing,productivity ``` -## What's new in 1.8.0 +## What's new in 2.0.0 ```text NEW -• Skills center with Reply, Summarize, Translate, custom skills, and - exports to Reminders, Calendar, Notes, and Maps. -• English QuickType-style suggestions with smarter system, contact, - text-replacement, and neighbor-key corrections. -• Improved Pinyin abbreviations and on-device typing-habit learning. -• Overlapping key presses, double-space period, contextual Return labels, - and a full-width iPad keyboard with editing controls. -• AI keyboard answers stream as they are generated and can use provider - web search for current information. -• Optional on-device clipboard history and one-tap clipboard skills. +• Optional Sign in with Apple account center with managed credits, + App Store credit packs, profile controls, and account deletion. +• Managed cloud speech and AI access for signed-in users. Local dictation + and user-owned provider keys continue to work without an account. +• Referral support and synchronized server-side credit balances. CHANGED -• Hold the microphone to edit the last verified input, then preview, - replace, or append the result. -• Undo now covers dictation, AI answers, edits, and clipboard pastes. -• Home cards and navigation make History, Personal Dictionary, Skills, - Styles, and Settings easier to find. +• Voice and AI now share one Assistant tab with adaptive field actions, + contextual suggestions, and safer answer insertion. +• Clipboard setup guidance is shorter and shows only unfinished steps. FIXED -• Improved Chinese input setup, Universal Clipboard responsiveness, - keyboard switching, recording cancellation, and AI session recovery. +• Account confirmation dialogs now open from the selected account action. ``` ## App Review information @@ -149,11 +142,17 @@ OSGKeyboard is a custom keyboard for iOS/iPadOS 26. 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. AI polish and AI mode require a user-owned provider key in Settings. - Without a key, local dictation still inserts recognized text. -6. Optional tip product `ByRockyACoffee` is consumable and unlocks no - feature. -7. Clipboard history is off by default. To test it, open Settings → +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 @@ -179,8 +178,9 @@ polish/translation, and AI mode even though local recognition is the default. - 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 provider -credential. +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 @@ -198,10 +198,43 @@ 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. + ### Do not select - Advertising, marketing, analytics, product personalization, or tracking -- Contact information, location, contacts, photos, browsing/search history +- 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 @@ -211,10 +244,13 @@ standard HTTPS. Re-evaluate this answer if non-exempt cryptography is added. ## Submission checklist -- [ ] Confirm `project.yml` still reads version 1.8.0 / build 79 +- [ ] Confirm `project.yml` still reads version 2.0.0 / build 83 - [ ] 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 -- [ ] Confirm the tip product remains optional and unlocks no feature -- [ ] Upload, select build 79, add review notes, and submit +- [ ] 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 83, add review notes, and submit diff --git a/docs/assets/site.css b/docs/assets/site.css new file mode 100644 index 0000000..2f9b1ae --- /dev/null +++ b/docs/assets/site.css @@ -0,0 +1,1284 @@ +:root { + color-scheme: light dark; + --bg: #f7faf8; + --bg-elevated: rgba(255, 255, 255, 0.74); + --bg-solid: #ffffff; + --ink: #102218; + --muted: #5c6c62; + --faint: #819087; + --line: rgba(16, 34, 24, 0.1); + --green: #3aa05a; + --green-bright: #72f49d; + --green-deep: #19783b; + --cyan: #52d9cc; + --violet: #9281ff; + --shadow: 0 24px 80px rgba(16, 65, 35, 0.13); + --radius-xl: 36px; + --radius-lg: 26px; + --radius-md: 18px; + --content: min(1180px, calc(100vw - 40px)); +} + +html[data-theme="dark"] { + --bg: #06100a; + --bg-elevated: rgba(11, 24, 16, 0.76); + --bg-solid: #0d1911; + --ink: #f1fff5; + --muted: #a6b9ac; + --faint: #738278; + --line: rgba(222, 255, 233, 0.12); + --green: #60d982; + --green-bright: #8affaa; + --green-deep: #38b963; + --cyan: #5ceee0; + --violet: #a99dff; + --shadow: 0 28px 90px rgba(0, 0, 0, 0.42); +} + +* { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; + scroll-padding-top: 88px; + background: var(--bg); +} + +body { + margin: 0; + overflow-x: hidden; + background: + radial-gradient(circle at 20% 12%, rgba(58, 160, 90, 0.1), transparent 26rem), + radial-gradient(circle at 80% 36%, rgba(82, 217, 204, 0.08), transparent 30rem), + var(--bg); + color: var(--ink); + font-family: "Outfit", -apple-system, BlinkMacSystemFont, "SF Pro Display", + "PingFang SC", "Helvetica Neue", sans-serif; + line-height: 1.55; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +body::before { + position: fixed; + z-index: -1; + inset: 0; + background-image: + linear-gradient(var(--line) 1px, transparent 1px), + linear-gradient(90deg, var(--line) 1px, transparent 1px); + background-size: 68px 68px; + content: ""; + opacity: 0.2; + pointer-events: none; + mask-image: linear-gradient(to bottom, black, transparent 76%); +} + +a { + color: inherit; + text-decoration: none; +} + +button, +a { + -webkit-tap-highlight-color: transparent; +} + +button { + color: inherit; + font: inherit; +} + +img, +video { + display: block; + max-width: 100%; +} + +.skip-link { + position: fixed; + z-index: 100; + top: 12px; + left: 12px; + padding: 10px 14px; + border-radius: 999px; + background: var(--ink); + color: var(--bg); + transform: translateY(-160%); +} + +.skip-link:focus { + transform: translateY(0); +} + +.site-nav { + position: fixed; + z-index: 50; + top: 14px; + right: 0; + left: 0; + width: var(--content); + margin: auto; +} + +.nav-shell { + display: flex; + align-items: center; + justify-content: space-between; + min-height: 58px; + padding: 8px 10px 8px 16px; + border: 1px solid var(--line); + border-radius: 999px; + background: color-mix(in srgb, var(--bg-elevated) 92%, transparent); + box-shadow: 0 12px 40px rgba(9, 34, 18, 0.08); + backdrop-filter: blur(24px) saturate(150%); + -webkit-backdrop-filter: blur(24px) saturate(150%); +} + +.brand { + display: inline-flex; + gap: 10px; + align-items: center; + font-size: 0.96rem; + font-weight: 750; + letter-spacing: -0.02em; +} + +.brand img { + width: 30px; + height: 30px; + border-radius: 9px; +} + +.nav-links, +.nav-actions { + display: flex; + align-items: center; +} + +.nav-links { + gap: clamp(12px, 2vw, 26px); + color: var(--muted); + font-size: 0.86rem; +} + +.nav-links a { + transition: color 180ms ease; +} + +.nav-links a:hover, +.nav-links a:focus-visible { + color: var(--ink); +} + +.nav-actions { + gap: 6px; +} + +.icon-button, +.language-button { + display: inline-grid; + min-width: 40px; + height: 40px; + padding: 0 12px; + border: 0; + border-radius: 999px; + background: transparent; + cursor: pointer; + place-items: center; +} + +.icon-button:hover, +.language-button:hover, +.icon-button:focus-visible, +.language-button:focus-visible { + background: color-mix(in srgb, var(--ink) 7%, transparent); +} + +.language-button { + font-size: 0.78rem; + font-weight: 700; +} + +.material-symbols-rounded { + font-variation-settings: "FILL" 0, "wght" 450, "GRAD" 0, "opsz" 24; +} + +.hero { + position: relative; + min-height: 960px; + padding: 150px 0 90px; + overflow: hidden; + background: #020805; + color: #effff4; +} + +.hero-beams, +.hero-scrim, +.hero-glow { + position: absolute; + inset: 0; +} + +.hero-beams canvas { + width: 100%; + height: 100%; +} + +.hero-scrim { + background: + linear-gradient(180deg, rgba(2, 8, 5, 0.12), rgba(2, 8, 5, 0.42) 60%, #020805 100%), + linear-gradient(90deg, rgba(2, 8, 5, 0.84), transparent 55%); + pointer-events: none; +} + +.hero-glow { + background: + radial-gradient(circle at 72% 46%, rgba(74, 255, 137, 0.18), transparent 27rem), + radial-gradient(circle at 54% 68%, rgba(82, 217, 204, 0.14), transparent 24rem); + pointer-events: none; +} + +.hero-inner { + position: relative; + z-index: 2; + display: grid; + grid-template-columns: minmax(0, 0.92fr) minmax(380px, 1.08fr); + gap: 38px; + align-items: center; + width: var(--content); + margin: auto; +} + +.eyebrow { + display: inline-flex; + gap: 8px; + align-items: center; + margin: 0 0 18px; + color: var(--green-deep); + font-size: 0.76rem; + font-weight: 800; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.hero .eyebrow { + color: var(--green-bright); +} + +.eyebrow::before { + width: 7px; + height: 7px; + border-radius: 50%; + background: currentColor; + box-shadow: 0 0 18px currentColor; + content: ""; +} + +h1, +h2, +h3, +p { + margin-top: 0; +} + +.hero h1 { + max-width: 720px; + margin-bottom: 22px; + font-size: clamp(3.5rem, 7vw, 7.3rem); + font-weight: 780; + letter-spacing: -0.075em; + line-height: 0.92; +} + +.gradient-text { + background: linear-gradient(115deg, #7dff9e, #78e5dc 45%, #b6a7ff 92%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; +} + +.hero-copy { + max-width: 650px; + margin-bottom: 30px; + color: rgba(236, 255, 242, 0.76); + font-size: clamp(1.06rem, 1.8vw, 1.28rem); + line-height: 1.7; +} + +.hero-actions, +.download-actions { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; +} + +.store-badge { + display: inline-flex; + transition: transform 180ms ease, filter 180ms ease; +} + +.store-badge img { + width: auto; + height: 48px; +} + +.store-badge:hover, +.store-badge:focus-visible { + filter: brightness(1.08); + transform: translateY(-2px); +} + +.secondary-button, +.primary-button { + display: inline-flex; + gap: 8px; + align-items: center; + justify-content: center; + min-height: 48px; + padding: 0 19px; + border: 1px solid rgba(255, 255, 255, 0.16); + border-radius: 14px; + background: rgba(255, 255, 255, 0.08); + color: #f4fff7; + font-size: 0.9rem; + font-weight: 700; + backdrop-filter: blur(14px); +} + +.primary-button { + border-color: transparent; + background: var(--green); + color: white; +} + +.hero-note { + margin: 16px 0 0; + color: rgba(236, 255, 242, 0.52); + font-size: 0.78rem; +} + +.hero-visual { + position: relative; + min-height: 650px; +} + +.phone-frame { + position: absolute; + z-index: 2; + top: 0; + right: 4%; + width: min(350px, 68%); + padding: 9px; + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 48px; + background: #080b09; + box-shadow: 0 40px 100px rgba(0, 0, 0, 0.55), 0 0 60px rgba(79, 236, 122, 0.15); + transform: rotate(3deg); +} + +.phone-frame img { + width: 100%; + border-radius: 40px; +} + +.video-card { + position: absolute; + z-index: 3; + bottom: 24px; + left: 0; + width: min(360px, 70%); + padding: 9px; + border: 1px solid rgba(255, 255, 255, 0.16); + border-radius: 30px; + background: rgba(10, 20, 13, 0.8); + box-shadow: 0 30px 80px rgba(0, 0, 0, 0.42); + backdrop-filter: blur(18px); + transform: rotate(-4deg); +} + +.video-card video { + width: 100%; + border-radius: 23px; +} + +.floating-chip { + position: absolute; + z-index: 4; + display: inline-flex; + gap: 8px; + align-items: center; + padding: 11px 15px; + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 999px; + background: rgba(11, 28, 17, 0.78); + box-shadow: 0 16px 45px rgba(0, 0, 0, 0.28); + color: #eaffef; + font-size: 0.78rem; + font-weight: 700; + backdrop-filter: blur(18px); +} + +.floating-chip.one { + top: 13%; + left: 0; +} + +.floating-chip.two { + right: 0; + bottom: 18%; +} + +.trust-strip { + position: relative; + z-index: 3; + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 1px; + width: var(--content); + margin: 70px auto 0; + overflow: hidden; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 24px; + background: rgba(255, 255, 255, 0.1); +} + +.trust-item { + min-height: 100px; + padding: 20px; + background: rgba(6, 15, 9, 0.82); +} + +.trust-item strong, +.trust-item span { + display: block; +} + +.trust-item strong { + margin-bottom: 5px; + color: #f2fff5; + font-size: 1rem; +} + +.trust-item span { + color: rgba(236, 255, 242, 0.58); + font-size: 0.8rem; +} + +.section { + padding: clamp(84px, 11vw, 150px) 0; +} + +.section-dark { + position: relative; + overflow: hidden; + background: #020805; + color: #effff4; +} + +.section-dark::before { + position: absolute; + inset: 0; + background: + radial-gradient(circle at 10% 10%, rgba(58, 160, 90, 0.17), transparent 27rem), + radial-gradient(circle at 85% 80%, rgba(146, 129, 255, 0.12), transparent 28rem); + content: ""; +} + +.section-inner { + position: relative; + width: var(--content); + margin: auto; +} + +.section-heading { + max-width: 780px; + margin-bottom: clamp(38px, 6vw, 68px); +} + +.section-heading h2 { + margin-bottom: 20px; + font-size: clamp(2.5rem, 5.2vw, 5.2rem); + font-weight: 750; + letter-spacing: -0.06em; + line-height: 0.98; +} + +.section-heading p { + max-width: 670px; + margin-bottom: 0; + color: var(--muted); + font-size: clamp(1rem, 1.6vw, 1.18rem); + line-height: 1.72; +} + +.section-dark .section-heading p { + color: rgba(236, 255, 242, 0.62); +} + +.feature-stage { + display: grid; + grid-template-columns: minmax(0, 0.9fr) minmax(420px, 1.1fr); + gap: clamp(30px, 6vw, 80px); + align-items: center; +} + +.feature-copy h3 { + margin-bottom: 16px; + font-size: clamp(2rem, 3.6vw, 3.3rem); + letter-spacing: -0.045em; + line-height: 1.04; +} + +.feature-copy > p { + max-width: 600px; + color: var(--muted); + font-size: 1.04rem; +} + +.section-dark .feature-copy > p { + color: rgba(236, 255, 242, 0.64); +} + +.feature-list { + display: grid; + gap: 12px; + margin: 28px 0 0; + padding: 0; + list-style: none; +} + +.feature-list li { + display: flex; + gap: 11px; + align-items: flex-start; +} + +.feature-list .material-symbols-rounded { + margin-top: 1px; + color: var(--green); + font-size: 21px; +} + +.media-shell { + position: relative; + padding: clamp(14px, 2.5vw, 28px); + overflow: hidden; + border: 1px solid var(--line); + border-radius: var(--radius-xl); + background: + linear-gradient(145deg, color-mix(in srgb, var(--green) 15%, transparent), transparent 45%), + var(--bg-elevated); + box-shadow: var(--shadow); +} + +.section-dark .media-shell { + border-color: rgba(255, 255, 255, 0.12); + background: + linear-gradient(145deg, rgba(102, 255, 149, 0.14), transparent 46%), + rgba(255, 255, 255, 0.055); +} + +.media-shell video, +.media-shell img { + width: 100%; + max-height: 650px; + object-fit: contain; + border-radius: calc(var(--radius-xl) - 13px); +} + +.media-label { + position: absolute; + right: 18px; + bottom: 18px; + padding: 7px 11px; + border: 1px solid rgba(255, 255, 255, 0.14); + border-radius: 999px; + background: rgba(3, 11, 6, 0.76); + color: rgba(255, 255, 255, 0.78); + font-size: 0.72rem; + backdrop-filter: blur(14px); +} + +.feature-grid, +.path-grid, +.platform-grid, +.step-grid, +.compare-grid { + display: grid; + gap: 16px; +} + +.feature-grid { + grid-template-columns: repeat(12, 1fr); +} + +.feature-card { + position: relative; + min-height: 300px; + padding: clamp(24px, 3vw, 34px); + overflow: hidden; + border: 1px solid var(--line); + border-radius: var(--radius-lg); + background: var(--bg-elevated); + box-shadow: 0 16px 60px rgba(17, 62, 33, 0.07); +} + +.feature-card:nth-child(1), +.feature-card:nth-child(4) { + grid-column: span 7; +} + +.feature-card:nth-child(2), +.feature-card:nth-child(3) { + grid-column: span 5; +} + +.feature-card::after { + position: absolute; + right: -120px; + bottom: -140px; + width: 300px; + height: 300px; + border-radius: 50%; + background: radial-gradient(circle, color-mix(in srgb, var(--card-accent, var(--green)) 18%, transparent), transparent 70%); + content: ""; +} + +.feature-card .icon { + display: grid; + width: 48px; + height: 48px; + margin-bottom: 42px; + border-radius: 15px; + background: color-mix(in srgb, var(--card-accent, var(--green)) 13%, transparent); + color: var(--card-accent, var(--green)); + place-items: center; +} + +.feature-card h3 { + margin-bottom: 10px; + font-size: clamp(1.35rem, 2.4vw, 2rem); + letter-spacing: -0.035em; +} + +.feature-card p { + max-width: 540px; + margin-bottom: 0; + color: var(--muted); +} + +.feature-card.violet { + --card-accent: var(--violet); +} + +.feature-card.cyan { + --card-accent: var(--cyan); +} + +.path-grid { + grid-template-columns: repeat(3, 1fr); +} + +.path-card { + position: relative; + min-height: 390px; + padding: 28px; + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: var(--radius-lg); + background: rgba(255, 255, 255, 0.055); +} + +.path-card.recommended { + background: + linear-gradient(150deg, rgba(74, 255, 137, 0.14), transparent 60%), + rgba(255, 255, 255, 0.065); +} + +.path-index { + display: inline-flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + margin-bottom: 44px; + border: 1px solid rgba(255, 255, 255, 0.16); + border-radius: 50%; + color: var(--green-bright); + font-size: 0.76rem; + font-weight: 800; +} + +.path-card h3 { + margin-bottom: 12px; + font-size: 1.75rem; + letter-spacing: -0.04em; +} + +.path-card p { + color: rgba(236, 255, 242, 0.6); +} + +.path-card ul { + margin: 24px 0 0; + padding-left: 18px; + color: rgba(236, 255, 242, 0.76); +} + +.path-card li { + margin-bottom: 8px; +} + +.account-stage { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(300px, 0.72fr); + gap: clamp(34px, 8vw, 110px); + align-items: center; +} + +.account-phone { + max-width: 390px; + margin: auto; + padding: 28px 18px; + border: 1px solid var(--line); + border-radius: 45px; + background: var(--bg-solid); + box-shadow: var(--shadow); +} + +.account-phone img { + border-radius: 37px; +} + +.credit-screen { + min-height: 560px; + padding: 12px 0 30px; + color: var(--ink); +} + +.credit-screen h3 { + margin-bottom: 36px; + text-align: center; + font-size: 1.2rem; +} + +.credit-label { + margin: 0 4px 8px; + color: var(--muted); + font-size: 0.76rem; +} + +.balance-card, +.credit-pack-list { + border: 1px solid var(--line); + border-radius: 23px; + background: color-mix(in srgb, var(--bg-solid) 86%, var(--bg)); +} + +.balance-card { + margin-bottom: 22px; + padding: 20px 18px; +} + +.balance-card strong { + display: block; + font-size: 2.5rem; + letter-spacing: -0.06em; +} + +.balance-card span { + color: var(--muted); + font-size: 0.78rem; +} + +.balance-bar { + height: 8px; + margin-top: 20px; + border-radius: 999px; + background: var(--green); +} + +.credit-pack { + display: flex; + align-items: center; + justify-content: space-between; + min-height: 76px; + padding: 0 16px; + border-bottom: 1px solid var(--line); +} + +.credit-pack:last-child { + border-bottom: 0; +} + +.credit-pack strong { + font-size: 1.2rem; +} + +.credit-pack .buy-chip { + padding: 8px 14px; + border-radius: 12px; + background: var(--green); + color: white; + font-size: 0.82rem; + font-weight: 700; +} + +.privacy-panel { + display: grid; + grid-template-columns: 0.82fr 1.18fr; + gap: clamp(30px, 7vw, 90px); + align-items: start; + padding: clamp(30px, 5vw, 64px); + border: 1px solid var(--line); + border-radius: var(--radius-xl); + background: + radial-gradient(circle at 10% 10%, color-mix(in srgb, var(--green) 14%, transparent), transparent 22rem), + var(--bg-elevated); + box-shadow: var(--shadow); +} + +.privacy-mark { + display: grid; + width: 90px; + height: 90px; + margin-bottom: 24px; + border-radius: 28px; + background: color-mix(in srgb, var(--green) 14%, transparent); + color: var(--green); + place-items: center; +} + +.privacy-mark span { + font-size: 46px; +} + +.privacy-panel h2 { + margin-bottom: 14px; + font-size: clamp(2.2rem, 4vw, 4rem); + letter-spacing: -0.055em; + line-height: 1; +} + +.privacy-panel p { + color: var(--muted); +} + +.privacy-points { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 14px; +} + +.privacy-point { + min-height: 150px; + padding: 21px; + border: 1px solid var(--line); + border-radius: 20px; + background: color-mix(in srgb, var(--bg-solid) 60%, transparent); +} + +.privacy-point strong { + display: block; + margin-bottom: 6px; +} + +.privacy-point span { + color: var(--muted); + font-size: 0.87rem; +} + +.text-link { + display: inline-flex; + gap: 6px; + align-items: center; + margin-top: 14px; + color: var(--green-deep); + font-weight: 750; +} + +.platform-grid { + grid-template-columns: repeat(2, 1fr); +} + +.platform-card { + min-height: 520px; + padding: clamp(28px, 4vw, 46px); + overflow: hidden; + border: 1px solid var(--line); + border-radius: var(--radius-xl); + background: var(--bg-elevated); + box-shadow: var(--shadow); +} + +.platform-card h3 { + margin: 20px 0 10px; + font-size: 2rem; + letter-spacing: -0.04em; +} + +.platform-card > p { + color: var(--muted); +} + +.platform-card img { + max-height: 330px; + margin: 32px auto 0; + object-fit: contain; + border-radius: 20px; +} + +.platform-pill { + display: inline-flex; + gap: 7px; + align-items: center; + padding: 7px 11px; + border-radius: 999px; + background: color-mix(in srgb, var(--green) 13%, transparent); + color: var(--green-deep); + font-size: 0.76rem; + font-weight: 800; +} + +.step-grid { + grid-template-columns: repeat(3, 1fr); + counter-reset: steps; +} + +.step { + min-height: 270px; + padding: 28px; + border-top: 1px solid var(--line); + counter-increment: steps; +} + +.step::before { + display: block; + margin-bottom: 55px; + color: var(--green-deep); + content: "0" counter(steps); + font-size: 0.78rem; + font-weight: 800; + letter-spacing: 0.08em; +} + +.step h3 { + margin-bottom: 10px; + font-size: 1.45rem; + letter-spacing: -0.03em; +} + +.step p { + color: var(--muted); +} + +.compare-grid { + grid-template-columns: repeat(6, 1fr); +} + +.compare-cell { + min-height: 180px; + padding: 22px; + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 20px; + background: rgba(255, 255, 255, 0.05); +} + +.compare-cell .material-symbols-rounded { + margin-bottom: 35px; + color: var(--green-bright); +} + +.compare-cell strong, +.compare-cell span { + display: block; +} + +.compare-cell strong { + margin-bottom: 6px; + font-size: 0.95rem; +} + +.compare-cell span { + color: rgba(236, 255, 242, 0.56); + font-size: 0.8rem; +} + +.faq-list { + max-width: 900px; + margin: 0 auto; + border-top: 1px solid var(--line); +} + +.faq-item { + border-bottom: 1px solid var(--line); +} + +.faq-item summary { + display: flex; + gap: 20px; + align-items: center; + justify-content: space-between; + padding: 25px 0; + cursor: pointer; + font-size: clamp(1.05rem, 2vw, 1.3rem); + font-weight: 700; + list-style: none; +} + +.faq-item summary::-webkit-details-marker { + display: none; +} + +.faq-item summary::after { + color: var(--green-deep); + content: "+"; + font-size: 1.5rem; + font-weight: 400; +} + +.faq-item[open] summary::after { + content: "−"; +} + +.faq-item p { + max-width: 760px; + padding: 0 0 25px; + color: var(--muted); +} + +.download-panel { + position: relative; + padding: clamp(40px, 7vw, 80px); + overflow: hidden; + border-radius: var(--radius-xl); + background: + radial-gradient(circle at 80% 20%, rgba(126, 255, 165, 0.25), transparent 26rem), + linear-gradient(135deg, #07170d, #0c2a17); + box-shadow: 0 35px 100px rgba(6, 42, 18, 0.24); + color: #effff4; +} + +.download-panel::after { + position: absolute; + right: -80px; + bottom: -160px; + width: 430px; + height: 430px; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 50%; + content: ""; + box-shadow: 0 0 0 50px rgba(255, 255, 255, 0.025), 0 0 0 100px rgba(255, 255, 255, 0.018); +} + +.download-panel h2 { + position: relative; + z-index: 1; + max-width: 780px; + margin-bottom: 16px; + font-size: clamp(2.7rem, 5.5vw, 5.5rem); + letter-spacing: -0.065em; + line-height: 0.95; +} + +.download-panel > p { + position: relative; + z-index: 1; + max-width: 650px; + margin-bottom: 28px; + color: rgba(236, 255, 242, 0.64); +} + +.download-actions { + position: relative; + z-index: 1; +} + +.site-footer { + padding: 40px 0 50px; +} + +.footer-inner { + display: flex; + gap: 24px; + align-items: center; + justify-content: space-between; + width: var(--content); + margin: auto; + color: var(--muted); + font-size: 0.82rem; +} + +.footer-links { + display: flex; + flex-wrap: wrap; + gap: 16px; +} + +.footer-links a:hover, +.footer-links a:focus-visible { + color: var(--ink); +} + +:focus-visible { + outline: 3px solid color-mix(in srgb, var(--green) 60%, white); + outline-offset: 3px; +} + +@media (max-width: 1000px) { + .nav-links { + display: none; + } + + .hero { + min-height: auto; + } + + .hero-inner, + .feature-stage, + .account-stage, + .privacy-panel { + grid-template-columns: 1fr; + } + + .hero-visual { + min-height: 680px; + margin-top: 20px; + } + + .trust-strip { + grid-template-columns: repeat(2, 1fr); + } + + .path-grid, + .step-grid { + grid-template-columns: 1fr; + } + + .path-card { + min-height: 300px; + } + + .compare-grid { + grid-template-columns: repeat(3, 1fr); + } +} + +@media (max-width: 720px) { + :root { + --content: min(100% - 28px, 1180px); + --radius-xl: 28px; + --radius-lg: 22px; + } + + .site-nav { + top: 9px; + } + + .nav-shell { + min-height: 52px; + padding: 6px 7px 6px 12px; + } + + .brand span { + display: none; + } + + .hero { + padding-top: 112px; + } + + .hero h1 { + font-size: clamp(3.35rem, 18vw, 5.2rem); + } + + .hero-inner { + gap: 0; + } + + .hero-visual { + min-height: 560px; + margin-top: 42px; + } + + .phone-frame { + right: 2%; + width: 72%; + border-radius: 38px; + } + + .phone-frame img { + border-radius: 31px; + } + + .video-card { + bottom: 10px; + width: 72%; + } + + .floating-chip.one { + top: 8%; + } + + .floating-chip.two { + right: 0; + bottom: 10%; + } + + .trust-strip, + .feature-grid, + .privacy-points, + .platform-grid, + .compare-grid { + grid-template-columns: 1fr; + } + + .trust-item { + min-height: 84px; + } + + .feature-card:nth-child(n) { + grid-column: span 1; + } + + .feature-card { + min-height: 270px; + } + + .feature-card .icon { + margin-bottom: 34px; + } + + .compare-cell { + min-height: 150px; + } + + .compare-cell .material-symbols-rounded { + margin-bottom: 25px; + } + + .account-phone { + max-width: 330px; + } + + .footer-inner { + align-items: flex-start; + flex-direction: column; + } +} + +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + + *, + *::before, + *::after { + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + } +} diff --git a/docs/assets/site.js b/docs/assets/site.js new file mode 100644 index 0000000..d570323 --- /dev/null +++ b/docs/assets/site.js @@ -0,0 +1,195 @@ +/*! + * OSGKeyboard website interactions. + * The page remains useful without JavaScript; this file adds language, theme, + * media, and the decorative Three.js hero enhancement. + */ +(function () { + "use strict"; + + const root = document.documentElement; + const languageButton = document.getElementById("languageToggle"); + const themeButton = document.getElementById("themeToggle"); + const themeIcon = themeButton?.querySelector(".material-symbols-rounded"); + const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; + const themeMedia = window.matchMedia("(prefers-color-scheme: dark)"); + const supportedLanguages = new Set(["zh", "en"]); + let beams = null; + + function preferredLanguage() { + const query = new URLSearchParams(window.location.search).get("lang"); + if (supportedLanguages.has(query)) return query; + const saved = localStorage.getItem("osg-site-language"); + if (supportedLanguages.has(saved)) return saved; + return navigator.language.toLowerCase().startsWith("zh") ? "zh" : "en"; + } + + function applyLanguage(language, persist) { + const lang = supportedLanguages.has(language) ? language : "zh"; + root.lang = lang === "zh" ? "zh-Hans" : "en"; + root.dataset.lang = lang; + + document.querySelectorAll("[data-zh][data-en]").forEach((element) => { + element.textContent = element.dataset[lang] || ""; + }); + + document.querySelectorAll("[data-zh-html][data-en-html]").forEach((element) => { + element.innerHTML = element.dataset[`${lang}Html`] || ""; + }); + + document.querySelectorAll("[data-zh-alt][data-en-alt]").forEach((element) => { + element.alt = element.dataset[`${lang}Alt`] || ""; + }); + + document.querySelectorAll("[data-zh-src][data-en-src]").forEach((element) => { + element.src = element.dataset[`${lang}Src`] || ""; + }); + + document.querySelectorAll("[data-zh-href][data-en-href]").forEach((element) => { + element.href = element.dataset[`${lang}Href`] || ""; + }); + + document.querySelectorAll("[data-zh-label][data-en-label]").forEach((element) => { + element.setAttribute("aria-label", element.dataset[`${lang}Label`] || ""); + }); + + document.querySelectorAll("[data-zh-content][data-en-content]").forEach((element) => { + element.setAttribute("content", element.dataset[`${lang}Content`] || ""); + }); + + document.querySelectorAll("[data-shot]").forEach((image) => { + const theme = root.dataset.theme || "light"; + image.src = `assets/screenshots/${lang}/${theme}/${image.dataset.shot}`; + }); + + document.querySelectorAll("[data-media-name]").forEach((media) => { + const name = media.dataset.mediaName; + const extension = media.dataset.mediaExtension || "mp4"; + media.src = `assets/whats-new/${name}-${lang}.${extension}`; + media.load?.(); + }); + + document.title = lang === "zh" + ? "OSGKeyboard — 语音、打字与 AI 助手键盘" + : "OSGKeyboard — Voice, typing, and an AI assistant keyboard"; + + if (languageButton) { + languageButton.textContent = lang === "zh" ? "EN" : "中文"; + languageButton.setAttribute( + "aria-label", + lang === "zh" ? "Switch to English" : "切换到中文" + ); + } + + if (persist) { + localStorage.setItem("osg-site-language", lang); + const url = new URL(window.location.href); + if (lang === "en") { + url.searchParams.set("lang", "en"); + } else { + url.searchParams.delete("lang"); + } + history.replaceState({}, "", url); + } + } + + function storedTheme() { + const saved = localStorage.getItem("osg-site-theme"); + return saved === "light" || saved === "dark" ? saved : null; + } + + function applyTheme(theme, persist) { + const resolved = theme === "dark" ? "dark" : "light"; + root.dataset.theme = resolved; + root.style.colorScheme = resolved; + + if (themeIcon) { + themeIcon.textContent = resolved === "dark" ? "light_mode" : "dark_mode"; + } + + if (themeButton) { + const lang = root.dataset.lang || "zh"; + const label = resolved === "dark" + ? (lang === "zh" ? "切换到浅色模式" : "Switch to light mode") + : (lang === "zh" ? "切换到深色模式" : "Switch to dark mode"); + themeButton.setAttribute("aria-label", label); + } + + document.querySelectorAll("[data-shot]").forEach((image) => { + const lang = root.dataset.lang || "zh"; + image.src = `assets/screenshots/${lang}/${resolved}/${image.dataset.shot}`; + }); + + beams?.setBackground(resolved === "dark" ? "#020805" : "#020805"); + beams?.setLightColor(resolved === "dark" ? "#72F49D" : "#61E987"); + + if (persist) localStorage.setItem("osg-site-theme", resolved); + } + + function initializeBeams() { + const canvas = document.getElementById("heroAuroraCanvas"); + if (!canvas || !window.OSGBeamsHero || !window.THREE) return; + + try { + beams = window.OSGBeamsHero.create(canvas, { + beamWidth: 4.5, + beamHeight: 6, + beamNumber: 26, + lightColor: "#72F49D", + speed: 4.5, + noiseIntensity: 2.65, + scale: 0.62, + rotation: 208, + background: "#020805", + reduceMotion + }); + } catch (error) { + console.warn("OSGKeyboard hero enhancement unavailable.", error); + canvas.hidden = true; + } + } + + function initializeVideos() { + document.querySelectorAll("video[autoplay]").forEach((video) => { + if (reduceMotion) { + video.autoplay = false; + video.pause(); + return; + } + + const observer = new IntersectionObserver((entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + video.play().catch(() => {}); + } else { + video.pause(); + } + }); + }, { threshold: 0.15 }); + observer.observe(video); + }); + } + + const initialLanguage = preferredLanguage(); + const initialTheme = storedTheme() || (themeMedia.matches ? "dark" : "light"); + applyLanguage(initialLanguage, false); + applyTheme(initialTheme, false); + initializeBeams(); + initializeVideos(); + + languageButton?.addEventListener("click", () => { + applyLanguage(root.dataset.lang === "zh" ? "en" : "zh", true); + applyTheme(root.dataset.theme, false); + }); + + themeButton?.addEventListener("click", () => { + applyTheme(root.dataset.theme === "dark" ? "light" : "dark", true); + }); + + themeMedia.addEventListener("change", (event) => { + if (!storedTheme()) applyTheme(event.matches ? "dark" : "light", false); + }); + + document.addEventListener("visibilitychange", () => { + beams?.setVisible(!document.hidden); + }); +})(); diff --git a/docs/index.html b/docs/index.html index 9c03c8c..936a343 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,129 +1,108 @@ - +
- -