d656bac8c3
Unify Bailian/Volcengine/OpenAI realtime streaming, ABE polish routing with fun styles, and a shared card-page Settings hierarchy; bump to 1.1 (build 32).
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)
|
||
}
|
||
}
|