@@ -757,15 +757,29 @@ public struct KokoroSynthesizer {
757757 )
758758 }
759759
760- /// Synthesize directly from a Kokoro-zh phoneme string (one codepoint per token).
761- /// This bypasses English lexicon + chunking and is useful for Mandarin.
762760 public static func synthesizePhonemeStringDetailed(
763761 phonemes: String ,
764762 voice: String = TtsConstants . recommendedVoice,
765763 voiceSpeed: Float = 1.0 ,
766764 variantPreference: ModelNames . TTS . Variant ? = . fifteenSecond
767765 ) async throws -> SynthesisResult {
768- logger. info ( " Starting synthesis from phoneme string; length= \( phonemes. count) " )
766+ return try await synthesizePhonemeStringsDetailed (
767+ phonemes: [ phonemes] ,
768+ voice: voice,
769+ voiceSpeed: voiceSpeed,
770+ variantPreference: variantPreference
771+ )
772+ }
773+
774+ /// Synthesize directly from a list of Kokoro-zh phoneme strings.
775+ /// Each string is treated as a separate chunk.
776+ public static func synthesizePhonemeStringsDetailed(
777+ phonemes: [ String ] ,
778+ voice: String = TtsConstants . recommendedVoice,
779+ voiceSpeed: Float = 1.0 ,
780+ variantPreference: ModelNames . TTS . Variant ? = . fifteenSecond
781+ ) async throws -> SynthesisResult {
782+ logger. info ( " Starting synthesis from \( phonemes. count) phoneme strings " )
769783
770784 try await ensureRequiredFiles ( )
771785 if !isVoiceEmbeddingPayloadCached( for: voice) {
@@ -778,18 +792,22 @@ public struct KokoroSynthesizer {
778792 let capacities = try await capacities ( for: variantPreference)
779793 let lexiconMetrics = await lexiconCache. metrics ( )
780794
781- // Build a single chunk from phoneme codepoints
782- let tokens : [ String ] = phonemes. map { String ( $0) }
783- let chunk = TextChunk (
784- words: [ ] ,
785- atoms: tokens,
786- phonemes: tokens,
787- totalFrames: 0 ,
788- pauseAfterMs: 0 ,
789- text: phonemes
790- )
795+ // Build chunks from phoneme strings
796+ var chunks : [ TextChunk ] = [ ]
797+ for p in phonemes {
798+ let tokens = p. map { String ( $0) }
799+ chunks. append ( TextChunk (
800+ words: [ ] ,
801+ atoms: tokens,
802+ phonemes: tokens,
803+ totalFrames: 0 ,
804+ pauseAfterMs: 0 ,
805+ text: p
806+ ) )
807+ }
808+
791809 let entries = try buildChunkEntries (
792- from: [ chunk ] ,
810+ from: chunks ,
793811 vocabulary: vocabulary,
794812 preference: variantPreference,
795813 capacities: capacities
@@ -976,28 +994,28 @@ public struct KokoroSynthesizer {
976994 let clamped = max ( 0.1 , factor)
977995 if abs ( clamped - 1.0 ) < 0.01 { return samples }
978996
979- if clamped < 1.0 {
980- let repeatCount = max ( 1 , Int ( round ( 1.0 / clamped ) ) )
981- var stretched : [ Float ] = [ ]
982- stretched . reserveCapacity ( samples . count * repeatCount )
983- for sample in samples {
984- for _ in 0 ..< repeatCount {
985- stretched . append ( sample )
986- }
987- }
988- return stretched
989- }
990-
991- let step = Int ( clamped )
992- guard step > 1 else { return samples }
993- var compressed : [ Float ] = [ ]
994- compressed . reserveCapacity ( samples . count / step + 1 )
995- var index = 0
996- while index < samples . count {
997- compressed . append ( samples [ index ] )
998- index += step
999- }
1000- return compressed
997+ let inputCount = samples . count
998+ guard inputCount > 1 else { return samples }
999+
1000+ // Calculate output size: new_duration = old_duration / factor
1001+ let outputCount = Int ( Float ( inputCount ) / clamped )
1002+ guard outputCount > 0 else { return [ ] }
1003+
1004+ // Pad input with one extra sample (duplicate last) to safely handle interpolation at the edge
1005+ // vDSP_vlint requires index < M-1, so we need M = inputCount + 1
1006+ var padded = samples
1007+ padded . append ( samples . last ?? 0 )
1008+
1009+ var indices = [ Float ] ( repeating : 0 , count : outputCount )
1010+ var start : Float = 0
1011+ var step : Float = clamped
1012+ vDSP_vramp ( & start , & step, & indices , 1 , vDSP_Length ( outputCount ) )
1013+
1014+ // vDSP.linearInterpolate(elements:using:)
1015+ // Requires Accelerate
1016+ let output = vDSP . linearInterpolate ( elementsOf : padded , using : indices )
1017+
1018+ return output
10011019 }
10021020
10031021 static func removeDelimiterCharacters( from text: String ) -> String {
0 commit comments