Skip to content

Commit d78c5bf

Browse files
committed
Unify registry override and allow users to override programtically
1 parent a5cecf8 commit d78c5bf

10 files changed

Lines changed: 206 additions & 108 deletions

File tree

Documentation/API.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ This page summarizes the primary public APIs across modules. See inline doc comm
66

77
**Audio Format:** All modules expect 16kHz mono Float32 audio samples. Use `FluidAudio.AudioConverter` to convert `AVAudioPCMBuffer` or files to 16kHz mono for both CLI and library paths.
88

9-
**Model Loading:** Models auto-download from HuggingFace on first use. Set `https_proxy` environment variable if behind corporate firewall.
9+
**Model Registry:** Models auto-download from HuggingFace by default. Customize the registry URL using:
10+
- `ModelRegistry.baseURL` (programmatic) - recommended for apps
11+
- `REGISTRY_URL` or `MODEL_REGISTRY_URL` environment variables - recommended for CLI/testing
12+
- Priority order: programmatic override → env vars → default (HuggingFace)
13+
14+
**Proxy Configuration:** If behind a corporate firewall, set the `https_proxy` (or `http_proxy`) environment variable. Both registry URL and proxy configuration are centralized in `ModelRegistry`.
1015

1116
**Error Handling:** All async methods throw descriptive errors. Use proper error handling in production code.
1217

README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,62 @@ Important: When adding FluidAudio as a package dependency, only add the library
6969

