ReadKeep/readeck/Utils/TTSManager.swift
Ilyas Hallak 09f1ddea58 Add text-to-speech functionality
- Add TTSManager and SpeechQueue utilities
- Create AddTextToSpeechQueueUseCase and ReadBookmarkUseCase
- Add SpeechPlayer UI components (GlobalPlayerContainerView, SpeechPlayerView, SpeechPlayerViewModel)
- Update BookmarkDetailView and BookmarkDetailViewModel for TTS integration
- Add audio background mode to Info.plist
- Update PhoneTabView for TTS controls
- Add StringExtensions for text processing
- Add StringExtensionsTests for testing
- Update Localizable.xcstrings with new strings
- Add VS Code settings
2025-07-09 22:31:17 +02:00

166 lines
5.7 KiB
Swift

import Foundation
import UIKit
import AVFoundation
class TTSManager: NSObject, AVSpeechSynthesizerDelegate {
static let shared = TTSManager()
private let synthesizer = AVSpeechSynthesizer()
private var isSpeaking = false
override private init() {
super.init()
synthesizer.delegate = self
configureAudioSession()
}
private func configureAudioSession() {
do {
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(.playback, mode: .spokenAudio, options: [.mixWithOthers, .duckOthers, .allowBluetooth, .allowBluetoothA2DP])
try audioSession.setActive(true)
// Background-Audio aktivieren
try audioSession.setCategory(.playback, mode: .spokenAudio, options: [.mixWithOthers, .duckOthers, .allowBluetooth, .allowBluetoothA2DP])
// Notification für App-Lifecycle
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAppDidEnterBackground),
name: UIApplication.didEnterBackgroundNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAppWillEnterForeground),
name: UIApplication.willEnterForegroundNotification,
object: nil
)
} catch {
print("Fehler beim Konfigurieren der Audio-Session: \(error)")
}
}
func speak(text: String, language: String = "de-DE") {
guard !text.isEmpty else { return }
if synthesizer.isSpeaking {
synthesizer.stopSpeaking(at: .immediate)
}
let utterance = AVSpeechUtterance(string: text)
// Versuche eine hochwertige Stimme zu finden
if let enhancedVoice = findEnhancedVoice(for: language) {
utterance.voice = enhancedVoice
} else {
utterance.voice = AVSpeechSynthesisVoice(language: language)
}
utterance.rate = 0.5
utterance.pitchMultiplier = 1.0
utterance.volume = 1.0
synthesizer.speak(utterance)
isSpeaking = true
}
private func findEnhancedVoice(for language: String) -> AVSpeechSynthesisVoice? {
let availableVoices = AVSpeechSynthesisVoice.speechVoices()
// Bevorzugte Stimmen für alle Sprachen
let preferredVoiceNames = [
"Anna", // Deutsche Premium-Stimme
"Helena", // Deutsche Premium-Stimme
"Siri", // Siri-Stimme (falls verfügbar)
"Enhanced", // Enhanced-Stimmen
"Karen", // Englische Premium-Stimme
"Daniel", // Englische Premium-Stimme
"Marie", // Französische Premium-Stimme
"Paolo", // Italienische Premium-Stimme
"Carmen", // Spanische Premium-Stimme
"Yuki" // Japanische Premium-Stimme
]
// Zuerst nach bevorzugten Stimmen für die spezifische Sprache suchen
for voiceName in preferredVoiceNames {
if let voice = availableVoices.first(where: {
$0.language == language &&
$0.name.contains(voiceName)
}) {
return voice
}
}
// Fallback: Erste verfügbare Stimme für die Sprache
return availableVoices.first(where: { $0.language == language })
}
func pause() {
synthesizer.pauseSpeaking(at: .word)
}
func resume() {
synthesizer.continueSpeaking()
}
func stop() {
synthesizer.stopSpeaking(at: .immediate)
isSpeaking = false
}
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
isSpeaking = false
}
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didCancel utterance: AVSpeechUtterance) {
isSpeaking = false
}
func isCurrentlySpeaking() -> Bool {
return synthesizer.isSpeaking
}
@objc private func handleAppDidEnterBackground() {
// App geht in Hintergrund - Audio-Session beibehalten
do {
try AVAudioSession.sharedInstance().setActive(true)
} catch {
print("Fehler beim Aktivieren der Audio-Session im Hintergrund: \(error)")
}
}
@objc private func handleAppWillEnterForeground() {
// App kommt in Vordergrund - Audio-Session erneuern
do {
try AVAudioSession.sharedInstance().setActive(true)
} catch {
print("Fehler beim Aktivieren der Audio-Session im Vordergrund: \(error)")
}
}
deinit {
NotificationCenter.default.removeObserver(self)
}
// Debug-Methode: Zeigt alle verfügbaren Stimmen für eine Sprache
func printAvailableVoices(for language: String = "de-DE") {
let voices = AVSpeechSynthesisVoice.speechVoices()
let filteredVoices = voices.filter { $0.language.starts(with: language.prefix(2)) }
print("Verfügbare Stimmen für \(language):")
for voice in filteredVoices {
print("- \(voice.name) (\(voice.language)) - Qualität: \(voice.quality.rawValue)")
}
}
// Debug-Methode: Zeigt alle verfügbaren Sprachen
func printAllAvailableLanguages() {
let voices = AVSpeechSynthesisVoice.speechVoices()
let languages = Set(voices.map { $0.language })
print("Verfügbare Sprachen:")
for language in languages.sorted() {
print("- \(language)")
}
}
}