Compare commits

...

2 Commits

Author SHA1 Message Date
Rocky 3033ca5b33 fix(polish): gate test-build style generation at 1,250 chars and cap export at 5,000
Remove the test-mode unlimited personal-style corpus bypass and replace it with a real 1,250-character minimum gate. Production stays at 2,500. Training-corpus export and the diagnostics view now use a 5,000-character upper bound so the user's full private history never leaves the device in a single export.

- Add PolishStyleLearningCorpusBuilder.testBuildEffectiveCharacterCount (1,250) and trainingExtractionMaximumCharacterCount (5,000); trainingWindow(from:maximumCharacterCount:) accepts a custom cap.
- PolishStylesView: drop bypassesStyleLearningCharacterGate in favor of styleLearningMinimumCharacterCount, unify the ready/remaining text, remove the obsolete polishStyles.learn.testBuildReady key.
- PolishStyleCorpusExportStore: pass the 5,000 cap to trainingWindow and expose trainingExtractionMaximumCharacterCount in the export schema.
- FlowDiagnosticsSettingsView: show the export-sized (5,000-cap) corpus so the diagnostics match what the export ZIP actually contains.
- Tests: rewrite the export cap test for 5,000, add below-cap coverage, add trainingWindow(maximumCharacterCount:) unit coverage, and convert the old UI test into a regression test for the 1,250 test-build minimum.
- L10n: drop the testBuildReady string in en + zh-Hans, update diagnostics.corpus.ready to mention 5,000.
- CHANGELOG: bilingual entry under [Unreleased].
2026-08-30 12:09:06 +08:00
Rocky eac44daaf4 chore: drop unused provider logos, dead code, placeholder screenshots, and zero-reference planning docs
Removes assets and docs that are zero-references in the codebase and the
GitHub Pages site (scanned with ripgrep across *.swift / *.md / *.html).

Removed assets (no Swift / NSImage / UIImage / Image callers):
- 19 provider logo imagesets in OSGKeyboard/Assets.xcassets/:
  anthropic, apple, ark, codingplanx, cometapi, custom, deepseek,
  gemini, groq, mimo, minimax, mistral, moonshot, openai, openrouter,
  qwen, siliconflow, xai, zhipu
- docs/assets/badges/appstore-{en,zh}.svg (README badges in use are
  ios-{en,zh}.svg and macos-{en,zh}.svg)

Removed dead code:
- OSGKeyboardShared/Models/ProviderLogo.swift (no callers; only maps
  provider ids to the now-deleted logo imagesets)

Removed placeholder screenshots:
- docs/screenshots/6.1/ and docs/screenshots/6.7/ (10 PNGs generated
  by scripts/generate_screenshot_placeholders.py; never replaced with
  real captures, no README/HTML/Swift references)

Removed zero-reference planning / audit md (no code, no README, no
CHANGELOG, no GitHub Pages HTML references):
- docs/ACCOUNT_MANAGED_GATEWAY_TESTING.md
- docs/APPSTORE_METADATA.md
- docs/STOREKIT_TESTING.md
- docs/TYPING_KEYBOARD_BASELINE.md
- docs/ios-pip-voice-session-plan.md
- docs/keyboard-accuracy-plan.md
- docs/keyboard-memory-budget.md
- docs/local-asr-architecture.md
- docs/mac-parity-audit-2026-08-23.md
- docs/personal-dictionary-icloud-kvs-checklist.md
- docs/screenshots/README.md (described the deleted placeholders)

Kept (still referenced):
- Brand marks: OSGBrandMark, OSGLogoWide, OSGStatusMark, osglogo
- docs/TESTING.md (referenced by README.md / README.en.md / CONTRIBUTING.md)
- docs/FLOW_BLUETOOTH_TESTING.md (referenced by docs/TESTING.md)
- docs/RELEASE_NOTES_1.8.0.md (referenced by CHANGELOG.md)
- docs/assets/{badges,whats-new,logo,app-icon,hero-devices} (GitHub Pages)
- AppStoreScreenshots/, marketing/ (App Store /策划素材, not in repo refs)

Updated .gitignore so the deleted md cannot be re-added locally and
pushed back by accident. Asset placeholders, dead code, and unused
badges are also covered by patterns under the same rationale.

