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
418 lines
13 KiB
Python
418 lines
13 KiB
Python
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,
|
|
),
|
|
}
|
|
|