ensureModel method

Future<String> ensureModel(
  1. SyniModelSpec spec, {
  2. required void onProgress(
    1. SyniInstallStage,
    2. double
    ),
})

Returns the path to the model file. Downloads the GGUF and its sibling tokenizer.json if not already on disk. Calls onProgress with the bytes-downloaded fraction.

The candle backend resolves the tokenizer as a tokenizer.json sibling of the GGUF, so both land in the same directory.

Implementation

Future<String> ensureModel(
  SyniModelSpec spec, {
  required void Function(SyniInstallStage, double) onProgress,
}) async {
  final dir = await _modelsDir();
  final path = '${dir.path}/${spec.filename}';
  final file = File(path);

  if (!file.existsSync()) {
    onProgress(SyniInstallStage.downloadingModel, 0.0);
    await _download(spec.downloadUrl, file, onProgress: (p) {
      // Reserve the last 5% of the download bar for the tokenizer.
      onProgress(SyniInstallStage.downloadingModel, p * 0.95);
    });
  }

  // tokenizer.json must sit next to the GGUF for the candle backend.
  final tokenizerFile = File('${dir.path}/tokenizer.json');
  if (!tokenizerFile.existsSync()) {
    await _download(spec.tokenizerUrl, tokenizerFile, onProgress: (p) {
      onProgress(SyniInstallStage.downloadingModel, 0.95 + p * 0.05);
    });
  }
  onProgress(SyniInstallStage.downloadingModel, 1.0);

  if (spec.sha256.isNotEmpty) {
    onProgress(SyniInstallStage.verifyingModel, 0.0);
    final actual = await _sha256(file);
    onProgress(SyniInstallStage.verifyingModel, 1.0);
    if (actual.toLowerCase() != spec.sha256.toLowerCase()) {
      // Discard a possibly-corrupted file so the next attempt re-downloads.
      try {
        file.deleteSync();
      } catch (_) {}
      throw SyniInstallException(
        'model SHA-256 mismatch: expected ${spec.sha256}, got $actual',
      );
    }
  }

  return path;
}