refactor(app): 模型不再打包进 APK,移除内置资产回退逻辑(版本 1.0.5+6)

- 移除 pubspec 的 model.tflite/labels.txt 资产与 DetectorWorker 内置回退
- 无下载模型时仅预览;modelsLabel 改为未下载
- 未打包 release APK(77.4MB,不含模型)
This commit is contained in:
2026-08-31 15:57:58 +08:00
parent 0b381f78ba
commit 6d7ba3b6ab
9 changed files with 22 additions and 47 deletions
+5 -5
View File
@@ -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 地址),或直接真机验证。
+1 -1
View File
@@ -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');
+1 -1
View File
@@ -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;
+6 -27
View File
@@ -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;
+4 -4
View File
@@ -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(',');
}
+1 -3
View File
@@ -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