Created
February 8, 2025 22:46
-
-
Save jpohhhh/fe0145a0bb9b3d35cb6eb8191ca62ec8 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:async'; | |
import 'dart:io'; | |
import 'package:background_downloader/background_downloader.dart'; | |
import 'package:dio/dio.dart'; | |
import 'package:dio/io.dart'; | |
import 'package:easy_debounce/easy_throttle.dart'; | |
import 'package:hooks_riverpod/hooks_riverpod.dart'; | |
import 'package:loggy/loggy.dart'; | |
import 'package:path/path.dart' as p; | |
import 'package:telosnex/extensions/is_web.dart'; | |
import 'package:telosnex/misc/hugging_face.dart'; | |
import 'package:telosnex/models/ai_model_view_model.dart'; | |
import 'package:telosnex/models/ai_model_view_model_web.dart'; | |
import 'package:telosnex/models/download_state.dart'; | |
import 'package:telosnex/persistence/path_providers.dart'; | |
import 'package:telosnex/providers/ai_model_download.dart'; | |
import 'package:telosnex/widgets/patrol_keys.dart'; | |
extension Download on AiModelViewModel { | |
Future<void> download(ProviderContainer ref, String filename) async { | |
@pragma('vm:awaiter-link') | |
final completer = Completer<void>(); | |
final url = getHuggingFaceUrl( | |
repoId: repoId, | |
filename: filename, | |
revision: 'main', | |
); | |
final path = p.join( | |
isWeb ? '' : PathProvider.documentsDirectory, | |
'models', | |
filename, | |
); | |
final startTime = DateTime.now(); | |
final dlManager = ref.read(aiModelDownloadProvider.notifier); | |
if (P.isTesting && !P.isIntegrationTesting) { | |
// Fake the download for testing. | |
// The below code can't work in unit tests. | |
// - web requires web APis for storage | |
// - native uses background_downloader package and doesn't support | |
// running async. | |
final newState = DownloadState( | |
currentProgress: 500, | |
totalSize: 1000, | |
startTime: startTime, | |
); | |
@pragma('vm:awaiter-link') | |
final updateCompleter = Completer<void>(); | |
// ignore: avoid_print | |
print( | |
'[AiModelViewModel._download] waiting 1 second for fake downloading model update for $path', | |
); | |
EasyThrottle.throttle(filename, const Duration(seconds: 1), () { | |
dlManager.update(this, newState); | |
updateCompleter.complete(); | |
}); | |
await updateCompleter.future; | |
// ignore: avoid_print | |
print( | |
'[AiModelViewModel._download] waiting 3 seconds for fake downloading model complete for $path', | |
); | |
await Future.delayed(const Duration(seconds: 3)); | |
final file = File(path); | |
file.createSync(recursive: true); | |
file.writeAsBytesSync(List.generate(1000, (index) => index)); | |
EasyThrottle.cancel(filename); | |
dlManager.update( | |
this, | |
DownloadState( | |
currentProgress: 1, | |
totalSize: 1, | |
startTime: startTime, | |
), | |
); | |
ref.invalidate(modelFileExistsProvider(path)); | |
logInfo('[AiModelViewModel._download] fake downloaded model: $path'); | |
completer.complete(); | |
return; | |
} | |
if (isWeb) { | |
logInfo('[AiModelViewModel._download] isWeb, downloading model: $url'); | |
doWebDownload(ref, completer); | |
} else if (Platform.isWindows) { | |
logInfo( | |
'[AiModelViewModel._download] isWindows, downloading model: $url'); | |
// Needed to work around strange error with SSL cert x huggingface seen on | |
// Framework laptop running Windows 11. Background downloader _may_ also | |
// work if we can figure out how to override bad cert when host is HF. | |
try { | |
final file = File(path); | |
if (file.existsSync()) { | |
logInfo( | |
'[AiModelViewModel._download] model already exists @ $path. Invalidating provider.', | |
); | |
ref.invalidate(modelFileExistsProvider(path)); | |
return; | |
} | |
// Note: Can't use Sentry dio here, it breaks the logging that lets us | |
// know ex. it was a bad cert. | |
final dio = Dio(); | |
// Avoid Error: HandshakeException: Handshake error in client (OS Error: | |
// CERTIFICATE_VERIFY_FAILED: unable to get local issuer certificate(../../flutter/third_party/boringssl/src/ssl/handshake.cc:393))) | |
(dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () => | |
HttpClient() | |
..badCertificateCallback = | |
(X509Certificate cert, String host, int port) { | |
// huggingface.co was added initially, sometime Q2-Q3 2024 | |
// cdn-lfs.hf.co added 23-10-2024 | |
if (host.endsWith('huggingface.co') || host.endsWith('hf.co')) { | |
return true; | |
} | |
logError( | |
'[AiModelViewModel._download] Bad certificate for $host:$port. Cert: $cert', | |
); | |
return false; | |
}; | |
unawaited(dio.download( | |
url, | |
path, | |
onReceiveProgress: (count, total) { | |
final newState = DownloadState( | |
currentProgress: count, | |
totalSize: total, | |
startTime: startTime, | |
); | |
EasyThrottle.throttle( | |
this.filename, | |
const Duration(seconds: 1), | |
() { | |
dlManager.update(this, newState); | |
}, | |
); | |
if (count == total) { | |
EasyThrottle.cancel(filename); | |
dlManager.update(this, newState); | |
} | |
}, | |
).then((value) { | |
ref.invalidate(modelFileExistsProvider(path)); | |
logInfo('[AiModelViewModel._download] downloaded model: $path'); | |
completer.complete(); | |
}).onError((error, stackTrace) { | |
logError( | |
'[AiModelViewModel._download] Error downloading model: $error', | |
error, | |
stackTrace, | |
); | |
completer.complete(); | |
})); | |
} catch (e, s) { | |
logError('Failed to download model: $filename. URL: $url', e, s); | |
completer.completeError( | |
'Failed to download model: $filename. URL: $url', | |
s, | |
); | |
} | |
} else { | |
logInfo( | |
'[AiModelViewModel._download] using DownloadTask to download66 model: $url'); | |
final file = File(path); | |
if (file.existsSync()) { | |
logInfo( | |
'[AiModelViewModel._download] model already exists @ $path. Invalidating provider.', | |
); | |
ref.invalidate(modelFileExistsProvider(path)); | |
return; | |
} | |
// define the task | |
final task = DownloadTask( | |
url: url, | |
directory: p.dirname(path), // must be a directory path (no filename | |
filename: p.basename(path), | |
baseDirectory: BaseDirectory.root, | |
updates: Updates.statusAndProgress, | |
allowPause: | |
true, // ensures Android can progress on DLs past 8-10 minutes. See https://github.com/781flyingdutchman/background_downloader/issues/13 | |
); | |
// enqueue the download | |
var size = await task.expectedFileSize(); | |
if (size < 0) { | |
completer.completeError( | |
'Invalid file size for $filename: $size. Most likely, the model download link is invalid. URL: $url', | |
StackTrace.current, | |
); | |
return completer.future; | |
} | |
// An `await` here causes method to never return. Observed on Windows when | |
// it thinks (erroneously) that the URL does not exist, then calls status | |
// callback with failed, which then attempted to call completer.completeError. | |
// The caller never received the error. | |
unawaited(FileDownloader().download( | |
task, | |
onProgress: (progress) { | |
if (size <= 0) { | |
loggy.debug('Progress: $progress but no size. URL: $url'); | |
return; | |
} | |
final count = (progress * size).clamp(0.0, size.toDouble()).floor(); | |
final newState = DownloadState( | |
currentProgress: count, | |
totalSize: size, | |
startTime: startTime, | |
); | |
EasyThrottle.throttle(filename, const Duration(seconds: 1), () { | |
dlManager.update(this, newState); | |
}); | |
}, | |
onStatus: (update) { | |
logInfo('[AiModelViewModel._download] status update: $update'); | |
switch (update) { | |
case TaskStatus.enqueued: | |
case TaskStatus.running: | |
case TaskStatus.canceled: | |
case TaskStatus.waitingToRetry: | |
case TaskStatus.paused: | |
break; | |
case TaskStatus.notFound: | |
logError( | |
'Failed to download model, not found: $filename. URL: $url'); | |
EasyThrottle.cancel(filename); | |
completer.completeError( | |
'Failed to download model: $filename. URL: $url', | |
StackTrace.current, | |
); | |
break; | |
case TaskStatus.complete: | |
EasyThrottle.cancel(filename); | |
dlManager.update( | |
this, | |
DownloadState( | |
currentProgress: size, | |
totalSize: size, | |
startTime: startTime, | |
), | |
); | |
ref.invalidate(modelFileExistsProvider(path)); | |
logInfo('[AiModelViewModel._download] downloaded model: $path'); | |
completer.complete(); | |
break; | |
case TaskStatus.failed: | |
EasyThrottle.cancel(filename); | |
completer.completeError( | |
'Failed to download model: $filename. URL: $url', | |
StackTrace.current, | |
); | |
break; | |
} | |
}, | |
)); | |
loggy.debug('[AiModelViewModel._download] await download returned'); | |
} | |
return completer.future; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment