Files
2026-09-10 21:40:31 +08:00

126 lines
8.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# observer
动物实时识别 AppFlutter 版)。Android / iOS 一套代码,后端接口与支付见
[`docs/PaymentApi.md`](docs/PaymentApi.md)。
## Android 打包
```bash
./build_apk.sh # 用当前 pubspec 版本构建,产物 build/app/outputs/flutter-apk/observer-x.y.z.apk
./build_apk.sh --bump # 自动递增 patch+1、build+1 后构建
./build_apk.sh 1.0.7 # 用指定版本(versionName)构建,build+1
```
产物命名 `observer-x.y.z.apk`(管理端上传版本号从文件名识别);构建的
`app-release.apk` 中间产物由脚本清理,只保留规范命名文件。
## iOS 真机部署(iPhone
### 构建与安装
```bash
# 生产包直接构建即可:默认 API_BASE_URL 为线上域名(lib/config/app_config.dart),无需传参
flutter build ios --release
# 仅本地联调(后端跑在 Mac 上、手机连同一 Wi-Fi)时才覆盖为 Mac 局域网 IP:
# flutter build ios --release --dart-define=API_BASE_URL=http://<Mac局域网IP>:8080
# 安装到真机(UDID 可用 `xcrun devicectl list devices` 查询)
xcrun devicectl device install app --device <UDID> build/ios/iphoneos/Runner.app
# 启动并抓控制台日志(--terminate-existing 先杀掉旧实例)
xcrun devicectl device process launch --console --terminate-existing \
--device <UDID> com.observer.app
```
### 注意事项(踩过的坑)
- **debug 构建不能在真机上从桌面图标启动**:iOS 14+ 会提示
"In iOS 14+, debug mode Flutter apps can only be launched from Flutter tooling"。
debug 调试必须用 `flutter run -d <设备ID>` 或 Xcode IDE 启动(`flutter devices` 查设备ID);
从图标启动只对 release 构建有效。
- **`flutter run` 真机 debug 附接失败(errno 49,多次复现)**:Xcode 构建、安装、
启动都成功,attach 阶段报 `OS Error: Can't assign requested address, errno = 49`
工具即退出——与本机 VPN(utun 隧道)环境相关,断 VPN 后可恢复。
需要真机验证时一律用上方 release + devicectl 流程(不依赖 attach);只有需要
热重载/看 debugPrint 才用 `flutter run`,遇 errno 49 先断 VPN 重试。
## 端侧推理加速(GPU / CoreML
识别慢的根因是 yolov8s@1280 推理量大(CPU 4 线程约每秒不到 1 帧),
`TfliteDetector.fromBuffer` 加载模型时按平台挂加速 delegate,均为**浮点计算不降精度**
(区别于 int8 量化掉点):
| 平台 | delegate | 说明 |
|---|---|---|
| Android | `GpuDelegateV2` | TFLite GPU delegate;依赖 `libtensorflowlite_gpu_jni.so`,已 vendor 到 `android/app/src/main/jniLibs/arm64-v8a/`AAR 因 AGP 9 namespace 冲突保持排除,升级 tflite_flutter 时需同步换 .so,版本对齐 base 2.11.0 |
| iOS | `CoreMlDelegate` | Core ML(苹果 ANE/GPU,插件 pod 自带 TensorFlowLiteSwift/CoreML,无需额外依赖) |
delegate 初始化失败(老设备/驱动/符号缺失)**自动回退纯 CPU 4 线程**,最后才返回 null
(仅预览不识别)。生效与否看日志:加载模型时输出
`[TfliteDetector] 加速生效 model=xxx (CoreML|GPU)`,回退输出 `回退 CPU` 及原因。
GPU delegate 默认允许 FP16 计算(YOLO 类精度损失可忽略);如需全精度改为传
`GpuDelegateOptionsV2(isPrecisionLossAllowed: false)`
## 模型热更新(多数据集模型)
模型与 APK 更新走**独立通道**:进入视野页时后台同步 `GET /api/v1/app/update`
随附的 `models` 目录(公开接口,无需登录;不阻塞相机预览启动),与
`UpdateChecker` 的 APK 检查并行。
- **目录条目**`{datasetId, datasetName, variant, version, labels[], sizeBytes, sha256,
downloadUrl, coverUrl}`——**双档位(2026-09-03)**:每数据集至多 2 条 = 高精度 s
(@1280 精度优先,默认)+ 高性能 n(@704 速度优先)各自的当前版本,条目带 `variant`
(s/n);服务器未发布模型时不返回 `models` 字段,App 无模型可用,
相机页仅预览不识别。
- **下载入口**:相机页设置弹层「模型清单」按需下载/使用(封面缩略图 2 列网格,
**同一物种合并一张卡**,卡上**不显示版本号、也不设档位状态行**
2026-09-10 删除「高精度/高性能 + 版本」灰字行,版本仅作内部记账,
档位状态看主按钮);点
「下载」一次性补下该动物高精度+高性能两个已训练文件——各档独立进度逐条展示、
可取消(已下过的档不重复下),**目标档落地即自动启用、伴档备好**;目标档已
启用显示「使用中」点击取消使用。弹层高度上限 70% 屏高(防盖满全屏无法关闭)。
**档位(2026-09-03 修订)**:同一物种一次只运行一个档位——激活某档会自动停用
同物种另一档,**不同物种可用不同档位并行识别**(如雉鸡用高性能、斑鸠用高精度)。
弹层顶部「识别模式」分段控件(高性能 / 高精度,默认高精度,持久化本地;
2026-09-03 高性能移到左位)是**目标档偏好**:不直接切换运行中的模型,
只决定卡片按钮面向哪个档。卡片主按钮(2026-09-03 简化:不再出现「改用X」、
不再显示当前运行档提示):目标档未下载 →「下载」(落地自动启用、伴档备好;
同一动物另一档在使用时补下目标档后自动切换过去);已下载未启用 →「使用」
点击即用(另一档在使用会被自动停用);启用中 →「使用中」点击取消使用。
- **存储**:应用私有目录 `models/<datasetId>/<variant>/`(双档位 2026-09-03
原无 variant 目录与存量 s 档一致——s 档复用 `models/<datasetId>/` 同级读取,
目录键 = 档位标识符),含 `model.tflite`、`labels.json`、`meta.json`
meta 记录 `{version, sha256}`)。版本与摘要都未变化时跳过下载;变化则下载到
`.part` 临时文件、sha256 校验通过后原子 rename 替换,失败重试一次并保留旧模型,
下次启动再试——检查记账按 `(datasetId, variant)` 独立。
- **目录缓存(2026-09-03)**:最近一次成功拉取的模型目录落盘应用私有目录
`catalog.json`;每次刷新先载入缓存、展示立即可用(弹层离线/弱网也有内容),
网络成功后再以权威目录覆盖缓存。清理与自动更新只在网络拉取成功时执行:
离线降级不清文件、不触发下载——升级后断网首启不会误删已下载模型。
- **清理**:服务器下线的数据集下次同步时删除本地对应目录(两档都无条目时才删)。
- **识别会话制(2026-09-03 修订)**:每次进入视野页即开始新识别会话——从
**仅预览**开始、**不自动恢复上次使用的模型**(上次崩溃/坏模型不会在下一次打开时
自动复现,用户总能进入模型清单调整;这也是打开即慢的根因消除项:进入页面不再
读模型/重建推理器)。识别需在模型清单**手动启用**,启用状态只在本会话内生效
(不跨会话持久化);已下载文件常驻本地,「使用」即点即用不重新下载。
服务器新版本仅对**已下载文件**后台自动补齐,补齐不改变启用状态。
- **并行推理合并**:识别时加载**本会话已启用**的全部模型(激活集即实际运行集,
会话内每数据集至多一个档位;`DetectorWorker` isolate 内逐模型加载,单个失败
不影响其他;worker 构建完成后原地挂到已启动的相机流,不重启相机),
同帧各模型独立推理后做**跨模型 NMS**(所有模型的框统一按 IoU 去重取高分——
异类别重叠也压制,实测多个模型会对同一目标检出不同类别),结果叠加 `modelName`
(数据集名)标注来源。
实现:`lib/models/model_manager.dart`(下载/校验/清单/会话制单档激活收敛,
`ModelManager` 单例 + ChangeNotifier,条目身份含档位)、
`lib/detection/detector_worker.dart`(多模型并行推理与 `mergeAcrossModels`)、
`lib/camera/camera_screen.dart`(会话重置从仅预览开始 + 先出预览后台同步 +
设置弹层「识别模式」目标档切换 + 底部状态胶囊:识别延迟)、
`lib/camera/model_catalog_section.dart`(清单网格:按数据集合并卡片、
双档下载与「使用/使用中/下载」主按钮动作,2026-09-03 简化文案)。
- **模型输入是 NHWC**:训练导出的模型需做字节级手术(开头 TRANSPOSE→RESHAPE
输入 [1,320,320,3])再发布给 App,否则 iOS 报
"Node number 0 (TRANSPOSE) failed to prepare"。
- **模拟器黑屏**:本机 iOS 模拟器 Impeller 渲染黑屏,验证 UI 用 VM service
`flutter run` 输出里的 DevTools 地址),或直接真机验证。