feat: stage6 — Image Pipeline (F05) and Rotation Manager (F06)

This commit is contained in:
Yuzviak
2026-03-22 22:51:00 +02:00
parent a2fb9ab404
commit 9ef046d623
9 changed files with 653 additions and 26 deletions
+23 -18
View File
@@ -5,6 +5,8 @@ from __future__ import annotations
import asyncio
from datetime import datetime, timezone
from gps_denied.core.pipeline import ImageInputPipeline
from gps_denied.core.results import ResultManager
from gps_denied.core.sse import SSEEventStreamer
from gps_denied.db.repository import FlightRepository
from gps_denied.schemas import GPSPoint
@@ -23,17 +25,20 @@ from gps_denied.schemas.flight import (
UserFixResponse,
Waypoint,
)
from gps_denied.schemas.image import ImageBatch
class FlightProcessor:
"""Orchestrates flight business logic."""
"""Manages business logic and background processing for flights."""
def __init__(self, repo: FlightRepository, sse: SSEEventStreamer) -> None:
self.repo = repo
self.sse = sse
def __init__(self, repository: FlightRepository, streamer: SSEEventStreamer) -> None:
self.repository = repository
self.streamer = streamer
self.result_manager = ResultManager(repository, streamer)
self.pipeline = ImageInputPipeline(storage_dir=".image_storage", max_queue_size=50)
async def create_flight(self, req: FlightCreateRequest) -> FlightResponse:
flight = await self.repo.insert_flight(
flight = await self.repository.insert_flight(
name=req.name,
description=req.description,
start_lat=req.start_gps.lat,
@@ -42,7 +47,7 @@ class FlightProcessor:
camera_params=req.camera_params.model_dump(),
)
for poly in req.geofences.polygons:
await self.repo.insert_geofence(
await self.repository.insert_geofence(
flight.id,
nw_lat=poly.north_west.lat,
nw_lon=poly.north_west.lon,
@@ -50,7 +55,7 @@ class FlightProcessor:
se_lon=poly.south_east.lon,
)
for w in req.rough_waypoints:
await self.repo.insert_waypoint(flight.id, lat=w.lat, lon=w.lon)
await self.repository.insert_waypoint(flight.id, lat=w.lat, lon=w.lon)
return FlightResponse(
flight_id=flight.id,
@@ -60,11 +65,11 @@ class FlightProcessor:
)
async def get_flight(self, flight_id: str) -> FlightDetailResponse | None:
flight = await self.repo.get_flight(flight_id)
flight = await self.repository.get_flight(flight_id)
if not flight:
return None
wps = await self.repo.get_waypoints(flight_id)
state = await self.repo.load_flight_state(flight_id)
wps = await self.repository.get_waypoints(flight_id)
state = await self.repository.load_flight_state(flight_id)
waypoints = [
Waypoint(
@@ -103,13 +108,13 @@ class FlightProcessor:
)
async def delete_flight(self, flight_id: str) -> DeleteResponse:
deleted = await self.repo.delete_flight(flight_id)
deleted = await self.repository.delete_flight(flight_id)
return DeleteResponse(deleted=deleted, flight_id=flight_id)
async def update_waypoint(
self, flight_id: str, waypoint_id: str, waypoint: Waypoint
) -> UpdateResponse:
ok = await self.repo.update_waypoint(
ok = await self.repository.update_waypoint(
flight_id,
waypoint_id,
lat=waypoint.lat,
@@ -126,7 +131,7 @@ class FlightProcessor:
failed = []
updated = 0
for wp in waypoints:
ok = await self.repo.update_waypoint(
ok = await self.repository.update_waypoint(
flight_id,
wp.id,
lat=wp.lat,
@@ -144,10 +149,10 @@ class FlightProcessor:
async def queue_images(
self, flight_id: str, metadata: BatchMetadata, file_count: int
) -> BatchResponse:
state = await self.repo.load_flight_state(flight_id)
state = await self.repository.load_flight_state(flight_id)
if state:
total = state.frames_total + file_count
await self.repo.save_flight_state(flight_id, frames_total=total, status="processing")
await self.repository.save_flight_state(flight_id, frames_total=total, status="processing")
next_seq = metadata.end_sequence + 1
seqs = list(range(metadata.start_sequence, metadata.end_sequence + 1))
@@ -159,13 +164,13 @@ class FlightProcessor:
)
async def handle_user_fix(self, flight_id: str, req: UserFixRequest) -> UserFixResponse:
await self.repo.save_flight_state(flight_id, blocked=False, status="processing")
await self.repository.save_flight_state(flight_id, blocked=False, status="processing")
return UserFixResponse(
accepted=True, processing_resumed=True, message="Fix applied."
)
async def get_flight_status(self, flight_id: str) -> FlightStatusResponse | None:
state = await self.repo.load_flight_state(flight_id)
state = await self.repository.load_flight_state(flight_id)
if not state:
return None
return FlightStatusResponse(
@@ -194,5 +199,5 @@ class FlightProcessor:
async def stream_events(self, flight_id: str, client_id: str):
"""Async generator for SSE stream."""
# Yield from the real SSE streamer generator
async for event in self.sse.stream_generator(flight_id, client_id):
async for event in self.streamer.stream_generator(flight_id, client_id):
yield event