feat(fish_api): SQLite 快照投递、日志与 watch 空闲告警

- 新增 SQLite:measure/health 快照、delivery_cursor 单消费者 pop;clear/start_fresh 可清空库
- biomass GET 仅返回约定 data 字段,X-Fish-Biomass-New 表示是否有新快照;poller 读响应头
- loguru 桥接 uvicorn,子进程 stdout 流式输出;format_json_pretty 与算法摘要日志
- measure/action watch 无新任务时限流 WARNING;watch_idle 共用逻辑
- 依赖 loguru;新增 db、logging_config、subprocess_run、watch_idle、启动脚本

FishMeasure: 更新 fish_video_weight_evaluation 与 predict_weigth_from_svo2;移除未用 refbox/segmentation 脚本
Made-with: Cursor
This commit is contained in:
zaiun xu
2026-04-09 11:54:30 +08:00
parent db181d4f84
commit 5e1b2117c1
29 changed files with 1464 additions and 1714 deletions

View File

@@ -7,16 +7,61 @@ import subprocess
import sys
from datetime import date, datetime, timezone
from pathlib import Path
from typing import Any, Dict, Optional, Tuple
from typing import Any, Dict, List, Optional, Tuple
from app.logging_config import format_json_pretty
from app.settings import Settings
from app.state import MeasureSnapshot
from app.subprocess_run import run_subprocess_with_log
from loguru import logger
def _py_exe(settings: Settings) -> str:
return settings.python_fish_measure or sys.executable
def _predict_weigth_from_svo2_extra_args(settings: Settings) -> List[str]:
"""Flags aligned with FishMeasure/predict_weigth_from_svo2.py CLI."""
out: List[str] = []
if settings.predict_filter_pointcloud:
out.append("--filter-pointcloud")
if settings.predict_use_density_filter:
out.append("--use-density-filter")
if settings.predict_use_clustering_filter:
out.append("--use-clustering-filter")
if (
settings.predict_use_pointcloud_classifier
and settings.predict_pointcloud_classifier
and Path(settings.predict_pointcloud_classifier).is_file()
):
out.extend(
[
"--pointcloud-classifier",
settings.predict_pointcloud_classifier,
"--use-pointcloud-classifier",
"--pointcloud-classifier-threshold",
str(settings.predict_pointcloud_classifier_threshold),
]
)
if settings.predict_use_flatness_filter:
out.append("--use-flatness-filter")
out.extend(["--flatness-threshold", str(settings.predict_flatness_threshold)])
out.extend(["--weight-top-k", str(settings.measure_weight_top_k)])
if settings.measure_weight_top_by_length:
out.append("--weight-top-by-length")
else:
out.append("--no-weight-top-by-length")
if settings.predict_fish_video_weight_overlay:
out.extend(
[
"--fish-video-weight-overlay",
"--minute-interval-sec",
str(settings.predict_minute_interval_sec),
]
)
return out
def run_measure_subprocess(svo_path: Path, settings: Settings) -> None:
script = settings.fish_measure_root / "predict_weigth_from_svo2.py"
if not script.is_file():
@@ -46,16 +91,16 @@ def run_measure_subprocess(svo_path: Path, settings: Settings) -> None:
"--frame-stride",
str(settings.predict_frame_stride),
]
cmd.extend(_predict_weigth_from_svo2_extra_args(settings))
proc = subprocess.run(
proc = run_subprocess_with_log(
cmd,
cwd=str(settings.fish_measure_root),
env=os.environ.copy(),
capture_output=True,
text=True,
log_name="FishMeasure",
)
if proc.returncode != 0:
err = (proc.stderr or "") + (proc.stdout or "")
err = proc.stdout or ""
raise RuntimeError(
f"predict_weigth_from_svo2.py failed ({proc.returncode}): {err[-4000:]}"
)
@@ -171,9 +216,28 @@ def build_measure_snapshot(svo_path: Path, settings: Settings) -> MeasureSnapsho
"date": today,
}
logger.info(
"[FishMeasure] parsed {}\navg_length_mm={} avg_weight_g={}\nweight_summary:\n{}",
svo_path.name,
length_mm,
weight_g,
format_json_pretty(summary if summary else {}),
)
logger.info(
"[FishMeasure] API result_item:\n{}",
format_json_pretty(result_item),
)
out_dir = Path(data.get("output_dir", settings.measure_output_root / svo_path.stem))
lv, rv = _find_preview_videos(out_dir)
v_left, v_right = _publish_media(lv, rv, settings)
logger.info(
"[FishMeasure] media preview_paths={} {} | published_left={} published_right={}",
lv,
rv,
v_left or "(none)",
v_right or "(none)",
)
return MeasureSnapshot(
result=[result_item],
@@ -187,5 +251,8 @@ def build_measure_snapshot(svo_path: Path, settings: Settings) -> MeasureSnapsho
def run_full_measure(svo_path: Path, settings: Settings) -> MeasureSnapshot:
logger.info("[FishMeasure] start svo={}", svo_path.resolve())
run_measure_subprocess(svo_path, settings)
return build_measure_snapshot(svo_path, settings)
snap = build_measure_snapshot(svo_path, settings)
logger.info("[FishMeasure] done svo={} result_len={}", svo_path.name, len(snap.result))
return snap