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 (
{!analysis && }
{analysis && !result && (
{analysis.status === 'failed' ? 'Analysis failed' : 'Analysis running'}
{analysis.error || 'Preparing the evidence layers.'}
refreshAnalysis(analysis.id)}>
Refresh
)}
{result && (
{verdictCopy[verdict.label] || verdict.label}
{Math.round(verdict.ai_probability * 100)}% AI evidence score
{verdict.confidence} confidence
This is calibrated evidence strength, not the probability that a person or image is fake.
downloadReport('json')}>
JSON
downloadReport('pdf')}>
PDF
downloadReport('pdf')}>
Save
Delete
)}
);
}
function LandingPage() {
return (
<>
AIDA
Public analyzer deployed
Evidence score, not certainty
Public evidence triage for synthetic media abuse
AI Deepfake Analyzer
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.
{heroHighlights.map(([title, detail]) => (
{title}
{detail}
))}
{methodFacts.map(([value, label, icon]) => {
const FactIcon = icon;
return (
{value}
{label}
);
})}
System architecture
One image enters. Independent evidence lanes return to a calibrated safety arbiter.
Provenance, visual models, and low-level forensics are deliberately separated so one noisy family of
signals cannot silently dominate the conclusion.
{architectureStages.map((stage, index) => {
const Icon = stage.icon;
return (
{stage.title}
{stage.detail}
{stage.checks.map((check) => (
{check}
))}
);
})}
Calibrated mixture of experts
Raw model outputs are converted into stances before they can influence a person.
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.
{expertPanels.map((panel) => (
{panel.title}
{panel.detail}
{panel.signals.map((signal) => (
{signal}
))}
{panel.guardrail}
))}
Downloadable reports
Every result becomes a decision brief and a reproducible technical evidence package.
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.
{reportFeatures.map((feature) => {
const FeatureIcon = feature.icon;
return (
{feature.title}
{feature.detail}
{feature.includes.map((item) => (
{item}
))}
);
})}
Generate a report
Public deployment posture
Designed for sensitive evidence handling, with a clear path from public beta to multi-worker scale.
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.
{deploymentControls.map((control) => (
{control.title}
{control.detail}
{control.checks.map((check) => (
{check}
))}
))}
>
);
}
function MoEFlowDiagram() {
return (
01
Preprocess
Validate, normalize color, measure quality risk, compute hashes, and prepare independent evidence inputs.
02
Multi-view Gate
Create original, 92% center-crop, JPEG-85, mirrored, and social-resize views for the broad primary stability check.
03
Expert Panel
One broad primary, two counter-models, and independent forensic and provenance experts.
{moeExperts.map((expert, index) => {
const ExpertIcon = expert.icon;
return (
{expert.title}
{expert.detail}
{expert.output}
);
})}
04
Stance Calibrator
Apply per-model AI/real thresholds. Scores in between become abstentions, then reliability weights are applied.
05
Safety Arbiter
Require provenance, independent support, unanimous strong votes, or primary-anchored alignment before a strong AI claim.
likely real
likely AI generated
likely manipulated
inconclusive
{moeRules.map((rule) => (
{rule}
))}
);
}
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 (
Evidence packet journey
Validation, parallel expert analysis, calibrated convergence, report
{flowSteps.map(([index, title, detail, icon], stepIndex) => {
const StepIcon = icon;
return (
{index}
{title}
{detail}
{stepIndex === 0 ? 'validated' : stepIndex === 1 ? 'forked' : stepIndex === 2 ? 'analyzed' : stepIndex === 3 ? 'calibrated' : 'documented'}
);
})}
Parallel evidence fan-out
Independent signals reconverge at the arbiter
{laneGroups.map(([lane, items], laneIndex) => (
{lane}
independent
{items.map(([title, detail, icon], itemIndex) => {
const LayerIcon = icon;
return (
{title}
{detail}
);
})}
))}
Safe outcome set
likely real
likely AI generated
likely manipulated
inconclusive
);
}
function ReportPreviewScene() {
return (
Likely AI generated
72%
medium confidence
{['Five-view model stability', 'Calibrated expert stances', 'Arbiter decision attribution', 'Diagnostic regional map'].map((item, index) => (
{item}
{index === 0 ? 'stable' : index === 1 ? 'aligned' : index === 2 ? 'used' : 'context'}
))}
JSON
PDF
Save
);
}
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 (
Calibrated evidence pipeline
{Array.from({ length: 16 }, (_, index) => )}
{nodes.map(([title, sub, icon], index) => {
const NodeIcon = icon;
return (
{title}
{sub}
);
})}
5 views
3 models
11 layers
15 min TTL
PDF + JSON
);
}
function EmptyState() {
return (
Ready
Upload an image or submit a public URL.
);
}
function Metric({ label, value }) {
return (
{label}
{Math.round(value * 100)}%
);
}
function DecisionSummary({ explainability }) {
const decision = explainability?.decision_support;
if (!decision) return null;
return (
Decision Summary
{decision.plain_summary}
);
}
function DecisionColumn({ title, items = [] }) {
return (
{title}
{items.slice(0, 4).map((item) => (
{item}
))}
);
}
function ExpertOpinions({ opinions }) {
if (!opinions.length) return null;
return (
Mixture Of Experts
{opinions.map((opinion) => (
{opinion.expert}
{opinion.stance}
{opinion.opinion} - {opinion.confidence} confidence
{typeof opinion.score === 'number' ? ` - ${Math.round(opinion.score * 100)}%` : ''}
{(opinion.evidence || []).slice(0, 3).map((item) => (
{item}
))}
))}
);
}
function EvidenceLayers({ layers }) {
return (
Evidence Layers
{layers.map((layer) => (
{layer.name}
{layer.status}
{layer.findings.slice(0, 4).map((finding) => (
{finding}
))}
))}
);
}
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 (
Explainability
{!!trace.length && (
Decision Trace
{trace.length} steps
{trace.slice(0, 5).map((item) => (
{item}
))}
)}
Model Consensus
{consensus.enabled_models || 0} models
{consensus.ai_votes || 0} model votes for AI-generated.
{consensus.lean_ai_votes || 0} of {consensus.enabled_models || 0} experts lean AI after their model-specific gates.
{consensus.real_votes || 0} model votes for real or human-origin.
Primary-anchored alignment: {consensus.primary_anchored_alignment ? 'yes' : 'no'}.
Calibrated model evidence score: {Math.round((consensus.average_ai_probability || 0) * 100)}%.
Raw average model output: {Math.round((consensus.raw_average_ai_probability || 0) * 100)}%.
Calibrated stance disagreement: {Math.round((consensus.calibrated_stance_disagreement || 0) * 100)}%.
Raw score range: {Math.round((consensus.raw_score_range || 0) * 100)}% (diagnostic only).
Scores inside each model's abstention band contribute neutral evidence.
{!!consensus.models?.length && (
{consensus.models.map((model) => (
{model.name.replace('hf:', '')}
{model.label.replaceAll('_', ' ')}
{model.details?.view_count || 1} view{(model.details?.view_count || 1) === 1 ? '' : 's'} · calibrated stance {Math.round((model.calibrated_stance_score || 0.5) * 100)}%
))}
)}
{!!standard.policy && (
Calibration Gate
{standard.policy}
{(standard.false_positive_controls || []).slice(0, 4).map((item) => (
{item}
))}
)}
Strongest Evidence
{strongest.length} signals
{strongest.slice(0, 5).map((item) => (
{item.source}: {item.label}
))}
);
}
function DecisionAttribution({ attribution }) {
if (!attribution.length) return null;
return (
Why The Arbiter Reached This Verdict
These are the signals that influenced the decision. Diagnostic-only image layers are identified separately so anomalies are not mistaken for proof.
{attribution.map((item) => (
{item.source}
{item.role.replaceAll('_', ' ')}
{item.used_by_arbiter ? 'used by arbiter' : 'review context'}
{item.finding}
{item.direction.replaceAll('_', ' ')} · {Math.round((item.strength || 0) * 100)}% relative influence
What would change it: {item.counterfactual}
))}
);
}
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 (
Region Evidence Map
{map.interpretation}
{tiles.map((tile) => (
R{tile.row} C{tile.col}
{Math.round((tile.severity || 0) * 100)}%
))}
Low
Medium
High
{map.high_severity_tile_count || 0} high-severity tiles
Max regional score {map.max_score}; mean regional score {map.mean_score}. This map is relative to the submitted image.
);
}
function AnalyticalLayers({ layers }) {
if (!layers.length) return null;
return (
Analytical Layer Ledger
{layers.map((layer) => (
{layer.name}
{layer.conclusion}
{layer.method}
{(layer.decision_role || 'review_context_only').replaceAll('_', ' ')}
{layer.reliability || layer.confidence || 'none'} reliability
{layer.used_by_arbiter ? 'used by arbiter' : 'review context only'}
{Math.round((layer.influence || 0) * 100)}% influence
{(layer.evidence || []).slice(0, 3).map((finding) => (
{finding}
))}
{layer.counterfactual && (
What would change it: {layer.counterfactual}
)}
))}
);
}
function TechnicalAppendix({ appendix }) {
const detectors = appendix.detectors || [];
return (
Technical Appendix
Hashes
{appendix.hashes.sha256}
{appendix.hashes.average_hash}
Detectors
{detectors.map((detector) => (
{detector.name} : {detector.label} ({detector.status})
{detector.weight ? `, weight ${Number(detector.weight).toFixed(2)}` : ''}
))}
);
}
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;