Files
OSGKeyboard/CHANGELOG.md
T
Rocky 537a68552a perf(asr): speed up local Flow dictation and land CLM/keyboard refactor
Reduce perceived latency from key release to final text:
- Adaptive chunking: 2.5s first chunk + 5s follow-ups so short
  utterances start on-device recognition while still recording.
- Session-level ASR warmup and audio-format cache reuse to remove
  per-utterance cold-start of SpeechAnalyzer.
- Mirror live pipelined partials to the keyboard transcript line via
  a new flow.transcriptionPartial App Group key + Darwin ping.

Also commits the accumulated custom language model, Flow session,
keyboard extension restructure, and Xiaomi MiMo provider work in
progress on this branch.
2026-07-06 00:00:19 +08:00

16 KiB
Raw Blame History

Changelog

All notable changes to OSGKeyboard will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[Unreleased]

Added

  • Xiaomi MiMo cloud provider: preset for the cloud engine with mimo-v2.5 polish via api.xiaomimimo.com (on-device ASR, same pipeline as other online providers). / 小米 MiMo 云端引擎:云端引擎新增预设,经 api.xiaomimimo.com 使用 mimo-v2.5 润色(端侧 ASR,与其他在线服务相同管线)。

Changed

  • Flow ASR pipelining: shorter first chunk (2.5s) and 5s follow-ups so short utterances start on-device recognition while still recording; session-level ASR warmup and format cache reuse; live partials mirrored to the keyboard transcript line. / Flow ASR 流水线:首块 2.5 秒、后续 5 秒,短句录音期间即开始端侧识别;会话级 ASR 预热与格式缓存复用;实时 partial 同步到键盘转写行。

[0.4.0] - 2026-07-05

Added

  • Custom ASR language model: on-device SFCustomLanguageModelData bias model (computer/IT terms + curated AI/tech brands) prepared via CustomLanguageModelManager and applied to DictationTranscriber for Chinese dictation; compiled LM/Vocab shared through the App Group. / 自定义语音识别语言模型:端侧 SFCustomLanguageModelData 偏置模型(计算机术语 + 精选 AI/科技品牌词),通过 CustomLanguageModelManager 在设备上准备并挂载到 DictationTranscriber 用于中文听写;编译后的 LM/Vocab 经 App Group 共享。
  • Cursor navigation: keyboard drag pad (CursorDragPad / CursorNavigation) for precise caret movement. / 光标导航:键盘拖动手势区(CursorDragPad / CursorNavigation),精确移动光标。
  • Key sound feedback: KeyboardSoundFeedback plays system key clicks on input. / 按键音反馈KeyboardSoundFeedback 在输入时播放系统按键音。
  • Personal dictionary tooling: DictionaryAliasGenerator and PersonalDictionaryEntrySheet for managing custom terms and aliases. / 个人词库工具DictionaryAliasGeneratorPersonalDictionaryEntrySheet,用于管理自定义词条与别名。
  • Transcript post-processing: TranscriptPostProcessor quality gate on the shared ASR path. / 转写后处理:共享 ASR 链路上的 TranscriptPostProcessor 质量校验。

Changed

  • Tab bar visibility: TabBarVisibility centralizes show/hide handling; retired PageHeaderRow / PageHeaderConfirmButton. / 标签栏可见性TabBarVisibility 统一管理显隐;移除 PageHeaderRow / PageHeaderConfirmButton

Removed

  • DictionaryLearner: replaced by the new dictionary tooling. / DictionaryLearner:由新的词库工具取代。

Security

  • DeepSeek key handling: move the hardcoded API key out of PreconfiguredKeys.swift into a gitignored PreconfiguredKeys.local.swift (seeded from .example by generate-xcodeproj.sh). / DeepSeek 密钥处理:将硬编码 API 密钥移出 PreconfiguredKeys.swift,改为 gitignore 的 PreconfiguredKeys.local.swift(由 generate-xcodeproj.sh.example 生成)。

