diff --git a/Sources/FluidAudio/TTS/KokoroAne/KokoroAneManager.swift b/Sources/FluidAudio/TTS/KokoroAne/KokoroAneManager.swift index aff1ae7e5..350d672be 100644 --- a/Sources/FluidAudio/TTS/KokoroAne/KokoroAneManager.swift +++ b/Sources/FluidAudio/TTS/KokoroAne/KokoroAneManager.swift @@ -452,9 +452,18 @@ public actor KokoroAneManager { /// and vocab-supported punctuation kept as prosody/pause tokens. private func phonemize(text: String) async throws -> String { let phonemizer = await ensureEnglishPhonemizer() - return try await phonemizer.phonemize(text) { word in + let raw = try await phonemizer.phonemize(text) { word in try await G2PModel.shared.phonemize(word: word) } + return Self.misakiOutputForm(raw) + } + + /// misaki's flap step for Kokoro v1.0 (`G2P.__call__`): `ɾ` → `T`. The + /// lexicon stores the raw flap ("metal" was read as "mattle"). misaki also + /// maps the glottal stop `ʔ` → `t`, but this chain reads `ʔn` better than + /// `tn` ("button" became "butt"), so `ʔ` is kept. + static func misakiOutputForm(_ phonemes: String) -> String { + phonemes.replacingOccurrences(of: "ɾ", with: "T") } /// Build (and cache) the English frontend: chain vocab → allowed diff --git a/Tests/FluidAudioTests/TTS/KokoroAne/KokoroAneMisakiOutputFormTests.swift b/Tests/FluidAudioTests/TTS/KokoroAne/KokoroAneMisakiOutputFormTests.swift new file mode 100644 index 000000000..f7bfe0262 --- /dev/null +++ b/Tests/FluidAudioTests/TTS/KokoroAne/KokoroAneMisakiOutputFormTests.swift @@ -0,0 +1,15 @@ +import XCTest + +@testable import FluidAudio + +/// misaki's `ɾ` → `T` flap step on the English frontend output. +final class KokoroAneMisakiOutputFormTests: XCTestCase { + + func testFlapBecomesT() { + XCTAssertEqual(KokoroAneManager.misakiOutputForm("mˈɛɾᵊl wˈɔɾəɹ"), "mˈɛTᵊl wˈɔTəɹ") + } + + func testGlottalStopIsKept() { + XCTAssertEqual(KokoroAneManager.misakiOutputForm("bˈʌʔn"), "bˈʌʔn") + } +}