Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions backend/app/api/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ async def analyze_paper(
{
"url": w.url, "title": w.title, "snippet": w.snippet,
"matched_text": w.matched_text, "similarity": w.similarity,
"match_type": "uncited",
}
for w in (web_result.matches if web_result else [])
],
Expand Down Expand Up @@ -277,6 +278,9 @@ async def analyze_paper(

result = AnalysisResult(
filename=record.filename,
author_name=record.author_name,
institution=record.institution,
full_text=record.full_text,
analyzed_at=(record.analyzed_at or datetime.now(timezone.utc)).isoformat(),
word_count=record.word_count,
section_count=len(sections_payload),
Expand Down Expand Up @@ -604,6 +608,7 @@ async def _process_analysis_task(task_id: str):
{
"url": w.url, "title": w.title, "snippet": w.snippet,
"matched_text": w.matched_text, "similarity": w.similarity,
"match_type": "uncited",
}
for w in (web_result.matches if web_result else [])
],
Expand Down
3 changes: 3 additions & 0 deletions backend/app/core/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ def _apply_analysis_result_migrations(engine) -> None:
("methodology_fingerprint_score", "INTEGER"),
("provenance_score", "INTEGER"),
("pcv_details", "JSON"),
("author_name", "TEXT"),
("institution", "TEXT"),
("full_text", "TEXT"),
]
try:
from sqlalchemy import inspect
Expand Down
6 changes: 6 additions & 0 deletions backend/app/models/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ class AnalysisResult(Base):

# File metadata
filename = Column(String(255), nullable=False, index=True)
author_name = Column(String(255), nullable=True)
institution = Column(String(255), nullable=True)
full_text = Column(JSON, nullable=True)
file_size = Column(Integer, nullable=False) # bytes
file_hash = Column(String(64), unique=True, index=True) # SHA256
word_count = Column(Integer, nullable=False, default=0)
Expand Down Expand Up @@ -63,6 +66,9 @@ def to_dict(self):
return {
"id": self.id,
"filename": self.filename,
"author_name": self.author_name,
"institution": self.institution,
"full_text": self.full_text,
"file_size": self.file_size,
"word_count": self.word_count,
"plagiarism_score": self.plagiarism_score,
Expand Down
5 changes: 5 additions & 0 deletions backend/app/models/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ class PlagiarismMatchSchema(BaseModel):
similarity: float = Field(..., ge=0, le=100)
source: str
matched_text: str
match_type: str = "uncited" # 'cited', 'uncited', or 'internal'



class PlagiarismModuleResult(BaseModel):
Expand Down Expand Up @@ -95,6 +97,9 @@ class ScoreContributions(BaseModel):

class AnalysisResult(BaseModel):
filename: str
author_name: Optional[str] = None
institution: Optional[str] = None
full_text: Optional[str] = None
analyzed_at: str
word_count: int
section_count: int
Expand Down
3 changes: 3 additions & 0 deletions backend/app/services/plagiarism.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ class SimilarityMatch:
source_corpus: str
similarity: float
matched_text: str
match_type: str = "uncited" # 'cited', 'uncited', or 'internal'



@dataclass
Expand Down Expand Up @@ -278,6 +280,7 @@ def _detect_internal_duplication(sections: List["Section"]) -> List[SimilarityMa
source_corpus="self",
similarity=round(jacc * 100, 1),
matched_text=text_i[:180],
match_type="internal",
)
)
return matches
Expand Down
216 changes: 127 additions & 89 deletions backend/app/services/report.py
Original file line number Diff line number Diff line change
@@ -1,107 +1,145 @@
"""PDF evidence report generation with per-module findings."""
import json
import logging
from pathlib import Path
from datetime import datetime, timezone
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import cm
from reportlab.lib.units import cm, inch
from reportlab.lib import colors
from reportlab.pdfgen import canvas
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak
from reportlab.lib.enums import TA_CENTER, TA_LEFT

