Spaces:
Running
Running
| import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; | |
| import { | |
| AlertTriangle, | |
| BadgeCheck, | |
| BrainCircuit, | |
| CheckCircle2, | |
| Cpu, | |
| Database, | |
| Download, | |
| Eye, | |
| EyeOff, | |
| FileJson, | |
| FileText, | |
| Gauge, | |
| ImageUp, | |
| Layers3, | |
| Link, | |
| Loader2, | |
| Lock, | |
| KeyRound, | |
| RefreshCw, | |
| ShieldCheck, | |
| ShieldAlert, | |
| Trash2, | |
| Workflow, | |
| } from 'lucide-react'; | |
| const API_BASE = | |
| import.meta.env.VITE_API_BASE_URL || | |
| (window.location.port === '5173' || window.location.port === '4173' ? 'http://127.0.0.1:8000' : window.location.origin); | |
| const ACCESS_TOKEN_ENABLED = import.meta.env.VITE_REQUIRE_ACCESS_TOKEN === 'true'; | |
| const verdictCopy = { | |
| likely_real: 'Likely Real', | |
| likely_ai_generated: 'Likely AI Generated', | |
| likely_manipulated_or_deepfake: 'Likely Manipulated', | |
| inconclusive: 'Inconclusive', | |
| }; | |
| const architectureStages = [ | |
| { | |
| title: 'Private Intake', | |
| detail: 'Upload or public URL, consent gate, MIME validation, SSRF protection, and encrypted temporary media.', | |
| checks: ['15 MB and pixel limits', 'Public URL restrictions', '15-minute media TTL'], | |
| icon: Lock, | |
| }, | |
| { | |
| title: 'Provenance First', | |
| detail: 'EXIF/XMP, C2PA content credentials, software markers, source context, and cryptographic hashes.', | |
| checks: ['Missing metadata stays neutral', 'C2PA claims are verified', 'GPS is redacted in reports'], | |
| icon: Database, | |
| }, | |
| { | |
| title: 'Calibrated MoE', | |
| detail: 'Community Forensics runs across five benign transforms, then two independent models provide counter-opinions.', | |
| checks: ['Model-specific thresholds', 'Five-view stability check', 'Raw logits never equal truth'], | |
| icon: BrainCircuit, | |
| }, | |
| { | |
| title: 'Safety Arbiter', | |
| detail: 'Calibrated stances are combined with provenance and forensic counter-evidence before any verdict is emitted.', | |
| checks: ['Primary-anchored consensus', 'Real-vote false-positive guard', 'Abstention when evidence conflicts'], | |
| icon: ShieldAlert, | |
| }, | |
| { | |
| title: 'Evidence Report', | |
| detail: 'A victim-friendly summary and technical PDF/JSON ledger preserve the reasoning, caveats, and reproducibility data.', | |
| checks: ['Decision and counter-evidence', 'Model and layer ledgers', 'Early deletion endpoint'], | |
| icon: FileText, | |
| }, | |
| ]; | |
| const expertPanels = [ | |
| { | |
| title: 'Broad Primary', | |
| detail: 'Community Forensics checks original, crop, JPEG, mirrored, and social-resize views before its stance is trusted.', | |
| signals: ['Five-view median', 'MAD/IQR stability', 'Broad generator coverage'], | |
| guardrail: 'A stable primary score still cannot decide the verdict alone.', | |
| }, | |
| { | |
| title: 'Counter-Models', | |
| detail: 'Two independently trained classifiers challenge the primary with different data and decision boundaries.', | |
| signals: ['Hard AI vote', 'Real/human vote', 'Abstention-band vote'], | |
| guardrail: 'A real vote or strong disagreement blocks an overconfident accusation.', | |
| }, | |
| { | |
| title: 'Forensic Residuals', | |
| detail: 'Image layers are decomposed into compression, noise, edge, frequency, and tile-level evidence.', | |
| signals: ['ELA score', 'Noise inconsistency', 'Regional anomaly severity'], | |
| guardrail: 'Forensics are weak supporting signals, never final proof alone.', | |
| }, | |
| { | |
| title: 'Provenance', | |
| detail: 'Metadata and content credentials are checked before pixel-based conclusions are trusted.', | |
| signals: ['EXIF/XMP fields', 'C2PA status', 'SHA/perceptual hashes'], | |
| guardrail: 'Missing metadata is treated as neutral, not proof of AI generation.', | |
| }, | |
| ]; | |
| const moeExperts = [ | |
| { | |
| title: 'Community Forensics', | |
| detail: 'Broad ViT primary trained on a highly diverse synthetic-image corpus and evaluated across five benign transforms.', | |
| output: 'Primary stance + stability', | |
| icon: Cpu, | |
| }, | |
| { | |
| title: 'Ateeqq Counter-Model', | |
| detail: 'Independent visual classifier with a strict AI threshold and reduced reliability weight.', | |
| output: 'Hard AI / real / abstain', | |
| icon: BrainCircuit, | |
| }, | |
| { | |
| title: 'Distilled Counter-Model', | |
| detail: 'A lightweight detector with a different boundary that widens model diversity.', | |
| output: 'Independent counter-opinion', | |
| icon: Gauge, | |
| }, | |
| { | |
| title: 'Forensic Expert', | |
| detail: 'Noise, ELA, frequency, edge, and regional-map evidence that supports or challenges model scores.', | |
| output: 'Non-model evidence', | |
| icon: Layers3, | |
| }, | |
| { | |
| title: 'Provenance Expert', | |
| detail: 'EXIF/XMP, C2PA status, generative markers, public URL context, and hashes.', | |
| output: 'Source context', | |
| icon: Database, | |
| }, | |
| ]; | |
| const moeRules = [ | |
| 'The broad primary must remain stable across original, crop, JPEG, mirror, and social-resize views.', | |
| 'Each raw score enters a model-specific AI, real, or abstention band.', | |
| 'Primary-anchored model-only consensus requires every counter-expert to lean AI.', | |
| 'A real/human vote, poor input quality, or disagreement lowers the evidence score.', | |
| 'The safety arbiter preserves inconclusive when independent evidence does not agree.', | |
| ]; | |
| const deploymentControls = [ | |
| { | |
| title: 'Ephemeral Storage', | |
| detail: 'Encrypted temp media, short TTLs, early delete endpoint, no raw media returned.', | |
| checks: ['15-minute raw-media TTL', '24-hour job/report metadata', 'User-triggered deletion'], | |
| }, | |
| { | |
| title: 'Public Safety', | |
| detail: 'No face search, no doxxing, no login scraping, no private identity inference.', | |
| checks: ['Public links only', 'No identity attribution', 'Sensitive-preview blur'], | |
| }, | |
| { | |
| title: 'Runtime Guardrails', | |
| detail: 'Rate limits, privacy-safe audit identifiers, readiness checks, security headers, and background execution.', | |
| checks: ['Health and readiness probes', 'Per-client rate limiting', 'No raw media in logs'], | |
| }, | |
| { | |
| title: 'Scale Profile', | |
| detail: 'The same API supports PostgreSQL and Redis/RQ when deployed across multiple workers and instances.', | |
| checks: ['Server database validation', 'Queue-backed workers', 'Deployment-time safety checks'], | |
| }, | |
| ]; | |
| const reportFeatures = [ | |
| { | |
| title: 'Victim Summary', | |
| detail: 'Plain-language verdict, confidence band, strongest evidence, and practical next steps.', | |
| includes: ['Verdict and confidence', 'Strongest evidence', 'Limitations and next steps'], | |
| icon: FileText, | |
| }, | |
| { | |
| title: 'Evidence Ledger', | |
| detail: 'Eleven layers expose provenance, model robustness, forensic residuals, regional differences, and uncertainty.', | |
| includes: ['Decision role and reliability', 'Direction and influence', 'Counterfactual and limitations'], | |
| icon: Layers3, | |
| }, | |
| { | |
| title: 'Model Arbitration', | |
| detail: 'Primary and counter-model scores, five-view stability, calibrated stances, attribution, and arbiter policy.', | |
| includes: ['Raw versus calibrated scores', 'Five-view MAD/IQR stability', 'Abstention rationale'], | |
| icon: BrainCircuit, | |
| }, | |
| { | |
| title: 'Technical Export', | |
| detail: 'JSON and PDF downloads with hashes, detector scores, regional map, and reproducibility notes.', | |
| includes: ['SHA/perceptual hashes', 'Detector scores', 'Reproducibility notes'], | |
| icon: Download, | |
| }, | |
| ]; | |
| const heroHighlights = [ | |
| ['Calibrated', 'Model-specific gates turn raw logits into AI, real, or abstain stances.'], | |
| ['Victim-safe', 'The arbiter preserves uncertainty instead of turning weak signals into accusations.'], | |
| ['Private', 'Raw media expires after 15 minutes and is never used for face or identity search.'], | |
| ]; | |
| const methodFacts = [ | |
| ['5 views', 'Primary stability check', Cpu], | |
| ['3 models', 'One primary, two counter-experts', BrainCircuit], | |
| ['11 layers', 'Provenance, transforms, residuals, regions', Layers3], | |
| ['15 min', 'Raw-media deletion window', Lock], | |
| ['PDF + JSON', 'Human and machine-readable evidence', FileText], | |
| ]; | |
| function App() { | |
| const [mode, setMode] = useState('upload'); | |
| const [file, setFile] = useState(null); | |
| const [url, setUrl] = useState(''); | |
| const [consent, setConsent] = useState(false); | |
| const [analysis, setAnalysis] = useState(null); | |
| const [error, setError] = useState(''); | |
| const [submitting, setSubmitting] = useState(false); | |
| const [accessToken, setAccessToken] = useState(() => window.localStorage.getItem('aida_access_token') || ''); | |
| const [previewBlurred, setPreviewBlurred] = useState(true); | |
| const previewUrl = useMemo(() => (file ? URL.createObjectURL(file) : ''), [file]); | |
| const pollRef = useRef(null); | |
| const authHeaders = useCallback(() => ( | |
| ACCESS_TOKEN_ENABLED && accessToken.trim() ? { 'X-AIDA-Access-Token': accessToken.trim() } : {} | |
| ), [accessToken]); | |
| const refreshAnalysis = useCallback(async (id, options = {}) => { | |
| try { | |
| const response = await fetch(`${API_BASE}/analyses/${id}`, { headers: authHeaders() }); | |
| const payload = await parseResponse(response); | |
| setAnalysis(payload); | |
| } catch (err) { | |
| if (!options.quiet) setError(err.message); | |
| } | |
| }, [authHeaders]); | |
| useEffect(() => () => previewUrl && URL.revokeObjectURL(previewUrl), [previewUrl]); | |
| useEffect(() => { | |
| if (!ACCESS_TOKEN_ENABLED) return; | |
| if (accessToken.trim()) { | |
| window.localStorage.setItem('aida_access_token', accessToken.trim()); | |
| } else { | |
| window.localStorage.removeItem('aida_access_token'); | |
| } | |
| }, [accessToken]); | |
| useEffect(() => { | |
| if (!analysis?.id || !['pending', 'running'].includes(analysis.status)) return undefined; | |
| pollRef.current = window.setInterval(() => { | |
| refreshAnalysis(analysis.id, { quiet: true }); | |
| }, 1600); | |
| return () => window.clearInterval(pollRef.current); | |
| }, [analysis?.id, analysis?.status, refreshAnalysis]); | |
| async function submitAnalysis(event) { | |
| event.preventDefault(); | |
| setError(''); | |
| setSubmitting(true); | |
| setAnalysis(null); | |
| try { | |
| const form = new FormData(); | |
| form.append('consent_confirmed', consent ? 'true' : 'false'); | |
| if (mode === 'upload') { | |
| if (!file) throw new Error('Choose an image file.'); | |
| form.append('file', file); | |
| } else { | |
| if (!url.trim()) throw new Error('Enter a public URL.'); | |
| form.append('url', url.trim()); | |
| } | |
| const response = await fetch(`${API_BASE}/analyses`, { method: 'POST', body: form, headers: authHeaders() }); | |
| const payload = await parseResponse(response); | |
| setAnalysis(payload); | |
| await refreshAnalysis(payload.id, { quiet: true }); | |
| } catch (err) { | |
| setError(err.message); | |
| } finally { | |
| setSubmitting(false); | |
| } | |
| } | |
| async function downloadReport(format) { | |
| if (!analysis?.id) return; | |
| try { | |
| const response = await fetch(`${API_BASE}/analyses/${analysis.id}/report?format=${format}`, { headers: authHeaders() }); | |
| if (!response.ok) { | |
| const payload = await response.json().catch(() => ({})); | |
| throw new Error(payload.detail || `Download failed with HTTP ${response.status}`); | |
| } | |
| const blob = await response.blob(); | |
| const url = window.URL.createObjectURL(blob); | |
| const anchor = document.createElement('a'); | |
| anchor.href = url; | |
| anchor.download = `analysis-${analysis.id}.${format}`; | |
| document.body.appendChild(anchor); | |
| anchor.click(); | |
| anchor.remove(); | |
| window.URL.revokeObjectURL(url); | |
| } catch (err) { | |
| setError(err.message); | |
| } | |
| } | |
| async function deleteAnalysis() { | |
| if (!analysis?.id) return; | |
| try { | |
| const response = await fetch(`${API_BASE}/analyses/${analysis.id}`, { method: 'DELETE', headers: authHeaders() }); | |
| if (!response.ok) { | |
| const payload = await response.json().catch(() => ({})); | |
| throw new Error(payload.detail || `Delete failed with HTTP ${response.status}`); | |
| } | |
| setAnalysis(null); | |
| setError(''); | |
| } catch (err) { | |
| setError(err.message); | |
| } | |
| } | |
| const result = analysis?.result; | |
| const verdict = result?.verdict; | |
| return ( | |
| <main> | |
| <LandingPage /> | |
| <section className="shell" id="analyzer"> | |
| <div className="workspace"> | |
| <header className="topbar"> | |
| <div> | |
| <p className="eyebrow">Privacy-first authenticity analysis</p> | |
| <h1>AI Deepfake Analyzer</h1> | |
| </div> | |
| <div className="status-pill"> | |
| <Lock size={16} /> | |
| Ephemeral media | |
| </div> | |
| </header> | |
| <div className="grid"> | |
| <form className="panel input-panel" onSubmit={submitAnalysis}> | |
| <div className="segmented" aria-label="Input type"> | |
| <button type="button" className={mode === 'upload' ? 'active' : ''} onClick={() => setMode('upload')}> | |
| <ImageUp size={17} /> | |
| Upload | |
| </button> | |
| <button type="button" className={mode === 'url' ? 'active' : ''} onClick={() => setMode('url')}> | |
| <Link size={17} /> | |
| Public URL | |
| </button> | |
| </div> | |
| {ACCESS_TOKEN_ENABLED && ( | |
| <label className="field compact-field"> | |
| <span>Access token</span> | |
| <div className="token-input"> | |
| <KeyRound size={17} /> | |
| <input | |
| value={accessToken} | |
| onChange={(event) => setAccessToken(event.target.value)} | |
| type="password" | |
| autoComplete="off" | |
| placeholder="Private beta token" | |
| /> | |
| </div> | |
| </label> | |
| )} | |
| {mode === 'upload' ? ( | |
| <label className="dropzone"> | |
| <input | |
| type="file" | |
| accept="image/png,image/jpeg,image/webp,image/gif,image/bmp,image/tiff" | |
| onChange={(event) => setFile(event.target.files?.[0] || null)} | |
| /> | |
| {previewUrl ? ( | |
| <div className="preview-wrap"> | |
| <img className={previewBlurred ? 'blurred preview' : 'preview'} src={previewUrl} alt="" /> | |
| <button type="button" className="icon-action" onClick={(event) => { | |
| event.preventDefault(); | |
| setPreviewBlurred((value) => !value); | |
| }}> | |
| {previewBlurred ? <Eye size={18} /> : <EyeOff size={18} />} | |
| {previewBlurred ? 'Reveal' : 'Blur'} | |
| </button> | |
| </div> | |
| ) : ( | |
| <div className="drop-empty"> | |
| <ImageUp size={28} /> | |
| <span>Choose image</span> | |
| </div> | |
| )} | |
| </label> | |
| ) : ( | |
| <label className="field"> | |
| <span>Public post or image URL</span> | |
| <input value={url} onChange={(event) => setUrl(event.target.value)} placeholder="https://example.com/post" /> | |
| </label> | |
| )} | |
| <label className="check-row"> | |
| <input type="checkbox" checked={consent} onChange={(event) => setConsent(event.target.checked)} /> | |
| <span>I have the right to submit this media for analysis.</span> | |
| </label> | |
| {error && ( | |
| <div className="alert"> | |
| <AlertTriangle size={18} /> | |
| {error} | |
| </div> | |
| )} | |
| <button className="primary" type="submit" disabled={submitting}> | |
| {submitting ? <Loader2 className="spin" size={18} /> : <ShieldCheck size={18} />} | |
| Analyze | |
| </button> | |
| </form> | |
| <section className="panel result-panel"> | |
| {!analysis && <EmptyState />} | |
| {analysis && !result && ( | |
| <div className="running"> | |
| <Loader2 className="spin" size={30} /> | |
| <h2>{analysis.status === 'failed' ? 'Analysis failed' : 'Analysis running'}</h2> | |
| <p>{analysis.error || 'Preparing the evidence layers.'}</p> | |
| <button type="button" className="secondary" onClick={() => refreshAnalysis(analysis.id)}> | |
| <RefreshCw size={16} /> | |
| Refresh | |
| </button> | |
| </div> | |
| )} | |
| {result && ( | |
| <div className="report"> | |
| <div className={`verdict ${verdict.label}`}> | |
| <p>{verdictCopy[verdict.label] || verdict.label}</p> | |
| <h2>{Math.round(verdict.ai_probability * 100)}% AI evidence score</h2> | |
| <span>{verdict.confidence} confidence</span> | |
| </div> | |
| <div className="meter-group"> | |
| <Metric label="AI evidence score" value={verdict.ai_probability} /> | |
| <Metric label="Manipulation evidence" value={verdict.manipulation_probability} /> | |
| <Metric label="Cross-layer disagreement" value={verdict.disagreement} /> | |
| </div> | |
| <p className="score-disclaimer"> | |
| This is calibrated evidence strength, not the probability that a person or image is fake. | |
| </p> | |
| <div className="actions"> | |
| <button type="button" className="secondary" onClick={() => downloadReport('json')}> | |
| <FileJson size={17} /> | |
| JSON | |
| </button> | |
| <button type="button" className="secondary" onClick={() => downloadReport('pdf')}> | |
| <FileText size={17} /> | |
| </button> | |
| <button type="button" className="secondary" onClick={() => downloadReport('pdf')}> | |
| <Download size={17} /> | |
| Save | |
| </button> | |
| <button type="button" className="secondary danger-action" onClick={deleteAnalysis}> | |
| <Trash2 size={17} /> | |
| Delete | |
| </button> | |
| </div> | |
| <DecisionSummary explainability={result.explainability} /> | |
| <DecisionAttribution attribution={result.explainability?.decision_attribution || []} /> | |
| <ExpertOpinions opinions={result.explainability?.expert_opinions || []} /> | |
| <EvidenceLayers layers={result.layers} /> | |
| <Explainability explainability={result.explainability} /> | |
| <RegionEvidenceMap map={result.explainability?.regional_evidence_map} /> | |
| <AnalyticalLayers layers={result.analytical_layers || result.explainability?.layer_ledger?.layers || []} /> | |
| <TechnicalAppendix appendix={result.technical_appendix} /> | |
| </div> | |
| )} | |
| </section> | |
| </div> | |
| </div> | |
| </section> | |
| </main> | |
| ); | |
| } | |
| function LandingPage() { | |
| return ( | |
| <> | |
| <section className="landing-hero" id="top"> | |
| <ArchitectureScene /> | |
| <nav className="landing-nav" aria-label="Primary"> | |
| <a className="landing-brand" href="#top"> | |
| <ShieldCheck size={18} /> | |
| AIDA | |
| </a> | |
| <div className="landing-nav-links"> | |
| <a href="#architecture">Architecture</a> | |
| <a href="#experts">Experts</a> | |
| <a href="#reports">Reports</a> | |
| <a href="#deployment">Deployment</a> | |
| <a className="nav-action" href="#analyzer">Analyze</a> | |
| </div> | |
| </nav> | |
| <div className="hero-content"> | |
| <div className="hero-copy"> | |
| <div className="hero-status"> | |
| <span /> | |
| <strong>Public analyzer deployed</strong> | |
| <em>Evidence score, not certainty</em> | |
| </div> | |
| <p className="eyebrow hero-eyebrow">Public evidence triage for synthetic media abuse</p> | |
| <h1>AI Deepfake Analyzer</h1> | |
| <p> | |
| A privacy-first authenticity platform that combines a multi-view visual ensemble, provenance checks, | |
| pixel forensics, and a false-positive-aware safety arbiter. Every result shows what supported it, | |
| what contradicted it, and why the system may still be wrong. | |
| </p> | |
| <div className="hero-actions"> | |
| <a className="hero-primary" href="#analyzer"> | |
| <ShieldCheck size={18} /> | |
| Start Analysis | |
| </a> | |
| <a className="hero-secondary" href="#architecture"> | |
| <Workflow size={18} /> | |
| View Architecture | |
| </a> | |
| </div> | |
| <div className="hero-proof-grid"> | |
| {heroHighlights.map(([title, detail]) => ( | |
| <article key={title}> | |
| <BadgeCheck size={17} /> | |
| <div> | |
| <strong>{title}</strong> | |
| <span>{detail}</span> | |
| </div> | |
| </article> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <section className="method-strip" aria-label="Current analysis method"> | |
| <div className="method-strip-inner"> | |
| {methodFacts.map(([value, label, icon]) => { | |
| const FactIcon = icon; | |
| return ( | |
| <article key={value}> | |
| <FactIcon size={18} /> | |
| <strong>{value}</strong> | |
| <span>{label}</span> | |
| </article> | |
| ); | |
| })} | |
| </div> | |
| </section> | |
| <section className="landing-band architecture-band" id="architecture"> | |
| <div className="section-head"> | |
| <p className="eyebrow">System architecture</p> | |
| <h2>One image enters. Independent evidence lanes return to a calibrated safety arbiter.</h2> | |
| <p> | |
| Provenance, visual models, and low-level forensics are deliberately separated so one noisy family of | |
| signals cannot silently dominate the conclusion. | |
| </p> | |
| </div> | |
| <ArchitectureFlowchart /> | |
| <div className="pipeline-grid"> | |
| {architectureStages.map((stage, index) => { | |
| const Icon = stage.icon; | |
| return ( | |
| <article className="pipeline-card" style={{ '--delay': `${index * 110}ms` }} key={stage.title}> | |
| <Icon size={22} /> | |
| <h3>{stage.title}</h3> | |
| <p>{stage.detail}</p> | |
| <ul> | |
| {stage.checks.map((check) => ( | |
| <li key={check}>{check}</li> | |
| ))} | |
| </ul> | |
| </article> | |
| ); | |
| })} | |
| </div> | |
| </section> | |
| <section className="landing-band expert-band" id="experts"> | |
| <div className="section-head"> | |
| <p className="eyebrow">Calibrated mixture of experts</p> | |
| <h2>Raw model outputs are converted into stances before they can influence a person.</h2> | |
| <p> | |
| The primary detector checks five transformed views. Two counter-models challenge it, while provenance | |
| and forensic experts contribute independent evidence. The arbiter can still abstain. | |
| </p> | |
| </div> | |
| <MoEFlowDiagram /> | |
| <div className="expert-grid"> | |
| {expertPanels.map((panel) => ( | |
| <article className="expert-card" key={panel.title}> | |
| <BrainCircuit size={21} /> | |
| <h3>{panel.title}</h3> | |
| <p>{panel.detail}</p> | |
| <div className="detail-tags"> | |
| {panel.signals.map((signal) => ( | |
| <span key={signal}>{signal}</span> | |
| ))} | |
| </div> | |
| <strong>{panel.guardrail}</strong> | |
| </article> | |
| ))} | |
| </div> | |
| </section> | |
| <section className="landing-band reports-band" id="reports"> | |
| <div className="reports-layout"> | |
| <div> | |
| <div className="section-head"> | |
| <p className="eyebrow">Downloadable reports</p> | |
| <h2>Every result becomes a decision brief and a reproducible technical evidence package.</h2> | |
| <p> | |
| The six-section PDF begins with a victim-friendly summary, then exposes calibration, expert votes, | |
| layer evidence, the regional map, file facts, limitations, and responsible next steps. | |
| </p> | |
| </div> | |
| <div className="report-feature-grid"> | |
| {reportFeatures.map((feature) => { | |
| const FeatureIcon = feature.icon; | |
| return ( | |
| <article className="report-feature" key={feature.title}> | |
| <FeatureIcon size={20} /> | |
| <div> | |
| <h3>{feature.title}</h3> | |
| <p>{feature.detail}</p> | |
| <ul> | |
| {feature.includes.map((item) => ( | |
| <li key={item}>{item}</li> | |
| ))} | |
| </ul> | |
| </div> | |
| </article> | |
| ); | |
| })} | |
| </div> | |
| <a className="reports-action" href="#analyzer"> | |
| <Download size={18} /> | |
| Generate a report | |
| </a> | |
| </div> | |
| <ReportPreviewScene /> | |
| </div> | |
| </section> | |
| <section className="landing-band deployment-band" id="deployment"> | |
| <div className="section-head"> | |
| <p className="eyebrow">Public deployment posture</p> | |
| <h2>Designed for sensitive evidence handling, with a clear path from public beta to multi-worker scale.</h2> | |
| <p> | |
| The current public runtime enforces privacy, limits, health checks, and deletion. PostgreSQL and Redis/RQ | |
| are supported for a scaled deployment, but the interface never claims that pixels alone prove authenticity. | |
| </p> | |
| </div> | |
| <div className="deployment-grid"> | |
| {deploymentControls.map((control) => ( | |
| <article className="deployment-item" key={control.title}> | |
| <CheckCircle2 size={20} /> | |
| <div> | |
| <h3>{control.title}</h3> | |
| <p>{control.detail}</p> | |
| <ul> | |
| {control.checks.map((check) => ( | |
| <li key={check}>{check}</li> | |
| ))} | |
| </ul> | |
| </div> | |
| </article> | |
| ))} | |
| </div> | |
| </section> | |
| </> | |
| ); | |
| } | |
| function MoEFlowDiagram() { | |
| return ( | |
| <div className="moe-diagram" aria-label="Mixture of experts model architecture"> | |
| <div className="moe-flow-shell"> | |
| <div className="moe-stage-card moe-input-card"> | |
| <span>01</span> | |
| <ImageUp size={24} /> | |
| <h3>Preprocess</h3> | |
| <p>Validate, normalize color, measure quality risk, compute hashes, and prepare independent evidence inputs.</p> | |
| </div> | |
| <div className="moe-stage-card moe-gate-card"> | |
| <span>02</span> | |
| <Gauge size={24} /> | |
| <h3>Multi-view Gate</h3> | |
| <p>Create original, 92% center-crop, JPEG-85, mirrored, and social-resize views for the broad primary stability check.</p> | |
| </div> | |
| <section className="moe-expert-panel" aria-label="Expert detector panel"> | |
| <div className="moe-panel-head"> | |
| <span>03</span> | |
| <div> | |
| <h3>Expert Panel</h3> | |
| <p>One broad primary, two counter-models, and independent forensic and provenance experts.</p> | |
| </div> | |
| </div> | |
| <div className="moe-expert-stack"> | |
| {moeExperts.map((expert, index) => { | |
| const ExpertIcon = expert.icon; | |
| return ( | |
| <article className="moe-expert-node" style={{ '--index': index }} key={expert.title}> | |
| <ExpertIcon size={18} /> | |
| <div> | |
| <h4>{expert.title}</h4> | |
| <p>{expert.detail}</p> | |
| <strong>{expert.output}</strong> | |
| </div> | |
| </article> | |
| ); | |
| })} | |
| </div> | |
| </section> | |
| <div className="moe-stage-card moe-normalizer-card"> | |
| <span>04</span> | |
| <BadgeCheck size={24} /> | |
| <h3>Stance Calibrator</h3> | |
| <p>Apply per-model AI/real thresholds. Scores in between become abstentions, then reliability weights are applied.</p> | |
| </div> | |
| <div className="moe-stage-card danger moe-arbiter-card"> | |
| <span>05</span> | |
| <ShieldAlert size={24} /> | |
| <h3>Safety Arbiter</h3> | |
| <p>Require provenance, independent support, unanimous strong votes, or primary-anchored alignment before a strong AI claim.</p> | |
| </div> | |
| <div className="moe-verdict-stack"> | |
| <span className="real">likely real</span> | |
| <span className="ai">likely AI generated</span> | |
| <span className="manipulated">likely manipulated</span> | |
| <span className="unknown">inconclusive</span> | |
| </div> | |
| </div> | |
| <div className="moe-rule-grid"> | |
| {moeRules.map((rule) => ( | |
| <div className="moe-rule" key={rule}> | |
| <CheckCircle2 size={16} /> | |
| <span>{rule}</span> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function ArchitectureFlowchart() { | |
| const laneGroups = [ | |
| ['Provenance lane', [ | |
| ['Metadata', 'EXIF/XMP, software markers, GPS redaction', Database], | |
| ['C2PA', 'Content credentials and signed generation claims', BadgeCheck], | |
| ]], | |
| ['Visual-model lane', [ | |
| ['Broad Primary', 'Community Forensics across five stable transforms', Cpu], | |
| ['Counter-Models', 'Ateeqq plus an independent distilled classifier', BrainCircuit], | |
| ]], | |
| ['Forensic lane', [ | |
| ['Pixel Residuals', 'ELA, noise, luminance, chroma, edge, frequency', Layers3], | |
| ['Regional Map', 'A 4x4 anomaly grid with explicit limitations', Gauge], | |
| ]], | |
| ]; | |
| const flowSteps = [ | |
| ['01', 'Private Intake', 'Consent, type and pixel limits, SSRF defense, encryption, and short media TTL.', Lock], | |
| ['02', 'Evidence Bus', 'Fork the validated packet into provenance, model, and forensic lanes.', Workflow], | |
| ['03', 'Expert Routing', 'Run only the evidence methods that are available and appropriate for this file.', BrainCircuit], | |
| ['04', 'Safety Arbiter', 'Calibrate stances, record disagreement, cap scores, or abstain.', ShieldAlert], | |
| ['05', 'Evidence Report', 'Return a plain-language verdict plus PDF and JSON technical ledgers.', FileText], | |
| ]; | |
| return ( | |
| <div className="architecture-map" aria-label="Deepfake analysis architecture flow"> | |
| <div className="diagram-livebar"> | |
| <div><span /> Evidence packet journey</div> | |
| <p>Validation, parallel expert analysis, calibrated convergence, report</p> | |
| </div> | |
| <div className="architecture-flow-grid"> | |
| <span className="flow-packet packet-one" /> | |
| <span className="flow-packet packet-two" /> | |
| <span className="flow-packet packet-three" /> | |
| {flowSteps.map(([index, title, detail, icon], stepIndex) => { | |
| const StepIcon = icon; | |
| return ( | |
| <article | |
| className={`flow-step ${title === 'Evidence Bus' ? 'hub-step' : ''}`} | |
| style={{ '--step-index': stepIndex }} | |
| key={title} | |
| > | |
| <span className="node-index">{index}</span> | |
| <div className="node-orb"><StepIcon size={24} /></div> | |
| <h3>{title}</h3> | |
| <p>{detail}</p> | |
| <span className="step-state"> | |
| {stepIndex === 0 ? 'validated' : stepIndex === 1 ? 'forked' : stepIndex === 2 ? 'analyzed' : stepIndex === 3 ? 'calibrated' : 'documented'} | |
| </span> | |
| </article> | |
| ); | |
| })} | |
| </div> | |
| <div className="evidence-convergence"> | |
| <span>Parallel evidence fan-out</span> | |
| <i /> | |
| <strong>Independent signals reconverge at the arbiter</strong> | |
| </div> | |
| <div className="evidence-lanes"> | |
| {laneGroups.map(([lane, items], laneIndex) => ( | |
| <section className="layer-cluster" style={{ '--lane-index': laneIndex }} key={lane}> | |
| <div className="lane-heading"> | |
| <span className="cluster-label">{lane}</span> | |
| <em>independent</em> | |
| </div> | |
| {items.map(([title, detail, icon], itemIndex) => { | |
| const LayerIcon = icon; | |
| return ( | |
| <article className="layer-chip" style={{ '--item-index': itemIndex }} key={title}> | |
| <LayerIcon size={18} /> | |
| <div> | |
| <strong>{title}</strong> | |
| <span>{detail}</span> | |
| </div> | |
| </article> | |
| ); | |
| })} | |
| </section> | |
| ))} | |
| <div className="decision-stack"> | |
| <strong>Safe outcome set</strong> | |
| <span>likely real</span> | |
| <span>likely AI generated</span> | |
| <span>likely manipulated</span> | |
| <span>inconclusive</span> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function ReportPreviewScene() { | |
| return ( | |
| <div className="report-preview-scene" aria-hidden="true"> | |
| <div className="report-preview-frame"> | |
| <div className="report-sheet"> | |
| <div className="report-sheet-head"> | |
| <div> | |
| <span /> | |
| <strong>Authenticity Report</strong> | |
| </div> | |
| <BadgeCheck size={20} /> | |
| </div> | |
| <div className="report-verdict-preview"> | |
| <p>Likely AI generated</p> | |
| <strong>72%</strong> | |
| <span>medium confidence</span> | |
| </div> | |
| <div className="report-bars"> | |
| <span /> | |
| <span /> | |
| <span /> | |
| </div> | |
| <div className="report-layer-list"> | |
| {['Five-view model stability', 'Calibrated expert stances', 'Arbiter decision attribution', 'Diagnostic regional map'].map((item, index) => ( | |
| <div className="report-layer-row" style={{ '--index': index }} key={item}> | |
| <i /> | |
| <span>{item}</span> | |
| <strong>{index === 0 ? 'stable' : index === 1 ? 'aligned' : index === 2 ? 'used' : 'context'}</strong> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| <div className="export-stack"> | |
| <span><FileJson size={17} /> JSON</span> | |
| <span><FileText size={17} /> PDF</span> | |
| <span><Download size={17} /> Save</span> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function ArchitectureScene() { | |
| const nodes = [ | |
| ['Primary', '5-view stability', Cpu], | |
| ['Counters', '2 independent votes', BrainCircuit], | |
| ['Provenance', 'EXIF/XMP + C2PA', Database], | |
| ['Forensics', '11 evidence layers', Layers3], | |
| ['Arbiter', 'Abstention gate', Gauge], | |
| ['Report', 'PDF + JSON ledger', FileText], | |
| ]; | |
| return ( | |
| <div className="architecture-scene" aria-hidden="true"> | |
| <div className="scene-rail rail-one" /> | |
| <div className="scene-rail rail-two" /> | |
| <div className="scene-rail rail-three" /> | |
| <div className="evidence-workflow"> | |
| <div className="workflow-header"> | |
| <span /> | |
| <span /> | |
| <span /> | |
| <strong>Calibrated evidence pipeline</strong> | |
| </div> | |
| <div className="workflow-body"> | |
| <div className="media-pane"> | |
| <div className="media-thumbnail"> | |
| <span className="scan-beam" /> | |
| <i /> | |
| <i /> | |
| <i /> | |
| </div> | |
| <div className="heat-map"> | |
| {Array.from({ length: 16 }, (_, index) => <span key={index} />)} | |
| </div> | |
| </div> | |
| <div className="signal-stack"> | |
| {nodes.map(([title, sub, icon], index) => { | |
| const NodeIcon = icon; | |
| return ( | |
| <div className="signal-node" style={{ '--index': index }} key={title}> | |
| <NodeIcon size={16} /> | |
| <strong>{title}</strong> | |
| <span>{sub}</span> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| <div className="confidence-row"> | |
| <span /> | |
| <span /> | |
| <span /> | |
| </div> | |
| </div> | |
| <div className="telemetry-strip"> | |
| <span><Cpu size={16} /> 5 views</span> | |
| <span><BrainCircuit size={16} /> 3 models</span> | |
| <span><Layers3 size={16} /> 11 layers</span> | |
| <span><Lock size={16} /> 15 min TTL</span> | |
| <span><FileText size={16} /> PDF + JSON</span> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function EmptyState() { | |
| return ( | |
| <div className="empty"> | |
| <ShieldCheck size={34} /> | |
| <h2>Ready</h2> | |
| <p>Upload an image or submit a public URL.</p> | |
| </div> | |
| ); | |
| } | |
| function Metric({ label, value }) { | |
| return ( | |
| <div className="metric"> | |
| <div className="metric-head"> | |
| <span>{label}</span> | |
| <strong>{Math.round(value * 100)}%</strong> | |
| </div> | |
| <div className="bar"> | |
| <span style={{ width: `${Math.round(value * 100)}%` }} /> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function DecisionSummary({ explainability }) { | |
| const decision = explainability?.decision_support; | |
| if (!decision) return null; | |
| return ( | |
| <div className="section-list"> | |
| <h3>Decision Summary</h3> | |
| <article className="decision-card"> | |
| <p>{decision.plain_summary}</p> | |
| <div className="decision-grid"> | |
| <DecisionColumn title="Primary drivers" items={decision.primary_drivers} /> | |
| <DecisionColumn title="Counter-evidence" items={decision.counter_evidence} /> | |
| <DecisionColumn title="Uncertainty" items={decision.uncertainty_factors} /> | |
| <DecisionColumn title="What would help" items={decision.what_would_help} /> | |
| </div> | |
| </article> | |
| </div> | |
| ); | |
| } | |
| function DecisionColumn({ title, items = [] }) { | |
| return ( | |
| <div className="decision-column"> | |
| <h4>{title}</h4> | |
| <ul> | |
| {items.slice(0, 4).map((item) => ( | |
| <li key={item}>{item}</li> | |
| ))} | |
| </ul> | |
| </div> | |
| ); | |
| } | |
| function ExpertOpinions({ opinions }) { | |
| if (!opinions.length) return null; | |
| return ( | |
| <div className="section-list"> | |
| <h3>Mixture Of Experts</h3> | |
| {opinions.map((opinion) => ( | |
| <article className="layer" key={opinion.expert}> | |
| <div> | |
| <h4>{opinion.expert}</h4> | |
| <span>{opinion.stance}</span> | |
| </div> | |
| <p className="layer-method"> | |
| {opinion.opinion} - {opinion.confidence} confidence | |
| {typeof opinion.score === 'number' ? ` - ${Math.round(opinion.score * 100)}%` : ''} | |
| </p> | |
| <ul> | |
| {(opinion.evidence || []).slice(0, 3).map((item) => ( | |
| <li key={item}>{item}</li> | |
| ))} | |
| </ul> | |
| </article> | |
| ))} | |
| </div> | |
| ); | |
| } | |
| function EvidenceLayers({ layers }) { | |
| return ( | |
| <div className="section-list"> | |
| <h3>Evidence Layers</h3> | |
| {layers.map((layer) => ( | |
| <article className="layer" key={layer.name}> | |
| <div> | |
| <h4>{layer.name}</h4> | |
| <span>{layer.status}</span> | |
| </div> | |
| <ul> | |
| {layer.findings.slice(0, 4).map((finding) => ( | |
| <li key={finding}>{finding}</li> | |
| ))} | |
| </ul> | |
| </article> | |
| ))} | |
| </div> | |
| ); | |
| } | |
| function Explainability({ explainability }) { | |
| if (!explainability) return null; | |
| const consensus = explainability.model_consensus || {}; | |
| const standard = explainability.decision_standard || {}; | |
| const strongest = explainability.strongest_evidence || []; | |
| const trace = explainability.decision_trace || []; | |
| return ( | |
| <div className="section-list"> | |
| <h3>Explainability</h3> | |
| {!!trace.length && ( | |
| <article className="layer"> | |
| <div> | |
| <h4>Decision Trace</h4> | |
| <span>{trace.length} steps</span> | |
| </div> | |
| <ul> | |
| {trace.slice(0, 5).map((item) => ( | |
| <li key={item}>{item}</li> | |
| ))} | |
| </ul> | |
| </article> | |
| )} | |
| <article className="layer"> | |
| <div> | |
| <h4>Model Consensus</h4> | |
| <span>{consensus.enabled_models || 0} models</span> | |
| </div> | |
| <ul> | |
| <li>{consensus.ai_votes || 0} model votes for AI-generated.</li> | |
| <li>{consensus.lean_ai_votes || 0} of {consensus.enabled_models || 0} experts lean AI after their model-specific gates.</li> | |
| <li>{consensus.real_votes || 0} model votes for real or human-origin.</li> | |
| <li>Primary-anchored alignment: {consensus.primary_anchored_alignment ? 'yes' : 'no'}.</li> | |
| <li>Calibrated model evidence score: {Math.round((consensus.average_ai_probability || 0) * 100)}%.</li> | |
| <li>Raw average model output: {Math.round((consensus.raw_average_ai_probability || 0) * 100)}%.</li> | |
| <li>Calibrated stance disagreement: {Math.round((consensus.calibrated_stance_disagreement || 0) * 100)}%.</li> | |
| <li>Raw score range: {Math.round((consensus.raw_score_range || 0) * 100)}% (diagnostic only).</li> | |
| <li>Scores inside each model's abstention band contribute neutral evidence.</li> | |
| </ul> | |
| {!!consensus.models?.length && ( | |
| <div className="model-audit-list"> | |
| {consensus.models.map((model) => ( | |
| <div key={model.name}> | |
| <strong>{model.name.replace('hf:', '')}</strong> | |
| <span>{model.label.replaceAll('_', ' ')}</span> | |
| <small> | |
| {model.details?.view_count || 1} view{(model.details?.view_count || 1) === 1 ? '' : 's'} · calibrated stance {Math.round((model.calibrated_stance_score || 0.5) * 100)}% | |
| </small> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| </article> | |
| {!!standard.policy && ( | |
| <article className="layer"> | |
| <div> | |
| <h4>Calibration Gate</h4> | |
| <span>{standard.policy}</span> | |
| </div> | |
| <ul> | |
| {(standard.false_positive_controls || []).slice(0, 4).map((item) => ( | |
| <li key={item}>{item}</li> | |
| ))} | |
| </ul> | |
| </article> | |
| )} | |
| <article className="layer"> | |
| <div> | |
| <h4>Strongest Evidence</h4> | |
| <span>{strongest.length} signals</span> | |
| </div> | |
| <ul> | |
| {strongest.slice(0, 5).map((item) => ( | |
| <li key={`${item.source}-${item.label}`}> | |
| {item.source}: {item.label} | |
| </li> | |
| ))} | |
| </ul> | |
| </article> | |
| </div> | |
| ); | |
| } | |
| function DecisionAttribution({ attribution }) { | |
| if (!attribution.length) return null; | |
| return ( | |
| <div className="section-list attribution-section"> | |
| <h3>Why The Arbiter Reached This Verdict</h3> | |
| <p className="section-intro"> | |
| These are the signals that influenced the decision. Diagnostic-only image layers are identified separately so anomalies are not mistaken for proof. | |
| </p> | |
| <div className="attribution-list"> | |
| {attribution.map((item) => ( | |
| <article className={`attribution-row ${item.used_by_arbiter ? 'used' : 'context'}`} key={item.source}> | |
| <div className="attribution-head"> | |
| <div> | |
| <strong>{item.source}</strong> | |
| <span>{item.role.replaceAll('_', ' ')}</span> | |
| </div> | |
| <em>{item.used_by_arbiter ? 'used by arbiter' : 'review context'}</em> | |
| </div> | |
| <div className="attribution-meter" aria-label={`${Math.round((item.strength || 0) * 100)} percent influence`}> | |
| <span style={{ width: `${Math.round((item.strength || 0) * 100)}%` }} /> | |
| </div> | |
| <p>{item.finding}</p> | |
| <small>{item.direction.replaceAll('_', ' ')} · {Math.round((item.strength || 0) * 100)}% relative influence</small> | |
| <p className="counterfactual"><strong>What would change it:</strong> {item.counterfactual}</p> | |
| </article> | |
| ))} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function RegionEvidenceMap({ map }) { | |
| if (!map?.tiles?.length) return null; | |
| const cols = map.grid?.cols || 4; | |
| const tiles = [...map.tiles].sort((a, b) => (a.row - b.row) || (a.col - b.col)); | |
| return ( | |
| <div className="section-list"> | |
| <h3>Region Evidence Map</h3> | |
| <article className="region-card"> | |
| <p>{map.interpretation}</p> | |
| <div className="region-grid" style={{ gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))` }}> | |
| {tiles.map((tile) => ( | |
| <div | |
| className={`region-tile ${tile.severity_band || 'low'}`} | |
| key={`${tile.row}-${tile.col}`} | |
| title={`Row ${tile.row}, column ${tile.col}. Anomaly ${Math.round((tile.severity || 0) * 100)}%. Brightness ${tile.brightness}; noise ${tile.noise}; edge ${tile.edge}.`} | |
| > | |
| <span>R{tile.row} C{tile.col}</span> | |
| <strong>{Math.round((tile.severity || 0) * 100)}%</strong> | |
| </div> | |
| ))} | |
| </div> | |
| <div className="region-legend"> | |
| <span><i className="low" /> Low</span> | |
| <span><i className="medium" /> Medium</span> | |
| <span><i className="high" /> High</span> | |
| <strong>{map.high_severity_tile_count || 0} high-severity tiles</strong> | |
| </div> | |
| <p className="layer-method"> | |
| Max regional score {map.max_score}; mean regional score {map.mean_score}. This map is relative to the submitted image. | |
| </p> | |
| </article> | |
| </div> | |
| ); | |
| } | |
| function AnalyticalLayers({ layers }) { | |
| if (!layers.length) return null; | |
| return ( | |
| <div className="section-list"> | |
| <h3>Analytical Layer Ledger</h3> | |
| {layers.map((layer) => ( | |
| <article className="layer" key={layer.id}> | |
| <div> | |
| <h4>{layer.name}</h4> | |
| <span>{layer.conclusion}</span> | |
| </div> | |
| <p className="layer-method">{layer.method}</p> | |
| <div className="layer-audit-row"> | |
| <span>{(layer.decision_role || 'review_context_only').replaceAll('_', ' ')}</span> | |
| <span>{layer.reliability || layer.confidence || 'none'} reliability</span> | |
| <span>{layer.used_by_arbiter ? 'used by arbiter' : 'review context only'}</span> | |
| <span>{Math.round((layer.influence || 0) * 100)}% influence</span> | |
| </div> | |
| <div className="mini-meters"> | |
| <Metric label="AI signal" value={layer.ai_signal || 0} /> | |
| <Metric label="Manipulation signal" value={layer.manipulation_signal || 0} /> | |
| </div> | |
| <ul> | |
| {(layer.evidence || []).slice(0, 3).map((finding) => ( | |
| <li key={finding}>{finding}</li> | |
| ))} | |
| </ul> | |
| {layer.counterfactual && ( | |
| <p className="counterfactual"><strong>What would change it:</strong> {layer.counterfactual}</p> | |
| )} | |
| </article> | |
| ))} | |
| </div> | |
| ); | |
| } | |
| function TechnicalAppendix({ appendix }) { | |
| const detectors = appendix.detectors || []; | |
| return ( | |
| <details className="appendix"> | |
| <summary>Technical Appendix</summary> | |
| <div className="appendix-grid"> | |
| <div> | |
| <h4>Hashes</h4> | |
| <code>{appendix.hashes.sha256}</code> | |
| <code>{appendix.hashes.average_hash}</code> | |
| </div> | |
| <div> | |
| <h4>Detectors</h4> | |
| {detectors.map((detector) => ( | |
| <p key={detector.name}> | |
| <strong>{detector.name}</strong>: {detector.label} ({detector.status}) | |
| {detector.weight ? `, weight ${Number(detector.weight).toFixed(2)}` : ''} | |
| </p> | |
| ))} | |
| </div> | |
| </div> | |
| </details> | |
| ); | |
| } | |
| async function parseResponse(response) { | |
| const payload = await response.json().catch(() => ({})); | |
| if (!response.ok) throw new Error(payload.detail || `Request failed with HTTP ${response.status}`); | |
| return payload; | |
| } | |
| export default App; | |