1
This commit is contained in:
@@ -9,11 +9,20 @@ import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../config/app_config.dart';
|
||||
|
||||
/// 识别档位标识:s = 高识别(@1280 精度优先,默认),n = 高性能(@704 速度优先)
|
||||
const String kVariantS = 's';
|
||||
const String kVariantN = 'n';
|
||||
|
||||
/// 模型身份键:同一数据集不同档位是两个独立条目(下载/激活/记账互不影响)
|
||||
typedef ModelKey = ({int datasetId, String variant});
|
||||
|
||||
/// 模型目录条目(GET /api/v1/app/update 响应 data.models[])。
|
||||
/// 服务器发布模型后随版本检查一同下发,App 按目录逐数据集下载/更新。
|
||||
/// 双档位(2026-09-03):每数据集至多 2 条(s/n 各一),[variant] 标识档位。
|
||||
class ModelCatalogItem {
|
||||
final int datasetId;
|
||||
final String datasetName;
|
||||
final String variant;
|
||||
final String version;
|
||||
final List<String> labels;
|
||||
final int sizeBytes;
|
||||
@@ -24,6 +33,7 @@ class ModelCatalogItem {
|
||||
const ModelCatalogItem({
|
||||
required this.datasetId,
|
||||
required this.datasetName,
|
||||
this.variant = kVariantS,
|
||||
required this.version,
|
||||
required this.labels,
|
||||
required this.sizeBytes,
|
||||
@@ -36,6 +46,8 @@ class ModelCatalogItem {
|
||||
ModelCatalogItem(
|
||||
datasetId: (j['datasetId'] as num?)?.toInt() ?? 0,
|
||||
datasetName: j['datasetName'] as String? ?? '',
|
||||
// 旧目录无 variant 字段(2026-09-03 前发布的单档 s)→ 归为 s
|
||||
variant: j['variant'] as String? ?? kVariantS,
|
||||
version: j['version'] as String? ?? '',
|
||||
labels: (j['labels'] as List? ?? const [])
|
||||
.map((e) => e.toString())
|
||||
@@ -51,6 +63,7 @@ class ModelCatalogItem {
|
||||
class ModelBundle {
|
||||
final int datasetId;
|
||||
final String datasetName;
|
||||
final String variant;
|
||||
final String version;
|
||||
final List<String> labels;
|
||||
final Uint8List bytes;
|
||||
@@ -58,6 +71,7 @@ class ModelBundle {
|
||||
const ModelBundle({
|
||||
required this.datasetId,
|
||||
required this.datasetName,
|
||||
required this.variant,
|
||||
required this.version,
|
||||
required this.labels,
|
||||
required this.bytes,
|
||||
@@ -67,8 +81,12 @@ class ModelBundle {
|
||||
/// 模型热更新管理:启动时拉取模型目录(随 /app/update 公开接口下发,无需登录态),
|
||||
/// 按需下载/校验/持久化各数据集模型,供相机页多模型并行推理。
|
||||
///
|
||||
/// 存储:应用私有目录 `models/<datasetId>/`(model.tflite + labels.json + meta.json),
|
||||
/// meta 记录 {version, sha256},服务器发布新版本时按版本+摘要重下,不重复下载旧模型。
|
||||
/// 双档位存储(2026-09-03):`models/<datasetId>/` 存放 s 档(legacy 布局,目录键 =
|
||||
/// 档位标识符的「无子目录」形态,存量设备无需迁移),n 档存 `models/<datasetId>/n/`;
|
||||
/// 各目录含 model.tflite + labels.json + meta.json,meta 记录 {version, sha256},
|
||||
/// 版本与摘要都未变化时跳过下载。记账键一律是 (datasetId, variant) 二元组。
|
||||
/// 激活集与识别档位无关(跨档保持);推理加载「当前档位」下全部激活模型,
|
||||
/// 切档即热加载新档位已激活模型([mode] 持久化本地)。
|
||||
class ModelManager extends ChangeNotifier {
|
||||
static final ModelManager instance = ModelManager._();
|
||||
|
||||
@@ -78,11 +96,13 @@ class ModelManager extends ChangeNotifier {
|
||||
|
||||
List<ModelBundle> _models = const [];
|
||||
List<ModelCatalogItem> _catalog = const [];
|
||||
final Set<int> _activeIds = {};
|
||||
final Set<int> _downloadedIds = {};
|
||||
final Map<int, double> _progress = {};
|
||||
final Map<int, String> _errors = {};
|
||||
final Set<int> _cancelRequested = {};
|
||||
final Set<ModelKey> _active = {};
|
||||
final Set<ModelKey> _downloaded = {};
|
||||
final Map<ModelKey, double> _progress = {};
|
||||
final Map<ModelKey, String> _errors = {};
|
||||
final Set<ModelKey> _cancelRequested = {};
|
||||
String _mode = kVariantS;
|
||||
bool _modeLoaded = false;
|
||||
bool _activeLoaded = false;
|
||||
bool _ready = false;
|
||||
bool _refreshing = false;
|
||||
@@ -94,27 +114,31 @@ class ModelManager extends ChangeNotifier {
|
||||
int _revision = 0;
|
||||
int get revision => _revision;
|
||||
|
||||
/// 服务器目录(弹层模型清单展示用)
|
||||
/// 服务器目录(弹层模型清单展示用;同一数据集可能 s/n 两行)
|
||||
List<ModelCatalogItem> get catalog => _catalog;
|
||||
|
||||
/// 激活模型 id 集合(多选叠加)
|
||||
Set<int> get activeDatasetIds => Set.unmodifiable(_activeIds);
|
||||
/// 当前识别档位(s 高识别默认 / n 高性能),持久化,切档即热加载新档位模型
|
||||
String get mode => _mode;
|
||||
|
||||
bool isActive(int datasetId) => _activeIds.contains(datasetId);
|
||||
bool isActive(int datasetId, String variant) =>
|
||||
_active.contains((datasetId: datasetId, variant: variant));
|
||||
|
||||
/// 该数据集模型文件是否已下载到本地(同步判断,内存态)
|
||||
bool isDownloaded(int datasetId) => _downloadedIds.contains(datasetId);
|
||||
/// 该 (数据集, 档位) 模型文件是否已下载到本地(同步判断,内存态)
|
||||
bool isDownloaded(int datasetId, String variant) =>
|
||||
_downloaded.contains((datasetId: datasetId, variant: variant));
|
||||
|
||||
/// 下载进度 0..1(无下载/已完成为 null)
|
||||
double? progressOf(int datasetId) => _progress[datasetId];
|
||||
double? progressOf(int datasetId, String variant) =>
|
||||
_progress[(datasetId: datasetId, variant: variant)];
|
||||
|
||||
/// 下载失败原因(失败后可重试)
|
||||
String? errorOf(int datasetId) => _errors[datasetId];
|
||||
String? errorOf(int datasetId, String variant) =>
|
||||
_errors[(datasetId: datasetId, variant: variant)];
|
||||
|
||||
/// 中断进行中的下载:下一个数据块到达时终止(丢弃 .part),卡片恢复「使用」。
|
||||
/// 取消不记错误,可再次下载。
|
||||
void cancelDownload(int datasetId) {
|
||||
_cancelRequested.add(datasetId);
|
||||
void cancelDownload(int datasetId, String variant) {
|
||||
_cancelRequested.add((datasetId: datasetId, variant: variant));
|
||||
}
|
||||
|
||||
ModelManager._({String? baseUrl, http.Client? client})
|
||||
@@ -130,7 +154,7 @@ class ModelManager extends ChangeNotifier {
|
||||
_client = client ?? http.Client(),
|
||||
_rootDirOverride = rootDir;
|
||||
|
||||
/// 已激活且已下载的模型列表(空 = 未下载任何模型,相机页仅预览)
|
||||
/// 已激活且已下载、且属于当前档位的模型列表(空 = 未加载任何模型,仅预览)
|
||||
List<ModelBundle> get models => _models;
|
||||
|
||||
/// 是否成功拉取过目录(即使下载失败也为 true,用于区分"从未联网"与"目录为空")
|
||||
@@ -141,10 +165,21 @@ class ModelManager extends ChangeNotifier {
|
||||
|
||||
bool get refreshing => _refreshing;
|
||||
|
||||
/// 模型名摘要(诊断行展示):未下载 / 数据集名×n
|
||||
/// 模型名摘要(诊断行展示):数据集名+档位
|
||||
String get modelsLabel {
|
||||
if (_models.isEmpty) return '未下载';
|
||||
return _models.map((m) => m.datasetName).join(',');
|
||||
return _models.map((m) => '${m.datasetName}(${m.variant})').join(',');
|
||||
}
|
||||
|
||||
/// 切换识别档位:持久化本地并热加载该档位已激活模型(激活集跨档保留)
|
||||
Future<void> setMode(String variant) async {
|
||||
if (variant != kVariantS && variant != kVariantN) return;
|
||||
if (_mode == variant) return;
|
||||
_mode = variant;
|
||||
await _saveMode();
|
||||
_models = await _loadBundles(_catalog);
|
||||
_revision++;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 拉取目录并同步本地模型;并发调用共享同一进行中的刷新。
|
||||
@@ -162,6 +197,7 @@ class ModelManager extends ChangeNotifier {
|
||||
Future<void> _doRefresh() async {
|
||||
try {
|
||||
await _loadActive();
|
||||
await _loadMode();
|
||||
final res = await _client
|
||||
.get(Uri.parse('$baseUrl/api/v1/app/update'))
|
||||
.timeout(const Duration(seconds: 30));
|
||||
@@ -175,19 +211,26 @@ class ModelManager extends ChangeNotifier {
|
||||
.toList();
|
||||
|
||||
// 只拉目录不下载;扫描本地已下载(meta+文件齐备)供清单展示
|
||||
final downloaded = <int>{};
|
||||
final downloaded = <ModelKey>{};
|
||||
for (final item in _catalog) {
|
||||
if (await _isLocal(item)) downloaded.add(item.datasetId);
|
||||
if (await _isLocal(item)) {
|
||||
downloaded.add((datasetId: item.datasetId, variant: item.variant));
|
||||
}
|
||||
}
|
||||
_downloadedIds
|
||||
_downloaded
|
||||
..clear()
|
||||
..addAll(downloaded);
|
||||
|
||||
await _prune(_catalog);
|
||||
// 服务器已下线的数据集移出激活集
|
||||
final catalogIds = _catalog.map((c) => c.datasetId).toSet();
|
||||
if (_activeIds.any((id) => !catalogIds.contains(id))) {
|
||||
_activeIds.removeWhere((id) => !catalogIds.contains(id));
|
||||
// 服务器已下线的 (数据集, 档位) 移出激活集
|
||||
final catalogKeys = _catalog
|
||||
.map((c) => (datasetId: c.datasetId, variant: c.variant))
|
||||
.toSet();
|
||||
final pruned = _active.where((k) => !catalogKeys.contains(k)).toList();
|
||||
if (pruned.isNotEmpty) {
|
||||
for (final k in pruned) {
|
||||
_active.remove(k);
|
||||
}
|
||||
await _saveActive();
|
||||
}
|
||||
|
||||
@@ -204,7 +247,7 @@ class ModelManager extends ChangeNotifier {
|
||||
|
||||
/// 本地是否已有匹配版本的文件(meta 版本+sha256 相符且文件存在)
|
||||
Future<bool> _isLocal(ModelCatalogItem item) async {
|
||||
final dir = await _modelDir(item.datasetId);
|
||||
final dir = await _modelDir(item.datasetId, item.variant);
|
||||
try {
|
||||
final meta = await _readMeta(dir);
|
||||
final file = File('${dir.path}/model.tflite');
|
||||
@@ -218,72 +261,74 @@ class ModelManager extends ChangeNotifier {
|
||||
}
|
||||
|
||||
/// 按需下载并激活:流式下载 + sha256 校验 + 落盘(labels/meta);
|
||||
/// 成功自动加入激活集(下载完成即使用)。失败重试一次并记录错误。
|
||||
/// 成功自动加入激活集(下载完成即使用;非当前档位则备好待切档)。
|
||||
/// 失败重试一次并记录错误。
|
||||
Future<bool> downloadModel(ModelCatalogItem item,
|
||||
{void Function(int received, int total)? onProgress}) async {
|
||||
// 并发保护:同一数据集已有进行中的下载则直接短路(预置 0 先占位,
|
||||
// 使 onProgress 首次回调前的双击/refresh 交错也被 containsKey 拦下)
|
||||
if (_progress.containsKey(item.datasetId)) return false;
|
||||
_cancelRequested.remove(item.datasetId);
|
||||
_progress[item.datasetId] = 0;
|
||||
final dir = await _modelDir(item.datasetId);
|
||||
final key = (datasetId: item.datasetId, variant: item.variant);
|
||||
// 并发保护:同一 (数据集, 档位) 已有进行中的下载则直接短路
|
||||
if (_progress.containsKey(key)) return false;
|
||||
_cancelRequested.remove(key);
|
||||
_progress[key] = 0;
|
||||
final dir = await _modelDir(item.datasetId, item.variant);
|
||||
final file = File('${dir.path}/model.tflite');
|
||||
try {
|
||||
for (var attempt = 0; attempt < 2; attempt++) {
|
||||
if (_cancelRequested.contains(item.datasetId)) break;
|
||||
if (_cancelRequested.contains(key)) break;
|
||||
final ok = await _downloadAndVerify(item, dir, file,
|
||||
onProgress: (r, t) {
|
||||
_progress[item.datasetId] = t == 0 ? 0 : r / t;
|
||||
_progress[key] = t == 0 ? 0 : r / t;
|
||||
onProgress?.call(r, t);
|
||||
notifyListeners();
|
||||
});
|
||||
if (ok) {
|
||||
_progress.remove(item.datasetId);
|
||||
_errors.remove(item.datasetId);
|
||||
_downloadedIds.add(item.datasetId);
|
||||
_revision++;
|
||||
// 新版本字节立即生效:自动更新时已激活模型 setActive 会因状态未变
|
||||
// 提前返回,不在此重载则重建 worker 仍读到旧模型
|
||||
_progress.remove(key);
|
||||
_errors.remove(key);
|
||||
_downloaded.add(key);
|
||||
if (item.variant == _mode) {
|
||||
// 字节替换生效(当前档位模型更新需重建 worker 读新文件)
|
||||
_revision++;
|
||||
}
|
||||
_models = await _loadBundles(_catalog);
|
||||
notifyListeners();
|
||||
await setActive(item.datasetId, true);
|
||||
await setActive(item.datasetId, item.variant, true);
|
||||
return true;
|
||||
}
|
||||
if (_cancelRequested.contains(item.datasetId)) break;
|
||||
if (_cancelRequested.contains(key)) break;
|
||||
await file.delete().catchError((_) => file);
|
||||
await File('${dir.path}/model.tflite.part')
|
||||
.delete()
|
||||
.catchError((_) => file);
|
||||
}
|
||||
if (_cancelRequested.contains(item.datasetId)) {
|
||||
if (_cancelRequested.contains(key)) {
|
||||
// 用户取消:清理残留,不记错误
|
||||
await file.delete().catchError((_) => file);
|
||||
await File('${dir.path}/model.tflite.part')
|
||||
.delete()
|
||||
.catchError((_) => file);
|
||||
_progress.remove(item.datasetId);
|
||||
_progress.remove(key);
|
||||
notifyListeners();
|
||||
debugPrint('[ModelManager] 下载已取消: ${item.datasetName}');
|
||||
return false;
|
||||
}
|
||||
_progress.remove(item.datasetId);
|
||||
_errors[item.datasetId] = '下载失败,请重试';
|
||||
_progress.remove(key);
|
||||
_errors[key] = '下载失败,请重试';
|
||||
notifyListeners();
|
||||
debugPrint('[ModelManager] 下载失败: ${item.datasetName} ${item.version}');
|
||||
return false;
|
||||
} catch (e) {
|
||||
if (_cancelRequested.contains(item.datasetId)) {
|
||||
if (_cancelRequested.contains(key)) {
|
||||
await file.delete().catchError((_) => file);
|
||||
await File('${dir.path}/model.tflite.part')
|
||||
.delete()
|
||||
.catchError((_) => file);
|
||||
_progress.remove(item.datasetId);
|
||||
_progress.remove(key);
|
||||
notifyListeners();
|
||||
debugPrint('[ModelManager] 下载已取消: ${item.datasetName}');
|
||||
return false;
|
||||
}
|
||||
_progress.remove(item.datasetId);
|
||||
_errors[item.datasetId] = '下载异常:$e';
|
||||
_progress.remove(key);
|
||||
_errors[key] = '下载异常:$e';
|
||||
notifyListeners();
|
||||
debugPrint('[ModelManager] 下载异常 ${item.datasetName}: $e');
|
||||
return false;
|
||||
@@ -309,12 +354,16 @@ class ModelManager extends ChangeNotifier {
|
||||
final total = res.contentLength ?? item.sizeBytes;
|
||||
await for (final chunk
|
||||
in res.stream.timeout(const Duration(seconds: 30))) {
|
||||
if (_cancelRequested.contains(item.datasetId)) break; // 用户取消
|
||||
if (_cancelRequested.contains(
|
||||
(datasetId: item.datasetId, variant: item.variant))) {
|
||||
break; // 用户取消
|
||||
}
|
||||
sink.add(chunk);
|
||||
received += chunk.length;
|
||||
onProgress?.call(received, total);
|
||||
}
|
||||
if (_cancelRequested.contains(item.datasetId)) {
|
||||
if (_cancelRequested.contains(
|
||||
(datasetId: item.datasetId, variant: item.variant))) {
|
||||
await sink.close();
|
||||
return false;
|
||||
}
|
||||
@@ -334,7 +383,7 @@ class ModelManager extends ChangeNotifier {
|
||||
'version': item.version,
|
||||
'sha256': item.sha256,
|
||||
}));
|
||||
debugPrint('[ModelManager] 已下载 ${item.datasetName} '
|
||||
debugPrint('[ModelManager] 已下载 ${item.datasetName}(${item.variant}) '
|
||||
'${bytes.length}B -> ${file.path}');
|
||||
return true;
|
||||
} catch (e) {
|
||||
@@ -344,30 +393,47 @@ class ModelManager extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 清理服务器目录中已下线的数据集模型(不再发布则删本地)
|
||||
/// 清理本地目录:数据集整体下线(s/n 两档都无目录条目)删整目录;
|
||||
/// 数据集仍在但某档已下线时清该档子目录(s 档为同级文件,无独立目录,
|
||||
/// 残留文件不再被引用,仅占用磁盘,不做细粒度清除)。
|
||||
Future<void> _prune(List<ModelCatalogItem> catalog) async {
|
||||
final root = await _rootDir();
|
||||
if (!await root.exists()) return;
|
||||
final keep = catalog.map((c) => '${c.datasetId}').toSet();
|
||||
final dsIds = catalog.map((c) => c.datasetId).toSet();
|
||||
final nDsIds = catalog
|
||||
.where((c) => c.variant == kVariantN)
|
||||
.map((c) => c.datasetId)
|
||||
.toSet();
|
||||
await for (final e in root.list()) {
|
||||
if (e is Directory) {
|
||||
// 目录 URI 末尾带 '/',pathSegments 末位为空串 → 过滤后取目录名
|
||||
final name = e.uri.pathSegments.where((s) => s.isNotEmpty).last;
|
||||
if (!keep.contains(name)) {
|
||||
await e.delete(recursive: true).catchError((_) => e);
|
||||
if (e is! Directory) continue;
|
||||
// 目录 URI 末尾带 '/',pathSegments 末位为空串 → 过滤后取目录名
|
||||
final name = e.uri.pathSegments.where((s) => s.isNotEmpty).last;
|
||||
final dsId = int.tryParse(name);
|
||||
if (dsId == null) continue;
|
||||
if (!dsIds.contains(dsId)) {
|
||||
await e.delete(recursive: true).catchError((_) => e);
|
||||
continue;
|
||||
}
|
||||
if (!nDsIds.contains(dsId)) {
|
||||
final sub = Directory('${e.path}/$kVariantN');
|
||||
if (await sub.exists()) {
|
||||
await sub.delete(recursive: true).catchError((_) => e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置激活状态(true=使用,false=取消);持久化到 `root/active.json`。
|
||||
/// 激活跨档位保留(切档后仍生效);仅影响当前档位加载时才需重建推理 worker。
|
||||
/// 未下载的模型不可激活(下载完成由 downloadModel 自动激活)。
|
||||
Future<void> setActive(int datasetId, bool active) async {
|
||||
final changed =
|
||||
active ? _activeIds.add(datasetId) : _activeIds.remove(datasetId);
|
||||
Future<void> setActive(int datasetId, String variant, bool active) async {
|
||||
final key = (datasetId: datasetId, variant: variant);
|
||||
final changed = active ? _active.add(key) : _active.remove(key);
|
||||
if (!changed) return;
|
||||
_revision++;
|
||||
_models = await _loadBundles(_catalog);
|
||||
if (variant == _mode) {
|
||||
_revision++;
|
||||
_models = await _loadBundles(_catalog);
|
||||
}
|
||||
await _saveActive();
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -377,15 +443,20 @@ class ModelManager extends ChangeNotifier {
|
||||
/// 2026-09-01 用户需求:发布新模型后 App 端自动更新,无需手动触发。
|
||||
Future<void> autoUpdate() async {
|
||||
if (_catalog.isEmpty) return;
|
||||
final tracked = {..._downloadedIds, ..._activeIds};
|
||||
final tracked = {..._downloaded, ..._active};
|
||||
for (final item in _catalog) {
|
||||
if (!tracked.contains(item.datasetId)) continue;
|
||||
if (await _isLocal(item)) continue;
|
||||
final wasActive = _activeIds.contains(item.datasetId);
|
||||
final key = (datasetId: item.datasetId, variant: item.variant);
|
||||
if (!tracked.contains(key)) continue;
|
||||
final wasActive = _active.contains(key);
|
||||
try {
|
||||
// 后台 fire-and-forget:本地检查/下载都可能撞上存储变动(如清理),
|
||||
// 不得向外抛未处理异步异常
|
||||
if (await _isLocal(item)) continue;
|
||||
final ok = await downloadModel(item);
|
||||
// 原本未激活:下载完成自动激活后恢复原状态
|
||||
if (ok && !wasActive) await setActive(item.datasetId, false);
|
||||
if (ok && !wasActive) {
|
||||
await setActive(item.datasetId, item.variant, false);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[ModelManager] 自动更新失败: ${item.datasetName} $e');
|
||||
}
|
||||
@@ -396,13 +467,17 @@ class ModelManager extends ChangeNotifier {
|
||||
try {
|
||||
final root = await _rootDir();
|
||||
await root.create(recursive: true);
|
||||
await File('${root.path}/active.json')
|
||||
.writeAsString(jsonEncode({'active': _activeIds.toList()}));
|
||||
await File('${root.path}/active.json').writeAsString(jsonEncode({
|
||||
'active': [
|
||||
for (final k in _active) {'d': k.datasetId, 'v': k.variant}
|
||||
]
|
||||
}));
|
||||
} catch (e) {
|
||||
debugPrint('[ModelManager] 激活集持久化失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 读取激活集。兼容旧版纯 int 列表(双档位前只有 s 档,int 一律归为 s)。
|
||||
Future<void> _loadActive() async {
|
||||
if (_activeLoaded) return;
|
||||
_activeLoaded = true;
|
||||
@@ -411,22 +486,56 @@ class ModelManager extends ChangeNotifier {
|
||||
final f = File('${root.path}/active.json');
|
||||
if (!await f.exists()) return;
|
||||
final data = jsonDecode(await f.readAsString()) as Map<String, dynamic>;
|
||||
_activeIds
|
||||
..clear()
|
||||
..addAll((data['active'] as List? ?? const [])
|
||||
.map((e) => (e as num).toInt()));
|
||||
for (final e in data['active'] as List? ?? const []) {
|
||||
if (e is num) {
|
||||
_active.add((datasetId: e.toInt(), variant: kVariantS));
|
||||
} else if (e is Map) {
|
||||
final v = e['v'] as String? ?? kVariantS;
|
||||
final d = (e['d'] as num?)?.toInt();
|
||||
if (d != null && v != kVariantS && v != kVariantN) continue;
|
||||
if (d != null) _active.add((datasetId: d, variant: v));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[ModelManager] 激活集读取失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveMode() async {
|
||||
try {
|
||||
final root = await _rootDir();
|
||||
await root.create(recursive: true);
|
||||
await File('${root.path}/mode.json')
|
||||
.writeAsString(jsonEncode({'mode': _mode}));
|
||||
} catch (e) {
|
||||
debugPrint('[ModelManager] 识别档位持久化失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadMode() async {
|
||||
if (_modeLoaded) return;
|
||||
_modeLoaded = true;
|
||||
try {
|
||||
final root = await _rootDir();
|
||||
final f = File('${root.path}/mode.json');
|
||||
if (!await f.exists()) return;
|
||||
final data = jsonDecode(await f.readAsString()) as Map<String, dynamic>;
|
||||
final m = data['mode'] as String? ?? kVariantS;
|
||||
if (m == kVariantS || m == kVariantN) _mode = m;
|
||||
} catch (e) {
|
||||
debugPrint('[ModelManager] 识别档位读取失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<ModelBundle>> _loadBundles(
|
||||
List<ModelCatalogItem> catalog) async {
|
||||
final bundles = <ModelBundle>[];
|
||||
for (final item in catalog) {
|
||||
if (!_activeIds.contains(item.datasetId)) continue;
|
||||
if (item.variant != _mode) continue;
|
||||
final key = (datasetId: item.datasetId, variant: item.variant);
|
||||
if (!_active.contains(key)) continue;
|
||||
try {
|
||||
final dir = await _modelDir(item.datasetId);
|
||||
final dir = await _modelDir(item.datasetId, item.variant);
|
||||
final file = File('${dir.path}/model.tflite');
|
||||
if (!await file.exists()) continue;
|
||||
final labels = await File('${dir.path}/labels.json').exists()
|
||||
@@ -439,6 +548,7 @@ class ModelManager extends ChangeNotifier {
|
||||
bundles.add(ModelBundle(
|
||||
datasetId: item.datasetId,
|
||||
datasetName: item.datasetName,
|
||||
variant: item.variant,
|
||||
version: item.version,
|
||||
labels: labels,
|
||||
bytes: await file.readAsBytes(),
|
||||
@@ -466,9 +576,12 @@ class ModelManager extends ChangeNotifier {
|
||||
return Directory('${support.path}/models');
|
||||
}
|
||||
|
||||
Future<Directory> _modelDir(int datasetId) async {
|
||||
/// 档位目录:s 档存 `models/<datasetId>/`(legacy 无子目录,目录键 = 档位
|
||||
/// 标识符的 s 形态,存量设备零迁移);n 档存 `models/<datasetId>/n/`。
|
||||
Future<Directory> _modelDir(int datasetId, String variant) async {
|
||||
final root = await _rootDir();
|
||||
final dir = Directory('${root.path}/$datasetId');
|
||||
final sub = variant == kVariantS ? '' : '/$variant';
|
||||
final dir = Directory('${root.path}/$datasetId$sub');
|
||||
await dir.create(recursive: true);
|
||||
return dir;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user