deepfake-moe / backend /tests /test_analysis.py
Devraj1990's picture
Harden calibrated evidence analysis
c00019b verified
Raw
History Blame Contribute Delete
16.6 kB
from io import BytesIO
import pytest
from PIL import Image, PngImagePlugin
from app.analysis import (
_huggingface_detector_for_model,
_map_classifier_outputs_to_ai_probability,
_model_inference_views,
analyze_image_bytes,
aggregate_verdict,
DetectorSignal,
extract_metadata,
model_ensemble_signal,
)
def _png_bytes(with_marker: bool = False) -> bytes:
image = Image.new("RGB", (96, 96), (120, 80, 200))
output = BytesIO()
pnginfo = PngImagePlugin.PngInfo()
if with_marker:
pnginfo.add_text("Software", "Stable Diffusion")
pnginfo.add_text("prompt", "synthetic portrait generated by a diffusion model")
image.save(output, format="PNG", pnginfo=pnginfo)
return output.getvalue()
def test_analysis_reports_generative_metadata_marker() -> None:
result = analyze_image_bytes(_png_bytes(with_marker=True))
assert result["verdict"]["label"] == "likely_ai_generated"
assert result["technical_appendix"]["metadata"]["generative_markers"]
assert result["technical_appendix"]["hashes"]["sha256"]
def test_analysis_is_cautious_without_strong_signals() -> None:
result = analyze_image_bytes(_png_bytes(with_marker=False))
assert result["verdict"]["label"] in {"inconclusive", "likely_manipulated_or_deepfake"}
assert "Image authenticity cannot be proven from pixels alone." in result["summary"]["limitations"]
def test_analysis_includes_explainable_analytical_layers() -> None:
result = analyze_image_bytes(_png_bytes(with_marker=False))
layers = result["analytical_layers"]
assert len(layers) >= 8
assert {layer["id"] for layer in layers} >= {
"source_provenance",
"visual_model_consensus",
"model_transform_robustness",
"luminance",
"chroma",
"edge_geometry",
"noise_residual",
"compression_ela",
"frequency",
"tile_regions",
}
for layer in layers:
assert layer["question"]
assert layer["method"]
assert layer["conclusion"]
assert 0 <= layer["ai_signal"] <= 1
assert 0 <= layer["manipulation_signal"] <= 1
assert layer["evidence"]
assert layer["decision_role"] in {
"primary_evidence",
"guard_or_supporting_evidence",
"review_context_only",
}
assert 0 <= layer["influence"] <= 1
assert layer["counterfactual"]
assert result["explainability"]["layer_ledger"]["layers"]
assert result["explainability"]["decision_attribution"]
assert result["explainability"]["explanation_contract"]["score_semantics"]
assert result["explainability"]["decision_support"]["primary_drivers"]
assert result["explainability"]["regional_evidence_map"]["grid"] == {"rows": 4, "cols": 4}
tile_layer = next(layer for layer in layers if layer["id"] == "tile_regions")
tile_grid = tile_layer["metrics"]["tile_grid"]
assert len(tile_grid) == 16
assert {tile["row"] for tile in tile_grid} == {1, 2, 3, 4}
assert {tile["col"] for tile in tile_grid} == {1, 2, 3, 4}
assert {tile["severity_band"] for tile in tile_grid} <= {"low", "medium", "high"}
def test_xmp_markers_are_summarized_without_raw_xml() -> None:
image = Image.new("RGB", (96, 96), (120, 80, 200))
output = BytesIO()
image.save(output, format="JPEG")
image_bytes = output.getvalue() + (
b'<x:xmpmeta xmlns:x="adobe:ns:meta/">'
b'<rdf:Description xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/" />'
b"</x:xmpmeta>"
)
metadata = extract_metadata(Image.open(BytesIO(image_bytes)), image_bytes)
software_summary = " ".join(metadata["software_values"])
assert metadata["editing_markers"] == ["photoshop"]
assert "XMP metadata mentions: photoshop" in software_summary
assert "<x:xmpmeta" not in software_summary
assert metadata["xmp_present"] is True
assert "raw XMP omitted" in metadata["xmp_excerpt"]
assert "<x:xmpmeta" not in metadata["xmp_excerpt"]
def test_model_label_mapping_handles_ai_and_human_labels() -> None:
assert _map_classifier_outputs_to_ai_probability(
[{"label": "ai", "score": 0.91}, {"label": "hum", "score": 0.09}]
) == pytest.approx(0.91)
assert _map_classifier_outputs_to_ai_probability(
[{"label": "hum", "score": 0.82}, {"label": "ai", "score": 0.18}]
) == pytest.approx(0.18)
def test_model_label_mapping_sums_multiclass_ai_labels() -> None:
assert _map_classifier_outputs_to_ai_probability(
[
{"label": "Real", "score": 0.23},
{"label": "Deepfake", "score": 0.46},
{"label": "Artificial", "score": 0.31},
]
) == pytest.approx(0.77)
def test_model_label_mapping_handles_human_artificial_labels() -> None:
assert _map_classifier_outputs_to_ai_probability(
[{"label": "human", "score": 0.99}, {"label": "artificial", "score": 0.01}]
) == pytest.approx(0.01)
def test_model_label_mapping_supports_explicit_numeric_labels() -> None:
profile = {"ai_labels": ["label_1"], "real_labels": ["label_0"]}
assert _map_classifier_outputs_to_ai_probability(
[{"label": "LABEL_1", "score": 0.83}, {"label": "LABEL_0", "score": 0.17}],
profile,
) == pytest.approx(0.83)
def test_broad_primary_uses_five_robustness_views() -> None:
image = Image.new("RGB", (320, 240), (80, 110, 140))
views = _model_inference_views(image, {"multi_view": True})
assert [name for name, _ in views] == [
"original",
"center_crop_92pct",
"jpeg_quality_85",
"horizontal_flip",
"social_resize_75pct",
]
assert views[-1][1].size == (240, 180)
def test_model_ensemble_signal_summarizes_votes() -> None:
signal = model_ensemble_signal(
[
DetectorSignal("hf:a", "ok", "model_likely_ai_generated", 0.96, None, "medium", [], 0.55),
DetectorSignal("hf:b", "ok", "model_likely_ai_generated", 0.92, None, "medium", [], 0.55),
DetectorSignal("hf:c", "ok", "model_likely_ai_generated", 0.90, None, "medium", [], 0.55),
]
)
assert signal.label == "ensemble_likely_ai_generated"
assert signal.confidence == "high"
assert signal.ai_probability == pytest.approx(0.869)
def test_model_ensemble_rejects_soft_leaning_ai_consensus() -> None:
signal = model_ensemble_signal(
[
DetectorSignal("hf:a", "ok", "model_likely_ai_generated", 0.99, None, "medium", [], 0.55),
DetectorSignal("hf:b", "ok", "model_inconclusive", 0.69, None, "medium", [], 0.55),
DetectorSignal("hf:c", "ok", "model_inconclusive", 0.62, None, "medium", [], 0.55),
]
)
assert signal.label == "ensemble_inconclusive"
assert signal.confidence == "low"
assert signal.ai_probability == pytest.approx(0.6607)
def test_model_ensemble_accepts_primary_anchored_alignment() -> None:
signal = model_ensemble_signal(
[
DetectorSignal(
"hf:buildborderless/CommunityForensics-DeepfakeDet-ViT",
"ok",
"model_inconclusive",
0.72,
None,
"low",
[],
1.0,
),
DetectorSignal(
"hf:Ateeqq/ai-vs-human-image-detector",
"ok",
"model_likely_ai_generated",
0.999,
None,
"medium",
[],
0.3,
),
DetectorSignal(
"hf:jacoballessio/ai-image-detect-distilled",
"ok",
"model_inconclusive",
0.62,
None,
"low",
[],
0.45,
),
]
)
assert signal.label == "ensemble_likely_ai_generated"
assert signal.confidence == "medium"
assert signal.ai_probability == pytest.approx(0.74)
def test_model_ensemble_is_inconclusive_when_detectors_disagree_strongly() -> None:
signal = model_ensemble_signal(
[
DetectorSignal("hf:a", "ok", "model_likely_ai_generated", 0.99, None, "medium", [], 0.55),
DetectorSignal("hf:b", "ok", "model_likely_human_or_real", 0.22, None, "medium", [], 0.55),
]
)
assert signal.label == "ensemble_inconclusive"
assert signal.confidence == "low"
def test_aggregate_verdict_caps_uncorroborated_model_false_positive() -> None:
detectors = [
DetectorSignal("metadata_provenance", "ok", "inconclusive", 0.5, 0.25, "low", ["No strong metadata signal."], 0.22),
DetectorSignal("compression_noise_forensics", "ok", "no_strong_forensic_signal", 0.38, 0.38, "low", ["Weak forensic signal."], 0.28),
DetectorSignal("hf:a", "ok", "model_likely_ai_generated", 0.99, None, "medium", ["a"], 0.55),
DetectorSignal("hf:b", "ok", "model_inconclusive", 0.61, None, "medium", ["b"], 0.55),
DetectorSignal("open_source_model_ensemble", "ok", "ensemble_inconclusive", 0.8, None, "low", ["disagreement"], 0.9),
]
verdict = aggregate_verdict(
detectors,
metadata={"generative_markers": [], "has_exif": False},
c2pa={"claim": None},
forensics={"artificiality_score": 0.08, "manipulation_score": 0.38},
)
assert verdict["label"] == "inconclusive"
assert verdict["confidence"] == "low"
assert verdict["ai_probability"] <= 0.64
assert any("capped" in item for item in verdict["rationale"])
def test_aggregate_verdict_requires_independent_support_for_two_model_ai_claim() -> None:
detectors = [
DetectorSignal("metadata_provenance", "ok", "inconclusive", 0.5, 0.25, "low", ["No strong metadata signal."], 0.22),
DetectorSignal("compression_noise_forensics", "ok", "no_strong_forensic_signal", 0.34, 0.29, "low", ["Weak forensic signal."], 0.28),
DetectorSignal("hf:a", "ok", "model_likely_ai_generated", 0.99, None, "medium", ["a"], 0.55),
DetectorSignal("hf:b", "ok", "model_likely_ai_generated", 0.88, None, "medium", ["b"], 0.55),
DetectorSignal("open_source_model_ensemble", "ok", "ensemble_likely_ai_generated", 0.935, None, "high", ["consensus"], 0.9),
]
verdict = aggregate_verdict(
detectors,
metadata={"generative_markers": [], "has_exif": False},
c2pa={"claim": None},
forensics={"artificiality_score": 0.06, "manipulation_score": 0.29},
)
assert verdict["label"] == "inconclusive"
assert verdict["confidence"] == "low"
assert verdict["ai_probability"] <= 0.55
assert any("independent metadata" in item for item in verdict["rationale"])
def test_aggregate_verdict_allows_overwhelming_model_consensus_without_real_vote() -> None:
detectors = [
DetectorSignal("metadata_provenance", "ok", "inconclusive", 0.5, 0.25, "low", ["No strong metadata signal."], 0.22),
DetectorSignal("compression_noise_forensics", "ok", "no_strong_forensic_signal", 0.36, 0.29, "low", ["Weak forensic signal."], 0.28),
DetectorSignal("hf:a", "ok", "model_likely_ai_generated", 0.97, None, "medium", ["a"], 0.55),
DetectorSignal("hf:b", "ok", "model_likely_ai_generated", 0.94, None, "medium", ["b"], 0.55),
DetectorSignal("hf:c", "ok", "model_likely_ai_generated", 0.92, None, "medium", ["c"], 0.55),
DetectorSignal("open_source_model_ensemble", "ok", "ensemble_likely_ai_generated", 0.9433, None, "high", ["consensus"], 0.9),
]
verdict = aggregate_verdict(
detectors,
metadata={"generative_markers": [], "has_exif": False, "format": "PNG"},
c2pa={"claim": None},
forensics={"artificiality_score": 0.06, "manipulation_score": 0.29},
)
assert verdict["label"] == "likely_ai_generated"
assert verdict["confidence"] == "medium"
assert verdict["ai_probability"] == pytest.approx(0.72)
def test_aggregate_verdict_allows_primary_anchored_consensus() -> None:
detectors = [
DetectorSignal("metadata_provenance", "ok", "inconclusive", 0.5, 0.25, "low", ["none"], 0.22),
DetectorSignal("compression_noise_forensics", "ok", "no_strong_forensic_signal", 0.36, 0.25, "low", ["weak"], 0.28),
DetectorSignal("hf:buildborderless/CommunityForensics-DeepfakeDet-ViT", "ok", "model_inconclusive", 0.72, None, "low", ["primary"], 1.0),
DetectorSignal("hf:Ateeqq/ai-vs-human-image-detector", "ok", "model_likely_ai_generated", 0.999, None, "medium", ["counter"], 0.3),
DetectorSignal("hf:jacoballessio/ai-image-detect-distilled", "ok", "model_inconclusive", 0.62, None, "low", ["counter"], 0.45),
DetectorSignal("open_source_model_ensemble", "ok", "ensemble_likely_ai_generated", 0.74, None, "medium", ["aligned"], 0.9),
]
verdict = aggregate_verdict(
detectors,
metadata={"generative_markers": [], "has_exif": False, "format": "PNG"},
c2pa={"claim": None},
forensics={"artificiality_score": 0.12, "manipulation_score": 0.25, "quality": {"risk_score": 0.1}},
)
assert verdict["label"] == "likely_ai_generated"
assert verdict["ai_probability"] == pytest.approx(0.72)
assert any("primary-anchored" in item for item in verdict["rationale"])
def test_aggregate_verdict_caps_camera_like_real_photo_even_with_model_consensus() -> None:
detectors = [
DetectorSignal("metadata_provenance", "ok", "camera_metadata_present", 0.35, 0.25, "low", ["EXIF"], 0.22),
DetectorSignal("compression_noise_forensics", "ok", "no_strong_forensic_signal", 0.36, 0.29, "low", ["Weak forensic signal."], 0.28),
DetectorSignal("hf:a", "ok", "model_likely_ai_generated", 0.97, None, "medium", ["a"], 0.55),
DetectorSignal("hf:b", "ok", "model_likely_ai_generated", 0.94, None, "medium", ["b"], 0.55),
DetectorSignal("hf:c", "ok", "model_likely_ai_generated", 0.92, None, "medium", ["c"], 0.55),
DetectorSignal("open_source_model_ensemble", "ok", "ensemble_likely_ai_generated", 0.9433, None, "high", ["consensus"], 0.9),
]
verdict = aggregate_verdict(
detectors,
metadata={"generative_markers": [], "has_exif": True, "format": "JPEG"},
c2pa={"claim": None},
forensics={
"artificiality_score": 0.06,
"manipulation_score": 0.29,
"noise": {"tile_variance_mean": 240.0, "low_noise_hint": 0.0},
"entropy": 6.5,
},
)
assert verdict["label"] != "likely_ai_generated"
assert verdict["ai_probability"] <= 0.48
assert any("real-photo false-positive guard" in item for item in verdict["rationale"])
def test_aggregate_verdict_uses_portrait_specialist_to_prevent_ai_false_positive() -> None:
detectors = [
DetectorSignal("metadata_provenance", "ok", "inconclusive", 0.5, 0.25, "low", ["No strong metadata signal."], 0.22),
DetectorSignal("compression_noise_forensics", "ok", "no_strong_forensic_signal", 0.36, 0.29, "low", ["Weak forensic signal."], 0.28),
DetectorSignal("hf:Ateeqq/ai-vs-human-image-detector", "ok", "model_likely_ai_generated", 0.99, None, "medium", ["a"], 0.72),
DetectorSignal("hf:dima806/ai_vs_real_image_detection", "ok", "model_likely_ai_generated", 0.87, None, "medium", ["b"], 0.35),
DetectorSignal("hf:jacoballessio/ai-image-detect-distilled", "ok", "model_likely_human_or_real", 0.26, None, "medium", ["c"], 0.70),
DetectorSignal("hf:SadraCoding/SDXL-Deepfake-Detector", "ok", "model_likely_human_or_real", 0.0, None, "medium", ["portrait"], 0.65),
DetectorSignal("open_source_model_ensemble", "ok", "ensemble_inconclusive", 0.5, None, "low", ["tie"], 0.9),
]
verdict = aggregate_verdict(
detectors,
metadata={"generative_markers": [], "has_exif": False},
c2pa={"claim": None},
forensics={
"artificiality_score": 0.06,
"manipulation_score": 0.38,
"quality": {"risk_score": 0.28},
},
)
assert verdict["label"] == "inconclusive"
assert verdict["confidence"] == "low"
assert verdict["ai_probability"] <= 0.52
def test_portrait_specialist_is_skipped_when_portrait_gate_is_low() -> None:
signal = _huggingface_detector_for_model(
Image.new("RGB", (256, 256), (30, 40, 50)),
"SadraCoding/SDXL-Deepfake-Detector",
{"portrait_score": 0.42, "evidence": ["low portrait score"]},
)
assert signal.status == "unavailable"
assert signal.label == "portrait_gate_not_met"
assert signal.ai_probability is None