Livrare LOT 1 - Didi

This commit is contained in:
Dezvoltari Evotech 2026-06-25 14:13:25 -07:00
commit 5380c3fc63
990 changed files with 133308 additions and 0 deletions

View file

@ -0,0 +1,45 @@
"""Shared test fixtures: synthetic media generated in-memory."""
from __future__ import annotations
import io
import numpy as np
import pytest
from PIL import Image
@pytest.fixture
def jpeg_bytes() -> bytes:
"""A small valid JPEG with no EXIF."""
img = Image.fromarray(
(np.random.default_rng(0).random((64, 64, 3)) * 255).astype("uint8")
)
buf = io.BytesIO()
img.save(buf, format="JPEG", quality=85)
return buf.getvalue()
@pytest.fixture
def png_bytes() -> bytes:
"""A small valid PNG."""
img = Image.new("RGB", (48, 48), (120, 30, 200))
buf = io.BytesIO()
img.save(buf, format="PNG")
return buf.getvalue()
@pytest.fixture
def jpeg_with_software(tmp_path):
"""Write a JPEG carrying a Photoshop Software EXIF tag; return its path."""
from PIL import Image as PImage
img = PImage.new("RGB", (32, 32), (10, 20, 30))
exif = img.getexif()
exif[0x0131] = "Adobe Photoshop 25.0" # Software tag
path = tmp_path / "edited.jpg"
img.save(path, format="JPEG", exif=exif)
return str(path)

View file

@ -0,0 +1,51 @@
"""End-to-end API tests via FastAPI TestClient."""
from __future__ import annotations
import io
from fastapi.testclient import TestClient
from PIL import Image
from extractors.app import app
client = TestClient(app)
def _png() -> bytes:
buf = io.BytesIO()
Image.new("RGB", (40, 40), (200, 10, 10)).save(buf, format="PNG")
return buf.getvalue()
def test_health():
r = client.get("/health")
assert r.status_code == 200
assert r.json()["status"] == "ok"
def test_ready_reports_tooling():
r = client.get("/ready")
assert r.status_code == 200
body = r.json()
assert "ffprobe" in body and "ffmpeg" in body
def test_metadata_image_runs_image_extractors():
r = client.post(
"/v1/metadata",
files={"file": ("photo.png", _png(), "image/png")},
)
assert r.status_code == 200
body = r.json()
assert body["media_type"] == "image"
assert set(body["analyses"]) >= {"integrity", "exif", "ela"}
assert len(body["sha256"]) == 64
assert body["execution_time_ms"] >= 0
def test_metadata_rejects_empty():
r = client.post(
"/v1/metadata", files={"file": ("empty.png", b"", "image/png")}
)
assert r.status_code == 400

View file

@ -0,0 +1,80 @@
"""Object-detection tests with a fake YOLO predictor (no torch/weights)."""
from __future__ import annotations
import io
from fastapi.testclient import TestClient
from PIL import Image
from extractors.app import app
from extractors.features import detect
class _Box:
def __init__(self, cls_id, conf, xyxy):
self.cls = [cls_id]
self.conf = [conf]
self.xyxy = [xyxy]
class _Result:
names = {0: "person", 2: "car"}
def __init__(self, boxes):
self.boxes = boxes
class FakeYOLO:
def __init__(self, boxes):
self._boxes = boxes
def predict(self, img, **kwargs):
return [_Result(self._boxes)]
def _png() -> bytes:
buf = io.BytesIO()
Image.new("RGB", (64, 64), (0, 0, 0)).save(buf, format="PNG")
return buf.getvalue()
def test_summarize_counts():
dets = [
{"label": "person", "confidence": 0.9, "box": [0, 0, 10, 10]},
{"label": "person", "confidence": 0.8, "box": [5, 5, 20, 20]},
{"label": "car", "confidence": 0.7, "box": [0, 0, 30, 30]},
]
res = detect._summarize(dets)
assert res.results["count"] == 3
assert res.results["by_label"] == {"person": 2, "car": 1}
assert "person×2" in res.evidence[0]
def test_analyze_parses_and_sorts():
fake = FakeYOLO([_Box(2, 0.6, [1, 1, 9, 9]), _Box(0, 0.95, [0, 0, 8, 8])])
res = detect.analyze(_png(), model=fake)
assert res.ok is True
assert res.results["count"] == 2
# sorted by confidence desc → person (0.95) first
assert res.results["detections"][0]["label"] == "person"
assert res.results["detections"][0]["confidence"] == 0.95
def test_analyze_empty_image():
res = detect.analyze(b"", model=FakeYOLO([]))
assert res.ok is False
def test_detect_endpoint_503_without_ml_extra():
client = TestClient(app)
r = client.post("/v1/detect", files={"file": ("x.png", _png(), "image/png")})
assert r.status_code == 503
def test_detect_endpoint_with_monkeypatched_model(monkeypatch):
monkeypatch.setattr(detect, "get_model", lambda: FakeYOLO([_Box(0, 0.9, [0, 0, 5, 5])]))
client = TestClient(app)
r = client.post("/v1/detect", files={"file": ("x.png", _png(), "image/png")})
assert r.status_code == 200
assert r.json()["results"]["by_label"] == {"person": 1}

