diff --git a/fish_api/README.md b/fish_api/README.md index 2e6dc49..03e9b56 100644 --- a/fish_api/README.md +++ b/fish_api/README.md @@ -97,6 +97,27 @@ FishMeasure 跑完后在输出目录查找 `*preview*.mp4`,复制到 `MEDIA_RO DGCNN 明细中同时输出 `mean_all_pred_g_after_filters`、`avg_topk_mean_pred_g` 等供对比参考。 +## Debug:单条鱼测量(与 fish_api 同逻辑) + +不启动 uvicorn、**不写 SQLite**、**不发布** `MEDIA_ROOT`(与 `run_full_measure_batch` 相比仅少快照与媒体发布;FishMeasure 子进程与 `measure_output/fish{N}` 与线上一致)。 + +调用 [`app/services/measure.py`](app/services/measure.py) 中的 `run_measure_batch_subprocess`,配置与 `fish_api/.env` 相同(`get_settings()`)。 + +```bash +cd fish_api +uv sync +# 默认:MEASURE_WATCH_DIR/fish{N}/ 下所有 .svo2 → 输出到 MEASURE_OUTPUT_ROOT/fish{N}(默认 fish_api/.data/measure_output/fish{N}) +uv run python -m app.measure_debug_cli --fish-id 1 + +# 或等价入口 +uv run fish-measure-debug --fish-id 1 + +# 指定 SVO 目录或输出目录 +uv run python -m app.measure_debug_cli --batch-folder /path/to/fish1 --fish-id 1 --output-root /path/to/out +``` + +结束后会在终端打印 `weight_prediction.json` 中的 `pred_weight_g`、`pred_weight_rule` 等摘要。 + ## 演进建议 - RTSP:用 `ffmpeg` 切段写入 MP4 后调用现有 `finalize` 逻辑 diff --git a/fish_api/app/measure_debug_cli.py b/fish_api/app/measure_debug_cli.py new file mode 100644 index 0000000..ba703ac --- /dev/null +++ b/fish_api/app/measure_debug_cli.py @@ -0,0 +1,127 @@ +"""CLI:对单条 fish 跑与 fish_api / measure-watch 相同的 FishMeasure 子进程(不写 SQLite、不发布 /media)。 + +使用 ``run_measure_batch_subprocess``,与 ``run_full_measure_batch`` 中的推理步骤一致。 +""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path +from typing import Any, Dict, List + +from app.services.measure import run_measure_batch_subprocess +from app.settings import get_settings + + +def _sorted_svo2_in_folder(folder: Path) -> List[Path]: + """与 measure_watch.iter_svo2_folders 中单目录内 .svo2 列表一致(按文件名排序)。""" + return sorted( + [p for p in folder.iterdir() if p.is_file() and p.suffix.lower() == ".svo2"], + key=lambda p: p.name, + ) + + +def _print_weight_summary(output_root: Path) -> None: + combined = output_root / "weight_prediction.json" + if not combined.is_file(): + print(f"[measure-debug] no combined file: {combined}", file=sys.stderr) + return + try: + with open(combined, encoding="utf-8") as f: + data: Dict[str, Any] = json.load(f) + except (OSError, json.JSONDecodeError) as e: + print(f"[measure-debug] failed to read {combined}: {e}", file=sys.stderr) + return + + summary = data.get("dgcnn_summary") or data.get("weight_summary") or {} + pred_g = data.get("pred_weight_g") + rule = data.get("pred_weight_rule") + print("[measure-debug] --- weight_prediction.json (summary) ---") + if pred_g is not None: + print(f" pred_weight_g: {pred_g}") + if rule is not None: + print(f" pred_weight_rule: {rule}") + if summary: + top_k = summary.get("top_k") + tbl = summary.get("top_by_length") + print(f" dgcnn_summary.top_k: {top_k}") + print(f" dgcnn_summary.top_by_length: {tbl}") + mean_all = data.get("mean_all_pred_g_after_filters") + if mean_all is not None: + print(f" mean_all_pred_g_after_filters: {mean_all}") + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Run FishMeasure for one fish folder (same subprocess as fish_api)." + ) + parser.add_argument( + "--fish-id", + default="1", + help='Fish id used for output dir measure_output/fish{N} (default: 1).', + ) + parser.add_argument( + "--batch-folder", + type=Path, + default=None, + help="Folder containing .svo2 files (overrides MEASURE_WATCH_DIR/fish{N}).", + ) + parser.add_argument( + "--watch-dir", + type=Path, + default=None, + help="Override MEASURE_WATCH_DIR when resolving fish{N}/ (ignored if --batch-folder is set).", + ) + parser.add_argument( + "--output-root", + type=Path, + default=None, + help="Override save-output directory (default: MEASURE_OUTPUT_ROOT/fish{N}).", + ) + args = parser.parse_args() + + settings = get_settings() + fish_id = str(args.fish_id).strip() or "1" + + if args.batch_folder is not None: + batch_folder = args.batch_folder.expanduser().resolve() + else: + watch = args.watch_dir if args.watch_dir is not None else settings.measure_watch_dir + if watch is None: + print( + "未配置输入目录:请设置 --batch-folder,或在 .env 中设置 MEASURE_WATCH_DIR", + file=sys.stderr, + ) + raise SystemExit(2) + watch = watch.expanduser().resolve() + batch_folder = watch / f"fish{fish_id}" + + if not batch_folder.is_dir(): + print(f"不是目录或不存在: {batch_folder}", file=sys.stderr) + raise SystemExit(2) + + svo_paths = _sorted_svo2_in_folder(batch_folder) + if not svo_paths: + print(f"目录下无 .svo2 文件: {batch_folder}", file=sys.stderr) + raise SystemExit(2) + + if args.output_root is not None: + out_root = args.output_root.expanduser().resolve() + else: + out_root = settings.measure_output_root / f"fish{fish_id}" + out_root.mkdir(parents=True, exist_ok=True) + + print(f"[measure-debug] batch_folder={batch_folder}") + print(f"[measure-debug] output_root={out_root}") + print(f"[measure-debug] {len(svo_paths)} SVO(s): {', '.join(p.name for p in svo_paths)}") + + run_measure_batch_subprocess(svo_paths, settings, output_root=out_root) + + _print_weight_summary(out_root) + print("[measure-debug] done.") + + +if __name__ == "__main__": + main() diff --git a/fish_api/pyproject.toml b/fish_api/pyproject.toml index 68f0c76..85d19a2 100644 --- a/fish_api/pyproject.toml +++ b/fish_api/pyproject.toml @@ -16,3 +16,4 @@ dev = ["httpx>=0.28.1"] [project.scripts] fish-action-watch = "app.action_watch_cli:main" +fish-measure-debug = "app.measure_debug_cli:main"