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.
This commit is contained in:
Rocky
2026-07-06 00:00:19 +08:00
parent cfbfb542cc
commit 537a68552a
76 changed files with 3456 additions and 121086 deletions
+3
View File
@@ -58,6 +58,9 @@ PreconfiguredKeys.local.swift
# Lexicon build cache (downloaded SogouPopularDict TSV) # Lexicon build cache (downloaded SogouPopularDict TSV)
.cache/ .cache/
# Custom LM compile output (Mac-only; device-side prepare uses .bin from bundle)
OSGKeyboard/Resources/CustomLanguageModel/v1/compiled/
# Python bytecode # Python bytecode
__pycache__/ __pycache__/
*.pyc *.pyc
+9
View File
@@ -8,6 +8,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added ### 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`),精确移动光标。 - **Cursor navigation**: keyboard drag pad (`CursorDragPad` / `CursorNavigation`) for precise caret movement. / **光标导航**:键盘拖动手势区(`CursorDragPad` / `CursorNavigation`),精确移动光标。
- **Key sound feedback**: `KeyboardSoundFeedback` plays system key clicks on input. / **按键音反馈**`KeyboardSoundFeedback` 在输入时播放系统按键音。 - **Key sound feedback**: `KeyboardSoundFeedback` plays system key clicks on input. / **按键音反馈**`KeyboardSoundFeedback` 在输入时播放系统按键音。
- **Personal dictionary tooling**: `DictionaryAliasGenerator` and `PersonalDictionaryEntrySheet` for managing custom terms and aliases. / **个人词库工具**`DictionaryAliasGenerator``PersonalDictionaryEntrySheet`,用于管理自定义词条与别名。 - **Personal dictionary tooling**: `DictionaryAliasGenerator` and `PersonalDictionaryEntrySheet` for managing custom terms and aliases. / **个人词库工具**`DictionaryAliasGenerator``PersonalDictionaryEntrySheet`,用于管理自定义词条与别名。
+15
View File
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "mimo.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

+6 -58
View File
@@ -6,76 +6,24 @@ import OSGKeyboardShared
@main @main
struct OSGKeyboardApp: App { struct OSGKeyboardApp: App {
@Environment(\.scenePhase) private var scenePhase
@StateObject private var config = ProviderConfig.shared
@StateObject private var dictationCoordinator = DictationSessionCoordinator()
@StateObject private var flowManager = FlowSessionManager()
init() { init() {
MaterialIconsFont.registerIfNeeded() MaterialIconsFont.registerIfNeeded()
// v0.2.0: no backend-specific ASR provider to install here. if AppGroup.isAvailable {
// The local engine uses iOS 26 `SpeechAnalyzer` + CustomLanguageModelManager.shared.prepareInBackgroundIfNeeded()
// `DictationTranscriber`, which the shared framework wires }
// up directly via `ASRServiceFactory.make(...)`. The previous
// Qwen3 CoreML backend (and its mlx-swift transitive
// dependency) was removed in this release.
} }
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
ThemedRoot {
if AppGroup.isAvailable { if AppGroup.isAvailable {
if config.hasCompletedOnboarding { ThemedRoot {
MainTabView() MainAppRoot()
} else {
OnboardingView(config: config)
} }
} else { } else {
ThemedRoot {
AppGroupErrorView() AppGroupErrorView()
} }
} }
.environment(\.locale, config.uiLanguage.swiftUILocale)
.environmentObject(flowManager)
.onAppear {
FlowAppLifecycle.shared.setForeground(scenePhase == .active)
flowManager.setAppForeground(scenePhase == .active)
}
.onOpenURL { url in
guard url.scheme == "osgkeyboard" else { return }
switch url.host {
case "dictate":
dictationCoordinator.present()
case "startflow":
flowManager.startSession()
default:
break
}
}
.fullScreenCover(isPresented: $dictationCoordinator.isPresenting) {
DictationCaptureView(
config: config,
coordinator: dictationCoordinator
)
}
.onChange(of: config.hasCompletedOnboarding) { _, done in
if done {
flowManager.autoStartIfNeeded()
// v0.2.0: no on-device ASR weights to warm up.
// iOS `SpeechAnalyzer` is always ready.
}
}
.onChange(of: scenePhase) { _, phase in
flowManager.handleScenePhase(phase)
guard phase == .active, AppGroup.isAvailable, config.hasCompletedOnboarding else { return }
// v0.2.0: iOS `SpeechAnalyzer` is bundled with the OS
// and needs no warm-up after a background trip.
if flowManager.isActive {
flowManager.extendSession()
} else {
flowManager.autoStartIfNeeded()
}
}
} }
} }
} }
@@ -1,21 +1,21 @@
{ {
"version": "v1", "version": "v1",
"name": "ai-tech-brands", "name": "ai-tech-brands",
"generated_at": "2026-07-05T08:00:01.226941+00:00", "generated_at": "2026-07-05T11:34:08.462301+00:00",
"locale": "zh-Hans", "locale": "zh-Hans",
"entry_count": 749, "entry_count": 1259,
"seed_file": "Scripts/lexicon/seeds/ai_tech_brands_seed.tsv", "seed_file": "Scripts/lexicon/seeds/ai_tech_brands_seed.tsv",
"license": "MIT (curated seed; OSGKeyboard contributors)", "license": "MIT (curated seed; OSGKeyboard contributors)",
"categories": { "categories": {
"ai_brand": 133, "ai_brand": 169,
"ai_model": 31, "ai_model": 90,
"ai_platform": 20, "ai_platform": 53,
"ai_term": 116, "ai_term": 197,
"dev_tool": 74, "dev_tool": 283,
"fintech": 7, "fintech": 7,
"tech_company": 280, "tech_company": 314,
"tech_leader": 37, "tech_leader": 37,
"tech_term": 51 "tech_term": 109
}, },
"notes": [ "notes": [
"Curated bilingual AI brands, tech companies, terminology, and hot words.", "Curated bilingual AI brands, tech companies, terminology, and hot words.",
@@ -18,10 +18,22 @@ open ai ai_tech_seed ai_brand 100 OpenAI
Open AI ai_tech_seed ai_brand 100 OpenAI Open AI ai_tech_seed ai_brand 100 OpenAI
OpenAI ai_tech_seed ai_brand 100 OpenAI OpenAI ai_tech_seed ai_brand 100 OpenAI
深度求索 shen du qiu suo ai_tech_seed ai_brand 100 深度求索 深度求索 shen du qiu suo ai_tech_seed ai_brand 100 深度求索
ChatGPT 5 ai_tech_seed ai_model 98 GPT-5
gpt five ai_tech_seed ai_model 98 GPT-5
GPT five ai_tech_seed ai_model 98 GPT-5
gpt five five ai_tech_seed ai_model 98 GPT-5.5
GPT five point five ai_tech_seed ai_model 98 GPT-5.5
GPT-5 ai_tech_seed ai_model 98 GPT-5
GPT-5.5 ai_tech_seed ai_model 98 GPT-5.5
gpt-5.5 ai_tech_seed ai_model 98 GPT-5.5
NVDA ai_tech_seed tech_company 98 Nvidia NVDA ai_tech_seed tech_company 98 Nvidia
Nvidia ai_tech_seed tech_company 98 Nvidia Nvidia ai_tech_seed tech_company 98 Nvidia
nvidia ai_tech_seed tech_company 98 Nvidia nvidia ai_tech_seed tech_company 98 Nvidia
英伟达 ai_tech_seed tech_company 98 Nvidia 英伟达 ai_tech_seed tech_company 98 Nvidia
Claude Code ai_tech_seed dev_tool 96 Claude Code
claude code ai_tech_seed dev_tool 96 Claude Code
ClaudeCode ai_tech_seed dev_tool 96 Claude Code
克劳德代码 ai_tech_seed dev_tool 96 Claude Code
Alibaba ai_tech_seed tech_company 95 Alibaba Alibaba ai_tech_seed tech_company 95 Alibaba
alibaba ai_tech_seed tech_company 95 Alibaba alibaba ai_tech_seed tech_company 95 Alibaba
Alphabet ai_tech_seed tech_company 95 Google Alphabet ai_tech_seed tech_company 95 Google
@@ -36,6 +48,9 @@ byd ai_tech_seed tech_company 95 BYD
byte dance ai_tech_seed tech_company 95 ByteDance byte dance ai_tech_seed tech_company 95 ByteDance
ByteDance ai_tech_seed tech_company 95 ByteDance ByteDance ai_tech_seed tech_company 95 ByteDance
Bytedance ai_tech_seed tech_company 95 ByteDance Bytedance ai_tech_seed tech_company 95 ByteDance
deepseek r one ai_tech_seed ai_model 95 DeepSeek R1
DeepSeek R1 ai_tech_seed ai_model 95 DeepSeek R1
DeepSeek-R1 ai_tech_seed ai_model 95 DeepSeek R1
Facebook ai_tech_seed tech_company 95 Meta Facebook ai_tech_seed tech_company 95 Meta
Gemini ai_tech_seed ai_brand 95 Gemini Gemini ai_tech_seed ai_brand 95 Gemini
gemini ai_tech_seed ai_brand 95 Gemini gemini ai_tech_seed ai_brand 95 Gemini
@@ -100,8 +115,18 @@ xiaomi ai_tech_seed tech_company 95 Xiaomi
通义千问 ai_tech_seed ai_brand 95 Qwen 通义千问 ai_tech_seed ai_brand 95 Qwen
阿里 ai_tech_seed tech_company 95 Alibaba 阿里 ai_tech_seed tech_company 95 Alibaba
阿里巴巴 ai_tech_seed tech_company 95 Alibaba 阿里巴巴 ai_tech_seed tech_company 95 Alibaba
claude opus ai_tech_seed ai_model 94 Claude Opus
claude sonnet ai_tech_seed ai_model 94 Claude Sonnet
Opus ai_tech_seed ai_model 94 Claude Opus
Opus 4 ai_tech_seed ai_model 94 Claude Opus
Opus 4.7 ai_tech_seed ai_model 94 Claude Opus
Sonnet ai_tech_seed ai_model 94 Claude Sonnet
Sonnet 4 ai_tech_seed ai_model 94 Claude Sonnet
Sonnet 4.6 ai_tech_seed ai_model 94 Claude Sonnet
agent ai_tech_seed ai_term 92 智能体 agent ai_tech_seed ai_term 92 智能体
Agent ai_tech_seed ai_term 92 智能体 Agent ai_tech_seed ai_term 92 智能体
Agent Mode ai_tech_seed ai_term 92 Agent Mode
agent mode ai_tech_seed ai_term 92 Agent Mode
AI agent ai_tech_seed ai_term 92 智能体 AI agent ai_tech_seed ai_term 92 智能体
Amazon Web Services ai_tech_seed tech_company 92 AWS Amazon Web Services ai_tech_seed tech_company 92 AWS
aws ai_tech_seed tech_company 92 AWS aws ai_tech_seed tech_company 92 AWS
@@ -109,13 +134,34 @@ Baidu ai_tech_seed tech_company 92 Baidu
baidu ai_tech_seed tech_company 92 Baidu baidu ai_tech_seed tech_company 92 Baidu
CATL ai_tech_seed tech_company 92 CATL CATL ai_tech_seed tech_company 92 CATL
catl ai_tech_seed tech_company 92 CATL catl ai_tech_seed tech_company 92 CATL
Codex ai_tech_seed dev_tool 92 OpenAI Codex
codex cli ai_tech_seed dev_tool 92 OpenAI Codex
Composer ai_tech_seed dev_tool 92 Composer
Composer 2 ai_tech_seed dev_tool 92 Composer
Composer 2.5 ai_tech_seed dev_tool 92 Composer
context engineering ai_tech_seed ai_term 92 context engineering
cursor composer ai_tech_seed dev_tool 92 Composer
Cursor Composer ai_tech_seed dev_tool 92 Cursor Composer
deepseek r1 ai_tech_seed ai_model 92 DeepSeek-R1 deepseek r1 ai_tech_seed ai_model 92 DeepSeek-R1
DeepSeek R1 ai_tech_seed ai_model 92 DeepSeek-R1 deepseek v four ai_tech_seed ai_model 92 DeepSeek V4
DeepSeek-R1 ai_tech_seed ai_model 92 DeepSeek-R1 deepseek v three ai_tech_seed ai_model 92 DeepSeek V3
DeepSeek V3 ai_tech_seed ai_model 92 DeepSeek V3
DeepSeek V4 ai_tech_seed ai_model 92 DeepSeek V4
DeepSeek V4 Pro ai_tech_seed ai_model 92 DeepSeek V4
DeepSeek-V3 ai_tech_seed ai_model 92 DeepSeek V3
DeepSeek-V4 ai_tech_seed ai_model 92 DeepSeek V4
dictation transcriber ai_tech_seed dev_tool 92 DictationTranscriber
Dictation Transcriber ai_tech_seed dev_tool 92 DictationTranscriber
DictationTranscriber ai_tech_seed dev_tool 92 DictationTranscriber
Elon Musk ai_tech_seed tech_leader 92 Elon Musk Elon Musk ai_tech_seed tech_leader 92 Elon Musk
elon musk ai_tech_seed tech_leader 92 Elon Musk elon musk ai_tech_seed tech_leader 92 Elon Musk
Gemini 3 Pro ai_tech_seed ai_model 92 Gemini 3 Pro
Gemini Pro ai_tech_seed ai_model 92 Gemini 3 Pro
gemini three pro ai_tech_seed ai_model 92 Gemini 3 Pro
GitHub ai_tech_seed tech_company 92 GitHub GitHub ai_tech_seed tech_company 92 GitHub
github ai_tech_seed tech_company 92 GitHub github ai_tech_seed tech_company 92 GitHub
gpt four o ai_tech_seed ai_model 92 GPT-4o
GPT four oh ai_tech_seed ai_model 92 GPT-4o
large language model ai_tech_seed ai_term 92 LLM large language model ai_tech_seed ai_term 92 LLM
large model ai_tech_seed ai_term 92 大模型 large model ai_tech_seed ai_term 92 大模型
LLM ai_tech_seed ai_term 92 LLM LLM ai_tech_seed ai_term 92 LLM
@@ -123,14 +169,21 @@ llm ai_tech_seed ai_term 92 LLM
MCP ai_tech_seed ai_term 92 MCP MCP ai_tech_seed ai_term 92 MCP
MCP server ai_tech_seed ai_term 92 MCP MCP server ai_tech_seed ai_term 92 MCP
model context protocol ai_tech_seed ai_term 92 MCP model context protocol ai_tech_seed ai_term 92 MCP
OpenAI Codex ai_tech_seed dev_tool 92 OpenAI Codex
openai codex ai_tech_seed dev_tool 92 OpenAI Codex
prompt ai_tech_seed ai_term 92 提示词 prompt ai_tech_seed ai_term 92 提示词
Prompt ai_tech_seed ai_term 92 提示词 Prompt ai_tech_seed ai_term 92 提示词
R1 ai_tech_seed ai_model 92 DeepSeek-R1 R1 ai_tech_seed ai_model 92 DeepSeek-R1
speech analyzer ai_tech_seed dev_tool 92 SpeechAnalyzer
Speech Analyzer ai_tech_seed dev_tool 92 SpeechAnalyzer
SpeechAnalyzer ai_tech_seed dev_tool 92 SpeechAnalyzer
tik tok ai_tech_seed tech_company 92 TikTok tik tok ai_tech_seed tech_company 92 TikTok
Tik Tok ai_tech_seed tech_company 92 TikTok Tik Tok ai_tech_seed tech_company 92 TikTok
TikTok ai_tech_seed tech_company 92 TikTok TikTok ai_tech_seed tech_company 92 TikTok
we chat ai_tech_seed tech_company 92 WeChat we chat ai_tech_seed tech_company 92 WeChat
WeChat ai_tech_seed tech_company 92 WeChat WeChat ai_tech_seed tech_company 92 WeChat
上下文工程 ai_tech_seed ai_term 92 context engineering
代理模式 ai_tech_seed ai_term 92 Agent Mode
大模型 da mo xing ai_tech_seed ai_term 92 大模型 大模型 da mo xing ai_tech_seed ai_term 92 大模型
大语言模型 ai_tech_seed ai_term 92 LLM 大语言模型 ai_tech_seed ai_term 92 LLM
宁德时代 ai_tech_seed tech_company 92 CATL 宁德时代 ai_tech_seed tech_company 92 CATL
@@ -138,20 +191,21 @@ WeChat ai_tech_seed tech_company 92 WeChat
抖音海外 ai_tech_seed tech_company 92 TikTok 抖音海外 ai_tech_seed tech_company 92 TikTok
提示词 ti shi ci ai_tech_seed ai_term 92 提示词 提示词 ti shi ci ai_tech_seed ai_term 92 提示词
智能体 zhi neng ti ai_tech_seed ai_term 92 智能体 智能体 zhi neng ti ai_tech_seed ai_term 92 智能体
智能体模式 ai_tech_seed ai_term 92 Agent Mode
百度 ai_tech_seed tech_company 92 Baidu 百度 ai_tech_seed tech_company 92 Baidu
马斯克 ai_tech_seed tech_leader 92 Elon Musk 马斯克 ai_tech_seed tech_leader 92 Elon Musk
agentic ai_tech_seed ai_term 90 agent agentic ai_tech_seed ai_term 90 agent
AGI ai_tech_seed ai_term 90 AGI AGI ai_tech_seed ai_term 90 AGI
agi ai_tech_seed ai_term 90 AGI agi ai_tech_seed ai_term 90 AGI
alphabet ai_tech_seed tech_company 90 Alphabet alphabet ai_tech_seed tech_company 90 Alphabet
Codex CLI ai_tech_seed dev_tool 90 Codex CLI
Copilot ai_tech_seed ai_brand 90 Copilot Copilot ai_tech_seed ai_brand 90 Copilot
copilot ai_tech_seed ai_brand 90 Copilot copilot ai_tech_seed ai_brand 90 Copilot
Cursor ai_tech_seed dev_tool 90 Cursor Cursor ai_tech_seed dev_tool 90 Cursor
cursor ai ai_tech_seed dev_tool 90 Cursor cursor ai ai_tech_seed dev_tool 90 Cursor
Cursor AI ai_tech_seed dev_tool 90 Cursor Cursor AI ai_tech_seed dev_tool 90 Cursor
custom language model data ai_tech_seed dev_tool 90 SFCustomLanguageModelData
deepseek v3 ai_tech_seed ai_model 90 DeepSeek-V3 deepseek v3 ai_tech_seed ai_model 90 DeepSeek-V3
DeepSeek V3 ai_tech_seed ai_model 90 DeepSeek-V3
DeepSeek-V3 ai_tech_seed ai_model 90 DeepSeek-V3
DJI ai_tech_seed tech_company 90 DJI DJI ai_tech_seed tech_company 90 DJI
dji ai_tech_seed tech_company 90 DJI dji ai_tech_seed tech_company 90 DJI
dou yin ai_tech_seed tech_company 90 Douyin dou yin ai_tech_seed tech_company 90 Douyin
@@ -164,6 +218,8 @@ ERNIE ai_tech_seed ai_brand 90 文心一言
Ernie ai_tech_seed ai_brand 90 文心一言 Ernie ai_tech_seed ai_brand 90 文心一言
fine tuning ai_tech_seed ai_term 90 微调 fine tuning ai_tech_seed ai_term 90 微调
fine-tuning ai_tech_seed ai_term 90 微调 fine-tuning ai_tech_seed ai_term 90 微调
Gemini 2.5 Pro ai_tech_seed ai_model 90 Gemini 2.5 Pro
gemini two five pro ai_tech_seed ai_model 90 Gemini 2.5 Pro
GitHub Copilot ai_tech_seed ai_brand 90 Copilot GitHub Copilot ai_tech_seed ai_brand 90 Copilot
GLM ai_tech_seed ai_brand 90 GLM GLM ai_tech_seed ai_brand 90 GLM
glm ai_tech_seed ai_brand 90 GLM glm ai_tech_seed ai_brand 90 GLM
@@ -173,6 +229,10 @@ Jensen Huang ai_tech_seed tech_leader 90 Jensen Huang
jensen huang ai_tech_seed tech_leader 90 Jensen Huang jensen huang ai_tech_seed tech_leader 90 Jensen Huang
k8s ai_tech_seed dev_tool 90 Kubernetes k8s ai_tech_seed dev_tool 90 Kubernetes
K8s ai_tech_seed dev_tool 90 Kubernetes K8s ai_tech_seed dev_tool 90 Kubernetes
kimi k two ai_tech_seed ai_model 90 Kimi K2
Kimi K2 ai_tech_seed ai_model 90 Kimi K2
Kimi K2.5 ai_tech_seed ai_model 90 Kimi K2
Kimi K2.6 ai_tech_seed ai_model 90 Kimi K2
Kubernetes ai_tech_seed dev_tool 90 Kubernetes Kubernetes ai_tech_seed dev_tool 90 Kubernetes
kubernetes ai_tech_seed dev_tool 90 Kubernetes kubernetes ai_tech_seed dev_tool 90 Kubernetes
Lei Jun ai_tech_seed tech_leader 90 雷军 Lei Jun ai_tech_seed tech_leader 90 雷军
@@ -186,12 +246,21 @@ Llama 3 ai_tech_seed ai_model 90 Llama
Llama 4 ai_tech_seed ai_model 90 Llama Llama 4 ai_tech_seed ai_model 90 Llama
LoRA ai_tech_seed ai_term 90 LoRA LoRA ai_tech_seed ai_term 90 LoRA
lora ai_tech_seed ai_term 90 LoRA lora ai_tech_seed ai_term 90 LoRA
mcp server ai_tech_seed ai_term 90 MCP server
Microsoft Copilot ai_tech_seed ai_brand 90 Copilot Microsoft Copilot ai_tech_seed ai_brand 90 Copilot
multi agent ai_tech_seed ai_term 90 multi-agent
multi-agent ai_tech_seed ai_term 90 multi-agent
NIO ai_tech_seed tech_company 90 NIO NIO ai_tech_seed tech_company 90 NIO
nio ai_tech_seed tech_company 90 NIO nio ai_tech_seed tech_company 90 NIO
prompt engineering ai_tech_seed ai_term 90 prompt prompt engineering ai_tech_seed ai_term 90 prompt
Qwen Coder ai_tech_seed ai_model 90 Qwen Coder
qwen coder ai_tech_seed ai_model 90 Qwen Coder
SF Custom Language Model Data ai_tech_seed dev_tool 90 SFCustomLanguageModelData
SFCustomLanguageModelData ai_tech_seed dev_tool 90 SFCustomLanguageModelData
Steve Jobs ai_tech_seed tech_leader 90 乔布斯 Steve Jobs ai_tech_seed tech_leader 90 乔布斯
steve jobs ai_tech_seed tech_leader 90 Steve Jobs steve jobs ai_tech_seed tech_leader 90 Steve Jobs
tool calling ai_tech_seed ai_term 90 tool calling
tool-calling ai_tech_seed ai_term 90 tool calling
TSMC ai_tech_seed tech_company 90 TSMC TSMC ai_tech_seed tech_company 90 TSMC
tsmc ai_tech_seed tech_company 90 TSMC tsmc ai_tech_seed tech_company 90 TSMC
X Peng ai_tech_seed tech_company 90 XPeng X Peng ai_tech_seed tech_company 90 XPeng
@@ -200,19 +269,24 @@ xpeng ai_tech_seed tech_company 90 XPeng
Zhipu ai_tech_seed ai_brand 90 GLM Zhipu ai_tech_seed ai_brand 90 GLM
乔布斯 qiao bu si ai_tech_seed tech_leader 90 乔布斯 乔布斯 qiao bu si ai_tech_seed tech_leader 90 乔布斯
低秩适配 ai_tech_seed ai_term 90 LoRA 低秩适配 ai_tech_seed ai_term 90 LoRA
千问 coder ai_tech_seed ai_model 90 Qwen Coder
台积电 ai_tech_seed tech_company 90 TSMC 台积电 ai_tech_seed tech_company 90 TSMC
多智能体 ai_tech_seed ai_term 90 multi-agent
大疆 ai_tech_seed tech_company 90 DJI 大疆 ai_tech_seed tech_company 90 DJI
小鹏汽车 ai_tech_seed tech_company 90 XPeng 小鹏汽车 ai_tech_seed tech_company 90 XPeng
嵌入 ai_tech_seed ai_term 90 embedding 嵌入 ai_tech_seed ai_term 90 embedding
工具调用 ai_tech_seed ai_term 90 tool calling
微调 wei tiao ai_tech_seed ai_term 90 微调 微调 wei tiao ai_tech_seed ai_term 90 微调
抖音 ai_tech_seed tech_company 90 Douyin 抖音 ai_tech_seed tech_company 90 Douyin
文心一言 wen xin yi yan ai_tech_seed ai_brand 90 文心一言 文心一言 wen xin yi yan ai_tech_seed ai_brand 90 文心一言
智谱 ai_tech_seed ai_brand 90 GLM 智谱 ai_tech_seed ai_brand 90 GLM
月之暗面 yue zhi an mian ai_tech_seed ai_brand 90 月之暗面 月之暗面 yue zhi an mian ai_tech_seed ai_brand 90 月之暗面
模型上下文协议服务器 ai_tech_seed ai_term 90 MCP server
理想 ai_tech_seed tech_company 90 Li Auto 理想 ai_tech_seed tech_company 90 Li Auto
理想汽车 ai_tech_seed tech_company 90 Li Auto 理想汽车 ai_tech_seed tech_company 90 Li Auto
蔚来 ai_tech_seed tech_company 90 NIO 蔚来 ai_tech_seed tech_company 90 NIO
豆包 dou bao ai_tech_seed ai_brand 90 豆包 豆包 dou bao ai_tech_seed ai_brand 90 豆包
通义千问 coder ai_tech_seed ai_model 90 Qwen Coder
通用人工智能 ai_tech_seed ai_term 90 AGI 通用人工智能 ai_tech_seed ai_term 90 AGI
雷军 lei jun ai_tech_seed tech_leader 90 雷军 雷军 lei jun ai_tech_seed tech_leader 90 雷军
黄仁勋 ai_tech_seed tech_leader 90 Jensen Huang 黄仁勋 ai_tech_seed tech_leader 90 Jensen Huang
@@ -230,14 +304,26 @@ bilibili ai_tech_seed tech_company 88 Bilibili
B站 ai_tech_seed tech_company 88 Bilibili B站 ai_tech_seed tech_company 88 Bilibili
chain of thought ai_tech_seed ai_term 88 chain of thought chain of thought ai_tech_seed ai_term 88 chain of thought
chain-of-thought ai_tech_seed ai_term 88 chain of thought chain-of-thought ai_tech_seed ai_term 88 chain of thought
Cognition Devin ai_tech_seed dev_tool 88 Devin
context window ai_tech_seed ai_term 88 上下文窗口 context window ai_tech_seed ai_term 88 上下文窗口
Core ML ai_tech_seed dev_tool 88 Core ML
core ml ai_tech_seed dev_tool 88 Core ML
CoreML ai_tech_seed dev_tool 88 Core ML
deep mind ai_tech_seed ai_brand 88 DeepMind deep mind ai_tech_seed ai_brand 88 DeepMind
DeepMind ai_tech_seed ai_brand 88 DeepMind DeepMind ai_tech_seed ai_brand 88 DeepMind
Devin ai_tech_seed dev_tool 88 Devin
devin ai_tech_seed dev_tool 88 Devin
Docker ai_tech_seed tech_company 88 Docker Docker ai_tech_seed tech_company 88 Docker
docker ai_tech_seed tech_company 88 Docker docker ai_tech_seed tech_company 88 Docker
dou bao ai_tech_seed ai_brand 88 Doubao dou bao ai_tech_seed ai_brand 88 Doubao
ernie ai_tech_seed ai_brand 88 ERNIE ernie ai_tech_seed ai_brand 88 ERNIE
function calling ai_tech_seed ai_term 88 function calling function calling ai_tech_seed ai_term 88 function calling
Gemini 2.5 Flash ai_tech_seed ai_model 88 Gemini Flash
Gemini 3 Flash ai_tech_seed ai_model 88 Gemini Flash
Gemini CLI ai_tech_seed dev_tool 88 Gemini CLI
gemini cli ai_tech_seed dev_tool 88 Gemini CLI
Gemini Flash ai_tech_seed ai_model 88 Gemini Flash
gemini flash ai_tech_seed ai_model 88 Gemini Flash
gen ai ai_tech_seed ai_term 88 GenAI gen ai ai_tech_seed ai_term 88 GenAI
GenAI ai_tech_seed ai_term 88 GenAI GenAI ai_tech_seed ai_term 88 GenAI
github copilot ai_tech_seed ai_brand 88 GitHub Copilot github copilot ai_tech_seed ai_brand 88 GitHub Copilot
@@ -254,6 +340,8 @@ intel ai_tech_seed tech_company 88 Intel
JD ai_tech_seed tech_company 88 JD.com JD ai_tech_seed tech_company 88 JD.com
JD.com ai_tech_seed tech_company 88 JD.com JD.com ai_tech_seed tech_company 88 JD.com
jd.com ai_tech_seed tech_company 88 JD.com jd.com ai_tech_seed tech_company 88 JD.com
Kimi Code ai_tech_seed dev_tool 88 Kimi Code
kimi code ai_tech_seed dev_tool 88 Kimi Code
Lenovo ai_tech_seed tech_company 88 Lenovo Lenovo ai_tech_seed tech_company 88 Lenovo
lenovo ai_tech_seed tech_company 88 Lenovo lenovo ai_tech_seed tech_company 88 Lenovo
Meituan ai_tech_seed tech_company 88 Meituan Meituan ai_tech_seed tech_company 88 Meituan
@@ -264,41 +352,60 @@ Midjourney ai_tech_seed ai_brand 88 Midjourney
mixture of experts ai_tech_seed ai_term 88 MoE mixture of experts ai_tech_seed ai_term 88 MoE
MoE ai_tech_seed ai_term 88 MoE MoE ai_tech_seed ai_term 88 MoE
moe ai_tech_seed ai_term 88 MoE moe ai_tech_seed ai_term 88 MoE
Moonshot Kimi Code ai_tech_seed dev_tool 88 Kimi Code
multimodal ai_tech_seed ai_term 88 多模态 multimodal ai_tech_seed ai_term 88 多模态
o1 ai_tech_seed ai_model 88 o1 o1 ai_tech_seed ai_model 88 o1
O1 ai_tech_seed ai_model 88 o1 O1 ai_tech_seed ai_model 88 o1
o3 ai_tech_seed ai_model 88 o3 o3 ai_tech_seed ai_model 88 o3
O3 ai_tech_seed ai_model 88 o3 O3 ai_tech_seed ai_model 88 o3
open code ai_tech_seed dev_tool 88 OpenCode
openai o1 ai_tech_seed ai_model 88 o1 openai o1 ai_tech_seed ai_model 88 o1
openai o3 ai_tech_seed ai_model 88 o3 openai o3 ai_tech_seed ai_model 88 o3
OpenCode ai_tech_seed dev_tool 88 OpenCode
opencode ai_tech_seed dev_tool 88 OpenCode
OpenCode AI ai_tech_seed dev_tool 88 OpenCode
PDD ai_tech_seed tech_company 88 Pinduoduo PDD ai_tech_seed tech_company 88 Pinduoduo
Perplexity ai_tech_seed ai_brand 88 Perplexity Perplexity ai_tech_seed ai_brand 88 Perplexity
perplexity ai ai_tech_seed ai_brand 88 Perplexity perplexity ai ai_tech_seed ai_brand 88 Perplexity
Perplexity AI ai_tech_seed ai_brand 88 Perplexity Perplexity AI ai_tech_seed ai_brand 88 Perplexity
Pinduoduo ai_tech_seed tech_company 88 Pinduoduo Pinduoduo ai_tech_seed tech_company 88 Pinduoduo
pinduoduo ai_tech_seed tech_company 88 Pinduoduo pinduoduo ai_tech_seed tech_company 88 Pinduoduo
postgre sql ai_tech_seed tech_term 88 Postgres
Postgres ai_tech_seed tech_term 88 Postgres
postgres ai_tech_seed tech_term 88 Postgres
PostgreSQL ai_tech_seed tech_term 88 Postgres
postgresql ai_tech_seed tech_term 88 PostgreSQL
Py Torch ai_tech_seed dev_tool 88 PyTorch Py Torch ai_tech_seed dev_tool 88 PyTorch
Python ai_tech_seed dev_tool 88 Python Python ai_tech_seed dev_tool 88 Python
python ai_tech_seed dev_tool 88 Python python ai_tech_seed dev_tool 88 Python
PyTorch ai_tech_seed dev_tool 88 PyTorch PyTorch ai_tech_seed dev_tool 88 PyTorch
pytorch ai_tech_seed dev_tool 88 PyTorch pytorch ai_tech_seed dev_tool 88 PyTorch
Qwen Max ai_tech_seed ai_model 88 Qwen Max
qwen max ai_tech_seed ai_model 88 Qwen Max
Qwen3 Max ai_tech_seed ai_model 88 Qwen Max
reasoning ai_tech_seed ai_term 88 推理 reasoning ai_tech_seed ai_term 88 推理
reasoning model ai_tech_seed ai_term 88 reasoning model reasoning model ai_tech_seed ai_term 88 reasoning model
Sam Altman ai_tech_seed tech_leader 88 Sam Altman Sam Altman ai_tech_seed tech_leader 88 Sam Altman
sam altman ai_tech_seed tech_leader 88 Sam Altman sam altman ai_tech_seed tech_leader 88 Sam Altman
Samsung ai_tech_seed tech_company 88 Samsung Samsung ai_tech_seed tech_company 88 Samsung
samsung ai_tech_seed tech_company 88 Samsung samsung ai_tech_seed tech_company 88 Samsung
SF Speech Language Model ai_tech_seed dev_tool 88 SFSpeechLanguageModel
SFSpeechLanguageModel ai_tech_seed dev_tool 88 SFSpeechLanguageModel
SMIC ai_tech_seed tech_company 88 SMIC SMIC ai_tech_seed tech_company 88 SMIC
smic ai_tech_seed tech_company 88 SMIC smic ai_tech_seed tech_company 88 SMIC
Sora ai_tech_seed ai_brand 88 Sora Sora ai_tech_seed ai_brand 88 Sora
sora ai ai_tech_seed ai_brand 88 Sora sora ai ai_tech_seed ai_brand 88 Sora
Sora AI ai_tech_seed ai_brand 88 Sora Sora AI ai_tech_seed ai_brand 88 Sora
speech language model ai_tech_seed dev_tool 88 SFSpeechLanguageModel
Stripe ai_tech_seed tech_company 88 Stripe Stripe ai_tech_seed tech_company 88 Stripe
stripe ai_tech_seed tech_company 88 Stripe stripe ai_tech_seed tech_company 88 Stripe
swift data ai_tech_seed dev_tool 88 SwiftData
SwiftData ai_tech_seed dev_tool 88 SwiftData
Taobao ai_tech_seed tech_company 88 Taobao Taobao ai_tech_seed tech_company 88 Taobao
taobao ai_tech_seed tech_company 88 Taobao taobao ai_tech_seed tech_company 88 Taobao
thinking model ai_tech_seed ai_term 88 reasoning model thinking model ai_tech_seed ai_term 88 reasoning model
tool calling ai_tech_seed ai_term 88 function calling tool use ai_tech_seed ai_term 88 tool use
tool-use ai_tech_seed ai_term 88 tool use
Visual Studio Code ai_tech_seed dev_tool 88 VS Code Visual Studio Code ai_tech_seed dev_tool 88 VS Code
VS Code ai_tech_seed dev_tool 88 VS Code VS Code ai_tech_seed dev_tool 88 VS Code
vs code ai_tech_seed dev_tool 88 VS Code vs code ai_tech_seed dev_tool 88 VS Code
@@ -315,7 +422,7 @@ Zhipu AI ai_tech_seed ai_brand 88 智谱
哔哩哔哩 ai_tech_seed tech_company 88 Bilibili 哔哩哔哩 ai_tech_seed tech_company 88 Bilibili
多模态 duo mo tai ai_tech_seed ai_term 88 多模态 多模态 duo mo tai ai_tech_seed ai_term 88 多模态
山姆奥特曼 ai_tech_seed tech_leader 88 Sam Altman 山姆奥特曼 ai_tech_seed tech_leader 88 Sam Altman
工具用 ai_tech_seed ai_term 88 function calling 工具使用 ai_tech_seed ai_term 88 tool use
幻觉 huan jue ai_tech_seed ai_term 88 幻觉 幻觉 huan jue ai_tech_seed ai_term 88 幻觉
思维链 ai_tech_seed ai_term 88 chain of thought 思维链 ai_tech_seed ai_term 88 chain of thought
拼多多 ai_tech_seed tech_company 88 Pinduoduo 拼多多 ai_tech_seed tech_company 88 Pinduoduo
@@ -330,6 +437,58 @@ Zhipu AI ai_tech_seed ai_brand 88 智谱
联想 ai_tech_seed tech_company 88 Lenovo 联想 ai_tech_seed tech_company 88 Lenovo
英特尔 ai_tech_seed tech_company 88 Intel 英特尔 ai_tech_seed tech_company 88 Intel
蚂蚁集团 ai_tech_seed tech_company 88 Ant Group 蚂蚁集团 ai_tech_seed tech_company 88 Ant Group
通义千问 Max ai_tech_seed ai_model 88 Qwen Max
App Group ai_tech_seed dev_tool 86 App Group
app group ai_tech_seed dev_tool 86 App Group
App Groups ai_tech_seed dev_tool 86 App Group
evals ai_tech_seed ai_term 86 evals
evaluation ai_tech_seed ai_term 86 evals
GLM 5 ai_tech_seed ai_model 86 GLM-5
glm five ai_tech_seed ai_model 86 GLM-5
Grok 4 ai_tech_seed ai_model 86 Grok 4
Grok 4.3 ai_tech_seed ai_model 86 Grok 4
Grok Build ai_tech_seed ai_model 86 Grok 4
grok four ai_tech_seed ai_model 86 Grok 4
humanoid ai_tech_seed tech_term 86 人形机器人
humanoid robot ai_tech_seed tech_term 86 人形机器人
Kling ai_tech_seed ai_brand 86 可灵
Kling AI ai_tech_seed ai_brand 86 可灵
kling ai ai_tech_seed ai_brand 86 Kling
llama four ai_tech_seed ai_model 86 Llama 4
Llama Maverick ai_tech_seed ai_model 86 Llama 4
Llama Scout ai_tech_seed ai_model 86 Llama 4
Manus ai_tech_seed ai_brand 86 Manus
manus ai_tech_seed ai_brand 86 Manus
Manus AI ai_tech_seed ai_brand 86 Manus
MCP client ai_tech_seed ai_term 86 MCP client
mcp client ai_tech_seed ai_term 86 MCP client
Qwen Thinking ai_tech_seed ai_model 86 Qwen Thinking
qwen thinking ai_tech_seed ai_model 86 Qwen Thinking
RedNote ai_tech_seed tech_company 86 小红书
speech transcriber ai_tech_seed dev_tool 86 SpeechTranscriber
Speech Transcriber ai_tech_seed dev_tool 86 SpeechTranscriber
SpeechTranscriber ai_tech_seed dev_tool 86 SpeechTranscriber
structured output ai_tech_seed ai_term 86 structured output
structured outputs ai_tech_seed ai_term 86 structured output
sub agent ai_tech_seed ai_term 86 subagent
subagent ai_tech_seed ai_term 86 subagent
Swift Testing ai_tech_seed dev_tool 86 Swift Testing
swift testing ai_tech_seed dev_tool 86 Swift Testing
Testing framework ai_tech_seed dev_tool 86 Swift Testing
v zero ai_tech_seed dev_tool 86 v0
v0 ai_tech_seed dev_tool 86 v0
V0 ai_tech_seed dev_tool 86 v0
vercel v0 ai_tech_seed dev_tool 86 v0
Xiaohongshu ai_tech_seed tech_company 86 小红书
人形机器人 ren xing ji qi ren ai_tech_seed tech_term 86 人形机器人
可灵 ke ling ai_tech_seed ai_brand 86 可灵
子智能体 ai_tech_seed ai_term 86 subagent
小红书 xiao hong shu ai_tech_seed tech_company 86 小红书
模型上下文协议客户端 ai_tech_seed ai_term 86 MCP client
模型评测 ai_tech_seed ai_term 86 evals
结构化输出 ai_tech_seed ai_term 86 structured output
评测集 ai_tech_seed ai_term 86 evals
通义千问 thinking ai_tech_seed ai_model 86 Qwen Thinking
Adobe ai_tech_seed tech_company 85 Adobe Adobe ai_tech_seed tech_company 85 Adobe
adobe ai_tech_seed tech_company 85 Adobe adobe ai_tech_seed tech_company 85 Adobe
API ai_tech_seed tech_term 85 API API ai_tech_seed tech_term 85 API
@@ -340,8 +499,6 @@ ARM Holdings ai_tech_seed tech_company 85 Arm
ASML ai_tech_seed tech_company 85 ASML ASML ai_tech_seed tech_company 85 ASML
asml ai_tech_seed tech_company 85 ASML asml ai_tech_seed tech_company 85 ASML
autonomous driving ai_tech_seed tech_term 85 自动驾驶 autonomous driving ai_tech_seed tech_term 85 自动驾驶
claude opus ai_tech_seed ai_model 85 Opus
claude sonnet ai_tech_seed ai_model 85 Sonnet
CUDA ai_tech_seed tech_term 85 CUDA CUDA ai_tech_seed tech_term 85 CUDA
cuda ai_tech_seed tech_term 85 CUDA cuda ai_tech_seed tech_term 85 CUDA
DALL E ai_tech_seed ai_brand 85 DALL-E DALL E ai_tech_seed ai_brand 85 DALL-E
@@ -389,8 +546,6 @@ NextJS ai_tech_seed dev_tool 85 Next.js
Oppo ai_tech_seed tech_company 85 Oppo Oppo ai_tech_seed tech_company 85 Oppo
oppo ai_tech_seed tech_company 85 Oppo oppo ai_tech_seed tech_company 85 Oppo
OPPO ai_tech_seed tech_company 85 Oppo OPPO ai_tech_seed tech_company 85 Oppo
Opus ai_tech_seed ai_model 85 Opus
Opus 4 ai_tech_seed ai_model 85 Opus
Qualcomm ai_tech_seed tech_company 85 Qualcomm Qualcomm ai_tech_seed tech_company 85 Qualcomm
qualcomm ai_tech_seed tech_company 85 Qualcomm qualcomm ai_tech_seed tech_company 85 Qualcomm
quantization ai_tech_seed ai_term 85 量化 quantization ai_tech_seed ai_term 85 量化
@@ -405,8 +560,6 @@ SD ai_tech_seed ai_brand 85 Stable Diffusion
Shein ai_tech_seed tech_company 85 Shein Shein ai_tech_seed tech_company 85 Shein
shein ai_tech_seed tech_company 85 Shein shein ai_tech_seed tech_company 85 Shein
SHEIN ai_tech_seed tech_company 85 Shein SHEIN ai_tech_seed tech_company 85 Shein
Sonnet ai_tech_seed ai_model 85 Sonnet
Sonnet 4 ai_tech_seed ai_model 85 Sonnet
Sony ai_tech_seed tech_company 85 Sony Sony ai_tech_seed tech_company 85 Sony
sony ai_tech_seed tech_company 85 Sony sony ai_tech_seed tech_company 85 Sony
Spark ai_tech_seed ai_brand 85 星火 Spark ai_tech_seed ai_brand 85 星火
@@ -464,24 +617,158 @@ zeekr ai_tech_seed tech_company 85 Zeekr
讯飞星火 xing huo ai_tech_seed ai_brand 85 星火 讯飞星火 xing huo ai_tech_seed ai_brand 85 星火
量化 liang hua ai_tech_seed ai_term 85 量化 量化 liang hua ai_tech_seed ai_term 85 量化
高通 ai_tech_seed tech_company 85 Qualcomm 高通 ai_tech_seed tech_company 85 Qualcomm
AI governance ai_tech_seed tech_term 84 AI治理
AI治理 AI zhi li ai_tech_seed tech_term 84 AI治理
Amazon Kiro ai_tech_seed dev_tool 84 Kiro
Amazon Q ai_tech_seed dev_tool 84 Amazon Q
amazon q ai_tech_seed dev_tool 84 Amazon Q
App Intents ai_tech_seed dev_tool 84 App Intents
app intents ai_tech_seed dev_tool 84 App Intents
App Store Connect ai_tech_seed dev_tool 84 App Store Connect
app store connect ai_tech_seed dev_tool 84 App Store Connect
AppIntents ai_tech_seed dev_tool 84 App Intents
AppStoreConnect ai_tech_seed dev_tool 84 App Store Connect
asset inventory ai_tech_seed dev_tool 84 AssetInventory
AssetInventory ai_tech_seed dev_tool 84 AssetInventory
av audio engine ai_tech_seed dev_tool 84 AVAudioEngine
av audio session ai_tech_seed dev_tool 84 AVAudioSession
AVAudioEngine ai_tech_seed dev_tool 84 AVAudioEngine
AVAudioSession ai_tech_seed dev_tool 84 AVAudioSession
AWS Q ai_tech_seed dev_tool 84 Amazon Q
Bolt AI ai_tech_seed dev_tool 84 Bolt.new
bolt new ai_tech_seed dev_tool 84 Bolt.new
Bolt.new ai_tech_seed dev_tool 84 Bolt.new
cap cut ai_tech_seed tech_company 84 CapCut
CapCut ai_tech_seed tech_company 84 剪映
GLM 4.6 ai_tech_seed ai_model 84 GLM-4.6
GLM Coding Plan ai_tech_seed dev_tool 84 GLM Coding Plan
glm coding plan ai_tech_seed dev_tool 84 GLM Coding Plan
glm four six ai_tech_seed ai_model 84 GLM-4.6
GLM-4.6 ai_tech_seed ai_model 84 GLM-4.6
grok build ai_tech_seed dev_tool 84 Grok Build
Grok Build CLI ai_tech_seed dev_tool 84 Grok Build
Hailuo ai_tech_seed ai_brand 84 海螺AI
Hangzhou Six Little Dragons ai_tech_seed tech_term 84 杭州六小龙
Jimeng ai_tech_seed ai_brand 84 即梦
Kiro ai_tech_seed dev_tool 84 Kiro
kiro ai_tech_seed dev_tool 84 Kiro
lang graph ai_tech_seed ai_platform 84 LangGraph
LangGraph ai_tech_seed ai_platform 84 LangGraph
Llama 4 Maverick ai_tech_seed ai_model 84 Llama Maverick
llama maverick ai_tech_seed ai_model 84 Llama Maverick
local model ai_tech_seed ai_term 84 本地模型
Lovable ai_tech_seed dev_tool 84 Lovable
lovable ai_tech_seed dev_tool 84 Lovable
Lovable AI ai_tech_seed dev_tool 84 Lovable
MiniMax Video ai_tech_seed ai_brand 84 海螺AI
Mistral Large ai_tech_seed ai_model 84 Mistral Large
mistral large ai_tech_seed ai_model 84 Mistral Large
Mistral Large 3 ai_tech_seed ai_model 84 Mistral Large
Neon ai_tech_seed tech_company 84 Neon
neon database ai_tech_seed tech_company 84 Neon
Neon Postgres ai_tech_seed tech_company 84 Neon
on-device model ai_tech_seed ai_term 84 端侧模型
open hands ai_tech_seed dev_tool 84 OpenHands
open telemetry ai_tech_seed tech_term 84 OpenTelemetry
OpenHands ai_tech_seed dev_tool 84 OpenHands
OpenHands AI ai_tech_seed dev_tool 84 OpenHands
OpenTelemetry ai_tech_seed tech_term 84 OpenTelemetry
OTel ai_tech_seed tech_term 84 OpenTelemetry
Prisma ai_tech_seed dev_tool 84 Prisma
prisma orm ai_tech_seed dev_tool 84 Prisma
Prisma ORM ai_tech_seed dev_tool 84 Prisma
prompt cache ai_tech_seed ai_term 84 prompt caching
prompt caching ai_tech_seed ai_term 84 prompt caching
Q Developer ai_tech_seed dev_tool 84 Amazon Q
Replit Agent ai_tech_seed dev_tool 84 Replit Agent
replit agent ai_tech_seed dev_tool 84 Replit Agent
Replit AI ai_tech_seed dev_tool 84 Replit Agent
semantic retrieval ai_tech_seed ai_term 84 semantic search
semantic search ai_tech_seed ai_term 84 semantic search
shad cn ai_tech_seed dev_tool 84 shadcn/ui
shadcn ai_tech_seed dev_tool 84 shadcn/ui
shadcn ui ai_tech_seed dev_tool 84 shadcn/ui
shadcn/ui ai_tech_seed dev_tool 84 shadcn/ui
Speech AssetInventory ai_tech_seed dev_tool 84 AssetInventory
SQLite ai_tech_seed tech_term 84 SQLite
sqlite ai_tech_seed tech_term 84 SQLite
StackBlitz Bolt ai_tech_seed dev_tool 84 Bolt.new
Tailwind ai_tech_seed dev_tool 84 Tailwind CSS
Tailwind CSS ai_tech_seed dev_tool 84 Tailwind CSS
tailwind css ai_tech_seed dev_tool 84 Tailwind CSS
test flight ai_tech_seed dev_tool 84 TestFlight
TestFlight ai_tech_seed dev_tool 84 TestFlight
Trae ai_tech_seed dev_tool 84 Trae
trae ai_tech_seed dev_tool 84 Trae
Trae AI ai_tech_seed dev_tool 84 Trae
Trae CN ai_tech_seed dev_tool 84 Trae
vector retrieval ai_tech_seed ai_term 84 vector search
vector search ai_tech_seed ai_term 84 vector search
world model ai_tech_seed ai_term 84 世界模型
xiao hong shu ai_tech_seed tech_company 84 Xiaohongshu
Yuanbao ai_tech_seed ai_brand 84 腾讯元宝
世界模型 shi jie mo xing ai_tech_seed ai_term 84 世界模型
剪映 jian ying ai_tech_seed tech_company 84 剪映
即梦 ji meng ai_tech_seed ai_brand 84 即梦
即梦AI ji meng ai_tech_seed ai_brand 84 即梦
向量搜索 ai_tech_seed ai_term 84 vector search
提示词缓存 ai_tech_seed ai_term 84 prompt caching
智谱 coding plan ai_tech_seed dev_tool 84 GLM Coding Plan
本地模型 ben di mo xing ai_tech_seed ai_term 84 本地模型
杭州六小龙 hang zhou liu xiao long ai_tech_seed tech_term 84 杭州六小龙
海螺AI hai luo AI ai_tech_seed ai_brand 84 海螺AI
端侧模型 duan ce mo xing ai_tech_seed ai_term 84 端侧模型
腾讯元宝 teng xun yuan bao ai_tech_seed ai_brand 84 腾讯元宝
语义搜索 ai_tech_seed ai_term 84 semantic search
01.AI ai_tech_seed ai_brand 82 零一万物 01.AI ai_tech_seed ai_brand 82 零一万物
activity kit ai_tech_seed dev_tool 82 ActivityKit
ActivityKit ai_tech_seed dev_tool 82 ActivityKit
AI native ai_tech_seed ai_term 82 AI native AI native ai_tech_seed ai_term 82 AI native
AI video ai_tech_seed tech_term 82 AI视频
AI workflow ai_tech_seed ai_term 82 AI workflow
ai workflow ai_tech_seed ai_term 82 AI workflow
AI 工作流 ai_tech_seed ai_term 82 AI workflow
AI-native ai_tech_seed ai_term 82 AI native AI-native ai_tech_seed ai_term 82 AI native
Airbnb ai_tech_seed tech_company 82 Airbnb Airbnb ai_tech_seed tech_company 82 Airbnb
airbnb ai_tech_seed tech_company 82 Airbnb airbnb ai_tech_seed tech_company 82 Airbnb
AI原生 ai_tech_seed ai_term 82 AI native AI原生 ai_tech_seed ai_term 82 AI native
AI视频 AI shi pin ai_tech_seed tech_term 82 AI视频
Amazon Q Developer ai_tech_seed dev_tool 82 Q Developer
Apache Kafka ai_tech_seed tech_term 82 Kafka
AutoGen ai_tech_seed ai_platform 82 AutoGen
autogen ai_tech_seed ai_platform 82 AutoGen
bai chuan ai_tech_seed ai_brand 82 百川 bai chuan ai_tech_seed ai_brand 82 百川
Baichuan ai_tech_seed ai_brand 82 百川 Baichuan ai_tech_seed ai_brand 82 百川
baichuan ai_tech_seed ai_brand 82 Baichuan baichuan ai_tech_seed ai_brand 82 Baichuan
blockchain ai_tech_seed tech_term 82 区块链 blockchain ai_tech_seed tech_term 82 区块链
Bun ai_tech_seed dev_tool 82 Bun
bun js ai_tech_seed dev_tool 82 Bun
Bun runtime ai_tech_seed dev_tool 82 Bun
Cloudflare ai_tech_seed tech_company 82 Cloudflare Cloudflare ai_tech_seed tech_company 82 Cloudflare
cloudflare ai_tech_seed tech_company 82 Cloudflare cloudflare ai_tech_seed tech_company 82 Cloudflare
Codeium Windsurf ai_tech_seed dev_tool 82 Windsurf Codeium Windsurf ai_tech_seed dev_tool 82 Windsurf
Cognition ai_tech_seed ai_brand 82 Cognition
cognition ai ai_tech_seed ai_brand 82 Cognition
Cognition AI ai_tech_seed ai_brand 82 Cognition
Coinbase ai_tech_seed tech_company 82 Coinbase Coinbase ai_tech_seed tech_company 82 Coinbase
coinbase ai_tech_seed tech_company 82 Coinbase coinbase ai_tech_seed tech_company 82 Coinbase
context compression ai_tech_seed ai_term 82 上下文压缩
Create ML ai_tech_seed dev_tool 82 Create ML
create ml ai_tech_seed dev_tool 82 Create ML
CreateML ai_tech_seed dev_tool 82 Create ML
crew ai ai_tech_seed ai_platform 82 CrewAI
Crew AI ai_tech_seed ai_platform 82 CrewAI
CrewAI ai_tech_seed ai_platform 82 CrewAI
deep research ai_tech_seed ai_term 82 deep research deep research ai_tech_seed ai_term 82 deep research
distillation ai_tech_seed ai_term 82 distillation distillation ai_tech_seed ai_term 82 distillation
Drizzle ai_tech_seed dev_tool 82 Drizzle
drizzle orm ai_tech_seed dev_tool 82 Drizzle
Drizzle ORM ai_tech_seed dev_tool 82 Drizzle
embodied agent ai_tech_seed ai_term 82 具身智能体
embodied AI ai_tech_seed tech_term 82 具身智能 embodied AI ai_tech_seed tech_term 82 具身智能
end-to-end ai_tech_seed tech_term 82 端到端
fast api ai_tech_seed dev_tool 82 FastAPI
FastAPI ai_tech_seed dev_tool 82 FastAPI
full self driving ai_tech_seed tech_term 82 FSD full self driving ai_tech_seed tech_term 82 FSD
Geely ai_tech_seed tech_company 82 Geely Geely ai_tech_seed tech_company 82 Geely
geely ai_tech_seed tech_company 82 Geely geely ai_tech_seed tech_company 82 Geely
@@ -491,17 +778,35 @@ Golang ai_tech_seed dev_tool 82 Go
Groq ai_tech_seed ai_brand 82 Groq Groq ai_tech_seed ai_brand 82 Groq
groq ai_tech_seed ai_brand 82 Groq groq ai_tech_seed ai_brand 82 Groq
GROQ ai_tech_seed ai_brand 82 Groq GROQ ai_tech_seed ai_brand 82 Groq
hailuo ai ai_tech_seed ai_brand 82 Hailuo
horizon ai_tech_seed tech_company 82 Horizon Robotics horizon ai_tech_seed tech_company 82 Horizon Robotics
Horizon Robotics ai_tech_seed tech_company 82 Horizon Robotics Horizon Robotics ai_tech_seed tech_company 82 Horizon Robotics
humanoid robot ai_tech_seed tech_term 82 人形机器人 hybrid retrieval ai_tech_seed ai_term 82 hybrid search
hybrid search ai_tech_seed ai_term 82 hybrid search
IBM ai_tech_seed tech_company 82 IBM IBM ai_tech_seed tech_company 82 IBM
ibm ai_tech_seed tech_company 82 IBM ibm ai_tech_seed tech_company 82 IBM
image-to-video ai_tech_seed tech_term 82 图生视频
Jeff Bezos ai_tech_seed tech_leader 82 Jeff Bezos Jeff Bezos ai_tech_seed tech_leader 82 Jeff Bezos
jeff bezos ai_tech_seed tech_leader 82 Jeff Bezos jeff bezos ai_tech_seed tech_leader 82 Jeff Bezos
jimeng ai ai_tech_seed ai_brand 82 Jimeng
Kafka ai_tech_seed tech_term 82 Kafka
kafka ai_tech_seed tech_term 82 Kafka
ling yi wan wu ai_tech_seed ai_brand 82 零一万物 ling yi wan wu ai_tech_seed ai_brand 82 零一万物
Live Activities ai_tech_seed dev_tool 82 Live Activities
live activities ai_tech_seed dev_tool 82 Live Activities
llama index ai_tech_seed ai_platform 82 LlamaIndex
LlamaIndex ai_tech_seed ai_platform 82 LlamaIndex
LM Studio ai_tech_seed ai_platform 82 LM Studio
lm studio ai_tech_seed ai_platform 82 LM Studio
LMStudio ai_tech_seed ai_platform 82 LM Studio
MCP Inspector ai_tech_seed dev_tool 82 MCP Inspector
mcp inspector ai_tech_seed dev_tool 82 MCP Inspector
MCP 调试器 ai_tech_seed dev_tool 82 MCP Inspector
Microsoft AutoGen ai_tech_seed ai_platform 82 AutoGen
Mongo DB ai_tech_seed tech_company 82 MongoDB Mongo DB ai_tech_seed tech_company 82 MongoDB
MongoDB ai_tech_seed tech_company 82 MongoDB MongoDB ai_tech_seed tech_company 82 MongoDB
mongodb ai_tech_seed tech_company 82 MongoDB mongodb ai_tech_seed tech_company 82 MongoDB
new quality productive forces ai_tech_seed tech_term 82 新质生产力
Nintendo ai_tech_seed tech_company 82 Nintendo Nintendo ai_tech_seed tech_company 82 Nintendo
nintendo ai_tech_seed tech_company 82 Nintendo nintendo ai_tech_seed tech_company 82 Nintendo
node js ai_tech_seed dev_tool 82 Node.js node js ai_tech_seed dev_tool 82 Node.js
@@ -516,20 +821,34 @@ Ollama ai_tech_seed ai_platform 82 Ollama
ollama ai_tech_seed ai_platform 82 Ollama ollama ai_tech_seed ai_platform 82 Ollama
open router ai_tech_seed ai_platform 82 OpenRouter open router ai_tech_seed ai_platform 82 OpenRouter
Open Router ai_tech_seed ai_platform 82 OpenRouter Open Router ai_tech_seed ai_platform 82 OpenRouter
open web ui ai_tech_seed ai_platform 82 Open WebUI
Open WebUI ai_tech_seed ai_platform 82 Open WebUI
open weight ai_tech_seed ai_term 82 open weight open weight ai_tech_seed ai_term 82 open weight
open weights ai_tech_seed ai_term 82 open weight open weights ai_tech_seed ai_term 82 open weight
OpenRouter ai_tech_seed ai_platform 82 OpenRouter OpenRouter ai_tech_seed ai_platform 82 OpenRouter
OpenWebUI ai_tech_seed ai_platform 82 Open WebUI
Optimus ai_tech_seed tech_term 82 人形机器人 Optimus ai_tech_seed tech_term 82 人形机器人
Oracle ai_tech_seed tech_company 82 Oracle Oracle ai_tech_seed tech_company 82 Oracle
oracle ai_tech_seed tech_company 82 Oracle oracle ai_tech_seed tech_company 82 Oracle
otel ai_tech_seed tech_term 82 OTel
Palantir ai_tech_seed tech_company 82 Palantir Palantir ai_tech_seed tech_company 82 Palantir
palantir ai_tech_seed tech_company 82 Palantir palantir ai_tech_seed tech_company 82 Palantir
PayPal ai_tech_seed tech_company 82 PayPal PayPal ai_tech_seed tech_company 82 PayPal
paypal ai_tech_seed tech_company 82 PayPal paypal ai_tech_seed tech_company 82 PayPal
planet scale ai_tech_seed tech_company 82 PlanetScale
PlanetScale ai_tech_seed tech_company 82 PlanetScale
PlanetScale MySQL ai_tech_seed tech_company 82 PlanetScale
pre-training ai_tech_seed ai_term 82 pretraining pre-training ai_tech_seed ai_term 82 pretraining
pretraining ai_tech_seed ai_term 82 pretraining pretraining ai_tech_seed ai_term 82 pretraining
q developer ai_tech_seed dev_tool 82 Q Developer
React Server Components ai_tech_seed dev_tool 82 React Server Components
react server components ai_tech_seed dev_tool 82 React Server Components
red note ai_tech_seed tech_company 82 RedNote
Redis ai_tech_seed tech_company 82 Redis Redis ai_tech_seed tech_company 82 Redis
redis ai_tech_seed tech_company 82 Redis redis ai_tech_seed tech_company 82 Redis
rerank ai_tech_seed ai_term 82 reranker
reranker ai_tech_seed ai_term 82 reranker
RSC ai_tech_seed dev_tool 82 React Server Components
Rust ai_tech_seed dev_tool 82 Rust Rust ai_tech_seed dev_tool 82 Rust
rust ai_tech_seed dev_tool 82 Rust rust ai_tech_seed dev_tool 82 Rust
SaaS ai_tech_seed tech_term 82 SaaS SaaS ai_tech_seed tech_term 82 SaaS
@@ -540,8 +859,14 @@ SDK ai_tech_seed tech_term 82 SDK
sdk ai_tech_seed tech_term 82 SDK sdk ai_tech_seed tech_term 82 SDK
SenseTime ai_tech_seed tech_company 82 SenseTime SenseTime ai_tech_seed tech_company 82 SenseTime
sensetime ai_tech_seed tech_company 82 SenseTime sensetime ai_tech_seed tech_company 82 SenseTime
Sentry ai_tech_seed dev_tool 82 Sentry
sentry ai_tech_seed dev_tool 82 Sentry
SF Symbols ai_tech_seed dev_tool 82 SF Symbols
sf symbols ai_tech_seed dev_tool 82 SF Symbols
SFSymbols ai_tech_seed dev_tool 82 SF Symbols
Shopify ai_tech_seed tech_company 82 Shopify Shopify ai_tech_seed tech_company 82 Shopify
shopify ai_tech_seed tech_company 82 Shopify shopify ai_tech_seed tech_company 82 Shopify
SPM ai_tech_seed dev_tool 82 Swift Package Manager
Spotify ai_tech_seed tech_company 82 Spotify Spotify ai_tech_seed tech_company 82 Spotify
spotify ai_tech_seed tech_company 82 Spotify spotify ai_tech_seed tech_company 82 Spotify
step fun ai_tech_seed ai_brand 82 StepFun step fun ai_tech_seed ai_brand 82 StepFun
@@ -549,51 +874,85 @@ StepFun ai_tech_seed ai_brand 82 StepFun
streaming ai_tech_seed ai_term 82 流式 streaming ai_tech_seed ai_term 82 流式
Sundar Pichai ai_tech_seed tech_leader 82 Sundar Pichai Sundar Pichai ai_tech_seed tech_leader 82 Sundar Pichai
sundar pichai ai_tech_seed tech_leader 82 Sundar Pichai sundar pichai ai_tech_seed tech_leader 82 Sundar Pichai
Swift Package Manager ai_tech_seed dev_tool 82 Swift Package Manager
swift package manager ai_tech_seed dev_tool 82 Swift Package Manager
swift ui ai_tech_seed dev_tool 82 SwiftUI swift ui ai_tech_seed dev_tool 82 SwiftUI
Swift UI ai_tech_seed dev_tool 82 SwiftUI Swift UI ai_tech_seed dev_tool 82 SwiftUI
SwiftUI ai_tech_seed dev_tool 82 SwiftUI SwiftUI ai_tech_seed dev_tool 82 SwiftUI
text-to-image ai_tech_seed tech_term 82 文生图
text-to-video ai_tech_seed tech_term 82 文生视频
TRAE ai_tech_seed dev_tool 82 TRAE
vector database ai_tech_seed ai_term 82 vector database vector database ai_tech_seed ai_term 82 vector database
Vercel ai_tech_seed tech_company 82 Vercel Vercel ai_tech_seed tech_company 82 Vercel
vercel ai_tech_seed tech_company 82 Vercel vercel ai_tech_seed tech_company 82 Vercel
vision os ai_tech_seed dev_tool 82 visionOS
Vision Pro ai_tech_seed dev_tool 82 visionOS
visionOS ai_tech_seed dev_tool 82 visionOS
Vue ai_tech_seed dev_tool 82 Vue Vue ai_tech_seed dev_tool 82 Vue
vue ai_tech_seed dev_tool 82 Vue vue ai_tech_seed dev_tool 82 Vue
Vue.js ai_tech_seed dev_tool 82 Vue Vue.js ai_tech_seed dev_tool 82 Vue
Vue3 ai_tech_seed dev_tool 82 Vue Vue3 ai_tech_seed dev_tool 82 Vue
Weibo ai_tech_seed tech_company 82 Weibo Weibo ai_tech_seed tech_company 82 Weibo
weibo ai_tech_seed tech_company 82 Weibo weibo ai_tech_seed tech_company 82 Weibo
widget kit ai_tech_seed dev_tool 82 WidgetKit
WidgetKit ai_tech_seed dev_tool 82 WidgetKit
Windsurf ai_tech_seed dev_tool 82 Windsurf Windsurf ai_tech_seed dev_tool 82 Windsurf
windsurf ai_tech_seed dev_tool 82 Windsurf windsurf ai_tech_seed dev_tool 82 Windsurf
workflow orchestration ai_tech_seed ai_term 82 工作流编排
xcode gen ai_tech_seed dev_tool 82 XcodeGen
XcodeGen ai_tech_seed dev_tool 82 XcodeGen
Yi ai_tech_seed ai_brand 82 零一万物 Yi ai_tech_seed ai_brand 82 零一万物
人形机器人 ren xing ji qi ren ai_tech_seed tech_term 82 人形机器人 上下文压缩 shang xia wen ya suo ai_tech_seed ai_term 82 上下文压缩
任天堂 ai_tech_seed tech_company 82 Nintendo 任天堂 ai_tech_seed tech_company 82 Nintendo
余承东 yu cheng dong ai_tech_seed tech_leader 82 余承东 余承东 yu cheng dong ai_tech_seed tech_leader 82 余承东
元宝 yuan bao ai_tech_seed ai_brand 82 元宝
全自动驾驶 ai_tech_seed tech_term 82 FSD 全自动驾驶 ai_tech_seed tech_term 82 FSD
具身智能 ju shen zhi neng ai_tech_seed tech_term 82 具身智能 具身智能 ju shen zhi neng ai_tech_seed tech_term 82 具身智能
具身智能体 ju shen zhi neng ti ai_tech_seed ai_term 82 具身智能体
区块链 qu kuai lian ai_tech_seed tech_term 82 区块链 区块链 qu kuai lian ai_tech_seed tech_term 82 区块链
吉利 ai_tech_seed tech_company 82 Geely 吉利 ai_tech_seed tech_company 82 Geely
向量数据库 ai_tech_seed ai_term 82 vector database 向量数据库 ai_tech_seed ai_term 82 vector database
商汤 ai_tech_seed tech_company 82 SenseTime 商汤 ai_tech_seed tech_company 82 SenseTime
图生视频 tu sheng shi pin ai_tech_seed tech_term 82 图生视频
地平线 ai_tech_seed tech_company 82 Horizon Robotics 地平线 ai_tech_seed tech_company 82 Horizon Robotics
字节 Trae ai_tech_seed dev_tool 82 TRAE
工作流编排 gong zuo liu bian pai ai_tech_seed ai_term 82 工作流编排
开放权重 ai_tech_seed ai_term 82 open weight 开放权重 ai_tech_seed ai_term 82 open weight
微博 ai_tech_seed tech_company 82 Weibo 微博 ai_tech_seed tech_company 82 Weibo
文生图 wen sheng tu ai_tech_seed tech_term 82 文生图
文生视频 wen sheng shi pin ai_tech_seed tech_term 82 文生视频
新质生产力 xin zhi sheng chan li ai_tech_seed tech_term 82 新质生产力
流式 liu shi ai_tech_seed ai_term 82 流式 流式 liu shi ai_tech_seed ai_term 82 流式
深度研究 ai_tech_seed ai_term 82 deep research 深度研究 ai_tech_seed ai_term 82 deep research
混合搜索 ai_tech_seed ai_term 82 hybrid search
灵动岛实时活动 ai_tech_seed dev_tool 82 Live Activities
百川 ai_tech_seed ai_brand 82 百川 百川 ai_tech_seed ai_brand 82 百川
皮查伊 ai_tech_seed tech_leader 82 Sundar Pichai 皮查伊 ai_tech_seed tech_leader 82 Sundar Pichai
知识蒸馏 ai_tech_seed ai_term 82 distillation 知识蒸馏 ai_tech_seed ai_term 82 distillation
端到端 duan dao duan ai_tech_seed tech_term 82 端到端
纳德拉 ai_tech_seed tech_leader 82 Satya Nadella 纳德拉 ai_tech_seed tech_leader 82 Satya Nadella
蒸馏 ai_tech_seed ai_term 82 distillation 蒸馏 ai_tech_seed ai_term 82 distillation
贝索斯 ai_tech_seed tech_leader 82 Jeff Bezos 贝索斯 ai_tech_seed tech_leader 82 Jeff Bezos
重排序模型 ai_tech_seed ai_term 82 reranker
阶跃星辰 ai_tech_seed ai_brand 82 StepFun 阶跃星辰 ai_tech_seed ai_brand 82 StepFun
零一万物 ai_tech_seed ai_brand 82 零一万物 零一万物 ai_tech_seed ai_brand 82 零一万物
预训练 ai_tech_seed ai_term 82 pretraining 预训练 ai_tech_seed ai_term 82 pretraining
01 ai ai_tech_seed ai_brand 80 01.AI 01 ai ai_tech_seed ai_brand 80 01.AI
agent harness ai_tech_seed ai_term 80 LLM harness
agentic harness ai_tech_seed ai_term 80 agent harness
Astro ai_tech_seed dev_tool 80 Astro
astro js ai_tech_seed dev_tool 80 Astro
Atlassian ai_tech_seed tech_company 80 Atlassian Atlassian ai_tech_seed tech_company 80 Atlassian
atlassian ai_tech_seed tech_company 80 Atlassian atlassian ai_tech_seed tech_company 80 Atlassian
authentically human ai_tech_seed tech_term 80 活人感
Broadcom ai_tech_seed tech_company 80 Broadcom Broadcom ai_tech_seed tech_company 80 Broadcom
broadcom ai_tech_seed tech_company 80 Broadcom broadcom ai_tech_seed tech_company 80 Broadcom
Canva ai_tech_seed tech_company 80 Canva Canva ai_tech_seed tech_company 80 Canva
canva ai_tech_seed tech_company 80 Canva canva ai_tech_seed tech_company 80 Canva
Clerk ai_tech_seed dev_tool 80 Clerk
clerk auth ai_tech_seed dev_tool 80 Clerk
click house ai_tech_seed tech_term 80 ClickHouse
ClickHouse ai_tech_seed tech_term 80 ClickHouse
Codeium ai_tech_seed ai_brand 80 Codeium Codeium ai_tech_seed ai_brand 80 Codeium
codeium ai_tech_seed ai_brand 80 Codeium codeium ai_tech_seed ai_brand 80 Codeium
Cohere ai_tech_seed ai_brand 80 Cohere Cohere ai_tech_seed ai_brand 80 Cohere
@@ -601,11 +960,21 @@ cohere ai_tech_seed ai_brand 80 Cohere
computer use ai_tech_seed ai_term 80 computer use computer use ai_tech_seed ai_term 80 computer use
computer-use ai_tech_seed ai_term 80 computer use computer-use ai_tech_seed ai_term 80 computer use
Confluence ai_tech_seed tech_company 80 Atlassian Confluence ai_tech_seed tech_company 80 Atlassian
Convex ai_tech_seed tech_company 80 Convex
Convex database ai_tech_seed tech_company 80 Convex
convex dev ai_tech_seed tech_company 80 Convex
Deno ai_tech_seed dev_tool 80 Deno
deno ai_tech_seed dev_tool 80 Deno
Deno Deploy ai_tech_seed dev_tool 80 Deno
Discord ai_tech_seed tech_company 80 Discord Discord ai_tech_seed tech_company 80 Discord
discord ai_tech_seed tech_company 80 Discord discord ai_tech_seed tech_company 80 Discord
duck db ai_tech_seed tech_term 80 DuckDB
Duck DB ai_tech_seed tech_term 80 DuckDB
DuckDB ai_tech_seed tech_term 80 DuckDB
eleven labs ai_tech_seed ai_brand 80 ElevenLabs eleven labs ai_tech_seed ai_brand 80 ElevenLabs
Eleven Labs ai_tech_seed ai_brand 80 ElevenLabs Eleven Labs ai_tech_seed ai_brand 80 ElevenLabs
ElevenLabs ai_tech_seed ai_brand 80 ElevenLabs ElevenLabs ai_tech_seed ai_brand 80 ElevenLabs
emotional value ai_tech_seed tech_term 80 情绪价值
Firebase ai_tech_seed tech_company 80 Firebase Firebase ai_tech_seed tech_company 80 Firebase
firebase ai_tech_seed tech_company 80 Firebase firebase ai_tech_seed tech_company 80 Firebase
GitLab ai_tech_seed tech_company 80 GitLab GitLab ai_tech_seed tech_company 80 GitLab
@@ -615,16 +984,30 @@ Jira ai_tech_seed tech_company 80 Atlassian
lang chain ai_tech_seed ai_platform 80 LangChain lang chain ai_tech_seed ai_platform 80 LangChain
LangChain ai_tech_seed ai_platform 80 LangChain LangChain ai_tech_seed ai_platform 80 LangChain
Langchain ai_tech_seed ai_platform 80 LangChain Langchain ai_tech_seed ai_platform 80 LangChain
libSQL ai_tech_seed tech_company 80 Turso
LLM harness ai_tech_seed ai_term 80 LLM harness
llm harness ai_tech_seed ai_term 80 LLM harness
low altitude economy ai_tech_seed tech_term 80 低空经济 low altitude economy ai_tech_seed tech_term 80 低空经济
Megvii ai_tech_seed tech_company 80 Megvii Megvii ai_tech_seed tech_company 80 Megvii
megvii ai_tech_seed tech_company 80 Megvii megvii ai_tech_seed tech_company 80 Megvii
Metal ai_tech_seed dev_tool 80 Metal
metal ai_tech_seed dev_tool 80 Metal
Metal Performance Shaders ai_tech_seed dev_tool 80 Metal
Metaso ai_tech_seed ai_brand 80 秘塔AI
on device ai ai_tech_seed ai_term 80 on-device AI on device ai ai_tech_seed ai_term 80 on-device AI
on-device AI ai_tech_seed ai_term 80 on-device AI on-device AI ai_tech_seed ai_term 80 on-device AI
optimus ai_tech_seed tech_term 80 Optimus optimus ai_tech_seed tech_term 80 Optimus
Quark AI ai_tech_seed ai_brand 80 夸克AI
real person vibe ai_tech_seed tech_term 80 活人感
reality kit ai_tech_seed dev_tool 80 RealityKit
RealityKit ai_tech_seed dev_tool 80 RealityKit
Reddit ai_tech_seed tech_company 80 Reddit Reddit ai_tech_seed tech_company 80 Reddit
reddit ai_tech_seed tech_company 80 Reddit reddit ai_tech_seed tech_company 80 Reddit
Rivian ai_tech_seed tech_company 80 Rivian Rivian ai_tech_seed tech_company 80 Rivian
rivian ai_tech_seed tech_company 80 Rivian rivian ai_tech_seed tech_company 80 Rivian
Roo Code ai_tech_seed dev_tool 80 Roo Code
roo code ai_tech_seed dev_tool 80 Roo Code
RooCode ai_tech_seed dev_tool 80 Roo Code
Runway ai_tech_seed ai_brand 80 Runway Runway ai_tech_seed ai_brand 80 Runway
runway ai ai_tech_seed ai_brand 80 Runway runway ai ai_tech_seed ai_brand 80 Runway
Runway ML ai_tech_seed ai_brand 80 Runway Runway ML ai_tech_seed ai_brand 80 Runway
@@ -639,69 +1022,148 @@ Snowflake ai_tech_seed tech_company 80 Snowflake
snowflake ai_tech_seed tech_company 80 Snowflake snowflake ai_tech_seed tech_company 80 Snowflake
Supabase ai_tech_seed tech_company 80 Supabase Supabase ai_tech_seed tech_company 80 Supabase
supabase ai_tech_seed tech_company 80 Supabase supabase ai_tech_seed tech_company 80 Supabase
svelte kit ai_tech_seed dev_tool 80 SvelteKit
SvelteKit ai_tech_seed dev_tool 80 SvelteKit
swe agent ai_tech_seed ai_term 80 SWE-agent
SWE Agent ai_tech_seed ai_term 80 SWE-agent
swe bench ai_tech_seed ai_term 80 SWE-bench swe bench ai_tech_seed ai_term 80 SWE-bench
SWE bench ai_tech_seed ai_term 80 SWE-bench SWE bench ai_tech_seed ai_term 80 SWE-bench
SWE-agent ai_tech_seed ai_term 80 SWE-agent
SWE-bench ai_tech_seed ai_term 80 SWE-bench SWE-bench ai_tech_seed ai_term 80 SWE-bench
Terraform ai_tech_seed tech_company 80 Terraform Terraform ai_tech_seed tech_company 80 Terraform
terraform ai_tech_seed tech_company 80 Terraform terraform ai_tech_seed tech_company 80 Terraform
TPU ai_tech_seed tech_term 80 TPU TPU ai_tech_seed tech_term 80 TPU
tpu ai_tech_seed tech_term 80 TPU tpu ai_tech_seed tech_term 80 TPU
Turso ai_tech_seed tech_company 80 Turso
turso ai_tech_seed tech_company 80 Turso
vLLM ai_tech_seed ai_platform 80 vLLM vLLM ai_tech_seed ai_platform 80 vLLM
vllm ai_tech_seed ai_platform 80 vLLM vllm ai_tech_seed ai_platform 80 vLLM
VLLM ai_tech_seed ai_platform 80 vLLM VLLM ai_tech_seed ai_platform 80 vLLM
Zed ai_tech_seed dev_tool 80 Zed
Zed AI ai_tech_seed dev_tool 80 Zed
zed editor ai_tech_seed dev_tool 80 Zed
Zoom ai_tech_seed tech_company 80 Zoom Zoom ai_tech_seed tech_company 80 Zoom
zoom ai_tech_seed tech_company 80 Zoom zoom ai_tech_seed tech_company 80 Zoom
低空经济 di kong jing ji ai_tech_seed tech_term 80 低空经济 低空经济 di kong jing ji ai_tech_seed tech_term 80 低空经济
夸克AI kua ke AI ai_tech_seed ai_brand 80 夸克AI
小语言模型 ai_tech_seed ai_term 80 SLM 小语言模型 ai_tech_seed ai_term 80 SLM
情绪价值 qing xu jia zhi ai_tech_seed tech_term 80 情绪价值
擎天柱 ai_tech_seed tech_term 80 Optimus 擎天柱 ai_tech_seed tech_term 80 Optimus
旷视 ai_tech_seed tech_company 80 Megvii 旷视 ai_tech_seed tech_company 80 Megvii
智能体框架 ai_tech_seed ai_term 80 agent harness
活人感 huo ren gan ai_tech_seed tech_term 80 活人感
电脑使用 ai_tech_seed ai_term 80 computer use 电脑使用 ai_tech_seed ai_term 80 computer use
秘塔AI mi ta AI ai_tech_seed ai_brand 80 秘塔AI
端侧AI ai_tech_seed ai_term 80 on-device AI 端侧AI ai_tech_seed ai_term 80 on-device AI
anything llm ai_tech_seed ai_platform 78 AnythingLLM
Anything LLM ai_tech_seed ai_platform 78 AnythingLLM
AnythingLLM ai_tech_seed ai_platform 78 AnythingLLM
auth zero ai_tech_seed dev_tool 78 Auth0
Auth Zero ai_tech_seed dev_tool 78 Auth0
Auth0 ai_tech_seed dev_tool 78 Auth0
benchmark ai_tech_seed ai_term 78 benchmark benchmark ai_tech_seed ai_term 78 benchmark
Biome ai_tech_seed dev_tool 78 Biome
biome js ai_tech_seed dev_tool 78 Biome
Block ai_tech_seed fintech 78 Block Block ai_tech_seed fintech 78 Block
block ai_tech_seed fintech 78 Block block ai_tech_seed fintech 78 Block
Character AI ai_tech_seed ai_brand 78 Character AI Character AI ai_tech_seed ai_brand 78 Character AI
character.ai ai_tech_seed ai_brand 78 Character AI character.ai ai_tech_seed ai_brand 78 Character AI
Character.AI ai_tech_seed ai_brand 78 Character AI Character.AI ai_tech_seed ai_brand 78 Character AI
Clerk Auth ai_tech_seed dev_tool 78 Clerk Auth
Cline ai_tech_seed dev_tool 78 Cline Cline ai_tech_seed dev_tool 78 Cline
cline ai_tech_seed dev_tool 78 Cline cline ai_tech_seed dev_tool 78 Cline
Cline AI ai_tech_seed dev_tool 78 Cline Cline AI ai_tech_seed dev_tool 78 Cline
comfy ui ai_tech_seed ai_platform 78 ComfyUI
ComfyUI ai_tech_seed ai_platform 78 ComfyUI
Continue ai_tech_seed dev_tool 78 Continue
continue dev ai_tech_seed dev_tool 78 Continue
Continue.dev ai_tech_seed dev_tool 78 Continue
cyber reconciliation ai_tech_seed tech_term 78 赛博对账
Datadog ai_tech_seed tech_company 78 Datadog Datadog ai_tech_seed tech_company 78 Datadog
datadog ai_tech_seed tech_company 78 Datadog datadog ai_tech_seed tech_company 78 Datadog
digital avatar ai_tech_seed tech_term 78 数字分身
DSPy ai_tech_seed ai_platform 78 DSPy
dspy ai_tech_seed ai_platform 78 DSPy
DSPy AI ai_tech_seed ai_platform 78 DSPy
Elastic ai_tech_seed tech_company 78 Elastic Elastic ai_tech_seed tech_company 78 Elastic
elastic ai_tech_seed tech_company 78 Elastic elastic ai_tech_seed tech_company 78 Elastic
Elasticsearch ai_tech_seed tech_company 78 Elastic Elasticsearch ai_tech_seed tech_company 78 Elastic
Fly ai_tech_seed tech_company 78 Fly.io
fly io ai_tech_seed tech_company 78 Fly.io
Fly.io ai_tech_seed tech_company 78 Fly.io
go global ai_tech_seed tech_term 78 出海 go global ai_tech_seed tech_term 78 出海
Grafana ai_tech_seed dev_tool 78 Grafana
grafana ai_tech_seed dev_tool 78 Grafana
Great Wall ai_tech_seed tech_company 78 Great Wall Great Wall ai_tech_seed tech_company 78 Great Wall
great wall ai_tech_seed tech_company 78 Great Wall great wall ai_tech_seed tech_company 78 Great Wall
Hono ai_tech_seed dev_tool 78 Hono
hono js ai_tech_seed dev_tool 78 Hono
Hugging Face Spaces ai_tech_seed ai_platform 78 Hugging Face Spaces
hugging face spaces ai_tech_seed ai_platform 78 Hugging Face Spaces
Jupyter ai_tech_seed dev_tool 78 Jupyter Jupyter ai_tech_seed dev_tool 78 Jupyter
jupyter ai_tech_seed dev_tool 78 Jupyter jupyter ai_tech_seed dev_tool 78 Jupyter
Jupyter Notebook ai_tech_seed dev_tool 78 Jupyter Jupyter Notebook ai_tech_seed dev_tool 78 Jupyter
lib sql ai_tech_seed tech_term 78 libSQL
Linear ai_tech_seed tech_company 78 Linear Linear ai_tech_seed tech_company 78 Linear
linear ai_tech_seed dev_tool 78 Linear linear ai_tech_seed dev_tool 78 Linear
linear app ai_tech_seed tech_company 78 Linear linear app ai_tech_seed tech_company 78 Linear
Lucid ai_tech_seed tech_company 78 Lucid Lucid ai_tech_seed tech_company 78 Lucid
lucid motors ai_tech_seed tech_company 78 Lucid lucid motors ai_tech_seed tech_company 78 Lucid
Lucid Motors ai_tech_seed tech_company 78 Lucid Lucid Motors ai_tech_seed tech_company 78 Lucid
MCP HTTP ai_tech_seed ai_term 78 Streamable HTTP
memory bank ai_tech_seed ai_term 78 memory bank
metaso ai_tech_seed ai_brand 78 Metaso
metaverse ai_tech_seed tech_term 78 元宇宙 metaverse ai_tech_seed tech_term 78 元宇宙
MiniCPM ai_tech_seed ai_brand 78 面壁智能 MiniCPM ai_tech_seed ai_brand 78 面壁智能
Modal ai_tech_seed tech_company 78 Modal
modal labs ai_tech_seed tech_company 78 Modal
Nami AI ai_tech_seed ai_brand 78 纳米AI
Nix ai_tech_seed tech_term 78 Nix
nix ai_tech_seed tech_term 78 Nix
NixOS ai_tech_seed tech_term 78 Nix
Nuxt ai_tech_seed dev_tool 78 Nuxt
nuxt ai_tech_seed dev_tool 78 Nuxt
Nuxt.js ai_tech_seed dev_tool 78 Nuxt
Postman ai_tech_seed dev_tool 78 Postman Postman ai_tech_seed dev_tool 78 Postman
postman ai_tech_seed dev_tool 78 Postman postman ai_tech_seed dev_tool 78 Postman
Prometheus ai_tech_seed dev_tool 78 Prometheus
prometheus ai_tech_seed dev_tool 78 Prometheus
Quark ai_tech_seed ai_brand 78 Quark
quark ai ai_tech_seed ai_brand 78 Quark
RAG pipeline ai_tech_seed ai_term 78 RAG pipeline RAG pipeline ai_tech_seed ai_term 78 RAG pipeline
rag pipeline ai_tech_seed ai_term 78 RAG pipeline rag pipeline ai_tech_seed ai_term 78 RAG pipeline
Railway ai_tech_seed tech_company 78 Railway
railway app ai_tech_seed tech_company 78 Railway
React Server Actions ai_tech_seed dev_tool 78 Server Actions
reasoning effort ai_tech_seed ai_term 78 reasoning effort
Remix ai_tech_seed dev_tool 78 Remix
remix run ai_tech_seed dev_tool 78 Remix
remote work ai_tech_seed tech_term 78 远程办公 remote work ai_tech_seed tech_term 78 远程办公
Replicate ai_tech_seed ai_platform 78 Replicate Replicate ai_tech_seed ai_platform 78 Replicate
replicate ai_tech_seed ai_platform 78 Replicate replicate ai_tech_seed ai_platform 78 Replicate
Replit ai_tech_seed dev_tool 78 Replit Replit ai_tech_seed dev_tool 78 Replit
replit ai_tech_seed dev_tool 78 Replit replit ai_tech_seed dev_tool 78 Replit
Replit Agent ai_tech_seed dev_tool 78 Replit
robin hood ai_tech_seed fintech 78 Robinhood robin hood ai_tech_seed fintech 78 Robinhood
Robin Hood ai_tech_seed fintech 78 Robinhood Robin Hood ai_tech_seed fintech 78 Robinhood
Robinhood ai_tech_seed fintech 78 Robinhood Robinhood ai_tech_seed fintech 78 Robinhood
Server Actions ai_tech_seed dev_tool 78 Server Actions
server actions ai_tech_seed dev_tool 78 Server Actions
service now ai_tech_seed tech_company 78 ServiceNow service now ai_tech_seed tech_company 78 ServiceNow
ServiceNow ai_tech_seed tech_company 78 ServiceNow ServiceNow ai_tech_seed tech_company 78 ServiceNow
Spaces ai_tech_seed ai_platform 78 Hugging Face Spaces
Square ai_tech_seed fintech 78 Block Square ai_tech_seed fintech 78 Block
square ai_tech_seed fintech 78 Block square ai_tech_seed fintech 78 Block
Streamable HTTP ai_tech_seed ai_term 78 Streamable HTTP
streamable http ai_tech_seed ai_term 78 Streamable HTTP
Tailscale ai_tech_seed tech_company 78 Tailscale
tailscale ai_tech_seed tech_company 78 Tailscale
test time compute ai_tech_seed ai_term 78 test-time compute
test-time compute ai_tech_seed ai_term 78 test-time compute
tiangong ai_tech_seed ai_brand 78 天工
Tuist ai_tech_seed dev_tool 78 Tuist
tuist ai_tech_seed dev_tool 78 Tuist
turbo repo ai_tech_seed dev_tool 78 Turborepo
Turborepo ai_tech_seed dev_tool 78 Turborepo
Twilio ai_tech_seed tech_company 78 Twilio Twilio ai_tech_seed tech_company 78 Twilio
twilio ai_tech_seed tech_company 78 Twilio twilio ai_tech_seed tech_company 78 Twilio
Visa ai_tech_seed tech_company 78 Visa Visa ai_tech_seed tech_company 78 Visa
@@ -709,13 +1171,53 @@ visa ai_tech_seed tech_company 78 Visa
Web 3 ai_tech_seed tech_term 78 Web3 Web 3 ai_tech_seed tech_term 78 Web3
Web3 ai_tech_seed tech_term 78 Web3 Web3 ai_tech_seed tech_term 78 Web3
web3 ai_tech_seed tech_term 78 Web3 web3 ai_tech_seed tech_term 78 Web3
work os ai_tech_seed dev_tool 78 WorkOS
Work OS ai_tech_seed dev_tool 78 WorkOS
WorkOS ai_tech_seed dev_tool 78 WorkOS
元宇宙 yuan yu zhou ai_tech_seed tech_term 78 元宇宙 元宇宙 yuan yu zhou ai_tech_seed tech_term 78 元宇宙
全球化 chu hai ai_tech_seed tech_term 78 出海 全球化 chu hai ai_tech_seed tech_term 78 出海
出海 chu hai ai_tech_seed tech_term 78 出海 出海 chu hai ai_tech_seed tech_term 78 出海
基准测试 ai_tech_seed ai_term 78 benchmark 基准测试 ai_tech_seed ai_term 78 benchmark
天工 tian gong ai_tech_seed ai_brand 78 天工
夸克 ai_tech_seed ai_brand 78 Quark
推理强度 ai_tech_seed ai_term 78 reasoning effort
数字分身 shu zi fen shen ai_tech_seed tech_term 78 数字分身
昆仑万维天工 tian gong ai_tech_seed ai_brand 78 天工
测试时计算 ai_tech_seed ai_term 78 test-time compute
纳米AI na mi AI ai_tech_seed ai_brand 78 纳米AI
记忆库 ai_tech_seed ai_term 78 memory bank
赛博对账 sai bo dui zhang ai_tech_seed tech_term 78 赛博对账
远程办公 yuan cheng ban gong ai_tech_seed tech_term 78 远程办公 远程办公 yuan cheng ban gong ai_tech_seed tech_term 78 远程办公
长城汽车 ai_tech_seed tech_company 78 Great Wall 长城汽车 ai_tech_seed tech_company 78 Great Wall
面壁智能 mian bi zhi neng ai_tech_seed ai_brand 78 面壁智能 面壁智能 mian bi zhi neng ai_tech_seed ai_brand 78 面壁智能
cyber ai_tech_seed tech_term 76 赛博
Deepset Haystack ai_tech_seed ai_platform 76 Haystack
Haystack ai_tech_seed ai_platform 76 Haystack
haystack ai ai_tech_seed ai_platform 76 Haystack
Ktor ai_tech_seed dev_tool 76 Ktor
ktor ai_tech_seed dev_tool 76 Ktor
Ktor server ai_tech_seed dev_tool 76 Ktor
Modal Labs ai_tech_seed tech_company 76 Modal Labs
Nix OS ai_tech_seed tech_term 76 NixOS
nixos ai_tech_seed tech_term 76 NixOS
Nx ai_tech_seed dev_tool 76 Nx
nx monorepo ai_tech_seed dev_tool 76 Nx
pnpm ai_tech_seed dev_tool 76 pnpm
red panda ai_tech_seed tech_company 76 Redpanda
Redpanda ai_tech_seed tech_company 76 Redpanda
Render ai_tech_seed tech_company 76 Render
render.com ai_tech_seed tech_company 76 Render
SenseChat ai_tech_seed ai_brand 76 商量
Serverless Stack ai_tech_seed dev_tool 76 SST
SST ai_tech_seed dev_tool 76 SST
sst ai_tech_seed dev_tool 76 SST
stdio transport ai_tech_seed ai_term 76 stdio transport
web dev arena ai_tech_seed ai_term 76 WebDev Arena
WebDev Arena ai_tech_seed ai_term 76 WebDev Arena
WebDevArena ai_tech_seed ai_term 76 WebDev Arena
商量 shang liang ai_tech_seed ai_brand 76 商量
标准输入输出传输 ai_tech_seed ai_term 76 stdio transport
赛博 sai bo ai_tech_seed tech_term 76 赛博
Aider ai_tech_seed ai_brand 75 Aider Aider ai_tech_seed ai_brand 75 Aider
aider ai_tech_seed ai_brand 75 Aider aider ai_tech_seed ai_brand 75 Aider
Bitbucket ai_tech_seed tech_company 75 Bitbucket Bitbucket ai_tech_seed tech_company 75 Bitbucket
@@ -744,7 +1246,15 @@ warp terminal ai_tech_seed dev_tool 75 Warp
数字游民 shu zi you min ai_tech_seed tech_term 75 数字游民 数字游民 shu zi you min ai_tech_seed tech_term 75 数字游民
越狱 ai_tech_seed ai_term 75 jailbreak 越狱 ai_tech_seed ai_term 75 jailbreak
面壁 ai_tech_seed ai_model 75 MiniCPM 面壁 ai_tech_seed ai_model 75 MiniCPM
Focus AI ai_tech_seed ai_platform 72 Fooocus
Fooocus ai_tech_seed ai_platform 72 Fooocus
fooocus ai_tech_seed ai_platform 72 Fooocus
Labubu ai_tech_seed tech_term 72 拉布布
labubu ai_tech_seed tech_term 72 Labubu
lying flat ai_tech_seed tech_term 72 躺平 lying flat ai_tech_seed tech_term 72 躺平
side hustle ai_tech_seed tech_term 72 副业 side hustle ai_tech_seed tech_term 72 副业
village coffee ai_tech_seed tech_term 72 村咖
副业 fu ye ai_tech_seed tech_term 72 副业 副业 fu ye ai_tech_seed tech_term 72 副业
拉布布 la bu bu ai_tech_seed tech_term 72 拉布布
村咖 cun ka ai_tech_seed tech_term 72 村咖
躺平 tang ping ai_tech_seed tech_term 72 躺平 躺平 tang ping ai_tech_seed tech_term 72 躺平
1 word pinyin source category weight canonical
18 Open AI ai_tech_seed ai_brand 100 OpenAI
19 OpenAI ai_tech_seed ai_brand 100 OpenAI
20 深度求索 shen du qiu suo ai_tech_seed ai_brand 100 深度求索
21 ChatGPT 5 ai_tech_seed ai_model 98 GPT-5
22 gpt five ai_tech_seed ai_model 98 GPT-5
23 GPT five ai_tech_seed ai_model 98 GPT-5
24 gpt five five ai_tech_seed ai_model 98 GPT-5.5
25 GPT five point five ai_tech_seed ai_model 98 GPT-5.5
26 GPT-5 ai_tech_seed ai_model 98 GPT-5
27 GPT-5.5 ai_tech_seed ai_model 98 GPT-5.5
28 gpt-5.5 ai_tech_seed ai_model 98 GPT-5.5
29 NVDA ai_tech_seed tech_company 98 Nvidia
30 Nvidia ai_tech_seed tech_company 98 Nvidia
31 nvidia ai_tech_seed tech_company 98 Nvidia
32 英伟达 ai_tech_seed tech_company 98 Nvidia
33 Claude Code ai_tech_seed dev_tool 96 Claude Code
34 claude code ai_tech_seed dev_tool 96 Claude Code
35 ClaudeCode ai_tech_seed dev_tool 96 Claude Code
36 克劳德代码 ai_tech_seed dev_tool 96 Claude Code
37 Alibaba ai_tech_seed tech_company 95 Alibaba
38 alibaba ai_tech_seed tech_company 95 Alibaba
39 Alphabet ai_tech_seed tech_company 95 Google
48 byte dance ai_tech_seed tech_company 95 ByteDance
49 ByteDance ai_tech_seed tech_company 95 ByteDance
50 Bytedance ai_tech_seed tech_company 95 ByteDance
51 deepseek r one ai_tech_seed ai_model 95 DeepSeek R1
52 DeepSeek R1 ai_tech_seed ai_model 95 DeepSeek R1
53 DeepSeek-R1 ai_tech_seed ai_model 95 DeepSeek R1
54 Facebook ai_tech_seed tech_company 95 Meta
55 Gemini ai_tech_seed ai_brand 95 Gemini
56 gemini ai_tech_seed ai_brand 95 Gemini
115 通义千问 ai_tech_seed ai_brand 95 Qwen
116 阿里 ai_tech_seed tech_company 95 Alibaba
117 阿里巴巴 ai_tech_seed tech_company 95 Alibaba
118 claude opus ai_tech_seed ai_model 94 Claude Opus
119 claude sonnet ai_tech_seed ai_model 94 Claude Sonnet
120 Opus ai_tech_seed ai_model 94 Claude Opus
121 Opus 4 ai_tech_seed ai_model 94 Claude Opus
122 Opus 4.7 ai_tech_seed ai_model 94 Claude Opus
123 Sonnet ai_tech_seed ai_model 94 Claude Sonnet
124 Sonnet 4 ai_tech_seed ai_model 94 Claude Sonnet
125 Sonnet 4.6 ai_tech_seed ai_model 94 Claude Sonnet
126 agent ai_tech_seed ai_term 92 智能体
127 Agent ai_tech_seed ai_term 92 智能体
128 Agent Mode ai_tech_seed ai_term 92 Agent Mode
129 agent mode ai_tech_seed ai_term 92 Agent Mode
130 AI agent ai_tech_seed ai_term 92 智能体
131 Amazon Web Services ai_tech_seed tech_company 92 AWS
132 aws ai_tech_seed tech_company 92 AWS
134 baidu ai_tech_seed tech_company 92 Baidu
135 CATL ai_tech_seed tech_company 92 CATL
136 catl ai_tech_seed tech_company 92 CATL
137 Codex ai_tech_seed dev_tool 92 OpenAI Codex
138 codex cli ai_tech_seed dev_tool 92 OpenAI Codex
139 Composer ai_tech_seed dev_tool 92 Composer
140 Composer 2 ai_tech_seed dev_tool 92 Composer
141 Composer 2.5 ai_tech_seed dev_tool 92 Composer
142 context engineering ai_tech_seed ai_term 92 context engineering
143 cursor composer ai_tech_seed dev_tool 92 Composer
144 Cursor Composer ai_tech_seed dev_tool 92 Cursor Composer
145 deepseek r1 ai_tech_seed ai_model 92 DeepSeek-R1
146 DeepSeek R1 deepseek v four ai_tech_seed ai_model 92 DeepSeek-R1 DeepSeek V4
147 DeepSeek-R1 deepseek v three ai_tech_seed ai_model 92 DeepSeek-R1 DeepSeek V3
148 DeepSeek V3 ai_tech_seed ai_model 92 DeepSeek V3
149 DeepSeek V4 ai_tech_seed ai_model 92 DeepSeek V4
150 DeepSeek V4 Pro ai_tech_seed ai_model 92 DeepSeek V4
151 DeepSeek-V3 ai_tech_seed ai_model 92 DeepSeek V3
152 DeepSeek-V4 ai_tech_seed ai_model 92 DeepSeek V4
153 dictation transcriber ai_tech_seed dev_tool 92 DictationTranscriber
154 Dictation Transcriber ai_tech_seed dev_tool 92 DictationTranscriber
155 DictationTranscriber ai_tech_seed dev_tool 92 DictationTranscriber
156 Elon Musk ai_tech_seed tech_leader 92 Elon Musk
157 elon musk ai_tech_seed tech_leader 92 Elon Musk
158 Gemini 3 Pro ai_tech_seed ai_model 92 Gemini 3 Pro
159 Gemini Pro ai_tech_seed ai_model 92 Gemini 3 Pro
160 gemini three pro ai_tech_seed ai_model 92 Gemini 3 Pro
161 GitHub ai_tech_seed tech_company 92 GitHub
162 github ai_tech_seed tech_company 92 GitHub
163 gpt four o ai_tech_seed ai_model 92 GPT-4o
164 GPT four oh ai_tech_seed ai_model 92 GPT-4o
165 large language model ai_tech_seed ai_term 92 LLM
166 large model ai_tech_seed ai_term 92 大模型
167 LLM ai_tech_seed ai_term 92 LLM
169 MCP ai_tech_seed ai_term 92 MCP
170 MCP server ai_tech_seed ai_term 92 MCP
171 model context protocol ai_tech_seed ai_term 92 MCP
172 OpenAI Codex ai_tech_seed dev_tool 92 OpenAI Codex
173 openai codex ai_tech_seed dev_tool 92 OpenAI Codex
174 prompt ai_tech_seed ai_term 92 提示词
175 Prompt ai_tech_seed ai_term 92 提示词
176 R1 ai_tech_seed ai_model 92 DeepSeek-R1
177 speech analyzer ai_tech_seed dev_tool 92 SpeechAnalyzer
178 Speech Analyzer ai_tech_seed dev_tool 92 SpeechAnalyzer
179 SpeechAnalyzer ai_tech_seed dev_tool 92 SpeechAnalyzer
180 tik tok ai_tech_seed tech_company 92 TikTok
181 Tik Tok ai_tech_seed tech_company 92 TikTok
182 TikTok ai_tech_seed tech_company 92 TikTok
183 we chat ai_tech_seed tech_company 92 WeChat
184 WeChat ai_tech_seed tech_company 92 WeChat
185 上下文工程 ai_tech_seed ai_term 92 context engineering
186 代理模式 ai_tech_seed ai_term 92 Agent Mode
187 大模型 da mo xing ai_tech_seed ai_term 92 大模型
188 大语言模型 ai_tech_seed ai_term 92 LLM
189 宁德时代 ai_tech_seed tech_company 92 CATL
191 抖音海外 ai_tech_seed tech_company 92 TikTok
192 提示词 ti shi ci ai_tech_seed ai_term 92 提示词
193 智能体 zhi neng ti ai_tech_seed ai_term 92 智能体
194 智能体模式 ai_tech_seed ai_term 92 Agent Mode
195 百度 ai_tech_seed tech_company 92 Baidu
196 马斯克 ai_tech_seed tech_leader 92 Elon Musk
197 agentic ai_tech_seed ai_term 90 agent
198 AGI ai_tech_seed ai_term 90 AGI
199 agi ai_tech_seed ai_term 90 AGI
200 alphabet ai_tech_seed tech_company 90 Alphabet
201 Codex CLI ai_tech_seed dev_tool 90 Codex CLI
202 Copilot ai_tech_seed ai_brand 90 Copilot
203 copilot ai_tech_seed ai_brand 90 Copilot
204 Cursor ai_tech_seed dev_tool 90 Cursor
205 cursor ai ai_tech_seed dev_tool 90 Cursor
206 Cursor AI ai_tech_seed dev_tool 90 Cursor
207 custom language model data ai_tech_seed dev_tool 90 SFCustomLanguageModelData
208 deepseek v3 ai_tech_seed ai_model 90 DeepSeek-V3
DeepSeek V3 ai_tech_seed ai_model 90 DeepSeek-V3
DeepSeek-V3 ai_tech_seed ai_model 90 DeepSeek-V3
209 DJI ai_tech_seed tech_company 90 DJI
210 dji ai_tech_seed tech_company 90 DJI
211 dou yin ai_tech_seed tech_company 90 Douyin
218 Ernie ai_tech_seed ai_brand 90 文心一言
219 fine tuning ai_tech_seed ai_term 90 微调
220 fine-tuning ai_tech_seed ai_term 90 微调
221 Gemini 2.5 Pro ai_tech_seed ai_model 90 Gemini 2.5 Pro
222 gemini two five pro ai_tech_seed ai_model 90 Gemini 2.5 Pro
223 GitHub Copilot ai_tech_seed ai_brand 90 Copilot
224 GLM ai_tech_seed ai_brand 90 GLM
225 glm ai_tech_seed ai_brand 90 GLM
229 jensen huang ai_tech_seed tech_leader 90 Jensen Huang
230 k8s ai_tech_seed dev_tool 90 Kubernetes
231 K8s ai_tech_seed dev_tool 90 Kubernetes
232 kimi k two ai_tech_seed ai_model 90 Kimi K2
233 Kimi K2 ai_tech_seed ai_model 90 Kimi K2
234 Kimi K2.5 ai_tech_seed ai_model 90 Kimi K2
235 Kimi K2.6 ai_tech_seed ai_model 90 Kimi K2
236 Kubernetes ai_tech_seed dev_tool 90 Kubernetes
237 kubernetes ai_tech_seed dev_tool 90 Kubernetes
238 Lei Jun ai_tech_seed tech_leader 90 雷军
246 Llama 4 ai_tech_seed ai_model 90 Llama
247 LoRA ai_tech_seed ai_term 90 LoRA
248 lora ai_tech_seed ai_term 90 LoRA
249 mcp server ai_tech_seed ai_term 90 MCP server
250 Microsoft Copilot ai_tech_seed ai_brand 90 Copilot
251 multi agent ai_tech_seed ai_term 90 multi-agent
252 multi-agent ai_tech_seed ai_term 90 multi-agent
253 NIO ai_tech_seed tech_company 90 NIO
254 nio ai_tech_seed tech_company 90 NIO
255 prompt engineering ai_tech_seed ai_term 90 prompt
256 Qwen Coder ai_tech_seed ai_model 90 Qwen Coder
257 qwen coder ai_tech_seed ai_model 90 Qwen Coder
258 SF Custom Language Model Data ai_tech_seed dev_tool 90 SFCustomLanguageModelData
259 SFCustomLanguageModelData ai_tech_seed dev_tool 90 SFCustomLanguageModelData
260 Steve Jobs ai_tech_seed tech_leader 90 乔布斯
261 steve jobs ai_tech_seed tech_leader 90 Steve Jobs
262 tool calling ai_tech_seed ai_term 90 tool calling
263 tool-calling ai_tech_seed ai_term 90 tool calling
264 TSMC ai_tech_seed tech_company 90 TSMC
265 tsmc ai_tech_seed tech_company 90 TSMC
266 X Peng ai_tech_seed tech_company 90 XPeng
269 Zhipu ai_tech_seed ai_brand 90 GLM
270 乔布斯 qiao bu si ai_tech_seed tech_leader 90 乔布斯
271 低秩适配 ai_tech_seed ai_term 90 LoRA
272 千问 coder ai_tech_seed ai_model 90 Qwen Coder
273 台积电 ai_tech_seed tech_company 90 TSMC
274 多智能体 ai_tech_seed ai_term 90 multi-agent
275 大疆 ai_tech_seed tech_company 90 DJI
276 小鹏汽车 ai_tech_seed tech_company 90 XPeng
277 嵌入 ai_tech_seed ai_term 90 embedding
278 工具调用 ai_tech_seed ai_term 90 tool calling
279 微调 wei tiao ai_tech_seed ai_term 90 微调
280 抖音 ai_tech_seed tech_company 90 Douyin
281 文心一言 wen xin yi yan ai_tech_seed ai_brand 90 文心一言
282 智谱 ai_tech_seed ai_brand 90 GLM
283 月之暗面 yue zhi an mian ai_tech_seed ai_brand 90 月之暗面
284 模型上下文协议服务器 ai_tech_seed ai_term 90 MCP server
285 理想 ai_tech_seed tech_company 90 Li Auto
286 理想汽车 ai_tech_seed tech_company 90 Li Auto
287 蔚来 ai_tech_seed tech_company 90 NIO
288 豆包 dou bao ai_tech_seed ai_brand 90 豆包
289 通义千问 coder ai_tech_seed ai_model 90 Qwen Coder
290 通用人工智能 ai_tech_seed ai_term 90 AGI
291 雷军 lei jun ai_tech_seed tech_leader 90 雷军
292 黄仁勋 ai_tech_seed tech_leader 90 Jensen Huang
304 B站 ai_tech_seed tech_company 88 Bilibili
305 chain of thought ai_tech_seed ai_term 88 chain of thought
306 chain-of-thought ai_tech_seed ai_term 88 chain of thought
307 Cognition Devin ai_tech_seed dev_tool 88 Devin
308 context window ai_tech_seed ai_term 88 上下文窗口
309 Core ML ai_tech_seed dev_tool 88 Core ML
310 core ml ai_tech_seed dev_tool 88 Core ML
311 CoreML ai_tech_seed dev_tool 88 Core ML
312 deep mind ai_tech_seed ai_brand 88 DeepMind
313 DeepMind ai_tech_seed ai_brand 88 DeepMind
314 Devin ai_tech_seed dev_tool 88 Devin
315 devin ai_tech_seed dev_tool 88 Devin
316 Docker ai_tech_seed tech_company 88 Docker
317 docker ai_tech_seed tech_company 88 Docker
318 dou bao ai_tech_seed ai_brand 88 Doubao
319 ernie ai_tech_seed ai_brand 88 ERNIE
320 function calling ai_tech_seed ai_term 88 function calling
321 Gemini 2.5 Flash ai_tech_seed ai_model 88 Gemini Flash
322 Gemini 3 Flash ai_tech_seed ai_model 88 Gemini Flash
323 Gemini CLI ai_tech_seed dev_tool 88 Gemini CLI
324 gemini cli ai_tech_seed dev_tool 88 Gemini CLI
325 Gemini Flash ai_tech_seed ai_model 88 Gemini Flash
326 gemini flash ai_tech_seed ai_model 88 Gemini Flash
327 gen ai ai_tech_seed ai_term 88 GenAI
328 GenAI ai_tech_seed ai_term 88 GenAI
329 github copilot ai_tech_seed ai_brand 88 GitHub Copilot
340 JD ai_tech_seed tech_company 88 JD.com
341 JD.com ai_tech_seed tech_company 88 JD.com
342 jd.com ai_tech_seed tech_company 88 JD.com
343 Kimi Code ai_tech_seed dev_tool 88 Kimi Code
344 kimi code ai_tech_seed dev_tool 88 Kimi Code
345 Lenovo ai_tech_seed tech_company 88 Lenovo
346 lenovo ai_tech_seed tech_company 88 Lenovo
347 Meituan ai_tech_seed tech_company 88 Meituan
352 mixture of experts ai_tech_seed ai_term 88 MoE
353 MoE ai_tech_seed ai_term 88 MoE
354 moe ai_tech_seed ai_term 88 MoE
355 Moonshot Kimi Code ai_tech_seed dev_tool 88 Kimi Code
356 multimodal ai_tech_seed ai_term 88 多模态
357 o1 ai_tech_seed ai_model 88 o1
358 O1 ai_tech_seed ai_model 88 o1
359 o3 ai_tech_seed ai_model 88 o3
360 O3 ai_tech_seed ai_model 88 o3
361 open code ai_tech_seed dev_tool 88 OpenCode
362 openai o1 ai_tech_seed ai_model 88 o1
363 openai o3 ai_tech_seed ai_model 88 o3
364 OpenCode ai_tech_seed dev_tool 88 OpenCode
365 opencode ai_tech_seed dev_tool 88 OpenCode
366 OpenCode AI ai_tech_seed dev_tool 88 OpenCode
367 PDD ai_tech_seed tech_company 88 Pinduoduo
368 Perplexity ai_tech_seed ai_brand 88 Perplexity
369 perplexity ai ai_tech_seed ai_brand 88 Perplexity
370 Perplexity AI ai_tech_seed ai_brand 88 Perplexity
371 Pinduoduo ai_tech_seed tech_company 88 Pinduoduo
372 pinduoduo ai_tech_seed tech_company 88 Pinduoduo
373 postgre sql ai_tech_seed tech_term 88 Postgres
374 Postgres ai_tech_seed tech_term 88 Postgres
375 postgres ai_tech_seed tech_term 88 Postgres
376 PostgreSQL ai_tech_seed tech_term 88 Postgres
377 postgresql ai_tech_seed tech_term 88 PostgreSQL
378 Py Torch ai_tech_seed dev_tool 88 PyTorch
379 Python ai_tech_seed dev_tool 88 Python
380 python ai_tech_seed dev_tool 88 Python
381 PyTorch ai_tech_seed dev_tool 88 PyTorch
382 pytorch ai_tech_seed dev_tool 88 PyTorch
383 Qwen Max ai_tech_seed ai_model 88 Qwen Max
384 qwen max ai_tech_seed ai_model 88 Qwen Max
385 Qwen3 Max ai_tech_seed ai_model 88 Qwen Max
386 reasoning ai_tech_seed ai_term 88 推理
387 reasoning model ai_tech_seed ai_term 88 reasoning model
388 Sam Altman ai_tech_seed tech_leader 88 Sam Altman
389 sam altman ai_tech_seed tech_leader 88 Sam Altman
390 Samsung ai_tech_seed tech_company 88 Samsung
391 samsung ai_tech_seed tech_company 88 Samsung
392 SF Speech Language Model ai_tech_seed dev_tool 88 SFSpeechLanguageModel
393 SFSpeechLanguageModel ai_tech_seed dev_tool 88 SFSpeechLanguageModel
394 SMIC ai_tech_seed tech_company 88 SMIC
395 smic ai_tech_seed tech_company 88 SMIC
396 Sora ai_tech_seed ai_brand 88 Sora
397 sora ai ai_tech_seed ai_brand 88 Sora
398 Sora AI ai_tech_seed ai_brand 88 Sora
399 speech language model ai_tech_seed dev_tool 88 SFSpeechLanguageModel
400 Stripe ai_tech_seed tech_company 88 Stripe
401 stripe ai_tech_seed tech_company 88 Stripe
402 swift data ai_tech_seed dev_tool 88 SwiftData
403 SwiftData ai_tech_seed dev_tool 88 SwiftData
404 Taobao ai_tech_seed tech_company 88 Taobao
405 taobao ai_tech_seed tech_company 88 Taobao
406 thinking model ai_tech_seed ai_term 88 reasoning model
407 tool calling tool use ai_tech_seed ai_term 88 function calling tool use
408 tool-use ai_tech_seed ai_term 88 tool use
409 Visual Studio Code ai_tech_seed dev_tool 88 VS Code
410 VS Code ai_tech_seed dev_tool 88 VS Code
411 vs code ai_tech_seed dev_tool 88 VS Code
422 哔哩哔哩 ai_tech_seed tech_company 88 Bilibili
423 多模态 duo mo tai ai_tech_seed ai_term 88 多模态
424 山姆奥特曼 ai_tech_seed tech_leader 88 Sam Altman
425 工具调用 工具使用 ai_tech_seed ai_term 88 function calling tool use
426 幻觉 huan jue ai_tech_seed ai_term 88 幻觉
427 思维链 ai_tech_seed ai_term 88 chain of thought
428 拼多多 ai_tech_seed tech_company 88 Pinduoduo
437 联想 ai_tech_seed tech_company 88 Lenovo
438 英特尔 ai_tech_seed tech_company 88 Intel
439 蚂蚁集团 ai_tech_seed tech_company 88 Ant Group
440 通义千问 Max ai_tech_seed ai_model 88 Qwen Max
441 App Group ai_tech_seed dev_tool 86 App Group
442 app group ai_tech_seed dev_tool 86 App Group
443 App Groups ai_tech_seed dev_tool 86 App Group
444 evals ai_tech_seed ai_term 86 evals
445 evaluation ai_tech_seed ai_term 86 evals
446 GLM 5 ai_tech_seed ai_model 86 GLM-5
447 glm five ai_tech_seed ai_model 86 GLM-5
448 Grok 4 ai_tech_seed ai_model 86 Grok 4
449 Grok 4.3 ai_tech_seed ai_model 86 Grok 4
450 Grok Build ai_tech_seed ai_model 86 Grok 4
451 grok four ai_tech_seed ai_model 86 Grok 4
452 humanoid ai_tech_seed tech_term 86 人形机器人
453 humanoid robot ai_tech_seed tech_term 86 人形机器人
454 Kling ai_tech_seed ai_brand 86 可灵
455 Kling AI ai_tech_seed ai_brand 86 可灵
456 kling ai ai_tech_seed ai_brand 86 Kling
457 llama four ai_tech_seed ai_model 86 Llama 4
458 Llama Maverick ai_tech_seed ai_model 86 Llama 4
459 Llama Scout ai_tech_seed ai_model 86 Llama 4
460 Manus ai_tech_seed ai_brand 86 Manus
461 manus ai_tech_seed ai_brand 86 Manus
462 Manus AI ai_tech_seed ai_brand 86 Manus
463 MCP client ai_tech_seed ai_term 86 MCP client
464 mcp client ai_tech_seed ai_term 86 MCP client
465 Qwen Thinking ai_tech_seed ai_model 86 Qwen Thinking
466 qwen thinking ai_tech_seed ai_model 86 Qwen Thinking
467 RedNote ai_tech_seed tech_company 86 小红书
468 speech transcriber ai_tech_seed dev_tool 86 SpeechTranscriber
469 Speech Transcriber ai_tech_seed dev_tool 86 SpeechTranscriber
470 SpeechTranscriber ai_tech_seed dev_tool 86 SpeechTranscriber
471 structured output ai_tech_seed ai_term 86 structured output
472 structured outputs ai_tech_seed ai_term 86 structured output
473 sub agent ai_tech_seed ai_term 86 subagent
474 subagent ai_tech_seed ai_term 86 subagent
475 Swift Testing ai_tech_seed dev_tool 86 Swift Testing
476 swift testing ai_tech_seed dev_tool 86 Swift Testing
477 Testing framework ai_tech_seed dev_tool 86 Swift Testing
478 v zero ai_tech_seed dev_tool 86 v0
479 v0 ai_tech_seed dev_tool 86 v0
480 V0 ai_tech_seed dev_tool 86 v0
481 vercel v0 ai_tech_seed dev_tool 86 v0
482 Xiaohongshu ai_tech_seed tech_company 86 小红书
483 人形机器人 ren xing ji qi ren ai_tech_seed tech_term 86 人形机器人
484 可灵 ke ling ai_tech_seed ai_brand 86 可灵
485 子智能体 ai_tech_seed ai_term 86 subagent
486 小红书 xiao hong shu ai_tech_seed tech_company 86 小红书
487 模型上下文协议客户端 ai_tech_seed ai_term 86 MCP client
488 模型评测 ai_tech_seed ai_term 86 evals
489 结构化输出 ai_tech_seed ai_term 86 structured output
490 评测集 ai_tech_seed ai_term 86 evals
491 通义千问 thinking ai_tech_seed ai_model 86 Qwen Thinking
492 Adobe ai_tech_seed tech_company 85 Adobe
493 adobe ai_tech_seed tech_company 85 Adobe
494 API ai_tech_seed tech_term 85 API
499 ASML ai_tech_seed tech_company 85 ASML
500 asml ai_tech_seed tech_company 85 ASML
501 autonomous driving ai_tech_seed tech_term 85 自动驾驶
claude opus ai_tech_seed ai_model 85 Opus
claude sonnet ai_tech_seed ai_model 85 Sonnet
502 CUDA ai_tech_seed tech_term 85 CUDA
503 cuda ai_tech_seed tech_term 85 CUDA
504 DALL E ai_tech_seed ai_brand 85 DALL-E
546 Oppo ai_tech_seed tech_company 85 Oppo
547 oppo ai_tech_seed tech_company 85 Oppo
548 OPPO ai_tech_seed tech_company 85 Oppo
Opus ai_tech_seed ai_model 85 Opus
Opus 4 ai_tech_seed ai_model 85 Opus
549 Qualcomm ai_tech_seed tech_company 85 Qualcomm
550 qualcomm ai_tech_seed tech_company 85 Qualcomm
551 quantization ai_tech_seed ai_term 85 量化
560 Shein ai_tech_seed tech_company 85 Shein
561 shein ai_tech_seed tech_company 85 Shein
562 SHEIN ai_tech_seed tech_company 85 Shein
Sonnet ai_tech_seed ai_model 85 Sonnet
Sonnet 4 ai_tech_seed ai_model 85 Sonnet
563 Sony ai_tech_seed tech_company 85 Sony
564 sony ai_tech_seed tech_company 85 Sony
565 Spark ai_tech_seed ai_brand 85 星火
617 讯飞星火 xing huo ai_tech_seed ai_brand 85 星火
618 量化 liang hua ai_tech_seed ai_term 85 量化
619 高通 ai_tech_seed tech_company 85 Qualcomm
620 AI governance ai_tech_seed tech_term 84 AI治理
621 AI治理 AI zhi li ai_tech_seed tech_term 84 AI治理
622 Amazon Kiro ai_tech_seed dev_tool 84 Kiro
623 Amazon Q ai_tech_seed dev_tool 84 Amazon Q
624 amazon q ai_tech_seed dev_tool 84 Amazon Q
625 App Intents ai_tech_seed dev_tool 84 App Intents
626 app intents ai_tech_seed dev_tool 84 App Intents
627 App Store Connect ai_tech_seed dev_tool 84 App Store Connect
628 app store connect ai_tech_seed dev_tool 84 App Store Connect
629 AppIntents ai_tech_seed dev_tool 84 App Intents
630 AppStoreConnect ai_tech_seed dev_tool 84 App Store Connect
631 asset inventory ai_tech_seed dev_tool 84 AssetInventory
632 AssetInventory ai_tech_seed dev_tool 84 AssetInventory
633 av audio engine ai_tech_seed dev_tool 84 AVAudioEngine
634 av audio session ai_tech_seed dev_tool 84 AVAudioSession
635 AVAudioEngine ai_tech_seed dev_tool 84 AVAudioEngine
636 AVAudioSession ai_tech_seed dev_tool 84 AVAudioSession
637 AWS Q ai_tech_seed dev_tool 84 Amazon Q
638 Bolt AI ai_tech_seed dev_tool 84 Bolt.new
639 bolt new ai_tech_seed dev_tool 84 Bolt.new
640 Bolt.new ai_tech_seed dev_tool 84 Bolt.new
641 cap cut ai_tech_seed tech_company 84 CapCut
642 CapCut ai_tech_seed tech_company 84 剪映
643 GLM 4.6 ai_tech_seed ai_model 84 GLM-4.6
644 GLM Coding Plan ai_tech_seed dev_tool 84 GLM Coding Plan
645 glm coding plan ai_tech_seed dev_tool 84 GLM Coding Plan
646 glm four six ai_tech_seed ai_model 84 GLM-4.6
647 GLM-4.6 ai_tech_seed ai_model 84 GLM-4.6
648 grok build ai_tech_seed dev_tool 84 Grok Build
649 Grok Build CLI ai_tech_seed dev_tool 84 Grok Build
650 Hailuo ai_tech_seed ai_brand 84 海螺AI
651 Hangzhou Six Little Dragons ai_tech_seed tech_term 84 杭州六小龙
652 Jimeng ai_tech_seed ai_brand 84 即梦
653 Kiro ai_tech_seed dev_tool 84 Kiro
654 kiro ai_tech_seed dev_tool 84 Kiro
655 lang graph ai_tech_seed ai_platform 84 LangGraph
656 LangGraph ai_tech_seed ai_platform 84 LangGraph
657 Llama 4 Maverick ai_tech_seed ai_model 84 Llama Maverick
658 llama maverick ai_tech_seed ai_model 84 Llama Maverick
659 local model ai_tech_seed ai_term 84 本地模型
660 Lovable ai_tech_seed dev_tool 84 Lovable
661 lovable ai_tech_seed dev_tool 84 Lovable
662 Lovable AI ai_tech_seed dev_tool 84 Lovable
663 MiniMax Video ai_tech_seed ai_brand 84 海螺AI
664 Mistral Large ai_tech_seed ai_model 84 Mistral Large
665 mistral large ai_tech_seed ai_model 84 Mistral Large
666 Mistral Large 3 ai_tech_seed ai_model 84 Mistral Large
667 Neon ai_tech_seed tech_company 84 Neon
668 neon database ai_tech_seed tech_company 84 Neon
669 Neon Postgres ai_tech_seed tech_company 84 Neon
670 on-device model ai_tech_seed ai_term 84 端侧模型
671 open hands ai_tech_seed dev_tool 84 OpenHands
672 open telemetry ai_tech_seed tech_term 84 OpenTelemetry
673 OpenHands ai_tech_seed dev_tool 84 OpenHands
674 OpenHands AI ai_tech_seed dev_tool 84 OpenHands
675 OpenTelemetry ai_tech_seed tech_term 84 OpenTelemetry
676 OTel ai_tech_seed tech_term 84 OpenTelemetry
677 Prisma ai_tech_seed dev_tool 84 Prisma
678 prisma orm ai_tech_seed dev_tool 84 Prisma
679 Prisma ORM ai_tech_seed dev_tool 84 Prisma
680 prompt cache ai_tech_seed ai_term 84 prompt caching
681 prompt caching ai_tech_seed ai_term 84 prompt caching
682 Q Developer ai_tech_seed dev_tool 84 Amazon Q
683 Replit Agent ai_tech_seed dev_tool 84 Replit Agent
684 replit agent ai_tech_seed dev_tool 84 Replit Agent
685 Replit AI ai_tech_seed dev_tool 84 Replit Agent
686 semantic retrieval ai_tech_seed ai_term 84 semantic search
687 semantic search ai_tech_seed ai_term 84 semantic search
688 shad cn ai_tech_seed dev_tool 84 shadcn/ui
689 shadcn ai_tech_seed dev_tool 84 shadcn/ui
690 shadcn ui ai_tech_seed dev_tool 84 shadcn/ui
691 shadcn/ui ai_tech_seed dev_tool 84 shadcn/ui
692 Speech AssetInventory ai_tech_seed dev_tool 84 AssetInventory
693 SQLite ai_tech_seed tech_term 84 SQLite
694 sqlite ai_tech_seed tech_term 84 SQLite
695 StackBlitz Bolt ai_tech_seed dev_tool 84 Bolt.new
696 Tailwind ai_tech_seed dev_tool 84 Tailwind CSS
697 Tailwind CSS ai_tech_seed dev_tool 84 Tailwind CSS
698 tailwind css ai_tech_seed dev_tool 84 Tailwind CSS
699 test flight ai_tech_seed dev_tool 84 TestFlight
700 TestFlight ai_tech_seed dev_tool 84 TestFlight
701 Trae ai_tech_seed dev_tool 84 Trae
702 trae ai_tech_seed dev_tool 84 Trae
703 Trae AI ai_tech_seed dev_tool 84 Trae
704 Trae CN ai_tech_seed dev_tool 84 Trae
705 vector retrieval ai_tech_seed ai_term 84 vector search
706 vector search ai_tech_seed ai_term 84 vector search
707 world model ai_tech_seed ai_term 84 世界模型
708 xiao hong shu ai_tech_seed tech_company 84 Xiaohongshu
709 Yuanbao ai_tech_seed ai_brand 84 腾讯元宝
710 世界模型 shi jie mo xing ai_tech_seed ai_term 84 世界模型
711 剪映 jian ying ai_tech_seed tech_company 84 剪映
712 即梦 ji meng ai_tech_seed ai_brand 84 即梦
713 即梦AI ji meng ai_tech_seed ai_brand 84 即梦
714 向量搜索 ai_tech_seed ai_term 84 vector search
715 提示词缓存 ai_tech_seed ai_term 84 prompt caching
716 智谱 coding plan ai_tech_seed dev_tool 84 GLM Coding Plan
717 本地模型 ben di mo xing ai_tech_seed ai_term 84 本地模型
718 杭州六小龙 hang zhou liu xiao long ai_tech_seed tech_term 84 杭州六小龙
719 海螺AI hai luo AI ai_tech_seed ai_brand 84 海螺AI
720 端侧模型 duan ce mo xing ai_tech_seed ai_term 84 端侧模型
721 腾讯元宝 teng xun yuan bao ai_tech_seed ai_brand 84 腾讯元宝
722 语义搜索 ai_tech_seed ai_term 84 semantic search
723 01.AI ai_tech_seed ai_brand 82 零一万物
724 activity kit ai_tech_seed dev_tool 82 ActivityKit
725 ActivityKit ai_tech_seed dev_tool 82 ActivityKit
726 AI native ai_tech_seed ai_term 82 AI native
727 AI video ai_tech_seed tech_term 82 AI视频
728 AI workflow ai_tech_seed ai_term 82 AI workflow
729 ai workflow ai_tech_seed ai_term 82 AI workflow
730 AI 工作流 ai_tech_seed ai_term 82 AI workflow
731 AI-native ai_tech_seed ai_term 82 AI native
732 Airbnb ai_tech_seed tech_company 82 Airbnb
733 airbnb ai_tech_seed tech_company 82 Airbnb
734 AI原生 ai_tech_seed ai_term 82 AI native
735 AI视频 AI shi pin ai_tech_seed tech_term 82 AI视频
736 Amazon Q Developer ai_tech_seed dev_tool 82 Q Developer
737 Apache Kafka ai_tech_seed tech_term 82 Kafka
738 AutoGen ai_tech_seed ai_platform 82 AutoGen
739 autogen ai_tech_seed ai_platform 82 AutoGen
740 bai chuan ai_tech_seed ai_brand 82 百川
741 Baichuan ai_tech_seed ai_brand 82 百川
742 baichuan ai_tech_seed ai_brand 82 Baichuan
743 blockchain ai_tech_seed tech_term 82 区块链
744 Bun ai_tech_seed dev_tool 82 Bun
745 bun js ai_tech_seed dev_tool 82 Bun
746 Bun runtime ai_tech_seed dev_tool 82 Bun
747 Cloudflare ai_tech_seed tech_company 82 Cloudflare
748 cloudflare ai_tech_seed tech_company 82 Cloudflare
749 Codeium Windsurf ai_tech_seed dev_tool 82 Windsurf
750 Cognition ai_tech_seed ai_brand 82 Cognition
751 cognition ai ai_tech_seed ai_brand 82 Cognition
752 Cognition AI ai_tech_seed ai_brand 82 Cognition
753 Coinbase ai_tech_seed tech_company 82 Coinbase
754 coinbase ai_tech_seed tech_company 82 Coinbase
755 context compression ai_tech_seed ai_term 82 上下文压缩
756 Create ML ai_tech_seed dev_tool 82 Create ML
757 create ml ai_tech_seed dev_tool 82 Create ML
758 CreateML ai_tech_seed dev_tool 82 Create ML
759 crew ai ai_tech_seed ai_platform 82 CrewAI
760 Crew AI ai_tech_seed ai_platform 82 CrewAI
761 CrewAI ai_tech_seed ai_platform 82 CrewAI
762 deep research ai_tech_seed ai_term 82 deep research
763 distillation ai_tech_seed ai_term 82 distillation
764 Drizzle ai_tech_seed dev_tool 82 Drizzle
765 drizzle orm ai_tech_seed dev_tool 82 Drizzle
766 Drizzle ORM ai_tech_seed dev_tool 82 Drizzle
767 embodied agent ai_tech_seed ai_term 82 具身智能体
768 embodied AI ai_tech_seed tech_term 82 具身智能
769 end-to-end ai_tech_seed tech_term 82 端到端
770 fast api ai_tech_seed dev_tool 82 FastAPI
771 FastAPI ai_tech_seed dev_tool 82 FastAPI
772 full self driving ai_tech_seed tech_term 82 FSD
773 Geely ai_tech_seed tech_company 82 Geely
774 geely ai_tech_seed tech_company 82 Geely
778 Groq ai_tech_seed ai_brand 82 Groq
779 groq ai_tech_seed ai_brand 82 Groq
780 GROQ ai_tech_seed ai_brand 82 Groq
781 hailuo ai ai_tech_seed ai_brand 82 Hailuo
782 horizon ai_tech_seed tech_company 82 Horizon Robotics
783 Horizon Robotics ai_tech_seed tech_company 82 Horizon Robotics
784 humanoid robot hybrid retrieval ai_tech_seed tech_term ai_term 82 人形机器人 hybrid search
785 hybrid search ai_tech_seed ai_term 82 hybrid search
786 IBM ai_tech_seed tech_company 82 IBM
787 ibm ai_tech_seed tech_company 82 IBM
788 image-to-video ai_tech_seed tech_term 82 图生视频
789 Jeff Bezos ai_tech_seed tech_leader 82 Jeff Bezos
790 jeff bezos ai_tech_seed tech_leader 82 Jeff Bezos
791 jimeng ai ai_tech_seed ai_brand 82 Jimeng
792 Kafka ai_tech_seed tech_term 82 Kafka
793 kafka ai_tech_seed tech_term 82 Kafka
794 ling yi wan wu ai_tech_seed ai_brand 82 零一万物
795 Live Activities ai_tech_seed dev_tool 82 Live Activities
796 live activities ai_tech_seed dev_tool 82 Live Activities
797 llama index ai_tech_seed ai_platform 82 LlamaIndex
798 LlamaIndex ai_tech_seed ai_platform 82 LlamaIndex
799 LM Studio ai_tech_seed ai_platform 82 LM Studio
800 lm studio ai_tech_seed ai_platform 82 LM Studio
801 LMStudio ai_tech_seed ai_platform 82 LM Studio
802 MCP Inspector ai_tech_seed dev_tool 82 MCP Inspector
803 mcp inspector ai_tech_seed dev_tool 82 MCP Inspector
804 MCP 调试器 ai_tech_seed dev_tool 82 MCP Inspector
805 Microsoft AutoGen ai_tech_seed ai_platform 82 AutoGen
806 Mongo DB ai_tech_seed tech_company 82 MongoDB
807 MongoDB ai_tech_seed tech_company 82 MongoDB
808 mongodb ai_tech_seed tech_company 82 MongoDB
809 new quality productive forces ai_tech_seed tech_term 82 新质生产力
810 Nintendo ai_tech_seed tech_company 82 Nintendo
811 nintendo ai_tech_seed tech_company 82 Nintendo
812 node js ai_tech_seed dev_tool 82 Node.js
821 ollama ai_tech_seed ai_platform 82 Ollama
822 open router ai_tech_seed ai_platform 82 OpenRouter
823 Open Router ai_tech_seed ai_platform 82 OpenRouter
824 open web ui ai_tech_seed ai_platform 82 Open WebUI
825 Open WebUI ai_tech_seed ai_platform 82 Open WebUI
826 open weight ai_tech_seed ai_term 82 open weight
827 open weights ai_tech_seed ai_term 82 open weight
828 OpenRouter ai_tech_seed ai_platform 82 OpenRouter
829 OpenWebUI ai_tech_seed ai_platform 82 Open WebUI
830 Optimus ai_tech_seed tech_term 82 人形机器人
831 Oracle ai_tech_seed tech_company 82 Oracle
832 oracle ai_tech_seed tech_company 82 Oracle
833 otel ai_tech_seed tech_term 82 OTel
834 Palantir ai_tech_seed tech_company 82 Palantir
835 palantir ai_tech_seed tech_company 82 Palantir
836 PayPal ai_tech_seed tech_company 82 PayPal
837 paypal ai_tech_seed tech_company 82 PayPal
838 planet scale ai_tech_seed tech_company 82 PlanetScale
839 PlanetScale ai_tech_seed tech_company 82 PlanetScale
840 PlanetScale MySQL ai_tech_seed tech_company 82 PlanetScale
841 pre-training ai_tech_seed ai_term 82 pretraining
842 pretraining ai_tech_seed ai_term 82 pretraining
843 q developer ai_tech_seed dev_tool 82 Q Developer
844 React Server Components ai_tech_seed dev_tool 82 React Server Components
845 react server components ai_tech_seed dev_tool 82 React Server Components
846 red note ai_tech_seed tech_company 82 RedNote
847 Redis ai_tech_seed tech_company 82 Redis
848 redis ai_tech_seed tech_company 82 Redis
849 rerank ai_tech_seed ai_term 82 reranker
850 reranker ai_tech_seed ai_term 82 reranker
851 RSC ai_tech_seed dev_tool 82 React Server Components
852 Rust ai_tech_seed dev_tool 82 Rust
853 rust ai_tech_seed dev_tool 82 Rust
854 SaaS ai_tech_seed tech_term 82 SaaS
859 sdk ai_tech_seed tech_term 82 SDK
860 SenseTime ai_tech_seed tech_company 82 SenseTime
861 sensetime ai_tech_seed tech_company 82 SenseTime
862 Sentry ai_tech_seed dev_tool 82 Sentry
863 sentry ai_tech_seed dev_tool 82 Sentry
864 SF Symbols ai_tech_seed dev_tool 82 SF Symbols
865 sf symbols ai_tech_seed dev_tool 82 SF Symbols
866 SFSymbols ai_tech_seed dev_tool 82 SF Symbols
867 Shopify ai_tech_seed tech_company 82 Shopify
868 shopify ai_tech_seed tech_company 82 Shopify
869 SPM ai_tech_seed dev_tool 82 Swift Package Manager
870 Spotify ai_tech_seed tech_company 82 Spotify
871 spotify ai_tech_seed tech_company 82 Spotify
872 step fun ai_tech_seed ai_brand 82 StepFun
874 streaming ai_tech_seed ai_term 82 流式
875 Sundar Pichai ai_tech_seed tech_leader 82 Sundar Pichai
876 sundar pichai ai_tech_seed tech_leader 82 Sundar Pichai
877 Swift Package Manager ai_tech_seed dev_tool 82 Swift Package Manager
878 swift package manager ai_tech_seed dev_tool 82 Swift Package Manager
879 swift ui ai_tech_seed dev_tool 82 SwiftUI
880 Swift UI ai_tech_seed dev_tool 82 SwiftUI
881 SwiftUI ai_tech_seed dev_tool 82 SwiftUI
882 text-to-image ai_tech_seed tech_term 82 文生图
883 text-to-video ai_tech_seed tech_term 82 文生视频
884 TRAE ai_tech_seed dev_tool 82 TRAE
885 vector database ai_tech_seed ai_term 82 vector database
886 Vercel ai_tech_seed tech_company 82 Vercel
887 vercel ai_tech_seed tech_company 82 Vercel
888 vision os ai_tech_seed dev_tool 82 visionOS
889 Vision Pro ai_tech_seed dev_tool 82 visionOS
890 visionOS ai_tech_seed dev_tool 82 visionOS
891 Vue ai_tech_seed dev_tool 82 Vue
892 vue ai_tech_seed dev_tool 82 Vue
893 Vue.js ai_tech_seed dev_tool 82 Vue
894 Vue3 ai_tech_seed dev_tool 82 Vue
895 Weibo ai_tech_seed tech_company 82 Weibo
896 weibo ai_tech_seed tech_company 82 Weibo
897 widget kit ai_tech_seed dev_tool 82 WidgetKit
898 WidgetKit ai_tech_seed dev_tool 82 WidgetKit
899 Windsurf ai_tech_seed dev_tool 82 Windsurf
900 windsurf ai_tech_seed dev_tool 82 Windsurf
901 workflow orchestration ai_tech_seed ai_term 82 工作流编排
902 xcode gen ai_tech_seed dev_tool 82 XcodeGen
903 XcodeGen ai_tech_seed dev_tool 82 XcodeGen
904 Yi ai_tech_seed ai_brand 82 零一万物
905 人形机器人 上下文压缩 ren xing ji qi ren shang xia wen ya suo ai_tech_seed tech_term ai_term 82 人形机器人 上下文压缩
906 任天堂 ai_tech_seed tech_company 82 Nintendo
907 余承东 yu cheng dong ai_tech_seed tech_leader 82 余承东
908 元宝 yuan bao ai_tech_seed ai_brand 82 元宝
909 全自动驾驶 ai_tech_seed tech_term 82 FSD
910 具身智能 ju shen zhi neng ai_tech_seed tech_term 82 具身智能
911 具身智能体 ju shen zhi neng ti ai_tech_seed ai_term 82 具身智能体
912 区块链 qu kuai lian ai_tech_seed tech_term 82 区块链
913 吉利 ai_tech_seed tech_company 82 Geely
914 向量数据库 ai_tech_seed ai_term 82 vector database
915 商汤 ai_tech_seed tech_company 82 SenseTime
916 图生视频 tu sheng shi pin ai_tech_seed tech_term 82 图生视频
917 地平线 ai_tech_seed tech_company 82 Horizon Robotics
918 字节 Trae ai_tech_seed dev_tool 82 TRAE
919 工作流编排 gong zuo liu bian pai ai_tech_seed ai_term 82 工作流编排
920 开放权重 ai_tech_seed ai_term 82 open weight
921 微博 ai_tech_seed tech_company 82 Weibo
922 文生图 wen sheng tu ai_tech_seed tech_term 82 文生图
923 文生视频 wen sheng shi pin ai_tech_seed tech_term 82 文生视频
924 新质生产力 xin zhi sheng chan li ai_tech_seed tech_term 82 新质生产力
925 流式 liu shi ai_tech_seed ai_term 82 流式
926 深度研究 ai_tech_seed ai_term 82 deep research
927 混合搜索 ai_tech_seed ai_term 82 hybrid search
928 灵动岛实时活动 ai_tech_seed dev_tool 82 Live Activities
929 百川 ai_tech_seed ai_brand 82 百川
930 皮查伊 ai_tech_seed tech_leader 82 Sundar Pichai
931 知识蒸馏 ai_tech_seed ai_term 82 distillation
932 端到端 duan dao duan ai_tech_seed tech_term 82 端到端
933 纳德拉 ai_tech_seed tech_leader 82 Satya Nadella
934 蒸馏 ai_tech_seed ai_term 82 distillation
935 贝索斯 ai_tech_seed tech_leader 82 Jeff Bezos
936 重排序模型 ai_tech_seed ai_term 82 reranker
937 阶跃星辰 ai_tech_seed ai_brand 82 StepFun
938 零一万物 ai_tech_seed ai_brand 82 零一万物
939 预训练 ai_tech_seed ai_term 82 pretraining
940 01 ai ai_tech_seed ai_brand 80 01.AI
941 agent harness ai_tech_seed ai_term 80 LLM harness
942 agentic harness ai_tech_seed ai_term 80 agent harness
943 Astro ai_tech_seed dev_tool 80 Astro
944 astro js ai_tech_seed dev_tool 80 Astro
945 Atlassian ai_tech_seed tech_company 80 Atlassian
946 atlassian ai_tech_seed tech_company 80 Atlassian
947 authentically human ai_tech_seed tech_term 80 活人感
948 Broadcom ai_tech_seed tech_company 80 Broadcom
949 broadcom ai_tech_seed tech_company 80 Broadcom
950 Canva ai_tech_seed tech_company 80 Canva
951 canva ai_tech_seed tech_company 80 Canva
952 Clerk ai_tech_seed dev_tool 80 Clerk
953 clerk auth ai_tech_seed dev_tool 80 Clerk
954 click house ai_tech_seed tech_term 80 ClickHouse
955 ClickHouse ai_tech_seed tech_term 80 ClickHouse
956 Codeium ai_tech_seed ai_brand 80 Codeium
957 codeium ai_tech_seed ai_brand 80 Codeium
958 Cohere ai_tech_seed ai_brand 80 Cohere
960 computer use ai_tech_seed ai_term 80 computer use
961 computer-use ai_tech_seed ai_term 80 computer use
962 Confluence ai_tech_seed tech_company 80 Atlassian
963 Convex ai_tech_seed tech_company 80 Convex
964 Convex database ai_tech_seed tech_company 80 Convex
965 convex dev ai_tech_seed tech_company 80 Convex
966 Deno ai_tech_seed dev_tool 80 Deno
967 deno ai_tech_seed dev_tool 80 Deno
968 Deno Deploy ai_tech_seed dev_tool 80 Deno
969 Discord ai_tech_seed tech_company 80 Discord
970 discord ai_tech_seed tech_company 80 Discord
971 duck db ai_tech_seed tech_term 80 DuckDB
972 Duck DB ai_tech_seed tech_term 80 DuckDB
973 DuckDB ai_tech_seed tech_term 80 DuckDB
974 eleven labs ai_tech_seed ai_brand 80 ElevenLabs
975 Eleven Labs ai_tech_seed ai_brand 80 ElevenLabs
976 ElevenLabs ai_tech_seed ai_brand 80 ElevenLabs
977 emotional value ai_tech_seed tech_term 80 情绪价值
978 Firebase ai_tech_seed tech_company 80 Firebase
979 firebase ai_tech_seed tech_company 80 Firebase
980 GitLab ai_tech_seed tech_company 80 GitLab
984 lang chain ai_tech_seed ai_platform 80 LangChain
985 LangChain ai_tech_seed ai_platform 80 LangChain
986 Langchain ai_tech_seed ai_platform 80 LangChain
987 libSQL ai_tech_seed tech_company 80 Turso
988 LLM harness ai_tech_seed ai_term 80 LLM harness
989 llm harness ai_tech_seed ai_term 80 LLM harness
990 low altitude economy ai_tech_seed tech_term 80 低空经济
991 Megvii ai_tech_seed tech_company 80 Megvii
992 megvii ai_tech_seed tech_company 80 Megvii
993 Metal ai_tech_seed dev_tool 80 Metal
994 metal ai_tech_seed dev_tool 80 Metal
995 Metal Performance Shaders ai_tech_seed dev_tool 80 Metal
996 Metaso ai_tech_seed ai_brand 80 秘塔AI
997 on device ai ai_tech_seed ai_term 80 on-device AI
998 on-device AI ai_tech_seed ai_term 80 on-device AI
999 optimus ai_tech_seed tech_term 80 Optimus
1000 Quark AI ai_tech_seed ai_brand 80 夸克AI
1001 real person vibe ai_tech_seed tech_term 80 活人感
1002 reality kit ai_tech_seed dev_tool 80 RealityKit
1003 RealityKit ai_tech_seed dev_tool 80 RealityKit
1004 Reddit ai_tech_seed tech_company 80 Reddit
1005 reddit ai_tech_seed tech_company 80 Reddit
1006 Rivian ai_tech_seed tech_company 80 Rivian
1007 rivian ai_tech_seed tech_company 80 Rivian
1008 Roo Code ai_tech_seed dev_tool 80 Roo Code
1009 roo code ai_tech_seed dev_tool 80 Roo Code
1010 RooCode ai_tech_seed dev_tool 80 Roo Code
1011 Runway ai_tech_seed ai_brand 80 Runway
1012 runway ai ai_tech_seed ai_brand 80 Runway
1013 Runway ML ai_tech_seed ai_brand 80 Runway
1022 snowflake ai_tech_seed tech_company 80 Snowflake
1023 Supabase ai_tech_seed tech_company 80 Supabase
1024 supabase ai_tech_seed tech_company 80 Supabase
1025 svelte kit ai_tech_seed dev_tool 80 SvelteKit
1026 SvelteKit ai_tech_seed dev_tool 80 SvelteKit
1027 swe agent ai_tech_seed ai_term 80 SWE-agent
1028 SWE Agent ai_tech_seed ai_term 80 SWE-agent
1029 swe bench ai_tech_seed ai_term 80 SWE-bench
1030 SWE bench ai_tech_seed ai_term 80 SWE-bench
1031 SWE-agent ai_tech_seed ai_term 80 SWE-agent
1032 SWE-bench ai_tech_seed ai_term 80 SWE-bench
1033 Terraform ai_tech_seed tech_company 80 Terraform
1034 terraform ai_tech_seed tech_company 80 Terraform
1035 TPU ai_tech_seed tech_term 80 TPU
1036 tpu ai_tech_seed tech_term 80 TPU
1037 Turso ai_tech_seed tech_company 80 Turso
1038 turso ai_tech_seed tech_company 80 Turso
1039 vLLM ai_tech_seed ai_platform 80 vLLM
1040 vllm ai_tech_seed ai_platform 80 vLLM
1041 VLLM ai_tech_seed ai_platform 80 vLLM
1042 Zed ai_tech_seed dev_tool 80 Zed
1043 Zed AI ai_tech_seed dev_tool 80 Zed
1044 zed editor ai_tech_seed dev_tool 80 Zed
1045 Zoom ai_tech_seed tech_company 80 Zoom
1046 zoom ai_tech_seed tech_company 80 Zoom
1047 低空经济 di kong jing ji ai_tech_seed tech_term 80 低空经济
1048 夸克AI kua ke AI ai_tech_seed ai_brand 80 夸克AI
1049 小语言模型 ai_tech_seed ai_term 80 SLM
1050 情绪价值 qing xu jia zhi ai_tech_seed tech_term 80 情绪价值
1051 擎天柱 ai_tech_seed tech_term 80 Optimus
1052 旷视 ai_tech_seed tech_company 80 Megvii
1053 智能体框架 ai_tech_seed ai_term 80 agent harness
1054 活人感 huo ren gan ai_tech_seed tech_term 80 活人感
1055 电脑使用 ai_tech_seed ai_term 80 computer use
1056 秘塔AI mi ta AI ai_tech_seed ai_brand 80 秘塔AI
1057 端侧AI ai_tech_seed ai_term 80 on-device AI
1058 anything llm ai_tech_seed ai_platform 78 AnythingLLM
1059 Anything LLM ai_tech_seed ai_platform 78 AnythingLLM
1060 AnythingLLM ai_tech_seed ai_platform 78 AnythingLLM
1061 auth zero ai_tech_seed dev_tool 78 Auth0
1062 Auth Zero ai_tech_seed dev_tool 78 Auth0
1063 Auth0 ai_tech_seed dev_tool 78 Auth0
1064 benchmark ai_tech_seed ai_term 78 benchmark
1065 Biome ai_tech_seed dev_tool 78 Biome
1066 biome js ai_tech_seed dev_tool 78 Biome
1067 Block ai_tech_seed fintech 78 Block
1068 block ai_tech_seed fintech 78 Block
1069 Character AI ai_tech_seed ai_brand 78 Character AI
1070 character.ai ai_tech_seed ai_brand 78 Character AI
1071 Character.AI ai_tech_seed ai_brand 78 Character AI
1072 Clerk Auth ai_tech_seed dev_tool 78 Clerk Auth
1073 Cline ai_tech_seed dev_tool 78 Cline
1074 cline ai_tech_seed dev_tool 78 Cline
1075 Cline AI ai_tech_seed dev_tool 78 Cline
1076 comfy ui ai_tech_seed ai_platform 78 ComfyUI
1077 ComfyUI ai_tech_seed ai_platform 78 ComfyUI
1078 Continue ai_tech_seed dev_tool 78 Continue
1079 continue dev ai_tech_seed dev_tool 78 Continue
1080 Continue.dev ai_tech_seed dev_tool 78 Continue
1081 cyber reconciliation ai_tech_seed tech_term 78 赛博对账
1082 Datadog ai_tech_seed tech_company 78 Datadog
1083 datadog ai_tech_seed tech_company 78 Datadog
1084 digital avatar ai_tech_seed tech_term 78 数字分身
1085 DSPy ai_tech_seed ai_platform 78 DSPy
1086 dspy ai_tech_seed ai_platform 78 DSPy
1087 DSPy AI ai_tech_seed ai_platform 78 DSPy
1088 Elastic ai_tech_seed tech_company 78 Elastic
1089 elastic ai_tech_seed tech_company 78 Elastic
1090 Elasticsearch ai_tech_seed tech_company 78 Elastic
1091 Fly ai_tech_seed tech_company 78 Fly.io
1092 fly io ai_tech_seed tech_company 78 Fly.io
1093 Fly.io ai_tech_seed tech_company 78 Fly.io
1094 go global ai_tech_seed tech_term 78 出海
1095 Grafana ai_tech_seed dev_tool 78 Grafana
1096 grafana ai_tech_seed dev_tool 78 Grafana
1097 Great Wall ai_tech_seed tech_company 78 Great Wall
1098 great wall ai_tech_seed tech_company 78 Great Wall
1099 Hono ai_tech_seed dev_tool 78 Hono
1100 hono js ai_tech_seed dev_tool 78 Hono
1101 Hugging Face Spaces ai_tech_seed ai_platform 78 Hugging Face Spaces
1102 hugging face spaces ai_tech_seed ai_platform 78 Hugging Face Spaces
1103 Jupyter ai_tech_seed dev_tool 78 Jupyter
1104 jupyter ai_tech_seed dev_tool 78 Jupyter
1105 Jupyter Notebook ai_tech_seed dev_tool 78 Jupyter
1106 lib sql ai_tech_seed tech_term 78 libSQL
1107 Linear ai_tech_seed tech_company 78 Linear
1108 linear ai_tech_seed dev_tool 78 Linear
1109 linear app ai_tech_seed tech_company 78 Linear
1110 Lucid ai_tech_seed tech_company 78 Lucid
1111 lucid motors ai_tech_seed tech_company 78 Lucid
1112 Lucid Motors ai_tech_seed tech_company 78 Lucid
1113 MCP HTTP ai_tech_seed ai_term 78 Streamable HTTP
1114 memory bank ai_tech_seed ai_term 78 memory bank
1115 metaso ai_tech_seed ai_brand 78 Metaso
1116 metaverse ai_tech_seed tech_term 78 元宇宙
1117 MiniCPM ai_tech_seed ai_brand 78 面壁智能
1118 Modal ai_tech_seed tech_company 78 Modal
1119 modal labs ai_tech_seed tech_company 78 Modal
1120 Nami AI ai_tech_seed ai_brand 78 纳米AI
1121 Nix ai_tech_seed tech_term 78 Nix
1122 nix ai_tech_seed tech_term 78 Nix
1123 NixOS ai_tech_seed tech_term 78 Nix
1124 Nuxt ai_tech_seed dev_tool 78 Nuxt
1125 nuxt ai_tech_seed dev_tool 78 Nuxt
1126 Nuxt.js ai_tech_seed dev_tool 78 Nuxt
1127 Postman ai_tech_seed dev_tool 78 Postman
1128 postman ai_tech_seed dev_tool 78 Postman
1129 Prometheus ai_tech_seed dev_tool 78 Prometheus
1130 prometheus ai_tech_seed dev_tool 78 Prometheus
1131 Quark ai_tech_seed ai_brand 78 Quark
1132 quark ai ai_tech_seed ai_brand 78 Quark
1133 RAG pipeline ai_tech_seed ai_term 78 RAG pipeline
1134 rag pipeline ai_tech_seed ai_term 78 RAG pipeline
1135 Railway ai_tech_seed tech_company 78 Railway
1136 railway app ai_tech_seed tech_company 78 Railway
1137 React Server Actions ai_tech_seed dev_tool 78 Server Actions
1138 reasoning effort ai_tech_seed ai_term 78 reasoning effort
1139 Remix ai_tech_seed dev_tool 78 Remix
1140 remix run ai_tech_seed dev_tool 78 Remix
1141 remote work ai_tech_seed tech_term 78 远程办公
1142 Replicate ai_tech_seed ai_platform 78 Replicate
1143 replicate ai_tech_seed ai_platform 78 Replicate
1144 Replit ai_tech_seed dev_tool 78 Replit
1145 replit ai_tech_seed dev_tool 78 Replit
Replit Agent ai_tech_seed dev_tool 78 Replit
1146 robin hood ai_tech_seed fintech 78 Robinhood
1147 Robin Hood ai_tech_seed fintech 78 Robinhood
1148 Robinhood ai_tech_seed fintech 78 Robinhood
1149 Server Actions ai_tech_seed dev_tool 78 Server Actions
1150 server actions ai_tech_seed dev_tool 78 Server Actions
1151 service now ai_tech_seed tech_company 78 ServiceNow
1152 ServiceNow ai_tech_seed tech_company 78 ServiceNow
1153 Spaces ai_tech_seed ai_platform 78 Hugging Face Spaces
1154 Square ai_tech_seed fintech 78 Block
1155 square ai_tech_seed fintech 78 Block
1156 Streamable HTTP ai_tech_seed ai_term 78 Streamable HTTP
1157 streamable http ai_tech_seed ai_term 78 Streamable HTTP
1158 Tailscale ai_tech_seed tech_company 78 Tailscale
1159 tailscale ai_tech_seed tech_company 78 Tailscale
1160 test time compute ai_tech_seed ai_term 78 test-time compute
1161 test-time compute ai_tech_seed ai_term 78 test-time compute
1162 tiangong ai_tech_seed ai_brand 78 天工
1163 Tuist ai_tech_seed dev_tool 78 Tuist
1164 tuist ai_tech_seed dev_tool 78 Tuist
1165 turbo repo ai_tech_seed dev_tool 78 Turborepo
1166 Turborepo ai_tech_seed dev_tool 78 Turborepo
1167 Twilio ai_tech_seed tech_company 78 Twilio
1168 twilio ai_tech_seed tech_company 78 Twilio
1169 Visa ai_tech_seed tech_company 78 Visa
1171 Web 3 ai_tech_seed tech_term 78 Web3
1172 Web3 ai_tech_seed tech_term 78 Web3
1173 web3 ai_tech_seed tech_term 78 Web3
1174 work os ai_tech_seed dev_tool 78 WorkOS
1175 Work OS ai_tech_seed dev_tool 78 WorkOS
1176 WorkOS ai_tech_seed dev_tool 78 WorkOS
1177 元宇宙 yuan yu zhou ai_tech_seed tech_term 78 元宇宙
1178 全球化 chu hai ai_tech_seed tech_term 78 出海
1179 出海 chu hai ai_tech_seed tech_term 78 出海
1180 基准测试 ai_tech_seed ai_term 78 benchmark
1181 天工 tian gong ai_tech_seed ai_brand 78 天工
1182 夸克 ai_tech_seed ai_brand 78 Quark
1183 推理强度 ai_tech_seed ai_term 78 reasoning effort
1184 数字分身 shu zi fen shen ai_tech_seed tech_term 78 数字分身
1185 昆仑万维天工 tian gong ai_tech_seed ai_brand 78 天工
1186 测试时计算 ai_tech_seed ai_term 78 test-time compute
1187 纳米AI na mi AI ai_tech_seed ai_brand 78 纳米AI
1188 记忆库 ai_tech_seed ai_term 78 memory bank
1189 赛博对账 sai bo dui zhang ai_tech_seed tech_term 78 赛博对账
1190 远程办公 yuan cheng ban gong ai_tech_seed tech_term 78 远程办公
1191 长城汽车 ai_tech_seed tech_company 78 Great Wall
1192 面壁智能 mian bi zhi neng ai_tech_seed ai_brand 78 面壁智能
1193 cyber ai_tech_seed tech_term 76 赛博
1194 Deepset Haystack ai_tech_seed ai_platform 76 Haystack
1195 Haystack ai_tech_seed ai_platform 76 Haystack
1196 haystack ai ai_tech_seed ai_platform 76 Haystack
1197 Ktor ai_tech_seed dev_tool 76 Ktor
1198 ktor ai_tech_seed dev_tool 76 Ktor
1199 Ktor server ai_tech_seed dev_tool 76 Ktor
1200 Modal Labs ai_tech_seed tech_company 76 Modal Labs
1201 Nix OS ai_tech_seed tech_term 76 NixOS
1202 nixos ai_tech_seed tech_term 76 NixOS
1203 Nx ai_tech_seed dev_tool 76 Nx
1204 nx monorepo ai_tech_seed dev_tool 76 Nx
1205 pnpm ai_tech_seed dev_tool 76 pnpm
1206 red panda ai_tech_seed tech_company 76 Redpanda
1207 Redpanda ai_tech_seed tech_company 76 Redpanda
1208 Render ai_tech_seed tech_company 76 Render
1209 render.com ai_tech_seed tech_company 76 Render
1210 SenseChat ai_tech_seed ai_brand 76 商量
1211 Serverless Stack ai_tech_seed dev_tool 76 SST
1212 SST ai_tech_seed dev_tool 76 SST
1213 sst ai_tech_seed dev_tool 76 SST
1214 stdio transport ai_tech_seed ai_term 76 stdio transport
1215 web dev arena ai_tech_seed ai_term 76 WebDev Arena
1216 WebDev Arena ai_tech_seed ai_term 76 WebDev Arena
1217 WebDevArena ai_tech_seed ai_term 76 WebDev Arena
1218 商量 shang liang ai_tech_seed ai_brand 76 商量
1219 标准输入输出传输 ai_tech_seed ai_term 76 stdio transport
1220 赛博 sai bo ai_tech_seed tech_term 76 赛博
1221 Aider ai_tech_seed ai_brand 75 Aider
1222 aider ai_tech_seed ai_brand 75 Aider
1223 Bitbucket ai_tech_seed tech_company 75 Bitbucket
1246 数字游民 shu zi you min ai_tech_seed tech_term 75 数字游民
1247 越狱 ai_tech_seed ai_term 75 jailbreak
1248 面壁 ai_tech_seed ai_model 75 MiniCPM
1249 Focus AI ai_tech_seed ai_platform 72 Fooocus
1250 Fooocus ai_tech_seed ai_platform 72 Fooocus
1251 fooocus ai_tech_seed ai_platform 72 Fooocus
1252 Labubu ai_tech_seed tech_term 72 拉布布
1253 labubu ai_tech_seed tech_term 72 Labubu
1254 lying flat ai_tech_seed tech_term 72 躺平
1255 side hustle ai_tech_seed tech_term 72 副业
1256 village coffee ai_tech_seed tech_term 72 村咖
1257 副业 fu ye ai_tech_seed tech_term 72 副业
1258 拉布布 la bu bu ai_tech_seed tech_term 72 拉布布
1259 村咖 cun ka ai_tech_seed tech_term 72 村咖
1260 躺平 tang ping ai_tech_seed tech_term 72 躺平
@@ -1,14 +0,0 @@
{
"bin_bytes" : 1976593,
"bin_file" : "OSGKeyboardCLM.bin",
"export_seconds" : 0.28117799758911133,
"generated_at" : "2026-07-05T08:19:00Z",
"identifier" : "com.osgkeyboard.custom-lm.v1",
"locale" : "zh_CN",
"phrase_count" : 129403,
"sources" : {
"ai_tech_seed" : 749,
"sogou_v1" : 128743
},
"version" : "1.0.0"
}
@@ -1,16 +0,0 @@
{
"client_identifier" : "com.osgkeyboard.custom-lm.v1",
"configuration" : {
"language_model" : "\/Users\/rocky\/Documents\/OSGKeyboard\/OSGKeyboard\/Resources\/CustomLanguageModel\/v1\/compiled\/LM",
"vocabulary" : "\/Users\/rocky\/Documents\/OSGKeyboard\/OSGKeyboard\/Resources\/CustomLanguageModel\/v1\/compiled\/Vocab",
"weight" : null
},
"generated_at" : "2026-07-05T08:24:55Z",
"input_bin" : "OSGKeyboardCLM.bin",
"input_bin_bytes" : 1976593,
"language_model" : "LM",
"language_model_bytes" : 6398585,
"prepare_seconds" : 28.565693974494934,
"vocabulary" : "Vocab",
"vocabulary_bytes" : 178816
}
@@ -1,32 +1,20 @@
{ {
"version": "v1", "version": "v1",
"generated_at": "2026-07-05T07:47:30.162461+00:00", "generated_at": "2026-07-05T11:27:48.823095+00:00",
"locale": "zh-Hans", "locale": "zh-Hans",
"entry_count": 128743, "entry_count": 10300,
"sources": [ "sources": [
{ {
"key": "computer_terms", "key": "computer_terms",
"label": "计算机词汇大全【官方推荐】", "label": "计算机词汇大全【官方推荐】",
"weight": 5, "weight": 5,
"raw_count": 10300 "raw_count": 10300
},
{
"key": "network_slang_local",
"label": "网络流行新词.scel",
"weight": 3,
"raw_count": 118599
},
{
"key": "sogou_popular_accumulated",
"label": "SogouPopularDict accumulated",
"weight": 1,
"raw_count": 118602
} }
], ],
"notes": [ "notes": [
"Sogou-derived data is for internal ASR experimentation only.", "Domain-specific computer/IT vocabulary only; casual network slang and Sogou popular words removed.",
"PhraseCount weights map to SFCustomLanguageModelData relative frequencies.", "PhraseCount weights map to SFCustomLanguageModelData relative frequencies.",
"Higher source weight wins on duplicate words." "Merged with ai-tech-brands seed at export time for the final .bin asset."
], ],
"files": { "files": {
"phrases": "phrases.tsv" "phrases": "phrases.tsv"
File diff suppressed because it is too large Load Diff
+4 -12
View File
@@ -1,22 +1,14 @@
// FlowDiagnostics.swift // FlowDiagnostics.swift
// OSGKeyboard · Main App // OSGKeyboard · Main App
// //
// Structured logging for the Flow dictation pipeline. Visible in Xcode // Structured logging for the Flow dictation pipeline. Delegates to
// console (DEBUG) and Console.app via `subsystem: com.osgkeyboard.ios`. // `OSGLog.flow` for Console.app visibility.
import Foundation import Foundation
import os import OSGKeyboardShared
enum FlowDiagnostics { enum FlowDiagnostics {
private static let logger = Logger(
subsystem: "com.osgkeyboard.ios",
category: "Flow"
)
static func log(_ message: String) { static func log(_ message: String) {
logger.info("\(message, privacy: .public)") OSGLog.flow.info("\(message, privacy: .public)")
#if DEBUG
print("🌊[OSGFlow] \(message)")
#endif
} }
} }
+71 -52
View File
@@ -22,9 +22,7 @@ final class FlowSessionManager: ObservableObject {
private let capture = FlowContinuousCapture() private let capture = FlowContinuousCapture()
private let store = AppGroupStore() private let store = AppGroupStore()
/// Cloud-engine polish; local engine now ALSO runs through the /// Cloud-engine polish; local engine runs through built-in DeepSeek polish.
/// polisher when `localModeCloudPolishEnabled` is on the same
/// `PolishingService` short-circuits to raw when the toggle is off.
private var polisher: PolishingService { private var polisher: PolishingService {
PolishingService() PolishingService()
} }
@@ -33,12 +31,13 @@ final class FlowSessionManager: ObservableObject {
/// factory-built service straight back without going through the /// factory-built service straight back without going through the
/// old `OnDeviceModelWarmup` registry. /// old `OnDeviceModelWarmup` registry.
private var sessionASR: ASRService? private var sessionASR: ASRService?
/// Tracks which engine mode `sessionASR` was created for.
private var sessionASREngineMode: String?
/// Locale id last passed to `warmup(locale:)`.
private var sessionASRWarmedLocaleID: String?
private var asr: ASRService { private var asr: ASRService {
if let sessionASR { return sessionASR } if let sessionASR { return sessionASR }
let service = ASRServiceFactory.make( let service = ASRServiceFactory.make()
engineMode: store.engineMode,
localBackend: store.localASRBackend
)
sessionASR = service sessionASR = service
return service return service
} }
@@ -155,7 +154,8 @@ final class FlowSessionManager: ObservableObject {
// v0.2.0: iOS `SpeechAnalyzer` needs no warm-up. We still // v0.2.0: iOS `SpeechAnalyzer` needs no warm-up. We still
// re-bind the cached `sessionASR` so a config flip mid-session // re-bind the cached `sessionASR` so a config flip mid-session
// (e.g. switching from cloud to local) is honoured. // (e.g. switching from cloud to local) is honoured.
bindSessionASR() bindSessionASRIfNeeded()
scheduleASRWarmup()
debug("Flow session restored (\(Int(remaining))s remaining)") debug("Flow session restored (\(Int(remaining))s remaining)")
} }
@@ -192,6 +192,8 @@ final class FlowSessionManager: ObservableObject {
endBackgroundKeepAlive() endBackgroundKeepAlive()
ScreenWakeLock.release() ScreenWakeLock.release()
sessionASR = nil sessionASR = nil
sessionASREngineMode = nil
sessionASRWarmedLocaleID = nil
FlowSessionBridge.markSessionInactive() FlowSessionBridge.markSessionInactive()
FlowSessionDarwin.postSessionChanged() FlowSessionDarwin.postSessionChanged()
isActive = false isActive = false
@@ -219,13 +221,11 @@ final class FlowSessionManager: ObservableObject {
func handleScenePhase(_ phase: ScenePhase) { func handleScenePhase(_ phase: ScenePhase) {
switch phase { switch phase {
case .active: case .active:
FlowAppLifecycle.shared.setForeground(true)
setAppForeground(true) setAppForeground(true)
resumeAfterForeground() resumeAfterForeground()
case .inactive: case .inactive:
writeHeartbeatIfActive() writeHeartbeatIfActive()
case .background: case .background:
FlowAppLifecycle.shared.setForeground(false)
setAppForeground(false) setAppForeground(false)
beginBackgroundKeepAlive() beginBackgroundKeepAlive()
@unknown default: @unknown default:
@@ -269,7 +269,8 @@ final class FlowSessionManager: ObservableObject {
await self?.reactivateCaptureIfNeeded() await self?.reactivateCaptureIfNeeded()
// v0.2.0: iOS `SpeechAnalyzer` is bundled with the OS; no // v0.2.0: iOS `SpeechAnalyzer` is bundled with the OS; no
// on-device weights to reload after a background trip. // on-device weights to reload after a background trip.
self?.bindSessionASR() self?.bindSessionASRIfNeeded()
self?.scheduleASRWarmup()
} }
} }
@@ -336,16 +337,39 @@ final class FlowSessionManager: ObservableObject {
// v0.2.0: iOS `SpeechAnalyzer` needs no warm-up; just refresh // v0.2.0: iOS `SpeechAnalyzer` needs no warm-up; just refresh
// the cached ASR service in case the user flipped engines // the cached ASR service in case the user flipped engines
// while the session was idle. // while the session was idle.
bindSessionASR() bindSessionASRIfNeeded()
scheduleASRWarmup()
debug("Flow session started (\(Int(duration))s), continuous capture running") debug("Flow session started (\(Int(duration))s), continuous capture running")
} }
private func bindSessionASR() { private func bindSessionASRIfNeeded(force: Bool = false) {
sessionASR = ASRServiceFactory.make( let engineMode = store.engineMode
engineMode: store.engineMode, if !force,
localBackend: store.localASRBackend let sessionASR,
) sessionASREngineMode == engineMode {
return
}
sessionASR?.cancel()
sessionASR = ASRServiceFactory.make()
sessionASREngineMode = engineMode
sessionASRWarmedLocaleID = nil
}
private func scheduleASRWarmup() {
Task { @MainActor [weak self] in
await self?.warmupASRIfNeeded()
}
}
private func warmupASRIfNeeded() async {
bindSessionASRIfNeeded()
let locale = SpeechLocaleResolver.resolve(store.localeId)
let localeID = locale.identifier(.bcp47)
guard sessionASRWarmedLocaleID != localeID else { return }
await asr.warmup(locale: locale)
sessionASRWarmedLocaleID = localeID
FlowDiagnostics.log("ASR warmup complete locale=\(localeID)")
} }
private func permissionWarningMessage() -> String { private func permissionWarningMessage() -> String {
@@ -384,7 +408,10 @@ final class FlowSessionManager: ObservableObject {
private func beginUtterance() { private func beginUtterance() {
guard capture.running else { guard capture.running else {
failUtterance(message: AppL10n.string("flow.error.audioUnavailable")) failUtterance(
message: AppL10n.string("flow.error.audioUnavailable"),
kind: .audioUnavailable
)
return return
} }
guard !isUtteranceProcessing else { guard !isUtteranceProcessing else {
@@ -392,8 +419,8 @@ final class FlowSessionManager: ObservableObject {
return return
} }
// Honor engine / ASR backend changes without restarting the session. // Usually already warm from session start; refresh without blocking the mic gate.
bindSessionASR() scheduleASRWarmup()
currentPartial = "" currentPartial = ""
lastFinal = "" lastFinal = ""
@@ -411,7 +438,7 @@ final class FlowSessionManager: ObservableObject {
isUtteranceRecording = true isUtteranceRecording = true
utteranceRecordingStartedAt = Date() utteranceRecordingStartedAt = Date()
FlowDiagnostics.log( FlowDiagnostics.log(
"beginUtterance engine=\(store.engineMode) asr=\(store.localASRBackend.rawValue) " + "beginUtterance engine=\(store.engineMode) " +
"asrType=\(type(of: asr)) pipelined=true max=\(Int(FlowSessionKeys.maxUtteranceDuration))s" "asrType=\(type(of: asr)) pipelined=true max=\(Int(FlowSessionKeys.maxUtteranceDuration))s"
) )
@@ -419,6 +446,7 @@ final class FlowSessionManager: ObservableObject {
let outcome = await pipeline.transcribe(stream: stream) { partial in let outcome = await pipeline.transcribe(stream: stream) { partial in
Task { @MainActor in Task { @MainActor in
manager?.currentPartial = partial manager?.currentPartial = partial
FlowSessionBridge.storeTranscriptionPartial(partial)
} }
} }
// Re-bind `manager` inside the `@MainActor` block so the // Re-bind `manager` inside the `@MainActor` block so the
@@ -439,9 +467,9 @@ final class FlowSessionManager: ObservableObject {
case .failure(let message): case .failure(let message):
manager.debug("asr error: \(message)") manager.debug("asr error: \(message)")
if manager.isUtteranceRecording { if manager.isUtteranceRecording {
manager.failUtterance(message: message) manager.failUtterance(message: message, kind: .asrFailed)
} else if manager.isUtteranceProcessing { } else if manager.isUtteranceProcessing {
manager.finishProcessing(withError: message) manager.finishProcessing(withError: message, kind: .asrFailed)
} }
case .cancelled: case .cancelled:
break break
@@ -487,11 +515,15 @@ final class FlowSessionManager: ObservableObject {
currentPartial = "" currentPartial = ""
lastFinal = "" lastFinal = ""
chunkWarnings = [] chunkWarnings = []
FlowSessionBridge.storeTranscriptionPartial("")
FlowSessionBridge.setRecordingState(.idle) FlowSessionBridge.setRecordingState(.idle)
debug("utterance aborted") debug("utterance aborted")
} }
private func failUtterance(message: String) { private func failUtterance(
message: String,
kind: FlowSessionKeys.TranscriptionErrorKind = .asrFailed
) {
isUtteranceRecording = false isUtteranceRecording = false
isUtteranceProcessing = false isUtteranceProcessing = false
utteranceRecordingStartedAt = nil utteranceRecordingStartedAt = nil
@@ -505,12 +537,16 @@ final class FlowSessionManager: ObservableObject {
currentPartial = "" currentPartial = ""
lastFinal = "" lastFinal = ""
chunkWarnings = [] chunkWarnings = []
FlowSessionBridge.storeTranscriptionError(message) FlowSessionBridge.storeTranscriptionPartial("")
FlowSessionBridge.storeTranscriptionError(message, kind: kind)
FlowSessionBridge.setRecordingState(.idle) FlowSessionBridge.setRecordingState(.idle)
debug("utterance failed: \(message)") debug("utterance failed: \(message)")
} }
private func finishProcessing(withError message: String) { private func finishProcessing(
withError message: String,
kind: FlowSessionKeys.TranscriptionErrorKind = .asrFailed
) {
isUtteranceProcessing = false isUtteranceProcessing = false
utteranceRecordingStartedAt = nil utteranceRecordingStartedAt = nil
finalizeTask?.cancel() finalizeTask?.cancel()
@@ -519,7 +555,8 @@ final class FlowSessionManager: ObservableObject {
currentPartial = "" currentPartial = ""
lastFinal = "" lastFinal = ""
chunkWarnings = [] chunkWarnings = []
FlowSessionBridge.storeTranscriptionError(message) FlowSessionBridge.storeTranscriptionPartial("")
FlowSessionBridge.storeTranscriptionError(message, kind: kind)
FlowSessionBridge.setRecordingState(.idle) FlowSessionBridge.setRecordingState(.idle)
debug("utterance processing failed: \(message)") debug("utterance processing failed: \(message)")
} }
@@ -533,8 +570,7 @@ final class FlowSessionManager: ObservableObject {
let asrWait = asrWaitTimeout() let asrWait = asrWaitTimeout()
FlowDiagnostics.log( FlowDiagnostics.log(
"finalize start asrWait=\(Int(asrWait))s engine=\(store.engineMode) " + "finalize start asrWait=\(Int(asrWait))s engine=\(store.engineMode)"
"backend=\(store.localASRBackend.rawValue)"
) )
let asrDeadline = Date().addingTimeInterval(asrWait) let asrDeadline = Date().addingTimeInterval(asrWait)
@@ -560,10 +596,13 @@ final class FlowSessionManager: ObservableObject {
let key = (asrTask?.isCancelled == true) let key = (asrTask?.isCancelled == true)
? "flow.error.recognitionInterrupted" ? "flow.error.recognitionInterrupted"
: "flow.error.noSpeech" : "flow.error.noSpeech"
let kind: FlowSessionKeys.TranscriptionErrorKind =
(asrTask?.isCancelled == true) ? .recognitionInterrupted : .noSpeech
FlowDiagnostics.log("finalize failed: empty transcript after \(String(format: "%.1f", asrElapsed))s") FlowDiagnostics.log("finalize failed: empty transcript after \(String(format: "%.1f", asrElapsed))s")
utteranceRecordingStartedAt = nil utteranceRecordingStartedAt = nil
FlowSessionBridge.storeTranscriptionError( FlowSessionBridge.storeTranscriptionError(
AppL10n.string(key) AppL10n.string(key),
kind: kind
) )
return return
} }
@@ -576,33 +615,12 @@ final class FlowSessionManager: ObservableObject {
// from the keyboard extension are visible before polish/translate. // from the keyboard extension are visible before polish/translate.
let pipelineStore = AppGroupStore() let pipelineStore = AppGroupStore()
if !pipelineStore.shouldRunCloudLLMStep {
// Local engine with cloud polish off ASR-only.
FlowSessionBridge.storeTranscriptionResult(text, polishWarning: chunkNote)
FlowDiagnostics.log(
"finalize ASR-only total=\(String(format: "%.1f", Date().timeIntervalSince(pipelineStarted)))s " +
"len=\(text.count)"
)
SpeechHistoryStore.shared.recordUtterance(
text: text,
engineMode: engineMode,
duration: recordingDuration,
wasTranslation: false
)
currentPartial = ""
lastFinal = ""
chunkWarnings = []
debug("utterance finalized length=\(text.count)")
return
}
var delivered = text var delivered = text
let polishStarted = Date() let polishStarted = Date()
let polishMode = pipelineStore.polishModeForPipeline let polishMode = pipelineStore.polishModeForPipeline
FlowDiagnostics.log( FlowDiagnostics.log(
"finalize LLM mode=\(Self.polishModeLogLabel(polishMode)) " + "finalize LLM mode=\(Self.polishModeLogLabel(polishMode)) " +
"translationTarget=\(pipelineStore.translationTargetLocaleId) " + "translationTarget=\(pipelineStore.translationTargetLocaleId)"
"cloudPolish=\(pipelineStore.localModeCloudPolishEnabled)"
) )
do { do {
let polished = try await polisher.polish( let polished = try await polisher.polish(
@@ -640,6 +658,7 @@ final class FlowSessionManager: ObservableObject {
currentPartial = "" currentPartial = ""
lastFinal = "" lastFinal = ""
chunkWarnings = [] chunkWarnings = []
FlowSessionBridge.storeTranscriptionPartial("")
chunkedPipeline = nil chunkedPipeline = nil
debug("utterance finalized length=\(text.count)") debug("utterance finalized length=\(text.count)")
} }
@@ -1,175 +0,0 @@
// DictationCaptureView.swift
// OSGKeyboard · Main App
//
// Host-app recording surface for keyboard handoff.
// Uses the shared `LiveDictationController` the same entry point as
// the keyboard preview sheet.
import SwiftUI
import OSGKeyboardShared
@MainActor
final class DictationSessionCoordinator: ObservableObject {
@Published var isPresenting: Bool = false
func present() {
isPresenting = true
}
func dismiss() {
isPresenting = false
}
}
struct DictationCaptureView: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.themePalette) private var palette: ThemePalette
@ObservedObject var config: ProviderConfig
@ObservedObject var coordinator: DictationSessionCoordinator
@StateObject private var dictation = LiveDictationController()
@State private var statusText: String = ""
@State private var isSaving: Bool = false
var body: some View {
ZStack {
palette.background.ignoresSafeArea()
VStack(spacing: Spacing.lg) {
Spacer()
Image(systemName: "mic.circle.fill")
.font(.system(size: 84, weight: .light))
.foregroundStyle(palette.accent)
Text(titleText)
.font(TypeStyle.title2)
.foregroundStyle(palette.textPrimary)
Text(statusText)
.font(TypeStyle.caption)
.foregroundStyle(palette.textSecondary)
.multilineTextAlignment(.center)
.padding(.horizontal, Spacing.lg)
ProgressView(value: dictation.level, total: 1.0)
.tint(palette.accent)
.padding(.horizontal, Spacing.lg)
Spacer()
HStack(spacing: Spacing.sm) {
Button {
cancelAndClose()
} label: {
Text("common.cancel")
.secondaryButton()
}
.buttonStyle(.plain)
.disabled(isSaving)
Button {
stopAndFinalize()
} label: {
Text("common.done")
.primaryButton()
}
.buttonStyle(.plain)
.disabled(isSaving || dictation.phase != .recording)
}
.padding(.horizontal, Spacing.md)
.padding(.bottom, Spacing.lg)
}
}
.onAppear {
statusText = AppL10n.string("dictation.status.ready")
DictationBridge.setStatus(.requested)
startRecording()
}
.onDisappear {
dictation.stop()
}
.onChange(of: dictation.phase) { _, new in
switch new {
case .recording:
DictationBridge.setStatus(.recording)
statusText = config.isLocalEngine
? AppL10n.string("dictation.status.listeningLive")
: AppL10n.string("dictation.status.listening")
case .processing:
DictationBridge.setStatus(.transcribing)
statusText = AppL10n.string("dictation.status.processing")
case .requestingPermission:
statusText = AppL10n.string("dictation.status.requestingPermission")
case .denied(let message):
DictationBridge.setStatus(.error, message: message)
statusText = message
case .error(let message):
DictationBridge.setStatus(.error, message: message)
statusText = message
case .idle:
break
}
}
.onChange(of: dictation.currentPartial) { _, new in
guard config.isLocalEngine, !new.isEmpty else { return }
statusText = new
}
.onChange(of: dictation.lastFinal) { _, new in
guard !new.isEmpty else { return }
saveAndClose(new)
}
.onChange(of: config.uiLanguage) { _, _ in
refreshStatusForCurrentPhase()
}
}
private var titleText: String {
isSaving
? AppL10n.string("dictation.status.saving")
: AppL10n.string("dictation.title")
}
private func startRecording() {
Task { await dictation.start(localeId: config.localeId) }
}
private func stopAndFinalize() {
dictation.stop()
statusText = AppL10n.string("dictation.status.waitingResult")
}
private func cancelAndClose() {
dictation.stop()
DictationBridge.setStatus(.cancelled)
coordinator.dismiss()
dismiss()
}
private func saveAndClose(_ transcript: String) {
guard !isSaving else { return }
isSaving = true
statusText = AppL10n.string("dictation.status.processing")
Task {
let delivered = transcript
DictationBridge.storePendingTranscript(delivered, polishWarning: nil)
coordinator.dismiss()
dismiss()
}
}
private func refreshStatusForCurrentPhase() {
switch dictation.phase {
case .idle:
statusText = AppL10n.string("dictation.status.ready")
case .recording:
statusText = config.isLocalEngine
? AppL10n.string("dictation.status.listeningLive")
: AppL10n.string("dictation.status.listening")
case .processing:
statusText = AppL10n.string("dictation.status.processing")
case .requestingPermission:
statusText = AppL10n.string("dictation.status.requestingPermission")
case .denied(let message), .error(let message):
statusText = message
}
}
}
+1 -2
View File
@@ -337,8 +337,7 @@ struct HomeView: View {
EngineServiceLabel.summary( EngineServiceLabel.summary(
engineMode: config.engineMode, engineMode: config.engineMode,
providerId: config.providerId, providerId: config.providerId,
model: config.model, model: config.model
localASRBackend: config.localASRBackend
) )
) )
.font(TypeStyle.caption2) .font(TypeStyle.caption2)
+1 -2
View File
@@ -143,8 +143,7 @@ struct KeyboardPreviewSheet: View {
EngineServiceLabel.summary( EngineServiceLabel.summary(
engineMode: config.engineMode, engineMode: config.engineMode,
providerId: config.providerId, providerId: config.providerId,
model: config.model, model: config.model
localASRBackend: config.localASRBackend
) )
} }
+54
View File
@@ -0,0 +1,54 @@
// MainAppRoot.swift
// OSGKeyboard · Main App
//
// Host-app shell that owns `ProviderConfig` and `FlowSessionManager`.
// Only constructed when `AppGroup.isAvailable` so the error path never
// touches App Groupbacked singletons.
import SwiftUI
import OSGKeyboardShared
struct MainAppRoot: View {
@Environment(\.scenePhase) private var scenePhase
@StateObject private var config = ProviderConfig.shared
@StateObject private var flowManager = FlowSessionManager()
var body: some View {
Group {
if config.hasCompletedOnboarding {
MainTabView()
} else {
OnboardingView(config: config)
}
}
.environment(\.locale, config.uiLanguage.swiftUILocale)
.environmentObject(flowManager)
.onAppear {
flowManager.setAppForeground(scenePhase == .active)
}
.onOpenURL { url in
guard url.scheme == "osgkeyboard" else { return }
switch url.host {
case "startflow":
flowManager.startSession()
default:
break
}
}
.onChange(of: config.hasCompletedOnboarding) { _, done in
if done {
flowManager.autoStartIfNeeded()
}
}
.onChange(of: scenePhase) { _, phase in
flowManager.handleScenePhase(phase)
guard phase == .active, config.hasCompletedOnboarding else { return }
if flowManager.isActive {
flowManager.extendSession()
} else {
flowManager.autoStartIfNeeded()
}
}
}
}
+1 -3
View File
@@ -107,9 +107,7 @@ struct OnboardingView: View {
private func applyOnboardingDefaultsIfNeeded() { private func applyOnboardingDefaultsIfNeeded() {
guard !config.hasCompletedOnboarding, config.onboardingPage == 0 else { return } guard !config.hasCompletedOnboarding, config.onboardingPage == 0 else { return }
// First-time users with no API key: default to local for a faster path. // First-time users with no API key: default to local for a faster path.
// v0.2.0: no per-user ASR backend selection iOS `SpeechAnalyzer` // v0.2.0: iOS SpeechAnalyzer is the only on-device ASR path.
// is the only local option, so we don't need to mutate
// `config.localASRBackend` here.
if config.apiKey.isEmpty, config.engineMode == "cloud" { if config.apiKey.isEmpty, config.engineMode == "cloud" {
config.engineMode = "local" config.engineMode = "local"
} }
@@ -90,6 +90,7 @@ enum ProviderLogo {
case "qwen": return "qwen" case "qwen": return "qwen"
case "moonshot": return "moonshot" case "moonshot": return "moonshot"
case "zhipu": return "zhipu" case "zhipu": return "zhipu"
case "mimo": return "mimo"
case "custom": return "custom" case "custom": return "custom"
default: return nil default: return nil
} }
+1
View File
@@ -106,6 +106,7 @@
"provider.qwen" = "Qwen (DashScope)"; "provider.qwen" = "Qwen (DashScope)";
"provider.zhipu" = "Zhipu GLM"; "provider.zhipu" = "Zhipu GLM";
"provider.moonshot" = "Moonshot"; "provider.moonshot" = "Moonshot";
"provider.mimo" = "Xiaomi MiMo";
"provider.custom" = "Custom"; "provider.custom" = "Custom";
"settings.api.title" = "API"; "settings.api.title" = "API";
"settings.language.title" = "Language"; "settings.language.title" = "Language";
@@ -106,6 +106,7 @@
"provider.qwen" = "通义千问"; "provider.qwen" = "通义千问";
"provider.zhipu" = "智谱 GLM"; "provider.zhipu" = "智谱 GLM";
"provider.moonshot" = "月之暗面"; "provider.moonshot" = "月之暗面";
"provider.mimo" = "小米 MiMo";
"provider.custom" = "自定义"; "provider.custom" = "自定义";
"settings.api.title" = "接口"; "settings.api.title" = "接口";
"settings.language.title" = "语言"; "settings.language.title" = "语言";
File diff suppressed because it is too large Load Diff
@@ -34,7 +34,6 @@ public struct AppGroupPersistor {
// Both engines always polish; ignore legacy off/transcribe modeId. // Both engines always polish; ignore legacy off/transcribe modeId.
state.mode = .polish state.mode = .polish
state.engineMode = store.engineMode state.engineMode = store.engineMode
state.localASRBackend = store.localASRBackend
// v0.2.1 follow-up: only the target locale is persisted // v0.2.1 follow-up: only the target locale is persisted
// `translationEnabled` is derived from it. Hydrate once at // `translationEnabled` is derived from it. Hydrate once at
// startup; `refreshRuntimeFlags` keeps the chip in sync while // startup; `refreshRuntimeFlags` keeps the chip in sync while
@@ -42,16 +41,10 @@ public struct AppGroupPersistor {
state.translationTargetLocaleId = store.translationTargetLocaleId state.translationTargetLocaleId = store.translationTargetLocaleId
state.handednessPreference = store.handednessPreference state.handednessPreference = store.handednessPreference
state.cursorDragNavigationEnabled = store.cursorDragNavigationEnabled state.cursorDragNavigationEnabled = store.cursorDragNavigationEnabled
state.localModeCloudPolishEnabled = store.localModeCloudPolishEnabled
state.micDisabled = store.isCloudAPIKeyMissingForVoiceInput state.micDisabled = store.isCloudAPIKeyMissingForVoiceInput
state.micDisabledHint = store.isCloudAPIKeyMissingForVoiceInput state.micDisabledHint = store.isCloudAPIKeyMissingForVoiceInput
? ExtL10n.string("keyboard.mic.disabled.missingApiKey") ? ExtL10n.string("keyboard.mic.disabled.missingApiKey")
: "" : ""
// v0.2.0: iOS `SpeechAnalyzer` is always ready; mirror that
// into the State flags so downstream consumers see the same
// shape they did when the previous Qwen3 stack reported "ready".
state.localModelsReady = true
state.localModelsLoaded = false
#if DEBUG #if DEBUG
// Print a masked view of the live App Group config so we can see // Print a masked view of the live App Group config so we can see
@@ -74,7 +67,6 @@ public struct AppGroupPersistor {
model = \(store.model) model = \(store.model)
modeId = \(store.modeId) modeId = \(store.modeId)
localeId = \(store.localeId) localeId = \(store.localeId)
localASRBackend = \(store.localASRBackend.rawValue)
""") """)
#endif #endif
return .loaded return .loaded
@@ -93,8 +85,6 @@ public struct AppGroupPersistor {
guard AppGroup.isAvailable else { return } guard AppGroup.isAvailable else { return }
let store = AppGroupStore() let store = AppGroupStore()
state.engineMode = store.engineMode state.engineMode = store.engineMode
state.localASRBackend = store.localASRBackend
state.localModeCloudPolishEnabled = store.localModeCloudPolishEnabled
let shouldProtectTranslation = protectTranslationUntil.map { Date() < $0 } ?? false let shouldProtectTranslation = protectTranslationUntil.map { Date() < $0 } ?? false
if !shouldProtectTranslation { if !shouldProtectTranslation {
state.translationTargetLocaleId = store.translationTargetLocaleId state.translationTargetLocaleId = store.translationTargetLocaleId
@@ -105,11 +95,6 @@ public struct AppGroupPersistor {
state.micDisabledHint = store.isCloudAPIKeyMissingForVoiceInput state.micDisabledHint = store.isCloudAPIKeyMissingForVoiceInput
? ExtL10n.string("keyboard.mic.disabled.missingApiKey") ? ExtL10n.string("keyboard.mic.disabled.missingApiKey")
: "" : ""
// v0.2.0: iOS `SpeechAnalyzer` is always ready. Keep these
// toggles here so the keyboard UI doesn't flicker if the host
// app briefly clears them while refactoring.
state.localModelsReady = true
state.localModelsLoaded = false
} }
/// Persist `mode` to the App Group store. /// Persist `mode` to the App Group store.
@@ -130,12 +115,6 @@ public struct AppGroupPersistor {
AppGroupStore().setEngineMode(engineMode) AppGroupStore().setEngineMode(engineMode)
} }
/// Persist `localASRBackend` to the App Group store.
public func persist(localASRBackend: LocalASRBackend) {
guard AppGroup.isAvailable else { return }
AppGroupStore().setLocalASRBackend(localASRBackend)
}
/// v0.2.1: persist translation target locale id (e.g. `"en"`, /// v0.2.1: persist translation target locale id (e.g. `"en"`,
/// `"ja"`, or `TranslationLanguageCatalog.offLocaleId`). The /// `"ja"`, or `TranslationLanguageCatalog.offLocaleId`). The
/// chip / picker call this through `KeyboardState.setTranslationTargetLocaleId`. /// chip / picker call this through `KeyboardState.setTranslationTargetLocaleId`.
@@ -0,0 +1,127 @@
// CursorDragController.swift
// OSGKeyboard · Keyboard Extension
//
// Cursor-drag hint chrome and batched caret moves via textDocumentProxy.
import UIKit
import OSGKeyboardShared
@MainActor
final class CursorDragController {
private let state: KeyboardState
private let adjustTextPosition: (Int) -> Void
private weak var parentView: UIView?
private var cursorDragHintLabel: UILabel?
private var pendingHorizontalCursorSteps = 0
private var pendingVerticalCursorSteps = 0
private var cursorMoveFlushScheduled = false
private let cursorLineHaptic = UIImpactFeedbackGenerator(style: .light)
private static let cursorVerticalChunkSize = 20
init(
state: KeyboardState,
adjustTextPosition: @escaping (Int) -> Void
) {
self.state = state
self.adjustTextPosition = adjustTextPosition
}
func install(on view: UIView) {
parentView = view
let hint = UILabel()
hint.text = ExtL10n.string("keyboard.cursorDrag.centerHint")
hint.font = .systemFont(ofSize: 22, weight: .medium)
hint.textColor = UIColor.label.withAlphaComponent(0.10)
hint.textAlignment = .center
hint.numberOfLines = 1
hint.adjustsFontSizeToFitWidth = true
hint.minimumScaleFactor = 0.7
hint.isUserInteractionEnabled = false
hint.isHidden = true
hint.alpha = 0
view.addSubview(hint)
cursorDragHintLabel = hint
layoutChrome()
}
func layoutChrome() {
guard let view = parentView else { return }
cursorDragHintLabel?.frame = view.bounds
}
func setCursorDragActive(_ active: Bool) {
state.cursorDragActive = active
updateCursorDragWash(active: active)
}
func moveCursorHorizontally(by steps: Int) {
guard steps != 0 else { return }
pendingHorizontalCursorSteps += steps
scheduleCursorMoveFlush()
}
func moveCursorVertically(by steps: Int) {
guard steps != 0 else { return }
pendingVerticalCursorSteps += steps
scheduleCursorMoveFlush()
}
private func updateCursorDragWash(active: Bool) {
if active {
cursorLineHaptic.prepare()
}
layoutChrome()
guard let hint = cursorDragHintLabel else { return }
if active {
hint.isHidden = false
UIView.animate(withDuration: 0.12) { hint.alpha = 1 }
} else {
UIView.animate(withDuration: 0.12, animations: { hint.alpha = 0 }) { [weak self] _ in
guard let self, !self.state.cursorDragActive else { return }
hint.isHidden = true
}
}
}
private func scheduleCursorMoveFlush() {
guard !cursorMoveFlushScheduled else { return }
cursorMoveFlushScheduled = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.012) { [weak self] in
guard let self else { return }
self.cursorMoveFlushScheduled = false
let horizontal = self.pendingHorizontalCursorSteps
let vertical = self.pendingVerticalCursorSteps
self.pendingHorizontalCursorSteps = 0
self.pendingVerticalCursorSteps = 0
if horizontal != 0 {
OSGLog.keyboardExt.info("adjustTextPosition h=\(horizontal)")
self.adjustTextPosition(horizontal)
}
if vertical != 0 {
self.applyVerticalCursorSteps(vertical)
}
if self.pendingHorizontalCursorSteps != 0 || self.pendingVerticalCursorSteps != 0 {
self.scheduleCursorMoveFlush()
}
}
}
private func applyVerticalCursorSteps(_ steps: Int) {
let direction = steps > 0 ? 1 : -1
var remaining = abs(steps)
let chunk = Self.cursorVerticalChunkSize
while remaining > 0 {
adjustTextPosition(direction * chunk)
cursorLineHaptic.impactOccurred()
cursorLineHaptic.prepare()
remaining -= 1
}
}
}
@@ -0,0 +1,124 @@
// KeyboardConfigSync.swift
// OSGKeyboard · Keyboard Extension
//
// App Group config hydration, Darwin observers, and onboarding mirroring.
import Foundation
import OSGKeyboardShared
@MainActor
final class KeyboardConfigSync {
private let state: KeyboardState
private let persistor: AppGroupPersistor
private let onFlowSessionChanged: () -> Void
/// Grace period after a chip-side translation write during which the
/// 1 Hz App Group poll must not overwrite `translationTargetLocaleId`.
var translationConfigProtectedUntil: Date?
private var flowSessionDarwinObserver: FlowSessionDarwinObserver?
private var transcriptionDarwinObserver: FlowSessionDarwinObserver?
private var configDarwinObserver: FlowSessionDarwinObserver?
init(
state: KeyboardState,
persistor: AppGroupPersistor,
onFlowSessionChanged: @escaping () -> Void
) {
self.state = state
self.persistor = persistor
self.onFlowSessionChanged = onFlowSessionChanged
}
func installDarwinObservers() {
flowSessionDarwinObserver = FlowSessionDarwinObserver { [weak self] in
self?.onFlowSessionChanged()
}
transcriptionDarwinObserver = FlowSessionDarwinObserver(
notificationName: FlowSessionDarwin.transcriptionNotificationName
) { [weak self] in
self?.onFlowSessionChanged()
}
configDarwinObserver = FlowSessionDarwinObserver(
notificationName: AppGroupConfigDarwin.notificationName
) { [weak self] in
self?.refreshConfigFromAppGroup()
}
}
func loadPersistedConfig() -> AppGroupLoadResult {
switch persistor.load(into: state) {
case .loaded:
OSGLog.keyboardExt.info(
"config loaded — cursorDragNavigationEnabled=\(self.state.cursorDragNavigationEnabled)"
)
syncOnboardingStateFromAppGroup()
return .loaded
case .unavailable:
state.phase = .error(
.appGroupUnavailable,
message: ExtL10n.string("keyboard.error.appGroupUnavailable")
)
return .unavailable
}
}
func refreshConfigFromAppGroup() {
persistor.refreshRuntimeFlags(
into: state,
protectTranslationUntil: translationConfigProtectedUntil
)
}
func syncOnboardingStateFromAppGroup() {
let store = AppGroupStore()
state.hasCompletedOnboarding = store.hasCompletedOnboarding
state.onboardingPage = store.onboardingPage
}
func autoAdvancePastKeyboardSetupStepIfNeeded() {
guard !state.hasCompletedOnboarding else { return }
guard state.onboardingPage == 3 else { return }
guard KeyboardSetupBridge.isReadyForOnboardingSkip else { return }
let store = AppGroupStore()
store.setOnboardingPage(4)
state.onboardingPage = 4
}
func advanceOnboarding() {
let store = AppGroupStore()
let nextPage = min(4, store.onboardingPage + 1)
store.setOnboardingPage(nextPage)
state.onboardingPage = nextPage
}
func completeOnboarding() {
let store = AppGroupStore()
store.setHasCompletedOnboarding(true)
store.setOnboardingPage(4)
state.hasCompletedOnboarding = true
state.onboardingPage = 4
}
func persistLocale(_ id: String) {
state.localeId = id
persistor.persist(localeId: id)
}
func persistEngineMode(_ mode: String) {
state.engineMode = mode
persistor.persist(engineMode: mode)
}
func persistTranslationTargetLocaleId(_ id: String) {
let resolved = TranslationLanguageCatalog.resolve(id).id
state.translationTargetLocaleId = resolved
translationConfigProtectedUntil = Date().addingTimeInterval(2.5)
persistor.persist(translationTargetLocaleId: resolved)
}
func persistMode(_ mode: KeyboardState.InputMode) {
state.mode = mode
persistor.persist(mode: mode)
}
}
@@ -0,0 +1,412 @@
// KeyboardFlowCoordinator.swift
// OSGKeyboard · Keyboard Extension
//
// Flow session start, recording, watchdogs, and result delivery handling.
import UIKit
import OSGKeyboardShared
@MainActor
final class KeyboardFlowCoordinator {
private enum FlowWatchdog {
static let pollIntervalNs: UInt64 = 200_000_000
/// Give the user time to manually open the host app when auto-jump fails.
static let startTimeout: TimeInterval = 30
static func resultTimeout(engineMode: String) -> TimeInterval {
FlowSessionKeys.keyboardResultTimeout(engineMode: engineMode)
}
}
private let state: KeyboardState
private let textInserter: KeyboardTextInserter
private let hasFullAccess: () -> Bool
private let wakeLockView: () -> UIView?
private let openHostApp: (String) -> Void
private let detectAndStoreAppContext: () -> Void
private let scheduleAutoClearError: () -> Void
private let refreshConfigFromAppGroup: () -> Void
private var isPendingFlowStart = false
private var flowStartDeadline: TimeInterval = 0
private var isFlowRecording = false
private var flowWatchdogTask: Task<Void, Never>?
private var utteranceTimerTask: Task<Void, Never>?
private var utteranceStartedAt: TimeInterval = 0
private var wasFlowSessionActive = false
private var flowSessionMonitorTask: Task<Void, Never>?
private var isAwaitingFlowResult = false
private var lastFlowAutoStartAttempt: TimeInterval = 0
private static let flowAutoStartCooldown: TimeInterval = 20
init(
state: KeyboardState,
textInserter: KeyboardTextInserter,
hasFullAccess: @escaping () -> Bool,
wakeLockView: @escaping () -> UIView?,
openHostApp: @escaping (String) -> Void,
detectAndStoreAppContext: @escaping () -> Void,
scheduleAutoClearError: @escaping () -> Void,
refreshConfigFromAppGroup: @escaping () -> Void
) {
self.state = state
self.textInserter = textInserter
self.hasFullAccess = hasFullAccess
self.wakeLockView = wakeLockView
self.openHostApp = openHostApp
self.detectAndStoreAppContext = detectAndStoreAppContext
self.scheduleAutoClearError = scheduleAutoClearError
self.refreshConfigFromAppGroup = refreshConfigFromAppGroup
}
var preservesLifecycleOnDisappear: Bool {
isPendingFlowStart || isFlowRecording || isAwaitingFlowResult
}
func startSessionMonitor() {
flowSessionMonitorTask?.cancel()
flowSessionMonitorTask = Task { @MainActor [weak self] in
while !Task.isCancelled {
self?.refreshSessionState()
try? await Task.sleep(nanoseconds: 1_000_000_000)
}
}
}
func stopSessionMonitor() {
flowSessionMonitorTask?.cancel()
flowSessionMonitorTask = nil
}
func refreshSessionState() {
refreshConfigFromAppGroup()
refreshFlowPartialIfNeeded()
consumePendingFlowDeliveryIfNeeded()
let active = FlowSessionBridge.isSessionActive()
state.flowSessionActive = active
if wasFlowSessionActive && !active && !isFlowRecording && !isPendingFlowStart {
switch state.phase {
case .recording, .processing:
break
default:
showFlowSessionExpiredHint()
}
}
wasFlowSessionActive = active
if !active {
maybeAutoStartFlowSession()
}
}
func toggleRecording() {
switch state.phase {
case .recording:
pressEnded()
case .idle, .denied, .error:
pressBegan()
case .requestingPermissions, .processing:
break
}
}
func pressBegan() {
switch state.phase {
case .idle, .denied, .error:
break
default:
return
}
guard !state.micDisabled else { return }
guard hasFullAccess() else {
let msg = ExtL10n.string("keyboard.error.fullAccessRequired")
state.phase = .error(.fullAccessRequired, message: msg)
scheduleAutoClearError()
return
}
guard AppGroup.isAvailable else {
let msg = ExtL10n.string("keyboard.error.appGroupCommunication")
state.phase = .error(.appGroupUnavailable, message: msg)
scheduleAutoClearError()
return
}
detectAndStoreAppContext()
if FlowSessionBridge.isSessionActive() {
startFlowRecording()
} else {
beginFlowStart()
}
}
func pressEnded() {
if isPendingFlowStart {
cancelPendingFlowStart()
return
}
guard isFlowRecording else { return }
isFlowRecording = false
stopUtteranceCountdown()
ExtensionScreenWakeLock.release()
FlowSessionBridge.setRecordingState(.stopped)
state.phase = .processing
state.lastTranscript = ExtL10n.string("keyboard.flow.transcribing")
startFlowResultWatchdog()
}
func beginFlowStart() {
guard !isPendingFlowStart else { return }
isPendingFlowStart = true
isFlowRecording = false
flowStartDeadline = Date().timeIntervalSince1970 + FlowWatchdog.startTimeout
state.lastTranscript = ExtL10n.string("keyboard.flow.startingSession")
state.phase = .processing
openHostApp("startflow")
startFlowStartWatchdog()
debug("beginFlowStart")
}
func handleHostAppOpenResult(path: String, success: Bool) {
debug("openHostApp path=\(path) success=\(success)")
guard !success else { return }
if path == "startflow", isPendingFlowStart {
state.lastTranscript = ExtL10n.string("keyboard.flow.manualOpenHost")
return
}
showManualOpenHint(path: path)
}
func cancelPipelineUnlessAwaitingResult() {
guard !isAwaitingFlowResult else { return }
if isFlowRecording || isPendingFlowStart {
if isFlowRecording {
FlowSessionBridge.setRecordingState(.aborted)
ExtensionScreenWakeLock.release()
}
isFlowRecording = false
isPendingFlowStart = false
stopUtteranceCountdown()
stopFlowWatchdog()
state.level = 0
}
}
// MARK: - Private
private func consumePendingFlowDeliveryIfNeeded() {
if isAwaitingFlowResult {
if let delivery = FlowSessionBridge.consumeTranscriptionDelivery() {
isAwaitingFlowResult = false
stopFlowWatchdog()
textInserter.handleFlowTranscript(delivery)
return
}
if let error = FlowSessionBridge.consumeTranscriptionError() {
isAwaitingFlowResult = false
stopFlowWatchdog()
state.phase = .error(
.fromFlowTranscription(error),
message: error.message
)
scheduleAutoClearError()
return
}
}
if isPendingFlowStart, FlowSessionBridge.isSessionActive() {
completeFlowStartHandoff()
}
}
private func maybeAutoStartFlowSession() {
guard !FlowSessionBridge.isSessionActive() else { return }
guard !isPendingFlowStart, !isFlowRecording, !isAwaitingFlowResult else { return }
guard hasFullAccess(), AppGroup.isAvailable else { return }
guard case .idle = state.phase else { return }
let now = Date().timeIntervalSince1970
guard now - lastFlowAutoStartAttempt >= Self.flowAutoStartCooldown else { return }
lastFlowAutoStartAttempt = now
beginFlowStart()
}
private func showFlowSessionExpiredHint() {
let message = ExtL10n.string("keyboard.flow.sessionExpired")
state.phase = .error(.flowSessionExpired, message: message)
scheduleAutoClearError()
}
private func showManualOpenHint(path: String) {
let msg: String
if !hasFullAccess() {
msg = ExtL10n.string("keyboard.error.fullAccessForJump")
} else if path == "settings" {
msg = ExtL10n.string("keyboard.error.manualOpenSettings")
} else if path == "startflow" {
msg = ExtL10n.string("keyboard.error.manualOpenForFlow")
} else {
msg = ExtL10n.string("keyboard.error.manualOpenSettings")
}
state.phase = .error(.manualOpenRequired, message: msg)
scheduleAutoClearError()
}
private func startFlowRecording() {
isPendingFlowStart = false
flowStartDeadline = 0
stopFlowWatchdog()
FlowSessionBridge.setTranscriptionLanguage(state.localeId)
FlowSessionBridge.setRecordingState(.recording)
isFlowRecording = true
state.lastTranscript = ""
state.phase = .recording
if let view = wakeLockView() {
ExtensionScreenWakeLock.acquire(from: view)
}
startUtteranceCountdown()
startFlowLevelWatchdog()
debug("startFlowRecording")
}
private func startUtteranceCountdown() {
utteranceStartedAt = Date().timeIntervalSince1970
state.utteranceRemainingSeconds = Int(FlowSessionKeys.maxUtteranceDuration)
utteranceTimerTask?.cancel()
utteranceTimerTask = Task { @MainActor [weak self] in
while let self, self.isFlowRecording, !Task.isCancelled {
let elapsed = Date().timeIntervalSince1970 - self.utteranceStartedAt
let remaining = max(0, Int(ceil(FlowSessionKeys.maxUtteranceDuration - elapsed)))
self.state.utteranceRemainingSeconds = remaining
if remaining <= 0 {
self.pressEnded()
return
}
try? await Task.sleep(nanoseconds: 200_000_000)
}
}
}
private func stopUtteranceCountdown() {
utteranceTimerTask?.cancel()
utteranceTimerTask = nil
state.utteranceRemainingSeconds = Int(FlowSessionKeys.maxUtteranceDuration)
}
private func cancelPendingFlowStart() {
isPendingFlowStart = false
flowStartDeadline = 0
stopFlowWatchdog()
state.phase = .idle
state.lastTranscript = ""
}
private func startFlowStartWatchdog() {
stopFlowWatchdog()
flowWatchdogTask = Task { @MainActor [weak self] in
while let self, !Task.isCancelled, self.isPendingFlowStart {
if FlowSessionBridge.isSessionActive() {
self.completeFlowStartHandoff()
return
}
let now = Date().timeIntervalSince1970
if self.flowStartDeadline > 0, now > self.flowStartDeadline {
self.isPendingFlowStart = false
self.flowStartDeadline = 0
self.showManualOpenHint(path: "startflow")
return
}
try? await Task.sleep(nanoseconds: FlowWatchdog.pollIntervalNs)
}
}
}
private func completeFlowStartHandoff() {
isPendingFlowStart = false
flowStartDeadline = 0
stopFlowWatchdog()
state.lastTranscript = ""
state.phase = .idle
refreshSessionState()
debug("completeFlowStartHandoff")
}
private func startFlowLevelWatchdog() {
stopFlowWatchdog()
flowWatchdogTask = Task { @MainActor [weak self] in
while let self, !Task.isCancelled, self.isFlowRecording {
let levels = FlowSessionBridge.audioLevels()
if let peak = levels.max(), peak > 0 {
self.state.level = Double(peak)
}
self.refreshFlowPartialIfNeeded()
try? await Task.sleep(nanoseconds: FlowWatchdog.pollIntervalNs)
}
}
}
private func refreshFlowPartialIfNeeded() {
guard isFlowRecording || isAwaitingFlowResult else { return }
switch state.phase {
case .recording, .processing:
if let partial = FlowSessionBridge.transcriptionPartial() {
state.lastTranscript = partial
}
default:
break
}
}
private func startFlowResultWatchdog() {
stopFlowWatchdog()
isAwaitingFlowResult = true
let startedAt = Date().timeIntervalSince1970
let resultTimeout = FlowWatchdog.resultTimeout(engineMode: state.engineMode)
flowWatchdogTask = Task { @MainActor [weak self] in
while let self, !Task.isCancelled {
if let delivery = FlowSessionBridge.consumeTranscriptionDelivery() {
self.isAwaitingFlowResult = false
self.stopFlowWatchdog()
self.textInserter.handleFlowTranscript(delivery)
return
}
if let error = FlowSessionBridge.consumeTranscriptionError() {
self.isAwaitingFlowResult = false
self.stopFlowWatchdog()
self.state.phase = .error(
.fromFlowTranscription(error),
message: error.message
)
self.scheduleAutoClearError()
return
}
self.refreshFlowPartialIfNeeded()
let now = Date().timeIntervalSince1970
if now - startedAt > resultTimeout {
self.isAwaitingFlowResult = false
self.stopFlowWatchdog()
let msg = ExtL10n.string("keyboard.flow.resultTimeout")
self.state.phase = .error(.flowResultTimeout, message: msg)
self.scheduleAutoClearError()
return
}
try? await Task.sleep(nanoseconds: FlowWatchdog.pollIntervalNs)
}
}
}
private func stopFlowWatchdog() {
flowWatchdogTask?.cancel()
flowWatchdogTask = nil
}
private func debug(_ message: String) {
OSGLog.keyboardExt.info("\(message, privacy: .public)")
}
}
@@ -0,0 +1,44 @@
// KeyboardTextInserter.swift
// OSGKeyboard · Keyboard Extension
//
// Inserts Flow transcripts from the host app and surfaces polish warnings
// without re-running LLM polish in the extension.
import OSGKeyboardShared
@MainActor
final class KeyboardTextInserter {
private let state: KeyboardState
private let insertText: (String) -> Void
private let scheduleAutoClearError: () -> Void
init(
state: KeyboardState,
insertText: @escaping (String) -> Void,
scheduleAutoClearError: @escaping () -> Void
) {
self.state = state
self.insertText = insertText
self.scheduleAutoClearError = scheduleAutoClearError
}
func handleFlowTranscript(_ delivery: TranscriptionDelivery) {
let trimmed = delivery.text.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else {
state.phase = .idle
state.level = 0
return
}
// Host app already polished when configured; keyboard only inserts.
insertText(trimmed)
state.lastTranscript = ""
state.level = 0
if let warning = delivery.polishWarning {
state.phase = .error(.polishDegraded(warning), message: warning)
scheduleAutoClearError()
} else {
state.phase = .idle
}
OSGLog.keyboardExt.info("flow insert length=\(trimmed.count, privacy: .public)")
}
}
@@ -141,9 +141,6 @@ public struct KeyboardRootView: View {
flowSessionActive: state.flowSessionActive, flowSessionActive: state.flowSessionActive,
micDisabled: state.micDisabled, micDisabled: state.micDisabled,
micDisabledHint: state.micDisabledHint, micDisabledHint: state.micDisabledHint,
isLocalEngine: state.isLocalEngine,
localModelsReady: state.localModelsReady,
localModelsLoaded: state.localModelsLoaded,
cursorDragHintActive: state.cursorDragActive, cursorDragHintActive: state.cursorDragActive,
openSettings: state.openSettings, openSettings: state.openSettings,
startFlowSession: state.startFlowSession startFlowSession: state.startFlowSession
@@ -337,9 +334,6 @@ private struct TranscriptLine: View {
let flowSessionActive: Bool let flowSessionActive: Bool
let micDisabled: Bool let micDisabled: Bool
let micDisabledHint: String let micDisabledHint: String
let isLocalEngine: Bool
let localModelsReady: Bool
let localModelsLoaded: Bool
let cursorDragHintActive: Bool let cursorDragHintActive: Bool
let openSettings: () -> Void let openSettings: () -> Void
let startFlowSession: () -> Void let startFlowSession: () -> Void
@@ -367,22 +361,6 @@ private struct TranscriptLine: View {
.foregroundStyle(palette.warning) .foregroundStyle(palette.warning)
.lineLimit(1) .lineLimit(1)
.truncationMode(.tail) .truncationMode(.tail)
} else if isLocalEngine, !localModelsReady {
Button(action: openSettings) {
HStack(spacing: 4) {
Text(ExtL10n.string("keyboard.models.notDownloaded"))
Image(systemName: "chevron.right")
.font(.system(size: 10, weight: .semibold))
}
.font(TypeStyle.caption)
.foregroundStyle(palette.warning)
.lineLimit(1)
.truncationMode(.tail)
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.accessibilityHint(ExtL10n.text("keyboard.models.downloadHint"))
} else if flowSessionActive { } else if flowSessionActive {
ExtL10n.text("keyboard.placeholder.idle") ExtL10n.text("keyboard.placeholder.idle")
.font(TypeStyle.caption) .font(TypeStyle.caption)
-148
View File
@@ -1,148 +0,0 @@
// TranslationChip.swift
// OSGKeyboard · Keyboard Extension
//
// Compact chip rendered to the right of `LocaleChip` on the keyboard
// top bar. Doubles as both the on/off switch and the target-language
// picker same Menu pattern as `LocaleChip` so muscle memory transfers.
//
// v0.2.1 follow-up: removed the explicit on/off toggle entry. The
// chip is now a pure picker over the 11 catalog rows (off + 10
// locales); selecting "" turns translation off, selecting any
// locale turns it on with that target. `translationEnabled` is
// derived from the locale id so the chip / pipeline read the same
// source of truth.
//
// v0.2.1 final review: dropped the "needs cloud" warning state
// both engines now run the translate-and-polish step (the local
// engine routes through DeepSeek via
// `ProviderConfig.localModeProviderId`). The chip is therefore just
// off / on, with the same accent treatment either way.
//
// Visual states:
// off dim outline, "" chip label (menu first row = "")
// on (any engine) accent fill, " EN" / " " style label
//
// Stays in the same visual family as `CloudEngineChip` / `LocaleChip`
// (Capsule + 28 pt min height + 6 pt vertical padding) so the top bar
// doesn't grow when translation is enabled.
import SwiftUI
import OSGKeyboardShared
struct TranslationChip: View, Equatable {
/// Passed in as a value (not read from `@Environment`) so the chip can
/// be wrapped in `.equatable()` at the call site: `EquatableView`
/// suppresses environment-driven refreshes, so injecting the palette
/// here keeps colours correct across dark/light switches.
let palette: ThemePalette
/// The active target-locale id (`offLocaleId` == translation off).
let targetLocaleId: String
/// Writes the picked locale id wired to `state.setTranslationTargetLocaleId`.
let onSelect: (String) -> Void
/// Only `palette` and `targetLocaleId` drive the visuals; the
/// `onSelect` closure is deliberately excluded from equality. Because
/// the keyboard polls the App Group at 1 Hz (each poll re-publishes the
/// `KeyboardState`), the parent view re-renders every second. Without
/// this, SwiftUI would rebuild the `Menu` on every poll dismissing an
/// open picker or snapping its scroll position back to the top. With
/// `.equatable()` the picker is rebuilt only on a real state change.
nonisolated static func == (lhs: TranslationChip, rhs: TranslationChip) -> Bool {
lhs.palette == rhs.palette && lhs.targetLocaleId == rhs.targetLocaleId
}
var body: some View {
Menu {
// v0.2.1 follow-up: pure picker over the full catalog,
// including `offLocaleId` at the top so "turn off" is one
// tap from any enabled state. Picking a row writes
// `translationTargetLocaleId`; `translationEnabled` is
// derived from it.
ForEach(TranslationLanguageCatalog.all) { language in
Button {
onSelect(language.id)
} label: {
if language.id == targetLocaleId {
Label(displayLabel(for: language), systemImage: "checkmark")
} else {
Text(displayLabel(for: language))
}
}
}
} label: {
label
}
.menuStyle(.button)
.accessibilityLabel(ExtL10n.text("keyboard.translation.a11y"))
.accessibilityHint(ExtL10n.text("keyboard.translation.a11yHint"))
}
@ViewBuilder
private var label: some View {
let target = TranslationLanguageCatalog.resolve(targetLocaleId)
let enabled = targetLocaleId != TranslationLanguageCatalog.offLocaleId
HStack(spacing: 4) {
Image(systemName: enabled ? "character.bubble" : "character.bubble.fill")
Text(chipLabel(target: target, enabled: enabled))
Image(systemName: "chevron.down")
.font(.system(size: 8, weight: .bold))
}
.font(TypeStyle.caption2)
.foregroundStyle(foreground(enabled: enabled))
.padding(.horizontal, Spacing.xs + 2)
.padding(.vertical, 6)
.frame(minHeight: 28)
.background(background(enabled: enabled), in: Capsule())
.overlay(Capsule().stroke(stroke(enabled: enabled), lineWidth: 0.5))
}
private func displayLabel(for language: TranslationLanguage) -> String {
if language.id == TranslationLanguageCatalog.offLocaleId {
return ExtL10n.string("keyboard.translation.offMenu")
}
return language.nativeName
}
private func chipLabel(target: TranslationLanguage, enabled: Bool) -> String {
if !enabled {
return ExtL10n.string("keyboard.translation.chip")
}
// Short form: "EN" / "" style. Falls back to the prompt
// language name for languages without a chip-style abbreviation
// (e.g. French "FR" via the 2-letter prefix).
let short = shortLabel(for: target)
return "\(short)"
}
private func shortLabel(for target: TranslationLanguage) -> String {
switch target.id {
case "en": return "EN"
case "zh-Hans": return ""
case "zh-Hant": return ""
case "ja": return ""
case "ko": return ""
case "fr": return "FR"
case "de": return "DE"
case "es": return "ES"
case "ru": return "RU"
case "pt": return "PT"
default: return target.promptLanguageName
}
}
private func foreground(enabled: Bool) -> Color {
if enabled { return palette.accent }
return palette.textPrimary
}
private func background(enabled: Bool) -> Color {
if enabled { return palette.accent.opacity(0.15) }
return palette.surfaceElevated
}
private func stroke(enabled: Bool) -> Color {
if enabled { return palette.accent.opacity(0.35) }
return palette.divider
}
}
+53 -15
View File
@@ -60,23 +60,61 @@ final class KeyboardStateTests: XCTestCase {
} }
} }
func testModeSwitchFromPolishToOff() { func testFlowStructuredErrorKinds() {
let s = KeyboardState() let s = KeyboardState()
XCTAssertEqual(s.mode, .polish) s.phase = .error(.manualOpenRequired, message: "open app")
s.mode = .off if case .error(.manualOpenRequired, let msg) = s.phase {
XCTAssertEqual(s.mode, .off) XCTAssertEqual(msg, "open app")
s.mode = .transcribe } else {
XCTAssertEqual(s.mode, .transcribe) XCTFail("expected manualOpenRequired")
s.mode = .polish
XCTAssertEqual(s.mode, .polish)
} }
func testInputModeRoundTripsThroughRawValue() { s.phase = .error(.polishDegraded("warn"), message: "warn")
// The mode is persisted by rawValue (see `AppGroupStore.setModeId`) if case .error(.polishDegraded("warn"), _) = s.phase {} else {
// so the round-trip is part of the public contract. XCTFail("expected polishDegraded")
for mode in KeyboardState.InputMode.allCases { }
let raw = mode.rawValue
XCTAssertNotNil(KeyboardState.InputMode(rawValue: raw)) s.phase = .error(.flowResultTimeout, message: "timeout")
} if case .error(.flowResultTimeout, _) = s.phase {} else {
XCTFail("expected flowResultTimeout")
}
s.phase = .error(.flowSessionExpired, message: "expired")
if case .error(.flowSessionExpired, _) = s.phase {} else {
XCTFail("expected flowSessionExpired")
}
s.phase = .error(.fullAccessRequired, message: "full access")
if case .error(.fullAccessRequired, _) = s.phase {} else {
XCTFail("expected fullAccessRequired")
}
s.phase = .error(.noSpeechDetected, message: "no speech")
if case .error(.noSpeechDetected, _) = s.phase {} else {
XCTFail("expected noSpeechDetected")
}
s.phase = .error(.recognitionInterrupted, message: "interrupted")
if case .error(.recognitionInterrupted, _) = s.phase {} else {
XCTFail("expected recognitionInterrupted")
}
s.phase = .error(.hostAudioUnavailable, message: "audio")
if case .error(.hostAudioUnavailable, _) = s.phase {} else {
XCTFail("expected hostAudioUnavailable")
}
let flowError = FlowTranscriptionError(message: "asr failed", kind: .asrFailed)
XCTAssertEqual(
KeyboardState.Phase.ErrorKind.fromFlowTranscription(flowError),
.hostTranscriptionFailed("asr failed")
)
}
func testInputModeIsPolishOnly() {
let s = KeyboardState()
XCTAssertEqual(s.mode, .polish)
XCTAssertEqual(KeyboardState.InputMode.allCases, [.polish])
XCTAssertEqual(KeyboardState.InputMode(rawValue: "polish"), .polish)
} }
} }
+15 -9
View File
@@ -23,6 +23,15 @@ public enum AppGroup {
) != nil ) != nil
}() }()
/// Shared UserDefaults when the App Group suite is available; `nil` otherwise.
///
/// Prefer this in Release builds and in the keyboard extension so callers
/// can surface a setup error instead of silently reading/writing the wrong suite.
public static var defaultsIfAvailable: UserDefaults? {
guard isAvailable else { return nil }
return UserDefaults(suiteName: identifier)
}
/// Shared UserDefaults instance for cross-process config. /// Shared UserDefaults instance for cross-process config.
/// ///
/// In DEBUG builds a missing App Group is a hard `fatalError`: silently /// In DEBUG builds a missing App Group is a hard `fatalError`: silently
@@ -32,14 +41,10 @@ public enum AppGroup {
/// the App an API key and nothing happens" which is exactly the bug /// the App an API key and nothing happens" which is exactly the bug
/// this is meant to prevent. /// this is meant to prevent.
/// ///
/// In release builds we keep the soft fallback + `NSLog` so an /// In Release builds there is **no** `.standard` fallback use
/// end-user whose developer account simply lacks the App Group still /// `defaultsIfAvailable` and handle `nil` when provisioning is missing.
/// gets a usable main App (the keyboard extension won't work, but at
/// least the App doesn't crash on launch).
public static var defaults: UserDefaults { public static var defaults: UserDefaults {
if let d = UserDefaults(suiteName: identifier) { guard let suite = defaultsIfAvailable else {
return d
}
#if DEBUG #if DEBUG
fatalError(""" fatalError("""
App Group \(identifier) unavailable. App Group \(identifier) unavailable.
@@ -57,8 +62,9 @@ public enum AppGroup {
only way to make the misconfiguration impossible to miss. only way to make the misconfiguration impossible to miss.
""") """)
#else #else
NSLog("⚠️ [OSGKeyboard] App Group \(identifier) unavailable, falling back to .standard. The keyboard extension will not see config written by the main app.") fatalError("App Group \(identifier) unavailable. Check entitlements and provisioning.")
return .standard
#endif #endif
} }
return suite
}
} }
@@ -1,32 +1,30 @@
// RecordButton.swift // RecordButton.swift
// OSGKeyboard · Keyboard Extension // OSGKeyboard · Shared
// //
// Tap-to-toggle mic: tap once to start, tap again to stop. Shows a // Tap-to-toggle mic button shared between the keyboard extension and
// remaining-time countdown while recording; last 10 seconds turn red. // host-app keyboard preview surfaces.
import SwiftUI import SwiftUI
import OSGKeyboardShared
struct RecordButton: View { public struct RecordButton: View {
@Environment(\.themePalette) private var palette: ThemePalette @Environment(\.themePalette) private var palette: ThemePalette
enum Phase: Equatable { public enum Phase: Equatable {
case idle case idle
case recording case recording
case processing case processing
case error case error
} }
let phase: Phase public let phase: Phase
let level: Double // 0...1 public let level: Double
/// Seconds left in the current utterance; shown only while recording. public let remainingSeconds: Int?
let remainingSeconds: Int? public let isEnabled: Bool
let isEnabled: Bool public let onToggle: () -> Void
let onToggle: () -> Void
@State private var breath: Bool = false @State private var breath = false
init( public init(
phase: Phase, phase: Phase,
level: Double, level: Double,
remainingSeconds: Int? = nil, remainingSeconds: Int? = nil,
@@ -45,8 +43,6 @@ struct RecordButton: View {
return remainingSeconds <= 10 return remainingSeconds <= 10
} }
/// Decorative rings are sized to stay inside the 121 pt frame applied
/// by `KeyboardRootView` so glow / breath animations are not clipped.
private enum Layout { private enum Layout {
static let disc: CGFloat = 95 static let disc: CGFloat = 95
static let outerRing: CGFloat = 106 static let outerRing: CGFloat = 106
@@ -54,7 +50,7 @@ struct RecordButton: View {
static let glow: CGFloat = 119 static let glow: CGFloat = 119
} }
var body: some View { public var body: some View {
ZStack { ZStack {
Circle() Circle()
.stroke(palette.recordRed.opacity(isUrgent ? 0.55 : 0.35), lineWidth: isUrgent ? 3 : 2) .stroke(palette.recordRed.opacity(isUrgent ? 0.55 : 0.35), lineWidth: isUrgent ? 3 : 2)
@@ -79,10 +75,7 @@ struct RecordButton: View {
.animation(Motion.soft, value: level) .animation(Motion.soft, value: level)
Circle() Circle()
.stroke( .stroke(Color.white.opacity(phase == .idle ? 0.08 : 0.12), lineWidth: 0.5)
Color.white.opacity(phase == .idle ? 0.08 : 0.12),
lineWidth: 0.5
)
.frame(width: Layout.outerRing, height: Layout.outerRing) .frame(width: Layout.outerRing, height: Layout.outerRing)
ZStack { ZStack {
@@ -106,7 +99,6 @@ struct RecordButton: View {
.foregroundStyle(.white) .foregroundStyle(.white)
.monospacedDigit() .monospacedDigit()
.contentTransition(.numericText()) .contentTransition(.numericText())
//
.offset(y: 3) .offset(y: 3)
} }
WaveformView( WaveformView(
@@ -145,13 +137,13 @@ struct RecordButton: View {
.onChange(of: phase) { _, new in .onChange(of: phase) { _, new in
breath = (new == .recording) breath = (new == .recording)
} }
.accessibilityLabel(ExtL10n.text("keyboard.tapToTalkA11y")) .accessibilityLabel(Text(SharedL10n.string("keyboard.tapToTalkA11y")))
} }
private func formatRemaining(_ seconds: Int) -> String { private func formatRemaining(_ seconds: Int) -> String {
let m = seconds / 60 let minutes = seconds / 60
let s = seconds % 60 let remainder = seconds % 60
return String(format: "%d:%02d", m, s) return String(format: "%d:%02d", minutes, remainder)
} }
private var discGradient: LinearGradient { private var discGradient: LinearGradient {
@@ -175,10 +167,7 @@ struct RecordButton: View {
) )
case .idle: case .idle:
return LinearGradient( return LinearGradient(
colors: [ colors: [palette.accent.opacity(0.95), palette.accent.opacity(0.75)],
palette.accent.opacity(0.95),
palette.accent.opacity(0.75)
],
startPoint: .top, startPoint: .top,
endPoint: .bottom endPoint: .bottom
) )
@@ -0,0 +1,110 @@
// TranslationChip.swift
// OSGKeyboard · Shared
//
// Translation target picker chip shared between keyboard extension and
// host-app preview surfaces.
import SwiftUI
public struct TranslationChip: View, Equatable {
public let palette: ThemePalette
public let targetLocaleId: String
public let onSelect: (String) -> Void
public init(
palette: ThemePalette,
targetLocaleId: String,
onSelect: @escaping (String) -> Void
) {
self.palette = palette
self.targetLocaleId = targetLocaleId
self.onSelect = onSelect
}
nonisolated public static func == (lhs: TranslationChip, rhs: TranslationChip) -> Bool {
lhs.palette == rhs.palette && lhs.targetLocaleId == rhs.targetLocaleId
}
public var body: some View {
Menu {
ForEach(TranslationLanguageCatalog.all) { language in
Button {
onSelect(language.id)
} label: {
if language.id == targetLocaleId {
Label(displayLabel(for: language), systemImage: "checkmark")
} else {
Text(displayLabel(for: language))
}
}
}
} label: {
label
}
.menuStyle(.button)
.accessibilityLabel(Text(SharedL10n.string("keyboard.translation.a11y")))
.accessibilityHint(Text(SharedL10n.string("keyboard.translation.a11yHint")))
}
@ViewBuilder
private var label: some View {
let target = TranslationLanguageCatalog.resolve(targetLocaleId)
let enabled = targetLocaleId != TranslationLanguageCatalog.offLocaleId
HStack(spacing: 4) {
Image(systemName: enabled ? "character.bubble" : "character.bubble.fill")
Text(chipLabel(target: target, enabled: enabled))
Image(systemName: "chevron.down")
.font(.system(size: 8, weight: .bold))
}
.font(TypeStyle.caption2)
.foregroundStyle(foreground(enabled: enabled))
.padding(.horizontal, Spacing.xs + 2)
.padding(.vertical, 6)
.frame(minHeight: 28)
.background(background(enabled: enabled), in: Capsule())
.overlay(Capsule().stroke(stroke(enabled: enabled), lineWidth: 0.5))
}
private func displayLabel(for language: TranslationLanguage) -> String {
if language.id == TranslationLanguageCatalog.offLocaleId {
return SharedL10n.string("keyboard.translation.offMenu")
}
return language.nativeName
}
private func chipLabel(target: TranslationLanguage, enabled: Bool) -> String {
if !enabled {
return SharedL10n.string("keyboard.translation.chip")
}
return "\(shortLabel(for: target))"
}
private func shortLabel(for target: TranslationLanguage) -> String {
switch target.id {
case "en": return "EN"
case "zh-Hans": return ""
case "zh-Hant": return ""
case "ja": return ""
case "ko": return ""
case "fr": return "FR"
case "de": return "DE"
case "es": return "ES"
case "ru": return "RU"
case "pt": return "PT"
default: return target.promptLanguageName
}
}
private func foreground(enabled: Bool) -> Color {
enabled ? palette.accent : palette.textPrimary
}
private func background(enabled: Bool) -> Color {
enabled ? palette.accent.opacity(0.15) : palette.surfaceElevated
}
private func stroke(enabled: Bool) -> Color {
enabled ? palette.accent.opacity(0.35) : palette.divider
}
}
@@ -1,23 +1,20 @@
// WaveformView.swift // WaveformView.swift
// OSGKeyboard · Keyboard Extension // OSGKeyboard · Shared
// //
// Symmetric, real-time driven waveform. 18 bars centred around a vertical // Symmetric, real-time driven waveform. Shared between the keyboard
// axis. The dominant bar is driven by the current RMS; surrounding bars // extension and any host-app preview that mirrors the mic UI.
// decay on a small position-based curve so the visual feels like a
// horizontal speaker cone, not random noise.
import SwiftUI import SwiftUI
import OSGKeyboardShared
struct WaveformView: View { public struct WaveformView: View {
@Environment(\.themePalette) private var palette: ThemePalette @Environment(\.themePalette) private var palette: ThemePalette
let level: Double // 0...1, smoothed RMS public let level: Double
let barCount: Int public let barCount: Int
let color: Color? public let color: Color?
let active: Bool // when false, bars collapse to a thin resting line public let active: Bool
init( public init(
level: Double, level: Double,
barCount: Int = 18, barCount: Int = 18,
color: Color? = nil, color: Color? = nil,
@@ -33,13 +30,16 @@ struct WaveformView: View {
color ?? palette.recordRed color ?? palette.recordRed
} }
var body: some View { public var body: some View {
TimelineView(.animation(minimumInterval: 1.0 / 30.0)) { context in TimelineView(.animation(minimumInterval: 1.0 / 30.0)) { context in
HStack(alignment: .center, spacing: 3) { HStack(alignment: .center, spacing: 3) {
ForEach(0..<barCount, id: \.self) { i in ForEach(0..<barCount, id: \.self) { index in
Capsule() Capsule()
.fill(resolvedColor) .fill(resolvedColor)
.frame(width: 2.4, height: height(for: i, time: context.date.timeIntervalSinceReferenceDate)) .frame(
width: 2.4,
height: height(for: index, time: context.date.timeIntervalSinceReferenceDate)
)
.opacity(active ? 1.0 : 0.45) .opacity(active ? 1.0 : 0.45)
} }
} }
@@ -50,7 +50,6 @@ struct WaveformView: View {
guard active else { return 4 } guard active else { return 4 }
let centre = Double(barCount - 1) / 2.0 let centre = Double(barCount - 1) / 2.0
let distance = abs(Double(index) - centre) / max(centre, 1) let distance = abs(Double(index) - centre) / max(centre, 1)
// Per-bar small wobble so the line is alive but tied to level.
let phase = sin(time * 4.0 + Double(index) * 0.45) let phase = sin(time * 4.0 + Double(index) * 0.45)
let wobble = 0.18 * phase let wobble = 0.18 * phase
let magnitude = max(0, min(1, Double(level) + wobble)) let magnitude = max(0, min(1, Double(level) + wobble))
@@ -0,0 +1,262 @@
// AppGroupConfiguration.swift
// OSGKeyboard · Shared
//
// Single source of truth for App Group UserDefaults keys (`config.*`).
// Both `ProviderConfig` (main app) and `AppGroupStore` (keyboard ext)
// should read/write through this type so keys and defaults stay aligned.
import Foundation
public struct AppGroupConfiguration: Sendable, Equatable {
// MARK: - Keys
public enum Keys {
public static let providerId = "config.providerId"
public static let baseURL = "config.baseURL"
/// Legacy plaintext slot migrated to Keychain on first read.
public static let apiKeyLegacy = "config.apiKey"
public static let model = "config.model"
public static let modeId = "config.modeId"
public static let localeId = "config.localeId"
public static let engineMode = "config.engineMode"
public static let hasCompletedOnboarding = "config.hasCompletedOnboarding"
public static let onboardingPage = "config.onboardingPage"
public static let hasAcknowledgedCloudSharing = "config.hasAcknowledgedCloudSharing"
public static let uiLanguage = "config.uiLanguage"
public static let translationTargetLocaleId = "config.translationTargetLocaleId"
public static let handednessPreference = "config.handednessPreference"
public static let cursorDragNavigationEnabled = "config.cursorDragNavigationEnabled"
public static let polishIntensity = "config.polishIntensity"
public static let detectedAppContext = "config.detectedAppContext"
public static let detectedAppContextAt = "config.detectedAppContextAt"
public static let personalDictionary = "config.personalDictionary.v1"
}
// MARK: - Stored fields
public var providerId: String
public var baseURL: String
public var model: String
public var modeId: String
public var localeId: String
public var engineMode: String
public var hasCompletedOnboarding: Bool
public var onboardingPage: Int
public var hasAcknowledgedCloudSharing: Bool
public var uiLanguage: AppUILanguage
public var translationTargetLocaleId: String
public var handednessPreference: HandednessPreference
public var cursorDragNavigationEnabled: Bool
public var polishIntensity: PolishIntensity
public var personalDictionary: PersonalDictionary
// MARK: - Derived
/// Translation is on iff a target locale other than `offLocaleId` is selected.
public var translationEnabled: Bool {
translationTargetLocaleId != TranslationLanguageCatalog.offLocaleId
}
public var isTranslationEffective: Bool {
translationEnabled
}
public var isLocalEngine: Bool {
engineMode == "local"
}
public var polishModeForPipeline: PolishingService.PolishMode {
isTranslationEffective
? .translate(targetLocaleId: translationTargetLocaleId)
: .polish
}
/// Local engine pins the LLM step to DeepSeek; cloud uses the user's provider.
public var polishProviderIdOverride: String? {
engineMode == "local" ? "deepseek" : nil
}
public var isCloudAPIKeyMissingForVoiceInput: Bool {
guard engineMode == "cloud" else { return false }
return apiKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
/// API key lives in the Keychain (cross-process, encrypted at rest).
public var apiKey: String {
Keychain.apiKey(for: providerId) ?? ""
}
public func makeClient() -> LLMClient {
OpenAICompatibleClient(
baseURL: baseURL,
apiKey: apiKey,
model: model
)
}
// MARK: - Detected app context
public func detectedAppContext(from defaults: UserDefaults) -> (context: AppContext, observedAt: Date)? {
guard let raw = defaults.string(forKey: Keys.detectedAppContext),
let value = AppContext(rawValue: raw)
else { return nil }
let timestamp = defaults.object(forKey: Keys.detectedAppContextAt) as? Date ?? .distantPast
return (value, timestamp)
}
public mutating func setDetectedAppContext(_ context: AppContext, at date: Date = Date(), to defaults: UserDefaults) {
defaults.set(context.rawValue, forKey: Keys.detectedAppContext)
defaults.set(date, forKey: Keys.detectedAppContextAt)
}
// MARK: - Load / save
/// Loads configuration from App Group defaults. Returns `nil` when the suite is unavailable.
public static func load(from defaults: UserDefaults? = nil) -> AppGroupConfiguration? {
guard let store = defaults ?? AppGroup.defaultsIfAvailable else { return nil }
return load(fromAvailable: store)
}
/// Loads configuration from a known-available UserDefaults suite.
public static func load(fromAvailable defaults: UserDefaults) -> AppGroupConfiguration {
var config = AppGroupConfiguration(
providerId: defaults.string(forKey: Keys.providerId) ?? "openai",
baseURL: "",
model: "",
modeId: defaults.string(forKey: Keys.modeId) ?? "polish",
localeId: defaults.string(forKey: Keys.localeId) ?? "auto",
engineMode: defaults.string(forKey: Keys.engineMode) ?? "cloud",
hasCompletedOnboarding: defaults.bool(forKey: Keys.hasCompletedOnboarding),
onboardingPage: {
let saved = defaults.integer(forKey: Keys.onboardingPage)
return saved > 0 ? saved : 0
}(),
hasAcknowledgedCloudSharing: defaults.bool(forKey: Keys.hasAcknowledgedCloudSharing),
uiLanguage: AppUILanguage.fromStored(defaults.string(forKey: Keys.uiLanguage)),
translationTargetLocaleId: defaults.string(forKey: Keys.translationTargetLocaleId)
?? TranslationLanguageCatalog.offLocaleId,
handednessPreference: HandednessPreference.fromStored(
defaults.string(forKey: Keys.handednessPreference)
),
cursorDragNavigationEnabled: {
if defaults.object(forKey: Keys.cursorDragNavigationEnabled) == nil {
return true
}
return defaults.bool(forKey: Keys.cursorDragNavigationEnabled)
}(),
polishIntensity: resolvePolishIntensity(from: defaults),
personalDictionary: decodePersonalDictionary(from: defaults)
)
let preset = LLMProvider.provider(id: config.providerId)
if config.baseURL.isEmpty {
config.baseURL = defaults.string(forKey: Keys.baseURL) ?? preset.defaultBaseURL
}
if config.model.isEmpty {
config.model = defaults.string(forKey: Keys.model) ?? preset.defaultModel
}
// One-shot legacy migration: plaintext apiKey in UserDefaults Keychain.
_ = resolveAPIKey(defaults: defaults, providerId: config.providerId)
// Cloud no longer exposes off/transcribe; migrate legacy values.
if config.engineMode == "cloud", config.modeId != "polish" {
config.modeId = "polish"
defaults.set("polish", forKey: Keys.modeId)
}
// DeepSeek is local-engine only never a cloud picker choice.
if config.engineMode == "cloud", config.providerId == "deepseek" {
let openAI = LLMProvider.provider(id: "openai")
config.providerId = openAI.id
config.baseURL = openAI.defaultBaseURL
config.model = openAI.defaultModel
defaults.set(openAI.id, forKey: Keys.providerId)
defaults.set(openAI.defaultBaseURL, forKey: Keys.baseURL)
defaults.set(openAI.defaultModel, forKey: Keys.model)
}
return config
}
public func save(to defaults: UserDefaults) {
defaults.set(providerId, forKey: Keys.providerId)
defaults.set(baseURL, forKey: Keys.baseURL)
defaults.set(model, forKey: Keys.model)
defaults.set(modeId, forKey: Keys.modeId)
defaults.set(localeId, forKey: Keys.localeId)
defaults.set(engineMode, forKey: Keys.engineMode)
defaults.set(hasCompletedOnboarding, forKey: Keys.hasCompletedOnboarding)
defaults.set(onboardingPage, forKey: Keys.onboardingPage)
defaults.set(hasAcknowledgedCloudSharing, forKey: Keys.hasAcknowledgedCloudSharing)
defaults.set(uiLanguage.rawValue, forKey: Keys.uiLanguage)
defaults.set(translationTargetLocaleId, forKey: Keys.translationTargetLocaleId)
defaults.set(handednessPreference.rawValue, forKey: Keys.handednessPreference)
defaults.set(cursorDragNavigationEnabled, forKey: Keys.cursorDragNavigationEnabled)
defaults.set(polishIntensity.rawValue, forKey: Keys.polishIntensity)
Self.encodePersonalDictionary(personalDictionary, to: defaults)
}
// MARK: - Private helpers
private static func resolvePolishIntensity(from defaults: UserDefaults) -> PolishIntensity {
guard let raw = defaults.string(forKey: Keys.polishIntensity) else {
return .default
}
let resolved = PolishIntensity.resolve(storedRawValue: raw)
if raw == PolishIntensity.legacyOffRawValue {
defaults.set(resolved.rawValue, forKey: Keys.polishIntensity)
}
return resolved
}
private static func decodePersonalDictionary(from defaults: UserDefaults) -> PersonalDictionary {
guard let data = defaults.data(forKey: Keys.personalDictionary) else {
return .empty
}
do {
var dictionary = try JSONDecoder().decode(PersonalDictionary.self, from: data)
if dictionary.entries.contains(where: { $0.source == .history }) {
for index in dictionary.entries.indices where dictionary.entries[index].source == .history {
dictionary.entries[index].source = .manual
}
dictionary.version += 1
if let migrated = try? JSONEncoder().encode(dictionary) {
defaults.set(migrated, forKey: Keys.personalDictionary)
}
}
return dictionary
} catch {
OSGLog.config.warning("personalDictionary decode failed: \(error.localizedDescription, privacy: .public)")
return .empty
}
}
private static func encodePersonalDictionary(_ dictionary: PersonalDictionary, to defaults: UserDefaults) {
do {
let data = try JSONEncoder().encode(dictionary)
defaults.set(data, forKey: Keys.personalDictionary)
} catch {
OSGLog.config.warning("personalDictionary encode failed: \(error.localizedDescription, privacy: .public)")
}
}
/// Read the API key from the Keychain, falling back to a one-time migration from UserDefaults.
static func resolveAPIKey(defaults: UserDefaults?, providerId: String) -> String {
if let stored = Keychain.apiKey(for: providerId), !stored.isEmpty {
return stored
}
if let legacyKeychain = Keychain.legacyAPIKey(), !legacyKeychain.isEmpty {
try? Keychain.setAPIKey(legacyKeychain, for: providerId)
try? Keychain.deleteLegacyAPIKey()
return legacyKeychain
}
if let defaults,
let legacy = defaults.string(forKey: Keys.apiKeyLegacy),
!legacy.isEmpty {
try? Keychain.setAPIKey(legacy, for: providerId)
defaults.removeObject(forKey: Keys.apiKeyLegacy)
return legacy
}
return ""
}
}
@@ -10,12 +10,11 @@ public enum EngineServiceLabel {
engineMode: String, engineMode: String,
providerId: String, providerId: String,
model: String, model: String,
localASRBackend: LocalASRBackend = .speechAnalyzer,
language: AppUILanguage? = nil language: AppUILanguage? = nil
) -> String { ) -> String {
let lang = language ?? AppGroupStore().uiLanguage let lang = language ?? AppGroupStore().uiLanguage
if engineMode == "local" { if engineMode == "local" {
let asrName = asrDisplayName(for: localASRBackend, language: lang) let asrName = SharedL10n.string("engine.asr.appleSpeech", language: lang)
return SharedL10n.format("engine.summary.local", language: lang, asrName) return SharedL10n.format("engine.summary.local", language: lang, asrName)
} }
let providerName = ProviderDisplayName.name(for: providerId, language: lang) let providerName = ProviderDisplayName.name(for: providerId, language: lang)
@@ -30,14 +29,4 @@ public enum EngineServiceLabel {
trimmedModel trimmedModel
) )
} }
private static func asrDisplayName(
for backend: LocalASRBackend,
language: AppUILanguage
) -> String {
// v0.2.0: only the iOS SpeechAnalyzer path remains. We keep the
// switch on `LocalASRBackend` so the next non-iOS backend can
// slot in without touching every call site.
return SharedL10n.string("engine.asr.appleSpeech", language: language)
}
} }
@@ -6,8 +6,10 @@
import Foundation import Foundation
public struct FlowUtteranceChunkConfig: Sendable, Equatable { public struct FlowUtteranceChunkConfig: Sendable, Equatable {
/// Target maximum duration per ASR chunk. /// Target duration for the first ASR chunk (starts pipelining early).
public let maxChunkDurationSeconds: TimeInterval public let firstChunkDurationSeconds: TimeInterval
/// Target duration for later chunks once pipelining is underway.
public let subsequentChunkDurationSeconds: TimeInterval
/// Tail overlap fed into the next chunk for boundary dedup when stitching. /// Tail overlap fed into the next chunk for boundary dedup when stitching.
public let overlapDurationSeconds: TimeInterval public let overlapDurationSeconds: TimeInterval
/// After hitting the max window, wait up to this long for a pause before hard-splitting. /// After hitting the max window, wait up to this long for a pause before hard-splitting.
@@ -17,21 +19,52 @@ public struct FlowUtteranceChunkConfig: Sendable, Equatable {
public let sampleRate: Int public let sampleRate: Int
public init( public init(
maxChunkDurationSeconds: TimeInterval, firstChunkDurationSeconds: TimeInterval = 2.5,
subsequentChunkDurationSeconds: TimeInterval = 5.0,
overlapDurationSeconds: TimeInterval, overlapDurationSeconds: TimeInterval,
pauseExtensionMaxSeconds: TimeInterval, pauseExtensionMaxSeconds: TimeInterval,
pauseRMSThreshold: Float, pauseRMSThreshold: Float,
sampleRate: Int sampleRate: Int
) { ) {
self.maxChunkDurationSeconds = maxChunkDurationSeconds self.firstChunkDurationSeconds = firstChunkDurationSeconds
self.subsequentChunkDurationSeconds = subsequentChunkDurationSeconds
self.overlapDurationSeconds = overlapDurationSeconds self.overlapDurationSeconds = overlapDurationSeconds
self.pauseExtensionMaxSeconds = pauseExtensionMaxSeconds self.pauseExtensionMaxSeconds = pauseExtensionMaxSeconds
self.pauseRMSThreshold = pauseRMSThreshold self.pauseRMSThreshold = pauseRMSThreshold
self.sampleRate = sampleRate self.sampleRate = sampleRate
} }
/// Uniform chunk size used by unit tests and legacy call sites.
public init(
maxChunkDurationSeconds: TimeInterval,
overlapDurationSeconds: TimeInterval,
pauseExtensionMaxSeconds: TimeInterval,
pauseRMSThreshold: Float,
sampleRate: Int
) {
self.firstChunkDurationSeconds = maxChunkDurationSeconds
self.subsequentChunkDurationSeconds = maxChunkDurationSeconds
self.overlapDurationSeconds = overlapDurationSeconds
self.pauseExtensionMaxSeconds = pauseExtensionMaxSeconds
self.pauseRMSThreshold = pauseRMSThreshold
self.sampleRate = sampleRate
}
/// Backward-compatible alias for tests that read `maxChunkSamples`.
public var maxChunkDurationSeconds: TimeInterval {
subsequentChunkDurationSeconds
}
public func maxChunkDurationSeconds(forChunkIndex index: Int) -> TimeInterval {
index == 0 ? firstChunkDurationSeconds : subsequentChunkDurationSeconds
}
public func maxChunkSamples(forChunkIndex index: Int) -> Int {
Int(maxChunkDurationSeconds(forChunkIndex: index) * Double(sampleRate))
}
public var maxChunkSamples: Int { public var maxChunkSamples: Int {
Int(maxChunkDurationSeconds * Double(sampleRate)) maxChunkSamples(forChunkIndex: 1)
} }
public var overlapSamples: Int { public var overlapSamples: Int {
@@ -44,7 +77,8 @@ public struct FlowUtteranceChunkConfig: Sendable, Equatable {
/// Default for keyboard Flow utterances ( 3 min, pipelined ASR). /// Default for keyboard Flow utterances ( 3 min, pipelined ASR).
public static let flowDefault = FlowUtteranceChunkConfig( public static let flowDefault = FlowUtteranceChunkConfig(
maxChunkDurationSeconds: 30, firstChunkDurationSeconds: 2.5,
subsequentChunkDurationSeconds: 5.0,
overlapDurationSeconds: 0.5, overlapDurationSeconds: 0.5,
pauseExtensionMaxSeconds: 2, pauseExtensionMaxSeconds: 2,
pauseRMSThreshold: 0.015, pauseRMSThreshold: 0.015,
@@ -82,6 +82,14 @@ public struct LLMProvider: Identifiable, Codable, Hashable, Sendable {
apiKeyURL: URL(string: "https://platform.moonshot.cn/console/api-keys"), apiKeyURL: URL(string: "https://platform.moonshot.cn/console/api-keys"),
blurb: "Kimi · 长上下文 · Long context" blurb: "Kimi · 长上下文 · Long context"
), ),
.init(
id: "mimo",
name: "小米 MiMo",
defaultBaseURL: "https://api.xiaomimimo.com/v1",
defaultModel: "mimo-v2.5",
apiKeyURL: URL(string: "https://platform.xiaomimimo.com"),
blurb: "mimo-v2.5 · 中文优化 · Chinese-optimized"
),
.init( .init(
id: "custom", id: "custom",
name: "Custom · 自定义", name: "Custom · 自定义",
@@ -1,50 +0,0 @@
// LocalASRBackend.swift
// OSGKeyboard · Shared
//
// Identifies which on-device speech recognition engine to use when the
// user picks the "local" engine (no cloud LLM polish). The shared
// factory `ASRServiceFactory` dispatches on this enum; the settings UI
// renders it as a picker.
//
// As of v0.2.0 the only on-device backend is iOS 26 `SpeechAnalyzer`
// + `DictationTranscriber`. The previous Qwen3-CoreML backend has
// been removed: that path required a ~1.6 GB CoreML bundle, a local
// SPM fork that pulled in mlx-swift, and significant app-side state
// (download manager, warm-up service, model registry). We now keep the
// local engine narrow same iOS ASR the cloud engine already uses
// and let users opt into a cloud polish step after the transcript is
// produced if they need stronger accuracy on noisy audio or dialectal
// Chinese. See `LocalPolishConfig` for the post-ASR polish toggle.
//
// Why an enum in `Shared` rather than a `Bool`: the value must remain
// serialisable into the App Group store (so the keyboard extension can
// observe the selection) and exposed via `ProviderConfig` (UI binding).
// Keeping the type stable even with a single case avoids a migration
// the next time someone adds a non-cloud backend (e.g. whisper.cpp).
import Foundation
public enum LocalASRBackend: String, CaseIterable, Identifiable, Sendable, Codable {
/// iOS 26 `SpeechAnalyzer` + `DictationTranscriber`. Always
/// on-device, no asset download, ships with iOS. The only local
/// backend in v0.2.0.
case speechAnalyzer
public var id: String { rawValue }
/// Localisation key for the human label in the settings picker.
public var labelKey: String {
"asr.backend.speechAnalyzer.label"
}
/// Localisation key for the one-line subtitle shown under the label.
public var blurbKey: String {
"asr.backend.speechAnalyzer.blurb"
}
/// Whether this backend needs the user to download a model file
/// before it can run. Always `false` for iOS-bundled speech.
public var requiresModelDownload: Bool {
false
}
}
@@ -108,7 +108,9 @@ extension PersonalDictionary.Entry {
/// learner. Users can re-classify later from Settings. /// learner. Users can re-classify later from Settings.
public static func inferCategory(for term: String) -> Category { public static func inferCategory(for term: String) -> Category {
let hasUpper = term.contains(where: { $0.isUppercase }) let hasUpper = term.contains(where: { $0.isUppercase })
let hasDigit = term.contains(where: { $0.isNumber }) let hasDigit = term.unicodeScalars.contains { scalar in
CharacterSet.decimalDigits.contains(scalar) && scalar.isASCII
}
let hasLatin = term.unicodeScalars.contains { scalar in let hasLatin = term.unicodeScalars.contains { scalar in
CharacterSet.letters.contains(scalar) && scalar.isASCII CharacterSet.letters.contains(scalar) && scalar.isASCII
} }
+117 -190
View File
@@ -15,144 +15,108 @@ import Combine
public final class ProviderConfig: ObservableObject, @unchecked Sendable { public final class ProviderConfig: ObservableObject, @unchecked Sendable {
public static let shared = ProviderConfig() public static let shared = ProviderConfig()
private enum Key {
static let providerId = "config.providerId"
static let baseURL = "config.baseURL"
// Legacy: apiKey used to live in UserDefaults before the
// migration. We still read it once (see init below) and then
// delete the entry, but no other code path touches this key.
static let apiKeyLegacy = "config.apiKey"
static let model = "config.model"
static let modeId = "config.modeId"
static let localeId = "config.localeId"
static let engineMode = "config.engineMode"
static let hasCompletedOnboarding = "config.hasCompletedOnboarding"
static let onboardingPage = "config.onboardingPage"
static let hasAcknowledgedCloudSharing = "config.hasAcknowledgedCloudSharing"
// Which on-device ASR engine to use when `engineMode == "local"`.
// Persisted in the App Group so the keyboard can read the
// selection even though it never instantiates the backend itself.
static let localASRBackend = "config.localASRBackend"
static let uiLanguage = "config.uiLanguage"
// v0.2.0: optional cloud polish step after on-device ASR finishes
// in the local engine. Default `false` keeps the local engine
// truly local unless the user explicitly opts in.
static let localModeCloudPolishEnabled = "config.localModeCloudPolishEnabled"
// v0.2.1: optional translation step after ASR. The
// post-ASR transcript is routed through the same LLM with a
// translate-and-polish prompt targeting `translationTargetLocaleId`.
// Mutually exclusive with the local-only promise see `TranslationPolicy`.
//
// v0.2.1 follow-up: `config.translationEnabled` was *removed*
// as a persisted key translation is now derived from
// `translationTargetLocaleId` (== offLocaleId means "off"). The
// store still tolerates legacy reads of the old key so users
// who upgraded from a build that wrote it don't see a flash of
// "on" state during init, but new writes never touch the key.
static let translationTargetLocaleId = "config.translationTargetLocaleId"
static let handednessPreference = "config.handednessPreference"
static let cursorDragNavigationEnabled = "config.cursorDragNavigationEnabled"
// v0.3.0: how aggressively the LLM should rewrite transcripts.
static let polishIntensity = "config.polishIntensity"
}
@Published public var providerId: String { @Published public var providerId: String {
didSet { didSet {
defaults.set(providerId, forKey: Key.providerId) guard !isApplyingConfiguration, providerId != configuration.providerId else { return }
// Keep API keys isolated per provider: switching provider in configuration.providerId = providerId
// Settings loads that provider's key instead of reusing the
// previously selected vendor's key.
isSyncingProviderAPIKey = true isSyncingProviderAPIKey = true
apiKey = Keychain.apiKey(for: providerId) ?? "" apiKey = configuration.apiKey
isSyncingProviderAPIKey = false isSyncingProviderAPIKey = false
persistConfiguration()
} }
} }
@Published public var baseURL: String { @Published public var baseURL: String {
didSet { defaults.set(baseURL, forKey: Key.baseURL) } didSet {
guard !isApplyingConfiguration, baseURL != configuration.baseURL else { return }
configuration.baseURL = baseURL
persistConfiguration()
}
} }
@Published public var apiKey: String { @Published public var apiKey: String {
didSet { didSet {
// Skip the round-trip on init we read from Keychain and
// writing the same value back is wasteful.
guard oldValue != apiKey, !isSyncingProviderAPIKey else { return } guard oldValue != apiKey, !isSyncingProviderAPIKey else { return }
do { do {
try Keychain.setAPIKey(apiKey, for: providerId) try Keychain.setAPIKey(apiKey, for: providerId)
} catch { } catch {
#if DEBUG OSGLog.config.warning("Keychain write failed: \(error.localizedDescription, privacy: .public)")
print("⚠️ [OSGKeyboard] Keychain write failed: \(error)")
#endif
} }
} }
} }
@Published public var model: String { @Published public var model: String {
didSet { defaults.set(model, forKey: Key.model) } didSet {
guard !isApplyingConfiguration, model != configuration.model else { return }
configuration.model = model
persistConfiguration()
}
} }
@Published public var modeId: String { @Published public var modeId: String {
didSet { defaults.set(modeId, forKey: Key.modeId) } didSet {
guard !isApplyingConfiguration, modeId != configuration.modeId else { return }
configuration.modeId = modeId
persistConfiguration()
}
} }
@Published public var localeId: String { @Published public var localeId: String {
didSet { defaults.set(localeId, forKey: Key.localeId) } didSet {
guard !isApplyingConfiguration, localeId != configuration.localeId else { return }
configuration.localeId = localeId
persistConfiguration()
}
} }
/// "local" on-device ASR + built-in DeepSeek polish. /// "local" on-device ASR + built-in DeepSeek polish.
/// "cloud" on-device ASR + user's cloud LLM polish. /// "cloud" on-device ASR + user's cloud LLM polish.
@Published public var engineMode: String { @Published public var engineMode: String {
didSet { didSet {
defaults.set(engineMode, forKey: Key.engineMode) guard !isApplyingConfiguration, engineMode != configuration.engineMode else { return }
configuration.engineMode = engineMode
applyEngineModeSideEffects() applyEngineModeSideEffects()
persistConfiguration()
} }
} }
@Published public var hasCompletedOnboarding: Bool { @Published public var hasCompletedOnboarding: Bool {
didSet { didSet {
defaults.set(hasCompletedOnboarding, forKey: Key.hasCompletedOnboarding) guard !isApplyingConfiguration,
hasCompletedOnboarding != configuration.hasCompletedOnboarding else { return }
configuration.hasCompletedOnboarding = hasCompletedOnboarding
if hasCompletedOnboarding { if hasCompletedOnboarding {
configuration.onboardingPage = 0
onboardingPage = 0 onboardingPage = 0
} }
persistConfiguration()
} }
} }
/// Persisted onboarding step so returning from Settings does not reset progress. /// Persisted onboarding step so returning from Settings does not reset progress.
@Published public var onboardingPage: Int { @Published public var onboardingPage: Int {
didSet { defaults.set(onboardingPage, forKey: Key.onboardingPage) } didSet {
guard !isApplyingConfiguration, onboardingPage != configuration.onboardingPage else { return }
configuration.onboardingPage = onboardingPage
persistConfiguration()
}
} }
/// User confirmed that Cloud polish sends transcripts to their configured third-party API. /// User confirmed that Cloud polish sends transcripts to their configured third-party API.
@Published public var hasAcknowledgedCloudSharing: Bool { @Published public var hasAcknowledgedCloudSharing: Bool {
didSet { defaults.set(hasAcknowledgedCloudSharing, forKey: Key.hasAcknowledgedCloudSharing) }
}
/// Which on-device ASR engine backs the "local" engine mode. Only
/// consulted when `isLocalEngine == true`; the cloud engine always
/// uses `SpeechAnalyzer`.
@Published public var localASRBackend: LocalASRBackend {
didSet { defaults.set(localASRBackend.rawValue, forKey: Key.localASRBackend) }
}
/// When `engineMode == "local"`, optionally route the ASR transcript
/// through the user's configured LLM (DeepSeek by default) before
/// inserting at the cursor. The polish step runs through the same
/// `LLMClient` + `PolishingService` stack the cloud engine uses.
///
/// Defaults to `false` the local engine is ASR-only out of the
/// box. Users opt in from Settings when the iOS ASR output isn't
/// strong enough (noisy far-field audio, dialectal Chinese, etc.).
@Published public var localModeCloudPolishEnabled: Bool {
didSet { didSet {
defaults.set(localModeCloudPolishEnabled, forKey: Key.localModeCloudPolishEnabled) guard !isApplyingConfiguration,
AppGroupConfigDarwin.postConfigChanged() hasAcknowledgedCloudSharing != configuration.hasAcknowledgedCloudSharing else { return }
configuration.hasAcknowledgedCloudSharing = hasAcknowledgedCloudSharing
persistConfiguration()
} }
} }
/// Host-app UI language. Also mirrored to the App Group for the keyboard extension. /// Host-app UI language. Also mirrored to the App Group for the keyboard extension.
@Published public var uiLanguage: AppUILanguage { @Published public var uiLanguage: AppUILanguage {
didSet { defaults.set(uiLanguage.rawValue, forKey: Key.uiLanguage) } didSet {
guard !isApplyingConfiguration, uiLanguage != configuration.uiLanguage else { return }
configuration.uiLanguage = uiLanguage
persistConfiguration()
}
} }
/// v0.2.1: whether to translate the transcript into /// v0.2.1: whether to translate the transcript into
/// `translationTargetLocaleId` before insertion. **Derived** /// `translationTargetLocaleId` before insertion. **Derived**
/// translation is on iff the user has selected a target locale /// translation is on iff the user has selected a target locale
/// (i.e. the persisted id is anything other than /// (i.e. the persisted id is anything other than
/// `TranslationLanguageCatalog.offLocaleId`). Default off. /// `TranslationLanguageCatalog.offLocaleId`). Default off.
///
/// This used to be a stored `@Published var ... { didSet }` but the
/// chip / picker now writes the locale directly; collapsing the
/// pair into one field removes the "two writes out of sync" bug
/// surface entirely.
public var translationEnabled: Bool { public var translationEnabled: Bool {
translationTargetLocaleId != TranslationLanguageCatalog.offLocaleId configuration.translationEnabled
} }
/// v0.2.1: BCP-47-ish target language id (e.g. `en`, `ja`, `ko`) the /// v0.2.1: BCP-47-ish target language id (e.g. `en`, `ja`, `ko`) the
/// translate-and-polish prompt should produce. Default `"off"` /// translate-and-polish prompt should produce. Default `"off"`
@@ -161,31 +125,37 @@ public final class ProviderConfig: ObservableObject, @unchecked Sendable {
/// the user's choice without a host-app round-trip). /// the user's choice without a host-app round-trip).
@Published public var translationTargetLocaleId: String { @Published public var translationTargetLocaleId: String {
didSet { didSet {
defaults.set(translationTargetLocaleId, forKey: Key.translationTargetLocaleId) guard !isApplyingConfiguration,
AppGroupConfigDarwin.postConfigChanged() translationTargetLocaleId != configuration.translationTargetLocaleId else { return }
configuration.translationTargetLocaleId = translationTargetLocaleId
persistConfiguration(postConfigChanged: true)
} }
} }
/// Which hand the user holds the phone with mirrors to the keyboard /// Which hand the user holds the phone with mirrors to the keyboard
/// extension so delete / return can swap on the bottom row. /// extension so delete / return can swap on the bottom row.
@Published public var handednessPreference: HandednessPreference { @Published public var handednessPreference: HandednessPreference {
didSet { didSet {
defaults.set(handednessPreference.rawValue, forKey: Key.handednessPreference) guard !isApplyingConfiguration,
AppGroupConfigDarwin.postConfigChanged() handednessPreference != configuration.handednessPreference else { return }
configuration.handednessPreference = handednessPreference
persistConfiguration(postConfigChanged: true)
} }
} }
/// Press-and-drag pads beside the mic for four-way caret movement. /// Press-and-drag pads beside the mic for four-way caret movement.
@Published public var cursorDragNavigationEnabled: Bool { @Published public var cursorDragNavigationEnabled: Bool {
didSet { didSet {
defaults.set(cursorDragNavigationEnabled, forKey: Key.cursorDragNavigationEnabled) guard !isApplyingConfiguration,
AppGroupConfigDarwin.postConfigChanged() cursorDragNavigationEnabled != configuration.cursorDragNavigationEnabled else { return }
configuration.cursorDragNavigationEnabled = cursorDragNavigationEnabled
persistConfiguration(postConfigChanged: true)
} }
} }
/// Whether the pipeline should run translate-and-polish (not just /// Whether the pipeline should run translate-and-polish (not just
/// polish). Both engines honour the selected target locale. /// polish). Both engines honour the selected target locale.
public var isTranslationEffective: Bool { public var isTranslationEffective: Bool {
translationEnabled configuration.isTranslationEffective
} }
/// Translation picker visibility available on both engines. /// Translation picker visibility available on both engines.
@@ -194,21 +164,22 @@ public final class ProviderConfig: ObservableObject, @unchecked Sendable {
/// v0.3.0: how aggressively the LLM should rewrite the ASR /// v0.3.0: how aggressively the LLM should rewrite the ASR
/// transcript. Default is `medium` (Typeless-equivalent). /// transcript. Default is `medium` (Typeless-equivalent).
@Published public var polishIntensity: PolishIntensity { @Published public var polishIntensity: PolishIntensity {
didSet { defaults.set(polishIntensity.rawValue, forKey: Key.polishIntensity) } didSet {
guard !isApplyingConfiguration, polishIntensity != configuration.polishIntensity else { return }
configuration.polishIntensity = polishIntensity
persistConfiguration()
}
} }
public var isConfigured: Bool { public var isConfigured: Bool {
// Local engine (on-device ASR only) doesn't need an API key, // Local engine uses on-device ASR + built-in DeepSeek polish and
// base URL, or model the LLM round-trip is skipped entirely. // does not need a user API key. Cloud needs base URL, key, and model.
// Treat it as always-configured so onboarding's "Next" button
// enables the moment the user picks the local path, instead
// of forcing them to fill in cloud fields they won't use.
if isLocalEngine { return true } if isLocalEngine { return true }
return !baseURL.isEmpty && !apiKey.isEmpty && !model.isEmpty return !baseURL.isEmpty && !apiKey.isEmpty && !model.isEmpty
} }
/// On-device ASR only; no cloud API required. /// On-device ASR only; no cloud API required.
public var isLocalEngine: Bool { engineMode == "local" } public var isLocalEngine: Bool { configuration.isLocalEngine }
/// Local engine always polishes via the built-in DeepSeek path. /// Local engine always polishes via the built-in DeepSeek path.
public var shouldPolishLocalTranscript: Bool { isLocalEngine } public var shouldPolishLocalTranscript: Bool { isLocalEngine }
@@ -217,83 +188,37 @@ public final class ProviderConfig: ObservableObject, @unchecked Sendable {
public var localModeProviderId: String { "deepseek" } public var localModeProviderId: String { "deepseek" }
private let defaults: UserDefaults private let defaults: UserDefaults
private var configuration: AppGroupConfiguration
private var isApplyingConfiguration = false
private var isSyncingProviderAPIKey = false private var isSyncingProviderAPIKey = false
public init(defaults: UserDefaults? = nil) { public init(defaults: UserDefaults? = nil) {
let resolvedDefaults: UserDefaults = defaults ?? (AppGroup.isAvailable ? AppGroup.defaults : .standard) guard let resolvedDefaults = defaults ?? AppGroup.defaultsIfAvailable else {
preconditionFailure(
"ProviderConfig requires App Group or injected UserDefaults — " +
"check AppGroup.isAvailable before constructing."
)
}
self.defaults = resolvedDefaults self.defaults = resolvedDefaults
let pid = resolvedDefaults.string(forKey: Key.providerId) ?? "openai" self.configuration = AppGroupConfiguration.load(fromAvailable: resolvedDefaults)
let preset = LLMProvider.provider(id: pid)
self.providerId = pid
self.baseURL = resolvedDefaults.string(forKey: Key.baseURL) ?? preset.defaultBaseURL
// Resolve the API key with a one-shot migration from the legacy isApplyingConfiguration = true
// UserDefaults slot. After this runs once, `Key.apiKeyLegacy` providerId = configuration.providerId
// is empty in the suite and all subsequent reads go through the baseURL = configuration.baseURL
// Keychain. apiKey = configuration.apiKey
self.apiKey = ProviderConfig.resolveAPIKey(defaults: resolvedDefaults, providerId: pid) model = configuration.model
modeId = configuration.modeId
self.model = resolvedDefaults.string(forKey: Key.model) ?? preset.defaultModel localeId = configuration.localeId
self.modeId = resolvedDefaults.string(forKey: Key.modeId) ?? "polish" engineMode = configuration.engineMode
self.localeId = resolvedDefaults.string(forKey: Key.localeId) ?? "auto" hasCompletedOnboarding = configuration.hasCompletedOnboarding
self.engineMode = resolvedDefaults.string(forKey: Key.engineMode) ?? "cloud" onboardingPage = configuration.onboardingPage
self.hasCompletedOnboarding = resolvedDefaults.bool(forKey: Key.hasCompletedOnboarding) hasAcknowledgedCloudSharing = configuration.hasAcknowledgedCloudSharing
let savedPage = resolvedDefaults.integer(forKey: Key.onboardingPage) uiLanguage = configuration.uiLanguage
self.onboardingPage = savedPage > 0 ? savedPage : 0 translationTargetLocaleId = configuration.translationTargetLocaleId
self.hasAcknowledgedCloudSharing = resolvedDefaults.bool(forKey: Key.hasAcknowledgedCloudSharing) handednessPreference = configuration.handednessPreference
// Tolerate missing / unknown raw values (e.g. an enum case that cursorDragNavigationEnabled = configuration.cursorDragNavigationEnabled
// was renamed in a later build) by falling back to the default polishIntensity = configuration.polishIntensity
// rather than crashing inside `RawRepresentable.init`. isApplyingConfiguration = false
let rawBackend = resolvedDefaults.string(forKey: Key.localASRBackend) ?? LocalASRBackend.speechAnalyzer.rawValue
self.localASRBackend = LocalASRBackend(rawValue: rawBackend) ?? .speechAnalyzer
// v0.2.0: local-mode cloud polish toggle. Defaults off; users
// opt in from Settings when iOS ASR is too lossy for their
// environment. `object(forKey:) == nil` covers fresh installs
// and upgrades from builds that never wrote the key.
if resolvedDefaults.object(forKey: Key.localModeCloudPolishEnabled) == nil {
self.localModeCloudPolishEnabled = false
} else {
self.localModeCloudPolishEnabled = resolvedDefaults.bool(forKey: Key.localModeCloudPolishEnabled)
}
self.uiLanguage = AppUILanguage.fromStored(
resolvedDefaults.string(forKey: Key.uiLanguage)
)
// v0.2.1 follow-up: `translationEnabled` is now derived from
// `translationTargetLocaleId` no separate init read.
// Default the locale id to `offLocaleId` so existing installs
// that never picked a target language stay in the "off" state
// (the previous build's default of `"en"` would silently turn
// translation on for every upgraded user; off is the safe
// conservative default that matches the picker / chip UX).
self.translationTargetLocaleId = resolvedDefaults.string(forKey: Key.translationTargetLocaleId)
?? TranslationLanguageCatalog.offLocaleId
self.handednessPreference = HandednessPreference.fromStored(
resolvedDefaults.string(forKey: Key.handednessPreference)
)
if resolvedDefaults.object(forKey: Key.cursorDragNavigationEnabled) == nil {
self.cursorDragNavigationEnabled = true
} else {
self.cursorDragNavigationEnabled = resolvedDefaults.bool(forKey: Key.cursorDragNavigationEnabled)
}
// v0.3.0: polish intensity. Default to `.medium` for new
// installs; legacy `"off"` migrates to `.medium`.
if let raw = resolvedDefaults.string(forKey: Key.polishIntensity) {
self.polishIntensity = PolishIntensity.resolve(storedRawValue: raw)
if raw == PolishIntensity.legacyOffRawValue {
resolvedDefaults.set(PolishIntensity.medium.rawValue, forKey: Key.polishIntensity)
}
} else {
self.polishIntensity = .default
}
// Cloud no longer exposes off/transcribe; migrate legacy values.
if self.engineMode == "cloud", self.modeId != "polish" {
self.modeId = "polish"
}
// DeepSeek is local-engine only never a cloud picker choice.
if self.engineMode == "cloud", self.providerId == "deepseek" {
apply(preset: LLMProvider.provider(id: "openai"))
}
} }
/// Keep cloud vs local provider choices isolated when the user /// Keep cloud vs local provider choices isolated when the user
@@ -304,29 +229,15 @@ public final class ProviderConfig: ObservableObject, @unchecked Sendable {
} }
} }
/// Read the API key from the Keychain, falling back to a one-time private func persistConfiguration(postConfigChanged: Bool = false) {
/// migration from the legacy UserDefaults slot. configuration.save(to: defaults)
private static func resolveAPIKey(defaults: UserDefaults, providerId: String) -> String { if postConfigChanged {
if let stored = Keychain.apiKey(for: providerId), !stored.isEmpty { AppGroupConfigDarwin.postConfigChanged()
return stored
} }
// Migration path: old builds stored one global key under
// Keychain account "current". Move it to the active provider.
if let legacyKeychain = Keychain.legacyAPIKey(), !legacyKeychain.isEmpty {
try? Keychain.setAPIKey(legacyKeychain, for: providerId)
try? Keychain.deleteLegacyAPIKey()
return legacyKeychain
}
if let legacy = defaults.string(forKey: Key.apiKeyLegacy),
!legacy.isEmpty {
try? Keychain.setAPIKey(legacy, for: providerId)
defaults.removeObject(forKey: Key.apiKeyLegacy)
return legacy
}
return ""
} }
public func apply(preset: LLMProvider) { public func apply(preset: LLMProvider) {
isApplyingConfiguration = true
providerId = preset.id providerId = preset.id
if !preset.defaultBaseURL.isEmpty { if !preset.defaultBaseURL.isEmpty {
baseURL = preset.defaultBaseURL baseURL = preset.defaultBaseURL
@@ -334,15 +245,31 @@ public final class ProviderConfig: ObservableObject, @unchecked Sendable {
if !preset.defaultModel.isEmpty { if !preset.defaultModel.isEmpty {
model = preset.defaultModel model = preset.defaultModel
} }
configuration.providerId = providerId
configuration.baseURL = baseURL
configuration.model = model
isSyncingProviderAPIKey = true
apiKey = configuration.apiKey
isSyncingProviderAPIKey = false
isApplyingConfiguration = false
persistConfiguration()
} }
public func reset() { public func reset() {
providerId = "openai" isApplyingConfiguration = true
let preset = LLMProvider.provider(id: "openai") let preset = LLMProvider.provider(id: "openai")
providerId = preset.id
baseURL = preset.defaultBaseURL baseURL = preset.defaultBaseURL
apiKey = "" apiKey = ""
model = preset.defaultModel model = preset.defaultModel
handednessPreference = .left handednessPreference = .left
hasAcknowledgedCloudSharing = false hasAcknowledgedCloudSharing = false
configuration.providerId = preset.id
configuration.baseURL = preset.defaultBaseURL
configuration.model = preset.defaultModel
configuration.handednessPreference = .left
configuration.hasAcknowledgedCloudSharing = false
isApplyingConfiguration = false
persistConfiguration()
} }
} }
@@ -0,0 +1,14 @@
{
"bin_bytes" : 174285,
"bin_file" : "OSGKeyboardCLM.bin",
"export_seconds" : 0.029847979545593262,
"generated_at" : "2026-07-05T11:34:09Z",
"identifier" : "com.osgkeyboard.custom-lm.v1",
"locale" : "zh_CN",
"phrase_count" : 11550,
"sources" : {
"ai_tech_seed" : 1259,
"computer_terms" : 10300
},
"version" : "1.0.0"
}
+56 -25
View File
@@ -47,6 +47,9 @@ public protocol ASRService: Sendable {
/// Clears cancellation / cached session state before a new utterance. /// Clears cancellation / cached session state before a new utterance.
func resetForNewUtterance() func resetForNewUtterance()
/// Pre-load locale assets and analyzer format for lower first-chunk latency.
func warmup(locale: Locale) async
/// Transcribe one PCM chunk (Flow pipelined path). Default wraps `transcribe(stream:)`. /// Transcribe one PCM chunk (Flow pipelined path). Default wraps `transcribe(stream:)`.
func transcribeChunk(samples: [Float], locale: Locale) async -> ASRChunkResult func transcribeChunk(samples: [Float], locale: Locale) async -> ASRChunkResult
} }
@@ -60,6 +63,8 @@ public enum ASRChunkResult: Sendable, Equatable {
extension ASRService { extension ASRService {
public func resetForNewUtterance() {} public func resetForNewUtterance() {}
public func warmup(locale: Locale) async {}
public func transcribeChunk(samples: [Float], locale: Locale) async -> ASRChunkResult { public func transcribeChunk(samples: [Float], locale: Locale) async -> ASRChunkResult {
guard !samples.isEmpty else { return .success("") } guard !samples.isEmpty else { return .success("") }
if Task.isCancelled { return .cancelled } if Task.isCancelled { return .cancelled }
@@ -116,27 +121,7 @@ public enum ASREvent: Sendable, Equatable {
// MARK: - Factory // MARK: - Factory
public enum ASRServiceFactory { public enum ASRServiceFactory {
/// Returns the on-device ASR backend. As of v0.2.0 the only /// Returns the on-device `SpeechAnalyzer` + `DictationTranscriber` backend.
/// supported `LocalASRBackend` is iOS 26 `SpeechAnalyzer` +
/// `DictationTranscriber` (always on-device, no asset download),
/// so the factory collapses to a single concrete type. We keep the
/// `localBackend` parameter on the signature so the next non-iOS
/// backend can slot in without touching every call site.
///
/// The cloud engine also routes through `SpeechAnalyzerASR`: the
/// user expectation is that ASR is the local half of the pipeline
/// regardless of where the LLM polish happens.
public static func make(
engineMode: String,
localBackend: LocalASRBackend = .speechAnalyzer
) -> ASRService {
SpeechAnalyzerASR()
}
/// Back-compat overload for callers that only ever want the
/// SpeechAnalyzer path. The previous single-backend build used
/// this signature; new code should pass the engine mode explicitly
/// so any future non-iOS backend is honoured.
public static func make() -> ASRService { public static func make() -> ASRService {
SpeechAnalyzerASR() SpeechAnalyzerASR()
} }
@@ -197,12 +182,52 @@ final class SpeechAnalyzerASR: ASRService, @unchecked Sendable {
private var chunkAnalyzerFormat: AVAudioFormat? private var chunkAnalyzerFormat: AVAudioFormat?
func resetForNewUtterance() { func resetForNewUtterance() {
// Keep chunk format / asset cache warm across utterances in one Flow session.
}
func invalidateChunkPreparationCache() {
lock.withLock { lock.withLock {
chunkPreparedLocaleID = nil chunkPreparedLocaleID = nil
chunkAnalyzerFormat = nil chunkAnalyzerFormat = nil
} }
} }
func warmup(locale: Locale) async {
guard let resolvedLocale = await DictationTranscriber.supportedLocale(equivalentTo: locale) else {
return
}
let localeID = resolvedLocale.identifier(.bcp47)
let cachedLocaleID = lock.withLock { chunkPreparedLocaleID }
if cachedLocaleID == localeID, lock.withLock({ chunkAnalyzerFormat != nil }) {
return
}
let lmConfiguration = CustomLanguageModelManager.shared.configurationForTranscription(
locale: resolvedLocale
)
let transcriber = CustomLanguageModelManager.makeDictationTranscriber(
locale: resolvedLocale,
lmConfiguration: lmConfiguration
)
do {
try await Self.prepareAssetsIfNeeded(for: transcriber, locale: resolvedLocale)
guard let format = await SpeechAnalyzer.bestAvailableAudioFormat(
compatibleWith: [transcriber],
considering: Self.captureFormat
) else {
return
}
lock.withLock {
chunkPreparedLocaleID = localeID
chunkAnalyzerFormat = format
}
Self.debug("warmup ready locale=\(localeID)")
} catch {
Self.debug("warmup failed: \(error.localizedDescription)")
}
}
func transcribeChunk(samples: [Float], locale: Locale) async -> ASRChunkResult { func transcribeChunk(samples: [Float], locale: Locale) async -> ASRChunkResult {
guard !samples.isEmpty else { return .success("") } guard !samples.isEmpty else { return .success("") }
if Task.isCancelled { return .cancelled } if Task.isCancelled { return .cancelled }
@@ -228,9 +253,12 @@ final class SpeechAnalyzerASR: ASRService, @unchecked Sendable {
throw ASRChunkError.localeUnsupported throw ASRChunkError.localeUnsupported
} }
let localeID = resolvedLocale.identifier(.bcp47) let localeID = resolvedLocale.identifier(.bcp47)
let transcriber = DictationTranscriber( let lmConfiguration = CustomLanguageModelManager.shared.configurationForTranscription(
locale: resolvedLocale
)
let transcriber = CustomLanguageModelManager.makeDictationTranscriber(
locale: resolvedLocale, locale: resolvedLocale,
preset: .progressiveLongDictation lmConfiguration: lmConfiguration
) )
let analyzerFormat: AVAudioFormat let analyzerFormat: AVAudioFormat
@@ -337,9 +365,12 @@ final class SpeechAnalyzerASR: ASRService, @unchecked Sendable {
} }
// Each pipelined chunk is 30 s; long dictation preset keeps a // Each pipelined chunk is 30 s; long dictation preset keeps a
// single chunk coherent (Flow utterances run up to 3 min). // single chunk coherent (Flow utterances run up to 3 min).
let transcriber = DictationTranscriber( let lmConfiguration = CustomLanguageModelManager.shared.configurationForTranscription(
locale: resolvedLocale
)
let transcriber = CustomLanguageModelManager.makeDictationTranscriber(
locale: resolvedLocale, locale: resolvedLocale,
preset: .progressiveLongDictation lmConfiguration: lmConfiguration
) )
do { do {
try await Self.prepareAssetsIfNeeded(for: transcriber, locale: resolvedLocale) try await Self.prepareAssetsIfNeeded(for: transcriber, locale: resolvedLocale)
+94 -287
View File
@@ -1,13 +1,10 @@
// AppGroupStore.swift // AppGroupStore.swift
// OSGKeyboard · Shared // OSGKeyboard · Shared
// //
// Convenience wrapper around App Group UserDefaults for non-Published reads. // Thin read/write facade over `AppGroupConfiguration` for the keyboard
// Used by the keyboard extension (no SwiftUI) to read config without // extension (no SwiftUI) and other non-ObservableObject call sites.
// instantiating an ObservableObject.
// //
// `apiKey` is NOT read from UserDefaults see `Keychain.swift`. We // `apiKey` is NOT stored in UserDefaults see `Keychain.swift`.
// share access between the host app and the keyboard extension via a
// shared keychain-access-group declared in both targets' entitlements.
import Foundation import Foundation
@@ -19,340 +16,150 @@ public struct AppGroupStore: @unchecked Sendable {
self.defaults = defaults self.defaults = defaults
return return
} }
// Never hard-crash on implicit construction sites (e.g. default guard let available = AppGroup.defaultsIfAvailable else {
// service initializers). If App Group is unavailable, use .standard #if DEBUG
// so callers can still surface a user-facing setup error. fatalError("App Group unavailable — inject UserDefaults in tests or fix entitlements.")
self.defaults = AppGroup.isAvailable ? AppGroup.defaults : .standard #else
// Callers must check `AppGroup.isAvailable` before constructing.
fatalError("App Group unavailable.")
#endif
}
self.defaults = available
} }
// MARK: - Keys private var configuration: AppGroupConfiguration {
AppGroupConfiguration.load(fromAvailable: defaults)
}
private enum Key { private func mutateConfiguration(_ transform: (inout AppGroupConfiguration) -> Void) {
static let providerId = "config.providerId" var config = AppGroupConfiguration.load(fromAvailable: defaults)
static let baseURL = "config.baseURL" transform(&config)
static let model = "config.model" config.save(to: defaults)
static let modeId = "config.modeId"
static let localeId = "config.localeId"
static let engineMode = "config.engineMode"
static let localASRBackend = "config.localASRBackend"
static let uiLanguage = "config.uiLanguage"
// v0.2.0: opt-in cloud polish step after local-mode ASR.
static let localModeCloudPolishEnabled = "config.localModeCloudPolishEnabled"
// v0.2.1 follow-up: `config.translationEnabled` was *removed* as a
// persisted key translation is derived from the target locale
// id. New code should only write/read `translationTargetLocaleId`;
// the `translationEnabled` Bool accessor below is kept as a
// computed shim for source compatibility.
static let translationTargetLocaleId = "config.translationTargetLocaleId"
static let handednessPreference = "config.handednessPreference"
// Drag pads beside the mic move the caret like arrow keys.
static let cursorDragNavigationEnabled = "config.cursorDragNavigationEnabled"
// v0.3.0: polish intensity (off / light / medium / heavy).
static let polishIntensity = "config.polishIntensity"
// v0.3.0: last app context detected by the keyboard extension.
// Reused across calls within a 30-minute window so the LLM
// prompt remains consistent during a single typing session.
static let detectedAppContext = "config.detectedAppContext"
static let detectedAppContextAt = "config.detectedAppContextAt"
// v0.3.0: personal dictionary JSON-encoded `PersonalDictionary`.
static let personalDictionary = "config.personalDictionary.v1"
} }
// MARK: - Reads // MARK: - Reads
public var providerId: String { public var providerId: String { configuration.providerId }
defaults.string(forKey: Key.providerId) ?? "openai" public var baseURL: String { configuration.baseURL }
} public var apiKey: String { configuration.apiKey }
public var model: String { configuration.model }
public var baseURL: String { public var modeId: String { configuration.modeId }
defaults.string(forKey: Key.baseURL) ?? LLMProvider.provider(id: providerId).defaultBaseURL public var localeId: String { configuration.localeId }
} public var engineMode: String { configuration.engineMode }
public var uiLanguage: AppUILanguage { configuration.uiLanguage }
/// API key lives in the Keychain (cross-process, encrypted at rest). public var translationEnabled: Bool { configuration.translationEnabled }
/// Returns "" when nothing is stored so the LLMClient can surface a public var translationTargetLocaleId: String { configuration.translationTargetLocaleId }
/// `noAPIKey` error rather than firing off an obviously-bad request. public var handednessPreference: HandednessPreference { configuration.handednessPreference }
public var apiKey: String { public var cursorDragNavigationEnabled: Bool { configuration.cursorDragNavigationEnabled }
Keychain.apiKey(for: providerId) ?? "" public var polishIntensity: PolishIntensity { configuration.polishIntensity }
} public var isTranslationEffective: Bool { configuration.isTranslationEffective }
public var isLocalEngine: Bool { configuration.isLocalEngine }
public var model: String { public var polishModeForPipeline: PolishingService.PolishMode { configuration.polishModeForPipeline }
defaults.string(forKey: Key.model) ?? LLMProvider.provider(id: providerId).defaultModel public var polishProviderIdOverride: String? { configuration.polishProviderIdOverride }
} public var isCloudAPIKeyMissingForVoiceInput: Bool { configuration.isCloudAPIKeyMissingForVoiceInput }
public var modeId: String {
defaults.string(forKey: Key.modeId) ?? "polish"
}
public var localeId: String {
defaults.string(forKey: Key.localeId) ?? "auto"
}
/// "local" on-device ASR only (raw transcript delivery).
/// "cloud" ASR + LLM polish (default behaviour).
public var engineMode: String {
defaults.string(forKey: Key.engineMode) ?? "cloud"
}
/// Which on-device ASR engine backs the "local" engine mode. Falls
/// back to the iOS SpeechAnalyzer path so legacy installs (which
/// never wrote this key) keep working.
public var localASRBackend: LocalASRBackend {
let raw = defaults.string(forKey: Key.localASRBackend) ?? LocalASRBackend.speechAnalyzer.rawValue
return LocalASRBackend(rawValue: raw) ?? .speechAnalyzer
}
/// v0.2.0: whether the local engine should route its transcript
/// through the configured cloud LLM (DeepSeek by default) before
/// insertion. Defaults to `false`; the keyboard extension reads
/// this so Flow sessions honour the toggle.
public var localModeCloudPolishEnabled: Bool {
guard defaults.object(forKey: Key.localModeCloudPolishEnabled) != nil else {
return false
}
return defaults.bool(forKey: Key.localModeCloudPolishEnabled)
}
/// Host-app UI language override (`auto` / `en` / `zh-Hans`).
public var uiLanguage: AppUILanguage {
AppUILanguage.fromStored(defaults.string(forKey: Key.uiLanguage))
}
/// v0.2.1 follow-up: derived translation is on iff a target locale
/// has been selected. The `translationTargetLocaleId` getter below
/// is the source of truth; this property exists for backwards
/// compatibility with call sites that read `store.translationEnabled`.
public var translationEnabled: Bool {
translationTargetLocaleId != TranslationLanguageCatalog.offLocaleId
}
/// v0.2.1: target locale id the translate-and-polish prompt should
/// produce (e.g. `"en"`, `"ja"`). Defaults to `offLocaleId` ("off")
/// when nothing is stored, matching the picker / chip UX where the
/// user has to actively pick a language to turn translation on.
public var translationTargetLocaleId: String {
defaults.string(forKey: Key.translationTargetLocaleId)
?? TranslationLanguageCatalog.offLocaleId
}
/// Bottom-row key order on the keyboard extension.
public var handednessPreference: HandednessPreference {
HandednessPreference.fromStored(defaults.string(forKey: Key.handednessPreference))
}
/// Press-and-drag pads beside the mic for four-way caret movement.
/// Defaults to `true` for new installs.
public var cursorDragNavigationEnabled: Bool {
guard defaults.object(forKey: Key.cursorDragNavigationEnabled) != nil else {
return true
}
return defaults.bool(forKey: Key.cursorDragNavigationEnabled)
}
// MARK: - Writes
public func setModeId(_ id: String) {
defaults.set(id, forKey: Key.modeId)
}
public func setLocaleId(_ id: String) {
defaults.set(id, forKey: Key.localeId)
}
public func setEngineMode(_ mode: String) {
defaults.set(mode, forKey: Key.engineMode)
}
public func setLocalASRBackend(_ backend: LocalASRBackend) {
defaults.set(backend.rawValue, forKey: Key.localASRBackend)
}
public func setUILanguage(_ language: AppUILanguage) {
defaults.set(language.rawValue, forKey: Key.uiLanguage)
}
/// v0.2.1 follow-up: kept for source compatibility with callers that
/// still pass a Bool (e.g. older tests, any leftover bridge code).
/// `enabled == true` selects `defaultLocaleId` ("en") as a sensible
/// on-ramp target; `enabled == false` resets to `offLocaleId`.
/// The keyboard chip / pipeline now write the locale id directly
/// via `setTranslationTargetLocaleId`, which is the preferred path.
public func setTranslationEnabled(_ enabled: Bool) {
defaults.set(
enabled ? TranslationLanguageCatalog.defaultLocaleId : TranslationLanguageCatalog.offLocaleId,
forKey: Key.translationTargetLocaleId
)
}
/// v0.2.1: persist target locale id (e.g. `"en"`, `"ja"`, or
/// `TranslationLanguageCatalog.offLocaleId`). The keyboard
/// extension reads this on every `load()` and `refreshRuntimeFlags()`
/// so the chip reflects the latest value without a host-app
/// round-trip.
public func setTranslationTargetLocaleId(_ id: String) {
defaults.set(id, forKey: Key.translationTargetLocaleId)
AppGroupConfigDarwin.postConfigChanged()
}
public func setHandednessPreference(_ preference: HandednessPreference) {
defaults.set(preference.rawValue, forKey: Key.handednessPreference)
AppGroupConfigDarwin.postConfigChanged()
}
public func setCursorDragNavigationEnabled(_ enabled: Bool) {
defaults.set(enabled, forKey: Key.cursorDragNavigationEnabled)
AppGroupConfigDarwin.postConfigChanged()
}
/// Whether ASR output should be sent through the LLM polish step.
/// Both engines always run polish after ASR completes (chunked
/// pipeline stitches first). Ultra-short structure-free utterances
/// may skip the LLM inside `PolishingService`.
public var shouldRunCloudLLMStep: Bool { true }
/// Whether translate-and-polish should run (vs polish-only).
public var isTranslationEffective: Bool {
translationEnabled
}
/// Whether the keyboard top-bar translation chip should render. /// Whether the keyboard top-bar translation chip should render.
public var isTranslationChipVisible: Bool { true } public var isTranslationChipVisible: Bool { true }
/// Cloud engine requires a provider-specific API key before the user // MARK: - Writes
/// can start voice input. Local engine uses the built-in DeepSeek path.
public var isCloudAPIKeyMissingForVoiceInput: Bool { public func setModeId(_ id: String) {
guard engineMode == "cloud" else { return false } mutateConfiguration { $0.modeId = id }
return apiKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
} }
/// Polish vs translate-and-polish for the active pipeline. public func setLocaleId(_ id: String) {
public var polishModeForPipeline: PolishingService.PolishMode { mutateConfiguration { $0.localeId = id }
isTranslationEffective
? .translate(targetLocaleId: translationTargetLocaleId)
: .polish
} }
/// Local engine pins the LLM step to DeepSeek; cloud uses the public func setEngineMode(_ mode: String) {
/// user's configured provider. mutateConfiguration { config in
public var polishProviderIdOverride: String? { config.engineMode = mode
engineMode == "local" ? "deepseek" : nil if mode == "cloud", config.providerId == "deepseek" {
let openAI = LLMProvider.provider(id: "openai")
config.providerId = openAI.id
config.baseURL = openAI.defaultBaseURL
config.model = openAI.defaultModel
}
}
} }
// MARK: - Polish settings (v0.3.0+) public func setUILanguage(_ language: AppUILanguage) {
mutateConfiguration { $0.uiLanguage = language }
}
/// How aggressively the LLM should rewrite the ASR transcript. public func setTranslationEnabled(_ enabled: Bool) {
/// Defaults to `medium` for new installs. setTranslationTargetLocaleId(
public var polishIntensity: PolishIntensity { enabled ? TranslationLanguageCatalog.defaultLocaleId : TranslationLanguageCatalog.offLocaleId
guard let raw = defaults.string(forKey: Key.polishIntensity) else { )
return .default
} }
let resolved = PolishIntensity.resolve(storedRawValue: raw)
if raw == PolishIntensity.legacyOffRawValue { public func setTranslationTargetLocaleId(_ id: String) {
defaults.set(resolved.rawValue, forKey: Key.polishIntensity) mutateConfiguration { $0.translationTargetLocaleId = id }
AppGroupConfigDarwin.postConfigChanged()
} }
return resolved
public func setHandednessPreference(_ preference: HandednessPreference) {
mutateConfiguration { $0.handednessPreference = preference }
AppGroupConfigDarwin.postConfigChanged()
}
public func setCursorDragNavigationEnabled(_ enabled: Bool) {
mutateConfiguration { $0.cursorDragNavigationEnabled = enabled }
AppGroupConfigDarwin.postConfigChanged()
} }
public func setPolishIntensity(_ intensity: PolishIntensity) { public func setPolishIntensity(_ intensity: PolishIntensity) {
defaults.set(intensity.rawValue, forKey: Key.polishIntensity) mutateConfiguration { $0.polishIntensity = intensity }
} }
// MARK: - Onboarding (v0.3.0+)
//
// Mirrored from `ProviderConfig` so the keyboard extension's
// overlay can read / write the same source of truth without
// instantiating the main-app config (which would drag in
// SwiftUI / Combine and fight the keyboard's main-thread budget).
public var hasCompletedOnboarding: Bool { public var hasCompletedOnboarding: Bool {
get { defaults.bool(forKey: "config.hasCompletedOnboarding") } get { configuration.hasCompletedOnboarding }
set { defaults.set(newValue, forKey: "config.hasCompletedOnboarding") } set { setHasCompletedOnboarding(newValue) }
} }
public var onboardingPage: Int { public var onboardingPage: Int {
get { defaults.integer(forKey: "config.onboardingPage") } get { configuration.onboardingPage }
set { defaults.set(newValue, forKey: "config.onboardingPage") } set { setOnboardingPage(newValue) }
} }
public func setHasCompletedOnboarding(_ completed: Bool) { public func setHasCompletedOnboarding(_ completed: Bool) {
defaults.set(completed, forKey: "config.hasCompletedOnboarding") mutateConfiguration { config in
config.hasCompletedOnboarding = completed
if completed {
config.onboardingPage = 0
}
}
} }
public func setOnboardingPage(_ page: Int) { public func setOnboardingPage(_ page: Int) {
defaults.set(page, forKey: "config.onboardingPage") mutateConfiguration { $0.onboardingPage = page }
} }
// MARK: - Detected app context (v0.3.0+) // MARK: - Detected app context
/// Last app context the keyboard extension detected for this
/// user, plus the timestamp it was observed. Callers should
/// treat values older than 30 minutes as stale.
public var detectedAppContext: (context: AppContext, observedAt: Date)? { public var detectedAppContext: (context: AppContext, observedAt: Date)? {
guard let raw = defaults.string(forKey: Key.detectedAppContext), configuration.detectedAppContext(from: defaults)
let value = AppContext(rawValue: raw)
else { return nil }
let timestamp = defaults.object(forKey: Key.detectedAppContextAt) as? Date ?? .distantPast
return (value, timestamp)
} }
public func setDetectedAppContext(_ context: AppContext, at date: Date = Date()) { public func setDetectedAppContext(_ context: AppContext, at date: Date = Date()) {
defaults.set(context.rawValue, forKey: Key.detectedAppContext) var config = configuration
defaults.set(date, forKey: Key.detectedAppContextAt) config.setDetectedAppContext(context, at: date, to: defaults)
} }
// MARK: - Personal dictionary (v0.3.0+) // MARK: - Personal dictionary
/// Personal dictionary persisted in the App Group so both the
/// main app's Settings UI and the keyboard extension's LLM call
/// read the same source of truth. Returns an empty dictionary
/// when nothing is stored (and when the stored JSON is corrupt
/// failing closed is safer than crashing the keyboard).
public var personalDictionary: PersonalDictionary { public var personalDictionary: PersonalDictionary {
get { get { configuration.personalDictionary }
guard let data = defaults.data(forKey: Key.personalDictionary) else { set { setPersonalDictionary(newValue) }
return .empty
}
do {
var dictionary = try JSONDecoder().decode(PersonalDictionary.self, from: data)
if dictionary.entries.contains(where: { $0.source == .history }) {
for index in dictionary.entries.indices where dictionary.entries[index].source == .history {
dictionary.entries[index].source = .manual
}
dictionary.version += 1
if let migrated = try? JSONEncoder().encode(dictionary) {
defaults.set(migrated, forKey: Key.personalDictionary)
}
}
return dictionary
} catch {
#if DEBUG
print("⚠️ [AppGroupStore] personalDictionary decode failed: \(error)")
#endif
return .empty
}
}
set {
setPersonalDictionary(newValue)
}
} }
public func setPersonalDictionary(_ dictionary: PersonalDictionary) { public func setPersonalDictionary(_ dictionary: PersonalDictionary) {
do { mutateConfiguration { $0.personalDictionary = dictionary }
let data = try JSONEncoder().encode(dictionary)
defaults.set(data, forKey: Key.personalDictionary)
} catch {
#if DEBUG
print("⚠️ [AppGroupStore] personalDictionary encode failed: \(error)")
#endif
}
} }
// MARK: - Client // MARK: - Client
public func makeClient() -> LLMClient { public func makeClient() -> LLMClient {
OpenAICompatibleClient( configuration.makeClient()
baseURL: baseURL,
apiKey: apiKey,
model: model
)
} }
} }
@@ -0,0 +1,374 @@
// CustomLanguageModelManager.swift
// OSGKeyboard · Shared
//
// Prepares the bundled SFCustomLanguageModelData asset on device and shares
// the compiled LM + Vocab through the App Group container. Both the host app
// and keyboard extension read the same prepared configuration for
// DictationTranscriber content hints.
import Foundation
import Speech
import os
public final class CustomLanguageModelManager: @unchecked Sendable {
public static let shared = CustomLanguageModelManager()
public enum PrepareState: Equatable, Sendable {
case idle
case preparing
case ready
case failed(String)
}
struct BundledManifest: Decodable {
let version: String
let bin_bytes: Int
let identifier: String
}
private enum Storage {
static let subdirectory = "CustomLanguageModel/v1"
static let fingerprintKey = "customLM.preparedFingerprint"
static let preparedAtKey = "customLM.preparedAt"
static let lastFailureAtKey = "customLM.lastFailureAt"
static let attemptCountKey = "customLM.attemptCount"
static let maxRetryAttempts = 3
/// Backoff after failure attempts 1, 2, and 3 (seconds).
static let backoffIntervals: [TimeInterval] = [30, 120, 600]
}
private let lock = OSAllocatedUnfairLock()
private var cachedConfiguration: SFSpeechLanguageModel.Configuration?
private var state: PrepareState = .idle
private var prepareTask: Task<Void, Never>?
private init() {}
// MARK: - Public API
/// Returns a prepared configuration for Chinese locales when available.
public func configurationForTranscription(locale: Locale) -> SFSpeechLanguageModel.Configuration? {
guard Self.isChineseLocale(locale) else { return nil }
return lock.withLock { () -> SFSpeechLanguageModel.Configuration? in
if let cachedConfiguration {
return cachedConfiguration
}
if let loaded = Self.loadCachedConfigurationFromDisk() {
cachedConfiguration = loaded
state = .ready
return loaded
}
return nil
}
}
public func currentState() -> PrepareState {
lock.withLock { state }
}
/// Fire-and-forget preparation for the host app. Safe to call repeatedly.
/// Retries after exponential backoff when a prior attempt failed.
public func prepareInBackgroundIfNeeded() {
guard AppGroup.isAvailable else { return }
let shouldStart = lock.withLock { () -> Bool in
if case .preparing = state { return false }
if cachedConfiguration != nil { return false }
if let loaded = Self.loadCachedConfigurationFromDisk() {
cachedConfiguration = loaded
state = .ready
Self.clearRetryState()
return false
}
if prepareTask != nil { return false }
if case .failed = state {
guard Self.canRetryAfterFailure() else { return false }
} else if !Self.canRetryAfterFailure() {
return false
}
state = .preparing
return true
}
guard shouldStart else { return }
prepareTask = Task.detached(priority: .utility) { [weak self] in
guard let self else { return }
defer {
self.lock.withLock { self.prepareTask = nil }
}
do {
_ = try await self.prepareIfNeeded()
} catch {
Self.recordFailure()
self.lock.withLock {
self.state = .failed(error.localizedDescription)
}
Self.log(
"prepare failed (attempt \(Self.storedAttemptCount())): \(error.localizedDescription)"
)
}
}
}
/// Prepares the bundled training asset into the App Group container.
@discardableResult
public func prepareIfNeeded() async throws -> SFSpeechLanguageModel.Configuration? {
if let existing = configurationForTranscription(locale: Locale(identifier: "zh-Hans")) {
lock.withLock { state = .ready }
Self.clearRetryState()
return existing
}
guard Self.canRetryAfterFailure() else {
throw PrepareError.retryBudgetExhausted
}
guard let manifest = Self.bundledManifest() else {
throw PrepareError.missingManifest
}
guard let assetURL = Self.bundledTrainingAssetURL() else {
throw PrepareError.missingTrainingAsset
}
guard let preparedDir = Self.preparedDirectoryURL() else {
throw PrepareError.missingAppGroupContainer
}
let fingerprint = Self.fingerprint(for: manifest)
if Self.storedFingerprint() == fingerprint,
let cached = Self.loadCachedConfigurationFromDisk() {
lock.withLock {
cachedConfiguration = cached
state = .ready
}
Self.clearRetryState()
return cached
}
lock.withLock { state = .preparing }
let languageModelURL = preparedDir.appendingPathComponent("LM")
let vocabularyURL = preparedDir.appendingPathComponent("Vocab")
try Self.removeItemIfExists(at: languageModelURL)
try Self.removeItemIfExists(at: vocabularyURL)
let configuration = SFSpeechLanguageModel.Configuration(
languageModel: languageModelURL,
vocabulary: vocabularyURL
)
Self.log("preparing custom LM (\(manifest.bin_bytes) byte asset)…")
try await Self.prepareLanguageModel(assetURL: assetURL, configuration: configuration)
guard FileManager.default.fileExists(atPath: languageModelURL.path),
FileManager.default.fileExists(atPath: vocabularyURL.path) else {
throw PrepareError.missingPreparedArtifacts
}
AppGroup.defaultsIfAvailable?.set(fingerprint, forKey: Storage.fingerprintKey)
AppGroup.defaultsIfAvailable?.set(Date().timeIntervalSince1970, forKey: Storage.preparedAtKey)
Self.clearRetryState()
lock.withLock {
cachedConfiguration = configuration
state = .ready
}
Self.log("custom LM ready at \(preparedDir.path)")
return configuration
}
// MARK: - DictationTranscriber factory
public static func makeDictationTranscriber(
locale: Locale,
lmConfiguration: SFSpeechLanguageModel.Configuration?
) -> DictationTranscriber {
let preset = DictationTranscriber.Preset.progressiveLongDictation
guard let lmConfiguration, isChineseLocale(locale) else {
return DictationTranscriber(locale: locale, preset: preset)
}
let contentHints = preset.contentHints.union([
.customizedLanguage(modelConfiguration: lmConfiguration),
])
return DictationTranscriber(
locale: locale,
contentHints: contentHints,
transcriptionOptions: preset.transcriptionOptions,
reportingOptions: preset.reportingOptions,
attributeOptions: preset.attributeOptions
)
}
// MARK: - Bundle / disk helpers
private static var resourceBundle: Bundle {
Bundle(for: CustomLanguageModelManager.self)
}
static func bundledTrainingAssetURL() -> URL? {
if let url = resourceBundle.url(
forResource: "OSGKeyboardCLM",
withExtension: "bin",
subdirectory: Storage.subdirectory
) {
return url
}
return resourceBundle.url(forResource: "OSGKeyboardCLM", withExtension: "bin")
}
static func bundledManifest() -> BundledManifest? {
let manifestURL =
resourceBundle.url(
forResource: "compiled-manifest",
withExtension: "json",
subdirectory: Storage.subdirectory
)
?? resourceBundle.url(forResource: "compiled-manifest", withExtension: "json")
guard let manifestURL,
let data = try? Data(contentsOf: manifestURL),
let manifest = try? JSONDecoder().decode(BundledManifest.self, from: data)
else {
return nil
}
return manifest
}
static func preparedDirectoryURL() -> URL? {
guard let container = FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: AppGroup.identifier
) else {
return nil
}
let directory = container.appendingPathComponent(Storage.subdirectory, isDirectory: true)
try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
return directory
}
static func loadCachedConfigurationFromDisk() -> SFSpeechLanguageModel.Configuration? {
guard let manifest = bundledManifest(),
storedFingerprint() == fingerprint(for: manifest),
let preparedDir = preparedDirectoryURL()
else {
return nil
}
let languageModelURL = preparedDir.appendingPathComponent("LM")
let vocabularyURL = preparedDir.appendingPathComponent("Vocab")
let fm = FileManager.default
guard fm.fileExists(atPath: languageModelURL.path),
fm.fileExists(atPath: vocabularyURL.path) else {
return nil
}
return SFSpeechLanguageModel.Configuration(
languageModel: languageModelURL,
vocabulary: vocabularyURL
)
}
static func isChineseLocale(_ locale: Locale) -> Bool {
locale.identifier(.bcp47).lowercased().hasPrefix("zh")
}
private static func fingerprint(for manifest: BundledManifest) -> String {
"\(manifest.identifier)|\(manifest.version)|\(manifest.bin_bytes)"
}
private static func storedFingerprint() -> String? {
AppGroup.defaultsIfAvailable?.string(forKey: Storage.fingerprintKey)
}
private static func removeItemIfExists(at url: URL) throws {
let fm = FileManager.default
if fm.fileExists(atPath: url.path) {
try fm.removeItem(at: url)
}
}
private static func prepareLanguageModel(
assetURL: URL,
configuration: SFSpeechLanguageModel.Configuration
) async throws {
try await withCheckedThrowingContinuation {
(continuation: CheckedContinuation<Void, Error>) in
SFSpeechLanguageModel.prepareCustomLanguageModel(
for: assetURL,
configuration: configuration
) { error in
if let error {
continuation.resume(throwing: error)
} else {
continuation.resume()
}
}
}
}
// MARK: - Retry / backoff
private static func storedAttemptCount() -> Int {
AppGroup.defaultsIfAvailable?.integer(forKey: Storage.attemptCountKey) ?? 0
}
private static func storedLastFailureAt() -> TimeInterval? {
let value = AppGroup.defaultsIfAvailable?.double(forKey: Storage.lastFailureAtKey) ?? 0
return value > 0 ? value : nil
}
private static func recordFailure() {
guard let defaults = AppGroup.defaultsIfAvailable else { return }
let nextAttempt = storedAttemptCount() + 1
defaults.set(nextAttempt, forKey: Storage.attemptCountKey)
defaults.set(Date().timeIntervalSince1970, forKey: Storage.lastFailureAtKey)
}
private static func clearRetryState() {
guard let defaults = AppGroup.defaultsIfAvailable else { return }
defaults.removeObject(forKey: Storage.attemptCountKey)
defaults.removeObject(forKey: Storage.lastFailureAtKey)
}
/// Returns false when retry budget is exhausted or backoff has not elapsed.
private static func canRetryAfterFailure() -> Bool {
let attempts = storedAttemptCount()
guard attempts > 0 else { return true }
guard attempts <= Storage.maxRetryAttempts else { return false }
guard let lastFailureAt = storedLastFailureAt() else { return true }
let backoffIndex = min(attempts - 1, Storage.backoffIntervals.count - 1)
let requiredDelay = Storage.backoffIntervals[backoffIndex]
let elapsed = Date().timeIntervalSince1970 - lastFailureAt
return elapsed >= requiredDelay
}
private static func log(_ message: String) {
OSGLog.clm.info("\(message, privacy: .public)")
}
enum PrepareError: LocalizedError {
case missingManifest
case missingTrainingAsset
case missingAppGroupContainer
case missingPreparedArtifacts
case retryBudgetExhausted
var errorDescription: String? {
switch self {
case .missingManifest:
return "Missing bundled custom language model manifest."
case .missingTrainingAsset:
return "Missing bundled custom language model training asset."
case .missingAppGroupContainer:
return "App Group container unavailable for custom language model preparation."
case .missingPreparedArtifacts:
return "Custom language model preparation did not produce LM/Vocab artifacts."
case .retryBudgetExhausted:
return "Custom language model preparation retry budget exhausted."
}
}
}
}
@@ -1,135 +0,0 @@
// DictationBridge.swift
// OSGKeyboard · Shared
//
// Lightweight App Group bridge for host-app dictation handoff:
// keyboard extension -> open host app for recording
// host app -> writes final transcript
// keyboard extension -> consumes pending transcript and inserts text
//
// STATUS (v0.1.2): Retained. Consumed by `KeyboardViewController` for
// the "one-shot" host-app dictation path (where the keyboard extension
// launches the host app, the user records there, and the resulting
// text is consumed back by the extension). The *continuous* path goes
// through `FlowSessionBridge` + `FlowSessionManager` instead.
import Foundation
public enum DictationBridge {
public enum Status: String, Sendable, Equatable {
case idle
case requested
case recording
case transcribing
case done
case cancelled
case error
}
private enum Key {
static let pendingText = "dictation.pendingText"
static let polishWarning = "dictation.polishWarning"
static let updatedAt = "dictation.updatedAt"
static let status = "dictation.status"
static let statusUpdatedAt = "dictation.statusUpdatedAt"
static let statusMessage = "dictation.statusMessage"
}
private static func resolvedDefaults(_ defaults: UserDefaults?) -> UserDefaults {
if let defaults {
return defaults
}
return AppGroup.isAvailable ? AppGroup.defaults : .standard
}
public static func setStatus(
_ status: Status,
message: String? = nil,
defaults: UserDefaults? = nil
) {
let store = resolvedDefaults(defaults)
store.set(status.rawValue, forKey: Key.status)
store.set(Date().timeIntervalSince1970, forKey: Key.statusUpdatedAt)
if let message, !message.isEmpty {
store.set(message, forKey: Key.statusMessage)
} else {
store.removeObject(forKey: Key.statusMessage)
}
}
public static func currentStatus(
defaults: UserDefaults? = nil
) -> (status: Status, message: String?, updatedAt: TimeInterval) {
let store = resolvedDefaults(defaults)
let raw = store.string(forKey: Key.status) ?? Status.idle.rawValue
let status = Status(rawValue: raw) ?? .idle
let message = store.string(forKey: Key.statusMessage)
let updatedAt = store.double(forKey: Key.statusUpdatedAt)
return (status, message, updatedAt)
}
public static func markRequested(defaults: UserDefaults? = nil) {
let store = resolvedDefaults(defaults)
store.removeObject(forKey: Key.pendingText)
setStatus(.requested, defaults: store)
}
/// Store a transcript for the keyboard extension to consume.
public static func storePendingTranscript(
_ text: String,
polishWarning: String? = nil,
defaults: UserDefaults? = nil
) {
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return }
let store = resolvedDefaults(defaults)
store.set(trimmed, forKey: Key.pendingText)
store.set(Date().timeIntervalSince1970, forKey: Key.updatedAt)
if let polishWarning, !polishWarning.isEmpty {
store.set(polishWarning, forKey: Key.polishWarning)
} else {
store.removeObject(forKey: Key.polishWarning)
}
setStatus(.done, defaults: store)
}
/// Returns and clears the pending transcript if present.
public static func consumePendingTranscript(
maxAge: TimeInterval = 180,
defaults: UserDefaults? = nil
) -> String? {
consumePendingDelivery(maxAge: maxAge, defaults: defaults)?.text
}
/// Returns and clears the pending delivery (text + optional polish
/// warning) if present.
public static func consumePendingDelivery(
maxAge: TimeInterval = 180,
defaults: UserDefaults? = nil
) -> TranscriptionDelivery? {
let store = resolvedDefaults(defaults)
guard let text = store.string(forKey: Key.pendingText) else {
return nil
}
if maxAge > 0 {
let ts = store.double(forKey: Key.updatedAt)
if ts > 0, Date().timeIntervalSince1970 - ts > maxAge {
clear(defaults: store)
return nil
}
}
let warning = store.string(forKey: Key.polishWarning)
store.removeObject(forKey: Key.pendingText)
store.removeObject(forKey: Key.polishWarning)
store.removeObject(forKey: Key.updatedAt)
setStatus(.idle, defaults: store)
return TranscriptionDelivery(text: text, polishWarning: warning)
}
public static func clear(defaults: UserDefaults? = nil) {
let store = resolvedDefaults(defaults)
store.removeObject(forKey: Key.pendingText)
store.removeObject(forKey: Key.polishWarning)
store.removeObject(forKey: Key.updatedAt)
setStatus(.idle, defaults: store)
}
}
@@ -1,39 +0,0 @@
// FlowAppLifecycle.swift
// OSGKeyboard · Shared
//
// Tracks whether the host app process is in the foreground.
// Retained for any future GPU-backed paths; CoreML ASR does not require it.
import Foundation
public final class FlowAppLifecycle: @unchecked Sendable {
public static let shared = FlowAppLifecycle()
private let lock = NSLock()
private var isForeground = true
private init() {}
/// `true` when the host app scene is active (`.active`).
public var allowsGPUInference: Bool {
lock.lock()
defer { lock.unlock() }
return isForeground
}
public func setForeground(_ foreground: Bool) {
lock.lock()
isForeground = foreground
lock.unlock()
}
/// Blocks until foreground or cancellation.
public func waitUntilForeground() async -> Bool {
while !allowsGPUInference {
if Task.isCancelled { return false }
try? await Task.sleep(nanoseconds: 200_000_000)
}
return true
}
}
@@ -2,15 +2,31 @@
// OSGKeyboard · Shared // OSGKeyboard · Shared
// //
// TypeWhisper-style Flow session bridge: keyboard writes recording // TypeWhisper-style Flow session bridge: keyboard writes recording
// signals; host app writes transcription results. Legacy one-shot // signals; host app writes transcription results.
// dictation handoff remains in `DictationBridge`.
import Foundation import Foundation
public struct FlowTranscriptionError: Equatable, Sendable {
public let message: String
public let kind: FlowSessionKeys.TranscriptionErrorKind
public init(message: String, kind: FlowSessionKeys.TranscriptionErrorKind) {
self.message = message
self.kind = kind
}
}
public enum FlowSessionBridge { public enum FlowSessionBridge {
private static func resolvedDefaults(_ defaults: UserDefaults?) -> UserDefaults { private static func resolvedDefaults(_ defaults: UserDefaults?) -> UserDefaults {
if let defaults { return defaults } if let defaults { return defaults }
return AppGroup.isAvailable ? AppGroup.defaults : .standard guard let available = AppGroup.defaultsIfAvailable else {
#if DEBUG
fatalError("App Group unavailable — inject UserDefaults in tests or fix entitlements.")
#else
fatalError("App Group unavailable.")
#endif
}
return available
} }
/// Force cross-process visibility. Must only be called on the main thread. /// Force cross-process visibility. Must only be called on the main thread.
@@ -70,7 +86,6 @@ public enum FlowSessionBridge {
/// background while the continuous audio session is frozen. /// background while the continuous audio session is frozen.
public static func isSessionActive(defaults: UserDefaults? = nil) -> Bool { public static func isSessionActive(defaults: UserDefaults? = nil) -> Bool {
let store = resolvedDefaults(defaults) let store = resolvedDefaults(defaults)
flush(store)
guard store.bool(forKey: FlowSessionKeys.flowSessionActive) else { return false } guard store.bool(forKey: FlowSessionKeys.flowSessionActive) else { return false }
let expires = store.double(forKey: FlowSessionKeys.flowSessionExpires) let expires = store.double(forKey: FlowSessionKeys.flowSessionExpires)
@@ -81,7 +96,6 @@ public enum FlowSessionBridge {
/// actively processing). Used for auto-start heuristics, not gating record. /// actively processing). Used for auto-start heuristics, not gating record.
public static func isHostReachable(defaults: UserDefaults? = nil) -> Bool { public static func isHostReachable(defaults: UserDefaults? = nil) -> Bool {
let store = resolvedDefaults(defaults) let store = resolvedDefaults(defaults)
flush(store)
guard isSessionActive(defaults: store) else { return false } guard isSessionActive(defaults: store) else { return false }
let heartbeat = store.double(forKey: FlowSessionKeys.flowHeartbeat) let heartbeat = store.double(forKey: FlowSessionKeys.flowHeartbeat)
@@ -144,6 +158,7 @@ public enum FlowSessionBridge {
let store = resolvedDefaults(defaults) let store = resolvedDefaults(defaults)
store.set(trimmed, forKey: FlowSessionKeys.transcriptionResult) store.set(trimmed, forKey: FlowSessionKeys.transcriptionResult)
store.removeObject(forKey: FlowSessionKeys.transcriptionError) store.removeObject(forKey: FlowSessionKeys.transcriptionError)
store.removeObject(forKey: FlowSessionKeys.transcriptionPartial)
if let polishWarning, !polishWarning.isEmpty { if let polishWarning, !polishWarning.isEmpty {
store.set(polishWarning, forKey: FlowSessionKeys.transcriptionPolishWarning) store.set(polishWarning, forKey: FlowSessionKeys.transcriptionPolishWarning)
} else { } else {
@@ -151,16 +166,46 @@ public enum FlowSessionBridge {
} }
setRecordingState(.idle, defaults: store) setRecordingState(.idle, defaults: store)
flush(store) flush(store)
FlowSessionDarwin.postTranscriptionChanged()
}
/// Host app: publish pipelined ASR partial while recording or finalizing.
public static func storeTranscriptionPartial(
_ text: String,
defaults: UserDefaults? = nil
) {
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
let store = resolvedDefaults(defaults)
if trimmed.isEmpty {
store.removeObject(forKey: FlowSessionKeys.transcriptionPartial)
} else {
store.set(trimmed, forKey: FlowSessionKeys.transcriptionPartial)
}
flush(store)
FlowSessionDarwin.postTranscriptionChanged()
}
/// Keyboard: read the latest partial without clearing it.
public static func transcriptionPartial(defaults: UserDefaults? = nil) -> String? {
let store = resolvedDefaults(defaults)
guard let text = store.string(forKey: FlowSessionKeys.transcriptionPartial),
!text.isEmpty else {
return nil
}
return text
} }
public static func storeTranscriptionError( public static func storeTranscriptionError(
_ message: String, _ message: String,
kind: FlowSessionKeys.TranscriptionErrorKind = .generic,
defaults: UserDefaults? = nil defaults: UserDefaults? = nil
) { ) {
let store = resolvedDefaults(defaults) let store = resolvedDefaults(defaults)
store.set(message, forKey: FlowSessionKeys.transcriptionError) store.set(message, forKey: FlowSessionKeys.transcriptionError)
store.set(kind.rawValue, forKey: FlowSessionKeys.transcriptionErrorKind)
setRecordingState(.idle, defaults: store) setRecordingState(.idle, defaults: store)
flush(store) flush(store)
FlowSessionDarwin.postTranscriptionChanged()
} }
/// Returns and clears a pending transcription result, if any. /// Returns and clears a pending transcription result, if any.
@@ -174,7 +219,6 @@ public enum FlowSessionBridge {
defaults: UserDefaults? = nil defaults: UserDefaults? = nil
) -> TranscriptionDelivery? { ) -> TranscriptionDelivery? {
let store = resolvedDefaults(defaults) let store = resolvedDefaults(defaults)
flush(store)
guard let text = store.string(forKey: FlowSessionKeys.transcriptionResult), !text.isEmpty else { guard let text = store.string(forKey: FlowSessionKeys.transcriptionResult), !text.isEmpty else {
return nil return nil
} }
@@ -186,20 +230,21 @@ public enum FlowSessionBridge {
} }
/// Returns and clears a pending transcription error, if any. /// Returns and clears a pending transcription error, if any.
public static func consumeTranscriptionError(defaults: UserDefaults? = nil) -> String? { public static func consumeTranscriptionError(defaults: UserDefaults? = nil) -> FlowTranscriptionError? {
let store = resolvedDefaults(defaults) let store = resolvedDefaults(defaults)
flush(store)
guard let message = store.string(forKey: FlowSessionKeys.transcriptionError), !message.isEmpty else { guard let message = store.string(forKey: FlowSessionKeys.transcriptionError), !message.isEmpty else {
return nil return nil
} }
let kindRaw = store.string(forKey: FlowSessionKeys.transcriptionErrorKind)
let kind = FlowSessionKeys.TranscriptionErrorKind(rawValue: kindRaw ?? "") ?? .generic
store.removeObject(forKey: FlowSessionKeys.transcriptionError) store.removeObject(forKey: FlowSessionKeys.transcriptionError)
store.removeObject(forKey: FlowSessionKeys.transcriptionErrorKind)
flush(store) flush(store)
return message return FlowTranscriptionError(message: message, kind: kind)
} }
public static func audioLevels(defaults: UserDefaults? = nil) -> [Float] { public static func audioLevels(defaults: UserDefaults? = nil) -> [Float] {
let store = resolvedDefaults(defaults) let store = resolvedDefaults(defaults)
flush(store)
if let levels = store.array(forKey: FlowSessionKeys.audioLevels) as? [Double], !levels.isEmpty { if let levels = store.array(forKey: FlowSessionKeys.audioLevels) as? [Double], !levels.isEmpty {
return levels.map { Float($0) } return levels.map { Float($0) }
} }
@@ -240,7 +285,9 @@ public enum FlowSessionBridge {
private static func clearTranscription(defaults: UserDefaults) { private static func clearTranscription(defaults: UserDefaults) {
defaults.removeObject(forKey: FlowSessionKeys.transcriptionResult) defaults.removeObject(forKey: FlowSessionKeys.transcriptionResult)
defaults.removeObject(forKey: FlowSessionKeys.transcriptionPartial)
defaults.removeObject(forKey: FlowSessionKeys.transcriptionPolishWarning) defaults.removeObject(forKey: FlowSessionKeys.transcriptionPolishWarning)
defaults.removeObject(forKey: FlowSessionKeys.transcriptionError) defaults.removeObject(forKey: FlowSessionKeys.transcriptionError)
defaults.removeObject(forKey: FlowSessionKeys.transcriptionErrorKind)
} }
} }
@@ -8,6 +8,8 @@ import Foundation
public enum FlowSessionDarwin { public enum FlowSessionDarwin {
public static let notificationName = "com.osgkeyboard.flow.session.changed" public static let notificationName = "com.osgkeyboard.flow.session.changed"
/// Posted when the host app writes a transcription result or error.
public static let transcriptionNotificationName = "com.osgkeyboard.flow.transcription.changed"
public static func postSessionChanged() { public static func postSessionChanged() {
CFNotificationCenterPostNotification( CFNotificationCenterPostNotification(
@@ -18,6 +20,16 @@ public enum FlowSessionDarwin {
true true
) )
} }
public static func postTranscriptionChanged() {
CFNotificationCenterPostNotification(
CFNotificationCenterGetDarwinNotifyCenter(),
CFNotificationName(transcriptionNotificationName as CFString),
nil,
nil,
true
)
}
} }
/// Observes Darwin notifications on a background thread; invokes /// Observes Darwin notifications on a background thread; invokes
@@ -13,9 +13,13 @@ public enum FlowSessionKeys {
public static let keyboardRecordingState = "flow.keyboardRecordingState" public static let keyboardRecordingState = "flow.keyboardRecordingState"
public static let transcriptionLanguage = "flow.transcriptionLanguage" public static let transcriptionLanguage = "flow.transcriptionLanguage"
public static let transcriptionResult = "flow.transcriptionResult" public static let transcriptionResult = "flow.transcriptionResult"
/// Live pipelined ASR partial for the keyboard transcript line.
public static let transcriptionPartial = "flow.transcriptionPartial"
/// Soft warning when polish failed but raw transcript was delivered. /// Soft warning when polish failed but raw transcript was delivered.
public static let transcriptionPolishWarning = "flow.transcriptionPolishWarning" public static let transcriptionPolishWarning = "flow.transcriptionPolishWarning"
public static let transcriptionError = "flow.transcriptionError" public static let transcriptionError = "flow.transcriptionError"
/// Structured kind paired with `transcriptionError` for keyboard UI.
public static let transcriptionErrorKind = "flow.transcriptionErrorKind"
public static let audioLevels = "flow.audioLevels" public static let audioLevels = "flow.audioLevels"
/// Heartbeat older than this while the host is foreground likely killed. /// Heartbeat older than this while the host is foreground likely killed.
@@ -36,15 +40,7 @@ public enum FlowSessionKeys {
/// Keyboard watchdog after the user stops recording (not utterance max length). /// Keyboard watchdog after the user stops recording (not utterance max length).
/// Must cover worst-case post-stop backlog: remaining SpeechAnalyzer chunks /// Must cover worst-case post-stop backlog: remaining SpeechAnalyzer chunks
/// plus cloud LLM polish (see `PolishingService.effectiveTimeout` cap). /// plus cloud LLM polish (see `PolishingService.effectiveTimeout` cap).
/// public static func keyboardResultTimeout(engineMode: String) -> TimeInterval {
/// As of v0.2.0 the local engine uses iOS `SpeechAnalyzer` only, so the
/// previous Qwen3-specific timeout (240 s) collapses into the shared
/// local path. We keep `localASRBackend` on the signature for symmetry
/// with other shared helpers.
public static func keyboardResultTimeout(
engineMode: String,
localASRBackend: LocalASRBackend
) -> TimeInterval {
if engineMode == "local" { if engineMode == "local" {
return 180 return 180
} }
@@ -58,4 +54,13 @@ public enum FlowSessionKeys {
case processing case processing
case aborted case aborted
} }
/// Structured host keyboard transcription failure kind.
public enum TranscriptionErrorKind: String, Sendable, Equatable {
case noSpeech
case recognitionInterrupted
case audioUnavailable
case asrFailed
case generic
}
} }
+36 -27
View File
@@ -35,26 +35,37 @@ public final class KeyboardState: ObservableObject {
case asr(String) case asr(String)
case llm(LLMError) case llm(LLMError)
case appGroupUnavailable case appGroupUnavailable
/// Keyboard extension lacks Full Access for host-app jumps.
case fullAccessRequired
/// Auto-jump to the host app failed; user must open it manually.
case manualOpenRequired
/// Host delivered raw transcript; polish step failed or was skipped.
case polishDegraded(String)
/// Host ASR finished with no usable speech.
case noSpeechDetected
/// Host ASR was interrupted before a final transcript arrived.
case recognitionInterrupted
/// Host could not start background audio capture.
case hostAudioUnavailable
/// Host ASR or pipeline failed with a user-facing message.
case hostTranscriptionFailed(String)
/// Flow result did not arrive before the keyboard watchdog expired.
case flowResultTimeout
/// Host Flow session ended while the keyboard was idle.
case flowSessionExpired
case unknown(String) case unknown(String)
} }
public enum Reason: Equatable { case mic, speech } public enum Reason: Equatable { case mic, speech }
} }
/// Voice input always runs through polish; legacy off/transcribe modes removed.
public enum InputMode: String, CaseIterable, Identifiable { public enum InputMode: String, CaseIterable, Identifiable {
case off
case transcribe
case polish case polish
public var id: String { rawValue } public var id: String { rawValue }
public var labelKey: String { public var labelKey: String { "mode.polish" }
switch self {
case .off: return "mode.off"
case .transcribe: return "mode.transcribe"
case .polish: return "mode.polish"
}
}
} }
@Published public var phase: Phase = .idle @Published public var phase: Phase = .idle
@@ -78,20 +89,6 @@ public final class KeyboardState: ObservableObject {
@Published public var micDisabledHint: String = "" @Published public var micDisabledHint: String = ""
/// "local" on-device ASR only. "cloud" ASR + LLM polish. /// "local" on-device ASR only. "cloud" ASR + LLM polish.
@Published public var engineMode: String = "cloud" @Published public var engineMode: String = "cloud"
/// Which on-device ASR engine to use when `engineMode == "local"`.
/// Mirrored from `ProviderConfig.localASRBackend` for UI display
/// and for `state` consumers that want a single source of truth.
@Published public var localASRBackend: LocalASRBackend = .speechAnalyzer
/// v0.2.0: kept for source compatibility with the previous Qwen3
/// CoreML local engine. Always `true` now iOS `SpeechAnalyzer`
/// ships with iOS 26 and has no per-user weights to download or
/// preload. Existing read sites will see `true` and behave the
/// same as the "stack ready" branch did.
@Published public var localModelsReady: Bool = true
/// v0.2.0: kept for source compatibility with the previous Qwen3
/// CoreML local engine. Always `false` now there are no weights
/// for the host app to preload.
@Published public var localModelsLoaded: Bool = false
/// v0.2.1 follow-up: derived translation is on iff a target /// v0.2.1 follow-up: derived translation is on iff a target
/// locale has been selected (mirrors `ProviderConfig.translationEnabled` /// locale has been selected (mirrors `ProviderConfig.translationEnabled`
/// so the chip / pipeline read the same source of truth). /// so the chip / pipeline read the same source of truth).
@@ -103,9 +100,6 @@ public final class KeyboardState: ObservableObject {
/// Defaults to `offLocaleId` so the keyboard boots in the "off" /// Defaults to `offLocaleId` so the keyboard boots in the "off"
/// state on first install. /// state on first install.
@Published public var translationTargetLocaleId: String = TranslationLanguageCatalog.offLocaleId @Published public var translationTargetLocaleId: String = TranslationLanguageCatalog.offLocaleId
/// v0.2.0: mirrored from App Group kept for source compatibility.
/// Local engine always runs built-in polish; the flag is ignored.
@Published public var localModeCloudPolishEnabled: Bool = true
/// Mirrored from App Group swaps delete / return on the bottom row. /// Mirrored from App Group swaps delete / return on the bottom row.
@Published public var handednessPreference: HandednessPreference = .left @Published public var handednessPreference: HandednessPreference = .left
/// Press-and-drag pads beside the mic for four-way caret movement. /// Press-and-drag pads beside the mic for four-way caret movement.
@@ -159,7 +153,6 @@ public final class KeyboardState: ObservableObject {
public var setMode: (InputMode) -> Void = { _ in } public var setMode: (InputMode) -> Void = { _ in }
public var setLocale: (String) -> Void = { _ in } public var setLocale: (String) -> Void = { _ in }
public var setEngineMode: (String) -> Void = { _ in } public var setEngineMode: (String) -> Void = { _ in }
public var setLocalASRBackend: (LocalASRBackend) -> Void = { _ in }
/// v0.2.1 follow-up: only the locale picker remains `enabled` /// v0.2.1 follow-up: only the locale picker remains `enabled`
/// is derived from the locale id, so there's no separate toggle to /// is derived from the locale id, so there's no separate toggle to
/// persist. Wired in `KeyboardViewController.installStateActions`. /// persist. Wired in `KeyboardViewController.installStateActions`.
@@ -210,3 +203,19 @@ public final class KeyboardState: ObservableObject {
} }
#endif #endif
} }
extension KeyboardState.Phase.ErrorKind {
/// Maps a host-app Flow transcription failure into a keyboard error kind.
public static func fromFlowTranscription(_ error: FlowTranscriptionError) -> Self {
switch error.kind {
case .noSpeech:
return .noSpeechDetected
case .recognitionInterrupted:
return .recognitionInterrupted
case .audioUnavailable:
return .hostAudioUnavailable
case .asrFailed, .generic:
return .hostTranscriptionFailed(error.message)
}
}
}
@@ -14,7 +14,7 @@
// This class is still imported by: // This class is still imported by:
// - `OSGKeyboard/Views/PreviewASRController.swift` (typealias) // - `OSGKeyboard/Views/PreviewASRController.swift` (typealias)
// - `OSGKeyboard/Views/KeyboardPreviewSheet.swift` (in-app preview) // - `OSGKeyboard/Views/KeyboardPreviewSheet.swift` (in-app preview)
// - `OSGKeyboard/Views/DictationCaptureView.swift` (host-app fallback) // - `OSGKeyboard/Views/KeyboardPreviewSheet.swift` (host-app ASR preview)
// - `OSGKeyboardTests/PreviewASRControllerStateTests.swift` // - `OSGKeyboardTests/PreviewASRControllerStateTests.swift`
// //
// Do NOT remove without updating those call sites. The earlier // Do NOT remove without updating those call sites. The earlier
@@ -104,14 +104,7 @@ public final class LiveDictationController: ObservableObject {
private var didInstallTap = false private var didInstallTap = false
public init(asr: ASRService? = nil) { public init(asr: ASRService? = nil) {
// Resolve through the factory so the user's `LocalASRBackend` self.asr = asr ?? ASRServiceFactory.make()
// selection is honoured. Tests can pass a stub `asr` directly
// to bypass the factory and exercise the controller in
// isolation.
self.asr = asr ?? ASRServiceFactory.make(
engineMode: ProviderConfig.shared.engineMode,
localBackend: ProviderConfig.shared.localASRBackend
)
} }
/// Start dictation using a persisted settings locale id (`auto`, `zh-Hans`, ). /// Start dictation using a persisted settings locale id (`auto`, `zh-Hans`, ).
+17
View File
@@ -0,0 +1,17 @@
// OSGLog.swift
// OSGKeyboard · Shared
//
// Unified os.Logger categories for cross-target diagnostics. Filter in
// Console.app with subsystem `com.osgkeyboard.ios`.
import os
public enum OSGLog {
private static let subsystem = "com.osgkeyboard.ios"
public static let flow = Logger(subsystem: subsystem, category: "flow")
public static let clm = Logger(subsystem: subsystem, category: "clm")
public static let config = Logger(subsystem: subsystem, category: "config")
public static let asr = Logger(subsystem: subsystem, category: "asr")
public static let keyboardExt = Logger(subsystem: subsystem, category: "keyboardExt")
}
@@ -17,7 +17,8 @@ public enum UtteranceStreamChunker {
AsyncStream { continuation in AsyncStream { continuation in
let task = Task { let task = Task {
var buffer: [Float] = [] var buffer: [Float] = []
buffer.reserveCapacity(config.maxChunkSamples + config.pauseExtensionSamples) let initialCapacity = config.maxChunkSamples(forChunkIndex: 0) + config.pauseExtensionSamples
buffer.reserveCapacity(initialCapacity)
var chunkIndex = 0 var chunkIndex = 0
func emit(upTo splitEnd: Int, isLast: Bool) { func emit(upTo splitEnd: Int, isLast: Bool) {
@@ -40,8 +41,12 @@ public enum UtteranceStreamChunker {
guard !snap.samples.isEmpty else { continue } guard !snap.samples.isEmpty else { continue }
buffer.append(contentsOf: snap.samples) buffer.append(contentsOf: snap.samples)
while buffer.count >= config.maxChunkSamples { while buffer.count >= config.maxChunkSamples(forChunkIndex: chunkIndex) {
let split = pauseAwareSplitIndex(in: buffer, config: config) let split = pauseAwareSplitIndex(
in: buffer,
config: config,
chunkIndex: chunkIndex
)
emit(upTo: split, isLast: false) emit(upTo: split, isLast: false)
} }
} }
@@ -66,9 +71,10 @@ public enum UtteranceStreamChunker {
/// Pick a split index at or after `maxChunkSamples`, preferring a pause. /// Pick a split index at or after `maxChunkSamples`, preferring a pause.
static func pauseAwareSplitIndex( static func pauseAwareSplitIndex(
in buffer: [Float], in buffer: [Float],
config: FlowUtteranceChunkConfig config: FlowUtteranceChunkConfig,
chunkIndex: Int = 1
) -> Int { ) -> Int {
let minSplit = config.maxChunkSamples let minSplit = config.maxChunkSamples(forChunkIndex: chunkIndex)
guard buffer.count >= minSplit else { return buffer.count } guard buffer.count >= minSplit else { return buffer.count }
let searchEnd = min(buffer.count, minSplit + config.pauseExtensionSamples) let searchEnd = min(buffer.count, minSplit + config.pauseExtensionSamples)
@@ -14,6 +14,7 @@
"provider.qwen" = "Qwen (DashScope)"; "provider.qwen" = "Qwen (DashScope)";
"provider.zhipu" = "Zhipu GLM"; "provider.zhipu" = "Zhipu GLM";
"provider.moonshot" = "Moonshot"; "provider.moonshot" = "Moonshot";
"provider.mimo" = "Xiaomi MiMo";
"provider.custom" = "Custom"; "provider.custom" = "Custom";
/* LLM errors */ /* LLM errors */
@@ -79,3 +80,10 @@
"dict.source.history" = "Auto-learned"; "dict.source.history" = "Auto-learned";
"dict.source.contacts" = "From Contacts"; "dict.source.contacts" = "From Contacts";
"dict.source.recentEdit" = "From recent edit"; "dict.source.recentEdit" = "From recent edit";
/* Keyboard UI (shared between extension + preview) */
"keyboard.tapToTalkA11y" = "Tap to talk";
"keyboard.translation.chip" = "Translate";
"keyboard.translation.offMenu" = "Don't translate";
"keyboard.translation.a11y" = "Translation";
"keyboard.translation.a11yHint" = "Toggle translation or change the target language.";
@@ -14,6 +14,7 @@
"provider.qwen" = "通义千问"; "provider.qwen" = "通义千问";
"provider.zhipu" = "智谱 GLM"; "provider.zhipu" = "智谱 GLM";
"provider.moonshot" = "月之暗面"; "provider.moonshot" = "月之暗面";
"provider.mimo" = "小米 MiMo";
"provider.custom" = "自定义"; "provider.custom" = "自定义";
/* LLM errors */ /* LLM errors */
@@ -79,3 +80,10 @@
"dict.source.history" = "自动学习"; "dict.source.history" = "自动学习";
"dict.source.contacts" = "来自通讯录"; "dict.source.contacts" = "来自通讯录";
"dict.source.recentEdit" = "来自最近编辑"; "dict.source.recentEdit" = "来自最近编辑";
/* 键盘 UI(扩展与预览共用) */
"keyboard.tapToTalkA11y" = "点击说话";
"keyboard.translation.chip" = "翻译";
"keyboard.translation.offMenu" = "不翻译";
"keyboard.translation.a11y" = "翻译";
"keyboard.translation.a11yHint" = "切换翻译或更改目标语言。";
@@ -0,0 +1,109 @@
// AppGroupConfigurationTests.swift
// OSGKeyboardTests
import XCTest
@testable import OSGKeyboardShared
final class AppGroupConfigurationTests: XCTestCase {
private func makeDefaults() -> UserDefaults {
let suite = "group.com.osgkeyboard.shared.tests.config.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suite)!
defaults.removePersistentDomain(forName: suite)
return defaults
}
func testLoadDefaultsWhenSuiteIsEmpty() {
let defaults = makeDefaults()
let config = AppGroupConfiguration.load(fromAvailable: defaults)
XCTAssertEqual(config.providerId, "openai")
XCTAssertEqual(config.modeId, "polish")
XCTAssertEqual(config.localeId, "auto")
XCTAssertEqual(config.engineMode, "cloud")
XCTAssertFalse(config.hasCompletedOnboarding)
XCTAssertEqual(config.onboardingPage, 0)
XCTAssertFalse(config.hasAcknowledgedCloudSharing)
XCTAssertEqual(config.translationTargetLocaleId, TranslationLanguageCatalog.offLocaleId)
XCTAssertFalse(config.translationEnabled)
XCTAssertEqual(config.handednessPreference, .left)
XCTAssertTrue(config.cursorDragNavigationEnabled)
XCTAssertEqual(config.polishIntensity, .default)
XCTAssertTrue(config.personalDictionary.entries.isEmpty)
}
func testSaveAndLoadRoundTrip() {
let defaults = makeDefaults()
var config = AppGroupConfiguration.load(fromAvailable: defaults)
config.providerId = "anthropic"
config.baseURL = "https://example.com/v1"
config.model = "claude-test"
config.modeId = "polish"
config.localeId = "zh-Hans"
config.engineMode = "local"
config.hasCompletedOnboarding = true
config.onboardingPage = 2
config.hasAcknowledgedCloudSharing = true
config.uiLanguage = .chinese
config.translationTargetLocaleId = "en"
config.handednessPreference = .right
config.cursorDragNavigationEnabled = false
config.polishIntensity = .light
config.save(to: defaults)
let loaded = AppGroupConfiguration.load(fromAvailable: defaults)
XCTAssertEqual(loaded.providerId, "anthropic")
XCTAssertEqual(loaded.baseURL, "https://example.com/v1")
XCTAssertEqual(loaded.model, "claude-test")
XCTAssertEqual(loaded.localeId, "zh-Hans")
XCTAssertEqual(loaded.engineMode, "local")
XCTAssertTrue(loaded.hasCompletedOnboarding)
XCTAssertEqual(loaded.onboardingPage, 2)
XCTAssertTrue(loaded.hasAcknowledgedCloudSharing)
XCTAssertEqual(loaded.uiLanguage, .chinese)
XCTAssertEqual(loaded.translationTargetLocaleId, "en")
XCTAssertTrue(loaded.translationEnabled)
XCTAssertEqual(loaded.handednessPreference, .right)
XCTAssertFalse(loaded.cursorDragNavigationEnabled)
XCTAssertEqual(loaded.polishIntensity, .light)
}
func testTranslationEnabledDerivedFromTargetLocale() {
let defaults = makeDefaults()
var config = AppGroupConfiguration.load(fromAvailable: defaults)
XCTAssertFalse(config.translationEnabled)
config.translationTargetLocaleId = "ja"
XCTAssertTrue(config.translationEnabled)
config.translationTargetLocaleId = TranslationLanguageCatalog.offLocaleId
XCTAssertFalse(config.translationEnabled)
}
func testCloudDeepSeekProviderMigratesToOpenAI() {
let defaults = makeDefaults()
defaults.set("deepseek", forKey: AppGroupConfiguration.Keys.providerId)
defaults.set("cloud", forKey: AppGroupConfiguration.Keys.engineMode)
let config = AppGroupConfiguration.load(fromAvailable: defaults)
XCTAssertEqual(config.providerId, "openai")
XCTAssertEqual(defaults.string(forKey: AppGroupConfiguration.Keys.providerId), "openai")
}
func testPolishIntensityLegacyOffMigratesToMedium() {
let defaults = makeDefaults()
defaults.set(PolishIntensity.legacyOffRawValue, forKey: AppGroupConfiguration.Keys.polishIntensity)
let config = AppGroupConfiguration.load(fromAvailable: defaults)
XCTAssertEqual(config.polishIntensity, .medium)
XCTAssertEqual(defaults.string(forKey: AppGroupConfiguration.Keys.polishIntensity), PolishIntensity.medium.rawValue)
}
func testLoadFromNilUsesAppGroupWhenAvailable() {
if AppGroup.defaultsIfAvailable != nil {
XCTAssertNotNil(AppGroupConfiguration.load(from: nil))
} else {
XCTAssertNil(AppGroupConfiguration.load(from: nil))
}
}
}
@@ -47,10 +47,11 @@ final class ChunkedUtterancePipelineTests: XCTestCase {
continuation.yield(AudioBufferSnapshot(samples: [Float](repeating: 0.1, count: 80), sampleRate: 1_000)) continuation.yield(AudioBufferSnapshot(samples: [Float](repeating: 0.1, count: 80), sampleRate: 1_000))
continuation.finish() continuation.finish()
var partials: [String] = [] let partialsLock = OSAllocatedUnfairLock(initialState: [String]())
let outcome = await pipeline.transcribe(stream: stream) { partial in let outcome = await pipeline.transcribe(stream: stream) { partial in
partials.append(partial) partialsLock.withLock { $0.append(partial) }
} }
let partials = partialsLock.withLock { $0 }
guard case .success(let success) = outcome else { guard case .success(let success) = outcome else {
return XCTFail("expected success, got \(outcome)") return XCTFail("expected success, got \(outcome)")
@@ -89,8 +90,7 @@ final class ChunkedUtterancePipelineTests: XCTestCase {
} }
private struct FailingSecondChunkASR: ASRService, @unchecked Sendable { private struct FailingSecondChunkASR: ASRService, @unchecked Sendable {
private let lock = OSAllocatedUnfairLock() private let callIndex = OSAllocatedUnfairLock(initialState: 0)
private var index = 0
func transcribe( func transcribe(
stream: AsyncStream<AudioBufferSnapshot>, stream: AsyncStream<AudioBufferSnapshot>,
@@ -103,9 +103,10 @@ private struct FailingSecondChunkASR: ASRService, @unchecked Sendable {
func transcribeChunk(samples: [Float], locale: Locale) async -> ASRChunkResult { func transcribeChunk(samples: [Float], locale: Locale) async -> ASRChunkResult {
_ = locale _ = locale
let current = lock.withLock { let current = callIndex.withLock { state in
defer { index += 1 } let value = state
return index state += 1
return value
} }
if current == 1 { if current == 1 {
return .failure("simulated chunk error") return .failure("simulated chunk error")
@@ -1,59 +0,0 @@
// DictationBridgeTests.swift
// OSGKeyboardTests
import XCTest
@testable import OSGKeyboardShared
final class DictationBridgeTests: XCTestCase {
private func makeDefaults() -> UserDefaults {
let suite = "group.com.osgkeyboard.shared.tests.dictation.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suite)!
defaults.removePersistentDomain(forName: suite)
return defaults
}
func testStoreAndConsumeTranscript() {
let defaults = makeDefaults()
DictationBridge.storePendingTranscript(" hello ", defaults: defaults)
let consumed = DictationBridge.consumePendingTranscript(defaults: defaults)
XCTAssertEqual(consumed, "hello")
XCTAssertNil(DictationBridge.consumePendingTranscript(defaults: defaults))
}
func testConsumeIgnoresExpiredTranscript() {
let defaults = makeDefaults()
DictationBridge.storePendingTranscript("stale", defaults: defaults)
// maxAge = 1ms, then delay to force expiry
usleep(2_000)
let consumed = DictationBridge.consumePendingTranscript(maxAge: 0.001, defaults: defaults)
XCTAssertNil(consumed)
}
func testStatusLifecycle() {
let defaults = makeDefaults()
DictationBridge.markRequested(defaults: defaults)
XCTAssertEqual(DictationBridge.currentStatus(defaults: defaults).status, .requested)
DictationBridge.setStatus(.recording, defaults: defaults)
XCTAssertEqual(DictationBridge.currentStatus(defaults: defaults).status, .recording)
DictationBridge.storePendingTranscript("ok", defaults: defaults)
XCTAssertEqual(DictationBridge.currentStatus(defaults: defaults).status, .done)
_ = DictationBridge.consumePendingTranscript(defaults: defaults)
XCTAssertEqual(DictationBridge.currentStatus(defaults: defaults).status, .idle)
}
func testStatusMessageAndTimestamp() {
let defaults = makeDefaults()
DictationBridge.setStatus(.error, message: "fail", defaults: defaults)
let snapshot = DictationBridge.currentStatus(defaults: defaults)
XCTAssertEqual(snapshot.status, .error)
XCTAssertEqual(snapshot.message, "fail")
XCTAssertGreaterThan(snapshot.updatedAt, 0)
}
}
@@ -84,6 +84,27 @@ final class FlowSessionBridgeTests: XCTestCase {
XCTAssertNil(FlowSessionBridge.remainingSessionDuration(defaults: defaults)) XCTAssertNil(FlowSessionBridge.remainingSessionDuration(defaults: defaults))
} }
func testConsumeTranscriptionErrorIncludesKind() {
let defaults = makeDefaults()
FlowSessionBridge.storeTranscriptionError(
"no speech",
kind: .noSpeech,
defaults: defaults
)
let error = FlowSessionBridge.consumeTranscriptionError(defaults: defaults)
XCTAssertEqual(error?.message, "no speech")
XCTAssertEqual(error?.kind, .noSpeech)
XCTAssertNil(FlowSessionBridge.consumeTranscriptionError(defaults: defaults))
}
func testTranscriptionPartialRoundTrip() {
let defaults = makeDefaults()
FlowSessionBridge.storeTranscriptionPartial("你好世界", defaults: defaults)
XCTAssertEqual(FlowSessionBridge.transcriptionPartial(defaults: defaults), "你好世界")
FlowSessionBridge.storeTranscriptionResult("final", defaults: defaults)
XCTAssertNil(FlowSessionBridge.transcriptionPartial(defaults: defaults))
}
func testDarwinNotificationPostsWithoutCrashing() { func testDarwinNotificationPostsWithoutCrashing() {
FlowSessionDarwin.postSessionChanged() FlowSessionDarwin.postSessionChanged()
} }
@@ -122,10 +122,7 @@ final class IntelligentPolishTests: XCTestCase {
func testPolishServiceMissingAPIKeyThrows() async { func testPolishServiceMissingAPIKeyThrows() async {
store.setEngineMode("cloud") store.setEngineMode("cloud")
let service = PolishingService( let service = PolishingService(store: store)
store: store,
client: EchoLLMClient()
)
do { do {
_ = try await service.polish("hello world", context: PolishContext(intensity: .medium)) _ = try await service.polish("hello world", context: PolishContext(intensity: .medium))
XCTFail("Expected missingAPIKey") XCTFail("Expected missingAPIKey")
@@ -218,7 +215,10 @@ final class IntelligentPolishTests: XCTestCase {
store.setEngineMode("local") store.setEngineMode("local")
let captured = CapturingLLMClient() let captured = CapturingLLMClient()
let service = PolishingService(store: store, client: captured) let service = PolishingService(store: store, client: captured)
_ = try await service.polish("hello", context: PolishContext(intensity: .medium)) _ = try await service.polish(
"今天我们部署 k8s 集群",
context: PolishContext(intensity: .medium)
)
XCTAssertTrue( XCTAssertTrue(
captured.lastPrompt.contains("全局输出契约"), captured.lastPrompt.contains("全局输出契约"),
"Local engine should get the Chinese prompt via DeepSeek. Got prefix: \(captured.lastPrompt.prefix(80))" "Local engine should get the Chinese prompt via DeepSeek. Got prefix: \(captured.lastPrompt.prefix(80))"
@@ -39,30 +39,36 @@ final class KeyboardOnboardingOverlayTests: XCTestCase {
func testOnboardingFlagsRoundTrip() { func testOnboardingFlagsRoundTrip() {
store.onboardingPage = 3 store.onboardingPage = 3
store.hasCompletedOnboarding = true
XCTAssertEqual(store.onboardingPage, 3) XCTAssertEqual(store.onboardingPage, 3)
store.hasCompletedOnboarding = true
XCTAssertTrue(store.hasCompletedOnboarding) XCTAssertTrue(store.hasCompletedOnboarding)
// Completing onboarding clears the in-progress page index.
XCTAssertEqual(store.onboardingPage, 0)
} }
func testOnboardingFlagsSurviveReconstruct() { func testOnboardingFlagsSurviveReconstruct() {
store.onboardingPage = 4 store.onboardingPage = 4
store.hasCompletedOnboarding = true
// Simulate the keyboard extension being torn down and rebuilt // Simulate the keyboard extension being torn down and rebuilt
// (which is what happens on every `viewDidLoad` cycle). // (which is what happens on every `viewDidLoad` cycle).
let store2 = AppGroupStore(defaults: defaults) var store2 = AppGroupStore(defaults: defaults)
XCTAssertEqual(store2.onboardingPage, 4) XCTAssertEqual(store2.onboardingPage, 4)
XCTAssertTrue(store2.hasCompletedOnboarding)
store2.hasCompletedOnboarding = true
let store3 = AppGroupStore(defaults: defaults)
XCTAssertTrue(store3.hasCompletedOnboarding)
XCTAssertEqual(store3.onboardingPage, 0)
} }
// MARK: - App context detection round-trip // MARK: - App context detection round-trip
func testDetectedAppContextRoundTrip() { func testDetectedAppContextRoundTrip() throws {
let now = Date() let now = Date()
store.setDetectedAppContext(.code, at: now) store.setDetectedAppContext(.code, at: now)
let result = store.detectedAppContext let result = store.detectedAppContext
XCTAssertEqual(result?.context, .code) XCTAssertEqual(result?.context, .code)
XCTAssertEqual(result?.observedAt.timeIntervalSinceReferenceDate, let observedAt = try XCTUnwrap(result?.observedAt)
XCTAssertEqual(observedAt.timeIntervalSinceReferenceDate,
now.timeIntervalSinceReferenceDate, now.timeIntervalSinceReferenceDate,
accuracy: 0.001) accuracy: 0.001)
} }
+11 -12
View File
@@ -237,13 +237,13 @@ final class LLMClientTests: XCTestCase {
/// Cross-process App Group contract: what `ProviderConfig` writes must /// Cross-process App Group contract: what `ProviderConfig` writes must
/// be readable through `AppGroupStore` on the same suite. /// be readable through `AppGroupStore` on the same suite.
func testAppGroupCrossProcessAndOffModeShortCircuit() async { func testAppGroupCrossProcessLegacyOffModeMigratesToPolish() async {
let suiteName = "group.com.osgkeyboard.shared.tests.\(UUID().uuidString)" let suiteName = "group.com.osgkeyboard.shared.tests.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)! let defaults = UserDefaults(suiteName: suiteName)!
defaults.removePersistentDomain(forName: suiteName) defaults.removePersistentDomain(forName: suiteName)
defer { defaults.removePersistentDomain(forName: suiteName) } defer { defaults.removePersistentDomain(forName: suiteName) }
// Writer side: ProviderConfig (main App) writes API key + mode = off. // Writer side: ProviderConfig (main App) writes API key + legacy off mode.
let config = ProviderConfig(defaults: defaults) let config = ProviderConfig(defaults: defaults)
config.apiKey = "sk-test-1234" config.apiKey = "sk-test-1234"
config.model = "gpt-4o-mini" config.model = "gpt-4o-mini"
@@ -254,7 +254,7 @@ final class LLMClientTests: XCTestCase {
// same suite. // same suite.
let store = AppGroupStore(defaults: defaults) let store = AppGroupStore(defaults: defaults)
XCTAssertEqual(store.apiKey, "sk-test-1234", "API key did not survive the cross-process boundary") XCTAssertEqual(store.apiKey, "sk-test-1234", "API key did not survive the cross-process boundary")
XCTAssertEqual(store.modeId, "off") XCTAssertEqual(store.modeId, "polish", "legacy off mode migrates to polish")
XCTAssertEqual(store.model, "gpt-4o-mini") XCTAssertEqual(store.model, "gpt-4o-mini")
} }
@@ -317,20 +317,19 @@ final class LLMClientTests: XCTestCase {
XCTAssertEqual(calls, 1, "cloud engine must polish even with legacy modeId=off") XCTAssertEqual(calls, 1, "cloud engine must polish even with legacy modeId=off")
} }
/// Local engine is ASR-only and never calls the cloud `LLMClient`. /// Local engine always runs the built-in DeepSeek polish step.
func testPolisherReturnsRawWhenEngineLocal() async throws { func testPolisherInvokesLLMWhenEngineLocal() async throws {
let suiteName = "group.com.osgkeyboard.shared.tests.\(UUID().uuidString)" let suiteName = "group.com.osgkeyboard.shared.tests.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)! let defaults = UserDefaults(suiteName: suiteName)!
defaults.removePersistentDomain(forName: suiteName) defaults.removePersistentDomain(forName: suiteName)
defer { defaults.removePersistentDomain(forName: suiteName) } defer { defaults.removePersistentDomain(forName: suiteName) }
defaults.set("local", forKey: "config.engineMode") defaults.set("local", forKey: "config.engineMode")
defaults.set("off", forKey: "config.modeId") defaults.set("polish", forKey: "config.modeId")
let counter = CallCounter() let counter = CallCounter()
let countingClient = CountingLLMClient(counter: counter) { _, _ in let countingClient = CountingLLMClient(counter: counter) { raw, _ in
XCTFail("cloud LLMClient must not run under local engine") "POLISHED: \(raw)"
return ""
} }
let store = AppGroupStore(defaults: defaults) let store = AppGroupStore(defaults: defaults)
@@ -340,10 +339,10 @@ final class LLMClientTests: XCTestCase {
timeout: 1 timeout: 1
) )
let result = try await polisher.polish(" hello ") let result = try await polisher.polish("hello world")
XCTAssertEqual(result, "hello") XCTAssertEqual(result, "POLISHED: hello world")
let calls = await counter.value() let calls = await counter.value()
XCTAssertEqual(calls, 0) XCTAssertEqual(calls, 1, "local engine must always invoke the polish LLM step")
} }
/// Local engine pins DeepSeek cloud-provider URL/model in App Group /// Local engine pins DeepSeek cloud-provider URL/model in App Group
@@ -24,6 +24,30 @@ final class UtteranceStreamChunkerTests: XCTestCase {
XCTAssertLessThanOrEqual(split, config.maxChunkSamples + config.pauseExtensionSamples) XCTAssertLessThanOrEqual(split, config.maxChunkSamples + config.pauseExtensionSamples)
} }
func testFirstChunkUsesShorterWindow() async {
let config = FlowUtteranceChunkConfig(
firstChunkDurationSeconds: 0.5,
subsequentChunkDurationSeconds: 1.0,
overlapDurationSeconds: 0,
pauseExtensionMaxSeconds: 0,
pauseRMSThreshold: 0.02,
sampleRate: 1_000
)
let firstChunkSamples = config.maxChunkSamples(forChunkIndex: 0) + 50
let samples = [Float](repeating: 0.05, count: firstChunkSamples)
let (stream, continuation) = AsyncStream<AudioBufferSnapshot>.makeStream()
continuation.yield(AudioBufferSnapshot(samples: samples, sampleRate: Double(config.sampleRate)))
continuation.finish()
var received: [UtteranceAudioChunk] = []
for await chunk in UtteranceStreamChunker.chunks(from: stream, config: config) {
received.append(chunk)
}
XCTAssertGreaterThanOrEqual(received.count, 2)
XCTAssertLessThanOrEqual(received[0].samples.count, config.maxChunkSamples(forChunkIndex: 0) + 50)
}
func testChunksEmitMultipleSegmentsForLongStream() async { func testChunksEmitMultipleSegmentsForLongStream() async {
let sampleCount = config.maxChunkSamples * 2 + 100 let sampleCount = config.maxChunkSamples * 2 + 100
let samples = [Float](repeating: 0.05, count: sampleCount) let samples = [Float](repeating: 0.05, count: sampleCount)
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
# check_l10n_keys.sh
# Compares localization key sets across App / Extension / Shared bundles.
# Fails when a key referenced in Shared.strings is missing from any bundle
# that should mirror it, or when Shared keys are absent from Shared.strings.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
extract_keys() {
local file="$1"
grep -E '^"[^"]+"' "$file" 2>/dev/null | sed -E 's/^"([^"]+)".*/\1/' | sort -u
}
SHARED_EN="$ROOT/OSGKeyboardShared/en.lproj/Shared.strings"
SHARED_ZH="$ROOT/OSGKeyboardShared/zh-Hans.lproj/Shared.strings"
APP_EN="$ROOT/OSGKeyboard/en.lproj/Localizable.strings"
APP_ZH="$ROOT/OSGKeyboard/zh-Hans.lproj/Localizable.strings"
EXT_EN="$ROOT/OSGKeyboardExt/en.lproj/Keyboard.strings"
EXT_ZH="$ROOT/OSGKeyboardExt/zh-Hans.lproj/Keyboard.strings"
fail=0
compare_pair() {
local left="$1"
local right="$2"
local label="$3"
local missing
missing="$(comm -23 "$left" "$right" || true)"
if [[ -n "$missing" ]]; then
echo "$label — keys in first file missing from second:"
echo "$missing" | sed 's/^/ /'
fail=1
fi
}
SHARED_EN_KEYS="$(mktemp)"
SHARED_ZH_KEYS="$(mktemp)"
APP_EN_KEYS="$(mktemp)"
APP_ZH_KEYS="$(mktemp)"
EXT_EN_KEYS="$(mktemp)"
EXT_ZH_KEYS="$(mktemp)"
trap 'rm -f "$SHARED_EN_KEYS" "$SHARED_ZH_KEYS" "$APP_EN_KEYS" "$APP_ZH_KEYS" "$EXT_EN_KEYS" "$EXT_ZH_KEYS"' EXIT
extract_keys "$SHARED_EN" > "$SHARED_EN_KEYS"
extract_keys "$SHARED_ZH" > "$SHARED_ZH_KEYS"
extract_keys "$APP_EN" > "$APP_EN_KEYS"
extract_keys "$APP_ZH" > "$APP_ZH_KEYS"
extract_keys "$EXT_EN" > "$EXT_EN_KEYS"
extract_keys "$EXT_ZH" > "$EXT_ZH_KEYS"
compare_pair "$SHARED_EN_KEYS" "$SHARED_ZH_KEYS" "Shared en vs zh-Hans"
compare_pair "$SHARED_ZH_KEYS" "$SHARED_EN_KEYS" "Shared zh-Hans vs en"
compare_pair "$APP_EN_KEYS" "$APP_ZH_KEYS" "App Localizable en vs zh-Hans"
compare_pair "$APP_ZH_KEYS" "$APP_EN_KEYS" "App Localizable zh-Hans vs en"
compare_pair "$EXT_EN_KEYS" "$EXT_ZH_KEYS" "Extension Keyboard en vs zh-Hans"
compare_pair "$EXT_ZH_KEYS" "$EXT_EN_KEYS" "Extension Keyboard zh-Hans vs en"
if [[ "$fail" -ne 0 ]]; then
echo ""
echo "L10n key parity check failed."
exit 1
fi
echo "✅ L10n key parity check passed (Shared / App / Extension en ↔ zh-Hans)."
+37 -91
View File
@@ -1,15 +1,19 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Build OSGKeyboard custom ASR lexicon v1 from Sogou-derived sources. """Build OSGKeyboard domain ASR lexicon v1 from the computer-terms scel source.
Sources (experimentation only Sogou data is non-commercial): Sources:
1. ASC8384/SogouPopularDict accumulated pinyin TSV Local 计算机词汇大全官方推荐.scel (IT / computer vocabulary only)
2. Local 计算机词汇大全官方推荐.scel
3. Local 网络流行新词.scel Casual network slang and Sogou popular-word dumps are intentionally excluded
they dilute custom LM phrase biasing without improving domain ASR accuracy.
Output: Output:
OSGKeyboard/Resources/CustomLanguageModel/v1/phrases.tsv OSGKeyboard/Resources/CustomLanguageModel/v1/phrases.tsv
OSGKeyboard/Resources/CustomLanguageModel/v1/manifest.json OSGKeyboard/Resources/CustomLanguageModel/v1/manifest.json
The compiled .bin asset is exported separately to
OSGKeyboardShared/Resources/CustomLanguageModel/v1/ via export_clm.swift.
""" """
from __future__ import annotations from __future__ import annotations
@@ -17,43 +21,19 @@ from __future__ import annotations
import argparse import argparse
import json import json
import sys import sys
import urllib.request
from collections import Counter
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from scel_parser import get_scel_info, load_pinyin_tsv, parse_scel_file from scel_parser import get_scel_info, parse_scel_file
REPO_ROOT = Path(__file__).resolve().parents[2] REPO_ROOT = Path(__file__).resolve().parents[2]
DEFAULT_OUTPUT_DIR = REPO_ROOT / "OSGKeyboard/Resources/CustomLanguageModel/v1" DEFAULT_OUTPUT_DIR = REPO_ROOT / "OSGKeyboard/Resources/CustomLanguageModel/v1"
SOGOU_ACCUMULATED_URL = (
"https://raw.githubusercontent.com/ASC8384/SogouPopularDict/main/"
"data/sogou_network_words_accumulated_pinyin.tsv"
)
DEFAULT_COMPUTER_SCEL = Path("/Users/rocky/Downloads/计算机词汇大全【官方推荐】.scel") DEFAULT_COMPUTER_SCEL = Path("/Users/rocky/Downloads/计算机词汇大全【官方推荐】.scel")
DEFAULT_NETWORK_SCEL = Path("/Users/rocky/Downloads/网络流行新词.scel")
SOURCE_KEY = "computer_terms"
@dataclass(frozen=True) SOURCE_LABEL = "计算机词汇大全【官方推荐】"
class SourceSpec: SOURCE_WEIGHT = 5
key: str
label: str
weight: int
SOURCES = [
SourceSpec("computer_terms", "计算机词汇大全【官方推荐】", weight=5),
SourceSpec("network_slang_local", "网络流行新词.scel", weight=3),
SourceSpec("sogou_popular_accumulated", "SogouPopularDict accumulated", weight=1),
]
def download_accumulated_tsv(destination: Path) -> None:
destination.parent.mkdir(parents=True, exist_ok=True)
with urllib.request.urlopen(SOGOU_ACCUMULATED_URL, timeout=120) as response:
destination.write_bytes(response.read())
@dataclass @dataclass
@@ -64,24 +44,26 @@ class LexiconEntry:
weight: int weight: int
def merge_entries(sources: list[tuple[SourceSpec, list[tuple[str, str]]]]) -> list[LexiconEntry]: def merge_entries(entries: list[tuple[str, str]]) -> list[LexiconEntry]:
merged: dict[str, LexiconEntry] = {} merged: dict[str, LexiconEntry] = {}
source_counts: Counter[str] = Counter()
for spec, entries in sources:
for word, pinyin in entries: for word, pinyin in entries:
source_counts[spec.key] += 1
current = merged.get(word) current = merged.get(word)
candidate = LexiconEntry(word=word, pinyin=pinyin, source=spec.key, weight=spec.weight) candidate = LexiconEntry(
if current is None or candidate.weight > current.weight: word=word,
pinyin=pinyin,
source=SOURCE_KEY,
weight=SOURCE_WEIGHT,
)
if current is None:
merged[word] = candidate merged[word] = candidate
elif current.weight == candidate.weight and not current.pinyin and pinyin: elif not current.pinyin and pinyin:
merged[word] = candidate merged[word] = candidate
return sorted(merged.values(), key=lambda item: (item.weight * -1, item.word)) return sorted(merged.values(), key=lambda item: item.word)
def write_outputs(entries: list[LexiconEntry], output_dir: Path, source_stats: dict[str, int]) -> None: def write_outputs(entries: list[LexiconEntry], output_dir: Path, raw_count: int) -> None:
output_dir.mkdir(parents=True, exist_ok=True) output_dir.mkdir(parents=True, exist_ok=True)
phrases_path = output_dir / "phrases.tsv" phrases_path = output_dir / "phrases.tsv"
@@ -97,17 +79,16 @@ def write_outputs(entries: list[LexiconEntry], output_dir: Path, source_stats: d
"entry_count": len(entries), "entry_count": len(entries),
"sources": [ "sources": [
{ {
"key": spec.key, "key": SOURCE_KEY,
"label": spec.label, "label": SOURCE_LABEL,
"weight": spec.weight, "weight": SOURCE_WEIGHT,
"raw_count": source_stats.get(spec.key, 0), "raw_count": raw_count,
} }
for spec in SOURCES
], ],
"notes": [ "notes": [
"Sogou-derived data is for internal ASR experimentation only.", "Domain-specific computer/IT vocabulary only; casual network slang removed.",
"PhraseCount weights map to SFCustomLanguageModelData relative frequencies.", "PhraseCount weights map to SFCustomLanguageModelData relative frequencies.",
"Higher source weight wins on duplicate words.", "Merged with ai-tech-brands seed at export time for the final .bin asset.",
], ],
"files": { "files": {
"phrases": phrases_path.name, "phrases": phrases_path.name,
@@ -118,47 +99,19 @@ def write_outputs(entries: list[LexiconEntry], output_dir: Path, source_stats: d
manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
def build( def build(*, computer_scel: Path, output_dir: Path) -> int:
*,
computer_scel: Path,
network_scel: Path,
output_dir: Path,
skip_download: bool,
) -> int:
cache_dir = REPO_ROOT / ".cache/lexicon"
cache_dir.mkdir(parents=True, exist_ok=True)
accumulated_tsv = cache_dir / "sogou_network_words_accumulated_pinyin.tsv"
if not skip_download and not accumulated_tsv.exists():
print(f"Downloading {SOGOU_ACCUMULATED_URL}")
download_accumulated_tsv(accumulated_tsv)
elif not accumulated_tsv.exists():
print(f"Missing accumulated TSV: {accumulated_tsv}", file=sys.stderr)
return 1
if not computer_scel.exists(): if not computer_scel.exists():
print(f"Missing computer scel: {computer_scel}", file=sys.stderr) print(f"Missing computer scel: {computer_scel}", file=sys.stderr)
return 1 return 1
if not network_scel.exists():
print(f"Missing network scel: {network_scel}", file=sys.stderr)
return 1
computer_info = get_scel_info(computer_scel) computer_info = get_scel_info(computer_scel)
network_info = get_scel_info(network_scel)
print(f"Computer dict: {computer_info.name} ({computer_info.word_count} header count)") print(f"Computer dict: {computer_info.name} ({computer_info.word_count} header count)")
print(f"Network dict: {network_info.name} ({network_info.word_count} header count)")
loaded_sources: list[tuple[SourceSpec, list[tuple[str, str]]]] = [ raw_entries = parse_scel_file(computer_scel)
(SOURCES[0], parse_scel_file(computer_scel)), merged = merge_entries(raw_entries)
(SOURCES[1], parse_scel_file(network_scel)), write_outputs(merged, output_dir, raw_count=len(raw_entries))
(SOURCES[2], load_pinyin_tsv(accumulated_tsv)),
]
source_stats = {spec.key: len(entries) for spec, entries in loaded_sources} print(f"Raw count: {len(raw_entries)}")
merged = merge_entries(loaded_sources)
write_outputs(merged, output_dir, source_stats)
print(f"Raw counts: {source_stats}")
print(f"Merged unique entries: {len(merged)}") print(f"Merged unique entries: {len(merged)}")
print(f"Wrote {output_dir / 'phrases.tsv'}") print(f"Wrote {output_dir / 'phrases.tsv'}")
print(f"Wrote {output_dir / 'manifest.json'}") print(f"Wrote {output_dir / 'manifest.json'}")
@@ -166,18 +119,11 @@ def build(
def main() -> int: def main() -> int:
parser = argparse.ArgumentParser(description="Build OSGKeyboard custom ASR lexicon v1") parser = argparse.ArgumentParser(description="Build OSGKeyboard domain ASR lexicon v1")
parser.add_argument("--computer-scel", type=Path, default=DEFAULT_COMPUTER_SCEL) parser.add_argument("--computer-scel", type=Path, default=DEFAULT_COMPUTER_SCEL)
parser.add_argument("--network-scel", type=Path, default=DEFAULT_NETWORK_SCEL)
parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT_DIR) parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT_DIR)
parser.add_argument("--skip-download", action="store_true")
args = parser.parse_args() args = parser.parse_args()
return build( return build(computer_scel=args.computer_scel, output_dir=args.output_dir)
computer_scel=args.computer_scel,
network_scel=args.network_scel,
output_dir=args.output_dir,
skip_download=args.skip_download,
)
if __name__ == "__main__": if __name__ == "__main__":
+14 -14
View File
@@ -15,7 +15,7 @@ import Speech
// MARK: - CLI // MARK: - CLI
struct CLIOptions { struct CLIOptions {
var sogouTSV: URL var domainTSV: URL
var aiTechTSV: URL var aiTechTSV: URL
var outputBin: URL var outputBin: URL
var localeID: String var localeID: String
@@ -29,14 +29,14 @@ struct CLIOptions {
.deletingLastPathComponent() .deletingLastPathComponent()
.deletingLastPathComponent() .deletingLastPathComponent()
var sogou = repoRoot.appendingPathComponent( var domain = repoRoot.appendingPathComponent(
"OSGKeyboard/Resources/CustomLanguageModel/v1/phrases.tsv" "OSGKeyboard/Resources/CustomLanguageModel/v1/phrases.tsv"
) )
var aiTech = repoRoot.appendingPathComponent( var aiTech = repoRoot.appendingPathComponent(
"OSGKeyboard/Resources/CustomLanguageModel/ai-tech-brands/v1/phrases.tsv" "OSGKeyboard/Resources/CustomLanguageModel/ai-tech-brands/v1/phrases.tsv"
) )
var output = repoRoot.appendingPathComponent( var output = repoRoot.appendingPathComponent(
"OSGKeyboard/Resources/CustomLanguageModel/v1/OSGKeyboardCLM.bin" "OSGKeyboardShared/Resources/CustomLanguageModel/v1/OSGKeyboardCLM.bin"
) )
var localeID = "zh_CN" var localeID = "zh_CN"
var modelID = "com.osgkeyboard.custom-lm.v1" var modelID = "com.osgkeyboard.custom-lm.v1"
@@ -46,8 +46,8 @@ struct CLIOptions {
var iterator = CommandLine.arguments.dropFirst().makeIterator() var iterator = CommandLine.arguments.dropFirst().makeIterator()
while let flag = iterator.next() { while let flag = iterator.next() {
switch flag { switch flag {
case "--sogou-tsv": case "--domain-tsv", "--sogou-tsv":
sogou = URL(fileURLWithPath: iterator.next() ?? "") domain = URL(fileURLWithPath: iterator.next() ?? "")
case "--ai-tech-tsv": case "--ai-tech-tsv":
aiTech = URL(fileURLWithPath: iterator.next() ?? "") aiTech = URL(fileURLWithPath: iterator.next() ?? "")
case "--output": case "--output":
@@ -71,7 +71,7 @@ struct CLIOptions {
} }
return CLIOptions( return CLIOptions(
sogouTSV: sogou, domainTSV: domain,
aiTechTSV: aiTech, aiTechTSV: aiTech,
outputBin: output, outputBin: output,
localeID: localeID, localeID: localeID,
@@ -86,7 +86,7 @@ struct CLIOptions {
export_clm.swift build SFCustomLanguageModelData .bin on macOS export_clm.swift build SFCustomLanguageModelData .bin on macOS
Options: Options:
--sogou-tsv <path> Sogou merged phrases TSV --domain-tsv <path> Domain phrases TSV (computer/IT terms)
--ai-tech-tsv <path> AI/tech seed phrases TSV --ai-tech-tsv <path> AI/tech seed phrases TSV
--output <path> Output .bin path --output <path> Output .bin path
--locale <id> Locale identifier (default: zh_CN) --locale <id> Locale identifier (default: zh_CN)
@@ -126,7 +126,7 @@ enum TSVLoader {
} }
// Formats: // Formats:
// sogou: word, pinyin, source, weight // domain: word, pinyin, source, weight
// ai-tech: word, pinyin, source, category, weight, canonical // ai-tech: word, pinyin, source, category, weight, canonical
let weight: Int let weight: Int
if parts.count >= 6, let parsed = Int(parts[4]) { if parts.count >= 6, let parsed = Int(parts[4]) {
@@ -171,17 +171,17 @@ enum ExportCLM {
let options = CLIOptions.parse() let options = CLIOptions.parse()
let fm = FileManager.default let fm = FileManager.default
guard fm.fileExists(atPath: options.sogouTSV.path) else { guard fm.fileExists(atPath: options.domainTSV.path) else {
throw ExportError.missingInput(options.sogouTSV.path) throw ExportError.missingInput(options.domainTSV.path)
} }
guard fm.fileExists(atPath: options.aiTechTSV.path) else { guard fm.fileExists(atPath: options.aiTechTSV.path) else {
throw ExportError.missingInput(options.aiTechTSV.path) throw ExportError.missingInput(options.aiTechTSV.path)
} }
fputs("Loading phrases…\n", stderr) fputs("Loading phrases…\n", stderr)
let sogou = try TSVLoader.load(from: options.sogouTSV, sourceLabel: "sogou_v1") let domain = try TSVLoader.load(from: options.domainTSV, sourceLabel: "computer_terms")
let aiTech = try TSVLoader.load(from: options.aiTechTSV, sourceLabel: "ai_tech_seed") let aiTech = try TSVLoader.load(from: options.aiTechTSV, sourceLabel: "ai_tech_seed")
var merged = TSVLoader.merge([sogou, aiTech]) var merged = TSVLoader.merge([domain, aiTech])
if let cap = options.maxEntries, merged.count > cap { if let cap = options.maxEntries, merged.count > cap {
merged = Array(merged.prefix(cap)) merged = Array(merged.prefix(cap))
@@ -189,7 +189,7 @@ enum ExportCLM {
} }
fputs( fputs(
"Merged \(merged.count) unique phrases (sogou=\(sogou.count), ai-tech=\(aiTech.count))\n", "Merged \(merged.count) unique phrases (domain=\(domain.count), ai-tech=\(aiTech.count))\n",
stderr stderr
) )
fputs("Locale=\(options.localeID) identifier=\(options.modelID) version=\(options.modelVersion)\n", stderr) fputs("Locale=\(options.localeID) identifier=\(options.modelID) version=\(options.modelVersion)\n", stderr)
@@ -236,7 +236,7 @@ enum ExportCLM {
"version": options.modelVersion, "version": options.modelVersion,
"phrase_count": merged.count, "phrase_count": merged.count,
"sources": [ "sources": [
"sogou_v1": sogou.count, "computer_terms": domain.count,
"ai_tech_seed": aiTech.count, "ai_tech_seed": aiTech.count,
], ],
"bin_file": outputURL.lastPathComponent, "bin_file": outputURL.lastPathComponent,
+1 -1
View File
@@ -29,7 +29,7 @@ struct PrepareOptions {
.deletingLastPathComponent() .deletingLastPathComponent()
var input = repoRoot.appendingPathComponent( var input = repoRoot.appendingPathComponent(
"OSGKeyboard/Resources/CustomLanguageModel/v1/OSGKeyboardCLM.bin" "OSGKeyboardShared/Resources/CustomLanguageModel/v1/OSGKeyboardCLM.bin"
) )
var output = repoRoot.appendingPathComponent( var output = repoRoot.appendingPathComponent(
"OSGKeyboard/Resources/CustomLanguageModel/v1/compiled" "OSGKeyboard/Resources/CustomLanguageModel/v1/compiled"
@@ -379,3 +379,242 @@ embodied AI 具身智能 tech_term 80
数字游民 shu zi you min digital nomad tech_term 75 数字游民 shu zi you min digital nomad tech_term 75
远程办公 yuan cheng ban gong remote work tech_term 78 远程办公 yuan cheng ban gong remote work tech_term 78
副业 fu ye side hustle tech_term 72 副业 fu ye side hustle tech_term 72
# --- 2026 AI models, agents & coding tools ---
GPT-5 gpt five|GPT five|ChatGPT 5 ai_model 98
GPT-5.5 gpt five five|GPT five point five|gpt-5.5 ai_model 98
GPT-4o gpt four o|GPT four oh|gpt4o ai_model 92
Claude Code claude code|ClaudeCode|克劳德代码 dev_tool 96
Claude Sonnet claude sonnet|Sonnet|Sonnet 4|Sonnet 4.6 ai_model 94
Claude Opus claude opus|Opus|Opus 4|Opus 4.7 ai_model 94
Composer cursor composer|Composer 2|Composer 2.5 dev_tool 92
Cursor Composer cursor composer|Composer dev_tool 92
OpenAI Codex openai codex|Codex|codex cli dev_tool 92
Codex CLI codex cli|OpenAI Codex dev_tool 90
Gemini CLI gemini cli dev_tool 88
Gemini 2.5 Pro gemini two five pro|Gemini Pro ai_model 90
Gemini 3 Pro gemini three pro|Gemini Pro ai_model 92
Gemini Flash gemini flash|Gemini 3 Flash|Gemini 2.5 Flash ai_model 88
DeepSeek V3 deepseek v three|DeepSeek-V3 ai_model 92
DeepSeek R1 deepseek r one|DeepSeek-R1 ai_model 95
DeepSeek V4 deepseek v four|DeepSeek-V4|DeepSeek V4 Pro ai_model 92
Qwen Coder qwen coder|千问 coder|通义千问 coder ai_model 90
Qwen Max qwen max|Qwen3 Max|通义千问 Max ai_model 88
Qwen Thinking qwen thinking|通义千问 thinking ai_model 86
Kimi K2 kimi k two|Kimi K2.6|Kimi K2.5 ai_model 90
Kimi Code kimi code|Moonshot Kimi Code dev_tool 88
GLM Coding Plan glm coding plan|智谱 coding plan dev_tool 84
GLM-4.6 glm four six|GLM 4.6 ai_model 84
GLM-5 glm five|GLM 5 ai_model 86
Grok 4 grok four|Grok 4.3|Grok Build ai_model 86
Grok Build grok build|Grok Build CLI dev_tool 84
Llama 4 llama four|Llama Maverick|Llama Scout ai_model 86
Llama Maverick llama maverick|Llama 4 Maverick ai_model 84
Mistral Large mistral large|Mistral Large 3 ai_model 84
Devin devin|Cognition Devin dev_tool 88
Cognition cognition ai|Cognition AI ai_brand 82
Kiro kiro|Amazon Kiro dev_tool 84
Amazon Q amazon q|AWS Q|Q Developer dev_tool 84
Q Developer q developer|Amazon Q Developer dev_tool 82
OpenCode open code|opencode|OpenCode AI dev_tool 88
OpenHands open hands|OpenHands AI dev_tool 84
Roo Code roo code|RooCode dev_tool 80
Continue continue dev|Continue.dev dev_tool 78
Zed zed editor|Zed AI dev_tool 80
Trae trae|Trae AI|Trae CN dev_tool 84
TRAE trae|字节 Trae dev_tool 82
Lovable lovable|Lovable AI dev_tool 84
Bolt.new bolt new|Bolt AI|StackBlitz Bolt dev_tool 84
v0 vercel v0|v zero|V0 dev_tool 86
Replit Agent replit agent|Replit AI dev_tool 84
Manus manus|Manus AI ai_brand 86
AutoGen autogen|Microsoft AutoGen ai_platform 82
CrewAI crew ai|Crew AI ai_platform 82
LangGraph lang graph|LangGraph ai_platform 84
DSPy dspy|DSPy AI ai_platform 78
LlamaIndex llama index|LlamaIndex ai_platform 82
Haystack haystack ai|Deepset Haystack ai_platform 76
ComfyUI comfy ui|ComfyUI ai_platform 78
Fooocus fooocus|Focus AI ai_platform 72
LM Studio lm studio|LMStudio ai_platform 82
Open WebUI open web ui|OpenWebUI ai_platform 82
AnythingLLM anything llm|Anything LLM ai_platform 78
# --- Agent, MCP & AI engineering terms ---
Agent Mode agent mode|代理模式|智能体模式 ai_term 92
智能体模式 zhi neng ti mo shi Agent Mode ai_term 90
multi-agent multi agent|多智能体 ai_term 90
多智能体 duo zhi neng ti multi-agent ai_term 88
subagent sub agent|子智能体 ai_term 86
子智能体 zi zhi neng ti subagent ai_term 84
tool use tool-use|工具使用 ai_term 88
tool calling tool-calling|工具调用 ai_term 90
structured output structured outputs|结构化输出 ai_term 86
结构化输出 jie gou hua shu chu structured output ai_term 84
context engineering context engineering|上下文工程 ai_term 92
上下文工程 shang xia wen gong cheng context engineering ai_term 90
prompt caching prompt cache|提示词缓存 ai_term 84
提示词缓存 ti shi ci huan cun prompt caching ai_term 82
semantic search 语义搜索|semantic retrieval ai_term 84
语义搜索 yu yi sou suo semantic search ai_term 82
vector search 向量搜索|vector retrieval ai_term 84
向量搜索 xiang liang sou suo vector search ai_term 82
hybrid search 混合搜索|hybrid retrieval ai_term 82
reranker rerank|重排序模型 ai_term 82
重排序模型 chong pai xu mo xing reranker ai_term 80
evals evaluation|模型评测|评测集 ai_term 86
模型评测 mo xing ping ce evals ai_term 84
SWE-agent swe agent|SWE Agent ai_term 80
WebDev Arena web dev arena|WebDevArena ai_term 76
LLM harness llm harness|agent harness ai_term 80
agent harness agentic harness|智能体框架 ai_term 80
reasoning effort reasoning effort|推理强度 ai_term 78
推理强度 tui li qiang du reasoning effort ai_term 76
test-time compute test time compute|测试时计算 ai_term 78
上下文压缩 shang xia wen ya suo context compression ai_term 82
context compression 上下文压缩 ai_term 80
memory bank memory bank|记忆库 ai_term 78
AI workflow ai workflow|AI 工作流 ai_term 82
工作流编排 gong zuo liu bian pai workflow orchestration ai_term 82
workflow orchestration 工作流编排 ai_term 80
MCP server mcp server|模型上下文协议服务器 ai_term 90
MCP client mcp client|模型上下文协议客户端 ai_term 86
MCP Inspector mcp inspector|MCP 调试器 dev_tool 82
Streamable HTTP streamable http|MCP HTTP ai_term 78
stdio transport stdio transport|标准输入输出传输 ai_term 76
# --- Developer platforms, databases & infra ---
Neon neon database|Neon Postgres tech_company 84
PlanetScale planet scale|PlanetScale MySQL tech_company 82
Turso turso|libSQL tech_company 80
libSQL lib sql|Turso tech_term 78
Convex convex dev|Convex database tech_company 80
Clerk clerk auth|Clerk dev_tool 80
Auth0 auth zero|Auth Zero dev_tool 78
WorkOS work os|Work OS dev_tool 78
Clerk Auth clerk auth|Clerk dev_tool 78
Drizzle drizzle orm|Drizzle ORM dev_tool 82
Prisma prisma orm|Prisma ORM dev_tool 84
Drizzle ORM drizzle orm|Drizzle dev_tool 82
Prisma ORM prisma orm|Prisma dev_tool 82
Postgres postgres|PostgreSQL|postgre sql tech_term 88
PostgreSQL postgresql|Postgres|postgre sql tech_term 88
SQLite sqlite|SQLite tech_term 84
DuckDB duck db|Duck DB tech_term 80
ClickHouse click house|ClickHouse tech_term 80
Kafka kafka|Apache Kafka tech_term 82
Redpanda red panda|Redpanda tech_company 76
OpenTelemetry open telemetry|OTel tech_term 84
OTel otel|OpenTelemetry tech_term 82
Grafana grafana dev_tool 78
Prometheus prometheus dev_tool 78
Sentry sentry dev_tool 82
SST sst|Serverless Stack dev_tool 76
Tailscale tailscale tech_company 78
Fly.io fly io|Fly tech_company 78
Render render.com|Render tech_company 76
Railway railway app|Railway tech_company 78
Modal modal labs|Modal tech_company 78
Modal Labs modal labs|Modal tech_company 76
Hugging Face Spaces hugging face spaces|Spaces ai_platform 78
Nix nix|NixOS tech_term 78
NixOS nixos|Nix OS tech_term 76
Bun bun js|Bun runtime dev_tool 82
Deno deno|Deno Deploy dev_tool 80
Biome biome js|Biome dev_tool 78
Turborepo turbo repo|Turborepo dev_tool 78
Nx nx monorepo|Nx dev_tool 76
pnpm pnpm dev_tool 76
shadcn/ui shadcn ui|shadcn|shad cn dev_tool 84
Tailwind CSS tailwind css|Tailwind dev_tool 84
React Server Components react server components|RSC dev_tool 82
RSC react server components dev_tool 80
Server Actions server actions|React Server Actions dev_tool 78
Astro astro js|Astro dev_tool 80
SvelteKit svelte kit|SvelteKit dev_tool 80
Remix remix run|Remix dev_tool 78
Nuxt nuxt|Nuxt.js dev_tool 78
Hono hono js|Hono dev_tool 78
FastAPI fast api|FastAPI dev_tool 82
Ktor ktor|Ktor server dev_tool 76
# --- Apple, Swift & on-device speech stack ---
SwiftData swift data|SwiftData dev_tool 88
Swift Testing swift testing|Testing framework dev_tool 86
App Intents app intents|AppIntents dev_tool 84
WidgetKit widget kit|WidgetKit dev_tool 82
ActivityKit activity kit|ActivityKit dev_tool 82
Live Activities live activities|灵动岛实时活动 dev_tool 82
App Group app group|App Groups dev_tool 86
Core ML core ml|CoreML dev_tool 88
Create ML create ml|CreateML dev_tool 82
SpeechAnalyzer speech analyzer|Speech Analyzer dev_tool 92
DictationTranscriber dictation transcriber|Dictation Transcriber dev_tool 92
SpeechTranscriber speech transcriber|Speech Transcriber dev_tool 86
SFCustomLanguageModelData custom language model data|SF Custom Language Model Data dev_tool 90
SFSpeechLanguageModel speech language model|SF Speech Language Model dev_tool 88
AssetInventory asset inventory|Speech AssetInventory dev_tool 84
AVAudioEngine av audio engine|AVAudioEngine dev_tool 84
AVAudioSession av audio session|AVAudioSession dev_tool 84
TestFlight test flight|TestFlight dev_tool 84
App Store Connect app store connect|AppStoreConnect dev_tool 84
XcodeGen xcode gen|XcodeGen dev_tool 82
Tuist tuist dev_tool 78
Swift Package Manager swift package manager|SPM dev_tool 82
SPM swift package manager dev_tool 80
SF Symbols sf symbols|SFSymbols dev_tool 82
visionOS vision os|Vision Pro dev_tool 82
RealityKit reality kit|RealityKit dev_tool 80
Metal metal|Metal Performance Shaders dev_tool 80
# --- China AI, creator tools & current tech buzzwords ---
可灵 ke ling Kling|Kling AI ai_brand 86
Kling kling ai|可灵 ai_brand 86
海螺AI hai luo AI Hailuo|MiniMax Video ai_brand 84
Hailuo hailuo ai|海螺AI ai_brand 82
即梦 ji meng Jimeng|即梦AI ai_brand 84
Jimeng jimeng ai|即梦 ai_brand 82
剪映 jian ying CapCut tech_company 84
CapCut cap cut|剪映 tech_company 84
腾讯元宝 teng xun yuan bao Yuanbao ai_brand 84
元宝 yuan bao 腾讯元宝|Yuanbao ai_brand 82
纳米AI na mi AI Nami AI ai_brand 78
秘塔AI mi ta AI Metaso ai_brand 80
Metaso metaso|秘塔AI ai_brand 78
夸克AI kua ke AI Quark AI ai_brand 80
Quark quark ai|夸克 ai_brand 78
天工 tian gong tiangong|昆仑万维天工 ai_brand 78
商量 shang liang SenseChat ai_brand 76
杭州六小龙 hang zhou liu xiao long Hangzhou Six Little Dragons tech_term 84
AI治理 AI zhi li AI governance tech_term 84
AI governance AI治理 tech_term 82
世界模型 shi jie mo xing world model ai_term 84
world model 世界模型 ai_term 82
端到端 duan dao duan end-to-end tech_term 82
end-to-end 端到端 tech_term 80
端侧模型 duan ce mo xing on-device model ai_term 84
on-device model 端侧模型 ai_term 82
本地模型 ben di mo xing local model ai_term 84
local model 本地模型 ai_term 82
具身智能体 ju shen zhi neng ti embodied agent ai_term 82
embodied agent 具身智能体 ai_term 80
人形机器人 ren xing ji qi ren humanoid robot|humanoid tech_term 86
humanoid robot 人形机器人 tech_term 84
新质生产力 xin zhi sheng chan li new quality productive forces tech_term 82
数字分身 shu zi fen shen digital avatar tech_term 78
digital avatar 数字分身 tech_term 76
AI视频 AI shi pin AI video tech_term 82
AI video AI视频 tech_term 80
文生图 wen sheng tu text-to-image tech_term 82
图生视频 tu sheng shi pin image-to-video tech_term 82
文生视频 wen sheng shi pin text-to-video tech_term 82
小红书 xiao hong shu Xiaohongshu|RedNote tech_company 86
Xiaohongshu xiao hong shu|小红书|RedNote tech_company 84
RedNote red note|Xiaohongshu|小红书 tech_company 82
活人感 huo ren gan authentically human|real person vibe tech_term 80
情绪价值 qing xu jia zhi emotional value tech_term 80
赛博对账 sai bo dui zhang cyber reconciliation tech_term 78
赛博 sai bo cyber tech_term 76
村咖 cun ka village coffee tech_term 72
拉布布 la bu bu Labubu tech_term 72
Labubu labubu|拉布布 tech_term 72
1 # OSGKeyboard · AI / Tech / Brand seed lexicon (curated, permissive sources only)
379 数字游民
380 远程办公
381 副业
382 # --- 2026 AI models, agents & coding tools ---
383 GPT-5
384 GPT-5.5
385 GPT-4o
386 Claude Code
387 Claude Sonnet
388 Claude Opus
389 Composer
390 Cursor Composer
391 OpenAI Codex
392 Codex CLI
393 Gemini CLI
394 Gemini 2.5 Pro
395 Gemini 3 Pro
396 Gemini Flash
397 DeepSeek V3
398 DeepSeek R1
399 DeepSeek V4
400 Qwen Coder
401 Qwen Max
402 Qwen Thinking
403 Kimi K2
404 Kimi Code
405 GLM Coding Plan
406 GLM-4.6
407 GLM-5
408 Grok 4
409 Grok Build
410 Llama 4
411 Llama Maverick
412 Mistral Large
413 Devin
414 Cognition
415 Kiro
416 Amazon Q
417 Q Developer
418 OpenCode
419 OpenHands
420 Roo Code
421 Continue
422 Zed
423 Trae
424 TRAE
425 Lovable
426 Bolt.new
427 v0
428 Replit Agent
429 Manus
430 AutoGen
431 CrewAI
432 LangGraph
433 DSPy
434 LlamaIndex
435 Haystack
436 ComfyUI
437 Fooocus
438 LM Studio
439 Open WebUI
440 AnythingLLM
441 # --- Agent, MCP & AI engineering terms ---
442 Agent Mode
443 智能体模式
444 multi-agent
445 多智能体
446 subagent
447 子智能体
448 tool use
449 tool calling
450 structured output
451 结构化输出
452 context engineering
453 上下文工程
454 prompt caching
455 提示词缓存
456 semantic search
457 语义搜索
458 vector search
459 向量搜索
460 hybrid search
461 reranker
462 重排序模型
463 evals
464 模型评测
465 SWE-agent
466 WebDev Arena
467 LLM harness
468 agent harness
469 reasoning effort
470 推理强度
471 test-time compute
472 上下文压缩
473 context compression
474 memory bank
475 AI workflow
476 工作流编排
477 workflow orchestration
478 MCP server
479 MCP client
480 MCP Inspector
481 Streamable HTTP
482 stdio transport
483 # --- Developer platforms, databases & infra ---
484 Neon
485 PlanetScale
486 Turso
487 libSQL
488 Convex
489 Clerk
490 Auth0
491 WorkOS
492 Clerk Auth
493 Drizzle
494 Prisma
495 Drizzle ORM
496 Prisma ORM
497 Postgres
498 PostgreSQL
499 SQLite
500 DuckDB
501 ClickHouse
502 Kafka
503 Redpanda
504 OpenTelemetry
505 OTel
506 Grafana
507 Prometheus
508 Sentry
509 SST
510 Tailscale
511 Fly.io
512 Render
513 Railway
514 Modal
515 Modal Labs
516 Hugging Face Spaces
517 Nix
518 NixOS
519 Bun
520 Deno
521 Biome
522 Turborepo
523 Nx
524 pnpm
525 shadcn/ui
526 Tailwind CSS
527 React Server Components
528 RSC
529 Server Actions
530 Astro
531 SvelteKit
532 Remix
533 Nuxt
534 Hono
535 FastAPI
536 Ktor
537 # --- Apple, Swift & on-device speech stack ---
538 SwiftData
539 Swift Testing
540 App Intents
541 WidgetKit
542 ActivityKit
543 Live Activities
544 App Group
545 Core ML
546 Create ML
547 SpeechAnalyzer
548 DictationTranscriber
549 SpeechTranscriber
550 SFCustomLanguageModelData
551 SFSpeechLanguageModel
552 AssetInventory
553 AVAudioEngine
554 AVAudioSession
555 TestFlight
556 App Store Connect
557 XcodeGen
558 Tuist
559 Swift Package Manager
560 SPM
561 SF Symbols
562 visionOS
563 RealityKit
564 Metal
565 # --- China AI, creator tools & current tech buzzwords ---
566 可灵
567 Kling
568 海螺AI
569 Hailuo
570 即梦
571 Jimeng
572 剪映
573 CapCut
574 腾讯元宝
575 元宝
576 纳米AI
577 秘塔AI
578 Metaso
579 夸克AI
580 Quark
581 天工
582 商量
583 杭州六小龙
584 AI治理
585 AI governance
586 世界模型
587 world model
588 端到端
589 end-to-end
590 端侧模型
591 on-device model
592 本地模型
593 local model
594 具身智能体
595 embodied agent
596 人形机器人
597 humanoid robot
598 新质生产力
599 数字分身
600 digital avatar
601 AI视频
602 AI video
603 文生图
604 图生视频
605 文生视频
606 小红书
607 Xiaohongshu
608 RedNote
609 活人感
610 情绪价值
611 赛博对账
612 赛博
613 村咖
614 拉布布
615 Labubu
616
617
618
619
620
+2 -2
View File
@@ -21,7 +21,7 @@
<h2>What we collect</h2> <h2>What we collect</h2>
<ul> <ul>
<li><strong>Voice audio</strong> — captured only while you actively record. Audio is transcribed on-device with Apple's <code>SpeechAnalyzer</code> + <code>DictationTranscriber</code>; raw audio is not uploaded by OSGKeyboard.</li> <li><strong>Voice audio</strong> — captured only while you actively record. Audio is transcribed on-device with Apple's <code>SpeechAnalyzer</code> + <code>DictationTranscriber</code>; raw audio is not uploaded by OSGKeyboard.</li>
<li><strong>Transcribed text</strong> — after on-device ASR, the transcript (not audio) is sent for polish. On the <strong>local engine</strong>, polish uses a built-in DeepSeek endpoint configured at build time. On the <strong>cloud engine</strong>, polish and optional translation use the OpenAI-compatible API you configure (e.g. OpenAI, Qwen DashScope, Moonshot, Zhipu, or your own server).</li> <li><strong>Transcribed text</strong> — after on-device ASR, the transcript (not audio) is sent for polish. On the <strong>local engine</strong>, polish uses a built-in DeepSeek endpoint configured at build time. On the <strong>cloud engine</strong>, polish and optional translation use the OpenAI-compatible API you configure (e.g. OpenAI, Qwen DashScope, Moonshot, Zhipu, Xiaomi MiMo, or your own server).</li>
<li><strong>API credentials</strong> — your cloud-engine LLM API key is stored in the iOS Keychain on your device and is read only when an LLM request is made. It is shared with the main app through a shared Keychain group, never through UserDefaults.</li> <li><strong>API credentials</strong> — your cloud-engine LLM API key is stored in the iOS Keychain on your device and is read only when an LLM request is made. It is shared with the main app through a shared Keychain group, never through UserDefaults.</li>
<li><strong>App preferences</strong> — engine mode, recognition language, polish intensity, translation target, handedness, cursor-navigation toggle, and keyboard settings are stored in App Group <code>UserDefaults</code> on your device so the main app and keyboard extension stay in sync.</li> <li><strong>App preferences</strong> — engine mode, recognition language, polish intensity, translation target, handedness, cursor-navigation toggle, and keyboard settings are stored in App Group <code>UserDefaults</code> on your device so the main app and keyboard extension stay in sync.</li>
<li><strong>Personal dictionary</strong> — terms and aliases you add in the Dictionary tab are stored locally on your device. They are included in LLM polish prompts so your vocabulary is preserved; dictionary data is not uploaded to a separate server.</li> <li><strong>Personal dictionary</strong> — terms and aliases you add in the Dictionary tab are stored locally on your device. They are included in LLM polish prompts so your vocabulary is preserved; dictionary data is not uploaded to a separate server.</li>
@@ -73,7 +73,7 @@
<h2>我们处理的数据</h2> <h2>我们处理的数据</h2>
<ul> <ul>
<li><strong>语音音频</strong> — 仅在你主动录音时采集。音频在设备端通过 <code>SpeechAnalyzer</code> + <code>DictationTranscriber</code> 转写,OSGKeyboard 不会上传原始录音。</li> <li><strong>语音音频</strong> — 仅在你主动录音时采集。音频在设备端通过 <code>SpeechAnalyzer</code> + <code>DictationTranscriber</code> 转写,OSGKeyboard 不会上传原始录音。</li>
<li><strong>转写文字</strong> — 端侧 ASR 完成后,转写文字(非音频)会发送润色。<strong>本地引擎</strong>使用构建时配置的内置 DeepSeek 端点;<strong>云端引擎</strong>的润色与可选翻译使用你配置的 OpenAI 兼容 APIOpenAI / 通义 DashScope / Moonshot / 智谱 / 自建服务等)。</li> <li><strong>转写文字</strong> — 端侧 ASR 完成后,转写文字(非音频)会发送润色。<strong>本地引擎</strong>使用构建时配置的内置 DeepSeek 端点;<strong>云端引擎</strong>的润色与可选翻译使用你配置的 OpenAI 兼容 APIOpenAI / 通义 DashScope / Moonshot / 智谱 / 小米 MiMo / 自建服务等)。</li>
<li><strong>API 凭证</strong> — 云端引擎的 LLM API Key 保存在设备 Keychain,仅在发起 LLM 请求时读取;通过共享 Keychain 组与主 App 共享,<strong>不会</strong>写入 <code>UserDefaults</code></li> <li><strong>API 凭证</strong> — 云端引擎的 LLM API Key 保存在设备 Keychain,仅在发起 LLM 请求时读取;通过共享 Keychain 组与主 App 共享,<strong>不会</strong>写入 <code>UserDefaults</code></li>
<li><strong>应用偏好</strong> — 引擎模式、识别语言、润色档位、翻译目标、握持偏好、光标导航开关、键盘设置等保存在 App Group <code>UserDefaults</code>,仅用于主 App 与键盘扩展之间的状态同步。</li> <li><strong>应用偏好</strong> — 引擎模式、识别语言、润色档位、翻译目标、握持偏好、光标导航开关、键盘设置等保存在 App Group <code>UserDefaults</code>,仅用于主 App 与键盘扩展之间的状态同步。</li>
<li><strong>个性词库</strong> — 你在「词库」Tab 添加的词条与别名保存在本机。润色时会写入 LLM 提示词以保留你的词汇;词库数据不会上传到独立服务器。</li> <li><strong>个性词库</strong> — 你在「词库」Tab 添加的词条与别名保存在本机。润色时会写入 LLM 提示词以保留你的词汇;词库数据不会上传到独立服务器。</li>
+2 -2
View File
@@ -22,7 +22,7 @@
<h2>What we collect</h2> <h2>What we collect</h2>
<ul> <ul>
<li><strong>Voice audio</strong> — captured only while you actively record. Audio is transcribed on-device with Apple's <code>SpeechAnalyzer</code> + <code>DictationTranscriber</code>; raw audio is not uploaded by OSGKeyboard.</li> <li><strong>Voice audio</strong> — captured only while you actively record. Audio is transcribed on-device with Apple's <code>SpeechAnalyzer</code> + <code>DictationTranscriber</code>; raw audio is not uploaded by OSGKeyboard.</li>
<li><strong>Transcribed text</strong> — after on-device ASR, the transcript (not audio) is sent for polish. On the <strong>local engine</strong>, polish uses a built-in DeepSeek endpoint configured at build time. On the <strong>cloud engine</strong>, polish and optional translation use the OpenAI-compatible API you configure (e.g. OpenAI, Qwen DashScope, Moonshot, Zhipu, or your own server).</li> <li><strong>Transcribed text</strong> — after on-device ASR, the transcript (not audio) is sent for polish. On the <strong>local engine</strong>, polish uses a built-in DeepSeek endpoint configured at build time. On the <strong>cloud engine</strong>, polish and optional translation use the OpenAI-compatible API you configure (e.g. OpenAI, Qwen DashScope, Moonshot, Zhipu, Xiaomi MiMo, or your own server).</li>
<li><strong>API credentials</strong> — your cloud-engine LLM API key is stored in the iOS Keychain on your device and is read only when an LLM request is made. It is shared with the main app through a shared Keychain group, never through UserDefaults.</li> <li><strong>API credentials</strong> — your cloud-engine LLM API key is stored in the iOS Keychain on your device and is read only when an LLM request is made. It is shared with the main app through a shared Keychain group, never through UserDefaults.</li>
<li><strong>App preferences</strong> — engine mode, recognition language, polish intensity, translation target, handedness, cursor-navigation toggle, and keyboard settings are stored in App Group <code>UserDefaults</code> on your device so the main app and keyboard extension stay in sync.</li> <li><strong>App preferences</strong> — engine mode, recognition language, polish intensity, translation target, handedness, cursor-navigation toggle, and keyboard settings are stored in App Group <code>UserDefaults</code> on your device so the main app and keyboard extension stay in sync.</li>
<li><strong>Personal dictionary</strong> — terms and aliases you add in the Dictionary tab are stored locally on your device. They are included in LLM polish prompts so your vocabulary is preserved; dictionary data is not uploaded to a separate server.</li> <li><strong>Personal dictionary</strong> — terms and aliases you add in the Dictionary tab are stored locally on your device. They are included in LLM polish prompts so your vocabulary is preserved; dictionary data is not uploaded to a separate server.</li>
@@ -74,7 +74,7 @@
<h2>我们处理的数据</h2> <h2>我们处理的数据</h2>
<ul> <ul>
<li><strong>语音音频</strong> — 仅在你主动录音时采集。音频在设备端通过 <code>SpeechAnalyzer</code> + <code>DictationTranscriber</code> 转写,OSGKeyboard 不会上传原始录音。</li> <li><strong>语音音频</strong> — 仅在你主动录音时采集。音频在设备端通过 <code>SpeechAnalyzer</code> + <code>DictationTranscriber</code> 转写,OSGKeyboard 不会上传原始录音。</li>
<li><strong>转写文字</strong> — 端侧 ASR 完成后,转写文字(非音频)会发送润色。<strong>本地引擎</strong>使用构建时配置的内置 DeepSeek 端点;<strong>云端引擎</strong>的润色与可选翻译使用你配置的 OpenAI 兼容 APIOpenAI / 通义 DashScope / Moonshot / 智谱 / 自建服务等)。</li> <li><strong>转写文字</strong> — 端侧 ASR 完成后,转写文字(非音频)会发送润色。<strong>本地引擎</strong>使用构建时配置的内置 DeepSeek 端点;<strong>云端引擎</strong>的润色与可选翻译使用你配置的 OpenAI 兼容 APIOpenAI / 通义 DashScope / Moonshot / 智谱 / 小米 MiMo / 自建服务等)。</li>
<li><strong>API 凭证</strong> — 云端引擎的 LLM API Key 保存在设备 Keychain,仅在发起 LLM 请求时读取;通过共享 Keychain 组与主 App 共享,<strong>不会</strong>写入 <code>UserDefaults</code></li> <li><strong>API 凭证</strong> — 云端引擎的 LLM API Key 保存在设备 Keychain,仅在发起 LLM 请求时读取;通过共享 Keychain 组与主 App 共享,<strong>不会</strong>写入 <code>UserDefaults</code></li>
<li><strong>应用偏好</strong> — 引擎模式、识别语言、润色档位、翻译目标、握持偏好、光标导航开关、键盘设置等保存在 App Group <code>UserDefaults</code>,仅用于主 App 与键盘扩展之间的状态同步。</li> <li><strong>应用偏好</strong> — 引擎模式、识别语言、润色档位、翻译目标、握持偏好、光标导航开关、键盘设置等保存在 App Group <code>UserDefaults</code>,仅用于主 App 与键盘扩展之间的状态同步。</li>
<li><strong>个性词库</strong> — 你在「词库」Tab 添加的词条与别名保存在本机。润色时会写入 LLM 提示词以保留你的词汇;词库数据不会上传到独立服务器。</li> <li><strong>个性词库</strong> — 你在「词库」Tab 添加的词条与别名保存在本机。润色时会写入 LLM 提示词以保留你的词汇;词库数据不会上传到独立服务器。</li>
+6 -2
View File
@@ -36,8 +36,8 @@ settings:
GENERATE_INFOPLIST_FILE: NO GENERATE_INFOPLIST_FILE: NO
ENABLE_MODULE_VERIFIER: YES ENABLE_MODULE_VERIFIER: YES
CLANG_CXX_LANGUAGE_STANDARD: c++17 CLANG_CXX_LANGUAGE_STANDARD: c++17
MARKETING_VERSION: "0.3.6" MARKETING_VERSION: "0.4.0"
CURRENT_PROJECT_VERSION: "10" CURRENT_PROJECT_VERSION: "11"
# 签名配置来自 Signing.local.xcconfiggitignored,不会被覆盖) # 签名配置来自 Signing.local.xcconfiggitignored,不会被覆盖)
# 项目级签名 xcconfig,适用于所有 target # 项目级签名 xcconfig,适用于所有 target
@@ -63,6 +63,7 @@ targets:
# Legacy PNG app icons must not coexist with AppIcon.icon — actool # Legacy PNG app icons must not coexist with AppIcon.icon — actool
# crashes when both are passed. iOS 26 uses Icon Composer only. # crashes when both are passed. iOS 26 uses Icon Composer only.
- "Assets.xcassets/AppIcon.appiconset" - "Assets.xcassets/AppIcon.appiconset"
- "Resources/CustomLanguageModel/**"
entitlements: entitlements:
path: OSGKeyboard/OSGKeyboard.entitlements path: OSGKeyboard/OSGKeyboard.entitlements
properties: properties:
@@ -220,6 +221,9 @@ targets:
buildPhase: resources buildPhase: resources
- path: OSGKeyboardShared/zh-Hans.lproj/Shared.strings - path: OSGKeyboardShared/zh-Hans.lproj/Shared.strings
buildPhase: resources buildPhase: resources
resources:
- path: OSGKeyboardShared/Resources/CustomLanguageModel/v1/OSGKeyboardCLM.bin
- path: OSGKeyboardShared/Resources/CustomLanguageModel/v1/compiled-manifest.json
info: info:
path: OSGKeyboardShared/Info.plist path: OSGKeyboardShared/Info.plist
settings: settings: