Devraj1990's picture
Harden calibrated evidence analysis
c00019b verified
Raw
History Blame Contribute Delete
48.2 kB
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} />
PDF
</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&apos;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;