diff --git a/Sources/FluidAudio/ModelNames.swift b/Sources/FluidAudio/ModelNames.swift index 6a015d237..edfa91bdb 100644 --- a/Sources/FluidAudio/ModelNames.swift +++ b/Sources/FluidAudio/ModelNames.swift @@ -1428,7 +1428,10 @@ public enum ModelNames { // re-download. See mobius laishere-coreml docs/trials-and-errors.md. public static let noise = "KokoroNoise_v2.mlmodelc" public static let vocoder = "KokoroVocoder.mlmodelc" - public static let tail = "KokoroTail.mlmodelc" + // v2: COLA-normalized iSTFT deconv weights (raw output was exactly 1.5x + // the PyTorch reference). Renamed (not overwritten) so cached clients + // re-download. See issue #852. + public static let tail = "KokoroTail_v2.mlmodelc" /// Auxiliary (non-CoreML) files that must accompany the mlmodelc bundles. public static let vocab = "vocab.json" diff --git a/Sources/FluidAudio/TTS/KokoroAne/KokoroAneConstants.swift b/Sources/FluidAudio/TTS/KokoroAne/KokoroAneConstants.swift index c42807a96..2ba2623af 100644 --- a/Sources/FluidAudio/TTS/KokoroAne/KokoroAneConstants.swift +++ b/Sources/FluidAudio/TTS/KokoroAne/KokoroAneConstants.swift @@ -16,7 +16,7 @@ public enum KokoroAneConstants { /// Default voice id for the Japanese (`ANE-ja/`) variant. public static let defaultVoiceJapanese = "jf_alpha" - /// Output sample rate of the iSTFT in `KokoroTail.mlpackage`. + /// Output sample rate of the iSTFT in `KokoroTail_v2.mlpackage`. public static let sampleRate = 24_000 /// BOS / EOS token id used by both `convert-coreml.py` and the iOS demo. diff --git a/Sources/FluidAudio/TTS/KokoroAne/KokoroAneManager.swift b/Sources/FluidAudio/TTS/KokoroAne/KokoroAneManager.swift index 80f02285c..e604ef525 100644 --- a/Sources/FluidAudio/TTS/KokoroAne/KokoroAneManager.swift +++ b/Sources/FluidAudio/TTS/KokoroAne/KokoroAneManager.swift @@ -358,14 +358,14 @@ public actor KokoroAneManager { private func wavData(from result: KokoroAneSynthesisResult) throws -> Data { do { - // Japanese writes at the model's native level (no peak-normalization) - // so the output matches the PyTorch reference instead of being - // slammed to 0 dBFS. English/Mandarin keep peak-normalization until - // their tails get the same COLA-corrected iSTFT (#698 follow-up). + // All variants write at the model's native level (no + // peak-normalization) so the output matches the PyTorch reference + // instead of being slammed to 0 dBFS. Requires the COLA-corrected + // KokoroTail_v2 (#852). return try AudioWAV.data( from: result.samples, sampleRate: Double(result.sampleRate), - normalize: variant != .japanese) + normalize: false) } catch { throw KokoroAneError.audioConversionFailed(error.localizedDescription) } diff --git a/Sources/FluidAudio/TTS/KokoroAne/Pipeline/KokoroAneSynthesizer+Types.swift b/Sources/FluidAudio/TTS/KokoroAne/Pipeline/KokoroAneSynthesizer+Types.swift index a7fddc7b5..c00de1884 100644 --- a/Sources/FluidAudio/TTS/KokoroAne/Pipeline/KokoroAneSynthesizer+Types.swift +++ b/Sources/FluidAudio/TTS/KokoroAne/Pipeline/KokoroAneSynthesizer+Types.swift @@ -96,7 +96,7 @@ public enum KokoroAneStage: String, CaseIterable, Sendable { case .prosody: return "KokoroProsody.mlmodelc" case .noise: return "KokoroNoise_v2.mlmodelc" // v2: atan2 phase-correction (HF-noise fix) case .vocoder: return "KokoroVocoder.mlmodelc" - case .tail: return "KokoroTail.mlmodelc" + case .tail: return "KokoroTail_v2.mlmodelc" // v2: COLA-normalized iSTFT (level fix, #852) } } } diff --git a/Sources/FluidAudioCLI/Commands/TTSCommand.swift b/Sources/FluidAudioCLI/Commands/TTSCommand.swift index 43575a473..0306d3088 100644 --- a/Sources/FluidAudioCLI/Commands/TTSCommand.swift +++ b/Sources/FluidAudioCLI/Commands/TTSCommand.swift @@ -879,10 +879,12 @@ public struct TTS { detailed = try await manager.synthesizeDetailed( text: text, voice: resolvedVoice, speed: 1.0) } + // Native level for all variants — matches the PyTorch reference + // now that KokoroTail_v2 carries the COLA-corrected iSTFT (#852). let wav = try AudioWAV.data( from: detailed.samples, sampleRate: Double(detailed.sampleRate), - normalize: variant != .japanese) + normalize: false) let tSynth1 = Date() let outURL = resolveInputURL(output)