View file

@ -0,0 +1,22 @@
"""ELA extractor tests."""
from __future__ import annotations
from extractors.features import ela
def test_ela_returns_stats(tmp_path, jpeg_bytes):
path = tmp_path / "img.jpg"
path.write_bytes(jpeg_bytes)
res = ela.extract(str(path))
assert res.ok is True
for key in ("mean_error", "max_error", "p99_error", "hot_pixel_fraction"):
assert key in res.results
assert 0.0 <= res.results["hot_pixel_fraction"] <= 1.0
def test_ela_on_bad_input(tmp_path):
path = tmp_path / "x.bin"
path.write_bytes(b"\x00\x01\x02")
res = ela.extract(str(path))
assert res.ok is False

View file

@ -0,0 +1,33 @@
"""EXIF extractor tests."""
from __future__ import annotations
import io
from PIL import Image
from extractors.features import exif
def test_no_exif_is_reported(tmp_path, png_bytes):
path = tmp_path / "plain.png"
path.write_bytes(png_bytes)
res = exif.extract(str(path))
assert res.ok is True
assert any("No EXIF" in e for e in res.evidence)
assert not res.anomalies
def test_editing_software_flagged_as_anomaly(jpeg_with_software):
res = exif.extract(jpeg_with_software)
assert res.ok is True
assert res.results.get("software", "").lower().startswith("adobe photoshop")
assert any("Editing/generation software" in a for a in res.anomalies)
def test_non_image_returns_error(tmp_path):
path = tmp_path / "notimage.bin"
path.write_bytes(b"not an image at all")
res = exif.extract(str(path))
assert res.ok is False
assert res.error

View file

@ -0,0 +1,32 @@
"""Integrity & signature extractor tests (no external deps)."""
from __future__ import annotations
from extractors.features import integrity
def test_identifies_jpeg_and_hashes(jpeg_bytes):
res = integrity.analyze_bytes(jpeg_bytes)
assert res.ok is True
assert res.results["container"] == "JPEG"
assert len(res.results["sha256"]) == 64
assert res.results["size_bytes"] == len(jpeg_bytes)
assert not res.anomalies
def test_identifies_png(png_bytes):
res = integrity.analyze_bytes(png_bytes)
assert res.results["container"] == "PNG"
def test_detects_trailing_data_after_jpeg_eoi(jpeg_bytes):
tampered = jpeg_bytes + b"\x00" * 64 # appended payload after FFD9
res = integrity.analyze_bytes(tampered)
assert res.results.get("trailing_bytes_after_eoi") == 64
assert any("appended after JPEG" in a for a in res.anomalies)
def test_detects_mp4_brand():
data = b"\x00\x00\x00\x18ftypmp42" + b"\x00" * 32
res = integrity.analyze_bytes(data)
assert "ISO-BMFF/MP4" in res.results["container"]

View file

