#!/usr/bin/env python3
"""TenderIQ v4 — Modern Gradient (PDF via reportlab)"""
import os
from reportlab.pdfgen.canvas import Canvas
from reportlab.lib.pagesizes import A4, landscape
from reportlab.lib import colors
from reportlab.lib.units import cm, mm
from reportlab.platypus import Paragraph
from reportlab.lib.styles import ParagraphStyle
W, H = landscape(A4) # 841.89 × 595.28 points
# Palette
C_PUR1 = colors.HexColor("#667EEA")
C_PUR2 = colors.HexColor("#764BA2")
C_BLU1 = colors.HexColor("#0EA5E9")
C_BLU2 = colors.HexColor("#2563EB")
C_DARK = colors.HexColor("#0F172A")
C_WHITE = colors.white
C_GRY = colors.HexColor("#64748B")
C_GRY2 = colors.HexColor("#E2E8F0")
C_GRN = colors.HexColor("#10B981")
C_RED = colors.HexColor("#F43F5E")
C_AMB = colors.HexColor("#FBBF24")
C_CARD = colors.HexColor("#FFFFFF")
M = 1.8 * cm # margin
def grad_rect(c, x, y, w, h, col1, col2, steps=40):
"""Approximate horizontal gradient with filled rects."""
sw = w / steps
from reportlab.lib.colors import Color
r1, g1, b1 = col1.red, col1.green, col1.blue
r2, g2, b2 = col2.red, col2.green, col2.blue
for i in range(steps):
t = i / (steps - 1)
r = r1 + t * (r2 - r1)
g = g1 + t * (g2 - g1)
b = b1 + t * (b2 - b1)
c.setFillColor(Color(r, g, b))
c.rect(x + i * sw, y, sw + 1, h, fill=1, stroke=0)
def page_num(c, n):
c.setFont("Helvetica", 9)
c.setFillColor(C_GRY)
c.drawRightString(W - M, 0.8 * cm, str(n))
def header_band(c, title, n):
"""Gradient header band for content slides."""
bh = H * 0.18
grad_rect(c, 0, H - bh, W, bh, C_BLU1, C_BLU2)
c.setFillColor(C_WHITE)
c.setFont("Helvetica-Bold", 22)
c.drawString(M, H - bh + 0.55 * cm, title)
page_num(c, n)
def card(c, x, y, w, h, accent=None):
"""White card with optional top accent border."""
c.setFillColor(C_CARD)
c.setStrokeColor(C_GRY2)
c.setLineWidth(0.5)
c.roundRect(x, y, w, h, 4, fill=1, stroke=1)
if accent:
c.setFillColor(accent)
c.setStrokeColor(accent)
c.roundRect(x, y + h - 4, w, 4, 2, fill=1, stroke=0)
def wrapped(c, text, x, y, width, font="Helvetica", size=12, color=C_DARK, leading=16):
style = ParagraphStyle('s', fontName=font, fontSize=size, leading=leading,
textColor=color)
p = Paragraph(text.replace("\n", "
"), style)
_, used = p.wrapOn(c, width, 999)
p.drawOn(c, x, y - used)
return used
def label(c, text, x, y, sz=10, font="Helvetica", color=C_GRY, align="left"):
c.setFont(font, sz)
c.setFillColor(color)
if align == "center":
c.drawCentredString(x, y, text)
elif align == "right":
c.drawRightString(x, y, text)
else:
c.drawString(x, y, text)
# ── Slide 1 ─────────────────────────────────────────────────────────────────
def s1(c):
grad_rect(c, 0, 0, W, H, C_PUR1, C_PUR2)
c.setFillColor(colors.white)
c.setFont("Helvetica-Bold", 72)
c.drawCentredString(W / 2, H / 2 + 1.5 * cm, "TenderIQ")
c.setFont("Helvetica", 22)
c.drawCentredString(W / 2, H / 2 - 0.5 * cm,
"Explainable AI for Government Tender Evaluation")
c.setFont("Helvetica", 14)
c.setFillColor(colors.HexColor("#DDD6FE"))
c.drawCentredString(W / 2, H / 2 - 1.6 * cm, "CRPF Hackathon · Theme 3")
# divider
c.setStrokeColor(colors.HexColor("#A78BFA"))
c.setLineWidth(1.5)
c.line(W / 2 - 5 * cm, H / 2 - 2.2 * cm, W / 2 + 5 * cm, H / 2 - 2.2 * cm)
c.setFont("Helvetica-Oblique", 13)
c.setFillColor(colors.white)
c.drawCentredString(W / 2, H / 2 - 3.0 * cm,
"From days to minutes. Every decision traceable.")
page_num(c, 1)
c.showPage()
# ── Slide 2 ─────────────────────────────────────────────────────────────────
def s2(c):
c.setFillColor(colors.white)
c.rect(0, 0, W, H, fill=1, stroke=0)
header_band(c, "The Problem with Manual Tender Evaluation", 2)
callouts = [
("3–5", "Days per\ntender evaluation", C_PUR1),
("≠", "Inconsistent verdicts\nfrom evaluators", C_BLU2),
("0", "Zero audit trail\nfor decisions", C_GRN),
]
bw = (W - 2 * M - 1.5 * cm) / 3
top = H * 0.72
for i, (big, sub, acc) in enumerate(callouts):
bx = M + i * (bw + 0.75 * cm)
by = top - 3.8 * cm
card(c, bx, by, bw, 3.8 * cm, acc)
c.setFont("Helvetica-Bold", 52)
c.setFillColor(acc)
c.drawCentredString(bx + bw / 2, by + 2.4 * cm, big)
c.setFont("Helvetica", 12)
c.setFillColor(C_DARK)
lines = sub.split("\n")
for li, line in enumerate(lines):
c.drawCentredString(bx + bw / 2, by + 1.4 * cm - li * 0.45 * cm, line)
bullets = [
"• Mixed document formats: typed PDFs, scanned certificates, phone photographs",
"• Government procurement worth ₹50 lakh crore+ annually in India",
"• Project execution delays traced directly to procurement bottlenecks",
]
by2 = top - 5.5 * cm
c.setFont("Helvetica", 12)
c.setFillColor(C_GRY)
for i, b in enumerate(bullets):
c.drawString(M, by2 - i * 0.55 * cm, b)
page_num(c, 2)
c.showPage()
# ── Slide 3 ─────────────────────────────────────────────────────────────────
def s3(c):
c.setFillColor(colors.white)
c.rect(0, 0, W, H, fill=1, stroke=0)
header_band(c, "TenderIQ — Four Stages, End to End", 3)
stages = [
("1 — Extract",
"DeepSeek LLM reads the tender PDF and returns each criterion as structured JSON: "
"category, mandatory flag, threshold rule, source clause, query hints.",
C_PUR1),
("2 — OCR & Index",
"Three-tier pipeline handles any document format. All text chunked and indexed "
"for semantic retrieval.",
C_BLU2),
("3 — Evaluate",
"Vector search finds relevant evidence. LLM produces a verdict with confidence. "
"Safety rule prevents silent disqualification.",
C_GRN),
("4 — Review & Audit",
"Borderline cases go to human review queue. Every action logged. "
"Full audit trail exportable as CSV.",
C_AMB),
]
cw = (W - 2 * M - 1.5 * cm) / 4
top = H * 0.77
for i, (title, body, acc) in enumerate(stages):
cx = M + i * (cw + 0.5 * cm)
cy = top - 3.5 * cm
card(c, cx, cy, cw, 3.5 * cm, acc)
c.setFont("Helvetica-Bold", 13)
c.setFillColor(acc)
c.drawString(cx + 0.3 * cm, cy + 3.0 * cm, title)
wrapped(c, body, cx + 0.3 * cm, cy + 2.8 * cm, cw - 0.6 * cm, size=11, color=C_DARK, leading=15)
# Callout
by = top - 5.2 * cm
grad_rect(c, M, by, W - 2 * M, 1.2 * cm, C_BLU1, C_BLU2)
c.setFillColor(C_WHITE)
c.setFont("Helvetica-BoldOblique", 13)
c.drawCentredString(W / 2, by + 0.38 * cm,
'"Minutes, not days. Every verdict traceable to a document and page."')
page_num(c, 3)
c.showPage()
# ── Slide 4 ─────────────────────────────────────────────────────────────────
def s4(c):
c.setFillColor(colors.white)
c.rect(0, 0, W, H, fill=1, stroke=0)
header_band(c, "System Architecture", 4)
bw, bh = 5.5 * cm, 1.1 * cm
top = H * 0.74
def abox(x, y, text, acc=C_BLU2):
card(c, x, y, bw, bh, acc)
c.setFont("Helvetica", 10)
c.setFillColor(C_DARK)
lines = text.split("\n")
for li, ln in enumerate(lines):
c.drawCentredString(x + bw / 2, y + bh - 0.4 * cm - li * 0.35 * cm, ln)
def arr(x, y):
c.setStrokeColor(C_BLU2)
c.setLineWidth(1.5)
c.line(x + bw / 2, y, x + bw / 2, y - 0.6 * cm)
# arrowhead
ax = x + bw / 2
ay = y - 0.6 * cm
c.setFillColor(C_BLU2)
p = c.beginPath()
p.moveTo(ax - 0.15 * cm, ay)
p.lineTo(ax + 0.15 * cm, ay)
p.lineTo(ax, ay - 0.25 * cm)
p.close()
c.drawPath(p, fill=1, stroke=0)
step = 1.7 * cm
lx = M
abox(lx, top, "Tender PDF")
arr(lx, top)
abox(lx, top - step, "DeepSeek LLM\n(Extract Criteria)")
arr(lx, top - step)
abox(lx, top - 2 * step, "Criteria JSON\n(C1–C5 structured)")
rx = M + 7 * cm
abox(rx, top, "Bidder Docs\n(PDFs · scans · photos)")
arr(rx, top)
abox(rx, top - step, "3-Tier OCR Pipeline\n① PyMuPDF ② Tesseract ③ Vision LLM")
arr(rx, top - step)
abox(rx, top - 2 * step, "Vector Index\n(all-MiniLM-L6-v2 embeddings)")
# Converge
mid_y = top - 2 * step - bh
mx = M + 3.5 * cm + bw / 2
c.setStrokeColor(C_BLU2)
c.setLineWidth(1.5)
c.line(lx + bw / 2, mid_y, mx, mid_y - 0.6 * cm)
c.line(rx + bw / 2, mid_y, mx, mid_y - 0.6 * cm)
abox(M + 3.5 * cm, mid_y - 1.7 * cm, "DeepSeek LLM — Evaluate\neach criterion")
eval_y = mid_y - 1.7 * cm
arr(M + 3.5 * cm, eval_y)
# Outputs
out_y = eval_y - step
c.setFillColor(colors.HexColor("#D1FAE5"))
c.setStrokeColor(C_GRN)
c.setLineWidth(1)
c.roundRect(M + 1 * cm, out_y, 3.5 * cm, bh, 3, fill=1, stroke=1)
c.setFont("Helvetica-Bold", 10)
c.setFillColor(C_GRN)
c.drawCentredString(M + 2.75 * cm, out_y + 0.38 * cm, "eligible / not_eligible")
c.setFillColor(colors.HexColor("#FEF3C7"))
c.setStrokeColor(C_AMB)
c.roundRect(M + 5.5 * cm, out_y, 3.8 * cm, bh, 3, fill=1, stroke=1)
c.setFont("Helvetica-Bold", 10)
c.setFillColor(C_AMB)
c.drawCentredString(M + 7.4 * cm, out_y + 0.38 * cm, "needs_review / Review Queue")
# SQLite
abox(M + 3.5 * cm, out_y - step, "SQLite Audit Log")
# Key facts panel
fx = W - M - 8.5 * cm
c.setFillColor(colors.HexColor("#EFF6FF"))
c.setStrokeColor(C_BLU2)
c.setLineWidth(1)
c.roundRect(fx, H * 0.18, 8.5 * cm, H * 0.56, 5, fill=1, stroke=1)
c.setFont("Helvetica-Bold", 13)
c.setFillColor(C_BLU2)
c.drawString(fx + 0.4 * cm, H * 0.7, "Key Technical Facts")
facts = [
"Single-process Streamlit app",
"Deployable: Streamlit Cloud / HuggingFace",
"All storage local: SQLite + vector index",
"Only external dep: DeepSeek API",
]
c.setFont("Helvetica", 11)
c.setFillColor(C_DARK)
for i, f in enumerate(facts):
c.drawString(fx + 0.4 * cm, H * 0.63 - i * 0.65 * cm, f"• {f}")
page_num(c, 4)
c.showPage()
# ── Slide 5 ─────────────────────────────────────────────────────────────────
def s5(c):
c.setFillColor(colors.white)
c.rect(0, 0, W, H, fill=1, stroke=0)
header_band(c, "Three-Tier OCR — Handling Any Document Format", 5)
tiers = [
("Tier 1 — PyMuPDF",
"Trigger: Typed / digital PDF\nCost: Free, instant\nConfidence: 1.0 (lossless)\nLabel: Typed PDF",
C_BLU2),
("Tier 2 — Tesseract",
"Trigger: Scanned PDF or image\nCost: Free, local, fast\nConfidence: Mean per-word OCR\nLabel: Tesseract",
colors.HexColor("#7C3AED")),
("Tier 3 — DeepSeek Vision LLM",
"Trigger: Tesseract confidence < 65%\nCost: One API call\nConfidence: 0.95\nLabel: Vision LLM\nLogged: vision_ocr_invoked",
colors.HexColor("#EA580C")),
]
cw = (W - 2 * M - 2 * cm) / 3
top = H * 0.74
for i, (title, body, acc) in enumerate(tiers):
cx = M + i * (cw + 1 * cm)
cy = top - 3.2 * cm
card(c, cx, cy, cw, 3.2 * cm, acc)
c.setFont("Helvetica-Bold", 13)
c.setFillColor(acc)
c.drawString(cx + 0.35 * cm, cy + 2.75 * cm, title)
wrapped(c, body.replace("\n", "
"), cx + 0.35 * cm, cy + 2.55 * cm,
cw - 0.7 * cm, size=11, color=C_DARK, leading=15)
# Demo callout
by = H * 0.27
grad_rect(c, M, by, W - 2 * M, 1.95 * cm, C_BLU1, C_BLU2, steps=30)
c.setFillColor(C_WHITE)
c.setFont("Helvetica-Bold", 12)
c.drawString(M + 0.4 * cm, by + 1.58 * cm, "Demo Scenario")
c.setFont("Helvetica", 11)
demo = ("Bidder C submits a blurry, rotated CA certificate scan. Tesseract reads it at ~55% confidence. "
"Vision LLM transcribes the turnover figure correctly. Combined confidence = 0.58 → routed to "
"human review. This is intentional — borderline evidence requires a human.")
wrapped(c, demo, M + 0.4 * cm, by + 1.35 * cm, W - 2 * M - 0.8 * cm,
size=11, color=C_WHITE, leading=15)
page_num(c, 5)
c.showPage()
# ── Slide 6 ─────────────────────────────────────────────────────────────────
def s6(c):
c.setFillColor(colors.white)
c.rect(0, 0, W, H, fill=1, stroke=0)
header_band(c, "Every Decision is Explainable and Auditable", 6)
half = (W - 2 * M - 0.8 * cm) / 2
lx = M
rx = M + half + 0.8 * cm
top = H * 0.76
ch = 3.4 * cm
card(c, lx, top - ch, half, ch, C_BLU2)
c.setFont("Helvetica-Bold", 13)
c.setFillColor(C_BLU2)
c.drawString(lx + 0.35 * cm, top - 0.45 * cm, "Criterion-Level Verdicts")
left_lines = [
"Each (bidder × criterion) pair shows:",
"• Which criterion was checked",
"• Document and page providing evidence",
"• Value extracted (e.g. 'INR 6.2 Cr')",
"• OCR tier that read the document",
"• Combined confidence score (0–100%)",
"• Plain-English reason",
]
c.setFont("Helvetica", 11)
c.setFillColor(C_DARK)
for i, ln in enumerate(left_lines):
c.drawString(lx + 0.35 * cm, top - 0.9 * cm - i * 0.42 * cm, ln)
card(c, rx, top - ch, half, ch, C_BLU2)
c.setFont("Helvetica-Bold", 13)
c.setFillColor(C_BLU2)
c.drawString(rx + 0.35 * cm, top - 0.45 * cm, "Audit Trail")
right_lines = [
"Every action logged with:",
"• UTC timestamp",
"• Action type (criteria_extracted /",
" bidder_processed / criterion_evaluated /",
" vision_ocr_invoked / precomputed_fallback)",
"• Model version & Actor",
"• Full payload JSON — Exportable as CSV",
]
c.setFont("Helvetica", 11)
c.setFillColor(C_DARK)
for i, ln in enumerate(right_lines):
c.drawString(rx + 0.35 * cm, top - 0.9 * cm - i * 0.42 * cm, ln)
# Safety rule
sy = top - ch - 0.4 * cm - 1.8 * cm
c.setFillColor(colors.HexColor("#FEF3C7"))
c.setStrokeColor(C_AMB)
c.setLineWidth(2)
c.roundRect(M, sy, W - 2 * M, 1.8 * cm, 4, fill=1, stroke=1)
c.setFont("Helvetica-Bold", 13)
c.setFillColor(colors.HexColor("#92400E"))
c.drawString(M + 0.4 * cm, sy + 1.35 * cm, "The Safety Rule:")
c.setFont("Helvetica", 12)
c.setFillColor(C_DARK)
rule = ("If combined confidence is 0.55–0.80 AND verdict is not_eligible, the verdict is "
"automatically downgraded to needs_review. A bidder is NEVER silently disqualified at medium confidence.")
wrapped(c, rule, M + 0.4 * cm, sy + 1.1 * cm, W - 2 * M - 0.8 * cm, size=11, color=C_DARK, leading=15)
page_num(c, 6)
c.showPage()
# ── Slide 7 ─────────────────────────────────────────────────────────────────
def s7(c):
c.setFillColor(colors.white)
c.rect(0, 0, W, H, fill=1, stroke=0)
header_band(c, "Demo: Three Bidders, Three Outcomes", 7)
bidders = [
("Bidder A\nApex Constructions Pvt. Ltd.", C_GRN, "ELIGIBLE",
colors.HexColor("#D1FAE5"),
["C1 Turnover: INR 6.37 Cr avg — PASS",
"C2 Projects: 5 completed (CRPF) — PASS",
"C3 GST: GSTIN active — PASS",
"C4 ISO 9001:2015 valid June 2027 — PASS",
"Confidence ≥ 93% on all criteria"]),
("Bidder B\nBuildRight Enterprises", C_RED, "NOT ELIGIBLE",
colors.HexColor("#FEE2E2"),
["C1 Turnover: INR 1.5 Cr — FAIL",
"Below required minimum of INR 5 Cr",
"C2–C4: All pass",
"Disqualified at high confidence (95%)"]),
("Bidder C\nShree Constructions & Services", C_AMB, "NEEDS REVIEW",
colors.HexColor("#FEF3C7"),
["C1 Turnover: Blurry scan submitted",
"Tesseract ~55% → Vision LLM: INR 5.4 Cr",
"Combined confidence 0.58 → safety rule",
"C2: Exactly 3 projects (borderline)",
"C3–C4: Pass"]),
]
cw = (W - 2 * M - 2 * cm) / 3
top = H * 0.76
for i, (name, color, verdict, vbg, bullets) in enumerate(bidders):
cx = M + i * (cw + 1 * cm)
cy = top - 3.5 * cm
card(c, cx, cy, cw, 3.5 * cm, color)
c.setFont("Helvetica-Bold", 12)
c.setFillColor(C_DARK)
for li, nl in enumerate(name.split("\n")):
c.drawString(cx + 0.35 * cm, cy + 3.2 * cm - li * 0.4 * cm, nl)
# verdict chip
c.setFillColor(vbg)
c.setStrokeColor(color)
c.setLineWidth(1)
c.roundRect(cx + 0.3 * cm, cy + 2.5 * cm, cw - 0.6 * cm, 0.5 * cm, 3, fill=1, stroke=1)
c.setFont("Helvetica-Bold", 11)
c.setFillColor(color)
c.drawCentredString(cx + cw / 2, cy + 2.65 * cm, verdict)
c.setFont("Helvetica", 10)
c.setFillColor(C_DARK)
for j, b in enumerate(bullets):
c.drawString(cx + 0.35 * cm, cy + 2.15 * cm - j * 0.42 * cm, b)
# Metric strip
metrics = [
("Criteria extracted", "5"),
("Bidder docs processed", "15"),
("LLM evaluation calls", "15"),
("Vision OCR invocations", "1"),
("Human review items", "1"),
("Total audit entries", "20+"),
]
sy = H * 0.18
grad_rect(c, M, sy, W - 2 * M, 1.5 * cm, C_BLU1, C_BLU2, steps=30)
mw = (W - 2 * M) / len(metrics)
for i, (lbl, val) in enumerate(metrics):
mx = M + i * mw + mw / 2
c.setFont("Helvetica-Bold", 20)
c.setFillColor(C_WHITE)
c.drawCentredString(mx, sy + 0.85 * cm, val)
c.setFont("Helvetica", 9)
c.setFillColor(colors.HexColor("#BFDBFE"))
c.drawCentredString(mx, sy + 0.3 * cm, lbl)
page_num(c, 7)
c.showPage()
# ── Slide 8 ─────────────────────────────────────────────────────────────────
def s8(c):
c.setFillColor(colors.white)
c.rect(0, 0, W, H, fill=1, stroke=0)
header_band(c, "Stack, Impact & What's Next", 8)
stack = [
("UI & orchestration", "Streamlit 1.39"),
("LLM", "DeepSeek API (OpenAI-compatible)"),
("OCR Tier 1", "PyMuPDF 1.24"),
("OCR Tier 2", "Tesseract"),
("OCR Tier 3", "DeepSeek Vision LLM"),
("Semantic retrieval", "sentence-transformers all-MiniLM-L6-v2"),
("Data validation", "Pydantic v2"),
("Audit log", "SQLite"),
("Deployment", "Streamlit Cloud / HuggingFace Spaces"),
]
lx = M
row_h = 0.5 * cm
top = H * 0.76
# Header
grad_rect(c, lx, top - row_h, 12.5 * cm, row_h, C_BLU1, C_BLU2, steps=20)
c.setFont("Helvetica-Bold", 11)
c.setFillColor(C_WHITE)
c.drawString(lx + 0.2 * cm, top - 0.38 * cm, "Component")
c.drawString(lx + 5.5 * cm, top - 0.38 * cm, "Technology")
for i, (comp, tech) in enumerate(stack):
ry = top - row_h - (i + 1) * row_h
bg_c = colors.white if i % 2 == 0 else colors.HexColor("#EFF6FF")
c.setFillColor(bg_c)
c.rect(lx, ry, 12.5 * cm, row_h, fill=1, stroke=0)
c.setFont("Helvetica", 10)
c.setFillColor(C_GRY)
c.drawString(lx + 0.2 * cm, ry + 0.13 * cm, comp)
c.setFillColor(C_DARK)
c.drawString(lx + 5.5 * cm, ry + 0.13 * cm, tech)
# Future work
fx = W - M - 11 * cm
c.setFont("Helvetica-Bold", 13)
c.setFillColor(C_BLU2)
c.drawString(fx, top - 0.4 * cm, "What's Next")
future = [
"Multi-tender workspace — same bidder pool, multiple tenders",
"GeM portal API integration — live tender ingestion",
"Automated bidder ranking with weighted scoring",
"LayoutLM for complex financial tables in scanned statements",
"Multi-evaluator workflow with role-based approval",
"Review queue email/SMS notifications",
"Audit PDF export for procurement oversight submissions",
]
c.setFont("Helvetica", 11)
c.setFillColor(C_DARK)
for i, f in enumerate(future):
c.drawString(fx, top - 0.9 * cm - i * 0.52 * cm, f"• {f}")
# Impact
iy = H * 0.19
grad_rect(c, M, iy, W - 2 * M, 1.2 * cm, C_BLU1, C_BLU2, steps=30)
c.setFont("Helvetica-Bold", 13)
c.setFillColor(C_WHITE)
c.drawCentredString(W / 2, iy + 0.65 * cm,
"3–5 days → minutes. Every verdict traceable to a document, page, and model version.")
c.setFont("Helvetica", 11)
c.drawCentredString(W / 2, iy + 0.28 * cm,
"Built in one hackathon session. Deployable today.")
page_num(c, 8)
c.showPage()
def main():
os.makedirs("deck", exist_ok=True)
out = "deck/TenderIQ_v4_modern_gradient.pdf"
c = Canvas(out, pagesize=landscape(A4))
s1(c); s2(c); s3(c); s4(c)
s5(c); s6(c); s7(c); s8(c)
c.save()
size = os.path.getsize(out)
print(f"OK: Saved {out} ({size:,} bytes, 8 slides)")
if __name__ == "__main__":
main()