from ..core.config import settings

logger = logging.getLogger(__name__)

RIGHT, TOP_MARGIN = A4[0] - 2 * cm, A4[1] - 2 * cm


def _wrap(text: str, max_width, font, fontsize, canvas_obj):
"""Wrap text into lines that fit max_width, return list of lines."""
canvas_obj.setFont(font, fontsize)
words = text.split()
lines, cur = [], ""
for word in words:
trial = (cur + " " + word).strip()
if canvas_obj.stringWidth(trial, font, fontsize) <= max_width:
cur = trial
else:
if cur:
lines.append(cur)
cur = word
if cur:
lines.append(cur)
return lines


def write_pdf_report(doc, record, breakdown, citation_result, statistics_result, writing_result, report_name):
"""Write a detailed evidence PDF for the analysis record."""
output_path = settings.REPORTS_DIR / report_name
def write_pdf_report(doc, record, breakdown, citation_result, statistics_result, writing_quality_result, filename):
"""Generate a professional research integrity report with cover page."""
output_path = settings.REPORTS_DIR / filename
settings.REPORTS_DIR.mkdir(parents=True, exist_ok=True)

c = canvas.Canvas(str(output_path), pagesize=A4)
width = A4[0]
y = TOP_MARGIN
doc_pdf = SimpleDocTemplate(
str(output_path),
pagesize=A4,
leftMargin=2*cm, rightMargin=2*cm,
topMargin=2*cm, bottomMargin=2*cm
)
styles = getSampleStyleSheet()

# Custom Styles
title_style = ParagraphStyle('TitleStyle', parent=styles['Title'], fontSize=24, spaceAfter=30, alignment=TA_CENTER)
subtitle_style = ParagraphStyle('SubtitleStyle', parent=styles['Normal'], fontSize=14, spaceAfter=12, alignment=TA_CENTER, textColor=colors.grey)
heading_style = ParagraphStyle('HeadingStyle', parent=styles['Heading1'], fontSize=16, spaceBefore=20, spaceAfter=12, textColor=colors.HexColor("#1e293b"))

story = []

# Header
c.setFont("Helvetica-Bold", 16)
c.drawString(2 * cm, y, "VeriPaper — Research Verification Report")
y -= 10
c.setFont("Helvetica", 9)
c.drawString(2 * cm, y, f"File: {record.filename} | {record.word_count} words | Analyzed {record.analyzed_at:%Y-%m-%d %H:%M UTC}")
y -= 18
# --- COVER PAGE ---
story.append(Spacer(1, 4 * cm))
story.append(Paragraph("RESEARCH INTEGRITY REPORT", title_style))
story.append(Paragraph("VeriPaper Autonomous Verification Platform", subtitle_style))
story.append(Spacer(1, 2 * cm))

meta_data = [
[Paragraph("<b>Document:</b>", styles['Normal']), Paragraph(record.filename, styles['Normal'])],
[Paragraph("<b>Author:</b>", styles['Normal']), Paragraph(record.author_name or "Not Specified", styles['Normal'])],
[Paragraph("<b>Institution:</b>", styles['Normal']), Paragraph(record.institution or "Not Specified", styles['Normal'])],
[Paragraph("<b>Analysis Date:</b>", styles['Normal']), Paragraph(record.analyzed_at.strftime('%B %d, %Y'), styles['Normal'])],
[Paragraph("<b>Report ID:</b>", styles['Normal']), Paragraph(f"VP-{record.id:06d}", styles['Normal'])],
]
meta_table = Table(meta_data, colWidths=[4 * cm, 10 * cm])
meta_table.setStyle(TableStyle([
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('BOTTOMPADDING', (0, 0), (-1, -1), 8),
]))
story.append(meta_table)

story.append(Spacer(1, 4 * cm))

