uaide-backend / src /utils /generateReport.js
ATS-27's picture
Upload folder using huggingface_hub
af980d7 verified
/**
* UAIDE β€” Forensic Report Generator
* Produces a self-contained, print-ready HTML report blob for download.
*/
const VERDICT_LABEL = {
authentic: 'AUTHENTIC',
ai_generated: 'AI-GENERATED',
suspect: 'SUSPECT / INCONCLUSIVE',
};
const VERDICT_COLOR = {
authentic: '#00c67a',
ai_generated: '#ff4757',
suspect: '#f59e0b',
};
const SEV_COLOR = {
critical: '#ff4757',
high: '#f59e0b',
medium: '#7c3aed',
low: '#8896ab',
};
function escapeHtml(str) {
return String(str ?? 'β€”')
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function modelBreakdownRows(models) {
return models.map(m => `
<tr>
<td>${escapeHtml(m.model)}</td>
<td class="mono">${m.score.toFixed(1)}%</td>
<td>
<div class="bar-track">
<div class="bar-fill" style="width:${m.score}%"></div>
</div>
</td>
<td class="mono">${(m.weight * 100).toFixed(0)}%</td>
</tr>
`).join('');
}
function artifactRows(artifacts) {
return artifacts.map(a => `
<tr>
<td><span class="sev-badge" style="background:${SEV_COLOR[a.severity]}20;color:${SEV_COLOR[a.severity]};border:1px solid ${SEV_COLOR[a.severity]}40">${a.severity.toUpperCase()}</span></td>
<td><strong>${escapeHtml(a.type)}</strong></td>
<td>${escapeHtml(a.detail)}</td>
</tr>
`).join('');
}
function timelineSection(result) {
if (result.type !== 'video' || !result.timeline) return '';
const segments = result.timeline.flaggedSegments.map(s => `
<tr>
<td><span class="sev-badge" style="background:${SEV_COLOR[s.severity]}20;color:${SEV_COLOR[s.severity]};border:1px solid ${SEV_COLOR[s.severity]}40">${s.severity.toUpperCase()}</span></td>
<td>${escapeHtml(s.reason)}</td>
<td class="mono">${s.start.toFixed(1)}s – ${s.end.toFixed(1)}s</td>
<td class="mono">${s.frames.length} frames</td>
</tr>
`).join('');
return `
<section>
<h2>4. Temporal Analysis</h2>
<p>Total flagged segments: <strong>${result.timeline.flaggedSegments.length}</strong> &nbsp;|&nbsp;
Clean segments: <strong>${result.timeline.cleanSegments.length}</strong></p>
<table>
<thead>
<tr><th>Severity</th><th>Anomaly</th><th>Timestamp</th><th>Frames</th></tr>
</thead>
<tbody>${segments}</tbody>
</table>
</section>
`;
}
function metadataRows(meta) {
return Object.entries(meta).map(([key, val]) => {
const label = key.replace(/([A-Z])/g, ' $1').replace(/^./, s => s.toUpperCase());
return `<tr><td>${escapeHtml(label)}</td><td class="mono">${escapeHtml(val)}</td></tr>`;
}).join('');
}
function gradcamTable(regions) {
return regions.map(r => `
<tr>
<td>${escapeHtml(r.label)}</td>
<td class="mono">${Math.round(r.intensity * 100)}%</td>
<td>
<span class="sev-badge" style="background:${r.intensity > 0.8 ? '#ff475720' : r.intensity > 0.5 ? '#f59e0b20' : '#00c67a20'};
color:${r.intensity > 0.8 ? '#ff4757' : r.intensity > 0.5 ? '#f59e0b' : '#00c67a'};
border:1px solid ${r.intensity > 0.8 ? '#ff475740' : r.intensity > 0.5 ? '#f59e0b40' : '#00c67a40'}">
${r.intensity > 0.8 ? 'High' : r.intensity > 0.5 ? 'Medium' : 'Low'}
</span>
</td>
<td class="mono">x:${r.x}% y:${r.y}% w:${r.w}% h:${r.h}%</td>
</tr>
`).join('');
}
export function generateReport(result, previewUrl) {
const verdictColor = VERDICT_COLOR[result.verdict];
const verdictLabel = VERDICT_LABEL[result.verdict];
const now = new Date().toUTCString();
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>UAIDE Forensic Report β€” ${escapeHtml(result.filename)}</title>
<style>
:root {
--accent: #0066ff;
--verdict: ${verdictColor};
--text: #141820;
--muted: #6b7280;
--border: #e2e8f0;
--bg: #f8f9fc;
--surface: #ffffff;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', Inter, system-ui, sans-serif;
background: var(--bg);
color: var(--text);
font-size: 13.5px;
line-height: 1.65;
padding: 0;
}
.mono { font-family: 'Cascadia Code', 'Fira Code', 'Courier New', monospace; font-size: 12.5px; }
/* Cover */
.cover {
background: linear-gradient(135deg, #0a0f1e 0%, #0d1930 60%, #091428 100%);
color: white;
padding: 56px 64px;
display: flex;
flex-direction: column;
gap: 0;
page-break-after: always;
min-height: 340px;
}
.cover-logo {
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 36px;
}
.logo-mark {
width: 44px;
height: 44px;
background: var(--accent);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 900;
font-size: 17px;
letter-spacing: -0.5px;
color: white;
flex-shrink: 0;
}
.logo-text {
display: flex;
flex-direction: column;
gap: 2px;
}
.logo-name {
font-size: 17px;
font-weight: 800;
letter-spacing: -0.3px;
color: white;
}
.logo-sub {
font-size: 10px;
font-weight: 500;
color: rgba(255,255,255,0.45);
letter-spacing: 1.2px;
text-transform: uppercase;
}
.cover h1 {
font-size: 28px;
font-weight: 800;
letter-spacing: -0.8px;
margin-bottom: 10px;
color: white;
line-height: 1.2;
}
.cover-sub {
font-size: 14px;
color: rgba(255,255,255,0.55);
margin-bottom: 32px;
}
.verdict-banner {
display: inline-flex;
align-items: center;
gap: 10px;
background: ${verdictColor}22;
border: 1.5px solid ${verdictColor}55;
border-radius: 10px;
padding: 12px 20px;
margin-bottom: 32px;
}
.verdict-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: ${verdictColor};
flex-shrink: 0;
}
.verdict-text {
font-size: 22px;
font-weight: 800;
letter-spacing: -0.3px;
color: ${verdictColor};
}
.verdict-score {
font-size: 13px;
color: rgba(255,255,255,0.6);
font-family: 'Fira Code', monospace;
}
.cover-meta {
display: flex;
flex-wrap: wrap;
gap: 24px;
margin-top: auto;
padding-top: 32px;
border-top: 1px solid rgba(255,255,255,0.1);
}
.cover-meta-item {
display: flex;
flex-direction: column;
gap: 3px;
}
.cover-meta-label {
font-size: 10px;
color: rgba(255,255,255,0.35);
text-transform: uppercase;
letter-spacing: 0.9px;
font-weight: 600;
}
.cover-meta-value {
font-size: 13px;
color: rgba(255,255,255,0.85);
font-family: 'Fira Code', monospace;
font-weight: 500;
}
/* Body */
.body {
padding: 48px 64px;
max-width: 960px;
margin: 0 auto;
}
section {
margin-bottom: 44px;
page-break-inside: avoid;
}
h2 {
font-size: 17px;
font-weight: 800;
color: var(--text);
letter-spacing: -0.4px;
margin-bottom: 14px;
padding-bottom: 10px;
border-bottom: 2px solid var(--border);
display: flex;
align-items: center;
gap: 10px;
}
h2::before {
content: '';
width: 4px;
height: 18px;
background: var(--accent);
border-radius: 2px;
flex-shrink: 0;
}
h3 {
font-size: 14px;
font-weight: 700;
color: var(--text);
margin: 18px 0 10px;
letter-spacing: -0.2px;
}
p { margin-bottom: 8px; }
/* Summary grid */
.summary-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
margin-bottom: 16px;
}
.summary-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
padding: 14px 16px;
}
.summary-card-label {
font-size: 10px;
font-weight: 700;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.7px;
margin-bottom: 5px;
}
.summary-card-value {
font-size: 22px;
font-weight: 800;
color: var(--text);
letter-spacing: -0.5px;
}
.summary-card-value.verdict-val {
font-size: 16px;
color: var(--verdict);
}
/* Score arc */
.score-section {
display: flex;
align-items: center;
gap: 28px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px 24px;
margin-bottom: 16px;
}
.score-label-group { flex: 1; }
.score-main { font-size: 48px; font-weight: 900; color: var(--verdict); letter-spacing: -2px; line-height: 1; }
.score-unit { font-size: 18px; font-weight: 600; color: var(--muted); }
.score-desc { font-size: 13px; color: var(--muted); margin-top: 6px; }
/* Tables */
table {
width: 100%;
border-collapse: collapse;
margin-top: 8px;
font-size: 13px;
}
thead tr {
background: var(--bg);
}
th {
padding: 9px 12px;
text-align: left;
font-size: 10.5px;
font-weight: 700;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.6px;
border-bottom: 1.5px solid var(--border);
}
td {
padding: 9px 12px;
border-bottom: 1px solid var(--border);
vertical-align: middle;
color: var(--text);
}
tr:last-child td { border-bottom: none; }
tr:nth-child(even) td { background: #fafbfd; }
.bar-track {
height: 6px;
background: var(--border);
border-radius: 99px;
overflow: hidden;
min-width: 80px;
}
.bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--accent) 0%, #4d94ff 100%);
border-radius: 99px;
}
.sev-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 99px;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.3px;
white-space: nowrap;
}
/* Image preview */
.media-preview {
text-align: center;
background: #0e0e14;
border-radius: 12px;
overflow: hidden;
padding: 16px;
margin-bottom: 16px;
}
.media-preview img {
max-width: 100%;
max-height: 340px;
border-radius: 8px;
object-fit: contain;
}
.media-preview p {
color: rgba(255,255,255,0.4);
font-size: 12px;
margin-top: 10px;
margin-bottom: 0;
font-family: monospace;
}
/* FFT */
.fft-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.fft-item {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px 14px;
}
.fft-item-label {
font-size: 10px;
font-weight: 700;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.6px;
margin-bottom: 4px;
}
.fft-item-value {
font-size: 13px;
font-weight: 600;
color: var(--text);
line-height: 1.5;
}
.anomaly-alert {
display: flex;
align-items: center;
gap: 10px;
background: #fff0f1;
border: 1.5px solid #ff475740;
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 16px;
font-size: 13px;
font-weight: 600;
color: #ff4757;
}
/* Disclaimer */
.disclaimer {
background: #fffbeb;
border: 1px solid #f59e0b40;
border-radius: 10px;
padding: 16px 20px;
font-size: 12.5px;
color: #92400e;
line-height: 1.7;
}
.disclaimer strong { color: #78350f; }
/* Footer */
.report-footer {
background: #0a0f1e;
color: rgba(255,255,255,0.4);
padding: 20px 64px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 11px;
margin-top: 60px;
}
.report-footer strong { color: rgba(255,255,255,0.7); }
@media print {
body { background: white; }
.cover { page-break-after: always; }
section { page-break-inside: avoid; }
.body { padding: 32px 48px; }
}
</style>
</head>
<body>
<!-- ── COVER PAGE ─────────────────────────────────────────── -->
<div class="cover">
<div class="cover-logo">
<div class="logo-mark">U</div>
<div class="logo-text">
<span class="logo-name">UAIDE</span>
<span class="logo-sub">Unified AI Origin Detection Engine</span>
</div>
</div>
<h1>Forensic Analysis Report</h1>
<p class="cover-sub">${escapeHtml(result.filename)} &nbsp;Β·&nbsp; Analysis ID: ${escapeHtml(result.analysisId)}</p>
<div class="verdict-banner">
<div class="verdict-dot"></div>
<span class="verdict-text">${verdictLabel}</span>
<span class="verdict-score">&nbsp;β€” ${result.confidenceScore.toFixed(1)}% confidence</span>
</div>
<div class="cover-meta">
<div class="cover-meta-item">
<span class="cover-meta-label">File</span>
<span class="cover-meta-value">${escapeHtml(result.filename)}</span>
</div>
<div class="cover-meta-item">
<span class="cover-meta-label">Type</span>
<span class="cover-meta-value">${escapeHtml(result.format)}</span>
</div>
<div class="cover-meta-item">
<span class="cover-meta-label">Resolution</span>
<span class="cover-meta-value">${escapeHtml(result.resolution)}</span>
</div>
<div class="cover-meta-item">
<span class="cover-meta-label">File Size</span>
<span class="cover-meta-value">${escapeHtml(result.filesize)}</span>
</div>
<div class="cover-meta-item">
<span class="cover-meta-label">Processing Time</span>
<span class="cover-meta-value">${escapeHtml(result.processingTime)}</span>
</div>
<div class="cover-meta-item">
<span class="cover-meta-label">Report Generated</span>
<span class="cover-meta-value">${now}</span>
</div>
</div>
</div>
<!-- ── BODY ───────────────────────────────────────────────── -->
<div class="body">
<!-- 1. Executive Summary -->
<section>
<h2>1. Executive Summary</h2>
<div class="score-section">
<div>
<div class="score-main">${result.confidenceScore.toFixed(1)}<span class="score-unit">%</span></div>
<div class="score-desc">AI Involvement Confidence Score</div>
</div>
<div class="score-label-group">
<p><strong>Verdict:</strong> <span style="color:${verdictColor};font-weight:800">${verdictLabel}</span></p>
<p style="margin-top:6px;color:#6b7280;font-size:13px">
${result.verdict === 'authentic'
? 'Multi-model ensemble analysis found no significant generative artifacts. Content is consistent with authentic human-captured media.'
: result.verdict === 'ai_generated'
? 'Multi-model ensemble analysis detected strong generative artifacts consistent with GAN or diffusion model synthesis. Content is highly likely to be AI-generated.'
: 'Multi-model ensemble analysis produced inconclusive results. The media exhibits some characteristics of synthetic generation but does not conclusively meet the threshold for either verdict. Manual expert review is advised.'}
</p>
</div>
</div>
<div class="summary-grid">
<div class="summary-card">
<div class="summary-card-label">Models Run</div>
<div class="summary-card-value">${result.modelBreakdown.length}</div>
</div>
<div class="summary-card">
<div class="summary-card-label">Artifacts Found</div>
<div class="summary-card-value">${result.artifacts.length}</div>
</div>
<div class="summary-card">
<div class="summary-card-label">Spectral Anomaly</div>
<div class="summary-card-value" style="font-size:15px;color:${result.fft.spectralAnomaly ? '#ff4757' : '#00c67a'}">
${result.fft.spectralAnomaly ? 'Detected' : 'Not Found'}
</div>
</div>
</div>
</section>
<!-- 2. Media Preview -->
<section>
<h2>2. Media Under Analysis</h2>
<div class="media-preview">
${result.type === 'image'
? `<img src="${previewUrl}" alt="Analysed media β€” ${escapeHtml(result.filename)}" />`
: `<p style="color:rgba(255,255,255,0.5);font-size:14px;padding:40px 0">Video preview not embedded in static report<br><span style="font-size:11px">File: ${escapeHtml(result.filename)} Β· ${escapeHtml(result.duration)} Β· ${result.totalFrames} frames</span></p>`
}
<p>${escapeHtml(result.filename)} &nbsp;Β·&nbsp; ${escapeHtml(result.resolution)} &nbsp;Β·&nbsp; ${escapeHtml(result.filesize)}</p>
</div>
</section>
<!-- 3. Model Ensemble Results -->
<section>
<h2>3. Model Ensemble Results</h2>
<p>The following forensic deep learning models were run in ensemble. Each model independently scores the likelihood of AI generation; results are weighted and fused into the final confidence score.</p>
<table>
<thead>
<tr>
<th>Model</th>
<th>Score</th>
<th>Confidence Bar</th>
<th>Weight</th>
</tr>
</thead>
<tbody>
${modelBreakdownRows(result.modelBreakdown)}
</tbody>
</table>
</section>
<!-- 4. Temporal Analysis (video only) -->
${timelineSection(result)}
<!-- 5. Grad-CAM Artifact Localisation -->
<section>
<h2>${result.type === 'video' ? '5' : '4'}. Grad-CAM Artifact Localisation</h2>
<p>Gradient-weighted Class Activation Mapping (Grad-CAM) was applied to highlight spatial regions in the media that contributed most to the AI-generation classification decision.</p>
<table>
<thead>
<tr><th>Region</th><th>Intensity</th><th>Level</th><th>Bounding Box</th></tr>
</thead>
<tbody>
${gradcamTable(result.gradcam.regions)}
</tbody>
</table>
</section>
<!-- 6. Generative Fingerprints & Artifacts -->
<section>
<h2>${result.type === 'video' ? '6' : '5'}. Detected Artifacts &amp; Generative Fingerprints</h2>
<table>
<thead>
<tr><th>Severity</th><th>Type</th><th>Detail</th></tr>
</thead>
<tbody>
${artifactRows(result.artifacts)}
</tbody>
</table>
</section>
<!-- 7. Frequency Analysis (FFT) -->
<section>
<h2>${result.type === 'video' ? '7' : '6'}. Frequency Domain Analysis (FFT)</h2>
${result.fft.spectralAnomaly ? `<div class="anomaly-alert">&#9888; Spectral anomaly detected in the frequency domain β€” a strong indicator of generative upsampling or GAN synthesis.</div>` : ''}
<div class="fft-grid">
<div class="fft-item">
<div class="fft-item-label">Peak Frequency</div>
<div class="fft-item-value mono">${escapeHtml(result.fft.peakFrequency)}</div>
</div>
<div class="fft-item">
<div class="fft-item-label">Anomaly Bands</div>
<div class="fft-item-value mono">${escapeHtml(result.fft.anomalyBands.join(', '))}</div>
</div>
<div class="fft-item" style="grid-column: span 2">
<div class="fft-item-label">DCT Coefficients</div>
<div class="fft-item-value">${escapeHtml(result.fft.dctCoefficients)}</div>
</div>
<div class="fft-item" style="grid-column: span 2">
<div class="fft-item-label">Noise Pattern</div>
<div class="fft-item-value">${escapeHtml(result.fft.noisePattern)}</div>
</div>
</div>
</section>
<!-- 8. File Metadata -->
<section>
<h2>${result.type === 'video' ? '8' : '7'}. File Metadata</h2>
<table>
<thead><tr><th>Field</th><th>Value</th></tr></thead>
<tbody>${metadataRows(result.metadata)}</tbody>
</table>
</section>
<!-- Disclaimer -->
<section>
<div class="disclaimer">
<strong>Disclaimer:</strong> This report was generated automatically by UAIDE (Unified AI Origin Detection Engine) v2.4 β€” Research Preview.
All results are probabilistic in nature and based on ensemble deep learning inference. No automated system can guarantee 100% accuracy in AI-generated content detection.
These findings <strong>must be corroborated with expert human review</strong> before being used as evidence in any legal, academic, or journalistic context.
UAIDE is developed for academic research and investigative purposes.
<br /><br />
Report generated: ${now} &nbsp;Β·&nbsp; Analysis ID: ${escapeHtml(result.analysisId)} &nbsp;Β·&nbsp; github.com/Deshna24/UAIDE
</div>
</section>
</div>
<!-- ── FOOTER ─────────────────────────────────────────────── -->
<div class="report-footer">
<div><strong>UAIDE</strong> β€” Unified AI Origin Detection Engine Β· Research Preview v2.4</div>
<div>Analysis ID: <strong>${escapeHtml(result.analysisId)}</strong></div>
<div>github.com/Deshna24/UAIDE</div>
</div>
</body>
</html>`;
return html;
}
export function downloadReport(result, previewUrl) {
const html = generateReport(result, previewUrl);
const blob = new Blob([html], { type: 'text/html;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const safeName = result.filename.replace(/\.[^.]+$/, '').replace(/[^a-zA-Z0-9_-]/g, '_');
a.download = `UAIDE_Report_${safeName}_${result.analysisId}.html`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(() => URL.revokeObjectURL(url), 10000);
}