Initial commit: FishServer monorepo (FishAction, FishMeasure, fish_api)

Made-with: Cursor
This commit is contained in:
zaiun xu
2026-04-08 19:32:23 +08:00
commit 9df21f80ef
180 changed files with 96298 additions and 0 deletions

View File

@@ -0,0 +1 @@
"""API routers."""

View File

@@ -0,0 +1,55 @@
from __future__ import annotations
from fastapi import APIRouter
from app.state import app_state
router = APIRouter(prefix="/api/v1/biomass", tags=["biomass"])
@router.get("/real/camera/")
async def get_real_camera():
"""双目实时结果(轮询最新一次 FishMeasure 完成快照)。"""
m = app_state.last_measure
if m.error:
return {
"code": 500,
"msg": m.error,
"data": {
"result": [],
"video_left": "",
"video_right": "",
},
}
return {
"code": 200,
"msg": "成功",
"data": {
"result": m.result,
"video_left": m.video_left,
"video_right": m.video_right,
},
}
@router.get("/health/result/")
async def get_health_result():
"""行为 / 健康结果(轮询最新一次 FishAction 完成快照)。"""
h = app_state.last_health
if h.error:
return {
"code": 500,
"msg": h.error,
"data": {
"behavior_result": "",
"health_result": "",
},
}
return {
"code": 200,
"msg": "成功",
"data": {
"behavior_result": h.behavior_result,
"health_result": h.health_result,
},
}

View File

@@ -0,0 +1,169 @@
from __future__ import annotations
import asyncio
from pathlib import Path
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Request, Response
from app.deps import require_ingest_auth
from app.services import action as action_svc
from app.services import measure as measure_svc
from app.services.sessions import (
finalize_rename,
new_session_dir,
partial_path,
write_chunk,
)
from app.settings import Settings, get_settings
from app.state import HealthSnapshot, MeasureSnapshot, app_state
router = APIRouter(prefix="/api/v1/ingest", tags=["ingest"])
async def _measure_job_serial(svo_path: Path, settings: Settings) -> None:
async with app_state.measure_lock:
app_state.measure_status = "running"
try:
snap = await asyncio.to_thread(
measure_svc.run_full_measure, svo_path, settings
)
app_state.last_measure = snap
app_state.measure_status = "idle"
except Exception as e:
app_state.last_measure = MeasureSnapshot(
result=[],
video_left="",
video_right="",
error=str(e),
)
app_state.measure_status = "error"
async def _action_job_serial(mp4_path: Path, settings: Settings) -> None:
async with app_state.action_lock:
app_state.action_status = "running"
try:
snap = await asyncio.to_thread(
action_svc.run_full_action, mp4_path, settings
)
app_state.last_health = snap
app_state.action_status = "idle"
except Exception as e:
app_state.last_health = HealthSnapshot(
behavior_result="",
health_result="",
error=str(e),
)
app_state.action_status = "error"
@router.post("/svo/session")
async def create_svo_session(
settings: Settings = Depends(get_settings),
_: None = Depends(require_ingest_auth),
):
settings.stream_tmp_dir.mkdir(parents=True, exist_ok=True)
sid, d = new_session_dir(settings.stream_tmp_dir)
return {"session_id": sid, "upload_path": str(d)}
@router.put("/svo/session/{session_id}")
async def append_svo_chunk(
session_id: str,
request: Request,
offset: int = 0,
settings: Settings = Depends(get_settings),
_: None = Depends(require_ingest_auth),
):
d = settings.stream_tmp_dir / session_id
if not d.is_dir():
raise HTTPException(status_code=404, detail="Unknown session")
body = await request.body()
try:
write_chunk(d, body, offset)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e)) from e
p = partial_path(d)
return {"bytes_total": p.stat().st_size}
@router.post("/svo/session/{session_id}/finalize")
async def finalize_svo(
session_id: str,
background_tasks: BackgroundTasks,
settings: Settings = Depends(get_settings),
_: None = Depends(require_ingest_auth),
):
d = settings.stream_tmp_dir / session_id
if not d.is_dir():
raise HTTPException(status_code=404, detail="Unknown session")
try:
final = finalize_rename(d, "recording.svo2")
except FileNotFoundError as e:
raise HTTPException(status_code=400, detail=str(e)) from e
async def job() -> None:
await _measure_job_serial(final, settings)
background_tasks.add_task(job)
return Response(
status_code=202,
content='{"status":"accepted","job":"fish_measure"}',
media_type="application/json",
)
@router.post("/mp4/session")
async def create_mp4_session(
settings: Settings = Depends(get_settings),
_: None = Depends(require_ingest_auth),
):
settings.stream_tmp_dir.mkdir(parents=True, exist_ok=True)
sid, d = new_session_dir(settings.stream_tmp_dir)
return {"session_id": sid, "upload_path": str(d)}
@router.put("/mp4/session/{session_id}")
async def append_mp4_chunk(
session_id: str,
request: Request,
offset: int = 0,
settings: Settings = Depends(get_settings),
_: None = Depends(require_ingest_auth),
):
d = settings.stream_tmp_dir / session_id
if not d.is_dir():
raise HTTPException(status_code=404, detail="Unknown session")
body = await request.body()
try:
write_chunk(d, body, offset)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e)) from e
p = partial_path(d)
return {"bytes_total": p.stat().st_size}
@router.post("/mp4/session/{session_id}/finalize")
async def finalize_mp4(
session_id: str,
background_tasks: BackgroundTasks,
settings: Settings = Depends(get_settings),
_: None = Depends(require_ingest_auth),
):
d = settings.stream_tmp_dir / session_id
if not d.is_dir():
raise HTTPException(status_code=404, detail="Unknown session")
try:
final = finalize_rename(d, "clip.mp4")
except FileNotFoundError as e:
raise HTTPException(status_code=400, detail=str(e)) from e
async def job() -> None:
await _action_job_serial(final, settings)
background_tasks.add_task(job)
return Response(
status_code=202,
content='{"status":"accepted","job":"fish_action"}',
media_type="application/json",
)