- 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
166 lines
5.7 KiB
Swift
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)")
|
|
}
|
|
}
|
|
}
|