7070
> **Note:** The Kokoro TTS tooling currently ships arm64-only dependencies. See the [arm64 build requirements](Documentation/TTS/README.md#arm64-only-builds) guide if you hit linker errors targeting x86_64.
7171
72+
## Configuration
73+
74+
<details>
75+
<summary><b>Model Registry URL</b> - Use custom registry/mirror</summary>
76+
77+
By default, FluidAudio downloads models from HuggingFace. You can override this to use a mirror, local server, or air-gapped environment.
78+
79+
**Programmatic override (recommended for apps):**
80+
```swift
81+
import FluidAudio
82+
83+
// Set custom registry before using any managers
84+
ModelRegistry.baseURL = "https://your-mirror.example.com"
85+
86+
// Models will now download from the custom registry
87+
let diarizer = DiarizerManager()
88+
```
89+
90+
**Environment Variables (recommended for CLI/testing):**
91+
```bash
92+
# Use custom registry
93+
export REGISTRY_URL=https://your-mirror.example.com
94+
swift run fluidaudio transcribe audio.wav
95+
96+
# Or use the MODEL_REGISTRY_URL alias
97+
export MODEL_REGISTRY_URL=https://models.internal.corp
98+
swift run fluidaudio diarization-benchmark --auto-download
99+
```
100+
101+
**Xcode Scheme Configuration:**
102+
1. Edit Scheme → Run → Arguments Passed On Launch
103+
2. Add environment variable: `REGISTRY_URL` = `https://your-mirror.example.com`
104+
3. The custom registry will apply to all debug runs
105+
106+
</details>
107+
108+
<details>
109+
<summary><b>Proxy Configuration</b> - Corporate firewall setup</summary>
110+
111+
If you're behind a corporate firewall, set the `https_proxy` environment variable:
112+
113+
```bash
114+
export https_proxy=http://proxy.company.com:8080
115+
# or for authenticated proxies:
116+
export https_proxy=http://user:password@proxy.company.com:8080
117+
118+
swift run fluidaudio transcribe audio.wav
119+
```
120+
121+
**Xcode Scheme Configuration for Proxy:**
122+
1. Edit Scheme → Run → Arguments → Environment Variables
123+
2. Add `https_proxy` with your proxy URL
124+
3. FluidAudio will automatically route downloads through the proxy
125+
126+
</details>
127+
72128
## Documentation
73129

74130
**[DeepWiki](https://deepwiki.com/FluidInference/FluidAudio)** for auto-generated docs for this repo.

Sources/FluidAudio/DownloadUtils.swift

Lines changed: 4 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,8 @@ public class DownloadUtils {
77

88
private static let logger = AppLogger(category: "DownloadUtils")
99

10-
public static let sharedSession: URLSession = {
11-
let configuration = URLSessionConfiguration.default
12-
13-
// Configure proxy settings if environment variables are set
14-
if let proxyConfig = configureProxySettings() {
15-
configuration.connectionProxyDictionary = proxyConfig
16-
}
17-
18-
return URLSession(configuration: configuration)
19-
}()
10+
/// Shared URLSession with registry and proxy configuration
11+
public static let sharedSession: URLSession = ModelRegistry.configuredSession()
2012

2113
private static let huggingFaceUserAgent = "FluidAudio/1.0 (HuggingFaceDownloader)"
2214

@@ -180,70 +172,6 @@ public class DownloadUtils {
180172
throw lastError ?? HuggingFaceDownloadError.invalidResponse
181173
}
182174

183-
private static func configureProxySettings() -> [String: Any]? {
184-
#if os(macOS)
185-
var proxyConfig: [String: Any] = [:]
186-
var hasProxyConfig = false
187-
188-
// Configure HTTPS proxy
189-
if let httpsProxy = ProcessInfo.processInfo.environment["https_proxy"],
190-
let proxySettings = parseProxyURL(httpsProxy, type: "HTTPS")
191-
{
192-
proxyConfig.merge(proxySettings) { _, new in new }
193-
hasProxyConfig = true
194-
}
195-
196-
// Configure HTTP proxy
197-
if let httpProxy = ProcessInfo.processInfo.environment["http_proxy"],
198-
let proxySettings = parseProxyURL(httpProxy, type: "HTTP")
199-
{
200-
proxyConfig.merge(proxySettings) { _, new in new }
201-
hasProxyConfig = true
202-
}
203-
204-
return hasProxyConfig ? proxyConfig : nil
205-
#else
206-
// Proxy configuration not available on iOS
207-
return nil
208-
#endif
209-
}
210-
211-
private static func parseProxyURL(_ proxyURLString: String, type: String) -> [String: Any]? {
212-
#if os(macOS)
213-
guard let proxyURL = URL(string: proxyURLString),
214-
let host = proxyURL.host,
215-
let port = proxyURL.port
216-
else {
217-
logger.warning("Invalid \(type) proxy URL: \(proxyURLString)")
218-
return nil
219-
}
220-
221-
let config: [String: Any]
222-
switch type {
223-
case "HTTPS":
224-
config = [
225-
kCFNetworkProxiesHTTPSEnable as String: true,
226-
kCFNetworkProxiesHTTPSProxy as String: host,
227-
kCFNetworkProxiesHTTPSPort as String: port,
228-
]
229-
case "HTTP":
230-
config = [
231-
kCFNetworkProxiesHTTPEnable as String: true,
232-
kCFNetworkProxiesHTTPProxy as String: host,
233-
kCFNetworkProxiesHTTPPort as String: port,
234-
]
235-
default:
236-
return nil
237-
}
238-
239-
logger.info("Configured \(type) proxy: \(host):\(port)")
240-
return config
241-
#else
242-
// Proxy configuration not available on iOS
243-
return nil
244-
#endif
245-
}
246-
247175
/// Download progress callback
248176
public typealias ProgressHandler = (Double) -> Void
249177

@@ -468,7 +396,7 @@ public class DownloadUtils {
468396
/// List files in a HuggingFace repository
469397
private static func listRepoFiles(_ repo: Repo, path: String = "") async throws -> [RepoFile] {
470398
let apiPath = path.isEmpty ? "tree/main" : "tree/main/\(path)"
471-
let apiURL = URL(string: "https://huggingface.co/api/models/\(repo.remotePath)/\(apiPath)")!
399+
let apiURL = ModelRegistry.apiModels(repo.remotePath, apiPath)
472400

473401
var request = URLRequest(url: apiURL)
474402
request.timeoutInterval = 30
@@ -575,8 +503,7 @@ public class DownloadUtils {
575503
}
576504

577505
// Download URL
578-
let downloadURL = URL(
579-
string: "https://huggingface.co/\(repo.remotePath)/resolve/main/\(path)")!
506+
let downloadURL = ModelRegistry.resolveModel(repo.remotePath, path)
580507

581508
// Download the file (no retries)
582509
do {
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import Foundation
2+
import OSLog
3+
4+
/// Model registry configuration for downloading models and datasets
5+
/// Handles both registry URL and proxy configuration
6+
public enum ModelRegistry {
7+
private static let logger = AppLogger(category: "ModelRegistry")
8+
9+
// MARK: - Registry URL Configuration
10+
11+
private static var _customBaseURL: String?
12+
13+
/// Base registry URL (default: HuggingFace)
14+
/// Can be overridden programmatically to use a different model registry or mirror.
15+
/// Priority: programmatic override → REGISTRY_URL env var → MODEL_REGISTRY_URL env var → default
16+
public static var baseURL: String {
17+
get {
18+
_customBaseURL
19+
?? ProcessInfo.processInfo.environment["REGISTRY_URL"]
20+
?? ProcessInfo.processInfo.environment["MODEL_REGISTRY_URL"]
21+
?? "https://huggingface.co"
22+
}
23+
set {
24+
_customBaseURL = newValue
25+
}
26+
}
27+
28+
// MARK: - URL Construction
29+
30+
/// Construct API URL for listing model repository contents
31+
public static func apiModels(_ repoPath: String, _ apiPath: String) -> URL {
32+
URL(string: "\(baseURL)/api/models/\(repoPath)/\(apiPath)")!
33+
}
34+
35+
/// Construct download URL for a model file
36+
public static func resolveModel(_ repoPath: String, _ filePath: String) -> URL {
37+
URL(string: "\(baseURL)/\(repoPath)/resolve/main/\(filePath)")!
38+
}
39+
40+
/// Construct API URL for listing dataset contents
41+
public static func apiDatasets(_ dataset: String, _ apiPath: String) -> URL {
42+
URL(string: "\(baseURL)/api/datasets/\(dataset)/\(apiPath)")!
43+
}
44+
45+
/// Construct download URL for a dataset file
46+
public static func resolveDataset(_ dataset: String, _ filePath: String) -> URL {
47+
URL(string: "\(baseURL)/datasets/\(dataset)/resolve/main/\(filePath)")!
48+
}
49+
50+
// MARK: - Session Configuration
51+
52+
/// Create a URLSession configured with registry URL and proxy settings
53+
static func configuredSession() -> URLSession {
54+
let configuration = URLSessionConfiguration.default
55+
56+
// Configure proxy settings if environment variables are set
57+
if let proxyConfig = configureProxySettings() {
58+
configuration.connectionProxyDictionary = proxyConfig
59+
}
60+
61+
return URLSession(configuration: configuration)
62+
}
63+
64+
// MARK: - Proxy Configuration (macOS only)
65+
66+
private static func configureProxySettings() -> [String: Any]? {
67+
#if os(macOS)
68+
var proxyConfig: [String: Any] = [:]
69+
var hasProxyConfig = false
70+
71+
// Configure HTTPS proxy
72+
if let httpsProxy = ProcessInfo.processInfo.environment["https_proxy"],
73+
let proxySettings = parseProxyURL(httpsProxy, type: "HTTPS")
74+
{
75+
proxyConfig.merge(proxySettings) { _, new in new }
76+
hasProxyConfig = true
77+
}
78+
79+
// Configure HTTP proxy
80+
if let httpProxy = ProcessInfo.processInfo.environment["http_proxy"],
81+
let proxySettings = parseProxyURL(httpProxy, type: "HTTP")
82+
{
83+
proxyConfig.merge(proxySettings) { _, new in new }
84+
hasProxyConfig = true
85+
}
86+
87+
return hasProxyConfig ? proxyConfig : nil
88+
#else
89+
// Proxy configuration not available on iOS
90+
return nil
91+
#endif
92+
}
93+
94+
private static func parseProxyURL(_ proxyURLString: String, type: String) -> [String: Any]? {
95+
#if os(macOS)
96+
guard let proxyURL = URL(string: proxyURLString),
97+
let host = proxyURL.host,
98+
let port = proxyURL.port
99+
else {
100+
logger.warning("Invalid \(type) proxy URL: \(proxyURLString)")
101+
return nil
102+
}
103+
104+
let config: [String: Any]
105+
switch type {
106+
case "HTTPS":
107+
config = [
108+
kCFNetworkProxiesHTTPSEnable as String: true,
109+
kCFNetworkProxiesHTTPSProxy as String: host,
110+
kCFNetworkProxiesHTTPSPort as String: port,
111+
]
112+
case "HTTP":
113+
config = [
114+
kCFNetworkProxiesHTTPEnable as String: true,
115+
kCFNetworkProxiesHTTPProxy as String: host,
116+
kCFNetworkProxiesHTTPPort as String: port,
117+
]
118+
default:
119+
return nil
120+
}
121+
122+
logger.info("Configured \(type) proxy: \(host):\(port)")
123+
return config
124+
#else
125+
// Proxy configuration not available on iOS
126+
return nil
127+
#endif
128+
}
129+
}

Sources/FluidAudio/TextToSpeech/Kokoro/Assets/Lexicon/KokoroVocabulary.swift

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,9 @@ public actor KokoroVocabulary {
7676
let kokoroDir = cacheDir.appendingPathComponent("Models/kokoro")
7777
try FileManager.default.createDirectory(at: kokoroDir, withIntermediateDirectories: true)
7878

79-
let baseURL = "https://huggingface.co/\(Repo.kokoro.remotePath)/resolve/main"
8079
let fileName = "vocab_index.json"
8180
let localPath = kokoroDir.appendingPathComponent(fileName)
82-
83-
guard let remoteURL = URL(string: "\(baseURL)/\(fileName)") else {
84-
throw TTSError.downloadFailed("Invalid Kokoro vocabulary URL: \(baseURL)/\(fileName)")
85-
}
81+
let remoteURL = ModelRegistry.resolveModel(Repo.kokoro.remotePath, fileName)
8682

8783
let descriptor = AssetDownloader.Descriptor(
8884
description: fileName,

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

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,10 @@ import Foundation
44
public enum TtsResourceDownloader {
55

66
private static let logger = AppLogger(category: "TtsResourceDownloader")
7-
private static let kokoroBaseURL = "https://huggingface.co/\(Repo.kokoro.remotePath)/resolve/main"
87

98
/// Download a voice embedding JSON file from HuggingFace
109
public static func downloadVoiceEmbedding(voice: String) async throws -> Data {
11-
let jsonURL = "\(kokoroBaseURL)/voices/\(voice).json"
12-
13-
guard let url = URL(string: jsonURL) else {
14-
throw TTSError.modelNotFound("Invalid URL for voice embedding: \(voice)")
15-
}
10+
let url = ModelRegistry.resolveModel(Repo.kokoro.remotePath, "voices/\(voice).json")
1611

1712
do {
1813
let data = try await AssetDownloader.fetchData(
@@ -61,9 +56,7 @@ public enum TtsResourceDownloader {
6156
return localURL
6257
}
6358

64-
guard let remoteURL = URL(string: "\(kokoroBaseURL)/\(filename)") else {
65-
throw TTSError.modelNotFound("Invalid URL for \(filename)")
66-
}
59+
let remoteURL = ModelRegistry.resolveModel(Repo.kokoro.remotePath, filename)
6760

6861
do {
6962
let descriptor = AssetDownloader.Descriptor(

Sources/FluidAudioCLI/Commands/ASR/AsrBenchmark.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ public class ASRBenchmark {
4949
let downloadURL: String
5050
switch subset {
5151
case "test-clean":
52-
downloadURL = "https://huggingface.co/datasets/FluidInference/librispeech/resolve/main/test-clean.tar.gz"
52+
downloadURL = ModelRegistry.resolveDataset("FluidInference/librispeech", "test-clean.tar.gz").absoluteString
5353
case "test-other":
54-
downloadURL = "https://huggingface.co/datasets/FluidInference/librispeech/resolve/main/test-other.tar.gz"
54+
downloadURL = ModelRegistry.resolveDataset("FluidInference/librispeech", "test-other.tar.gz").absoluteString
5555
case "dev-clean":
5656
downloadURL = "https://www.openslr.org/resources/12/dev-clean.tar.gz"
5757
case "dev-other":

0 commit comments

Comments
 (0)