refactor(app): 模型不再打包进 APK,移除内置资产回退逻辑(版本 1.0.5+6)
- 移除 pubspec 的 model.tflite/labels.txt 资产与 DetectorWorker 内置回退 - 无下载模型时仅预览;modelsLabel 改为未下载 - 未打包 release APK(77.4MB,不含模型)
This commit is contained in:
@@ -33,8 +33,8 @@ xcrun devicectl device process launch --console --terminate-existing \
|
||||
`models` 目录(公开接口,无需登录),与 `UpdateChecker` 的 APK 检查并行。
|
||||
|
||||
- **目录条目**:`{datasetId, datasetName, version, labels[], sizeBytes, sha256,
|
||||
downloadUrl}`;服务器未发布模型时不返回 `models` 字段,App 回退内置
|
||||
`assets/model.tflite`(标签 `内置`)。
|
||||
downloadUrl}`;服务器未发布模型时不返回 `models` 字段,App 无模型可用,
|
||||
相机页仅预览不识别。
|
||||
- **存储**:应用私有目录 `models/<datasetId>/`,含 `model.tflite`、
|
||||
`labels.json`、`meta.json`(meta 记录 `{version, sha256}`)。版本与摘要都
|
||||
未变化时跳过下载;变化则下载到 `.part` 临时文件、sha256 校验通过后
|
||||
@@ -43,15 +43,15 @@ xcrun devicectl device process launch --console --terminate-existing \
|
||||
- **并行推理合并**:识别时加载全部已下载模型(`DetectorWorker` isolate 内
|
||||
逐模型加载,单个失败不影响其他),同帧各模型独立推理后按类别分组做
|
||||
**跨模型 NMS**(同类别不同模型检出同一目标取高分去重,不同类别互不压制),
|
||||
结果叠加 `modelName` 标注来源,内置模型兜底。
|
||||
结果叠加 `modelName` 标注来源。
|
||||
|
||||
实现:`lib/models/model_manager.dart`(下载/校验/持久化,`ModelManager`
|
||||
单例 + ChangeNotifier)、`lib/detection/detector_worker.dart`(多模型并行
|
||||
推理与 `mergeAcrossModels`)、`lib/camera/camera_screen.dart`(启动同步 +
|
||||
诊断行展示模型列表)。
|
||||
|
||||
- **模型输入是 NHWC**:`assets/model.tflite` 做过字节级手术(开头 TRANSPOSE→RESHAPE,
|
||||
输入 [1,320,320,3]),改动记录见 git 历史,重导模型需同步处理,否则 iOS 报
|
||||
- **模型输入是 NHWC**:训练导出的模型需做字节级手术(开头 TRANSPOSE→RESHAPE,
|
||||
输入 [1,320,320,3])再发布给 App,否则 iOS 报
|
||||
"Node number 0 (TRANSPOSE) failed to prepare"。
|
||||
- **模拟器黑屏**:本机 iOS 模拟器 Impeller 渲染黑屏,验证 UI 用 VM service
|
||||
(`flutter run` 输出里的 DevTools 地址),或直接真机验证。
|
||||
|
||||
@@ -95,7 +95,7 @@ class _StartupGateState extends State<StartupGate> {
|
||||
final token = await session.readToken();
|
||||
if (!mounted) return;
|
||||
// 模型热更新:后台拉取模型目录并下载/更新各数据集模型(不阻塞启动,
|
||||
// 相机页打开前若未就绪会兜底等待;下载失败回退内置资产模型)
|
||||
// 相机页打开前若未就绪会兜底等待;未下载模型时仅预览)
|
||||
ModelManager.instance.refresh();
|
||||
Navigator.of(context)
|
||||
.pushReplacementNamed(token == null ? '/login' : '/home');
|
||||
|
||||
@@ -121,7 +121,7 @@ class _CameraScreenState extends State<CameraScreen> {
|
||||
if (!granted) return;
|
||||
|
||||
// 模型热更新:优先使用已下载的数据集模型(启动时后台拉取;此处兜底等待,
|
||||
// 下载慢/失败不阻塞相机启动——无下载模型时 worker 回退内置资产)
|
||||
// 下载慢/失败不阻塞相机启动——无下载模型时仅预览)
|
||||
if (!ModelManager.instance.ready) {
|
||||
try {
|
||||
await ModelManager.instance
|
||||
|
||||
@@ -87,9 +87,9 @@ class _OverlayPainter extends CustomPainter {
|
||||
_drawDashedRect(canvas, box, paint);
|
||||
}
|
||||
|
||||
// 标签:框上方,含距离;多模型时标注来源模型名(内置资产不标)
|
||||
// 标签:框上方,含距离;多模型时标注来源模型名
|
||||
final dist = _distanceLabel(r);
|
||||
final modelTag = r.modelName.isNotEmpty && r.modelName != '内置'
|
||||
final modelTag = r.modelName.isNotEmpty
|
||||
? '[${r.modelName}]'
|
||||
: '';
|
||||
final text =
|
||||
|
||||
@@ -9,7 +9,7 @@ class DetectionResult {
|
||||
/// 轨迹已确认(多帧稳定/高分/活动确认),false = 候选,渲染为虚线
|
||||
final bool confirmed;
|
||||
|
||||
/// 产出该框的模型(数据集 id 与名称;内置资产模型为 -1/空)
|
||||
/// 产出该框的模型(数据集 id 与名称;无来源为 -1/空)
|
||||
final int modelId;
|
||||
final String modelName;
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import 'dart:typed_data';
|
||||
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/foundation.dart' show debugPrint;
|
||||
import 'package:flutter/services.dart' show rootBundle;
|
||||
|
||||
import '../camera/motion_detector.dart';
|
||||
import '../models/model_manager.dart';
|
||||
@@ -19,15 +18,8 @@ import 'visual_prior.dart';
|
||||
///
|
||||
/// 多模型并行推理:传入 [models](各数据集下载模型)后,每帧逐模型推理,
|
||||
/// 结果按类别分组跨模型 NMS 合并(同标签重复框取高分,不同标签互不压制);
|
||||
/// 无下载模型时回退内置资产模型。
|
||||
/// 无下载模型时不启动推理(仅预览)。
|
||||
class DetectorWorker {
|
||||
static const String modelAsset = 'assets/model.tflite';
|
||||
static const String labelsAsset = 'assets/labels.txt';
|
||||
|
||||
/// 内置资产回退模型的标识
|
||||
static const int builtinModelId = -1;
|
||||
static const String builtinModelName = '内置';
|
||||
|
||||
final Isolate _isolate;
|
||||
final ReceivePort _responses;
|
||||
|
||||
@@ -64,26 +56,13 @@ class DetectorWorker {
|
||||
});
|
||||
}
|
||||
|
||||
/// 加载模型并启动后台推理 isolate;加载失败返回 null(App 降级为仅预览)。
|
||||
/// [models] 为空时回退内置资产模型(模型缺失同样返回 null)。
|
||||
/// 加载模型并启动后台推理 isolate;无模型或加载失败返回 null(App 降级为仅预览)。
|
||||
static Future<DetectorWorker?> create({List<ModelBundle>? models}) async {
|
||||
try {
|
||||
final payload = <List<Object?>>[];
|
||||
if (models != null && models.isNotEmpty) {
|
||||
for (final m in models) {
|
||||
payload.add(
|
||||
[m.bytes, m.labels, m.datasetId, m.datasetName]);
|
||||
}
|
||||
} else {
|
||||
final data = await rootBundle.load(modelAsset);
|
||||
final modelBytes =
|
||||
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
|
||||
final labels = (await rootBundle.loadString(labelsAsset))
|
||||
.split('\n')
|
||||
.where((l) => l.trim().isNotEmpty)
|
||||
.toList();
|
||||
payload.add([modelBytes, labels, builtinModelId, builtinModelName]);
|
||||
}
|
||||
if (models == null || models.isEmpty) return null;
|
||||
final payload = <List<Object?>>[
|
||||
for (final m in models) [m.bytes, m.labels, m.datasetId, m.datasetName],
|
||||
];
|
||||
|
||||
final responses = ReceivePort();
|
||||
final isolate = await Isolate.spawn(_workerMain, responses.sendPort);
|
||||
|
||||
@@ -19,8 +19,6 @@ class TfliteDetector {
|
||||
double minScore = 0.10;
|
||||
static const double iouThreshold = 0.45;
|
||||
static const int maxDetections = 20;
|
||||
static const String modelAsset = 'assets/model.tflite';
|
||||
static const String labelsAsset = 'assets/labels.txt';
|
||||
|
||||
final Interpreter _interpreter;
|
||||
final List<String> _labels;
|
||||
@@ -28,7 +26,7 @@ class TfliteDetector {
|
||||
final int _numAnchors;
|
||||
final int inputSize;
|
||||
|
||||
/// 模型身份(多模型并行推理区分来源;内置资产模型为 -1/空)
|
||||
/// 模型身份(多模型并行推理区分来源;无来源为 -1/空)
|
||||
final int modelId;
|
||||
final String modelName;
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ class ModelCatalogItem {
|
||||
);
|
||||
}
|
||||
|
||||
/// 已就绪模型(字节 + 标签,供推理 worker 加载;含内置资产回退模型)
|
||||
/// 已就绪模型(字节 + 标签,供推理 worker 加载)
|
||||
class ModelBundle {
|
||||
final int datasetId;
|
||||
final String datasetName;
|
||||
@@ -91,7 +91,7 @@ class ModelManager extends ChangeNotifier {
|
||||
_client = client ?? http.Client(),
|
||||
_rootDirOverride = rootDir;
|
||||
|
||||
/// 已就绪模型列表(空 = 无服务器模型,回退内置资产)
|
||||
/// 已就绪模型列表(空 = 未下载任何模型,相机页仅预览)
|
||||
List<ModelBundle> get models => _models;
|
||||
|
||||
/// 是否成功拉取过目录(即使下载失败也为 true,用于区分"从未联网"与"目录为空")
|
||||
@@ -102,9 +102,9 @@ class ModelManager extends ChangeNotifier {
|
||||
|
||||
bool get refreshing => _refreshing;
|
||||
|
||||
/// 模型名摘要(诊断行展示):内置 / 数据集名×n
|
||||
/// 模型名摘要(诊断行展示):未下载 / 数据集名×n
|
||||
String get modelsLabel {
|
||||
if (_models.isEmpty) return '内置';
|
||||
if (_models.isEmpty) return '未下载';
|
||||
return _models.map((m) => m.datasetName).join(',');
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ name: observer
|
||||
description: "视野 - 动物实时识别 (环颈雉鸡/生境), YOLOv8 + 充值付费"
|
||||
publish_to: 'none'
|
||||
|
||||
version: 1.0.4+5
|
||||
version: 1.0.5+6
|
||||
|
||||
environment:
|
||||
sdk: ^3.12.2
|
||||
@@ -52,6 +52,4 @@ dev_dependencies:
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
assets:
|
||||
- assets/model.tflite
|
||||
- assets/labels.txt
|
||||
- assets/beep.wav
|
||||
|
||||
Reference in New Issue
Block a user