diff --git a/app/Typoless/Domain/Models/AppConfig.swift b/app/Typoless/Domain/Models/AppConfig.swift index 0edf587..39f185f 100644 --- a/app/Typoless/Domain/Models/AppConfig.swift +++ b/app/Typoless/Domain/Models/AppConfig.swift @@ -390,13 +390,11 @@ struct XunfeiASRConfig: Codable, Equatable, Sendable { /// 小米 MiMo ASR 配置 struct XiaomiMiMoASRConfig: Codable, Equatable, Sendable { var apiKey: String = "" - var language: String = "auto" var validationStatus: CloudASRValidationStatus = .unvalidated var lastValidationError: String? enum CodingKeys: String, CodingKey { case apiKey - case language case validationStatus case lastValidationError } @@ -406,8 +404,6 @@ struct XiaomiMiMoASRConfig: Codable, Equatable, Sendable { init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) apiKey = try container.decodeIfPresent(String.self, forKey: .apiKey) ?? "" - let decodedLanguage = try container.decodeIfPresent(String.self, forKey: .language) ?? Self.defaultLanguage - language = Self.normalizedLanguage(decodedLanguage) validationStatus = try container.decodeIfPresent(CloudASRValidationStatus.self, forKey: .validationStatus) ?? .unvalidated lastValidationError = try container.decodeIfPresent(String.self, forKey: .lastValidationError) } @@ -415,14 +411,6 @@ struct XiaomiMiMoASRConfig: Codable, Equatable, Sendable { var isComplete: Bool { !apiKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } - - static let defaultLanguage = "auto" - static let supportedLanguages = ["auto", "zh", "en"] - - static func normalizedLanguage(_ language: String) -> String { - let trimmed = language.trimmingCharacters(in: .whitespacesAndNewlines) - return supportedLanguages.contains(trimmed) ? trimmed : defaultLanguage - } } extension TencentASRConfig: CloudASRConfigState { diff --git a/app/Typoless/Domain/Models/CloudASRValidation.swift b/app/Typoless/Domain/Models/CloudASRValidation.swift index 9988845..4465cea 100644 --- a/app/Typoless/Domain/Models/CloudASRValidation.swift +++ b/app/Typoless/Domain/Models/CloudASRValidation.swift @@ -47,9 +47,9 @@ struct CloudASRValidationInput: Equatable, Sendable { case .xunfeiSentence: return "\(platform.rawValue)\n\(asrConfig.xunfei.appID)\n\(asrConfig.xunfei.apiKey)\n\(asrConfig.xunfei.apiSecret)" case .xiaomiMiMoASR: - return "\(platform.rawValue)\n\(asrConfig.xiaomiMiMo.apiKey)\n\(asrConfig.xiaomiMiMo.language)" + return "\(platform.rawValue)\n\(asrConfig.xiaomiMiMo.apiKey)" case .xiaomiMiMoTokenPlanASR: - return "\(platform.rawValue)\n\(asrConfig.xiaomiMiMoTokenPlan.apiKey)\n\(asrConfig.xiaomiMiMoTokenPlan.language)" + return "\(platform.rawValue)\n\(asrConfig.xiaomiMiMoTokenPlan.apiKey)" } } } diff --git a/app/Typoless/Domain/Services/CloudASRValidationService.swift b/app/Typoless/Domain/Services/CloudASRValidationService.swift index d702799..8b2ad23 100644 --- a/app/Typoless/Domain/Services/CloudASRValidationService.swift +++ b/app/Typoless/Domain/Services/CloudASRValidationService.swift @@ -189,13 +189,11 @@ final class CloudASRValidationService { case .xiaomiMiMoASR: return XiaomiMiMoASRProvider( apiKey: input.asrConfig.xiaomiMiMo.apiKey, - language: input.asrConfig.xiaomiMiMo.language, baseURL: XiaomiMiMoASRProvider.defaultBaseURL ) case .xiaomiMiMoTokenPlanASR: return XiaomiMiMoASRProvider( apiKey: input.asrConfig.xiaomiMiMoTokenPlan.apiKey, - language: input.asrConfig.xiaomiMiMoTokenPlan.language, baseURL: XiaomiMiMoASRProvider.tokenPlanBaseURL ) } diff --git a/app/Typoless/Persistence/ConfigStore.swift b/app/Typoless/Persistence/ConfigStore.swift index 1704112..c746b7c 100644 --- a/app/Typoless/Persistence/ConfigStore.swift +++ b/app/Typoless/Persistence/ConfigStore.swift @@ -402,14 +402,12 @@ final class ConfigStore { newConfig.xunfei.lastValidationError = nil } - if oldConfig.xiaomiMiMo.apiKey != newConfig.xiaomiMiMo.apiKey - || oldConfig.xiaomiMiMo.language != newConfig.xiaomiMiMo.language { + if oldConfig.xiaomiMiMo.apiKey != newConfig.xiaomiMiMo.apiKey { newConfig.xiaomiMiMo.validationStatus = .unvalidated newConfig.xiaomiMiMo.lastValidationError = nil } - if oldConfig.xiaomiMiMoTokenPlan.apiKey != newConfig.xiaomiMiMoTokenPlan.apiKey - || oldConfig.xiaomiMiMoTokenPlan.language != newConfig.xiaomiMiMoTokenPlan.language { + if oldConfig.xiaomiMiMoTokenPlan.apiKey != newConfig.xiaomiMiMoTokenPlan.apiKey { newConfig.xiaomiMiMoTokenPlan.validationStatus = .unvalidated newConfig.xiaomiMiMoTokenPlan.lastValidationError = nil } diff --git a/app/Typoless/Providers/ASRProviderFactory.swift b/app/Typoless/Providers/ASRProviderFactory.swift index 7dda3ff..2daadf4 100644 --- a/app/Typoless/Providers/ASRProviderFactory.swift +++ b/app/Typoless/Providers/ASRProviderFactory.swift @@ -29,13 +29,11 @@ struct ASRProviderFactory { case .xiaomiMiMoASR: XiaomiMiMoASRProvider( apiKey: config.xiaomiMiMo.apiKey, - language: config.xiaomiMiMo.language, baseURL: XiaomiMiMoASRProvider.defaultBaseURL ) case .xiaomiMiMoTokenPlanASR: XiaomiMiMoASRProvider( apiKey: config.xiaomiMiMoTokenPlan.apiKey, - language: config.xiaomiMiMoTokenPlan.language, baseURL: XiaomiMiMoASRProvider.tokenPlanBaseURL ) } diff --git a/app/Typoless/Providers/XiaomiMiMoASRProvider.swift b/app/Typoless/Providers/XiaomiMiMoASRProvider.swift index 8f5674c..13fe0ea 100644 --- a/app/Typoless/Providers/XiaomiMiMoASRProvider.swift +++ b/app/Typoless/Providers/XiaomiMiMoASRProvider.swift @@ -17,18 +17,15 @@ final class XiaomiMiMoASRProvider: ASRProvider, CloudASRValidating, @unchecked S private static let defaultTimeout: TimeInterval = 15 private let apiKey: String - private let language: String private let recognizeURL: URL private let httpClient: any XiaomiMiMoASRHTTPClient init( apiKey: String, - language: String = XiaomiMiMoASRProvider.defaultLanguage, baseURL: URL = XiaomiMiMoASRProvider.defaultBaseURL, httpClient: any XiaomiMiMoASRHTTPClient = URLSession.shared ) { self.apiKey = apiKey.trimmingCharacters(in: .whitespacesAndNewlines) - self.language = XiaomiMiMoASRConfig.normalizedLanguage(language) self.recognizeURL = Self.chatCompletionsURL(for: baseURL) self.httpClient = httpClient } @@ -60,7 +57,7 @@ final class XiaomiMiMoASRProvider: ASRProvider, CloudASRValidating, @unchecked S ] ), ], - asrOptions: XiaomiMiMoASROptions(language: language), + asrOptions: XiaomiMiMoASROptions(language: Self.defaultLanguage), stream: false ) diff --git a/app/Typoless/UI/Settings/ASRSettingsView.swift b/app/Typoless/UI/Settings/ASRSettingsView.swift index 3272384..862a1db 100644 --- a/app/Typoless/UI/Settings/ASRSettingsView.swift +++ b/app/Typoless/UI/Settings/ASRSettingsView.swift @@ -104,9 +104,7 @@ struct ASRSettingsView: View { @State private var xunfeiAPISecret: String = "" @State private var xiaomiMiMoAPIKey: String = "" - @State private var xiaomiMiMoLanguage: String = XiaomiMiMoASRConfig.defaultLanguage @State private var xiaomiMiMoTokenPlanAPIKey: String = "" - @State private var xiaomiMiMoTokenPlanLanguage: String = XiaomiMiMoASRConfig.defaultLanguage @State private var isLoaded = false @State private var hasTriggeredValidation = false @@ -151,17 +149,9 @@ struct ASRSettingsView: View { case .xunfeiSentence: xunfeiPanel case .xiaomiMiMoASR: - xiaomiMiMoPanel( - apiKey: $xiaomiMiMoAPIKey, - language: $xiaomiMiMoLanguage, - platform: .xiaomiMiMoASR - ) + xiaomiMiMoPanel(apiKey: $xiaomiMiMoAPIKey, platform: .xiaomiMiMoASR) case .xiaomiMiMoTokenPlanASR: - xiaomiMiMoPanel( - apiKey: $xiaomiMiMoTokenPlanAPIKey, - language: $xiaomiMiMoTokenPlanLanguage, - platform: .xiaomiMiMoTokenPlanASR - ) + xiaomiMiMoPanel(apiKey: $xiaomiMiMoTokenPlanAPIKey, platform: .xiaomiMiMoTokenPlanASR) } } footer: { Text(selectedPlatform.cloudConfigSummary) @@ -185,9 +175,7 @@ struct ASRSettingsView: View { .onChange(of: xunfeiAPIKey) { debouncedSaveCloudConfig() } .onChange(of: xunfeiAPISecret) { debouncedSaveCloudConfig() } .onChange(of: xiaomiMiMoAPIKey) { debouncedSaveCloudConfig() } - .onChange(of: xiaomiMiMoLanguage) { debouncedSaveCloudConfig() } .onChange(of: xiaomiMiMoTokenPlanAPIKey) { debouncedSaveCloudConfig() } - .onChange(of: xiaomiMiMoTokenPlanLanguage) { debouncedSaveCloudConfig() } } // MARK: - Panels @@ -244,11 +232,9 @@ struct ASRSettingsView: View { @ViewBuilder private func xiaomiMiMoPanel( apiKey: Binding, - language: Binding, platform: ASRPlatform ) -> some View { cloudSecureField(title: "API Key", text: apiKey) - xiaomiMiMoLanguageRow(language: language) cloudStatusRow(for: platform) } @@ -266,18 +252,6 @@ struct ASRSettingsView: View { } } - private func xiaomiMiMoLanguageRow(language: Binding) -> some View { - SettingsFormRow(title: "识别语言") { - Picker("识别语言", selection: language) { - Text("自动").tag("auto") - Text("中文").tag("zh") - Text("英文").tag("en") - } - .labelsHidden() - .fixedSize() - } - } - private func cloudStatusRow(for platform: ASRPlatform) -> some View { SettingsFormRow(title: "引擎状态") { VStack(alignment: .leading, spacing: 4) { @@ -385,9 +359,7 @@ struct ASRSettingsView: View { xunfeiAPISecret = configStore.asrConfig.xunfei.apiSecret xiaomiMiMoAPIKey = configStore.asrConfig.xiaomiMiMo.apiKey - xiaomiMiMoLanguage = configStore.asrConfig.xiaomiMiMo.language xiaomiMiMoTokenPlanAPIKey = configStore.asrConfig.xiaomiMiMoTokenPlan.apiKey - xiaomiMiMoTokenPlanLanguage = configStore.asrConfig.xiaomiMiMoTokenPlan.language hasTriggeredValidation = false downloadManager = ModelDownloadManager(configStore: configStore) @@ -412,9 +384,7 @@ struct ASRSettingsView: View { config.xunfei.apiKey = xunfeiAPIKey.trimmingCharacters(in: .whitespacesAndNewlines) config.xunfei.apiSecret = xunfeiAPISecret.trimmingCharacters(in: .whitespacesAndNewlines) config.xiaomiMiMo.apiKey = xiaomiMiMoAPIKey.trimmingCharacters(in: .whitespacesAndNewlines) - config.xiaomiMiMo.language = XiaomiMiMoASRConfig.normalizedLanguage(xiaomiMiMoLanguage) config.xiaomiMiMoTokenPlan.apiKey = xiaomiMiMoTokenPlanAPIKey.trimmingCharacters(in: .whitespacesAndNewlines) - config.xiaomiMiMoTokenPlan.language = XiaomiMiMoASRConfig.normalizedLanguage(xiaomiMiMoTokenPlanLanguage) return config } diff --git a/app/TypolessTests/ASRConfigTests.swift b/app/TypolessTests/ASRConfigTests.swift index 44a10bc..a971136 100644 --- a/app/TypolessTests/ASRConfigTests.swift +++ b/app/TypolessTests/ASRConfigTests.swift @@ -133,10 +133,9 @@ final class ASRConfigTests: XCTestCase { XCTAssertEqual(decoded.selectedPlatform, .localSenseVoice) } - func testXiaomiMiMoConfigNormalizesUnsupportedLanguageField() throws { + func testXiaomiMiMoConfigIgnoresLegacyLanguageField() throws { let data = Data(#"{"apiKey":"key","language":"ja"}"#.utf8) let decoded = try JSONDecoder().decode(XiaomiMiMoASRConfig.self, from: data) XCTAssertEqual(decoded.apiKey, "key") - XCTAssertEqual(decoded.language, "auto") } } diff --git a/app/TypolessTests/CloudASRValidationServiceTests.swift b/app/TypolessTests/CloudASRValidationServiceTests.swift index a5bf928..529b5d0 100644 --- a/app/TypolessTests/CloudASRValidationServiceTests.swift +++ b/app/TypolessTests/CloudASRValidationServiceTests.swift @@ -187,30 +187,27 @@ final class CloudASRValidationServiceTests: XCTestCase { XCTAssertNil(service.lastErrorMessage) } - func testXiaomiMiMoValidationInputFingerprintIncludesLanguage() { + func testXiaomiMiMoValidationInputFingerprintUsesAPIKey() { var config = ASRConfig() config.xiaomiMiMo.apiKey = "mimo-key" - config.xiaomiMiMo.language = "zh" let input = CloudASRValidationInput(platform: .xiaomiMiMoASR, asrConfig: config) XCTAssertTrue(input.isCloudPlatform) XCTAssertTrue(input.isComplete) - XCTAssertEqual(input.fingerprint, "xiaomiMiMoASR\nmimo-key\nzh") + XCTAssertEqual(input.fingerprint, "xiaomiMiMoASR\nmimo-key") } func testXiaomiMiMoTokenPlanValidationInputFingerprintUsesIndependentPlatform() { var config = ASRConfig() config.xiaomiMiMo.apiKey = "mimo-key" - config.xiaomiMiMo.language = "zh" config.xiaomiMiMoTokenPlan.apiKey = "token-plan-key" - config.xiaomiMiMoTokenPlan.language = "en" let input = CloudASRValidationInput(platform: .xiaomiMiMoTokenPlanASR, asrConfig: config) XCTAssertTrue(input.isCloudPlatform) XCTAssertTrue(input.isComplete) - XCTAssertEqual(input.fingerprint, "xiaomiMiMoTokenPlanASR\ntoken-plan-key\nen") + XCTAssertEqual(input.fingerprint, "xiaomiMiMoTokenPlanASR\ntoken-plan-key") } @MainActor diff --git a/app/TypolessTests/ConfigStoreTests.swift b/app/TypolessTests/ConfigStoreTests.swift index 13f7052..22afe98 100644 --- a/app/TypolessTests/ConfigStoreTests.swift +++ b/app/TypolessTests/ConfigStoreTests.swift @@ -69,9 +69,7 @@ final class ConfigStoreTests: XCTestCase { asrConfig.aliyun.accessKeySecret = "secret" asrConfig.aliyun.appKey = "app" asrConfig.xiaomiMiMo.apiKey = "mimo-key" - asrConfig.xiaomiMiMo.language = "zh" asrConfig.xiaomiMiMoTokenPlan.apiKey = "token-plan-key" - asrConfig.xiaomiMiMoTokenPlan.language = "en" try firstStore.saveASRConfig(asrConfig) try firstStore.updateCloudValidationState(for: .volcengineSentence, status: .verified) try firstStore.updateCloudValidationState(for: .xiaomiMiMoASR, status: .verified) @@ -85,10 +83,8 @@ final class ConfigStoreTests: XCTestCase { XCTAssertEqual(secondStore.asrConfig.aliyun.accessKeySecret, "secret") XCTAssertEqual(secondStore.asrConfig.aliyun.appKey, "app") XCTAssertEqual(secondStore.asrConfig.xiaomiMiMo.apiKey, "mimo-key") - XCTAssertEqual(secondStore.asrConfig.xiaomiMiMo.language, "zh") XCTAssertEqual(secondStore.asrConfig.xiaomiMiMo.validationStatus, .verified) XCTAssertEqual(secondStore.asrConfig.xiaomiMiMoTokenPlan.apiKey, "token-plan-key") - XCTAssertEqual(secondStore.asrConfig.xiaomiMiMoTokenPlan.language, "en") XCTAssertEqual(secondStore.asrConfig.xiaomiMiMoTokenPlan.validationStatus, .verified) } @@ -110,24 +106,6 @@ final class ConfigStoreTests: XCTestCase { XCTAssertNil(store.asrConfig.tencentCloud.lastValidationError) } - @MainActor - func testChangingXiaomiMiMoTokenPlanLanguageInvalidatesValidationState() throws { - let store = ConfigStore(configDirectory: tempDirectory) - var asrConfig = store.asrConfig - asrConfig.selectedPlatform = .xiaomiMiMoTokenPlanASR - asrConfig.xiaomiMiMoTokenPlan.apiKey = "token-plan-key" - asrConfig.xiaomiMiMoTokenPlan.language = "auto" - try store.saveASRConfig(asrConfig) - try store.updateCloudValidationState(for: .xiaomiMiMoTokenPlanASR, status: .verified) - - var changedConfig = store.asrConfig - changedConfig.xiaomiMiMoTokenPlan.language = "zh" - try store.saveASRConfig(changedConfig) - - XCTAssertEqual(store.asrConfig.xiaomiMiMoTokenPlan.validationStatus, .unvalidated) - XCTAssertNil(store.asrConfig.xiaomiMiMoTokenPlan.lastValidationError) - } - @MainActor func testSaveAndReloadGeneralConfigPersistsInteractionSoundEnabled() throws { let firstStore = ConfigStore(configDirectory: tempDirectory) diff --git a/app/TypolessTests/XiaomiMiMoASRProviderTests.swift b/app/TypolessTests/XiaomiMiMoASRProviderTests.swift index e773e10..5353c8f 100644 --- a/app/TypolessTests/XiaomiMiMoASRProviderTests.swift +++ b/app/TypolessTests/XiaomiMiMoASRProviderTests.swift @@ -83,23 +83,6 @@ final class XiaomiMiMoASRProviderTests: XCTestCase { XCTAssertEqual(asrOptions?["language"] as? String, "auto") } - func testRecognizeUsesConfiguredLanguage() async throws { - let client = StubXiaomiMiMoHTTPClient( - responseData: Data(#"{"choices":[{"message":{"content":"text"}}]}"#.utf8), - statusCode: 200 - ) - let provider = XiaomiMiMoASRProvider(apiKey: "mimo-key", language: "zh", httpClient: client) - - _ = try await provider.recognize(audioData: Data([1]), timeout: nil) - - let lastRequest = await client.lastRequest - let request = try XCTUnwrap(lastRequest) - let body = try XCTUnwrap(request.httpBody) - let json = try JSONSerialization.jsonObject(with: body) as? [String: Any] - let asrOptions = json?["asr_options"] as? [String: Any] - XCTAssertEqual(asrOptions?["language"] as? String, "zh") - } - func testRecognizeThrowsConfigurationIncompleteWhenAPIKeyMissing() async { let client = StubXiaomiMiMoHTTPClient( responseData: Data(#"{"choices":[{"message":{"content":"text"}}]}"#.utf8), diff --git a/docs/TDD.md b/docs/TDD.md index a2faa91..9c45deb 100644 --- a/docs/TDD.md +++ b/docs/TDD.md @@ -486,11 +486,9 @@ Segment 级诊断字段(每段独立记录): - `asr.xunfei.validationStatus`:科大讯飞配置验证状态(unvalidated / validating / verified / failed) - `asr.xunfei.lastValidationError`:科大讯飞最近一次验证失败摘要 - `asr.xiaomiMiMo.apiKey`:小米 MiMo API Key -- `asr.xiaomiMiMo.language`:小米 MiMo 识别语言(auto / zh / en) - `asr.xiaomiMiMo.validationStatus`:小米 MiMo 配置验证状态(unvalidated / validating / verified / failed) - `asr.xiaomiMiMo.lastValidationError`:小米 MiMo 最近一次验证失败摘要 - `asr.xiaomiMiMoTokenPlan.apiKey`:小米 MiMo Token Plan API Key -- `asr.xiaomiMiMoTokenPlan.language`:小米 MiMo Token Plan 识别语言(auto / zh / en) - `asr.xiaomiMiMoTokenPlan.validationStatus`:小米 MiMo Token Plan 配置验证状态(unvalidated / validating / verified / failed) - `asr.xiaomiMiMoTokenPlan.lastValidationError`:小米 MiMo Token Plan 最近一次验证失败摘要 @@ -563,8 +561,8 @@ Segment 级诊断字段(每段独立记录): - `XunfeiSentenceASRProvider` 使用语音听写 WebSocket 接口,并从 `wav` 中提取 PCM 数据按帧发送。 - 配置:AppID、API Key、API Secret,存于 `asr.xunfei`。 - `XiaomiMiMoASRProvider` 调用 OpenAI Chat Completions 兼容接口;普通 MiMo Base URL 为 `https://api.xiaomimimo.com/v1`,Token Plan Base URL 为 `https://token-plan-cn.xiaomimimo.com/v1`,最终请求路径均为 `/chat/completions`。 -- 模型固定为 `mimo-v2.5-asr`,请求体通过 `messages[].content[].input_audio.data` 传入 `data:audio/wav;base64,