feat(api): add backend
routes and WebSockets
This commit is contained in:
16
services/api/app/services/audio_service.py
Normal file
16
services/api/app/services/audio_service.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from datetime import datetime
|
||||
from app.repositories.audio_repository import AudioRepository
|
||||
from app.schemas.audio import AudioPoint
|
||||
|
||||
|
||||
class AudioService:
|
||||
def __init__(self, repo: AudioRepository):
|
||||
self.repo = repo
|
||||
|
||||
async def latest(self, limit: int) -> list[AudioPoint]:
|
||||
rows = await self.repo.latest(limit)
|
||||
return [AudioPoint.model_validate(r, from_attributes=True) for r in rows]
|
||||
|
||||
async def range(self, time_from: datetime, time_to: datetime) -> list[AudioPoint]:
|
||||
rows = await self.repo.range(time_from, time_to)
|
||||
return [AudioPoint.model_validate(r, from_attributes=True) for r in rows]
|
||||
54
services/api/app/services/events_service.py
Normal file
54
services/api/app/services/events_service.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from app.repositories.audio_repository import AudioRepository
|
||||
from app.schemas.events import LoudEvent
|
||||
|
||||
|
||||
class EventsService:
|
||||
def __init__(self, repo: AudioRepository):
|
||||
self.repo = repo
|
||||
|
||||
async def loud_events(
|
||||
self,
|
||||
threshold: float,
|
||||
time_from: datetime | None,
|
||||
time_to: datetime | None,
|
||||
max_gap_sec: float = 1.0,
|
||||
) -> list[LoudEvent]:
|
||||
samples = await self.repo.loud_samples(threshold, time_from, time_to)
|
||||
if not samples:
|
||||
return []
|
||||
|
||||
events: list[LoudEvent] = []
|
||||
start = samples[0].time
|
||||
end = samples[0].time
|
||||
max_db = samples[0].rms_db
|
||||
freq = samples[0].frequency_hz
|
||||
|
||||
for s in samples[1:]:
|
||||
gap = (s.time - end).total_seconds()
|
||||
if gap <= max_gap_sec:
|
||||
end = s.time
|
||||
if s.rms_db > max_db:
|
||||
max_db = s.rms_db
|
||||
else:
|
||||
events.append(
|
||||
LoudEvent(
|
||||
time=start,
|
||||
rms_db=round(float(max_db), 2),
|
||||
frequency_hz=int(freq),
|
||||
duration_sec=round((end - start).total_seconds(), 2),
|
||||
)
|
||||
)
|
||||
start, end, max_db, freq = s.time, s.time, s.rms_db, s.frequency_hz
|
||||
|
||||
events.append(
|
||||
LoudEvent(
|
||||
time=start,
|
||||
rms_db=round(float(max_db), 2),
|
||||
frequency_hz=int(freq),
|
||||
duration_sec=round((end - start).total_seconds(), 2),
|
||||
)
|
||||
)
|
||||
return events
|
||||
35
services/api/app/services/stats_service.py
Normal file
35
services/api/app/services/stats_service.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from datetime import datetime, timedelta
|
||||
from app.repositories.audio_repository import AudioRepository
|
||||
from app.schemas.stats import StatsSummary
|
||||
|
||||
_PERIODS = {
|
||||
"10s": timedelta(seconds=10),
|
||||
"1m": timedelta(minutes=1),
|
||||
"1h": timedelta(hours=1),
|
||||
"6h": timedelta(hours=6),
|
||||
"24h": timedelta(hours=24),
|
||||
"7d": timedelta(days=7),
|
||||
"30d": timedelta(days=30),
|
||||
}
|
||||
|
||||
|
||||
class StatsService:
|
||||
def __init__(self, repo: AudioRepository):
|
||||
self.repo = repo
|
||||
|
||||
async def summary(self, period: str) -> StatsSummary:
|
||||
if period not in _PERIODS:
|
||||
raise ValueError(f"Unsupported period: {period}")
|
||||
|
||||
since = datetime.utcnow() - _PERIODS[period]
|
||||
raw = await self.repo.summary_since(since)
|
||||
|
||||
total = raw["total_count"]
|
||||
silence_percent = (raw["silence_count"] / total * 100.0) if total else 0.0
|
||||
|
||||
return StatsSummary(
|
||||
avg_db=round(raw["avg_db"], 2),
|
||||
max_db=round(raw["max_db"], 2),
|
||||
dominant_freq=raw["dominant_freq"],
|
||||
silence_percent=round(silence_percent, 2),
|
||||
)
|
||||
Reference in New Issue
Block a user