Initial commit: FishServer monorepo (FishAction, FishMeasure, fish_api)
Made-with: Cursor
This commit is contained in:
1
fish_api/app/routers/__init__.py
Normal file
1
fish_api/app/routers/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""API routers."""
|
||||
55
fish_api/app/routers/biomass.py
Normal file
55
fish_api/app/routers/biomass.py
Normal 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,
|
||||
},
|
||||
}
|
||||
169
fish_api/app/routers/ingest.py
Normal file
169
fish_api/app/routers/ingest.py
Normal 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",
|
||||
)
|
||||
Reference in New Issue
Block a user