fix(release): prepare 1.8 disclosures and licensed resources
Replace the unclear CLM source, align release and privacy disclosures, complete third-party notices, and keep the expanded test suites deterministic for release validation.
This commit is contained in:
@@ -14,7 +14,7 @@ writing commit messages that will ship to users.
|
|||||||
|
|
||||||
### Version format
|
### Version format
|
||||||
|
|
||||||
The current source-of-truth version is **1.7.5 (build 71)**. Releases use stable SemVer:
|
The current source-of-truth version is **1.7.5 (build 72)**. Releases use stable SemVer:
|
||||||
|
|
||||||
| Field | File | Rule |
|
| Field | File | Rule |
|
||||||
|-------|------|------|
|
|-------|------|------|
|
||||||
|
|||||||
+25
-1
@@ -1,4 +1,8 @@
|
|||||||
# Third-party notices — Typing keyboard
|
# Third-party notices — OSGKeyboard
|
||||||
|
|
||||||
|
OSGKeyboard itself is source-available under the repository `LICENSE`; it is
|
||||||
|
not an open-source or MIT-licensed application. The notices below apply only
|
||||||
|
to the named third-party components or explicitly identified data subsets.
|
||||||
|
|
||||||
The generated Chinese source manifest is
|
The generated Chinese source manifest is
|
||||||
`OSGKeyboard/Resources/Typing/Rime/manifest.json`. Exact librime
|
`OSGKeyboard/Resources/Typing/Rime/manifest.json`. Exact librime
|
||||||
@@ -14,6 +18,7 @@ binary dependency licenses are bundled beside `NOTICE.txt`.
|
|||||||
| [fxsjy/jieba](https://github.com/fxsjy/jieba) | `67fa2e3…` | MIT | Modern Simplified-Chinese word frequencies |
|
| [fxsjy/jieba](https://github.com/fxsjy/jieba) | `67fa2e3…` | MIT | Modern Simplified-Chinese word frequencies |
|
||||||
| [phrase-pinyin-data](https://github.com/mozillazg/phrase-pinyin-data) | `cee0ed6…` | MIT | Phrase pronunciations |
|
| [phrase-pinyin-data](https://github.com/mozillazg/phrase-pinyin-data) | `cee0ed6…` | MIT | Phrase pronunciations |
|
||||||
| [pinyin-data](https://github.com/mozillazg/pinyin-data) | `923b108…` | MIT | Character-pronunciation fallback |
|
| [pinyin-data](https://github.com/mozillazg/pinyin-data) | `923b108…` | MIT | Character-pronunciation fallback |
|
||||||
|
| [Google Material Icons](https://github.com/google/material-design-icons) | Bundled font snapshot | Apache-2.0 | iOS Settings and navigation iconography |
|
||||||
|
|
||||||
`Scripts/typing/build_rime_dictionary.py` deterministically merges these
|
`Scripts/typing/build_rime_dictionary.py` deterministically merges these
|
||||||
sources into `osg_pinyin.dict.yaml`; its manifest records every source URL,
|
sources into `osg_pinyin.dict.yaml`; its manifest records every source URL,
|
||||||
@@ -28,6 +33,10 @@ commit, SHA-256 and output SHA-256.
|
|||||||
- Microsoft/Sogou mappings were independently encoded from public key-map
|
- Microsoft/Sogou mappings were independently encoded from public key-map
|
||||||
specifications; GPL schema files were not copied.
|
specifications; GPL schema files were not copied.
|
||||||
- Eight opt-in fuzzy-pinyin rule groups.
|
- Eight opt-in fuzzy-pinyin rule groups.
|
||||||
|
- Project-curated bilingual AI/technology speech phrases under
|
||||||
|
`Scripts/lexicon/seeds/ai_tech_brands_seed.tsv`. This data subset and its
|
||||||
|
generated CLM phrase list are MIT-licensed; that grant does not change the
|
||||||
|
source-available license of the app.
|
||||||
- Offline English typing data under
|
- Offline English typing data under
|
||||||
`OSGKeyboardShared/Resources/Typing/English/`:
|
`OSGKeyboardShared/Resources/Typing/English/`:
|
||||||
- `english_lexicon.bin` — mmap-friendly binary of the top 40k alphabetic
|
- `english_lexicon.bin` — mmap-friendly binary of the top 40k alphabetic
|
||||||
@@ -40,6 +49,21 @@ commit, SHA-256 and output SHA-256.
|
|||||||
- Rebuild with `python3 Scripts/typing/build_english_lexicon.py`
|
- Rebuild with `python3 Scripts/typing/build_english_lexicon.py`
|
||||||
(add `--from-tsv` to compile the binary from existing TSV without network)
|
(add `--from-tsv` to compile the binary from existing TSV without network)
|
||||||
|
|
||||||
|
## macOS local speech stack
|
||||||
|
|
||||||
|
| Component | Pinned version | License | Purpose |
|
||||||
|
|-----------|----------------|---------|---------|
|
||||||
|
| [mlx-audio-swift](https://github.com/Blaizzy/mlx-audio-swift) | `d302a5c…` | MIT | Qwen3 streaming ASR integration |
|
||||||
|
| [mlx-swift](https://github.com/ml-explore/mlx-swift) | `0.31.3` | MIT | Apple MLX tensor runtime |
|
||||||
|
| [mlx-swift-lm](https://github.com/ml-explore/mlx-swift-lm) | `3.31.3` | MIT | MLX model utilities |
|
||||||
|
| [swift-transformers](https://github.com/huggingface/swift-transformers) | `1.1.9` | Apache-2.0 | Tokenizer and model utilities |
|
||||||
|
| [swift-huggingface](https://github.com/huggingface/swift-huggingface) | `0.8.1` | Apache-2.0 | Model download client |
|
||||||
|
| [Qwen3-ASR 0.6B / 1.7B MLX 4-bit](https://huggingface.co/mlx-community) | Runtime download | Apache-2.0 | Optional on-device speech-model weights |
|
||||||
|
|
||||||
|
The Qwen models are downloaded only after the user selects a local model on
|
||||||
|
macOS. They are not committed to this repository. Their model cards identify
|
||||||
|
the original Qwen3-ASR model and the mlx-community conversion.
|
||||||
|
|
||||||
|
|
||||||
## Reference only
|
## Reference only
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -1,14 +1,13 @@
|
|||||||
{
|
{
|
||||||
"bin_bytes" : 198494,
|
"bin_bytes" : 41456,
|
||||||
"bin_file" : "OSGKeyboardCLM.bin",
|
"bin_file" : "OSGKeyboardCLM.bin",
|
||||||
"export_seconds" : 0.035165071487426758,
|
"export_seconds" : 0.030717015266418457,
|
||||||
"generated_at" : "2026-07-06T13:16:27Z",
|
"generated_at" : "2026-08-14T15:07:23Z",
|
||||||
"identifier" : "com.osgkeyboard.custom-lm.v1",
|
"identifier" : "com.osgkeyboard.custom-lm.v1",
|
||||||
"locale" : "zh_CN",
|
"locale" : "zh_CN",
|
||||||
"phrase_count" : 13329,
|
"phrase_count" : 3040,
|
||||||
"sources" : {
|
"sources" : {
|
||||||
"ai_tech_seed" : 3040,
|
"ai_tech_seed" : 3040
|
||||||
"computer_terms" : 10300
|
|
||||||
},
|
},
|
||||||
"version" : "1.0.0"
|
"version" : "1.0.1"
|
||||||
}
|
}
|
||||||
@@ -1,22 +1,22 @@
|
|||||||
{
|
{
|
||||||
"version": "v1",
|
"entry_count" : 3040,
|
||||||
"generated_at": "2026-07-05T11:27:48.823095+00:00",
|
"files" : {
|
||||||
"locale": "zh-Hans",
|
"phrases" : "phrases.tsv"
|
||||||
"entry_count": 10300,
|
},
|
||||||
"sources": [
|
"generated_at" : "2026-08-14T15:07:23Z",
|
||||||
|
"locale" : "zh-Hans",
|
||||||
|
"notes" : [
|
||||||
|
"Project-curated bilingual AI brands, technology terms, companies, and names.",
|
||||||
|
"No third-party cell dictionaries or Sogou-derived data.",
|
||||||
|
"PhraseCount weights map to SFCustomLanguageModelData relative frequencies."
|
||||||
|
],
|
||||||
|
"sources" : [
|
||||||
{
|
{
|
||||||
"key": "computer_terms",
|
"key" : "ai_tech_seed",
|
||||||
"label": "计算机词汇大全【官方推荐】",
|
"label" : "OSGKeyboard curated AI\/tech lexicon",
|
||||||
"weight": 5,
|
"license" : "MIT (curated seed; OSGKeyboard contributors)",
|
||||||
"raw_count": 10300
|
"raw_count" : 3040
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"notes": [
|
"version" : "v1"
|
||||||
"Domain-specific computer/IT vocabulary only; casual network slang and Sogou popular words removed.",
|
}
|
||||||
"PhraseCount weights map to SFCustomLanguageModelData relative frequencies.",
|
|
||||||
"Merged with ai-tech-brands seed at export time for the final .bin asset."
|
|
||||||
],
|
|
||||||
"files": {
|
|
||||||
"phrases": "phrases.tsv"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -1,14 +1,13 @@
|
|||||||
{
|
{
|
||||||
"bin_bytes" : 198494,
|
"bin_bytes" : 41456,
|
||||||
"bin_file" : "OSGKeyboardCLM.bin",
|
"bin_file" : "OSGKeyboardCLM.bin",
|
||||||
"export_seconds" : 0.035165071487426758,
|
"export_seconds" : 0.030717015266418457,
|
||||||
"generated_at" : "2026-07-06T13:16:27Z",
|
"generated_at" : "2026-08-14T15:07:23Z",
|
||||||
"identifier" : "com.osgkeyboard.custom-lm.v1",
|
"identifier" : "com.osgkeyboard.custom-lm.v1",
|
||||||
"locale" : "zh_CN",
|
"locale" : "zh_CN",
|
||||||
"phrase_count" : 13329,
|
"phrase_count" : 3040,
|
||||||
"sources" : {
|
"sources" : {
|
||||||
"ai_tech_seed" : 3040,
|
"ai_tech_seed" : 3040
|
||||||
"computer_terms" : 10300
|
|
||||||
},
|
},
|
||||||
"version" : "1.0.0"
|
"version" : "1.0.1"
|
||||||
}
|
}
|
||||||
@@ -22,27 +22,28 @@
|
|||||||
<p class="lang"><a href="#zh">中文</a> · <a href="#top">English</a></p>
|
<p class="lang"><a href="#zh">中文</a> · <a href="#top">English</a></p>
|
||||||
<div id="top">
|
<div id="top">
|
||||||
<h1>OSGKeyboard Privacy Policy</h1>
|
<h1>OSGKeyboard Privacy Policy</h1>
|
||||||
<p><strong>Last updated:</strong> August 12, 2026</p>
|
<p><strong>Last updated:</strong> August 14, 2026 · <em>v1.3</em></p>
|
||||||
<p>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.</p>
|
<p>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.</p>
|
||||||
|
|
||||||
<h2>What we collect</h2>
|
<h2>What we collect</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>Voice audio</strong> — 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.</li>
|
<li><strong>Voice audio</strong> — captured only while you actively record. On a local engine, audio is transcribed on-device and raw audio is not uploaded. If you actively select a cloud engine in Settings, recordings are sent directly to the ASR provider you configure; that provider’s privacy policy applies. OSGKeyboard does not store or proxy audio on its own servers.</li>
|
||||||
<li><strong>Transcribed text</strong> — when AI polish is enabled, the final text (not audio) is sent to the LLM provider whose API key you configured (e.g. OpenAI, DeepSeek) for punctuation and formatting. Without an API key, raw ASR text is inserted and no polish request is sent.</li>
|
<li><strong>Transcribed text and cursor context</strong> — after ASR, the transcript (not audio) may be sent for polish when you have configured an LLM API key. A small amount of text around the cursor may be included to continue naturally. Secure fields are never captured, and cursor context is not written to logs or voice history. Without an API key, raw ASR text is inserted.</li>
|
||||||
<li><strong>AI mode questions</strong> — in AI keyboard mode, your spoken question text—or a question you tap from an idle suggestion—is sent to the same configured LLM provider to generate an answer. When that provider supports server-side web search, the provider may retrieve public web results to answer time-sensitive questions. Search queries and retrieved snippets are processed by that provider under its own privacy policy; OSGKeyboard does not operate a search index or proxy search traffic.</li>
|
<li><strong>AI mode questions</strong> — in AI keyboard mode, your spoken question text—or a question you tap from an idle suggestion—is sent to the same configured LLM provider to generate an answer. When that provider supports server-side web search, the provider may retrieve public web results to answer time-sensitive questions. Search queries and retrieved snippets are processed by that provider under its own privacy policy; OSGKeyboard does not operate a search index or proxy search traffic.</li>
|
||||||
<li><strong>AI idle suggestions</strong> — the main app may periodically download public hint titles (e.g. hot topics) from OSGKeyboard’s hint feed and, using your configured polish LLM, compress them into short on-device suggestion labels. Suggestion packs are cached in the App Group for the keyboard; the keyboard extension does not fetch the feed itself.</li>
|
<li><strong>AI suggestions and OSGKeyboard web pages</strong> — the main app may request public suggestion titles from <code>key.osglab.com</code>; no voice, transcript, clipboard body, API key, or ordinary keystroke content is included. Opening the update page requests public content from <code>download.osglab.com</code>. Those hosts may process ordinary connection metadata such as IP address and request time.</li>
|
||||||
<li><strong>Clipboard history (opt-in)</strong> — when you enable Clipboard History, the keyboard may read plain-text pasteboard content while it is visible and keep recent copies on device for the history panel and optional suggestion strip. Within about 30 seconds after a copy, AI mode may also offer clipboard-related idle suggestions; tapping one sends the clipboard text with that prompt to your configured LLM. Saying “clipboard” in an AI question does the same, because naming it is how you choose that text; every other AI question is sent without it. In both cases the clipboard body travels as separate quoted data, never as instructions. Clipboard history is local-only and not synced via iCloud.</li>
|
<li><strong>AI Agent skills, Shortcuts, and maps</strong> — when you deliberately run a skill, the selected or copied text and skill prompt are sent to your configured LLM provider. Reminders, Calendar, and Notes exports pass the generated result on-device to an Apple Shortcut you install. Navigation opens an address or route in Apple Maps, Amap, or Baidu Maps; the selected provider's policy applies.</li>
|
||||||
<li><strong>API credentials</strong> — 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).</li>
|
<li><strong>API credentials</strong> — 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).</li>
|
||||||
<li><strong>App preferences</strong> — engine mode, language, and keyboard settings stored in App Group UserDefaults. Optional iCloud sync mirrors eligible preferences, usage statistics, and voice history through your private iCloud account. Clipboard-history consent and its suggestion-strip switch stay device-local and are not activated by iCloud settings sync.</li>
|
<li><strong>App preferences</strong> — engine mode, language, and keyboard settings stored in App Group UserDefaults. Optional iCloud sync mirrors eligible preferences, usage statistics, and voice history through your private iCloud account. Clipboard-history consent and its suggestion-strip switch stay device-local and are not activated by iCloud settings sync.</li>
|
||||||
<li><strong>Optional clipboard history</strong> — off by default. When you turn it on, the keyboard may read text from the clipboard on this device or from Universal Clipboard; iOS does not provide a reliable way to distinguish those sources. Up to 15 accepted text items are stored only in the local App Group shared by this device’s host app and keyboard extension. Turning history off stops capture, turns off the suggestion strip, and keeps existing items. Reset Settings also keeps them; only the separate confirmed “Clear clipboard history” action removes them. There is no fixed expiry. Secure fields immediately hide clipboard UI and are not captured. Conservative filters reject common OTP shapes, private-key headers, JWTs, Bearer tokens, recognizable provider-key prefixes, and common Luhn-valid 16-digit card numbers, but cannot identify every password or secret. A rejected item can still be pasted through iOS; it is simply not added to history. Clipboard history is not automatically sent to AI. If you insert an item and then actively use polish, the inserted text may be included as context sent to the provider you configured.</li>
|
<li><strong>Optional clipboard history</strong> — off by default. When you turn it on, the keyboard may read text from the clipboard on this device or from Universal Clipboard; iOS does not provide a reliable way to distinguish those sources. Up to 15 accepted text items are stored only in the local App Group shared by this device’s host app and keyboard extension. Turning history off stops capture, turns off the suggestion strip, and keeps existing items. Reset Settings also keeps them; only the separate confirmed “Clear clipboard history” action removes them. There is no fixed expiry. Secure fields immediately hide clipboard UI and are not captured. Conservative filters reject common OTP shapes, private-key headers, JWTs, Bearer tokens, recognizable provider-key prefixes, and common Luhn-valid 16-digit card numbers, but cannot identify every password or secret. A rejected item can still be pasted through iOS; it is simply not added to history. Clipboard history is not automatically sent to AI. If you insert an item and then actively use polish, the inserted text may be included as context sent to the provider you configured.</li>
|
||||||
<li><strong>On-device typing learning</strong> — the Chinese keyboard stores selected words and candidate frequencies in the App Group on your device. OSGKeyboard does not upload this user dictionary.</li>
|
<li><strong>Personal dictionary and typing learning</strong> — personal terms may sync through your private iCloud account when enabled. Chinese candidate frequencies and English suggestion choices remain in the local App Group and can be cleared separately without deleting the personal dictionary. OSGKeyboard does not upload this learning data.</li>
|
||||||
|
<li><strong>macOS local speech models</strong> — when selected, Qwen3-ASR model files are downloaded from Hugging Face and remain in the Mac app's local application-support directory until you remove them. Local inference does not send your audio to Hugging Face or OSGKeyboard.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>Clipboard sensitive-content filtering is applied to newly captured items. Existing history is retained until you use the confirmed clear action.</p>
|
<p>Clipboard sensitive-content filtering is applied to newly captured items. Existing history is retained until you use the confirmed clear action.</p>
|
||||||
|
|
||||||
<h2>What we do not collect</h2>
|
<h2>What we do not collect</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>We do <strong>not</strong> log or upload ordinary keystrokes. Only the on-device Chinese candidate-learning data described above is retained locally.</li>
|
<li>We do <strong>not</strong> log or upload ordinary keystrokes.</li>
|
||||||
<li>We do <strong>not</strong> operate analytics or advertising SDKs.</li>
|
<li>We do <strong>not</strong> operate analytics, crash-reporting, or advertising SDKs.</li>
|
||||||
<li>We do <strong>not</strong> sell personal data.</li>
|
<li>We do <strong>not</strong> sell personal data.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
@@ -54,12 +55,13 @@
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2>Third parties</h2>
|
<h2>Third parties</h2>
|
||||||
<p>When you enable cloud recognition, recordings are sent directly to the speech provider you configure. When you configure an LLM API key, transcribed text may be sent for polish, and AI-mode questions may be sent for answering (including optional provider-side web search). Those providers’ privacy policies apply to the requests.</p>
|
<p>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 <code>key.osglab.com</code> and <code>download.osglab.com</code>. Optional Mac models are downloaded from Hugging Face. Each third party's privacy policy applies; OSGKeyboard does not proxy, log, or aggregate provider requests.</p>
|
||||||
|
|
||||||
<h2>Data retention</h2>
|
<h2>Data retention</h2>
|
||||||
<p>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.</p>
|
<p>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.</p>
|
||||||
<p><strong>Voice history</strong> — 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.</p>
|
<p><strong>Voice history</strong> — 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.</p>
|
||||||
<p><strong>Clipboard history</strong> — 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.</p>
|
<p><strong>Clipboard history</strong> — 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.</p>
|
||||||
|
<p><strong>Typing learning and Mac models</strong> — 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.</p>
|
||||||
|
|
||||||
<h2>Contact</h2>
|
<h2>Contact</h2>
|
||||||
<p>Questions: open an issue at <a href="https://github.com/hkgood/OSGKeyboard">github.com/hkgood/OSGKeyboard</a>.</p>
|
<p>Questions: open an issue at <a href="https://github.com/hkgood/OSGKeyboard">github.com/hkgood/OSGKeyboard</a>.</p>
|
||||||
@@ -67,26 +69,28 @@
|
|||||||
|
|
||||||
<hr id="zh">
|
<hr id="zh">
|
||||||
<h1>OSGKeyboard 隐私政策</h1>
|
<h1>OSGKeyboard 隐私政策</h1>
|
||||||
<p><strong>更新日期:</strong>2026 年 8 月 12 日</p>
|
<p><strong>更新日期:</strong>2026 年 8 月 14 日 · <em>v1.3</em></p>
|
||||||
<p>OSGKeyboard 是一款 iOS 自定义键盘,可将语音转为文字。本政策说明应用处理哪些数据及用途。</p>
|
<p>OSGKeyboard 包含 iOS 自定义键盘与 macOS 菜单栏听写应用。iOS 默认使用 Apple 端侧语音能力;macOS 可使用下载到本机的 Qwen3-ASR 模型。你也可以在设置中主动选择云端识别引擎。转写后,文字可能通过你配置的服务商用于润色、翻译或 AI Agent 功能。</p>
|
||||||
|
|
||||||
<h2>我们处理的数据</h2>
|
<h2>我们处理的数据</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>语音音频</strong> — 仅在你主动录音时采集。默认本地模式通过 Apple 语音能力在设备端转写,不会上传原始录音。若你主动启用云端识别,录音会发送到你配置的语音服务商完成转写,并适用该服务商的隐私政策。OSGKeyboard 自身不会存储或中转音频。</li>
|
<li><strong>语音音频</strong> — 仅在你主动录音时采集。使用本地引擎时,音频在设备端转写且原始录音不会上传。若你在设置中主动选择云端引擎,录音会直接发送到你配置的识别服务商。OSGKeyboard 自身不存储或中转音频。</li>
|
||||||
<li><strong>转写文字</strong> — 当你配置了 LLM API Key 并启用润色时,最终文字(非音频)会发送到该服务商以整理标点和格式。未填写 API Key 时直接插入原始识别结果,不会发起润色请求。</li>
|
<li><strong>转写文字与光标上下文</strong> — 配置 LLM API Key 后,转写文字(非音频)可能发送润色,并可能包含光标附近少量文字以自然衔接。密码框不会采集,光标上下文不会写入日志或语音历史。未填写 API Key 时直接插入原始识别结果。</li>
|
||||||
<li><strong>AI 模式问题</strong> — 在 AI 键盘模式下,语音转写后的问题文字,或你点选空闲建议后生成的提问,会发送到同一套已配置的 LLM 服务商以生成回答。若该服务商支持服务端联网搜索,可能为回答时效性问题检索公开网页结果。搜索词与检索片段由该服务商按其隐私政策处理;OSGKeyboard 不运营搜索索引,也不中转搜索流量。</li>
|
<li><strong>AI 模式问题</strong> — 在 AI 键盘模式下,语音转写后的问题文字,或你点选空闲建议后生成的提问,会发送到同一套已配置的 LLM 服务商以生成回答。若该服务商支持服务端联网搜索,可能为回答时效性问题检索公开网页结果。搜索词与检索片段由该服务商按其隐私政策处理;OSGKeyboard 不运营搜索索引,也不中转搜索流量。</li>
|
||||||
<li><strong>AI 空闲建议</strong> — 主 App 可能定期从 OSGKeyboard 热点建议源下载公开标题,并使用你配置的润色 LLM 压缩为短标签,缓存在 App Group 供键盘读取;键盘扩展本身不会直接请求该源。</li>
|
<li><strong>AI 建议与 OSGKeyboard 网页</strong> — 主 App 可能从 <code>key.osglab.com</code> 请求公开建议标题;请求不包含语音、转写、剪贴板正文、API Key 或普通击键内容。打开更新页时会从 <code>download.osglab.com</code> 请求公开内容。相关主机可能处理 IP 地址、请求时间等普通连接元数据。</li>
|
||||||
<li><strong>剪贴板历史(可选)</strong> — 当你开启「剪贴板历史」后,键盘在可见期间可能读取纯文本粘贴板内容并在本机保存,供历史面板与可选建议条使用。复制后约 30 秒内,AI 模式也可能展示剪贴板相关空闲建议;点选后会将剪贴板正文与提示一并发送到你配置的 LLM。在 AI 提问中明确说出「剪贴板」同样如此——说出即代表你选择了这段材料;其余 AI 提问不会附带剪贴板。两种情况下剪贴板正文都作为单独引用的数据发送,绝不作为指令。剪贴板历史仅存本机,不经 iCloud 同步。</li>
|
<li><strong>AI Agent 技能、快捷指令与地图</strong> — 仅在你主动运行技能时,所选或复制文字及技能提示词才会发送到你配置的 LLM。提醒事项、日历与备忘录导出会在设备端把生成结果交给你安装的 Apple 快捷指令;导航会在 Apple 地图、高德地图或百度地图中打开地址或路线。</li>
|
||||||
<li><strong>API 凭证</strong> — 保存在设备 Keychain,在主 App 与键盘扩展间共享。开启 iCloud 设置同步后,经 iCloud 钥匙串同步(非 iCloud KVS JSON)。</li>
|
<li><strong>API 凭证</strong> — 保存在设备 Keychain,在主 App 与键盘扩展间共享。开启 iCloud 设置同步后,经 iCloud 钥匙串同步(非 iCloud KVS JSON)。</li>
|
||||||
<li><strong>应用偏好</strong> — 引擎、语言等设置保存在 App Group。可选 iCloud 同步经私有 iCloud 账户镜像可同步的偏好、统计与语音历史。剪贴板历史采集许可与建议条开关仅属于本机,不会被 iCloud 设置同步开启。</li>
|
<li><strong>应用偏好</strong> — 引擎、语言等设置保存在 App Group。可选 iCloud 同步经私有 iCloud 账户镜像可同步的偏好、统计与语音历史。剪贴板历史采集许可与建议条开关仅属于本机,不会被 iCloud 设置同步开启。</li>
|
||||||
<li><strong>可选剪贴板历史</strong> — 默认关闭。开启后,键盘可能读取本机剪贴板或通用剪贴板中的文字;iOS 无法可靠区分两者来源。最多 15 条通过规则的文本仅保存在本机主 App 与键盘扩展共享的 App Group。关闭历史只会停止采集、关闭建议条并保留已有记录;重置设置同样不会清除,只有单独确认的「清空剪贴板历史」操作会删除。历史没有固定过期时间。进入安全输入框会立即隐藏剪贴板入口与正文,且不会采集。保守过滤会拒绝常见 OTP 形态、私钥头、JWT、Bearer Token、具有明确服务商前缀的密钥以及常见的通过 Luhn 校验的 16 位卡号,但无法识别所有密码或秘密。被拒绝的内容仍可通过 iOS 一次性粘贴,只是不进入历史。剪贴板历史不会自动发送给 AI;插入后若主动使用润色,已插入文字可能作为上下文发送给你配置的服务商。</li>
|
<li><strong>可选剪贴板历史</strong> — 默认关闭。开启后,键盘可能读取本机剪贴板或通用剪贴板中的文字;iOS 无法可靠区分两者来源。最多 15 条通过规则的文本仅保存在本机主 App 与键盘扩展共享的 App Group。关闭历史只会停止采集、关闭建议条并保留已有记录;重置设置同样不会清除,只有单独确认的「清空剪贴板历史」操作会删除。历史没有固定过期时间。进入安全输入框会立即隐藏剪贴板入口与正文,且不会采集。保守过滤会拒绝常见 OTP 形态、私钥头、JWT、Bearer Token、具有明确服务商前缀的密钥以及常见的通过 Luhn 校验的 16 位卡号,但无法识别所有密码或秘密。被拒绝的内容仍可通过 iOS 一次性粘贴,只是不进入历史。剪贴板历史不会自动发送给 AI;插入后若主动使用润色,已插入文字可能作为上下文发送给你配置的服务商。</li>
|
||||||
|
<li><strong>个性词库与本地输入学习</strong> — 个性词条可在开启后经你的私有 iCloud 账户同步。中文候选频率与英文候选选择偏好保存在本机 App Group,可在输入设置中单独清除且不影响个性词库,OSGKeyboard 不会上传这些学习数据。</li>
|
||||||
|
<li><strong>macOS 本地语音模型</strong> — 选择后,Qwen3-ASR 模型会从 Hugging Face 下载到 Mac 本地应用支持目录,直到你主动移除。本地推理不会把录音发送给 Hugging Face 或 OSGKeyboard。</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>剪贴板敏感内容过滤仅在新内容采集时执行;已有历史会继续保留,直到你使用带确认的清空操作。</p>
|
<p>剪贴板敏感内容过滤仅在新内容采集时执行;已有历史会继续保留,直到你使用带确认的清空操作。</p>
|
||||||
|
|
||||||
<h2>我们不收集的内容</h2>
|
<h2>我们不收集的内容</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>我们<strong>不会</strong>记录或上传你平时在键盘上的击键内容。</li>
|
<li>我们<strong>不会</strong>记录或上传你平时在键盘上的击键内容。</li>
|
||||||
<li>我们<strong>不会</strong>集成广告或第三方分析 SDK。</li>
|
<li>我们<strong>不会</strong>集成分析、崩溃上报或广告 SDK。</li>
|
||||||
<li>我们<strong>不会</strong>出售个人数据。</li>
|
<li>我们<strong>不会</strong>出售个人数据。</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
@@ -98,12 +102,13 @@
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2>第三方</h2>
|
<h2>第三方</h2>
|
||||||
<p>启用云端识别时,录音会直接发送到你配置的语音服务商;配置 LLM API Key 后,转写文字可能用于润色,AI 模式问题可能用于生成回答(含服务商侧可选联网搜索)。相关请求适用对应服务商的隐私政策。</p>
|
<p>云端 ASR 与 LLM 请求直接发送到你配置的服务商。AI 技能也可能把生成数据交给 Apple 快捷指令,或在 Apple 地图、高德地图、百度地图中打开路线。公开建议与更新页面分别由 <code>key.osglab.com</code>、<code>download.osglab.com</code> 提供;可选 Mac 模型从 Hugging Face 下载。各第三方的隐私政策适用。</p>
|
||||||
|
|
||||||
<h2>数据保留</h2>
|
<h2>数据保留</h2>
|
||||||
<p>设置保留在设备上,直至卸载或重置。开启 iCloud 同步后,API 密钥走 iCloud 钥匙串;偏好、统计与历史可能经私有 iCloud 账户同步。</p>
|
<p>设置保留在设备上,直至卸载或重置。开启 iCloud 同步后,API 密钥走 iCloud 钥匙串;可同步的偏好、统计、词库与语音历史可能经私有 iCloud 账户同步。</p>
|
||||||
<p><strong>语音历史</strong> — 成功转写可保存在主 App「历史」页(最多 300 条)。开启 iCloud 设置同步后,历史也可能在多设备间同步。</p>
|
<p><strong>语音历史</strong> — 成功转写可保存在主 App「历史」页(最多 300 条)。开启 iCloud 设置同步后,历史也可能在多设备间同步。</p>
|
||||||
<p><strong>剪贴板历史</strong> — 仅保存在本机 App Group,上限 15 条,不经 iCloud 同步,也没有固定过期时间。关闭功能或重置设置会保留已有记录;需使用单独确认的清空操作才能删除。</p>
|
<p><strong>剪贴板历史</strong> — 仅保存在本机 App Group,上限 15 条,不经 iCloud 同步,也没有固定过期时间。关闭功能或重置设置会保留已有记录;需使用单独确认的清空操作才能删除。</p>
|
||||||
|
<p><strong>输入学习与 Mac 模型</strong> — 中英文输入学习数据仅留本机,可在输入设置中单独清除。下载的 Mac 语音模型会保留到你移除模型或删除本地文件。</p>
|
||||||
|
|
||||||
<h2>联系</h2>
|
<h2>联系</h2>
|
||||||
<p>问题反馈:<a href="https://github.com/hkgood/OSGKeyboard">github.com/hkgood/OSGKeyboard</a></p>
|
<p>问题反馈:<a href="https://github.com/hkgood/OSGKeyboard">github.com/hkgood/OSGKeyboard</a></p>
|
||||||
|
|||||||
@@ -0,0 +1,205 @@
|
|||||||
|
Google Material Icons
|
||||||
|
Copyright 2014 Google LLC
|
||||||
|
https://github.com/google/material-design-icons
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
@@ -28,9 +28,23 @@ rime-ice, rime-double-pinyin, rime-luna-pinyin, rime-essay and KeyboardKit Pro.
|
|||||||
The Apache-2.0 and MIT license texts are available from the source links above;
|
The Apache-2.0 and MIT license texts are available from the source links above;
|
||||||
all required copyright and permission notices must remain with distributions.
|
all required copyright and permission notices must remain with distributions.
|
||||||
|
|
||||||
|
Interface iconography
|
||||||
|
---------------------
|
||||||
|
Google Material Icons are distributed under the Apache License 2.0.
|
||||||
|
Copyright 2014 Google LLC.
|
||||||
|
See LICENSE-MATERIAL-ICONS-APACHE.txt for the full attribution and terms.
|
||||||
|
|
||||||
English typing lexicon
|
English typing lexicon
|
||||||
----------------------
|
----------------------
|
||||||
english_lexicon.bin (from english_lexicon.tsv / english_bigrams.tsv) is an
|
english_lexicon.bin (from english_lexicon.tsv / english_bigrams.tsv) is an
|
||||||
OSG-curated mmap ranking table with synthetic relative frequency ranks for
|
OSG-curated mmap ranking table with synthetic relative frequency ranks for
|
||||||
offline autocomplete / autocorrect / next-word ranking. It is not derived
|
offline autocomplete / autocorrect / next-word ranking. It is not derived
|
||||||
from GPL/LGPL dictionaries.
|
from GPL/LGPL dictionaries.
|
||||||
|
|
||||||
|
Local speech lexicon
|
||||||
|
--------------------
|
||||||
|
The bundled custom-language-model phrase list is generated only from the
|
||||||
|
project-curated Scripts/lexicon/seeds/ai_tech_brands_seed.tsv. That data
|
||||||
|
subset is MIT-licensed by OSGKeyboard contributors. OSGKeyboard itself remains
|
||||||
|
source-available under the repository LICENSE and is not MIT-licensed or an
|
||||||
|
open-source application.
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ enum OpenSourceLicenseCatalog {
|
|||||||
purpose: "MaterialIcons-Regular.ttf bundled with the iOS app for Settings and navigation iconography.",
|
purpose: "MaterialIcons-Regular.ttf bundled with the iOS app for Settings and navigation iconography.",
|
||||||
url: URL(string: "https://github.com/google/material-design-icons"),
|
url: URL(string: "https://github.com/google/material-design-icons"),
|
||||||
licenseText: resourceText(
|
licenseText: resourceText(
|
||||||
named: "LICENSE-PINYIN-SIMP-APACHE",
|
named: "LICENSE-MATERIAL-ICONS-APACHE",
|
||||||
fallback: apache2Text
|
fallback: apache2Text
|
||||||
),
|
),
|
||||||
platforms: [.iOS]
|
platforms: [.iOS]
|
||||||
@@ -110,6 +110,17 @@ enum OpenSourceLicenseCatalog {
|
|||||||
licenseText: englishLexiconNoticeText,
|
licenseText: englishLexiconNoticeText,
|
||||||
platforms: [.iOS]
|
platforms: [.iOS]
|
||||||
),
|
),
|
||||||
|
.init(
|
||||||
|
id: "osg-ai-tech-lexicon",
|
||||||
|
name: "OSG AI/technology speech lexicon",
|
||||||
|
licenseName: "MIT (data subset only)",
|
||||||
|
purpose: "Project-curated bilingual names and technology terms used by the iOS custom language model and Mac local-ASR prompt bias. This notice applies only to the lexicon data, not to OSGKeyboard as a whole.",
|
||||||
|
url: URL(
|
||||||
|
string: "https://github.com/hkgood/OSGKeyboard/tree/main/Scripts/lexicon/seeds"
|
||||||
|
),
|
||||||
|
licenseText: curatedLexiconNoticeText,
|
||||||
|
platforms: [.iOS, .macOS]
|
||||||
|
),
|
||||||
.init(
|
.init(
|
||||||
id: "mlx-audio-swift",
|
id: "mlx-audio-swift",
|
||||||
name: "mlx-audio-swift",
|
name: "mlx-audio-swift",
|
||||||
@@ -119,6 +130,51 @@ enum OpenSourceLicenseCatalog {
|
|||||||
licenseText: mlxAudioMITText,
|
licenseText: mlxAudioMITText,
|
||||||
platforms: [.macOS]
|
platforms: [.macOS]
|
||||||
),
|
),
|
||||||
|
.init(
|
||||||
|
id: "mlx-swift",
|
||||||
|
name: "mlx-swift 0.31.3",
|
||||||
|
licenseName: "MIT",
|
||||||
|
purpose: "Apple MLX tensor and neural-network runtime used transitively by mlx-audio-swift.",
|
||||||
|
url: URL(string: "https://github.com/ml-explore/mlx-swift"),
|
||||||
|
licenseText: mitLicenseText(copyright: "Copyright (c) 2023 ml-explore"),
|
||||||
|
platforms: [.macOS]
|
||||||
|
),
|
||||||
|
.init(
|
||||||
|
id: "mlx-swift-lm",
|
||||||
|
name: "mlx-swift-lm 3.31.3",
|
||||||
|
licenseName: "MIT",
|
||||||
|
purpose: "MLX language-model utilities used transitively by the Mac local speech runtime.",
|
||||||
|
url: URL(string: "https://github.com/ml-explore/mlx-swift-lm"),
|
||||||
|
licenseText: mitLicenseText(copyright: "Copyright (c) 2024 ml-explore"),
|
||||||
|
platforms: [.macOS]
|
||||||
|
),
|
||||||
|
.init(
|
||||||
|
id: "swift-transformers",
|
||||||
|
name: "swift-transformers 1.1.9",
|
||||||
|
licenseName: "Apache-2.0",
|
||||||
|
purpose: "Tokenizer and model utilities used transitively by mlx-audio-swift.",
|
||||||
|
url: URL(string: "https://github.com/huggingface/swift-transformers"),
|
||||||
|
licenseText: apacheLicenseText(copyright: "Copyright 2022 Hugging Face SAS."),
|
||||||
|
platforms: [.macOS]
|
||||||
|
),
|
||||||
|
.init(
|
||||||
|
id: "swift-huggingface",
|
||||||
|
name: "swift-huggingface 0.8.1",
|
||||||
|
licenseName: "Apache-2.0",
|
||||||
|
purpose: "Hugging Face Hub client used to download the user-selected Mac speech model.",
|
||||||
|
url: URL(string: "https://github.com/huggingface/swift-huggingface"),
|
||||||
|
licenseText: apacheLicenseText(copyright: "Copyright 2025 Hugging Face SAS."),
|
||||||
|
platforms: [.macOS]
|
||||||
|
),
|
||||||
|
.init(
|
||||||
|
id: "qwen3-asr-mlx",
|
||||||
|
name: "Qwen3-ASR 0.6B / 1.7B MLX 4-bit models",
|
||||||
|
licenseName: "Apache-2.0",
|
||||||
|
purpose: "Optional speech-model weights downloaded at runtime for on-device Mac transcription. The MLX conversions are published by mlx-community from Qwen/Qwen3-ASR.",
|
||||||
|
url: URL(string: "https://huggingface.co/collections/mlx-community/qwen3-asr"),
|
||||||
|
licenseText: qwenModelApacheText,
|
||||||
|
platforms: [.macOS]
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
static func entries(for platform: Platform) -> [Entry] {
|
static func entries(for platform: Platform) -> [Entry] {
|
||||||
@@ -188,6 +244,48 @@ enum OpenSourceLicenseCatalog {
|
|||||||
\(mitText.components(separatedBy: "\n").dropFirst(2).joined(separator: "\n"))
|
\(mitText.components(separatedBy: "\n").dropFirst(2).joined(separator: "\n"))
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
static let curatedLexiconNoticeText = """
|
||||||
|
OSG AI/technology speech lexicon
|
||||||
|
|
||||||
|
Copyright (c) OSGKeyboard contributors
|
||||||
|
|
||||||
|
The phrase list under Scripts/lexicon/seeds/ai_tech_brands_seed.tsv and
|
||||||
|
its generated CustomLanguageModel phrase data are made available under
|
||||||
|
the MIT License below. This grant covers only that curated data subset.
|
||||||
|
OSGKeyboard itself remains source-available under the repository LICENSE
|
||||||
|
and is not distributed under the MIT License.
|
||||||
|
|
||||||
|
\(mitText.components(separatedBy: "\n").dropFirst(2).joined(separator: "\n"))
|
||||||
|
"""
|
||||||
|
|
||||||
|
static let qwenModelApacheText = """
|
||||||
|
mlx-community/Qwen3-ASR-0.6B-4bit and Qwen3-ASR-1.7B-4bit
|
||||||
|
|
||||||
|
Converted to MLX format from Qwen/Qwen3-ASR using mlx-audio 0.3.1.
|
||||||
|
Both model cards declare license: apache-2.0. Model weights are optional
|
||||||
|
runtime downloads and are not stored in the OSGKeyboard repository.
|
||||||
|
|
||||||
|
\(apacheLicenseText(copyright: "Copyright Qwen and model contributors."))
|
||||||
|
"""
|
||||||
|
|
||||||
|
private static func mitLicenseText(copyright: String) -> String {
|
||||||
|
"""
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
\(copyright)
|
||||||
|
|
||||||
|
\(mitText.components(separatedBy: "\n").dropFirst(2).joined(separator: "\n"))
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func apacheLicenseText(copyright: String) -> String {
|
||||||
|
let fullLicense = resourceText(
|
||||||
|
named: "LICENSE-PINYIN-SIMP-APACHE",
|
||||||
|
fallback: apache2Text
|
||||||
|
)
|
||||||
|
return "\(copyright)\n\n\(fullLicense)"
|
||||||
|
}
|
||||||
|
|
||||||
static let englishLexiconNoticeText = """
|
static let englishLexiconNoticeText = """
|
||||||
OSG English typing lexicon (project-owned notice)
|
OSG English typing lexicon (project-owned notice)
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ final class EnglishTypingTests: XCTestCase {
|
|||||||
@MainActor
|
@MainActor
|
||||||
func testChineseTypingDoesNotLoadEnglishLexicon() {
|
func testChineseTypingDoesNotLoadEnglishLexicon() {
|
||||||
EnglishLexicon.shared.unload()
|
EnglishLexicon.shared.unload()
|
||||||
let typing = TypingSessionController()
|
let typing = makeTypingSession(language: .chinese)
|
||||||
_ = typing.setLanguage(.chinese)
|
|
||||||
typing.enterTypingMode()
|
typing.enterTypingMode()
|
||||||
XCTAssertFalse(EnglishLexicon.shared.isLoaded)
|
XCTAssertFalse(EnglishLexicon.shared.isLoaded)
|
||||||
_ = typing.setLanguage(.english)
|
_ = typing.setLanguage(.english)
|
||||||
@@ -124,10 +123,10 @@ final class EnglishTypingTests: XCTestCase {
|
|||||||
func testEnglishShiftArmsAfterReturnLikeNotes() {
|
func testEnglishShiftArmsAfterReturnLikeNotes() {
|
||||||
// Simulates Notes: proxy preceding text gains a trailing newline after Return.
|
// Simulates Notes: proxy preceding text gains a trailing newline after Return.
|
||||||
var preceding = "Hello"
|
var preceding = "Hello"
|
||||||
let typing = TypingSessionController()
|
let typing = makeTypingSession()
|
||||||
typing.precedingTextProvider = { preceding }
|
typing.precedingTextProvider = { preceding }
|
||||||
typing.autocapitalizationModeProvider = { .sentences }
|
typing.autocapitalizationModeProvider = { .sentences }
|
||||||
_ = typing.setLanguage(.english)
|
typing.syncAutocapitalization()
|
||||||
XCTAssertFalse(typing.shiftActive, "mid-word should not arm Shift")
|
XCTAssertFalse(typing.shiftActive, "mid-word should not arm Shift")
|
||||||
|
|
||||||
_ = typing.handleReturn()
|
_ = typing.handleReturn()
|
||||||
@@ -144,10 +143,10 @@ final class EnglishTypingTests: XCTestCase {
|
|||||||
func testEnglishShiftArmsWhenProxyLagsAfterReturn() {
|
func testEnglishShiftArmsWhenProxyLagsAfterReturn() {
|
||||||
// Notes often still reports pre-Return context right after insertText("\n").
|
// Notes often still reports pre-Return context right after insertText("\n").
|
||||||
var preceding = "Hello"
|
var preceding = "Hello"
|
||||||
let typing = TypingSessionController()
|
let typing = makeTypingSession()
|
||||||
typing.precedingTextProvider = { preceding }
|
typing.precedingTextProvider = { preceding }
|
||||||
typing.autocapitalizationModeProvider = { .sentences }
|
typing.autocapitalizationModeProvider = { .sentences }
|
||||||
_ = typing.setLanguage(.english)
|
typing.syncAutocapitalization()
|
||||||
|
|
||||||
_ = typing.handleReturn()
|
_ = typing.handleReturn()
|
||||||
// Proxy intentionally stale — still "Hello" without "\n".
|
// Proxy intentionally stale — still "Hello" without "\n".
|
||||||
@@ -159,10 +158,10 @@ final class EnglishTypingTests: XCTestCase {
|
|||||||
@MainActor
|
@MainActor
|
||||||
func testEnglishShiftArmsWhenProxyLagsAfterPeriod() {
|
func testEnglishShiftArmsWhenProxyLagsAfterPeriod() {
|
||||||
var preceding = "Hello"
|
var preceding = "Hello"
|
||||||
let typing = TypingSessionController()
|
let typing = makeTypingSession()
|
||||||
typing.precedingTextProvider = { preceding }
|
typing.precedingTextProvider = { preceding }
|
||||||
typing.autocapitalizationModeProvider = { .sentences }
|
typing.autocapitalizationModeProvider = { .sentences }
|
||||||
_ = typing.setLanguage(.english)
|
typing.syncAutocapitalization()
|
||||||
|
|
||||||
_ = typing.handleKey(".")
|
_ = typing.handleKey(".")
|
||||||
typing.syncAutocapitalization(accountingForInsert: ".")
|
typing.syncAutocapitalization(accountingForInsert: ".")
|
||||||
@@ -171,9 +170,7 @@ final class EnglishTypingTests: XCTestCase {
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func testEnglishIdleShowsNoCandidatesUntilLetterTyped() {
|
func testEnglishIdleShowsNoCandidatesUntilLetterTyped() {
|
||||||
let typing = TypingSessionController()
|
let typing = makeTypingSession()
|
||||||
typing.suggestionsEnabled = true
|
|
||||||
_ = typing.setLanguage(.english)
|
|
||||||
typing.enterTypingMode()
|
typing.enterTypingMode()
|
||||||
XCTAssertTrue(typing.composition.candidates.isEmpty)
|
XCTAssertTrue(typing.composition.candidates.isEmpty)
|
||||||
|
|
||||||
@@ -202,9 +199,7 @@ final class EnglishTypingTests: XCTestCase {
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func testEnglishTypingEmitsCompletionsIntoComposition() {
|
func testEnglishTypingEmitsCompletionsIntoComposition() {
|
||||||
let typing = TypingSessionController()
|
let typing = makeTypingSession()
|
||||||
typing.suggestionsEnabled = true
|
|
||||||
_ = typing.setLanguage(.english)
|
|
||||||
typing.enterTypingMode()
|
typing.enterTypingMode()
|
||||||
|
|
||||||
_ = typing.handleKey("h")
|
_ = typing.handleKey("h")
|
||||||
@@ -219,11 +214,9 @@ final class EnglishTypingTests: XCTestCase {
|
|||||||
func testOldWordBackspaceRehydratesSuggestionsFromDocumentContext() {
|
func testOldWordBackspaceRehydratesSuggestionsFromDocumentContext() {
|
||||||
var preceding = "board"
|
var preceding = "board"
|
||||||
let following = "\n"
|
let following = "\n"
|
||||||
let typing = TypingSessionController()
|
let typing = makeTypingSession()
|
||||||
typing.suggestionsEnabled = true
|
|
||||||
typing.precedingTextProvider = { preceding }
|
typing.precedingTextProvider = { preceding }
|
||||||
typing.followingTextProvider = { following }
|
typing.followingTextProvider = { following }
|
||||||
_ = typing.setLanguage(.english)
|
|
||||||
typing.enterTypingMode()
|
typing.enterTypingMode()
|
||||||
typing.synchronizeEnglishDocumentContext(caretMoved: true)
|
typing.synchronizeEnglishDocumentContext(caretMoved: true)
|
||||||
|
|
||||||
@@ -259,11 +252,9 @@ final class EnglishTypingTests: XCTestCase {
|
|||||||
@MainActor
|
@MainActor
|
||||||
func testCandidateReplacementRejectsStaleDocumentAnchor() {
|
func testCandidateReplacementRejectsStaleDocumentAnchor() {
|
||||||
var preceding = ""
|
var preceding = ""
|
||||||
let typing = TypingSessionController()
|
let typing = makeTypingSession()
|
||||||
typing.suggestionsEnabled = true
|
|
||||||
typing.precedingTextProvider = { preceding }
|
typing.precedingTextProvider = { preceding }
|
||||||
typing.followingTextProvider = { "" }
|
typing.followingTextProvider = { "" }
|
||||||
_ = typing.setLanguage(.english)
|
|
||||||
typing.enterTypingMode()
|
typing.enterTypingMode()
|
||||||
|
|
||||||
for key in ["b", "o", "a"] {
|
for key in ["b", "o", "a"] {
|
||||||
@@ -286,11 +277,9 @@ final class EnglishTypingTests: XCTestCase {
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func testMidWordCaretSuppressesUnsafeBackwardOnlyReplacement() {
|
func testMidWordCaretSuppressesUnsafeBackwardOnlyReplacement() {
|
||||||
let typing = TypingSessionController()
|
let typing = makeTypingSession()
|
||||||
typing.suggestionsEnabled = true
|
|
||||||
typing.precedingTextProvider = { "boa" }
|
typing.precedingTextProvider = { "boa" }
|
||||||
typing.followingTextProvider = { "rd" }
|
typing.followingTextProvider = { "rd" }
|
||||||
_ = typing.setLanguage(.english)
|
|
||||||
typing.enterTypingMode()
|
typing.enterTypingMode()
|
||||||
|
|
||||||
typing.synchronizeEnglishDocumentContext(caretMoved: true)
|
typing.synchronizeEnglishDocumentContext(caretMoved: true)
|
||||||
@@ -302,11 +291,9 @@ final class EnglishTypingTests: XCTestCase {
|
|||||||
@MainActor
|
@MainActor
|
||||||
func testStaleHostCallbackDoesNotDiscardLocalEnglishWord() {
|
func testStaleHostCallbackDoesNotDiscardLocalEnglishWord() {
|
||||||
var preceding = ""
|
var preceding = ""
|
||||||
let typing = TypingSessionController()
|
let typing = makeTypingSession()
|
||||||
typing.suggestionsEnabled = true
|
|
||||||
typing.precedingTextProvider = { preceding }
|
typing.precedingTextProvider = { preceding }
|
||||||
typing.followingTextProvider = { "" }
|
typing.followingTextProvider = { "" }
|
||||||
_ = typing.setLanguage(.english)
|
|
||||||
typing.enterTypingMode()
|
typing.enterTypingMode()
|
||||||
|
|
||||||
let output = typing.handleKey("h")
|
let output = typing.handleKey("h")
|
||||||
@@ -337,7 +324,7 @@ final class EnglishTypingTests: XCTestCase {
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func testPeriodShortcutReplacesDoubleSpace() {
|
func testPeriodShortcutReplacesDoubleSpace() {
|
||||||
let typing = TypingSessionController()
|
let typing = makeTypingSession()
|
||||||
_ = typing.setLanguage(.english)
|
_ = typing.setLanguage(.english)
|
||||||
typing.enterTypingMode()
|
typing.enterTypingMode()
|
||||||
|
|
||||||
@@ -356,7 +343,7 @@ final class EnglishTypingTests: XCTestCase {
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func testPeriodShortcutDoesNotFireAfterAnotherLetter() {
|
func testPeriodShortcutDoesNotFireAfterAnotherLetter() {
|
||||||
let typing = TypingSessionController()
|
let typing = makeTypingSession()
|
||||||
_ = typing.setLanguage(.english)
|
_ = typing.setLanguage(.english)
|
||||||
typing.enterTypingMode()
|
typing.enterTypingMode()
|
||||||
|
|
||||||
@@ -369,8 +356,7 @@ final class EnglishTypingTests: XCTestCase {
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func testPeriodShortcutDoesNotFireAfterSentenceTerminator() {
|
func testPeriodShortcutDoesNotFireAfterSentenceTerminator() {
|
||||||
let typing = TypingSessionController()
|
let typing = makeTypingSession()
|
||||||
_ = typing.setLanguage(.english)
|
|
||||||
typing.precedingTextProvider = { "Hello." }
|
typing.precedingTextProvider = { "Hello." }
|
||||||
typing.enterTypingMode()
|
typing.enterTypingMode()
|
||||||
typing.syncAutocapitalization()
|
typing.syncAutocapitalization()
|
||||||
@@ -382,8 +368,7 @@ final class EnglishTypingTests: XCTestCase {
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func testChineseSpaceDoesNotUsePeriodShortcut() {
|
func testChineseSpaceDoesNotUsePeriodShortcut() {
|
||||||
let typing = TypingSessionController()
|
let typing = makeTypingSession(language: .chinese)
|
||||||
_ = typing.setLanguage(.chinese)
|
|
||||||
let first = typing.handleSpace()
|
let first = typing.handleSpace()
|
||||||
let second = typing.handleSpace()
|
let second = typing.handleSpace()
|
||||||
XCTAssertEqual(first.text, " ")
|
XCTAssertEqual(first.text, " ")
|
||||||
@@ -526,9 +511,7 @@ final class EnglishTypingTests: XCTestCase {
|
|||||||
let defaults = UserDefaults(suiteName: suite)!
|
let defaults = UserDefaults(suiteName: suite)!
|
||||||
defaults.removePersistentDomain(forName: suite)
|
defaults.removePersistentDomain(forName: suite)
|
||||||
let store = EnglishLearningStore(defaults: defaults)
|
let store = EnglishLearningStore(defaults: defaults)
|
||||||
let typing = TypingSessionController(learningStore: store)
|
let typing = makeTypingSession(learningStore: store)
|
||||||
typing.suggestionsEnabled = true
|
|
||||||
_ = typing.setLanguage(.english)
|
|
||||||
typing.enterTypingMode()
|
typing.enterTypingMode()
|
||||||
|
|
||||||
for character in ["t", "e", "h"] {
|
for character in ["t", "e", "h"] {
|
||||||
@@ -545,9 +528,7 @@ final class EnglishTypingTests: XCTestCase {
|
|||||||
let defaults = UserDefaults(suiteName: suite)!
|
let defaults = UserDefaults(suiteName: suite)!
|
||||||
defaults.removePersistentDomain(forName: suite)
|
defaults.removePersistentDomain(forName: suite)
|
||||||
let store = EnglishLearningStore(defaults: defaults)
|
let store = EnglishLearningStore(defaults: defaults)
|
||||||
let typing = TypingSessionController(learningStore: store)
|
let typing = makeTypingSession(learningStore: store)
|
||||||
typing.suggestionsEnabled = true
|
|
||||||
_ = typing.setLanguage(.english)
|
|
||||||
typing.enterTypingMode()
|
typing.enterTypingMode()
|
||||||
|
|
||||||
for character in ["t", "e", "h"] {
|
for character in ["t", "e", "h"] {
|
||||||
@@ -581,13 +562,28 @@ final class EnglishTypingTests: XCTestCase {
|
|||||||
let defaults = UserDefaults(suiteName: suite)!
|
let defaults = UserDefaults(suiteName: suite)!
|
||||||
defaults.removePersistentDomain(forName: suite)
|
defaults.removePersistentDomain(forName: suite)
|
||||||
let store = EnglishLearningStore(defaults: defaults)
|
let store = EnglishLearningStore(defaults: defaults)
|
||||||
let typing = TypingSessionController(learningStore: store)
|
let typing = makeTypingSession(learningStore: store)
|
||||||
typing.suggestionsEnabled = true
|
|
||||||
_ = typing.setLanguage(.english)
|
|
||||||
typing.enterTypingMode()
|
typing.enterTypingMode()
|
||||||
return typing
|
return typing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func makeTypingSession(
|
||||||
|
learningStore: EnglishLearningStore? = nil,
|
||||||
|
language: TypingInputLanguage = .english
|
||||||
|
) -> TypingSessionController {
|
||||||
|
let store = learningStore ?? EnglishLearningStore(
|
||||||
|
defaults: UserDefaults(suiteName: "english.tests.\(UUID().uuidString)")!
|
||||||
|
)
|
||||||
|
let typing = TypingSessionController(
|
||||||
|
engine: { NoopRimeEngine() },
|
||||||
|
learningStore: store
|
||||||
|
)
|
||||||
|
typing.suggestionsEnabled = true
|
||||||
|
_ = typing.setLanguage(language)
|
||||||
|
return typing
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func apply(_ typing: TypingSessionController, _ output: TypingOutput) {
|
private func apply(_ typing: TypingSessionController, _ output: TypingOutput) {
|
||||||
typing.syncAutocapitalization(
|
typing.syncAutocapitalization(
|
||||||
@@ -596,3 +592,26 @@ final class EnglishTypingTests: XCTestCase {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private final class NoopRimeEngine: RimeEngineBridging {
|
||||||
|
var composition: TypingComposition = .empty
|
||||||
|
var isReady = true
|
||||||
|
var schema: TypingInputSchema = .fullPinyin
|
||||||
|
|
||||||
|
func prepare() async throws {}
|
||||||
|
func teardown() { composition = .empty }
|
||||||
|
func setLanguage(_ language: TypingInputLanguage) {}
|
||||||
|
@discardableResult
|
||||||
|
func setSchema(_ schema: TypingInputSchema) -> Bool {
|
||||||
|
self.schema = schema
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
func processCharacter(_ character: Character) -> String? { nil }
|
||||||
|
func processBackspace() -> String? { nil }
|
||||||
|
func processSpace() -> String? { " " }
|
||||||
|
func processReturn() -> String? { "\n" }
|
||||||
|
func selectCandidate(at index: Int) -> String { "" }
|
||||||
|
func flushPreedit() -> String { "" }
|
||||||
|
func clearComposition() { composition = .empty }
|
||||||
|
}
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ final class KeyboardSurfaceStateTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testKeyRowsFollowTypingLanguageOnNumberPage() {
|
func testKeyRowsFollowTypingLanguageOnNumberPage() {
|
||||||
let typing = TypingSessionController()
|
let typing = TypingSessionController(engine: { TrackingStubRimeEngine() })
|
||||||
typing.setPage(.numbers)
|
typing.setPage(.numbers)
|
||||||
XCTAssertEqual(typing.keyRows[1], ["-", "/", ":", ";", "(", ")", "¥", "@", "“", "”"])
|
XCTAssertEqual(typing.keyRows[1], ["-", "/", ":", ";", "(", ")", "¥", "@", "“", "”"])
|
||||||
|
|
||||||
@@ -325,7 +325,7 @@ final class KeyboardSurfaceStateTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testResourceRetryIsSkippedWhenNoErrorIsPending() {
|
func testResourceRetryIsSkippedWhenNoErrorIsPending() {
|
||||||
let typing = TypingSessionController()
|
let typing = TypingSessionController(engine: { TrackingStubRimeEngine() })
|
||||||
XCTAssertNil(typing.lastError)
|
XCTAssertNil(typing.lastError)
|
||||||
XCTAssertFalse(typing.lastErrorNeedsHostDeployment)
|
XCTAssertFalse(typing.lastErrorNeedsHostDeployment)
|
||||||
|
|
||||||
@@ -338,7 +338,7 @@ final class KeyboardSurfaceStateTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testSharedCapsuleCanSelectSpecificTypingLanguage() {
|
func testSharedCapsuleCanSelectSpecificTypingLanguage() {
|
||||||
let typing = TypingSessionController()
|
let typing = TypingSessionController(engine: { TrackingStubRimeEngine() })
|
||||||
XCTAssertEqual(typing.language, .chinese)
|
XCTAssertEqual(typing.language, .chinese)
|
||||||
|
|
||||||
XCTAssertEqual(typing.setLanguage(.english), .none)
|
XCTAssertEqual(typing.setLanguage(.english), .none)
|
||||||
@@ -349,7 +349,7 @@ final class KeyboardSurfaceStateTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testShiftStateProducesUppercaseKeyRows() {
|
func testShiftStateProducesUppercaseKeyRows() {
|
||||||
let typing = TypingSessionController()
|
let typing = TypingSessionController(engine: { TrackingStubRimeEngine() })
|
||||||
XCTAssertFalse(typing.shiftActive)
|
XCTAssertFalse(typing.shiftActive)
|
||||||
|
|
||||||
_ = typing.handleKey("⇧")
|
_ = typing.handleKey("⇧")
|
||||||
@@ -360,7 +360,7 @@ final class KeyboardSurfaceStateTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testManualShiftSurvivesAutocapitalizationSync() {
|
func testManualShiftSurvivesAutocapitalizationSync() {
|
||||||
let typing = TypingSessionController()
|
let typing = TypingSessionController(engine: { TrackingStubRimeEngine() })
|
||||||
// Providers must be set before language switch (which syncs autocap).
|
// Providers must be set before language switch (which syncs autocap).
|
||||||
typing.precedingTextProvider = { "hello " } // mid-sentence: auto stays off
|
typing.precedingTextProvider = { "hello " } // mid-sentence: auto stays off
|
||||||
typing.autocapitalizationModeProvider = { .sentences }
|
typing.autocapitalizationModeProvider = { .sentences }
|
||||||
@@ -376,7 +376,7 @@ final class KeyboardSurfaceStateTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testShiftHoldTypesUppercaseWithoutEnteringCapsLock() {
|
func testShiftHoldTypesUppercaseWithoutEnteringCapsLock() {
|
||||||
let typing = TypingSessionController()
|
let typing = TypingSessionController(engine: { TrackingStubRimeEngine() })
|
||||||
_ = typing.setLanguage(.english)
|
_ = typing.setLanguage(.english)
|
||||||
typing.precedingTextProvider = { "hello " }
|
typing.precedingTextProvider = { "hello " }
|
||||||
typing.autocapitalizationModeProvider = { .sentences }
|
typing.autocapitalizationModeProvider = { .sentences }
|
||||||
@@ -396,7 +396,7 @@ final class KeyboardSurfaceStateTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testShiftHoldWithoutTypingActsAsTap() {
|
func testShiftHoldWithoutTypingActsAsTap() {
|
||||||
let typing = TypingSessionController()
|
let typing = TypingSessionController(engine: { TrackingStubRimeEngine() })
|
||||||
typing.beginShiftHold()
|
typing.beginShiftHold()
|
||||||
typing.endShiftHold()
|
typing.endShiftHold()
|
||||||
XCTAssertTrue(typing.shiftActive)
|
XCTAssertTrue(typing.shiftActive)
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ final class LibrimeIntegrationTests: XCTestCase {
|
|||||||
distributionVersion: "tests"
|
distributionVersion: "tests"
|
||||||
)
|
)
|
||||||
try bridge.start()
|
try bridge.start()
|
||||||
|
defer { bridge.finalizeRuntime() }
|
||||||
|
|
||||||
let vectors: [(TypingInputSchema, String)] = [
|
let vectors: [(TypingInputSchema, String)] = [
|
||||||
(.fullPinyin, "nihao"),
|
(.fullPinyin, "nihao"),
|
||||||
@@ -141,40 +142,34 @@ final class LibrimeIntegrationTests: XCTestCase {
|
|||||||
let userFiles = fileManager.enumerator(atPath: user.path)?
|
let userFiles = fileManager.enumerator(atPath: user.path)?
|
||||||
.compactMap { $0 as? String } ?? []
|
.compactMap { $0 as? String } ?? []
|
||||||
XCTAssertTrue(userFiles.contains(where: { $0.contains("userdb") }))
|
XCTAssertTrue(userFiles.contains(where: { $0.contains("userdb") }))
|
||||||
|
}
|
||||||
|
|
||||||
// Redeploy with one fuzzy pair and verify it affects actual Rime
|
func testNLFuzzyMapsLihaoToNihao() throws {
|
||||||
// candidates rather than just generated YAML.
|
let env = try stagedRimeEnvironment(fuzzyPairs: [.nL])
|
||||||
for schema in TypingInputSchema.allCases {
|
defer { try? env.fileManager.removeItem(at: env.root) }
|
||||||
try RimeSchemaGenerator.schema(for: schema, fuzzyPairs: [.nL]).write(
|
|
||||||
to: shared.appendingPathComponent("\(schema.rawValue).schema.yaml"),
|
|
||||||
atomically: true,
|
|
||||||
encoding: .utf8
|
|
||||||
)
|
|
||||||
}
|
|
||||||
let fuzzyDeployer = OSGRimeBridge(
|
|
||||||
sharedDataDirectory: shared.path,
|
|
||||||
userDataDirectory: user.path,
|
|
||||||
distributionVersion: "tests-fuzzy"
|
|
||||||
)
|
|
||||||
try fuzzyDeployer.deploy(withFullCheck: true)
|
|
||||||
fuzzyDeployer.finalizeRuntime()
|
|
||||||
|
|
||||||
let fuzzyBridge = OSGRimeBridge(
|
let deployer = OSGRimeBridge(
|
||||||
sharedDataDirectory: shared.path,
|
sharedDataDirectory: env.shared.path,
|
||||||
userDataDirectory: user.path,
|
userDataDirectory: env.user.path,
|
||||||
distributionVersion: "tests-fuzzy"
|
distributionVersion: "tests-fuzzy-nl"
|
||||||
)
|
)
|
||||||
try fuzzyBridge.start()
|
try deployer.deploy(withFullCheck: true)
|
||||||
XCTAssertTrue(fuzzyBridge.selectSchema(TypingInputSchema.fullPinyin.rawValue))
|
deployer.finalizeRuntime()
|
||||||
for scalar in "lihao".utf8 {
|
|
||||||
XCTAssertTrue(fuzzyBridge.processKeyCode(Int32(scalar), modifiers: 0))
|
let bridge = OSGRimeBridge(
|
||||||
}
|
sharedDataDirectory: env.shared.path,
|
||||||
let fuzzySnapshot = fuzzyBridge.snapshot(withCandidateLimit: 100)
|
userDataDirectory: env.user.path,
|
||||||
|
distributionVersion: "tests-fuzzy-nl"
|
||||||
|
)
|
||||||
|
try bridge.start()
|
||||||
|
defer { bridge.finalizeRuntime() }
|
||||||
|
XCTAssertTrue(bridge.selectSchema(TypingInputSchema.fullPinyin.rawValue))
|
||||||
|
type("lihao", on: bridge)
|
||||||
|
let snapshot = bridge.snapshot(withCandidateLimit: 100)
|
||||||
XCTAssertTrue(
|
XCTAssertTrue(
|
||||||
fuzzySnapshot.candidates.contains(where: { $0.text == "你好" }),
|
snapshot.candidates.contains(where: { $0.text == "你好" }),
|
||||||
"n/l fuzzy candidates: \(fuzzySnapshot.candidates.map(\.text).prefix(20))"
|
"n/l fuzzy candidates: \(snapshot.candidates.map(\.text).prefix(20))"
|
||||||
)
|
)
|
||||||
fuzzyBridge.finalizeRuntime()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testPersonalDictionarySidecarPinsSameCodeCandidates() throws {
|
func testPersonalDictionarySidecarPinsSameCodeCandidates() throws {
|
||||||
@@ -541,7 +536,9 @@ final class LibrimeIntegrationTests: XCTestCase {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func stagedRimeEnvironment() throws -> (
|
private func stagedRimeEnvironment(
|
||||||
|
fuzzyPairs: Set<PinyinFuzzyPair> = []
|
||||||
|
) throws -> (
|
||||||
root: URL,
|
root: URL,
|
||||||
shared: URL,
|
shared: URL,
|
||||||
user: URL,
|
user: URL,
|
||||||
@@ -569,7 +566,7 @@ final class LibrimeIntegrationTests: XCTestCase {
|
|||||||
encoding: .utf8
|
encoding: .utf8
|
||||||
)
|
)
|
||||||
for schema in TypingInputSchema.allCases {
|
for schema in TypingInputSchema.allCases {
|
||||||
try RimeSchemaGenerator.schema(for: schema, fuzzyPairs: []).write(
|
try RimeSchemaGenerator.schema(for: schema, fuzzyPairs: fuzzyPairs).write(
|
||||||
to: shared.appendingPathComponent("\(schema.rawValue).schema.yaml"),
|
to: shared.appendingPathComponent("\(schema.rawValue).schema.yaml"),
|
||||||
atomically: true,
|
atomically: true,
|
||||||
encoding: .utf8
|
encoding: .utf8
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// BuiltinLexiconIndex.swift
|
// BuiltinLexiconIndex.swift
|
||||||
// OSGKeyboard · Shared
|
// OSGKeyboard · Shared
|
||||||
//
|
//
|
||||||
// In-memory index over bundled `phrases.tsv` (~10k computer terms).
|
// In-memory index over the bundled project-curated AI/technology `phrases.tsv`.
|
||||||
// macOS local ASR consumes a Top-N subset; the full index also backs
|
// macOS local ASR consumes a Top-N subset; the full index also backs
|
||||||
// polish supplements and future retrieval.
|
// polish supplements and future retrieval.
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import Foundation
|
|||||||
|
|
||||||
public enum LocalASRBiasAdapter {
|
public enum LocalASRBiasAdapter {
|
||||||
|
|
||||||
/// Bundle IDs where computer-science vocabulary is especially likely.
|
/// Bundle IDs where the curated AI/technology vocabulary is especially likely.
|
||||||
private static let codeEditorBundleIDs: Set<String> = [
|
private static let codeEditorBundleIDs: Set<String> = [
|
||||||
"com.apple.dt.Xcode",
|
"com.apple.dt.Xcode",
|
||||||
"com.microsoft.VSCode",
|
"com.microsoft.VSCode",
|
||||||
@@ -119,7 +119,7 @@ public enum LocalASRBiasAdapter {
|
|||||||
|
|
||||||
private static func preferredLexiconSources(for bundleId: String?) -> Set<String>? {
|
private static func preferredLexiconSources(for bundleId: String?) -> Set<String>? {
|
||||||
guard let bundleId, codeEditorBundleIDs.contains(bundleId) else { return nil }
|
guard let bundleId, codeEditorBundleIDs.contains(bundleId) else { return nil }
|
||||||
return ["computer_terms"]
|
return ["ai_tech_seed"]
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func hardHotwordList(from terms: [String], maxCount: Int) -> [String] {
|
private static func hardHotwordList(from terms: [String], maxCount: Int) -> [String] {
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
// AIUserSkillStoreTests.swift
|
||||||
|
// OSGKeyboardTests
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
@testable import OSGKeyboardShared
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class AIUserSkillStoreTests: XCTestCase {
|
||||||
|
private func makeDefaults() -> UserDefaults {
|
||||||
|
let suite = "group.com.osgkeyboard.shared.tests.userSkills.\(UUID().uuidString)"
|
||||||
|
let defaults = UserDefaults(suiteName: suite)!
|
||||||
|
defaults.removePersistentDomain(forName: suite)
|
||||||
|
return defaults
|
||||||
|
}
|
||||||
|
|
||||||
|
func testChangingShortcutLinkDropsConfirmation() throws {
|
||||||
|
let store = AIAgentSkillLayoutStore(defaults: makeDefaults())
|
||||||
|
let firstURL = URL(string: "https://www.icloud.com/shortcuts/65bf33ba4206484ba78d582eaf1e9c44")!
|
||||||
|
let secondURL = URL(string: "https://www.icloud.com/shortcuts/1f4afcf7ee22400cbf84e319d969aadf")!
|
||||||
|
var skill = AIUserSkill(
|
||||||
|
name: "Custom",
|
||||||
|
prompt: "Do it",
|
||||||
|
shortcutICloudURL: firstURL,
|
||||||
|
shortcutName: "One"
|
||||||
|
)
|
||||||
|
try store.saveUserSkill(skill)
|
||||||
|
XCTAssertEqual(store.confirmShortcutAndEnable(skill.id), .enabled)
|
||||||
|
XCTAssertTrue(store.layout.isEnabled(skill.id))
|
||||||
|
|
||||||
|
skill.shortcutICloudURL = secondURL
|
||||||
|
skill.shortcutName = "Two"
|
||||||
|
try store.saveUserSkill(skill)
|
||||||
|
XCTAssertFalse(store.layout.hasConfirmedShortcut(skill.id))
|
||||||
|
XCTAssertFalse(store.layout.isEnabled(skill.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRemovingShortcutLinkKeepsEnabledTextSkill() throws {
|
||||||
|
let store = AIAgentSkillLayoutStore(defaults: makeDefaults())
|
||||||
|
var skill = AIUserSkill(
|
||||||
|
name: "Custom",
|
||||||
|
prompt: "Do it",
|
||||||
|
shortcutICloudURL: URL(
|
||||||
|
string: "https://www.icloud.com/shortcuts/65bf33ba4206484ba78d582eaf1e9c44"
|
||||||
|
),
|
||||||
|
shortcutName: "Run Me"
|
||||||
|
)
|
||||||
|
try store.saveUserSkill(skill)
|
||||||
|
XCTAssertEqual(store.confirmShortcutAndEnable(skill.id), .enabled)
|
||||||
|
|
||||||
|
skill.shortcutICloudURL = nil
|
||||||
|
try store.saveUserSkill(skill)
|
||||||
|
|
||||||
|
XCTAssertTrue(store.layout.isEnabled(skill.id))
|
||||||
|
XCTAssertFalse(store.layout.hasConfirmedShortcut(skill.id))
|
||||||
|
XCTAssertEqual(store.userSkill(id: skill.id)?.asClipboardSkill().kind, .transform)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAddingShortcutLinkDisablesTextSkillUntilConfirmed() throws {
|
||||||
|
let store = AIAgentSkillLayoutStore(defaults: makeDefaults())
|
||||||
|
var skill = AIUserSkill(
|
||||||
|
name: "Custom",
|
||||||
|
prompt: "Do it"
|
||||||
|
)
|
||||||
|
try store.saveUserSkill(skill)
|
||||||
|
XCTAssertEqual(store.enable(skill.id), .enabled)
|
||||||
|
|
||||||
|
skill.shortcutICloudURL = URL(
|
||||||
|
string: "https://www.icloud.com/shortcuts/65bf33ba4206484ba78d582eaf1e9c44"
|
||||||
|
)
|
||||||
|
skill.shortcutName = "Run Me"
|
||||||
|
try store.saveUserSkill(skill)
|
||||||
|
|
||||||
|
XCTAssertFalse(store.layout.isEnabled(skill.id))
|
||||||
|
XCTAssertFalse(store.layout.hasConfirmedShortcut(skill.id))
|
||||||
|
XCTAssertEqual(store.enable(skill.id), .needsShortcut)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -236,75 +236,3 @@ final class AIUserSkillTests: XCTestCase {
|
|||||||
XCTAssertEqual(catalog.entries.count, 12)
|
XCTAssertEqual(catalog.entries.count, 12)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
|
||||||
final class AIUserSkillStoreTests: XCTestCase {
|
|
||||||
private func makeDefaults() -> UserDefaults {
|
|
||||||
let suite = "group.com.osgkeyboard.shared.tests.userSkills.\(UUID().uuidString)"
|
|
||||||
let defaults = UserDefaults(suiteName: suite)!
|
|
||||||
defaults.removePersistentDomain(forName: suite)
|
|
||||||
return defaults
|
|
||||||
}
|
|
||||||
|
|
||||||
func testChangingShortcutLinkDropsConfirmation() throws {
|
|
||||||
let store = AIAgentSkillLayoutStore(defaults: makeDefaults())
|
|
||||||
let firstURL = URL(string: "https://www.icloud.com/shortcuts/65bf33ba4206484ba78d582eaf1e9c44")!
|
|
||||||
let secondURL = URL(string: "https://www.icloud.com/shortcuts/1f4afcf7ee22400cbf84e319d969aadf")!
|
|
||||||
var skill = AIUserSkill(
|
|
||||||
name: "Custom",
|
|
||||||
prompt: "Do it",
|
|
||||||
shortcutICloudURL: firstURL,
|
|
||||||
shortcutName: "One"
|
|
||||||
)
|
|
||||||
try store.saveUserSkill(skill)
|
|
||||||
XCTAssertEqual(store.confirmShortcutAndEnable(skill.id), .enabled)
|
|
||||||
XCTAssertTrue(store.layout.isEnabled(skill.id))
|
|
||||||
|
|
||||||
skill.shortcutICloudURL = secondURL
|
|
||||||
skill.shortcutName = "Two"
|
|
||||||
try store.saveUserSkill(skill)
|
|
||||||
XCTAssertFalse(store.layout.hasConfirmedShortcut(skill.id))
|
|
||||||
XCTAssertFalse(store.layout.isEnabled(skill.id))
|
|
||||||
}
|
|
||||||
|
|
||||||
func testRemovingShortcutLinkKeepsEnabledTextSkill() throws {
|
|
||||||
let store = AIAgentSkillLayoutStore(defaults: makeDefaults())
|
|
||||||
var skill = AIUserSkill(
|
|
||||||
name: "Custom",
|
|
||||||
prompt: "Do it",
|
|
||||||
shortcutICloudURL: URL(
|
|
||||||
string: "https://www.icloud.com/shortcuts/65bf33ba4206484ba78d582eaf1e9c44"
|
|
||||||
),
|
|
||||||
shortcutName: "Run Me"
|
|
||||||
)
|
|
||||||
try store.saveUserSkill(skill)
|
|
||||||
XCTAssertEqual(store.confirmShortcutAndEnable(skill.id), .enabled)
|
|
||||||
|
|
||||||
skill.shortcutICloudURL = nil
|
|
||||||
try store.saveUserSkill(skill)
|
|
||||||
|
|
||||||
XCTAssertTrue(store.layout.isEnabled(skill.id))
|
|
||||||
XCTAssertFalse(store.layout.hasConfirmedShortcut(skill.id))
|
|
||||||
XCTAssertEqual(store.userSkill(id: skill.id)?.asClipboardSkill().kind, .transform)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testAddingShortcutLinkDisablesTextSkillUntilConfirmed() throws {
|
|
||||||
let store = AIAgentSkillLayoutStore(defaults: makeDefaults())
|
|
||||||
var skill = AIUserSkill(
|
|
||||||
name: "Custom",
|
|
||||||
prompt: "Do it"
|
|
||||||
)
|
|
||||||
try store.saveUserSkill(skill)
|
|
||||||
XCTAssertEqual(store.enable(skill.id), .enabled)
|
|
||||||
|
|
||||||
skill.shortcutICloudURL = URL(
|
|
||||||
string: "https://www.icloud.com/shortcuts/65bf33ba4206484ba78d582eaf1e9c44"
|
|
||||||
)
|
|
||||||
skill.shortcutName = "Run Me"
|
|
||||||
try store.saveUserSkill(skill)
|
|
||||||
|
|
||||||
XCTAssertFalse(store.layout.isEnabled(skill.id))
|
|
||||||
XCTAssertFalse(store.layout.hasConfirmedShortcut(skill.id))
|
|
||||||
XCTAssertEqual(store.enable(skill.id), .needsShortcut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ final class LLMClientTests: XCTestCase {
|
|||||||
try? Keychain.deleteAPIKey(for: "qwen")
|
try? Keychain.deleteAPIKey(for: "qwen")
|
||||||
try? Keychain.deleteAPIKey(for: "openai")
|
try? Keychain.deleteAPIKey(for: "openai")
|
||||||
try? Keychain.deleteAPIKey(for: "deepseek")
|
try? Keychain.deleteAPIKey(for: "deepseek")
|
||||||
|
try? Keychain.deleteAPIKey(for: "qwen", useICloudSync: true)
|
||||||
|
try? Keychain.deleteAPIKey(for: "openai", useICloudSync: true)
|
||||||
|
try? Keychain.deleteAPIKey(for: "deepseek", useICloudSync: true)
|
||||||
StubURLProtocolStorage.config = nil
|
StubURLProtocolStorage.config = nil
|
||||||
StubURLProtocolStorage.delaySeconds = 0
|
StubURLProtocolStorage.delaySeconds = 0
|
||||||
StubURLProtocolStorage.lastRequest = nil
|
StubURLProtocolStorage.lastRequest = nil
|
||||||
@@ -31,6 +34,9 @@ final class LLMClientTests: XCTestCase {
|
|||||||
try? Keychain.deleteAPIKey(for: "qwen")
|
try? Keychain.deleteAPIKey(for: "qwen")
|
||||||
try? Keychain.deleteAPIKey(for: "openai")
|
try? Keychain.deleteAPIKey(for: "openai")
|
||||||
try? Keychain.deleteAPIKey(for: "deepseek")
|
try? Keychain.deleteAPIKey(for: "deepseek")
|
||||||
|
try? Keychain.deleteAPIKey(for: "qwen", useICloudSync: true)
|
||||||
|
try? Keychain.deleteAPIKey(for: "openai", useICloudSync: true)
|
||||||
|
try? Keychain.deleteAPIKey(for: "deepseek", useICloudSync: true)
|
||||||
Keychain.resetTestMemoryStore()
|
Keychain.resetTestMemoryStore()
|
||||||
StubURLProtocolStorage.config = nil
|
StubURLProtocolStorage.config = nil
|
||||||
StubURLProtocolStorage.delaySeconds = 0
|
StubURLProtocolStorage.delaySeconds = 0
|
||||||
@@ -65,6 +71,7 @@ final class LLMClientTests: XCTestCase {
|
|||||||
defer { defaults.removePersistentDomain(forName: suiteName) }
|
defer { defaults.removePersistentDomain(forName: suiteName) }
|
||||||
|
|
||||||
let config = ProviderConfig(defaults: defaults)
|
let config = ProviderConfig(defaults: defaults)
|
||||||
|
config.apiKey = ""
|
||||||
config.engineMode = "cloud"
|
config.engineMode = "cloud"
|
||||||
XCTAssertFalse(config.isConfigured)
|
XCTAssertFalse(config.isConfigured)
|
||||||
config.engineMode = "local"
|
config.engineMode = "local"
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ final class LocalASRBiasAdapterTests: XCTestCase {
|
|||||||
let url = dir.appendingPathComponent("phrases.tsv")
|
let url = dir.appendingPathComponent("phrases.tsv")
|
||||||
let tsv = """
|
let tsv = """
|
||||||
word\tpinyin\tsource\tweight
|
word\tpinyin\tsource\tweight
|
||||||
SwiftUI\tswift ui\tcomputer_terms\t5
|
SwiftUI\tswift ui\tai_tech_seed\t90
|
||||||
Kubernetes\tku bo ne si\tcomputer_terms\t5
|
Kubernetes\tku bo ne si\tai_tech_seed\t90
|
||||||
一致性\tyi zhi xing\tcomputer_terms\t5
|
一致性\tyi zhi xing\tai_tech_seed\t80
|
||||||
"""
|
"""
|
||||||
try tsv.write(to: url, atomically: true, encoding: .utf8)
|
try tsv.write(to: url, atomically: true, encoding: .utf8)
|
||||||
addTeardownBlock {
|
addTeardownBlock {
|
||||||
@@ -103,11 +103,12 @@ final class LocalASRBiasAdapterTests: XCTestCase {
|
|||||||
|
|
||||||
func testBuiltinLexiconParsesTSV() {
|
func testBuiltinLexiconParsesTSV() {
|
||||||
let terms = BuiltinLexiconIndex.parseTSV(
|
let terms = BuiltinLexiconIndex.parseTSV(
|
||||||
"word\tpinyin\tsource\tweight\nFoo\tfoo\tcomputer_terms\t5\n"
|
"word\tpinyin\tsource\tweight\nFoo\tfoo\tai_tech_seed\t80\n"
|
||||||
)
|
)
|
||||||
XCTAssertEqual(terms.count, 1)
|
XCTAssertEqual(terms.count, 1)
|
||||||
XCTAssertEqual(terms[0].word, "Foo")
|
XCTAssertEqual(terms[0].word, "Foo")
|
||||||
XCTAssertEqual(terms[0].weight, 5)
|
XCTAssertEqual(terms[0].source, "ai_tech_seed")
|
||||||
|
XCTAssertEqual(terms[0].weight, 80)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testPolishingServiceMergesDictionarySupplement() {
|
func testPolishingServiceMergesDictionarySupplement() {
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ final class LocalASRModelCatalogTests: XCTestCase {
|
|||||||
func testMLXAdapterProducesPromptBiasNotHardHotwords() throws {
|
func testMLXAdapterProducesPromptBiasNotHardHotwords() throws {
|
||||||
let fixtureURL = FileManager.default.temporaryDirectory
|
let fixtureURL = FileManager.default.temporaryDirectory
|
||||||
.appendingPathComponent("phrases-\(UUID().uuidString).tsv")
|
.appendingPathComponent("phrases-\(UUID().uuidString).tsv")
|
||||||
try "word\tpinyin\tsource\tweight\nSwiftUI\tswift ui\tcomputer_terms\t5\n"
|
try "word\tpinyin\tsource\tweight\nSwiftUI\tswift ui\tai_tech_seed\t90\n"
|
||||||
.write(to: fixtureURL, atomically: true, encoding: .utf8)
|
.write(to: fixtureURL, atomically: true, encoding: .utf8)
|
||||||
defer { try? FileManager.default.removeItem(at: fixtureURL) }
|
defer { try? FileManager.default.removeItem(at: fixtureURL) }
|
||||||
|
|
||||||
|
|||||||
@@ -5,16 +5,18 @@ import XCTest
|
|||||||
@testable import OSGKeyboard
|
@testable import OSGKeyboard
|
||||||
|
|
||||||
final class OpenSourceLicenseCatalogTests: XCTestCase {
|
final class OpenSourceLicenseCatalogTests: XCTestCase {
|
||||||
func testIOSCatalogIncludesRimeAndDictionaryDataOnly() {
|
func testIOSCatalogIncludesRimeDictionariesIconsAndCuratedSpeechData() {
|
||||||
let entries = OpenSourceLicenseCatalog.entries(for: .iOS)
|
let entries = OpenSourceLicenseCatalog.entries(for: .iOS)
|
||||||
let ids = Set(entries.map(\.id))
|
let ids = Set(entries.map(\.id))
|
||||||
|
|
||||||
|
XCTAssertTrue(ids.contains("material-icons"))
|
||||||
XCTAssertTrue(ids.contains("librime-static"))
|
XCTAssertTrue(ids.contains("librime-static"))
|
||||||
XCTAssertTrue(ids.contains("rime-pinyin-simp"))
|
XCTAssertTrue(ids.contains("rime-pinyin-simp"))
|
||||||
XCTAssertTrue(ids.contains("jieba"))
|
XCTAssertTrue(ids.contains("jieba"))
|
||||||
XCTAssertTrue(ids.contains("phrase-pinyin-data"))
|
XCTAssertTrue(ids.contains("phrase-pinyin-data"))
|
||||||
XCTAssertTrue(ids.contains("pinyin-data"))
|
XCTAssertTrue(ids.contains("pinyin-data"))
|
||||||
XCTAssertTrue(ids.contains("english-typing-lexicon"))
|
XCTAssertTrue(ids.contains("english-typing-lexicon"))
|
||||||
|
XCTAssertTrue(ids.contains("osg-ai-tech-lexicon"))
|
||||||
XCTAssertFalse(ids.contains("mlx-audio-swift"))
|
XCTAssertFalse(ids.contains("mlx-audio-swift"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,7 +24,15 @@ final class OpenSourceLicenseCatalogTests: XCTestCase {
|
|||||||
let entries = OpenSourceLicenseCatalog.entries(for: .macOS)
|
let entries = OpenSourceLicenseCatalog.entries(for: .macOS)
|
||||||
let ids = Set(entries.map(\.id))
|
let ids = Set(entries.map(\.id))
|
||||||
|
|
||||||
XCTAssertEqual(ids, ["mlx-audio-swift"])
|
XCTAssertEqual(ids, [
|
||||||
|
"mlx-audio-swift",
|
||||||
|
"mlx-swift",
|
||||||
|
"mlx-swift-lm",
|
||||||
|
"osg-ai-tech-lexicon",
|
||||||
|
"qwen3-asr-mlx",
|
||||||
|
"swift-huggingface",
|
||||||
|
"swift-transformers",
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
func testBundledDictionaryLicensesKeepCopyrightNotices() throws {
|
func testBundledDictionaryLicensesKeepCopyrightNotices() throws {
|
||||||
@@ -46,4 +56,39 @@ final class OpenSourceLicenseCatalogTests: XCTestCase {
|
|||||||
XCTAssertTrue(entry.licenseText.contains("leveldb.txt"))
|
XCTAssertTrue(entry.licenseText.contains("leveldb.txt"))
|
||||||
XCTAssertTrue(entry.licenseText.contains("yaml-cpp.txt"))
|
XCTAssertTrue(entry.licenseText.contains("yaml-cpp.txt"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testMaterialIconsUseGoogleAttributionAndFullApacheLicense() throws {
|
||||||
|
let entry = try XCTUnwrap(
|
||||||
|
OpenSourceLicenseCatalog.entries(for: .iOS)
|
||||||
|
.first { $0.id == "material-icons" }
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertTrue(entry.licenseText.contains("Copyright 2014 Google LLC"))
|
||||||
|
XCTAssertTrue(entry.licenseText.contains("TERMS AND CONDITIONS FOR USE"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMacMLXAndQwenNoticesContainUpstreamAttribution() throws {
|
||||||
|
let entries = OpenSourceLicenseCatalog.entries(for: .macOS)
|
||||||
|
let mlxAudio = try XCTUnwrap(entries.first { $0.id == "mlx-audio-swift" })
|
||||||
|
let mlxSwift = try XCTUnwrap(entries.first { $0.id == "mlx-swift" })
|
||||||
|
let transformers = try XCTUnwrap(entries.first { $0.id == "swift-transformers" })
|
||||||
|
let qwen = try XCTUnwrap(entries.first { $0.id == "qwen3-asr-mlx" })
|
||||||
|
|
||||||
|
XCTAssertTrue(mlxAudio.licenseText.contains("Copyright (c) 2025 Prince Canuma"))
|
||||||
|
XCTAssertTrue(mlxSwift.licenseText.contains("Copyright (c) 2023 ml-explore"))
|
||||||
|
XCTAssertTrue(transformers.licenseText.contains("Copyright 2022 Hugging Face SAS."))
|
||||||
|
XCTAssertTrue(qwen.licenseText.contains("Qwen3-ASR-0.6B-4bit"))
|
||||||
|
XCTAssertTrue(qwen.licenseText.contains("Apache License"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCuratedLexiconNoticeDoesNotRelicenseApplication() throws {
|
||||||
|
let entry = try XCTUnwrap(
|
||||||
|
OpenSourceLicenseCatalog.entries(for: .iOS)
|
||||||
|
.first { $0.id == "osg-ai-tech-lexicon" }
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertTrue(entry.licenseText.contains("data subset"))
|
||||||
|
XCTAssertTrue(entry.licenseText.contains("source-available"))
|
||||||
|
XCTAssertTrue(entry.licenseText.contains("not distributed under the MIT License"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -63,7 +63,7 @@ Voice input for iPhone, iPad, and Mac. Speak in any app — polished text lands
|
|||||||
|
|
||||||
## Privacy
|
## Privacy
|
||||||
|
|
||||||
Speech is transcribed on-device by default. Polish sends **text only** — the transcript and a small amount of nearby cursor text for continuity, never raw audio. Chinese candidate learning and English suggestion/autocorrect learning stay in the on-device App Group and are not uploaded. Secure fields disable English suggestions and autocorrect. Cursor context is not logged or saved to voice history. See the [Privacy Policy](https://hkgood.github.io/OSGKeyboard/privacy/).
|
Speech is transcribed on-device by default. Audio or text leaves the device only when you actively choose a cloud engine, polish, AI, or a skill, and it goes directly to your configured provider. Clipboard history is off by default, capped at 15 local items, and sent only after an explicit clipboard action. Chinese and English typing learning stays local. API keys use Keychain (optionally iCloud Keychain), while downloaded Mac speech models and local inference remain on the Mac. See the [Privacy Policy](https://hkgood.github.io/OSGKeyboard/privacy/).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,10 @@
|
|||||||
|
|
||||||
- **默认本地识别** — 录音在设备上转写,不经过我们的服务器
|
- **默认本地识别** — 录音在设备上转写,不经过我们的服务器
|
||||||
- **润色只发文字** — 发给 LLM 的是转写文本,以及用于衔接的少量光标附近文字,不是原始音频
|
- **润色只发文字** — 发给 LLM 的是转写文本,以及用于衔接的少量光标附近文字,不是原始音频
|
||||||
|
- **服务商由你选择** — 只有你在设置中主动选择云端识别、润色、AI 或技能时,相关音频或文字才会直接发给你配置的服务商
|
||||||
|
- **剪贴板默认关闭** — 历史最多 15 条、仅保存在本机;只有你主动运行剪贴板技能、在 AI 提问中点名剪贴板或粘贴后润色时,正文才会离开设备
|
||||||
- **不上传击键** — 中文候选学习与英文补全/纠错学习仅留在设备 App Group;密码框关闭英文建议与纠错,普通击键不会上传
|
- **不上传击键** — 中文候选学习与英文补全/纠错学习仅留在设备 App Group;密码框关闭英文建议与纠错,普通击键不会上传
|
||||||
|
- **密钥与模型归你控制** — API Key 保存在 Keychain,可选经 iCloud 钥匙串同步;Mac 本地模型下载后保存在本机,端侧推理不上传录音
|
||||||
- 详见 [隐私政策](https://hkgood.github.io/OSGKeyboard/privacy/)
|
- 详见 [隐私政策](https://hkgood.github.io/OSGKeyboard/privacy/)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,130 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""Build OSGKeyboard domain ASR lexicon v1 from the computer-terms scel source.
|
|
||||||
|
|
||||||
Sources:
|
|
||||||
Local 计算机词汇大全【官方推荐】.scel (IT / computer vocabulary only)
|
|
||||||
|
|
||||||
Casual network slang and Sogou popular-word dumps are intentionally excluded —
|
|
||||||
they dilute custom LM phrase biasing without improving domain ASR accuracy.
|
|
||||||
|
|
||||||
Output:
|
|
||||||
OSGKeyboard/Resources/CustomLanguageModel/v1/phrases.tsv
|
|
||||||
OSGKeyboard/Resources/CustomLanguageModel/v1/manifest.json
|
|
||||||
|
|
||||||
The compiled .bin asset is exported separately to
|
|
||||||
OSGKeyboard/Resources/CustomLanguageModel/v1/ via export_clm.swift.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from scel_parser import get_scel_info, parse_scel_file
|
|
||||||
|
|
||||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
||||||
DEFAULT_OUTPUT_DIR = REPO_ROOT / "OSGKeyboard/Resources/CustomLanguageModel/v1"
|
|
||||||
DEFAULT_COMPUTER_SCEL = Path("/Users/rocky/Downloads/计算机词汇大全【官方推荐】.scel")
|
|
||||||
|
|
||||||
SOURCE_KEY = "computer_terms"
|
|
||||||
SOURCE_LABEL = "计算机词汇大全【官方推荐】"
|
|
||||||
SOURCE_WEIGHT = 5
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class LexiconEntry:
|
|
||||||
word: str
|
|
||||||
pinyin: str
|
|
||||||
source: str
|
|
||||||
weight: int
|
|
||||||
|
|
||||||
|
|
||||||
def merge_entries(entries: list[tuple[str, str]]) -> list[LexiconEntry]:
|
|
||||||
merged: dict[str, LexiconEntry] = {}
|
|
||||||
|
|
||||||
for word, pinyin in entries:
|
|
||||||
current = merged.get(word)
|
|
||||||
candidate = LexiconEntry(
|
|
||||||
word=word,
|
|
||||||
pinyin=pinyin,
|
|
||||||
source=SOURCE_KEY,
|
|
||||||
weight=SOURCE_WEIGHT,
|
|
||||||
)
|
|
||||||
if current is None:
|
|
||||||
merged[word] = candidate
|
|
||||||
elif not current.pinyin and pinyin:
|
|
||||||
merged[word] = candidate
|
|
||||||
|
|
||||||
return sorted(merged.values(), key=lambda item: item.word)
|
|
||||||
|
|
||||||
|
|
||||||
def write_outputs(entries: list[LexiconEntry], output_dir: Path, raw_count: int) -> None:
|
|
||||||
output_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
phrases_path = output_dir / "phrases.tsv"
|
|
||||||
with phrases_path.open("w", encoding="utf-8") as handle:
|
|
||||||
handle.write("word\tpinyin\tsource\tweight\n")
|
|
||||||
for entry in entries:
|
|
||||||
handle.write(f"{entry.word}\t{entry.pinyin}\t{entry.source}\t{entry.weight}\n")
|
|
||||||
|
|
||||||
manifest = {
|
|
||||||
"version": "v1",
|
|
||||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
||||||
"locale": "zh-Hans",
|
|
||||||
"entry_count": len(entries),
|
|
||||||
"sources": [
|
|
||||||
{
|
|
||||||
"key": SOURCE_KEY,
|
|
||||||
"label": SOURCE_LABEL,
|
|
||||||
"weight": SOURCE_WEIGHT,
|
|
||||||
"raw_count": raw_count,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"notes": [
|
|
||||||
"Domain-specific computer/IT vocabulary only; casual network slang removed.",
|
|
||||||
"PhraseCount weights map to SFCustomLanguageModelData relative frequencies.",
|
|
||||||
"Merged with ai-tech-brands seed at export time for the final .bin asset.",
|
|
||||||
],
|
|
||||||
"files": {
|
|
||||||
"phrases": phrases_path.name,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
manifest_path = output_dir / "manifest.json"
|
|
||||||
manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
||||||
|
|
||||||
|
|
||||||
def build(*, computer_scel: Path, output_dir: Path) -> int:
|
|
||||||
if not computer_scel.exists():
|
|
||||||
print(f"Missing computer scel: {computer_scel}", file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
computer_info = get_scel_info(computer_scel)
|
|
||||||
print(f"Computer dict: {computer_info.name} ({computer_info.word_count} header count)")
|
|
||||||
|
|
||||||
raw_entries = parse_scel_file(computer_scel)
|
|
||||||
merged = merge_entries(raw_entries)
|
|
||||||
write_outputs(merged, output_dir, raw_count=len(raw_entries))
|
|
||||||
|
|
||||||
print(f"Raw count: {len(raw_entries)}")
|
|
||||||
print(f"Merged unique entries: {len(merged)}")
|
|
||||||
print(f"Wrote {output_dir / 'phrases.tsv'}")
|
|
||||||
print(f"Wrote {output_dir / 'manifest.json'}")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
parser = argparse.ArgumentParser(description="Build OSGKeyboard domain ASR lexicon v1")
|
|
||||||
parser.add_argument("--computer-scel", type=Path, default=DEFAULT_COMPUTER_SCEL)
|
|
||||||
parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT_DIR)
|
|
||||||
args = parser.parse_args()
|
|
||||||
return build(computer_scel=args.computer_scel, output_dir=args.output_dir)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
raise SystemExit(main())
|
|
||||||
@@ -3,7 +3,9 @@
|
|||||||
// export_clm.swift
|
// export_clm.swift
|
||||||
// OSGKeyboard · offline SFCustomLanguageModelData exporter (macOS 14+)
|
// OSGKeyboard · offline SFCustomLanguageModelData exporter (macOS 14+)
|
||||||
//
|
//
|
||||||
// Reads merged phrase TSVs and writes a .bin training asset via Speech framework.
|
// Reads the project-curated AI/tech TSV and writes a .bin training asset via
|
||||||
|
// Speech framework. The normalized four-column TSV beside the binary also
|
||||||
|
// powers the Mac runtime bias index.
|
||||||
// Usage:
|
// Usage:
|
||||||
// swift Scripts/lexicon/export_clm.swift
|
// swift Scripts/lexicon/export_clm.swift
|
||||||
// swift Scripts/lexicon/export_clm.swift --max-entries 30000
|
// swift Scripts/lexicon/export_clm.swift --max-entries 30000
|
||||||
@@ -15,7 +17,6 @@ import Speech
|
|||||||
// MARK: - CLI
|
// MARK: - CLI
|
||||||
|
|
||||||
struct CLIOptions {
|
struct CLIOptions {
|
||||||
var domainTSV: URL
|
|
||||||
var aiTechTSV: URL
|
var aiTechTSV: URL
|
||||||
var outputBin: URL
|
var outputBin: URL
|
||||||
var localeID: String
|
var localeID: String
|
||||||
@@ -29,9 +30,6 @@ struct CLIOptions {
|
|||||||
.deletingLastPathComponent()
|
.deletingLastPathComponent()
|
||||||
.deletingLastPathComponent()
|
.deletingLastPathComponent()
|
||||||
|
|
||||||
var domain = repoRoot.appendingPathComponent(
|
|
||||||
"OSGKeyboard/Resources/CustomLanguageModel/v1/phrases.tsv"
|
|
||||||
)
|
|
||||||
var aiTech = repoRoot.appendingPathComponent(
|
var aiTech = repoRoot.appendingPathComponent(
|
||||||
"OSGKeyboard/Resources/CustomLanguageModel/ai-tech-brands/v1/phrases.tsv"
|
"OSGKeyboard/Resources/CustomLanguageModel/ai-tech-brands/v1/phrases.tsv"
|
||||||
)
|
)
|
||||||
@@ -40,14 +38,12 @@ struct CLIOptions {
|
|||||||
)
|
)
|
||||||
var localeID = "zh_CN"
|
var localeID = "zh_CN"
|
||||||
var modelID = "com.osgkeyboard.custom-lm.v1"
|
var modelID = "com.osgkeyboard.custom-lm.v1"
|
||||||
var modelVersion = "1.0.0"
|
var modelVersion = "1.0.1"
|
||||||
var maxEntries: Int?
|
var maxEntries: Int?
|
||||||
|
|
||||||
var iterator = CommandLine.arguments.dropFirst().makeIterator()
|
var iterator = CommandLine.arguments.dropFirst().makeIterator()
|
||||||
while let flag = iterator.next() {
|
while let flag = iterator.next() {
|
||||||
switch flag {
|
switch flag {
|
||||||
case "--domain-tsv", "--sogou-tsv":
|
|
||||||
domain = URL(fileURLWithPath: iterator.next() ?? "")
|
|
||||||
case "--ai-tech-tsv":
|
case "--ai-tech-tsv":
|
||||||
aiTech = URL(fileURLWithPath: iterator.next() ?? "")
|
aiTech = URL(fileURLWithPath: iterator.next() ?? "")
|
||||||
case "--output":
|
case "--output":
|
||||||
@@ -71,7 +67,6 @@ struct CLIOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return CLIOptions(
|
return CLIOptions(
|
||||||
domainTSV: domain,
|
|
||||||
aiTechTSV: aiTech,
|
aiTechTSV: aiTech,
|
||||||
outputBin: output,
|
outputBin: output,
|
||||||
localeID: localeID,
|
localeID: localeID,
|
||||||
@@ -86,8 +81,7 @@ struct CLIOptions {
|
|||||||
export_clm.swift — build SFCustomLanguageModelData .bin on macOS
|
export_clm.swift — build SFCustomLanguageModelData .bin on macOS
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--domain-tsv <path> Domain phrases TSV (computer/IT terms)
|
--ai-tech-tsv <path> Project-curated AI/tech phrases TSV
|
||||||
--ai-tech-tsv <path> AI/tech seed phrases TSV
|
|
||||||
--output <path> Output .bin path
|
--output <path> Output .bin path
|
||||||
--locale <id> Locale identifier (default: zh_CN)
|
--locale <id> Locale identifier (default: zh_CN)
|
||||||
--identifier <id> Custom LM identifier
|
--identifier <id> Custom LM identifier
|
||||||
@@ -102,6 +96,7 @@ struct CLIOptions {
|
|||||||
|
|
||||||
struct PhraseEntry: Hashable {
|
struct PhraseEntry: Hashable {
|
||||||
let phrase: String
|
let phrase: String
|
||||||
|
let pinyin: String
|
||||||
let weight: Int
|
let weight: Int
|
||||||
let source: String
|
let source: String
|
||||||
}
|
}
|
||||||
@@ -126,7 +121,7 @@ enum TSVLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Formats:
|
// Formats:
|
||||||
// domain: word, pinyin, source, weight
|
// normalized: word, pinyin, source, weight
|
||||||
// ai-tech: word, pinyin, source, category, weight, canonical
|
// ai-tech: word, pinyin, source, category, weight, canonical
|
||||||
let weight: Int
|
let weight: Int
|
||||||
if parts.count >= 6, let parsed = Int(parts[4]) {
|
if parts.count >= 6, let parsed = Int(parts[4]) {
|
||||||
@@ -137,8 +132,16 @@ enum TSVLoader {
|
|||||||
weight = 1
|
weight = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let pinyin = parts.count >= 2 ? parts[1] : ""
|
||||||
let source = parts.count >= 3 ? parts[2] : sourceLabel
|
let source = parts.count >= 3 ? parts[2] : sourceLabel
|
||||||
entries.append(PhraseEntry(phrase: word, weight: max(1, weight), source: source))
|
entries.append(
|
||||||
|
PhraseEntry(
|
||||||
|
phrase: word,
|
||||||
|
pinyin: pinyin,
|
||||||
|
weight: max(1, weight),
|
||||||
|
source: source
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return entries
|
return entries
|
||||||
@@ -171,17 +174,13 @@ enum ExportCLM {
|
|||||||
let options = CLIOptions.parse()
|
let options = CLIOptions.parse()
|
||||||
let fm = FileManager.default
|
let fm = FileManager.default
|
||||||
|
|
||||||
guard fm.fileExists(atPath: options.domainTSV.path) else {
|
|
||||||
throw ExportError.missingInput(options.domainTSV.path)
|
|
||||||
}
|
|
||||||
guard fm.fileExists(atPath: options.aiTechTSV.path) else {
|
guard fm.fileExists(atPath: options.aiTechTSV.path) else {
|
||||||
throw ExportError.missingInput(options.aiTechTSV.path)
|
throw ExportError.missingInput(options.aiTechTSV.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
fputs("Loading phrases…\n", stderr)
|
fputs("Loading phrases…\n", stderr)
|
||||||
let domain = try TSVLoader.load(from: options.domainTSV, sourceLabel: "computer_terms")
|
|
||||||
let aiTech = try TSVLoader.load(from: options.aiTechTSV, sourceLabel: "ai_tech_seed")
|
let aiTech = try TSVLoader.load(from: options.aiTechTSV, sourceLabel: "ai_tech_seed")
|
||||||
var merged = TSVLoader.merge([domain, aiTech])
|
var merged = TSVLoader.merge([aiTech])
|
||||||
|
|
||||||
if let cap = options.maxEntries, merged.count > cap {
|
if let cap = options.maxEntries, merged.count > cap {
|
||||||
merged = Array(merged.prefix(cap))
|
merged = Array(merged.prefix(cap))
|
||||||
@@ -189,7 +188,7 @@ enum ExportCLM {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fputs(
|
fputs(
|
||||||
"Merged \(merged.count) unique phrases (domain=\(domain.count), ai-tech=\(aiTech.count))\n",
|
"Loaded \(merged.count) unique project-curated AI/tech phrases\n",
|
||||||
stderr
|
stderr
|
||||||
)
|
)
|
||||||
fputs("Locale=\(options.localeID) identifier=\(options.modelID) version=\(options.modelVersion)\n", stderr)
|
fputs("Locale=\(options.localeID) identifier=\(options.modelID) version=\(options.modelVersion)\n", stderr)
|
||||||
@@ -218,6 +217,15 @@ enum ExportCLM {
|
|||||||
try fm.removeItem(at: outputURL)
|
try fm.removeItem(at: outputURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let phrasesURL = parent.appendingPathComponent("phrases.tsv")
|
||||||
|
let normalizedLines = merged.map { entry in
|
||||||
|
"\(entry.phrase)\t\(entry.pinyin)\t\(entry.source)\t\(entry.weight)"
|
||||||
|
}
|
||||||
|
let normalizedTSV = (["word\tpinyin\tsource\tweight"] + normalizedLines)
|
||||||
|
.joined(separator: "\n") + "\n"
|
||||||
|
try normalizedTSV.write(to: phrasesURL, atomically: true, encoding: .utf8)
|
||||||
|
fputs("Wrote \(phrasesURL.path)\n", stderr)
|
||||||
|
|
||||||
fputs("Exporting to \(outputURL.path)…\n", stderr)
|
fputs("Exporting to \(outputURL.path)…\n", stderr)
|
||||||
try await data.export(to: outputURL)
|
try await data.export(to: outputURL)
|
||||||
|
|
||||||
@@ -236,7 +244,6 @@ enum ExportCLM {
|
|||||||
"version": options.modelVersion,
|
"version": options.modelVersion,
|
||||||
"phrase_count": merged.count,
|
"phrase_count": merged.count,
|
||||||
"sources": [
|
"sources": [
|
||||||
"computer_terms": domain.count,
|
|
||||||
"ai_tech_seed": aiTech.count,
|
"ai_tech_seed": aiTech.count,
|
||||||
],
|
],
|
||||||
"bin_file": outputURL.lastPathComponent,
|
"bin_file": outputURL.lastPathComponent,
|
||||||
@@ -246,6 +253,32 @@ enum ExportCLM {
|
|||||||
let manifestData = try JSONSerialization.data(withJSONObject: manifest, options: [.prettyPrinted, .sortedKeys])
|
let manifestData = try JSONSerialization.data(withJSONObject: manifest, options: [.prettyPrinted, .sortedKeys])
|
||||||
try manifestData.write(to: manifestURL)
|
try manifestData.write(to: manifestURL)
|
||||||
fputs("Wrote \(manifestURL.path)\n", stderr)
|
fputs("Wrote \(manifestURL.path)\n", stderr)
|
||||||
|
|
||||||
|
let sourceManifestURL = parent.appendingPathComponent("manifest.json")
|
||||||
|
let sourceManifest: [String: Any] = [
|
||||||
|
"version": "v1",
|
||||||
|
"generated_at": ISO8601DateFormatter().string(from: Date()),
|
||||||
|
"locale": "zh-Hans",
|
||||||
|
"entry_count": merged.count,
|
||||||
|
"sources": [[
|
||||||
|
"key": "ai_tech_seed",
|
||||||
|
"label": "OSGKeyboard curated AI/tech lexicon",
|
||||||
|
"license": "MIT (curated seed; OSGKeyboard contributors)",
|
||||||
|
"raw_count": aiTech.count,
|
||||||
|
]],
|
||||||
|
"notes": [
|
||||||
|
"Project-curated bilingual AI brands, technology terms, companies, and names.",
|
||||||
|
"No third-party cell dictionaries or Sogou-derived data.",
|
||||||
|
"PhraseCount weights map to SFCustomLanguageModelData relative frequencies.",
|
||||||
|
],
|
||||||
|
"files": ["phrases": "phrases.tsv"],
|
||||||
|
]
|
||||||
|
let sourceManifestData = try JSONSerialization.data(
|
||||||
|
withJSONObject: sourceManifest,
|
||||||
|
options: [.prettyPrinted, .sortedKeys]
|
||||||
|
)
|
||||||
|
try sourceManifestData.write(to: sourceManifestURL)
|
||||||
|
fputs("Wrote \(sourceManifestURL.path)\n", stderr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,133 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""Parse Sogou .scel cell dictionaries into (word, pinyin) entries.
|
|
||||||
|
|
||||||
Layout follows the classic SCEL format used by imewlconverter / SogouPopularDict.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import struct
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class ScelInfo:
|
|
||||||
word_count: int
|
|
||||||
name: str
|
|
||||||
type_name: str
|
|
||||||
description: str
|
|
||||||
|
|
||||||
|
|
||||||
def _read_uint16(handle) -> int:
|
|
||||||
data = handle.read(2)
|
|
||||||
if not data or len(data) < 2:
|
|
||||||
return 0
|
|
||||||
return struct.unpack("<H", data)[0]
|
|
||||||
|
|
||||||
|
|
||||||
def _read_uint32(handle) -> int:
|
|
||||||
data = handle.read(4)
|
|
||||||
if not data or len(data) < 4:
|
|
||||||
return 0
|
|
||||||
return struct.unpack("<I", data)[0]
|
|
||||||
|
|
||||||
|
|
||||||
def _read_utf16_str(handle, *, offset: int = -1, length: int = 0) -> str:
|
|
||||||
if offset >= 0:
|
|
||||||
handle.seek(offset)
|
|
||||||
if length > 0:
|
|
||||||
data = handle.read(length)
|
|
||||||
end = 0
|
|
||||||
for index in range(0, len(data), 2):
|
|
||||||
if index + 1 < len(data) and data[index] == 0 and data[index + 1] == 0:
|
|
||||||
end = index
|
|
||||||
break
|
|
||||||
if end > 0:
|
|
||||||
data = data[:end]
|
|
||||||
return data.decode("utf-16le", errors="ignore")
|
|
||||||
|
|
||||||
result = bytearray()
|
|
||||||
while True:
|
|
||||||
char = handle.read(2)
|
|
||||||
if not char or len(char) < 2 or (char[0] == 0 and char[1] == 0):
|
|
||||||
break
|
|
||||||
result.extend(char)
|
|
||||||
return result.decode("utf-16le", errors="ignore")
|
|
||||||
|
|
||||||
|
|
||||||
def is_valid_word(word: str) -> bool:
|
|
||||||
if not word or not (1 <= len(word) <= 10):
|
|
||||||
return False
|
|
||||||
allowed_punct = ",。:;?!()【】《》""''、"
|
|
||||||
return all("\u4e00" <= char <= "\u9fff" or char.isdigit() or char in allowed_punct for char in word)
|
|
||||||
|
|
||||||
|
|
||||||
def get_scel_info(scel_path: Path) -> ScelInfo:
|
|
||||||
with scel_path.open("rb") as handle:
|
|
||||||
handle.seek(0x124)
|
|
||||||
word_count = _read_uint32(handle)
|
|
||||||
handle.seek(0x130)
|
|
||||||
name = _read_utf16_str(handle, length=64)
|
|
||||||
handle.seek(0x338)
|
|
||||||
type_name = _read_utf16_str(handle, length=64)
|
|
||||||
handle.seek(0x540)
|
|
||||||
description = _read_utf16_str(handle, length=1024)
|
|
||||||
return ScelInfo(word_count=word_count, name=name, type_name=type_name, description=description)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_scel_file(scel_path: Path) -> list[tuple[str, str]]:
|
|
||||||
"""Return ordered (word, pinyin) pairs from a .scel file."""
|
|
||||||
entries: list[tuple[str, str]] = []
|
|
||||||
|
|
||||||
with scel_path.open("rb") as handle:
|
|
||||||
handle.seek(0x1540)
|
|
||||||
pinyin_count = _read_uint32(handle)
|
|
||||||
pinyin_dict: dict[int, str] = {}
|
|
||||||
for _ in range(pinyin_count):
|
|
||||||
pinyin_idx = _read_uint16(handle)
|
|
||||||
pinyin_len = _read_uint16(handle)
|
|
||||||
pinyin = handle.read(pinyin_len).decode("utf-16le", errors="ignore").strip().lower()
|
|
||||||
pinyin_dict[pinyin_idx] = pinyin
|
|
||||||
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
same_pinyin_count = _read_uint16(handle)
|
|
||||||
pinyin_index_len = _read_uint16(handle)
|
|
||||||
if pinyin_index_len <= 0 or same_pinyin_count <= 0:
|
|
||||||
break
|
|
||||||
|
|
||||||
pinyin_parts: list[str] = []
|
|
||||||
for _ in range(pinyin_index_len // 2):
|
|
||||||
idx = _read_uint16(handle)
|
|
||||||
part = pinyin_dict.get(idx, "")
|
|
||||||
if part:
|
|
||||||
pinyin_parts.append(part)
|
|
||||||
joined_pinyin = " ".join(pinyin_parts).strip()
|
|
||||||
|
|
||||||
for _ in range(same_pinyin_count):
|
|
||||||
word_len = _read_uint16(handle)
|
|
||||||
word = handle.read(word_len).decode("utf-16le", errors="ignore")
|
|
||||||
_ = _read_uint16(handle)
|
|
||||||
_ = _read_uint32(handle)
|
|
||||||
_ = handle.read(6)
|
|
||||||
if is_valid_word(word):
|
|
||||||
entries.append((word, joined_pinyin))
|
|
||||||
except (struct.error, OSError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
return entries
|
|
||||||
|
|
||||||
|
|
||||||
def load_pinyin_tsv(tsv_path: Path) -> list[tuple[str, str]]:
|
|
||||||
entries: list[tuple[str, str]] = []
|
|
||||||
with tsv_path.open(encoding="utf-8") as handle:
|
|
||||||
for raw_line in handle:
|
|
||||||
line = raw_line.strip()
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
word, _, pinyin = line.partition("\t")
|
|
||||||
if word and pinyin:
|
|
||||||
entries.append((word, pinyin.strip()))
|
|
||||||
return entries
|
|
||||||
@@ -130,6 +130,7 @@
|
|||||||
"OSGKeyboardExtTests/PinyinNextKeyResolverTests",
|
"OSGKeyboardExtTests/PinyinNextKeyResolverTests",
|
||||||
"OSGKeyboardExtTests/RimeSchemaGeneratorTests",
|
"OSGKeyboardExtTests/RimeSchemaGeneratorTests",
|
||||||
"OSGKeyboardExtTests/RimePersonalDictionaryExporterTests",
|
"OSGKeyboardExtTests/RimePersonalDictionaryExporterTests",
|
||||||
|
"OSGKeyboardExtTests/TypingTouchTrackerTests",
|
||||||
"OSGKeyboardTests/CursorNavigationTests",
|
"OSGKeyboardTests/CursorNavigationTests",
|
||||||
"OSGKeyboardTests/KeyboardTranslationConfigProtectionTests",
|
"OSGKeyboardTests/KeyboardTranslationConfigProtectionTests",
|
||||||
"OSGKeyboardTests/AIHintPoolTests",
|
"OSGKeyboardTests/AIHintPoolTests",
|
||||||
@@ -139,6 +140,7 @@
|
|||||||
"OSGKeyboardTests/AIUserSkillStoreTests",
|
"OSGKeyboardTests/AIUserSkillStoreTests",
|
||||||
"OSGKeyboardTests/AIEventExtractionTests",
|
"OSGKeyboardTests/AIEventExtractionTests",
|
||||||
"OSGKeyboardTests/AIAddressExtractionTests",
|
"OSGKeyboardTests/AIAddressExtractionTests",
|
||||||
|
"OSGKeyboardTests/AINoteExportTests",
|
||||||
"OSGKeyboardTests/ClipboardHistoryPolicyTests",
|
"OSGKeyboardTests/ClipboardHistoryPolicyTests",
|
||||||
"OSGKeyboardTests/ClipboardHistoryStoreTests"
|
"OSGKeyboardTests/ClipboardHistoryStoreTests"
|
||||||
]
|
]
|
||||||
|
|||||||
Vendored
+10
@@ -13,3 +13,13 @@ Before `xcodegen generate` or opening the project, run:
|
|||||||
This clones the package (if missing) and applies OSG's `StreamingConfig.context` patch for vocabulary prompts.
|
This clones the package (if missing) and applies OSG's `StreamingConfig.context` patch for vocabulary prompts.
|
||||||
|
|
||||||
The clone lives at `ThirdParty/mlx-audio-swift/` (gitignored). SPM resolves mlx-audio-swift's own dependencies on first Mac build.
|
The clone lives at `ThirdParty/mlx-audio-swift/` (gitignored). SPM resolves mlx-audio-swift's own dependencies on first Mac build.
|
||||||
|
|
||||||
|
The resolved Mac speech stack also includes:
|
||||||
|
|
||||||
|
- `mlx-swift` and `mlx-swift-lm` (MIT)
|
||||||
|
- `swift-transformers` and `swift-huggingface` (Apache-2.0)
|
||||||
|
- optional Qwen3-ASR 0.6B / 1.7B MLX 4-bit model downloads
|
||||||
|
(Apache-2.0)
|
||||||
|
|
||||||
|
See `NOTICE-TYPING.md` and the in-app Third-Party Licenses screen for pinned
|
||||||
|
versions, purposes, upstream links, and license texts.
|
||||||
|
|||||||
+32
-22
@@ -1,4 +1,4 @@
|
|||||||
# App Store Connect — OSGKeyboard 1.7.5 (build 66)
|
# App Store Connect — OSGKeyboard 1.8.0 (build 72)
|
||||||
|
|
||||||
> Current metadata baseline for the iOS/iPadOS App Store build. Version and build
|
> Current metadata baseline for the iOS/iPadOS App Store build. Version and build
|
||||||
> numbers come from `project.yml`. The repository also contains a separate
|
> numbers come from `project.yml`. The repository also contains a separate
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
| App name | `OSGKeyboard` | ≤ 30 characters |
|
| App name | `OSGKeyboard` | ≤ 30 characters |
|
||||||
| Subtitle | `Voice input, everywhere` | ≤ 30 characters |
|
| Subtitle | `Voice input, everywhere` | ≤ 30 characters |
|
||||||
| Bundle ID | `com.osgkeyboard.ios` | iOS host target |
|
| Bundle ID | `com.osgkeyboard.ios` | iOS host target |
|
||||||
| Version / build | `1.7.5` / `66` | `MARKETING_VERSION` / `CURRENT_PROJECT_VERSION` |
|
| Version / build | `1.8.0` / `72` | `MARKETING_VERSION` / `CURRENT_PROJECT_VERSION` |
|
||||||
| Minimum system | iOS/iPadOS 26 | iPhone and iPad |
|
| Minimum system | iOS/iPadOS 26 | iPhone and iPad |
|
||||||
| Primary locale | `en-US` | Simplified Chinese is also bundled |
|
| Primary locale | `en-US` | Simplified Chinese is also bundled |
|
||||||
| Primary category | Utilities | |
|
| Primary category | Utilities | |
|
||||||
@@ -101,25 +101,31 @@ 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
|
keyboard,voice,dictation,speech,transcribe,AI,pinyin,Chinese,English,polish,typing,productivity
|
||||||
```
|
```
|
||||||
|
|
||||||
## What's new in 1.7.5
|
## What's new in 1.8.0
|
||||||
|
|
||||||
```text
|
```text
|
||||||
NEW
|
NEW
|
||||||
• Optional clipboard history (latest 15 plain-text items) with a top-bar
|
• Skills center with Reply, Summarize, Translate, custom skills, and
|
||||||
panel and suggestion strip; AI idle hints can use a fresh copy for ~30s.
|
exports to Reminders, Calendar, Notes, and Maps.
|
||||||
• AI mode empty state rotates one-line suggestions; tap sends the card
|
• English QuickType-style suggestions with smarter system, contact,
|
||||||
prompt without using the mic.
|
text-replacement, and neighbor-key corrections.
|
||||||
• Spoken clipboard requests in AI mode attach stored clipboard text when
|
• Improved Pinyin abbreviations and on-device typing-habit learning.
|
||||||
you name the clipboard out loud.
|
• Overlapping key presses, double-space period, contextual Return labels,
|
||||||
• Settings → AI Agent adds Response length (Short / Medium / Detailed).
|
and a full-width iPad keyboard with editing controls.
|
||||||
• AI answers stream into the keyboard as the model writes.
|
• 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.
|
||||||
|
|
||||||
CHANGED
|
CHANGED
|
||||||
• History and Personal dictionary move from the tab bar onto Home preview
|
• Hold the microphone to edit the last verified input, then preview,
|
||||||
cards; engine status scrolls with the page.
|
replace, or append the result.
|
||||||
• Translation control sits beside the mic (mirrored with Undo); the former
|
• Undo now covers dictation, AI answers, edits, and clipboard pastes.
|
||||||
top-bar slot is the Clipboard button.
|
• Home cards and navigation make History, Personal Dictionary, Skills,
|
||||||
• Undo covers clipboard pastes as well as dictation, AI answers, and edits.
|
Styles, and Settings easier to find.
|
||||||
|
|
||||||
|
FIXED
|
||||||
|
• Improved Chinese input setup, Universal Clipboard responsiveness,
|
||||||
|
keyboard switching, recording cancellation, and AI session recovery.
|
||||||
```
|
```
|
||||||
|
|
||||||
## App Review information
|
## App Review information
|
||||||
@@ -183,10 +189,14 @@ credential.
|
|||||||
- Linked to the user: Yes
|
- Linked to the user: Yes
|
||||||
- Used for tracking: No
|
- Used for tracking: No
|
||||||
|
|
||||||
This covers transcripts, polish/translation text, AI questions, optional
|
This covers transcripts and nearby cursor context used for polish/translation,
|
||||||
provider search requests, dictionary terms included in provider prompts, and
|
AI questions and skill prompts, optional provider search requests, dictionary
|
||||||
clipboard text only after the user inserts it and actively invokes polish.
|
terms included in provider prompts, and clipboard text only after the user
|
||||||
Device-local clipboard history by itself is not collected by the developer.
|
actively invokes a clipboard skill, names the clipboard in AI mode, or inserts
|
||||||
|
it and requests polish. Skill results may also be handed on-device to an Apple
|
||||||
|
Shortcut, while navigation addresses may be opened in the selected map app.
|
||||||
|
Device-local clipboard history and typing-learning data by themselves are not
|
||||||
|
collected by the developer.
|
||||||
|
|
||||||
### Do not select
|
### Do not select
|
||||||
|
|
||||||
@@ -201,10 +211,10 @@ standard HTTPS. Re-evaluate this answer if non-exempt cryptography is added.
|
|||||||
|
|
||||||
## Submission checklist
|
## Submission checklist
|
||||||
|
|
||||||
- [ ] Confirm `project.yml` still reads version 1.7.5 / build 66
|
- [ ] Confirm `project.yml` still reads version 1.8.0 / build 72
|
||||||
- [ ] Open the existing Xcode project (do not regenerate unless needed)
|
- [ ] Open the existing Xcode project (do not regenerate unless needed)
|
||||||
- [ ] Run the release build and test suites on macOS with Xcode 26
|
- [ ] Run the release build and test suites on macOS with Xcode 26
|
||||||
- [ ] Replace screenshots with captures from the submitted build
|
- [ ] Replace screenshots with captures from the submitted build
|
||||||
- [ ] Verify the privacy answers against the submitted provider features
|
- [ ] Verify the privacy answers against the submitted provider features
|
||||||
- [ ] Confirm the tip product remains optional and unlocks no feature
|
- [ ] Confirm the tip product remains optional and unlocks no feature
|
||||||
- [ ] Upload, select build 66, add review notes, and submit
|
- [ ] Upload, select build 72, add review notes, and submit
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ LocalASRBiasAdapter.adapt(
|
|||||||
| `hardHotwords` | 为具备 hard-hotword capability 的 backend 保留;当前 Qwen3 MLX 不使用 |
|
| `hardHotwords` | 为具备 hard-hotword capability 的 backend 保留;当前 Qwen3 MLX 不使用 |
|
||||||
|
|
||||||
个人词优先;`BuiltinLexiconIndex` 从 `phrases.tsv` 选择 `weight >= 4` 的 Top-N,
|
个人词优先;`BuiltinLexiconIndex` 从 `phrases.tsv` 选择 `weight >= 4` 的 Top-N,
|
||||||
代码编辑器/终端前台场景优先 `computer_terms`。默认最多考虑 300 个内置 ASR 词,
|
代码编辑器/终端前台场景优先项目维护的 `ai_tech_seed`。默认最多考虑 300 个内置 ASR 词,
|
||||||
Qwen3 soft prompt 最长 800 字符,润色补充最多 40 个内置词。
|
Qwen3 soft prompt 最长 800 字符,润色补充最多 40 个内置词。
|
||||||
|
|
||||||
### 2.3 Apple Speech fallback
|
### 2.3 Apple Speech fallback
|
||||||
|
|||||||
@@ -176,6 +176,135 @@
|
|||||||
<p class="empty" id="empty-zh">暂无与当前版本匹配的更新说明。</p>
|
<p class="empty" id="empty-zh">暂无与当前版本匹配的更新说明。</p>
|
||||||
<p class="empty" id="empty-en">No release notes match this app version.</p>
|
<p class="empty" id="empty-en">No release notes match this app version.</p>
|
||||||
|
|
||||||
|
<!-- ========== 1.8.0 ========== -->
|
||||||
|
<article class="version-block" data-version="1.8.0">
|
||||||
|
<div class="version-chip">1.8.0</div>
|
||||||
|
|
||||||
|
<div class="lang-block" data-lang="zh">
|
||||||
|
<section class="section">
|
||||||
|
<h2>新功能</h2>
|
||||||
|
|
||||||
|
<div class="group">
|
||||||
|
<p class="group-title">AI 与语音助手</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>AI 键盘模式</strong>:新增独立 AI 输入面,可通过语音提问、查看回答,并在确认后插入或发送</li>
|
||||||
|
<li><strong>AI 联网搜索</strong>:支持服务商侧联网搜索,可回答天气、新闻等需要最新资料的问题,搜索失败时自动重试</li>
|
||||||
|
<li><strong>AI 流式回答</strong>:模型生成内容时,回答会实时显示在键盘中,无需等待整段完成</li>
|
||||||
|
<li><strong>AI 空闲建议</strong>:AI 面板会轮播常用提问和热点建议,点选后可直接生成回答</li>
|
||||||
|
<li><strong>回复篇幅设置</strong>:新增简短、中等、详细三档 AI 回复长度,并支持随设置同步</li>
|
||||||
|
<li><strong>编辑上次输入</strong>:长按麦克风即可口述修改要求,预览后替换上次输入或追加新内容</li>
|
||||||
|
<li><strong>问句保护</strong>:润色会保留用户原本的提问语气,不再把「你能听到吗」等问句改写成回答</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="group">
|
||||||
|
<p class="group-title">剪贴板与技能</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>剪贴板历史</strong>:可选保存最近 15 条纯文本,支持查看、插入和单独清空,默认关闭且仅保存在本机</li>
|
||||||
|
<li><strong>剪贴板建议条</strong>:开启后,最新复制内容会显示在键盘上方,可一键插入</li>
|
||||||
|
<li><strong>粘贴权限引导</strong>:剪贴板设置页可直接打开系统设置,方便将「从其他 App 粘贴」设为允许</li>
|
||||||
|
<li><strong>口述剪贴板请求</strong>:在 AI 模式中说出「回复剪贴板」或「翻译剪贴板」,即可处理已保存的复制内容</li>
|
||||||
|
<li><strong>剪贴板快捷技能</strong>:复制后可直接选择回复、总结或翻译,无需先粘贴到输入框</li>
|
||||||
|
<li><strong>技能中心</strong>:首页新增技能页,可管理最多 8 个键盘技能,并通过长按拖动调整顺序</li>
|
||||||
|
<li><strong>自定义技能</strong>:可自定义名称、图标和提示词,也可连接自己的 iCloud 快捷指令</li>
|
||||||
|
<li><strong>提取待办</strong>:可从复制内容识别待办事项,并通过快捷指令写入提醒事项</li>
|
||||||
|
<li><strong>提取日程</strong>:可识别日期、时间、地点和结束时间,并写入系统日历</li>
|
||||||
|
<li><strong>存入备忘录</strong>:可根据复制内容生成简短标题,并将原文保存到 Apple 备忘录</li>
|
||||||
|
<li><strong>地址导航</strong>:可识别地址或起点终点,并直接打开高德、百度或 Apple 地图开始驾车导航</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>中英文输入</h2>
|
||||||
|
<ul>
|
||||||
|
<li><strong>英文 QuickType 候选栏</strong>:输入英文时同时显示原词、纠错和补全,提交后自动清空</li>
|
||||||
|
<li><strong>英文词汇覆盖扩充</strong>:加入约 4 万词的离线词表,并结合系统词库、通讯录名称和文本替换提供建议</li>
|
||||||
|
<li><strong>英文邻键纠错</strong>:按错相邻按键时,会优先推荐更符合真实键盘位置的单词</li>
|
||||||
|
<li><strong>英文自动更正更克制</strong>:减少对专名、短词和全大写词的误改;拒绝纠错后会学习保留原词</li>
|
||||||
|
<li><strong>拼音简拼改进</strong>:优化 <code>wom</code> 等混合简拼排序,并支持 <code>zh</code>、<code>ch</code>、<code>sh</code> 两键简拼</li>
|
||||||
|
<li><strong>输入习惯学习</strong>:中文候选频率和英文选择偏好会保存在本机,设置中可单独清除,不影响个性词库</li>
|
||||||
|
<li><strong>叠指连打</strong>:支持上一键尚未抬起时按下下一键,并按按下顺序输出文字</li>
|
||||||
|
<li><strong>双空格句号</strong>:英文单词后连续按两次空格,会自动转换为句号和空格,并开启句首大写</li>
|
||||||
|
<li><strong>回车键场景文案</strong>:回车键会根据当前输入框显示前往、搜索、发送、完成、下一项等系统文案</li>
|
||||||
|
<li><strong>iPad 键盘适配</strong>:键盘横竖屏均铺满可用宽度,底行加入逗号、句号和系统地球键,并提供撤销、重做、复制和剪切</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>界面、稳定性与隐私</h2>
|
||||||
|
<ul>
|
||||||
|
<li><strong>统一撤销上次输入</strong>:听写、AI 回答、编辑结果和剪贴板粘贴均可一次撤销</li>
|
||||||
|
<li><strong>首页与导航调整</strong>:历史记录和个性词库改为首页资料卡;技能、风格和设置入口重新整理</li>
|
||||||
|
<li><strong>输入稳定性改进</strong>:修复中文输入初始化失败、跨设备剪贴板导致键盘卡住、切换键盘崩溃、录音取消不及时及 AI 会话结束后麦克风不可用等问题</li>
|
||||||
|
<li><strong>数据控制更新</strong>:默认使用设备端语音识别;AI 使用用户自己的 API Key;剪贴板功能默认关闭,内容仅在用户主动触发时交给所配置的服务商</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lang-block" data-lang="en">
|
||||||
|
<section class="section">
|
||||||
|
<h2>New Features</h2>
|
||||||
|
|
||||||
|
<div class="group">
|
||||||
|
<p class="group-title">AI and voice assistant</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>AI keyboard mode</strong>: ask by voice, review the answer on the keyboard, then insert or send it when ready</li>
|
||||||
|
<li><strong>AI web search</strong>: supported providers can search for current information such as weather and news, with automatic fallback when search fails</li>
|
||||||
|
<li><strong>Streaming AI answers</strong>: answers appear on the keyboard as the model writes instead of waiting for the full response</li>
|
||||||
|
<li><strong>AI idle suggestions</strong>: the AI panel rotates useful prompts and current topics that can be sent with one tap</li>
|
||||||
|
<li><strong>Response length</strong>: choose Short, Medium, or Detailed AI answers and sync the preference with settings</li>
|
||||||
|
<li><strong>Edit last input</strong>: hold the microphone, describe the change, preview it, then replace the last input or append the result</li>
|
||||||
|
<li><strong>Question protection</strong>: polish preserves the user's question instead of rewriting prompts such as “Can you hear me?” into answers</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="group">
|
||||||
|
<p class="group-title">Clipboard and Skills</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Clipboard history</strong>: optionally keep the latest 15 plain-text copies on this device, with separate view, insert, and clear controls</li>
|
||||||
|
<li><strong>Clipboard suggestion strip</strong>: show the newest copy above the keyboard for one-tap insertion</li>
|
||||||
|
<li><strong>Paste permission guide</strong>: open system Settings directly to set Paste from Other Apps to Allow</li>
|
||||||
|
<li><strong>Spoken clipboard requests</strong>: say “reply to the clipboard” or “translate my clipboard” in AI mode to use the saved copy</li>
|
||||||
|
<li><strong>Clipboard quick skills</strong>: choose Reply, Summarize, or Translate after copying without first pasting into a field</li>
|
||||||
|
<li><strong>Skills center</strong>: manage up to eight keyboard skills and reorder them with a long-press drag</li>
|
||||||
|
<li><strong>Custom skills</strong>: create a name, icon, and prompt, with an optional iCloud Shortcut connection</li>
|
||||||
|
<li><strong>Extract to-dos</strong>: recognize tasks in copied text and add them to Reminders through a companion Shortcut</li>
|
||||||
|
<li><strong>Extract events</strong>: recognize dates, times, locations, and end times, then add them to Calendar</li>
|
||||||
|
<li><strong>Save to Notes</strong>: generate a short title and save the original copied text to Apple Notes</li>
|
||||||
|
<li><strong>Navigate to an address</strong>: recognize an address or route and open driving directions in Amap, Baidu Maps, or Apple Maps</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Chinese and English Typing</h2>
|
||||||
|
<ul>
|
||||||
|
<li><strong>English QuickType bar</strong>: see the typed word, correction, and completion together; the bar clears after commit</li>
|
||||||
|
<li><strong>Broader English vocabulary</strong>: an offline list of about 40,000 words works with the system lexicon, contact names, and text replacements</li>
|
||||||
|
<li><strong>Neighbor-key correction</strong>: mistypes on nearby keys prioritize words that match the physical keyboard layout</li>
|
||||||
|
<li><strong>More conservative autocorrect</strong>: fewer unwanted changes to names, short words, and all-caps text; rejecting a correction teaches the original</li>
|
||||||
|
<li><strong>Pinyin abbreviation improvements</strong>: mixed forms such as <code>wom</code> rank better, with two-key <code>zh</code>, <code>ch</code>, and <code>sh</code> abbreviations</li>
|
||||||
|
<li><strong>Typing habit learning</strong>: Chinese candidate frequency and English boosts stay on device and can be cleared without deleting the personal dictionary</li>
|
||||||
|
<li><strong>Overlapping key presses</strong>: press the next key before lifting the previous finger while characters still commit in press order</li>
|
||||||
|
<li><strong>Double-space period</strong>: a second Space after an English word becomes a period and space, then enables sentence capitalization</li>
|
||||||
|
<li><strong>Contextual Return labels</strong>: the action key shows Go, Search, Send, Done, Next, and other system labels for the current field</li>
|
||||||
|
<li><strong>iPad keyboard layout</strong>: fills portrait and landscape width, adds comma, period, and the system globe key, plus undo, redo, copy, and cut</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Interface, Stability & Privacy</h2>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Unified undo</strong>: roll back dictation, AI answers, edit results, and clipboard pastes in one step</li>
|
||||||
|
<li><strong>Home and navigation updates</strong>: History and Personal Dictionary become Home cards, with reorganized Skills, Styles, and Settings navigation</li>
|
||||||
|
<li><strong>Input stability</strong>: fixes Chinese setup failures, Universal Clipboard keyboard freezes, keyboard-switch crashes, delayed recording cancellation, and a stuck microphone after AI sessions</li>
|
||||||
|
<li><strong>Data controls</strong>: on-device speech recognition remains the default; AI uses your own API key; Clipboard features stay off until enabled and send text only after your action</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
<!-- ========== 1.7.5 ========== -->
|
<!-- ========== 1.7.5 ========== -->
|
||||||
<article class="version-block" data-version="1.7.5">
|
<article class="version-block" data-version="1.7.5">
|
||||||
<div class="version-chip">1.7.5</div>
|
<div class="version-chip">1.7.5</div>
|
||||||
|
|||||||
+20
-13
@@ -25,19 +25,22 @@
|
|||||||
<p class="top-nav"><a href="./">← OSGKeyboard</a> · <a href="https://apps.apple.com/app/osgkeyboard/id6781553267">App Store</a></p>
|
<p class="top-nav"><a href="./">← OSGKeyboard</a> · <a href="https://apps.apple.com/app/osgkeyboard/id6781553267">App Store</a></p>
|
||||||
<p class="lang"><a href="#zh">中文</a></p>
|
<p class="lang"><a href="#zh">中文</a></p>
|
||||||
<h1>OSGKeyboard Privacy Policy</h1>
|
<h1>OSGKeyboard Privacy Policy</h1>
|
||||||
<p><strong>Last updated:</strong> August 11, 2026 · <em>v1.2</em></p>
|
<p><strong>Last updated:</strong> August 14, 2026 · <em>v1.3</em></p>
|
||||||
<p>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 <code>SpeechAnalyzer</code> + <code>DictationTranscriber</code> 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.</p>
|
<p>OSGKeyboard provides an iOS custom keyboard and a macOS menu-bar dictation app. iOS uses Apple's on-device <code>SpeechAnalyzer</code> + <code>DictationTranscriber</code> 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. This policy explains what data the apps process and how it is used.</p>
|
||||||
|
|
||||||
<h2>What we collect</h2>
|
<h2>What we collect</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>Voice audio</strong> — captured only while you actively record. On the default <strong>local engine</strong>, audio is transcribed on-device with Apple's <code>SpeechAnalyzer</code> + <code>DictationTranscriber</code> and raw audio is not uploaded. If you explicitly enable the <strong>cloud engine</strong> (a confirmation dialog is shown first), your recordings are uploaded to the ASR provider you configure (e.g. OpenAI, Qwen DashScope, Zhipu) for transcription; that provider's privacy policy applies. OSGKeyboard never stores or proxies your audio on its own servers.</li>
|
<li><strong>Voice audio</strong> — captured only while you actively record. On a <strong>local engine</strong>, audio is transcribed on-device and raw audio is not uploaded. If you actively select a <strong>cloud engine</strong> in Settings, recordings are uploaded directly to the ASR provider you configure (e.g. OpenAI, Qwen DashScope, Zhipu); that provider's privacy policy applies. OSGKeyboard never stores or proxies your audio on its own servers.</li>
|
||||||
<li><strong>Transcribed text and cursor context</strong> — after ASR, the transcript (not audio) may be sent for polish when you have configured an LLM API key. To continue naturally at the insertion point, a small amount of text immediately before and after the cursor may be included. Secure fields are never captured; cursor context is not written to logs or voice history. Without an API key, raw ASR text is inserted and no polish request is sent. Polish and optional translation use the OpenAI-compatible (or Anthropic) API you configure (e.g. OpenAI, DeepSeek, Qwen DashScope, Moonshot, Zhipu, Xiaomi MiMo, or your own server).</li>
|
<li><strong>Transcribed text and cursor context</strong> — after ASR, the transcript (not audio) may be sent for polish when you have configured an LLM API key. To continue naturally at the insertion point, a small amount of text immediately before and after the cursor may be included. Secure fields are never captured; cursor context is not written to logs or voice history. Without an API key, raw ASR text is inserted and no polish request is sent. Polish and optional translation use the OpenAI-compatible (or Anthropic) API you configure (e.g. OpenAI, DeepSeek, Qwen DashScope, Moonshot, Zhipu, Xiaomi MiMo, or your own server).</li>
|
||||||
<li><strong>AI mode questions</strong> — in AI keyboard mode, your spoken question text is sent to the same configured LLM provider. When that provider supports server-side web search, it may retrieve public web results for time-sensitive answers. Search queries and snippets are handled by that provider under its privacy policy; OSGKeyboard does not operate a search index or proxy search traffic.</li>
|
<li><strong>AI mode questions</strong> — in AI keyboard mode, your spoken question text is sent to the same configured LLM provider. When that provider supports server-side web search, it may retrieve public web results for time-sensitive answers. Search queries and snippets are handled by that provider under its privacy policy; OSGKeyboard does not operate a search index or proxy search traffic.</li>
|
||||||
|
<li><strong>AI suggestions and OSGKeyboard web pages</strong> — the main app may request public suggestion titles from <code>key.osglab.com</code>; no voice, transcript, clipboard body, API key, or ordinary keystroke content is included in that request. Opening the update page requests public content from <code>download.osglab.com</code>. As with ordinary HTTPS sites, those hosts and their infrastructure may process connection metadata such as IP address and request time.</li>
|
||||||
|
<li><strong>AI Agent skills, Shortcuts, and maps</strong> — when you deliberately run a skill, the selected or copied text and skill prompt are sent to your configured LLM provider. For Reminders, Calendar, or Notes exports, the generated result is passed on-device to the Apple Shortcut you install; OSGKeyboard does not receive the resulting item. For navigation, an address or route is placed in a URL opened by Apple Maps, Amap, or Baidu Maps, and the selected map provider's policy applies.</li>
|
||||||
<li><strong>API credentials</strong> — 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. When you enable <strong>iCloud settings sync</strong>, API keys replicate through Apple's <strong>iCloud Keychain</strong> to your other signed-in devices — not through iCloud Key-Value Store JSON.</li>
|
<li><strong>API credentials</strong> — 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. When you enable <strong>iCloud settings sync</strong>, API keys replicate through Apple's <strong>iCloud Keychain</strong> to your other signed-in devices — not through iCloud Key-Value Store JSON.</li>
|
||||||
<li><strong>App preferences</strong> — engine mode, recognition language, polish intensity, translation target, handedness, cursor-navigation toggle, and keyboard settings are stored in App Group <code>UserDefaults</code> on your device so the main app and keyboard extension stay in sync. When iCloud settings sync is enabled, eligible preferences (excluding API keys and the device-local clipboard consent switches) may also be mirrored in your private iCloud Key-Value Store account.</li>
|
<li><strong>App preferences</strong> — engine mode, recognition language, polish intensity, translation target, handedness, cursor-navigation toggle, and keyboard settings are stored in App Group <code>UserDefaults</code> on your device so the main app and keyboard extension stay in sync. When iCloud settings sync is enabled, eligible preferences (excluding API keys and the device-local clipboard consent switches) may also be mirrored in your private iCloud Key-Value Store account.</li>
|
||||||
<li><strong>Optional clipboard history</strong> — off by default. When enabled, the keyboard may read text copied on this device or received through Universal Clipboard; iOS cannot reliably identify which source supplied an item. Up to 15 accepted items are stored only in this device’s App Group for the host app and keyboard extension. Turning history off stops capture, disables the suggestion strip, and keeps existing items. Reset Settings also keeps them; deletion requires the separate confirmed “Clear clipboard history” action. There is no fixed expiry. Secure fields immediately clear the current suggestion, close the history panel, and hide the clipboard entry point. Conservative filters reject common OTP shapes, PEM private-key headers, long three-part JWTs, Bearer tokens, recognizable provider-key prefixes of safe lengths, and common Luhn-valid 16-digit card numbers; they do not claim to detect every password. Normal order numbers, years, and short labeled account numbers remain eligible. Rejected text can still be pasted once through iOS; it is simply not recorded. Clipboard text is never sent to AI on its own. It leaves the device only when you deliberately ask for it — tapping a clipboard suggestion in AI mode, saying “clipboard” in an AI question, or invoking polish after inserting an item — and it is then sent to your configured provider as separate quoted data, never as instructions.</li>
|
<li><strong>Optional clipboard history</strong> — off by default. When enabled, the keyboard may read text copied on this device or received through Universal Clipboard; iOS cannot reliably identify which source supplied an item. Up to 15 accepted items are stored only in this device’s App Group for the host app and keyboard extension. Turning history off stops capture, disables the suggestion strip, and keeps existing items. Reset Settings also keeps them; deletion requires the separate confirmed “Clear clipboard history” action. There is no fixed expiry. Secure fields immediately clear the current suggestion, close the history panel, and hide the clipboard entry point. Conservative filters reject common OTP shapes, PEM private-key headers, long three-part JWTs, Bearer tokens, recognizable provider-key prefixes of safe lengths, and common Luhn-valid 16-digit card numbers; they do not claim to detect every password. Normal order numbers, years, and short labeled account numbers remain eligible. Rejected text can still be pasted once through iOS; it is simply not recorded. Clipboard text is never sent to AI on its own. It leaves the device only when you deliberately ask for it — tapping a clipboard suggestion in AI mode, saying “clipboard” in an AI question, or invoking polish after inserting an item — and it is then sent to your configured provider as separate quoted data, never as instructions.</li>
|
||||||
<li><strong>Personal dictionary</strong> — 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. Optional <strong>iCloud dictionary sync</strong> mirrors your dictionary through your private iCloud Key-Value Store; OSGKeyboard does not operate a separate dictionary server.</li>
|
<li><strong>Personal dictionary</strong> — 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. Optional <strong>iCloud dictionary sync</strong> mirrors your dictionary through your private iCloud Key-Value Store; OSGKeyboard does not operate a separate dictionary server.</li>
|
||||||
<li><strong>On-device typing learning</strong> — the Chinese keyboard stores selected words and candidate frequencies in the App Group on your device so future candidates improve. This user dictionary is not uploaded by OSGKeyboard.</li>
|
<li><strong>On-device typing learning</strong> — Chinese candidate frequencies and English suggestion choices are stored in the local App Group. You can clear this learning separately in typing settings without deleting the personal dictionary. OSGKeyboard does not upload it.</li>
|
||||||
|
<li><strong>macOS local speech models</strong> — when you choose a Qwen3-ASR local model, the Mac app downloads model files from Hugging Face and stores them in its local application-support directory until you remove them. Local inference does not send your audio to Hugging Face or OSGKeyboard.</li>
|
||||||
<li><strong>Voice history</strong> — 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.</li>
|
<li><strong>Voice history</strong> — 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.</li>
|
||||||
<li><strong>Usage statistics</strong> — cumulative dictation time, dictation characters, and translation characters are computed for the home screen stats card. When iCloud settings sync is enabled, per-device totals may merge through your private iCloud Key-Value Store.</li>
|
<li><strong>Usage statistics</strong> — cumulative dictation time, dictation characters, and translation characters are computed for the home screen stats card. When iCloud settings sync is enabled, per-device totals may merge through your private iCloud Key-Value Store.</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -47,7 +50,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li>We do <strong>not</strong> log or upload ordinary keystrokes. Only the on-device Chinese candidate-learning data described above is retained locally.</li>
|
<li>We do <strong>not</strong> log or upload ordinary keystrokes. Only the on-device Chinese candidate-learning data described above is retained locally.</li>
|
||||||
<li>We do <strong>not</strong> operate analytics, crash reporting, or advertising SDKs.</li>
|
<li>We do <strong>not</strong> operate analytics, crash reporting, or advertising SDKs.</li>
|
||||||
<li>We do <strong>not</strong> upload raw audio anywhere on the default local engine. The only exception is the optional cloud ASR engine: if you explicitly enable it (a confirmation dialog is shown first), recordings go to the ASR provider <em>you</em> configure — never to servers of ours.</li>
|
<li>We do <strong>not</strong> upload raw audio on a local engine. If you actively select a cloud ASR engine in Settings, recordings go to the ASR provider <em>you</em> configure — never to servers of ours.</li>
|
||||||
<li>We do <strong>not</strong> sell personal data.</li>
|
<li>We do <strong>not</strong> sell personal data.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
@@ -62,10 +65,10 @@
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2>Third parties</h2>
|
<h2>Third parties</h2>
|
||||||
<p>After ASR, transcribed text may be sent for polish and optional translation when you configure an LLM API key. AI-mode questions may also be sent to that provider, which may perform server-side web search. That provider's privacy policy applies. OSGKeyboard does not proxy, log, or aggregate your requests.</p>
|
<p>Cloud ASR and LLM requests go directly to the provider you configure. AI skills may also hand generated data to Apple Shortcuts or open a route in Apple Maps, Amap, or Baidu Maps. Public suggestions and update pages are served by <code>key.osglab.com</code> and <code>download.osglab.com</code>. Optional Mac models are downloaded from Hugging Face. Each third party's privacy policy applies; OSGKeyboard does not proxy, log, or aggregate your provider requests.</p>
|
||||||
|
|
||||||
<h2>Data retention</h2>
|
<h2>Data retention</h2>
|
||||||
<p>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 eligible preferences, statistics, and voice 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. Clipboard history remains device-local, is capped at 15 entries, has no fixed expiry, and is retained when capture is turned off or settings are reset; use the separate confirmed clear action to delete it.</p>
|
<p>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 eligible preferences, statistics, dictionary data, and voice history may sync through your private iCloud account. Voice history is capped at 300 entries and can be cleared from History or by resetting settings. Clipboard history remains device-local, is capped at 15 entries, has no fixed expiry, and remains when capture is turned off or settings are reset; use the separate confirmed clear action to delete it. Chinese and English typing-learning data stays local and can be cleared from typing settings without deleting the personal dictionary. Downloaded Mac speech models remain until you remove the model in the app or delete its local files.</p>
|
||||||
|
|
||||||
<h2>Children's privacy</h2>
|
<h2>Children's privacy</h2>
|
||||||
<p>OSGKeyboard is not directed to children under 13 and does not knowingly collect personal data from children.</p>
|
<p>OSGKeyboard is not directed to children under 13 and does not knowingly collect personal data from children.</p>
|
||||||
@@ -81,18 +84,22 @@
|
|||||||
|
|
||||||
<hr id="zh">
|
<hr id="zh">
|
||||||
<h1>OSGKeyboard 隐私政策</h1>
|
<h1>OSGKeyboard 隐私政策</h1>
|
||||||
<p><strong>更新日期:</strong>2026 年 8 月 11 日 · <em>v1.2</em></p>
|
<p><strong>更新日期:</strong>2026 年 8 月 14 日 · <em>v1.3</em></p>
|
||||||
<p>OSGKeyboard 是一款 iOS 自定义键盘,可将语音转为文字。它以自定义键盘扩展的形式运行,需要 iOS 26 及以上系统,默认使用 Apple 端侧的 <code>SpeechAnalyzer</code> + <code>DictationTranscriber</code> 转写;可选的云端识别引擎(需显式二次确认开启)会把录音上传到你配置的服务商。转写完成后,文字可能经云端 LLM 润色或翻译。本政策说明应用处理哪些数据及用途。</p>
|
<p>OSGKeyboard 包含 iOS 自定义键盘与 macOS 菜单栏听写应用。iOS 默认使用 Apple 端侧的 <code>SpeechAnalyzer</code> + <code>DictationTranscriber</code>;macOS 可使用下载到本机的 Qwen3-ASR 模型。你也可以在设置中主动选择云端识别引擎。转写后,文字可能通过你配置的服务商用于润色、翻译或 AI Agent 功能。本政策说明应用处理哪些数据及用途。</p>
|
||||||
|
|
||||||
<h2>我们处理的数据</h2>
|
<h2>我们处理的数据</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>语音音频</strong> — 仅在你主动录音时采集。默认<strong>本地引擎</strong>下,音频在设备端通过 <code>SpeechAnalyzer</code> + <code>DictationTranscriber</code> 转写,原始录音不会上传。若你显式开启<strong>云端引擎</strong>(会先弹出确认对话框),录音会上传到你配置的识别服务商(如 OpenAI、通义 DashScope、智谱)完成转写,适用该服务商的隐私政策。OSGKeyboard 自身绝不存储或中转你的音频。</li>
|
<li><strong>语音音频</strong> — 仅在你主动录音时采集。使用<strong>本地引擎</strong>时,音频在设备端完成转写,原始录音不会上传。若你在设置中主动选择<strong>云端引擎</strong>,录音会直接上传到你配置的识别服务商(如 OpenAI、通义 DashScope、智谱),适用该服务商的隐私政策。OSGKeyboard 自身不存储或中转音频。</li>
|
||||||
<li><strong>转写文字与光标上下文</strong> — ASR 完成后,若你已配置 LLM API Key,转写文字(非音频)可能发送润色。为了在插入点自然衔接,请求可能同时包含光标前后的少量文字。密码框绝不采集,光标上下文不会写入日志或语音历史。未填写 API Key 时直接插入原始识别结果。润色与可选翻译使用你配置的 OpenAI 兼容(或 Anthropic)API(OpenAI / DeepSeek / 通义 DashScope / Moonshot / 智谱 / 小米 MiMo / 自建服务等)。</li>
|
<li><strong>转写文字与光标上下文</strong> — ASR 完成后,若你已配置 LLM API Key,转写文字(非音频)可能发送润色。为了在插入点自然衔接,请求可能同时包含光标前后的少量文字。密码框绝不采集,光标上下文不会写入日志或语音历史。未填写 API Key 时直接插入原始识别结果。润色与可选翻译使用你配置的 OpenAI 兼容(或 Anthropic)API(OpenAI / DeepSeek / 通义 DashScope / Moonshot / 智谱 / 小米 MiMo / 自建服务等)。</li>
|
||||||
<li><strong>AI 模式问题</strong> — 在 AI 键盘模式下,语音转写后的问题文字会发送到同一套已配置的 LLM 服务商。若该服务商支持服务端联网搜索,可能为时效性问题检索公开网页结果。搜索词与检索片段由该服务商按其隐私政策处理;OSGKeyboard 不运营搜索索引,也不中转搜索流量。</li>
|
<li><strong>AI 模式问题</strong> — 在 AI 键盘模式下,语音转写后的问题文字会发送到同一套已配置的 LLM 服务商。若该服务商支持服务端联网搜索,可能为时效性问题检索公开网页结果。搜索词与检索片段由该服务商按其隐私政策处理;OSGKeyboard 不运营搜索索引,也不中转搜索流量。</li>
|
||||||
|
<li><strong>AI 建议与 OSGKeyboard 网页</strong> — 主 App 可能从 <code>key.osglab.com</code> 请求公开建议标题;该请求不包含语音、转写、剪贴板正文、API Key 或普通击键内容。打开更新页时会从 <code>download.osglab.com</code> 请求公开内容。与普通 HTTPS 网站相同,相关主机与基础设施可能处理 IP 地址、请求时间等连接元数据。</li>
|
||||||
|
<li><strong>AI Agent 技能、快捷指令与地图</strong> — 仅在你主动运行技能时,所选或复制的文字及技能提示词才会发送到你配置的 LLM 服务商。导出提醒事项、日历或备忘录时,生成结果会在设备端交给你安装的 Apple 快捷指令;OSGKeyboard 不会收到最终创建的项目。导航时,地址或路线会写入 URL 并交给 Apple 地图、高德地图或百度地图,相应地图服务商的政策适用。</li>
|
||||||
<li><strong>API 凭证</strong> — 云端引擎的 LLM API Key 保存在设备 Keychain,仅在发起 LLM 请求时读取;通过共享 Keychain 组与主 App 共享,<strong>不会</strong>写入 <code>UserDefaults</code>。开启<strong>iCloud 设置同步</strong>后,API 密钥经 Apple <strong>iCloud 钥匙串</strong>同步到你其他已登录设备,<strong>不会</strong>写入 iCloud 键值存储 JSON。</li>
|
<li><strong>API 凭证</strong> — 云端引擎的 LLM API Key 保存在设备 Keychain,仅在发起 LLM 请求时读取;通过共享 Keychain 组与主 App 共享,<strong>不会</strong>写入 <code>UserDefaults</code>。开启<strong>iCloud 设置同步</strong>后,API 密钥经 Apple <strong>iCloud 钥匙串</strong>同步到你其他已登录设备,<strong>不会</strong>写入 iCloud 键值存储 JSON。</li>
|
||||||
<li><strong>应用偏好</strong> — 引擎模式、识别语言、润色档位、翻译目标、握持偏好、光标导航开关、键盘设置等保存在 App Group <code>UserDefaults</code>,用于主 App 与键盘扩展之间的状态同步。开启 iCloud 设置同步后,可同步的偏好(不含 API 密钥与本机剪贴板许可开关)也可能镜像到你私有的 iCloud 键值存储账户。</li>
|
<li><strong>应用偏好</strong> — 引擎模式、识别语言、润色档位、翻译目标、握持偏好、光标导航开关、键盘设置等保存在 App Group <code>UserDefaults</code>,用于主 App 与键盘扩展之间的状态同步。开启 iCloud 设置同步后,可同步的偏好(不含 API 密钥与本机剪贴板许可开关)也可能镜像到你私有的 iCloud 键值存储账户。</li>
|
||||||
<li><strong>可选剪贴板历史</strong> — 默认关闭。开启后,键盘可能读取本机复制或经通用剪贴板收到的文字;iOS 无法可靠判断具体来源。最多 15 条通过规则的文本仅保存在本机主 App 与键盘扩展共享的 App Group。关闭历史只会停止采集、关闭建议条并保留已有记录;重置设置也会保留,只有单独确认的「清空剪贴板历史」操作会删除。历史没有固定过期时间。进入安全输入框会立即清除当前建议、关闭历史面板并隐藏剪贴板入口。保守过滤会拒绝常见 OTP 形态、PEM 私钥头、三段较长的 JWT、Bearer Token、达到安全长度的明确服务商密钥前缀,以及常见的通过 Luhn 校验的 16 位卡号;不宣称识别所有密码。普通订单号、年份与带标签的短账号仍可进入历史。被拒绝的文本仍可经 iOS 一次性粘贴,只是不被记录。剪贴板正文不会自行发送给 AI。只有你主动要求时它才会离开设备——在 AI 模式点选剪贴板建议、在 AI 提问中明确说出「剪贴板」,或插入后主动使用润色——此时会作为单独引用的数据发送给你配置的服务商,绝不作为指令。</li>
|
<li><strong>可选剪贴板历史</strong> — 默认关闭。开启后,键盘可能读取本机复制或经通用剪贴板收到的文字;iOS 无法可靠判断具体来源。最多 15 条通过规则的文本仅保存在本机主 App 与键盘扩展共享的 App Group。关闭历史只会停止采集、关闭建议条并保留已有记录;重置设置也会保留,只有单独确认的「清空剪贴板历史」操作会删除。历史没有固定过期时间。进入安全输入框会立即清除当前建议、关闭历史面板并隐藏剪贴板入口。保守过滤会拒绝常见 OTP 形态、PEM 私钥头、三段较长的 JWT、Bearer Token、达到安全长度的明确服务商密钥前缀,以及常见的通过 Luhn 校验的 16 位卡号;不宣称识别所有密码。普通订单号、年份与带标签的短账号仍可进入历史。被拒绝的文本仍可经 iOS 一次性粘贴,只是不被记录。剪贴板正文不会自行发送给 AI。只有你主动要求时它才会离开设备——在 AI 模式点选剪贴板建议、在 AI 提问中明确说出「剪贴板」,或插入后主动使用润色——此时会作为单独引用的数据发送给你配置的服务商,绝不作为指令。</li>
|
||||||
<li><strong>个性词库</strong> — 你在「词库」Tab 添加的词条与别名保存在本机,润色时会写入 LLM 提示词。可选的<strong>iCloud 词库同步</strong>经私有 iCloud 键值存储在多设备间镜像;OSGKeyboard 不运营独立词库服务器。</li>
|
<li><strong>个性词库</strong> — 你在「词库」Tab 添加的词条与别名保存在本机,润色时会写入 LLM 提示词。可选的<strong>iCloud 词库同步</strong>经私有 iCloud 键值存储在多设备间镜像;OSGKeyboard 不运营独立词库服务器。</li>
|
||||||
|
<li><strong>本地输入学习</strong> — 中文候选频率与英文候选选择偏好保存在本机 App Group,可在输入设置中单独清除,不会因此删除个性词库,OSGKeyboard 不会上传这些学习数据。</li>
|
||||||
|
<li><strong>macOS 本地语音模型</strong> — 选择 Qwen3-ASR 本地模型后,Mac App 会从 Hugging Face 下载模型文件,并保存在本机应用支持目录,直到你主动移除。本地推理不会把录音发送到 Hugging Face 或 OSGKeyboard。</li>
|
||||||
<li><strong>语音历史</strong> — 主 App 可在「历史」页保留近期成功转写,上限 300 条。开启 iCloud 设置同步后,历史也可能经私有 iCloud 键值存储同步。</li>
|
<li><strong>语音历史</strong> — 主 App 可在「历史」页保留近期成功转写,上限 300 条。开启 iCloud 设置同步后,历史也可能经私有 iCloud 键值存储同步。</li>
|
||||||
<li><strong>用量统计</strong> — 首页统计卡片的累计听写时长、听写字数、翻译字数在本地计算。开启 iCloud 设置同步后,各设备分量可能经私有 iCloud 键值存储合并。</li>
|
<li><strong>用量统计</strong> — 首页统计卡片的累计听写时长、听写字数、翻译字数在本地计算。开启 iCloud 设置同步后,各设备分量可能经私有 iCloud 键值存储合并。</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -102,7 +109,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li>我们<strong>不会</strong>记录或上传你平时在键盘上的击键内容。</li>
|
<li>我们<strong>不会</strong>记录或上传你平时在键盘上的击键内容。</li>
|
||||||
<li>我们<strong>不会</strong>集成分析、崩溃上报或广告 SDK。</li>
|
<li>我们<strong>不会</strong>集成分析、崩溃上报或广告 SDK。</li>
|
||||||
<li>默认本地引擎下,我们<strong>不会</strong>将原始录音上传至任何服务器。唯一例外是可选的云端识别引擎:你显式开启后(会先弹出确认对话框),录音会发送到<em>你</em>配置的识别服务商——绝不会发送到我们的服务器。</li>
|
<li>使用本地引擎时,我们<strong>不会</strong>上传原始录音。若你在设置中主动选择云端 ASR,引擎会将录音发送到<em>你</em>配置的识别服务商,而不是我们的服务器。</li>
|
||||||
<li>我们<strong>不会</strong>出售个人数据。</li>
|
<li>我们<strong>不会</strong>出售个人数据。</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
@@ -117,10 +124,10 @@
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2>第三方</h2>
|
<h2>第三方</h2>
|
||||||
<p>ASR 完成后,若你配置了 LLM API Key,转写文字可能用于润色与可选翻译;AI 模式问题也可能发送至同一服务商(含服务商侧可选联网搜索)。该服务商的隐私政策适用于相关请求。OSGKeyboard 不代理、不记录、不聚合这些请求。</p>
|
<p>云端 ASR 与 LLM 请求直接发送到你配置的服务商。AI 技能也可能把生成数据交给 Apple 快捷指令,或在 Apple 地图、高德地图、百度地图中打开路线。公开建议与更新页面分别由 <code>key.osglab.com</code>、<code>download.osglab.com</code> 提供;可选 Mac 模型从 Hugging Face 下载。各第三方的隐私政策适用;OSGKeyboard 不代理、记录或聚合你的服务商请求。</p>
|
||||||
|
|
||||||
<h2>数据保留</h2>
|
<h2>数据保留</h2>
|
||||||
<p>设置保留在设备上,直至卸载或重置。开启 iCloud 设置同步后,API 密钥经 iCloud 钥匙串同步,可同步的偏好、统计与语音历史可能经私有 iCloud 账户同步。识别结果写入你正在使用的宿主 App,OSGKeyboard 不会长期存储。语音历史上限 300 条,可随时在「历史」页清空或通过重置设置清除。剪贴板历史仅保存在本机,上限 15 条,没有固定过期时间;关闭采集或重置设置均会保留,需使用单独确认的清空操作删除。</p>
|
<p>设置保留在设备上,直至卸载或重置。开启 iCloud 设置同步后,API 密钥经 iCloud 钥匙串同步,可同步的偏好、统计、词库和语音历史可能经私有 iCloud 账户同步。语音历史上限 300 条,可在「历史」页清空或通过重置设置清除。剪贴板历史仅保存在本机,上限 15 条,没有固定过期时间;关闭采集或重置设置均会保留,需使用单独确认的清空操作删除。中英文输入学习数据仅留本机,可在输入设置中单独清除且不影响个性词库。下载的 Mac 语音模型会保留到你在应用中移除模型或删除本地文件。</p>
|
||||||
|
|
||||||
<h2>儿童隐私</h2>
|
<h2>儿童隐私</h2>
|
||||||
<p>OSGKeyboard 不面向 13 岁以下儿童,亦不会明知地从儿童处收集个人信息。</p>
|
<p>OSGKeyboard 不面向 13 岁以下儿童,亦不会明知地从儿童处收集个人信息。</p>
|
||||||
|
|||||||
+20
-13
@@ -25,19 +25,22 @@
|
|||||||
<p class="top-nav"><a href="../">← OSGKeyboard</a> · <a href="https://apps.apple.com/app/osgkeyboard/id6781553267">App Store</a></p>
|
<p class="top-nav"><a href="../">← OSGKeyboard</a> · <a href="https://apps.apple.com/app/osgkeyboard/id6781553267">App Store</a></p>
|
||||||
<p class="lang"><a href="#zh">中文</a></p>
|
<p class="lang"><a href="#zh">中文</a></p>
|
||||||
<h1>OSGKeyboard Privacy Policy</h1>
|
<h1>OSGKeyboard Privacy Policy</h1>
|
||||||
<p><strong>Last updated:</strong> August 11, 2026 · <em>v1.2</em></p>
|
<p><strong>Last updated:</strong> August 14, 2026 · <em>v1.3</em></p>
|
||||||
<p>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 <code>SpeechAnalyzer</code> + <code>DictationTranscriber</code> 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.</p>
|
<p>OSGKeyboard provides an iOS custom keyboard and a macOS menu-bar dictation app. iOS uses Apple's on-device <code>SpeechAnalyzer</code> + <code>DictationTranscriber</code> 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. This policy explains what data the apps process and how it is used.</p>
|
||||||
|
|
||||||
<h2>What we collect</h2>
|
<h2>What we collect</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>Voice audio</strong> — captured only while you actively record. On the default <strong>local engine</strong>, audio is transcribed on-device with Apple's <code>SpeechAnalyzer</code> + <code>DictationTranscriber</code> and raw audio is not uploaded. If you explicitly enable the <strong>cloud engine</strong> (a confirmation dialog is shown first), your recordings are uploaded to the ASR provider you configure (e.g. OpenAI, Qwen DashScope, Zhipu) for transcription; that provider's privacy policy applies. OSGKeyboard never stores or proxies your audio on its own servers.</li>
|
<li><strong>Voice audio</strong> — captured only while you actively record. On a <strong>local engine</strong>, audio is transcribed on-device and raw audio is not uploaded. If you actively select a <strong>cloud engine</strong> in Settings, recordings are uploaded directly to the ASR provider you configure (e.g. OpenAI, Qwen DashScope, Zhipu); that provider's privacy policy applies. OSGKeyboard never stores or proxies your audio on its own servers.</li>
|
||||||
<li><strong>Transcribed text and cursor context</strong> — after ASR, the transcript (not audio) may be sent for polish when you have configured an LLM API key. To continue naturally at the insertion point, a small amount of text immediately before and after the cursor may be included. Secure fields are never captured; cursor context is not written to logs or voice history. Without an API key, raw ASR text is inserted and no polish request is sent. Polish and optional translation use the OpenAI-compatible (or Anthropic) API you configure (e.g. OpenAI, DeepSeek, Qwen DashScope, Moonshot, Zhipu, Xiaomi MiMo, or your own server).</li>
|
<li><strong>Transcribed text and cursor context</strong> — after ASR, the transcript (not audio) may be sent for polish when you have configured an LLM API key. To continue naturally at the insertion point, a small amount of text immediately before and after the cursor may be included. Secure fields are never captured; cursor context is not written to logs or voice history. Without an API key, raw ASR text is inserted and no polish request is sent. Polish and optional translation use the OpenAI-compatible (or Anthropic) API you configure (e.g. OpenAI, DeepSeek, Qwen DashScope, Moonshot, Zhipu, Xiaomi MiMo, or your own server).</li>
|
||||||
<li><strong>AI mode questions</strong> — in AI keyboard mode, your spoken question text is sent to the same configured LLM provider. When that provider supports server-side web search, it may retrieve public web results for time-sensitive answers. Search queries and snippets are handled by that provider under its privacy policy; OSGKeyboard does not operate a search index or proxy search traffic.</li>
|
<li><strong>AI mode questions</strong> — in AI keyboard mode, your spoken question text is sent to the same configured LLM provider. When that provider supports server-side web search, it may retrieve public web results for time-sensitive answers. Search queries and snippets are handled by that provider under its privacy policy; OSGKeyboard does not operate a search index or proxy search traffic.</li>
|
||||||
|
<li><strong>AI suggestions and OSGKeyboard web pages</strong> — the main app may request public suggestion titles from <code>key.osglab.com</code>; no voice, transcript, clipboard body, API key, or ordinary keystroke content is included in that request. Opening the update page requests public content from <code>download.osglab.com</code>. As with ordinary HTTPS sites, those hosts and their infrastructure may process connection metadata such as IP address and request time.</li>
|
||||||
|
<li><strong>AI Agent skills, Shortcuts, and maps</strong> — when you deliberately run a skill, the selected or copied text and skill prompt are sent to your configured LLM provider. For Reminders, Calendar, or Notes exports, the generated result is passed on-device to the Apple Shortcut you install; OSGKeyboard does not receive the resulting item. For navigation, an address or route is placed in a URL opened by Apple Maps, Amap, or Baidu Maps, and the selected map provider's policy applies.</li>
|
||||||
<li><strong>API credentials</strong> — 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. When you enable <strong>iCloud settings sync</strong>, API keys replicate through Apple's <strong>iCloud Keychain</strong> to your other signed-in devices — not through iCloud Key-Value Store JSON.</li>
|
<li><strong>API credentials</strong> — 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. When you enable <strong>iCloud settings sync</strong>, API keys replicate through Apple's <strong>iCloud Keychain</strong> to your other signed-in devices — not through iCloud Key-Value Store JSON.</li>
|
||||||
<li><strong>App preferences</strong> — engine mode, recognition language, polish intensity, translation target, handedness, cursor-navigation toggle, and keyboard settings are stored in App Group <code>UserDefaults</code> on your device so the main app and keyboard extension stay in sync. Device-local clipboard consent switches are not activated by iCloud settings sync.</li>
|
<li><strong>App preferences</strong> — engine mode, recognition language, polish intensity, translation target, handedness, cursor-navigation toggle, and keyboard settings are stored in App Group <code>UserDefaults</code> on your device so the main app and keyboard extension stay in sync. Device-local clipboard consent switches are not activated by iCloud settings sync.</li>
|
||||||
<li><strong>Optional clipboard history</strong> — off by default. When enabled, the keyboard may read text copied on this device or received through Universal Clipboard; iOS cannot reliably identify which source supplied an item. Up to 15 accepted items are stored only in this device’s App Group for the host app and keyboard extension. Turning history off stops capture, disables the suggestion strip, and keeps existing items. Reset Settings also keeps them; deletion requires the separate confirmed “Clear clipboard history” action. There is no fixed expiry. Secure fields immediately clear the current suggestion, close the history panel, and hide the clipboard entry point. Conservative filters reject common OTP shapes, PEM private-key headers, long three-part JWTs, Bearer tokens, recognizable provider-key prefixes of safe lengths, and common Luhn-valid 16-digit card numbers; they do not claim to detect every password. Normal order numbers, years, and short labeled account numbers remain eligible. Rejected text can still be pasted once through iOS; it is simply not recorded. Clipboard text is never sent to AI on its own. It leaves the device only when you deliberately ask for it — tapping a clipboard suggestion in AI mode, saying “clipboard” in an AI question, or invoking polish after inserting an item — and it is then sent to your configured provider as separate quoted data, never as instructions.</li>
|
<li><strong>Optional clipboard history</strong> — off by default. When enabled, the keyboard may read text copied on this device or received through Universal Clipboard; iOS cannot reliably identify which source supplied an item. Up to 15 accepted items are stored only in this device’s App Group for the host app and keyboard extension. Turning history off stops capture, disables the suggestion strip, and keeps existing items. Reset Settings also keeps them; deletion requires the separate confirmed “Clear clipboard history” action. There is no fixed expiry. Secure fields immediately clear the current suggestion, close the history panel, and hide the clipboard entry point. Conservative filters reject common OTP shapes, PEM private-key headers, long three-part JWTs, Bearer tokens, recognizable provider-key prefixes of safe lengths, and common Luhn-valid 16-digit card numbers; they do not claim to detect every password. Normal order numbers, years, and short labeled account numbers remain eligible. Rejected text can still be pasted once through iOS; it is simply not recorded. Clipboard text is never sent to AI on its own. It leaves the device only when you deliberately ask for it — tapping a clipboard suggestion in AI mode, saying “clipboard” in an AI question, or invoking polish after inserting an item — and it is then sent to your configured provider as separate quoted data, never as instructions.</li>
|
||||||
<li><strong>Personal dictionary</strong> — 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. Optional <strong>iCloud dictionary sync</strong> mirrors your dictionary through your private iCloud Key-Value Store; OSGKeyboard does not operate a separate dictionary server.</li>
|
<li><strong>Personal dictionary</strong> — 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. Optional <strong>iCloud dictionary sync</strong> mirrors your dictionary through your private iCloud Key-Value Store; OSGKeyboard does not operate a separate dictionary server.</li>
|
||||||
<li><strong>On-device typing learning</strong> — the Chinese keyboard stores selected words and candidate frequencies in the App Group on your device so future candidates improve. This user dictionary is not uploaded by OSGKeyboard.</li>
|
<li><strong>On-device typing learning</strong> — Chinese candidate frequencies and English suggestion choices are stored in the local App Group. You can clear this learning separately in typing settings without deleting the personal dictionary. OSGKeyboard does not upload it.</li>
|
||||||
|
<li><strong>macOS local speech models</strong> — when you choose a Qwen3-ASR local model, the Mac app downloads model files from Hugging Face and stores them in its local application-support directory until you remove them. Local inference does not send your audio to Hugging Face or OSGKeyboard.</li>
|
||||||
<li><strong>Voice history</strong> — 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.</li>
|
<li><strong>Voice history</strong> — 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.</li>
|
||||||
<li><strong>Usage statistics</strong> — cumulative dictation time, dictation characters, translation characters, and dictionary entry count are computed and stored locally on the home screen stats card.</li>
|
<li><strong>Usage statistics</strong> — cumulative dictation time, dictation characters, translation characters, and dictionary entry count are computed and stored locally on the home screen stats card.</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -47,7 +50,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li>We do <strong>not</strong> log or upload ordinary keystrokes. Only the on-device Chinese candidate-learning data described above is retained locally.</li>
|
<li>We do <strong>not</strong> log or upload ordinary keystrokes. Only the on-device Chinese candidate-learning data described above is retained locally.</li>
|
||||||
<li>We do <strong>not</strong> operate analytics, crash reporting, or advertising SDKs.</li>
|
<li>We do <strong>not</strong> operate analytics, crash reporting, or advertising SDKs.</li>
|
||||||
<li>We do <strong>not</strong> upload raw audio anywhere on the default local engine. The only exception is the optional cloud ASR engine: if you explicitly enable it (a confirmation dialog is shown first), recordings go to the ASR provider <em>you</em> configure — never to servers of ours.</li>
|
<li>We do <strong>not</strong> upload raw audio on a local engine. If you actively select a cloud ASR engine in Settings, recordings go to the ASR provider <em>you</em> configure — never to servers of ours.</li>
|
||||||
<li>We do <strong>not</strong> sell personal data.</li>
|
<li>We do <strong>not</strong> sell personal data.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
@@ -62,10 +65,10 @@
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2>Third parties</h2>
|
<h2>Third parties</h2>
|
||||||
<p>After ASR, transcribed text may be sent for polish and optional translation when you configure an LLM API key. AI-mode questions may also be sent to that provider, which may perform server-side web search. That provider's privacy policy applies. OSGKeyboard does not proxy, log, or aggregate your requests.</p>
|
<p>Cloud ASR and LLM requests go directly to the provider you configure. AI skills may also hand generated data to Apple Shortcuts or open a route in Apple Maps, Amap, or Baidu Maps. Public suggestions and update pages are served by <code>key.osglab.com</code> and <code>download.osglab.com</code>. Optional Mac models are downloaded from Hugging Face. Each third party's privacy policy applies; OSGKeyboard does not proxy, log, or aggregate your provider requests.</p>
|
||||||
|
|
||||||
<h2>Data retention</h2>
|
<h2>Data retention</h2>
|
||||||
<p>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 eligible preferences, statistics, and voice 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. Clipboard history remains device-local, is capped at 15 entries, has no fixed expiry, and is retained when capture is turned off or settings are reset; use the separate confirmed clear action to delete it.</p>
|
<p>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 eligible preferences, statistics, dictionary data, and voice history may sync through your private iCloud account. Voice history is capped at 300 entries and can be cleared from History or by resetting settings. Clipboard history remains device-local, is capped at 15 entries, has no fixed expiry, and remains when capture is turned off or settings are reset; use the separate confirmed clear action to delete it. Chinese and English typing-learning data stays local and can be cleared from typing settings without deleting the personal dictionary. Downloaded Mac speech models remain until you remove the model in the app or delete its local files.</p>
|
||||||
|
|
||||||
<h2>Children's privacy</h2>
|
<h2>Children's privacy</h2>
|
||||||
<p>OSGKeyboard is not directed to children under 13 and does not knowingly collect personal data from children.</p>
|
<p>OSGKeyboard is not directed to children under 13 and does not knowingly collect personal data from children.</p>
|
||||||
@@ -81,18 +84,22 @@
|
|||||||
|
|
||||||
<hr id="zh">
|
<hr id="zh">
|
||||||
<h1>OSGKeyboard 隐私政策</h1>
|
<h1>OSGKeyboard 隐私政策</h1>
|
||||||
<p><strong>更新日期:</strong>2026 年 8 月 11 日 · <em>v1.2</em></p>
|
<p><strong>更新日期:</strong>2026 年 8 月 14 日 · <em>v1.3</em></p>
|
||||||
<p>OSGKeyboard 是一款 iOS 自定义键盘,可将语音转为文字。它以自定义键盘扩展的形式运行,需要 iOS 26 及以上系统,默认使用 Apple 端侧的 <code>SpeechAnalyzer</code> + <code>DictationTranscriber</code> 转写;可选的云端识别引擎(需显式二次确认开启)会把录音上传到你配置的服务商。转写完成后,文字可能经云端 LLM 润色或翻译。本政策说明应用处理哪些数据及用途。</p>
|
<p>OSGKeyboard 包含 iOS 自定义键盘与 macOS 菜单栏听写应用。iOS 默认使用 Apple 端侧的 <code>SpeechAnalyzer</code> + <code>DictationTranscriber</code>;macOS 可使用下载到本机的 Qwen3-ASR 模型。你也可以在设置中主动选择云端识别引擎。转写后,文字可能通过你配置的服务商用于润色、翻译或 AI Agent 功能。本政策说明应用处理哪些数据及用途。</p>
|
||||||
|
|
||||||
<h2>我们处理的数据</h2>
|
<h2>我们处理的数据</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>语音音频</strong> — 仅在你主动录音时采集。默认<strong>本地引擎</strong>下,音频在设备端通过 <code>SpeechAnalyzer</code> + <code>DictationTranscriber</code> 转写,原始录音不会上传。若你显式开启<strong>云端引擎</strong>(会先弹出确认对话框),录音会上传到你配置的识别服务商(如 OpenAI、通义 DashScope、智谱)完成转写,适用该服务商的隐私政策。OSGKeyboard 自身绝不存储或中转你的音频。</li>
|
<li><strong>语音音频</strong> — 仅在你主动录音时采集。使用<strong>本地引擎</strong>时,音频在设备端完成转写,原始录音不会上传。若你在设置中主动选择<strong>云端引擎</strong>,录音会直接上传到你配置的识别服务商(如 OpenAI、通义 DashScope、智谱),适用该服务商的隐私政策。OSGKeyboard 自身不存储或中转音频。</li>
|
||||||
<li><strong>转写文字与光标上下文</strong> — ASR 完成后,若你已配置 LLM API Key,转写文字(非音频)可能发送润色。为了在插入点自然衔接,请求可能同时包含光标前后的少量文字。密码框绝不采集,光标上下文不会写入日志或语音历史。未填写 API Key 时直接插入原始识别结果。润色与可选翻译使用你配置的 OpenAI 兼容(或 Anthropic)API(OpenAI / DeepSeek / 通义 DashScope / Moonshot / 智谱 / 小米 MiMo / 自建服务等)。</li>
|
<li><strong>转写文字与光标上下文</strong> — ASR 完成后,若你已配置 LLM API Key,转写文字(非音频)可能发送润色。为了在插入点自然衔接,请求可能同时包含光标前后的少量文字。密码框绝不采集,光标上下文不会写入日志或语音历史。未填写 API Key 时直接插入原始识别结果。润色与可选翻译使用你配置的 OpenAI 兼容(或 Anthropic)API(OpenAI / DeepSeek / 通义 DashScope / Moonshot / 智谱 / 小米 MiMo / 自建服务等)。</li>
|
||||||
<li><strong>AI 模式问题</strong> — 在 AI 键盘模式下,语音转写后的问题文字会发送到同一套已配置的 LLM 服务商。若该服务商支持服务端联网搜索,可能为时效性问题检索公开网页结果。搜索词与检索片段由该服务商按其隐私政策处理;OSGKeyboard 不运营搜索索引,也不中转搜索流量。</li>
|
<li><strong>AI 模式问题</strong> — 在 AI 键盘模式下,语音转写后的问题文字会发送到同一套已配置的 LLM 服务商。若该服务商支持服务端联网搜索,可能为时效性问题检索公开网页结果。搜索词与检索片段由该服务商按其隐私政策处理;OSGKeyboard 不运营搜索索引,也不中转搜索流量。</li>
|
||||||
|
<li><strong>AI 建议与 OSGKeyboard 网页</strong> — 主 App 可能从 <code>key.osglab.com</code> 请求公开建议标题;该请求不包含语音、转写、剪贴板正文、API Key 或普通击键内容。打开更新页时会从 <code>download.osglab.com</code> 请求公开内容。与普通 HTTPS 网站相同,相关主机与基础设施可能处理 IP 地址、请求时间等连接元数据。</li>
|
||||||
|
<li><strong>AI Agent 技能、快捷指令与地图</strong> — 仅在你主动运行技能时,所选或复制的文字及技能提示词才会发送到你配置的 LLM 服务商。导出提醒事项、日历或备忘录时,生成结果会在设备端交给你安装的 Apple 快捷指令;OSGKeyboard 不会收到最终创建的项目。导航时,地址或路线会写入 URL 并交给 Apple 地图、高德地图或百度地图,相应地图服务商的政策适用。</li>
|
||||||
<li><strong>API 凭证</strong> — 云端引擎的 LLM API Key 保存在设备 Keychain,仅在发起 LLM 请求时读取;通过共享 Keychain 组与主 App 共享,<strong>不会</strong>写入 <code>UserDefaults</code>。开启<strong>iCloud 设置同步</strong>后,API 密钥经 Apple <strong>iCloud 钥匙串</strong>同步到你其他已登录设备,<strong>不会</strong>写入 iCloud 键值存储 JSON。</li>
|
<li><strong>API 凭证</strong> — 云端引擎的 LLM API Key 保存在设备 Keychain,仅在发起 LLM 请求时读取;通过共享 Keychain 组与主 App 共享,<strong>不会</strong>写入 <code>UserDefaults</code>。开启<strong>iCloud 设置同步</strong>后,API 密钥经 Apple <strong>iCloud 钥匙串</strong>同步到你其他已登录设备,<strong>不会</strong>写入 iCloud 键值存储 JSON。</li>
|
||||||
<li><strong>应用偏好</strong> — 引擎模式、识别语言、润色档位、翻译目标、握持偏好、光标导航开关、键盘设置等保存在 App Group <code>UserDefaults</code>,用于主 App 与键盘扩展之间的状态同步。本机剪贴板许可开关不会被 iCloud 设置同步开启。</li>
|
<li><strong>应用偏好</strong> — 引擎模式、识别语言、润色档位、翻译目标、握持偏好、光标导航开关、键盘设置等保存在 App Group <code>UserDefaults</code>,用于主 App 与键盘扩展之间的状态同步。本机剪贴板许可开关不会被 iCloud 设置同步开启。</li>
|
||||||
<li><strong>可选剪贴板历史</strong> — 默认关闭。开启后,键盘可能读取本机复制或经通用剪贴板收到的文字;iOS 无法可靠判断具体来源。最多 15 条通过规则的文本仅保存在本机主 App 与键盘扩展共享的 App Group。关闭历史只会停止采集、关闭建议条并保留已有记录;重置设置也会保留,只有单独确认的「清空剪贴板历史」操作会删除。历史没有固定过期时间。进入安全输入框会立即清除当前建议、关闭历史面板并隐藏剪贴板入口。保守过滤会拒绝常见 OTP 形态、PEM 私钥头、三段较长的 JWT、Bearer Token、达到安全长度的明确服务商密钥前缀,以及常见的通过 Luhn 校验的 16 位卡号;不宣称识别所有密码。普通订单号、年份与带标签的短账号仍可进入历史。被拒绝的文本仍可经 iOS 一次性粘贴,只是不被记录。剪贴板正文不会自行发送给 AI。只有你主动要求时它才会离开设备——在 AI 模式点选剪贴板建议、在 AI 提问中明确说出「剪贴板」,或插入后主动使用润色——此时会作为单独引用的数据发送给你配置的服务商,绝不作为指令。</li>
|
<li><strong>可选剪贴板历史</strong> — 默认关闭。开启后,键盘可能读取本机复制或经通用剪贴板收到的文字;iOS 无法可靠判断具体来源。最多 15 条通过规则的文本仅保存在本机主 App 与键盘扩展共享的 App Group。关闭历史只会停止采集、关闭建议条并保留已有记录;重置设置也会保留,只有单独确认的「清空剪贴板历史」操作会删除。历史没有固定过期时间。进入安全输入框会立即清除当前建议、关闭历史面板并隐藏剪贴板入口。保守过滤会拒绝常见 OTP 形态、PEM 私钥头、三段较长的 JWT、Bearer Token、达到安全长度的明确服务商密钥前缀,以及常见的通过 Luhn 校验的 16 位卡号;不宣称识别所有密码。普通订单号、年份与带标签的短账号仍可进入历史。被拒绝的文本仍可经 iOS 一次性粘贴,只是不被记录。剪贴板正文不会自行发送给 AI。只有你主动要求时它才会离开设备——在 AI 模式点选剪贴板建议、在 AI 提问中明确说出「剪贴板」,或插入后主动使用润色——此时会作为单独引用的数据发送给你配置的服务商,绝不作为指令。</li>
|
||||||
<li><strong>个性词库</strong> — 你在「词库」Tab 添加的词条与别名保存在本机,润色时会写入 LLM 提示词。可选的<strong>iCloud 词库同步</strong>经私有 iCloud 键值存储在多设备间镜像;OSGKeyboard 不运营独立词库服务器。</li>
|
<li><strong>个性词库</strong> — 你在「词库」Tab 添加的词条与别名保存在本机,润色时会写入 LLM 提示词。可选的<strong>iCloud 词库同步</strong>经私有 iCloud 键值存储在多设备间镜像;OSGKeyboard 不运营独立词库服务器。</li>
|
||||||
|
<li><strong>本地输入学习</strong> — 中文候选频率与英文候选选择偏好保存在本机 App Group,可在输入设置中单独清除,不会因此删除个性词库,OSGKeyboard 不会上传这些学习数据。</li>
|
||||||
|
<li><strong>macOS 本地语音模型</strong> — 选择 Qwen3-ASR 本地模型后,Mac App 会从 Hugging Face 下载模型文件,并保存在本机应用支持目录,直到你主动移除。本地推理不会把录音发送到 Hugging Face 或 OSGKeyboard。</li>
|
||||||
<li><strong>语音历史</strong> — 主 App 可在「历史」页保留近期成功转写,上限 300 条。开启 iCloud 设置同步后,历史也可能经私有 iCloud 键值存储同步。</li>
|
<li><strong>语音历史</strong> — 主 App 可在「历史」页保留近期成功转写,上限 300 条。开启 iCloud 设置同步后,历史也可能经私有 iCloud 键值存储同步。</li>
|
||||||
<li><strong>用量统计</strong> — 首页统计卡片的累计听写时长、听写字数、翻译字数、词库词条数均在本地计算与保存。</li>
|
<li><strong>用量统计</strong> — 首页统计卡片的累计听写时长、听写字数、翻译字数、词库词条数均在本地计算与保存。</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -102,7 +109,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li>我们<strong>不会</strong>记录或上传你平时在键盘上的击键内容。</li>
|
<li>我们<strong>不会</strong>记录或上传你平时在键盘上的击键内容。</li>
|
||||||
<li>我们<strong>不会</strong>集成分析、崩溃上报或广告 SDK。</li>
|
<li>我们<strong>不会</strong>集成分析、崩溃上报或广告 SDK。</li>
|
||||||
<li>默认本地引擎下,我们<strong>不会</strong>将原始录音上传至任何服务器。唯一例外是可选的云端识别引擎:你显式开启后(会先弹出确认对话框),录音会发送到<em>你</em>配置的识别服务商——绝不会发送到我们的服务器。</li>
|
<li>使用本地引擎时,我们<strong>不会</strong>上传原始录音。若你在设置中主动选择云端 ASR,引擎会将录音发送到<em>你</em>配置的识别服务商,而不是我们的服务器。</li>
|
||||||
<li>我们<strong>不会</strong>出售个人数据。</li>
|
<li>我们<strong>不会</strong>出售个人数据。</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
@@ -117,10 +124,10 @@
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2>第三方</h2>
|
<h2>第三方</h2>
|
||||||
<p>ASR 完成后,若你配置了 LLM API Key,转写文字可能用于润色与可选翻译;AI 模式问题也可能发送至同一服务商(含服务商侧可选联网搜索)。该服务商的隐私政策适用于相关请求。OSGKeyboard 不代理、不记录、不聚合这些请求。</p>
|
<p>云端 ASR 与 LLM 请求直接发送到你配置的服务商。AI 技能也可能把生成数据交给 Apple 快捷指令,或在 Apple 地图、高德地图、百度地图中打开路线。公开建议与更新页面分别由 <code>key.osglab.com</code>、<code>download.osglab.com</code> 提供;可选 Mac 模型从 Hugging Face 下载。各第三方的隐私政策适用;OSGKeyboard 不代理、记录或聚合你的服务商请求。</p>
|
||||||
|
|
||||||
<h2>数据保留</h2>
|
<h2>数据保留</h2>
|
||||||
<p>设置保留在设备上,直至卸载或重置。开启 iCloud 设置同步后,API 密钥经 iCloud 钥匙串同步,可同步的偏好、统计与语音历史可能经私有 iCloud 账户同步。识别结果写入你正在使用的宿主 App,OSGKeyboard 不会长期存储。语音历史上限 300 条,可随时在「历史」页清空或通过重置设置清除。剪贴板历史仅保存在本机,上限 15 条,没有固定过期时间;关闭采集或重置设置均会保留,需使用单独确认的清空操作删除。</p>
|
<p>设置保留在设备上,直至卸载或重置。开启 iCloud 设置同步后,API 密钥经 iCloud 钥匙串同步,可同步的偏好、统计、词库和语音历史可能经私有 iCloud 账户同步。语音历史上限 300 条,可在「历史」页清空或通过重置设置清除。剪贴板历史仅保存在本机,上限 15 条,没有固定过期时间;关闭采集或重置设置均会保留,需使用单独确认的清空操作删除。中英文输入学习数据仅留本机,可在输入设置中单独清除且不影响个性词库。下载的 Mac 语音模型会保留到你在应用中移除模型或删除本地文件。</p>
|
||||||
|
|
||||||
<h2>儿童隐私</h2>
|
<h2>儿童隐私</h2>
|
||||||
<p>OSGKeyboard 不面向 13 岁以下儿童,亦不会明知地从儿童处收集个人信息。</p>
|
<p>OSGKeyboard 不面向 13 岁以下儿童,亦不会明知地从儿童处收集个人信息。</p>
|
||||||
|
|||||||
+4
-1
@@ -49,7 +49,7 @@ settings:
|
|||||||
STRING_CATALOG_GENERATE_SYMBOLS: YES
|
STRING_CATALOG_GENERATE_SYMBOLS: YES
|
||||||
CLANG_CXX_LANGUAGE_STANDARD: c++17
|
CLANG_CXX_LANGUAGE_STANDARD: c++17
|
||||||
MARKETING_VERSION: "1.7.5"
|
MARKETING_VERSION: "1.7.5"
|
||||||
CURRENT_PROJECT_VERSION: "71"
|
CURRENT_PROJECT_VERSION: "72"
|
||||||
# 签名配置来自 Signing.local.xcconfig(gitignored,不会被覆盖)
|
# 签名配置来自 Signing.local.xcconfig(gitignored,不会被覆盖)
|
||||||
|
|
||||||
# 项目级签名 xcconfig,适用于所有 target
|
# 项目级签名 xcconfig,适用于所有 target
|
||||||
@@ -136,6 +136,7 @@ targets:
|
|||||||
- path: OSGKeyboard/Resources/Typing/Licenses/NOTICE.txt
|
- path: OSGKeyboard/Resources/Typing/Licenses/NOTICE.txt
|
||||||
- path: OSGKeyboard/Resources/Typing/Licenses/LICENSE.txt
|
- path: OSGKeyboard/Resources/Typing/Licenses/LICENSE.txt
|
||||||
- path: OSGKeyboard/Resources/Typing/Licenses/LICENSE-PINYIN-SIMP-APACHE.txt
|
- path: OSGKeyboard/Resources/Typing/Licenses/LICENSE-PINYIN-SIMP-APACHE.txt
|
||||||
|
- path: OSGKeyboard/Resources/Typing/Licenses/LICENSE-MATERIAL-ICONS-APACHE.txt
|
||||||
- path: OSGKeyboard/Resources/Typing/Licenses/LICENSE-JIEBA-MIT.txt
|
- path: OSGKeyboard/Resources/Typing/Licenses/LICENSE-JIEBA-MIT.txt
|
||||||
- path: OSGKeyboard/Resources/Typing/Licenses/LICENSE-PHRASE-PINYIN-DATA-MIT.txt
|
- path: OSGKeyboard/Resources/Typing/Licenses/LICENSE-PHRASE-PINYIN-DATA-MIT.txt
|
||||||
- path: OSGKeyboard/Resources/Typing/Licenses/LICENSE-PINYIN-DATA-MIT.txt
|
- path: OSGKeyboard/Resources/Typing/Licenses/LICENSE-PINYIN-DATA-MIT.txt
|
||||||
@@ -575,6 +576,8 @@ targets:
|
|||||||
buildPhase: resources
|
buildPhase: resources
|
||||||
- path: OSGKeyboard/Resources/Typing/Licenses/LICENSE-PINYIN-SIMP-APACHE.txt
|
- path: OSGKeyboard/Resources/Typing/Licenses/LICENSE-PINYIN-SIMP-APACHE.txt
|
||||||
buildPhase: resources
|
buildPhase: resources
|
||||||
|
- path: OSGKeyboard/Resources/Typing/Licenses/LICENSE-MATERIAL-ICONS-APACHE.txt
|
||||||
|
buildPhase: resources
|
||||||
- path: OSGKeyboard/Resources/Typing/Licenses/LICENSE-JIEBA-MIT.txt
|
- path: OSGKeyboard/Resources/Typing/Licenses/LICENSE-JIEBA-MIT.txt
|
||||||
buildPhase: resources
|
buildPhase: resources
|
||||||
- path: OSGKeyboard/Resources/Typing/Licenses/LICENSE-PHRASE-PINYIN-DATA-MIT.txt
|
- path: OSGKeyboard/Resources/Typing/Licenses/LICENSE-PHRASE-PINYIN-DATA-MIT.txt
|
||||||
|
|||||||
Reference in New Issue
Block a user