Skip to content

Instantly share code, notes, and snippets.

@jpohhhh
Created February 8, 2025 22:46
Show Gist options
  • Save jpohhhh/fe0145a0bb9b3d35cb6eb8191ca62ec8 to your computer and use it in GitHub Desktop.
Save jpohhhh/fe0145a0bb9b3d35cb6eb8191ca62ec8 to your computer and use it in GitHub Desktop.
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