[0.3.6] - 2026-07-05

Changed

  • ASR polish pipeline: global output contract (no new emojis, punctuation, structure at every intensity), TranscriptPostProcessor quality gate, ultra-short utterances skip LLM, removed Off polish tier (legacy off migrates to Medium), preceding-text context in keyboard polish path.

[0.3.4] - 2026-07-04

Added

  • Home usage statistics: new home-screen stats card showing cumulative dictation time, dictation characters, translation characters, and personal-dictionary entry count.

Changed

  • Local engine polish path: local mode now uses the built-in DeepSeek polish path by default, removing the separate "Cloud polish after ASR" toggle and user-facing DeepSeek API key setup.
  • Provider API keys: cloud-provider API keys are isolated per provider in Keychain, so switching providers no longer reuses the previous vendor's key.
  • Translation availability: translation settings are visible for both local and cloud engines.
  • DeepSeek provider visibility: DeepSeek is reserved for the local engine's built-in path and is no longer shown as a cloud-provider picker option.

Fixed

  • Home stats rendering: the stats card gradient background now uses a View-backed background compatible with SwiftUI's type system.
  • Usage statistics imports: the usage statistics store now imports the shared module required for translation-state checks.

[0.3.0] - 2026-06-24

Added

  • Post-polish translation for cloud and local engines: target-language picker in Settings / onboarding, TranslationChip on the keyboard top bar, and PolishMode.translate in PolishingService.
  • Preconfigured DeepSeek key (PreconfiguredKeys) for local-engine cloud polish without round-tripping the Settings API card.

Changed

  • Local-engine translation is gated on cloud polish: the translation row and LLM step are hidden/disabled until "Cloud polish after ASR" is enabled; turning polish off clears a stale translation target.
  • Local-engine LLM endpoint pinning: when the pipeline routes through DeepSeek, base URL and model come from the DeepSeek preset instead of the user's cloud-provider settings (fixes DeepSeek key + Qwen URL 401s).

Fixed

  • Translation chip always visible when the engine can run cloud LLM (cloud always; local when cloud polish is on) — no longer hidden when target is "不翻译".
  • Keyboard translation menu first item shows "不翻译"; chip label when off stays "翻译".
  • Translation toggle race: 2.5s protect window after chip writes, Darwin config notification, host finalize re-reads App Group; turning off cloud polish no longer clears saved translation target.

[0.2.1] - 2026-06-24

Removed

  • Qwen3 CoreML on-device ASR stack (rolled back from v0.2.0). Deleted the vendored Qwen3Speech SPM package, the Qwen3ASRService, the ModelManager / OnDeviceModelWarmup / OnDeviceModelsView / DownloadConfirmSheet UI and downloaders, the OnDeviceModel and OnDeviceModelStatus shared models, and the corresponding LocalASRBackend.qwen3ASR enum case. Removed the Qwen3ASRServiceProvider registration in OSGKeyboardApp, the ModelScope/HuggingFace mirror picker, and the Qwen3Speech SPM package declaration from project.yml. No more model download / loading / warm-up code paths or UI state.

Changed

  • Local engine narrows to iOS ASR. The on-device engine is now exclusively iOS 26 SpeechAnalyzer + DictationTranscriber (with SFSpeechRecognizer as the pre-26 fallback). The previous .qwen3ASR backend has been removed; LocalASRBackend retains a single .speechAnalyzer case so the next non-iOS backend can slot in without touching every call site.
  • Local engine is genuinely local by default. When the user picks "local" and leaves the new polish toggle off, the transcript is inserted at the cursor as-is — no cloud LLM round-trip.
  • DeepSeek preset defaults to deepseek-v4-flash. The DeepSeek LLMProvider.presets entry's defaultModel was bumped from deepseek-chat; existing users keep their saved model name until they re-pick the preset.

