""" Deepfake Hunter Lightweight media-forensics signal explorer for Hugging Face CPU Spaces. """ import os import sys from typing import Dict, Tuple import gradio as gr import numpy as np import plotly.graph_objects as go from PIL import Image, ImageDraw, ImageFilter sys.path.append(os.path.join(os.path.dirname(__file__), '..')) from shared.components import create_method_panel, create_premium_hero def normalize_image(image: np.ndarray) -> np.ndarray: arr = np.asarray(image).astype(np.float32) if arr.max() > 1: arr = arr / 255.0 if arr.ndim == 2: arr = np.stack([arr, arr, arr], axis=-1) return arr[:, :, :3] def compute_signal_scores(image: np.ndarray) -> Dict[str, float]: arr = normalize_image(image) gray = arr.mean(axis=2) # High-frequency residual approximates compression / synthesis artifacts. pil_gray = Image.fromarray((gray * 255).astype(np.uint8)) blurred = np.asarray(pil_gray.filter(ImageFilter.GaussianBlur(radius=1.5))).astype(np.float32) / 255.0 residual = np.abs(gray - blurred) artifact = float(np.clip(residual.mean() * 8.0, 0, 1)) # Channel disagreement can indicate odd blending or color-space artifacts. channel_gap = np.mean([ np.abs(arr[:, :, 0] - arr[:, :, 1]).mean(), np.abs(arr[:, :, 1] - arr[:, :, 2]).mean(), np.abs(arr[:, :, 0] - arr[:, :, 2]).mean(), ]) color_consistency = float(np.clip(channel_gap * 3.0, 0, 1)) # Edge irregularity uses simple gradients, avoiding OpenCV dependencies. gx = np.diff(gray, axis=1, append=gray[:, -1:]) gy = np.diff(gray, axis=0, append=gray[-1:, :]) edge_energy = np.sqrt(gx * gx + gy * gy) edge_irregularity = float(np.clip(edge_energy.std() * 5.0, 0, 1)) # Texture flatness catches over-smoothed generated faces/backgrounds. texture_flatness = float(np.clip(1.0 - gray.std() * 4.0, 0, 1)) return { "Artifact residual": artifact, "Color-channel mismatch": color_consistency, "Edge irregularity": edge_irregularity, "Texture flatness": texture_flatness, } def aggregate_score(scores: Dict[str, float], context: str) -> float: weights = { "Artifact residual": 0.34, "Color-channel mismatch": 0.22, "Edge irregularity": 0.24, "Texture flatness": 0.20, } score = sum(scores[key] * weight for key, weight in weights.items()) if context in {"Legal Evidence", "News Interview", "Political Speech"}: score = min(1.0, score * 1.08) return float(score) def create_gauge(score: float) -> go.Figure: if score < 0.32: color, label = "#15803d", "LOW SIGNAL" elif score < 0.62: color, label = "#e8935c", "REVIEW" else: color, label = "#991b1b", "HIGH SIGNAL" fig = go.Figure(go.Indicator( mode="gauge+number", value=score * 100, title={"text": label, "font": {"size": 22, "color": color}}, number={"suffix": "%", "font": {"size": 42, "color": color}}, gauge={ "axis": {"range": [0, 100]}, "bar": {"color": color}, "steps": [ {"range": [0, 32], "color": "#dcfce7"}, {"range": [32, 62], "color": "#ffedd5"}, {"range": [62, 100], "color": "#fee2e2"}, ], }, )) fig.update_layout(height=280, margin=dict(l=20, r=20, t=60, b=20), paper_bgcolor="white") return fig def create_breakdown(scores: Dict[str, float]) -> go.Figure: labels = list(scores.keys()) values = [scores[label] * 100 for label in labels] colors = ["#ffad7a", "#b8a9d9", "#7accff", "#e8935c"] fig = go.Figure(go.Bar( x=values, y=labels, orientation="h", marker=dict(color=colors), text=[f"{value:.1f}%" for value in values], textposition="outside", )) fig.update_layout( title="Detection Signal Breakdown", xaxis=dict(range=[0, 105], title="Signal strength"), height=280, margin=dict(l=20, r=20, t=50, b=20), paper_bgcolor="white", plot_bgcolor="white", ) return fig def annotate_image(image: np.ndarray, score: float) -> np.ndarray: arr = np.asarray(image).astype(np.uint8) pil = Image.fromarray(arr) draw = ImageDraw.Draw(pil) label = "Review suggested" if score >= 0.32 else "Low manipulation signal" color = (232, 147, 92) if score >= 0.32 else (21, 128, 61) draw.rounded_rectangle([14, 14, 300, 58], radius=8, fill=color) draw.text((26, 27), f"{label}: {score:.0%}", fill=(255, 255, 255)) return np.asarray(pil) def explain(scores: Dict[str, float], score: float, context: str) -> str: strongest = sorted(scores.items(), key=lambda item: item[1], reverse=True)[:2] parts = ", ".join(f"{name.lower()} ({value:.0%})" for name, value in strongest) if score < 0.32: verdict = "The image has low signal under these lightweight checks." elif score < 0.62: verdict = "The image deserves human review; signals are mixed rather than decisive." else: verdict = "The image shows elevated manipulation-like signals and should be reviewed carefully." return ( f"{verdict}\n\n" f"Strongest cues: {parts}.\n\n" f"Context: {context}. This Space is a signal explorer, not an authenticity oracle." ) def analyze_image(image: np.ndarray, context: str) -> Tuple[np.ndarray, go.Figure, go.Figure, str]: if image is None: empty = go.Figure() return None, empty, empty, "Upload an image to inspect manipulation signals." scores = compute_signal_scores(image) score = aggregate_score(scores, context) return annotate_image(image, score), create_gauge(score), create_breakdown(scores), explain(scores, score, context) with gr.Blocks(title="Deepfake Hunter", theme=gr.themes.Soft()) as app: create_premium_hero( "Deepfake Hunter", "Inspect image manipulation signals with visual overlays, confidence-style gauges, and explainable detection cues.", "🎭", badge="Media Forensics", highlights=["Signal breakdown", "Visual overlay", "Responsible AI framing"], ) create_method_panel({ "Technique": "Lightweight artifact, color, edge, and texture checks combined into an explainable score.", "What it proves": "Safety-sensitive AI should expose evidence and uncertainty instead of overconfident labels.", "Community value": "A teaching Space for media literacy and detection-model evaluation.", }) with gr.Row(): with gr.Column(scale=1): image_input = gr.Image(label="Upload Image", type="numpy") context_input = gr.Dropdown( ["General", "Political Speech", "News Interview", "Legal Evidence", "Entertainment", "Satire/Parody"], value="General", label="Review Context", ) analyze_btn = gr.Button("Analyze Image", variant="primary") with gr.Column(scale=1): image_output = gr.Image(label="Annotated Image") with gr.Row(): gauge_output = gr.Plot(label="Signal Gauge") breakdown_output = gr.Plot(label="Signal Breakdown") explanation_output = gr.Textbox(label="Explanation", lines=6) gr.Markdown(""" ## Reading The Output This Space is intentionally conservative. A high score means "inspect this carefully", not "this is definitely fake". The value is in the signal breakdown: it shows which visual cues contributed to the review recommendation. """) analyze_btn.click( analyze_image, inputs=[image_input, context_input], outputs=[image_output, gauge_output, breakdown_output, explanation_output], ) if __name__ == "__main__": app.launch(server_name="0.0.0.0", server_port=7860)