@@ -18,6 +18,168 @@ public class DownloadUtils {
1818 return URLSession ( configuration: configuration)
1919 } ( )
2020
21+ private static let huggingFaceUserAgent = " FluidAudio/1.0 (HuggingFaceDownloader) "
22+
23+ public enum HuggingFaceDownloadError : LocalizedError {
24+ case invalidResponse
25+ case rateLimited( statusCode: Int , message: String )
26+ case unexpectedContent( statusCode: Int , mimeType: String ? , snippet: String )
27+
28+ public var errorDescription : String ? {
29+ switch self {
30+ case . invalidResponse:
31+ return " Received an invalid response from Hugging Face. "
32+ case . rateLimited( _, let message) :
33+ return " Hugging Face rate limit encountered: \( message) "
34+ case . unexpectedContent( _, let mimeType, let snippet) :
35+ let mimeInfo = mimeType ?? " unknown MIME type "
36+ return " Unexpected Hugging Face content ( \( mimeInfo) ): \( snippet) "
37+ }
38+ }
39+ }
40+
41+ private static func huggingFaceToken( ) -> String ? {
42+ let env = ProcessInfo . processInfo. environment
43+ return env [ " HF_TOKEN " ]
44+ ?? env [ " HUGGINGFACEHUB_API_TOKEN " ]
45+ ?? env [ " HUGGING_FACE_HUB_TOKEN " ]
46+ }
47+
48+ private static func isLikelyHtml( _ data: Data ) -> Bool {
49+ guard !data. isEmpty,
50+ let prefix = String ( data: data. prefix ( 128 ) , encoding: . utf8) ?
51+ . trimmingCharacters ( in: . whitespacesAndNewlines)
52+ . lowercased ( )
53+ else {
54+ return false
55+ }
56+
57+ return prefix. hasPrefix ( " <!doctype html " ) || prefix. hasPrefix ( " <html " )
58+ }
59+
60+ private static func makeHuggingFaceRequest( for url: URL ) -> URLRequest {
61+ var request = URLRequest ( url: url)
62+ request. httpMethod = " GET "
63+ request. setValue ( huggingFaceUserAgent, forHTTPHeaderField: " User-Agent " )
64+ request. setValue ( " application/octet-stream " , forHTTPHeaderField: " Accept " )
65+ request. timeoutInterval = DownloadConfig . default. timeout
66+
67+ if let token = huggingFaceToken ( ) {
68+ request. setValue ( " Bearer \( token) " , forHTTPHeaderField: " Authorization " )
69+ }
70+
71+ return request
72+ }
73+
74+ public static func fetchHuggingFaceFile(
75+ from url: URL ,
76+ description: String ,
77+ maxAttempts: Int = 4 ,
78+ minBackoff: TimeInterval = 1.0
79+ ) async throws -> Data {
80+ var lastError : Error ?
81+
82+ for attempt in 1 ... maxAttempts {
83+ do {
84+ let request = makeHuggingFaceRequest ( for: url)
85+ let ( data, response) = try await sharedSession. data ( for: request)
86+
87+ guard let httpResponse = response as? HTTPURLResponse else {
88+ throw HuggingFaceDownloadError . invalidResponse
89+ }
90+
91+ if httpResponse. statusCode == 429 || httpResponse. statusCode == 503 {
92+ let message = " HTTP \( httpResponse. statusCode) "
93+ throw HuggingFaceDownloadError . rateLimited (
94+ statusCode: httpResponse. statusCode, message: message)
95+ }
96+
97+ if let mimeType = httpResponse. mimeType? . lowercased ( ) ,
98+ mimeType == " text/html "
99+ {
100+ let snippet = String ( data: data. prefix ( 256 ) , encoding: . utf8) ?? " "
101+ throw HuggingFaceDownloadError . unexpectedContent (
102+ statusCode: httpResponse. statusCode,
103+ mimeType: mimeType,
104+ snippet: snippet
105+ )
106+ }
107+
108+ if isLikelyHtml ( data) {
109+ let snippet = String ( data: data. prefix ( 256 ) , encoding: . utf8) ?? " "
110+ throw HuggingFaceDownloadError . unexpectedContent (
111+ statusCode: httpResponse. statusCode,
112+ mimeType: httpResponse. mimeType,
113+ snippet: snippet
114+ )
115+ }
116+
117+ return data
118+
119+ } catch let error as HuggingFaceDownloadError {
120+ lastError = error
121+
122+ if attempt == maxAttempts {
123+ break
124+ }
125+
126+ let backoffSeconds = pow ( 2.0 , Double ( attempt - 1 ) ) * minBackoff
127+ let backoffNanoseconds = UInt64 ( backoffSeconds * 1_000_000_000 )
128+ let formattedBackoff = String ( format: " %.1f " , backoffSeconds)
129+
130+ switch error {
131+ case . rateLimited( let statusCode, _) :
132+ if huggingFaceToken ( ) == nil {
133+ logger. warning (
134+ " Rate limit (HTTP \( statusCode) ) while downloading \( description) . "
135+ + " Set HF_TOKEN or HUGGINGFACEHUB_API_TOKEN for higher limits. "
136+ + " Retrying in \( formattedBackoff) s. "
137+ )
138+ } else {
139+ logger. warning (
140+ " Rate limit (HTTP \( statusCode) ) while downloading \( description) . "
141+ + " Retrying in \( formattedBackoff) s. "
142+ )
143+ }
144+ case . unexpectedContent( _, _, let snippet) :
145+ logger. warning (
146+ " Unexpected content while downloading \( description) . "
147+ + " Snippet: \( snippet. prefix ( 100 ) ) . "
148+ + " Retrying in \( formattedBackoff) s. "
149+ )
150+ case . invalidResponse:
151+ logger. warning (
152+ " Invalid response while downloading \( description) . "
153+ + " Retrying in \( formattedBackoff) s. "
154+ )
155+ }
156+
157+ try await Task . sleep ( nanoseconds: backoffNanoseconds)
158+
159+ } catch {
160+ lastError = error
161+
162+ if attempt == maxAttempts {
163+ break
164+ }
165+
166+ let backoffSeconds = pow ( 2.0 , Double ( attempt - 1 ) ) * minBackoff
167+ let backoffNanoseconds = UInt64 ( backoffSeconds * 1_000_000_000 )
168+ let formattedBackoff = String ( format: " %.1f " , backoffSeconds)
169+
170+ logger. warning (
171+ " Download attempt \( attempt) for \( description) failed: "
172+ + " \( error. localizedDescription) . "
173+ + " Retrying in \( formattedBackoff) s. "
174+ )
175+
176+ try await Task . sleep ( nanoseconds: backoffNanoseconds)
177+ }
178+ }
179+
180+ throw lastError ?? HuggingFaceDownloadError . invalidResponse
181+ }
182+
21183 private static func configureProxySettings( ) -> [ String : Any ] ? {
22184 #if os(macOS)
23185 var proxyConfig : [ String : Any ] = [ : ]
0 commit comments