No version bump: this is a pure cleanup (chore:) with no user-visible
behavior change.
2026-08-29 22:15:52 +08:00
75 changed files with 203 additions and 3062 deletions
+15
View File
@@ -112,3 +112,18 @@ CHANGES.md
.ai-skills-layout-shots/ .ai-skills-layout-shots/
.workbuddy-ai/ .workbuddy-ai/
reports/ reports/
# Internal planning / audit docs kept locally only (zero in-repo or
# GitHub-Pages references; never re-add these — they were removed
# in 2026-08 to keep the public docs/ limited to GitHub Pages assets).
/docs/ACCOUNT_MANAGED_GATEWAY_TESTING.md
/docs/APPSTORE_METADATA.md
/docs/STOREKIT_TESTING.md
/docs/TYPING_KEYBOARD_BASELINE.md
/docs/ios-pip-voice-session-plan.md
/docs/keyboard-accuracy-plan.md
/docs/keyboard-memory-budget.md
/docs/local-asr-architecture.md
/docs/mac-parity-audit-2026-08-23.md
/docs/personal-dictionary-icloud-kvs-checklist.md
/docs/screenshots/README.md
+1
View File
@@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Monthly usage calendar**: replace the seven-day Home and Mac Dashboard chart with a full current-month calendar whose date-circle opacity reflects each day's dictation characters, placing the Home calendar between its four metrics and Personal Dictionary. / **月度用量日历**:以完整的当月日历替换首页与 Mac 总览的近七天图表,并通过日期圆形背景透明度表示每天的听写字数;首页日历位于四项统计指标与个性词库之间。 - **Monthly usage calendar**: replace the seven-day Home and Mac Dashboard chart with a full current-month calendar whose date-circle opacity reflects each day's dictation characters, placing the Home calendar between its four metrics and Personal Dictionary. / **月度用量日历**:以完整的当月日历替换首页与 Mac 总览的近七天图表,并通过日期圆形背景透明度表示每天的听写字数;首页日历位于四项统计指标与个性词库之间。
- **Home credits and navigation**: move the credit and invitation card from Settings to Home above the usage metrics, explain free signup and referral rewards before sign-in, and open History or Personal Dictionary directly from their metric cards. / **首页积分与导航**:将积分与邀请卡片从设置迁移到首页统计指标上方,登录前明确说明免费注册与邀请奖励,并支持从听写字数和词库指标卡片直接进入历史记录或个性词库。 - **Home credits and navigation**: move the credit and invitation card from Settings to Home above the usage metrics, explain free signup and referral rewards before sign-in, and open History or Personal Dictionary directly from their metric cards. / **首页积分与导航**:将积分与邀请卡片从设置迁移到首页统计指标上方,登录前明确说明免费注册与邀请奖励,并支持从听写字数和词库指标卡片直接进入历史记录或个性词库。
- **Personal style threshold**: allow generating a learned speaking style after 2,500 effective dictation characters instead of 5,000. / **专属风格门槛**:生成学习型说话风格所需的有效听写字符由 5,000 降至 2,500。 - **Personal style threshold**: allow generating a learned speaking style after 2,500 effective dictation characters instead of 5,000. / **专属风格门槛**:生成学习型说话风格所需的有效听写字符由 5,000 降至 2,500。
- **Personal style corpus gate**: replace the test-mode unlimited gate with a 1,250-character minimum while keeping the production 2,500-character gate, and cap the training-corpus export at 5,000 effective characters so the user's full private history never leaves the device in one export. / **个人风格语料门槛**:移除测试版本的「无下限」逻辑,改为 1,250 字符最低门槛,正式版保持 2,500 字符;训练语料导出上限调整为 5,000 字符,避免一次性带出全部听写历史。
### Fixed ### Fixed
- **Personal style generation**: derive every reviewed prompt through two-stage corpus evidence, apply concrete low-confidence ASR tendencies instead of replacing them with a neutral template, recover wrapped model JSON once, keep one provider configuration and a 45-second budget across both stages, and distinguish cancellation from timeout. / **专属风格生成**:每个待审阅 Prompt 均通过两阶段语料证据生成,并在证据较少时仍应用原始 ASR 中具体的低置信度表达倾向,而非替换为中性模板;同时支持一次模型 JSON 包装恢复,在两阶段固定同一服务配置与 45 秒预算,并区分主动取消和请求超时。 - **Personal style generation**: derive every reviewed prompt through two-stage corpus evidence, apply concrete low-confidence ASR tendencies instead of replacing them with a neutral template, recover wrapped model JSON once, keep one provider configuration and a 45-second budget across both stages, and distinguish cancellation from timeout. / **专属风格生成**:每个待审阅 Prompt 均通过两阶段语料证据生成,并在证据较少时仍应用原始 ASR 中具体的低置信度表达倾向,而非替换为中性模板;同时支持一次模型 JSON 包装恢复,在两阶段固定同一服务配置与 45 秒预算,并区分主动取消和请求超时。
@@ -1,16 +0,0 @@
{
"images" : [
{
"filename" : "anthropic.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
@@ -1 +0,0 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Anthropic</title><path d="M13.827 3.52h3.603L24 20h-3.603l-6.57-16.48zm-7.258 0h3.767L16.906 20h-3.674l-1.343-3.461H5.017l-1.344 3.46H0L6.57 3.522zm4.132 9.959L8.453 7.687 6.205 13.48H10.7z"></path></svg>

Before

Width:  |  Height:  |  Size: 368 B

@@ -1,16 +0,0 @@
{
"images" : [
{
"filename" : "apple.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
-1
View File
@@ -1 +0,0 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Apple</title><path d="M11.932 6.908c.95 0 2.727-1.291 4.595-1.1.782.032 2.976.316 4.388 2.38-.113.069-2.622 1.528-2.593 4.565.034 3.617 3.166 4.828 3.221 4.85-.029.086-.506 1.723-1.658 3.416-1.002 1.463-2.039 2.919-3.675 2.95-1.606.03-2.125-.955-3.96-.955s-2.409.923-3.931.984c-1.581.06-2.78-1.58-3.79-3.037-2.065-2.98-3.64-8.422-1.527-12.087 1.051-1.824 2.93-2.98 4.969-3.009 1.549-.032 3.011 1.043 3.96 1.043zM16.552 0c.153 1.407-.411 2.817-1.251 3.833-.837 1.013-2.214 1.804-3.555 1.7-.185-1.378.495-2.814 1.27-3.712C13.883.805 15.346.05 16.553 0z"></path></svg>

Before

Width:  |  Height:  |  Size: 729 B

-16
View File
@@ -1,16 +0,0 @@
{
"images" : [
{
"filename" : "ark.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
-1
View File
@@ -1 +0,0 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Volcengine</title><path d="M7.29 5.36L3.148 21.737a.215.215 0 00.203.261h8.29a.214.214 0 00.215-.261L7.7 5.359a.214.214 0 00-.41 0z" fill-opacity=".5"></path><path clip-rule="evenodd" d="M4.553 16.18l-1.406 5.558a.214.214 0 00.203.261h2.42-4.551a.214.214 0 01-.214-.26l2.275-8.961a.214.214 0 01.409 0l.864 3.402z"></path><path d="M14.44.15a.214.214 0 00-.41 0L8.366 21.739a.214.214 0 00.214.261H19.9a.214.214 0 00.215-.261L14.44.151z" fill-opacity=".5"></path><path clip-rule="evenodd" d="M16.694 22h3.207a.215.215 0 00.214-.262l-1.839-6.993 1.164-4.592a.214.214 0 01.411 0l2.951 11.586a.214.214 0 01-.214.261h-5.894z"></path><path d="M10.278 7.741L6.685 21.736a.214.214 0 00.214.264h7.17a.216.216 0 00.214-.166.216.216 0 000-.098L10.687 7.742a.214.214 0 00-.409 0z"></path></svg>

Before

Width:  |  Height:  |  Size: 944 B

@@ -1,16 +0,0 @@
{
"images" : [
{
"filename" : "codingplanx.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
@@ -1 +0,0 @@
<svg fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M8.5 6.5 3 12l5.5 5.5 1.4-1.4L5.8 12l4.1-4.1-1.4-1.4Zm7 0-1.4 1.4L18.2 12l-4.1 4.1 1.4 1.4L21 12l-5.5-5.5Z"/></svg>

Before

Width:  |  Height:  |  Size: 205 B

@@ -1,16 +0,0 @@
{
"images" : [
{
"filename" : "cometapi.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
@@ -1 +0,0 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>CometAPI</title><path d="M7.754 3.248C9.483.97 12.144-.223 14.99.035c4.67.422 8.023 4.694 7.27 9.384-.266 1.667-1 3.125-2.203 4.374-.468.487-1.025.9-1.662 1.422-2.554 2.09-6.026 4.854-10.413 8.294-.224.176-.669.495-.94.49a.19.19 0 01-.137-.06.192.192 0 01-.05-.14c.01-.207.077-.473.202-.8.04-.108.44-.956 1.197-2.545a1.99 1.99 0 00.179-.577.143.143 0 00-.007-.068.142.142 0 00-.098-.09.144.144 0 00-.07 0 1.479 1.479 0 00-.505.237c-.414.288-.86.648-1.337 1.078-.506.453-1.137 1.025-1.895 1.716a8.873 8.873 0 01-1.252.977.155.155 0 01-.064.021.152.152 0 01-.123-.04.154.154 0 01-.037-.055c-.027-.067-.024-.165.01-.292.113-.423.283-.902.511-1.437.17-.396.52-1.206 1.051-2.428.17-.39.697-1.592.61-1.897a.167.167 0 00-.102-.111.166.166 0 00-.15.018c-.284.194-.593.485-.93.87-.782.895-1.569 1.78-2.358 2.657-.248.274-.477.388-.687.343v-.238c.058-.215.104-.438.178-.642C4.075 12.378 5.938 7.2 6.764 4.964c.198-.537.529-1.11.99-1.716zm6.49-1.771a6.641 6.641 0 100 13.283 6.641 6.641 0 000-13.283z"></path><path d="M14.244 3.104a5.017 5.017 0 11-.002 10.033 5.017 5.017 0 01.002-10.033zm2.049 1.695a3.087 3.087 0 00-4.363 1.187 1.583 1.583 0 00-.165.442c-.015.067-.027.13-.033.194a1.308 1.308 0 00.078.56c.025.07.056.137.091.203.287.529.884.944 1.43 1.288.135.086.269.167.393.245l.392.246c.343.212.72.43 1.102.568.305.112.613.173.908.142a1.34 1.34 0 00.535-.178c.103-.061.202-.14.298-.237.064-.065.127-.137.186-.22a3.133 3.133 0 00.445-.868 3.08 3.08 0 00.056-1.71A3.063 3.063 0 0016.293 4.8z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

@@ -1,16 +0,0 @@
{
"images" : [
{
"filename" : "custom.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
@@ -1 +0,0 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Custom</title><path d="M4 8h4.34a3 3 0 016.66 0H20a1 1 0 110 2h-5a3 3 0 01-6.66 0H4a1 1 0 110-2zm0 8h9.34a3 3 0 016.66 0H20a1 1 0 110 2h-5a3 3 0 01-6.66 0H4a1 1 0 110-2z"></path></svg>

Before

Width:  |  Height:  |  Size: 349 B

@@ -1,16 +0,0 @@
{
"images" : [
{
"filename" : "deepseek.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
@@ -1 +0,0 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>DeepSeek</title><path d="M23.748 4.482c-.254-.124-.364.113-.512.234-.051.039-.094.09-.137.136-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.156-.708-.311-.955-.65-.172-.241-.219-.51-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.093.172.187.129.323-.082.28-.18.552-.266.833-.055.179-.137.217-.329.14a5.526 5.526 0 01-1.736-1.18c-.857-.828-1.631-1.742-2.597-2.458a11.365 11.365 0 00-.689-.471c-.985-.957.13-1.743.388-1.836.27-.098.093-.432-.779-.428-.872.004-1.67.295-2.687.684a3.055 3.055 0 01-.465.137 9.597 9.597 0 00-2.883-.102c-1.885.21-3.39 1.102-4.497 2.623C.082 8.606-.231 10.684.152 12.85c.403 2.284 1.569 4.175 3.36 5.653 1.858 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.133-.284 4.994-1.86.47.234.962.327 1.78.397.63.059 1.236-.03 1.705-.128.735-.156.684-.837.419-.961-2.155-1.004-1.682-.595-2.113-.926 1.096-1.296 2.746-2.642 3.392-7.003.05-.347.007-.565 0-.845-.004-.17.035-.237.23-.256a4.173 4.173 0 001.545-.475c1.396-.763 1.96-2.015 2.093-3.517.02-.23-.004-.467-.247-.588zM11.581 18c-2.089-1.642-3.102-2.183-3.52-2.16-.392.024-.321.471-.235.763.09.288.207.486.371.739.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.167-1.361-.802-2.5-1.86-3.301-3.307-.774-1.393-1.224-2.887-1.298-4.482-.02-.386.093-.522.477-.592a4.696 4.696 0 011.529-.039c2.132.312 3.946 1.265 5.468 2.774.868.86 1.525 1.887 2.202 2.891.72 1.066 1.494 2.082 2.48 2.914.348.292.625.514.891.677-.802.09-2.14.11-3.054-.614zm1-6.44a.306.306 0 01.415-.287.302.302 0 01.2.288.306.306 0 01-.31.307.303.303 0 01-.304-.308zm3.11 1.596c-.2.081-.399.151-.59.16a1.245 1.245 0 01-.798-.254c-.274-.23-.47-.358-.552-.758a1.73 1.73 0 01.016-.588c.07-.327-.008-.537-.239-.727-.187-.156-.426-.199-.688-.199a.559.559 0 01-.254-.078c-.11-.054-.2-.19-.114-.358.028-.054.16-.186.192-.21.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.391.451.462.576.685.914.176.265.336.537.445.848.067.195-.019.354-.25.452z"></path></svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

@@ -1,16 +0,0 @@
{
"images" : [
{
"filename" : "gemini.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
@@ -1 +0,0 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Gemini</title><path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z"></path></svg>

Before

Width:  |  Height:  |  Size: 651 B

-16
View File
@@ -1,16 +0,0 @@
{
"images" : [
{
"filename" : "groq.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
-1
View File
@@ -1 +0,0 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Groq</title><path d="M12.036 2c-3.853-.035-7 3-7.036 6.781-.035 3.782 3.055 6.872 6.908 6.907h2.42v-2.566h-2.292c-2.407.028-4.38-1.866-4.408-4.23-.029-2.362 1.901-4.298 4.308-4.326h.1c2.407 0 4.358 1.915 4.365 4.278v6.305c0 2.342-1.944 4.25-4.323 4.279a4.375 4.375 0 01-3.033-1.252l-1.851 1.818A7 7 0 0012.029 22h.092c3.803-.056 6.858-3.083 6.879-6.816v-6.5C18.907 4.963 15.817 2 12.036 2z"></path></svg>

Before

Width:  |  Height:  |  Size: 568 B

-15
View File
@@ -1,15 +0,0 @@
{
"images" : [
{
"filename" : "mimo.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

@@ -1,16 +0,0 @@
{
"images" : [
{
"filename" : "minimax.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
@@ -1 +0,0 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Minimax</title><path d="M16.278 2c1.156 0 2.093.927 2.093 2.07v12.501a.74.74 0 00.744.709.74.74 0 00.743-.709V9.099a2.06 2.06 0 012.071-2.049A2.06 2.06 0 0124 9.1v6.561a.649.649 0 01-.652.645.649.649 0 01-.653-.645V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v7.472a2.037 2.037 0 01-2.048 2.026 2.037 2.037 0 01-2.048-2.026v-12.5a.785.785 0 00-.788-.753.785.785 0 00-.789.752l-.001 15.904A2.037 2.037 0 0113.441 22a2.037 2.037 0 01-2.048-2.026V18.04c0-.356.292-.645.652-.645.36 0 .652.289.652.645v1.934c0 .263.142.506.372.638.23.131.514.131.744 0a.734.734 0 00.372-.638V4.07c0-1.143.937-2.07 2.093-2.07zm-5.674 0c1.156 0 2.093.927 2.093 2.07v11.523a.648.648 0 01-.652.645.648.648 0 01-.652-.645V4.07a.785.785 0 00-.789-.78.785.785 0 00-.789.78v14.013a2.06 2.06 0 01-2.07 2.048 2.06 2.06 0 01-2.071-2.048V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v3.8a2.06 2.06 0 01-2.071 2.049A2.06 2.06 0 010 12.9v-1.378c0-.357.292-.646.652-.646.36 0 .653.29.653.646V12.9c0 .418.343.757.766.757s.766-.339.766-.757V9.099a2.06 2.06 0 012.07-2.048 2.06 2.06 0 012.071 2.048v8.984c0 .419.343.758.767.758.423 0 .766-.339.766-.758V4.07c0-1.143.937-2.07 2.093-2.07z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

@@ -1,16 +0,0 @@
{
"images" : [
{
"filename" : "mistral.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
@@ -1 +0,0 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Mistral</title><path clip-rule="evenodd" d="M3.428 3.4h3.429v3.428h3.429v3.429h-.002 3.431V6.828h3.427V3.4h3.43v13.714H24v3.429H13.714v-3.428h-3.428v-3.429h-3.43v3.428h3.43v3.429H0v-3.429h3.428V3.4zm10.286 13.715h3.428v-3.429h-3.427v3.429z"></path></svg>

Before

Width:  |  Height:  |  Size: 418 B

@@ -1,16 +0,0 @@
{
"images" : [
{
"filename" : "moonshot.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
@@ -1 +0,0 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>MoonshotAI</title><path d="M1.052 16.916l9.539 2.552a21.007 21.007 0 00.06 2.033l5.956 1.593a11.997 11.997 0 01-5.586.865l-.18-.016-.044-.004-.084-.009-.094-.01a11.605 11.605 0 01-.157-.02l-.107-.014-.11-.016a11.962 11.962 0 01-.32-.051l-.042-.008-.075-.013-.107-.02-.07-.015-.093-.019-.075-.016-.095-.02-.097-.023-.094-.022-.068-.017-.088-.022-.09-.024-.095-.025-.082-.023-.109-.03-.062-.02-.084-.025-.093-.028-.105-.034-.058-.019-.08-.026-.09-.031-.066-.024a6.293 6.293 0 01-.044-.015l-.068-.025-.101-.037-.057-.022-.08-.03-.087-.035-.088-.035-.079-.032-.095-.04-.063-.028-.063-.027a5.655 5.655 0 01-.041-.018l-.066-.03-.103-.047-.052-.024-.096-.046-.062-.03-.084-.04-.086-.044-.093-.047-.052-.027-.103-.055-.057-.03-.058-.032a6.49 6.49 0 01-.046-.026l-.094-.053-.06-.034-.051-.03-.072-.041-.082-.05-.093-.056-.052-.032-.084-.053-.061-.039-.079-.05-.07-.047-.053-.035a7.785 7.785 0 01-.054-.036l-.044-.03-.044-.03a6.066 6.066 0 01-.04-.028l-.057-.04-.076-.054-.069-.05-.074-.054-.056-.042-.076-.057-.076-.059-.086-.067-.045-.035-.064-.052-.074-.06-.089-.073-.046-.039-.046-.039a7.516 7.516 0 01-.043-.037l-.045-.04-.061-.053-.07-.062-.068-.06-.062-.058-.067-.062-.053-.05-.088-.084a13.28 13.28 0 01-.099-.097l-.029-.028-.041-.042-.069-.07-.05-.051-.05-.053a6.457 6.457 0 01-.168-.179l-.08-.088-.062-.07-.071-.08-.042-.049-.053-.062-.058-.068-.046-.056a7.175 7.175 0 01-.027-.033l-.045-.055-.066-.082-.041-.052-.05-.064-.02-.025a11.99 11.99 0 01-1.44-2.402zm-1.02-5.794l11.353 3.037a20.468 20.468 0 00-.469 2.011l10.817 2.894a12.076 12.076 0 01-1.845 2.005L.657 15.923l-.016-.046-.035-.104a11.965 11.965 0 01-.05-.153l-.007-.023a11.896 11.896 0 01-.207-.741l-.03-.126-.018-.08-.021-.097-.018-.081-.018-.09-.017-.084-.018-.094c-.026-.141-.05-.283-.071-.426l-.017-.118-.011-.083-.013-.102a12.01 12.01 0 01-.019-.161l-.005-.047a12.12 12.12 0 01-.034-2.145zm1.593-5.15l11.948 3.196c-.368.605-.705 1.231-1.01 1.875l11.295 3.022c-.142.82-.368 1.612-.668 2.365l-11.55-3.09L.124 10.26l.015-.1.008-.049.01-.067.015-.087.018-.098c.026-.148.056-.295.088-.442l.028-.124.02-.085.024-.097c.022-.09.045-.18.07-.268l.028-.102.023-.083.03-.1.025-.082.03-.096.026-.082.031-.095a11.896 11.896 0 011.01-2.232zm4.442-4.4L17.352 4.59a20.77 20.77 0 00-1.688 1.721l7.823 2.093c.267.852.442 1.744.513 2.665L2.106 5.213l.045-.065.027-.04.04-.055.046-.065.055-.076.054-.072.064-.086.05-.065.057-.073.055-.07.06-.074.055-.069.065-.077.054-.066.066-.077.053-.06.072-.082.053-.06.067-.074.054-.058.073-.078.058-.06.063-.067.168-.17.1-.098.059-.056.076-.071a12.084 12.084 0 012.272-1.677zM12.017 0h.097l.082.001.069.001.054.002.068.002.046.001.076.003.047.002.06.003.054.002.087.005.105.007.144.011.088.007.044.004.077.008.082.008.047.005.102.012.05.006.108.014.081.01.042.006.065.01.207.032.07.012.065.011.14.026.092.018.11.022.046.01.075.016.041.01L14.7.3l.042.01.065.015.049.012.071.017.096.024.112.03.113.03.113.032.05.015.07.02.078.024.073.023.05.016.05.016.076.025.099.033.102.036.048.017.064.023.093.034.11.041.116.045.1.04.047.02.06.024.041.018.063.026.04.018.057.025.11.048.1.046.074.035.075.036.06.028.092.046.091.045.102.052.053.028.049.026.046.024.06.033.041.022.052.029.088.05.106.06.087.051.057.034.053.032.096.059.088.055.098.062.036.024.064.041.084.056.04.027.062.042.062.043.023.017c.054.037.108.075.161.114l.083.06.065.048.056.043.086.065.082.064.04.03.05.041.086.069.079.065.085.071c.712.6 1.353 1.283 1.909 2.031L7.222.994l.062-.027.065-.028.081-.034.086-.035c.113-.045.227-.09.341-.131l.096-.035.093-.033.084-.03.096-.031c.087-.03.176-.058.264-.085l.091-.027.086-.025.102-.03.085-.023.1-.026L9.04.37l.09-.023.091-.022.095-.022.09-.02.098-.021.091-.02.095-.018.092-.018.1-.018.091-.016.098-.017.092-.014.097-.015.092-.013.102-.013.091-.012.105-.012.09-.01.105-.01c.093-.01.186-.018.28-.024l.106-.008.09-.005.11-.006.093-.004.1-.004.097-.002.099-.002.197-.002z"></path></svg>

Before

Width:  |  Height:  |  Size: 3.9 KiB

@@ -1,16 +0,0 @@
{
"images" : [
{
"filename" : "openai.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
@@ -1 +0,0 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>OpenAI</title><path d="M9.205 8.658v-2.26c0-.19.072-.333.238-.428l4.543-2.616c.619-.357 1.356-.523 2.117-.523 2.854 0 4.662 2.212 4.662 4.566 0 .167 0 .357-.024.547l-4.71-2.759a.797.797 0 00-.856 0l-5.97 3.473zm10.609 8.8V12.06c0-.333-.143-.57-.429-.737l-5.97-3.473 1.95-1.118a.433.433 0 01.476 0l4.543 2.617c1.309.76 2.189 2.378 2.189 3.948 0 1.808-1.07 3.473-2.76 4.163zM7.802 12.703l-1.95-1.142c-.167-.095-.239-.238-.239-.428V5.899c0-2.545 1.95-4.472 4.591-4.472 1 0 1.927.333 2.712.928L8.23 5.067c-.285.166-.428.404-.428.737v6.898zM12 15.128l-2.795-1.57v-3.33L12 8.658l2.795 1.57v3.33L12 15.128zm1.796 7.23c-1 0-1.927-.332-2.712-.927l4.686-2.712c.285-.166.428-.404.428-.737v-6.898l1.974 1.142c.167.095.238.238.238.428v5.233c0 2.545-1.974 4.472-4.614 4.472zm-5.637-5.303l-4.544-2.617c-1.308-.761-2.188-2.378-2.188-3.948A4.482 4.482 0 014.21 6.327v5.423c0 .333.143.571.428.738l5.947 3.449-1.95 1.118a.432.432 0 01-.476 0zm-.262 3.9c-2.688 0-4.662-2.021-4.662-4.519 0-.19.024-.38.047-.57l4.686 2.71c.286.167.571.167.856 0l5.97-3.448v2.26c0 .19-.07.333-.237.428l-4.543 2.616c-.619.357-1.356.523-2.117.523zm5.899 2.83a5.947 5.947 0 005.827-4.756C22.287 18.339 24 15.84 24 13.296c0-1.665-.713-3.282-1.998-4.448.119-.5.19-.999.19-1.498 0-3.401-2.759-5.947-5.946-5.947-.642 0-1.26.095-1.88.31A5.962 5.962 0 0010.205 0a5.947 5.947 0 00-5.827 4.757C1.713 5.447 0 7.945 0 10.49c0 1.666.713 3.283 1.998 4.448-.119.5-.19 1-.19 1.499 0 3.401 2.759 5.946 5.946 5.946.642 0 1.26-.095 1.88-.309a5.96 5.96 0 004.162 1.713z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

@@ -1,16 +0,0 @@
{
"images" : [
{
"filename" : "openrouter.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
@@ -1 +0,0 @@
<svg fill="currentColor" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>OpenRouter</title><path d="M16.778 1.844v1.919q-.569-.026-1.138-.032-.708-.008-1.415.037c-1.93.126-4.023.728-6.149 2.237-2.911 2.066-2.731 1.95-4.14 2.75-.396.223-1.342.574-2.185.798-.841.225-1.753.333-1.751.333v4.229s.768.108 1.61.333c.842.224 1.789.575 2.185.799 1.41.798 1.228.683 4.14 2.75 2.126 1.509 4.22 2.11 6.148 2.236.88.058 1.716.041 2.555.005v1.918l7.222-4.168-7.222-4.17v2.176c-.86.038-1.611.065-2.278.021-1.364-.09-2.417-.357-3.979-1.465-2.244-1.593-2.866-2.027-3.68-2.508.889-.518 1.449-.906 3.822-2.59 1.56-1.109 2.614-1.377 3.978-1.466.667-.044 1.418-.017 2.278.02v2.176L24 6.014Z"/></svg>

Before

Width:  |  Height:  |  Size: 704 B

-16
View File
@@ -1,16 +0,0 @@
{
"images" : [
{
"filename" : "qwen.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
-1
View File
@@ -1 +0,0 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Qwen</title><path d="M12.604 1.34c.393.69.784 1.382 1.174 2.075a.18.18 0 00.157.091h5.552c.174 0 .322.11.446.327l1.454 2.57c.19.337.24.478.024.837-.26.43-.513.864-.76 1.3l-.367.658c-.106.196-.223.28-.04.512l2.652 4.637c.172.301.111.494-.043.77-.437.785-.882 1.564-1.335 2.34-.159.272-.352.375-.68.37-.777-.016-1.552-.01-2.327.016a.099.099 0 00-.081.05 575.097 575.097 0 01-2.705 4.74c-.169.293-.38.363-.725.364-.997.003-2.002.004-3.017.002a.537.537 0 01-.465-.271l-1.335-2.323a.09.09 0 00-.083-.049H4.982c-.285.03-.553-.001-.805-.092l-1.603-2.77a.543.543 0 01-.002-.54l1.207-2.12a.198.198 0 000-.197 550.951 550.951 0 01-1.875-3.272l-.79-1.395c-.16-.31-.173-.496.095-.965.465-.813.927-1.625 1.387-2.436.132-.234.304-.334.584-.335a338.3 338.3 0 012.589-.001.124.124 0 00.107-.063l2.806-4.895a.488.488 0 01.422-.246c.524-.001 1.053 0 1.583-.006L11.704 1c.341-.003.724.032.9.34zm-3.432.403a.06.06 0 00-.052.03L6.254 6.788a.157.157 0 01-.135.078H3.253c-.056 0-.07.025-.041.074l5.81 10.156c.025.042.013.062-.034.063l-2.795.015a.218.218 0 00-.2.116l-1.32 2.31c-.044.078-.021.118.068.118l5.716.008c.046 0 .08.02.104.061l1.403 2.454c.046.081.092.082.139 0l5.006-8.76.783-1.382a.055.055 0 01.096 0l1.424 2.53a.122.122 0 00.107.062l2.763-.02a.04.04 0 00.035-.02.041.041 0 000-.04l-2.9-5.086a.108.108 0 010-.113l.293-.507 1.12-1.977c.024-.041.012-.062-.035-.062H9.2c-.059 0-.073-.026-.043-.077l1.434-2.505a.107.107 0 000-.114L9.225 1.774a.06.06 0 00-.053-.031zm6.29 8.02c.046 0 .058.02.034.06l-.832 1.465-2.613 4.585a.056.056 0 01-.05.029.058.058 0 01-.05-.029L8.498 9.841c-.02-.034-.01-.052.028-.054l.216-.012 6.722-.012z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

@@ -1,16 +0,0 @@
{
"images" : [
{
"filename" : "siliconflow.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
@@ -1 +0,0 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>SiliconCloud</title><path clip-rule="evenodd" d="M22.956 6.521H12.522c-.577 0-1.044.468-1.044 1.044v3.13c0 .577-.466 1.044-1.043 1.044H1.044c-.577 0-1.044.467-1.044 1.044v4.174C0 17.533.467 18 1.044 18h10.434c.577 0 1.044-.467 1.044-1.043v-3.13c0-.578.466-1.044 1.043-1.044h9.391c.577 0 1.044-.467 1.044-1.044V7.565c0-.576-.467-1.044-1.044-1.044z"></path></svg>

Before

Width:  |  Height:  |  Size: 525 B

-16
View File
@@ -1,16 +0,0 @@
{
"images" : [
{
"filename" : "xai.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
-1
View File
@@ -1 +0,0 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Grok</title><path d="M6.469 8.776L16.512 23h-4.464L2.005 8.776H6.47zm-.004 7.9l2.233 3.164L6.467 23H2l4.465-6.324zM22 2.582V23h-3.659V7.764L22 2.582zM22 1l-9.952 14.095-2.233-3.163L17.533 1H22z"></path></svg>

Before

Width:  |  Height:  |  Size: 372 B

@@ -1,16 +0,0 @@
{
"images" : [
{
"filename" : "zhipu.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
-1
View File
@@ -1 +0,0 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Zhipu</title><path d="M11.991 23.503a.24.24 0 00-.244.248.24.24 0 00.244.249.24.24 0 00.245-.249.24.24 0 00-.22-.247l-.025-.001zM9.671 5.365a1.697 1.697 0 011.099 2.132l-.071.172-.016.04-.018.054c-.07.16-.104.32-.104.498-.035.71.47 1.279 1.186 1.314h.366c1.309.053 2.338 1.173 2.286 2.523-.052 1.332-1.152 2.38-2.478 2.327h-.174c-.715.018-1.274.64-1.239 1.368 0 .124.018.23.053.337.209.373.54.658.96.8.75.23 1.517-.125 1.9-.782l.018-.035c.402-.64 1.17-.96 1.92-.711.854.284 1.378 1.226 1.099 2.167a1.661 1.661 0 01-2.077 1.102 1.711 1.711 0 01-.907-.711l-.017-.035c-.2-.323-.463-.58-.851-.711l-.056-.018a1.646 1.646 0 00-1.954.746 1.66 1.66 0 01-1.065.764 1.677 1.677 0 01-1.989-1.279c-.209-.906.332-1.83 1.257-2.043a1.51 1.51 0 01.296-.035h.018c.68-.071 1.151-.622 1.116-1.333a1.307 1.307 0 00-.227-.693 2.515 2.515 0 01-.366-1.403 2.39 2.39 0 01.366-1.208c.14-.195.21-.444.227-.693.018-.71-.506-1.261-1.186-1.332l-.07-.018a1.43 1.43 0 01-.299-.07l-.05-.019a1.7 1.7 0 01-1.047-2.114 1.68 1.68 0 012.094-1.101zm-5.575 10.11c.26-.264.639-.367.994-.27.355.096.633.379.728.74.095.362-.007.748-.267 1.013-.402.41-1.053.41-1.455 0a1.062 1.062 0 010-1.482zm14.845-.294c.359-.09.738.024.992.297.254.274.344.665.237 1.025-.107.36-.396.634-.756.718-.551.128-1.1-.22-1.23-.781a1.05 1.05 0 01.757-1.26zm-.064-4.39c.314.32.49.753.49 1.206 0 .452-.176.886-.49 1.206-.315.32-.74.5-1.185.5-.444 0-.87-.18-1.184-.5a1.727 1.727 0 010-2.412 1.654 1.654 0 012.369 0zm-11.243.163c.364.484.447 1.128.218 1.691a1.665 1.665 0 01-2.188.923c-.855-.36-1.26-1.358-.907-2.228a1.68 1.68 0 011.33-1.038c.593-.08 1.183.169 1.547.652zm11.545-4.221c.368 0 .708.2.892.524.184.324.184.724 0 1.048a1.026 1.026 0 01-.892.524c-.568 0-1.03-.47-1.03-1.048 0-.579.462-1.048 1.03-1.048zm-14.358 0c.368 0 .707.2.891.524.184.324.184.724 0 1.048a1.026 1.026 0 01-.891.524c-.569 0-1.03-.47-1.03-1.048 0-.579.461-1.048 1.03-1.048zm10.031-1.475c.925 0 1.675.764 1.675 1.706s-.75 1.705-1.675 1.705-1.674-.763-1.674-1.705c0-.942.75-1.706 1.674-1.706zm-2.626-.684c.362-.082.653-.356.761-.718a1.062 1.062 0 00-.238-1.028 1.017 1.017 0 00-.996-.294c-.547.14-.881.7-.752 1.257.13.558.675.907 1.225.783zm0 16.876c.359-.087.644-.36.75-.72a1.062 1.062 0 00-.237-1.019 1.018 1.018 0 00-.985-.301 1.037 1.037 0 00-.762.717c-.108.361-.017.754.239 1.028.245.263.606.377.953.305l.043-.01zM17.19 3.5a.631.631 0 00.628-.64c0-.355-.279-.64-.628-.64a.631.631 0 00-.628.64c0 .355.28.64.628.64zm-10.38 0a.631.631 0 00.628-.64c0-.355-.28-.64-.628-.64a.631.631 0 00-.628.64c0 .355.279.64.628.64zm-5.182 7.852a.631.631 0 00-.628.64c0 .354.28.639.628.639a.63.63 0 00.627-.606l.001-.034a.62.62 0 00-.628-.64zm5.182 9.13a.631.631 0 00-.628.64c0 .355.279.64.628.64a.631.631 0 00.628-.64c0-.355-.28-.64-.628-.64zm10.38.018a.631.631 0 00-.628.64c0 .355.28.64.628.64a.631.631 0 00.628-.64c0-.355-.279-.64-.628-.64zm5.182-9.148a.631.631 0 00-.628.64c0 .354.279.639.628.639a.631.631 0 00.628-.64c0-.355-.28-.64-.628-.64zm-.384-4.992a.24.24 0 00.244-.249.24.24 0 00-.244-.249.24.24 0 00-.244.249c0 .142.122.249.244.249zM11.991.497a.24.24 0 00.245-.248A.24.24 0 0011.99 0a.24.24 0 00-.244.249c0 .133.108.236.223.247l.021.001zM2.011 6.36a.24.24 0 00.245-.249.24.24 0 00-.244-.249.24.24 0 00-.244.249.24.24 0 00.244.249zm0 11.263a.24.24 0 00-.243.248.24.24 0 00.244.249.24.24 0 00.244-.249.252.252 0 00-.244-.248zm19.995-.018a.24.24 0 00-.245.248.24.24 0 00.245.25.24.24 0 00.244-.25.252.252 0 00-.244-.248z"></path></svg>

Before

Width:  |  Height:  |  Size: 3.5 KiB

@@ -15,6 +15,10 @@ struct PolishStyleCorpusExport: Codable, Equatable {
let appBuild: String let appBuild: String
let effectiveCharacterCount: Int let effectiveCharacterCount: Int
let requiredEffectiveCharacterCount: Int let requiredEffectiveCharacterCount: Int
/// Maximum effective characters included in this export. Caps the
/// training-corpus window even when the user has accumulated far
/// more history than the production 2,500-character unlock gate.
let trainingExtractionMaximumCharacterCount: Int
let examples: [Example] let examples: [Example]
struct Example: Codable, Equatable { struct Example: Codable, Equatable {
@@ -55,7 +59,10 @@ final class PolishStyleCorpusExportStore {
let eligibleCorpus = PolishStyleLearningCorpusBuilder.build(from: history) let eligibleCorpus = PolishStyleLearningCorpusBuilder.build(from: history)
guard !eligibleCorpus.examples.isEmpty else { return nil } guard !eligibleCorpus.examples.isEmpty else { return nil }
let corpus = PolishStyleLearningCorpusBuilder.trainingWindow( let corpus = PolishStyleLearningCorpusBuilder.trainingWindow(
from: eligibleCorpus.examples from: eligibleCorpus.examples,
maximumCharacterCount:
PolishStyleLearningCorpusBuilder
.trainingExtractionMaximumCharacterCount
) )
let examples = corpus.examples let examples = corpus.examples
@@ -77,6 +84,9 @@ final class PolishStyleCorpusExportStore {
effectiveCharacterCount: corpus.effectiveCharacterCount, effectiveCharacterCount: corpus.effectiveCharacterCount,
requiredEffectiveCharacterCount: requiredEffectiveCharacterCount:
PolishStyleLearningCorpusBuilder.requiredEffectiveCharacterCount, PolishStyleLearningCorpusBuilder.requiredEffectiveCharacterCount,
trainingExtractionMaximumCharacterCount:
PolishStyleLearningCorpusBuilder
.trainingExtractionMaximumCharacterCount,
examples: examples examples: examples
) )
} }
@@ -135,7 +135,10 @@ struct FlowDiagnosticsSettingsView: View {
from: historyStore.snapshot() from: historyStore.snapshot()
) )
return PolishStyleLearningCorpusBuilder.trainingWindow( return PolishStyleLearningCorpusBuilder.trainingWindow(
from: eligibleCorpus.examples from: eligibleCorpus.examples,
maximumCharacterCount:
PolishStyleLearningCorpusBuilder
.trainingExtractionMaximumCharacterCount
) )
} }
+13 -1
View File
@@ -8,7 +8,19 @@ struct MainTabView: View {
@Environment(\.themePalette) private var palette: ThemePalette @Environment(\.themePalette) private var palette: ThemePalette
@Environment(\.horizontalSizeClass) private var horizontalSizeClass @Environment(\.horizontalSizeClass) private var horizontalSizeClass
@State private var tab: AppTab = .keyboard @State private var tab: AppTab = {
#if DEBUG
if let raw = ProcessInfo.processInfo.arguments
.first(where: { $0.hasPrefix("-osgStartTab=") })?
.split(separator: "=").last,
let value = Int(raw),
let initial = AppTab(rawValue: value)
{
return initial
}
#endif
return .keyboard
}()
private var usesSplitLayout: Bool { private var usesSplitLayout: Bool {
horizontalSizeClass == .regular horizontalSizeClass == .regular
+19 -11
View File
@@ -55,7 +55,8 @@ struct PolishStylesView: View {
outputLanguage: language, outputLanguage: language,
minimumEffectiveCharacterCount: minimumEffectiveCharacterCount:
AppDistributionChannel.allowsInternalTools AppDistributionChannel.allowsInternalTools
? 0 ? PolishStyleLearningCorpusBuilder
.testBuildEffectiveCharacterCount
: PolishStyleLearningCorpusBuilder : PolishStyleLearningCorpusBuilder
.requiredEffectiveCharacterCount .requiredEffectiveCharacterCount
) )
@@ -158,17 +159,20 @@ struct PolishStylesView: View {
PolishStyleLearningCorpusBuilder.build(from: history.snapshot()) PolishStyleLearningCorpusBuilder.build(from: history.snapshot())
} }
/// Debug and TestFlight builds may exercise the complete generation /// Minimum effective character count required to unlock personal style
/// pipeline before enough personal corpus exists. App Store builds keep /// generation. Debug + TestFlight builds use a lower 1,250-character
/// the production 2,500-character gate. /// gate so internal testers can still exercise the pipeline; the
private var bypassesStyleLearningCharacterGate: Bool { /// previous 0 / "unlimited" bypass has been removed.
private var styleLearningMinimumCharacterCount: Int {
AppDistributionChannel.allowsInternalTools AppDistributionChannel.allowsInternalTools
? PolishStyleLearningCorpusBuilder.testBuildEffectiveCharacterCount
: PolishStyleLearningCorpusBuilder.requiredEffectiveCharacterCount
} }
private func isEligibleForStyleGeneration( private func isEligibleForStyleGeneration(
_ corpus: PolishStyleLearningCorpus _ corpus: PolishStyleLearningCorpus
) -> Bool { ) -> Bool {
bypassesStyleLearningCharacterGate || corpus.isReady corpus.effectiveCharacterCount >= styleLearningMinimumCharacterCount
} }
private var learnedStylePack: PolishStylePack? { private var learnedStylePack: PolishStylePack? {
@@ -185,7 +189,7 @@ struct PolishStylesView: View {
private var styleLearningCard: some View { private var styleLearningCard: some View {
let corpus = styleLearningCorpus let corpus = styleLearningCorpus
let required = PolishStyleLearningCorpusBuilder.requiredEffectiveCharacterCount let required = styleLearningMinimumCharacterCount
let reachedLimit = catalog.entries.count >= PolishStyleLimits.maximumUserPacks let reachedLimit = catalog.entries.count >= PolishStyleLimits.maximumUserPacks
let isActionAvailable = isEligibleForStyleGeneration(corpus) && !reachedLimit let isActionAvailable = isEligibleForStyleGeneration(corpus) && !reachedLimit
let canGenerate = isActionAvailable && !isGeneratingLearnedStyle let canGenerate = isActionAvailable && !isGeneratingLearnedStyle
@@ -240,13 +244,17 @@ struct PolishStylesView: View {
Spacer() Spacer()
Text( Text(
bypassesStyleLearningCharacterGate && !corpus.isReady isEligibleForStyleGeneration(corpus)
? AppL10n.string("polishStyles.learn.testBuildReady")
: corpus.isReady
? AppL10n.string("polishStyles.learn.ready") ? AppL10n.string("polishStyles.learn.ready")
: AppL10n.format( : AppL10n.format(
"polishStyles.learn.remaining", "polishStyles.learn.remaining",
Int64(corpus.remainingCharacterCount) Int64(
max(
0,
styleLearningMinimumCharacterCount
- corpus.effectiveCharacterCount
)
)
) )
) )
.font(TypeStyle.caption2) .font(TypeStyle.caption2)
@@ -134,7 +134,8 @@ private struct PolishStylesServiceUITestHarness: View {
outputLanguage: language, outputLanguage: language,
minimumEffectiveCharacterCount: minimumEffectiveCharacterCount:
AppDistributionChannel.allowsInternalTools AppDistributionChannel.allowsInternalTools
? 0 ? PolishStyleLearningCorpusBuilder
.testBuildEffectiveCharacterCount
: PolishStyleLearningCorpusBuilder : PolishStyleLearningCorpusBuilder
.requiredEffectiveCharacterCount .requiredEffectiveCharacterCount
) )
+4 -2
View File
@@ -128,6 +128,7 @@
"common.continue" = "Continue"; "common.continue" = "Continue";
"common.reset" = "Reset"; "common.reset" = "Reset";
"common.cancel" = "Cancel"; "common.cancel" = "Cancel";
"common.dismiss" = "Dismiss";
"common.save" = "Save"; "common.save" = "Save";
"common.clear" = "Clear"; "common.clear" = "Clear";
"common.delete" = "Delete"; "common.delete" = "Delete";
@@ -294,7 +295,7 @@
"settings.diagnostics.corpus.section" = "Personal Style Training Corpus"; "settings.diagnostics.corpus.section" = "Personal Style Training Corpus";
"settings.diagnostics.corpus.count" = "%d eligible samples · %d effective characters"; "settings.diagnostics.corpus.count" = "%d eligible samples · %d effective characters";
"settings.diagnostics.corpus.empty" = "No eligible paired dictations are available."; "settings.diagnostics.corpus.empty" = "No eligible paired dictations are available.";
"settings.diagnostics.corpus.ready" = "Newest complete samples through the 2,500-character target."; "settings.diagnostics.corpus.ready" = "Newest complete samples through the 5,000-character export cap.";
"settings.diagnostics.corpus.export" = "Export Training Corpus"; "settings.diagnostics.corpus.export" = "Export Training Corpus";
"settings.diagnostics.corpus.privacy" = "Contains dictated text and prior polish prompts. Nothing is uploaded automatically. Share only with trusted devices or services."; "settings.diagnostics.corpus.privacy" = "Contains dictated text and prior polish prompts. Nothing is uploaded automatically. Share only with trusted devices or services.";
"settings.diagnostics.privacy.section" = "Privacy"; "settings.diagnostics.privacy.section" = "Privacy";
@@ -683,7 +684,6 @@
"polishStyles.learn.progress" = "%lld / %lld characters"; "polishStyles.learn.progress" = "%lld / %lld characters";
"polishStyles.learn.remaining" = "%lld to go"; "polishStyles.learn.remaining" = "%lld to go";
"polishStyles.learn.ready" = "Ready"; "polishStyles.learn.ready" = "Ready";
"polishStyles.learn.testBuildReady" = "Test build: 2,500-character limit disabled";
"polishStyles.learn.action" = "Generate Style"; "polishStyles.learn.action" = "Generate Style";
"polishStyles.learn.generating" = "Generating…"; "polishStyles.learn.generating" = "Generating…";
"polishStyles.learn.generated.description" = "Your personal style learned from reviewed evidence."; "polishStyles.learn.generated.description" = "Your personal style learned from reviewed evidence.";
@@ -959,6 +959,8 @@
"account.referral.status.rewarded" = "Rewarded"; "account.referral.status.rewarded" = "Rewarded";
"account.referral.status.ineligible" = "Ineligible"; "account.referral.status.ineligible" = "Ineligible";
"account.referral.pendingAfterSignIn" = "Your invitation is saved and will be redeemed after sign-in."; "account.referral.pendingAfterSignIn" = "Your invitation is saved and will be redeemed after sign-in.";
"account.referral.rewardGranted" = "You received %@ credits from the invitation.";
"account.referral.rewardGrantedGeneric" = "Your invitation reward has been added to your balance.";
"account.security.section" = "Account controls"; "account.security.section" = "Account controls";
"account.signOut.action" = "Sign Out"; "account.signOut.action" = "Sign Out";
"account.signOut.confirmTitle" = "Sign out?"; "account.signOut.confirmTitle" = "Sign out?";
@@ -128,6 +128,7 @@
"common.continue" = "继续"; "common.continue" = "继续";
"common.reset" = "重置"; "common.reset" = "重置";
"common.cancel" = "取消"; "common.cancel" = "取消";
"common.dismiss" = "关闭";
"common.save" = "保存"; "common.save" = "保存";
"common.clear" = "清空"; "common.clear" = "清空";
"common.delete" = "删除"; "common.delete" = "删除";
@@ -294,7 +295,7 @@
"settings.diagnostics.corpus.section" = "个人风格训练语料"; "settings.diagnostics.corpus.section" = "个人风格训练语料";
"settings.diagnostics.corpus.count" = "%d 条可用样本 · %d 个有效字符"; "settings.diagnostics.corpus.count" = "%d 条可用样本 · %d 个有效字符";
"settings.diagnostics.corpus.empty" = "暂无符合条件的成对听写语料。"; "settings.diagnostics.corpus.empty" = "暂无符合条件的成对听写语料。";
"settings.diagnostics.corpus.ready" = "选取最新完整样本,累积至至少 2,500 个有效字符。"; "settings.diagnostics.corpus.ready" = "选取最新完整样本,导出时最多累积 5,000 个有效字符。";
"settings.diagnostics.corpus.export" = "导出训练语料"; "settings.diagnostics.corpus.export" = "导出训练语料";
"settings.diagnostics.corpus.privacy" = "文件包含口述文本和历史润色提示词,不会自动上传。请仅分享给可信设备或服务。"; "settings.diagnostics.corpus.privacy" = "文件包含口述文本和历史润色提示词,不会自动上传。请仅分享给可信设备或服务。";
"settings.diagnostics.privacy.section" = "隐私"; "settings.diagnostics.privacy.section" = "隐私";
@@ -682,7 +683,6 @@
"polishStyles.learn.progress" = "%lld / %lld 字"; "polishStyles.learn.progress" = "%lld / %lld 字";
"polishStyles.learn.remaining" = "还差 %lld 字"; "polishStyles.learn.remaining" = "还差 %lld 字";
"polishStyles.learn.ready" = "可以生成"; "polishStyles.learn.ready" = "可以生成";
"polishStyles.learn.testBuildReady" = "测试版本:已暂时取消 2500 字限制";
"polishStyles.learn.action" = "生成风格"; "polishStyles.learn.action" = "生成风格";
"polishStyles.learn.generating" = "生成中…"; "polishStyles.learn.generating" = "生成中…";
"polishStyles.learn.generated.description" = "根据经过确认的证据学习得到的个人表达风格。"; "polishStyles.learn.generated.description" = "根据经过确认的证据学习得到的个人表达风格。";
@@ -958,6 +958,8 @@
"account.referral.status.rewarded" = "已奖励"; "account.referral.status.rewarded" = "已奖励";
"account.referral.status.ineligible" = "不符合条件"; "account.referral.status.ineligible" = "不符合条件";
"account.referral.pendingAfterSignIn" = "邀请已暂存,登录后会自动兑换。"; "account.referral.pendingAfterSignIn" = "邀请已暂存,登录后会自动兑换。";
"account.referral.rewardGranted" = "恭喜获得 %@ 积分";
"account.referral.rewardGrantedGeneric" = "邀请奖励已发放,积分已到账。";
"account.security.section" = "账号操作"; "account.security.section" = "账号操作";
"account.signOut.action" = "退出登录"; "account.signOut.action" = "退出登录";
"account.signOut.confirmTitle" = "退出登录?"; "account.signOut.confirmTitle" = "退出登录?";
@@ -1,36 +0,0 @@
// ProviderLogo.swift
// OSGKeyboard · Shared
//
// Maps a provider id to its asset-catalog logo name. Shared by the iOS
// app and the macOS menu-bar app so both show identical brand marks.
import Foundation
public enum ProviderLogo {
/// Asset name for the provider's logo, or `nil` when there is no bundled logo.
public static func assetName(for providerId: String) -> String? {
switch providerId {
case "openai", "whisper": return "openai"
case "deepseek": return "deepseek"
case "qwen", "bailian", "alibabaCoding": return "qwen"
case "moonshot": return "moonshot"
case "zhipu": return "zhipu"
case "mimo": return "mimo"
case "ark", "volcengine": return "ark"
case "siliconflow": return "siliconflow"
case "groq": return "groq"
case "minimax": return "minimax"
case "openrouter": return "openrouter"
case "gemini": return "gemini"
case "anthropic": return "anthropic"
case "xai": return "xai"
case "mistral": return "mistral"
case "cometapi": return "cometapi"
case "codingPlanX": return "codingplanx"
case "codex_oauth": return "openai"
case "apple": return "apple"
case "custom": return "custom"
default: return nil
}
}
}
@@ -150,8 +150,24 @@ public struct PolishStyleLearningCorpus: Equatable, Sendable {
} }
public enum PolishStyleLearningCorpusBuilder { public enum PolishStyleLearningCorpusBuilder {
/// Production minimum effective character count required to unlock
/// personal style generation. App Store builds enforce this gate.
public static let requiredEffectiveCharacterCount = 2_500 public static let requiredEffectiveCharacterCount = 2_500
/// Test builds (Debug + TestFlight) lower the unlock threshold so
/// internal testers can exercise the complete generation pipeline
/// without dictating a full production corpus. The threshold is
/// still a real gate; the previous "0 / unlimited" bypass is gone.
public static let testBuildEffectiveCharacterCount = 1_250
/// Upper bound on effective characters included in a single
/// training-corpus export. Even when the user has accumulated
/// significantly more history than the unlock threshold, the
/// exported training set never exceeds this cap so the user's
/// full private dictation history does not leave the device in
/// one shot.
public static let trainingExtractionMaximumCharacterCount = 5_000
public static func build( public static func build(
from entries: [SpeechHistoryEntry] from entries: [SpeechHistoryEntry]
) -> PolishStyleLearningCorpus { ) -> PolishStyleLearningCorpus {
@@ -167,12 +183,23 @@ public enum PolishStyleLearningCorpusBuilder {
) )
} }
/// Selects the newest complete examples until the learning threshold is /// Selects the newest complete examples until `maximumCharacterCount`
/// reached. If less history is available, every eligible example is kept. /// is reached. If less history is available, every eligible example
/// The returned order is chronological for export and model input. /// is kept. The returned order is chronological for export and
/// model input.
///
/// - Parameter maximumCharacterCount: Hard upper bound for the
/// selected window. Defaults to the production unlock threshold
/// (2,500) so live generation still fits the LLM request budget.
/// Callers that build a training-corpus export should pass
/// `trainingExtractionMaximumCharacterCount` (5,000) so the
/// export can carry up to the broader extraction cap when the
/// user has accumulated more history than the live gate.
public static func trainingWindow( public static func trainingWindow(
from examples: [PolishStyleLearningExample] from examples: [PolishStyleLearningExample],
maximumCharacterCount: Int = requiredEffectiveCharacterCount
) -> PolishStyleLearningCorpus { ) -> PolishStyleLearningCorpus {
let limit = max(0, maximumCharacterCount)
let newestFirst = examples.sorted { $0.createdAt > $1.createdAt } let newestFirst = examples.sorted { $0.createdAt > $1.createdAt }
var selected: [PolishStyleLearningExample] = [] var selected: [PolishStyleLearningExample] = []
var effectiveCharacterCount = 0 var effectiveCharacterCount = 0
@@ -182,7 +209,7 @@ public enum PolishStyleLearningCorpusBuilder {
effectiveCharacterCount += self.effectiveCharacterCount( effectiveCharacterCount += self.effectiveCharacterCount(
in: example.prePolishText in: example.prePolishText
) )
if effectiveCharacterCount >= requiredEffectiveCharacterCount { if effectiveCharacterCount >= limit {
break break
} }
} }
@@ -46,6 +46,7 @@ final class PolishStyleCorpusExportStoreTests: XCTestCase {
XCTAssertEqual(export.appBuild, "94") XCTAssertEqual(export.appBuild, "94")
XCTAssertEqual(export.effectiveCharacterCount, 4) XCTAssertEqual(export.effectiveCharacterCount, 4)
XCTAssertEqual(export.requiredEffectiveCharacterCount, 2_500) XCTAssertEqual(export.requiredEffectiveCharacterCount, 2_500)
XCTAssertEqual(export.trainingExtractionMaximumCharacterCount, 5_000)
XCTAssertEqual(export.examples.count, 1) XCTAssertEqual(export.examples.count, 1)
XCTAssertEqual(export.examples[0].prePolishText, "你好 世界") XCTAssertEqual(export.examples[0].prePolishText, "你好 世界")
XCTAssertEqual(export.examples[0].finalText, "你好,世界。") XCTAssertEqual(export.examples[0].finalText, "你好,世界。")
@@ -105,7 +106,50 @@ final class PolishStyleCorpusExportStoreTests: XCTestCase {
XCTAssertEqual(decoded.examples.count, 1) XCTAssertEqual(decoded.examples.count, 1)
} }
func testExportUsesNewestCompleteExamplesThroughThreshold() throws { func testExportUsesNewestCompleteExamplesThroughFiveThousandThreshold() throws {
// Total available characters: 7,000 above the export cap.
// Newest first: newest (3,000) + middle (2,000) = 5,000 (cap reached,
// stop). Oldest is dropped from the export window.
let history = SyncedSpeechHistory(
entries: [
SpeechHistoryEntry(
text: "oldest",
prePolishText: String(repeating: "", count: 2_000),
createdAt: Date(timeIntervalSince1970: 1)
),
SpeechHistoryEntry(
text: "middle-complete",
prePolishText: String(repeating: "", count: 2_000),
createdAt: Date(timeIntervalSince1970: 2)
),
SpeechHistoryEntry(
text: "newest-complete",
prePolishText: String(repeating: "", count: 3_000),
createdAt: Date(timeIntervalSince1970: 3)
)
]
)
let export = try XCTUnwrap(
PolishStyleCorpusExportStore(
directoryURL: temporaryDirectory()
).makeExport(from: history)
)
XCTAssertEqual(export.effectiveCharacterCount, 5_000)
XCTAssertEqual(export.requiredEffectiveCharacterCount, 2_500)
XCTAssertEqual(export.trainingExtractionMaximumCharacterCount, 5_000)
XCTAssertEqual(
export.examples.map(\.finalText),
["middle-complete", "newest-complete"]
)
XCTAssertEqual(export.examples[0].prePolishText.count, 2_000)
XCTAssertEqual(export.examples[1].prePolishText.count, 3_000)
}
func testExportKeepsEveryEligibleExampleWhenUnderFiveThousandThreshold() throws {
// 3,600 characters total below the 5,000 cap, so the export keeps
// every eligible example chronologically.
let history = SyncedSpeechHistory( let history = SyncedSpeechHistory(
entries: [ entries: [
SpeechHistoryEntry( SpeechHistoryEntry(
@@ -132,13 +176,11 @@ final class PolishStyleCorpusExportStoreTests: XCTestCase {
).makeExport(from: history) ).makeExport(from: history)
) )
XCTAssertEqual(export.effectiveCharacterCount, 2_600) XCTAssertEqual(export.effectiveCharacterCount, 3_600)
XCTAssertEqual( XCTAssertEqual(
export.examples.map(\.finalText), export.examples.map(\.finalText),
["middle-complete", "newest-complete"] ["oldest", "middle-complete", "newest-complete"]
) )
XCTAssertEqual(export.examples[0].prePolishText.count, 1_600)
XCTAssertEqual(export.examples[1].prePolishText.count, 1_000)
} }
func testEmptyCorpusRemovesPreviousExport() throws { func testEmptyCorpusRemovesPreviousExport() throws {
@@ -146,6 +146,42 @@ final class PolishStyleLearningServiceTests: XCTestCase {
XCTAssertEqual(window.examples.map(\.finalText), ["older", "newer"]) XCTAssertEqual(window.examples.map(\.finalText), ["older", "newer"])
} }
func testTrainingWindowHonorsCustomMaximumCharacterCount() {
// Total available characters: 7,000. With a 5,000-character
// cap (training-corpus export), newest (3,000) + middle (2,000)
// fills the window and oldest is dropped.
let oldest = PolishStyleLearningExample(
prePolishText: String(repeating: "", count: 2_000),
finalText: "oldest",
polishStyleID: nil,
createdAt: Date(timeIntervalSince1970: 1)
)
let middle = PolishStyleLearningExample(
prePolishText: String(repeating: "", count: 2_000),
finalText: "middle",
polishStyleID: nil,
createdAt: Date(timeIntervalSince1970: 2)
)
let newest = PolishStyleLearningExample(
prePolishText: String(repeating: "", count: 3_000),
finalText: "newest",
polishStyleID: nil,
createdAt: Date(timeIntervalSince1970: 3)
)
let window = PolishStyleLearningCorpusBuilder.trainingWindow(
from: [oldest, newest, middle],
maximumCharacterCount: PolishStyleLearningCorpusBuilder
.trainingExtractionMaximumCharacterCount
)
XCTAssertEqual(window.effectiveCharacterCount, 5_000)
XCTAssertEqual(
window.examples.map(\.finalText),
["middle", "newest"]
)
}
func testGenerationRunsExtractorBeforeSynthesizerWithSeparatedPayloads() async throws { func testGenerationRunsExtractorBeforeSynthesizerWithSeparatedPayloads() async throws {
var catalog = PolishStyleCatalog() var catalog = PolishStyleCatalog()
let activeStyle = PolishStylePack( let activeStyle = PolishStylePack(
@@ -1,21 +1,29 @@
import XCTest import XCTest
final class PolishStylesGenerationUITests: XCTestCase { final class PolishStylesGenerationUITests: XCTestCase {
func testTestBuildCanGenerateWithoutPersonalCorpus() { func testTestBuildRequiresTwelveFiftyCharacterMinimum() {
// The old "unlimited" test-mode bypass has been removed. Test
// builds now enforce a 1,250-character minimum: an empty corpus
// must keep the generation button disabled, and the visible
// progress text should reflect the remaining distance to the
// active gate.
continueAfterFailure = false continueAfterFailure = false
let app = launchServiceHarness( let app = launchServiceHarness(
additionalArgument: "--polish-styles-service-ui-test-no-corpus" additionalArgument: "--polish-styles-service-ui-test-no-corpus"
) )
XCTAssertTrue( let generate = app.buttons["polishStyles.learn.generate"]
XCTAssertTrue(generate.waitForExistence(timeout: 5))
XCTAssertFalse(
generate.isEnabled,
"Empty corpus in a test build must not unlock generation; the 1,250-character minimum still applies."
)
XCTAssertFalse(
app.staticTexts[ app.staticTexts[
"Test build: 2,500-character limit disabled" "Test build: 2,500-character limit disabled"
] ].exists,
.waitForExistence(timeout: 5) "The old unlimited-bypass label must no longer be shown."
) )
let generate = app.buttons["polishStyles.learn.generate"]
XCTAssertTrue(generate.exists)
XCTAssertTrue(generate.isEnabled)
} }
func testGeneratedStyleReviewAndSaveFlow() { func testGeneratedStyleReviewAndSaveFlow() {
-127
View File
@@ -1,127 +0,0 @@
# Account and managed gateway verification
This checklist validates the optional OSG account path without changing the
existing local or BYOK defaults. Never record tokens, Apple identifiers, audio,
prompts, transcripts, or model output while running these checks.
## Automated gate
```bash
./Scripts/run-tests.sh validate
./Scripts/run-tests.sh pr
swiftlint lint --quiet --strict
xcodebuild \
-project OSGKeyboard.xcodeproj \
-scheme OSGKeyboardUITests \
-destination 'platform=iOS Simulator,name=iPhone 17' \
-only-testing:OSGKeyboardUITests/AccountCenterUITests \
test
xcodebuild \
-project OSGKeyboard.xcodeproj \
-scheme OSGKeyboard \
-destination 'generic/platform=iOS Simulator' \
-configuration Release \
-onlyUsePackageVersionsFromResolvedFile \
CODE_SIGNING_ALLOWED=NO \
build
```
The account and managed gateway tests must remain hermetic. They use injected
Apple integrity adapters, URL loading stubs, and WebSocket fakes rather than
production credentials.
## Physical-device prerequisites
- A development build signed for `com.osgkeyboard.ios`.
- Sign in with Apple, App Attest, DeviceCheck, Associated Domains, and both
Keychain groups enabled in the provisioning profile.
- `https://osglab.com/.well-known/apple-app-site-association` returns HTTP 200
without a redirect and includes `X329MZU23S.com.osgkeyboard.ios` for `/i/*`.
- The production account service is ready at `https://account.osglab.com`.
- The production service temporarily enables `ALLOW_DEVELOPMENT_APP_ATTEST=true`
for the test window; disable it again after physical-device testing.
- OSGKeyboard is installed and enabled with Full Access for managed requests.
- The test account has enough non-production credits for the requested checks.
## Identity and account
1. Sign in with Apple and verify that nickname, balance, and referral state load
after a cold launch.
2. Confirm the raw nonce is never persisted and an App Attest assertion is
accepted. Repeat after an access-token expiry to exercise one refresh.
3. Open `https://osglab.com/i/{test-code}` while signed out. Sign in, then
verify the pending code is redeemed exactly once.
4. Force-quit and reopen the app. Verify session recovery without another Apple
prompt and confirm the keyboard extension cannot read the account session.
5. Sign out and verify account tokens plus shared gateway grants are removed.
6. Sign in again, choose Delete Account, complete Apple reauthentication, and
verify local and BYOK features still work afterward.
For destructive verification, use a disposable Apple sandbox identity:
1. Set a nickname, generate an invitation code, and select **Use Credits**.
2. Delete the account after both confirmations and fresh Apple authorization.
3. Confirm the app returns to signed-out/BYOK state and no account, grant,
profile, or purchase state remains visible.
4. Confirm old access and refresh tokens receive `401`; an Apple revoke outage
must not restore the locally deleted account.
5. Sign in again and confirm a new local App Attest key state is registered.
Only pseudonymous immutable ledger, StoreKit audit, and time-limited anti-abuse
records remain where required for replay and abuse prevention.
## Managed DeepSeek
1. Select **Use Credits** for the first time. Verify the managed-cloud data
disclosure appears, Cancel leaves BYOK selected, and Agree enables credits.
Switch away and back again to confirm the disclosure is not repeated.
2. Verify the runtime uses managed Volcengine ASR and managed polishing together.
3. Run one polish request and one AI request. Verify actionable behavior for
insufficient balance, expired grant, timeout, and cancellation.
4. In the server ledger, verify one reservation and one settlement per request.
Retrying the same transport request must not create a second charge.
## Managed Volcengine ASR
1. Record approximately ten seconds of Mandarin PCM16LE at 16 kHz.
2. Verify partial and final results, then run translate-and-polish.
3. Cancel one recording mid-stream and verify the WebSocket closes without a
stuck reservation.
4. Verify session-open fallback, idle timeout, empty result, insufficient
balance, and concurrency-limit behavior.
5. Confirm the ledger settles successful sessions and releases failed or
cancelled reservations.
Managed ASR currently does not send hotwords. Treat this as an explicit product
difference until the server request schema supports them.
## Regression gate
- Signed-out use remains valid.
- Local ASR never requires an account.
- Existing BYOK LLM and ASR credentials still use their direct providers.
- iCloud settings sync never contains account or gateway tokens.
- Flow, keyboard typing, and keyboard-extension memory-budget tests pass.
## StoreKit credits
The existing `ByRockyACoffee` product remains a voluntary consumable and never
grants credits. Configure `500tks` for 500 credits at USD 0.99, `1500tks` for
1,500 credits at USD 1.99 / CNY 18, and `3000tks` for 3,000 credits at
USD 2.99 / CNY 28.
1. Use a Sandbox Apple account and sign in to the same OSG account before
purchasing.
2. Confirm the purchase supplies the OSG account UUID as `appAccountToken`.
3. Buy each product and verify the server grants exactly 500, 1,500, or 3,000
credits and appends one `STOREKIT_PURCHASE` ledger entry before the app
finishes the transaction.
4. Submit the same signed transaction again and verify the response is marked
as replayed without changing the balance.
5. Interrupt the network after App Store success but before server
acknowledgement. Relaunch and verify the unfinished transaction reconciles
once.
6. Sign in to another OSG account and verify the first account's transaction is
rejected.
7. Confirm there is no Restore Purchases action for credit packs and that the
voluntary tip still changes only the local support count.
-296
View File
@@ -1,296 +0,0 @@
# App Store Connect — OSGKeyboard 2.0.3 (build 94)
> Current metadata baseline for the iOS/iPadOS App Store build. Version and build
> numbers come from `project.yml`. The repository also contains a separate
> macOS 15+ Developer ID target; it is not this App Store listing.
## App information
| Field | Value | Notes |
|---|---|---|
| App name | `OSGKeyboard` | ≤ 30 characters |
| Subtitle | `Voice input, everywhere` | ≤ 30 characters |
| Bundle ID | `com.osgkeyboard.ios` | iOS host target |
| Version / build | `2.0.3` / `94` | `MARKETING_VERSION` / `CURRENT_PROJECT_VERSION` |
| Minimum system | iOS/iPadOS 26 | iPhone and iPad |
| Primary locale | `en-US` | Simplified Chinese is also bundled |
| Primary category | Utilities | |
| Secondary category | Productivity | Optional |
| Age rating | 4+ | No objectionable content |
## URLs
| Field | Value |
|---|---|
| Support URL | `https://github.com/hkgood/OSGKeyboard/issues` |
| Marketing URL | `https://hkgood.github.io/OSGKeyboard/` |
| Privacy Policy URL | `https://hkgood.github.io/OSGKeyboard/privacy/` |
| EULA | Leave blank; use Apple's standard EULA |
## Pricing and availability
| Field | Value |
|---|---|
| Price | Free |
| In-App Purchases | Optional consumables: tip `ByRockyACoffee`; managed-credit packs `500tks`, `1500tks`, `3000tks` |
| Availability | All configured App Store territories |
| Pre-order | No |
## Description (≤ 4000 characters)
```text
OSGKeyboard is a voice and typing keyboard for iPhone and iPad. Speak in
any app and insert the transcript at the cursor, or switch to Chinese and
English typing without leaving the keyboard.
VOICE INPUT
• On-device by default. iOS 26 SpeechAnalyzer and DictationTranscriber
transcribe locally.
• Optional cloud recognition. Use your own provider credentials, or
sign in with Apple and choose managed credits.
• Optional AI polish and translation. Use your own provider API key or
managed credits; without either, recognized text can still be inserted.
• AI keyboard mode. Ask a spoken question, review the generated answer,
then explicitly insert or send it.
• Edit the last verified OSGKeyboard insertion by voice before replacing
or appending the result.
TYPING
• Chinese full pinyin, Microsoft double pinyin, and Sogou double pinyin,
with optional fuzzy-pinyin pairs.
• English autocomplete, autocorrect, and next-word prediction from
offline resources.
• Personal dictionary terms can participate in Chinese candidates,
English suggestions, ASR correction, and polish protection.
• iPhone and iPad layouts, including iPad globe and editing controls.
• Optional clipboard history is off by default and keeps up to 15 text
items from this device or Universal Clipboard in this device's App Group.
Turning it off keeps existing history; clearing is a separate confirmed action.
PRIVACY
• Limited first-party product analytics; no third-party analytics,
advertising, tracking SDKs, ATT, or IDFA.
• Product analytics never includes keyboard input, audio, transcripts,
prompts, model output, credentials, or personal identifiers. It can be
disabled in Settings, which deletes queued events.
• Local recognition does not upload audio.
• User-configured cloud requests go directly to that provider. Managed-credit
requests go through OSGKeyboard's managed gateway to the managed provider.
• Provider keys are stored in Keychain.
• Clipboard history stays device-local, does not iCloud-sync, and is not
sent to AI automatically. Text you insert may later be included when you
actively invoke polish with your configured provider.
• Core use requires no OSGKeyboard account.
OSGKeyboard's own code is source available for audit and personal,
non-commercial local use. It is not MIT-licensed or open source; see the
repository LICENSE for redistribution and commercial-use restrictions.
Requires iOS or iPadOS 26 or later.
https://github.com/hkgood/OSGKeyboard
```
## Promotional text (≤ 170 characters)
```text
Voice input anywhere, with on-device recognition by default. Use your own AI key or optional managed credits for cloud speech, polish, translation, and AI answers.
```
## Keywords (≤ 100 characters)
```text
keyboard,voice,dictation,speech,transcribe,AI,pinyin,Chinese,English,polish,typing,productivity
```
## What's new in 2.0.3
```text
NEW
• Home can now suggest repeated rare English terms alongside Chinese terms
for explicit review before adding them to the Personal Dictionary.
CHANGED
• Cards, settings typography, native selection menus, account rows, and
primary-to-detail navigation transitions now follow one visual system.
FIXED
• Apple account restoration now retries transient secure-session failures and
keeps sign-in loading feedback consistent.
• English and Chinese candidates now recover reliably after switching apps
without carrying state from the previous input field.
```
## App Review information
| Field | Value |
|---|---|
| Sign-in required | No |
| Demo account | Not applicable |
| Contact info | Maintainer's Apple Developer account details |
### Notes to App Review
```text
OSGKeyboard is a custom keyboard for iOS/iPadOS 26.
1. Add the keyboard:
Settings → General → Keyboard → Keyboards → Add New Keyboard →
OSGKeyboard.
2. Enable Full Access. It is required for App Group communication between
the keyboard and host app and for optional provider network requests.
3. Complete onboarding in the OSGKeyboard host app.
4. In any editable field, switch to OSGKeyboard and tap the microphone.
The default local engine uses on-device Apple speech recognition.
5. Local dictation and user-configured providers require no OSGKeyboard
account. The Account tab offers optional Sign in with Apple.
6. After signing in, Settings → AI Service → Use Credits enables the managed
cloud path. The consumable products are `500tks`, `1500tks`, and `3000tks`.
Purchased credits are verified by the account service before StoreKit
transactions are finished.
7. AI polish and AI mode can use either managed credits or a user-owned
provider key. Without either, local dictation still inserts recognized text.
8. Optional tip `ByRockyACoffee` remains a consumable support purchase and
does not grant managed credits or unlock features.
9. Clipboard history is off by default. To test it, open Settings →
Clipboard, enable History, copy text on this device or through Universal
Clipboard, then return to the keyboard. Secure fields hide the clipboard
entry point. Turning History off preserves saved items; use the separate
confirmed clear action to delete them.
10. First-party Product Analytics is enabled by default under Settings →
About → Privacy. Turning it off deletes queued events. It does not collect
keyboard input, audio, transcripts, prompts, model output, or credentials.
Privacy policy:
https://hkgood.github.io/OSGKeyboard/privacy/
Source and license:
https://github.com/hkgood/OSGKeyboard
```
## App Privacy answers
Use conservative disclosures that cover optional cloud recognition, cloud
polish/translation, and AI mode even though local recognition is the default.
### User Content → Audio Data
- Collected: Yes
- Purpose: App Functionality
- Linked to the user: Yes
- Used for tracking: No
Audio is sent off-device only when the user enables cloud recognition. The
configured provider may associate requests with the user's credential. In
managed-credit mode, audio is linked to the OSGKeyboard account for service
authorization and credit accounting.
### User Content → Other User Content
- Collected: Yes
- Purpose: App Functionality
- Linked to the user: Yes
- Used for tracking: No
This covers transcripts and nearby cursor context used for polish/translation,
AI questions and skill prompts, optional provider search requests, dictionary
terms included in provider prompts, and clipboard text only after the user
actively invokes a clipboard skill, names the clipboard in AI mode, or inserts
it and requests polish. Skill results may also be handed on-device to an Apple
Shortcut, while navigation addresses may be opened in the selected map app.
Device-local clipboard history and typing-learning data by themselves are not
collected by the developer.
### Contact Info → Name
- Collected: Yes
- Purpose: App Functionality
- Linked to the user: Yes
- Used for tracking: No
The display name supplied by Sign in with Apple is optional and is used only
for the optional OSGKeyboard account profile.
### Purchases → Purchase History
- Collected: Yes
- Purpose: App Functionality
- Linked to the user: Yes
- Used for tracking: No
StoreKit transaction identifiers, product identifiers, and granted-credit
results are processed to verify consumable managed-credit purchases, prevent
replay, and maintain the account credit ledger.
### Identifiers → User ID
- Collected: Yes
- Purpose: App Functionality
- Linked to the user: Yes
- Used for tracking: No
This covers the pseudonymous OSGKeyboard account identifier and scoped
managed-service grant identifiers. Core use remains available without an
OSGKeyboard account.
### Identifiers → Device ID
- Collected: Yes
- Purpose: Analytics
- Linked to the user: Yes
- Used for tracking: No
This is an app-scoped random installation identifier. It rotates when analytics
is re-enabled, after account deletion, or when a different account signs in. It
is not IDFA and is not used across apps.
### Usage Data → Product Interaction
- Collected: Yes
- Purpose: Analytics
- Linked to the user: Yes
- Used for tracking: No
This covers fixed event names for app and keyboard sessions, purchase-page
interactions, and invitation actions. It contains no free-form properties.
### Usage Data → Other Usage Data
- Collected: Yes
- Purpose: Analytics
- Linked to the user: Yes
- Used for tracking: No
This covers fixed AI feature categories, execution modes, outcome categories,
and coarse duration buckets. It does not include prompts, transcripts, model
output, audio, or keyboard content.
### Do not select
- Advertising, marketing, product personalization, or tracking
- Email address, phone number, physical address, location, contacts, photos,
browsing history, or search history
- Usage data or diagnostics stored only locally or in the user's private iCloud
## Encryption
`Info.plist` declares `ITSAppUsesNonExemptEncryption = false`. Network calls use
standard HTTPS. Re-evaluate this answer if non-exempt cryptography is added.
## Submission checklist
- [ ] Confirm `project.yml` still reads version 2.0.3 / build 94
- [ ] Open the existing Xcode project (do not regenerate unless needed)
- [ ] Run the release build and test suites on macOS with Xcode 26
- [ ] Replace screenshots with captures from the submitted build
- [ ] Verify the privacy answers against the submitted provider features
- [ ] In App Store Connect, add Device ID, Product Interaction, and Other
Usage Data for Analytics; linked to the user, not used for tracking
- [ ] Confirm `500tks`, `1500tks`, and `3000tks` are approved, consumable,
and mapped to the server credit catalog
- [ ] Confirm `ByRockyACoffee` remains an optional consumable tip and unlocks
no feature
- [ ] Upload, select build 94, add review notes, and submit
-122
View File
@@ -1,122 +0,0 @@
# StoreKit — Voluntary Support Tip
OSGKeyboard offers a single **Consumable** in-app purchase:
| Field | Value |
|---|---|
| **Type** | Consumable |
| **Product ID** | `ByRockyACoffee` |
| **Reference name (Connect)** | 给老刘买杯咖啡 |
| **Display name (en)** | Buy me a cup of coffee |
| **Display name (zh-Hans)** | 给老刘买杯冰美式 |
| **Price (China)** | ¥28 (nearest available App Store tier) |
| **Price (US, suggested)** | $3.99 equivalent tier |
**Important:** this tip does **not** unlock translation, dictionary limits,
long Flow sessions, or any other capability. The app remains fully free.
Consumable purchases **cannot be restored** (Apple policy). Settings copy
explains this to users.
---
## App Store Connect setup
1. Open **App Store Connect → OSGKeyboard → In-App Purchases**.
2. Create **Consumable** with Product ID `ByRockyACoffee`.
3. Add localizations (en + zh-Hans) using the strings in
`OSGKeyboardShared/*/Shared.strings` (`tip.*` keys) and
`docs/APPSTORE_METADATA.md`.
4. Set pricing: **China ¥28** (App Store only offers fixed tiers — ¥28 is
the nearest to ¥30); pick equivalent tiers for other territories.
5. Submit the IAP for review **with** the app version that includes the
Settings → Support the Developer entry.
### Review screenshot (required — fixes「元数据丢失」)
App Store Connect → IAP **ByRockyACoffee****审核信息****截屏**:
1. Run the app (Simulator or device) with Settings open at the top
**支持开发者** card showing the green **打赏 ¥28.00** button.
2. Capture that screen (⌘S in Simulator, or device screenshot).
3. Upload to **截屏 → 选取文件**.
4. Optional **审核备注**:
```
Optional voluntary tip only (Consumable IAP ByRockyACoffee).
All features free before and after purchase. Settings tab → top of page.
Consumable — cannot restore (stated in UI).
```
Save — status should become **准备提交**.
---
## Local testing (StoreKit Test — no Connect / sandbox account)
Uses [`OSGKeyboard.storekit`](../OSGKeyboard.storekit). The **OSGKeyboard**
scheme already references it in `project.yml` (`storeKitConfiguration`).
1. `xcodegen generate && open OSGKeyboard.xcodeproj`
2. **Product → Scheme → Edit Scheme → Run → Options**
- Confirm **StoreKit Configuration** = `OSGKeyboard.storekit`
3. Run **OSGKeyboard** on Simulator (e.g. iPhone 17) or a plugged-in device
4. Open **Settings** tab → top card **支持开发者**
5. Tap **打赏 ¥28.00** → StoreKit Test purchase sheet appears
6. **Buy** → thank-you alert; **Cancel** → no error
7. Repeat buy once (Consumable allows multiple)
**Debug menu (optional):** Xcode → **Debug → StoreKit → Manage Transactions**
to view / delete test purchases.
---
## Sandbox testing (real App Store sandbox — after Connect IAP is 准备提交)
1. App Store Connect → **用户和访问****沙盒** → create a **Sandbox Tester**
2. On device: **设置 → App Store → 沙盒账户** → sign in (not your real Apple ID)
3. Install via **TestFlight** or **Debug run without** `.storekit`:
- To hit Connect products: Edit Scheme → Run → Options → set StoreKit
Configuration to **None**, then run on device
4. Settings → **支持开发者** → purchase with sandbox account
5. Sandbox charges are free; receipt is real sandbox flow
---
## Submit to App Review (after local + sandbox pass)
Yes — **wait until testing looks good**, then:
1. IAP **ByRockyACoffee** status = **准备提交** (pricing + screenshot + localizations)
2. Bump app version in `project.yml` / `CHANGELOG.md` if needed
3. **Archive** → upload build to App Store Connect
4. Open the new **App Store version** page → **App 内购买项目****+** → select **ByRockyACoffee**
5. Fill metadata, attach build, submit **version + IAP together** (first IAP rule)
### Sandbox checklist
- [ ] Product loads and shows localized price
- [ ] Successful purchase shows thank-you alert
- [ ] User cancel returns to idle (no error spam)
- [ ] Repeat purchase works (Consumable allows multiple)
- [ ] No Restore button for this product (Consumable)
- [ ] Translation, Flow, dictionary, BYOK unchanged after tipping
### macOS note
The menu-bar Mac build (`com.osgkeyboard.mac`) ships via Developer ID outside
the Mac App Store today. StoreKit products load only for App Store builds.
The Mac Settings UI is present for parity; tip IAP requires an App Store
distribution if Mac tipping is enabled later.
---
## Files
| File | Role |
|---|---|
| `OSGKeyboardShared/Services/Tip/TipProduct.swift` | Product ID constants |
| `OSGKeyboardShared/Services/Tip/TipPurchaseManager.swift` | StoreKit 2 purchase flow |
| `OSGKeyboardShared/DesignSystem/SupportDeveloperSection.swift` | iOS Settings UI |
| `OSGKeyboardMac/MacSupportDeveloperTipRows.swift` | macOS Settings UI |
| `OSGKeyboard.storekit` | Local StoreKit Test catalog |
-55
View File
@@ -1,55 +0,0 @@
# Typing Keyboard Baseline (Phase 0)
Frozen reference for NanoMouse × OSGKeyboard fusion.
## NanoMouse reference
| Field | Value |
|-------|--------|
| Repo | https://github.com/xjwhnxjwhn/nanomouse |
| Frozen commit | `a6177d898a01662ce551b43a01cf82a9f84ca54c` (2026-07-29 tip at clone) |
| Local clone (gitignored) | `.refs/nanomouse/` |
## KeyboardKit / key shell
NanoMouse does **not** SPM-pin upstream KeyboardKit Pro. It vendors an MIT KeyboardKit tree inside:
`ios/Packages/HamsterKeyboardKit/Sources/KeyboardKit/` (see that folders `LICENSE`).
OSG ships a **lean SwiftUI key shell** in-repo (`OSGKeyboardExt/Typing/`)
behind `TypingLayoutProviding`; KeyboardKit is not linked.
**KeyboardKit Pro is not used.**
## Chinese schema / lexicon
| Choice | Detail |
|--------|--------|
| Product intent | Full pinyin + Microsoft/Sogou double pinyin + opt-in fuzzy pairs |
| Engine | librime 1.17.0 via static XCFramework (BSD-3-Clause) |
| Binary package | `ghostflyby/librime-xcframework` `1.17.0-pack.1`, checksum `0f0fc13b…1164` |
| Baseline | rime-pinyin-simp (Apache-2.0) |
| Modern words | Jieba frequencies + phrase-pinyin-data + pinyin-data (MIT) |
| Generated lexicon | `Resources/Typing/Rime/osg_pinyin.dict.yaml` (~365K entries) |
| User learning | librime userdb in App Group |
| Explicitly excluded | rime-ice / rime-double-pinyin (GPL), Luna / Essay (LGPL) |
The dictionary is rebuilt deterministically by
`Scripts/typing/build_rime_dictionary.py`; `manifest.json` pins every source
commit and SHA-256.
## Memory / height budget
| Mode | Target height | Memory notes |
|------|---------------|--------------|
| Voice | 281 pt (`KeyboardRootView.totalHeight`) | Matches typing height; no typing engine loaded |
| Typing | 281 pt (`TypingRootView.totalHeight`) | Host-prebuilt Rime data; extension opens one session |
| RSS goal | Typing peak &lt; 50 MB | Session closes on voice switch / memory warning |
## Success criteria (Phase 1)
- Top-right tab switches voice ↔ typing; recording/processing locks voice.
- English QWERTY + 123 / basic symbols.
- Full pinyin and Microsoft/Sogou double pinyin produce phrase candidates.
- Fuzzy pairs default off and are enabled individually in Settings.
- No GPL/LGPL input data in the app; NOTICE lists exact source licenses.
-46
View File
@@ -1,46 +0,0 @@
<svg id="livetype" xmlns="http://www.w3.org/2000/svg" width="119.66407" height="40" viewBox="0 0 119.66407 40">
<title>Download_on_the_App_Store_Badge_US-UK_RGB_blk_4SVG_092917</title>
<g>
<g>
<g>
<path d="M110.13477,0H9.53468c-.3667,0-.729,0-1.09473.002-.30615.002-.60986.00781-.91895.0127A13.21476,13.21476,0,0,0,5.5171.19141a6.66509,6.66509,0,0,0-1.90088.627A6.43779,6.43779,0,0,0,1.99757,1.99707,6.25844,6.25844,0,0,0,.81935,3.61816a6.60119,6.60119,0,0,0-.625,1.90332,12.993,12.993,0,0,0-.1792,2.002C.00587,7.83008.00489,8.1377,0,8.44434V31.5586c.00489.3105.00587.6113.01515.9219a12.99232,12.99232,0,0,0,.1792,2.0019,6.58756,6.58756,0,0,0,.625,1.9043A6.20778,6.20778,0,0,0,1.99757,38.001a6.27445,6.27445,0,0,0,1.61865,1.1787,6.70082,6.70082,0,0,0,1.90088.6308,13.45514,13.45514,0,0,0,2.0039.1768c.30909.0068.6128.0107.91895.0107C8.80567,40,9.168,40,9.53468,40H110.13477c.3594,0,.7246,0,1.084-.002.3047,0,.6172-.0039.9219-.0107a13.279,13.279,0,0,0,2-.1768,6.80432,6.80432,0,0,0,1.9082-.6308,6.27742,6.27742,0,0,0,1.6172-1.1787,6.39482,6.39482,0,0,0,1.1816-1.6143,6.60413,6.60413,0,0,0,.6191-1.9043,13.50643,13.50643,0,0,0,.1856-2.0019c.0039-.3106.0039-.6114.0039-.9219.0078-.3633.0078-.7246.0078-1.0938V9.53613c0-.36621,0-.72949-.0078-1.09179,0-.30664,0-.61426-.0039-.9209a13.5071,13.5071,0,0,0-.1856-2.002,6.6177,6.6177,0,0,0-.6191-1.90332,6.46619,6.46619,0,0,0-2.7988-2.7998,6.76754,6.76754,0,0,0-1.9082-.627,13.04394,13.04394,0,0,0-2-.17676c-.3047-.00488-.6172-.01074-.9219-.01269-.3594-.002-.7246-.002-1.084-.002Z" style="fill: #a6a6a6"/>
<path d="M8.44483,39.125c-.30468,0-.602-.0039-.90429-.0107a12.68714,12.68714,0,0,1-1.86914-.1631,5.88381,5.88381,0,0,1-1.65674-.5479,5.40573,5.40573,0,0,1-1.397-1.0166,5.32082,5.32082,0,0,1-1.02051-1.3965,5.72186,5.72186,0,0,1-.543-1.6572,12.41351,12.41351,0,0,1-.1665-1.875c-.00634-.2109-.01464-.9131-.01464-.9131V8.44434S.88185,7.75293.8877,7.5498a12.37039,12.37039,0,0,1,.16553-1.87207,5.7555,5.7555,0,0,1,.54346-1.6621A5.37349,5.37349,0,0,1,2.61183,2.61768,5.56543,5.56543,0,0,1,4.01417,1.59521a5.82309,5.82309,0,0,1,1.65332-.54394A12.58589,12.58589,0,0,1,7.543.88721L8.44532.875H111.21387l.9131.0127a12.38493,12.38493,0,0,1,1.8584.16259,5.93833,5.93833,0,0,1,1.6709.54785,5.59374,5.59374,0,0,1,2.415,2.41993,5.76267,5.76267,0,0,1,.5352,1.64892,12.995,12.995,0,0,1,.1738,1.88721c.0029.2832.0029.5874.0029.89014.0079.375.0079.73193.0079,1.09179V30.4648c0,.3633,0,.7178-.0079,1.0752,0,.3252,0,.6231-.0039.9297a12.73126,12.73126,0,0,1-.1709,1.8535,5.739,5.739,0,0,1-.54,1.67,5.48029,5.48029,0,0,1-1.0156,1.3857,5.4129,5.4129,0,0,1-1.3994,1.0225,5.86168,5.86168,0,0,1-1.668.5498,12.54218,12.54218,0,0,1-1.8692.1631c-.2929.0068-.5996.0107-.8974.0107l-1.084.002Z"/>
</g>
<g id="_Group_" data-name="&lt;Group&gt;">
<g id="_Group_2" data-name="&lt;Group&gt;">
<g id="_Group_3" data-name="&lt;Group&gt;">
<path id="_Path_" data-name="&lt;Path&gt;" d="M24.76888,20.30068a4.94881,4.94881,0,0,1,2.35656-4.15206,5.06566,5.06566,0,0,0-3.99116-2.15768c-1.67924-.17626-3.30719,1.00483-4.1629,1.00483-.87227,0-2.18977-.98733-3.6085-.95814a5.31529,5.31529,0,0,0-4.47292,2.72787c-1.934,3.34842-.49141,8.26947,1.3612,10.97608.9269,1.32535,2.01018,2.8058,3.42763,2.7533,1.38706-.05753,1.9051-.88448,3.5794-.88448,1.65876,0,2.14479.88448,3.591.8511,1.48838-.02416,2.42613-1.33124,3.32051-2.66914a10.962,10.962,0,0,0,1.51842-3.09251A4.78205,4.78205,0,0,1,24.76888,20.30068Z" style="fill: #fff"/>
<path id="_Path_2" data-name="&lt;Path&gt;" d="M22.03725,12.21089a4.87248,4.87248,0,0,0,1.11452-3.49062,4.95746,4.95746,0,0,0-3.20758,1.65961,4.63634,4.63634,0,0,0-1.14371,3.36139A4.09905,4.09905,0,0,0,22.03725,12.21089Z" style="fill: #fff"/>
</g>
</g>
<g>
<path d="M42.30227,27.13965h-4.7334l-1.13672,3.35645H34.42727l4.4834-12.418h2.083l4.4834,12.418H43.438ZM38.0591,25.59082h3.752l-1.84961-5.44727h-.05176Z" style="fill: #fff"/>
<path d="M55.15969,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238H48.4302v1.50586h.03418a3.21162,3.21162,0,0,1,2.88281-1.60059C53.645,21.34766,55.15969,23.16406,55.15969,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C52.30227,29.01563,53.24953,27.81934,53.24953,25.96973Z" style="fill: #fff"/>
<path d="M65.12453,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238H58.395v1.50586h.03418A3.21162,3.21162,0,0,1,61.312,21.34766C63.60988,21.34766,65.12453,23.16406,65.12453,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C62.26711,29.01563,63.21438,27.81934,63.21438,25.96973Z" style="fill: #fff"/>
<path d="M71.71047,27.03613c.1377,1.23145,1.334,2.04,2.96875,2.04,1.56641,0,2.69336-.80859,2.69336-1.91895,0-.96387-.67969-1.541-2.28906-1.93652l-1.60937-.3877c-2.28027-.55078-3.33887-1.61719-3.33887-3.34766,0-2.14258,1.86719-3.61426,4.51855-3.61426,2.624,0,4.42285,1.47168,4.4834,3.61426h-1.876c-.1123-1.23926-1.13672-1.9873-2.63379-1.9873s-2.52148.75684-2.52148,1.8584c0,.87793.6543,1.39453,2.25488,1.79l1.36816.33594c2.54785.60254,3.60645,1.626,3.60645,3.44238,0,2.32324-1.85059,3.77832-4.79395,3.77832-2.75391,0-4.61328-1.4209-4.7334-3.667Z" style="fill: #fff"/>
<path d="M83.34621,19.2998v2.14258h1.72168v1.47168H83.34621v4.99121c0,.77539.34473,1.13672,1.10156,1.13672a5.80752,5.80752,0,0,0,.61133-.043v1.46289a5.10351,5.10351,0,0,1-1.03223.08594c-1.833,0-2.54785-.68848-2.54785-2.44434V22.91406H80.16262V21.44238H81.479V19.2998Z" style="fill: #fff"/>
<path d="M86.065,25.96973c0-2.84863,1.67773-4.63867,4.29395-4.63867,2.625,0,4.29492,1.79,4.29492,4.63867,0,2.85645-1.66113,4.63867-4.29492,4.63867C87.72609,30.6084,86.065,28.82617,86.065,25.96973Zm6.69531,0c0-1.9541-.89551-3.10742-2.40137-3.10742s-2.40039,1.16211-2.40039,3.10742c0,1.96191.89453,3.10645,2.40039,3.10645S92.76027,27.93164,92.76027,25.96973Z" style="fill: #fff"/>
<path d="M96.18606,21.44238h1.77246v1.541h.043a2.1594,2.1594,0,0,1,2.17773-1.63574,2.86616,2.86616,0,0,1,.63672.06934v1.73828a2.59794,2.59794,0,0,0-.835-.1123,1.87264,1.87264,0,0,0-1.93652,2.083v5.37012h-1.8584Z" style="fill: #fff"/>
<path d="M109.3843,27.83691c-.25,1.64355-1.85059,2.77148-3.89844,2.77148-2.63379,0-4.26855-1.76465-4.26855-4.5957,0-2.83984,1.64355-4.68164,4.19043-4.68164,2.50488,0,4.08008,1.7207,4.08008,4.46582v.63672h-6.39453v.1123a2.358,2.358,0,0,0,2.43555,2.56445,2.04834,2.04834,0,0,0,2.09082-1.27344Zm-6.28223-2.70215h4.52637a2.1773,2.1773,0,0,0-2.2207-2.29785A2.292,2.292,0,0,0,103.10207,25.13477Z" style="fill: #fff"/>
</g>
</g>
</g>
<g id="_Group_4" data-name="&lt;Group&gt;">
<g>
<path d="M37.82619,8.731a2.63964,2.63964,0,0,1,2.80762,2.96484c0,1.90625-1.03027,3.002-2.80762,3.002H35.67092V8.731Zm-1.22852,5.123h1.125a1.87588,1.87588,0,0,0,1.96777-2.146,1.881,1.881,0,0,0-1.96777-2.13379h-1.125Z" style="fill: #fff"/>
<path d="M41.68068,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C44.57522,13.99463,45.01369,13.42432,45.01369,12.44434Z" style="fill: #fff"/>
<path d="M51.57326,14.69775h-.92187l-.93066-3.31641h-.07031l-.92676,3.31641h-.91309l-1.24121-4.50293h.90137l.80664,3.436h.06641l.92578-3.436h.85254l.92578,3.436h.07031l.80273-3.436h.88867Z" style="fill: #fff"/>
<path d="M53.85354,10.19482H54.709v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915h-.88867V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z" style="fill: #fff"/>
<path d="M59.09377,8.437h.88867v6.26074h-.88867Z" style="fill: #fff"/>
<path d="M61.21779,12.44434a2.13346,2.13346,0,1,1,4.24756,0,2.1338,2.1338,0,1,1-4.24756,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C64.11232,13.99463,64.5508,13.42432,64.5508,12.44434Z" style="fill: #fff"/>
<path d="M66.4009,13.42432c0-.81055.60352-1.27783,1.6748-1.34424l1.21973-.07031v-.38867c0-.47559-.31445-.74414-.92187-.74414-.49609,0-.83984.18213-.93848.50049h-.86035c.09082-.77344.81836-1.26953,1.83984-1.26953,1.12891,0,1.76563.562,1.76563,1.51318v3.07666h-.85547v-.63281h-.07031a1.515,1.515,0,0,1-1.35254.707A1.36026,1.36026,0,0,1,66.4009,13.42432Zm2.89453-.38477v-.37646l-1.09961.07031c-.62012.0415-.90137.25244-.90137.64941,0,.40527.35156.64111.835.64111A1.0615,1.0615,0,0,0,69.29543,13.03955Z" style="fill: #fff"/>
<path d="M71.34816,12.44434c0-1.42285.73145-2.32422,1.86914-2.32422a1.484,1.484,0,0,1,1.38086.79h.06641V8.437h.88867v6.26074h-.85156v-.71143h-.07031a1.56284,1.56284,0,0,1-1.41406.78564C72.0718,14.772,71.34816,13.87061,71.34816,12.44434Zm.918,0c0,.95508.4502,1.52979,1.20313,1.52979.749,0,1.21191-.583,1.21191-1.52588,0-.93848-.46777-1.52979-1.21191-1.52979C72.72121,10.91846,72.26613,11.49707,72.26613,12.44434Z" style="fill: #fff"/>
<path d="M79.23,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C82.12453,13.99463,82.563,13.42432,82.563,12.44434Z" style="fill: #fff"/>
<path d="M84.66945,10.19482h.85547v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915H87.605V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z" style="fill: #fff"/>
<path d="M93.51516,9.07373v1.1416h.97559v.74854h-.97559V13.2793c0,.47168.19434.67822.63672.67822a2.96657,2.96657,0,0,0,.33887-.02051v.74023a2.9155,2.9155,0,0,1-.4834.04541c-.98828,0-1.38184-.34766-1.38184-1.21582v-2.543h-.71484v-.74854h.71484V9.07373Z" style="fill: #fff"/>
<path d="M95.70461,8.437h.88086v2.48145h.07031a1.3856,1.3856,0,0,1,1.373-.80664,1.48339,1.48339,0,0,1,1.55078,1.67871v2.90723H98.69v-2.688c0-.71924-.335-1.0835-.96289-1.0835a1.05194,1.05194,0,0,0-1.13379,1.1416v2.62988h-.88867Z" style="fill: #fff"/>
<path d="M104.76125,13.48193a1.828,1.828,0,0,1-1.95117,1.30273A2.04531,2.04531,0,0,1,100.73,12.46045a2.07685,2.07685,0,0,1,2.07617-2.35254c1.25293,0,2.00879.856,2.00879,2.27V12.688h-3.17969v.0498a1.1902,1.1902,0,0,0,1.19922,1.29,1.07934,1.07934,0,0,0,1.07129-.5459Zm-3.126-1.45117h2.27441a1.08647,1.08647,0,0,0-1.1084-1.1665A1.15162,1.15162,0,0,0,101.63527,12.03076Z" style="fill: #fff"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 11 KiB

-29
View File
@@ -1,29 +0,0 @@
<svg id="CNSC" xmlns="http://www.w3.org/2000/svg" width="108.85157" height="40" viewBox="0 0 108.85157 40">
<title>Download_on_the_App_Store_Badge_CNSC_RGB_blk_092917</title>
<g>
<g>
<path d="M99.32227,0H9.53468c-.3667,0-.729,0-1.09473.002-.30615.002-.60986.00781-.91895.0127A13.21476,13.21476,0,0,0,5.5171.19141a6.66509,6.66509,0,0,0-1.90088.627A6.43779,6.43779,0,0,0,1.99757,1.99707,6.25844,6.25844,0,0,0,.81935,3.61816a6.60119,6.60119,0,0,0-.625,1.90332,12.993,12.993,0,0,0-.1792,2.002C.00587,7.83008.00489,8.1377,0,8.44434V31.5586c.00489.3105.00587.6113.01515.9219a12.99232,12.99232,0,0,0,.1792,2.0019,6.58756,6.58756,0,0,0,.625,1.9043A6.20778,6.20778,0,0,0,1.99757,38.001a6.27445,6.27445,0,0,0,1.61865,1.1787,6.70082,6.70082,0,0,0,1.90088.6308,13.45514,13.45514,0,0,0,2.0039.1768c.30909.0068.6128.0107.91895.0107C8.80567,40,9.168,40,9.53468,40H99.32227c.3594,0,.7246,0,1.084-.002.3047,0,.6172-.0039.9219-.0107a13.279,13.279,0,0,0,2-.1768,6.80432,6.80432,0,0,0,1.9082-.6308,6.27742,6.27742,0,0,0,1.6172-1.1787,6.39482,6.39482,0,0,0,1.1816-1.6143,6.60413,6.60413,0,0,0,.6191-1.9043,13.50643,13.50643,0,0,0,.1856-2.0019c.0039-.3106.0039-.6114.0039-.9219.0078-.3633.0078-.7246.0078-1.0938V9.53613c0-.36621,0-.72949-.0078-1.09179,0-.30664,0-.61426-.0039-.9209a13.5071,13.5071,0,0,0-.1856-2.002,6.6177,6.6177,0,0,0-.6191-1.90332,6.46619,6.46619,0,0,0-2.7988-2.7998,6.76754,6.76754,0,0,0-1.9082-.627,13.04394,13.04394,0,0,0-2-.17676c-.3047-.00488-.6172-.01074-.9219-.01269C100.04687,0,99.68167,0,99.32227,0Z" style="fill: #a6a6a6"/>
<path d="M8.44483,39.125c-.30468,0-.602-.0039-.90429-.0107a12.68714,12.68714,0,0,1-1.86914-.1631,5.88381,5.88381,0,0,1-1.65674-.5479,5.40573,5.40573,0,0,1-1.397-1.0166,5.32082,5.32082,0,0,1-1.02051-1.3965,5.72186,5.72186,0,0,1-.543-1.6572,12.41351,12.41351,0,0,1-.1665-1.875c-.00634-.2109-.01464-.9131-.01464-.9131V8.44434S.88185,7.75293.8877,7.5498a12.37039,12.37039,0,0,1,.16553-1.87207,5.7555,5.7555,0,0,1,.54346-1.6621A5.37349,5.37349,0,0,1,2.61183,2.61768,5.56543,5.56543,0,0,1,4.01417,1.59521a5.82309,5.82309,0,0,1,1.65332-.54394A12.58589,12.58589,0,0,1,7.543.88721L8.44532.875h91.956l.9131.0127a12.38493,12.38493,0,0,1,1.8584.16259,5.93833,5.93833,0,0,1,1.6709.54785,5.59374,5.59374,0,0,1,2.415,2.41993A5.76267,5.76267,0,0,1,107.794,5.667a12.995,12.995,0,0,1,.1738,1.88721c.0029.2832.0029.5874.0029.89014.0079.375.0079.73193.0079,1.09179V30.4648c0,.3633,0,.7178-.0079,1.0752,0,.3252,0,.6231-.0039.9297a12.73126,12.73126,0,0,1-.1709,1.8535,5.739,5.739,0,0,1-.54,1.67,5.48029,5.48029,0,0,1-1.0156,1.3857,5.4129,5.4129,0,0,1-1.3994,1.0225,5.86168,5.86168,0,0,1-1.668.5498,12.54218,12.54218,0,0,1-1.8692.1631c-.2929.0068-.5996.0107-.8974.0107l-1.084.002Z"/>
</g>
<g>
<path d="M41.16742,16.55462H37.11585l-.973,2.873H34.42678L38.26437,8.79837h1.783l3.83759,10.62922H42.13957Zm-3.632-1.32573H40.747l-1.58318-4.66261h-.0443Z" style="fill: #fff"/>
<path d="M52.1728,15.55322c0,2.40821-1.28895,3.95545-3.23407,3.95545a2.62719,2.62719,0,0,1-2.4383-1.35582h-.03678v3.83842H44.87294V11.678h1.53972V12.967h.02926a2.749,2.749,0,0,1,2.46756-1.37C50.87633,11.59694,52.1728,13.1517,52.1728,15.55322Zm-1.635,0c0-1.569-.81082-2.60046-2.04794-2.60046-1.21539,0-2.03289,1.05323-2.03289,2.60046,0,1.56145.8175,2.60715,2.03289,2.60715C49.727,18.16037,50.53779,17.1364,50.53779,15.55322Z" style="fill: #fff"/>
<path d="M60.70267,15.55322c0,2.40821-1.28937,3.95545-3.23449,3.95545a2.62719,2.62719,0,0,1-2.4383-1.35582H54.9931v3.83842H53.4024V11.678h1.53972V12.967h.02926a2.749,2.749,0,0,1,2.46756-1.37C59.40578,11.59694,60.70267,13.1517,60.70267,15.55322Zm-1.63543,0c0-1.569-.81082-2.60046-2.04794-2.60046-1.21539,0-2.03289,1.05323-2.03289,2.60046,0,1.56145.8175,2.60715,2.03289,2.60715C58.25643,18.16037,59.06725,17.1364,59.06725,15.55322Z" style="fill: #fff"/>
<path d="M66.3391,16.466c.11786,1.05406,1.14183,1.74618,2.54112,1.74618,1.34077,0,2.30539-.69212,2.30539-1.64253,0-.825-.58178-1.319-1.95933-1.65758l-1.37755-.33185c-1.95181-.47144-2.85792-1.38424-2.85792-2.86544,0-1.834,1.59823-3.09364,3.86684-3.09364,2.24688,0,3.7866,1.25969,3.83842,3.09364H71.09032c-.09613-1.06075-.973-1.701-2.2544-1.701s-2.15828.64782-2.15828,1.59071c0,.75147.56,1.19366,1.93008,1.53219l1.17109.28755c2.18085.51575,3.08612,1.39176,3.08612,2.94652,0,1.98859-1.58318,3.23407-4.10257,3.23407-2.35722,0-3.94876-1.21623-4.05158-3.13878Z" style="fill: #fff"/>
<path d="M76.29961,9.84407v1.834h1.47368v1.25969H76.29961V17.21c0,.6637.29507.973.94289.973a4.971,4.971,0,0,0,.52327-.03678v1.25217a4.36838,4.36838,0,0,1-.88354.07356c-1.569,0-2.18085-.5893-2.18085-2.09224V12.93771H73.5746V11.678h1.12678v-1.834Z" style="fill: #fff"/>
<path d="M78.6259,15.55322c0-2.4383,1.43607-3.97049,3.67542-3.97049,2.24688,0,3.67626,1.53219,3.67626,3.97049,0,2.445-1.42185,3.97049-3.67626,3.97049C80.04776,19.52372,78.6259,17.99821,78.6259,15.55322Zm5.73089,0c0-1.67262-.76651-2.65981-2.05546-2.65981s-2.05546.99471-2.05546,2.65981c0,1.67931.76651,2.659,2.05546,2.659S84.35679,17.23253,84.35679,15.55322Z" style="fill: #fff"/>
<path d="M87.2891,11.678h1.51715v1.319H88.843a1.84835,1.84835,0,0,1,1.864-1.40012,2.4533,2.4533,0,0,1,.545.05935v1.48789a2.22372,2.22372,0,0,0-.71469-.09613,1.6029,1.6029,0,0,0-1.65758,1.783v4.59658H87.2891Z" style="fill: #fff"/>
<path d="M98.5862,17.15145a3.11011,3.11011,0,0,1-3.33689,2.37227c-2.2544,0-3.65369-1.51046-3.65369-3.93372,0-2.43078,1.40681-4.00727,3.58682-4.00727,2.14407,0,3.49236,1.47284,3.49236,3.82254v.545H93.20138v.09613a2.01837,2.01837,0,0,0,2.08472,2.19506,1.75329,1.75329,0,0,0,1.78965-1.09Zm-5.3773-2.31292h3.87437a1.86367,1.86367,0,0,0-1.90082-1.96686A1.96188,1.96188,0,0,0,93.2089,14.83853Z" style="fill: #fff"/>
</g>
<g>
<path d="M34.42873,24.04h7.46143v.78027H38.34328v1.10352q.76245.353,1.53467.77832.772.4248,1.562.91211l-.45947.7168q-.813-.51855-1.46094-.91406-.64819-.39551-1.17627-.65723v4.6084h-.79053V24.82031h-3.124Z" style="fill: #fff"/>
<path d="M43.95607,24.27441h1.562v-.62988h.772v.62988h1.44727v.7168H46.29006v.61035h1.82422q-.01831-.44971-.03662-.93945-.019-.48926-.01855-1.02637h.76709q0,.54639.01611,1.03125t.03467.93457h2.03076v.72656H48.91359q.03662.50537.09424.92285c.03809.2793.07275.52832.10352.749a1.65659,1.65659,0,0,1,.05518.22363c.01221.07129.02441.14453.03662.21777A8.07449,8.07449,0,0,0,49.543,27.582q.15161-.4541.27588-.97363l.70313.32129a9.7383,9.7383,0,0,1-1.00635,2.57813c.02441.06055.05127.123.08057.18555q.04321.09375.08936.18164.28931.57422.45947.57422.115,0,.2666-1.208l.67529.3584q-.28931,1.67725-.85449,1.67676-.42773,0-.96973-.75391a2.1332,2.1332,0,0,1-.188-.3584,5.12,5.12,0,0,1-1.40186,1.2041l-.436-.62891a4.19187,4.19187,0,0,0,1.48828-1.40234q-.08276-.271-.16748-.55762-.085-.28711-.145-.59961a12.605,12.605,0,0,1-.26172-1.85156H43.51955v-.72656h1.99854v-.61035h-1.562Zm3.78125,5.28809V30.252q-.30322.03662-.61328.06836c-.20654.02148-.416.04492-.627.06934v.95117h-.772v-.85449q-.43652.03662-.88721.06836c-.2998.02148-.605.0498-.91406.083l-.10107-.74414q.50977-.019.97852-.05273.46875-.03516.92383-.0625V29.1582H44.19045l-.16113-.62988a5.21327,5.21327,0,0,0,.55615-.88672h-.80859v-.70312h1.11621a1.73325,1.73325,0,0,0,.09668-.26855q.03662-.13477.0918-.28711l.75342.13379c-.03027.085-.05469.16113-.07324.22754a.99246.99246,0,0,1-.07812.19434h2.09033v.70313H45.40334q-.12378.249-.25049.46t-.23682.40918h.80908V27.9541h.772v.55664h1.19434v.64746H46.49709v.53711c.21094-.01758.42041-.03906.627-.06152Q47.43386,29.59863,47.73732,29.5625Zm1.34619-5.459.5835-.39453a12.55594,12.55594,0,0,1,.979,1.23633l-.59277.42188q-.17944-.28418-.42041-.60352Q49.39113,24.44434,49.08352,24.10352Z" style="fill: #fff"/>
</g>
<g id="_Group_" data-name="&lt;Group&gt;">
<g id="_Group_2" data-name="&lt;Group&gt;">
<path id="_Path_" data-name="&lt;Path&gt;" d="M24.76888,20.30068a4.94881,4.94881,0,0,1,2.35656-4.15206,5.06566,5.06566,0,0,0-3.99116-2.15768c-1.67924-.17626-3.30719,1.00483-4.1629,1.00483-.87227,0-2.18977-.98733-3.6085-.95814a5.31529,5.31529,0,0,0-4.47292,2.72787c-1.934,3.34842-.49141,8.26947,1.3612,10.97608.9269,1.32535,2.01018,2.8058,3.42763,2.7533,1.38706-.05753,1.9051-.88448,3.5794-.88448,1.65876,0,2.14479.88448,3.591.8511,1.48838-.02416,2.42613-1.33124,3.32051-2.66914a10.962,10.962,0,0,0,1.51842-3.09251A4.78205,4.78205,0,0,1,24.76888,20.30068Z" style="fill: #fff"/>
<path id="_Path_2" data-name="&lt;Path&gt;" d="M22.03725,12.21089a4.87248,4.87248,0,0,0,1.11452-3.49062,4.95746,4.95746,0,0,0-3.20758,1.65961,4.63634,4.63634,0,0,0-1.14371,3.36139A4.09905,4.09905,0,0,0,22.03725,12.21089Z" style="fill: #fff"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 8.4 KiB

-385
View File
@@ -1,385 +0,0 @@
# iOS PiP 语音会话保活规划
> **文档状态**:产品与架构规划(待验证,未进入实现)
> **适用范围**iOS 主 App + 键盘扩展 + Live Activity
> **目标版本**:待产品验证后确定
> **创建日期**2026-07-26
---
## 1. Executive Summary
### 1.1 目标
在不要求 OSGKeyboard 长时间占用麦克风的前提下,尽量保持主 App 可响应键盘扩展的听写指令,降低用户在宿主 App 与 OSGKeyboard 之间反复切换的频率。
核心方向是将两个当前耦合的能力拆开:
1. **会话可用性**:主 App 仍可接收键盘命令。
2. **麦克风采集**:仅在用户明确开始听写时启用,完成后立即释放。
PiP(画中画)只承担系统可见的多任务会话载体,不绕过麦克风授权,也不应使用静音音频循环伪造后台活动。
### 1.2 核心结论
| 决策 | 规划选择 |
|------|----------|
| 产品定位 | 将 PiP 作为可选的「免切换模式」,不替代普通 Flow |
| 麦克风策略 | PiP 空闲时关闭;键盘点按听写后按需激活 |
| 默认策略 | 保留当前隐私友好的 5 分钟 Flow;PiP 由用户主动开启 |
| 降级路径 | PiP 不可用或失效时回落到现有 `startflow` 冷启动流程 |
| 状态展示 | PiP 显示有意义的语音会话状态;Live Activity 继续负责锁屏与灵动岛 |
| 禁止方案 | 不播放静音文件保活,不使用定位或 VoIP 等无关后台模式 |
| 上线方式 | 先做真机技术验证和 TestFlight 审核验证,再决定正式产品化 |
### 1.3 非目标
- 不让键盘扩展直接访问麦克风;这是 iOS 平台限制。
- 不承诺 App 被用户强制退出后仍可免切换听写。
- 不承诺电话、Siri、相机或其他录音 App 抢占音频设备时继续录音。
- 不用 PiP 绕过麦克风权限、隐私提示或系统音频策略。
- 第一阶段不重写 ASR、润色、App Group 或 Darwin 通知管线。
---
## 2. 问题定义
### 2.1 平台约束
iOS 键盘扩展无法直接申请或使用麦克风。系统级语音键盘因此必须采用:
```text
键盘扩展
→ 发送开始/停止命令
→ 主 App 采集并转写
→ App Group 返回结果
→ 键盘插入文本
```
当主 App 被系统挂起或终止时,键盘无法即时启动录音,只能打开主 App 重新建立会话。当前 Flow 通过持续运行 `AVAudioEngine` 输入链路换取后台可用性,但会带来麦克风长期占用、橙色隐私指示、电量消耗和音频冲突。
### 2.2 用户问题
| 用户感知 | 当前根因 | 目标变化 |
|----------|----------|----------|
| 频繁跳转主 App | 后台主进程不可响应 | PiP 有效时直接响应键盘命令 |
| 麦克风指示长时间亮起 | Flow 会话级连续采集 | 空闲时释放麦克风 |
| 耗电或发热 | 音频引擎持续采样和处理 | 仅听写期间采样 |
| 其他 App 无法使用麦克风 | OSGKeyboard 持有输入设备 | 听写结束后主动释放 |
| 不知道会话是否可用 | Flow、麦克风和进程状态混为一体 | 分开展示「免切换已就绪」和「正在录音」 |
### 2.3 成功定义
PiP 模式下,用户应能:
1. 在 OSGKeyboard 主 App 中主动开启免切换模式。
2. 将 PiP 小窗收纳到屏幕边缘。
3. 回到微信、邮件等宿主 App。
4. 点击键盘麦克风后直接开始听写。
5. 停止听写后收到文本,同时麦克风在短时间内释放。
6. PiP 失效时收到明确提示,并能通过现有冷启动路径恢复。
---
## 3. 竞品与行业模式
### 3.1 Typeless
Typeless iOS 1.9.0 将该能力命名为 Picture in picture / Skip app switching
- 用户先在主 App 中主动开启。
- PiP 可拖到屏幕边缘收纳。
- 用户在其他 App 的 Typeless 键盘中开始说话。
- 官方产品说明强调麦克风空闲时关闭,以降低电量消耗。
其公开资料无法证明具体内部实现,因此本规划只借鉴产品模型,不假定其私有代码结构。
### 3.2 Wispr Flow、TypeWhisper 与同类开源项目
常见架构是主 App 持有 `AVAudioEngine`,键盘通过 App Group 与 Darwin 通知控制句子开始和停止。优点是首字延迟低,缺点是会话期间通常持续占用音频输入。
OSGKeyboard 当前 Flow 已属于此模式,并已具备:
- 主 App 会话所有权;
- 键盘与主 App IPC
- 连续采集与 utterance gate
- App Group 结果回传;
- Live Activity
- 冷启动与恢复流程。
因此 PiP 应作为会话生命周期的新载体,而不是重建整条语音管线。
### 3.3 SuperWhisper / App Intents 路线
更保守的方案是不做长期后台保活,使用 App Intents、Action Button、快捷指令或显式 App 切换启动录音。该方案最符合系统预期,但无法完全满足键盘内即时听写。
OSGKeyboard 应保留这类入口作为稳定降级,而不是依赖 PiP 达到 100% 可用。
### 3.4 合规边界
以下方式不应采用:
- 循环播放静音音频以防止挂起;
- 声明与产品无关的定位、VoIP 后台能力;
- 使用不可见或无实际产品意义的伪视频,仅为延长进程生命;
- 在用户未明确开启会话时自动恢复麦克风。
PiP 内容需要能被解释为真实的语音会话控制面,例如展示:
- 「免切换已就绪」;
- 「正在聆听」及音量反馈;
- 「正在转写」;
- 暂停、结束或返回 App 操作。
---
## 4. 目标产品模型
### 4.1 三层可用性
```text
层级 0:冷启动
主 App 不可用
→ 键盘打开 startflow
→ 主 App 建立语音会话
层级 1:短时 Flow
AVAudioEngine 会话保持
→ 最低首字延迟
→ 默认 5 分钟无活动后结束
层级 2PiP 免切换模式
PiP 保持用户可见的多任务会话
→ 空闲时麦克风关闭
→ 键盘命令触发按需开麦
```
三个层级必须共用同一份 `FlowSessionBridge` 状态合约,键盘不应根据实现细节分别写三套逻辑。
### 4.2 用户入口
建议在首页提供独立状态卡,而不是继续扩张设置开关:
- 未开启:`开启免切换模式`
- 启动中:`正在准备画中画`
- 已就绪:`免切换已就绪 · 麦克风未使用`
- 录音中:`正在聆听`
- 失效:`会话已断开,点击恢复`
首次开启时应明确说明:
1. 屏幕上会出现可收纳的 PiP 小窗。
2. 空闲时不会使用麦克风。
3. 用户关闭 PiP、强制退出 App 或系统回收进程后,需要重新开启。
### 4.3 键盘状态
键盘麦克风状态应从「主 App 是否活着」升级为明确能力状态:
| 状态 | 表现 | 点击结果 |
|------|------|----------|
| 不可用 | 灰色 | 引导权限或 Full Access |
| 需恢复 | 橙色 | 打开主 App 恢复会话 |
| PiP 就绪、麦克风关闭 | 绿色 | 请求主 App 按需开麦 |
| 正在激活麦克风 | 绿色加载态 | 等待真实音频 proof |
| 正在录音 | 红色/波形 | 发送停止命令 |
| 正在转写 | 处理中 | 等待结果 |
---
## 5. 目标架构
### 5.1 组件边界
```text
Keyboard Extension
└─ FlowSessionBridge / Darwin command
Host App
├─ VoiceSessionCoordinator
│ ├─ FlowSessionManager
│ ├─ PiPVoiceSessionController
│ └─ AudioCaptureLifecycle
├─ FlowContinuousCapture
├─ ASR + Polish pipeline
└─ Live Activity
```
规划职责:
- `PiPVoiceSessionController`:只管理 PiP 生命周期和展示状态。
- `AudioCaptureLifecycle`:管理按需激活、音频 proof、停止及释放。
- `FlowSessionManager`:继续负责命令、ASR、润色和结果回传。
- `FlowSessionBridge`:发布跨进程能力快照,不让键盘猜测主 App 状态。
### 5.2 状态机
```text
inactive
→ preparingPiP
→ pipReadyMicOff
→ activatingMic
→ recording
→ processing
→ releasingMic
→ pipReadyMicOff
任意状态
→ interrupted
→ recovering 或 inactive
```
重要不变量:
1. `pipReadyMicOff` 必须确认音频输入已停止并释放。
2. 键盘只有在收到 `recording` 和真实 audio proof 后才显示正在录音。
3. PiP 存活不能等价于麦克风可用。
4. 电话/Siri 中断后不得静默恢复录音。
5. 任何超时都要回收麦克风并写入明确错误。
### 5.3 PiP 内容方案
技术验证阶段应比较两类 Apple 官方能力:
1. 基于 `AVPlayerLayer` 的媒体 PiP
2. 基于 `AVSampleBufferDisplayLayer` / 视频通话内容源的实时 PiP。
选择标准不是「哪种最容易保活」,而是:
- 是否符合 OSGKeyboard 的真实产品用途;
- 能否展示动态语音会话状态;
- 麦克风激活/释放是否稳定;
- 收纳、锁屏、音频中断行为是否可预测;
- App Review 是否能清楚理解其用途。
在完成真机和审核验证前,不冻结具体 AVKit 实现。
---
## 6. 实施阶段
### Phase 0:技术与审核可行性验证
目标:证明「PiP 存活 + 闲时关麦 + 键盘触发按需开麦」在目标 iOS 版本可行。
验证项:
- PiP 启动、收纳、恢复与关闭;
- 空闲 30 分钟后主 App 是否仍能响应;
- 空闲期间系统麦克风指示是否消失;
- 键盘命令到首个有效音频帧的延迟;
- 连续 20 次开始/停止是否稳定;
- 电话、Siri、蓝牙切换、锁屏、低电量模式;
- 用户关闭 PiP 后的降级行为;
- TestFlight / App Review 说明是否被接受。
退出标准:
- 空闲时没有麦克风占用;
- P95 命令到有效音频帧小于 1 秒;
- 20 次连续听写无僵尸录音或失联状态;
- 失败后都能回到冷启动路径;
- 没有使用静音循环或无关后台能力。
### Phase 1:内部可用版本
- 新增 PiP 会话控制器;
- 将持续采集改造成可重复激活/释放;
- 扩展跨进程状态快照;
- 键盘增加激活中、PiP 就绪和失效状态;
- 复用现有 ASR、润色、结果回传和 Live Activity
- 添加状态机与 IPC 单元测试。
### Phase 2:产品化
- 首页免切换状态卡;
- 首次开启说明与 PiP 收纳引导;
- 中英文文案与隐私说明;
- 诊断页增加 PiP、音频会话和最近中断原因;
- 增加遥测指标,但不采集音频内容。
### Phase 3:灰度与决策
- TestFlight 小流量开启;
- 比较 PiP 与普通 Flow 的成功率、首字延迟和耗电;
- 根据审核反馈决定默认入口和长期支持范围;
- 若 PiP 不稳定或审核风险不可接受,保留为实验功能或停止上线。
---
## 7. 测试矩阵
### 7.1 功能场景
| 场景 | 预期 |
|------|------|
| PiP 空闲 | 主 App 可响应,麦克风未占用 |
| 键盘开始听写 | 按需激活并获得真实音频帧 |
| 停止听写 | 完成转写并及时释放麦克风 |
| 连续多句 | 每句均重新激活成功,无第二句无音频 |
| PiP 被关闭 | 键盘切为需恢复,不显示假就绪 |
| App 被强退 | 清除旧 generation 和僵尸状态 |
| 电话/Siri 中断 | 当前句失败并提示,不自动偷录 |
| 蓝牙设备变化 | 音频格式重建,不崩溃 |
| 网络失败 | 本地 ASR 保留;润色按现有策略降级 |
### 7.2 设备与系统
- 最低支持 iOS 版本、当前稳定版和最新 beta;
- 刘海机、灵动岛机型、iPad
- AirPods、普通蓝牙耳机、车载音频、有线设备;
- 微信、信息、邮件、Slack、Notes 及自定义文本输入控件;
- 锁屏、横竖屏、多窗口、低电量和后台刷新关闭状态。
---
## 8. 指标与验收
### 8.1 核心指标
| 指标 | 定义 | 目标 |
|------|------|------|
| 免切换成功率 | PiP 就绪时无需打开主 App完成听写 | ≥ 98% |
| 麦克风空闲占用 | 非录音期间仍占麦的时长比例 | 接近 0 |
| 首帧延迟 P95 | 键盘点击到真实音频 proof | < 1 秒 |
| 结果回传成功率 | 停止后键盘收到最终结果 | ≥ 99% |
| 僵尸状态率 | 键盘显示可用但主 App无法响应 | < 0.5% |
| 恢复成功率 | 失效后通过冷启动恢复 | ≥ 99% |
### 8.2 观察指标
- 每日 PiP 开启人数与启用留存;
- PiP 被用户主动关闭的比例;
- 每小时耗电和温升相对普通 Flow 的变化;
- 音频中断类型分布;
- 用户因橙色麦克风指示或隐私产生的反馈;
- App Review 反馈和政策变化。
---
## 9. 风险与应对
| 风险 | 等级 | 应对 |
|------|------|------|
| PiP 被认定与媒体用途不匹配 | 高 | 提供真实会话 UI、明确审核说明;先 TestFlight/审核验证 |
| 系统版本改变 PiP 行为 | 高 | 保持冷启动降级;按系统版本做兼容验证 |
| 按需开麦首字丢失 | 中 | 激活态 + audio proof + 预录缓冲,不提前向键盘宣告录音 |
| 频繁激活导致音频路由异常 | 中 | 串行状态机、格式重建、媒体服务重置恢复 |
| PiP 与 Live Activity 状态冲突 | 中 | 单一 coordinator 发布状态,两个 UI 只消费 |
| 用户误解 PiP 仍在监听 | 中 | 空闲状态明确写「麦克风未使用」,隐私说明可验证 |
| 其他 App 抢占麦克风 | 中 | 显式中断提示,不承诺并发录音 |
---
## 10. 产品决策门
进入代码实现前,需要确认:
1. 是否接受 PiP 作为用户主动开启、系统可见且可收纳的产品形态;
2. 是否优先「空闲关麦」而接受约数百毫秒的重新激活延迟;
3. PiP 中展示哪些真实功能,确保它不是纯保活黑窗;
4. 是否将现有「跳过 App 切换」重命名并拆为普通 Flow / PiP 两种模式;
5. 最低支持系统和目标测试设备;
6. 技术验证失败或审核风险过高时,是否接受回退到短 Flow + App Intents。
在上述决策和 Phase 0 证据完成前,不建议直接进入正式实现。
-350
View File
@@ -1,350 +0,0 @@
# 打字键盘输入准确率提升计划
> **文档状态**:产品与工程规划(**实现中**:Phase 1~4 已落地,待真机试打)
> **适用范围**iOS 键盘扩展打字面(`OSGKeyboardExt` 打字键网格)+ 共享布局度量(`OSGKeyboardShared`
> **关联基线**`docs/TYPING_KEYBOARD_BASELINE.md`
> **实现分支**`feat/keyboard-touch-accuracy`
> **创建日期**2026-08-03
> **目标版本**:待 Phase 0 基线试打后确定发布节奏
---
## 1. Executive Summary
### 1.1 目标
在拼音打字主路径上,将点按准确率与按键契约提升到**接近系统键盘的日常体感**:
- 漏触(按了没反应)接近 0
- 邻键误触明显下降
- 「不准」不再成为用户槽点
**成功定义(产品语言)**:同设备、同姿势对比系统键盘时,内部试打主观分接近;键缝无空响应;字母键支持预览与滑动改选。
### 1.2 核心结论(冻结)
| 决策 | 选择 |
|------|------|
| 准确率策略 | 对齐系统**触控契约 + 分层纠错**,不复刻私有算法 |
| 字母 / 数字 / 符号 | 按下高亮 → 可滑动改选 → **松手确认** |
| 删除 | **按下即删** + 长按连删(与现状及系统一致) |
| 空格 / 回车 | 与字母一致:**松手确认** |
| 键缝 | 视觉保留间距;触控**不允许无响应**(最近键中心) |
| 滑出键平面再松手 | **取消本次输入**(不出字) |
| 视觉 | 键位外观不因热区/智能变形(热区隐形) |
| 智能边界 | 只微调;用户明确落点或滑动可覆盖 |
| 语言策略 | 中文拼音过程可偏心;英文先求稳 |
| 反馈最低标准 | Phase 1~2:**高亮 + 按键音**;气泡为加分项 |
| 实施顺序 | 无死区 → 按键契约 → 触点微调 → 拼音偏心 → 气泡等 |
### 1.3 非目标(本期不做)
- 绝对准确率 100% 追平系统键盘
- QuickPath / 滑行输入
- 依赖 `UIKeyboardLayoutStar` 等系统私有 hit API
- 完整双 strike zone 个性化、键盘整体漂移重定位(re-spotting
- 英文词级 predictive hit 放大(后置评估)
- 重写 Rime / 抛光引擎(触控层与词级兜底分工,不互相替代)
### 1.4 现实预期
| 目标 | 是否现实 |
|------|----------|
| 漏触接近系统 | ✅ Phase 1~2 可达 |
| 邻键误触主观接近系统 | ✅ Phase 2~4 可达 |
| 中文拼音连打体感不输系统 | ✅ 相对最有机会(拼音偏心) |
| 全场景绝对准确率 = 系统 | ❌ 不作为本期 KPI |
---
## 2. 背景与现状差距
### 2.1 问题定义(第一性原理)
用户打字时只关心两件事:
1. **我以为我点了谁**(视觉反馈)
2. **机器最后认了谁**(实际输入)
系统键盘准,是因为整条链路在缩小二者差距,并在不一致时提供改选与词级兜底。OSG 当前布局已对齐系统键位,但触控模型仍接近「普通按钮网格」。
### 2.2 系统键盘能力分层(参考模型)
系统做法是多层流水线(专利 + 私有头文件命名 + 可观察行为),不是单一「热区 +N pt」:
| 层 | 含义 | 本期态度 |
|----|------|----------|
| ① 触点估计 | 触点中心 ≠ 意图点,可做偏移/加权 | Phase 3 轻量做 |
| ② 几何热区 | 视觉 ≠ hit;缝可命中 | Phase 1 必做 |
| ③ 动态偏心 | 下一键更可能则更好点 | Phase 4 拼音合法优先 |
| ④ 触摸生命周期 | Down 反馈 / Move 改选 / Up 提交 | Phase 2 必做 |
| ⑤ 个性化双区 | 按节奏/习惯自适应 | 后置 |
| ⑥ 词级兜底 | 自动更正 / 候选 | 已有 Rime + 抛光,需保住 |
### 2.3 现状(代码事实)
| 区域 | 现状 | 影响 |
|------|------|------|
| 字符键 | SwiftUI `Button`,视觉 frame ≈ hit | 键间 6~8pt 为死区;松手且需仍在键内 |
| 删除 / 部分工具栏 | `DragGesture(minimumDistance: 0)` 按下即响 | 已接近系统删除契约 |
| 主动扩大 hit | 基本仅候选栏展开按钮(34 视觉 / 44 hit | 字母区无 fat-finger 扩展 |
| 滑动改选 | 无行级/网格级跟踪 | 邻键误触难在松手前纠正 |
| 点击音 | 字符键未统一走 `KeyboardSoundFeedback` | 反馈弱于系统 |
| 词级 | Rime 候选 + 抛光 | 后段较强;前段触控不稳会浪费兜底 |
主要相关位置:
- `OSGKeyboardExt/Typing/TypingRootView.swift`(键网格、间距、`keyButton`
- `OSGKeyboardExt/Views/ToolbarActionButtons.swift`(按下即响模板)
- `OSGKeyboardExt/Utilities/KeyboardSoundFeedback.swift`
- `OSGKeyboardShared/Typing/TypingLayoutProviding.swift`(键位布局)
### 2.4 用户失败模式
| 体感 | 主因 | 主要靠哪一层治 |
|------|------|----------------|
| 点到缝没反应 | 间距死区 | Phase 1 |
| 经常打成邻键 | 无改选窗口 + 无触点/上下文加权 | Phase 24 |
| 滑出键外丢输入 | per-key Button 生命周期 | Phase 2 |
---
## 3. 产品宪法(全程遵守)
1. **字母 / 数字 / 符号**:按下高亮 → 可滑动改选 → **松手确认**
2. **删除****按下即删**,长按连删
3. **键平面内不允许无响应**(缝必命中最近键中心)
4. **视觉键位不因智能变形**(热区与偏心均隐形)
5. **智能只微调**;用户滑动或明确落点可覆盖
6. **中文拼音过程可以更聪明;英文先求稳**
对内沟通口径(Phase 2):
> 不是变慢,是把确认点从按下挪到松开,换来改选能力——和系统键盘一样。
---
## 4. 分阶段计划
### Phase 0 — 基线与验收设计
**目的**:能量化「准不准」,避免凭感觉争论。
| 事项 | 说明 |
|------|------|
| 记录现状问题 | 漏触 / 邻键错 / 滑出丢键的典型场景 |
| 定试打脚本 | 至少 3 类:缝点击、邻键偏触、常见拼音连打(如 `zhongguo``pingguo` |
| 定对比方式 | 同设备、同姿势;系统键盘 vs OSG;主观 1~5 分 + 错字/漏触次数 |
| 定反馈最低标准 | Phase 1~2:高亮 + 按键音;气泡单列加分 |
**工期**0.51 天
**产出**:试打清单 + 打分表(可放飞书/Notion;关键结论可回写本文「附录」)
**出口**:脚本固定、打分表可用,再进入开发。
---
### Phase 1 — 无死区热区(P0
**产品一句话**:缝也算键,别点空。
**范围**
- 字母 / 数字 / 符号区:视觉间距保留,触控无空洞
- 缝内命中:**最近键中心**
- 边缘键(如 Q/P、A/L)外侧略扩,降低边缘难点
- 不改变键的视觉大小与间距观感
**工期**:可与 Phase 2 合并开发,但**验收分开**
**建议**:约短迭代内完成几何命中;若与 Phase 2 同 PR,须分 commit 或分验收项
**验收**
- [ ] 故意点键缝 20 次,0 次无响应
- [ ] 视觉观感与改前一致
- [ ] 无明显「看在 A 上却出 S」的跳键
---
### Phase 2 — 系统同款按键契约(P0)
**产品一句话**:先告诉你按了谁,松手再算数。
**范围**
| 键类 | 行为 |
|------|------|
| 字母 / 数字 / 符号 | Down 高亮 → Move 改选 → Up 提交 |
| 删除 | Down 即删 + 长按连删(保持) |
| 空格 / 回车 | 松手确认 |
| 滑出键平面再松手 | 取消本次输入 |
| 反馈 | 高亮 + `KeyboardSoundFeedback.keyClick()` |
**本阶段不做**:按键气泡、长按附加符号、滑行输入。
**工期**:约 1 个迭代(含内部试打调参)
**验收**
- [ ] 按下 F 滑到 G 松开 → 只出 G,高亮跟随
- [ ] 按下后滑出键盘再松 → 不出字
- [ ] 删除仍为按下即删、可连删
- [ ] 内部试打:「手感接近系统」主观分显著高于改前
**工程注意(规划级)**
- 宜收敛为网格/行级触摸跟踪,而非继续堆 per-key `Button`
- 可复用 `RepeatingPressButton` 与现有 UIKit 触控跟踪经验,但字母键生命周期以 Up 确认为准
- 需单列与候选栏、Shift、123 切换的手势边界用例
---
### Phase 3 — 触点微调(P1
**产品一句话**:按手指意图认键,不只认触点中心。
**范围**
- 对命中点做轻微、稳定的向上偏移(或按触点大小微调)
- 规则保持可预期;偏移量建议可配置便于试打
- 中英文均可生效(与语言无关的几何修正)
**工期**0.51 天(可挂在 Phase 2 尾声)
**验收**
- [ ] 拇指自然打字时,偏下误触主观减少
- [ ] 不引入新的「明显认错键」投诉
- [ ] 偏移可调,便于内测收敛
---
### Phase 4 — 拼音下一键偏心(P1)
**产品一句话**:拼到一半时,更可能的下一家更好点。
**范围(档位 1:合法字母优先)**
- 仅拼音组词过程中启用
- 依据当前拼音/Rime 状态:合法后续字母热区略增,非法略减
- **偏心有上限**(微调,不霸道)
- 用户滑动改选 / 明确落点可覆盖
- 英文模式:关闭或明显减弱
- 上屏或开始新音节:偏心状态重置
**后置**:英文词预测式放大、重度概率模型、个性化学习。
**工期**:约 1 个迭代
**依赖**:必须在 Phase 2 稳定之后(无改选窗口时,偏心误判会被感知为「自作主张」)
**验收**
- [ ] 常见拼音连打,邻键误触下降
- [ ] 故意打冷门 / 非法后续字母,仍能打上
- [ ] 视觉上看不出键变大;体感更顺
- [ ] 可用试打脚本对比「偏心开 / 关」
---
### Phase 5 — 体验加分(P2,不阻塞主目标)
| 项 | 说明 | 优先级 |
|----|------|--------|
| 按键气泡预览 | 强化所见即所得 | 高(体验) |
| 候选 / 抛光联动检查 | 触控改善后词级兜底不回退 | 高(质量) |
| 长按附加符号 | 系统能力对齐,非准确率核心 | 中 |
| 个性化热区 | 长期数据,后置 | 低 |
---
## 5. 推荐排期
```text
Week 0 Phase 0 基线试打脚本 + 打分
Week 1 Phase 1+2 无死区 + 松手确认/滑动改选(可合并开发,分开验收)
Week 1 末 Phase 3 触点微调
Week 2 内部试打 / 调参 / 边界修复(滑出取消、宽键、Shift、候选冲突)
Week 3 Phase 4 拼音合法下一键偏心
Week 34 Phase 5 气泡等加分项(可选)
```
**底线**Week 1 必须交出 Phase 1 + 2;没有这两项,不宣称「接近系统准确率」。
---
## 6. 范围边界
| 做 | 不做(本项目) |
|----|----------------|
| 点按准确率与系统同款契约 | QuickPath |
| 拼音过程热区偏心(合法优先) | 系统私有 hit API |
| 高亮 + 按键音 | 完整自动更正引擎重写 |
| 与现有 Rime / 候选协同 | 键盘整体自动平移 / 缩放 |
| 可配置调参(偏移、偏心强度) | 为准确率牺牲视觉键位一致性 |
---
## 7. 风险与对策
| 风险 | 对策 |
|------|------|
| 「松手确认」被觉得变钝 | 对内对齐系统口径;用试打错字率对比说话 |
| 热区重叠导致跳键 | 最近中心 + 偏心上限;内测可关偏心 |
| 拼音偏心误伤冷门输入 | 合法优先 + 可覆盖;可选调试开关 |
| 与删除 / 空格 / 候选手势冲突 | Phase 2 单列边界用例;滑出取消 |
| 扩展内难自动化 UI 测 | Phase 0 人工脚本为主;命中几何/偏心纯逻辑补单测 |
| 改动面集中在打字面 | 语音面工具栏契约保持;避免一次改两套手感却无验收 |
---
## 8. 角色与出口标准
| 角色 | 职责 |
|------|------|
| 产品 | 守宪法、主观验收、决定气泡是否进本期 |
| 设计 | 高亮态;(可选)气泡视觉 |
| 工程 | Phase 1~4 实现;关键常量可配置 |
| 内测 | 每 Phase 用同一试打脚本回归 |
**每阶段出口**:试打清单通过 + 无 P0「无响应 / 乱出字」再进入下一阶段。
---
## 9. 与版本发布的关系
- 本能力属用户可感知的打字体验提升,合并 `main` 并准备发布时:
- 按 `AGENTS.md` 用 Conventional Commits 评估版本 bump(预期含 `feat`**MINOR**
- `CHANGELOG.md``[Unreleased]` 下补双语条目(实现时再写,不在本文提前编造版本号)
- 功能分支开发期间**不**提前 bump `project.yml` 版本号
---
## 10. 一句话执行顺序
> **先量基线 → 无死区 + 系统按键契约 → 触点微调 → 拼音偏心 → 气泡等加分。**
---
## 附录 A. 试打脚本草案(Phase 0 细化)
| 编号 | 场景 | 操作 | 记录 |
|------|------|------|------|
| T1 | 键缝 | 在同行相邻键缝点击 20 次 | 无响应次数 |
| T2 | 邻键偏触 | 瞄准某键但故意偏向邻键 20 次 | 错键次数;有/无滑动改选 |
| T3 | 滑动改选 | 按下 A 滑到 S 松开(Phase 2+ | 是否只出 S |
| T4 | 滑出取消 | 按下后滑出键盘松开(Phase 2+) | 是否不出字 |
| T5 | 删除契约 | 点按 / 长按删除 | 是否按下即删、可连删 |
| T6 | 拼音连打 | `zhongguo` / `pingguo` 等各 5 遍 | 错字次数、主观 1~5 |
| T7 | 偏心开关 | Phase 4 后 T6 对比开/关 | 错字差、误伤冷门键与否 |
对比时尽量:同一 iPhone、竖屏、单手拇指、系统键盘与 OSG 各跑一轮。
---
## 附录 B. 修订记录
| 日期 | 说明 |
|------|------|
| 2026-08-03 | 初稿:基于系统键盘分层分析与产品讨论冻结 Phase 0~5 |
| 2026-08-05 | 开分支 `feat/keyboard-touch-accuracy`;落地 Phase 1(无死区)+ Phase 2(网格 Down/Move/Up+ Phase 3(触点上偏);纯逻辑见 `KeyHitTesting` / `TypingKeyLayout`,触控见 `TypingKeyTouchPad` |
| 2026-08-05 | Phase 4:全拼合法下一键偏心(`PinyinNextKeyResolver` + `rawInput`);双拼/英文保持中性;歧义最近键加权,单键明确命中不受偏置 |
-81
View File
@@ -1,81 +0,0 @@
# Keyboard memory budget — acceptance (OSGDiag)
Phase 0 / 2 memory work is validated on a **physical iPhone** with Console
filtering for `OSGDiag`. Unit tests and `xcodebuild` cover compile-time
wiring; jetsam behavior is device-only.
## Hybrid Flow (product default)
- Foreground host is **light by default**: orphan Live Activity cleanup only —
**no** auto `startSession` / continuous capture on appear.
- Capture starts only on explicit Start / `osgkeyboard://startflow` / mic press.
- Idle background capture is stopped so a parked host does not jetsam the keyboard.
- **Do not** stack CLM + Rime + ASR in the same second after onboarding.
- ASR warmup runs on **first mic press** (`beginUtterance`), gated by
`HostMemoryBudget` (~260 MB RSS). `hostHeavy` is set only while heavy work
actually runs, then cleared. A sticky `hostHeavy` (host died mid-work)
expires after `hostHeavyMaxAge` (~120 s) and is cleared on
`clearFlowState` / host-launch reconciliation so typing 中文/EN is not
permanently blocked.
## Console checklist
Filter Console by process separately: host `OSGKeyboard` vs extension
`com.osgkeyboard.ios.keyboard`. Host-only filter will never show `KVC.*`.
| Scenario | Expect |
|----------|--------|
| **Force-quit host**, open Notes, switch to OSG | First `dyld.constructor`, then `KVC.init``viewDidLoad``viewDidAppear` |
| Host foreground right after launch | `skip capture` + `postOnboardingWarmup scheduled … delay=45s` (no immediate Rime/CLM) |
| ~45 s later, host still active | Serial `rime.installIfNeeded` then `clm.prepare` |
| First mic press | `scheduleASRWarmup`; `hostHeavy` only while work runs |
| Switch to typing | Single `rime.prepare` / `englishPrepare` |
If neither `dyld.constructor` nor `KVC.init` appears after force-quitting the host,
the extension is dying in dyld (Shared≈9.4 MB + librime) — next lever is splitting
Rime out of Shared.
## RSS comparison (optional)
Record `OSGDiag` `rss=` tags for:
1. Cold voice surface only
2. Cold typing after prepare
3. Host foreground + extension
Target: typing peak below the old “voice + eager Librime construct” baseline.
## Extension physical-footprint budget
`phys_footprint` is the primary extension metric because device jetsam follows
it more closely than RSS. `KeyboardExtensionMemoryTelemetry` records structured
`[OSGDiag/memory] extMemory` lines at lifecycle and heavy-resource milestones,
plus 50 ms samples during the first four seconds:
- **Normal:** below 36 MiB
- **Warning:** 3640 MiB
- **High:** 4048 MiB; 40 MiB is the internal safe peak
- **Critical:** 48 MiB or above
The approximate 60 MiB device boundary is not a public Apple contract. The
40 MiB target deliberately reserves room for transient SwiftUI, Rime, and
system-framework pages. Telemetry is observation-only: crossing a band logs
`crossed=1` but does not change the selected surface or unload resources.
Each record includes the current and peak footprint, delta from process start,
elapsed startup time, surface/language, Full Access, clipboard state, and
operation-specific context. Filter Console by `OSGDiag/memory` and compare:
1. `KVC.viewDidLoad.afterInstallServices`
2. `KVC.viewDidLoad.afterInstallSwiftUI`
3. `typing.englishPrepare.done`
4. `typing.rimePrepare.done`
5. `clipboard.reload.done`
6. `KVC.viewDidAppear.done`
## Structural split
- Extension links **OSGKeyboardShared** only (no Charts / StoreKit / Speech / HostSupport).
- Host embeds **OSGKeyboardHostSupport** (ASR, CLM, CloudASR, tip/charts UI).
- Heavy assets (`osg_pinyin.dict.yaml`, CLM bin, licenses, local-asr catalog)
ship in the **host app** bundle; extension reads Rime from App Group after deploy.
-259
View File
@@ -1,259 +0,0 @@
# OSGKeyboard 本地 ASR 技术架构
> **文档状态**:1.7.0 代码事实 + 后续评测方向
> **适用范围**macOS 15+ 本地听写;iOS 26+ 使用 Apple SpeechAnalyzer。
> **当前结论**Mac 默认安装目录模型为
> `qwen3-mlx-0.6b-4bit`,通过 `mlx-audio-swift` 做 Qwen3 MLX 真流式识别;
> 模型不可用时走 Apple Speech fallback。Sherpa 已不在当前 catalog 或运行路径中。
## 1. 架构摘要
本地 ASR 的专有名词质量由三层共同完成:
1. **ASR bias**`PersonalDictionary` 与内置技术词经
`LocalASRBiasAdapter` 生成 Qwen3 `promptBias`
2. **确定性纠错**:识别后按个人词库 `aliases → term` 做边界受控替换。
3. **Polish 保真**:把内置词参考作为补充上下文交给润色层;无 API Key 或润色失败时
仍返回本地识别与纠错结果。
这三层已经接入 Mac 的 live 与 batch 路径。“Mac 本地路径不消费词库”不再是当前事实。
## 2. 当前端到端数据流
```mermaid
flowchart LR
Audio["MacAudioRecorder · 16 kHz samples"] --> Pipeline["MacDictationPipeline"]
Dict["PersonalDictionary"] --> Bias["LocalASRBiasAdapter"]
Lexicon["phrases.tsv · BuiltinLexiconIndex"] --> Bias
App["Front app + locale"] --> Bias
Bias --> Prompt["promptBias"]
Bias --> Pairs["correctionPairs"]
Bias --> PolishTerms["polishFragment"]
Pipeline -->|local + installed MLX| Live["MacMLXLiveCapture"]
Pipeline -->|batch/recovery| Local["MacLocalASRService"]
Live --> Qwen["MacMLXStreamingASRProvider"]
Local --> Qwen
Local -->|model unavailable / Apple selected| Apple["MacSpeechLocalASR"]
Prompt --> Qwen
Qwen --> Raw["Raw transcript"]
Apple --> Raw
Raw --> Correct["LocalASRTranscriptCorrector"]
Pairs --> Correct
Correct --> Polish["PolishingService"]
PolishTerms --> Polish
Polish --> Insert["MacTextInsertionService"]
```
### 2.1 引擎选择与回退
| 项目 | 当前事实 |
|---|---|
| 默认模型 ID | `qwen3-mlx-0.6b-4bit` |
| 可选 MLX 模型 | 0.6B 4-bit、1.7B 4-bit |
| Catalog | `OSGKeyboard/Resources/LocalASR/local-asr-catalog.json` |
| 下载源 | `hf-mirror.com` 与 Hugging Face repository files |
| 推理 | `MLXAudioSTT.Qwen3ASRModel` |
| Live partial | 100 ms 音频 feed;流式 session 定期 decode |
| Batch | 同一 MLX 模型的 `generate(audio:context:language:)` |
| Fallback | Apple Speech 的本地 `SFSpeechURLRecognitionRequest` |
旧 Sherpa model ID 只在偏好迁移逻辑中映射到当前 MLX 默认值,不代表 Sherpa
backend 仍可运行。当前 catalog 的 `runtimes` 为空。
### 2.2 词库接线
`MacDictationPipeline.resolveLocalBias``MacMLXLiveCapture.resolveBias` 都读取
`store.personalDictionary`,再调用:
```text
LocalASRBiasAdapter.adapt(
dictionary + locale + frontAppBundleId + backend capabilities
)
```
适配器当前输出:
| 输出 | 当前消费者 |
|---|---|
| `promptBias` | Qwen3 MLX streaming `StreamingConfig.context` 与 batch `generate(context:)` |
| `correctionPairs` | `LocalASRTranscriptCorrector`,在 polish 前修正 aliases |
| `polishFragment` | `PolishingService``dictionarySupplement` |
| `diagnostics` | `LocalASRBiasDiagnosticsStore` |
| `hardHotwords` | 为具备 hard-hotword capability 的 backend 保留;当前 Qwen3 MLX 不使用 |
个人词优先;`BuiltinLexiconIndex``phrases.tsv` 选择 `weight >= 4` 的 Top-N
代码编辑器/终端前台场景优先 `computer_terms`。默认最多考虑 300 个内置 ASR 词,
Qwen3 soft prompt 最长 800 字符,润色补充最多 40 个内置词。
### 2.3 Apple Speech fallback
Apple fallback 强制 `requiresOnDeviceRecognition = true`,系统缺少对应语言模型时会
明确失败,不会静默切云。中文路径会准备打包的 Apple Custom Language Model
个人词库仍参与识别后的 alias 纠错与 polish 保真。
当前 `LocalASRCapabilities.appleSpeech` 声明 `hotwordMode = none`,因此不要把
`contextualStrings` 描述为已经由个人词库动态注入。相关 API 虽有适配入口,但当前
capability 不生成 hard hotwords。
## 3. 代码索引
| 主题 | 当前路径 |
|---|---|
| Mac 听写编排 | `OSGKeyboardMac/MacDictationPipeline.swift` |
| 本地引擎选择 / fallback | `OSGKeyboardMac/MacLocalASRService.swift` |
| MLX live capture | `OSGKeyboardMac/MacMLXLiveCapture.swift` |
| MLX provider | `OSGKeyboardMac/MacMLXStreamingASRProvider.swift` |
| MLX streaming session | `OSGKeyboardMac/MacMLXStreamingSession.swift` |
| Apple Speech fallback | `OSGKeyboardMac/MacSpeechLocalASR.swift` |
| Bias payload / capability | `OSGKeyboardShared/Models/LocalASRBiasPayload.swift`, `LocalASRCapabilities.swift` |
| Bias 构建 | `OSGKeyboardShared/Services/LocalASRBiasAdapter.swift` |
| 内置词索引 | `OSGKeyboardShared/Services/BuiltinLexiconIndex.swift` |
| 用户词库 | `OSGKeyboardShared/Models/PersonalDictionary.swift` |
| 云 ASR bias | `OSGKeyboardShared/Models/PersonalDictionary+ASRBias.swift` |
| Apple CLM | `OSGKeyboardHostSupport/Services/CustomLanguageModelManager.swift` |
| 内置 TSV | `OSGKeyboard/Resources/CustomLanguageModel/v1/phrases.tsv` |
## 4. 设计目标与边界
### 4.1 目标
- Mac 本地听写离线可用,音频默认不离开设备。
- iOS 与 Mac 复用 `PersonalDictionary``phrases.tsv` 的源数据。
- 每个 backend 如实声明 soft prompt、hard hotword、streaming 与 reload 成本。
- 模型下载、校验、安装和选择显式可管理。
- 质量、延迟、内存与误触发均可量化评测。
- 本地失败不静默回退云端。
### 4.2 非目标
- 不把一万词全量塞进 ASR prompt。
- 不把 polish 当作唯一专名纠错层。
- 不把尚未进入 catalog/runtime 的 Sherpa 或 SenseVoice 描述成当前能力。
- 不承诺未经同一测试集验证的模型质量。
- 不把 iOS 生成的 CLM `.bin` 直接喂给 MLX 或其他 backend。
## 5. 能力模型
`LocalASRCapabilities` 区分:
| 字段 | 含义 |
|---|---|
| `hotwordMode` | `none` / `promptOnly` / `perRequest` / `recognizerScoped` / `cloudVocabulary` |
| `maxPromptCharacters` | soft prompt 上限 |
| `maxHotwordCount` | hard hotword 上限 |
| `supportsStreaming` | 是否提供真流式 partial |
| `hotwordReloadCost` | `none` / `recognizerReload` / `modelReload` |
当前实际运行矩阵:
| Backend | 当前角色 | Bias | Streaming |
|---|---|---|---|
| Qwen3 MLX | Mac 默认 | `promptOnly`800 字符 | 是 |
| Apple Speech | Mac fallback / 显式选择 | ASR 层 `none`;后处理与 polish 仍接词库 | 否 |
| iOS SpeechAnalyzer | iOS 主路径 | Apple CLM + 本地纠错 | 渐进结果 |
| Cloud ASR | 用户显式选择 | 按 provider 使用个人词库 | 按 provider |
## 6. 竞品研究中仍有效的结论
基于 2026-03 的公开源码快照,保留以下架构结论,不把它们当作 OSG 当前实现:
| 项目 | 观察 | 对 OSG 的启示 |
|---|---|---|
| OpenLess | 多 provider;本地 Qwen3 词典接线有限 | README 不能把统一词库接口等同于 backend 已消费 |
| Typeflux | 词库限额、动态排序、项目词学习 | Top-N 与上下文排序有价值;自动学习必须可确认 |
| SayIt | Sherpa Qwen3 recognizer-scoped hotwords | 若未来重做 Sherpa POC,词库变化需计入 recognizer 重建成本 |
| VoiceSnap | SenseVoice 离线与静音处理 | 可作为速度/资源基线,不代表具备个性词库 |
| OpenBroca | 显式 model selection 与 manifest | 禁止扫描目录后任取第一个模型 |
这些研究只支撑未来实验。当前发布架构仍是 Qwen3 MLX + Apple Speech fallback。
## 7. 模型管理
当前安装由 catalog 描述 repository files、模型布局与体积,安装状态由 manifest
和必要文件校验决定。设计原则保持:
- Catalog 与 runtime 分离。
- `selectedModelId` 显式保存;旧 ID 有确定迁移规则。
- 下载进入 staging,经校验后原子发布。
- 无效或未安装模型不能假装可用。
- 中国大陆镜像与官方 Hugging Face 可按偏好选择。
ModelScope、自定义企业镜像与额外 backend 可以作为后续 catalog 扩展,但不是
1.7.0 当前下载路径。
## 8. 评测方法
### 8.1 测试集
| 类别 | 内容 | 目的 |
|---|---|---|
| A 普通中文 | 日常口语 50 句 | CER / 误触发基线 |
| B 技术术语 | SwiftUI、Cursor、Qwen3-ASR 等 50 句 | 专名召回 |
| C 用户词典 | 20 个个人词,每词多句 | bias 与 alias 纠错 |
| D 长句 | 30 秒以上口语 | streaming 稳定性与 finalize |
| E 噪声 / 短句 | 低 SNR、少于 2 秒 | 幻觉与热词污染 |
| F 中英混合 | 技术会议和代码口述 | 语言提示与专名保真 |
### 8.2 当前对照矩阵
| 配置 | 说明 |
|---|---|
| Baseline | Qwen3 MLX 0.6B,不传 bias |
| B1 | 0.6B + `promptBias` |
| B2 | B1 + alias correction + polish supplement |
| Quality | Qwen3 MLX 1.7B + B2 |
| Fallback | Apple Speech + correction / polish |
| Reference | 用户所选 Cloud ASR + `PersonalDictionary` |
未来 Sherpa/SenseVoice 只能作为新增 POC 行,不能替换当前 baseline 名称。
### 8.3 指标
- Raw CER/WER。
- 用户词 raw/final hotword recall。
- False hotword rate。
- 录音结束到最终插入的延迟,以及 live partial 首次可见延迟。
- 8 GB Apple-silicon Mac 上的峰值内存、CPU 与模型加载时间。
- 无网络完成率、模型安装成功率、fallback 成功率。
若未来评估 hard-hotword backend,建议门槛:
- 用户词召回相对当前 Qwen3 MLX + B2 提升至少 20%。
- False hotword rate 不高于 2%。
- 30 秒音频端到端延迟不超过当前基线 1.5 倍。
- 分发体积、签名、公证与 8 GB 设备内存均可接受。
## 9. 回退与风险
| 条件 | 当前/要求行为 |
|---|---|
| MLX 模型未安装 | 使用 Apple Speech fallback |
| MLX live 失败 | 返回 batch recovery 信号 |
| Apple 本地语言模型缺失 | 明确报错并提示系统下载,不切云 |
| Polish 缺 Key、超时或失败 | 返回本地识别/纠错文本 |
| 用户禁用云 | 不静默切换云 ASR |
| 热词 dump / 近静音幻觉 | 丢弃可疑 live 结果并尝试 batch |
主要后续风险:
1. 800 字符 soft prompt 对低频专名的提升有限。
2. 内置 Top-N 过多会污染短句或近静音输入。
3. alias 替换必须保持整词/高置信边界。
4. 1.7B 模型在低配 Mac 上的内存与首载延迟需持续量化。
5. 新 backend 必须先证明收益,再承担下载、签名和维护成本。
## 10. 后续方向
1. 用固定测试集持续比较 0.6B / 1.7B、无 bias / 分层 bias。
2. 将 diagnostics 与实际命中/截断数据用于调节 Top-N 和 prompt 上限。
3. 验证 Apple Speech fallback 的个人词动态提示能力后,再决定是否修改 capability。
4. 只有 hard-hotword 收益达到阈值时,才恢复 Sherpa Qwen3 POC。
5. 自动词库学习若实验,必须默认关闭、本地处理、用户确认后写入,并可审计/清空。
## 11. 修订记录
| 日期 | 说明 |
|---|---|
| 2026-08-11 | 按 1.7.0 代码重写:Qwen3 MLX streaming 默认、Apple Speech fallback、LocalASRBiasAdapter/PersonalDictionary 已接线;移除 Sherpa 当前路径叙述 |
| 2026-03-31 | 初版竞品研究与评测框架 |
-818
View File
@@ -1,818 +0,0 @@
# OSGKeyboard · iOS 完整检查 + macOS 差距与移植建议报告
> **报告日期**2026-08-23
> **当前版本**2.0.1build 90)— `project.yml:51-52`
> **审查范围**iOS Host (`OSGKeyboard/`, 88 .swift) + iOS HostSupport (`OSGKeyboardHostSupport/`, 29 .swift) + iOS Extension (`OSGKeyboardExt/`, 27 .swift) + Shared (`OSGKeyboardShared/`, 230 .swift) + macOS (`OSGKeyboardMac/`, 39 .swift) + macOS Tests (3 .swift)
> **方法**:只读代码考古,证据来自 `project.yml` 共享 / 排除清单 + 文件结构 + 关键源码摘录 + `CHANGELOG.md`
---
## TL;DR
| 维度 | iOS | macOS | 结论 |
|---|---|---|---|
| 产品形态 | 宿主 App + 自定义键盘扩展 | 单窗口菜单栏 App + 浮动 HUD | **不同形态**——Mac 不是键盘,是"按住 Option 说话→⌘V 注入" |
| 核心流水线 | Flow 跨进程会话 + SpeechAnalyzer | 单一进程 + MLX Audio Qwen3 流式 | **架构不可同构** |
| LLM 润色 | ✅ 完整(`PolishingService`) | ✅ 完整(**直接复用** iOS `PolishingService` | **完全对齐** |
| 本地 ASR | iOS 26 `SpeechAnalyzer + DictationTranscriber` | **MLX Qwen3-ASR 0.6B/1.7B 4-bit** + Apple Speech 兜底 | **完全不同的技术栈** |
| 云 ASR | 7 家供应商 + 流式 | ✅ **完全对齐**(同 `CloudASRClientFactory` | **完全对齐** |
| 个人词典 | ✅ | ✅ | **完全对齐** |
| 历史 / 使用统计 | ✅ | ✅(iCloud KVS 共用) | **完全对齐** |
| 润色样式 + 学习 | ✅ | ✅ | **完全对齐** |
| iCloud 同步 | ✅(5 路:设置/词典/历史/使用/样式) | ✅(**5 路全部对齐**) | **完全对齐** |
| 提示页 (Tip) | ✅ | ✅ | **完全对齐** |
| 自定义语言模型 CLM | ✅ UI + 后台 | ⚠️ 后台调用了,**无 UI** | **部分缺失** |
| 剪贴板历史 + AI 技能 | ✅ | ❌ 共享代码已编译但**无 Mac UI** | **完全缺失** |
| AI 提示轮播 / 上下文技能 | ✅ | ❌ 共享代码已编译但**无 Mac UI** | **完全缺失** |
| Flow 跨进程会话 / PiP | ✅ | ❌ | **Mac 形态不需要** |
| 自定义键盘 (Rime/拼音/英文) | ✅ | ❌(`Typing/**` 全排除) | **Mac 形态不需要** |
| OSG 账户 (Sign in with Apple + 积分 + 推荐) | ✅ | ❌ | **完全缺失** |
| App Attest | ✅ | ❌(无对应 entitlement | **完全缺失** |
| 一方分析 (Analytics) | ✅(108 测试覆盖) | ❌(共享代码已编译但**无任何调用**) | **完全缺失** |
| 助手指令 (Shortcuts) | ✅ | ❌ | **完全缺失** |
| 屏幕唤醒 / 锁 / Live Activity | ✅ | ❌ | **Mac 形态不需要** |
**最关键的发现**
1. **macOS 复用策略 = 源码级 include**。Mac 不 `import OSGKeyboardShared`——`project.yml:570-602` 把 Shared + HostSupport 的 `.swift` 文件直接编进 Mac target,再用 `excludes:` 列表过滤 iOS-only 文件。**整个框架里没有 `#if os(macOS)` 条件编译**Mac target 内部有几处 `import AppKit` 守卫,是防御性的)。
2. **LLM 润色、云 ASR、iCloud 同步、本地 ASR 模型管理、模型目录、个人词典、润色样式、提示页、Onboarding——这 9 大块是 iOS / Mac 完全对齐的**。差异主要在"宿主 App 才有"的系统集成(Account、App Attest、Analytics、Shortcuts、Flow 跨进程会话)。
3. **"共享代码已编进 Mac 但零调用"的浪费**`OSGKeyboardShared/Features/Account/``Features/ManagedGateway/``Features/Analytics/``Services/AIClipboard*.swift``Services/AIHint*.swift``Services/AIUserSkill*.swift` —— 这些文件编译进了 Mac 二进制但没有任何 Mac 文件引用。**编译器开销 + 二进制体积 + 维护面全白付**。要么真正接进 Mac,要么从 Mac target 排除。
4. **Mac 真正独占的体验是 MLX 流式 ASR**。这是 iOS 受限于 ARM NEON + iOS 26 平台绑定做不到的能力,恰好把 Mac 的 Apple Silicon 算力用足。
5. **移植优先级不应是"把 iOS 全搬到 Mac"**。Mac 是"按住说话→润色→注入"工具,自然形态完全不同。**真正应该补的是:剪贴板 AI、CLM 管理、Account 体系、Analytics。**其他(Flow、自定义键盘)不应该硬塞进 Mac。
---
## 1. iOS 端完整状态
### 1.1 Target 拓扑
| Target | 平台 | 类型 | .swift 数 | 备注 |
|---|---|---|---|---|
| `OSGKeyboard` | iOS 26 | App | 88 | 宿主 App,沙盒、StoreKit、iCloud、App Attest |
| `OSGKeyboardExt` | iOS 26 | App Extension (键盘服务) | 27 | `RequestsOpenAccess: true` |
| `OSGKeyboardShared` | iOS 26 | Framework | 230 | 跨进程共用 + 复用到 Mac |
| `OSGKeyboardHostSupport` | iOS 26 | Framework | 29 | 宿主专用(Speech/AVFoundation/Charts/StoreKit |
| `OSGKeyboardMac` | macOS 15+ | App | 39 | 菜单栏单窗口,**沙盒关闭**Accessibility 需要) |
| `OSGKeyboardMacTests` | macOS 15+ | Unit Test | 3 | 挂在 Mac App bundle 内 |
| `OSGKeyboardTests` | iOS 26 | Unit Test | 108 | 覆盖 host + shared + hostsupport |
| `OSGKeyboardExtTests` | iOS 26 | Unit Test | 16 | 覆盖 ext + shared |
| `OSGKeyboardUITests` | iOS 26 | UI Test | — | TestFlight 截图自动化 |
### 1.2 iOS Host App (`OSGKeyboard/`) — 用户可见的所有功能
**导航**4 Tab (Keyboard / Skills / Styles / Settings) on iPhoneiPad 自动切换到 `NavigationSplitView` + 240pt 侧栏 + `WideStatusFooter`
**7 大用户场景**
1. **Onboarding**`Views/OnboardingExperienceView.swift`)—— 7 步:欢迎 → 权限 → 键盘安装 → 切换引导 → 4-feature 实践(语音/翻译/回复/AI)→ 登录奖励 → 完成。匿名实践用 `OOBEClientInfrastructure` 拿服务器发放的体验金。
2. **Home 仪表盘**`Views/HomeView.swift:122`)—— 7 天柱状图 + 指标瓦片 + Flow 连接卡 + 词典/历史卡 + 中英文高频词条 chip。
3. **Settings**`Views/SettingsView.swift:30`)—— 7 个子面板:账户、AI 代理、剪贴板、Locale、润色强度、翻译目标、语音识别、文本润色、通用、关于。NavigationStack + `SettingsRoute` 枚举。
4. **Account Center**`Views/Account/AccountCenterView.swift:11`)—— 已登录摘要、积分 + 等级、StoreKit 商品列表、购买历史链接、登出 / 删号 (reauth)、推荐链接。下拉刷新。
5. **Polish Styles**`Views/PolishStylesView.swift:11`)—— 内置/社区/自定义样式网格 + 编辑器 sheet + "从历史学习"按钮。
6. **AI Agent Skills**`Views/AIAgentSkillsView.swift:12`)—— 已装/未装技能卡,安装调起 `shortcuts://` 或 bundled `.shortcut` 文件。
7. **Legal / 隐私**—— `PrivacyPolicyView` / `OpenSourceLicensesView` / 远程 web view。
**宿主端 Services`OSGKeyboard/Services/`** 关键件:
| 文件 | 职责 | LOC 估算 |
|---|---|---|
| `FlowSessionManager.swift` | **唯一拥有者**——AVAudioSession 激活、连续 `.playAndRecord` 捕获、ASR 选择、后处理、`PolishingService` 调用、AI 模式、App Group 写、utterance token、launch reconciliation、start/stop/end session | ~3500 |
| `FlowPictureInPictureController.swift` | PiP keep-alive`AVPictureInPictureVideoCallViewController` 0.1pt contentno audio session | — |
| `FlowASRPostProcessor.swift` | 本地引擎转写后处理(`LocalASRTranscriptCorrector.apply` | — |
| `FlowTerminationCoordinator.swift` | 进程退出时同步释放 mic、结束 LiveActivity | — |
| `FlowAnalyticsOperationRegistry.swift` | 会话级分析去重,订阅 `FlowSessionManager` 事件 | — |
| `RimeDeploymentController.swift` | Rime 资源部署(host-owned | — |
| `AIAgentShortcutInstaller.swift` | 调起 `shortcuts://` 安装 iCloud 分享的技能 / bundled `.shortcut` | — |
| `OOBEClientInfrastructure.swift` | 匿名 OOBE 体验金客户端 | — |
| `OfficialSkillCatalogRefreshService.swift` | 15 分钟 ETag-aware 刷新 `https://account.osglab.com/v1/content/skills` | — |
| `AIHintRefreshService.swift` | 12 小时静默刷新远程提示包 | — |
| `AnalyticsHostService.swift` | `BGTaskScheduler` 注册 (`com.osgkeyboard.ios.analytics-sync`) + `NWPathMonitor` + 后台 drain | — |
| `HostReturnService.swift` | 解析 `FlowSessionBridge.pendingHostBundleId()` 通过 `HostAppURLRegistry` 重开原 App | — |
| `AppURLHandler.swift` | `UIApplicationDelegate` + `UISceneDelegate``osgkeyboard://startflow` / `https://osglab.com/i/...` 通用链接 / `didBecomeActiveNotification` 反弹保留 `sourceApplication` | — |
| `AppPermissions.swift` | `AVAudioApplication.requestRecordPermission` (iOS 17+) + `SFSpeechRecognizer` + `PasteAccessResult` 一次性 `pasteAccessVerified` | — |
**宿主端 Models 关键点**
- `AppGroupConfiguration` 完整 Codable blob —— 镜像到 App Group + iCloud KVS
- `ProviderConfig` 8 家供应商预设(openai/ark/deepseek/qwen/zhipu/moonshot/siliconflow/groq
**关键 iOS-only 调用**
- `UIOpenURLContext.options.sourceApplication` (iOS 26 only) —— 保留源 bundle idhost-return 白名单唯一路径)
- `AVAudioApplication.requestRecordPermission` (iOS 17+)
- `SpeechAnalyzer + DictationTranscriber` (iOS 26 only)
- `OSAllocatedUnfairLock` (iOS 16+)
- iOS 26 Icon Composer (`OSGKeyboard/AppIcon.icon`)
- `UIBackgroundModes: audio` + `BGTaskScheduler`
- `applinks:osglab.com` Universal Links
- `LSApplicationQueriesSchemes` 白名单 50+ 国外 App scheme
- `SKIncludeConsumableInAppPurchaseHistory=true` —— StoreKit 2 一次性商品进历史
**iOS-only entitlements**(在 `OSGKeyboard.entitlements`):
- App Group `group.com.osgkeyboard.shared`(与扩展共享)
- 两个 keychain-access-groupsshared + iOS-only
- `aps-environment: $(APP_ATTEST_ENVIRONMENT)` ← xcconfig 注入(debug=development / release=production
- Sign in with Apple `Default`
- iCloud KVS `$(TeamIdentifierPrefix)com.osgkeyboard.ios`(**单字符串**,非数组 —— 数组会破坏自动签名)
- 音频输入
- 关联域名 `applinks:osglab.com`
**StoreKit 配置**`OSGKeyboard.storekit`):4 个消耗型 —— 自愿打赏 `ByRockyACoffee` (¥28) + 3 档账户积分 `500tks/1500tks/3000tks` (¥8/¥18/¥28)。**无订阅**。
### 1.3 iOS HostSupport 框架 (`OSGKeyboardHostSupport/`)
设计原则:**唯一**链接 `Speech / AVFoundation / Charts / StoreKit` 的 iOS 框架——键盘扩展**不**链接它,把 jetsam 预算留给 Shared。
**子目录**
| 路径 | 内容 |
|---|---|
| `Services/` | ASR + 云 ASR + Flow 捕获 + Dictation + Tip + `CloudASR/` + `Tip/` |
| `Models/` | `AudioBufferSnapshot+AVFoundation.swift` —— `AVAudioPCMBuffer` ↔ 共享 `AudioBufferSnapshot` 适配 |
| `DesignSystem/` | `SevenDayUsageChart` + `UsageStatsCluster` + `SupportDeveloperSection` |
| `Features/Account/` | `AccountAPIClient` / `SignInWithApple` / `DeviceIntegrity` (App Attest) / `HostPrivateAccountKeychain` / `AccountModels` / `OOBEGrantProvisioningCoordinator` |
**关键 Services**
- **`ASRService.swift:1`** —— 公共协议 `transcribe(stream:locale:)` 返回 `AsyncStream<ASREvent>` (`.capability/.partial/.final/.error`)`transcribeChunk(samples:locale:)` 用于流水线 Flow 路径。`ASRServiceFactory.make(store:)` 返回 `SpeechAnalyzerASR`(本地)或 `CloudASRService`(云)。`SpeechAnalyzerASR`**iOS 26 `SpeechAnalyzer + DictationTranscriber` + bundled `SFCustomLanguageModelData`**,锁用 `OSAllocatedUnfairLock`,支持流水线 chunk 复用。
- **`CloudASRService.swift:1`** —— 云 ASR 前门。`CloudASRClientFactory.make(...)` 返回 8 个客户端:`ZhipuCloudASRClient` / `AlibabaFunASRClient` / `BailianRealtimeASRClient` / `VolcengineCloudASRClient` / `OpenAIRealtimeASRClient` / `PromptCloudASRClient` / `ManagedVolcengineASRClient` / `UnsupportedCloudASRClient`
- **`LiveDictationController.swift:1`** —— `@MainActor ObservableObject`,阶段机 `idle/recording/processing/denied/error`,自管 `AVAudioEngine + AVAudioSession`。生产键盘**不**用它,**生产用 `FlowSessionManager + FlowContinuousCapture`**。
- **`FlowAudioSessionCoordinator.swift:1`** —— 进程级 `AVAudioSession` + Flow 的 `AVAudioEngine` 拥有者。`.voiceChat` 模式。
- **`CustomLanguageModelManager.swift`** —— 单例,准备 bundled `OSGKeyboardCLM.bin` 写进 App Group。指数退避 30s/120s/600s。**重到 `MainAppRoot.scheduleCLMWarmup` 延后 45s 启动,Flow 忙时拒绝运行**。
**HostSupport 持久化**
- CLM 编译产物 → App Group container
- Account 令牌 → host-only keychain**不**走 App Group
- Managed AI 凭证 → 单独 `GatewayGrantKeychainStore`**只**镜像"会话可用"布尔进 App Group
- Tip 计数 → 标准 `UserDefaults`
### 1.4 iOS Keyboard Extension (`OSGKeyboardExt/`)
**入口**`KeyboardViewController.swift:33-38` —— `@objc(KeyboardViewController) @MainActor` `UIInputViewController`。**单一** `KeyboardState` ObservableObject。
**生命周期(`KVC.swift`**
- `init` → 启动 `KeyboardExtensionMemoryTelemetry`warning 36MB / safe 40MB / critical 48MB
- `viewDidLoad` (`:148-207`) → 设 `showsSystemGlobeKey = isPad``primaryLanguage = "mis"`(隐藏"English"副标题误导),读 `TypingInputConfiguration.preferredSurfaceOnOpen()`,调 `refreshLayoutMode() + installKeyboardHeight() + configureDictationBehavior() + installServices() + installSwiftUI()`
- `viewWillAppear` (`:256-293`) → 标记 `KeyboardSetupBridge.markExtensionAppearance(hasFullAccess:)`、刷新 Flow/config、同步 onboarding、准备 `KeyboardHapticFeedback` generators
- `viewIsAppearing/viewDidAppear` (`:295-356`) → 锁高度 `lockPresentedKeyboardHeight()`,禁系统手势延迟,记录 analytics
- `viewWillDisappear` (`:209-254`) → 通知 `AnalyticsExtensionService.keyboardWillDisappear()`、取消 `assistantFieldActionRefreshTask`、使 `editHintScheduler` 失效、持久化最后 surface
- `textDidChange/selectionDidChange``refreshReturnKeyRole`、同步 English 文档上下文
- `didReceiveMemoryWarning` (`:390-404`) → **取消流水线、退出 typing 模式、强制回 voice surface** —— typing 引擎是最大内存消费者
**`installServices` (`:419-547`)** 安装的 8 个 coordinator
- `EditHintScheduler`mic 上方一句话提示)
- `KeyboardTextInserter`(插入 Flow 转写 / undo / redo / 粘贴剪贴板)
- `KeyboardConfigSync`App Group 配置 + Darwin observers + onboarding 镜像)
- `KeyboardFlowCoordinator`Flow start/stop + session monitor + watchdogs
- `LastInputEditCoordinator`(长按 → 编辑上次输入)
- `AIKeyboardCoordinator`(长按 → AI 模式)
- `ClipboardCaptureCoordinator``changeCount` 轮询,secure-field 抑制,建议条)
- `AnalyticsExtensionService`
**`installStateActions` (`:551-655`)** 装的 30+ action`beginRecording/endRecording/tapMic/cancelVoiceInput/beginEditLastInput/.../setMode/setLocale/setEngineMode/setTranslationTargetLocaleId/insertNewline/insertSpace/deleteBackward/undoLastInsertion/redoLastInsertion/copySelection/cutSelection/setSurface`
**手势模型**
- 长按 push-to-talk → `RecordButtonGesturePolicy` 阈值 `longPressDuration = 0.45s`
- 点击 mic → 切换(`RecordButton.tapAction`
- 重复删除 → `RepeatingPressButton` + `RepeatingDeleteTiming`80→50→30→15 ms 加速)
- Shift 长按 → `TypingSessionController.shiftHeld`,双击 capsLock
- 语言切换 → 3-tab `KeyboardInputTab`assistant / 中文 / 英文)
- 触觉 → `KeyboardHapticFeedback`5 个 Taptic generator + 4 个按键角色 + `KeyboardHapticIntensity` off/light/strong
- 声音 → `UIDevice.playInputClick()` + 系统音 1155 (delete),通过 `@retroactive UIInputView: UIInputViewAudioFeedback` 启用点击声
- 主题 → 始终深色(`Palette.dark` 是 canonical
**iOS-only 扩展调用**`UIInputViewController` / `UIInputView` / `UITextDocumentProxy` / `NSExtensionContext` / `UIPasteboard.general` / `UIImpactFeedbackGenerator` / `UIDevice.playInputClick()` / `AudioServicesPlaySystemSound(1155)` / `UIButton` / `UICollectionView` + `UICollectionViewCompositionalLayout` / `UIHostingController<KeyboardSurfaceRoot>`
**Host/Extension 边界数据**`AppGroupPersistor.load(into:)` 读取):
- `providerId` / `baseURL` / `apiKey``Keychain.apiKeyOutcome` 解析 shared keychain `$(AppIdentifierPrefix)com.osgkeyboard.shared`
- `model` / `modeId`(恒为 `polish`/ `localeId` / `engineMode`local/cloud
- `translationTargetLocaleId` / `handednessPreference`(左/右手 delete↔space 互换)/ `clipboardHistoryEnabled` / `clipboardCandidateBarEnabled`
- `keyboardHapticIntensity` / `clipboardSkillSnapshot`(启用技能 ID/ `apiKeyAvailability`
**`KeyboardConfigSync` (`KeyboardConfigSync.swift:24-62`)** 安装 4 个 `FlowSessionDarwinObserver``session.changed` / `command.changed` / `transcription.changed` / `host.ready.changed`
### 1.5 iOS Shared Framework (`OSGKeyboardShared/`)
**目录**
| 路径 | 内容 |
|---|---|
| `Constants/AppGroup.swift` | App Group ID 唯一源 |
| `Core/Configuration/` | `ConfigurationStore` 协议 + iOS 实现 `AppGroupStore+ConfigurationStore.swift` + `LiveConfigurationStore` |
| `DesignSystem/` (9 文件) | `Theme` / `RecordButton` / `RecordButtonGesturePolicy` / `WaveformView` / `EditTextPager` / `CardPageLayout` / `SonicParticleField` / `ThemedRoot` / `UsageStatCard` / `UsageSurfaceCard` |
| `Features/Analytics/` (10 文件) | `AnalyticsClient` / `AnalyticsRepository` (SQLite) / `AnalyticsUploadCoordinator` / `KeyboardUsageRepository` (跨进程 SQLite WAL) / ... |
| `Features/ManagedGateway/` (8 文件) | `ManagedLLMClient` / `GatewayGrantCoordinator` (actor) / `GatewayGrantCredentialStore` / `ManagedGatewayAccountAccessPolicy` / `ManagedGatewayScopePolicy` / `ManagedGatewayQuestionRouter` / `ManagedGatewayModels` / `OOBEGatewayGrantCoordinator` |
| `Localization/SharedL10n.swift` | `NSLocalizedString` against `Shared.strings`;解析 `AppUILanguage` |
| `Models/` (~50 文件) | 见下 |
| `Resources/` | `ClipboardSemantics/{*.mlmodel,*.json}` (6 CoreML) / `PolishStyles/{manifest.json,builtin.*.json}` (10 内置样式) / `Typing/English/{english_lexicon.{bin,tsv},english_bigrams.tsv}` |
| `Services/` (~80 文件) | 见下 |
| `Typing/` (24 文件) | librime + 拼音 + 英文 autocorrect + 触屏 + 布局 |
| `Utilities/` (21 文件) | 日志、错误模型、keychain、locale、Han 脚本、PCM/WAV、内存预算等 |
**关键 Services 详解**
- **`PolishingService.swift:34-105`** —— `public actor``polish(_ raw:mode:systemPrompt:context:) -> PolishOutcome(text:qualityDegraded:polishStyleID:polishStylePrompt:)`。**单一融合 LLM 调用**:T1 自纠 → T2 填料清理 → T3 同音纠正 → T4 标点 → T5 结构 → 应用样式。Fun personality 在 heavy 强度切到"仅格式"共享核心。
- **`LLMClient` 协议** (`Services/LLMClient.swift:91-150`) —— `polish(_:systemPrompt:timeout:)` + `complete(messages:tools:timeout:options:)`。实现:`LLMClientFactory`OpenAI 兼容)、`AnthropicLLMClient`Anthropic Messages API)、`ResponsesAPILLMClient`OpenAI Responses)、`ManagedLLMClient`(账户 grant 范围)、`AIModeSearchFallbackClient`(搜索→纯)。
- **`Keychain.swift:15`** —— `kSecClassGenericPassword` 读写 + iCloud 同步变体 + `OnFirstUnlockThisDeviceOnly` 本地变体 + XCTest 内存回退。
- **`AIClipboardSkill.swift:44`** —— 技能模型:`id` / `systemImage` / `titleKey` / `kind``.direct/.transform/.export`/ `isDefault` / `shortcutName` + iCloud 分享 URL + bundled `.shortcut` 资源 / `thinkingEnabled` / `customName/Summary/Prompt` / `requiresShortcut` / `isUserCreated` / `isOfficial` / `managedGatewayTaskKind` / `supportsReplyStyle`
- **`ClipboardHistoryStore.swift:10-84`** —— App-Group-backed observable store。合并通过 `ClipboardHistoryPolicy`(去重、清洗、保留)。
- **`ClipboardSemanticAnalyzer.swift:1-58`** —— 本地:`NLTokenizer/NLTagger` + 6 CoreML 分类器(`ClipboardSemantics/*.mlmodel`)产生 `language/dates/addresses/phoneNumbers/urls/personNames/organizationNames/sentiment/task/question/invitation/complaint/replyableMessage` 意图。
- **`ClipboardSkillSemanticRanker.swift:11-66`** —— 用语义分析 + 偏好语言对完整目录排序,固定 generic Reply 兜底。Stateless / `Sendable`
- **`SpeechHistoryStore.swift:10-78`** —— iCloud-KVS 镜像 observableprompt-fingerprint dedup。
- **`LocalASRModelManager.swift:11`** —— macOS Qwen3-MLX 权重 Application-Support 安装管理器。状态持久化到 `installed-manifest.json`。**Mac 专用**iOS 用系统 SpeechAnalyzer)。
- **`LocalASRBiasAdapter.swift:9`** —— 分层偏差:`PersonalDictionary.effectiveEntries` + 内置 `BuiltinLexiconIndex.topTerms(...)`;按前台 App bundle id 选 `builtin-computer` vs `builtin-top`Xcode/VSCode/Android Studio/IntelliJ/AppCode/Sublime/Terminal/iTerm2/Warp)。
- **`LocalASRTranscriptCorrector.swift:9-65`** —— 确定式 alias→canonical 替换,最长匹配优先,ASCII whole-word 感知。
- **`PolishStyleLearningService.swift:64`** —— `build(from:)``SpeechHistoryEntry` 历史生成 `PolishStyleLearningCorpus`5000 字符 `requiredEffectiveCharacterCount`)。
- **`AIQuestionService.swift:20`** —— `actor AIConversationStore` 保留 `retainedConversationRounds` 轮 / 对话。
- **`AIEventExtraction` / `AIAddressExtraction` / `AITodoExtraction`** —— 解析 LLM 输出为 `start|end|title|location` / `origin|destination` / 列表。**全部 fail-closed**。
- **`HostAppURLRegistry`** —— 白名单 deep-link 路径(`deployrime``settings/clipboard``skill/run` 等),host + ext 共用。
- **`KeyboardSetupBridge`** —— `markExtensionAppearance(hasFullAccess:)` + OOBE 实践会话标志。
- **`AppGroupConfigDarwin`** —— `CFNotificationCenter``com.osgkeyboard.config.changed`host 写完后)。
- **`FlowSessionBridge` + `FlowSessionKeys`** —— 跨进程 mailbox 全部 App-Group key`flowHostReady` / `flowHostReadyAt` / `flowHeartbeat` / `flowSessionActive` / `flowSessionExpires` / `hostHeavy/At` / `hostGeneration` / `transcriptionResult` / `transcriptionPartial` / `transcriptionError{Kind,PolishWarning}` / `pendingKeyboardUtteranceId` / `lastPiPArmAttemptAt` / `flow.commandPayload.v1` / `flow.commandJournalPayload.v2` / `flow.resultPayload.v1` / `flow.ackPayload.v1` / `flow.startTransaction.v1` / `flow.readyPayload.v1`
- **`SettingsCloudSync` / `SpeechHistoryCloudSync` / `UsageStatisticsCloudSync` / `AppCloudSync`** —— KVS 镜像;`ICloudSyncPreferences` + `SyncDeviceID` 保留每设备身份。
- **`AIHintStore` + `AIHintPool` + `AIHintKeywordExtractor` + `AIHintKeywordCompressor` + `AIHintLocalCatalog`** —— 闲置热词轮播:App-Group-pack loader、合并远端 + 本地卡片、确定性 10 或 22 字符关键词抽取(zh/en)。
- **`TranscriptionPolishFallback.swift`** —— LLM 润色失败或跳过时,交付原始转写 + 软警告。
- **`WhatsNewDemoScenario`** —— "What's New" 时间轴 driverhostpeek/consume)和 extDEBUG)共用。
**Polish 子系统详细**
- `PolishContext` (`Models/PolishContext.swift`)appContext + precedingText + followingText + fieldHints + dictionarySupplement + maxPrecedingChars=600 + maxFollowingChars=200
- `AppContext` 5 种(code/email/chat/document/unknown
- `AppContextDetector` (`:30-116`)3-fallback 链——`textDocumentProxy.documentContextBeforeInput` 启发式(iPad 沙盒安全;键盘读不到前台 bundle id)→ 30 分钟缓存 → 环境 blend
- `PolishStylePack` (`Models/PolishStylePack.swift:9-87`)`Codable, Equatable, Identifiable, Sendable``id/name/prompt/allowsAddedEmoji/kind(.builtin/.user)/createdAt/updatedAt`
- `PolishStyleLimits``maximumUserPacks = 8` / `maximumPromptCharacters = 6_000`
- 内置样式分两组:`.practical`light/structured/formal/chat+ `.fun`dating/flex/corp/diba/xhs
**Typing 子系统**`Typing/`):
- `LibrimeEngine.swift` —— `@MainActor` 中文 IME,部署 3 套 schema`osg_pinyin` / `osg_double_pinyin_mspy` / `osg_double_pinyin_sogou`
- `EnglishSuggestionEngine.swift` —— 3 槽 QuickTypeverbatim / correction / completion),`inVocabularyFrequencyGap = 250`
- `EnglishLexicon.swift` —— mmap 40k 词 `english_lexicon.bin`
- `EnglishSystemLexicon.swift` —— `#if canImport(UIKit)` 守卫包 `UITextChecker + UILexicon`
- `EnglishQWERTYProximity.swift` —— 空间编辑代价
- `TypingSessionController` —— 拥有 typing surfacelanguage/page/shift state、English 引擎、librime 引擎、拼音/英文消歧、autocap、句号快捷、shadow preceding text、`supplementaryWords`、hot reload
- `TypingSurfaceMetrics` (`Models/TypingSurfaceMetrics.swift`) + `KeyboardChromeLayout` —— 单一源:键行指标、底部动作行分数、总高 281ptiPad narrow/widenarrow 54pt / wide 76pt),`wideIPadWidthThreshold = 1100`
- `TypingInputConfiguration` —— `TypingInputSchema` (3 case) + `DefaultInputMode` (voice/pinyin/english)
- `PeriodShortcut.swift` —— iOS 句号快捷:letter→number 后第二个空格 0.45s 内变 ". " + arm sentence Shift
- `TypingAutocapitalization` —— 镜像 `UITextAutocapitalizationType`
**Models (~50 文件) 关键**
- `AppGroupConfiguration` —— 完整 Codable blob
- `SyncedAppSettingsV2` —— per-field `SyncedField<T>` LWW + broken-clock containment (6h skew horizon)
- `LLMProvider` —— 8 家云预设
- `AIUserSkill` / `AIAgentSkillLayout` / `OfficialSkillCatalog` / `OfficialSkillDefinition` —— 技能体系
- `PolishStylePack` / `PolishStyleCatalog` / `PolishStyleLearningCorpus`
- `PersonalDictionary` / `+Merging` / `+ASRBias` —— 跨设备词典,iCloud 合并;提供 `hotwords` / `asrPromptBias` / `alibabaHotwordEntries` / `vocabularySyncFingerprint`
- `ClipboardHistoryEntry` / `SpeechHistoryEntry` / `SyncedSpeechHistory`
- `PolishContext` / `FieldHints` / `AppContext` / `AppUILanguage`
- `ProviderConfig` / `CloudProviderRole` / `CloudASRModels` / `LocalASRModelCatalog` / `LocalASRCapabilities` / `LocalASRBiasPayload`
- `TypingInputConfiguration` / `TypingSurfaceMetrics` / `KeyboardChromeLayout`
- `HandednessPreference` / `KeyboardHapticIntensity` / `PolishIntensity` / `AIResponseLength`
- `FlowUtteranceRequest` / `FlowUtteranceMode` / `FlowUtteranceChunkConfig` / `FlowHandoffPolicy` / `FlowAck` / `FlowResult` / `FlowCommand` / `FlowReadySnapshot` / `FlowStartTransaction` / `FlowTranscriptionError` / `FlowFieldContext`
- `MicVoiceAvailability` / `+Keyboard` —— mic 状态 + 派生
- `EditableInputReference` —— 跨进程最后插入引用(10 min TTL、≤1200 graphemes、schema v1
- `EditSessionState` —— 关闭状态机 `.inactive/.preparing/.listening/.processing/.review/.applying/.appending/.failed`
- `AISessionState` —— `.inactive/.idle/.preparing/.listening/.recognizing/.generating/.ready/.awaitingSend/.inserted/.sent/.failed`
**Utilities (21 文件)**
- `OSGLog.swift` —— subsystem + 类别(`flow/clm/config/asr/keyboardExt`
- `OSGDiag.swift` —— NSLog + 内存快照(`task_info`
- `FlowTrace.swift` —— `[trace] stage=...`
- `FlowPipelineDiagnostics.swift` / `FlowCaptureTailDrain.swift` / `FlowUtteranceEndCoordinator.swift` / `FlowUtterancePCMStore.swift`
- `UtteranceStreamChunker.swift` / `UtteranceBatchFallbackPolicy.swift` / `UtteranceTranscriptGuard.swift` / `UtteranceTranscriptStitcher.swift` / `TranscriptOverlapUtilities.swift` / `TranscriptLanguageDetector.swift`
- `ProgressiveDictationTranscriptAccumulator.swift`**Mac 排除**
- `DictationTextComposer.swift` / `FinalChunkRecovery.swift` / `PCMSampleWavEncoder.swift`mono Float32 @16kHz → WAV
- `PromptXMLEscaping.swift` / `HanScript.swift`BMP 汉字谓词)
- `HostMemoryBudget.swift` / `KeyboardExtensionMemoryTelemetry.swift`**Mac 排除**
- `ProviderDisplayName.swift` / `AppVersionDisplay.swift`
**iOS-only 排除(`project.yml:570-586`**
```
DesignSystem/WaveformView.swift
DesignSystem/RecordButton.swift
DesignSystem/RecordButtonGesturePolicy.swift
Services/KeyboardState.swift
Services/KeyboardOpenSurfacePolicy.swift
Models/MicVoiceAvailability+Keyboard.swift
Models/TypingInputConfiguration.swift
Models/TypingSurfaceMetrics.swift
Typing/**
Utilities/ProgressiveDictationTranscriptAccumulator.swift
```
### 1.6 iOS 测试覆盖(108 + 16 文件)
**`OSGKeyboardTests/` (108 文件)** —— host + shared + hostsupport 端到端:
- **ASR/云 ASR**`CloudASRServiceTests` / `CloudASRHTTPClientTests` / `CloudASRStreamingHelpersTests` / `CloudASRStreamingEventParsingTests` / `CloudASRTests` / `ASRConversionTests` / `LocalASRModelCatalogTests` / `LocalASRDownloadSourceSorterTests` / `LocalASRBiasAdapterTests` / `AlibabaVocabularySyncTests` / `FrequentTermStoreTests` / `PreviewASRControllerStateTests`
- **语音流水线**`VoicePipelinePerformanceTests` / `FlowPhysicalAudioStressTests` / `FlowReliabilityTests` / `FlowASRPostProcessorTests` / `ChunkedUtterancePipelineTests` / `FlowBudgetAndMergeTests` / `FlowCaptureTailDrainTests` / `FlowUtteranceEndCoordinatorTests` / `FlowUtterancePCMStoreTests` / `UtteranceBatchFallbackPolicyTests` / `UtteranceStreamChunkerTests` / `UtteranceTranscriptGuardTests` / `UtteranceTranscriptStitcherTests` / `ProgressiveDictationTranscriptAccumulatorTests` / `FlowSessionBridgeTests` / `FlowSessionManagerAnalyticsTests` / `FlowSessionPolicyTests` / `FlowHandoffPolicyTests` / `FlowPiPRecoveryPolicyTests` / `FlowStartTransactionPolicyTests` / `FlowHomePiPStatusPolicyTests` / `FlowKeyboardPoliciesTests` / `KeyboardExtensionMemoryBudgetTests`
- **Polish/LLM**`PolishStylePackTests` / `PolishStyleLearningServiceTests` / `PolishPromptComposerQuestionTests` / `PolishOutputValidatorTests` / `IntelligentPolishTests` / `LLMClientTests` / `AIModeLLMClientTests`
- **AI 特性**`AIQuestionServiceTests` / `AIEventExtractionTests` / `AIAddressExtractionTests` / `AINoteExportTests` / `AIHintKeywordExtractorTests` / `AIHintPoolTests` / `AIUserSkillTests` / `AIUserSkillStoreTests` / `AIAgentSkillLayoutTests` / `AIHistoryAndUsageTests` / `AssistantFieldActionTests` / `AISessionStateTests` / `AIClipboardPromptTests` / `AnalyticsAIOperationTests` / `AppleNaturalLanguageCapabilityTests` / `SpeechHistoryRevisionTests` / `SpeechHistoryDayDeletionTests` / `SpeechHistoryCloudSyncTests` / `PublicContentRefreshServiceTests`
- **剪贴板/工具**`ClipboardSkillSemanticRankerTests` / `ClipboardSemanticAnalyzerTests` / `ClipboardHistoryStoreTests` / `ClipboardHistoryPolicyTests` / `AccountCenterViewModelTests` / `AccountSnapshotLoaderTests` / `AccountAPIClientTests` / `AccountSignInCoordinatorTests` / `AccountCreditPurchaseManagerTests` / `AccountSecurityPrimitiveTests` / `ReferralProfileTests` / `EditTransactionStoreTests` / `EditableInputReferenceTests` / `EditLastInputPromptTests`
- **iCloud 同步**`PersonalDictionaryCloudSyncTests` / `PersonalDictionaryMergeTests` / `SettingsCloudSyncTests` / `UsageStatisticsCloudSyncTests` / `KeyboardUsageRepositoryTests` / `KeyboardUsageModelTests` / `KeyboardUsageUploadCoordinatorTests` / `AnalyticsUploadCoordinatorTests` / `AnalyticsUploadSchedulingTests` / `AnalyticsRepositoryTests` / `AnalyticsModelTests` / `AnalyticsAttributionTests`
- **本地 ASR + 内存**`LocalASRModelCatalogTests` / `KeyboardExtensionMemoryBudgetTests`
- **设置 + 工具**`AppGroupConfigurationTests` / `AppGroupOnboardingStoreTests` / `ConfigurationStoreTests` / `HostAppURLRegistryTests` / `KeyboardTranslationConfigProtectionTests` / `KeychainTests` / `DeviceIntegrityTests` / `MicVoiceAvailabilityTests` / `OpenSourceLicenseCatalogTests` / `TipProductTests` / `EnglishTypingOnDeviceTests` / `TranscriptLanguageDetectorTests`
- **账户/Managed gateway**`AccountAPIClientTests` / `AccountSignInCoordinatorTests` / `AccountCenterViewModelTests` / `AccountCreditPurchaseManagerTests` / `AccountSecurityPrimitiveTests` / `AccountSnapshotLoaderTests`
- **测试支持**`AnalyticsTestSupport` / `KeyboardUsageTestSupport` / `FakeUbiquitousKeyValueStore`
**`OSGKeyboardExtTests/` (16 文件)**
- `EnglishTypingTests` / `RimePersonalDictionaryExporterTests` / `KeyHitTestingTests` / `CandidatePanelExpandTests` / `KeyboardUsageTypingTests` / `LibrimeIntegrationTests` / `ManagedGatewayTests` / `RimeSchemaGeneratorTests` / `KeyboardSurfaceStateTests` / `AnalyticsExtensionPrivacyTests` / `PinyinNextKeyResolverTests` / `TypingTouchTrackerTests` / `FinalChunkRecoveryTests` / `KeyboardStateTests` / `EditHintSchedulerTests` / `ClipboardSuggestionLifecycleTests`
---
## 2. macOS 端完整状态
### 2.1 目标基本事实
- Bundle ID: `com.osgkeyboard.mac`Developer ID + 公证,**沙盒关闭**
- 最低系统:macOS 15.0。Swift 6 / strict concurrency
- 装包名:`OSGKeyboard.app`target 名 `OSGKeyboardMac``PRODUCT_NAME`
- 本地 ASR**MLX Audio Qwen3-ASR 0.6B/1.7B 4-bit**`ThirdParty/mlx-audio-swift/` SPM
- 复用 iOS AppIcon (`OSGKeyboard/AppIcon.icon`) + iOS 资产目录(除 `AppIcon.appiconset`
### 2.2 macOS App 目录结构(`OSGKeyboardMac/`39 Swift 文件,无子目录)
**入口与生命周期**
- `OSGKeyboardMacApp.swift:1` —— `@main struct OSGKeyboardMacApp: App`。**单一** `Window``.windowStyle(.hiddenTitleBar)``.defaultSize(width: 860, height: 600)`+ `MacAppDelegate: NSApplicationDelegate + NSPopoverDelegate` 拥有 `NSStatusItem + NSPopover`340×420 transient+ 浮动 dictation overlay
- **不**用 `MenuBarExtra``OSGKeyboardMacApp.swift:104-105` 注释:与主 Window 同存时图标会消失)
- 订阅 `Notification.Name``.settingsDidSyncFromCloud` / `.personalDictionaryDidSyncFromCloud` / `.usageStatisticsDidSyncFromCloud` / `.speechHistoryDidSyncFromCloud`iCloud pull 刷新
- `onOpenURL``osgkeyboard://seed-demo`DEBUG `DemoDataSeeder`
**侧栏/Shell**
- `MacRootView.swift` —— `NavigationSplitView(.balanced)`,侧栏:`OSGLogoWide` 品牌头 + 5 个 `MacSection` 行(`.dashboard/.history/.dictionary/.styles/.settings`+ "Devices" 页脚;detail 切换 `DashboardView/MacHistoryView/MacDictionaryView/MacPolishStylesView/MacSettingsView`;底部 `MacStatusFooter`
- `MacDictationViewModel.swift:12` —— `MacSection` enum
- `MacTheme.swift:114` —— `MacSystemPalette` 双模式(light 暖白 + dark stepped systemGray6→4),通过 `\.themePalette` 注入,颜色方案变更重渲染
- `MacAppearance.swift` —— `MacAppearancePreference`system/light/dark)存 `mac.appearancePreference``applyToApp` 推到 `NSApp.appearance` 和每个 window
**菜单栏 UI**Status Item + Popover):
- `OSGKeyboardMacApp.swift:140` —— variable-length `NSStatusItem` 模板 NSImagetarget/action = `togglePopover(_:)`
- Popover 内容:`NSHostingController(rootView: MacMenuBarPopover())` (`:214`)。未完成 onboarding → "open the main window" 提示;否则 `MacContentView`(品牌 + record 按钮 + 热键提示 + 状态文本 + 滚动转写 ≤120pt + 模式/翻译/连接状态 + footer)
- Popover 打开:`prepareForPopoverPresentation()` snapshot 前台 App**OSGKeyboard 成为 key 时不丢失粘贴目标**
**录制 overlay**
- `MacDictationOverlayController.swift` —— 一个 borderless non-activating `NSPanel`level `floatingWindow + 1``.canJoinAllSpaces + .fullScreenAuxiliary + .stationary`
- 状态机驱动 show/hide`viewModel.$isRecording/$isPreparingToRecord/$isProcessing` CombineLatest3
- 位置 user-draggable(双击重置底部居中),持久化到 UserDefaults`mac.overlay.hasCustomPosition / centerX / originY`
- `MacDictationOverlayView.swift` —— SwiftUI pill500pt 内容宽)+ 状态点 + 一行转写 + 实时徽章 + `MiniWaveform` + stop 按钮
- `MiniWaveform` 复用 `OSGKeyboardShared/DesignSystem`
**主窗口 5 大页**
- `DashboardView.swift` —— `GeometryReader` 垂直布局:品牌头 + 7 天图(`UsageStatsCluster layout: .split`+ `dictationStage` 卡片(`viewModel.homePreviewText` 累计预览)+ `BottomDictationBar`(翻译选择器 + record 按钮 + ready chip
- `MacHistoryView.swift` —— 按天分组 `SpeechHistoryStore`,每行 copy/delete context menu"Clear" 带确认
- `MacDictionaryView.swift` —— `PersonalDictionary``Entry.Category` 分组,按使用次数 + 术语排序;搜索框过滤;`+``MacDictionaryEntryEditor` sheet
- `MacPolishStylesView.swift` —— `PolishStylePack` 目录(内置 practical+fun + 用户),只读 prompt 详情 / 完整编辑器(`allowsAddedEmoji` 切换 + 2400 字符 prompt 限制);学习卡:语料进度 + "Generate learned style" → `PolishStyleLearningService.generateStyle`
- `MacSettingsView.swift` —— `NavigationStack + ScrollView` 7 段:support developer (Tip) / general (appearance / interface language / recognition language / iCloud sync) / recognition method (cloud vs local) / cloud ASR provider / local ASR model catalog / polish provider / input (hotkey / auto-paste / accessibility 状态) / legal (Privacy Policy + Third-Party Licenses + Restart Onboarding + version)
**Onboarding**
- `MacOnboardingView.swift:21` —— 6 步:`welcome → microphone → accessibility → engine → [.cloudAPI | .localModel]`,根据 `viewModel.config.engineMode` 分支
- Mic 步调 `AVCaptureDevice.requestAccess(for: .audio)`accessibility 步打开 `x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility` 并 1s 后重查 `AXIsProcessTrusted`
- Cloud 步收 LLM provider + key + modellocal 步显示默认 Qwen3-MLX 0.6B + 下载按钮 + 进度 + "skip" 提示
- 状态存 `mac.hasCompletedMacOnboarding`
- 注释 `MacOnboardingView.swift:7-8` —— "Sherpa identifiers and install records are retained only for migration compatibility"**近期从 Sherpa 换到 MLX**
- Settings → About 有 "Restart Onboarding" 按钮
**Legal / Support**
- `MacLegalSettingsViews.swift` —— `MacPrivacyPolicyView``WKWebView` 加载 `OSGKeyboard/Resources/PrivacyPolicy.html`,按 `uiLanguage` 切滚动锚点)+ `MacOpenSourceLicensesView``OpenSourceLicenseCatalog.entries(for: .macOS)` 列表)→ `MacOpenSourceLicenseDetailView`(许可证名 / upstream 链接 / 用途 / 文本)
- `MacSupportDeveloperTipRows.swift` —— StoreKit 2 `TipPurchaseManager.shared` 消耗型打赏行
**DEBUG 工具**
- `osgkeyboard://seed-demo` URL 触发 `DemoDataSeeder.seedRichPlaceholderData`
- `MacAudioRecorder.hasLiveSnapshotSink`DEBUG seam,见 `MacAudioRecorder.swift:143`
### 2.3 全局热键 + 辅助功能 + 粘贴注入
**`MacHotkeyService.swift`**
- 用**两个** `NSEvent.add*MonitorForEvents(matching: .flagsChanged)`global + local
- 区分左/右 Option`NX_DEVICELALTKEYMASK = 0x20` / `NX_DEVICERALTKEYMASK = 0x40`);`MacHotkeyTrigger``rightOption / leftOption / eitherOption`
- 150ms 持有防抖(`scheduleBegin``Task.sleep(150ms)` → fire `onPressBegan`),松开取消未发起的 begin
- `start()` 返回 Accessibility 状态(`globalFlagsMonitor == nil` ⇒ 大概率无权限),DEBUG 下 NSLog
**`MacTextInsertionService.swift`** —— Accessibility 门控合成 ⌘V
- `AXIsProcessTrustedWithOptions([kAXTrustedCheckOptionPrompt: true])` 按需
- `insert(_:autoPaste:targetApp:)`:清 `NSPasteboard.general` → 写转写 → snapshot 原始 pasteboard**所有 representation,不仅是 `.string`**)→ `CGEvent + kVK_ANSI_V + cghidEventTap` 合成 ⌘V → 等 500ms 后**仅当 change count 仍匹配 post-transcript write**才恢复 snapshot(用户/clipboard-manager 写入期间不覆盖)
- `FrontmostAppTracker``:177`)跟踪 `NSWorkspace.didActivateApplicationNotification`**OSGKeyboard popover 激活时**保留原始 paste 目标
- `activate(_:)` 重新激活目标 App,等最多 1s 成为 frontmost
- `shouldRestorePasteboard` + `restoreItems` 是单测 seam
### 2.4 音频捕获
**`MacAudioRecorder.swift`**
- `MacAudioRecording` 协议
- **`AVAudioEngine.inputNode`** + 4096 帧 tap
- 输入格式 → 16kHz mono Float32 via `AVAudioConverter`
- 10 分钟 @16kHz 硬上限 + 30s trim hysteresis(卡住的热键不会无限增长 buffer)
- RMS 平滑(attack 0.5 / decay 0.15
- 权限:`AVCaptureDevice.requestAccess(for: .audio)`;拒/受限抛 `microphoneAccessDenied`
- `makeSnapshotStream() -> AsyncStream<AudioBufferSnapshot>`
- Sink 安装按 generation 追踪,避免前一 sink 延迟终止处理器卸载新 sink
- **无输入设备选择器**`AVAudioEngine.inputNode` 用系统默认;iOS `AudioBufferSnapshot+AVFoundation.swift``project.yml:568` 排除)
**`MacHallucinationFilter.swift`**
- 剥 `<asr_text>` 脚手架、`language XX` 前缀、元数据噪声行(`language/emotion/event` 等)
- `silencePeakThreshold = 0.0005`RMS 低于此跳喂 MLX stream
- `shouldDiscardHotwordDump`(音频能量低 + 转写主要是词典热词 → 丢假触发)
### 2.5 MLX / 本地 ASR 流水线
**关键文件**
- `MacLocalASRService.swift` —— Mac 端公共面:`selectedModelDefinition()` / `isModelInstalled(_:)` / `usesMLXLiveStreaming()` / `transcribe(samples:locale:bias:)`。MLX 后端模型走 `MacMLXStreamingASRProvider``.appleSpeech` 后端或未装模型时落 `MacSpeechLocalASR`。Legacy Sherpa 标识符(`sherpaQwen3/sherpaSenseVoice/sherpaParaformer`)只接受以便抛 `qwen3ModelMissing` —— **明确迁移期处理**
- `MacMLXStreamingASRProvider.swift` —— `actor`,拥有单 `Qwen3ASRModel` 缓存(`cachedModelId / cachedModel / didWarmup`)。`loadModel``LocalASRModelInstallState.modelRootURL(definition)` 读并 `Qwen3ASRModel.fromModelDirectory(root)``warmupIfNeeded` 用 1s 静音预热 Metal kernel。`makeSession` 构建 `StreamingConfig`0.5s decode interval、0.2s boundary、1s boundary boost、encoder window overlap 1s、max cached windows 8、`delayPreset: .realtime`、language hint 从 `MacQwen3LanguageHint.from(locale:)``context: bias?.promptBias``temperature: 0`、512 max tokens/pass、2/2 min-agreement passes、`finalizeCompletedWindows: true`
- `MacMLXStreamingSession.swift` —— 包装 `mlx-audio-swift``StreamingInferenceSession`。订阅 `AsyncStream<TranscriptionEvent>``displayUpdate/ended/provisional/confirmed/stats`);`MacHallucinationFilter` 过滤 `display/ended``onDisplayUpdate` 转发 `display``feed(samples:)` 通过 `FlowCaptureDrainTracker.rms(of:)` 跳过低于 `silencePeakThreshold` 的数据;`stop()` 通过 `CheckedContinuation` 返回最终 `String``peakAudioRMS()` 暴露 hotword-dump 守卫用最大 RMS
- `MacMLXLiveCapture.swift` —— Option 持有本地 ASR 编排:建 streaming session`TaskGroup` 内 2 个并发任务:(1) 等首 `finishSignal``draining.withLock { $0 = true }``await FlowUtteranceEndCoordinator.awaitTailCapture(policy: .macMLX)`)——**显式 `break` 单信号**,永不结束的 stream 不会挂住 group(2) 消费 `AudioBufferSnapshot` stream100ms chunk1600 samples @16kHz)喂 MLXdraining 时 `drainTracker.noteAudio`。然后喂余量,调 `session.stop()`,跑 `MacHallucinationFilter.shouldDiscardHotwordDump` 丢热词-only 转写
- `MacSpeechLocalASR.swift` —— Apple Speech 兜底。16kHz PCM 写临时 WAV`PCMSampleWavEncoder`),`SFSpeechURLRecognitionRequest` 强制 `requiresOnDeviceRecognition`locale 无 on-device 模型时立即失败 → 清晰错误"去 系统设置 → 键盘 → 听写 下载")。Chinese locale 应用 `CustomLanguageModelManager.applyCustomLanguageModel(to:locale:bias:)`(与 iOS 共用)。2× 音频长硬超时 + 30s floor`RecognitionSession` 锁保护单次 resume,识别回调和超时 task 正确竞速
- `MacCloudASRChunkAdapter.swift` —— 薄 `ASRChunkTranscribing` 适配器,包装 `CloudASRClientFactory.make(store:)``MacDictationPipeline`
**模型目录**`OSGKeyboard/Resources/LocalASR/local-asr-catalog.json`2.0.1 状态):
- `schemaVersion: 1`
- `defaultModelId: "qwen3-mlx-0.6b-4bit"`
- `runtimes: []`**空**——runtime 随 bundle 走,MLX 内置)
- 2 个模型:
1. **`qwen3-mlx-0.6b-4bit`** —— Qwen3-ASR 0.6B`backend: mlx``runtimePlatform: macos``sizeBytes: 730000000`~0.7 GB),locale `zh-CN/en-US`,支持热词(`promptOnly`),`badgeKey: mac.localASR.badge.balanced`,路径 `models/qwen3-mlx-0.6b-4bit`。两个下载源(`hfmirror`/`huggingface`)都在 `mlx-community/Qwen3-ASR-0.6B-4bit`。文件:`config.json` / `generation_config.json` / `preprocessor_config.json` / `model.safetensors`~708MB / `model.safetensors.index.json` / `tokenizer_config.json` / `merges.txt` / `vocab.json`
2. **`qwen3-mlx-1.7b-4bit`** —— Qwen3-ASR 1.7B,同上形状,`sizeBytes: 1700000000`~1.7GB),`badgeKey: mac.localASR.badge.quality`。源在 `mlx-community/Qwen3-ASR-1.7B-4bit`
**`local-asr-catalog.json` 中零 iOS 模型**。iOS app 运行时**不**用本目录——iOS 用 iOS 26 `SpeechAnalyzer + DictationTranscriber`,代码路径不同(`OSGKeyboardHostSupport/Services/ASRService.swift`Mac 排除)。
**模型生命周期**
- `loadModel(_:)` (`MacMLXStreamingASRProvider` 第 69-81 行) 缓存 loaded `Qwen3ASRModel` per model id。切模型失效缓存并 re-warm。`warmupIfNeeded` 每个冷模型跑一次
- 卸载(`MacLocalASRModelSettingsViewModel.deleteModel`)→ 共享 `LocalASRModelManager.deleteModel(_:catalog:)` 删磁盘权重;actor 缓存**不**自动清——下次不同 id 的 `loadModel` 自然替换
**Mac 不支持 iOS 风格的"on-device + cloud hybrid"**——只有 engine-mode 开关(`local` → MLX 或 Apple Speech`cloud` → 已配云 ASR)。两种模式共用同一 LLM 润色步。
### 2.6 LLM 润色 + 供应商集成
- `MacDictationPipeline.swift:254` —— 跑的是**同一份** `PolishingService(store: store).polishWithOutcome(...)`iOS `OSGKeyboardShared/Services/PolishingService.swift`
- `PolishContext``appContext`(从目标 App bundle id 通过 `MacAppContextService` 抓)+ `dictionarySupplement`(来自 local-bias `polishFragment`),**和 iOS 同款**
- `MacSettingsView.swift:111` (`polishProviderSection`) 用 `viewModel.polishSelectableProviders``LLMProvider.userSelectablePresets`——**完整集,与 iOS 相同**)。行:provider / API key / baseURL / model`MacProviderModelRow` "拉模型列表" 动作) / thinking 切换 / `MacProviderToolsRow` "Test connection" / 翻译目标
- `MacSettingsView.swift:173` (`asrProviderSection`) 用 `viewModel.asrSelectableProviders``LLMProvider.asrSelectablePresets`)。Volcengine 有自定义 auth-modeAPI key vs App ID + Access Token)通过 `VolcengineASRFields.parse` / `updateMacVolcengine(...)`;其他供应商走通用 `baseURL/apiKey/model` 行 + "Test connection"
- **Mac 没有 Mac 专用 polish/ASR 供应商集**——供应商面和 iOS 一致(DeepSeek/OpenAI/Anthropic 兼容 LLMVolcengine/OpenAI Realtime/Bailian/Alibaba 云 ASR
- **MLX 供应商仅用于本地 ASR**——**无** Mac 端 MLX LLM 润色
### 2.7 持久化
| 类别 | 实现 | Key 前缀 / 位置 |
|---|---|---|
| UserDefaults | 标准 | `mac.*``MacHotkeyService:41` / `MacAppearance:56` / `MacDictationViewModel:103-105` / `MacDictationOverlayController:43-45`)—— 热键、auto-paste、appearance、onboarding、overlay 位置、MLX 模型选择、下载源、界面语言 |
| App Group | 共享 `AppGroupStore` | 与 iOS 同:`PersonalDictionary` / `PolishStyleCatalog+active id` / `translationTarget` / `detected appContext` / `speechHistory` / `usageStatistics` / `engineMode` / `localeId` / LLM API key & baseURL / 云 ASR provider id/apiKey/model/baseURL / iCloud-sync-enabled |
| Keychain | 共享 `Keychain` | `keychain-access-groups: [$(AppIdentifierPrefix)com.osgkeyboard.shared]`entitlement)—— 存 LLM API key |
| iCloud KVS | 5 路全部接好 | `MacICloudSyncBootstrap.swift``AppCloudSync(makeStore:historyDefaults:)`,暴露 `settingsSync/dictionarySync/polishStyleSync/appCloudSync`**entitlement 用 `$(TeamIdentifierPrefix)com.osgkeyboard.ios` 和 iOS 同桶** |
| 语音历史 | `SpeechHistoryStore.shared` + `SpeechHistoryStorage` | UserDefaults 后端 + iCloud `SpeechHistoryCloudSync` |
| 模型权重 | `LocalASRModelInstallState.rootDirectory()` | 由共享 `LocalASRModelManager` 管 |
| 临时文件 | `FileManager.default.temporaryDirectory` | Apple Speech WAV`osg-mac-asr-<uuid>.wav``defer` 删,`MacSpeechLocalASR:71` |
### 2.8 系统集成
- ✅ 菜单栏 status item + popover
- ✅ 全局 ⌥ 热键 + 合成 ⌘V
- ✅ URL scheme `osgkeyboard`(仅 DEBUG `osgkeyboard://seed-demo`
- ❌ Login item / launchd**无** `SMLoginItemSetEnabled`
- ❌ 通知(`UNUserNotificationCenter`)—— HUD 本身就是唯一直达面
- ❌ Touch Bar
- ❌ Services / Share extensions
- ❌ Dock badge
- ✅ `LSApplicationCategoryType: public.app-category.utilities`
- ✅ `ENABLE_HARDENED_RUNTIME: YES`Developer ID + 公证必需)
- ❌ **沙盒关闭** `com.apple.security.app-sandbox: false``project.yml:628`)—— Accessibility + 全局事件 tap + ⌘V 注入都禁在沙盒里
- 发行渠道:**Developer ID + 公证****不进** Mac App Store / TestFlight
- `applicationShouldTerminateAfterLastWindowClosed → false`(菜单栏项保活)
- `LSUIElement: false`(**不**设——启动时仍显 Dock 图标)
### 2.9 macOS 独占(iOS 没有)的能力
1. **`NSStatusItem + NSPopover` 菜单栏 UI** —— SwiftUI `MenuBarExtra` 故意不用,AppKit 为源
2. **全局 ⌥ 持有热键**`NSEvent.add*MonitorForEvents(matching: .flagsChanged)`)区分左/右/任意 Option
3. **Accessibility 门控 ⌘V 注入**`CGEvent + cghidEventTap + AXIsProcessTrusted`
4. **前台 App 跟踪**`FrontmostAppTracker`)让 popover 激活 OSGKeyboard 时不丢粘贴目标
5. **MLX Audio Qwen3 流式 ASR**Qwen3-ASR 0.6B/1.7B 4-bit)通过 `mlx-audio-swift` SPM
6. **系统原生 macOS light/dark 调色板**(暖白 light + stepped gray6→4 dark),从 AppKit semantic colors 解析,让 window chrome 跟随 SwiftUI 颜色方案
7. **浮动 non-activating `NSPanel` HUD**`floatingWindow + 1`),user-draggable 位置持久化
8. **Bundle-ID → `AppContext` 映射**`MacAppContextService.swift`)—— iOS 走 `AppContextDetector` 启发式(键盘读不到前台 bundle id)
9. **手动 MLX 模型下载/暂停/恢复**per-file 字节进度
10. **"Open Storage"** reveal-in-Finder MLX 模型根
11. **Volcengine 云 ASR auth-mode 切换**API key vs App ID + Access Token
12. **同 `URL scheme: osgkeyboard`**`osgkeyboard://seed-demo` DEBUG seed
13. **Apple Speech on-device 兜底**(仅当未装 MLX 模型或 backend == `.appleSpeech`
14. **`NSApplicationDelegate`-driven AppKit 生命周期**`applicationShouldTerminateAfterLastWindowClosed → false`、status-item 创建、overlay controller 启动、热键接续
15. **`MacMLXStreamingASRProvider` actor + 模型缓存 + warmup** 保单 `Qwen3ASRModel` 驻留
16. **两列 `MacInlinePicker`(供应商行)** vs iOS `.menu` `Picker`
17. **`MacProviderSettingRow` 显式 200pt label 列** 防长凭证输入挤
### 2.10 macOS 测试覆盖(`OSGKeyboardMacTests/`3 文件,挂在 Mac App bundle 内)
`project.yml:513-533` 覆盖 `@testable import OSGKeyboard`host target 名 `OSGKeyboardMac` 但装包为 `OSGKeyboard.app`,所以 override `TEST_HOST`)。
1. **`MacDictationViewModelTests.swift`** —— `testCancellingButtonPreparationKeepsGateClosedUntilStartUnwinds`:替换 recorder 为 `SuspendedMacAudioRecorder``start()``CheckedContinuation`),验证第二次 `toggleRecording()`(取消)不重开 `isPreparingToRecord`
2. **`MacAudioRecorderSnapshotStreamTests.swift`** —— `testReplacingSnapshotStreamDoesNotDeadlock` + `testReplacingSnapshotStreamKeepsTheNewSinkAttached`:回归守卫,防 Option 松开时 lock 重入冻结(`AsyncStream.Continuation.finish()` 在调用线程同步跑 `onTermination`handler 重新拿 installer 持有的同 `NSLock` 死锁主线程)。第一个测试在 global queue 装 + 2s semaphore 超时,让回归失败而非挂住套件;第二个测读 `MacAudioRecorder.hasLiveSnapshotSink`DEBUG seam)确认新 stream 顶替前辈
3. **`MacTextInsertionServiceTests.swift`** —— `testRestoreRequiresTranscriptToStillOwnPasteboard`pasteboard change-count 守卫)/ `testRestoringOriginallyEmptyPasteboardClearsTranscript` / `testCapturedBundleIdentifierDrivesPolishContext`Xcode/WeChat/OSGKeyboard 自身经 `MacAppContextService.detectContext(bundleIdentifier:)`
---
## 3. macOS 落后 iOS 的功能(按缺口大小排序)
### 3.1 完全缺失(共享代码已编进 Mac 但零调用)
| 缺失功能 | iOS 路径 | 共享代码状态 | 影响 |
|---|---|---|---|
| **剪贴板历史 + AI 技能** | `OSGKeyboard/Views/ClipboardSettingsView``OSGKeyboardExt/Views/ClipboardKeyboardViews` | `ClipboardHistoryStore` / `ClipboardSemanticAnalyzer` / `ClipboardSkillSemanticRanker` / `AIClipboardSkill` / `AIClipboardSkillLayoutStore` / `AIClipboardPrompt` / `ClipboardHistoryPolicy` **全部编进 Mac 二进制,无任何 Mac 文件引用** | Mac 用户能说话能润色,但**没法**让"复制即智能" |
| **AI 提示轮播 / 上下文技能** | `OSGKeyboard/Services/AIHintRefreshService``OSGKeyboardExt/Views/AIKeyboardView` 内的 hint carousel | `AIHintStore` / `AIHintPool` / `AIHintKeywordExtractor` / `AIHintKeywordCompressor` / `AIHintLocalCatalog` / `AIHintModels` 全部已编,**无 Mac 调用** | Mac 没有 idle 屏的"想一句"灵感卡 |
| **OSG 账户(Sign in with Apple + 积分 + 推荐)** | `OSGKeyboard/Views/Account/AccountCenterView``AccountPurchaseHistoryView` | `OSGKeyboardHostSupport/Features/Account/` 全部编进 Mac`AccountAPIClient` / `SignInWithApple` / `DeviceIntegrity` / `HostPrivateAccountKeychain` / `AccountModels` / `OOBEGrantProvisioningCoordinator`),**无 Mac UI 也无调用**,且 Mac entitlement 无 `com.apple.developer.applesignin` | Mac 只能用 tip 打赏;OSG 积分体系是 iOS 独占 |
| **App Attest** | `OSGKeyboardHostSupport/Features/Account/DeviceIntegrity.swift``SystemAppAttestProvider`)| 已编,但 Mac 无对应 entitlement`com.apple.developer.devicecheck.appattest-environment`)也无调用 | Mac 不在 attestation 流程里 |
| **一方分析(Analytics** | `OSGKeyboard/Services/AnalyticsHostService``OSGKeyboardExt/Services/AnalyticsExtensionService` | `OSGKeyboardShared/Features/Analytics/` 12+ 文件全部编进 Mac**零 Mac 调用** | Mac 用户行为完全无遥测;产品迭代失去数据源 |
| **助手指令(Shortcuts** | `OSGKeyboard/Services/AIAgentShortcutInstaller``AIAgentShortcutRunner`、3 个 bundled `.shortcut``OSGExtractTodos/OSGExtractEvents/OSGSaveToNotes` | 共享 `AIAgentSkill*` / `AIShortcutShareLink` / `AIGenericSkillExport` / `AINoteExport` / `AIMapNavigation` / `AIPhoneNumberActions` 已编,无 Mac 调用;bundled `.shortcut` **Mac 不装**`project.yml:97-105` 仅 iOS 装) | Mac 没法"说一句把当前文本加到 Notes / 提取 Todo" |
| **CLM 用户管理 UI** | iOS 后台用 `CustomLanguageModelManager` | Mac **用**了 CLM`OSGKeyboardMacApp.swift:117``prepareInBackgroundIfNeeded``MacSpeechLocalASR:67-98``applyCustomLanguageModel`),**但**无 `MacCLMSettingsView` | Mac 用户不知道 CLM 在跑;没法改 phrase 库 |
| **最后输入编辑(Edit hint / Last-input edit** | `OSGKeyboardExt/Views/LastInputEditView``EditHintScheduler``LastInputEditCoordinator` | 已编,无 Mac 调用 | Mac 没法长按"改刚才那句" |
| **Flow 跨进程会话** | `OSGKeyboard/Services/FlowSessionManager``FlowPictureInPictureController``FlowTerminationCoordinator``FlowAnalyticsOperationRegistry``FlowASRPostProcessor``FlowDiagnostics``OSGKeyboardExt/Services/KeyboardFlowCoordinator``OSGKeyboardShared/Services/FlowSessionBridge*` | 已编,Mac 仅借用 `FlowSessionKeys.cloudASRWaitTimeout` 一个常量(`MacDictationViewModel.swift:498` | **架构性差异**——Mac 单进程、单用户、单次录制;不需要也不该硬塞 |
| **自定义键盘(Rime/拼音/英文)** | 整个 `OSGKeyboardExt/Typing/` + `OSGKeyboardShared/Typing/**` + `LibrimeEngine` + `RimeResourceInstaller` + `EnglishSuggestionEngine` + `Pinyin*` + `Rime*` | **Mac target 完全排除** `Typing/**``project.yml:585` | **架构性差异**——Mac 不是键盘扩展;librime 是 iOS C/Objective-C++ 框架,Mac 结构性不兼容 |
| **后台任务** | iOS `UIBackgroundModes: audio` + `BGTaskSchedulerPermittedIdentifiers: [com.osgkeyboard.ios.analytics-sync]` | Mac Info.plist **无** `BG*` keys | Mac app 不能后台运行;iOS 的 analytics 同步、Flow PiP 都没 Mac 对应 |
| **Settings 子页面** | `TypingInputSettingsView` / `AIAgentSkillsView` / `Account/*` / `HelpFeedbackView` / `ReleaseNotesSheet` / `AppGroupErrorView` / `NewContactSheet` / `KeyboardPreviewSheet` / 各种 DEBUG demo | 无 Mac 对应 | 见上表 |
| **Universal Links 启动 handoff** | iOS `applinks:osglab.com` + `AppURLHandler` 保留 `sourceApplication` | Mac entitlement 共享同 `com.apple.developer.associated-domains` 但**无** Mac-side 解析 | Mac 不能从网页/邮件 deep-link 进 Onboarding 或 Skill |
### 3.2 部分缺失
| 缺失 | iOS | macOS | 差异点 |
|---|---|---|---|
| **Onboarding 深度** | 7 步 + OOBE 4-feature 实践(用 `OOBEClientInfrastructure` 拿服务器体验金) | 6 步(welcome / mic / accessibility / engine / cloud\|local | 无 Sign in with Apple、无 OOBE 实践 |
| **设置覆盖面** | 7 个子面板 + Account + AI Agent + Clipboard + Typing Input | 5 个段落(support / general / recognition / ASR / local ASR / polish / input / legal | iOS 的 AI Agent / Clipboard / Typing 在 Mac 没对应;Mac 的 input 段含热键 + Accessibility 状态(iOS 没有) |
| **AI Hint / Shortcut 链接** | 提示页 + 3 bundled `.shortcut` | 仅 0Mac 装包不含 `.shortcut` | 整套 AI mode 体验 Mac 无 |
| **Provider 切换粒度** | iOS 通过 iOS `EnginePickerSection` + `ProviderPickerSection` 在 Settings 内 + 主页快捷 | Mac 整段 `recognitionSection` + `asrProviderSection` + `polishProviderSection` 拆开 | iOS 较紧,Mac 较松 |
### 3.3 Mac 自身落后 iOS 之处("应该补"清单)
按"代码复用难度 + 用户价值"排序:
| 优先级 | 功能 | 现状 | 工作量 | 建议 |
|---|---|---|---|---|
| **P0** | **剪贴板历史 + AI 技能面板(Mac UI)** | 共享代码全到位,零 Mac UI | **小**——`MacClipboardHistoryView` + `MacAIClipboardSkillView`(仿 `MacHistoryView`+ 改 `MacSettingsView` 加一个 Clipboard 段 | 立刻做。代码复用 100%,价值高 |
| **P0** | **CLM 设置页(Mac UI** | 后台在跑(`OSGKeyboardMacApp:117`),无 UI | **小**——`MacCLMSettingsView`,仿 `MacLocalASRModelSettingsView` 模板 | 立刻做。让用户能编辑 phrase 库 |
| **P1** | **OSG 账户 + Sign in with Apple + 积分购买** | 共享代码编进 Mac,零 UIMac entitlement 缺 `com.apple.developer.applesignin` + 无 `SKIncludeConsumableInAppPurchaseHistory` | **中**——加 entitlement + 改 `MacICloudSyncBootstrap` 已涵盖大部分,加 `MacAccountCenterView`(仿 `AccountCenterView` 但精简)+ 改 Settings | 一周内可交付 |
| **P1** | **OSG credits 消耗型 IAP500tks/1500tks/3000tks** | 共享 `TipPurchaseManager` 已装;iOS 走 `AccountCreditPurchaseManager` | **中**——`MacAccountCenterView` 出来后一气呵成 | 同上 |
| **P2** | **一方分析(Mac** | 共享 `AnalyticsClient` 等全编,零调用 | **中-大**——`MacAnalyticsHostService` + 隐私 / opt-in + 后台任务(Mac 上走 `NSProcessInfo.thermalState` 节流 + 用户同意时 `NSTask` 跑)| 半年窗口内 |
| **P2** | **AI 提示轮播 / 上下文技能(Mac)** | 共享代码全到位 | **中**——加 `MacAIHintPool` 屏 + `MacAIQuestionService` 入口 + Shortcuts bundle 装载 | 与 P0 剪贴板可联动 |
| **P3** | **助手指令集成** | 共享代码 + iOS bundled `.shortcut` | **中**——Mac 装 `.shortcut` + `AIAgentShortcutInstaller` 调起(`shortcuts://` URL 仍可用)| 与 P2 AI 提示联动 |
| **P3** | **最后输入编辑** | 共享代码全到位 | **大**——需要 Mac 端保留"刚才插入的引用"(`EditableInputReference`),但 Mac 走 Accessibility ⌘V 注入,**没有 `EditableInputReference` 的来源**——需要新协议 | 架构问题,先做 PoC |
| **P3** | **后台同步(Analytics 任务)** | 无 | **中**——`NSTask`/`SMAppService` LaunchAgent 拉 analytics;与 P2 联动 | |
### 3.4 Mac 形态上"不应该补"的功能
| 功能 | 原因 |
|---|---|
| **Flow 跨进程会话** | Mac 单进程、单用户、单次录制——Flow 设计为键盘扩展 ↔ 宿主 App 的两进程 mailbox。强行移植会引入不必要的 IPC 开销。**Mac 用 `MacDictationPipeline` + `MacDictationViewModel` + `MacDictationOverlayController` 三件套已足够** |
| **PiP keep-alive** | macOS 无系统 PiP。`MacDictationOverlayController` 的 NSPanel 已占位 |
| **Rime / 拼音 / 英文 autocorrect** | Mac 不是键盘扩展,没输入面。`LibrimeEngine` 是 iOS C/Objective-C++ xcframework**Mac 结构性不兼容**。即便 Mac 装键盘扩展(`Designed for iPad` 那种),librime 也需要重编译 |
| **`UIBackgroundModes: audio`** | Mac 没用——按住说话时 NSPanel 是 non-activating,不需要 audio session 后台保持 |
| **`UIOpenURLContext.options.sourceApplication`** | iOS 26 only APIMac 走 `NSWorkspace.didActivateApplicationNotification` 已解决前台 App 跟踪 |
| **`AVAudioApplication.requestRecordPermission`** | iOS 17+ onlyMac 走 `AVCaptureDevice.requestAccess(for: .audio)` |
| **App Group 跨进程 mailbox** | Mac 单进程不需要。但 `AppGroup` + iCloud KVS 是用户态多设备同步用的,**保留** |
| **iOS 风格的 idle 屏** | Mac 主窗口即 dashboard,不需要 idle 屏的"AI hint 轮播";但 `MacAIHintPool` 屏可以放在 Dashboard 顶部,与 P2 联动 |
| **`LSApplicationQueriesSchemes`** | iOS onlyMac 无 `canOpenURL` 限制 |
| **`SKIncludeConsumableInAppPurchaseHistory` / Mac storekit 文件** | Mac App Store 与 iOS App Store 独立 SKU;目前 Mac 装包是 Developer ID 渠道,**不进 Mac App Store**,所以 storekit 不需要 |
---
## 4. 适合在 Mac 上做开发和移植的功能
### 4.1 应该现在做的(P0
#### ① 剪贴板历史 + AI 技能面板
**为什么 Mac 适合**:共享代码 100% 到位(`ClipboardHistoryStore` / `ClipboardSemanticAnalyzer` / `ClipboardSkillSemanticRanker` / `AIClipboardSkill` / `AIClipboardPrompt`),iOS 8 个 UI 调试稳定,只需套 Mac 风格。
**具体工作**
- 新增 `OSGKeyboardMac/Views/MacClipboardHistoryView.swift`(仿 `MacHistoryView`
- 新增 `OSGKeyboardMac/Views/MacAIClipboardSkillView.swift`(仿 `AIAgentSkillsView`,精简为 Read/Edit/Disable
- `MacSettingsView.swift``clipboardSection`(含 toggle 启用历史 / toggle 启用技能候选条 / "Manage Skills" 链接 / "Open History" 链接)
- `MacDictationViewModel` 订阅 `ClipboardHistoryStore.entries``@Published var clipboardEntries`
- 复用 `ClipboardCaptureCoordinator` 不可(ext-only)——需要 Mac 端 `MacClipboardMonitor``NSPasteboard.general.changeCount` 轮询 + sanitize
**测试**`MacClipboardMonitorTests`(用 in-memory pasteboard 模拟)/ `MacClipboardHistoryViewModelTests`
**工作量**1-2 周
#### ② CLM 设置页
**为什么 Mac 适合**:Mac 后台已经在跑 CLM(`OSGKeyboardMacApp:117`),只是用户管不到。
**具体工作**
- 新增 `OSGKeyboardMac/Views/MacCLMSettingsView.swift`(仿 `MacLocalASRModelSettingsView`
- 复用 `CustomLanguageModelManager.shared.state``@Published idle/preparing/ready/failed`
- 复用 `OSGKeyboard/Resources/HostCLM/v1/OSGKeyboardCLM.bin`(已在 Mac 装包)
- 复用 `PersonalDictionary` 共享存储(用户输入的 phrase 库 + iCloud 同步)
- 提供 phrase 列表 / 编辑 / 触发"重编译"按钮
**测试**`MacCLMSettingsViewModelTests`(验 `state` 状态机)
**工作量**1 周
### 4.2 应该中期做的(P1
#### ③ OSG 账户 + Sign in with Apple + 积分购买
**为什么 Mac 适合**:共享代码编进 Mac 都没报错,缺的是 entitlement + UI。
**先决条件**
- `OSGKeyboardMac.entitlements``com.apple.developer.applesignin: [Default]`
- `OSGKeyboardMac/Info.plist``SKIncludeConsumableInAppPurchaseHistory: true`
- (选做)把 `OSGKeyboard.storekit` 内容 mirror 到 `OSGKeyboardMac.storekit` 或共享
**具体工作**
- 加 `MacAccountCenterView`(仿 iOS `AccountCenterView` 但精简)
- 复用 `LiveAccountServices` / `AccountSessionCoordinator``@MainActor` state machine
- 复用 `AccountCreditPurchaseManager`StoreKit 2 product list → 服务器校验)
- Settings → 加 "Account" 段,含 sign-in / 积分余额 / 购买历史 / 登出 / 删号
**测试**`MacAccountSessionCoordinatorTests`in-memory `AccountAPIClient` fake
**工作量**2-3 周
#### ④ Mac App Attest / DeviceCheck
**为什么 Mac 适合**iOS attestation 后端已经在用,Mac 端可以通过 `DCAppAttestService`macOS 13+,需要 Mac Catalyst 但当前 `SUPPORTS_MACCATALYST: NO`)或者改用 DeviceCheck `DCDevice.generateToken`macOS 13+ available without Catalyst)。
**先决条件**
- `OSGKeyboardMac.entitlements``com.apple.developer.devicecheck.appattest-environment: $(APP_ATTEST_ENVIRONMENT)`
- 若走 App Attest,需要 `SUPPORTS_MACCATALYST: YES`(改大改)或改 DeviceCheck-only
**具体工作**
- `MacDeviceIntegrityCoordinator`(仿 iOS,但用 `DCDevice`
- `AccountAPIClient` 复用,调 `/v1/integrity/attest`
**工作量**1-2 周(仅 Mac);+ Catalyst 决策时间
### 4.3 应该长期做的(P2
#### ⑤ 一方分析(Mac
**为什么 Mac 适合**:Mac 用户行为和 iOS 同样有价值(知道用户用不用 MLX 模式、热键触发频率、词条覆盖、润色样式流行度)。
**先决条件**
- 共享 `AnalyticsClient` / `AnalyticsRepository` 已经完备
- Mac 上加 `MacAnalyticsHostService`(仿 iOS):监听 `viewModel.$isRecording/$isPreparingToRecord/$isProcessing` 推送事件
- 隐私 / opt-in:在 onboarding 末加一步
**挑战**
- Mac 后台无 `BGTaskScheduler`——改用 `NSProcessInfo.thermalState` + `ProcessInfo.isLowPowerModeEnabled` 节流,**或** `SMAppService` 拉个 LaunchAgent
- `AnalyticsUploadCoordinator` 的移动策略(threshold 20 / 60s flush)要 Mac 化
**工作量**3-4 周
#### ⑥ AI 提示轮播 / 上下文技能
**为什么 Mac 适合**Mac dashboard 缺内容——除了使用统计,闲置时可推"想一句"灵感卡。
**具体工作**
- `MacAIHintPool` 嵌在 `DashboardView` 顶部(仿 iOS `AIKeyboardView` 的 carousel 区域)
- 复用 `AIHintStore` / `AIHintPool` / `AIHintKeywordExtractor` / `AIHintLocalCatalog`
- 上下文技能入口放 `MacContentView`popover 内 record 按钮旁的下拉)
**工作量**2-3 周
#### ⑦ 助手指令集成
**为什么 Mac 适合**Mac 上有 Shortcuts.app(更成熟),bundled `.shortcut` 装入 Mac App bundle 即可。
**具体工作**
- 装 `OSGKeyboard/Resources/Shortcuts/*.shortcut` 进 Mac bundle
- 复用 `AIAgentShortcutInstaller` / `AIAgentShortcutRun`
- popover / dashboard 加技能快捷入口
**工作量**1-2 周
### 4.4 不应该在 Mac 做的(但代码可以清理)
| 共享代码 | 为什么不该在 Mac 跑 | 处置 |
|---|---|---|
| `OSGKeyboardHostSupport/Features/Account/*`Mac 已编) | App Attest 路径 iOS 专属;Mac 没 entitlement | 等 ④ 决策后从 Mac target 排除或接 UI |
| `OSGKeyboardShared/Features/Analytics/*`(Mac 已编) | 无 Mac 端调用 | 等 ⑤ 决策后从 Mac target 排除或接服务 |
| `OSGKeyboardShared/Features/ManagedGateway/*`Mac 已编) | 是账户 grant 体系,与 ③ 绑定 | 等 ③ 决策后处理 |
| `OSGKeyboardShared/Services/AIClipboard*`(Mac 已编) | 与 ① 绑定 | ① 实现后从"无调用"变"有调用" |
| `OSGKeyboardShared/Services/AIHint*`(Mac 已编) | 与 ⑥ 绑定 | 同上 |
| `OSGKeyboardShared/Services/AIUserSkill*` / `AIAgentSkill*`(Mac 已编) | 与 ③⑦ 绑定 | 同上 |
| `OSGKeyboardShared/Services/Flow*`(Mac 已编) | Mac 架构不兼容 | **立刻**从 Mac target 排除(白付编译器开销) |
| `OSGKeyboardShared/Services/KeyboardState.swift`(Mac 已排除)| 已经是 Mac 排除 ✓ | 无 |
| `OSGKeyboardShared/Services/EditTransactionStore.swift` 等 | 与"最后输入编辑"绑定 | 见 P3 |
### 4.5 立即可清的"白编译"清单
```
OSGKeyboardShared/Services/FlowSessionBridge.swift + 所有 +* 文件
OSGKeyboardShared/Services/FlowSessionKeys.swift
OSGKeyboardShared/Services/FlowSessionDarwin.swift
OSGKeyboardShared/Services/FlowSessionPolicy.swift
OSGKeyboardShared/Services/FlowStartTransactionPolicy.swift
OSGKeyboardShared/Services/FlowKeyboardPolicies.swift
OSGKeyboardShared/Services/FlowHandoffPolicy.swift
OSGKeyboardShared/Services/UtteranceStreamChunker.swift
OSGKeyboardShared/Services/UtteranceBatchFallbackPolicy.swift
OSGKeyboardShared/Services/UtteranceTranscriptGuard.swift
OSGKeyboardShared/Services/UtteranceTranscriptStitcher.swift
OSGKeyboardShared/Services/TranscriptOverlapUtilities.swift
OSGKeyboardShared/Services/ProgressiveDictationTranscriptAccumulator.swift # 已排除
OSGKeyboardShared/Services/EditTransactionStore.swift
OSGKeyboardShared/Services/EditUsageMetricsStore.swift
OSGKeyboardShared/Services/EditLastInputPromptComposer.swift
OSGKeyboardShared/Services/EditOutputValidator.swift
OSGKeyboardShared/Models/Flow*.swift (全部)
OSGKeyboardShared/Models/EditableInputReference.swift
OSGKeyboardShared/Models/EditSessionState.swift
OSGKeyboardShared/Models/FlowUtterance*.swift
OSGKeyboardShared/Models/FlowInactivityDuration.swift
OSGKeyboardShared/Utilities/FlowCaptureTailDrain.swift
OSGKeyboardShared/Utilities/FlowUtteranceEndCoordinator.swift
OSGKeyboardShared/Utilities/FlowUtterancePCMStore.swift
OSGKeyboardShared/Utilities/FlowTrace.swift
OSGKeyboardShared/Utilities/FlowPipelineDiagnostics.swift
OSGKeyboardShared/Utilities/HostMemoryBudget.swift
OSGKeyboardShared/Utilities/KeyboardExtensionMemoryTelemetry.swift # 已排除
OSGKeyboardShared/Features/Analytics/ # 全部 12+ 文件
OSGKeyboardShared/Features/ManagedGateway/ # 全部 8+ 文件
OSGKeyboardHostSupport/Features/Account/ # 全部
OSGKeyboardHostSupport/Services/CloudASR/AlibabaVocabularySync.swift # iOS-only API
OSGKeyboardHostSupport/Services/ASRChunkTranscribing.swift # 需检查
```
**建议**):在 `project.yml:558-602` 给 Mac target 的 source list 加 `excludes:`,避免编译这些文件后被链接器裁掉造成的 dead-code 体积。
---
## 5. iOS 端需要"修一下"或注意的地方
报告主体是 macOS 差距,但 iOS 端顺手列出 5 个明显可改进点(不修不影响功能,但提升质量):
1. **`OSGKeyboardHostSupport/Features/Account/*` 整块**虽然不通过 Mac UI 暴露,但通过 source include 编进 Mac 二进制——长期应从 Mac target 排除或加 `MacAccountServices` 实际使用
2. **`OSGKeyboardShared/Features/Analytics/*`** 同上——Mac 编译进二进制但无任何调用
3. **`OSGKeyboardShared/Features/ManagedGateway/*`** 同上
4. **`project.yml:570-602`** 的 Mac target source list 很长且依赖手写排除,**建议**拆出 `OSGKeyboardMacExcludes.yml` 维护
5. **`OSGKeyboardShared/Typing/EnglishSystemLexicon.swift:11`** 是**唯一**有 `import UIKit` 的 Shared 文件——是 iOS ext 独享的 QuickType 数据源。考虑改名/移动到 `OSGKeyboardExt/` 减面
6. **iOS `FlowSessionManager` ~3500 LOC** 是单一 god object;建议把 `FlowPictureInPictureController` / `FlowTerminationCoordinator` / `FlowAnalyticsOperationRegistry` 三个生命周期拥有者抽离
7. **iOS `appDelegate` 反弹 `.onOpenURL`** (`AppURLHandler.swift`) 注释里写 `UIOpenURLContext.options.sourceApplication` iOS 26 only——这个 API 在 iOS 26 还在吗?值得 verify(影响 deep-link host-return 白名单)
8. **`KeyboardViewController` `didReceiveMemoryWarning` (`:390-404`)** 强制回 voice surface——typing 引擎是最大内存消费者,但每次收到警告就退出 typing 体验略激进;考虑加 cooldown
---
## 6. 实施建议
### 6.1 短期(未来 1-2 周)
- **任务 A:从 Mac target 排除 `Flow*` / `Edit*` / `Analytics*` / `ManagedGateway*` / `Account*`HostSupport)所有共享代码**
- 修改 `project.yml:570-602`,加 `excludes:` 列表
- 验证 Mac target 仍能 build & test pass
- 节省 Mac 二进制 ~2-4 MB + 减少编译时间 ~10-15s
- **任务 B:Mac 剪贴板历史 + AI 技能面板**P0 ①)
- **任务 CMac CLM 设置页**P0 ②)
### 6.2 中期(1-2 月)
- **任务 DOSG 账户 + Sign in with Apple + 积分**P1 ③)
- **任务 EApp Attest 决策 + 实施**P1 ④)
### 6.3 长期(3-6 月)
- **任务 FMac 一方分析**P2 ⑤)
- **任务 G:AI 提示轮播 / 上下文技能**P2 ⑥)
- **任务 H:助手指令集成**P2 ⑦)
### 6.4 决策项
| 决策 | 选项 | 影响 |
|---|---|---|
| **App Attest 路径** | (a) Catalyst + `DCAppAttestService`macOS 13+<br>(b) 仅 `DCDevice.generateToken`(无 attestation<br>(c) 不做 | (a) 需开 `SUPPORTS_MACCATALYST: YES` 改大改;<br>(b) 安全性较弱但改动小;<br>(c) Mac 不在 attestation 内 |
| **Mac 走 Mac App Store 还是 Developer ID** | (a) 维持 Developer ID(现状)<br>(b) 走 Mac App Store | (a) 维持沙盒关闭,Accessibility 自由;<br>(b) 需重写热键 + 注入方案(沙盒内 Accessibility 拿不到) |
| **是否上 Rime 替代方案** | (a) 不做(Mac 不是键盘)<br>(b) 内部输入法,绕过 librime | (b) 工作量极大,且与产品形态不符 |
| **Mac 是否支持 iOS 风格的"on-device + cloud hybrid"** | (a) 仅 engine-mode toggle(现状)<br>(b) 支持每句 hybridlocal first,失败 fallback cloud | (b) 需要重新设计 MLX streaming 端 |
---
## 7. 关键引用
| 主题 | 文件 | 行号 |
|---|---|---|
| iOS 宿主入口 | `OSGKeyboard/OSGKeyboardApp.swift` | 1 |
| iOS Flow 单一拥有者 | `OSGKeyboard/Services/FlowSessionManager.swift` | 1 (~3500 LOC) |
| iOS 键盘入口 | `OSGKeyboardExt/KeyboardViewController.swift` | 33-38 |
| iOS 键盘生命周期 | `OSGKeyboardExt/KeyboardViewController.swift` | 148-655 |
| iOS 共享 ASR 协议 | `OSGKeyboardShared/Services/PolishingService.swift` | 34-105 |
| iOS HostSupport ASR 工厂 | `OSGKeyboardHostSupport/Services/ASRService.swift` | 1 |
| iOS HostSupport 云 ASR 工厂 | `OSGKeyboardHostSupport/Services/CloudASR/CloudASRClientFactory` | 44 |
| Mac 入口 | `OSGKeyboardMac/OSGKeyboardMacApp.swift` | 1 |
| Mac MLX 流式 provider | `OSGKeyboardMac/MacMLXStreamingASRProvider.swift` | 1 |
| Mac MLX 流式 session | `OSGKeyboardMac/MacMLXStreamingSession.swift` | 1 |
| Mac MLX 实时捕获编排 | `OSGKeyboardMac/MacMLXLiveCapture.swift` | 1 |
| Mac 热键 | `OSGKeyboardMac/MacHotkeyService.swift` | 1 |
| Mac 文本注入 | `OSGKeyboardMac/MacTextInsertionService.swift` | 1 |
| Mac 音频 | `OSGKeyboardMac/MacAudioRecorder.swift` | 1 |
| Mac 字幕过滤器 | `OSGKeyboardMac/MacHallucinationFilter.swift` | 1 |
| Mac 字典流水线 | `OSGKeyboardMac/MacDictationPipeline.swift` | 254 |
| Mac 模型目录 | `OSGKeyboard/Resources/LocalASR/local-asr-catalog.json` | 1-111 |
| Mac target 定义 | `project.yml` | 543-674 |
| Mac target 共享 source 排除 | `project.yml` | 558-602 |
| Mac entitlements | `OSGKeyboardMac/OSGKeyboardMac.entitlements` | 1 |
---
## 8. 附录:版本与里程碑
| 版本 | 日期 | 关键 Mac 相关变更 |
|---|---|---|
| 2.0.1 (build 90) | 2026-08-21 | (无 Mac-specific changelog 条目) |
| 2.0.0 (build 85) | 2026-08-19 | "Optional OSG account"iOS only,但相关 HostSupport 代码编进 Mac|
| 1.8.0 (build 72) | 2026-08-14 | "Mac Styles / Settings follow the same pairing"(设计系统共用)+ "Mac model downloads"**MLX 模型下装载入**|
| Pre-1.8.0 | — | Sherpa → MLX 迁移(`MacOnboardingView.swift:7-8` 注释)|
---
**报告完。**
下次更新窗口:2026-09-XX 复审 P0 ① ② 实施情况 + 决策项 6.4 落地。
@@ -1,28 +0,0 @@
# Personal Dictionary iCloud KVS — Manual Verification Checklist
Use this on **macOS with Xcode 16+** and at least two devices signed into the **same Apple ID** with iCloud Drive / iCloud enabled.
## Prerequisites
1. In Apple Developer Portal, enable **iCloud****Key-value storage** for `com.osgkeyboard.ios`.
2. Regenerate provisioning profiles after entitlements change.
3. Run `xcodegen generate` and install a fresh build on each device.
## Scenarios
| # | Steps | Expected |
|---|--------|----------|
| 1 | Device A: open Personal Dictionary, enable **Sync via iCloud**, add term `TestWordA` | Toggle stays on; term appears locally |
| 2 | Device B: open app → Personal Dictionary tab | `TestWordA` appears after pull (may take up to ~1 min) |
| 3 | Device B: add `TestWordB` | Device A eventually shows both terms |
| 4 | Both devices: edit same term offline, then go online | Newer edit wins; aliases union when terms match |
| 5 | Device A: delete a term | Term disappears on Device B after sync |
| 6 | Device A: disable iCloud sync | Local dictionary remains; Device B stops receiving new edits from A |
| 7 | Sign out of iCloud on one device | App keeps local dictionary; sync errors may surface in UI |
| 8 | Keyboard extension on Device A | Uses App Group cache immediately after main-app save — no iCloud wait |
## Notes
- KVS propagation is **eventual**; force-quit and reopen the app to speed up pulls.
- The keyboard extension never talks to iCloud directly; only the main app syncs.
- Payload limit is ~1 MB per key; very large dictionaries should show the “too large” error.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

-73
View File
@@ -1,73 +0,0 @@
# App Store Screenshots
> ⚠️ **PLACEHOLDERS.** The 10 PNGs in this directory are
> automatically generated blanks produced by
> `scripts/generate_screenshot_placeholders.py` and must be
> **replaced with real Simulator screenshots** before App Store
> submission. They use the correct dimensions (1290×2796 for
> 6.7", 1179×2556 for 6.1") so the upload validator will accept
> them, but they contain no real UI.
## Required dimensions (2026)
| Size | Devices | Dimensions | Apple requirement |
|------|---------|-----------|---|
| 6.7" | iPhone 17 Pro Max, 17, 16 Pro Max, 16 Plus, 15 Pro Max, 15 Plus | 1290 × 2796 px | **Required** (3-10 images) |
| 6.1" | iPhone 17 Pro, 17, 16 Pro, 16, 15 Pro, 15, 14 Pro, 14 | 1179 × 2556 px | **Required** (3-10 images) |
| 5.5" | iPhone 8 Plus (legacy) | 1242 × 2208 px | Optional since 2024 |
iPad screenshots are not required because OSGKeyboard is iPhone-only
(`TARGETED_DEVICE_FAMILY = 1`).
## Layout
```
docs/screenshots/
├── 6.7/ ← 1290×2796 (iPhone 17 Pro Max / 17)
│ ├── 01-keyboard-default.png
│ ├── 02-flow-session.png
│ ├── 03-on-device-asr.png
│ ├── 04-llm-polish.png
│ └── 05-providers.png
└── 6.1/ ← 1179×2556 (iPhone 17 Pro / 17)
├── 01-keyboard-default.png
├── 02-flow-session.png
├── 03-on-device-asr.png
├── 04-llm-polish.png
└── 05-providers.png
```
## How to capture real screenshots
1. Open `OSGKeyboard.xcodeproj` in Xcode 26+
2. Run on **iPhone 17 Pro** simulator (6.1" set) and **iPhone 17 Pro Max** simulator (6.7" set)
3. For each scene:
```bash
# Take a screenshot of the simulator window
xcrun simctl io booted screenshot ~/Desktop/shot.png
```
4. Process for App Store (Apple rejects frames containing the device bezel — full-screen content only):
```bash
# The simulator screenshot already has a thin device frame.
# Open in Preview, crop to full screen (⌘+K with ⌥ for precision),
# export as PNG at 1290×2796 or 1179×2556.
sips -z 2796 1290 shot.png --out final-6.7.png
sips -z 2556 1179 shot.png --out final-6.1.png
```
5. Replace the placeholders with the real captures, keeping the
same filenames so the App Store Connect → Version → Uploads UI
auto-pairs by file.
## Scenes to capture
The 5 placeholders are intentional scene placeholders. Capture these
*exact* screens, in this order:
1. **Keyboard at rest** — the iOS keyboard, OSGKeyboard mode, no recording
2. **Flow session active** — keyboard with the green/orange recording ring,
partial transcript visible in the host text field
3. **On-device ASR** — Settings view with the locale list, the on-device
indicator (iPhone icon) visible next to ≥ 3 supported locales
4. **LLM polish** — Settings view with the API provider card, a sample
"polish" transformation shown in the inline preview
5. **Providers** — API Settings card scrolled to show all 6 provider logos