104 lines
6.2 KiB
Markdown
104 lines
6.2 KiB
Markdown
# Fish API
|
||
|
||
FastAPI 网关:分块接收 **SVO2**(FishMeasure)与 **MP4**(FishAction),在后台调用现有脚本;对外提供文档约定的 **GET** 轮询接口,并托管预览视频 URL。
|
||
|
||
## 依赖环境
|
||
|
||
- Python 3.11+(uv 可管理 3.13 等版本)
|
||
- **FishMeasure**:需与本机 `FishMeasure/` 一致的环境(ZED SDK、`pyzed`、CUDA、YOLO/SAM 权重等)
|
||
- **FishAction**:需可运行 `FishAction/predict_video_x3d_3class.py`(PyTorch、PyTorchVideo、checkpoint)
|
||
|
||
若 FishMeasure 与 FishAction 使用不同虚拟环境,可设置:
|
||
|
||
- `PYTHON_FISH_MEASURE` — 运行 `predict_weigth_from_svo2.py` 的解释器路径
|
||
- `PYTHON_FISH_ACTION` — 运行 `predict_video_x3d_3class.py` 的解释器路径
|
||
|
||
**单一 Conda 环境**:若 FishMeasure 与 FishAction 已与网关停在同一个 env(例如仓库根目录 [`packaging/conda-fishserver.yaml`](../packaging/conda-fishserver.yaml) 定义的 `fishserver`),则**不要**设置上述两个变量,子进程会使用当前 `uvicorn` 的 Python。可用 [`scripts/start_fresh.sh`](../scripts/start_fresh.sh)(清空后启动)或 [`scripts/start_no_fresh.sh`](../scripts/start_no_fresh.sh)(保留缓存)启动。
|
||
|
||
## 配置(环境变量)
|
||
|
||
| 变量 | 说明 | 默认 |
|
||
|------|------|------|
|
||
| `PUBLIC_BASE_URL` | 返回 JSON 中 `video_left` / `video_right` 的前缀(勿带末尾 `/`) | `http://127.0.0.1:8000` |
|
||
| `INGEST_API_KEY` | 非空时,`/api/v1/ingest/*` 需请求头 `X-API-Key` | 空(不校验) |
|
||
| `STREAM_TMP_DIR` | 分块上传临时目录 | `<repo>/fish_api/.data/ingest` |
|
||
| `MEDIA_ROOT` | 对外托管每次测量生成的 `*_left.mp4` / `*_right.mp4` | `<repo>/fish_api/.data/media` |
|
||
| `FISH_MEASURE_ROOT` | `FishMeasure` 根目录 | 自动相对仓库 |
|
||
| `FISH_ACTION_ROOT` | `FishAction` 根目录 | 自动相对仓库 |
|
||
| `MEASURE_OUTPUT_ROOT` | 传给 `--save-output` 的目录 | `<repo>/fish_api/.data/measure_output` |
|
||
| `YOLO_MODEL` / `WEIGHT_CHECKPOINT` / `ACTION_CHECKPOINT` | 模型路径 | 与仓库内脚本默认一致 |
|
||
| `SAM_DEVICE` | `cuda` 或 `cpu` | `cuda` |
|
||
可在 `fish_api/.env` 中填写上述变量(`pydantic-settings` 会读取)。
|
||
|
||
## 安装与启动
|
||
|
||
```bash
|
||
cd fish_api
|
||
uv sync
|
||
# 可选:包含 httpx,便于本地用 FastAPI TestClient 做冒烟测试
|
||
# uv sync --group dev
|
||
bash start_fresh.sh # 默认仅重置 client_id 投递进度,保留 SQLite 历史与快照
|
||
# CLEAR_SQLITE_DATABASE=1 bash start_fresh.sh # 需要时才彻底清 SQLite
|
||
# 或:uv run uvicorn app.main:app --host 0.0.0.0 --port 8000(需自行 prestart)
|
||
```
|
||
|
||
OpenAPI:`http://127.0.0.1:8000/docs`
|
||
|
||
## 对外 GET(由其它系统轮询)
|
||
|
||
- `GET /api/v1/biomass/real/camera/` — 每次 GET 消费**该客户端**下一条未投递的称重快照(SQLite **按客户端独立游标**);无新数据时 `data.result` 为空,响应头 `X-Fish-Biomass-New: 0`
|
||
- `GET /api/v1/biomass/health/result/` — 同上,行为 / 健康快照队列
|
||
|
||
**客户端区分**:请求头 `X-Fish-Client-Id: <字符串>` 或查询参数 `client_id=<字符串>`(**优先头**)。未携带时等价于 `default`,与旧版「全局一条游标」行为一致。不同 `client_id` 各自从当前队列消费,互不影响。
|
||
|
||
`result` 中每条鱼含算法字段:`id`(跟踪 ID)、`weight`(g)、`length`(mm)。不可交付或失败的推理不会进入客户端队列。
|
||
|
||
## 流式输入(分块上传)
|
||
|
||
1. `POST /api/v1/ingest/svo/session` 或 `POST /api/v1/ingest/mp4/session` → `session_id`
|
||
2. `PUT /api/v1/ingest/{svo|mp4}/session/{session_id}?offset=0`(多次)
|
||
- **追加语义**:`offset` 必须等于当前已写入字节数(从 0 开始顺序上传)
|
||
3. `POST /api/v1/ingest/{svo|mp4}/session/{session_id}/finalize` → `202 Accepted`,后台开始跑对应算法
|
||
|
||
示例(小文件一次性,`$API_KEY` 可选):
|
||
|
||
```bash
|
||
BASE=http://127.0.0.1:8000
|
||
H=()
|
||
# H=(-H "X-API-Key: your-secret")
|
||
|
||
sid=$(curl -sS "${H[@]}" -X POST "$BASE/api/v1/ingest/svo/session" | jq -r .session_id)
|
||
curl -sS "${H[@]}" -T sample.svo2 -X PUT "$BASE/api/v1/ingest/svo/session/$sid?offset=0"
|
||
curl -sS "${H[@]}" -X POST "$BASE/api/v1/ingest/svo/session/$sid/finalize"
|
||
curl -sS "$BASE/api/v1/biomass/real/camera/"
|
||
```
|
||
|
||
MP4 将 `svo` 换成 `mp4`,本地文件换成 `clip.mp4`,轮询 `GET /api/v1/biomass/health/result/`。
|
||
|
||
**说明**:`curl -T` 发送 PUT 时 offset 为 0 且一次性传完整文件适合单块场景;多块时请自行递增 `offset`。
|
||
|
||
## 行为与健康映射
|
||
|
||
- X3D 输出 `feeding` / `normal` / `scared` → 中文 **吃饵** / **正常游行** / **惊吓**
|
||
- 健康:`scared` → **不健康**,其余 → **健康**(启发式,可后续换专用模型)
|
||
|
||
## 视频 URL
|
||
|
||
FishMeasure 跑完后在输出目录查找 `*preview*.mp4`,复制到 `MEDIA_ROOT/`,文件名为 `{UTC时间戳}_{svo_stem}_left.mp4` / `_right.mp4`(每次测量不覆盖;仅一个预览文件时可能左右 URL 指向同一逻辑源经 SBS 拆分)。确保 `PUBLIC_BASE_URL` 与前端/文档中的域名端口一致。
|
||
|
||
## Weight Rule (Current)
|
||
|
||
最终体重 `pred_weight_g` 由以下规则链决定(按优先级从高到低):
|
||
|
||
1. **440g 全池均值保护**(规则 B):若 `avg_g_filtered`(所有 candidates 均值)> `--mean-pool-fallback-max-if-over-g`(默认 440g),则 `pred_weight_g = max_predicted_weight_g_after_filter`,`pred_weight_rule = "max_after_filter_high_mean_pool_over_g"`。
|
||
2. **400g mean-all fallback**(规则 A,仅 `--average-all-after-filter` 开启时):若全池 mean > `--average-all-fallback-max-if-mean-over-g`(默认 400g),则 `pred_weight_g = max_predicted_weight_g_after_filter`,`pred_weight_rule = "max_after_filter_high_mean_all"`。
|
||
3. **`--average-all-after-filter`**(默认关):全部 candidates 均值作为最终值,`pred_weight_rule = "mean_all_filtered"`。
|
||
4. **Top-K 聚合**(默认路径):按 `--top-by-length`(默认开)选 top-K 帧,candidates < 5 用 max 否则用 mean,`pred_weight_rule = "top_k_aggregate"`。
|
||
|
||
DGCNN 明细中同时输出 `mean_all_pred_g_after_filters`、`avg_topk_mean_pred_g` 等供对比参考。
|
||
|
||
## 演进建议
|
||
|
||
- RTSP:用 `ffmpeg` 切段写入 MP4 后调用现有 `finalize` 逻辑
|
||
- 任务状态:`finalize` 返回 `job_id`,增加 `GET /jobs/{id}` 查询进度
|