498f407585
Introduce optional Apple account-backed credits with scoped gateway access while preserving local and BYOK paths. Refresh assistant behavior, tests, privacy disclosures, docs, and the website for the 2.0 experience.
200 lines
7.4 KiB
Swift
200 lines
7.4 KiB
Swift
// CloudASRModels.swift
|
||
// OSGKeyboard · Shared
|
||
//
|
||
// Cloud-engine ASR routing: which provider uses official hotwords /
|
||
// vocabulary APIs vs. a transcription prompt bias.
|
||
|
||
import Foundation
|
||
|
||
/// How a cloud provider applies the user's personal dictionary during ASR.
|
||
public enum CloudASRStrategy: String, Sendable, Equatable {
|
||
/// 智谱 GLM-ASR — `hotwords` + optional `prompt`.
|
||
case zhipuHotwords
|
||
/// 百炼 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
|
||
/// OpenAI Realtime transcription(WebSocket,真流式)。
|
||
case openaiRealtimeStreaming
|
||
/// Moonshot 托管 API 暂无音频转写;云端引擎回退端侧 ASR。
|
||
case localFallback
|
||
}
|
||
|
||
public enum CloudASRError: Error, LocalizedError, Sendable, Equatable {
|
||
case noAPIKey
|
||
case invalidURL
|
||
case http(status: Int, message: String?)
|
||
case decoding(String)
|
||
case transport(String)
|
||
case emptyTranscript
|
||
case audioTooLong
|
||
case providerUnsupported
|
||
|
||
public var errorDescription: String? {
|
||
switch self {
|
||
case .noAPIKey:
|
||
return SharedL10n.string("error.cloudASR.noAPIKey")
|
||
case .invalidURL:
|
||
return SharedL10n.string("error.cloudASR.invalidURL")
|
||
case .http(let status, let message):
|
||
if let message, !message.isEmpty {
|
||
return SharedL10n.format("error.cloudASR.httpWithMessage", status, message)
|
||
}
|
||
return SharedL10n.format("error.cloudASR.http", status)
|
||
case .decoding(let detail):
|
||
return SharedL10n.format("error.cloudASR.decoding", detail)
|
||
case .transport(let detail):
|
||
return SharedL10n.format("error.cloudASR.transport", detail)
|
||
case .emptyTranscript:
|
||
return SharedL10n.string("error.cloudASR.emptyTranscript")
|
||
case .audioTooLong:
|
||
return SharedL10n.string("error.cloudASR.audioTooLong")
|
||
case .providerUnsupported:
|
||
return SharedL10n.string("error.cloudASR.providerUnsupported")
|
||
}
|
||
}
|
||
}
|
||
|
||
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
|
||
|
||
public static let zhipuGLMASR = "glm-asr-2512"
|
||
public static let openAITranscribe = "gpt-4o-mini-transcribe"
|
||
public static let openAIWhisper = "whisper-1"
|
||
/// OpenAI Realtime transcription model (utterance-level streaming).
|
||
public static let openAIRealtimeWhisper = "gpt-realtime-whisper"
|
||
public static let openAIRealtimeEndpoint = "wss://api.openai.com/v1/realtime?intent=transcription"
|
||
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 "bailian":
|
||
return .bailianStreaming
|
||
case "moonshot":
|
||
return .localFallback
|
||
case "volcengine":
|
||
return .volcengineStreaming
|
||
case "openai":
|
||
return .openaiRealtimeStreaming
|
||
case "openrouter":
|
||
return .openRouterJson
|
||
case "whisper", "mimo", "groq", "siliconflow", "custom":
|
||
return .prompt
|
||
default:
|
||
return .localFallback
|
||
}
|
||
}
|
||
|
||
/// Providers whose Flow path uses utterance-level true streaming ASR.
|
||
public static func supportsTrueStreamingASR(for providerId: String) -> Bool {
|
||
switch strategy(for: providerId) {
|
||
case .bailianStreaming, .volcengineStreaming, .openaiRealtimeStreaming:
|
||
return true
|
||
case .zhipuHotwords, .prompt, .openRouterJson, .localFallback:
|
||
return false
|
||
}
|
||
}
|
||
|
||
public static func defaultModel(for providerId: String) -> String {
|
||
switch providerId {
|
||
case "zhipu":
|
||
return zhipuGLMASR
|
||
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":
|
||
return openAIRealtimeWhisper
|
||
case "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, .openaiRealtimeStreaming:
|
||
return true
|
||
case .zhipuHotwords, .volcengineStreaming, .localFallback:
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
|
||
extension LLMProvider {
|
||
public var cloudASRStrategy: CloudASRStrategy {
|
||
CloudASRModelCatalog.strategy(for: id)
|
||
}
|
||
|
||
public var defaultCloudASRModel: String {
|
||
CloudASRModelCatalog.defaultModel(for: id)
|
||
}
|
||
|
||
/// Official hotwords / vocabulary APIs during cloud ASR (not prompt-only bias).
|
||
public var supportsPersonalDictionaryCloudASR: Bool {
|
||
switch cloudASRStrategy {
|
||
case .zhipuHotwords:
|
||
return true
|
||
case .bailianStreaming, .prompt, .openRouterJson, .volcengineStreaming,
|
||
.openaiRealtimeStreaming, .localFallback:
|
||
return false
|
||
}
|
||
}
|
||
|
||
/// Product badge: true streaming ASR path is wired for this provider.
|
||
public var supportsStreamingCloudASR: Bool {
|
||
CloudASRModelCatalog.supportsTrueStreamingASR(for: id)
|
||
}
|
||
}
|