from __future__ import annotations import os class ExplainModule: """Google Gemini text explainer.""" def __init__(self): self.model = os.environ.get("GEMINI_EXPLAIN_MODEL", "gemini-3.1-flash-lite-preview") self.client = None try: from google import genai self.client = genai.Client(api_key=os.environ.get("GEMINI_API_KEY", "")) except Exception as exc: print(f"ExplainModule using Gemini fallback: {exc}") def explain( self, fakescore, s1, s2, s3, weights, attribution, segments, top_generator, ) -> str: verdict = "FAKE" if fakescore > 0.5 else "REAL" confidence = ( "high" if abs(fakescore - 0.5) > 0.3 else "moderate" if abs(fakescore - 0.5) > 0.15 else "low" ) seg_text = "" if segments: seg_text = "Flagged timestamps: " + ", ".join( [f"{segment['time']}s (score={segment['score']})" for segment in segments[:5]] ) attr_text = "" if attribution: top3 = sorted(attribution.items(), key=lambda item: -item[1])[:3] attr_text = "Top generators: " + ", ".join( [f"{name}: {prob * 100:.1f}%" for name, prob in top3] ) prompt = f"""You are a forensic AI analyst. Analyze these deepfake detection results. Be specific about evidence. Results: - Verdict: {verdict} (FakeScore: {fakescore:.3f}, confidence: {confidence}) - Lip-Sync (M1): {s1:.3f} (weight: {weights.get('lip_sync', 'N/A')}) - Fingerprint (M2): {s2:.3f} (weight: {weights.get('fingerprint', 'N/A')}) - Graph-GNN (M3): {s3:.3f} (weight: {weights.get('graph_gnn', 'N/A')}) {seg_text} {attr_text} - Most likely generator: {top_generator} Write 3-5 sentences. Reference specific scores and timestamps.""" try: if self.client is None: raise RuntimeError("Gemini client unavailable") from google.genai import types response = self.client.models.generate_content( model=self.model, contents=prompt, config=types.GenerateContentConfig( system_instruction="You are a forensic deepfake analyst. Be precise.", max_output_tokens=300, temperature=0.3, ), ) return (response.text or "").strip() except Exception: return self._fallback(verdict, fakescore, s1, s2, s3, top_generator, confidence) def _fallback(self, verdict, fakescore, s1, s2, s3, top_gen, conf): if verdict == "FAKE": return ( f"Video classified as {verdict} with {conf} confidence " f"(FakeScore: {fakescore:.3f}). " f"Lip-sync scored {s1:.2f}, indicating " f"{'significant' if s1 > 0.7 else 'moderate' if s1 > 0.5 else 'minimal'} " f"audio-visual inconsistency. " f"Style fingerprinting scored {s2:.2f}, top attribution: {top_gen}. " f"Graph analysis scored {s3:.2f}." ) return ( f"Video classified as {verdict} with {conf} confidence " f"(FakeScore: {fakescore:.3f}). " f"All modules returned scores below detection threshold." )