Add MEASURE_WATCH_* settings and measure_watch background loop parallel to action watch. Gitignore SAM 2.4GB weight and fix corrupted DGCNN checkpoint. Clear stale outputs for fresh rerun. Made-with: Cursor
140 lines
4.5 KiB
Python
140 lines
4.5 KiB
Python
"""后台轮询目录中的 .svo2,跑 FishMeasure,写入 app_state.last_measure(与 ingest 共用状态)。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
import traceback
|
||
from pathlib import Path
|
||
from typing import Dict, Set
|
||
|
||
from app.services import measure as measure_svc
|
||
from app.services.action_watch import load_processed, save_processed
|
||
from app.settings import Settings
|
||
from app.state import MeasureSnapshot, app_state
|
||
|
||
|
||
def _state_path(settings: Settings) -> Path:
|
||
if settings.measure_watch_state_file is not None:
|
||
return settings.measure_watch_state_file
|
||
assert settings.measure_watch_dir is not None
|
||
return settings.measure_watch_dir / ".fishmeasure_watch_processed.json"
|
||
|
||
|
||
def iter_svo2(watch_dir: Path, recursive: bool) -> list[Path]:
|
||
if recursive:
|
||
return sorted(
|
||
p
|
||
for p in watch_dir.rglob("*")
|
||
if p.is_file() and p.suffix.lower() == ".svo2"
|
||
)
|
||
return sorted(
|
||
p
|
||
for p in watch_dir.iterdir()
|
||
if p.is_file() and p.suffix.lower() == ".svo2"
|
||
)
|
||
|
||
|
||
async def _run_measure_and_state(
|
||
svo: Path,
|
||
settings: Settings,
|
||
processed: Set[str],
|
||
state_file: Path,
|
||
) -> None:
|
||
key = str(svo.resolve())
|
||
if key in processed:
|
||
return
|
||
print(f"[measure-watch] inference: {svo}", flush=True)
|
||
async with app_state.measure_lock:
|
||
app_state.measure_status = "running"
|
||
try:
|
||
snap = await asyncio.to_thread(measure_svc.run_full_measure, svo, settings)
|
||
app_state.last_measure = snap
|
||
app_state.measure_status = "idle"
|
||
processed.add(key)
|
||
if settings.measure_watch_use_state_file:
|
||
save_processed(state_file, processed)
|
||
r0 = snap.result[0] if snap.result else {}
|
||
w = r0.get("weight", "")
|
||
print(f"[measure-watch] done: {svo.name} weight={w!r}", flush=True)
|
||
except Exception as e:
|
||
app_state.last_measure = MeasureSnapshot(
|
||
result=[],
|
||
video_left="",
|
||
video_right="",
|
||
error=str(e),
|
||
)
|
||
app_state.measure_status = "error"
|
||
print(f"[measure-watch] error on {svo}: {e}", flush=True)
|
||
traceback.print_exc()
|
||
raise
|
||
|
||
|
||
async def watch_tick(
|
||
settings: Settings,
|
||
processed: Set[str],
|
||
stability: Dict[str, tuple[int, int]],
|
||
state_file: Path,
|
||
) -> bool:
|
||
assert settings.measure_watch_dir is not None
|
||
watch_dir = settings.measure_watch_dir
|
||
did = False
|
||
seen_keys: Set[str] = set()
|
||
for svo in iter_svo2(watch_dir, settings.measure_watch_recursive):
|
||
key = str(svo.resolve())
|
||
seen_keys.add(key)
|
||
if key in processed:
|
||
continue
|
||
try:
|
||
st = svo.stat()
|
||
except OSError:
|
||
continue
|
||
size = int(st.st_size)
|
||
if size <= 0:
|
||
stability.pop(key, None)
|
||
continue
|
||
last = stability.get(key)
|
||
if last is None or last[0] != size:
|
||
stability[key] = (size, 1)
|
||
else:
|
||
_, cnt = last
|
||
stability[key] = (size, cnt + 1)
|
||
_, cnt = stability[key]
|
||
if cnt >= settings.measure_watch_stable_polls:
|
||
try:
|
||
await _run_measure_and_state(svo, settings, processed, state_file)
|
||
stability.pop(key, None)
|
||
did = True
|
||
except Exception:
|
||
stability[key] = (size, 1)
|
||
for k in list(stability.keys()):
|
||
if k not in seen_keys:
|
||
del stability[k]
|
||
return did
|
||
|
||
|
||
async def run_measure_watch_loop(settings: Settings) -> None:
|
||
assert settings.measure_watch_dir is not None
|
||
wd = settings.measure_watch_dir
|
||
if not wd.is_dir():
|
||
print(f"[measure-watch] skip: not a directory: {wd}", flush=True)
|
||
return
|
||
|
||
state_file = _state_path(settings)
|
||
processed: Set[str] = (
|
||
load_processed(state_file) if settings.measure_watch_use_state_file else set()
|
||
)
|
||
stability: Dict[str, tuple[int, int]] = {}
|
||
|
||
print(
|
||
f"[measure-watch] watching {wd} "
|
||
f"(poll={settings.measure_watch_poll_interval}s, "
|
||
f"stable_polls={settings.measure_watch_stable_polls}, "
|
||
f"state={'on' if settings.measure_watch_use_state_file else 'off'} "
|
||
f"{state_file if settings.measure_watch_use_state_file else ''})",
|
||
flush=True,
|
||
)
|
||
|
||
while True:
|
||
await watch_tick(settings, processed, stability, state_file)
|
||
await asyncio.sleep(max(settings.measure_watch_poll_interval, 0.1))
|