Added

  • Cloud polish toggle for the local engine. Settings → On-device models → "Cloud polish after ASR". When enabled, the local-engine transcript is routed through the user's configured cloud LLM (DeepSeek by default) via the existing PolishingService + LLMClient chain. When disabled, the local engine is ASR-only. The toggle is a plain Bool (ProviderConfig.localModeCloudPolishEnabled) and persists in the App Group so the keyboard extension can honour it during live dictation.
  • DeepSeek API key path for the local polish flow. SettingsView reveals the providerSection / apiSection cards when the polish toggle is on so the user can paste a DeepSeek key into the existing Keychain-bound field. PolishingService short-circuits with a new PolishError.missingAPIKey and surfaces a localised "fill in your DeepSeek key" warning when the toggle is on but the Keychain is empty. The raw transcript is still inserted (no data loss).
  • iOS 26 SpeechAnalyzer + DictationTranscriber is now the documented local ASR path. The pre-v0.2.0 code already supported this; v0.2.1 makes it the default and only on-device backend and adds a "Built-in" badge on the local-engine card so users see there's nothing to download.

Fixed

  • Two Swift 6 strict-concurrency issues in LiveDictationController and FlowSessionManager (the weak [weak self] capture inside await MainActor.run { } blocks) that were blocking xcodebuild clean builds under SWIFT_STRICT_CONCURRENCY=complete. The detached-task closure now re-captures the weak reference under @MainActor isolation.
  • OpenSourceLicensesView no longer lists the deleted speech-swift, swift-transformers, qwen3-asr-coreml, or qwen3-asr-upstream entries; only Google Material Icons remains.

[0.2.0] - 2026-06-22

Added

  • Local engine with on-device Qwen models: Optional Qwen3-ASR 0.6B speech recognition and Qwen3.5-0.8B text polish, fully offline after download.
  • On-device model management: Download, progress, delete, and readiness status in Settings; mirror auto-selection between ModelScope and Hugging Face with fallback.
  • Engine picker: Choose between local (on-device ASR + polish) and cloud (ASR + user-configured LLM polish).
  • Flow session dictation: TypeWhisper-style continuous capture in the host app with keyboard handoff via App Group.
  • Open-source licenses screen for bundled third-party components.

Changed

  • Settings simplified: Merged language and model sections; cloud mode always enables polish (removed off/transcribe mode picker).
  • Keyboard UI: Local/cloud engine badges replace the mode menu; shows model-not-downloaded guidance when the local stack is incomplete.
  • iPhone only: Set TARGETED_DEVICE_FAMILY to "1" for both targets; portrait-only orientations.
  • Keyboard preview always dark: Preview injects the dark palette regardless of app theme.
  • Docs consistency: README/README.zh aligned to the iOS 26+ capability set.

Fixed

  • ModelScope download progress: Progress now tracks byte counts instead of jumping to 50% after the first file.
  • Light/Dark mode consistency for shared button/card modifiers via @Environment(\.themePalette).

[0.1.2] - 2026-06-20

Fixed

  • Light/Dark mode consistency: cardSurface(), primaryButton(), secondaryButton(), and pillChip() view modifiers in Theme.swift now use ViewModifier structs that read from @Environment(\.themePalette). Previously they used hardcoded dark Palette constants, causing cards and buttons to always render in dark mode even when the main App was in light mode.
  • TestFlight error 90474 (Invalid bundle): Added UIRequiresFullScreen: true and all four UISupportedInterfaceOrientations values to Info.plist via project.yml. The app targets iPhone + iPad (TARGETED_DEVICE_FAMILY: "1,2"), and Apple requires all four orientations for iPad multitasking; setting UIRequiresFullScreen opts out of slide-over/split-view while still satisfying the validator.
  • Keyboard Preview cycling: The "Tap the disc to cycle states" prompt now actually works. KeyboardPreviewStub gained an onTap closure wired to cyclePhase() in KeyboardPreviewSheet, which rotates .idle → .recording → .processing → .idle with animation. Sample transcript text is shown during the .recording phase.

