deepdetection / src /explainability /explainer.py
akagtag's picture
Fix Gradio entrypoint and switch explainability to Gemini
604836a
Raw
History Blame Contribute Delete
4.37 kB
from __future__ import annotations
import logging
import os
from typing import Any
from src.types import DetectionResponse, EngineResult
logger = logging.getLogger(__name__)
SYSTEM_INSTRUCTION = (
"You are a deepfake forensics analyst writing reports for security professionals. "
"Given detection engine outputs, write exactly 2-3 sentences in plain English "
"explaining why the content is real or fake. "
"Be specific and name the strongest signals. "
"Use direct declarative sentences. "
"Output only the explanation text."
)
DEFAULT_MODEL_CANDIDATES = (
"gemini-3.1-flash-lite-preview",
"gemini-2.5-flash",
)
_configured_candidates = [
value.strip()
for value in os.environ.get("GEMINI_MODEL_CANDIDATES", "").split(",")
if value.strip()
]
MODEL_CANDIDATES = (
tuple(_configured_candidates)
if _configured_candidates
else DEFAULT_MODEL_CANDIDATES
)
REQUEST_TIMEOUT_S = float(os.environ.get("GEMINI_REQUEST_TIMEOUT_S", "20"))
MAX_MODEL_ATTEMPTS = max(1, int(os.environ.get("GEMINI_MAX_MODEL_ATTEMPTS", "3")))
TEMPERATURE = float(os.environ.get("GEMINI_EXPLAIN_TEMPERATURE", "0.3"))
TOP_P = float(os.environ.get("GEMINI_EXPLAIN_TOP_P", "0.95"))
MAX_TOKENS = int(os.environ.get("GEMINI_EXPLAIN_MAX_TOKENS", "300"))
_client: Any | None = None
def _get_api_key() -> str:
return os.environ.get("GEMINI_API_KEY", "").strip()
def _get_client():
global _client
if _client is not None:
return _client
api_key = _get_api_key()
if not api_key:
raise RuntimeError("GEMINI_API_KEY is not configured")
try:
from google import genai
except Exception as exc:
raise RuntimeError("google-genai package is not installed") from exc
_client = genai.Client(api_key=api_key)
return _client
def _generate(prompt: str) -> str:
client = _get_client()
last_error: Exception | None = None
try:
from google.genai import types
except Exception as exc:
raise RuntimeError("google-genai types module is unavailable") from exc
for model_name in MODEL_CANDIDATES[:MAX_MODEL_ATTEMPTS]:
try:
response = client.models.generate_content(
model=model_name,
contents=prompt,
config=types.GenerateContentConfig(
system_instruction=SYSTEM_INSTRUCTION,
temperature=TEMPERATURE,
top_p=TOP_P,
max_output_tokens=MAX_TOKENS,
),
)
content = getattr(response, "text", None)
if content and content.strip():
logger.info("Gemini explain model selected: %s", model_name)
return content.strip()
except Exception as exc:
last_error = exc
logger.debug("Gemini explain model %s failed: %s", model_name, exc)
if last_error is not None:
raise last_error
raise RuntimeError("No Gemini model candidates succeeded")
def explain(
verdict: str,
confidence: float,
engine_results: list[EngineResult],
generator: str,
) -> str:
breakdown = "\n".join(
f"- {result.engine}: {result.verdict} ({result.confidence:.0%}) - {result.explanation}"
for result in engine_results
)
prompt = (
f"Verdict: {verdict} ({confidence:.0%} confidence)\n"
f"Attributed generator: {generator}\n"
f"Engine breakdown:\n{breakdown}\n\n"
"Write the forensics explanation."
)
try:
return _generate(prompt)
except Exception as exc:
logger.error("Gemini explain failed: %s", exc)
top = engine_results[0] if engine_results else None
primary = f"Primary signal came from the {top.engine} engine." if top else ""
return (
f"Content classified as {verdict} with {confidence:.0%} confidence. "
f"Attributed generator: {generator}. "
f"{primary}"
).strip()
class Explainer:
"""Compatibility wrapper for legacy callers expecting an object API."""
def explain(self, response: DetectionResponse) -> str:
return explain(
verdict=response.verdict,
confidence=response.confidence,
engine_results=response.engine_breakdown,
generator=response.attributed_generator,
)