feat: macOS architecture, cloud ASR/LLM providers, and 6-step iOS onboarding
- Add macOS menu-bar dictation app with local ASR models (SenseVoice/Qwen3),
global Option hotkey, and bottom overlay
- Add cloud ASR/LLM providers (Anthropic, Volcengine, Bailian, and more) with
provider logos, model listing, and connection checks
- Add shared 7-day usage stats UI (UsageStatsCluster / SevenDayUsageChart)
- Add iOS onboarding step 6 for polish LLM setup; hide custom-language-model
diagnostic toggle behind DEBUG
- Unify iOS onboarding tagline with the macOS brand line ("开口即文字。")
- Rewrite README (Chinese-first, product-oriented) and refresh GitHub Pages
This commit is contained in:
@@ -10,10 +10,14 @@ import Foundation
|
||||
public enum CloudASRStrategy: String, Sendable, Equatable {
|
||||
/// 智谱 GLM-ASR — `hotwords` + optional `prompt`.
|
||||
case zhipuHotwords
|
||||
/// 阿里百炼 Fun-ASR — managed `vocabulary_id` + context text.
|
||||
case alibabaVocabulary
|
||||
/// OpenAI / 小米 MiMo / 自定义端点 — `prompt` on transcription APIs.
|
||||
/// 百炼 Fun-ASR Realtime — DashScope 经典 inference WebSocket 流式。
|
||||
case bailianStreaming
|
||||
/// OpenAI / Groq / 硅基流动 / Whisper 等 — `prompt` on transcription APIs.
|
||||
case prompt
|
||||
/// OpenRouter `/audio/transcriptions` — JSON body + base64 WAV (not multipart).
|
||||
case openRouterJson
|
||||
/// 火山引擎 SAUC 大模型流式 ASR(WebSocket + binary frame)。
|
||||
case volcengineStreaming
|
||||
/// Moonshot 托管 API 暂无音频转写;云端引擎回退端侧 ASR。
|
||||
case localFallback
|
||||
}
|
||||
@@ -54,8 +58,23 @@ public enum CloudASRError: Error, LocalizedError, Sendable, Equatable {
|
||||
}
|
||||
|
||||
public enum CloudASRModelCatalog {
|
||||
/// Provider ids shown in the cloud ASR picker (explicit allowlist).
|
||||
public static let selectableProviderIds: Set<String> = [
|
||||
"openai",
|
||||
"whisper",
|
||||
"bailian",
|
||||
"zhipu",
|
||||
"groq",
|
||||
"siliconflow",
|
||||
"openrouter",
|
||||
"mimo",
|
||||
"volcengine",
|
||||
"custom",
|
||||
]
|
||||
|
||||
/// Sync Fun-ASR Flash — base64 upload, ≤ 5 min, supports context + vocabulary.
|
||||
public static let alibabaFunASRFlash = "fun-asr-flash-2026-06-15"
|
||||
public static let alibabaFunASRRealtime = "fun-asr-realtime"
|
||||
/// Must match the ASR model used at recognition time.
|
||||
public static let alibabaVocabularyTargetModel = alibabaFunASRFlash
|
||||
|
||||
@@ -63,24 +82,38 @@ public enum CloudASRModelCatalog {
|
||||
public static let openAITranscribe = "gpt-4o-mini-transcribe"
|
||||
public static let openAIWhisper = "whisper-1"
|
||||
public static let mimoASR = "mimo-v2.5-asr"
|
||||
public static let groqWhisper = "whisper-large-v3-turbo"
|
||||
public static let siliconflowASR = "FunAudioLLM/SenseVoiceSmall"
|
||||
public static let openrouterWhisper = "openai/whisper-large-v3-turbo"
|
||||
public static let volcengineDefaultResourceID = "volc.seedasr.sauc.duration"
|
||||
public static let volcengineEndpoint = "wss://openspeech.bytedance.com/api/v3/sauc/bigmodel_async"
|
||||
public static let bailianDefaultEndpoint = "wss://dashscope.aliyuncs.com/api-ws/v1/inference/"
|
||||
|
||||
public static let alibabaAPIBase = "https://dashscope.aliyuncs.com/api/v1"
|
||||
public static let alibabaCustomizationPath = "/services/audio/asr/customization"
|
||||
public static let alibabaMultimodalPath = "/services/aigc/multimodal-generation/generation"
|
||||
public static let zhipuTranscriptionPath = "/audio/transcriptions"
|
||||
|
||||
public static func supportsCloudASRSelection(providerId: String) -> Bool {
|
||||
selectableProviderIds.contains(providerId)
|
||||
}
|
||||
|
||||
public static func strategy(for providerId: String) -> CloudASRStrategy {
|
||||
switch providerId {
|
||||
case "zhipu":
|
||||
return .zhipuHotwords
|
||||
case "qwen":
|
||||
return .alibabaVocabulary
|
||||
case "bailian":
|
||||
return .bailianStreaming
|
||||
case "moonshot":
|
||||
return .localFallback
|
||||
case "openai", "mimo", "custom":
|
||||
case "volcengine":
|
||||
return .volcengineStreaming
|
||||
case "openrouter":
|
||||
return .openRouterJson
|
||||
case "openai", "whisper", "mimo", "groq", "siliconflow", "custom":
|
||||
return .prompt
|
||||
default:
|
||||
return .prompt
|
||||
return .localFallback
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,16 +121,36 @@ public enum CloudASRModelCatalog {
|
||||
switch providerId {
|
||||
case "zhipu":
|
||||
return zhipuGLMASR
|
||||
case "qwen":
|
||||
return alibabaFunASRFlash
|
||||
case "bailian":
|
||||
return alibabaFunASRRealtime
|
||||
case "whisper":
|
||||
return openAIWhisper
|
||||
case "mimo":
|
||||
return mimoASR
|
||||
case "groq":
|
||||
return groqWhisper
|
||||
case "siliconflow":
|
||||
return siliconflowASR
|
||||
case "openrouter":
|
||||
return openrouterWhisper
|
||||
case "volcengine":
|
||||
return volcengineDefaultResourceID
|
||||
case "openai", "custom":
|
||||
return openAITranscribe
|
||||
default:
|
||||
return openAITranscribe
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the ASR settings card should expose a custom endpoint field.
|
||||
public static func showsASREndpointField(for providerId: String) -> Bool {
|
||||
switch strategy(for: providerId) {
|
||||
case .prompt, .openRouterJson, .bailianStreaming:
|
||||
return true
|
||||
case .zhipuHotwords, .volcengineStreaming, .localFallback:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension LLMProvider {
|
||||
@@ -112,9 +165,9 @@ extension LLMProvider {
|
||||
/// Official hotwords / vocabulary APIs during cloud ASR (not prompt-only bias).
|
||||
public var supportsPersonalDictionaryCloudASR: Bool {
|
||||
switch cloudASRStrategy {
|
||||
case .zhipuHotwords, .alibabaVocabulary:
|
||||
case .zhipuHotwords:
|
||||
return true
|
||||
case .prompt, .localFallback:
|
||||
case .bailianStreaming, .prompt, .openRouterJson, .volcengineStreaming, .localFallback:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user