# Credibility Badge
score_color = colors.green if record.overall_research_credibility >= 75 else (colors.orange if record.overall_research_credibility >= 50 else colors.red)
badge_data = [[Paragraph(f"<font size=12 color=white>OVERALL CREDIBILITY</font><br/><font size=36 color=white><b>{record.overall_research_credibility}%</b></font>", ParagraphStyle('Badge', alignment=TA_CENTER))]]
badge = Table(badge_data, colWidths=[6 * cm])
badge.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, -1), score_color),
('ROUNDEDCORNERS', [10, 10, 10, 10]),
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
('BOTTOMPADDING', (0, 0), (-1, -1), 20),
('TOPPADDING', (0, 0), (-1, -1), 20),
]))
story.append(badge)

story.append(PageBreak())

# Verdict box
verdict_colors = {"Credible": colors.green, "Needs review": colors.orange, "High risk": colors.red}
color = verdict_colors.get(record.verdict, colors.grey)
c.setFillColor(color)
c.roundRect(2 * cm, y - 30, width - 4 * cm, 36, 6, fill=1, stroke=0)
c.setFillColor(colors.white)
c.setFont("Helvetica-Bold", 13)
c.drawString(2 * cm + 12, y - 22, f"{record.verdict} — overall credibility {record.overall_research_credibility}/100")
c.setFont("Helvetica", 9)
c.drawCentredString(width / 2, y - 14, "Scores are assistive likelihood signals, not conclusive proof of misconduct.")
y -= 52
c.setFillColor(colors.black)
# --- EXECUTIVE SUMMARY ---
story.append(Paragraph("1. Integrity Overview", heading_style))

summary_text = f"The document '{record.filename}' has undergone a comprehensive multi-module verification. "
if record.overall_research_credibility >= 75:
summary_text += "The analysis indicates a high level of research integrity with standard citation patterns and natural statistical distributions."
elif record.overall_research_credibility >= 50:
summary_text += "The analysis identifies moderate risks that require manual review, particularly in the areas of citation alignment and statistical consistency."
else:
summary_text += "Significant integrity risks were detected. Multiple forensic modules flagged irregularities that deviate from standard academic writing and reporting."

story.append(Paragraph(summary_text, styles['Normal']))
story.append(Spacer(1, 0.5 * cm))

# Verdict Box
verdict_data = [[Paragraph(f"<b>VERDICT: {record.verdict.upper()}</b>", ParagraphStyle('Verdict', textColor=colors.white, alignment=TA_CENTER))]]
verdict_table = Table(verdict_data, colWidths=[14 * cm])
verdict_table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, -1), score_color),
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
('PADDING', (0, 0), (-1, -1), 10),
]))
story.append(verdict_table)
story.append(Spacer(1, 1 * cm))

