Compare commits
5 Commits
pr-18
...
ios/issue2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ba44107ee | ||
|
|
7941a24074 | ||
|
|
833dc1858b | ||
|
|
0ea52c526e | ||
|
|
0fd9b11372 |
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user