5 Commits

Author SHA1 Message Date
haeon
9ba44107ee minor refactoring 2025-11-26 11:11:57 +09:00
haeon
7941a24074 fix array type 2025-11-26 10:55:36 +09:00
ANLGBOY
833dc1858b remove comment leftover (Fixes #24) 2025-11-26 02:05:07 +09:00
ANLGBOY
0ea52c526e fix curly quotes errors (Fixes #23) 2025-11-25 07:58:21 +09:00
Hyeongju Kim
0fd9b11372 Merge pull request #20 from supertone-inc/pr-18 2025-11-24 12:05:36 +09:00
5 changed files with 39 additions and 89 deletions

View File

@@ -77,10 +77,10 @@ std::string UnicodeProcessor::preprocessText(const std::string& text) {
{"", "-"}, // em dash
{"¯", " "}, // macron
{"_", " "}, // underscore
{""", "\""}, // left double quote (U+201C)
{""", "\""}, // right double quote (U+201D)
{"'", "'"}, // left single quote (U+2018)
{"'", "'"}, // right single quote (U+2019)
{ u8"\u201C", "\"" }, // left double quote
{ u8"\u201D", "\"" }, // right double quote
{ u8"\u2018", "'" }, // left single quote
{ u8"\u2019", "'" }, // right single quote
{"´", "'"}, // acute accent
{"`", "'"}, // grave accent
{"[", " "}, // left bracket
@@ -175,9 +175,9 @@ std::string UnicodeProcessor::preprocessText(const std::string& text) {
last_three == "" || last_three == "" ||
last_three == "" || last_three == "" ||
last_three == "" || last_three == "" ||
last_three == "»" || last_three == """ ||
last_three == """ || last_three == "'" ||
last_three == "'") {
last_three == "»" || last_three == u8"\u201C" ||
last_three == u8"\u201D" || last_three == u8"\u2018" ||
last_three == u8"\u2019") {
ends_with_punct = true;
}
}

View File

@@ -16,13 +16,6 @@ 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)
@@ -32,7 +25,6 @@ struct ContentView: View {
RoundedRectangle(cornerRadius: 12)
.stroke(Color.secondary.opacity(0.3), lineWidth: 1)
)
}
.padding(.horizontal)
HStack(spacing: 12) {
@@ -92,7 +84,3 @@ struct ContentView: View {
.onAppear { vm.startup() }
}
}
#Preview {
ContentView()
}

View File

@@ -4,25 +4,11 @@ import OnnxRuntimeBindings
final class TTSService {
enum Voice { case male, female }
struct Settings {
var nTest: Int = 1
}
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)
@@ -30,50 +16,21 @@ 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, settings: Settings = Settings()) 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)
let audioSeconds = Double(duration[0])
let (wav, duration) = try textToSpeech.call(text, style, nfe)
let audioSeconds = Double(duration)
let wavLenSample = min(Int(Double(sampleRate) * audioSeconds), wav.count)
let wavOut = Array(wav[0..<wavLenSample])
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

View File

@@ -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
}
}

View File

@@ -12,7 +12,7 @@ targets:
deploymentTarget: "15.0"
sources:
- path: .
- path: ../../swift/Sources/Helper.swift <<- 여기
- path: ../../swift/Sources/Helper.swift
type: file
resources:
- path: onnx