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.
@@ -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,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 +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 |
@@ -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 +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 |
@@ -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 +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 |
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"filename" : "mimo.png",
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
},
|
|
||||||
"properties" : {
|
|
||||||
"template-rendering-intent" : "template"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
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 |
@@ -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 +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 |
@@ -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 +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 +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 |
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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.
|
|
||||||
@@ -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
|
|
||||||
@@ -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 |
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
# Typing Keyboard Baseline (Phase 0)
|
|
||||||
|
|
||||||
Frozen reference for NanoMouse × OSGKeyboard fusion.
|
|
||||||
|
|
||||||
## NanoMouse reference
|
|
||||||
|
|
||||||
| Field | Value |
|
|
||||||
|-------|--------|
|
|
||||||
| Repo | https://github.com/xjwhnxjwhn/nanomouse |
|
|
||||||
| Frozen commit | `a6177d898a01662ce551b43a01cf82a9f84ca54c` (2026-07-29 tip at clone) |
|
|
||||||
| Local clone (gitignored) | `.refs/nanomouse/` |
|
|
||||||
|
|
||||||
## KeyboardKit / key shell
|
|
||||||
|
|
||||||
NanoMouse does **not** SPM-pin upstream KeyboardKit Pro. It vendors an MIT KeyboardKit tree inside:
|
|
||||||
|
|
||||||
`ios/Packages/HamsterKeyboardKit/Sources/KeyboardKit/` (see that folder’s `LICENSE`).
|
|
||||||
|
|
||||||
OSG ships a **lean SwiftUI key shell** in-repo (`OSGKeyboardExt/Typing/`)
|
|
||||||
behind `TypingLayoutProviding`; KeyboardKit is not linked.
|
|
||||||
|
|
||||||
**KeyboardKit Pro is not used.**
|
|
||||||
|
|
||||||
## Chinese schema / lexicon
|
|
||||||
|
|
||||||
| Choice | Detail |
|
|
||||||
|--------|--------|
|
|
||||||
| Product intent | Full pinyin + Microsoft/Sogou double pinyin + opt-in fuzzy pairs |
|
|
||||||
| Engine | librime 1.17.0 via static XCFramework (BSD-3-Clause) |
|
|
||||||
| Binary package | `ghostflyby/librime-xcframework` `1.17.0-pack.1`, checksum `0f0fc13b…1164` |
|
|
||||||
| Baseline | rime-pinyin-simp (Apache-2.0) |
|
|
||||||
| Modern words | Jieba frequencies + phrase-pinyin-data + pinyin-data (MIT) |
|
|
||||||
| Generated lexicon | `Resources/Typing/Rime/osg_pinyin.dict.yaml` (~365K entries) |
|
|
||||||
| User learning | librime userdb in App Group |
|
|
||||||
| Explicitly excluded | rime-ice / rime-double-pinyin (GPL), Luna / Essay (LGPL) |
|
|
||||||
|
|
||||||
The dictionary is rebuilt deterministically by
|
|
||||||
`Scripts/typing/build_rime_dictionary.py`; `manifest.json` pins every source
|
|
||||||
commit and SHA-256.
|
|
||||||
|
|
||||||
## Memory / height budget
|
|
||||||
|
|
||||||
| Mode | Target height | Memory notes |
|
|
||||||
|------|---------------|--------------|
|
|
||||||
| Voice | 281 pt (`KeyboardRootView.totalHeight`) | Matches typing height; no typing engine loaded |
|
|
||||||
| Typing | 281 pt (`TypingRootView.totalHeight`) | Host-prebuilt Rime data; extension opens one session |
|
|
||||||
| RSS goal | Typing peak < 50 MB | Session closes on voice switch / memory warning |
|
|
||||||
|
|
||||||
## Success criteria (Phase 1)
|
|
||||||
|
|
||||||
- Top-right tab switches voice ↔ typing; recording/processing locks voice.
|
|
||||||
- English QWERTY + 123 / basic symbols.
|
|
||||||
- Full pinyin and Microsoft/Sogou double pinyin produce phrase candidates.
|
|
||||||
- Fuzzy pairs default off and are enabled individually in Settings.
|
|
||||||
- No GPL/LGPL input data in the app; NOTICE lists exact source licenses.
|
|
||||||
@@ -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="<Group>">
|
|
||||||
<g id="_Group_2" data-name="<Group>">
|
|
||||||
<g id="_Group_3" data-name="<Group>">
|
|
||||||
<path id="_Path_" data-name="<Path>" 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="<Path>" 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="<Group>">
|
|
||||||
<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 |
@@ -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="<Group>">
|
|
||||||
<g id="_Group_2" data-name="<Group>">
|
|
||||||
<path id="_Path_" data-name="<Path>" 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="<Path>" 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 |
@@ -1,385 +0,0 @@
|
|||||||
# iOS PiP 语音会话保活规划
|
|
||||||
|
|
||||||
> **文档状态**:产品与架构规划(待验证,未进入实现)
|
|
||||||
> **适用范围**:iOS 主 App + 键盘扩展 + Live Activity
|
|
||||||
> **目标版本**:待产品验证后确定
|
|
||||||
> **创建日期**:2026-07-26
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Executive Summary
|
|
||||||
|
|
||||||
### 1.1 目标
|
|
||||||
|
|
||||||
在不要求 OSGKeyboard 长时间占用麦克风的前提下,尽量保持主 App 可响应键盘扩展的听写指令,降低用户在宿主 App 与 OSGKeyboard 之间反复切换的频率。
|
|
||||||
|
|
||||||
核心方向是将两个当前耦合的能力拆开:
|
|
||||||
|
|
||||||
1. **会话可用性**:主 App 仍可接收键盘命令。
|
|
||||||
2. **麦克风采集**:仅在用户明确开始听写时启用,完成后立即释放。
|
|
||||||
|
|
||||||
PiP(画中画)只承担系统可见的多任务会话载体,不绕过麦克风授权,也不应使用静音音频循环伪造后台活动。
|
|
||||||
|
|
||||||
### 1.2 核心结论
|
|
||||||
|
|
||||||
| 决策 | 规划选择 |
|
|
||||||
|------|----------|
|
|
||||||
| 产品定位 | 将 PiP 作为可选的「免切换模式」,不替代普通 Flow |
|
|
||||||
| 麦克风策略 | PiP 空闲时关闭;键盘点按听写后按需激活 |
|
|
||||||
| 默认策略 | 保留当前隐私友好的 5 分钟 Flow;PiP 由用户主动开启 |
|
|
||||||
| 降级路径 | PiP 不可用或失效时回落到现有 `startflow` 冷启动流程 |
|
|
||||||
| 状态展示 | PiP 显示有意义的语音会话状态;Live Activity 继续负责锁屏与灵动岛 |
|
|
||||||
| 禁止方案 | 不播放静音文件保活,不使用定位或 VoIP 等无关后台模式 |
|
|
||||||
| 上线方式 | 先做真机技术验证和 TestFlight 审核验证,再决定正式产品化 |
|
|
||||||
|
|
||||||
### 1.3 非目标
|
|
||||||
|
|
||||||
- 不让键盘扩展直接访问麦克风;这是 iOS 平台限制。
|
|
||||||
- 不承诺 App 被用户强制退出后仍可免切换听写。
|
|
||||||
- 不承诺电话、Siri、相机或其他录音 App 抢占音频设备时继续录音。
|
|
||||||
- 不用 PiP 绕过麦克风权限、隐私提示或系统音频策略。
|
|
||||||
- 第一阶段不重写 ASR、润色、App Group 或 Darwin 通知管线。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 问题定义
|
|
||||||
|
|
||||||
### 2.1 平台约束
|
|
||||||
|
|
||||||
iOS 键盘扩展无法直接申请或使用麦克风。系统级语音键盘因此必须采用:
|
|
||||||
|
|
||||||
```text
|
|
||||||
键盘扩展
|
|
||||||
→ 发送开始/停止命令
|
|
||||||
→ 主 App 采集并转写
|
|
||||||
→ App Group 返回结果
|
|
||||||
→ 键盘插入文本
|
|
||||||
```
|
|
||||||
|
|
||||||
当主 App 被系统挂起或终止时,键盘无法即时启动录音,只能打开主 App 重新建立会话。当前 Flow 通过持续运行 `AVAudioEngine` 输入链路换取后台可用性,但会带来麦克风长期占用、橙色隐私指示、电量消耗和音频冲突。
|
|
||||||
|
|
||||||
### 2.2 用户问题
|
|
||||||
|
|
||||||
| 用户感知 | 当前根因 | 目标变化 |
|
|
||||||
|----------|----------|----------|
|
|
||||||
| 频繁跳转主 App | 后台主进程不可响应 | PiP 有效时直接响应键盘命令 |
|
|
||||||
| 麦克风指示长时间亮起 | Flow 会话级连续采集 | 空闲时释放麦克风 |
|
|
||||||
| 耗电或发热 | 音频引擎持续采样和处理 | 仅听写期间采样 |
|
|
||||||
| 其他 App 无法使用麦克风 | OSGKeyboard 持有输入设备 | 听写结束后主动释放 |
|
|
||||||
| 不知道会话是否可用 | Flow、麦克风和进程状态混为一体 | 分开展示「免切换已就绪」和「正在录音」 |
|
|
||||||
|
|
||||||
### 2.3 成功定义
|
|
||||||
|
|
||||||
PiP 模式下,用户应能:
|
|
||||||
|
|
||||||
1. 在 OSGKeyboard 主 App 中主动开启免切换模式。
|
|
||||||
2. 将 PiP 小窗收纳到屏幕边缘。
|
|
||||||
3. 回到微信、邮件等宿主 App。
|
|
||||||
4. 点击键盘麦克风后直接开始听写。
|
|
||||||
5. 停止听写后收到文本,同时麦克风在短时间内释放。
|
|
||||||
6. PiP 失效时收到明确提示,并能通过现有冷启动路径恢复。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 竞品与行业模式
|
|
||||||
|
|
||||||
### 3.1 Typeless
|
|
||||||
|
|
||||||
Typeless iOS 1.9.0 将该能力命名为 Picture in picture / Skip app switching:
|
|
||||||
|
|
||||||
- 用户先在主 App 中主动开启。
|
|
||||||
- PiP 可拖到屏幕边缘收纳。
|
|
||||||
- 用户在其他 App 的 Typeless 键盘中开始说话。
|
|
||||||
- 官方产品说明强调麦克风空闲时关闭,以降低电量消耗。
|
|
||||||
|
|
||||||
其公开资料无法证明具体内部实现,因此本规划只借鉴产品模型,不假定其私有代码结构。
|
|
||||||
|
|
||||||
### 3.2 Wispr Flow、TypeWhisper 与同类开源项目
|
|
||||||
|
|
||||||
常见架构是主 App 持有 `AVAudioEngine`,键盘通过 App Group 与 Darwin 通知控制句子开始和停止。优点是首字延迟低,缺点是会话期间通常持续占用音频输入。
|
|
||||||
|
|
||||||
OSGKeyboard 当前 Flow 已属于此模式,并已具备:
|
|
||||||
|
|
||||||
- 主 App 会话所有权;
|
|
||||||
- 键盘与主 App IPC;
|
|
||||||
- 连续采集与 utterance gate;
|
|
||||||
- App Group 结果回传;
|
|
||||||
- Live Activity;
|
|
||||||
- 冷启动与恢复流程。
|
|
||||||
|
|
||||||
因此 PiP 应作为会话生命周期的新载体,而不是重建整条语音管线。
|
|
||||||
|
|
||||||
### 3.3 SuperWhisper / App Intents 路线
|
|
||||||
|
|
||||||
更保守的方案是不做长期后台保活,使用 App Intents、Action Button、快捷指令或显式 App 切换启动录音。该方案最符合系统预期,但无法完全满足键盘内即时听写。
|
|
||||||
|
|
||||||
OSGKeyboard 应保留这类入口作为稳定降级,而不是依赖 PiP 达到 100% 可用。
|
|
||||||
|
|
||||||
### 3.4 合规边界
|
|
||||||
|
|
||||||
以下方式不应采用:
|
|
||||||
|
|
||||||
- 循环播放静音音频以防止挂起;
|
|
||||||
- 声明与产品无关的定位、VoIP 后台能力;
|
|
||||||
- 使用不可见或无实际产品意义的伪视频,仅为延长进程生命;
|
|
||||||
- 在用户未明确开启会话时自动恢复麦克风。
|
|
||||||
|
|
||||||
PiP 内容需要能被解释为真实的语音会话控制面,例如展示:
|
|
||||||
|
|
||||||
- 「免切换已就绪」;
|
|
||||||
- 「正在聆听」及音量反馈;
|
|
||||||
- 「正在转写」;
|
|
||||||
- 暂停、结束或返回 App 操作。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 目标产品模型
|
|
||||||
|
|
||||||
### 4.1 三层可用性
|
|
||||||
|
|
||||||
```text
|
|
||||||
层级 0:冷启动
|
|
||||||
主 App 不可用
|
|
||||||
→ 键盘打开 startflow
|
|
||||||
→ 主 App 建立语音会话
|
|
||||||
|
|
||||||
层级 1:短时 Flow
|
|
||||||
AVAudioEngine 会话保持
|
|
||||||
→ 最低首字延迟
|
|
||||||
→ 默认 5 分钟无活动后结束
|
|
||||||
|
|
||||||
层级 2:PiP 免切换模式
|
|
||||||
PiP 保持用户可见的多任务会话
|
|
||||||
→ 空闲时麦克风关闭
|
|
||||||
→ 键盘命令触发按需开麦
|
|
||||||
```
|
|
||||||
|
|
||||||
三个层级必须共用同一份 `FlowSessionBridge` 状态合约,键盘不应根据实现细节分别写三套逻辑。
|
|
||||||
|
|
||||||
### 4.2 用户入口
|
|
||||||
|
|
||||||
建议在首页提供独立状态卡,而不是继续扩张设置开关:
|
|
||||||
|
|
||||||
- 未开启:`开启免切换模式`
|
|
||||||
- 启动中:`正在准备画中画`
|
|
||||||
- 已就绪:`免切换已就绪 · 麦克风未使用`
|
|
||||||
- 录音中:`正在聆听`
|
|
||||||
- 失效:`会话已断开,点击恢复`
|
|
||||||
|
|
||||||
首次开启时应明确说明:
|
|
||||||
|
|
||||||
1. 屏幕上会出现可收纳的 PiP 小窗。
|
|
||||||
2. 空闲时不会使用麦克风。
|
|
||||||
3. 用户关闭 PiP、强制退出 App 或系统回收进程后,需要重新开启。
|
|
||||||
|
|
||||||
### 4.3 键盘状态
|
|
||||||
|
|
||||||
键盘麦克风状态应从「主 App 是否活着」升级为明确能力状态:
|
|
||||||
|
|
||||||
| 状态 | 表现 | 点击结果 |
|
|
||||||
|------|------|----------|
|
|
||||||
| 不可用 | 灰色 | 引导权限或 Full Access |
|
|
||||||
| 需恢复 | 橙色 | 打开主 App 恢复会话 |
|
|
||||||
| PiP 就绪、麦克风关闭 | 绿色 | 请求主 App 按需开麦 |
|
|
||||||
| 正在激活麦克风 | 绿色加载态 | 等待真实音频 proof |
|
|
||||||
| 正在录音 | 红色/波形 | 发送停止命令 |
|
|
||||||
| 正在转写 | 处理中 | 等待结果 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 目标架构
|
|
||||||
|
|
||||||
### 5.1 组件边界
|
|
||||||
|
|
||||||
```text
|
|
||||||
Keyboard Extension
|
|
||||||
└─ FlowSessionBridge / Darwin command
|
|
||||||
↓
|
|
||||||
Host App
|
|
||||||
├─ VoiceSessionCoordinator
|
|
||||||
│ ├─ FlowSessionManager
|
|
||||||
│ ├─ PiPVoiceSessionController
|
|
||||||
│ └─ AudioCaptureLifecycle
|
|
||||||
├─ FlowContinuousCapture
|
|
||||||
├─ ASR + Polish pipeline
|
|
||||||
└─ Live Activity
|
|
||||||
```
|
|
||||||
|
|
||||||
规划职责:
|
|
||||||
|
|
||||||
- `PiPVoiceSessionController`:只管理 PiP 生命周期和展示状态。
|
|
||||||
- `AudioCaptureLifecycle`:管理按需激活、音频 proof、停止及释放。
|
|
||||||
- `FlowSessionManager`:继续负责命令、ASR、润色和结果回传。
|
|
||||||
- `FlowSessionBridge`:发布跨进程能力快照,不让键盘猜测主 App 状态。
|
|
||||||
|
|
||||||
### 5.2 状态机
|
|
||||||
|
|
||||||
```text
|
|
||||||
inactive
|
|
||||||
→ preparingPiP
|
|
||||||
→ pipReadyMicOff
|
|
||||||
→ activatingMic
|
|
||||||
→ recording
|
|
||||||
→ processing
|
|
||||||
→ releasingMic
|
|
||||||
→ pipReadyMicOff
|
|
||||||
|
|
||||||
任意状态
|
|
||||||
→ interrupted
|
|
||||||
→ recovering 或 inactive
|
|
||||||
```
|
|
||||||
|
|
||||||
重要不变量:
|
|
||||||
|
|
||||||
1. `pipReadyMicOff` 必须确认音频输入已停止并释放。
|
|
||||||
2. 键盘只有在收到 `recording` 和真实 audio proof 后才显示正在录音。
|
|
||||||
3. PiP 存活不能等价于麦克风可用。
|
|
||||||
4. 电话/Siri 中断后不得静默恢复录音。
|
|
||||||
5. 任何超时都要回收麦克风并写入明确错误。
|
|
||||||
|
|
||||||
### 5.3 PiP 内容方案
|
|
||||||
|
|
||||||
技术验证阶段应比较两类 Apple 官方能力:
|
|
||||||
|
|
||||||
1. 基于 `AVPlayerLayer` 的媒体 PiP;
|
|
||||||
2. 基于 `AVSampleBufferDisplayLayer` / 视频通话内容源的实时 PiP。
|
|
||||||
|
|
||||||
选择标准不是「哪种最容易保活」,而是:
|
|
||||||
|
|
||||||
- 是否符合 OSGKeyboard 的真实产品用途;
|
|
||||||
- 能否展示动态语音会话状态;
|
|
||||||
- 麦克风激活/释放是否稳定;
|
|
||||||
- 收纳、锁屏、音频中断行为是否可预测;
|
|
||||||
- App Review 是否能清楚理解其用途。
|
|
||||||
|
|
||||||
在完成真机和审核验证前,不冻结具体 AVKit 实现。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 实施阶段
|
|
||||||
|
|
||||||
### Phase 0:技术与审核可行性验证
|
|
||||||
|
|
||||||
目标:证明「PiP 存活 + 闲时关麦 + 键盘触发按需开麦」在目标 iOS 版本可行。
|
|
||||||
|
|
||||||
验证项:
|
|
||||||
|
|
||||||
- PiP 启动、收纳、恢复与关闭;
|
|
||||||
- 空闲 30 分钟后主 App 是否仍能响应;
|
|
||||||
- 空闲期间系统麦克风指示是否消失;
|
|
||||||
- 键盘命令到首个有效音频帧的延迟;
|
|
||||||
- 连续 20 次开始/停止是否稳定;
|
|
||||||
- 电话、Siri、蓝牙切换、锁屏、低电量模式;
|
|
||||||
- 用户关闭 PiP 后的降级行为;
|
|
||||||
- TestFlight / App Review 说明是否被接受。
|
|
||||||
|
|
||||||
退出标准:
|
|
||||||
|
|
||||||
- 空闲时没有麦克风占用;
|
|
||||||
- P95 命令到有效音频帧小于 1 秒;
|
|
||||||
- 20 次连续听写无僵尸录音或失联状态;
|
|
||||||
- 失败后都能回到冷启动路径;
|
|
||||||
- 没有使用静音循环或无关后台能力。
|
|
||||||
|
|
||||||
### Phase 1:内部可用版本
|
|
||||||
|
|
||||||
- 新增 PiP 会话控制器;
|
|
||||||
- 将持续采集改造成可重复激活/释放;
|
|
||||||
- 扩展跨进程状态快照;
|
|
||||||
- 键盘增加激活中、PiP 就绪和失效状态;
|
|
||||||
- 复用现有 ASR、润色、结果回传和 Live Activity;
|
|
||||||
- 添加状态机与 IPC 单元测试。
|
|
||||||
|
|
||||||
### Phase 2:产品化
|
|
||||||
|
|
||||||
- 首页免切换状态卡;
|
|
||||||
- 首次开启说明与 PiP 收纳引导;
|
|
||||||
- 中英文文案与隐私说明;
|
|
||||||
- 诊断页增加 PiP、音频会话和最近中断原因;
|
|
||||||
- 增加遥测指标,但不采集音频内容。
|
|
||||||
|
|
||||||
### Phase 3:灰度与决策
|
|
||||||
|
|
||||||
- TestFlight 小流量开启;
|
|
||||||
- 比较 PiP 与普通 Flow 的成功率、首字延迟和耗电;
|
|
||||||
- 根据审核反馈决定默认入口和长期支持范围;
|
|
||||||
- 若 PiP 不稳定或审核风险不可接受,保留为实验功能或停止上线。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. 测试矩阵
|
|
||||||
|
|
||||||
### 7.1 功能场景
|
|
||||||
|
|
||||||
| 场景 | 预期 |
|
|
||||||
|------|------|
|
|
||||||
| PiP 空闲 | 主 App 可响应,麦克风未占用 |
|
|
||||||
| 键盘开始听写 | 按需激活并获得真实音频帧 |
|
|
||||||
| 停止听写 | 完成转写并及时释放麦克风 |
|
|
||||||
| 连续多句 | 每句均重新激活成功,无第二句无音频 |
|
|
||||||
| PiP 被关闭 | 键盘切为需恢复,不显示假就绪 |
|
|
||||||
| App 被强退 | 清除旧 generation 和僵尸状态 |
|
|
||||||
| 电话/Siri 中断 | 当前句失败并提示,不自动偷录 |
|
|
||||||
| 蓝牙设备变化 | 音频格式重建,不崩溃 |
|
|
||||||
| 网络失败 | 本地 ASR 保留;润色按现有策略降级 |
|
|
||||||
|
|
||||||
### 7.2 设备与系统
|
|
||||||
|
|
||||||
- 最低支持 iOS 版本、当前稳定版和最新 beta;
|
|
||||||
- 刘海机、灵动岛机型、iPad;
|
|
||||||
- AirPods、普通蓝牙耳机、车载音频、有线设备;
|
|
||||||
- 微信、信息、邮件、Slack、Notes 及自定义文本输入控件;
|
|
||||||
- 锁屏、横竖屏、多窗口、低电量和后台刷新关闭状态。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. 指标与验收
|
|
||||||
|
|
||||||
### 8.1 核心指标
|
|
||||||
|
|
||||||
| 指标 | 定义 | 目标 |
|
|
||||||
|------|------|------|
|
|
||||||
| 免切换成功率 | PiP 就绪时无需打开主 App完成听写 | ≥ 98% |
|
|
||||||
| 麦克风空闲占用 | 非录音期间仍占麦的时长比例 | 接近 0 |
|
|
||||||
| 首帧延迟 P95 | 键盘点击到真实音频 proof | < 1 秒 |
|
|
||||||
| 结果回传成功率 | 停止后键盘收到最终结果 | ≥ 99% |
|
|
||||||
| 僵尸状态率 | 键盘显示可用但主 App无法响应 | < 0.5% |
|
|
||||||
| 恢复成功率 | 失效后通过冷启动恢复 | ≥ 99% |
|
|
||||||
|
|
||||||
### 8.2 观察指标
|
|
||||||
|
|
||||||
- 每日 PiP 开启人数与启用留存;
|
|
||||||
- PiP 被用户主动关闭的比例;
|
|
||||||
- 每小时耗电和温升相对普通 Flow 的变化;
|
|
||||||
- 音频中断类型分布;
|
|
||||||
- 用户因橙色麦克风指示或隐私产生的反馈;
|
|
||||||
- App Review 反馈和政策变化。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. 风险与应对
|
|
||||||
|
|
||||||
| 风险 | 等级 | 应对 |
|
|
||||||
|------|------|------|
|
|
||||||
| PiP 被认定与媒体用途不匹配 | 高 | 提供真实会话 UI、明确审核说明;先 TestFlight/审核验证 |
|
|
||||||
| 系统版本改变 PiP 行为 | 高 | 保持冷启动降级;按系统版本做兼容验证 |
|
|
||||||
| 按需开麦首字丢失 | 中 | 激活态 + audio proof + 预录缓冲,不提前向键盘宣告录音 |
|
|
||||||
| 频繁激活导致音频路由异常 | 中 | 串行状态机、格式重建、媒体服务重置恢复 |
|
|
||||||
| PiP 与 Live Activity 状态冲突 | 中 | 单一 coordinator 发布状态,两个 UI 只消费 |
|
|
||||||
| 用户误解 PiP 仍在监听 | 中 | 空闲状态明确写「麦克风未使用」,隐私说明可验证 |
|
|
||||||
| 其他 App 抢占麦克风 | 中 | 显式中断提示,不承诺并发录音 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. 产品决策门
|
|
||||||
|
|
||||||
进入代码实现前,需要确认:
|
|
||||||
|
|
||||||
1. 是否接受 PiP 作为用户主动开启、系统可见且可收纳的产品形态;
|
|
||||||
2. 是否优先「空闲关麦」而接受约数百毫秒的重新激活延迟;
|
|
||||||
3. PiP 中展示哪些真实功能,确保它不是纯保活黑窗;
|
|
||||||
4. 是否将现有「跳过 App 切换」重命名并拆为普通 Flow / PiP 两种模式;
|
|
||||||
5. 最低支持系统和目标测试设备;
|
|
||||||
6. 技术验证失败或审核风险过高时,是否接受回退到短 Flow + App Intents。
|
|
||||||
|
|
||||||
在上述决策和 Phase 0 证据完成前,不建议直接进入正式实现。
|
|
||||||
@@ -1,350 +0,0 @@
|
|||||||
# 打字键盘输入准确率提升计划
|
|
||||||
|
|
||||||
> **文档状态**:产品与工程规划(**实现中**:Phase 1~4 已落地,待真机试打)
|
|
||||||
> **适用范围**:iOS 键盘扩展打字面(`OSGKeyboardExt` 打字键网格)+ 共享布局度量(`OSGKeyboardShared`)
|
|
||||||
> **关联基线**:`docs/TYPING_KEYBOARD_BASELINE.md`
|
|
||||||
> **实现分支**:`feat/keyboard-touch-accuracy`
|
|
||||||
> **创建日期**:2026-08-03
|
|
||||||
> **目标版本**:待 Phase 0 基线试打后确定发布节奏
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Executive Summary
|
|
||||||
|
|
||||||
### 1.1 目标
|
|
||||||
|
|
||||||
在拼音打字主路径上,将点按准确率与按键契约提升到**接近系统键盘的日常体感**:
|
|
||||||
|
|
||||||
- 漏触(按了没反应)接近 0
|
|
||||||
- 邻键误触明显下降
|
|
||||||
- 「不准」不再成为用户槽点
|
|
||||||
|
|
||||||
**成功定义(产品语言)**:同设备、同姿势对比系统键盘时,内部试打主观分接近;键缝无空响应;字母键支持预览与滑动改选。
|
|
||||||
|
|
||||||
### 1.2 核心结论(冻结)
|
|
||||||
|
|
||||||
| 决策 | 选择 |
|
|
||||||
|------|------|
|
|
||||||
| 准确率策略 | 对齐系统**触控契约 + 分层纠错**,不复刻私有算法 |
|
|
||||||
| 字母 / 数字 / 符号 | 按下高亮 → 可滑动改选 → **松手确认** |
|
|
||||||
| 删除 | **按下即删** + 长按连删(与现状及系统一致) |
|
|
||||||
| 空格 / 回车 | 与字母一致:**松手确认** |
|
|
||||||
| 键缝 | 视觉保留间距;触控**不允许无响应**(最近键中心) |
|
|
||||||
| 滑出键平面再松手 | **取消本次输入**(不出字) |
|
|
||||||
| 视觉 | 键位外观不因热区/智能变形(热区隐形) |
|
|
||||||
| 智能边界 | 只微调;用户明确落点或滑动可覆盖 |
|
|
||||||
| 语言策略 | 中文拼音过程可偏心;英文先求稳 |
|
|
||||||
| 反馈最低标准 | Phase 1~2:**高亮 + 按键音**;气泡为加分项 |
|
|
||||||
| 实施顺序 | 无死区 → 按键契约 → 触点微调 → 拼音偏心 → 气泡等 |
|
|
||||||
|
|
||||||
### 1.3 非目标(本期不做)
|
|
||||||
|
|
||||||
- 绝对准确率 100% 追平系统键盘
|
|
||||||
- QuickPath / 滑行输入
|
|
||||||
- 依赖 `UIKeyboardLayoutStar` 等系统私有 hit API
|
|
||||||
- 完整双 strike zone 个性化、键盘整体漂移重定位(re-spotting)
|
|
||||||
- 英文词级 predictive hit 放大(后置评估)
|
|
||||||
- 重写 Rime / 抛光引擎(触控层与词级兜底分工,不互相替代)
|
|
||||||
|
|
||||||
### 1.4 现实预期
|
|
||||||
|
|
||||||
| 目标 | 是否现实 |
|
|
||||||
|------|----------|
|
|
||||||
| 漏触接近系统 | ✅ Phase 1~2 可达 |
|
|
||||||
| 邻键误触主观接近系统 | ✅ Phase 2~4 可达 |
|
|
||||||
| 中文拼音连打体感不输系统 | ✅ 相对最有机会(拼音偏心) |
|
|
||||||
| 全场景绝对准确率 = 系统 | ❌ 不作为本期 KPI |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 背景与现状差距
|
|
||||||
|
|
||||||
### 2.1 问题定义(第一性原理)
|
|
||||||
|
|
||||||
用户打字时只关心两件事:
|
|
||||||
|
|
||||||
1. **我以为我点了谁**(视觉反馈)
|
|
||||||
2. **机器最后认了谁**(实际输入)
|
|
||||||
|
|
||||||
系统键盘准,是因为整条链路在缩小二者差距,并在不一致时提供改选与词级兜底。OSG 当前布局已对齐系统键位,但触控模型仍接近「普通按钮网格」。
|
|
||||||
|
|
||||||
### 2.2 系统键盘能力分层(参考模型)
|
|
||||||
|
|
||||||
系统做法是多层流水线(专利 + 私有头文件命名 + 可观察行为),不是单一「热区 +N pt」:
|
|
||||||
|
|
||||||
| 层 | 含义 | 本期态度 |
|
|
||||||
|----|------|----------|
|
|
||||||
| ① 触点估计 | 触点中心 ≠ 意图点,可做偏移/加权 | Phase 3 轻量做 |
|
|
||||||
| ② 几何热区 | 视觉 ≠ hit;缝可命中 | Phase 1 必做 |
|
|
||||||
| ③ 动态偏心 | 下一键更可能则更好点 | Phase 4 拼音合法优先 |
|
|
||||||
| ④ 触摸生命周期 | Down 反馈 / Move 改选 / Up 提交 | Phase 2 必做 |
|
|
||||||
| ⑤ 个性化双区 | 按节奏/习惯自适应 | 后置 |
|
|
||||||
| ⑥ 词级兜底 | 自动更正 / 候选 | 已有 Rime + 抛光,需保住 |
|
|
||||||
|
|
||||||
### 2.3 现状(代码事实)
|
|
||||||
|
|
||||||
| 区域 | 现状 | 影响 |
|
|
||||||
|------|------|------|
|
|
||||||
| 字符键 | SwiftUI `Button`,视觉 frame ≈ hit | 键间 6~8pt 为死区;松手且需仍在键内 |
|
|
||||||
| 删除 / 部分工具栏 | `DragGesture(minimumDistance: 0)` 按下即响 | 已接近系统删除契约 |
|
|
||||||
| 主动扩大 hit | 基本仅候选栏展开按钮(34 视觉 / 44 hit) | 字母区无 fat-finger 扩展 |
|
|
||||||
| 滑动改选 | 无行级/网格级跟踪 | 邻键误触难在松手前纠正 |
|
|
||||||
| 点击音 | 字符键未统一走 `KeyboardSoundFeedback` | 反馈弱于系统 |
|
|
||||||
| 词级 | Rime 候选 + 抛光 | 后段较强;前段触控不稳会浪费兜底 |
|
|
||||||
|
|
||||||
主要相关位置:
|
|
||||||
|
|
||||||
- `OSGKeyboardExt/Typing/TypingRootView.swift`(键网格、间距、`keyButton`)
|
|
||||||
- `OSGKeyboardExt/Views/ToolbarActionButtons.swift`(按下即响模板)
|
|
||||||
- `OSGKeyboardExt/Utilities/KeyboardSoundFeedback.swift`
|
|
||||||
- `OSGKeyboardShared/Typing/TypingLayoutProviding.swift`(键位布局)
|
|
||||||
|
|
||||||
### 2.4 用户失败模式
|
|
||||||
|
|
||||||
| 体感 | 主因 | 主要靠哪一层治 |
|
|
||||||
|------|------|----------------|
|
|
||||||
| 点到缝没反应 | 间距死区 | Phase 1 |
|
|
||||||
| 经常打成邻键 | 无改选窗口 + 无触点/上下文加权 | Phase 2~4 |
|
|
||||||
| 滑出键外丢输入 | per-key Button 生命周期 | Phase 2 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 产品宪法(全程遵守)
|
|
||||||
|
|
||||||
1. **字母 / 数字 / 符号**:按下高亮 → 可滑动改选 → **松手确认**
|
|
||||||
2. **删除**:**按下即删**,长按连删
|
|
||||||
3. **键平面内不允许无响应**(缝必命中最近键中心)
|
|
||||||
4. **视觉键位不因智能变形**(热区与偏心均隐形)
|
|
||||||
5. **智能只微调**;用户滑动或明确落点可覆盖
|
|
||||||
6. **中文拼音过程可以更聪明;英文先求稳**
|
|
||||||
|
|
||||||
对内沟通口径(Phase 2):
|
|
||||||
|
|
||||||
> 不是变慢,是把确认点从按下挪到松开,换来改选能力——和系统键盘一样。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 分阶段计划
|
|
||||||
|
|
||||||
### Phase 0 — 基线与验收设计
|
|
||||||
|
|
||||||
**目的**:能量化「准不准」,避免凭感觉争论。
|
|
||||||
|
|
||||||
| 事项 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| 记录现状问题 | 漏触 / 邻键错 / 滑出丢键的典型场景 |
|
|
||||||
| 定试打脚本 | 至少 3 类:缝点击、邻键偏触、常见拼音连打(如 `zhongguo`、`pingguo`) |
|
|
||||||
| 定对比方式 | 同设备、同姿势;系统键盘 vs OSG;主观 1~5 分 + 错字/漏触次数 |
|
|
||||||
| 定反馈最低标准 | Phase 1~2:高亮 + 按键音;气泡单列加分 |
|
|
||||||
|
|
||||||
**工期**:0.5~1 天
|
|
||||||
**产出**:试打清单 + 打分表(可放飞书/Notion;关键结论可回写本文「附录」)
|
|
||||||
|
|
||||||
**出口**:脚本固定、打分表可用,再进入开发。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Phase 1 — 无死区热区(P0)
|
|
||||||
|
|
||||||
**产品一句话**:缝也算键,别点空。
|
|
||||||
|
|
||||||
**范围**
|
|
||||||
|
|
||||||
- 字母 / 数字 / 符号区:视觉间距保留,触控无空洞
|
|
||||||
- 缝内命中:**最近键中心**
|
|
||||||
- 边缘键(如 Q/P、A/L)外侧略扩,降低边缘难点
|
|
||||||
- 不改变键的视觉大小与间距观感
|
|
||||||
|
|
||||||
**工期**:可与 Phase 2 合并开发,但**验收分开**
|
|
||||||
**建议**:约短迭代内完成几何命中;若与 Phase 2 同 PR,须分 commit 或分验收项
|
|
||||||
|
|
||||||
**验收**
|
|
||||||
|
|
||||||
- [ ] 故意点键缝 20 次,0 次无响应
|
|
||||||
- [ ] 视觉观感与改前一致
|
|
||||||
- [ ] 无明显「看在 A 上却出 S」的跳键
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Phase 2 — 系统同款按键契约(P0)
|
|
||||||
|
|
||||||
**产品一句话**:先告诉你按了谁,松手再算数。
|
|
||||||
|
|
||||||
**范围**
|
|
||||||
|
|
||||||
| 键类 | 行为 |
|
|
||||||
|------|------|
|
|
||||||
| 字母 / 数字 / 符号 | Down 高亮 → Move 改选 → Up 提交 |
|
|
||||||
| 删除 | Down 即删 + 长按连删(保持) |
|
|
||||||
| 空格 / 回车 | 松手确认 |
|
|
||||||
| 滑出键平面再松手 | 取消本次输入 |
|
|
||||||
| 反馈 | 高亮 + `KeyboardSoundFeedback.keyClick()` |
|
|
||||||
|
|
||||||
**本阶段不做**:按键气泡、长按附加符号、滑行输入。
|
|
||||||
|
|
||||||
**工期**:约 1 个迭代(含内部试打调参)
|
|
||||||
|
|
||||||
**验收**
|
|
||||||
|
|
||||||
- [ ] 按下 F 滑到 G 松开 → 只出 G,高亮跟随
|
|
||||||
- [ ] 按下后滑出键盘再松 → 不出字
|
|
||||||
- [ ] 删除仍为按下即删、可连删
|
|
||||||
- [ ] 内部试打:「手感接近系统」主观分显著高于改前
|
|
||||||
|
|
||||||
**工程注意(规划级)**
|
|
||||||
|
|
||||||
- 宜收敛为网格/行级触摸跟踪,而非继续堆 per-key `Button`
|
|
||||||
- 可复用 `RepeatingPressButton` 与现有 UIKit 触控跟踪经验,但字母键生命周期以 Up 确认为准
|
|
||||||
- 需单列与候选栏、Shift、123 切换的手势边界用例
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Phase 3 — 触点微调(P1)
|
|
||||||
|
|
||||||
**产品一句话**:按手指意图认键,不只认触点中心。
|
|
||||||
|
|
||||||
**范围**
|
|
||||||
|
|
||||||
- 对命中点做轻微、稳定的向上偏移(或按触点大小微调)
|
|
||||||
- 规则保持可预期;偏移量建议可配置便于试打
|
|
||||||
- 中英文均可生效(与语言无关的几何修正)
|
|
||||||
|
|
||||||
**工期**:0.5~1 天(可挂在 Phase 2 尾声)
|
|
||||||
|
|
||||||
**验收**
|
|
||||||
|
|
||||||
- [ ] 拇指自然打字时,偏下误触主观减少
|
|
||||||
- [ ] 不引入新的「明显认错键」投诉
|
|
||||||
- [ ] 偏移可调,便于内测收敛
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Phase 4 — 拼音下一键偏心(P1)
|
|
||||||
|
|
||||||
**产品一句话**:拼到一半时,更可能的下一家更好点。
|
|
||||||
|
|
||||||
**范围(档位 1:合法字母优先)**
|
|
||||||
|
|
||||||
- 仅拼音组词过程中启用
|
|
||||||
- 依据当前拼音/Rime 状态:合法后续字母热区略增,非法略减
|
|
||||||
- **偏心有上限**(微调,不霸道)
|
|
||||||
- 用户滑动改选 / 明确落点可覆盖
|
|
||||||
- 英文模式:关闭或明显减弱
|
|
||||||
- 上屏或开始新音节:偏心状态重置
|
|
||||||
|
|
||||||
**后置**:英文词预测式放大、重度概率模型、个性化学习。
|
|
||||||
|
|
||||||
**工期**:约 1 个迭代
|
|
||||||
**依赖**:必须在 Phase 2 稳定之后(无改选窗口时,偏心误判会被感知为「自作主张」)
|
|
||||||
|
|
||||||
**验收**
|
|
||||||
|
|
||||||
- [ ] 常见拼音连打,邻键误触下降
|
|
||||||
- [ ] 故意打冷门 / 非法后续字母,仍能打上
|
|
||||||
- [ ] 视觉上看不出键变大;体感更顺
|
|
||||||
- [ ] 可用试打脚本对比「偏心开 / 关」
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Phase 5 — 体验加分(P2,不阻塞主目标)
|
|
||||||
|
|
||||||
| 项 | 说明 | 优先级 |
|
|
||||||
|----|------|--------|
|
|
||||||
| 按键气泡预览 | 强化所见即所得 | 高(体验) |
|
|
||||||
| 候选 / 抛光联动检查 | 触控改善后词级兜底不回退 | 高(质量) |
|
|
||||||
| 长按附加符号 | 系统能力对齐,非准确率核心 | 中 |
|
|
||||||
| 个性化热区 | 长期数据,后置 | 低 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 推荐排期
|
|
||||||
|
|
||||||
```text
|
|
||||||
Week 0 Phase 0 基线试打脚本 + 打分
|
|
||||||
Week 1 Phase 1+2 无死区 + 松手确认/滑动改选(可合并开发,分开验收)
|
|
||||||
Week 1 末 Phase 3 触点微调
|
|
||||||
Week 2 内部试打 / 调参 / 边界修复(滑出取消、宽键、Shift、候选冲突)
|
|
||||||
Week 3 Phase 4 拼音合法下一键偏心
|
|
||||||
Week 3~4 Phase 5 气泡等加分项(可选)
|
|
||||||
```
|
|
||||||
|
|
||||||
**底线**:Week 1 必须交出 Phase 1 + 2;没有这两项,不宣称「接近系统准确率」。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 范围边界
|
|
||||||
|
|
||||||
| 做 | 不做(本项目) |
|
|
||||||
|----|----------------|
|
|
||||||
| 点按准确率与系统同款契约 | QuickPath |
|
|
||||||
| 拼音过程热区偏心(合法优先) | 系统私有 hit API |
|
|
||||||
| 高亮 + 按键音 | 完整自动更正引擎重写 |
|
|
||||||
| 与现有 Rime / 候选协同 | 键盘整体自动平移 / 缩放 |
|
|
||||||
| 可配置调参(偏移、偏心强度) | 为准确率牺牲视觉键位一致性 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. 风险与对策
|
|
||||||
|
|
||||||
| 风险 | 对策 |
|
|
||||||
|------|------|
|
|
||||||
| 「松手确认」被觉得变钝 | 对内对齐系统口径;用试打错字率对比说话 |
|
|
||||||
| 热区重叠导致跳键 | 最近中心 + 偏心上限;内测可关偏心 |
|
|
||||||
| 拼音偏心误伤冷门输入 | 合法优先 + 可覆盖;可选调试开关 |
|
|
||||||
| 与删除 / 空格 / 候选手势冲突 | Phase 2 单列边界用例;滑出取消 |
|
|
||||||
| 扩展内难自动化 UI 测 | Phase 0 人工脚本为主;命中几何/偏心纯逻辑补单测 |
|
|
||||||
| 改动面集中在打字面 | 语音面工具栏契约保持;避免一次改两套手感却无验收 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. 角色与出口标准
|
|
||||||
|
|
||||||
| 角色 | 职责 |
|
|
||||||
|------|------|
|
|
||||||
| 产品 | 守宪法、主观验收、决定气泡是否进本期 |
|
|
||||||
| 设计 | 高亮态;(可选)气泡视觉 |
|
|
||||||
| 工程 | Phase 1~4 实现;关键常量可配置 |
|
|
||||||
| 内测 | 每 Phase 用同一试打脚本回归 |
|
|
||||||
|
|
||||||
**每阶段出口**:试打清单通过 + 无 P0「无响应 / 乱出字」再进入下一阶段。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. 与版本发布的关系
|
|
||||||
|
|
||||||
- 本能力属用户可感知的打字体验提升,合并 `main` 并准备发布时:
|
|
||||||
- 按 `AGENTS.md` 用 Conventional Commits 评估版本 bump(预期含 `feat` → **MINOR**)
|
|
||||||
- `CHANGELOG.md` 在 `[Unreleased]` 下补双语条目(实现时再写,不在本文提前编造版本号)
|
|
||||||
- 功能分支开发期间**不**提前 bump `project.yml` 版本号
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. 一句话执行顺序
|
|
||||||
|
|
||||||
> **先量基线 → 无死区 + 系统按键契约 → 触点微调 → 拼音偏心 → 气泡等加分。**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 附录 A. 试打脚本草案(Phase 0 细化)
|
|
||||||
|
|
||||||
| 编号 | 场景 | 操作 | 记录 |
|
|
||||||
|------|------|------|------|
|
|
||||||
| T1 | 键缝 | 在同行相邻键缝点击 20 次 | 无响应次数 |
|
|
||||||
| T2 | 邻键偏触 | 瞄准某键但故意偏向邻键 20 次 | 错键次数;有/无滑动改选 |
|
|
||||||
| T3 | 滑动改选 | 按下 A 滑到 S 松开(Phase 2+) | 是否只出 S |
|
|
||||||
| T4 | 滑出取消 | 按下后滑出键盘松开(Phase 2+) | 是否不出字 |
|
|
||||||
| T5 | 删除契约 | 点按 / 长按删除 | 是否按下即删、可连删 |
|
|
||||||
| T6 | 拼音连打 | `zhongguo` / `pingguo` 等各 5 遍 | 错字次数、主观 1~5 |
|
|
||||||
| T7 | 偏心开关 | Phase 4 后 T6 对比开/关 | 错字差、误伤冷门键与否 |
|
|
||||||
|
|
||||||
对比时尽量:同一 iPhone、竖屏、单手拇指、系统键盘与 OSG 各跑一轮。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 附录 B. 修订记录
|
|
||||||
|
|
||||||
| 日期 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| 2026-08-03 | 初稿:基于系统键盘分层分析与产品讨论冻结 Phase 0~5 |
|
|
||||||
| 2026-08-05 | 开分支 `feat/keyboard-touch-accuracy`;落地 Phase 1(无死区)+ Phase 2(网格 Down/Move/Up)+ Phase 3(触点上偏);纯逻辑见 `KeyHitTesting` / `TypingKeyLayout`,触控见 `TypingKeyTouchPad` |
|
|
||||||
| 2026-08-05 | Phase 4:全拼合法下一键偏心(`PinyinNextKeyResolver` + `rawInput`);双拼/英文保持中性;歧义最近键加权,单键明确命中不受偏置 |
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
# Keyboard memory budget — acceptance (OSGDiag)
|
|
||||||
|
|
||||||
Phase 0 / 2 memory work is validated on a **physical iPhone** with Console
|
|
||||||
filtering for `OSGDiag`. Unit tests and `xcodebuild` cover compile-time
|
|
||||||
wiring; jetsam behavior is device-only.
|
|
||||||
|
|
||||||
## Hybrid Flow (product default)
|
|
||||||
|
|
||||||
- Foreground host is **light by default**: orphan Live Activity cleanup only —
|
|
||||||
**no** auto `startSession` / continuous capture on appear.
|
|
||||||
- Capture starts only on explicit Start / `osgkeyboard://startflow` / mic press.
|
|
||||||
- Idle background capture is stopped so a parked host does not jetsam the keyboard.
|
|
||||||
- **Do not** stack CLM + Rime + ASR in the same second after onboarding.
|
|
||||||
- ASR warmup runs on **first mic press** (`beginUtterance`), gated by
|
|
||||||
`HostMemoryBudget` (~260 MB RSS). `hostHeavy` is set only while heavy work
|
|
||||||
actually runs, then cleared. A sticky `hostHeavy` (host died mid-work)
|
|
||||||
expires after `hostHeavyMaxAge` (~120 s) and is cleared on
|
|
||||||
`clearFlowState` / host-launch reconciliation so typing 中文/EN is not
|
|
||||||
permanently blocked.
|
|
||||||
|
|
||||||
## Console checklist
|
|
||||||
|
|
||||||
Filter Console by process separately: host `OSGKeyboard` vs extension
|
|
||||||
`com.osgkeyboard.ios.keyboard`. Host-only filter will never show `KVC.*`.
|
|
||||||
|
|
||||||
| Scenario | Expect |
|
|
||||||
|----------|--------|
|
|
||||||
| **Force-quit host**, open Notes, switch to OSG | First `dyld.constructor`, then `KVC.init` → `viewDidLoad` → `viewDidAppear` |
|
|
||||||
| Host foreground right after launch | `skip capture` + `postOnboardingWarmup scheduled … delay=45s` (no immediate Rime/CLM) |
|
|
||||||
| ~45 s later, host still active | Serial `rime.installIfNeeded` then `clm.prepare` |
|
|
||||||
| First mic press | `scheduleASRWarmup`; `hostHeavy` only while work runs |
|
|
||||||
| Switch to typing | Single `rime.prepare` / `englishPrepare` |
|
|
||||||
|
|
||||||
If neither `dyld.constructor` nor `KVC.init` appears after force-quitting the host,
|
|
||||||
the extension is dying in dyld (Shared≈9.4 MB + librime) — next lever is splitting
|
|
||||||
Rime out of Shared.
|
|
||||||
|
|
||||||
## RSS comparison (optional)
|
|
||||||
|
|
||||||
Record `OSGDiag` `rss=` tags for:
|
|
||||||
|
|
||||||
1. Cold voice surface only
|
|
||||||
2. Cold typing after prepare
|
|
||||||
3. Host foreground + extension
|
|
||||||
|
|
||||||
Target: typing peak below the old “voice + eager Librime construct” baseline.
|
|
||||||
|
|
||||||
## Extension physical-footprint budget
|
|
||||||
|
|
||||||
`phys_footprint` is the primary extension metric because device jetsam follows
|
|
||||||
it more closely than RSS. `KeyboardExtensionMemoryTelemetry` records structured
|
|
||||||
`[OSGDiag/memory] extMemory` lines at lifecycle and heavy-resource milestones,
|
|
||||||
plus 50 ms samples during the first four seconds:
|
|
||||||
|
|
||||||
- **Normal:** below 36 MiB
|
|
||||||
- **Warning:** 36–40 MiB
|
|
||||||
- **High:** 40–48 MiB; 40 MiB is the internal safe peak
|
|
||||||
- **Critical:** 48 MiB or above
|
|
||||||
|
|
||||||
The approximate 60 MiB device boundary is not a public Apple contract. The
|
|
||||||
40 MiB target deliberately reserves room for transient SwiftUI, Rime, and
|
|
||||||
system-framework pages. Telemetry is observation-only: crossing a band logs
|
|
||||||
`crossed=1` but does not change the selected surface or unload resources.
|
|
||||||
|
|
||||||
Each record includes the current and peak footprint, delta from process start,
|
|
||||||
elapsed startup time, surface/language, Full Access, clipboard state, and
|
|
||||||
operation-specific context. Filter Console by `OSGDiag/memory` and compare:
|
|
||||||
|
|
||||||
1. `KVC.viewDidLoad.afterInstallServices`
|
|
||||||
2. `KVC.viewDidLoad.afterInstallSwiftUI`
|
|
||||||
3. `typing.englishPrepare.done`
|
|
||||||
4. `typing.rimePrepare.done`
|
|
||||||
5. `clipboard.reload.done`
|
|
||||||
6. `KVC.viewDidAppear.done`
|
|
||||||
|
|
||||||
## Structural split
|
|
||||||
|
|
||||||
- Extension links **OSGKeyboardShared** only (no Charts / StoreKit / Speech / HostSupport).
|
|
||||||
- Host embeds **OSGKeyboardHostSupport** (ASR, CLM, CloudASR, tip/charts UI).
|
|
||||||
- Heavy assets (`osg_pinyin.dict.yaml`, CLM bin, licenses, local-asr catalog)
|
|
||||||
ship in the **host app** bundle; extension reads Rime from App Group after deploy.
|
|
||||||
@@ -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 | 初版竞品研究与评测框架 |
|
|
||||||
@@ -1,818 +0,0 @@
|
|||||||
# OSGKeyboard · iOS 完整检查 + macOS 差距与移植建议报告
|
|
||||||
|
|
||||||
> **报告日期**:2026-08-23
|
|
||||||
> **当前版本**:2.0.1(build 90)— `project.yml:51-52`
|
|
||||||
> **审查范围**:iOS Host (`OSGKeyboard/`, 88 .swift) + iOS HostSupport (`OSGKeyboardHostSupport/`, 29 .swift) + iOS Extension (`OSGKeyboardExt/`, 27 .swift) + Shared (`OSGKeyboardShared/`, 230 .swift) + macOS (`OSGKeyboardMac/`, 39 .swift) + macOS Tests (3 .swift)
|
|
||||||
> **方法**:只读代码考古,证据来自 `project.yml` 共享 / 排除清单 + 文件结构 + 关键源码摘录 + `CHANGELOG.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## TL;DR
|
|
||||||
|
|
||||||
| 维度 | iOS | macOS | 结论 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| 产品形态 | 宿主 App + 自定义键盘扩展 | 单窗口菜单栏 App + 浮动 HUD | **不同形态**——Mac 不是键盘,是"按住 Option 说话→⌘V 注入" |
|
|
||||||
| 核心流水线 | Flow 跨进程会话 + SpeechAnalyzer | 单一进程 + MLX Audio Qwen3 流式 | **架构不可同构** |
|
|
||||||
| LLM 润色 | ✅ 完整(`PolishingService`) | ✅ 完整(**直接复用** iOS `PolishingService`) | **完全对齐** |
|
|
||||||
| 本地 ASR | iOS 26 `SpeechAnalyzer + DictationTranscriber` | **MLX Qwen3-ASR 0.6B/1.7B 4-bit** + Apple Speech 兜底 | **完全不同的技术栈** |
|
|
||||||
| 云 ASR | 7 家供应商 + 流式 | ✅ **完全对齐**(同 `CloudASRClientFactory`) | **完全对齐** |
|
|
||||||
| 个人词典 | ✅ | ✅ | **完全对齐** |
|
|
||||||
| 历史 / 使用统计 | ✅ | ✅(iCloud KVS 共用) | **完全对齐** |
|
|
||||||
| 润色样式 + 学习 | ✅ | ✅ | **完全对齐** |
|
|
||||||
| iCloud 同步 | ✅(5 路:设置/词典/历史/使用/样式) | ✅(**5 路全部对齐**) | **完全对齐** |
|
|
||||||
| 提示页 (Tip) | ✅ | ✅ | **完全对齐** |
|
|
||||||
| 自定义语言模型 CLM | ✅ UI + 后台 | ⚠️ 后台调用了,**无 UI** | **部分缺失** |
|
|
||||||
| 剪贴板历史 + AI 技能 | ✅ | ❌ 共享代码已编译但**无 Mac UI** | **完全缺失** |
|
|
||||||
| AI 提示轮播 / 上下文技能 | ✅ | ❌ 共享代码已编译但**无 Mac UI** | **完全缺失** |
|
|
||||||
| Flow 跨进程会话 / PiP | ✅ | ❌ | **Mac 形态不需要** |
|
|
||||||
| 自定义键盘 (Rime/拼音/英文) | ✅ | ❌(`Typing/**` 全排除) | **Mac 形态不需要** |
|
|
||||||
| OSG 账户 (Sign in with Apple + 积分 + 推荐) | ✅ | ❌ | **完全缺失** |
|
|
||||||
| App Attest | ✅ | ❌(无对应 entitlement) | **完全缺失** |
|
|
||||||
| 一方分析 (Analytics) | ✅(108 测试覆盖) | ❌(共享代码已编译但**无任何调用**) | **完全缺失** |
|
|
||||||
| 助手指令 (Shortcuts) | ✅ | ❌ | **完全缺失** |
|
|
||||||
| 屏幕唤醒 / 锁 / Live Activity | ✅ | ❌ | **Mac 形态不需要** |
|
|
||||||
|
|
||||||
**最关键的发现**:
|
|
||||||
|
|
||||||
1. **macOS 复用策略 = 源码级 include**。Mac 不 `import OSGKeyboardShared`——`project.yml:570-602` 把 Shared + HostSupport 的 `.swift` 文件直接编进 Mac target,再用 `excludes:` 列表过滤 iOS-only 文件。**整个框架里没有 `#if os(macOS)` 条件编译**(Mac target 内部有几处 `import AppKit` 守卫,是防御性的)。
|
|
||||||
2. **LLM 润色、云 ASR、iCloud 同步、本地 ASR 模型管理、模型目录、个人词典、润色样式、提示页、Onboarding——这 9 大块是 iOS / Mac 完全对齐的**。差异主要在"宿主 App 才有"的系统集成(Account、App Attest、Analytics、Shortcuts、Flow 跨进程会话)。
|
|
||||||
3. **"共享代码已编进 Mac 但零调用"的浪费**:`OSGKeyboardShared/Features/Account/`、`Features/ManagedGateway/`、`Features/Analytics/`、`Services/AIClipboard*.swift`、`Services/AIHint*.swift`、`Services/AIUserSkill*.swift` —— 这些文件编译进了 Mac 二进制但没有任何 Mac 文件引用。**编译器开销 + 二进制体积 + 维护面全白付**。要么真正接进 Mac,要么从 Mac target 排除。
|
|
||||||
4. **Mac 真正独占的体验是 MLX 流式 ASR**。这是 iOS 受限于 ARM NEON + iOS 26 平台绑定做不到的能力,恰好把 Mac 的 Apple Silicon 算力用足。
|
|
||||||
5. **移植优先级不应是"把 iOS 全搬到 Mac"**。Mac 是"按住说话→润色→注入"工具,自然形态完全不同。**真正应该补的是:剪贴板 AI、CLM 管理、Account 体系、Analytics。**其他(Flow、自定义键盘)不应该硬塞进 Mac。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. iOS 端完整状态
|
|
||||||
|
|
||||||
### 1.1 Target 拓扑
|
|
||||||
|
|
||||||
| Target | 平台 | 类型 | .swift 数 | 备注 |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| `OSGKeyboard` | iOS 26 | App | 88 | 宿主 App,沙盒、StoreKit、iCloud、App Attest |
|
|
||||||
| `OSGKeyboardExt` | iOS 26 | App Extension (键盘服务) | 27 | `RequestsOpenAccess: true` |
|
|
||||||
| `OSGKeyboardShared` | iOS 26 | Framework | 230 | 跨进程共用 + 复用到 Mac |
|
|
||||||
| `OSGKeyboardHostSupport` | iOS 26 | Framework | 29 | 宿主专用(Speech/AVFoundation/Charts/StoreKit) |
|
|
||||||
| `OSGKeyboardMac` | macOS 15+ | App | 39 | 菜单栏单窗口,**沙盒关闭**(Accessibility 需要) |
|
|
||||||
| `OSGKeyboardMacTests` | macOS 15+ | Unit Test | 3 | 挂在 Mac App bundle 内 |
|
|
||||||
| `OSGKeyboardTests` | iOS 26 | Unit Test | 108 | 覆盖 host + shared + hostsupport |
|
|
||||||
| `OSGKeyboardExtTests` | iOS 26 | Unit Test | 16 | 覆盖 ext + shared |
|
|
||||||
| `OSGKeyboardUITests` | iOS 26 | UI Test | — | TestFlight 截图自动化 |
|
|
||||||
|
|
||||||
### 1.2 iOS Host App (`OSGKeyboard/`) — 用户可见的所有功能
|
|
||||||
|
|
||||||
**导航**:4 Tab (Keyboard / Skills / Styles / Settings) on iPhone;iPad 自动切换到 `NavigationSplitView` + 240pt 侧栏 + `WideStatusFooter`。
|
|
||||||
|
|
||||||
**7 大用户场景**:
|
|
||||||
|
|
||||||
1. **Onboarding**(`Views/OnboardingExperienceView.swift`)—— 7 步:欢迎 → 权限 → 键盘安装 → 切换引导 → 4-feature 实践(语音/翻译/回复/AI)→ 登录奖励 → 完成。匿名实践用 `OOBEClientInfrastructure` 拿服务器发放的体验金。
|
|
||||||
2. **Home 仪表盘**(`Views/HomeView.swift:122`)—— 7 天柱状图 + 指标瓦片 + Flow 连接卡 + 词典/历史卡 + 中英文高频词条 chip。
|
|
||||||
3. **Settings**(`Views/SettingsView.swift:30`)—— 7 个子面板:账户、AI 代理、剪贴板、Locale、润色强度、翻译目标、语音识别、文本润色、通用、关于。NavigationStack + `SettingsRoute` 枚举。
|
|
||||||
4. **Account Center**(`Views/Account/AccountCenterView.swift:11`)—— 已登录摘要、积分 + 等级、StoreKit 商品列表、购买历史链接、登出 / 删号 (reauth)、推荐链接。下拉刷新。
|
|
||||||
5. **Polish Styles**(`Views/PolishStylesView.swift:11`)—— 内置/社区/自定义样式网格 + 编辑器 sheet + "从历史学习"按钮。
|
|
||||||
6. **AI Agent Skills**(`Views/AIAgentSkillsView.swift:12`)—— 已装/未装技能卡,安装调起 `shortcuts://` 或 bundled `.shortcut` 文件。
|
|
||||||
7. **Legal / 隐私**—— `PrivacyPolicyView` / `OpenSourceLicensesView` / 远程 web view。
|
|
||||||
|
|
||||||
**宿主端 Services(`OSGKeyboard/Services/`)** 关键件:
|
|
||||||
|
|
||||||
| 文件 | 职责 | LOC 估算 |
|
|
||||||
|---|---|---|
|
|
||||||
| `FlowSessionManager.swift` | **唯一拥有者**——AVAudioSession 激活、连续 `.playAndRecord` 捕获、ASR 选择、后处理、`PolishingService` 调用、AI 模式、App Group 写、utterance token、launch reconciliation、start/stop/end session | ~3500 |
|
|
||||||
| `FlowPictureInPictureController.swift` | PiP keep-alive(`AVPictureInPictureVideoCallViewController` 0.1pt content,no audio session) | — |
|
|
||||||
| `FlowASRPostProcessor.swift` | 本地引擎转写后处理(`LocalASRTranscriptCorrector.apply`) | — |
|
|
||||||
| `FlowTerminationCoordinator.swift` | 进程退出时同步释放 mic、结束 LiveActivity | — |
|
|
||||||
| `FlowAnalyticsOperationRegistry.swift` | 会话级分析去重,订阅 `FlowSessionManager` 事件 | — |
|
|
||||||
| `RimeDeploymentController.swift` | Rime 资源部署(host-owned) | — |
|
|
||||||
| `AIAgentShortcutInstaller.swift` | 调起 `shortcuts://` 安装 iCloud 分享的技能 / bundled `.shortcut` | — |
|
|
||||||
| `OOBEClientInfrastructure.swift` | 匿名 OOBE 体验金客户端 | — |
|
|
||||||
| `OfficialSkillCatalogRefreshService.swift` | 15 分钟 ETag-aware 刷新 `https://account.osglab.com/v1/content/skills` | — |
|
|
||||||
| `AIHintRefreshService.swift` | 12 小时静默刷新远程提示包 | — |
|
|
||||||
| `AnalyticsHostService.swift` | `BGTaskScheduler` 注册 (`com.osgkeyboard.ios.analytics-sync`) + `NWPathMonitor` + 后台 drain | — |
|
|
||||||
| `HostReturnService.swift` | 解析 `FlowSessionBridge.pendingHostBundleId()` 通过 `HostAppURLRegistry` 重开原 App | — |
|
|
||||||
| `AppURLHandler.swift` | `UIApplicationDelegate` + `UISceneDelegate`:`osgkeyboard://startflow` / `https://osglab.com/i/...` 通用链接 / `didBecomeActiveNotification` 反弹保留 `sourceApplication` | — |
|
|
||||||
| `AppPermissions.swift` | `AVAudioApplication.requestRecordPermission` (iOS 17+) + `SFSpeechRecognizer` + `PasteAccessResult` 一次性 `pasteAccessVerified` | — |
|
|
||||||
|
|
||||||
**宿主端 Models 关键点**:
|
|
||||||
- `AppGroupConfiguration` 完整 Codable blob —— 镜像到 App Group + iCloud KVS
|
|
||||||
- `ProviderConfig` 8 家供应商预设(openai/ark/deepseek/qwen/zhipu/moonshot/siliconflow/groq)
|
|
||||||
|
|
||||||
**关键 iOS-only 调用**:
|
|
||||||
- `UIOpenURLContext.options.sourceApplication` (iOS 26 only) —— 保留源 bundle id(host-return 白名单唯一路径)
|
|
||||||
- `AVAudioApplication.requestRecordPermission` (iOS 17+)
|
|
||||||
- `SpeechAnalyzer + DictationTranscriber` (iOS 26 only)
|
|
||||||
- `OSAllocatedUnfairLock` (iOS 16+)
|
|
||||||
- iOS 26 Icon Composer (`OSGKeyboard/AppIcon.icon`)
|
|
||||||
- `UIBackgroundModes: audio` + `BGTaskScheduler`
|
|
||||||
- `applinks:osglab.com` Universal Links
|
|
||||||
- `LSApplicationQueriesSchemes` 白名单 50+ 国外 App scheme
|
|
||||||
- `SKIncludeConsumableInAppPurchaseHistory=true` —— StoreKit 2 一次性商品进历史
|
|
||||||
|
|
||||||
**iOS-only entitlements**(在 `OSGKeyboard.entitlements`):
|
|
||||||
- App Group `group.com.osgkeyboard.shared`(与扩展共享)
|
|
||||||
- 两个 keychain-access-groups(shared + iOS-only)
|
|
||||||
- `aps-environment: $(APP_ATTEST_ENVIRONMENT)` ← xcconfig 注入(debug=development / release=production)
|
|
||||||
- Sign in with Apple `Default`
|
|
||||||
- iCloud KVS `$(TeamIdentifierPrefix)com.osgkeyboard.ios`(**单字符串**,非数组 —— 数组会破坏自动签名)
|
|
||||||
- 音频输入
|
|
||||||
- 关联域名 `applinks:osglab.com`
|
|
||||||
|
|
||||||
**StoreKit 配置**(`OSGKeyboard.storekit`):4 个消耗型 —— 自愿打赏 `ByRockyACoffee` (¥28) + 3 档账户积分 `500tks/1500tks/3000tks` (¥8/¥18/¥28)。**无订阅**。
|
|
||||||
|
|
||||||
### 1.3 iOS HostSupport 框架 (`OSGKeyboardHostSupport/`)
|
|
||||||
|
|
||||||
设计原则:**唯一**链接 `Speech / AVFoundation / Charts / StoreKit` 的 iOS 框架——键盘扩展**不**链接它,把 jetsam 预算留给 Shared。
|
|
||||||
|
|
||||||
**子目录**:
|
|
||||||
|
|
||||||
| 路径 | 内容 |
|
|
||||||
|---|---|
|
|
||||||
| `Services/` | ASR + 云 ASR + Flow 捕获 + Dictation + Tip + `CloudASR/` + `Tip/` |
|
|
||||||
| `Models/` | `AudioBufferSnapshot+AVFoundation.swift` —— `AVAudioPCMBuffer` ↔ 共享 `AudioBufferSnapshot` 适配 |
|
|
||||||
| `DesignSystem/` | `SevenDayUsageChart` + `UsageStatsCluster` + `SupportDeveloperSection` |
|
|
||||||
| `Features/Account/` | `AccountAPIClient` / `SignInWithApple` / `DeviceIntegrity` (App Attest) / `HostPrivateAccountKeychain` / `AccountModels` / `OOBEGrantProvisioningCoordinator` |
|
|
||||||
|
|
||||||
**关键 Services**:
|
|
||||||
|
|
||||||
- **`ASRService.swift:1`** —— 公共协议 `transcribe(stream:locale:)` 返回 `AsyncStream<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 镜像 observable,prompt-fingerprint dedup。
|
|
||||||
- **`LocalASRModelManager.swift:11`** —— macOS Qwen3-MLX 权重 Application-Support 安装管理器。状态持久化到 `installed-manifest.json`。**Mac 专用**(iOS 用系统 SpeechAnalyzer)。
|
|
||||||
- **`LocalASRBiasAdapter.swift:9`** —— 分层偏差:`PersonalDictionary.effectiveEntries` + 内置 `BuiltinLexiconIndex.topTerms(...)`;按前台 App bundle id 选 `builtin-computer` vs `builtin-top`(Xcode/VSCode/Android Studio/IntelliJ/AppCode/Sublime/Terminal/iTerm2/Warp)。
|
|
||||||
- **`LocalASRTranscriptCorrector.swift:9-65`** —— 确定式 alias→canonical 替换,最长匹配优先,ASCII whole-word 感知。
|
|
||||||
- **`PolishStyleLearningService.swift:64`** —— `build(from:)` 从 `SpeechHistoryEntry` 历史生成 `PolishStyleLearningCorpus`(5000 字符 `requiredEffectiveCharacterCount`)。
|
|
||||||
- **`AIQuestionService.swift:20`** —— `actor AIConversationStore` 保留 `retainedConversationRounds` 轮 / 对话。
|
|
||||||
- **`AIEventExtraction` / `AIAddressExtraction` / `AITodoExtraction`** —— 解析 LLM 输出为 `start|end|title|location` / `origin|destination` / 列表。**全部 fail-closed**。
|
|
||||||
- **`HostAppURLRegistry`** —— 白名单 deep-link 路径(`deployrime`、`settings/clipboard`、`skill/run` 等),host + ext 共用。
|
|
||||||
- **`KeyboardSetupBridge`** —— `markExtensionAppearance(hasFullAccess:)` + OOBE 实践会话标志。
|
|
||||||
- **`AppGroupConfigDarwin`** —— `CFNotificationCenter` 发 `com.osgkeyboard.config.changed`(host 写完后)。
|
|
||||||
- **`FlowSessionBridge` + `FlowSessionKeys`** —— 跨进程 mailbox 全部 App-Group key:`flowHostReady` / `flowHostReadyAt` / `flowHeartbeat` / `flowSessionActive` / `flowSessionExpires` / `hostHeavy/At` / `hostGeneration` / `transcriptionResult` / `transcriptionPartial` / `transcriptionError{Kind,PolishWarning}` / `pendingKeyboardUtteranceId` / `lastPiPArmAttemptAt` / `flow.commandPayload.v1` / `flow.commandJournalPayload.v2` / `flow.resultPayload.v1` / `flow.ackPayload.v1` / `flow.startTransaction.v1` / `flow.readyPayload.v1`。
|
|
||||||
- **`SettingsCloudSync` / `SpeechHistoryCloudSync` / `UsageStatisticsCloudSync` / `AppCloudSync`** —— KVS 镜像;`ICloudSyncPreferences` + `SyncDeviceID` 保留每设备身份。
|
|
||||||
- **`AIHintStore` + `AIHintPool` + `AIHintKeywordExtractor` + `AIHintKeywordCompressor` + `AIHintLocalCatalog`** —— 闲置热词轮播:App-Group-pack loader、合并远端 + 本地卡片、确定性 10 或 22 字符关键词抽取(zh/en)。
|
|
||||||
- **`TranscriptionPolishFallback.swift`** —— LLM 润色失败或跳过时,交付原始转写 + 软警告。
|
|
||||||
- **`WhatsNewDemoScenario`** —— "What's New" 时间轴 driver,host(peek/consume)和 ext(DEBUG)共用。
|
|
||||||
|
|
||||||
**Polish 子系统详细**:
|
|
||||||
- `PolishContext` (`Models/PolishContext.swift`):appContext + precedingText + followingText + fieldHints + dictionarySupplement + maxPrecedingChars=600 + maxFollowingChars=200
|
|
||||||
- `AppContext` 5 种(code/email/chat/document/unknown)
|
|
||||||
- `AppContextDetector` (`:30-116`):3-fallback 链——`textDocumentProxy.documentContextBeforeInput` 启发式(iPad 沙盒安全;键盘读不到前台 bundle id)→ 30 分钟缓存 → 环境 blend
|
|
||||||
- `PolishStylePack` (`Models/PolishStylePack.swift:9-87`):`Codable, Equatable, Identifiable, Sendable`;`id/name/prompt/allowsAddedEmoji/kind(.builtin/.user)/createdAt/updatedAt`
|
|
||||||
- `PolishStyleLimits`:`maximumUserPacks = 8` / `maximumPromptCharacters = 6_000`
|
|
||||||
- 内置样式分两组:`.practical`(light/structured/formal/chat)+ `.fun`(dating/flex/corp/diba/xhs)
|
|
||||||
|
|
||||||
**Typing 子系统**(`Typing/`):
|
|
||||||
- `LibrimeEngine.swift` —— `@MainActor` 中文 IME,部署 3 套 schema:`osg_pinyin` / `osg_double_pinyin_mspy` / `osg_double_pinyin_sogou`
|
|
||||||
- `EnglishSuggestionEngine.swift` —— 3 槽 QuickType(verbatim / correction / completion),`inVocabularyFrequencyGap = 250`
|
|
||||||
- `EnglishLexicon.swift` —— mmap 40k 词 `english_lexicon.bin`
|
|
||||||
- `EnglishSystemLexicon.swift` —— `#if canImport(UIKit)` 守卫包 `UITextChecker + UILexicon`
|
|
||||||
- `EnglishQWERTYProximity.swift` —— 空间编辑代价
|
|
||||||
- `TypingSessionController` —— 拥有 typing surface:language/page/shift state、English 引擎、librime 引擎、拼音/英文消歧、autocap、句号快捷、shadow preceding text、`supplementaryWords`、hot reload
|
|
||||||
- `TypingSurfaceMetrics` (`Models/TypingSurfaceMetrics.swift`) + `KeyboardChromeLayout` —— 单一源:键行指标、底部动作行分数、总高 281pt,iPad narrow/wide(narrow 54pt / wide 76pt),`wideIPadWidthThreshold = 1100`
|
|
||||||
- `TypingInputConfiguration` —— `TypingInputSchema` (3 case) + `DefaultInputMode` (voice/pinyin/english)
|
|
||||||
- `PeriodShortcut.swift` —— iOS 句号快捷:letter→number 后第二个空格 0.45s 内变 ". " + arm sentence Shift
|
|
||||||
- `TypingAutocapitalization` —— 镜像 `UITextAutocapitalizationType`
|
|
||||||
|
|
||||||
**Models (~50 文件) 关键**:
|
|
||||||
- `AppGroupConfiguration` —— 完整 Codable blob
|
|
||||||
- `SyncedAppSettingsV2` —— per-field `SyncedField<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` 模板 NSImage,target/action = `togglePopover(_:)`
|
|
||||||
- Popover 内容:`NSHostingController(rootView: MacMenuBarPopover())` (`:214`)。未完成 onboarding → "open the main window" 提示;否则 `MacContentView`(品牌 + record 按钮 + 热键提示 + 状态文本 + 滚动转写 ≤120pt + 模式/翻译/连接状态 + footer)
|
|
||||||
- Popover 打开:`prepareForPopoverPresentation()` snapshot 前台 App,**OSGKeyboard 成为 key 时不丢失粘贴目标**
|
|
||||||
|
|
||||||
**录制 overlay**:
|
|
||||||
- `MacDictationOverlayController.swift` —— 一个 borderless non-activating `NSPanel`(level `floatingWindow + 1`、`.canJoinAllSpaces + .fullScreenAuxiliary + .stationary`)
|
|
||||||
- 状态机驱动 show/hide(`viewModel.$isRecording/$isPreparingToRecord/$isProcessing` CombineLatest3)
|
|
||||||
- 位置 user-draggable(双击重置底部居中),持久化到 UserDefaults(`mac.overlay.hasCustomPosition / centerX / originY`)
|
|
||||||
- `MacDictationOverlayView.swift` —— SwiftUI pill(500pt 内容宽)+ 状态点 + 一行转写 + 实时徽章 + `MiniWaveform` + stop 按钮
|
|
||||||
- `MiniWaveform` 复用 `OSGKeyboardShared/DesignSystem`
|
|
||||||
|
|
||||||
**主窗口 5 大页**:
|
|
||||||
- `DashboardView.swift` —— `GeometryReader` 垂直布局:品牌头 + 7 天图(`UsageStatsCluster layout: .split`)+ `dictationStage` 卡片(`viewModel.homePreviewText` 累计预览)+ `BottomDictationBar`(翻译选择器 + record 按钮 + ready chip)
|
|
||||||
- `MacHistoryView.swift` —— 按天分组 `SpeechHistoryStore`,每行 copy/delete context menu,"Clear" 带确认
|
|
||||||
- `MacDictionaryView.swift` —— `PersonalDictionary` 按 `Entry.Category` 分组,按使用次数 + 术语排序;搜索框过滤;`+` 弹 `MacDictionaryEntryEditor` sheet
|
|
||||||
- `MacPolishStylesView.swift` —— `PolishStylePack` 目录(内置 practical+fun + 用户),只读 prompt 详情 / 完整编辑器(`allowsAddedEmoji` 切换 + 2400 字符 prompt 限制);学习卡:语料进度 + "Generate learned style" → `PolishStyleLearningService.generateStyle`
|
|
||||||
- `MacSettingsView.swift` —— `NavigationStack + ScrollView` 7 段:support developer (Tip) / general (appearance / interface language / recognition language / iCloud sync) / recognition method (cloud vs local) / cloud ASR provider / local ASR model catalog / polish provider / input (hotkey / auto-paste / accessibility 状态) / legal (Privacy Policy + Third-Party Licenses + Restart Onboarding + version)
|
|
||||||
|
|
||||||
**Onboarding**:
|
|
||||||
- `MacOnboardingView.swift:21` —— 6 步:`welcome → microphone → accessibility → engine → [.cloudAPI | .localModel]`,根据 `viewModel.config.engineMode` 分支
|
|
||||||
- Mic 步调 `AVCaptureDevice.requestAccess(for: .audio)`;accessibility 步打开 `x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility` 并 1s 后重查 `AXIsProcessTrusted`
|
|
||||||
- Cloud 步收 LLM provider + key + model;local 步显示默认 Qwen3-MLX 0.6B + 下载按钮 + 进度 + "skip" 提示
|
|
||||||
- 状态存 `mac.hasCompletedMacOnboarding`
|
|
||||||
- 注释 `MacOnboardingView.swift:7-8` —— "Sherpa identifiers and install records are retained only for migration compatibility":**近期从 Sherpa 换到 MLX**
|
|
||||||
- Settings → About 有 "Restart Onboarding" 按钮
|
|
||||||
|
|
||||||
**Legal / Support**:
|
|
||||||
- `MacLegalSettingsViews.swift` —— `MacPrivacyPolicyView`(`WKWebView` 加载 `OSGKeyboard/Resources/PrivacyPolicy.html`,按 `uiLanguage` 切滚动锚点)+ `MacOpenSourceLicensesView`(`OpenSourceLicenseCatalog.entries(for: .macOS)` 列表)→ `MacOpenSourceLicenseDetailView`(许可证名 / upstream 链接 / 用途 / 文本)
|
|
||||||
- `MacSupportDeveloperTipRows.swift` —— StoreKit 2 `TipPurchaseManager.shared` 消耗型打赏行
|
|
||||||
|
|
||||||
**DEBUG 工具**:
|
|
||||||
- `osgkeyboard://seed-demo` URL 触发 `DemoDataSeeder.seedRichPlaceholderData`
|
|
||||||
- `MacAudioRecorder.hasLiveSnapshotSink`(DEBUG seam,见 `MacAudioRecorder.swift:143`)
|
|
||||||
|
|
||||||
### 2.3 全局热键 + 辅助功能 + 粘贴注入
|
|
||||||
|
|
||||||
**`MacHotkeyService.swift`**:
|
|
||||||
- 用**两个** `NSEvent.add*MonitorForEvents(matching: .flagsChanged)`(global + local)
|
|
||||||
- 区分左/右 Option(`NX_DEVICELALTKEYMASK = 0x20` / `NX_DEVICERALTKEYMASK = 0x40`);`MacHotkeyTrigger`(`rightOption / leftOption / eitherOption`)
|
|
||||||
- 150ms 持有防抖(`scheduleBegin` → `Task.sleep(150ms)` → fire `onPressBegan`),松开取消未发起的 begin
|
|
||||||
- `start()` 返回 Accessibility 状态(`globalFlagsMonitor == nil` ⇒ 大概率无权限),DEBUG 下 NSLog
|
|
||||||
|
|
||||||
**`MacTextInsertionService.swift`** —— Accessibility 门控合成 ⌘V:
|
|
||||||
- `AXIsProcessTrustedWithOptions([kAXTrustedCheckOptionPrompt: true])` 按需
|
|
||||||
- `insert(_:autoPaste:targetApp:)`:清 `NSPasteboard.general` → 写转写 → snapshot 原始 pasteboard(**所有 representation,不仅是 `.string`**)→ `CGEvent + kVK_ANSI_V + cghidEventTap` 合成 ⌘V → 等 500ms 后**仅当 change count 仍匹配 post-transcript write**才恢复 snapshot(用户/clipboard-manager 写入期间不覆盖)
|
|
||||||
- `FrontmostAppTracker`(`:177`)跟踪 `NSWorkspace.didActivateApplicationNotification`,**OSGKeyboard popover 激活时**保留原始 paste 目标
|
|
||||||
- `activate(_:)` 重新激活目标 App,等最多 1s 成为 frontmost
|
|
||||||
- `shouldRestorePasteboard` + `restoreItems` 是单测 seam
|
|
||||||
|
|
||||||
### 2.4 音频捕获
|
|
||||||
|
|
||||||
**`MacAudioRecorder.swift`**:
|
|
||||||
- `MacAudioRecording` 协议
|
|
||||||
- **`AVAudioEngine.inputNode`** + 4096 帧 tap
|
|
||||||
- 输入格式 → 16kHz mono Float32 via `AVAudioConverter`
|
|
||||||
- 10 分钟 @16kHz 硬上限 + 30s trim hysteresis(卡住的热键不会无限增长 buffer)
|
|
||||||
- RMS 平滑(attack 0.5 / decay 0.15)
|
|
||||||
- 权限:`AVCaptureDevice.requestAccess(for: .audio)`;拒/受限抛 `microphoneAccessDenied`
|
|
||||||
- `makeSnapshotStream() -> AsyncStream<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` stream,100ms chunk(1600 samples @16kHz)喂 MLX,draining 时 `drainTracker.noteAudio`。然后喂余量,调 `session.stop()`,跑 `MacHallucinationFilter.shouldDiscardHotwordDump` 丢热词-only 转写
|
|
||||||
- `MacSpeechLocalASR.swift` —— Apple Speech 兜底。16kHz PCM 写临时 WAV(`PCMSampleWavEncoder`),`SFSpeechURLRecognitionRequest` 强制 `requiresOnDeviceRecognition`(locale 无 on-device 模型时立即失败 → 清晰错误"去 系统设置 → 键盘 → 听写 下载")。Chinese locale 应用 `CustomLanguageModelManager.applyCustomLanguageModel(to:locale:bias:)`(与 iOS 共用)。2× 音频长硬超时 + 30s floor,`RecognitionSession` 锁保护单次 resume,识别回调和超时 task 正确竞速
|
|
||||||
- `MacCloudASRChunkAdapter.swift` —— 薄 `ASRChunkTranscribing` 适配器,包装 `CloudASRClientFactory.make(store:)` 给 `MacDictationPipeline` 用
|
|
||||||
|
|
||||||
**模型目录**(`OSGKeyboard/Resources/LocalASR/local-asr-catalog.json`,2.0.1 状态):
|
|
||||||
- `schemaVersion: 1`
|
|
||||||
- `defaultModelId: "qwen3-mlx-0.6b-4bit"`
|
|
||||||
- `runtimes: []`(**空**——runtime 随 bundle 走,MLX 内置)
|
|
||||||
- 2 个模型:
|
|
||||||
1. **`qwen3-mlx-0.6b-4bit`** —— Qwen3-ASR 0.6B,`backend: mlx`,`runtimePlatform: macos`,`sizeBytes: 730000000`(~0.7 GB),locale `zh-CN/en-US`,支持热词(`promptOnly`),`badgeKey: mac.localASR.badge.balanced`,路径 `models/qwen3-mlx-0.6b-4bit`。两个下载源(`hfmirror`/`huggingface`)都在 `mlx-community/Qwen3-ASR-0.6B-4bit`。文件:`config.json` / `generation_config.json` / `preprocessor_config.json` / `model.safetensors`(~708MB) / `model.safetensors.index.json` / `tokenizer_config.json` / `merges.txt` / `vocab.json`
|
|
||||||
2. **`qwen3-mlx-1.7b-4bit`** —— Qwen3-ASR 1.7B,同上形状,`sizeBytes: 1700000000`(~1.7GB),`badgeKey: mac.localASR.badge.quality`。源在 `mlx-community/Qwen3-ASR-1.7B-4bit`
|
|
||||||
|
|
||||||
**`local-asr-catalog.json` 中零 iOS 模型**。iOS app 运行时**不**用本目录——iOS 用 iOS 26 `SpeechAnalyzer + DictationTranscriber`,代码路径不同(`OSGKeyboardHostSupport/Services/ASRService.swift`,Mac 排除)。
|
|
||||||
|
|
||||||
**模型生命周期**:
|
|
||||||
- `loadModel(_:)` (`MacMLXStreamingASRProvider` 第 69-81 行) 缓存 loaded `Qwen3ASRModel` per model id。切模型失效缓存并 re-warm。`warmupIfNeeded` 每个冷模型跑一次
|
|
||||||
- 卸载(`MacLocalASRModelSettingsViewModel.deleteModel`)→ 共享 `LocalASRModelManager.deleteModel(_:catalog:)` 删磁盘权重;actor 缓存**不**自动清——下次不同 id 的 `loadModel` 自然替换
|
|
||||||
|
|
||||||
**Mac 不支持 iOS 风格的"on-device + cloud hybrid"**——只有 engine-mode 开关(`local` → MLX 或 Apple Speech;`cloud` → 已配云 ASR)。两种模式共用同一 LLM 润色步。
|
|
||||||
|
|
||||||
### 2.6 LLM 润色 + 供应商集成
|
|
||||||
|
|
||||||
- `MacDictationPipeline.swift:254` —— 跑的是**同一份** `PolishingService(store: store).polishWithOutcome(...)`(iOS `OSGKeyboardShared/Services/PolishingService.swift`)
|
|
||||||
- `PolishContext` 用 `appContext`(从目标 App bundle id 通过 `MacAppContextService` 抓)+ `dictionarySupplement`(来自 local-bias `polishFragment`),**和 iOS 同款**
|
|
||||||
- `MacSettingsView.swift:111` (`polishProviderSection`) 用 `viewModel.polishSelectableProviders`(`LLMProvider.userSelectablePresets`——**完整集,与 iOS 相同**)。行:provider / API key / baseURL / model(`MacProviderModelRow` "拉模型列表" 动作) / thinking 切换 / `MacProviderToolsRow` "Test connection" / 翻译目标
|
|
||||||
- `MacSettingsView.swift:173` (`asrProviderSection`) 用 `viewModel.asrSelectableProviders`(`LLMProvider.asrSelectablePresets`)。Volcengine 有自定义 auth-mode(API key vs App ID + Access Token)通过 `VolcengineASRFields.parse` / `updateMacVolcengine(...)`;其他供应商走通用 `baseURL/apiKey/model` 行 + "Test connection"
|
|
||||||
- **Mac 没有 Mac 专用 polish/ASR 供应商集**——供应商面和 iOS 一致(DeepSeek/OpenAI/Anthropic 兼容 LLM;Volcengine/OpenAI Realtime/Bailian/Alibaba 云 ASR)
|
|
||||||
- **MLX 供应商仅用于本地 ASR**——**无** Mac 端 MLX LLM 润色
|
|
||||||
|
|
||||||
### 2.7 持久化
|
|
||||||
|
|
||||||
| 类别 | 实现 | Key 前缀 / 位置 |
|
|
||||||
|---|---|---|
|
|
||||||
| UserDefaults | 标准 | `mac.*`(`MacHotkeyService:41` / `MacAppearance:56` / `MacDictationViewModel:103-105` / `MacDictationOverlayController:43-45`)—— 热键、auto-paste、appearance、onboarding、overlay 位置、MLX 模型选择、下载源、界面语言 |
|
|
||||||
| App Group | 共享 `AppGroupStore` | 与 iOS 同:`PersonalDictionary` / `PolishStyleCatalog+active id` / `translationTarget` / `detected appContext` / `speechHistory` / `usageStatistics` / `engineMode` / `localeId` / LLM API key & baseURL / 云 ASR provider id/apiKey/model/baseURL / iCloud-sync-enabled |
|
|
||||||
| Keychain | 共享 `Keychain` | `keychain-access-groups: [$(AppIdentifierPrefix)com.osgkeyboard.shared]`(entitlement)—— 存 LLM API key |
|
|
||||||
| iCloud KVS | 5 路全部接好 | `MacICloudSyncBootstrap.swift` 接 `AppCloudSync(makeStore:historyDefaults:)`,暴露 `settingsSync/dictionarySync/polishStyleSync/appCloudSync`,**entitlement 用 `$(TeamIdentifierPrefix)com.osgkeyboard.ios` 和 iOS 同桶** |
|
|
||||||
| 语音历史 | `SpeechHistoryStore.shared` + `SpeechHistoryStorage` | UserDefaults 后端 + iCloud `SpeechHistoryCloudSync` |
|
|
||||||
| 模型权重 | `LocalASRModelInstallState.rootDirectory()` | 由共享 `LocalASRModelManager` 管 |
|
|
||||||
| 临时文件 | `FileManager.default.temporaryDirectory` | Apple Speech WAV(`osg-mac-asr-<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` | 仅 0(Mac 装包不含 `.shortcut`) | 整套 AI mode 体验 Mac 无 |
|
|
||||||
| **Provider 切换粒度** | iOS 通过 iOS `EnginePickerSection` + `ProviderPickerSection` 在 Settings 内 + 主页快捷 | Mac 整段 `recognitionSection` + `asrProviderSection` + `polishProviderSection` 拆开 | iOS 较紧,Mac 较松 |
|
|
||||||
|
|
||||||
### 3.3 Mac 自身落后 iOS 之处("应该补"清单)
|
|
||||||
|
|
||||||
按"代码复用难度 + 用户价值"排序:
|
|
||||||
|
|
||||||
| 优先级 | 功能 | 现状 | 工作量 | 建议 |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| **P0** | **剪贴板历史 + AI 技能面板(Mac UI)** | 共享代码全到位,零 Mac UI | **小**——`MacClipboardHistoryView` + `MacAIClipboardSkillView`(仿 `MacHistoryView`)+ 改 `MacSettingsView` 加一个 Clipboard 段 | 立刻做。代码复用 100%,价值高 |
|
|
||||||
| **P0** | **CLM 设置页(Mac UI)** | 后台在跑(`OSGKeyboardMacApp:117`),无 UI | **小**——`MacCLMSettingsView`,仿 `MacLocalASRModelSettingsView` 模板 | 立刻做。让用户能编辑 phrase 库 |
|
|
||||||
| **P1** | **OSG 账户 + Sign in with Apple + 积分购买** | 共享代码编进 Mac,零 UI;Mac entitlement 缺 `com.apple.developer.applesignin` + 无 `SKIncludeConsumableInAppPurchaseHistory` | **中**——加 entitlement + 改 `MacICloudSyncBootstrap` 已涵盖大部分,加 `MacAccountCenterView`(仿 `AccountCenterView` 但精简)+ 改 Settings | 一周内可交付 |
|
|
||||||
| **P1** | **OSG credits 消耗型 IAP(500tks/1500tks/3000tks)** | 共享 `TipPurchaseManager` 已装;iOS 走 `AccountCreditPurchaseManager` | **中**——`MacAccountCenterView` 出来后一气呵成 | 同上 |
|
|
||||||
| **P2** | **一方分析(Mac)** | 共享 `AnalyticsClient` 等全编,零调用 | **中-大**——`MacAnalyticsHostService` + 隐私 / opt-in + 后台任务(Mac 上走 `NSProcessInfo.thermalState` 节流 + 用户同意时 `NSTask` 跑)| 半年窗口内 |
|
|
||||||
| **P2** | **AI 提示轮播 / 上下文技能(Mac)** | 共享代码全到位 | **中**——加 `MacAIHintPool` 屏 + `MacAIQuestionService` 入口 + Shortcuts bundle 装载 | 与 P0 剪贴板可联动 |
|
|
||||||
| **P3** | **助手指令集成** | 共享代码 + iOS bundled `.shortcut` | **中**——Mac 装 `.shortcut` + `AIAgentShortcutInstaller` 调起(`shortcuts://` URL 仍可用)| 与 P2 AI 提示联动 |
|
|
||||||
| **P3** | **最后输入编辑** | 共享代码全到位 | **大**——需要 Mac 端保留"刚才插入的引用"(`EditableInputReference`),但 Mac 走 Accessibility ⌘V 注入,**没有 `EditableInputReference` 的来源**——需要新协议 | 架构问题,先做 PoC |
|
|
||||||
| **P3** | **后台同步(Analytics 任务)** | 无 | **中**——`NSTask`/`SMAppService` LaunchAgent 拉 analytics;与 P2 联动 | |
|
|
||||||
|
|
||||||
### 3.4 Mac 形态上"不应该补"的功能
|
|
||||||
|
|
||||||
| 功能 | 原因 |
|
|
||||||
|---|---|
|
|
||||||
| **Flow 跨进程会话** | Mac 单进程、单用户、单次录制——Flow 设计为键盘扩展 ↔ 宿主 App 的两进程 mailbox。强行移植会引入不必要的 IPC 开销。**Mac 用 `MacDictationPipeline` + `MacDictationViewModel` + `MacDictationOverlayController` 三件套已足够** |
|
|
||||||
| **PiP keep-alive** | macOS 无系统 PiP。`MacDictationOverlayController` 的 NSPanel 已占位 |
|
|
||||||
| **Rime / 拼音 / 英文 autocorrect** | Mac 不是键盘扩展,没输入面。`LibrimeEngine` 是 iOS C/Objective-C++ xcframework,**Mac 结构性不兼容**。即便 Mac 装键盘扩展(`Designed for iPad` 那种),librime 也需要重编译 |
|
|
||||||
| **`UIBackgroundModes: audio`** | Mac 没用——按住说话时 NSPanel 是 non-activating,不需要 audio session 后台保持 |
|
|
||||||
| **`UIOpenURLContext.options.sourceApplication`** | iOS 26 only API;Mac 走 `NSWorkspace.didActivateApplicationNotification` 已解决前台 App 跟踪 |
|
|
||||||
| **`AVAudioApplication.requestRecordPermission`** | iOS 17+ only;Mac 走 `AVCaptureDevice.requestAccess(for: .audio)` |
|
|
||||||
| **App Group 跨进程 mailbox** | Mac 单进程不需要。但 `AppGroup` + iCloud KVS 是用户态多设备同步用的,**保留** |
|
|
||||||
| **iOS 风格的 idle 屏** | Mac 主窗口即 dashboard,不需要 idle 屏的"AI hint 轮播";但 `MacAIHintPool` 屏可以放在 Dashboard 顶部,与 P2 联动 |
|
|
||||||
| **`LSApplicationQueriesSchemes`** | iOS only,Mac 无 `canOpenURL` 限制 |
|
|
||||||
| **`SKIncludeConsumableInAppPurchaseHistory` / Mac storekit 文件** | Mac App Store 与 iOS App Store 独立 SKU;目前 Mac 装包是 Developer ID 渠道,**不进 Mac App Store**,所以 storekit 不需要 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 适合在 Mac 上做开发和移植的功能
|
|
||||||
|
|
||||||
### 4.1 应该现在做的(P0)
|
|
||||||
|
|
||||||
#### ① 剪贴板历史 + AI 技能面板
|
|
||||||
|
|
||||||
**为什么 Mac 适合**:共享代码 100% 到位(`ClipboardHistoryStore` / `ClipboardSemanticAnalyzer` / `ClipboardSkillSemanticRanker` / `AIClipboardSkill` / `AIClipboardPrompt`),iOS 8 个 UI 调试稳定,只需套 Mac 风格。
|
|
||||||
|
|
||||||
**具体工作**:
|
|
||||||
- 新增 `OSGKeyboardMac/Views/MacClipboardHistoryView.swift`(仿 `MacHistoryView`)
|
|
||||||
- 新增 `OSGKeyboardMac/Views/MacAIClipboardSkillView.swift`(仿 `AIAgentSkillsView`,精简为 Read/Edit/Disable)
|
|
||||||
- `MacSettingsView.swift` 加 `clipboardSection`(含 toggle 启用历史 / toggle 启用技能候选条 / "Manage Skills" 链接 / "Open History" 链接)
|
|
||||||
- `MacDictationViewModel` 订阅 `ClipboardHistoryStore.entries`(`@Published var clipboardEntries`)
|
|
||||||
- 复用 `ClipboardCaptureCoordinator` 不可(ext-only)——需要 Mac 端 `MacClipboardMonitor`(`NSPasteboard.general.changeCount` 轮询 + sanitize)
|
|
||||||
|
|
||||||
**测试**:`MacClipboardMonitorTests`(用 in-memory pasteboard 模拟)/ `MacClipboardHistoryViewModelTests`
|
|
||||||
|
|
||||||
**工作量**:1-2 周
|
|
||||||
|
|
||||||
#### ② CLM 设置页
|
|
||||||
|
|
||||||
**为什么 Mac 适合**:Mac 后台已经在跑 CLM(`OSGKeyboardMacApp:117`),只是用户管不到。
|
|
||||||
|
|
||||||
**具体工作**:
|
|
||||||
- 新增 `OSGKeyboardMac/Views/MacCLMSettingsView.swift`(仿 `MacLocalASRModelSettingsView`)
|
|
||||||
- 复用 `CustomLanguageModelManager.shared.state`(`@Published idle/preparing/ready/failed`)
|
|
||||||
- 复用 `OSGKeyboard/Resources/HostCLM/v1/OSGKeyboardCLM.bin`(已在 Mac 装包)
|
|
||||||
- 复用 `PersonalDictionary` 共享存储(用户输入的 phrase 库 + iCloud 同步)
|
|
||||||
- 提供 phrase 列表 / 编辑 / 触发"重编译"按钮
|
|
||||||
|
|
||||||
**测试**:`MacCLMSettingsViewModelTests`(验 `state` 状态机)
|
|
||||||
|
|
||||||
**工作量**:1 周
|
|
||||||
|
|
||||||
### 4.2 应该中期做的(P1)
|
|
||||||
|
|
||||||
#### ③ OSG 账户 + Sign in with Apple + 积分购买
|
|
||||||
|
|
||||||
**为什么 Mac 适合**:共享代码编进 Mac 都没报错,缺的是 entitlement + UI。
|
|
||||||
|
|
||||||
**先决条件**:
|
|
||||||
- `OSGKeyboardMac.entitlements` 加 `com.apple.developer.applesignin: [Default]`
|
|
||||||
- `OSGKeyboardMac/Info.plist` 加 `SKIncludeConsumableInAppPurchaseHistory: true`
|
|
||||||
- (选做)把 `OSGKeyboard.storekit` 内容 mirror 到 `OSGKeyboardMac.storekit` 或共享
|
|
||||||
|
|
||||||
**具体工作**:
|
|
||||||
- 加 `MacAccountCenterView`(仿 iOS `AccountCenterView` 但精简)
|
|
||||||
- 复用 `LiveAccountServices` / `AccountSessionCoordinator`(`@MainActor` state machine)
|
|
||||||
- 复用 `AccountCreditPurchaseManager`(StoreKit 2 product list → 服务器校验)
|
|
||||||
- Settings → 加 "Account" 段,含 sign-in / 积分余额 / 购买历史 / 登出 / 删号
|
|
||||||
|
|
||||||
**测试**:`MacAccountSessionCoordinatorTests`(in-memory `AccountAPIClient` fake)
|
|
||||||
|
|
||||||
**工作量**:2-3 周
|
|
||||||
|
|
||||||
#### ④ Mac App Attest / DeviceCheck
|
|
||||||
|
|
||||||
**为什么 Mac 适合**:iOS attestation 后端已经在用,Mac 端可以通过 `DCAppAttestService`(macOS 13+,需要 Mac Catalyst 但当前 `SUPPORTS_MACCATALYST: NO`)或者改用 DeviceCheck `DCDevice.generateToken`(macOS 13+ available without Catalyst)。
|
|
||||||
|
|
||||||
**先决条件**:
|
|
||||||
- `OSGKeyboardMac.entitlements` 加 `com.apple.developer.devicecheck.appattest-environment: $(APP_ATTEST_ENVIRONMENT)`
|
|
||||||
- 若走 App Attest,需要 `SUPPORTS_MACCATALYST: YES`(改大改)或改 DeviceCheck-only
|
|
||||||
|
|
||||||
**具体工作**:
|
|
||||||
- `MacDeviceIntegrityCoordinator`(仿 iOS,但用 `DCDevice`)
|
|
||||||
- `AccountAPIClient` 复用,调 `/v1/integrity/attest`
|
|
||||||
|
|
||||||
**工作量**:1-2 周(仅 Mac);+ Catalyst 决策时间
|
|
||||||
|
|
||||||
### 4.3 应该长期做的(P2)
|
|
||||||
|
|
||||||
#### ⑤ 一方分析(Mac)
|
|
||||||
|
|
||||||
**为什么 Mac 适合**:Mac 用户行为和 iOS 同样有价值(知道用户用不用 MLX 模式、热键触发频率、词条覆盖、润色样式流行度)。
|
|
||||||
|
|
||||||
**先决条件**:
|
|
||||||
- 共享 `AnalyticsClient` / `AnalyticsRepository` 已经完备
|
|
||||||
- Mac 上加 `MacAnalyticsHostService`(仿 iOS):监听 `viewModel.$isRecording/$isPreparingToRecord/$isProcessing` 推送事件
|
|
||||||
- 隐私 / opt-in:在 onboarding 末加一步
|
|
||||||
|
|
||||||
**挑战**:
|
|
||||||
- Mac 后台无 `BGTaskScheduler`——改用 `NSProcessInfo.thermalState` + `ProcessInfo.isLowPowerModeEnabled` 节流,**或** `SMAppService` 拉个 LaunchAgent
|
|
||||||
- `AnalyticsUploadCoordinator` 的移动策略(threshold 20 / 60s flush)要 Mac 化
|
|
||||||
|
|
||||||
**工作量**:3-4 周
|
|
||||||
|
|
||||||
#### ⑥ AI 提示轮播 / 上下文技能
|
|
||||||
|
|
||||||
**为什么 Mac 适合**:Mac dashboard 缺内容——除了使用统计,闲置时可推"想一句"灵感卡。
|
|
||||||
|
|
||||||
**具体工作**:
|
|
||||||
- `MacAIHintPool` 嵌在 `DashboardView` 顶部(仿 iOS `AIKeyboardView` 的 carousel 区域)
|
|
||||||
- 复用 `AIHintStore` / `AIHintPool` / `AIHintKeywordExtractor` / `AIHintLocalCatalog`
|
|
||||||
- 上下文技能入口放 `MacContentView`(popover 内 record 按钮旁的下拉)
|
|
||||||
|
|
||||||
**工作量**:2-3 周
|
|
||||||
|
|
||||||
#### ⑦ 助手指令集成
|
|
||||||
|
|
||||||
**为什么 Mac 适合**:Mac 上有 Shortcuts.app(更成熟),bundled `.shortcut` 装入 Mac App bundle 即可。
|
|
||||||
|
|
||||||
**具体工作**:
|
|
||||||
- 装 `OSGKeyboard/Resources/Shortcuts/*.shortcut` 进 Mac bundle
|
|
||||||
- 复用 `AIAgentShortcutInstaller` / `AIAgentShortcutRun`
|
|
||||||
- popover / dashboard 加技能快捷入口
|
|
||||||
|
|
||||||
**工作量**:1-2 周
|
|
||||||
|
|
||||||
### 4.4 不应该在 Mac 做的(但代码可以清理)
|
|
||||||
|
|
||||||
| 共享代码 | 为什么不该在 Mac 跑 | 处置 |
|
|
||||||
|---|---|---|
|
|
||||||
| `OSGKeyboardHostSupport/Features/Account/*`(Mac 已编) | App Attest 路径 iOS 专属;Mac 没 entitlement | 等 ④ 决策后从 Mac target 排除或接 UI |
|
|
||||||
| `OSGKeyboardShared/Features/Analytics/*`(Mac 已编) | 无 Mac 端调用 | 等 ⑤ 决策后从 Mac target 排除或接服务 |
|
|
||||||
| `OSGKeyboardShared/Features/ManagedGateway/*`(Mac 已编) | 是账户 grant 体系,与 ③ 绑定 | 等 ③ 决策后处理 |
|
|
||||||
| `OSGKeyboardShared/Services/AIClipboard*`(Mac 已编) | 与 ① 绑定 | ① 实现后从"无调用"变"有调用" |
|
|
||||||
| `OSGKeyboardShared/Services/AIHint*`(Mac 已编) | 与 ⑥ 绑定 | 同上 |
|
|
||||||
| `OSGKeyboardShared/Services/AIUserSkill*` / `AIAgentSkill*`(Mac 已编) | 与 ③⑦ 绑定 | 同上 |
|
|
||||||
| `OSGKeyboardShared/Services/Flow*`(Mac 已编) | Mac 架构不兼容 | **立刻**从 Mac target 排除(白付编译器开销) |
|
|
||||||
| `OSGKeyboardShared/Services/KeyboardState.swift`(Mac 已排除)| 已经是 Mac 排除 ✓ | 无 |
|
|
||||||
| `OSGKeyboardShared/Services/EditTransactionStore.swift` 等 | 与"最后输入编辑"绑定 | 见 P3 |
|
|
||||||
|
|
||||||
### 4.5 立即可清的"白编译"清单
|
|
||||||
|
|
||||||
```
|
|
||||||
OSGKeyboardShared/Services/FlowSessionBridge.swift + 所有 +* 文件
|
|
||||||
OSGKeyboardShared/Services/FlowSessionKeys.swift
|
|
||||||
OSGKeyboardShared/Services/FlowSessionDarwin.swift
|
|
||||||
OSGKeyboardShared/Services/FlowSessionPolicy.swift
|
|
||||||
OSGKeyboardShared/Services/FlowStartTransactionPolicy.swift
|
|
||||||
OSGKeyboardShared/Services/FlowKeyboardPolicies.swift
|
|
||||||
OSGKeyboardShared/Services/FlowHandoffPolicy.swift
|
|
||||||
OSGKeyboardShared/Services/UtteranceStreamChunker.swift
|
|
||||||
OSGKeyboardShared/Services/UtteranceBatchFallbackPolicy.swift
|
|
||||||
OSGKeyboardShared/Services/UtteranceTranscriptGuard.swift
|
|
||||||
OSGKeyboardShared/Services/UtteranceTranscriptStitcher.swift
|
|
||||||
OSGKeyboardShared/Services/TranscriptOverlapUtilities.swift
|
|
||||||
OSGKeyboardShared/Services/ProgressiveDictationTranscriptAccumulator.swift # 已排除
|
|
||||||
OSGKeyboardShared/Services/EditTransactionStore.swift
|
|
||||||
OSGKeyboardShared/Services/EditUsageMetricsStore.swift
|
|
||||||
OSGKeyboardShared/Services/EditLastInputPromptComposer.swift
|
|
||||||
OSGKeyboardShared/Services/EditOutputValidator.swift
|
|
||||||
OSGKeyboardShared/Models/Flow*.swift (全部)
|
|
||||||
OSGKeyboardShared/Models/EditableInputReference.swift
|
|
||||||
OSGKeyboardShared/Models/EditSessionState.swift
|
|
||||||
OSGKeyboardShared/Models/FlowUtterance*.swift
|
|
||||||
OSGKeyboardShared/Models/FlowInactivityDuration.swift
|
|
||||||
OSGKeyboardShared/Utilities/FlowCaptureTailDrain.swift
|
|
||||||
OSGKeyboardShared/Utilities/FlowUtteranceEndCoordinator.swift
|
|
||||||
OSGKeyboardShared/Utilities/FlowUtterancePCMStore.swift
|
|
||||||
OSGKeyboardShared/Utilities/FlowTrace.swift
|
|
||||||
OSGKeyboardShared/Utilities/FlowPipelineDiagnostics.swift
|
|
||||||
OSGKeyboardShared/Utilities/HostMemoryBudget.swift
|
|
||||||
OSGKeyboardShared/Utilities/KeyboardExtensionMemoryTelemetry.swift # 已排除
|
|
||||||
OSGKeyboardShared/Features/Analytics/ # 全部 12+ 文件
|
|
||||||
OSGKeyboardShared/Features/ManagedGateway/ # 全部 8+ 文件
|
|
||||||
OSGKeyboardHostSupport/Features/Account/ # 全部
|
|
||||||
OSGKeyboardHostSupport/Services/CloudASR/AlibabaVocabularySync.swift # iOS-only API
|
|
||||||
OSGKeyboardHostSupport/Services/ASRChunkTranscribing.swift # 需检查
|
|
||||||
```
|
|
||||||
|
|
||||||
(**建议**):在 `project.yml:558-602` 给 Mac target 的 source list 加 `excludes:`,避免编译这些文件后被链接器裁掉造成的 dead-code 体积。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. iOS 端需要"修一下"或注意的地方
|
|
||||||
|
|
||||||
报告主体是 macOS 差距,但 iOS 端顺手列出 5 个明显可改进点(不修不影响功能,但提升质量):
|
|
||||||
|
|
||||||
1. **`OSGKeyboardHostSupport/Features/Account/*` 整块**虽然不通过 Mac UI 暴露,但通过 source include 编进 Mac 二进制——长期应从 Mac target 排除或加 `MacAccountServices` 实际使用
|
|
||||||
2. **`OSGKeyboardShared/Features/Analytics/*`** 同上——Mac 编译进二进制但无任何调用
|
|
||||||
3. **`OSGKeyboardShared/Features/ManagedGateway/*`** 同上
|
|
||||||
4. **`project.yml:570-602`** 的 Mac target source list 很长且依赖手写排除,**建议**拆出 `OSGKeyboardMacExcludes.yml` 维护
|
|
||||||
5. **`OSGKeyboardShared/Typing/EnglishSystemLexicon.swift:11`** 是**唯一**有 `import UIKit` 的 Shared 文件——是 iOS ext 独享的 QuickType 数据源。考虑改名/移动到 `OSGKeyboardExt/` 减面
|
|
||||||
6. **iOS `FlowSessionManager` ~3500 LOC** 是单一 god object;建议把 `FlowPictureInPictureController` / `FlowTerminationCoordinator` / `FlowAnalyticsOperationRegistry` 三个生命周期拥有者抽离
|
|
||||||
7. **iOS `appDelegate` 反弹 `.onOpenURL`** (`AppURLHandler.swift`) 注释里写 `UIOpenURLContext.options.sourceApplication` iOS 26 only——这个 API 在 iOS 26 还在吗?值得 verify(影响 deep-link host-return 白名单)
|
|
||||||
8. **`KeyboardViewController` `didReceiveMemoryWarning` (`:390-404`)** 强制回 voice surface——typing 引擎是最大内存消费者,但每次收到警告就退出 typing 体验略激进;考虑加 cooldown
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 实施建议
|
|
||||||
|
|
||||||
### 6.1 短期(未来 1-2 周)
|
|
||||||
|
|
||||||
- **任务 A:从 Mac target 排除 `Flow*` / `Edit*` / `Analytics*` / `ManagedGateway*` / `Account*`(HostSupport)所有共享代码**
|
|
||||||
- 修改 `project.yml:570-602`,加 `excludes:` 列表
|
|
||||||
- 验证 Mac target 仍能 build & test pass
|
|
||||||
- 节省 Mac 二进制 ~2-4 MB + 减少编译时间 ~10-15s
|
|
||||||
- **任务 B:Mac 剪贴板历史 + AI 技能面板**(P0 ①)
|
|
||||||
- **任务 C:Mac CLM 设置页**(P0 ②)
|
|
||||||
|
|
||||||
### 6.2 中期(1-2 月)
|
|
||||||
|
|
||||||
- **任务 D:OSG 账户 + Sign in with Apple + 积分**(P1 ③)
|
|
||||||
- **任务 E:App Attest 决策 + 实施**(P1 ④)
|
|
||||||
|
|
||||||
### 6.3 长期(3-6 月)
|
|
||||||
|
|
||||||
- **任务 F:Mac 一方分析**(P2 ⑤)
|
|
||||||
- **任务 G:AI 提示轮播 / 上下文技能**(P2 ⑥)
|
|
||||||
- **任务 H:助手指令集成**(P2 ⑦)
|
|
||||||
|
|
||||||
### 6.4 决策项
|
|
||||||
|
|
||||||
| 决策 | 选项 | 影响 |
|
|
||||||
|---|---|---|
|
|
||||||
| **App Attest 路径** | (a) Catalyst + `DCAppAttestService`(macOS 13+)<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) 支持每句 hybrid(local first,失败 fallback cloud) | (b) 需要重新设计 MLX streaming 端 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. 关键引用
|
|
||||||
|
|
||||||
| 主题 | 文件 | 行号 |
|
|
||||||
|---|---|---|
|
|
||||||
| iOS 宿主入口 | `OSGKeyboard/OSGKeyboardApp.swift` | 1 |
|
|
||||||
| iOS Flow 单一拥有者 | `OSGKeyboard/Services/FlowSessionManager.swift` | 1 (~3500 LOC) |
|
|
||||||
| iOS 键盘入口 | `OSGKeyboardExt/KeyboardViewController.swift` | 33-38 |
|
|
||||||
| iOS 键盘生命周期 | `OSGKeyboardExt/KeyboardViewController.swift` | 148-655 |
|
|
||||||
| iOS 共享 ASR 协议 | `OSGKeyboardShared/Services/PolishingService.swift` | 34-105 |
|
|
||||||
| iOS HostSupport ASR 工厂 | `OSGKeyboardHostSupport/Services/ASRService.swift` | 1 |
|
|
||||||
| iOS HostSupport 云 ASR 工厂 | `OSGKeyboardHostSupport/Services/CloudASR/CloudASRClientFactory` | 44 |
|
|
||||||
| Mac 入口 | `OSGKeyboardMac/OSGKeyboardMacApp.swift` | 1 |
|
|
||||||
| Mac MLX 流式 provider | `OSGKeyboardMac/MacMLXStreamingASRProvider.swift` | 1 |
|
|
||||||
| Mac MLX 流式 session | `OSGKeyboardMac/MacMLXStreamingSession.swift` | 1 |
|
|
||||||
| Mac MLX 实时捕获编排 | `OSGKeyboardMac/MacMLXLiveCapture.swift` | 1 |
|
|
||||||
| Mac 热键 | `OSGKeyboardMac/MacHotkeyService.swift` | 1 |
|
|
||||||
| Mac 文本注入 | `OSGKeyboardMac/MacTextInsertionService.swift` | 1 |
|
|
||||||
| Mac 音频 | `OSGKeyboardMac/MacAudioRecorder.swift` | 1 |
|
|
||||||
| Mac 字幕过滤器 | `OSGKeyboardMac/MacHallucinationFilter.swift` | 1 |
|
|
||||||
| Mac 字典流水线 | `OSGKeyboardMac/MacDictationPipeline.swift` | 254 |
|
|
||||||
| Mac 模型目录 | `OSGKeyboard/Resources/LocalASR/local-asr-catalog.json` | 1-111 |
|
|
||||||
| Mac target 定义 | `project.yml` | 543-674 |
|
|
||||||
| Mac target 共享 source 排除 | `project.yml` | 558-602 |
|
|
||||||
| Mac entitlements | `OSGKeyboardMac/OSGKeyboardMac.entitlements` | 1 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. 附录:版本与里程碑
|
|
||||||
|
|
||||||
| 版本 | 日期 | 关键 Mac 相关变更 |
|
|
||||||
|---|---|---|
|
|
||||||
| 2.0.1 (build 90) | 2026-08-21 | (无 Mac-specific changelog 条目) |
|
|
||||||
| 2.0.0 (build 85) | 2026-08-19 | "Optional OSG account"(iOS only,但相关 HostSupport 代码编进 Mac)|
|
|
||||||
| 1.8.0 (build 72) | 2026-08-14 | "Mac Styles / Settings follow the same pairing"(设计系统共用)+ "Mac model downloads"(**MLX 模型下装载入**)|
|
|
||||||
| Pre-1.8.0 | — | Sherpa → MLX 迁移(`MacOnboardingView.swift:7-8` 注释)|
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**报告完。**
|
|
||||||
下次更新窗口:2026-09-XX 复审 P0 ① ② 实施情况 + 决策项 6.4 落地。
|
|
||||||
@@ -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.
|
|
||||||
|
Before Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 84 KiB |
@@ -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
|
|
||||||