Spaces:
Paused
Paused
| 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, | |
| ) | |