feat(keyboard): improve typing, voice flow, and polish reliability
Reduce extension memory pressure and delivery races while adding richer candidates, tactile feedback, and safer two-level creative polishing.
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
|
||||
import SwiftUI
|
||||
import OSGKeyboardShared
|
||||
import OSGKeyboardHostSupport
|
||||
|
||||
@main
|
||||
struct OSGKeyboardApp: App {
|
||||
@@ -18,9 +19,11 @@ struct OSGKeyboardApp: App {
|
||||
|
||||
init() {
|
||||
MaterialIconsFont.registerIfNeeded()
|
||||
if AppGroup.isAvailable {
|
||||
CustomLanguageModelManager.shared.prepareInBackgroundIfNeeded()
|
||||
}
|
||||
// Deliberately do NOT prepare Custom LM here. App launch already sits
|
||||
// near ~150 MB RSS in Debug; compiling CLM during onboarding races the
|
||||
// keyboard extension and gets the host jetsammed (signal 9). Warmup
|
||||
// runs from MainAppRoot only after onboarding completes.
|
||||
OSGDiag.log("OSGKeyboardApp.init \(OSGDiag.memoryTag())", category: "flow")
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"bin_bytes" : 198494,
|
||||
"bin_file" : "OSGKeyboardCLM.bin",
|
||||
"export_seconds" : 0.035165071487426758,
|
||||
"generated_at" : "2026-07-06T13:16:27Z",
|
||||
"identifier" : "com.osgkeyboard.custom-lm.v1",
|
||||
"locale" : "zh_CN",
|
||||
"phrase_count" : 13329,
|
||||
"sources" : {
|
||||
"ai_tech_seed" : 3040,
|
||||
"computer_terms" : 10300
|
||||
},
|
||||
"version" : "1.0.0"
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"bin_bytes" : 198494,
|
||||
"bin_file" : "OSGKeyboardCLM.bin",
|
||||
"export_seconds" : 0.035165071487426758,
|
||||
"generated_at" : "2026-07-06T13:16:27Z",
|
||||
"identifier" : "com.osgkeyboard.custom-lm.v1",
|
||||
"locale" : "zh_CN",
|
||||
"phrase_count" : 13329,
|
||||
"sources" : {
|
||||
"ai_tech_seed" : 3040,
|
||||
"computer_terms" : 10300
|
||||
},
|
||||
"version" : "1.0.0"
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"defaultModelId": "qwen3-mlx-0.6b-4bit",
|
||||
"runtimes": [],
|
||||
"models": [
|
||||
{
|
||||
"id": "qwen3-mlx-0.6b-4bit",
|
||||
"displayName": "Qwen3-ASR 0.6B",
|
||||
"backend": "mlx",
|
||||
"runtimePlatform": "macos",
|
||||
"sizeBytes": 730000000,
|
||||
"recommendedLocales": ["zh-CN", "en-US"],
|
||||
"supportsHotwords": true,
|
||||
"hotwordMode": "promptOnly",
|
||||
"badgeKey": "mac.localASR.badge.balanced",
|
||||
"installKind": "repository",
|
||||
"installRelativePath": "models/qwen3-mlx-0.6b-4bit",
|
||||
"archiveBaseName": "Qwen3-ASR-0.6B-4bit",
|
||||
"layout": {
|
||||
"mlxConfig": "config.json",
|
||||
"mlxWeights": "model.safetensors"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"type": "hfmirror",
|
||||
"priority": 1,
|
||||
"url": "",
|
||||
"baseURL": "https://hf-mirror.com/mlx-community/Qwen3-ASR-0.6B-4bit/resolve/main/{path}",
|
||||
"files": [
|
||||
{ "remotePath": "config.json", "localPath": "config.json", "sizeBytes": 5000 },
|
||||
{ "remotePath": "generation_config.json", "localPath": "generation_config.json", "sizeBytes": 200 },
|
||||
{ "remotePath": "preprocessor_config.json", "localPath": "preprocessor_config.json", "sizeBytes": 500 },
|
||||
{ "remotePath": "model.safetensors", "localPath": "model.safetensors", "sizeBytes": 708236945 },
|
||||
{ "remotePath": "model.safetensors.index.json", "localPath": "model.safetensors.index.json", "sizeBytes": 80000 },
|
||||
{ "remotePath": "tokenizer_config.json", "localPath": "tokenizer_config.json", "sizeBytes": 10000 },
|
||||
{ "remotePath": "merges.txt", "localPath": "merges.txt", "sizeBytes": 1700000 },
|
||||
{ "remotePath": "vocab.json", "localPath": "vocab.json", "sizeBytes": 2800000 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "huggingface",
|
||||
"priority": 1,
|
||||
"url": "",
|
||||
"baseURL": "https://huggingface.co/mlx-community/Qwen3-ASR-0.6B-4bit/resolve/main/{path}",
|
||||
"files": [
|
||||
{ "remotePath": "config.json", "localPath": "config.json", "sizeBytes": 5000 },
|
||||
{ "remotePath": "generation_config.json", "localPath": "generation_config.json", "sizeBytes": 200 },
|
||||
{ "remotePath": "preprocessor_config.json", "localPath": "preprocessor_config.json", "sizeBytes": 500 },
|
||||
{ "remotePath": "model.safetensors", "localPath": "model.safetensors", "sizeBytes": 708236945 },
|
||||
{ "remotePath": "model.safetensors.index.json", "localPath": "model.safetensors.index.json", "sizeBytes": 80000 },
|
||||
{ "remotePath": "tokenizer_config.json", "localPath": "tokenizer_config.json", "sizeBytes": 10000 },
|
||||
{ "remotePath": "merges.txt", "localPath": "merges.txt", "sizeBytes": 1700000 },
|
||||
{ "remotePath": "vocab.json", "localPath": "vocab.json", "sizeBytes": 2800000 }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "qwen3-mlx-1.7b-4bit",
|
||||
"displayName": "Qwen3-ASR 1.7B",
|
||||
"backend": "mlx",
|
||||
"runtimePlatform": "macos",
|
||||
"sizeBytes": 1700000000,
|
||||
"recommendedLocales": ["zh-CN", "en-US"],
|
||||
"supportsHotwords": true,
|
||||
"hotwordMode": "promptOnly",
|
||||
"badgeKey": "mac.localASR.badge.quality",
|
||||
"installKind": "repository",
|
||||
"installRelativePath": "models/qwen3-mlx-1.7b-4bit",
|
||||
"archiveBaseName": "Qwen3-ASR-1.7B-4bit",
|
||||
"layout": {
|
||||
"mlxConfig": "config.json",
|
||||
"mlxWeights": "model.safetensors"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"type": "hfmirror",
|
||||
"priority": 1,
|
||||
"url": "",
|
||||
"baseURL": "https://hf-mirror.com/mlx-community/Qwen3-ASR-1.7B-4bit/resolve/main/{path}",
|
||||
"files": [
|
||||
{ "remotePath": "config.json", "localPath": "config.json", "sizeBytes": 5000 },
|
||||
{ "remotePath": "generation_config.json", "localPath": "generation_config.json", "sizeBytes": 200 },
|
||||
{ "remotePath": "preprocessor_config.json", "localPath": "preprocessor_config.json", "sizeBytes": 500 },
|
||||
{ "remotePath": "model.safetensors", "localPath": "model.safetensors", "sizeBytes": 1650000000 },
|
||||
{ "remotePath": "model.safetensors.index.json", "localPath": "model.safetensors.index.json", "sizeBytes": 90000 },
|
||||
{ "remotePath": "tokenizer_config.json", "localPath": "tokenizer_config.json", "sizeBytes": 10000 },
|
||||
{ "remotePath": "merges.txt", "localPath": "merges.txt", "sizeBytes": 1700000 },
|
||||
{ "remotePath": "vocab.json", "localPath": "vocab.json", "sizeBytes": 2800000 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "huggingface",
|
||||
"priority": 1,
|
||||
"url": "",
|
||||
"baseURL": "https://huggingface.co/mlx-community/Qwen3-ASR-1.7B-4bit/resolve/main/{path}",
|
||||
"files": [
|
||||
{ "remotePath": "config.json", "localPath": "config.json", "sizeBytes": 5000 },
|
||||
{ "remotePath": "generation_config.json", "localPath": "generation_config.json", "sizeBytes": 200 },
|
||||
{ "remotePath": "preprocessor_config.json", "localPath": "preprocessor_config.json", "sizeBytes": 500 },
|
||||
{ "remotePath": "model.safetensors", "localPath": "model.safetensors", "sizeBytes": 1650000000 },
|
||||
{ "remotePath": "model.safetensors.index.json", "localPath": "model.safetensors.index.json", "sizeBytes": 90000 },
|
||||
{ "remotePath": "tokenizer_config.json", "localPath": "tokenizer_config.json", "sizeBytes": 10000 },
|
||||
{ "remotePath": "merges.txt", "localPath": "merges.txt", "sizeBytes": 1700000 },
|
||||
{ "remotePath": "vocab.json", "localPath": "vocab.json", "sizeBytes": 2800000 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 Sun Junyi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 mozillazg
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 mozillazg
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,32 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2026, librime-xcframework contributors
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in
|
||||
the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -0,0 +1,35 @@
|
||||
Typing keyboard third-party notices
|
||||
===================================
|
||||
|
||||
Runtime
|
||||
-------
|
||||
librime-xcframework 1.17.0-pack.1 and librime 1.17.0 (BSD-3-Clause).
|
||||
See LICENSE.txt, THIRD_PARTY_NOTICES.md and third-party-notices.zip bundled
|
||||
with this file for full binary dependency license texts.
|
||||
|
||||
Dictionary data
|
||||
---------------
|
||||
- rime-pinyin-simp @ 0c6861ef7420ee780270ca6d993d18d4101049d0
|
||||
Apache License 2.0
|
||||
- fxsjy/jieba @ 67fa2e36e72f69d9134b8a1037b83fbb070b9775
|
||||
MIT License
|
||||
- mozillazg/phrase-pinyin-data @ cee0ed6e6e4898580cafd2bd5e3723e20b214aa0
|
||||
MIT License
|
||||
- mozillazg/pinyin-data @ 923b108dc5d45dee061324c011b478fb649f8b73
|
||||
MIT License
|
||||
|
||||
The generated manifest.json records exact source URLs and SHA-256 values.
|
||||
OSGKeyboard's schema and transformation code is project-owned.
|
||||
|
||||
Explicitly not distributed
|
||||
--------------------------
|
||||
rime-ice, rime-double-pinyin, rime-luna-pinyin, rime-essay and KeyboardKit Pro.
|
||||
|
||||
The Apache-2.0 and MIT license texts are available from the source links above;
|
||||
all required copyright and permission notices must remain with distributions.
|
||||
|
||||
English typing lexicon
|
||||
----------------------
|
||||
english_lexicon.tsv and english_bigrams.tsv are OSG-curated word lists with
|
||||
synthetic relative frequency ranks for offline autocomplete / autocorrect /
|
||||
next-word ranking. They are not derived from GPL/LGPL dictionaries.
|
||||
@@ -0,0 +1,57 @@
|
||||
# Third-Party Notices
|
||||
|
||||
This repository packages upstream `librime` and its build dependencies into
|
||||
macOS and iOS XCFramework release artifacts.
|
||||
|
||||
The wrapper scripts, manifests, and documentation in this repository are
|
||||
licensed under the BSD 3-Clause License. See `LICENSE`.
|
||||
|
||||
Binary release artifacts include upstream `librime` and may include statically
|
||||
linked third-party dependency code resolved by vcpkg. Keep these notices with
|
||||
any redistributed binary artifacts.
|
||||
|
||||
Release assets include:
|
||||
|
||||
- `LICENSE.txt`: the license for this packaging wrapper.
|
||||
- `THIRD_PARTY_NOTICES.md`: this overview and the upstream `librime` notice.
|
||||
- `third-party-notices.zip`: vcpkg-provided license texts for bundled
|
||||
third-party dependencies.
|
||||
|
||||
## Upstream librime
|
||||
|
||||
Upstream project: <https://github.com/rime/librime>
|
||||
|
||||
License: BSD 3-Clause License
|
||||
|
||||
```text
|
||||
Copyright (c) 2014, RIME Developers
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in
|
||||
the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
```
|
||||
Binary file not shown.
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"sources": {
|
||||
"pinyin_simp": {
|
||||
"license": "Apache-2.0",
|
||||
"commit": "0c6861ef7420ee780270ca6d993d18d4101049d0",
|
||||
"url": "https://raw.githubusercontent.com/rime/rime-pinyin-simp/0c6861ef7420ee780270ca6d993d18d4101049d0/pinyin_simp.dict.yaml",
|
||||
"sha256": "e341598343a0f0f2035bb1aafc34a7f3bb7887deeecb3f60796262aaa2983e6b"
|
||||
},
|
||||
"jieba": {
|
||||
"license": "MIT",
|
||||
"commit": "67fa2e36e72f69d9134b8a1037b83fbb070b9775",
|
||||
"url": "https://raw.githubusercontent.com/fxsjy/jieba/67fa2e36e72f69d9134b8a1037b83fbb070b9775/jieba/dict.txt",
|
||||
"sha256": "7197c3211ddd98962b036cdf40324d1ea2bfaa12bd028e68faa70111a88e12a8"
|
||||
},
|
||||
"phrase_pinyin": {
|
||||
"license": "MIT",
|
||||
"commit": "cee0ed6e6e4898580cafd2bd5e3723e20b214aa0",
|
||||
"url": "https://raw.githubusercontent.com/mozillazg/phrase-pinyin-data/cee0ed6e6e4898580cafd2bd5e3723e20b214aa0/pinyin.txt",
|
||||
"sha256": "dcc769607c220b312fea3e71cb63421298b4b891b1f7356a95ab58f2c96fff81"
|
||||
},
|
||||
"character_pinyin": {
|
||||
"license": "MIT",
|
||||
"commit": "923b108dc5d45dee061324c011b478fb649f8b73",
|
||||
"url": "https://raw.githubusercontent.com/mozillazg/pinyin-data/923b108dc5d45dee061324c011b478fb649f8b73/pinyin.txt",
|
||||
"sha256": "621f8ca9eff8519f47e2b17b564fd318161e13bca07eea8c8e04993cd5d3b52e"
|
||||
}
|
||||
},
|
||||
"statistics": {
|
||||
"baselineEntries": 65125,
|
||||
"jiebaWordsAccepted": 337338,
|
||||
"jiebaWordsUsingCharacterFallback": 293794,
|
||||
"outputEntries": 364926
|
||||
},
|
||||
"output": {
|
||||
"file": "osg_pinyin.dict.yaml",
|
||||
"sha256": "35b0664df8906712e4051392d83be35bf76587e4c9a7b068a71e0d6f7b989425"
|
||||
},
|
||||
"excluded": [
|
||||
"rime-ice (GPL-3.0)",
|
||||
"rime-double-pinyin (GPL-3.0)",
|
||||
"rime-essay (LGPL-3.0)",
|
||||
"rime-luna-pinyin (LGPL-3.0)"
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,16 @@
|
||||
// FlowDiagnostics.swift
|
||||
// OSGKeyboard · Main App
|
||||
//
|
||||
// Structured logging for the Flow dictation pipeline. Delegates to
|
||||
// `OSGLog.flow` for Console.app visibility.
|
||||
// Structured logging for the Flow dictation pipeline. Dual-writes to NSLog
|
||||
// (`[OSGDiag/flow]`) and `OSGLog.flow` so Console shows lines even when the
|
||||
// keyboard extension process is selected.
|
||||
|
||||
import Foundation
|
||||
import OSGKeyboardShared
|
||||
|
||||
enum FlowDiagnostics {
|
||||
static func log(_ message: String) {
|
||||
OSGLog.flow.info("\(message, privacy: .public)")
|
||||
OSGDiag.log(message, category: "flow")
|
||||
}
|
||||
|
||||
static func logDrain(_ report: FlowCaptureDrainReport) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import AVFoundation
|
||||
import AVKit
|
||||
import CoreMedia
|
||||
import UIKit
|
||||
import OSGKeyboardHostSupport
|
||||
|
||||
/// Why `startAndWait` could not prove an active PiP window.
|
||||
enum FlowPiPStartFailure: Equatable, Sendable {
|
||||
@@ -95,7 +96,7 @@ final class FlowPictureInPictureController: NSObject {
|
||||
// MARK: - Lifecycle
|
||||
|
||||
@discardableResult
|
||||
func start() -> Bool {
|
||||
func start() async -> Bool {
|
||||
lastSystemStartFailure = nil
|
||||
guard AVPictureInPictureController.isPictureInPictureSupported() else {
|
||||
return false
|
||||
@@ -104,7 +105,7 @@ final class FlowPictureInPictureController: NSObject {
|
||||
|
||||
// Required before constructing the controller; without an active
|
||||
// session, `isPictureInPicturePossible` stays false forever.
|
||||
guard activateAudioSessionForPiP() else {
|
||||
guard await activateAudioSessionForPiP() else {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -150,7 +151,7 @@ final class FlowPictureInPictureController: NSObject {
|
||||
}
|
||||
|
||||
lastSystemStartFailure = nil
|
||||
guard start() else {
|
||||
guard await start() else {
|
||||
stopFramePump()
|
||||
if lastSystemStartFailure != nil {
|
||||
return .failed(.systemRejected)
|
||||
@@ -211,9 +212,9 @@ final class FlowPictureInPictureController: NSObject {
|
||||
|
||||
/// Nudge the sample-buffer source right before resigning active so
|
||||
/// `canStartPictureInPictureAutomaticallyFromInline` can take over.
|
||||
func prepareForBackgroundAutoStart() {
|
||||
func prepareForBackgroundAutoStart() async {
|
||||
guard isPictureInPictureActive || pipController != nil else { return }
|
||||
_ = activateAudioSessionForPiP()
|
||||
_ = await activateAudioSessionForPiP()
|
||||
startFramePump()
|
||||
enqueueGuideFrame()
|
||||
pipController?.invalidatePlaybackState()
|
||||
@@ -222,12 +223,12 @@ final class FlowPictureInPictureController: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
/// Re-activate the playback session after utterance capture releases the
|
||||
/// mic (`setActive(false)`). Without this, PiP can lose eligibility between
|
||||
/// utterances even though the floating window is still visible.
|
||||
/// Keep the shared audio session eligible for PiP after capture stops its
|
||||
/// engine. After the first utterance the coordinator intentionally retains
|
||||
/// the stable playAndRecord category instead of flipping back to playback.
|
||||
@discardableResult
|
||||
func reassertKeepAliveAudioSession() -> Bool {
|
||||
activateAudioSessionForPiP()
|
||||
func reassertKeepAliveAudioSession() async -> Bool {
|
||||
await activateAudioSessionForPiP()
|
||||
}
|
||||
|
||||
/// Kept for FlowSessionManager call sites; guide animation ignores live levels.
|
||||
@@ -241,19 +242,15 @@ final class FlowPictureInPictureController: NSObject {
|
||||
private var didActivateAudioSessionBeforeController = false
|
||||
|
||||
@discardableResult
|
||||
private func activateAudioSessionForPiP() -> Bool {
|
||||
do {
|
||||
let session = AVAudioSession.sharedInstance()
|
||||
// Playback (not record) keeps PiP eligible between utterances without
|
||||
// holding the mic. Utterance capture later switches to playAndRecord.
|
||||
try session.setCategory(.playback, mode: .moviePlayback, options: [.mixWithOthers])
|
||||
try session.setActive(true)
|
||||
FlowDiagnostics.log("PiP audio session active category=playback")
|
||||
private func activateAudioSessionForPiP() async -> Bool {
|
||||
// The first PiP start uses playback. After capture establishes a
|
||||
// playAndRecord route, the coordinator keeps that category active and
|
||||
// only stops the engine/tap between utterances.
|
||||
if await FlowAudioSessionCoordinator.shared.activatePlayback() {
|
||||
FlowDiagnostics.log("PiP audio session ready")
|
||||
return true
|
||||
} catch {
|
||||
FlowDiagnostics.log("PiP audio session failed: \(error.localizedDescription)")
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private func waitForHostInWindow(timeout: TimeInterval) async -> Bool {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@
|
||||
|
||||
import SwiftUI
|
||||
import OSGKeyboardShared
|
||||
import OSGKeyboardHostSupport
|
||||
|
||||
struct ASRSettingsCard: View {
|
||||
@Environment(\.themePalette) private var palette: ThemePalette
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import SwiftUI
|
||||
import OSGKeyboardShared
|
||||
import OSGKeyboardHostSupport
|
||||
|
||||
struct HomeUsageStatsSection: View {
|
||||
let layout: UsageStatsCluster.Layout
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
import SwiftUI
|
||||
import OSGKeyboardShared
|
||||
import OSGKeyboardHostSupport
|
||||
|
||||
// MARK: - Layout metrics
|
||||
|
||||
|
||||
@@ -6,8 +6,14 @@
|
||||
// ready. Failure states reuse the same minimal layout and only change the
|
||||
// text — permission issues are handled with a single "open Settings" link,
|
||||
// never a second in-app permission flow.
|
||||
//
|
||||
// The overlay ignores the keyboard safe area (full-bleed over Home), so it
|
||||
// manually tracks keyboard overlap and lifts the gradient + copy together
|
||||
// to stay glued to the keyboard's top edge. MainTabView also ignores the
|
||||
// keyboard inset, so system safe-area push cannot be relied on here.
|
||||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
import OSGKeyboardShared
|
||||
|
||||
struct FlowColdStartContext: Equatable {
|
||||
@@ -32,6 +38,7 @@ enum FlowColdStartFailure: Equatable {
|
||||
|
||||
struct FlowColdStartOverlay: View {
|
||||
@Environment(\.themePalette) private var palette: ThemePalette
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
|
||||
let context: FlowColdStartContext
|
||||
let onReturnToHost: () -> Void
|
||||
@@ -39,9 +46,12 @@ struct FlowColdStartOverlay: View {
|
||||
let onRetry: () -> Void
|
||||
let onOpenSettings: () -> Void
|
||||
|
||||
/// Fraction of the screen height the bottom gradient occupies.
|
||||
/// Fraction of the *visible* height (above the keyboard) the gradient occupies.
|
||||
private let gradientHeightFraction: CGFloat = 0.50
|
||||
|
||||
/// Distance from the screen bottom to the keyboard's top edge.
|
||||
@State private var keyboardOverlap: CGFloat = 0
|
||||
|
||||
/// Ready and failure states dismiss on blank tap; preparing stays
|
||||
/// informational only (no accidental dismiss while proving audio).
|
||||
private var allowsBlankTapDismiss: Bool {
|
||||
@@ -55,7 +65,30 @@ struct FlowColdStartOverlay: View {
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geo in
|
||||
// Keep gradient proportions relative to the canvas above the keyboard
|
||||
// so the near-opaque band stays behind the title when the keyboard is up.
|
||||
let visibleHeight = max(geo.size.height - keyboardOverlap, 1)
|
||||
let gradientHeight = visibleHeight * gradientHeightFraction
|
||||
// Above the keyboard the home-indicator inset is already consumed by
|
||||
// `keyboardOverlap`; only apply it when the keyboard is hidden.
|
||||
let contentBottomPad = keyboardOverlap > 0
|
||||
? Spacing.sm
|
||||
: max(geo.safeAreaInsets.bottom, Spacing.sm)
|
||||
|
||||
ZStack(alignment: .bottom) {
|
||||
// Full-screen hit sink: must expand explicitly — a bare Color.clear
|
||||
// in a bottom-aligned ZStack can collapse and let taps reach Home
|
||||
// (e.g. focusing the preview field while this overlay is visible).
|
||||
Color.clear
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
if allowsBlankTapDismiss {
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
.allowsHitTesting(true)
|
||||
|
||||
// Full-width bottom gradient: transparent at the top of the
|
||||
// band, nearly opaque at the bottom so hint text stays readable.
|
||||
LinearGradient(
|
||||
@@ -67,35 +100,161 @@ struct FlowColdStartOverlay: View {
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
.frame(height: geo.size.height * gradientHeightFraction)
|
||||
.frame(height: gradientHeight)
|
||||
.frame(maxWidth: .infinity, alignment: .bottom)
|
||||
.allowsHitTesting(false)
|
||||
|
||||
if allowsBlankTapDismiss {
|
||||
// Captures taps on empty overlay space (dismiss) and blocks
|
||||
// pass-through to the host shell underneath. Action buttons in
|
||||
// `content` sit above this layer and remain tappable.
|
||||
Color.clear
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture(perform: onDismiss)
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
|
||||
VStack(spacing: Spacing.lg) {
|
||||
content
|
||||
.padding(.horizontal, Spacing.xl)
|
||||
|
||||
homeIndicator
|
||||
.padding(.bottom, max(geo.safeAreaInsets.bottom, Spacing.sm))
|
||||
.padding(.bottom, contentBottomPad)
|
||||
}
|
||||
}
|
||||
// Lift gradient + copy as one unit so the opaque band stays glued
|
||||
// to the keyboard top edge (or the home indicator when idle).
|
||||
.padding(.bottom, keyboardOverlap)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
.onAppear {
|
||||
// Cold-start copy asks the user to swipe back — dismiss any in-app
|
||||
// keyboard first so Home's preview field cannot steal the scene.
|
||||
Self.resignEditingFocus()
|
||||
syncKeyboardOverlap(animated: false)
|
||||
}
|
||||
.onChange(of: scenePhase) { _, phase in
|
||||
guard phase == .active else { return }
|
||||
syncKeyboardOverlap(animated: false)
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification)) { notification in
|
||||
applyKeyboardOverlap(from: notification)
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardDidChangeFrameNotification)) { notification in
|
||||
// Catches frames missed between mount and the first WillChange
|
||||
// (keyboard already visible when the overlay appears).
|
||||
applyKeyboardOverlap(from: notification)
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { notification in
|
||||
applyKeyboardOverlap(0, from: notification)
|
||||
}
|
||||
.animation(.easeInOut(duration: 0.2), value: context.state)
|
||||
.accessibilityElement(children: .contain)
|
||||
}
|
||||
|
||||
/// Reads the keyboard end frame and animates `keyboardOverlap` with the
|
||||
/// system keyboard curve so the gradient rides the same motion.
|
||||
private func applyKeyboardOverlap(from notification: Notification) {
|
||||
let overlap = Self.keyboardOverlap(from: notification)
|
||||
applyKeyboardOverlap(overlap, from: notification)
|
||||
}
|
||||
|
||||
private func applyKeyboardOverlap(_ overlap: CGFloat, from notification: Notification) {
|
||||
let duration = (notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?
|
||||
.doubleValue ?? 0.25
|
||||
withAnimation(.easeOut(duration: duration)) {
|
||||
keyboardOverlap = overlap
|
||||
}
|
||||
}
|
||||
|
||||
private func syncKeyboardOverlap(animated: Bool) {
|
||||
let overlap = Self.probedKeyboardOverlap()
|
||||
if animated {
|
||||
withAnimation(.easeOut(duration: 0.2)) {
|
||||
keyboardOverlap = overlap
|
||||
}
|
||||
} else {
|
||||
keyboardOverlap = overlap
|
||||
}
|
||||
}
|
||||
|
||||
/// Screen-bottom → keyboard-top distance in the key window.
|
||||
private static func keyboardOverlap(from notification: Notification) -> CGFloat {
|
||||
guard let frame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else {
|
||||
return 0
|
||||
}
|
||||
guard let window = keyWindow else {
|
||||
let bounds = UIScreen.main.bounds
|
||||
return max(0, bounds.maxY - frame.minY)
|
||||
}
|
||||
let frameInWindow = window.convert(frame, from: nil)
|
||||
return max(0, window.bounds.maxY - frameInWindow.minY)
|
||||
}
|
||||
|
||||
/// Best-effort read when we may have missed keyboard notifications
|
||||
/// (overlay mounted while the keyboard was already up).
|
||||
private static func probedKeyboardOverlap() -> CGFloat {
|
||||
guard let scene = UIApplication.shared.connectedScenes
|
||||
.compactMap({ $0 as? UIWindowScene })
|
||||
.first(where: { $0.activationState == .foregroundActive })
|
||||
?? UIApplication.shared.connectedScenes.compactMap({ $0 as? UIWindowScene }).first
|
||||
else {
|
||||
return 0
|
||||
}
|
||||
|
||||
let reference = keyWindow ?? scene.windows.first
|
||||
let bounds = reference?.bounds ?? scene.screen.bounds
|
||||
|
||||
// UITextEffectsWindow / UIRemoteKeyboardWindow host the keyboard chrome.
|
||||
for window in scene.windows {
|
||||
let name = String(describing: type(of: window))
|
||||
guard name.contains("Keyboard") || name.contains("TextEffects") else { continue }
|
||||
let overlap = max(0, bounds.maxY - window.frame.minY)
|
||||
// Full-screen effects windows are not themselves the keyboard —
|
||||
// walk for a bottom-docked subview that looks like the input host.
|
||||
if overlap >= bounds.height - 1 {
|
||||
if let docked = deepestBottomDockedSubview(in: window, referenceBounds: bounds) {
|
||||
return max(0, bounds.maxY - docked.minY)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if overlap > 0 {
|
||||
return overlap
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
private static func deepestBottomDockedSubview(
|
||||
in window: UIWindow,
|
||||
referenceBounds: CGRect
|
||||
) -> CGRect? {
|
||||
var best: CGRect?
|
||||
func visit(_ view: UIView) {
|
||||
let frame = view.convert(view.bounds, to: nil)
|
||||
let touchesBottom = abs(frame.maxY - referenceBounds.maxY) < 1.5
|
||||
let tallEnough = frame.height > 120
|
||||
let notFullScreen = frame.height < referenceBounds.height * 0.92
|
||||
if touchesBottom, tallEnough, notFullScreen {
|
||||
if best == nil || frame.minY < best!.minY {
|
||||
best = frame
|
||||
}
|
||||
}
|
||||
for child in view.subviews {
|
||||
visit(child)
|
||||
}
|
||||
}
|
||||
visit(window)
|
||||
return best
|
||||
}
|
||||
|
||||
private static var keyWindow: UIWindow? {
|
||||
UIApplication.shared.connectedScenes
|
||||
.compactMap { $0 as? UIWindowScene }
|
||||
.flatMap(\.windows)
|
||||
.first(where: \.isKeyWindow)
|
||||
}
|
||||
|
||||
private static func resignEditingFocus() {
|
||||
UIApplication.shared.sendAction(
|
||||
#selector(UIResponder.resignFirstResponder),
|
||||
to: nil,
|
||||
from: nil,
|
||||
for: nil
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var content: some View {
|
||||
VStack(spacing: Spacing.md) {
|
||||
|
||||
@@ -76,8 +76,18 @@ struct HomeView: View {
|
||||
}
|
||||
.onChange(of: previewFocused) { _, focused in
|
||||
guard focused else { return }
|
||||
// Cold-start overlay owns the scene (swipe-back hint); don't let
|
||||
// the preview field summon the keyboard underneath it.
|
||||
if flowManager.coldStartContext != nil {
|
||||
previewFocused = false
|
||||
return
|
||||
}
|
||||
Task { await flowManager.refreshForInlineKeyboardFocus() }
|
||||
}
|
||||
.onChange(of: flowManager.coldStartContext != nil) { _, showingOverlay in
|
||||
guard showingOverlay else { return }
|
||||
previewFocused = false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Phone layout
|
||||
@@ -333,10 +343,13 @@ struct HomeView: View {
|
||||
.buttonStyle(.plain)
|
||||
.padding(.leading, Spacing.xs)
|
||||
} else if canManuallyStartSession {
|
||||
// Sessions auto-start on foreground, but after a manual stop /
|
||||
// expiry the user needs a reliable, no-jump way back in.
|
||||
// Sessions no longer auto-start on foreground (keyboard survival).
|
||||
// After a manual stop / expiry, Start is the explicit re-entry.
|
||||
Button {
|
||||
flowManager.activateOnForeground()
|
||||
flowManager.activateOnForeground(
|
||||
reason: "HomeView.startButton",
|
||||
startCapture: true
|
||||
)
|
||||
} label: {
|
||||
Text("home.flow.startShort")
|
||||
.font(TypeStyle.caption2)
|
||||
@@ -481,7 +494,10 @@ struct HomeView: View {
|
||||
.stroke(previewFocused ? palette.dividerStrong : palette.dividerStrong.opacity(0.75), lineWidth: 1)
|
||||
)
|
||||
.contentShape(RoundedRectangle(cornerRadius: Radius.large, style: .continuous))
|
||||
.onTapGesture { previewFocused = true }
|
||||
.onTapGesture {
|
||||
guard flowManager.coldStartContext == nil else { return }
|
||||
previewFocused = true
|
||||
}
|
||||
}
|
||||
|
||||
private var previewFieldContent: some View {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
import SwiftUI
|
||||
import OSGKeyboardShared
|
||||
import OSGKeyboardHostSupport
|
||||
|
||||
struct KeyboardPreviewSheet: View {
|
||||
@Environment(\.themePalette) private var palette: ThemePalette
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import SwiftUI
|
||||
import OSGKeyboardShared
|
||||
import OSGKeyboardHostSupport
|
||||
|
||||
struct MainAppRoot: View {
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
@@ -15,6 +16,7 @@ struct MainAppRoot: View {
|
||||
// `@ObservedObject` keeps subscriptions correct across Settings replay.
|
||||
@ObservedObject private var config = ProviderConfig.shared
|
||||
@StateObject private var flowManager = FlowSessionManager()
|
||||
@State private var postOnboardingWarmupTask: Task<Void, Never>?
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
@@ -63,15 +65,24 @@ struct MainAppRoot: View {
|
||||
AppOpenURLRouter.shared.register { url in
|
||||
handleIncomingURL(url)
|
||||
}
|
||||
flowManager.activateOnForeground()
|
||||
OSGDiag.log(
|
||||
"MainAppRoot.onAppear scene=\(String(describing: scenePhase)) "
|
||||
+ "onboarding=\(config.hasCompletedOnboarding) \(OSGDiag.memoryTag())",
|
||||
category: "flow"
|
||||
)
|
||||
AppCloudSync.shared.startObservingExternalChanges()
|
||||
Task {
|
||||
await AppCloudSync.shared.pullAllIfEnabled()
|
||||
}
|
||||
Task {
|
||||
let typingConfig = TypingInputConfiguration.shared.snapshot
|
||||
try? await RimeResourceInstaller.shared.installIfNeeded(
|
||||
configuration: typingConfig
|
||||
|
||||
// Heavy work (Flow / CLM / Rime) only after onboarding. Doing it
|
||||
// earlier jetsams the host (~150 MB+) and the keyboard dies with it.
|
||||
if config.hasCompletedOnboarding {
|
||||
// Light path only: never auto-start continuous capture here.
|
||||
// Capture starts on Home Start / keyboard startflow / mic press.
|
||||
flowManager.activateOnForeground(reason: "MainAppRoot.onAppear")
|
||||
schedulePostOnboardingWarmup(reason: "MainAppRoot.onAppear")
|
||||
} else {
|
||||
OSGDiag.log(
|
||||
"MainAppRoot.onAppear skip Flow/CLM/Rime (onboarding incomplete)",
|
||||
category: "flow"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -80,14 +91,22 @@ struct MainAppRoot: View {
|
||||
}
|
||||
.onChange(of: config.hasCompletedOnboarding) { _, done in
|
||||
if done {
|
||||
flowManager.activateOnForeground()
|
||||
flowManager.activateOnForeground(reason: "onboardingCompleted")
|
||||
schedulePostOnboardingWarmup(reason: "onboardingCompleted")
|
||||
}
|
||||
}
|
||||
.onChange(of: scenePhase) { _, phase in
|
||||
flowManager.handleScenePhase(phase)
|
||||
guard phase == .active else { return }
|
||||
guard phase == .active else {
|
||||
postOnboardingWarmupTask?.cancel()
|
||||
postOnboardingWarmupTask = nil
|
||||
FlowSessionBridge.setHostHeavy(false)
|
||||
return
|
||||
}
|
||||
if config.hasCompletedOnboarding {
|
||||
flowManager.activateOnForeground()
|
||||
flowManager.activateOnForeground(reason: "scenePhase.active")
|
||||
// Retry deferred Rime/CLM after a jetsam-prone launch.
|
||||
schedulePostOnboardingWarmup(reason: "scenePhase.active.retry")
|
||||
}
|
||||
Task {
|
||||
await AppCloudSync.shared.pullAllIfEnabled()
|
||||
@@ -95,11 +114,64 @@ struct MainAppRoot: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Serial host warmup: Rime deploy → CLM. Never parallel with ASR.
|
||||
/// ASR warms on first mic press (`FlowSessionManager.beginUtterance`).
|
||||
///
|
||||
/// Intentionally delayed: running Rime/CLM on `onAppear` kept the host at
|
||||
/// ~175 MB while the user switched to the keyboard, and the extension died
|
||||
/// before `KVC.init` (no dyld breadcrumb).
|
||||
private func schedulePostOnboardingWarmup(reason: String) {
|
||||
OSGDiag.log(
|
||||
"postOnboardingWarmup scheduled reason=\(reason) delay=45s \(OSGDiag.memoryTag())",
|
||||
category: "flow"
|
||||
)
|
||||
postOnboardingWarmupTask?.cancel()
|
||||
postOnboardingWarmupTask = Task { @MainActor in
|
||||
// Let the user leave the host / cold-start the keyboard first.
|
||||
try? await Task.sleep(nanoseconds: 45_000_000_000)
|
||||
guard !Task.isCancelled else { return }
|
||||
guard scenePhase == .active else {
|
||||
OSGDiag.log(
|
||||
"postOnboardingWarmup skip reason=notActive \(OSGDiag.memoryTag())",
|
||||
category: "flow"
|
||||
)
|
||||
return
|
||||
}
|
||||
guard !flowManager.shouldDeferHostHeavyWork else {
|
||||
OSGDiag.log(
|
||||
"postOnboardingWarmup skip reason=flowBusy \(OSGDiag.memoryTag())",
|
||||
category: "flow"
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
await AppCloudSync.shared.pullAllIfEnabled()
|
||||
|
||||
// hostHeavy only while heavy work runs — never leave it stuck at 1
|
||||
// just because RSS is above the soft gate (that blocked typing).
|
||||
guard HostMemoryBudget.gate("rime.installIfNeeded") else { return }
|
||||
|
||||
FlowSessionBridge.setHostHeavy(true)
|
||||
defer { FlowSessionBridge.setHostHeavy(false) }
|
||||
|
||||
let typingConfig = TypingInputConfiguration.shared.snapshot
|
||||
OSGDiag.log("rime.installIfNeeded begin \(OSGDiag.memoryTag())", category: "flow")
|
||||
try? await RimeResourceInstaller.shared.installIfNeeded(
|
||||
configuration: typingConfig
|
||||
)
|
||||
OSGDiag.log("rime.installIfNeeded done \(OSGDiag.memoryTag())", category: "flow")
|
||||
|
||||
guard HostMemoryBudget.gate("clm.prepare", category: "asr") else { return }
|
||||
CustomLanguageModelManager.shared.prepareInBackgroundIfNeeded()
|
||||
OSGDiag.log("clm.prepare scheduled \(OSGDiag.memoryTag())", category: "asr")
|
||||
}
|
||||
}
|
||||
|
||||
private func handleIncomingURL(_ url: URL) {
|
||||
guard url.scheme == "osgkeyboard" else { return }
|
||||
switch url.host {
|
||||
case "startflow":
|
||||
flowManager.startSession(coldStart: true)
|
||||
flowManager.startSession(coldStart: true, reason: "url.startflow")
|
||||
#if DEBUG
|
||||
case "seed-demo":
|
||||
DemoDataSeeder.seedRichPlaceholderData()
|
||||
|
||||
@@ -224,7 +224,10 @@ private struct WideStatusFooter: View {
|
||||
.buttonStyle(.plain)
|
||||
} else if canManuallyStartSession && !needsCloudSetup {
|
||||
Button {
|
||||
flowManager.activateOnForeground()
|
||||
flowManager.activateOnForeground(
|
||||
reason: "MainSplitView.startButton",
|
||||
startCapture: true
|
||||
)
|
||||
} label: {
|
||||
Text("home.flow.startShort")
|
||||
.foregroundStyle(palette.accent)
|
||||
|
||||
@@ -5,5 +5,6 @@
|
||||
// lives in `OSGKeyboardShared` as `LiveDictationController`.
|
||||
|
||||
import OSGKeyboardShared
|
||||
import OSGKeyboardHostSupport
|
||||
|
||||
typealias PreviewASRController = LiveDictationController
|
||||
|
||||
@@ -129,22 +129,49 @@ struct HandednessPickerRow: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Keyboard haptic picker row
|
||||
|
||||
struct KeyboardHapticPickerRow: View {
|
||||
@Binding var selection: KeyboardHapticIntensity
|
||||
|
||||
private var options: [(id: String, label: String)] {
|
||||
KeyboardHapticIntensity.allCases.map { intensity in
|
||||
(intensity.rawValue, AppL10n.string(intensity.labelKey))
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
SettingsMenuPickerRow(
|
||||
title: AppL10n.string("settings.keyboardHaptic.title"),
|
||||
options: options,
|
||||
selection: Binding(
|
||||
get: { selection.rawValue },
|
||||
set: { newValue in
|
||||
selection = KeyboardHapticIntensity(rawValue: newValue) ?? .default
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Polish intensity picker row
|
||||
|
||||
struct PolishIntensityPickerRow: View {
|
||||
@ObservedObject var config: ProviderConfig
|
||||
|
||||
var body: some View {
|
||||
// 与「惯用手」一致的右侧下拉菜单行样式,保持偏好设置卡片内各行风格统一。
|
||||
SettingsMenuPickerRow(
|
||||
title: AppL10n.string("settings.polishIntensity.title"),
|
||||
options: PolishIntensity.allCases.map { intensity in
|
||||
(intensity.rawValue, SharedL10n.string(intensity.labelKey, language: config.uiLanguage))
|
||||
(
|
||||
intensity.rawValue,
|
||||
SharedL10n.string(intensity.labelKey, language: config.uiLanguage)
|
||||
)
|
||||
},
|
||||
selection: Binding(
|
||||
get: { config.polishIntensity.rawValue },
|
||||
set: { newValue in
|
||||
config.polishIntensity = PolishIntensity(rawValue: newValue) ?? .medium
|
||||
set: { rawValue in
|
||||
config.polishIntensity = PolishIntensity(rawValue: rawValue) ?? .default
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -174,6 +201,27 @@ struct DefaultTypingInputToggleRow: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct RememberLastSurfaceToggleRow: View {
|
||||
@Environment(\.themePalette) private var palette: ThemePalette
|
||||
@Binding var isOn: Bool
|
||||
|
||||
var body: some View {
|
||||
Toggle(isOn: $isOn) {
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text("settings.typingInput.rememberLast.title")
|
||||
.font(TypeStyle.body)
|
||||
.foregroundStyle(palette.textPrimary)
|
||||
Text("settings.typingInput.rememberLast.description")
|
||||
.font(TypeStyle.caption)
|
||||
.foregroundStyle(palette.textSecondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
.tint(palette.accent)
|
||||
.settingsListRow()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Cursor drag navigation toggle
|
||||
|
||||
struct CursorDragNavigationToggleRow: View {
|
||||
|
||||
@@ -39,6 +39,26 @@ struct SettingsNavigationRow: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Read-only version / build row for Settings home (below About).
|
||||
struct SettingsVersionRow: View {
|
||||
@Environment(\.themePalette) private var palette: ThemePalette
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: Spacing.sm) {
|
||||
Text("settings.version.title")
|
||||
.font(TypeStyle.body)
|
||||
.foregroundStyle(palette.textPrimary)
|
||||
Spacer(minLength: Spacing.xs)
|
||||
Text(AppVersionDisplay.detailedLabel)
|
||||
.font(TypeStyle.body)
|
||||
.foregroundStyle(palette.textSecondary)
|
||||
.multilineTextAlignment(.trailing)
|
||||
}
|
||||
.settingsListRow()
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Config entry summaries (shown on Settings home)
|
||||
|
||||
enum SettingsConfigSummary {
|
||||
@@ -248,6 +268,12 @@ struct GeneralSettingsView: View {
|
||||
|
||||
Divider().background(palette.divider)
|
||||
|
||||
RememberLastSurfaceToggleRow(
|
||||
isOn: $typingConfiguration.rememberLastSurface
|
||||
)
|
||||
|
||||
Divider().background(palette.divider)
|
||||
|
||||
NavigationLink {
|
||||
TypingInputSettingsView()
|
||||
} label: {
|
||||
@@ -264,6 +290,13 @@ struct GeneralSettingsView: View {
|
||||
)
|
||||
)
|
||||
Divider().background(palette.divider)
|
||||
KeyboardHapticPickerRow(
|
||||
selection: Binding(
|
||||
get: { config.keyboardHapticIntensity },
|
||||
set: { config.keyboardHapticIntensity = $0 }
|
||||
)
|
||||
)
|
||||
Divider().background(palette.divider)
|
||||
CursorDragNavigationToggleRow(
|
||||
isOn: $config.cursorDragNavigationEnabled
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import SwiftUI
|
||||
import OSGKeyboardShared
|
||||
import OSGKeyboardHostSupport
|
||||
|
||||
enum SettingsPresentation {
|
||||
case tab
|
||||
@@ -171,6 +172,11 @@ struct SettingsView: View {
|
||||
private var moreEntriesSection: some View {
|
||||
VStack(spacing: 0) {
|
||||
settingsRouteButton(.about, title: "settings.about.title")
|
||||
|
||||
Divider().background(palette.divider)
|
||||
|
||||
// Read-only version row — not tappable.
|
||||
SettingsVersionRow()
|
||||
}
|
||||
.surfaceCard()
|
||||
}
|
||||
|
||||
@@ -178,6 +178,7 @@
|
||||
"settings.systemPrompt.edit" = "Edit system prompt";
|
||||
"settings.systemPrompt.hint" = "Used by the cloud LLM when polishing your dictation. Local engine skips this step.";
|
||||
"settings.about.title" = "About";
|
||||
"settings.version.title" = "Version";
|
||||
"settings.daily.title" = "Daily";
|
||||
"settings.config.title" = "Configuration";
|
||||
"settings.general.title" = "General";
|
||||
@@ -187,6 +188,8 @@
|
||||
"settings.typingInput.title" = "Text Input";
|
||||
"settings.typingInput.default.title" = "Default to Text Input";
|
||||
"settings.typingInput.default.description" = "Open the text input keyboard instead of voice input by default";
|
||||
"settings.typingInput.rememberLast.title" = "Remember Last Choice";
|
||||
"settings.typingInput.rememberLast.description" = "Reopen on the voice or text surface you left last time";
|
||||
"settings.speechRecognition.title" = "Speech Recognition";
|
||||
"settings.textPolish.title" = "Text Polish";
|
||||
"settings.preferences.title" = "Preferences";
|
||||
@@ -195,6 +198,10 @@
|
||||
"settings.handedness.title" = "Handedness";
|
||||
"settings.handedness.left" = "Left hand";
|
||||
"settings.handedness.right" = "Right hand";
|
||||
"settings.keyboardHaptic.title" = "Haptics";
|
||||
"settings.keyboardHaptic.off" = "Off";
|
||||
"settings.keyboardHaptic.light" = "Light";
|
||||
"settings.keyboardHaptic.strong" = "Strong";
|
||||
"settings.cursorDragNavigation.title" = "Drag beside mic to move cursor";
|
||||
"settings.systemPrompt.reset" = "Reset";
|
||||
"settings.asrLocale" = "ASR locale";
|
||||
@@ -398,7 +405,7 @@
|
||||
"polishStyles.editor.name" = "Name";
|
||||
"polishStyles.editor.namePlaceholder" = "Style name";
|
||||
"polishStyles.editor.prompt" = "Complete prompt";
|
||||
"polishStyles.editor.hint" = "Use {{DICTIONARY}} where the personal dictionary should be inserted. System safety, rewrite intensity, and output rules are appended automatically.";
|
||||
"polishStyles.editor.hint" = "Describe only the role, tone, style boundaries, and examples. The system appends the personal dictionary, safety contract, and output rules.";
|
||||
"polishStyles.error.title" = "Couldn’t save style";
|
||||
"polishStyles.error.emptyName" = "Enter a style name.";
|
||||
"polishStyles.error.emptyPrompt" = "The prompt cannot be empty.";
|
||||
@@ -465,7 +472,8 @@
|
||||
"settings.appSettings.iCloudSync.synced" = "Synced";
|
||||
"settings.appSettings.iCloudSync.error.generic" = "Could not sync settings with iCloud. Try again later.";
|
||||
|
||||
/* v0.3.0: Polish intensity */
|
||||
|
||||
/* Polish intensity */
|
||||
"settings.polishIntensity.title" = "Polish intensity";
|
||||
|
||||
/* Flow session policy */
|
||||
|
||||
@@ -178,6 +178,7 @@
|
||||
"settings.systemPrompt.edit" = "编辑系统提示";
|
||||
"settings.systemPrompt.hint" = "云端润色时使用。本地识别模式不会用到此提示词。";
|
||||
"settings.about.title" = "关于";
|
||||
"settings.version.title" = "版本";
|
||||
"settings.daily.title" = "日常";
|
||||
"settings.config.title" = "配置";
|
||||
"settings.general.title" = "通用";
|
||||
@@ -187,6 +188,8 @@
|
||||
"settings.typingInput.title" = "文本输入";
|
||||
"settings.typingInput.default.title" = "默认进行文字输入";
|
||||
"settings.typingInput.default.description" = "打开键盘时,默认使用文字输入键盘而非语音输入";
|
||||
"settings.typingInput.rememberLast.title" = "记住上次选择";
|
||||
"settings.typingInput.rememberLast.description" = "下次打开键盘时,保持你上次离开时的语音或文字输入界面";
|
||||
"settings.speechRecognition.title" = "语音识别配置";
|
||||
"settings.textPolish.title" = "文本润色配置";
|
||||
"settings.preferences.title" = "偏好设置";
|
||||
@@ -195,6 +198,10 @@
|
||||
"settings.handedness.title" = "握持偏好";
|
||||
"settings.handedness.left" = "左手";
|
||||
"settings.handedness.right" = "右手";
|
||||
"settings.keyboardHaptic.title" = "震动";
|
||||
"settings.keyboardHaptic.off" = "关";
|
||||
"settings.keyboardHaptic.light" = "轻";
|
||||
"settings.keyboardHaptic.strong" = "强";
|
||||
"settings.cursorDragNavigation.title" = "麦克风旁拖动移动光标";
|
||||
"settings.systemPrompt.reset" = "重置";
|
||||
"settings.asrLocale" = "识别语言";
|
||||
@@ -397,7 +404,7 @@
|
||||
"polishStyles.editor.name" = "名称";
|
||||
"polishStyles.editor.namePlaceholder" = "风格名称";
|
||||
"polishStyles.editor.prompt" = "完整提示词";
|
||||
"polishStyles.editor.hint" = "使用 {{DICTIONARY}} 指定个人词典的插入位置。系统会自动追加安全边界、润色力度和输出契约。";
|
||||
"polishStyles.editor.hint" = "只需描述角色、语气、风格边界和示例。系统会自动追加个人词典、安全边界和输出契约。";
|
||||
"polishStyles.error.title" = "无法保存风格";
|
||||
"polishStyles.error.emptyName" = "请输入风格名称。";
|
||||
"polishStyles.error.emptyPrompt" = "提示词不能为空。";
|
||||
@@ -464,7 +471,8 @@
|
||||
"settings.appSettings.iCloudSync.synced" = "已同步";
|
||||
"settings.appSettings.iCloudSync.error.generic" = "无法与 iCloud 同步设置,请稍后重试。";
|
||||
|
||||
/* v0.3.0: 润色强度 */
|
||||
|
||||
/* 润色强度 */
|
||||
"settings.polishIntensity.title" = "润色强度";
|
||||
|
||||
/* Flow 会话策略 */
|
||||
|
||||
Reference in New Issue
Block a user