import Foundation import AVFoundation // MARK: - Errors enum TtsServiceError: LocalizedError { case notAvailable case synthesisFailed(String) var errorDescription: String? { switch self { case .notAvailable: return "Text-to-speech is not available on this device" case .synthesisFailed(let reason): return "Speech synthesis failed: \(reason)" } } } // MARK: - State enum TtsState: Equatable { case initializing case ready case speaking case error(String) } // MARK: - TTS Service /// Text-to-Speech service using AVSpeechSynthesizer. actor TtsService: NSObject { // MARK: - Properties private var synthesizer: AVSpeechSynthesizer? private var currentState: TtsState = .initializing private var completionContinuation: CheckedContinuation? /// Current TTS state var state: TtsState { currentState } /// Whether the synthesizer is currently speaking var isSpeaking: Bool { synthesizer?.isSpeaking ?? false } /// Whether TTS is available on this device var isAvailable: Bool { AVSpeechSynthesisVoice.speechVoices().count > 0 } // MARK: - Initialization override init() { super.init() } deinit { synthesizer?.stopSpeaking(at: .immediate) } // MARK: - Setup /// Initializes the TTS engine. /// - Returns: Async stream of state changes func initialize() async -> AsyncStream { AsyncStream { continuation in Task { await setupSynthesizer() continuation.yield(currentState) continuation.finish() } } } private func setupSynthesizer() { guard isAvailable else { currentState = .error("No voices available") return } synthesizer = AVSpeechSynthesizer() synthesizer?.delegate = self currentState = .ready } // MARK: - Speech Methods /// Speaks the given text. /// - Parameters: /// - text: The text to speak /// - language: Optional language code (e.g., "en-US"). Defaults to system language. /// - Throws: TtsServiceError if synthesis fails func speak(text: String, language: String? = nil) async throws { guard let synthesizer = synthesizer else { throw TtsServiceError.notAvailable } // Stop any current speech synthesizer.stopSpeaking(at: .immediate) let utterance = AVSpeechUtterance(string: text) // Configure voice if let language = language { utterance.voice = AVSpeechSynthesisVoice(language: language) } else { // Try system language, fallback to US English let systemLanguage = Locale.current.language.languageCode?.identifier ?? "en" let region = Locale.current.region?.identifier ?? "US" let locale = "\(systemLanguage)-\(region)" if let voice = AVSpeechSynthesisVoice(language: locale) { utterance.voice = voice } else if let voice = AVSpeechSynthesisVoice(language: "en-US") { utterance.voice = voice } } // Configure speech parameters utterance.rate = AVSpeechUtteranceDefaultSpeechRate utterance.pitchMultiplier = 1.0 utterance.volume = 1.0 // Speak and wait for completion await withCheckedContinuation { continuation in self.completionContinuation = continuation synthesizer.speak(utterance) } } /// Speaks the given text with completion callback. /// - Parameters: /// - text: The text to speak /// - language: Optional language code /// - onComplete: Called when speech completes or fails func speak(text: String, language: String? = nil, onComplete: (() -> Void)? = nil) { Task { do { try await speak(text: text, language: language) } catch { print("TTS error: \(error)") } onComplete?() } } /// Stops the current speech immediately. func stop() { synthesizer?.stopSpeaking(at: .immediate) completionContinuation?.resume() completionContinuation = nil currentState = .ready } /// Stops the current speech at the end of the word. func stopAtEndOfWord() { synthesizer?.stopSpeaking(at: .word) } /// Shuts down the TTS engine and releases resources. func shutdown() { synthesizer?.stopSpeaking(at: .immediate) synthesizer?.delegate = nil synthesizer = nil completionContinuation = nil currentState = .initializing } /// Sets the speech rate (0.0 to 1.0, default is 0.5). func setRate(_ rate: Float) { // Applied per utterance } /// Sets the speech volume (0.0 to 1.0). func setVolume(_ volume: Float) { // Applied per utterance } } // MARK: - AVSpeechSynthesizerDelegate extension TtsService: AVSpeechSynthesizerDelegate { nonisolated func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didStart utterance: AVSpeechUtterance) { Task { await updateState(.speaking) } } nonisolated func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) { Task { await completeSpeech() } } nonisolated func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didCancel utterance: AVSpeechUtterance) { Task { await completeSpeech() } } nonisolated func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didPause utterance: AVSpeechUtterance) { // Handle pause if needed } nonisolated func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didContinue utterance: AVSpeechUtterance) { Task { await updateState(.speaking) } } private func updateState(_ state: TtsState) { currentState = state } private func completeSpeech() { completionContinuation?.resume() completionContinuation = nil currentState = .ready } }