feat(platform): add new services, tools, tests and crews modules
New router intelligence modules (26 files): alert_ingest/store, audit_store, architecture_pressure, backlog_generator/store, cost_analyzer, data_governance, dependency_scanner, drift_analyzer, incident_* (5 files), llm_enrichment, platform_priority_digest, provider_budget, release_check_runner, risk_* (6 files), signature_state_store, sofiia_auto_router, tool_governance New services: - sofiia-console: Dockerfile, adapters/, monitor/nodes/ops/voice modules, launchd, react static - memory-service: integration_endpoints, integrations, voice_endpoints, static UI - aurora-service: full app suite (analysis, job_store, orchestrator, reporting, schemas, subagents) - sofiia-supervisor: new supervisor service - aistalk-bridge-lite: Telegram bridge lite - calendar-service: CalDAV calendar service with reminders - mlx-stt-service / mlx-tts-service: Apple Silicon speech services - binance-bot-monitor: market monitor service - node-worker: STT/TTS memory providers New tools (9): agent_email, browser_tool, contract_tool, observability_tool, oncall_tool, pr_reviewer_tool, repo_tool, safe_code_executor, secure_vault New crews: agromatrix_crew (10 modules: depth_classifier, doc_facts, doc_focus, farm_state, light_reply, llm_factory, memory_manager, proactivity, reflection_engine, session_context, style_adapter, telemetry) Tests: 85+ test files for all new modules Made-with: Cursor
This commit is contained in:
417
services/aurora-service/app/analysis.py
Normal file
417
services/aurora-service/app/analysis.py
Normal file
@@ -0,0 +1,417 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import statistics
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
try:
|
||||
import cv2 # type: ignore[import-untyped]
|
||||
except Exception: # pragma: no cover
|
||||
cv2 = None
|
||||
|
||||
|
||||
def _safe_float(value: Any, default: float = 0.0) -> float:
|
||||
try:
|
||||
return float(value)
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
def _safe_int(value: Any, default: int = 0) -> int:
|
||||
try:
|
||||
return int(float(value))
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
def _iso_clamp(v: int, lo: int, hi: int) -> int:
|
||||
return max(lo, min(hi, v))
|
||||
|
||||
|
||||
def _detect_faces(gray_img) -> List[Dict[str, Any]]:
|
||||
if cv2 is None:
|
||||
return []
|
||||
cascade_path = str(Path(cv2.data.haarcascades) / "haarcascade_frontalface_default.xml")
|
||||
detector = cv2.CascadeClassifier(cascade_path)
|
||||
if detector.empty():
|
||||
return []
|
||||
faces = detector.detectMultiScale(
|
||||
gray_img,
|
||||
scaleFactor=1.1,
|
||||
minNeighbors=4,
|
||||
minSize=(20, 20),
|
||||
)
|
||||
out: List[Dict[str, Any]] = []
|
||||
for (x, y, w, h) in faces:
|
||||
out.append(
|
||||
{
|
||||
"bbox": [int(x), int(y), int(w), int(h)],
|
||||
"confidence": 0.75,
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def _detect_plates(gray_img) -> List[Dict[str, Any]]:
|
||||
if cv2 is None:
|
||||
return []
|
||||
cascade_path = str(Path(cv2.data.haarcascades) / "haarcascade_russian_plate_number.xml")
|
||||
if not Path(cascade_path).exists():
|
||||
return []
|
||||
detector = cv2.CascadeClassifier(cascade_path)
|
||||
if detector.empty():
|
||||
return []
|
||||
plates = detector.detectMultiScale(
|
||||
gray_img,
|
||||
scaleFactor=1.1,
|
||||
minNeighbors=3,
|
||||
minSize=(28, 10),
|
||||
)
|
||||
out: List[Dict[str, Any]] = []
|
||||
for (x, y, w, h) in plates:
|
||||
out.append(
|
||||
{
|
||||
"bbox": [int(x), int(y), int(w), int(h)],
|
||||
"confidence": 0.65,
|
||||
"text": None,
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def _noise_label(noise_sigma: float) -> str:
|
||||
if noise_sigma >= 28:
|
||||
return "high"
|
||||
if noise_sigma >= 14:
|
||||
return "medium"
|
||||
return "low"
|
||||
|
||||
|
||||
def _brightness_label(brightness: float) -> str:
|
||||
if brightness < 75:
|
||||
return "low"
|
||||
if brightness > 180:
|
||||
return "high"
|
||||
return "medium"
|
||||
|
||||
|
||||
def _blur_label(laplacian_var: float) -> str:
|
||||
if laplacian_var < 45:
|
||||
return "high"
|
||||
if laplacian_var < 120:
|
||||
return "medium"
|
||||
return "low"
|
||||
|
||||
|
||||
def _analyze_quality(gray_img) -> Dict[str, Any]:
|
||||
if cv2 is None:
|
||||
return {
|
||||
"noise_level": "unknown",
|
||||
"brightness": "unknown",
|
||||
"blur_level": "unknown",
|
||||
"brightness_value": None,
|
||||
"noise_sigma": None,
|
||||
"laplacian_var": None,
|
||||
}
|
||||
brightness = float(gray_img.mean())
|
||||
noise_sigma = float(gray_img.std())
|
||||
lap_var = float(cv2.Laplacian(gray_img, cv2.CV_64F).var())
|
||||
return {
|
||||
"noise_level": _noise_label(noise_sigma),
|
||||
"brightness": _brightness_label(brightness),
|
||||
"blur_level": _blur_label(lap_var),
|
||||
"brightness_value": round(brightness, 2),
|
||||
"noise_sigma": round(noise_sigma, 2),
|
||||
"laplacian_var": round(lap_var, 2),
|
||||
}
|
||||
|
||||
|
||||
def _aggregate_quality(samples: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
if not samples:
|
||||
return {
|
||||
"noise_level": "unknown",
|
||||
"brightness": "unknown",
|
||||
"blur_level": "unknown",
|
||||
"brightness_value": None,
|
||||
"noise_sigma": None,
|
||||
"laplacian_var": None,
|
||||
}
|
||||
brightness_values = [float(s["brightness_value"]) for s in samples if s.get("brightness_value") is not None]
|
||||
noise_values = [float(s["noise_sigma"]) for s in samples if s.get("noise_sigma") is not None]
|
||||
lap_values = [float(s["laplacian_var"]) for s in samples if s.get("laplacian_var") is not None]
|
||||
brightness = statistics.mean(brightness_values) if brightness_values else 0.0
|
||||
noise_sigma = statistics.mean(noise_values) if noise_values else 0.0
|
||||
lap_var = statistics.mean(lap_values) if lap_values else 0.0
|
||||
return {
|
||||
"noise_level": _noise_label(noise_sigma),
|
||||
"brightness": _brightness_label(brightness),
|
||||
"blur_level": _blur_label(lap_var),
|
||||
"brightness_value": round(brightness, 2),
|
||||
"noise_sigma": round(noise_sigma, 2),
|
||||
"laplacian_var": round(lap_var, 2),
|
||||
}
|
||||
|
||||
|
||||
def probe_video_metadata(path: Path) -> Dict[str, Any]:
|
||||
cmd = [
|
||||
"ffprobe",
|
||||
"-v",
|
||||
"error",
|
||||
"-select_streams",
|
||||
"v:0",
|
||||
"-show_entries",
|
||||
"stream=width,height,nb_frames,r_frame_rate,duration",
|
||||
"-show_entries",
|
||||
"format=duration",
|
||||
"-of",
|
||||
"json",
|
||||
str(path),
|
||||
]
|
||||
try:
|
||||
p = subprocess.run(cmd, check=False, capture_output=True, text=True)
|
||||
if p.returncode != 0 or not p.stdout:
|
||||
return {}
|
||||
payload = json.loads(p.stdout)
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
stream = (payload.get("streams") or [{}])[0] if isinstance(payload, dict) else {}
|
||||
fmt = payload.get("format") or {}
|
||||
width = _safe_int(stream.get("width"))
|
||||
height = _safe_int(stream.get("height"))
|
||||
nb_frames = _safe_int(stream.get("nb_frames"))
|
||||
fps_raw = str(stream.get("r_frame_rate") or "0/1")
|
||||
duration = _safe_float(stream.get("duration")) or _safe_float(fmt.get("duration"))
|
||||
fps = 0.0
|
||||
if "/" in fps_raw:
|
||||
num_s, den_s = fps_raw.split("/", 1)
|
||||
num = _safe_float(num_s)
|
||||
den = _safe_float(den_s, 1.0)
|
||||
if den > 0:
|
||||
fps = num / den
|
||||
elif fps_raw:
|
||||
fps = _safe_float(fps_raw)
|
||||
if nb_frames <= 0 and duration > 0 and fps > 0:
|
||||
nb_frames = int(duration * fps)
|
||||
return {
|
||||
"width": width,
|
||||
"height": height,
|
||||
"fps": round(fps, 3) if fps > 0 else None,
|
||||
"frame_count": nb_frames if nb_frames > 0 else None,
|
||||
"duration_seconds": round(duration, 3) if duration > 0 else None,
|
||||
}
|
||||
|
||||
|
||||
def estimate_processing_seconds(
|
||||
*,
|
||||
media_type: str,
|
||||
mode: str,
|
||||
width: int = 0,
|
||||
height: int = 0,
|
||||
frame_count: int = 0,
|
||||
) -> Optional[int]:
|
||||
if media_type == "video":
|
||||
if frame_count <= 0:
|
||||
return None
|
||||
megapixels = max(0.15, (max(1, width) * max(1, height)) / 1_000_000.0)
|
||||
per_frame = 0.8 * megapixels if mode == "tactical" else 1.35 * megapixels
|
||||
per_frame = max(0.08, min(9.0, per_frame))
|
||||
overhead = 6 if mode == "tactical" else 12
|
||||
return int(math.ceil(frame_count * per_frame + overhead))
|
||||
if media_type == "photo":
|
||||
megapixels = max(0.15, (max(1, width) * max(1, height)) / 1_000_000.0)
|
||||
base = 3.0 if mode == "tactical" else 6.0
|
||||
return int(math.ceil(base + megapixels * (3.0 if mode == "tactical" else 5.0)))
|
||||
return None
|
||||
|
||||
|
||||
def _recommendations(
|
||||
*,
|
||||
faces_count: int,
|
||||
plates_count: int,
|
||||
quality: Dict[str, Any],
|
||||
media_type: str,
|
||||
) -> Tuple[List[str], str]:
|
||||
recs: List[str] = []
|
||||
noise_level = quality.get("noise_level")
|
||||
brightness = quality.get("brightness")
|
||||
blur_level = quality.get("blur_level")
|
||||
|
||||
if noise_level == "high":
|
||||
recs.append("Enable denoise (FastDVDnet/SCUNet) before enhancement.")
|
||||
if brightness == "low":
|
||||
recs.append("Apply low-light normalization before super-resolution.")
|
||||
if blur_level in {"medium", "high"}:
|
||||
recs.append("Enable sharpening after upscaling to recover edges.")
|
||||
if faces_count > 0:
|
||||
recs.append("Run face restoration (GFPGAN) as priority stage.")
|
||||
if plates_count > 0:
|
||||
recs.append("Run license-plate ROI enhancement with focused sharpening.")
|
||||
if not recs:
|
||||
recs.append("Balanced enhancement pipeline is sufficient for this media.")
|
||||
|
||||
if faces_count > 0 and faces_count >= plates_count:
|
||||
priority = "faces"
|
||||
elif plates_count > 0:
|
||||
priority = "plates"
|
||||
elif media_type == "photo":
|
||||
priority = "details"
|
||||
else:
|
||||
priority = "balanced"
|
||||
return recs, priority
|
||||
|
||||
|
||||
def _suggested_export(media_type: str, quality: Dict[str, Any], width: int, height: int) -> Dict[str, Any]:
|
||||
if media_type == "video":
|
||||
if width >= 3840 or height >= 2160:
|
||||
resolution = "original"
|
||||
elif width >= 1920 or height >= 1080:
|
||||
resolution = "4k"
|
||||
else:
|
||||
resolution = "1080p"
|
||||
codec = "mp4_h264" if quality.get("noise_level") != "high" else "mp4_h265"
|
||||
return {
|
||||
"resolution": resolution,
|
||||
"format": codec,
|
||||
"roi": "auto_faces",
|
||||
}
|
||||
return {
|
||||
"resolution": "original",
|
||||
"format": "png",
|
||||
"roi": "full_frame",
|
||||
}
|
||||
|
||||
|
||||
def analyze_photo(path: Path) -> Dict[str, Any]:
|
||||
if cv2 is None:
|
||||
raise RuntimeError("opencv-python-headless is not installed")
|
||||
frame = cv2.imread(str(path), cv2.IMREAD_COLOR)
|
||||
if frame is None:
|
||||
raise RuntimeError("Cannot decode uploaded image")
|
||||
h, w = frame.shape[:2]
|
||||
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
||||
faces = _detect_faces(gray)
|
||||
plates = _detect_plates(gray)
|
||||
quality = _analyze_quality(gray)
|
||||
recs, priority = _recommendations(
|
||||
faces_count=len(faces),
|
||||
plates_count=len(plates),
|
||||
quality=quality,
|
||||
media_type="photo",
|
||||
)
|
||||
return {
|
||||
"media_type": "photo",
|
||||
"frame_sampled": 1,
|
||||
"resolution": {"width": w, "height": h},
|
||||
"faces": faces,
|
||||
"license_plates": plates,
|
||||
"quality_analysis": quality,
|
||||
"recommendations": recs,
|
||||
"suggested_priority": priority,
|
||||
"suggested_export": _suggested_export("photo", quality, w, h),
|
||||
"estimated_processing_seconds": estimate_processing_seconds(
|
||||
media_type="photo",
|
||||
mode="tactical",
|
||||
width=w,
|
||||
height=h,
|
||||
frame_count=1,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _sample_video_frames(path: Path, max_samples: int = 24) -> Tuple[List[Tuple[int, Any]], Dict[str, Any]]:
|
||||
if cv2 is None:
|
||||
raise RuntimeError("opencv-python-headless is not installed")
|
||||
cap = cv2.VideoCapture(str(path))
|
||||
if not cap.isOpened():
|
||||
raise RuntimeError("Cannot open uploaded video")
|
||||
frame_count = _safe_int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||
fps = _safe_float(cap.get(cv2.CAP_PROP_FPS))
|
||||
width = _safe_int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
||||
height = _safe_int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||||
|
||||
indices: List[int] = []
|
||||
if frame_count > 0:
|
||||
sample_count = min(max_samples, frame_count)
|
||||
if sample_count <= 1:
|
||||
indices = [0]
|
||||
else:
|
||||
indices = sorted({int(i * (frame_count - 1) / (sample_count - 1)) for i in range(sample_count)})
|
||||
else:
|
||||
indices = list(range(max_samples))
|
||||
|
||||
sampled: List[Tuple[int, Any]] = []
|
||||
for idx in indices:
|
||||
if frame_count > 0:
|
||||
cap.set(cv2.CAP_PROP_POS_FRAMES, idx)
|
||||
ok, frame = cap.read()
|
||||
if not ok or frame is None:
|
||||
continue
|
||||
sampled.append((idx, frame))
|
||||
|
||||
cap.release()
|
||||
duration = (frame_count / fps) if (frame_count > 0 and fps > 0) else None
|
||||
meta = {
|
||||
"frame_count": frame_count if frame_count > 0 else None,
|
||||
"fps": round(fps, 3) if fps > 0 else None,
|
||||
"width": width,
|
||||
"height": height,
|
||||
"duration_seconds": round(duration, 3) if duration else None,
|
||||
}
|
||||
return sampled, meta
|
||||
|
||||
|
||||
def analyze_video(path: Path) -> Dict[str, Any]:
|
||||
sampled, meta = _sample_video_frames(path, max_samples=24)
|
||||
if not sampled:
|
||||
raise RuntimeError("Cannot sample frames from uploaded video")
|
||||
|
||||
all_faces: List[Dict[str, Any]] = []
|
||||
all_plates: List[Dict[str, Any]] = []
|
||||
quality_samples: List[Dict[str, Any]] = []
|
||||
|
||||
for frame_idx, frame in sampled:
|
||||
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # type: ignore[union-attr]
|
||||
faces = _detect_faces(gray)
|
||||
plates = _detect_plates(gray)
|
||||
for f in faces:
|
||||
f["frame_index"] = frame_idx
|
||||
all_faces.append(f)
|
||||
for p in plates:
|
||||
p["frame_index"] = frame_idx
|
||||
all_plates.append(p)
|
||||
quality_samples.append(_analyze_quality(gray))
|
||||
|
||||
quality = _aggregate_quality(quality_samples)
|
||||
recs, priority = _recommendations(
|
||||
faces_count=len(all_faces),
|
||||
plates_count=len(all_plates),
|
||||
quality=quality,
|
||||
media_type="video",
|
||||
)
|
||||
width = _safe_int(meta.get("width"))
|
||||
height = _safe_int(meta.get("height"))
|
||||
frame_count = _safe_int(meta.get("frame_count"))
|
||||
|
||||
return {
|
||||
"media_type": "video",
|
||||
"frame_sampled": len(sampled),
|
||||
"video_metadata": meta,
|
||||
"faces": all_faces[:120],
|
||||
"license_plates": all_plates[:120],
|
||||
"quality_analysis": quality,
|
||||
"recommendations": recs,
|
||||
"suggested_priority": priority,
|
||||
"suggested_export": _suggested_export("video", quality, width, height),
|
||||
"estimated_processing_seconds": estimate_processing_seconds(
|
||||
media_type="video",
|
||||
mode="tactical",
|
||||
width=width,
|
||||
height=height,
|
||||
frame_count=frame_count,
|
||||
),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user