deepfake-hunter / app.py
sammoftah's picture
Fix Deepfake Hunter build
71ba0ee verified
"""
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)