2025-11-23 15:54:59 +09:00
import Foundation
import AVFoundation
@ MainActor
final class TTSViewModel : ObservableObject {
@ Published var text : String = " This morning, I took a walk in the park, and the sound of the birds and the breeze was so pleasant that I stopped for a long time just to listen. "
@ Published var nfe : Double = 5
@ Published var voice : TTSService . Voice = . male
@ Published var isGenerating : Bool = false
@ Published var isPlaying : Bool = false
@ Published var errorMessage : String ?
@ Published var audioURL : URL ?
@ Published var elapsedSeconds : Double ?
@ Published var audioSeconds : Double ?
private var service : TTSService ?
private var player = AudioPlayer ( )
var rtfText : String ? {
guard let e = elapsedSeconds , let a = audioSeconds , a > 0 else { return nil }
2025-11-26 11:11:57 +09:00
return String ( format : " RTF %.2fx · %.2fs / %.2fs " , e / a , e , a )
2025-11-23 15:54:59 +09:00
}
func startup ( ) {
do {
service = try TTSService ( )
} catch {
errorMessage = " Failed to init TTS: \( error . localizedDescription ) "
}
}
func generate ( ) {
guard let service = service else { return }
isGenerating = true
errorMessage = nil
audioURL = nil
elapsedSeconds = nil
audioSeconds = nil
Task {
2025-11-26 11:11:57 +09:00
let tic = Date ( )
2025-11-23 15:54:59 +09:00
do {
2025-11-26 11:11:57 +09:00
let url = try await service . synthesize ( text : text , nfe : Int ( nfe ) , voice : voice )
let elapsed = Date ( ) . timeIntervalSince ( tic )
let audio = audioDuration ( at : url )
2025-11-23 15:54:59 +09:00
await MainActor . run {
2025-11-26 11:11:57 +09:00
self . audioURL = url
self . elapsedSeconds = elapsed
self . audioSeconds = audio
2025-11-23 15:54:59 +09:00
self . isGenerating = false
2025-11-26 11:11:57 +09:00
self . play ( url : url )
2025-11-23 15:54:59 +09:00
}
} catch {
await MainActor . run {
self . errorMessage = error . localizedDescription
self . isGenerating = false
}
}
}
}
func togglePlay ( ) {
if isPlaying {
player . stop ( )
isPlaying = false
} else if let url = audioURL {
play ( url : url )
}
}
private func play ( url : URL ) {
player . play ( url : url ) { [ weak self ] in
DispatchQueue . main . async { self ? . isPlaying = false }
}
isPlaying = true
}
2025-11-26 11:11:57 +09:00
private func audioDuration ( at url : URL ) -> Double ? {
guard let file = try ? AVAudioFile ( forReading : url ) else { return nil }
return Double ( file . length ) / file . fileFormat . sampleRate
}
2025-11-23 15:54:59 +09:00
}