@ -0,0 +1,133 @@
"""Tests for LLM-delegated features (sentiment, OCR) with a fake gateway."""
from __future__ import annotations
import io
import pytest
from fastapi.testclient import TestClient
from PIL import Image
from extractors.app import app
from extractors.deps import get_llm_client
from extractors.features import ocr, sentiment
from extractors.features._jsonparse import extract_json_object
class FakeLLM:
"""Stand-in for LLMClient that returns a canned completion."""
configured = True
def __init__(self, reply: str):
self._reply = reply
self.calls: list[list[dict]] = []
async def chat(self, messages, *, max_tokens=1024, temperature=0.0):
self.calls.append(messages)
return self._reply
# --- JSON parsing --------------------------------------------------------
def test_extract_json_plain():
assert extract_json_object('{"a": 1}') == {"a": 1}
def test_extract_json_fenced_with_prose():
txt = 'Sigur!\n```json\n{"label": "negativ", "score": -0.7}\n```\nGata.'
assert extract_json_object(txt)["label"] == "negativ"
def test_extract_json_missing_raises():
with pytest.raises(ValueError):
extract_json_object("no json here")
# --- Sentiment (unit) ----------------------------------------------------
@pytest.mark.asyncio
async def test_sentiment_parses_model_json():
fake = FakeLLM('{"label":"negativ","score":-0.8,"confidence":0.9,'
'"rationale":"Ton agresiv."}')
res = await sentiment.analyze("Ești un mincompetent total.", fake)
assert res.ok is True
assert res.results["label"] == "negativ"
assert res.results["score"] == -0.8
assert res.confidence == 0.9
@pytest.mark.asyncio
async def test_sentiment_clamps_and_defaults():
fake = FakeLLM('{"label":"ceva-invalid","score":5,"confidence":2}')
res = await sentiment.analyze("text", fake)
assert res.results["label"] == "neutru" # invalid label → neutru
assert res.results["score"] == 1.0 # clamped to 1.0
@pytest.mark.asyncio
async def test_sentiment_empty_text():
res = await sentiment.analyze(" ", FakeLLM("{}"))
assert res.ok is False
# --- OCR (unit) ----------------------------------------------------------
@pytest.mark.asyncio
async def test_ocr_parses_text():
fake = FakeLLM('{"text":"BREAKING: fake news","language":"en","has_text":true}')
res = await ocr.analyze(b"\xff\xd8\xff_fake_jpeg", fake)
assert res.ok is True
assert res.results["text"] == "BREAKING: fake news"
assert res.results["char_count"] == 19
@pytest.mark.asyncio
async def test_ocr_accepts_plain_text_fallback():
fake = FakeLLM("Doar text simplu, fără JSON")
res = await ocr.analyze(b"img", fake)
assert res.ok is True
assert "text simplu" in res.results["text"]
# --- API level -----------------------------------------------------------
def _png() -> bytes:
buf = io.BytesIO()
Image.new("RGB", (32, 32), (0, 0, 0)).save(buf, format="PNG")
return buf.getvalue()
def test_sentiment_endpoint():
fake = FakeLLM('{"label":"pozitiv","score":0.6,"confidence":0.8}')
app.dependency_overrides[get_llm_client] = lambda: fake
try:
client = TestClient(app)
r = client.post("/v1/sentiment", json={"text": "Ce zi frumoasă!"})
assert r.status_code == 200
assert r.json()["results"]["label"] == "pozitiv"
finally:
app.dependency_overrides.clear()
def test_ocr_endpoint():
fake = FakeLLM('{"text":"STOP","language":"en","has_text":true}')
app.dependency_overrides[get_llm_client] = lambda: fake
try:
client = TestClient(app)
r = client.post("/v1/ocr", files={"file": ("x.png", _png(), "image/png")})
assert r.status_code == 200
assert r.json()["results"]["text"] == "STOP"
finally:
app.dependency_overrides.clear()
def test_sentiment_endpoint_503_without_gateway():
# Default client has no gateway URL configured → 503.
client = TestClient(app)
r = client.post("/v1/sentiment", json={"text": "test"})
assert r.status_code == 503

View file

