Files
FishServer/fish_api/app/services/action_watch.py
zaiun xu c1aafc69bf 验收1
2026-04-13 17:13:02 +08:00

177 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import asyncio
from pathlib import Path
from typing import Dict, Set
from loguru import logger
from app.db import (
add_watch_processed,
health_snapshot_deliverable,
load_watch_processed,
save_health_snapshot,
)
from app.services import action as action_svc
from app.settings import Settings
from app.state import 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:
if settings.action_watch_state_file is not None:
return settings.action_watch_state_file
assert settings.action_watch_dir is not None
return settings.action_watch_dir / ".fishaction_watch_processed.json"
def iter_mp4(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() == ".mp4"
)
return sorted(
p
for p in watch_dir.iterdir()
if p.is_file() and p.suffix.lower() == ".mp4"
)
async def _run_inference_and_state(
mp4: Path,
settings: Settings,
processed: Set[str],
state_file: Path,
) -> None:
key = str(mp4.resolve())
if key in processed:
return
logger.info("[action-watch] inference: {}", mp4)
async with app_state.action_lock:
app_state.action_status = "running"
try:
# 返回 (第一个快照, 所有切片快照列表)
first_snap, all_snaps = await asyncio.to_thread(
action_svc.run_full_action, mp4, settings
)
# 将所有切片作为独立视频保存到数据库
saved_count = 0
for i, snap in enumerate(all_snaps):
if health_snapshot_deliverable(snap):
# 为每个切片生成独立的 source_path
slice_key = f"{key}#slice{i}"
save_health_snapshot(settings, snap, source_path=slice_key)
saved_count += 1
if saved_count == 0:
logger.warning(
"[action-watch] no deliverable health snapshot for {}, skip SQLite",
mp4.name,
)
app_state.action_status = "idle"
processed.add(key)
if settings.action_watch_use_state_file:
add_watch_processed(settings, key, "action")
pred = (first_snap.raw_class_en or "").strip()
logger.info(
"[action-watch] done: {} -> {} (saved {} slices)",
mp4.name,
pred,
saved_count,
)
except Exception as e:
logger.exception("[action-watch] error on {}: {}", mp4, e)
app_state.action_status = "idle"
processed.add(key)
if settings.action_watch_use_state_file:
add_watch_processed(settings, key, "action")
async def watch_tick(
settings: Settings,
processed: Set[str],
stability: Dict[str, tuple[int, int]],
state_file: Path,
) -> bool:
"""处理一轮目录扫描;若处理了至少一个文件返回 True。"""
assert settings.action_watch_dir is not None
watch_dir = settings.action_watch_dir
did = False
seen_keys: Set[str] = set()
for mp4 in iter_mp4(watch_dir, settings.action_watch_recursive):
key = str(mp4.resolve())
seen_keys.add(key)
if key in processed:
continue
try:
st = mp4.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.action_watch_stable_polls:
await _run_inference_and_state(mp4, settings, processed, state_file)
stability.pop(key, None)
did = True
for k in list(stability.keys()):
if k not in seen_keys:
del stability[k]
return did
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():
logger.warning("[action-watch] skip: not a directory: {}", wd)
return
state_file = _state_path(settings)
processed: Set[str] = (
load_watch_processed(settings, state_file, "action")
if settings.action_watch_use_state_file
else set()
)
stability: Dict[str, tuple[int, int]] = {}
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:
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))