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:
@@ -1,14 +1,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Set
|
||||
from typing import Dict, Set
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from app.db import add_watch_processed, load_watch_processed, save_health_snapshot
|
||||
from app.services import action as action_svc
|
||||
from app.settings import Settings
|
||||
from app.state import HealthSnapshot, app_state
|
||||
from app.watch_idle import IdleWatchWarnState, idle_warn_interval_sec, maybe_warn_idle_watch
|
||||
|
||||
_ACTION_IDLE_WARN_INTERVAL_SEC = idle_warn_interval_sec(
|
||||
"FISH_ACTION_WATCH_IDLE_WARN_INTERVAL_SEC"
|
||||
)
|
||||
|
||||
|
||||
def _state_path(settings: Settings) -> Path:
|
||||
@@ -18,29 +24,6 @@ def _state_path(settings: Settings) -> Path:
|
||||
return settings.action_watch_dir / ".fishaction_watch_processed.json"
|
||||
|
||||
|
||||
def load_processed(path: Path) -> Set[str]:
|
||||
if not path.is_file():
|
||||
return set()
|
||||
try:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
data: Any = json.load(f)
|
||||
if isinstance(data, list):
|
||||
return set(str(x) for x in data)
|
||||
if isinstance(data, dict) and "processed" in data:
|
||||
return set(str(x) for x in data["processed"])
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
return set()
|
||||
|
||||
|
||||
def save_processed(path: Path, processed: Set[str]) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = path.with_suffix(path.suffix + ".tmp")
|
||||
with open(tmp, "w", encoding="utf-8") as f:
|
||||
json.dump(sorted(processed), f, indent=0, ensure_ascii=False)
|
||||
tmp.replace(path)
|
||||
|
||||
|
||||
def iter_mp4(watch_dir: Path, recursive: bool) -> list[Path]:
|
||||
if recursive:
|
||||
return sorted(
|
||||
@@ -64,27 +47,30 @@ async def _run_inference_and_state(
|
||||
key = str(mp4.resolve())
|
||||
if key in processed:
|
||||
return
|
||||
print(f"[action-watch] inference: {mp4}", flush=True)
|
||||
logger.info("[action-watch] inference: {}", mp4)
|
||||
async with app_state.action_lock:
|
||||
app_state.action_status = "running"
|
||||
try:
|
||||
snap = await asyncio.to_thread(action_svc.run_full_action, mp4, settings)
|
||||
app_state.last_health = snap
|
||||
save_health_snapshot(settings, snap, source_path=key)
|
||||
app_state.action_status = "idle"
|
||||
processed.add(key)
|
||||
if settings.action_watch_use_state_file:
|
||||
save_processed(state_file, processed)
|
||||
add_watch_processed(settings, key, "action")
|
||||
pred = (snap.raw_class_en or "").strip()
|
||||
print(f"[action-watch] done: {mp4.name} -> {pred}", flush=True)
|
||||
logger.info("[action-watch] done: {} -> {}", mp4.name, pred)
|
||||
except Exception as e:
|
||||
app_state.last_health = HealthSnapshot(
|
||||
behavior_result="",
|
||||
health_result="",
|
||||
error=str(e),
|
||||
save_health_snapshot(
|
||||
settings,
|
||||
HealthSnapshot(
|
||||
behavior_result="",
|
||||
health_result="",
|
||||
error=str(e),
|
||||
),
|
||||
source_path=key,
|
||||
)
|
||||
app_state.action_status = "error"
|
||||
print(f"[action-watch] error on {mp4}: {e}", flush=True)
|
||||
traceback.print_exc()
|
||||
logger.exception("[action-watch] error on {}: {}", mp4, e)
|
||||
raise
|
||||
|
||||
|
||||
@@ -136,24 +122,36 @@ async def run_action_watch_loop(settings: Settings) -> None:
|
||||
assert settings.action_watch_dir is not None
|
||||
wd = settings.action_watch_dir
|
||||
if not wd.is_dir():
|
||||
print(f"[action-watch] skip: not a directory: {wd}", flush=True)
|
||||
logger.warning("[action-watch] skip: not a directory: {}", wd)
|
||||
return
|
||||
|
||||
state_file = _state_path(settings)
|
||||
processed: Set[str] = (
|
||||
load_processed(state_file) if settings.action_watch_use_state_file else set()
|
||||
load_watch_processed(settings, state_file, "action")
|
||||
if settings.action_watch_use_state_file
|
||||
else set()
|
||||
)
|
||||
stability: Dict[str, tuple[int, int]] = {}
|
||||
|
||||
print(
|
||||
f"[action-watch] watching {wd} "
|
||||
f"(poll={settings.action_watch_poll_interval}s, "
|
||||
f"stable_polls={settings.action_watch_stable_polls}, "
|
||||
f"state={'on' if settings.action_watch_use_state_file else 'off'} "
|
||||
f"{state_file if settings.action_watch_use_state_file else ''})",
|
||||
flush=True,
|
||||
logger.info(
|
||||
"[action-watch] watching {} (poll={}s, stable_polls={}, state={} {})",
|
||||
wd,
|
||||
settings.action_watch_poll_interval,
|
||||
settings.action_watch_stable_polls,
|
||||
"on" if settings.action_watch_use_state_file else "off",
|
||||
state_file if settings.action_watch_use_state_file else "",
|
||||
)
|
||||
|
||||
idle_warn_state = IdleWatchWarnState()
|
||||
while True:
|
||||
await watch_tick(settings, processed, stability, state_file)
|
||||
did = await watch_tick(settings, processed, stability, state_file)
|
||||
maybe_warn_idle_watch(
|
||||
did_work=did,
|
||||
log_tag="action-watch",
|
||||
algo_name="FishAction",
|
||||
idle_hint="目录内无 .mp4、已全部处理完毕,或文件尚未达到稳定轮询次数",
|
||||
watch_dir=wd,
|
||||
state=idle_warn_state,
|
||||
interval_sec=_ACTION_IDLE_WARN_INTERVAL_SEC,
|
||||
)
|
||||
await asyncio.sleep(max(settings.action_watch_poll_interval, 0.1))
|
||||
|
||||
Reference in New Issue
Block a user