// // SpeechService.swift // Zyquo MLX // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // import Foundation /// Transcription result from a speech model. struct TranscriptionResult: Sendable { var text: String var language: String? var segments: Int var duration: TimeInterval } /// Speech-to-text via the Python bridge (mlx-whisper — Python-only per /// docs/MLX-RESEARCH.md §2). Installs the pinned package on first use. actor SpeechService { static let shared = SpeechService() /// Exact pin per docs/MLX-RESEARCH.md version snapshot. static let whisperPin = "mlx-whisper==0.4.3" private var whisperInstalled = false func transcribe(model: LocalModel, audio: URL) async throws -> TranscriptionResult { try await ensureWhisper() let start = Date() let stream = try await PythonRunner.shared.stream( script: "zyquo_transcribe", arguments: ["--model", model.directory.path, "--audio", audio.path]) var text = "" var language: String? var segments = 0 for try await event in stream { switch event.event { case "done": text = event.string("text") ?? "" language = event.string("language") segments = event.int("segments") ?? 0 case "error": throw PythonRunnerError.processFailed( exitCode: 1, stderr: event.string("message") ?? "transcription failed") default: break } } return TranscriptionResult( text: text, language: language, segments: segments, duration: Date().timeIntervalSince(start)) } private func ensureWhisper() async throws { guard !whisperInstalled else { return } let env = PythonEnvironment.shared if await !env.isProvisioned { try await env.provision() } // Cheap idempotent install (uv resolves instantly when satisfied). try await env.install(pins: [Self.whisperPin]) whisperInstalled = true } }