sections = [
("AI Generation Detection", [
f"AI-likelihood score: {record.ai_probability}% (confidence: {record.ai_confidence})",
f"Engine: {record.ai_engine}",
]),
("Similarity (Plagiarism) Check", [
f"Similarity score: {record.plagiarism_score}/100",
] + ([f"Match: {m['title']} ({m['similarity']}%)" for m in
(json.loads(record.plagiarism_matches) if record.plagiarism_matches else [])][:5])),
("Citation Validation", [
f"Validity score: {record.citation_validity_score}/100",
f"DOIs checked: {json.loads(record.citation_details).get('total_dois', 0) if record.citation_details else 0} "
f"(valid: {json.loads(record.citation_details).get('valid_dois', 0) if record.citation_details else 0})",
]),
("Statistical Integrity", [
f"Risk score: {record.statistical_risk_score}/100 — {statistics_result.summary}",
] + [f"• {f.category}: {f.detail}" for f in statistics_result.findings][:4]),
("Academic Writing Standards", [
f"Writing quality: {record.writing_quality_score}/100 ({writing_result.grade})",
] + [f"• {'PASS' if ck['passed'] else 'FAIL'} {ck['name']}: {ck['detail']}" for ck in
(json.loads(record.writing_checks) if record.writing_checks else [])]),
# --- MODULE DETAILS ---
story.append(Paragraph("2. Detailed Module Findings", heading_style))

modules = [
("AI Detection", f"{record.ai_probability}% probability (Engine: {record.ai_engine})"),
("Similarity Check", f"{record.plagiarism_score}% overlap with indexed corpus"),
("Citation Validity", f"{record.citation_validity_score}% CrossRef validation score"),
("Statistical Integrity", f"{record.statistical_risk_score}% risk based on distribution forensics"),
("Writing Quality", f"{record.writing_quality_score}% adherence to academic standards"),
]

mod_table = Table(modules, colWidths=[5 * cm, 9 * cm])
mod_table.setStyle(TableStyle([
('GRID', (0, 0), (-1, -1), 0.5, colors.grey),
('BACKGROUND', (0, 0), (0, -1), colors.whitesmoke),
('PADDING', (0, 0), (-1, -1), 6),
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
]))
story.append(mod_table)

# --- SIMILARITY TAXONOMY ---
story.append(Paragraph("3. Similarity & Attribution Taxonomy", heading_style))
plag_matches = json.loads(record.plagiarism_matches) if record.plagiarism_matches else []
if plag_matches:
match_data = [["Source Title", "Similarity", "Type"]]
for m in plag_matches[:10]:
m_type = m.get("match_type", "uncited").capitalize()
match_data.append([m["title"], f"{m['similarity']}%", m_type])

t = Table(match_data, colWidths=[8 * cm, 3 * cm, 3 * cm])
t.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor("#f1f5f9")),
('GRID', (0, 0), (-1, -1), 0.5, colors.grey),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('PADDING', (0, 0), (-1, -1), 6),
]))
story.append(t)
else:
story.append(Paragraph("No significant similarities detected.", styles['Normal']))

for title, lines in sections:
c.setFont("Helvetica-Bold", 11)
c.drawString(2 * cm, y, title)
y -= 13
c.setFont("Helvetica", 9)
for line in lines:
wrapped = _wrap(line, width - 4 * cm, "Helvetica", 9, c)
for wline in wrapped:
if y < 3 * cm:
c.showPage()
y = TOP_MARGIN
c.setFont("Helvetica", 9)
c.drawString(2 * cm + 6, y, wline)
y -= 12
y -= 8
# Footer
story.append(Spacer(1, 2 * cm))
story.append(Paragraph("<i>Generated by VeriPaper Autonomous Platform. This report is intended for academic assistance only.</i>", styles['Normal']))

c.setFont("Helvetica-Oblique", 8)
c.drawString(2 * cm, 1.4 * cm, "Generated by VeriPaper. This report is assistive evidence only.")
c.showPage()
c.save()
logger.info("Report written: %s", output_path)
doc_pdf.build(story)
logger.info("Professional report written: %s", output_path)
1 change: 1 addition & 0 deletions backend/frontend/dist/assets/index-BWQc25CZ.css

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion backend/frontend/dist/assets/index-DO6YDTte.css

This file was deleted.

4 changes: 2 additions & 2 deletions backend/frontend/dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>VeriPaper — Research Paper Verification Suite</title>
<meta name="description" content="VeriPaper checks research papers in seconds: plagiarism, AI authorship, citation authenticity, statistical integrity, and academic writing standards. Upload PDF, DOCX, or TXT and get a credibility score with plain-language explanations." />
<script type="module" crossorigin src="/static/assets/index-BOzsEJay.js"></script>
<link rel="stylesheet" crossorigin href="/static/assets/index-DO6YDTte.css">
<script type="module" crossorigin src="/static/assets/index-BzxcpKo6.js"></script>
<link rel="stylesheet" crossorigin href="/static/assets/index-BWQc25CZ.css">
</head>
<body class="bg-slate-950 text-slate-100">
<div id="root"></div>
Expand Down
Loading
Loading