@ -0,0 +1,58 @@
"""NER tests with a fake GLiNER predictor (no torch/model download)."""
from __future__ import annotations
from fastapi.testclient import TestClient
from extractors.app import app
from extractors.features import ner
class FakePredictor:
"""Mimics GLiNER.predict_entities with canned spans."""
def __init__(self, ents):
self._ents = ents
def predict_entities(self, text, labels, threshold=0.5):
return self._ents
_SAMPLE = [
{"text": "Klaus Iohannis", "label": "persoană", "start": 0, "end": 14, "score": 0.97},
{"text": "Guvernul României", "label": "instituție publică", "start": 20,
"end": 37, "score": 0.91},
]
def test_ner_parses_and_sorts():
res = ner.analyze("Klaus Iohannis și Guvernul României.", model=FakePredictor(_SAMPLE))
assert res.ok is True
assert res.results["count"] == 2
assert res.results["entities"][0]["text"] == "Klaus Iohannis"
assert res.results["by_label"]["persoană"] == 1
assert "persoană×1" in res.evidence[0]
def test_ner_empty_entities():
res = ner.analyze("text neutru", model=FakePredictor([]))
assert res.ok is True
assert res.results["count"] == 0
assert "No entities" in res.evidence[0]
def test_ner_endpoint_503_without_ml_extra():
# gliner/torch are not installed in the fast (dev) environment.
client = TestClient(app)
r = client.post("/v1/ner", json={"text": "Klaus Iohannis"})
assert r.status_code == 503
def test_ner_endpoint_with_monkeypatched_model(monkeypatch):
monkeypatch.setattr(ner, "get_model", lambda: FakePredictor(_SAMPLE))
client = TestClient(app)
r = client.post("/v1/ner", json={"text": "Klaus Iohannis și Guvernul României."})
assert r.status_code == 200
body = r.json()
assert body["results"]["count"] == 2
assert body["results"]["entities"][1]["label"] == "instituție publică"

View file

@ -0,0 +1,34 @@
"""Spectrogram DSP tests (synthetic signals, no ffmpeg)."""
from __future__ import annotations
import numpy as np
from extractors.features import spectrogram
def test_compute_features_on_tone():
sr = 16_000
t = np.linspace(0, 1.0, sr, endpoint=False)
pcm = 0.5 * np.sin(2 * np.pi * 440 * t).astype(np.float32)
feats = spectrogram.compute_features(pcm, sr)
assert feats["sample_rate"] == sr
assert abs(feats["duration_s"] - 1.0) < 0.01
assert feats["spectral_centroid_hz_mean"] > 0
assert feats["discontinuities_s"] == []
def test_detects_energy_discontinuity():
sr = 16_000
quiet = np.full(sr // 2, 0.001, dtype=np.float32)
loud = (0.6 * np.sin(2 * np.pi * 300 * np.linspace(0, 0.5, sr // 2))).astype(
np.float32
)
pcm = np.concatenate([quiet, loud])
feats = spectrogram.compute_features(pcm, sr)
assert len(feats["discontinuities_s"]) >= 1
def test_empty_signal():
feats = spectrogram.compute_features(np.array([], dtype=np.float32), 16_000)
assert feats["frames"] == 0

View file

@ -0,0 +1,59 @@
"""ffprobe parser tests (no binary needed — fixture-driven)."""
from __future__ import annotations
from extractors.features import video_meta
_FIXTURE = {
"format": {
"format_name": "mov,mp4,m4a,3gp,3g2,mj2",
"duration": "12.500",
"bit_rate": "2500000",
"tags": {"encoder": "Lavf58.76.100"},
},
"streams": [
{
"codec_type": "video",
"codec_name": "h264",
"width": 1920,
"height": 1080,
"avg_frame_rate": "30000/1001",
"pix_fmt": "yuv420p",
},
{
"codec_type": "audio",
"codec_name": "aac",
"sample_rate": "48000",
"channels": 2,
},
],
}
def test_parse_fps_fractional():
assert video_meta._parse_fps("30000/1001") == 29.97
assert video_meta._parse_fps("25/1") == 25.0
assert video_meta._parse_fps("0/0") is None
assert video_meta._parse_fps(None) is None
def test_parse_ffprobe_full():
res = video_meta.parse_ffprobe(_FIXTURE)
assert res.ok is True
assert res.results["video"]["codec"] == "h264"
assert res.results["video"]["width"] == 1920
assert res.results["video"]["fps"] == 29.97
assert res.results["bitrate_bps"] == 2500000
assert res.results["has_audio"] is True
assert res.results["audio"]["codec"] == "aac"
# Lavf encoder tag should be flagged as a re-encoding tool.
assert any("Lavf" in a for a in res.anomalies)
def test_parse_ffprobe_audio_only():
res = video_meta.parse_ffprobe(
{"format": {"format_name": "wav"}, "streams": [{"codec_type": "audio"}]}
)
assert res.ok is True
assert res.results["has_audio"] is True
assert "video" not in res.results