frp
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -14,7 +14,8 @@ build/
|
||||
.ruff_cache/
|
||||
*.egg
|
||||
|
||||
# Large model weights (download or copy from backup)
|
||||
# Large model weights — 统一放仓库根目录 models/(见 fish_api 默认 settings)
|
||||
models/
|
||||
FishMeasure/sam_vit_h_4b8939.pth
|
||||
|
||||
# Local / runtime outputs (regenerate on server)
|
||||
@@ -41,3 +42,5 @@ mockdata/**
|
||||
# OS / IDE
|
||||
.DS_Store
|
||||
.cursor/
|
||||
|
||||
frp/
|
||||
@@ -1,13 +1,14 @@
|
||||
"""仅 FastAPI 进程使用 SQLite:落库测量/健康结果与 watch 已处理路径。
|
||||
|
||||
FishMeasure / FishAction 子进程不连接、不依赖本库;它们只读写各自文件(如 output 下
|
||||
FishMeasure / FishAction 子进程不连接、不依赖本库;它们只读写各自文件(如 measure_output 下
|
||||
weight_prediction.json、临时 pred.json 等),由 fish_api 在子进程结束后读文件并写入本表。
|
||||
视频仍使用 measure_output_root、media_root 等原路径。
|
||||
预览视频在 media_root;start_fresh 会清空 measure_output、media、ingest 临时目录。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import sqlite3
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
@@ -386,6 +387,30 @@ def add_watch_processed(settings: Settings, path: str, kind: str) -> None:
|
||||
conn.close()
|
||||
|
||||
|
||||
def clear_runtime_compute_dirs(settings: Settings) -> None:
|
||||
"""清空 FishMeasure / FishAction 运行时目录、托管预览、ingest 临时文件(保留目录本身)。
|
||||
|
||||
与 remove_sqlite_database_files 一起在启动脚本中调用,使两条流水线重启后均重新计算。
|
||||
"""
|
||||
for base in (
|
||||
settings.measure_output_root,
|
||||
settings.action_output_root,
|
||||
settings.media_root,
|
||||
settings.stream_tmp_dir,
|
||||
):
|
||||
p = Path(base).resolve()
|
||||
if not p.is_dir():
|
||||
continue
|
||||
for child in p.iterdir():
|
||||
try:
|
||||
if child.is_dir():
|
||||
shutil.rmtree(child)
|
||||
else:
|
||||
child.unlink()
|
||||
except OSError as e:
|
||||
print(f"[prestart-fresh] skip remove {child}: {e}", flush=True)
|
||||
|
||||
|
||||
def remove_sqlite_database_files(settings: Settings) -> None:
|
||||
"""删除 SQLite 主库及 WAL/SHM 副文件;不存在则忽略。下次 init_db 会重建空库。"""
|
||||
base = settings.sqlite_path.resolve()
|
||||
|
||||
67
fish_api/app/prestart_fresh.py
Normal file
67
fish_api/app/prestart_fresh.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""启动前清空状态:SQLite、watch 旧 JSON、测量/行为运行时目录。
|
||||
|
||||
由 start.sh / start_fresh.sh 在 uvicorn 之前调用,使 FishMeasure 与 FishAction 均在无缓存下重新推理。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from app.db import clear_runtime_compute_dirs, remove_sqlite_database_files
|
||||
from app.settings import get_settings
|
||||
|
||||
|
||||
def _rm_legacy_json(path: Path | None) -> None:
|
||||
if path is None:
|
||||
return
|
||||
try:
|
||||
if path.is_file():
|
||||
path.unlink()
|
||||
print(f"[prestart-fresh] removed legacy JSON {path}", flush=True)
|
||||
except OSError as e:
|
||||
print(f"[prestart-fresh] skip {path}: {e}", flush=True)
|
||||
|
||||
|
||||
def run_prestart_fresh() -> None:
|
||||
s = get_settings()
|
||||
|
||||
remove_sqlite_database_files(s)
|
||||
print(
|
||||
f"[prestart-fresh] removed SQLite at {s.sqlite_path} (and -wal/-shm if present).",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
clear_runtime_compute_dirs(s)
|
||||
print(
|
||||
"[prestart-fresh] cleared compute dirs: "
|
||||
f"measure_output={s.measure_output_root}, "
|
||||
f"action_output={s.action_output_root}, "
|
||||
f"media={s.media_root}, stream_tmp={s.stream_tmp_dir}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
if s.measure_watch_use_state_file:
|
||||
if s.measure_watch_state_file is not None:
|
||||
_rm_legacy_json(s.measure_watch_state_file)
|
||||
elif s.measure_watch_dir is not None:
|
||||
_rm_legacy_json(
|
||||
s.measure_watch_dir / ".fishmeasure_watch_processed.json"
|
||||
)
|
||||
|
||||
if s.action_watch_use_state_file:
|
||||
if s.action_watch_state_file is not None:
|
||||
_rm_legacy_json(s.action_watch_state_file)
|
||||
elif s.action_watch_dir is not None:
|
||||
_rm_legacy_json(
|
||||
s.action_watch_dir / ".fishaction_watch_processed.json"
|
||||
)
|
||||
|
||||
print("[prestart-fresh] done.", flush=True)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
run_prestart_fresh()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -13,6 +13,11 @@ def fish_repo_root() -> Path:
|
||||
return Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def models_dir() -> Path:
|
||||
"""仓库内统一权重目录(YOLO / DGCNN / PointNet / X3D / SAM 等),与 FishMeasure 代码目录解耦。"""
|
||||
return fish_repo_root() / "models"
|
||||
|
||||
|
||||
def _default_stream_tmp() -> Path:
|
||||
return fish_repo_root() / "fish_api" / ".data" / "ingest"
|
||||
|
||||
@@ -25,6 +30,10 @@ def _default_sqlite_path() -> Path:
|
||||
return fish_repo_root() / "fish_api" / ".data" / "app.db"
|
||||
|
||||
|
||||
def _default_action_output_root() -> Path:
|
||||
return fish_repo_root() / "fish_api" / ".data" / "action_output"
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
@@ -43,7 +52,10 @@ class Settings(BaseSettings):
|
||||
fish_measure_root: Path = fish_repo_root() / "FishMeasure"
|
||||
fish_action_root: Path = fish_repo_root() / "FishAction"
|
||||
|
||||
measure_output_root: Path = fish_repo_root() / "FishMeasure" / "output_weight_estimator"
|
||||
#: FishMeasure 推理输出(与 SQLite、媒体缓存同属 fish_api/.data,启动脚本会清空)
|
||||
measure_output_root: Path = fish_repo_root() / "fish_api" / ".data" / "measure_output"
|
||||
#: FishAction 侧预留目录(与 measure 对称;当前推理多为临时文件,仍随启动清空)
|
||||
action_output_root: Path = Field(default_factory=_default_action_output_root)
|
||||
|
||||
python_fish_measure: str = ""
|
||||
python_fish_action: str = ""
|
||||
@@ -114,41 +126,19 @@ class Settings(BaseSettings):
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _default_paths(self) -> "Settings":
|
||||
md = models_dir()
|
||||
if not self.yolo_model:
|
||||
object.__setattr__(
|
||||
self,
|
||||
"yolo_model",
|
||||
str(
|
||||
self.fish_measure_root
|
||||
/ "runs/train/fish_detection_20251127_104658/weights/best.pt"
|
||||
),
|
||||
)
|
||||
object.__setattr__(self, "yolo_model", str(md / "yolo" / "best.pt"))
|
||||
if not self.weight_checkpoint:
|
||||
object.__setattr__(
|
||||
self,
|
||||
"weight_checkpoint",
|
||||
str(
|
||||
self.fish_measure_root
|
||||
/ "weight_estimator/runs/dgcnn_20260312_171043/best.pt"
|
||||
),
|
||||
self, "weight_checkpoint", str(md / "weight_estimator" / "best.pt")
|
||||
)
|
||||
if not self.action_checkpoint:
|
||||
object.__setattr__(
|
||||
self,
|
||||
"action_checkpoint",
|
||||
str(self.fish_action_root / "checkpoints/ptv_x3d_m/checkpoint_best.pt"),
|
||||
self, "action_checkpoint", str(md / "action_x3d" / "checkpoint_best.pt")
|
||||
)
|
||||
if not self.predict_pointcloud_classifier:
|
||||
_pc = (
|
||||
self.fish_measure_root
|
||||
/ "pointcloud_classifier"
|
||||
/ "Pointnet_Pointnet2_pytorch"
|
||||
/ "log"
|
||||
/ "classification"
|
||||
/ "fish_pointnet2_finetune"
|
||||
/ "checkpoints"
|
||||
/ "best_model.pth"
|
||||
)
|
||||
_pc = md / "pointcloud" / "best_model.pth"
|
||||
if _pc.is_file():
|
||||
object.__setattr__(self, "predict_pointcloud_classifier", str(_pc))
|
||||
return self
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# 一键启动 Fish API(在 fish_api 目录下执行 uvicorn,读取同目录 .env)
|
||||
# 一键启动 Fish API:先清空 SQLite、watch 状态与运行时目录(与 start_fresh.sh 相同),再启动 uvicorn。
|
||||
#
|
||||
# bash fish_api/start.sh
|
||||
# PORT=8001 HOST=0.0.0.0 bash fish_api/start.sh
|
||||
@@ -14,6 +14,14 @@ cd "$DIR"
|
||||
export PUBLIC_BASE_URL="${PUBLIC_BASE_URL:-http://127.0.0.1:8000}"
|
||||
unset PYTHON_FISH_MEASURE PYTHON_FISH_ACTION 2>/dev/null || true
|
||||
|
||||
if command -v uv >/dev/null 2>&1; then
|
||||
PY=(uv run python)
|
||||
else
|
||||
PY=(python3)
|
||||
fi
|
||||
|
||||
"${PY[@]}" -m app.prestart_fresh
|
||||
|
||||
PORT="${PORT:-8000}"
|
||||
HOST="${HOST:-0.0.0.0}"
|
||||
|
||||
|
||||
@@ -1,69 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
# 一键启动 Fish API:删除整个 SQLite 库文件(含 -wal/-shm),并删除旧版 watch JSON 状态文件,再启动服务。
|
||||
# 与 start.sh 相同(历史名称保留):清空状态后启动 Fish API。
|
||||
#
|
||||
# bash fish_api/start_fresh.sh
|
||||
# PORT=8001 HOST=0.0.0.0 bash fish_api/start_fresh.sh
|
||||
#
|
||||
# 首次使用请先:cd fish_api && uv sync
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$DIR"
|
||||
|
||||
export PUBLIC_BASE_URL="${PUBLIC_BASE_URL:-http://127.0.0.1:8000}"
|
||||
unset PYTHON_FISH_MEASURE PYTHON_FISH_ACTION 2>/dev/null || true
|
||||
|
||||
if command -v uv >/dev/null 2>&1; then
|
||||
PY=(uv run python)
|
||||
else
|
||||
PY=(python3)
|
||||
fi
|
||||
|
||||
"${PY[@]}" - <<'PY'
|
||||
from pathlib import Path
|
||||
|
||||
from app.db import remove_sqlite_database_files
|
||||
from app.settings import get_settings
|
||||
|
||||
s = get_settings()
|
||||
|
||||
|
||||
def _rm(path: Path | None) -> None:
|
||||
if path is None:
|
||||
return
|
||||
try:
|
||||
if path.is_file():
|
||||
path.unlink()
|
||||
print(f"[start-fresh] removed legacy JSON {path}", flush=True)
|
||||
except OSError as e:
|
||||
print(f"[start-fresh] skip {path}: {e}", flush=True)
|
||||
|
||||
|
||||
remove_sqlite_database_files(s)
|
||||
print(f"[start-fresh] removed SQLite database at {s.sqlite_path} (and -wal/-shm if present).", flush=True)
|
||||
|
||||
# 旧版 JSON 若仍存在,启动时会被 load_watch_processed 合并进 SQLite,必须一并删除
|
||||
if s.measure_watch_use_state_file:
|
||||
if s.measure_watch_state_file is not None:
|
||||
_rm(s.measure_watch_state_file)
|
||||
elif s.measure_watch_dir is not None:
|
||||
_rm(s.measure_watch_dir / ".fishmeasure_watch_processed.json")
|
||||
|
||||
if s.action_watch_use_state_file:
|
||||
if s.action_watch_state_file is not None:
|
||||
_rm(s.action_watch_state_file)
|
||||
elif s.action_watch_dir is not None:
|
||||
_rm(s.action_watch_dir / ".fishaction_watch_processed.json")
|
||||
|
||||
print("[start-fresh] done.", flush=True)
|
||||
PY
|
||||
|
||||
PORT="${PORT:-8000}"
|
||||
HOST="${HOST:-0.0.0.0}"
|
||||
|
||||
if command -v uv >/dev/null 2>&1; then
|
||||
exec uv run uvicorn app.main:app --host "$HOST" --port "$PORT"
|
||||
else
|
||||
exec uvicorn app.main:app --host "$HOST" --port "$PORT"
|
||||
fi
|
||||
exec bash "$DIR/start.sh"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# 仓库根目录入口:与 fish_api/start.sh 等价
|
||||
# 仓库根目录入口:与 fish_api/start.sh 等价(启动前清空 SQLite、watch 状态、measure/action 输出目录等)
|
||||
#
|
||||
# conda activate fishserver # 若不用 uv
|
||||
# export PUBLIC_BASE_URL=http://<本机对外IP>:8001
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# 仓库根目录入口:删除 SQLite 库文件(及旧 JSON 状态文件)后启动 Fish API
|
||||
# 与 scripts/run_fishserver.sh 等价(历史名称保留);均会先执行 app.prestart_fresh 再启动 uvicorn
|
||||
#
|
||||
# bash scripts/start_fishapi_fresh.sh
|
||||
# PORT=8001 bash scripts/start_fishapi_fresh.sh
|
||||
|
||||
Reference in New Issue
Block a user