import gradio as gr import torch import numpy as np import cv2 import os from PIL import Image from transformers import pipeline # ----------------------------------------------- # Load the AI image detection model from HuggingFace # Vision Transformer trained to detect AI generated # images vs real photographs # ----------------------------------------------- print("Loading AI detection model... please wait") detector = pipeline( "image-classification", model="umm-maybe/AI-image-detector", device=0 if torch.cuda.is_available() else -1 ) print("Model loaded successfully!") # ----------------------------------------------- # Analyze a single image # ----------------------------------------------- def analyze_image(image): if image is None: return None, "Please upload an image first." try: results = detector(image) top = results[0] label = top["label"] score = top["score"] * 100 if "artificial" in label.lower() or "fake" in label.lower() or "ai" in label.lower(): verdict = "AI GENERATED" description = f"This image appears to be AI generated with {score:.1f}% confidence. The visual patterns, textures, and artifacts are consistent with images produced by generative AI models such as Midjourney, DALL-E, or Stable Diffusion." else: verdict = "REAL IMAGE" description = f"This image appears to be authentic with {score:.1f}% confidence. The visual characteristics are consistent with a genuine photograph taken by a camera." bar_length = 20 filled = int((score / 100) * bar_length) bar = "█" * filled + "░" * (bar_length - filled) result_text = f"""## {verdict} **Confidence:** {score:.1f}% `{bar}` {score:.1f}% --- **Analysis:** {description} --- **Model:** umm-maybe/AI-image-detector (Vision Transformer) *No AI detector is 100% accurate. Use this as a guide, not a definitive verdict.*""" return image, result_text except Exception as e: return image, f"Error analyzing image: {str(e)}" # ----------------------------------------------- # Extract frames from a video file # ----------------------------------------------- def extract_frames(video_path, max_frames=10): cap = cv2.VideoCapture(video_path) total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) fps = cap.get(cv2.CAP_PROP_FPS) duration = total_frames / fps if fps > 0 else 0 interval = max(1, total_frames // max_frames) frames = [] frame_count = 0 while True: ret, frame = cap.read() if not ret: break if frame_count % interval == 0 and len(frames) < max_frames: rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) pil_image = Image.fromarray(rgb_frame) frames.append(pil_image) frame_count += 1 cap.release() return frames, duration, fps # ----------------------------------------------- # Analyze a video by checking multiple frames # ----------------------------------------------- def analyze_video(video_path): if video_path is None: return "Please upload a video first." try: frames, duration, fps = extract_frames(video_path, max_frames=10) if len(frames) == 0: return "Could not extract frames from this video. Please try a different file." ai_scores = [] real_scores = [] frame_results = [] for i, frame in enumerate(frames): results = detector(frame) for r in results: lbl = r["label"].lower() sc = r["score"] if "artificial" in lbl or "fake" in lbl or "ai" in lbl: ai_scores.append(sc) else: real_scores.append(sc) top = results[0] is_ai = "artificial" in top["label"].lower() or "fake" in top["label"].lower() frame_results.append({ "frame": i + 1, "verdict": "AI" if is_ai else "Real", "confidence": top["score"] * 100 }) avg_ai = np.mean(ai_scores) * 100 if ai_scores else 0 avg_real = np.mean(real_scores) * 100 if real_scores else 0 ai_frame_count = sum(1 for r in frame_results if r["verdict"] == "AI") if ai_frame_count > len(frames) / 2: verdict = "AI GENERATED VIDEO" confidence = avg_ai description = f"This video appears to be AI generated. {ai_frame_count} out of {len(frames)} sampled frames showed AI-generated characteristics." else: verdict = "REAL VIDEO" confidence = avg_real description = f"This video appears to be authentic. {len(frames) - ai_frame_count} out of {len(frames)} sampled frames showed real characteristics." bar_length = 20 filled = int((confidence / 100) * bar_length) bar = "█" * filled + "░" * (bar_length - filled) frame_table = "| Frame | Verdict | Confidence |\n|-------|---------|------------|\n" for r in frame_results: frame_table += f"| {r['frame']} | {r['verdict']} | {r['confidence']:.1f}% |\n" result_text = f"""## {verdict} **Overall Confidence:** {confidence:.1f}% `{bar}` {confidence:.1f}% --- **Analysis:** {description} **Video Info:** {duration:.1f}s duration | {fps:.0f} FPS | {len(frames)} frames analyzed --- ### Frame-by-Frame Breakdown {frame_table} --- **Model:** umm-maybe/AI-image-detector (Vision Transformer) *Video analysis is based on frame sampling. Results may vary with compression artifacts.*""" return result_text except Exception as e: return f"Error analyzing video: {str(e)}" # ----------------------------------------------- # Custom CSS # ----------------------------------------------- custom_css = """ @import url('https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Sans:ital,wght@0,300;0,400;0,500;1,300&display=swap'); * { box-sizing: border-box; } :root { --ink: #0f0e0d; --paper: #f5f0e8; --cream: #ede8dc; --accent: #c8392b; --muted: #7a7469; --border: #d4cfc4; } html, body { background: var(--paper) !important; } .gradio-container { font-family: 'DM Sans', sans-serif !important; background: var(--paper) !important; max-width: 100% !important; margin: 0 !important; color: var(--ink) !important; padding: 0 2rem !important; } body, .gradio-container, .main, .wrap, .block { background: var(--paper) !important; color: var(--ink) !important; } p, span, label, h1, h2, h3, h4 { color: var(--ink) !important; } strong { color: var(--ink) !important; font-weight: 600 !important; } code, pre { color: var(--accent) !important; background: transparent !important; } .prose *, .markdown-body *, .result-panel * { color: var(--ink) !important; } /* Table */ table, tr, td, th { color: var(--ink) !important; background: var(--cream) !important; border-color: var(--border) !important; } th { background: var(--border) !important; color: var(--ink) !important; font-family: 'Syne', sans-serif !important; font-size: 0.72rem !important; letter-spacing: 0.08em !important; text-transform: uppercase !important; } /* Tabs */ .tab-nav { border-bottom: 1.5px solid var(--border) !important; background: transparent !important; margin-bottom: 2.5rem !important; } .tab-nav button, .tab-nav button span, div[role="tablist"] button, div[role="tablist"] button span, button[role="tab"], button[role="tab"] span { font-family: 'Syne', sans-serif !important; font-weight: 600 !important; font-size: 0.82rem !important; letter-spacing: 0.06em !important; text-transform: uppercase !important; color: #0f0e0d !important; opacity: 1 !important; border: none !important; border-bottom: 2px solid transparent !important; border-radius: 0 !important; padding: 0.8rem 1.4rem !important; background: transparent !important; transition: all 0.2s !important; } div[role="tablist"] button:hover, div[role="tablist"] button:hover span, button[role="tab"]:hover, button[role="tab"]:hover span { color: #2563eb !important; border-bottom-color: #2563eb !important; opacity: 1 !important; } div[role="tablist"] button[aria-selected="true"], div[role="tablist"] button[aria-selected="true"] span, button[role="tab"][aria-selected="true"], button[role="tab"][aria-selected="true"] span { color: #2563eb !important; border-bottom-color: #2563eb !important; opacity: 1 !important; } /* Upload area */ .upload-region { border: 1.5px dashed var(--border) !important; border-radius: 3px !important; background: var(--cream) !important; transition: all 0.2s !important; min-height: 280px !important; } .upload-region:hover { border-color: var(--accent) !important; background: #fdf5f3 !important; } /* Analyze button */ button.lg.primary { font-family: 'Syne', sans-serif !important; font-weight: 700 !important; font-size: 0.82rem !important; letter-spacing: 0.1em !important; text-transform: uppercase !important; background: var(--ink) !important; color: var(--paper) !important; border: none !important; border-radius: 3px !important; padding: 0.9rem 2rem !important; transition: all 0.2s !important; width: 100% !important; } button.lg.primary:hover { background: var(--accent) !important; transform: translateY(-1px) !important; } /* Result panel */ .result-panel { background: var(--cream) !important; border: 1.5px solid var(--border) !important; border-radius: 3px !important; padding: 1.5rem !important; min-height: 280px; animation: fadeUp 0.4s ease; } @keyframes fadeUp { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } } .result-panel h2 { font-family: 'Syne', sans-serif !important; font-size: 1.4rem !important; font-weight: 800 !important; letter-spacing: -0.02em !important; margin-bottom: 0.8rem !important; color: var(--ink) !important; } .result-panel code { font-size: 0.78rem !important; color: var(--accent) !important; background: transparent !important; } /* Tip bar */ .tip-bar { margin-top: 1.5rem; padding: 0.9rem 1.2rem; background: var(--cream); border-left: 2px solid var(--accent); border-radius: 0 3px 3px 0; font-size: 0.82rem; color: var(--muted); line-height: 1.6; } /* Stats row */ .stats-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.5px; background: var(--border); border: 1.5px solid var(--border); border-radius: 3px; overflow: hidden; margin-top: 3.5rem; } .stat-cell { background: var(--cream); padding: 1.4rem 1.8rem; text-align: center; } .stat-number { font-family: 'Syne', sans-serif; font-size: 2rem; font-weight: 800; letter-spacing: -0.04em; color: var(--ink); display: block; } .stat-desc { font-size: 0.75rem; color: var(--muted); letter-spacing: 0.05em; text-transform: uppercase; margin-top: 3px; display: block; } /* Hero */ .site-header { display: flex; justify-content: space-between; align-items: center; padding: 1.4rem 0; border-bottom: 1.5px solid var(--border); margin-bottom: 3.5rem; } .site-logo { font-family: 'Syne', sans-serif; font-weight: 800; font-size: 1.25rem; letter-spacing: -0.02em; color: var(--ink); display: flex; align-items: center; gap: 10px; } .logo-indicator { width: 8px; height: 8px; background: var(--accent); border-radius: 50%; animation: pulse 2s ease-in-out infinite; } @keyframes pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.4; transform: scale(0.75); } } .hero-section { display: grid; grid-template-columns: 1.1fr 0.9fr; gap: 3rem; align-items: center; margin-bottom: 4rem; } .hero-eyebrow { font-size: 0.68rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--accent); font-weight: 500; margin-bottom: 1rem; display: flex; align-items: center; gap: 10px; } .hero-eyebrow::before { content: ''; display: block; width: 28px; height: 1.5px; background: var(--accent); } .hero-title { font-family: 'Syne', sans-serif !important; font-size: 3.6rem !important; font-weight: 800 !important; line-height: 1.0 !important; letter-spacing: -0.04em !important; color: var(--ink) !important; margin-bottom: 1.4rem !important; } .hero-title em { font-style: italic; color: var(--muted); font-weight: 400; } .hero-body { font-size: 0.92rem; color: var(--muted); line-height: 1.75; max-width: 320px; } .scan-visual { border: 1.5px solid var(--border); border-radius: 3px; background: var(--cream); height: 240px; position: relative; overflow: hidden; display: flex; align-items: center; justify-content: center; } .scan-grid-bg { position: absolute; inset: 0; background-image: linear-gradient(var(--border) 1px, transparent 1px), linear-gradient(90deg, var(--border) 1px, transparent 1px); background-size: 36px 36px; opacity: 0.5; } .scan-sweep { position: absolute; left: 0; right: 0; height: 1.5px; background: linear-gradient(90deg, transparent 0%, var(--accent) 50%, transparent 100%); animation: sweep 3s ease-in-out infinite; } @keyframes sweep { 0% { top: 0%; opacity: 0; } 8% { opacity: 1; } 92% { opacity: 1; } 100% { top: 100%; opacity: 0; } } .scan-corner { position: absolute; width: 14px; height: 14px; border-color: var(--accent); border-style: solid; } .sc-tl { top: 14px; left: 14px; border-width: 2px 0 0 2px; } .sc-tr { top: 14px; right: 14px; border-width: 2px 2px 0 0; } .sc-bl { bottom: 14px; left: 14px; border-width: 0 0 2px 2px; } .sc-br { bottom: 14px; right: 14px; border-width: 0 2px 2px 0; } .scan-status { font-family: 'Syne', sans-serif; font-size: 0.65rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); position: relative; z-index: 2; } .section-divider { font-size: 0.68rem; letter-spacing: 0.16em; text-transform: uppercase; color: var(--muted); display: flex; align-items: center; gap: 14px; margin-bottom: 1.5rem; } .section-divider::after { content: ''; flex: 1; height: 1px; background: var(--border); } .site-footer { margin-top: 3.5rem; padding: 1.5rem 0; border-top: 1.5px solid var(--border); display: flex; justify-content: space-between; align-items: center; font-size: 0.78rem; color: var(--muted); } .footer-name { font-family: 'Syne', sans-serif; font-weight: 600; color: var(--ink); } label.block span { font-family: 'Syne', sans-serif !important; font-size: 0.72rem !important; font-weight: 600 !important; letter-spacing: 0.1em !important; text-transform: uppercase !important; color: var(--muted) !important; } footer { display: none !important; } """ # ----------------------------------------------- # Build the Gradio UI # ----------------------------------------------- with gr.Blocks( css=custom_css, title="TruthLens", theme=gr.themes.Base( primary_hue="orange", neutral_hue="stone", font=gr.themes.GoogleFont("DM Sans"), font_mono=gr.themes.GoogleFont("DM Mono"), text_size=gr.themes.sizes.text_md, ).set( body_background_fill="#f5f0e8", body_text_color="#0f0e0d", block_background_fill="#ede8dc", block_label_text_color="#7a7469", block_title_text_color="#0f0e0d", input_background_fill="#ede8dc", button_primary_background_fill="#0f0e0d", button_primary_text_color="#f5f0e8", button_primary_background_fill_hover="#c8392b", ) ) as app: gr.HTML("""
Upload any image or video. Our Vision Transformer model analyzes it in seconds and tells you whether it was created by a human or an AI system.