diff --git a/Sources/Fluid/Services/ExternalCoreMLModelRegistry.swift b/Sources/Fluid/Services/ExternalCoreMLModelRegistry.swift index 0e99c0a7..bacd5819 100644 --- a/Sources/Fluid/Services/ExternalCoreMLModelRegistry.swift +++ b/Sources/Fluid/Services/ExternalCoreMLModelRegistry.swift @@ -28,8 +28,6 @@ enum ExternalCoreMLArtifactsValidationError: LocalizedError { case manifestUnreadable(URL, Error) case unexpectedModelID(expected: String, actual: String) case unexpectedSampleRate(expected: Int, actual: Int) - case unexpectedMaxAudioSamples(expected: Int, actual: Int) - case unexpectedMaxAudioSeconds(expected: Double, actual: Double) var errorDescription: String? { switch self { @@ -43,10 +41,6 @@ enum ExternalCoreMLArtifactsValidationError: LocalizedError { return "Unexpected model_id '\(actual)'. Expected '\(expected)'." case let .unexpectedSampleRate(expected, actual): return "Unexpected sample rate \(actual). Expected \(expected)." - case let .unexpectedMaxAudioSamples(expected, actual): - return "Unexpected max audio samples \(actual). Expected \(expected)." - case let .unexpectedMaxAudioSeconds(expected, actual): - return "Unexpected max audio seconds \(actual). Expected \(expected)." } } } @@ -62,8 +56,6 @@ struct ExternalCoreMLASRModelSpec { let cachedDecoderFileName: String let expectedModelID: String let expectedSampleRate: Int - let expectedMaxAudioSamples: Int - let expectedMaxAudioSeconds: Double let computeConfiguration: CohereTranscribeComputeConfiguration let sourceURL: URL? let repositoryOwner: String? @@ -137,20 +129,6 @@ struct ExternalCoreMLASRModelSpec { actual: manifest.sampleRate ) } - - guard manifest.maxAudioSamples == self.expectedMaxAudioSamples else { - throw ExternalCoreMLArtifactsValidationError.unexpectedMaxAudioSamples( - expected: self.expectedMaxAudioSamples, - actual: manifest.maxAudioSamples - ) - } - - guard manifest.maxAudioSeconds == self.expectedMaxAudioSeconds else { - throw ExternalCoreMLArtifactsValidationError.unexpectedMaxAudioSeconds( - expected: self.expectedMaxAudioSeconds, - actual: manifest.maxAudioSeconds - ) - } } } @@ -169,8 +147,6 @@ enum ExternalCoreMLModelRegistry { cachedDecoderFileName: "cohere_decoder_cached.mlpackage", expectedModelID: "CohereLabs/cohere-transcribe-03-2026", expectedSampleRate: 16_000, - expectedMaxAudioSamples: 560_000, - expectedMaxAudioSeconds: 35.0, computeConfiguration: .aneSmall, sourceURL: URL(string: "https://huggingface.co/BarathwajAnandan/cohere-transcribe-03-2026-CoreML-6bit"), repositoryOwner: "BarathwajAnandan", diff --git a/Sources/Fluid/Services/ExternalCoreMLTranscriptionProvider.swift b/Sources/Fluid/Services/ExternalCoreMLTranscriptionProvider.swift index 115bda27..34a12df0 100644 --- a/Sources/Fluid/Services/ExternalCoreMLTranscriptionProvider.swift +++ b/Sources/Fluid/Services/ExternalCoreMLTranscriptionProvider.swift @@ -58,6 +58,7 @@ final class ExternalCoreMLTranscriptionProvider: TranscriptionProvider { case .cohereTranscribe: let manager = CohereTranscribeAsrManager() progressHandler?(0.9) + try self.invalidateCompiledCohereCacheIfNeeded(at: directory) let computeSummary = [ String(describing: spec.computeConfiguration.frontend), String(describing: spec.computeConfiguration.encoder), @@ -65,11 +66,12 @@ final class ExternalCoreMLTranscriptionProvider: TranscriptionProvider { String(describing: spec.computeConfiguration.decoder), ].joined(separator: "/") DebugLogger.shared.info( - "ExternalCoreML: loading Cohere models [splitCompute=\(computeSummary), maxAudioSamples=\(self.loadedManifest?.maxAudioSamples ?? spec.expectedMaxAudioSamples)]", + "ExternalCoreML: loading Cohere models [splitCompute=\(computeSummary), maxAudioSamples=\(self.loadedManifest?.maxAudioSamples ?? 0)]", source: "ExternalCoreML" ) try await manager.loadModels(from: directory, computeConfiguration: spec.computeConfiguration) self.cohereManager = manager + self.persistCompiledCohereCacheStamp(at: directory) } self.isReady = true @@ -274,6 +276,48 @@ final class ExternalCoreMLTranscriptionProvider: TranscriptionProvider { ) } + private func invalidateCompiledCohereCacheIfNeeded(at directory: URL) throws { + guard let manifest = self.loadedManifest else { return } + + let compiledDirectory = CohereTranscribeAsrModels.compiledArtifactsDirectory(for: directory) + guard FileManager.default.fileExists(atPath: compiledDirectory.path) else { return } + + let currentStamp = Self.compiledCohereCacheStamp(for: manifest) + let stampURL = Self.compiledCohereCacheStampURL(for: directory) + let previousStamp = try? String(contentsOf: stampURL, encoding: .utf8) + .trimmingCharacters(in: .whitespacesAndNewlines) + + guard previousStamp != currentStamp else { return } + + let reason = previousStamp == nil ? "missing cache stamp" : "manifest changed" + DebugLogger.shared.warning( + "ExternalCoreML: clearing stale compiled Cohere cache [reason=\(reason)]", + source: "ExternalCoreML" + ) + try FileManager.default.removeItem(at: compiledDirectory) + } + + private func persistCompiledCohereCacheStamp(at directory: URL) { + guard let manifest = self.loadedManifest else { return } + let stampURL = Self.compiledCohereCacheStampURL(for: directory) + let stamp = Self.compiledCohereCacheStamp(for: manifest) + try? stamp.write(to: stampURL, atomically: true, encoding: .utf8) + } + + private static func compiledCohereCacheStampURL(for directory: URL) -> URL { + directory.appendingPathComponent(".cohere_compiled_cache_stamp", isDirectory: false) + } + + private static func compiledCohereCacheStamp(for manifest: ExternalCoreMLManifestIdentity) -> String { + [ + manifest.modelID, + String(manifest.sampleRate), + String(manifest.maxAudioSamples), + String(manifest.maxAudioSeconds), + String(manifest.overlapSamples ?? 0), + ].joined(separator: "|") + } + private func previewSamples(for samples: [Float]) -> [Float] { let sampleRate = self.loadedManifest?.sampleRate ?? (self.modelOverride ?? SettingsStore.shared.selectedSpeechModel).externalCoreMLSpec?.expectedSampleRate