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

@@ -102,6 +102,19 @@ def _run_fish_video_evaluation_subprocess(args: argparse.Namespace, *, batch_fol
cmd.append("--use-flatness-filter")
cmd.extend(["--flatness-threshold", str(args.flatness_threshold)])
if getattr(args, "fish_video_weight_overlay", False):
wck = Path(args.weight_checkpoint).expanduser().resolve()
cmd.extend(
[
"--run-weight-estimation",
"--weight-estimator-checkpoint",
str(wck),
"--weight-overlay-video",
"--minute-interval-sec",
str(getattr(args, "minute_interval_sec", 60.0)),
]
)
print(f"Invoking fish_video_weight_evaluation.py:\n {' '.join(cmd)}")
proc = subprocess.run(cmd, cwd=str(REPO_ROOT))
if proc.returncode != 0:
@@ -277,6 +290,7 @@ def run_weight_prediction_for_svo(
weight_outlier_method: str,
weight_xyz_scale: float,
weight_labels_json: Optional[str],
force_dgcnn_subprocess: bool = False,
) -> Dict[str, Any]:
svo_path = svo_path.expanduser().resolve()
if not svo_path.exists():
@@ -293,6 +307,28 @@ def run_weight_prediction_for_svo(
f"fish_video_weight_evaluation.py first."
)
fish_wj = out_dir / "weight_estimation" / "weight_estimation_results.json"
if not force_dgcnn_subprocess and fish_wj.is_file():
try:
dgcnn_data = json.loads(fish_wj.read_text(encoding="utf-8"))
if dgcnn_data.get("summary") is not None or dgcnn_data.get("per_file"):
print(f"Using existing DGCNN results from fish_video: {fish_wj}")
result = _merge_weight_prediction_json(
svo_path=svo_path,
svo_name=svo_name,
out_dir=out_dir,
cloud_dir=cloud_dir,
dgcnn_json_path=fish_wj,
dgcnn_data=dgcnn_data,
)
(out_dir / "weight_prediction.json").write_text(
json.dumps(_sanitize_for_json(result), indent=2, ensure_ascii=False),
encoding="utf-8",
)
return result
except Exception as e:
print(f"WARNING: Could not merge {fish_wj}, falling back to test_dgcnn subprocess: {e}")
dgcnn_path, dgcnn_data = _run_test_dgcnn_weight_estimator_subprocess(
cloud_dir=cloud_dir,
ply_list_file=None,
@@ -468,6 +504,24 @@ def main() -> None:
)
parser.add_argument("--no-reuse-existing-clouds", action="store_false", dest="reuse_existing_clouds")
parser.add_argument(
"--fish-video-weight-overlay",
action="store_true",
help="Run fish_video with DGCNN + preview video overlay (fish weight g, top-5, per-window avg). "
"Avoids a duplicate test_dgcnn pass when weight_estimation_results.json is present.",
)
parser.add_argument(
"--minute-interval-sec",
type=float,
default=60.0,
help="Passed to fish_video --minute-interval-sec for on-video bucket stats (default: 60).",
)
parser.add_argument(
"--force-dgcnn-subprocess",
action="store_true",
help="Always run test_dgcnn_weight_estimator.py subprocess even if fish_video left weight_estimation_results.json.",
)
args = parser.parse_args()
if args.frame_stride < 1:
raise SystemExit("--frame-stride must be >= 1")
@@ -551,6 +605,7 @@ def main() -> None:
weight_outlier_method=args.weight_outlier_method,
weight_xyz_scale=args.weight_xyz_scale,
weight_labels_json=args.weight_labels_json,
force_dgcnn_subprocess=args.force_dgcnn_subprocess,
)
)
except Exception as e: