diff --git a/ios/ExampleiOSApp/ContentView.swift b/ios/ExampleiOSApp/ContentView.swift index 0d380af..ee236b4 100644 --- a/ios/ExampleiOSApp/ContentView.swift +++ b/ios/ExampleiOSApp/ContentView.swift @@ -16,24 +16,16 @@ struct ContentView: View { .font(.title2.weight(.semibold)) .foregroundColor(.primary) - ZStack(alignment: .topLeading) { - if vm.text.isEmpty { - Text("Type text to synthesize") - .foregroundColor(.secondary) - .padding(.horizontal, 14) - .padding(.vertical, 12) - } - TextEditor(text: $vm.text) - .frame(minHeight: 120, maxHeight: 180) - .padding(8) - .background(Color(.secondarySystemBackground)) - .cornerRadius(12) - .overlay( - RoundedRectangle(cornerRadius: 12) - .stroke(Color.secondary.opacity(0.3), lineWidth: 1) - ) - } - .padding(.horizontal) + TextEditor(text: $vm.text) + .frame(minHeight: 120, maxHeight: 180) + .padding(8) + .background(Color(.secondarySystemBackground)) + .cornerRadius(12) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(Color.secondary.opacity(0.3), lineWidth: 1) + ) + .padding(.horizontal) HStack(spacing: 12) { Text("NFE") @@ -92,7 +84,3 @@ struct ContentView: View { .onAppear { vm.startup() } } } - -#Preview { - ContentView() -} diff --git a/ios/ExampleiOSApp/TTSService.swift b/ios/ExampleiOSApp/TTSService.swift index fc683c0..4c446e5 100644 --- a/ios/ExampleiOSApp/TTSService.swift +++ b/ios/ExampleiOSApp/TTSService.swift @@ -4,21 +4,11 @@ import OnnxRuntimeBindings final class TTSService { enum Voice { case male, female } - struct SynthesisResult { - let url: URL - let elapsedSeconds: Double - let audioSeconds: Double - var rtf: Double { elapsedSeconds / max(audioSeconds, 1e-6) } - } - private let env: ORTEnv private let textToSpeech: TextToSpeech private let bundleOnnxDir: String private let sampleRate: Int - // Cached style per voice (precomputed at startup or on first use) - private var cachedStyle: [Voice: Style] = [:] - init() throws { bundleOnnxDir = try Self.locateOnnxDirInBundle() env = try ORTEnv(loggingLevel: .warning) @@ -26,24 +16,10 @@ final class TTSService { sampleRate = textToSpeech.sampleRate } - // Public warmup: precompute styles and run a quick generation to warm models - func warmup(nfe: Int = 1) async { - do { try precomputeStyle(for: .male) } catch { print("Warmup style (M) error: \(error)") } - do { try precomputeStyle(for: .female) } catch { print("Warmup style (F) error: \(error)") } - // Run a tiny synthesis to JIT/warm up kernels; discard file - do { - let res = try await synthesize(text: "Warm up", nfe: max(1, nfe), voice: .male) - try? FileManager.default.removeItem(at: res.url) - } catch { - print("Warmup synth error: \(error)") - } - } - - func synthesize(text: String, nfe: Int, voice: Voice) async throws -> SynthesisResult { - let tic = Date() - - // 1) Get or compute style for the selected voice - let style = try getStyle(voice: voice) + func synthesize(text: String, nfe: Int, voice: Voice) async throws -> URL { + // Load style for the selected voice + let styleURL = try Self.locateVoiceStyleURL(voice: voice) + let style = try loadVoiceStyle([styleURL.path], verbose: false) // 2) Synthesize via packed TextToSpeech component let (wav, duration) = try textToSpeech.call(text, style, nfe) @@ -54,22 +30,7 @@ final class TTSService { let tmpURL = FileManager.default.temporaryDirectory.appendingPathComponent("supertonic_tts_\(UUID().uuidString).wav") try writeWavFile(tmpURL.path, wavOut, sampleRate) - let elapsed = Date().timeIntervalSince(tic) - return SynthesisResult(url: tmpURL, elapsedSeconds: elapsed, audioSeconds: audioSeconds) - } - - // MARK: - Style helpers - private func precomputeStyle(for voice: Voice) throws { - if cachedStyle[voice] != nil { return } - let styleURL = try Self.locateVoiceStyleURL(voice: voice) - let style = try loadVoiceStyle([styleURL.path], verbose: false) - cachedStyle[voice] = style - } - - private func getStyle(voice: Voice) throws -> Style { - if let style = cachedStyle[voice] { return style } - try precomputeStyle(for: voice) - return cachedStyle[voice]! + return tmpURL } // MARK: - Resource location helpers diff --git a/ios/ExampleiOSApp/TTSViewModel.swift b/ios/ExampleiOSApp/TTSViewModel.swift index 318c862..8146d1f 100644 --- a/ios/ExampleiOSApp/TTSViewModel.swift +++ b/ios/ExampleiOSApp/TTSViewModel.swift @@ -10,7 +10,6 @@ final class TTSViewModel: ObservableObject { @Published var isPlaying: Bool = false @Published var errorMessage: String? @Published var audioURL: URL? - @Published var elapsedSeconds: Double? @Published var audioSeconds: Double? @@ -19,14 +18,12 @@ final class TTSViewModel: ObservableObject { var rtfText: String? { guard let e = elapsedSeconds, let a = audioSeconds, a > 0 else { return nil } - let rtf = e / a - return String(format: "RTF %.2fx · %.2fs / %.2fs", rtf, e, a) + return String(format: "RTF %.2fx · %.2fs / %.2fs", e / a, e, a) } func startup() { do { service = try TTSService() - Task { await self.service?.warmup(nfe: 5) } } catch { errorMessage = "Failed to init TTS: \(error.localizedDescription)" } @@ -40,15 +37,18 @@ final class TTSViewModel: ObservableObject { elapsedSeconds = nil audioSeconds = nil Task { + let tic = Date() do { - let result = try await service.synthesize(text: text, nfe: Int(nfe), voice: voice) + let url = try await service.synthesize(text: text, nfe: Int(nfe), voice: voice) + let elapsed = Date().timeIntervalSince(tic) + let audio = audioDuration(at: url) await MainActor.run { - self.audioURL = result.url - self.elapsedSeconds = result.elapsedSeconds - self.audioSeconds = result.audioSeconds + self.audioURL = url + self.elapsedSeconds = elapsed + self.audioSeconds = audio self.isGenerating = false + self.play(url: url) } - self.play(url: result.url) } catch { await MainActor.run { self.errorMessage = error.localizedDescription @@ -73,4 +73,9 @@ final class TTSViewModel: ObservableObject { } isPlaying = true } + + private func audioDuration(at url: URL) -> Double? { + guard let file = try? AVAudioFile(forReading: url) else { return nil } + return Double(file.length) / file.fileFormat.sampleRate + } }