Added

  • Dynamic ASR locale picker: Settings now loads the full list of supported locales from SFSpeechRecognizer.supportedLocales() on appear (off the main thread). Each locale shows an on-device badge (iPhone icon) when the device supports on-device recognition for that language — giving users confidence about which locales avoid sending audio to the cloud. A static fallback list is shown while the async load is in progress.
  • Speech.framework linked to the main App target in project.yml (needed by the new dynamic locale loader in SettingsView).

Changed

  • pillChip(foreground:) signature changed from foreground: Color = Palette.textSecondary to foreground: Color? = nil; callers that pass an explicit color are unaffected.

Note: v0.1.1 polish — this is a small follow-up to v0.1.0 focused on review-driven cleanup (theme follow-up, ASR robustness, debug-print hygiene, docs). No features are removed.

Fixed

  • PrivacyInfo.xcprivacy audited for honesty: removed the three undeclared NSPrivacyAccessedAPIType entries (FileTimestamp / DiskSpace / SystemBootTime) the project doesn't actually use, and added ActiveKeyboards (reason DDA9.1) to the keyboard extension's manifest because advanceToNextInputMode() is in the tap path. The main App now declares only UserDefaults (reason CA92.1), which is the only Required Reason API it touches.
  • Theme follows system appearance: main App now renders a true light palette in light mode via ThemedRoot + EnvironmentKey<ThemePalette>. The keyboard extension deliberately stays dark (Apple's default) and now uses a transparent .background(Color.clear) so the system UI chrome shows through.
  • Speech Recognition permission requested on first press: added NSSpeechRecognitionUsageDescription to both targets' Info.plist and an explicit SFSpeechRecognizer.requestAuthorization call inside pressBegan(). Without these, speech authorization could remain .denied and recognition would fail silently.
  • ASRService emits a DEBUG warning when on-device recognition isn't supported, so it's obvious during dev that the request fell back to cloud.
  • App Group fallback behaviour:
    • In DEBUG, a missing App Group now fatalErrors with a precise remediation message (was a soft print + .standard fallback, which desynced the keyboard extension from the main App).
    • In release, the fallback is preserved but logged via NSLog.
  • KeyboardViewController.loadPersistedLocale self-check (DEBUG only) prints the active provider / baseURL / masked API key / mode / locale, so the keyboard extension's view of the App Group is visible in the device console.
  • KeyboardViewController.handleFinalTranscript typed-error routing: LLMError.noAPIKey (401) and LLMError.http(429) now surface as red, explicit error messages instead of silently inserting the raw transcript. Network / timeout errors still fall back to the raw transcript + error badge (no data loss).
  • PolishingService timeout raised from 12 s to 15 s to align with LLMClient's URL request timeout. Previously the polisher would race the network call and discard a successful response that arrived in the 1215 s window.
  • DEBUG print cleanup: the four 🔥 [OSGKeyboardApp] … instrumentation prints in OSGKeyboardApp.init() and root views are now wrapped in #if DEBUG.

Added

  • "Test connection" button in APISettingsCard: fires a single polish("ping") round-trip and surfaces success or the typed LLM error inline. Helps the user confirm the App Group + key are working without leaving the main App.
  • 5 new unit tests in OSGKeyboardTests: App Group cross-process persistence, 401 / 429 / timeout / noAPIKey catch paths, and mode = .off short-circuit.

Changed

  • README + README.zh.md: replaced <OWNER> placeholder with hkgood and aligned capability statements to the implemented iOS 26 path.

Known limitations

  • The keyboard does not work in password fields (iOS limitation).
  • Microphone requires "Allow Full Access" to be enabled in iOS Settings.
  • Whisper.cpp / on-device LLM polish is intentionally out of scope for v1 (cloud-only).

[0.1.0] - 2026-06-17

Added

  • First public pre-release.