Skip to content

Commit 3dfd5d8

Browse files
committed
add auto download for the missing mlmodelc, phoene mappers & vocabs
1 parent 47b45dd commit 3dfd5d8

2 files changed

Lines changed: 96 additions & 5 deletions

File tree

Sources/FluidAudio/TextToSpeech/Kokoro/Assets/TtsResourceDownloader.swift

Lines changed: 88 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,36 @@ import Foundation
44
public enum TtsResourceDownloader {
55

66
private static let logger = AppLogger(category: "TtsResourceDownloader")
7+
private static let zhRepoRemotePath = "alexwengg/tts-zh"
78

89
/// Download a voice embedding JSON file from HuggingFace
910
public static func downloadVoiceEmbedding(voice: String) async throws -> Data {
10-
let url = try ModelRegistry.resolveModel(Repo.kokoro.remotePath, "voices/\(voice).json")
11+
// Primary repo (FluidInference/kokoro-82m-coreml)
12+
let primary = try ModelRegistry.resolveModel(Repo.kokoro.remotePath, "voices/\(voice).json")
13+
// Fallback repo for zh voices
14+
let fallback = try ModelRegistry.resolveModel(zhRepoRemotePath, "voices/\(voice).json")
1115

12-
do {
13-
let data = try await AssetDownloader.fetchData(
16+
func attempt(_ url: URL) async throws -> Data {
17+
try await AssetDownloader.fetchData(
1418
from: url,
1519
description: "\(voice) voice embedding JSON",
1620
logger: logger
1721
)
18-
logger.info("Downloaded voice embedding JSON for \(voice)")
22+
}
23+
24+
do {
25+
let data = try await attempt(primary)
26+
logger.info("Downloaded voice embedding JSON for \(voice) from primary repo")
1927
return data
2028
} catch {
21-
throw TTSError.modelNotFound("Voice embedding JSON unavailable for \(voice): \(error.localizedDescription)")
29+
logger.warning("Primary voice embedding missing for \(voice); trying zh repo")
30+
do {
31+
let data = try await attempt(fallback)
32+
logger.info("Downloaded voice embedding JSON for \(voice) from zh repo")
33+
return data
34+
} catch {
35+
throw TTSError.modelNotFound("Voice embedding JSON unavailable for \(voice)")
36+
}
2237
}
2338
}
2439

@@ -74,3 +89,71 @@ public enum TtsResourceDownloader {
7489
}
7590

7691
}
92+
93+
// MARK: - zh assets (vocab + char lexicon)
94+
95+
extension TtsResourceDownloader {
96+
/// Ensure `zh_vocab_index.json` exists in Kokoro cache. Falls back to downloading `zh.json` and writing it
97+
/// as `zh_vocab_index.json` if needed.
98+
@discardableResult
99+
public static func ensureZhVocabularyInCache() async throws -> URL {
100+
let cacheDir = try TtsModels.cacheDirectoryURL()
101+
let kokoroDir = cacheDir.appendingPathComponent("Models/kokoro")
102+
try FileManager.default.createDirectory(at: kokoroDir, withIntermediateDirectories: true)
103+
104+
let zhVocabURL = kokoroDir.appendingPathComponent("zh_vocab_index.json")
105+
if FileManager.default.fileExists(atPath: zhVocabURL.path) {
106+
return zhVocabURL
107+
}
108+
109+
let primary = try ModelRegistry.resolveModel(zhRepoRemotePath, "zh_vocab_index.json")
110+
let alt = try ModelRegistry.resolveModel(zhRepoRemotePath, "zh.json")
111+
112+
do {
113+
let descriptor = AssetDownloader.Descriptor(
114+
description: "zh_vocab_index.json",
115+
remoteURL: primary,
116+
destinationURL: zhVocabURL
117+
)
118+
return try await AssetDownloader.ensure(descriptor, logger: logger)
119+
} catch {
120+
logger.warning("zh_vocab_index.json not found in zh repo; trying zh.json")
121+
let data = try await AssetDownloader.fetchData(
122+
from: alt,
123+
description: "zh.json",
124+
logger: logger
125+
)
126+
try data.write(to: zhVocabURL, options: [.atomic])
127+
logger.info("Cached zh vocabulary as zh_vocab_index.json")
128+
return zhVocabURL
129+
}
130+
}
131+
132+
/// Ensure `zh_char_phonemes.json` exists in Kokoro cache, downloading from zh repo if missing.
133+
@discardableResult
134+
public static func ensureZhCharPhonemesInCache() async throws -> URL {
135+
let cacheDir = try TtsModels.cacheDirectoryURL()
136+
let kokoroDir = cacheDir.appendingPathComponent("Models/kokoro")
137+
try FileManager.default.createDirectory(at: kokoroDir, withIntermediateDirectories: true)
138+
139+
let localURL = kokoroDir.appendingPathComponent("zh_char_phonemes.json")
140+
if FileManager.default.fileExists(atPath: localURL.path) {
141+
return localURL
142+
}
143+
144+
let remoteURL = try ModelRegistry.resolveModel(zhRepoRemotePath, "zh_char_phonemes.json")
145+
let descriptor = AssetDownloader.Descriptor(
146+
description: "zh_char_phonemes.json",
147+
remoteURL: remoteURL,
148+
destinationURL: localURL
149+
)
150+
return try await AssetDownloader.ensure(descriptor, logger: logger)
151+
}
152+
153+
/// Convenience: ensure both zh vocab and char phoneme lexicon exist in cache.
154+
public static func ensureZhAssetsInCache() async throws -> (vocabURL: URL, lexURL: URL) {
155+
async let v = ensureZhVocabularyInCache()
156+
async let l = ensureZhCharPhonemesInCache()
157+
return try await (vocabURL: v, lexURL: l)
158+
}
159+
}

Sources/FluidAudioCLI/Commands/TTSZhCommand.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,14 @@ enum TTSZh {
9292
}
9393
}
9494

95+
// Ensure zh assets (vocab + char lexicon) in cache as a fallback, and prefer zh vocab override.
96+
do {
97+
let ensured = try await TtsResourceDownloader.ensureZhAssetsInCache()
98+
await KokoroVocabulary.shared.setOverrideURL(ensured.vocabURL)
99+
} catch {
100+
logger.warning("Failed to ensure zh assets; continuing: \(error.localizedDescription)")
101+
}
102+
95103
// Provide extra voices dirs: explicit + sibling voices
96104
var extraDirs: [URL] = []
97105
if let dir = voicesDir, !dir.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {

0 commit comments

Comments
 (0)