Add presentation generation scripts, gitignore deck binaries
Browse files- Add scripts/make_v1-6.py for generating all 6 presentation variants
- Remove old deck/TenderIQ_Pitch.pdf (superseded)
- Ignore deck/*.pptx and deck/*.pdf — generated locally, not tracked
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- .gitignore +4 -0
- deck/TenderIQ_Pitch.pdf +0 -3
- scripts/make_v1.py +463 -0
- scripts/make_v2.py +450 -0
- scripts/make_v3.py +453 -0
- scripts/make_v4.py +575 -0
- scripts/make_v5.py +552 -0
- scripts/make_v6.py +547 -0
.gitignore
CHANGED
|
@@ -35,3 +35,7 @@ Thumbs.db
|
|
| 35 |
|
| 36 |
# Streamlit
|
| 37 |
.streamlit/secrets.toml
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
# Streamlit
|
| 37 |
.streamlit/secrets.toml
|
| 38 |
+
|
| 39 |
+
# Generated presentations (keep locally, don't track in git)
|
| 40 |
+
deck/*.pptx
|
| 41 |
+
deck/*.pdf
|
deck/TenderIQ_Pitch.pdf
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:b0b09f58d390bbd8e074811a9ddef8bd64489db204f0278041f97658e3a29c80
|
| 3 |
-
size 18040
|
|
|
|
|
|
|
|
|
|
|
|
scripts/make_v1.py
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""TenderIQ v1 — Dark Professional (PPTX)"""
|
| 3 |
+
import os
|
| 4 |
+
from pptx import Presentation
|
| 5 |
+
from pptx.util import Inches, Pt
|
| 6 |
+
from pptx.dml.color import RGBColor
|
| 7 |
+
from pptx.enum.text import PP_ALIGN
|
| 8 |
+
|
| 9 |
+
# Palette
|
| 10 |
+
BG = RGBColor(0x0D, 0x1B, 0x2A)
|
| 11 |
+
CARD = RGBColor(0x1E, 0x3A, 0x5F)
|
| 12 |
+
CARD2 = RGBColor(0x16, 0x2D, 0x4A)
|
| 13 |
+
BDR = RGBColor(0x2D, 0x4A, 0x6B)
|
| 14 |
+
GOLD = RGBColor(0xF0, 0xA5, 0x00)
|
| 15 |
+
WHT = RGBColor(0xF1, 0xF5, 0xF9)
|
| 16 |
+
MUT = RGBColor(0x94, 0xA3, 0xB8)
|
| 17 |
+
GRN = RGBColor(0x22, 0xC5, 0x5E)
|
| 18 |
+
RED_C = RGBColor(0xEF, 0x44, 0x44)
|
| 19 |
+
AMB = RGBColor(0xF5, 0x9E, 0x0B)
|
| 20 |
+
FONT = "Arial"
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def new_prs():
|
| 24 |
+
prs = Presentation()
|
| 25 |
+
prs.slide_width = Inches(13.33)
|
| 26 |
+
prs.slide_height = Inches(7.5)
|
| 27 |
+
return prs
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def blank(prs):
|
| 31 |
+
return prs.slides.add_slide(prs.slide_layouts[6])
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def rect(slide, x, y, w, h, fill, line=None, lw=Pt(1)):
|
| 35 |
+
s = slide.shapes.add_shape(1, x, y, w, h)
|
| 36 |
+
s.fill.solid()
|
| 37 |
+
s.fill.fore_color.rgb = fill
|
| 38 |
+
if line:
|
| 39 |
+
s.line.color.rgb = line
|
| 40 |
+
s.line.width = lw
|
| 41 |
+
else:
|
| 42 |
+
s.line.fill.background()
|
| 43 |
+
return s
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def txt(slide, text, x, y, w, h, sz=15, bold=False, clr=None, align=PP_ALIGN.LEFT):
|
| 47 |
+
tb = slide.shapes.add_textbox(x, y, w, h)
|
| 48 |
+
tf = tb.text_frame
|
| 49 |
+
tf.word_wrap = True
|
| 50 |
+
p = tf.paragraphs[0]
|
| 51 |
+
p.alignment = align
|
| 52 |
+
r = p.add_run()
|
| 53 |
+
r.text = text
|
| 54 |
+
r.font.size = Pt(sz)
|
| 55 |
+
r.font.bold = bold
|
| 56 |
+
r.font.name = FONT
|
| 57 |
+
if clr:
|
| 58 |
+
r.font.color.rgb = clr
|
| 59 |
+
return tb
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def heading(slide, text, y=Inches(0.3)):
|
| 63 |
+
rect(slide, Inches(0.5), y + Inches(0.05), Inches(0.07), Inches(0.48), GOLD)
|
| 64 |
+
txt(slide, text, Inches(0.7), y, Inches(12.3), Inches(0.6), sz=26, bold=True, clr=GOLD)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def bg(slide):
|
| 68 |
+
rect(slide, 0, 0, Inches(13.33), Inches(7.5), BG)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def card_box(slide, x, y, w, h, title=None, lines=None, title_clr=None, accent=None):
|
| 72 |
+
border_clr = accent or GOLD
|
| 73 |
+
rect(slide, x, y, w, h, CARD, border_clr, Pt(1))
|
| 74 |
+
cy = y + Inches(0.18)
|
| 75 |
+
if title:
|
| 76 |
+
txt(slide, title, x + Inches(0.18), cy, w - Inches(0.36), Inches(0.45),
|
| 77 |
+
sz=15, bold=True, clr=title_clr or GOLD)
|
| 78 |
+
cy += Inches(0.45)
|
| 79 |
+
if lines:
|
| 80 |
+
for line in lines:
|
| 81 |
+
txt(slide, line, x + Inches(0.18), cy, w - Inches(0.36), Inches(0.35),
|
| 82 |
+
sz=12, clr=WHT)
|
| 83 |
+
cy += Inches(0.32)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def chip(slide, x, y, w, h, fill, text):
|
| 87 |
+
rect(slide, x, y, w, h, fill)
|
| 88 |
+
txt(slide, text, x, y, w, h, sz=12, bold=True,
|
| 89 |
+
clr=RGBColor(0xFF, 0xFF, 0xFF), align=PP_ALIGN.CENTER)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
# ── Slide 1 ─────────────────────────────────────────────────────────────────
|
| 93 |
+
def s1(prs):
|
| 94 |
+
slide = blank(prs)
|
| 95 |
+
bg(slide)
|
| 96 |
+
rect(slide, Inches(0), Inches(0), Inches(13.33), Inches(0.6), RGBColor(0x1E, 0x3A, 0x5F))
|
| 97 |
+
txt(slide, "⚖", Inches(5.2), Inches(0.8), Inches(3), Inches(1.6),
|
| 98 |
+
sz=80, align=PP_ALIGN.CENTER, clr=GOLD)
|
| 99 |
+
txt(slide, "TenderIQ", Inches(1), Inches(2.4), Inches(11.33), Inches(1.4),
|
| 100 |
+
sz=64, bold=True, clr=GOLD, align=PP_ALIGN.CENTER)
|
| 101 |
+
txt(slide, "Explainable AI for Government Tender Evaluation",
|
| 102 |
+
Inches(1), Inches(3.85), Inches(11.33), Inches(0.65),
|
| 103 |
+
sz=22, clr=WHT, align=PP_ALIGN.CENTER)
|
| 104 |
+
txt(slide, "CRPF Hackathon · Theme 3",
|
| 105 |
+
Inches(1), Inches(4.6), Inches(11.33), Inches(0.5),
|
| 106 |
+
sz=17, clr=MUT, align=PP_ALIGN.CENTER)
|
| 107 |
+
txt(slide, "From days to minutes. Every decision traceable.",
|
| 108 |
+
Inches(1), Inches(5.25), Inches(11.33), Inches(0.5),
|
| 109 |
+
sz=16, clr=GOLD, align=PP_ALIGN.CENTER)
|
| 110 |
+
rect(slide, Inches(3.5), Inches(6.85), Inches(6.33), Inches(0.05), GOLD)
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
# ── Slide 2 ─────────────────────────────────────────────────────────────────
|
| 114 |
+
def s2(prs):
|
| 115 |
+
slide = blank(prs)
|
| 116 |
+
bg(slide)
|
| 117 |
+
heading(slide, "The Problem with Manual Tender Evaluation")
|
| 118 |
+
callouts = [
|
| 119 |
+
("3–5 Days", "per tender evaluation\nby committee"),
|
| 120 |
+
("Inconsistent", "two evaluators,\ntwo different conclusions"),
|
| 121 |
+
("No Audit Trail", "decisions made\nuntraceably"),
|
| 122 |
+
]
|
| 123 |
+
cw, ch = Inches(3.8), Inches(2.2)
|
| 124 |
+
for i, (big, sub) in enumerate(callouts):
|
| 125 |
+
sx = Inches(0.55) + i * Inches(4.18)
|
| 126 |
+
rect(slide, sx, Inches(1.1), cw, ch, CARD, GOLD, Pt(1))
|
| 127 |
+
txt(slide, big, sx + Inches(0.15), Inches(1.3), cw - Inches(0.3), Inches(0.85),
|
| 128 |
+
sz=30, bold=True, clr=GOLD, align=PP_ALIGN.CENTER)
|
| 129 |
+
txt(slide, sub, sx + Inches(0.15), Inches(2.2), cw - Inches(0.3), Inches(0.85),
|
| 130 |
+
sz=14, clr=WHT, align=PP_ALIGN.CENTER)
|
| 131 |
+
bullets = [
|
| 132 |
+
"• Mixed document formats: typed PDFs, scans, phone photographs",
|
| 133 |
+
"• Government procurement worth ₹50 lakh crore+ annually in India",
|
| 134 |
+
"• Project delays traced directly to procurement bottlenecks",
|
| 135 |
+
]
|
| 136 |
+
for i, b in enumerate(bullets):
|
| 137 |
+
txt(slide, b, Inches(0.7), Inches(3.55) + Inches(0.43 * i),
|
| 138 |
+
Inches(12.3), Inches(0.38), sz=14, clr=MUT)
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
# ── Slide 3 ─────────────────────────────────────────────────────────────────
|
| 142 |
+
def s3(prs):
|
| 143 |
+
slide = blank(prs)
|
| 144 |
+
bg(slide)
|
| 145 |
+
heading(slide, "TenderIQ — Four Stages, End to End")
|
| 146 |
+
stages = [
|
| 147 |
+
("Stage 1 — Extract",
|
| 148 |
+
"DeepSeek LLM reads the tender PDF\nand returns each criterion as structured\nJSON: category, mandatory flag, threshold\nrule, source clause, query hints."),
|
| 149 |
+
("Stage 2 — OCR & Index",
|
| 150 |
+
"Three-tier pipeline handles any document\nformat. All text chunked and indexed for\nsemantic retrieval."),
|
| 151 |
+
("Stage 3 — Evaluate",
|
| 152 |
+
"Vector search finds relevant evidence.\nLLM produces a verdict with confidence.\nSafety rule prevents silent disqualification."),
|
| 153 |
+
("Stage 4 — Review & Audit",
|
| 154 |
+
"Borderline cases go to human review\nqueue. Every action logged. Full audit\ntrail exportable as CSV."),
|
| 155 |
+
]
|
| 156 |
+
cw, ch = Inches(2.9), Inches(3.85)
|
| 157 |
+
for i, (title, body) in enumerate(stages):
|
| 158 |
+
sx = Inches(0.5) + i * Inches(3.15)
|
| 159 |
+
rect(slide, sx, Inches(1.1), cw, ch, CARD, GOLD, Pt(1))
|
| 160 |
+
rect(slide, sx, Inches(1.1), cw, Inches(0.07), GOLD)
|
| 161 |
+
txt(slide, title, sx + Inches(0.15), Inches(1.2), cw - Inches(0.3), Inches(0.5),
|
| 162 |
+
sz=15, bold=True, clr=GOLD)
|
| 163 |
+
txt(slide, body, sx + Inches(0.15), Inches(1.75), cw - Inches(0.3), Inches(2.9),
|
| 164 |
+
sz=13, clr=WHT)
|
| 165 |
+
rect(slide, Inches(0.7), Inches(5.3), Inches(11.93), Inches(0.8),
|
| 166 |
+
RGBColor(0x1A, 0x30, 0x50), GOLD, Pt(2))
|
| 167 |
+
txt(slide, '"Minutes, not days. Every verdict traceable to a document and page."',
|
| 168 |
+
Inches(0.9), Inches(5.42), Inches(11.5), Inches(0.6),
|
| 169 |
+
sz=15, bold=True, clr=GOLD, align=PP_ALIGN.CENTER)
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
# ── Slide 4 ─────────────────────────────────────────────────────────────────
|
| 173 |
+
def s4(prs):
|
| 174 |
+
slide = blank(prs)
|
| 175 |
+
bg(slide)
|
| 176 |
+
heading(slide, "System Architecture")
|
| 177 |
+
|
| 178 |
+
bw, bh = Inches(2.5), Inches(0.52)
|
| 179 |
+
|
| 180 |
+
def abox(x, y, label, fill=CARD):
|
| 181 |
+
rect(slide, x, y, bw, bh, fill, GOLD, Pt(1))
|
| 182 |
+
txt(slide, label, x + Inches(0.1), y + Inches(0.06),
|
| 183 |
+
bw - Inches(0.2), bh - Inches(0.12),
|
| 184 |
+
sz=12, clr=WHT, align=PP_ALIGN.CENTER)
|
| 185 |
+
|
| 186 |
+
def arr(x, y):
|
| 187 |
+
rect(slide, x + Inches(1.15), y, Inches(0.2), Inches(0.3), GOLD)
|
| 188 |
+
|
| 189 |
+
# Left: Tender PDF path
|
| 190 |
+
lx = Inches(0.5)
|
| 191 |
+
abox(lx, Inches(1.0), "Tender PDF")
|
| 192 |
+
arr(lx, Inches(1.52))
|
| 193 |
+
abox(lx, Inches(1.82), "DeepSeek LLM\n(Extract Criteria)")
|
| 194 |
+
arr(lx, Inches(2.34))
|
| 195 |
+
abox(lx, Inches(2.64), "Criteria JSON\n(C1–C5 structured)")
|
| 196 |
+
|
| 197 |
+
# Right: Bidder docs path
|
| 198 |
+
rx = Inches(4.0)
|
| 199 |
+
abox(rx, Inches(1.0), "Bidder Docs\n(PDFs · scans · photos)")
|
| 200 |
+
arr(rx, Inches(1.52))
|
| 201 |
+
abox(rx, Inches(1.82), "3-Tier OCR Pipeline\n① PyMuPDF ② Tesseract ③ Vision LLM")
|
| 202 |
+
arr(rx, Inches(2.34))
|
| 203 |
+
abox(rx, Inches(2.64), "Vector Index\n(all-MiniLM-L6-v2 embeddings)")
|
| 204 |
+
|
| 205 |
+
# Converging arrow
|
| 206 |
+
cx = Inches(3.25)
|
| 207 |
+
arr(cx, Inches(3.16))
|
| 208 |
+
abox(Inches(2.0), Inches(3.46), "DeepSeek LLM — Evaluate each criterion")
|
| 209 |
+
|
| 210 |
+
# Outputs
|
| 211 |
+
rect(slide, Inches(0.7), Inches(4.2), Inches(2.0), Inches(0.52),
|
| 212 |
+
RGBColor(0x14, 0x33, 0x14), GRN, Pt(1))
|
| 213 |
+
txt(slide, "eligible /\nnot_eligible", Inches(0.7), Inches(4.2), Inches(2.0), Inches(0.52),
|
| 214 |
+
sz=11, clr=GRN, align=PP_ALIGN.CENTER)
|
| 215 |
+
|
| 216 |
+
rect(slide, Inches(3.3), Inches(4.2), Inches(2.0), Inches(0.52),
|
| 217 |
+
RGBColor(0x3D, 0x28, 0x00), AMB, Pt(1))
|
| 218 |
+
txt(slide, "needs_review\nHuman Review Queue", Inches(3.3), Inches(4.2), Inches(2.0), Inches(0.52),
|
| 219 |
+
sz=11, clr=AMB, align=PP_ALIGN.CENTER)
|
| 220 |
+
|
| 221 |
+
abox(Inches(1.5), Inches(5.1), "SQLite Audit Log", fill=RGBColor(0x0D, 0x20, 0x38))
|
| 222 |
+
|
| 223 |
+
# Key facts sidebar
|
| 224 |
+
rect(slide, Inches(7.0), Inches(1.0), Inches(5.8), Inches(5.2), CARD, GOLD, Pt(1))
|
| 225 |
+
txt(slide, "Key Technical Facts", Inches(7.15), Inches(1.1), Inches(5.5), Inches(0.4),
|
| 226 |
+
sz=15, bold=True, clr=GOLD)
|
| 227 |
+
facts = [
|
| 228 |
+
"Single-process Streamlit app — no separate backend",
|
| 229 |
+
"Deployable to Streamlit Cloud or HuggingFace Spaces",
|
| 230 |
+
"All storage is local: SQLite + in-memory vector index",
|
| 231 |
+
"Only external dependency: DeepSeek API",
|
| 232 |
+
]
|
| 233 |
+
for i, f in enumerate(facts):
|
| 234 |
+
txt(slide, f"• {f}", Inches(7.15), Inches(1.65) + Inches(0.65 * i),
|
| 235 |
+
Inches(5.5), Inches(0.55), sz=13, clr=WHT)
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
# ── Slide 5 ─────────────────────────────────────────────────────────────────
|
| 239 |
+
def s5(prs):
|
| 240 |
+
slide = blank(prs)
|
| 241 |
+
bg(slide)
|
| 242 |
+
heading(slide, "Three-Tier OCR — Handling Any Document Format")
|
| 243 |
+
tiers = [
|
| 244 |
+
("Tier 1 — PyMuPDF",
|
| 245 |
+
"Trigger: Typed / digital PDF\n"
|
| 246 |
+
"Cost: Free, instant\n"
|
| 247 |
+
"Confidence: 1.0 (lossless text extraction)\n"
|
| 248 |
+
"Source label: Typed PDF",
|
| 249 |
+
RGBColor(0x1D, 0x4E, 0xD8)),
|
| 250 |
+
("Tier 2 — Tesseract",
|
| 251 |
+
"Trigger: Scanned PDF or image file\n"
|
| 252 |
+
"Cost: Free, local, fast\n"
|
| 253 |
+
"Confidence: Mean of per-word OCR scores\n"
|
| 254 |
+
"Source label: Tesseract",
|
| 255 |
+
RGBColor(0x7C, 0x3A, 0xED)),
|
| 256 |
+
("Tier 3 — DeepSeek Vision LLM",
|
| 257 |
+
"Trigger: Tesseract confidence < 65%\n"
|
| 258 |
+
"Cost: One API call\n"
|
| 259 |
+
"Confidence: 0.95\n"
|
| 260 |
+
"Source label: Vision LLM\n"
|
| 261 |
+
"Logged: vision_ocr_invoked",
|
| 262 |
+
RGBColor(0xC2, 0x51, 0x0E)),
|
| 263 |
+
]
|
| 264 |
+
cw, ch = Inches(3.6), Inches(3.2)
|
| 265 |
+
for i, (title, body, accent) in enumerate(tiers):
|
| 266 |
+
sx = Inches(0.5) + i * Inches(4.3)
|
| 267 |
+
rect(slide, sx, Inches(1.1), cw, ch, CARD, accent, Pt(2))
|
| 268 |
+
rect(slide, sx, Inches(1.1), cw, Inches(0.1), accent)
|
| 269 |
+
txt(slide, title, sx + Inches(0.15), Inches(1.25), cw - Inches(0.3), Inches(0.5),
|
| 270 |
+
sz=15, bold=True, clr=accent)
|
| 271 |
+
txt(slide, body, sx + Inches(0.15), Inches(1.85), cw - Inches(0.3), Inches(2.2),
|
| 272 |
+
sz=13, clr=WHT)
|
| 273 |
+
|
| 274 |
+
rect(slide, Inches(0.5), Inches(4.55), Inches(12.33), Inches(1.72),
|
| 275 |
+
RGBColor(0x1A, 0x28, 0x40), AMB, Pt(2))
|
| 276 |
+
txt(slide, "Demo Scenario",
|
| 277 |
+
Inches(0.7), Inches(4.65), Inches(12), Inches(0.38),
|
| 278 |
+
sz=14, bold=True, clr=AMB)
|
| 279 |
+
demo = ("Bidder C submits a blurry, rotated CA certificate scan. "
|
| 280 |
+
"Tesseract reads it at ~55% confidence. Vision LLM transcribes the turnover figure correctly. "
|
| 281 |
+
"Combined confidence = 0.58 → routed to human review. "
|
| 282 |
+
"This is intentional — borderline evidence requires a human.")
|
| 283 |
+
txt(slide, demo, Inches(0.7), Inches(5.1), Inches(12), Inches(1.0), sz=13, clr=WHT)
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
# ── Slide 6 ─────────────────────────────────────────────────────────────────
|
| 287 |
+
def s6(prs):
|
| 288 |
+
slide = blank(prs)
|
| 289 |
+
bg(slide)
|
| 290 |
+
heading(slide, "Every Decision is Explainable and Auditable")
|
| 291 |
+
|
| 292 |
+
# Left
|
| 293 |
+
lx = Inches(0.5)
|
| 294 |
+
rect(slide, lx, Inches(1.1), Inches(5.85), Inches(4.0), CARD, GOLD, Pt(1))
|
| 295 |
+
txt(slide, "Criterion-Level Verdicts", lx + Inches(0.15), Inches(1.2),
|
| 296 |
+
Inches(5.5), Inches(0.42), sz=15, bold=True, clr=GOLD)
|
| 297 |
+
left_lines = [
|
| 298 |
+
"Each (bidder × criterion) pair shows:",
|
| 299 |
+
"• Which criterion was checked",
|
| 300 |
+
"• Which document and page provided evidence",
|
| 301 |
+
"• What value was extracted (e.g. 'INR 6.2 Cr')",
|
| 302 |
+
"• Which OCR tier read the document",
|
| 303 |
+
"• Combined confidence score (0–100%)",
|
| 304 |
+
"• Plain-English reason",
|
| 305 |
+
]
|
| 306 |
+
for i, line in enumerate(left_lines):
|
| 307 |
+
clr = MUT if i == 0 else WHT
|
| 308 |
+
txt(slide, line, lx + Inches(0.15), Inches(1.65) + Inches(0.42 * i),
|
| 309 |
+
Inches(5.55), Inches(0.38), sz=13, clr=clr)
|
| 310 |
+
|
| 311 |
+
# Right
|
| 312 |
+
rx = Inches(6.98)
|
| 313 |
+
rect(slide, rx, Inches(1.1), Inches(5.85), Inches(4.0), CARD, GOLD, Pt(1))
|
| 314 |
+
txt(slide, "Audit Trail", rx + Inches(0.15), Inches(1.2),
|
| 315 |
+
Inches(5.5), Inches(0.42), sz=15, bold=True, clr=GOLD)
|
| 316 |
+
right_lines = [
|
| 317 |
+
"Every action logged with:",
|
| 318 |
+
"• UTC timestamp",
|
| 319 |
+
"• Action type: criteria_extracted / bidder_processed",
|
| 320 |
+
" criterion_evaluated / human_review_action",
|
| 321 |
+
" vision_ocr_invoked / precomputed_fallback_used",
|
| 322 |
+
"• Model version & Actor (system / officer)",
|
| 323 |
+
"• Full payload JSON — Exportable as CSV",
|
| 324 |
+
]
|
| 325 |
+
for i, line in enumerate(right_lines):
|
| 326 |
+
clr = MUT if i == 0 else WHT
|
| 327 |
+
txt(slide, line, rx + Inches(0.15), Inches(1.65) + Inches(0.42 * i),
|
| 328 |
+
Inches(5.55), Inches(0.38), sz=13, clr=clr)
|
| 329 |
+
|
| 330 |
+
# Safety rule
|
| 331 |
+
rect(slide, Inches(0.5), Inches(5.38), Inches(12.33), Inches(1.22),
|
| 332 |
+
RGBColor(0x3D, 0x1A, 0x00), AMB, Pt(2))
|
| 333 |
+
txt(slide, "The Safety Rule:",
|
| 334 |
+
Inches(0.7), Inches(5.45), Inches(4.5), Inches(0.4),
|
| 335 |
+
sz=15, bold=True, clr=AMB)
|
| 336 |
+
txt(slide, ("If combined confidence is 0.55–0.80 AND verdict is not_eligible, "
|
| 337 |
+
"the verdict is automatically downgraded to needs_review. "
|
| 338 |
+
"A bidder is NEVER silently disqualified at medium confidence."),
|
| 339 |
+
Inches(0.7), Inches(5.88), Inches(12), Inches(0.65),
|
| 340 |
+
sz=13, clr=WHT)
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
# ── Slide 7 ─────────────────────────────────────────────────────────────────
|
| 344 |
+
def s7(prs):
|
| 345 |
+
slide = blank(prs)
|
| 346 |
+
bg(slide)
|
| 347 |
+
heading(slide, "Demo: Three Bidders, Three Outcomes")
|
| 348 |
+
bidders = [
|
| 349 |
+
("Bidder A — Apex Constructions Pvt. Ltd.", GRN, "ELIGIBLE",
|
| 350 |
+
["C1 Turnover: INR 6.37 Cr avg (threshold 5 Cr) — PASS",
|
| 351 |
+
"C2 Projects: 5 completed incl. CRPF barracks — PASS",
|
| 352 |
+
"C3 GST: GSTIN 27AABCA1234F1Z5, Active — PASS",
|
| 353 |
+
"C4 ISO 9001:2015: Valid June 2027 — PASS",
|
| 354 |
+
"All typed PDFs, confidence ≥ 93% on all criteria"]),
|
| 355 |
+
("Bidder B — BuildRight Enterprises", RED_C, "NOT ELIGIBLE",
|
| 356 |
+
["C1 Turnover: INR 1.5 Cr avg (threshold 5 Cr) — FAIL",
|
| 357 |
+
"\"INR 1.5 Cr is below required minimum of INR 5 Cr\"",
|
| 358 |
+
"C2–C4: All pass",
|
| 359 |
+
"Auto-disqualified at high confidence (95%)"]),
|
| 360 |
+
("Bidder C — Shree Constructions & Services", AMB, "NEEDS REVIEW",
|
| 361 |
+
["C1 Turnover: Submitted as blurry scan",
|
| 362 |
+
"Tesseract ~55% → Vision LLM: INR 5.4 Cr",
|
| 363 |
+
"Combined confidence 0.58 → safety rule triggered",
|
| 364 |
+
"C2: Exactly 3 projects (borderline)",
|
| 365 |
+
"C3–C4: Pass"]),
|
| 366 |
+
]
|
| 367 |
+
cw, ch = Inches(3.85), Inches(3.6)
|
| 368 |
+
for i, (name, color, verdict, bullets) in enumerate(bidders):
|
| 369 |
+
sx = Inches(0.5) + i * Inches(4.25)
|
| 370 |
+
rect(slide, sx, Inches(1.1), cw, ch, CARD, color, Pt(2))
|
| 371 |
+
txt(slide, name, sx + Inches(0.15), Inches(1.2), cw - Inches(0.3), Inches(0.5),
|
| 372 |
+
sz=12, bold=True, clr=WHT)
|
| 373 |
+
chip(slide, sx + Inches(0.15), Inches(1.75), Inches(3.3), Inches(0.35), color, verdict)
|
| 374 |
+
for j, b in enumerate(bullets):
|
| 375 |
+
txt(slide, b, sx + Inches(0.15), Inches(2.2) + Inches(0.37 * j),
|
| 376 |
+
cw - Inches(0.3), Inches(0.33), sz=11, clr=WHT)
|
| 377 |
+
|
| 378 |
+
# Metric strip
|
| 379 |
+
metrics = [
|
| 380 |
+
("Criteria extracted", "5"),
|
| 381 |
+
("Bidder docs processed", "15"),
|
| 382 |
+
("LLM evaluation calls", "15"),
|
| 383 |
+
("Vision OCR invocations", "1"),
|
| 384 |
+
("Human review items", "1"),
|
| 385 |
+
("Total audit entries", "20+"),
|
| 386 |
+
]
|
| 387 |
+
rect(slide, Inches(0.5), Inches(5.0), Inches(12.33), Inches(0.98),
|
| 388 |
+
RGBColor(0x10, 0x26, 0x40), GOLD, Pt(1))
|
| 389 |
+
mw = Inches(2.0)
|
| 390 |
+
for i, (label, val) in enumerate(metrics):
|
| 391 |
+
mx = Inches(0.65) + i * Inches(2.0)
|
| 392 |
+
txt(slide, val, mx, Inches(5.07), mw, Inches(0.38),
|
| 393 |
+
sz=20, bold=True, clr=GOLD, align=PP_ALIGN.CENTER)
|
| 394 |
+
txt(slide, label, mx, Inches(5.5), mw, Inches(0.35),
|
| 395 |
+
sz=11, clr=MUT, align=PP_ALIGN.CENTER)
|
| 396 |
+
|
| 397 |
+
|
| 398 |
+
# ── Slide 8 ─────────────────────────────────────────────────────────────────
|
| 399 |
+
def s8(prs):
|
| 400 |
+
slide = blank(prs)
|
| 401 |
+
bg(slide)
|
| 402 |
+
heading(slide, "Stack, Impact & What's Next")
|
| 403 |
+
stack = [
|
| 404 |
+
("UI & orchestration", "Streamlit 1.39"),
|
| 405 |
+
("LLM", "DeepSeek API (OpenAI-compatible)"),
|
| 406 |
+
("OCR Tier 1", "PyMuPDF 1.24"),
|
| 407 |
+
("OCR Tier 2", "Tesseract"),
|
| 408 |
+
("OCR Tier 3", "DeepSeek Vision LLM"),
|
| 409 |
+
("Semantic retrieval", "sentence-transformers all-MiniLM-L6-v2"),
|
| 410 |
+
("Data validation", "Pydantic v2"),
|
| 411 |
+
("Audit log", "SQLite"),
|
| 412 |
+
("Deployment", "Streamlit Cloud / HuggingFace Spaces"),
|
| 413 |
+
]
|
| 414 |
+
rect(slide, Inches(0.5), Inches(1.05), Inches(6.2), Inches(0.42),
|
| 415 |
+
RGBColor(0x0D, 0x28, 0x48), GOLD, Pt(1))
|
| 416 |
+
txt(slide, "Component", Inches(0.6), Inches(1.1), Inches(2.8), Inches(0.32),
|
| 417 |
+
sz=13, bold=True, clr=GOLD)
|
| 418 |
+
txt(slide, "Technology", Inches(3.5), Inches(1.1), Inches(3.0), Inches(0.32),
|
| 419 |
+
sz=13, bold=True, clr=GOLD)
|
| 420 |
+
for i, (comp, tech) in enumerate(stack):
|
| 421 |
+
row = CARD if i % 2 == 0 else CARD2
|
| 422 |
+
ry = Inches(1.47) + Inches(0.39 * i)
|
| 423 |
+
rect(slide, Inches(0.5), ry, Inches(6.2), Inches(0.39), row)
|
| 424 |
+
txt(slide, comp, Inches(0.6), ry + Inches(0.06), Inches(2.8), Inches(0.3),
|
| 425 |
+
sz=12, clr=MUT)
|
| 426 |
+
txt(slide, tech, Inches(3.5), ry + Inches(0.06), Inches(3.0), Inches(0.3),
|
| 427 |
+
sz=12, clr=WHT)
|
| 428 |
+
|
| 429 |
+
txt(slide, "What's Next", Inches(7.3), Inches(1.05), Inches(5.6), Inches(0.42),
|
| 430 |
+
sz=16, bold=True, clr=GOLD)
|
| 431 |
+
future = [
|
| 432 |
+
"Multi-tender workspace — same bidder pool, multiple tenders",
|
| 433 |
+
"GeM portal API integration — live tender ingestion",
|
| 434 |
+
"Automated bidder ranking with weighted scoring",
|
| 435 |
+
"LayoutLM for complex financial tables in scanned statements",
|
| 436 |
+
"Multi-evaluator workflow with role-based approval",
|
| 437 |
+
"Review queue email/SMS notifications",
|
| 438 |
+
"Audit PDF export for procurement oversight submissions",
|
| 439 |
+
]
|
| 440 |
+
for i, f in enumerate(future):
|
| 441 |
+
txt(slide, f"• {f}", Inches(7.3), Inches(1.55) + Inches(0.47 * i),
|
| 442 |
+
Inches(5.8), Inches(0.42), sz=13, clr=WHT)
|
| 443 |
+
|
| 444 |
+
rect(slide, Inches(0.5), Inches(6.15), Inches(12.33), Inches(1.0),
|
| 445 |
+
RGBColor(0x1A, 0x32, 0x50), GOLD, Pt(2))
|
| 446 |
+
txt(slide, ("3–5 days → minutes. Every verdict traceable to a document, page, and model version."
|
| 447 |
+
" Built in one hackathon session. Deployable today."),
|
| 448 |
+
Inches(0.7), Inches(6.25), Inches(12), Inches(0.8),
|
| 449 |
+
sz=14, bold=True, clr=GOLD, align=PP_ALIGN.CENTER)
|
| 450 |
+
|
| 451 |
+
|
| 452 |
+
def main():
|
| 453 |
+
os.makedirs("deck", exist_ok=True)
|
| 454 |
+
prs = new_prs()
|
| 455 |
+
s1(prs); s2(prs); s3(prs); s4(prs)
|
| 456 |
+
s5(prs); s6(prs); s7(prs); s8(prs)
|
| 457 |
+
out = "deck/TenderIQ_v1_dark_professional.pptx"
|
| 458 |
+
prs.save(out)
|
| 459 |
+
print(f"OK: Saved {out} ({len(prs.slides)} slides)")
|
| 460 |
+
|
| 461 |
+
|
| 462 |
+
if __name__ == "__main__":
|
| 463 |
+
main()
|
scripts/make_v2.py
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""TenderIQ v2 — Clean Minimal (PPTX)"""
|
| 3 |
+
import os
|
| 4 |
+
from pptx import Presentation
|
| 5 |
+
from pptx.util import Inches, Pt
|
| 6 |
+
from pptx.dml.color import RGBColor
|
| 7 |
+
from pptx.enum.text import PP_ALIGN
|
| 8 |
+
|
| 9 |
+
# Palette
|
| 10 |
+
BG = RGBColor(0xFF, 0xFF, 0xFF)
|
| 11 |
+
TXT1 = RGBColor(0x11, 0x18, 0x27)
|
| 12 |
+
TXT2 = RGBColor(0x37, 0x41, 0x51)
|
| 13 |
+
TXT3 = RGBColor(0x6B, 0x72, 0x80)
|
| 14 |
+
ACC = RGBColor(0x25, 0x63, 0xEB)
|
| 15 |
+
ACBG = RGBColor(0xEF, 0xF6, 0xFF)
|
| 16 |
+
DIV = RGBColor(0xE5, 0xE7, 0xEB)
|
| 17 |
+
GRN = RGBColor(0x05, 0x96, 0x69)
|
| 18 |
+
GRNBG = RGBColor(0xD1, 0xFA, 0xE5)
|
| 19 |
+
RED_C = RGBColor(0xDC, 0x26, 0x26)
|
| 20 |
+
REDBG = RGBColor(0xFE, 0xE2, 0xE2)
|
| 21 |
+
AMB = RGBColor(0xD9, 0x77, 0x06)
|
| 22 |
+
AMBBG = RGBColor(0xFE, 0xF3, 0xC7)
|
| 23 |
+
FONT = "Calibri"
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def new_prs():
|
| 27 |
+
prs = Presentation()
|
| 28 |
+
prs.slide_width = Inches(13.33)
|
| 29 |
+
prs.slide_height = Inches(7.5)
|
| 30 |
+
return prs
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def blank(prs):
|
| 34 |
+
return prs.slides.add_slide(prs.slide_layouts[6])
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def rect(slide, x, y, w, h, fill, line=None, lw=Pt(1)):
|
| 38 |
+
s = slide.shapes.add_shape(1, x, y, w, h)
|
| 39 |
+
s.fill.solid()
|
| 40 |
+
s.fill.fore_color.rgb = fill
|
| 41 |
+
if line:
|
| 42 |
+
s.line.color.rgb = line
|
| 43 |
+
s.line.width = lw
|
| 44 |
+
else:
|
| 45 |
+
s.line.fill.background()
|
| 46 |
+
return s
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def txt(slide, text, x, y, w, h, sz=15, bold=False, clr=None, align=PP_ALIGN.LEFT, italic=False):
|
| 50 |
+
tb = slide.shapes.add_textbox(x, y, w, h)
|
| 51 |
+
tf = tb.text_frame
|
| 52 |
+
tf.word_wrap = True
|
| 53 |
+
p = tf.paragraphs[0]
|
| 54 |
+
p.alignment = align
|
| 55 |
+
r = p.add_run()
|
| 56 |
+
r.text = text
|
| 57 |
+
r.font.size = Pt(sz)
|
| 58 |
+
r.font.bold = bold
|
| 59 |
+
r.font.italic = italic
|
| 60 |
+
r.font.name = FONT
|
| 61 |
+
if clr:
|
| 62 |
+
r.font.color.rgb = clr
|
| 63 |
+
return tb
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def bg(slide):
|
| 67 |
+
rect(slide, 0, 0, Inches(13.33), Inches(7.5), BG)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def heading(slide, text, y=Inches(0.35)):
|
| 71 |
+
rect(slide, Inches(0.75), y + Inches(0.04), Inches(0.05), Inches(0.5), ACC)
|
| 72 |
+
txt(slide, text, Inches(0.95), y, Inches(12.0), Inches(0.6),
|
| 73 |
+
sz=28, bold=True, clr=TXT1)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def card(slide, x, y, w, h):
|
| 77 |
+
rect(slide, x, y, w, h, RGBColor(0xFA, 0xFB, 0xFF), DIV, Pt(1))
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def chip(slide, x, y, w, h, bg_c, text_c, text):
|
| 81 |
+
rect(slide, x, y, w, h, bg_c)
|
| 82 |
+
txt(slide, text, x, y, w, h, sz=12, bold=True, clr=text_c, align=PP_ALIGN.CENTER)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
# ── Slide 1 ─────────────────────────────────────────────────────────────────
|
| 86 |
+
def s1(prs):
|
| 87 |
+
slide = blank(prs)
|
| 88 |
+
bg(slide)
|
| 89 |
+
rect(slide, Inches(0.75), Inches(2.2), Inches(0.05), Inches(3.0), ACC)
|
| 90 |
+
txt(slide, "⚖", Inches(1.0), Inches(2.2), Inches(1.5), Inches(1.2),
|
| 91 |
+
sz=56, clr=ACC)
|
| 92 |
+
txt(slide, "TenderIQ", Inches(2.7), Inches(2.2), Inches(10), Inches(1.2),
|
| 93 |
+
sz=64, bold=True, clr=TXT1)
|
| 94 |
+
txt(slide, "Explainable AI for Government Tender Evaluation",
|
| 95 |
+
Inches(2.7), Inches(3.5), Inches(10), Inches(0.7),
|
| 96 |
+
sz=22, clr=TXT3)
|
| 97 |
+
rect(slide, Inches(2.7), Inches(4.32), Inches(7), Inches(0.03), DIV)
|
| 98 |
+
txt(slide, "CRPF Hackathon · Theme 3",
|
| 99 |
+
Inches(2.7), Inches(4.5), Inches(6), Inches(0.45),
|
| 100 |
+
sz=16, clr=TXT3)
|
| 101 |
+
txt(slide, "From days to minutes. Every decision traceable.",
|
| 102 |
+
Inches(2.7), Inches(5.05), Inches(8), Inches(0.45),
|
| 103 |
+
sz=15, italic=True, clr=ACC)
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
# ── Slide 2 ─────────────────────────────────────────────────────────────────
|
| 107 |
+
def s2(prs):
|
| 108 |
+
slide = blank(prs)
|
| 109 |
+
bg(slide)
|
| 110 |
+
heading(slide, "The Problem with Manual Tender Evaluation")
|
| 111 |
+
callouts = [
|
| 112 |
+
("3–5", "Days per tender evaluation"),
|
| 113 |
+
("≠", "Inconsistent verdicts\nfrom different evaluators"),
|
| 114 |
+
("0", "Zero audit trail\nfor decisions made"),
|
| 115 |
+
]
|
| 116 |
+
cw = Inches(3.5)
|
| 117 |
+
for i, (big, sub) in enumerate(callouts):
|
| 118 |
+
sx = Inches(0.75) + i * Inches(4.15)
|
| 119 |
+
card(slide, sx, Inches(1.15), cw, Inches(2.4))
|
| 120 |
+
txt(slide, big, sx + Inches(0.2), Inches(1.3), cw - Inches(0.4), Inches(1.1),
|
| 121 |
+
sz=72, bold=True, clr=ACC, align=PP_ALIGN.CENTER)
|
| 122 |
+
txt(slide, sub, sx + Inches(0.2), Inches(2.45), cw - Inches(0.4), Inches(0.85),
|
| 123 |
+
sz=14, clr=TXT2, align=PP_ALIGN.CENTER)
|
| 124 |
+
rect(slide, Inches(0.75), Inches(3.8), Inches(11.83), Inches(0.03), DIV)
|
| 125 |
+
bullets = [
|
| 126 |
+
"• Mixed document formats: typed PDFs, scans, phone photographs",
|
| 127 |
+
"• Government procurement worth ₹50 lakh crore+ annually in India",
|
| 128 |
+
"• Project delays traced directly to procurement bottlenecks",
|
| 129 |
+
]
|
| 130 |
+
for i, b in enumerate(bullets):
|
| 131 |
+
txt(slide, b, Inches(0.95), Inches(4.0) + Inches(0.45 * i),
|
| 132 |
+
Inches(12), Inches(0.38), sz=14, clr=TXT3)
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
# ── Slide 3 ─────────────────────────────────────────────────────────────────
|
| 136 |
+
def s3(prs):
|
| 137 |
+
slide = blank(prs)
|
| 138 |
+
bg(slide)
|
| 139 |
+
heading(slide, "TenderIQ — Four Stages, End to End")
|
| 140 |
+
stages = [
|
| 141 |
+
("1", "Extract",
|
| 142 |
+
"DeepSeek LLM reads the tender PDF\nand returns each criterion as structured JSON:\ncategory, mandatory flag, threshold rule,\nsource clause, query hints."),
|
| 143 |
+
("2", "OCR & Index",
|
| 144 |
+
"Three-tier pipeline handles any document\nformat. All text chunked and indexed for\nsemantic retrieval."),
|
| 145 |
+
("3", "Evaluate",
|
| 146 |
+
"Vector search finds relevant evidence.\nLLM produces a verdict with confidence.\nSafety rule prevents silent disqualification."),
|
| 147 |
+
("4", "Review & Audit",
|
| 148 |
+
"Borderline cases go to human review\nqueue. Every action logged. Full audit\ntrail exportable as CSV."),
|
| 149 |
+
]
|
| 150 |
+
cw, ch = Inches(2.9), Inches(3.7)
|
| 151 |
+
for i, (num, title, body) in enumerate(stages):
|
| 152 |
+
sx = Inches(0.75) + i * Inches(3.15)
|
| 153 |
+
card(slide, sx, Inches(1.1), cw, ch)
|
| 154 |
+
rect(slide, sx, Inches(1.1), Inches(0.05), Inches(0.6), ACC)
|
| 155 |
+
txt(slide, num, sx + Inches(0.12), Inches(1.12), Inches(0.5), Inches(0.55),
|
| 156 |
+
sz=28, bold=True, clr=ACC)
|
| 157 |
+
txt(slide, title, sx + Inches(0.7), Inches(1.15), cw - Inches(0.85), Inches(0.5),
|
| 158 |
+
sz=17, bold=True, clr=TXT1)
|
| 159 |
+
txt(slide, body, sx + Inches(0.12), Inches(1.75), cw - Inches(0.25), Inches(2.8),
|
| 160 |
+
sz=13, clr=TXT2)
|
| 161 |
+
|
| 162 |
+
rect(slide, Inches(0.75), Inches(5.1), Inches(11.83), Inches(0.85),
|
| 163 |
+
ACBG, ACC, Pt(1))
|
| 164 |
+
txt(slide, '"Minutes, not days. Every verdict traceable to a document and page."',
|
| 165 |
+
Inches(0.95), Inches(5.22), Inches(11.5), Inches(0.65),
|
| 166 |
+
sz=15, bold=True, clr=ACC, align=PP_ALIGN.CENTER)
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
# ── Slide 4 ─────────────────────────────────────────────────────────────────
|
| 170 |
+
def s4(prs):
|
| 171 |
+
slide = blank(prs)
|
| 172 |
+
bg(slide)
|
| 173 |
+
heading(slide, "System Architecture")
|
| 174 |
+
|
| 175 |
+
bw, bh = Inches(2.5), Inches(0.52)
|
| 176 |
+
grey = RGBColor(0xF3, 0xF4, 0xF6)
|
| 177 |
+
|
| 178 |
+
def abox(x, y, label):
|
| 179 |
+
rect(slide, x, y, bw, bh, grey, DIV, Pt(1))
|
| 180 |
+
txt(slide, label, x + Inches(0.1), y + Inches(0.06),
|
| 181 |
+
bw - Inches(0.2), bh - Inches(0.1),
|
| 182 |
+
sz=12, clr=TXT1, align=PP_ALIGN.CENTER)
|
| 183 |
+
|
| 184 |
+
def arr(x, y):
|
| 185 |
+
rect(slide, x + Inches(1.15), y, Inches(0.2), Inches(0.28), ACC)
|
| 186 |
+
|
| 187 |
+
lx = Inches(0.75)
|
| 188 |
+
abox(lx, Inches(1.0), "Tender PDF")
|
| 189 |
+
arr(lx, Inches(1.52))
|
| 190 |
+
abox(lx, Inches(1.8), "DeepSeek LLM\n(Extract Criteria)")
|
| 191 |
+
arr(lx, Inches(2.32))
|
| 192 |
+
abox(lx, Inches(2.6), "Criteria JSON\n(C1–C5 structured)")
|
| 193 |
+
|
| 194 |
+
rx = Inches(4.2)
|
| 195 |
+
abox(rx, Inches(1.0), "Bidder Docs\n(PDFs · scans · photos)")
|
| 196 |
+
arr(rx, Inches(1.52))
|
| 197 |
+
abox(rx, Inches(1.8), "3-Tier OCR\n① PyMuPDF ② Tesseract ③ Vision LLM")
|
| 198 |
+
arr(rx, Inches(2.32))
|
| 199 |
+
abox(rx, Inches(2.6), "Vector Index\n(all-MiniLM-L6-v2 embeddings)")
|
| 200 |
+
|
| 201 |
+
cx = Inches(3.25)
|
| 202 |
+
arr(cx, Inches(3.12))
|
| 203 |
+
abox(Inches(2.25), Inches(3.4), "DeepSeek LLM — Evaluate each criterion")
|
| 204 |
+
|
| 205 |
+
rect(slide, Inches(1.05), Inches(4.15), Inches(2.0), Inches(0.52),
|
| 206 |
+
GRNBG, GRN, Pt(1))
|
| 207 |
+
txt(slide, "eligible /\nnot_eligible", Inches(1.05), Inches(4.15), Inches(2.0), Inches(0.52),
|
| 208 |
+
sz=11, clr=GRN, align=PP_ALIGN.CENTER)
|
| 209 |
+
|
| 210 |
+
rect(slide, Inches(3.6), Inches(4.15), Inches(2.0), Inches(0.52),
|
| 211 |
+
AMBBG, AMB, Pt(1))
|
| 212 |
+
txt(slide, "needs_review\nHuman Review Queue", Inches(3.6), Inches(4.15), Inches(2.0), Inches(0.52),
|
| 213 |
+
sz=11, clr=AMB, align=PP_ALIGN.CENTER)
|
| 214 |
+
|
| 215 |
+
abox(Inches(2.25), Inches(5.0), "SQLite Audit Log")
|
| 216 |
+
|
| 217 |
+
# Key facts
|
| 218 |
+
rect(slide, Inches(7.3), Inches(1.0), Inches(5.6), Inches(5.0), ACBG, ACC, Pt(1))
|
| 219 |
+
txt(slide, "Key Technical Facts", Inches(7.45), Inches(1.1), Inches(5.3), Inches(0.42),
|
| 220 |
+
sz=15, bold=True, clr=ACC)
|
| 221 |
+
facts = [
|
| 222 |
+
"Single-process Streamlit app — no separate backend",
|
| 223 |
+
"Deployable to Streamlit Cloud or HuggingFace Spaces",
|
| 224 |
+
"All storage is local: SQLite + in-memory vector index",
|
| 225 |
+
"Only external dependency: DeepSeek API",
|
| 226 |
+
]
|
| 227 |
+
for i, f in enumerate(facts):
|
| 228 |
+
txt(slide, f"• {f}", Inches(7.45), Inches(1.65) + Inches(0.65 * i),
|
| 229 |
+
Inches(5.3), Inches(0.55), sz=13, clr=TXT2)
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
# ── Slide 5 ─────────────────────────────────────────────────────────────────
|
| 233 |
+
def s5(prs):
|
| 234 |
+
slide = blank(prs)
|
| 235 |
+
bg(slide)
|
| 236 |
+
heading(slide, "Three-Tier OCR — Handling Any Document Format")
|
| 237 |
+
tiers = [
|
| 238 |
+
("Tier 1", "PyMuPDF",
|
| 239 |
+
"Trigger: Typed / digital PDF\nCost: Free, instant\nConfidence: 1.0 (lossless text)\nSource label: Typed PDF",
|
| 240 |
+
ACC, ACBG),
|
| 241 |
+
("Tier 2", "Tesseract",
|
| 242 |
+
"Trigger: Scanned PDF or image\nCost: Free, local, fast\nConfidence: Mean per-word OCR scores\nSource label: Tesseract",
|
| 243 |
+
RGBColor(0x7C, 0x3A, 0xED), RGBColor(0xED, 0xE9, 0xFE)),
|
| 244 |
+
("Tier 3", "DeepSeek Vision LLM",
|
| 245 |
+
"Trigger: Tesseract confidence < 65%\nCost: One API call\nConfidence: 0.95\nSource label: Vision LLM\nLogged: vision_ocr_invoked",
|
| 246 |
+
RGBColor(0xEA, 0x58, 0x0C), RGBColor(0xFF, 0xED, 0xD5)),
|
| 247 |
+
]
|
| 248 |
+
cw, ch = Inches(3.6), Inches(3.2)
|
| 249 |
+
for i, (tier, name, body, accent, abg) in enumerate(tiers):
|
| 250 |
+
sx = Inches(0.75) + i * Inches(4.15)
|
| 251 |
+
card(slide, sx, Inches(1.1), cw, ch)
|
| 252 |
+
rect(slide, sx, Inches(1.1), cw, Inches(0.08), accent)
|
| 253 |
+
txt(slide, tier, sx + Inches(0.15), Inches(1.23), Inches(1.2), Inches(0.38),
|
| 254 |
+
sz=12, bold=True, clr=accent)
|
| 255 |
+
txt(slide, name, sx + Inches(0.15), Inches(1.65), cw - Inches(0.3), Inches(0.42),
|
| 256 |
+
sz=16, bold=True, clr=TXT1)
|
| 257 |
+
txt(slide, body, sx + Inches(0.15), Inches(2.12), cw - Inches(0.3), Inches(2.0),
|
| 258 |
+
sz=13, clr=TXT2)
|
| 259 |
+
|
| 260 |
+
rect(slide, Inches(0.75), Inches(4.55), Inches(11.83), Inches(1.75),
|
| 261 |
+
ACBG, ACC, Pt(1))
|
| 262 |
+
txt(slide, "Demo Scenario",
|
| 263 |
+
Inches(0.95), Inches(4.65), Inches(8), Inches(0.38),
|
| 264 |
+
sz=14, bold=True, clr=ACC)
|
| 265 |
+
demo = ("Bidder C submits a blurry, rotated CA certificate scan. "
|
| 266 |
+
"Tesseract reads it at ~55% confidence. Vision LLM transcribes the turnover figure correctly. "
|
| 267 |
+
"Combined confidence = 0.58 → routed to human review. "
|
| 268 |
+
"This is intentional — borderline evidence requires a human.")
|
| 269 |
+
txt(slide, demo, Inches(0.95), Inches(5.1), Inches(11.6), Inches(1.1), sz=13, clr=TXT2)
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
# ── Slide 6 ─────────────────────────────────────────────────────────────────
|
| 273 |
+
def s6(prs):
|
| 274 |
+
slide = blank(prs)
|
| 275 |
+
bg(slide)
|
| 276 |
+
heading(slide, "Every Decision is Explainable and Auditable")
|
| 277 |
+
|
| 278 |
+
lx = Inches(0.75)
|
| 279 |
+
card(slide, lx, Inches(1.1), Inches(5.7), Inches(4.0))
|
| 280 |
+
rect(slide, lx, Inches(1.1), Inches(0.05), Inches(4.0), ACC)
|
| 281 |
+
txt(slide, "Criterion-Level Verdicts", lx + Inches(0.2), Inches(1.2),
|
| 282 |
+
Inches(5.4), Inches(0.42), sz=16, bold=True, clr=TXT1)
|
| 283 |
+
left_lines = [
|
| 284 |
+
"Each (bidder × criterion) pair shows:",
|
| 285 |
+
"• Which criterion was checked",
|
| 286 |
+
"• Which document and page provided evidence",
|
| 287 |
+
"• What value was extracted (e.g. 'INR 6.2 Cr')",
|
| 288 |
+
"• Which OCR tier read the document",
|
| 289 |
+
"• Combined confidence score (0–100%)",
|
| 290 |
+
"• Plain-English reason",
|
| 291 |
+
]
|
| 292 |
+
for i, line in enumerate(left_lines):
|
| 293 |
+
c = TXT3 if i == 0 else TXT2
|
| 294 |
+
txt(slide, line, lx + Inches(0.2), Inches(1.68) + Inches(0.42 * i),
|
| 295 |
+
Inches(5.3), Inches(0.38), sz=13, clr=c)
|
| 296 |
+
|
| 297 |
+
rx = Inches(6.88)
|
| 298 |
+
card(slide, rx, Inches(1.1), Inches(5.7), Inches(4.0))
|
| 299 |
+
rect(slide, rx, Inches(1.1), Inches(0.05), Inches(4.0), ACC)
|
| 300 |
+
txt(slide, "Audit Trail", rx + Inches(0.2), Inches(1.2),
|
| 301 |
+
Inches(5.4), Inches(0.42), sz=16, bold=True, clr=TXT1)
|
| 302 |
+
right_lines = [
|
| 303 |
+
"Every action logged with:",
|
| 304 |
+
"• UTC timestamp",
|
| 305 |
+
"• Action type: criteria_extracted / bidder_processed",
|
| 306 |
+
" criterion_evaluated / human_review_action",
|
| 307 |
+
" vision_ocr_invoked / precomputed_fallback_used",
|
| 308 |
+
"• Model version & Actor (system / officer)",
|
| 309 |
+
"• Full payload JSON — Exportable as CSV",
|
| 310 |
+
]
|
| 311 |
+
for i, line in enumerate(right_lines):
|
| 312 |
+
c = TXT3 if i == 0 else TXT2
|
| 313 |
+
txt(slide, line, rx + Inches(0.2), Inches(1.68) + Inches(0.42 * i),
|
| 314 |
+
Inches(5.3), Inches(0.38), sz=13, clr=c)
|
| 315 |
+
|
| 316 |
+
rect(slide, Inches(0.75), Inches(5.35), Inches(11.83), Inches(1.25),
|
| 317 |
+
AMBBG, AMB, Pt(2))
|
| 318 |
+
txt(slide, "The Safety Rule",
|
| 319 |
+
Inches(0.95), Inches(5.42), Inches(5), Inches(0.38),
|
| 320 |
+
sz=15, bold=True, clr=AMB)
|
| 321 |
+
txt(slide, ("If combined confidence is 0.55–0.80 AND verdict is not_eligible, "
|
| 322 |
+
"the verdict is automatically downgraded to needs_review. "
|
| 323 |
+
"A bidder is NEVER silently disqualified at medium confidence."),
|
| 324 |
+
Inches(0.95), Inches(5.85), Inches(11.5), Inches(0.65),
|
| 325 |
+
sz=13, clr=TXT2)
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
# ── Slide 7 ─────────────────────────────────────────────────────────────────
|
| 329 |
+
def s7(prs):
|
| 330 |
+
slide = blank(prs)
|
| 331 |
+
bg(slide)
|
| 332 |
+
heading(slide, "Demo: Three Bidders, Three Outcomes")
|
| 333 |
+
bidders = [
|
| 334 |
+
("Bidder A — Apex Constructions Pvt. Ltd.", GRN, GRNBG, "ELIGIBLE",
|
| 335 |
+
["C1 Turnover: INR 6.37 Cr avg (threshold 5 Cr) — PASS",
|
| 336 |
+
"C2 Projects: 5 completed incl. CRPF barracks — PASS",
|
| 337 |
+
"C3 GST: GSTIN 27AABCA1234F1Z5, Active — PASS",
|
| 338 |
+
"C4 ISO 9001:2015: Valid June 2027 — PASS",
|
| 339 |
+
"All typed PDFs, confidence ≥ 93% on all criteria"]),
|
| 340 |
+
("Bidder B — BuildRight Enterprises", RED_C, REDBG, "NOT ELIGIBLE",
|
| 341 |
+
["C1 Turnover: INR 1.5 Cr avg (threshold 5 Cr) — FAIL",
|
| 342 |
+
"\"INR 1.5 Cr is below required minimum of INR 5 Cr\"",
|
| 343 |
+
"C2–C4: All pass",
|
| 344 |
+
"Auto-disqualified at high confidence (95%)"]),
|
| 345 |
+
("Bidder C — Shree Constructions & Services", AMB, AMBBG, "NEEDS REVIEW",
|
| 346 |
+
["C1 Turnover: Submitted as blurry scan",
|
| 347 |
+
"Tesseract ~55% → Vision LLM: INR 5.4 Cr",
|
| 348 |
+
"Combined confidence 0.58 → safety rule triggered",
|
| 349 |
+
"C2: Exactly 3 projects (borderline)",
|
| 350 |
+
"C3–C4: Pass"]),
|
| 351 |
+
]
|
| 352 |
+
cw, ch = Inches(3.85), Inches(3.6)
|
| 353 |
+
for i, (name, color, cbg, verdict, bullets) in enumerate(bidders):
|
| 354 |
+
sx = Inches(0.75) + i * Inches(4.2)
|
| 355 |
+
card(slide, sx, Inches(1.1), cw, ch)
|
| 356 |
+
rect(slide, sx, Inches(1.1), cw, Inches(0.07), color)
|
| 357 |
+
txt(slide, name, sx + Inches(0.15), Inches(1.2), cw - Inches(0.3), Inches(0.5),
|
| 358 |
+
sz=12, bold=True, clr=TXT1)
|
| 359 |
+
chip(slide, sx + Inches(0.15), Inches(1.78), Inches(3.3), Inches(0.35),
|
| 360 |
+
cbg, color, verdict)
|
| 361 |
+
for j, b in enumerate(bullets):
|
| 362 |
+
txt(slide, b, sx + Inches(0.15), Inches(2.24) + Inches(0.37 * j),
|
| 363 |
+
cw - Inches(0.3), Inches(0.33), sz=11, clr=TXT2)
|
| 364 |
+
|
| 365 |
+
# Metric strip
|
| 366 |
+
metrics = [
|
| 367 |
+
("Criteria extracted", "5"),
|
| 368 |
+
("Bidder docs processed", "15"),
|
| 369 |
+
("LLM evaluation calls", "15"),
|
| 370 |
+
("Vision OCR invocations", "1"),
|
| 371 |
+
("Human review items", "1"),
|
| 372 |
+
("Total audit entries", "20+"),
|
| 373 |
+
]
|
| 374 |
+
rect(slide, Inches(0.75), Inches(5.0), Inches(11.83), Inches(1.0),
|
| 375 |
+
ACBG, ACC, Pt(1))
|
| 376 |
+
mw = Inches(1.95)
|
| 377 |
+
for i, (label, val) in enumerate(metrics):
|
| 378 |
+
mx = Inches(0.85) + i * Inches(1.97)
|
| 379 |
+
txt(slide, val, mx, Inches(5.08), mw, Inches(0.4),
|
| 380 |
+
sz=22, bold=True, clr=ACC, align=PP_ALIGN.CENTER)
|
| 381 |
+
txt(slide, label, mx, Inches(5.53), mw, Inches(0.35),
|
| 382 |
+
sz=11, clr=TXT3, align=PP_ALIGN.CENTER)
|
| 383 |
+
|
| 384 |
+
|
| 385 |
+
# ── Slide 8 ─────────────────────────────────────────────────────────────────
|
| 386 |
+
def s8(prs):
|
| 387 |
+
slide = blank(prs)
|
| 388 |
+
bg(slide)
|
| 389 |
+
heading(slide, "Stack, Impact & What's Next")
|
| 390 |
+
stack = [
|
| 391 |
+
("UI & orchestration", "Streamlit 1.39"),
|
| 392 |
+
("LLM", "DeepSeek API (OpenAI-compatible)"),
|
| 393 |
+
("OCR Tier 1", "PyMuPDF 1.24"),
|
| 394 |
+
("OCR Tier 2", "Tesseract"),
|
| 395 |
+
("OCR Tier 3", "DeepSeek Vision LLM"),
|
| 396 |
+
("Semantic retrieval", "sentence-transformers all-MiniLM-L6-v2"),
|
| 397 |
+
("Data validation", "Pydantic v2"),
|
| 398 |
+
("Audit log", "SQLite"),
|
| 399 |
+
("Deployment", "Streamlit Cloud / HuggingFace Spaces"),
|
| 400 |
+
]
|
| 401 |
+
rect(slide, Inches(0.75), Inches(1.05), Inches(6.0), Inches(0.4),
|
| 402 |
+
ACBG, ACC, Pt(1))
|
| 403 |
+
txt(slide, "Component", Inches(0.85), Inches(1.1), Inches(2.7), Inches(0.3),
|
| 404 |
+
sz=13, bold=True, clr=ACC)
|
| 405 |
+
txt(slide, "Technology", Inches(3.65), Inches(1.1), Inches(2.9), Inches(0.3),
|
| 406 |
+
sz=13, bold=True, clr=ACC)
|
| 407 |
+
for i, (comp, tech) in enumerate(stack):
|
| 408 |
+
ry = Inches(1.45) + Inches(0.38 * i)
|
| 409 |
+
bg_r = RGBColor(0xFA, 0xFB, 0xFF) if i % 2 == 0 else BG
|
| 410 |
+
rect(slide, Inches(0.75), ry, Inches(6.0), Inches(0.38), bg_r)
|
| 411 |
+
txt(slide, comp, Inches(0.85), ry + Inches(0.06), Inches(2.7), Inches(0.3),
|
| 412 |
+
sz=12, clr=TXT3)
|
| 413 |
+
txt(slide, tech, Inches(3.65), ry + Inches(0.06), Inches(2.9), Inches(0.3),
|
| 414 |
+
sz=12, clr=TXT2)
|
| 415 |
+
|
| 416 |
+
txt(slide, "What's Next", Inches(7.2), Inches(1.05), Inches(5.7), Inches(0.42),
|
| 417 |
+
sz=16, bold=True, clr=TXT1)
|
| 418 |
+
future = [
|
| 419 |
+
"Multi-tender workspace — same bidder pool, multiple tenders",
|
| 420 |
+
"GeM portal API integration — live tender ingestion",
|
| 421 |
+
"Automated bidder ranking with weighted scoring",
|
| 422 |
+
"LayoutLM for complex financial tables in scanned statements",
|
| 423 |
+
"Multi-evaluator workflow with role-based approval",
|
| 424 |
+
"Review queue email/SMS notifications",
|
| 425 |
+
"Audit PDF export for procurement oversight submissions",
|
| 426 |
+
]
|
| 427 |
+
for i, f in enumerate(future):
|
| 428 |
+
txt(slide, f"• {f}", Inches(7.2), Inches(1.55) + Inches(0.47 * i),
|
| 429 |
+
Inches(5.8), Inches(0.42), sz=13, clr=TXT2)
|
| 430 |
+
|
| 431 |
+
rect(slide, Inches(0.75), Inches(6.18), Inches(11.83), Inches(0.95),
|
| 432 |
+
ACBG, ACC, Pt(2))
|
| 433 |
+
txt(slide, ("3–5 days → minutes. Every verdict traceable to a document, page, and model version."
|
| 434 |
+
" Built in one hackathon session. Deployable today."),
|
| 435 |
+
Inches(0.95), Inches(6.3), Inches(11.5), Inches(0.75),
|
| 436 |
+
sz=14, bold=True, clr=ACC, align=PP_ALIGN.CENTER)
|
| 437 |
+
|
| 438 |
+
|
| 439 |
+
def main():
|
| 440 |
+
os.makedirs("deck", exist_ok=True)
|
| 441 |
+
prs = new_prs()
|
| 442 |
+
s1(prs); s2(prs); s3(prs); s4(prs)
|
| 443 |
+
s5(prs); s6(prs); s7(prs); s8(prs)
|
| 444 |
+
out = "deck/TenderIQ_v2_clean_minimal.pptx"
|
| 445 |
+
prs.save(out)
|
| 446 |
+
print(f"OK: Saved {out} ({len(prs.slides)} slides)")
|
| 447 |
+
|
| 448 |
+
|
| 449 |
+
if __name__ == "__main__":
|
| 450 |
+
main()
|
scripts/make_v3.py
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""TenderIQ v3 — Government Official (PPTX)"""
|
| 3 |
+
import os
|
| 4 |
+
from pptx import Presentation
|
| 5 |
+
from pptx.util import Inches, Pt
|
| 6 |
+
from pptx.dml.color import RGBColor
|
| 7 |
+
from pptx.enum.text import PP_ALIGN
|
| 8 |
+
|
| 9 |
+
# Palette
|
| 10 |
+
BLUE = RGBColor(0x00, 0x35, 0x80) # deep government blue
|
| 11 |
+
SAF = RGBColor(0xFF, 0x99, 0x33) # saffron
|
| 12 |
+
IGRN = RGBColor(0x13, 0x88, 0x08) # India green
|
| 13 |
+
BGOFF = RGBColor(0xF5, 0xF5, 0xF0) # off-white
|
| 14 |
+
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
|
| 15 |
+
TXT = RGBColor(0x1A, 0x1A, 0x1A)
|
| 16 |
+
LTBLUE = RGBColor(0xEB, 0xF0, 0xFF) # light blue fill
|
| 17 |
+
FONT_H = "Times New Roman"
|
| 18 |
+
FONT_B = "Arial"
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def new_prs():
|
| 22 |
+
prs = Presentation()
|
| 23 |
+
prs.slide_width = Inches(13.33)
|
| 24 |
+
prs.slide_height = Inches(7.5)
|
| 25 |
+
return prs
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def blank(prs):
|
| 29 |
+
return prs.slides.add_slide(prs.slide_layouts[6])
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def rect(slide, x, y, w, h, fill, line=None, lw=Pt(1)):
|
| 33 |
+
s = slide.shapes.add_shape(1, x, y, w, h)
|
| 34 |
+
s.fill.solid()
|
| 35 |
+
s.fill.fore_color.rgb = fill
|
| 36 |
+
if line:
|
| 37 |
+
s.line.color.rgb = line
|
| 38 |
+
s.line.width = lw
|
| 39 |
+
else:
|
| 40 |
+
s.line.fill.background()
|
| 41 |
+
return s
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def txt(slide, text, x, y, w, h, sz=14, bold=False, clr=None, align=PP_ALIGN.LEFT,
|
| 45 |
+
font=None, italic=False):
|
| 46 |
+
tb = slide.shapes.add_textbox(x, y, w, h)
|
| 47 |
+
tf = tb.text_frame
|
| 48 |
+
tf.word_wrap = True
|
| 49 |
+
p = tf.paragraphs[0]
|
| 50 |
+
p.alignment = align
|
| 51 |
+
r = p.add_run()
|
| 52 |
+
r.text = text
|
| 53 |
+
r.font.size = Pt(sz)
|
| 54 |
+
r.font.bold = bold
|
| 55 |
+
r.font.italic = italic
|
| 56 |
+
r.font.name = font or FONT_B
|
| 57 |
+
if clr:
|
| 58 |
+
r.font.color.rgb = clr
|
| 59 |
+
return tb
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def header_bar(slide, title):
|
| 63 |
+
"""Blue header band with title, saffron underline, footer."""
|
| 64 |
+
rect(slide, 0, 0, Inches(13.33), Inches(0.9), BLUE)
|
| 65 |
+
rect(slide, 0, Inches(0.9), Inches(13.33), Inches(0.06), SAF)
|
| 66 |
+
txt(slide, title, Inches(0.5), Inches(0.1), Inches(12.33), Inches(0.7),
|
| 67 |
+
sz=22, bold=True, clr=WHITE, font=FONT_H)
|
| 68 |
+
footer(slide)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def footer(slide):
|
| 72 |
+
rect(slide, 0, Inches(7.32), Inches(13.33), Inches(0.18), BLUE)
|
| 73 |
+
txt(slide, "TenderIQ · CRPF Hackathon · Theme 3",
|
| 74 |
+
Inches(0.3), Inches(7.33), Inches(12.5), Inches(0.16),
|
| 75 |
+
sz=9, clr=WHITE, align=PP_ALIGN.CENTER)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def bg(slide):
|
| 79 |
+
rect(slide, 0, 0, Inches(13.33), Inches(7.5), BGOFF)
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def callout(slide, x, y, w, h, text):
|
| 83 |
+
rect(slide, x, y, w, h, LTBLUE, BLUE, Pt(1))
|
| 84 |
+
txt(slide, text, x + Inches(0.15), y + Inches(0.1), w - Inches(0.3), h - Inches(0.2),
|
| 85 |
+
sz=13, clr=TXT)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
# ── Slide 1 ─────────────────────────────────────────────────────────────────
|
| 89 |
+
def s1(prs):
|
| 90 |
+
slide = blank(prs)
|
| 91 |
+
bg(slide)
|
| 92 |
+
rect(slide, 0, 0, Inches(13.33), Inches(1.2), BLUE)
|
| 93 |
+
rect(slide, 0, Inches(1.2), Inches(13.33), Inches(0.08), SAF)
|
| 94 |
+
txt(slide, "Government of India · Ministry of Home Affairs",
|
| 95 |
+
Inches(0.5), Inches(0.1), Inches(12.33), Inches(0.4),
|
| 96 |
+
sz=13, clr=WHITE, align=PP_ALIGN.CENTER, font=FONT_H)
|
| 97 |
+
txt(slide, "Central Reserve Police Force",
|
| 98 |
+
Inches(0.5), Inches(0.55), Inches(12.33), Inches(0.5),
|
| 99 |
+
sz=18, bold=True, clr=WHITE, align=PP_ALIGN.CENTER, font=FONT_H)
|
| 100 |
+
rect(slide, Inches(0), Inches(1.28), Inches(13.33), Inches(0.04), SAF)
|
| 101 |
+
txt(slide, "TenderIQ",
|
| 102 |
+
Inches(0.5), Inches(1.8), Inches(12.33), Inches(1.4),
|
| 103 |
+
sz=64, bold=True, clr=BLUE, align=PP_ALIGN.CENTER, font=FONT_H)
|
| 104 |
+
txt(slide, "Explainable AI for Government Tender Evaluation",
|
| 105 |
+
Inches(0.5), Inches(3.3), Inches(12.33), Inches(0.65),
|
| 106 |
+
sz=22, bold=True, clr=TXT, align=PP_ALIGN.CENTER, font=FONT_H)
|
| 107 |
+
rect(slide, Inches(3), Inches(4.1), Inches(7.33), Inches(0.04), SAF)
|
| 108 |
+
txt(slide, "CRPF Hackathon — Theme 3: AI-Based Tender Evaluation",
|
| 109 |
+
Inches(0.5), Inches(4.3), Inches(12.33), Inches(0.5),
|
| 110 |
+
sz=16, clr=BLUE, align=PP_ALIGN.CENTER, font=FONT_H)
|
| 111 |
+
txt(slide, "From days to minutes. Every decision traceable.",
|
| 112 |
+
Inches(0.5), Inches(5.0), Inches(12.33), Inches(0.45),
|
| 113 |
+
sz=14, italic=True, clr=TXT, align=PP_ALIGN.CENTER)
|
| 114 |
+
footer(slide)
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
# ── Slide 2 ─────────────────────────────────────────────────────────────────
|
| 118 |
+
def s2(prs):
|
| 119 |
+
slide = blank(prs)
|
| 120 |
+
bg(slide)
|
| 121 |
+
header_bar(slide, "The Problem with Manual Tender Evaluation")
|
| 122 |
+
callouts_data = [
|
| 123 |
+
("3–5 Days", "per tender evaluation by committee"),
|
| 124 |
+
("Inconsistent", "two evaluators, two different conclusions"),
|
| 125 |
+
("No Audit Trail", "decisions made untraceably"),
|
| 126 |
+
]
|
| 127 |
+
cw, ch = Inches(3.7), Inches(2.0)
|
| 128 |
+
for i, (big, sub) in enumerate(callouts_data):
|
| 129 |
+
sx = Inches(0.6) + i * Inches(4.1)
|
| 130 |
+
rect(slide, sx, Inches(1.2), cw, ch, WHITE, BLUE, Pt(2))
|
| 131 |
+
rect(slide, sx, Inches(1.2), cw, Inches(0.08), SAF)
|
| 132 |
+
txt(slide, big, sx + Inches(0.15), Inches(1.4), cw - Inches(0.3), Inches(0.8),
|
| 133 |
+
sz=28, bold=True, clr=BLUE, align=PP_ALIGN.CENTER, font=FONT_H)
|
| 134 |
+
txt(slide, sub, sx + Inches(0.15), Inches(2.25), cw - Inches(0.3), Inches(0.7),
|
| 135 |
+
sz=13, clr=TXT, align=PP_ALIGN.CENTER)
|
| 136 |
+
rect(slide, Inches(0.6), Inches(3.45), Inches(12.13), Inches(0.04), SAF)
|
| 137 |
+
bullets = [
|
| 138 |
+
"• Mixed document formats: typed PDFs, scanned certificates, phone photographs",
|
| 139 |
+
"• Government procurement worth ₹50 lakh crore+ annually in India",
|
| 140 |
+
"• Project execution delays traced directly to procurement bottlenecks",
|
| 141 |
+
]
|
| 142 |
+
for i, b in enumerate(bullets):
|
| 143 |
+
txt(slide, b, Inches(0.75), Inches(3.65) + Inches(0.45 * i),
|
| 144 |
+
Inches(12), Inches(0.38), sz=14, clr=TXT)
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
# ── Slide 3 ─────────────────────────────────────────────────────────────────
|
| 148 |
+
def s3(prs):
|
| 149 |
+
slide = blank(prs)
|
| 150 |
+
bg(slide)
|
| 151 |
+
header_bar(slide, "TenderIQ — Four Stages, End to End")
|
| 152 |
+
stages = [
|
| 153 |
+
("Stage 1", "Extract",
|
| 154 |
+
"DeepSeek LLM reads the tender PDF\nand returns each criterion as structured\nJSON: category, mandatory flag, threshold\nrule, source clause, query hints."),
|
| 155 |
+
("Stage 2", "OCR & Index",
|
| 156 |
+
"Three-tier pipeline handles any document\nformat. All text chunked and indexed for\nsemantic retrieval."),
|
| 157 |
+
("Stage 3", "Evaluate",
|
| 158 |
+
"Vector search finds relevant evidence.\nLLM produces a verdict with confidence.\nSafety rule prevents silent disqualification."),
|
| 159 |
+
("Stage 4", "Review & Audit",
|
| 160 |
+
"Borderline cases go to human review\nqueue. Every action logged. Full audit\ntrail exportable as CSV."),
|
| 161 |
+
]
|
| 162 |
+
cw, ch = Inches(2.9), Inches(3.8)
|
| 163 |
+
for i, (num, title, body) in enumerate(stages):
|
| 164 |
+
sx = Inches(0.6) + i * Inches(3.15)
|
| 165 |
+
rect(slide, sx, Inches(1.1), cw, ch, WHITE, BLUE, Pt(1))
|
| 166 |
+
rect(slide, sx, Inches(1.1), cw, Inches(0.07), BLUE)
|
| 167 |
+
txt(slide, num, sx + Inches(0.15), Inches(1.2), cw - Inches(0.3), Inches(0.38),
|
| 168 |
+
sz=11, bold=True, clr=SAF, font=FONT_H)
|
| 169 |
+
txt(slide, title, sx + Inches(0.15), Inches(1.6), cw - Inches(0.3), Inches(0.45),
|
| 170 |
+
sz=16, bold=True, clr=BLUE, font=FONT_H)
|
| 171 |
+
txt(slide, body, sx + Inches(0.15), Inches(2.1), cw - Inches(0.3), Inches(2.5),
|
| 172 |
+
sz=12, clr=TXT)
|
| 173 |
+
rect(slide, Inches(0.6), Inches(5.1), Inches(12.13), Inches(0.85),
|
| 174 |
+
LTBLUE, BLUE, Pt(1))
|
| 175 |
+
txt(slide, '"Minutes, not days. Every verdict traceable to a document and page."',
|
| 176 |
+
Inches(0.8), Inches(5.22), Inches(11.9), Inches(0.65),
|
| 177 |
+
sz=14, bold=True, clr=BLUE, align=PP_ALIGN.CENTER, font=FONT_H)
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
# ── Slide 4 ─────────────────────────────────────────────────────────────────
|
| 181 |
+
def s4(prs):
|
| 182 |
+
slide = blank(prs)
|
| 183 |
+
bg(slide)
|
| 184 |
+
header_bar(slide, "System Architecture")
|
| 185 |
+
|
| 186 |
+
bw, bh = Inches(2.5), Inches(0.52)
|
| 187 |
+
|
| 188 |
+
def abox(x, y, label):
|
| 189 |
+
rect(slide, x, y, bw, bh, WHITE, BLUE, Pt(1))
|
| 190 |
+
txt(slide, label, x + Inches(0.1), y + Inches(0.06),
|
| 191 |
+
bw - Inches(0.2), bh - Inches(0.1),
|
| 192 |
+
sz=11, clr=TXT, align=PP_ALIGN.CENTER)
|
| 193 |
+
|
| 194 |
+
def arr(x, y):
|
| 195 |
+
rect(slide, x + Inches(1.15), y, Inches(0.2), Inches(0.28), BLUE)
|
| 196 |
+
|
| 197 |
+
lx = Inches(0.6)
|
| 198 |
+
abox(lx, Inches(1.05), "Tender PDF")
|
| 199 |
+
arr(lx, Inches(1.57))
|
| 200 |
+
abox(lx, Inches(1.85), "DeepSeek LLM\n(Extract Criteria)")
|
| 201 |
+
arr(lx, Inches(2.37))
|
| 202 |
+
abox(lx, Inches(2.65), "Criteria JSON\n(C1–C5 structured)")
|
| 203 |
+
|
| 204 |
+
rx = Inches(4.0)
|
| 205 |
+
abox(rx, Inches(1.05), "Bidder Docs\n(PDFs · scans · photos)")
|
| 206 |
+
arr(rx, Inches(1.57))
|
| 207 |
+
abox(rx, Inches(1.85), "3-Tier OCR Pipeline\n① PyMuPDF ② Tesseract ③ Vision LLM")
|
| 208 |
+
arr(rx, Inches(2.37))
|
| 209 |
+
abox(rx, Inches(2.65), "Vector Index\n(all-MiniLM-L6-v2 embeddings)")
|
| 210 |
+
|
| 211 |
+
cx = Inches(3.15)
|
| 212 |
+
arr(cx, Inches(3.17))
|
| 213 |
+
abox(Inches(2.1), Inches(3.45), "DeepSeek LLM — Evaluate each criterion")
|
| 214 |
+
|
| 215 |
+
rect(slide, Inches(0.9), Inches(4.2), Inches(2.0), Inches(0.52),
|
| 216 |
+
RGBColor(0xD1, 0xFA, 0xE5), IGRN, Pt(1))
|
| 217 |
+
txt(slide, "ELIGIBLE /\nNOT ELIGIBLE", Inches(0.9), Inches(4.2), Inches(2.0), Inches(0.52),
|
| 218 |
+
sz=11, clr=IGRN, align=PP_ALIGN.CENTER, bold=True)
|
| 219 |
+
|
| 220 |
+
rect(slide, Inches(3.5), Inches(4.2), Inches(2.0), Inches(0.52),
|
| 221 |
+
RGBColor(0xFE, 0xF3, 0xC7), SAF, Pt(1))
|
| 222 |
+
txt(slide, "UNDER REVIEW\nHuman Review Queue", Inches(3.5), Inches(4.2), Inches(2.0), Inches(0.52),
|
| 223 |
+
sz=11, clr=RGBColor(0xB4, 0x5A, 0x09), align=PP_ALIGN.CENTER, bold=True)
|
| 224 |
+
|
| 225 |
+
abox(Inches(2.1), Inches(5.0), "SQLite Audit Log")
|
| 226 |
+
|
| 227 |
+
# Key facts
|
| 228 |
+
rect(slide, Inches(7.2), Inches(1.05), Inches(5.7), Inches(5.2), WHITE, BLUE, Pt(1))
|
| 229 |
+
rect(slide, Inches(7.2), Inches(1.05), Inches(5.7), Inches(0.06), BLUE)
|
| 230 |
+
txt(slide, "Key Technical Facts", Inches(7.35), Inches(1.15), Inches(5.4), Inches(0.42),
|
| 231 |
+
sz=14, bold=True, clr=BLUE, font=FONT_H)
|
| 232 |
+
facts = [
|
| 233 |
+
"Single-process Streamlit app — no separate backend",
|
| 234 |
+
"Deployable to Streamlit Cloud or HuggingFace Spaces",
|
| 235 |
+
"All storage is local: SQLite + in-memory vector index",
|
| 236 |
+
"Only external dependency: DeepSeek API",
|
| 237 |
+
]
|
| 238 |
+
for i, f in enumerate(facts):
|
| 239 |
+
txt(slide, f"• {f}", Inches(7.35), Inches(1.72) + Inches(0.65 * i),
|
| 240 |
+
Inches(5.4), Inches(0.55), sz=13, clr=TXT)
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
# ── Slide 5 ─────────────────────────────────────────────────────────────────
|
| 244 |
+
def s5(prs):
|
| 245 |
+
slide = blank(prs)
|
| 246 |
+
bg(slide)
|
| 247 |
+
header_bar(slide, "Three-Tier OCR — Handling Any Document Format")
|
| 248 |
+
tiers = [
|
| 249 |
+
("Tier 1 — PyMuPDF",
|
| 250 |
+
"Trigger: Typed / digital PDF\nCost: Free, instant\nConfidence: 1.0 (lossless)\nSource label: Typed PDF",
|
| 251 |
+
BLUE),
|
| 252 |
+
("Tier 2 — Tesseract",
|
| 253 |
+
"Trigger: Scanned PDF or image\nCost: Free, local, fast\nConfidence: Mean per-word OCR\nSource label: Tesseract",
|
| 254 |
+
RGBColor(0x7C, 0x3A, 0xED)),
|
| 255 |
+
("Tier 3 — DeepSeek Vision LLM",
|
| 256 |
+
"Trigger: Tesseract confidence < 65%\nCost: One API call\nConfidence: 0.95\nSource label: Vision LLM\nLogged: vision_ocr_invoked",
|
| 257 |
+
SAF),
|
| 258 |
+
]
|
| 259 |
+
cw, ch = Inches(3.6), Inches(3.1)
|
| 260 |
+
for i, (title, body, accent) in enumerate(tiers):
|
| 261 |
+
sx = Inches(0.6) + i * Inches(4.25)
|
| 262 |
+
rect(slide, sx, Inches(1.1), cw, ch, WHITE, BLUE, Pt(1))
|
| 263 |
+
rect(slide, sx, Inches(1.1), cw, Inches(0.08), accent)
|
| 264 |
+
txt(slide, title, sx + Inches(0.15), Inches(1.25), cw - Inches(0.3), Inches(0.48),
|
| 265 |
+
sz=14, bold=True, clr=accent, font=FONT_H)
|
| 266 |
+
txt(slide, body, sx + Inches(0.15), Inches(1.78), cw - Inches(0.3), Inches(2.2),
|
| 267 |
+
sz=13, clr=TXT)
|
| 268 |
+
|
| 269 |
+
callout(slide, Inches(0.6), Inches(4.5), Inches(12.13), Inches(1.85),
|
| 270 |
+
"Demo Scenario — Bidder C submits a blurry, rotated CA certificate scan.\n"
|
| 271 |
+
"Tesseract reads it at ~55% confidence. Vision LLM transcribes the turnover figure correctly.\n"
|
| 272 |
+
"Combined confidence = 0.58 → routed to human review.\n"
|
| 273 |
+
"This is intentional — borderline evidence requires a human.")
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
# ── Slide 6 ─────────────────────────────────────────────────────────────────
|
| 277 |
+
def s6(prs):
|
| 278 |
+
slide = blank(prs)
|
| 279 |
+
bg(slide)
|
| 280 |
+
header_bar(slide, "Every Decision is Explainable and Auditable")
|
| 281 |
+
|
| 282 |
+
lx = Inches(0.6)
|
| 283 |
+
rect(slide, lx, Inches(1.1), Inches(5.85), Inches(4.0), WHITE, BLUE, Pt(1))
|
| 284 |
+
rect(slide, lx, Inches(1.1), Inches(5.85), Inches(0.07), BLUE)
|
| 285 |
+
txt(slide, "Criterion-Level Verdicts", lx + Inches(0.15), Inches(1.2),
|
| 286 |
+
Inches(5.5), Inches(0.42), sz=14, bold=True, clr=BLUE, font=FONT_H)
|
| 287 |
+
for i, line in enumerate([
|
| 288 |
+
"Each (bidder × criterion) pair shows:",
|
| 289 |
+
"• Which criterion was checked",
|
| 290 |
+
"• Which document and page provided evidence",
|
| 291 |
+
"• What value was extracted (e.g. 'INR 6.2 Cr')",
|
| 292 |
+
"• Which OCR tier read the document",
|
| 293 |
+
"• Combined confidence score (0–100%)",
|
| 294 |
+
"• Plain-English reason",
|
| 295 |
+
]):
|
| 296 |
+
txt(slide, line, lx + Inches(0.15), Inches(1.65) + Inches(0.42 * i),
|
| 297 |
+
Inches(5.55), Inches(0.38), sz=13, clr=TXT)
|
| 298 |
+
|
| 299 |
+
rx = Inches(6.98)
|
| 300 |
+
rect(slide, rx, Inches(1.1), Inches(5.85), Inches(4.0), WHITE, BLUE, Pt(1))
|
| 301 |
+
rect(slide, rx, Inches(1.1), Inches(5.85), Inches(0.07), BLUE)
|
| 302 |
+
txt(slide, "Audit Trail", rx + Inches(0.15), Inches(1.2),
|
| 303 |
+
Inches(5.5), Inches(0.42), sz=14, bold=True, clr=BLUE, font=FONT_H)
|
| 304 |
+
for i, line in enumerate([
|
| 305 |
+
"Every action logged with:",
|
| 306 |
+
"• UTC timestamp",
|
| 307 |
+
"• Action type: criteria_extracted / bidder_processed",
|
| 308 |
+
" criterion_evaluated / human_review_action",
|
| 309 |
+
" vision_ocr_invoked / precomputed_fallback_used",
|
| 310 |
+
"• Model version & Actor (system / officer)",
|
| 311 |
+
"• Full payload JSON — Exportable as CSV",
|
| 312 |
+
]):
|
| 313 |
+
txt(slide, line, rx + Inches(0.15), Inches(1.65) + Inches(0.42 * i),
|
| 314 |
+
Inches(5.55), Inches(0.38), sz=13, clr=TXT)
|
| 315 |
+
|
| 316 |
+
rect(slide, Inches(0.6), Inches(5.35), Inches(12.13), Inches(1.2),
|
| 317 |
+
LTBLUE, BLUE, Pt(2))
|
| 318 |
+
txt(slide, "The Safety Rule:",
|
| 319 |
+
Inches(0.8), Inches(5.42), Inches(5), Inches(0.38),
|
| 320 |
+
sz=14, bold=True, clr=BLUE, font=FONT_H)
|
| 321 |
+
txt(slide, ("If combined confidence is 0.55–0.80 AND verdict is NOT ELIGIBLE, "
|
| 322 |
+
"the verdict is automatically downgraded to UNDER REVIEW. "
|
| 323 |
+
"A bidder is NEVER silently disqualified at medium confidence."),
|
| 324 |
+
Inches(0.8), Inches(5.85), Inches(11.9), Inches(0.65),
|
| 325 |
+
sz=13, clr=TXT)
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
# ── Slide 7 ─────────────────────────────────────────────────────────────────
|
| 329 |
+
def s7(prs):
|
| 330 |
+
slide = blank(prs)
|
| 331 |
+
bg(slide)
|
| 332 |
+
header_bar(slide, "Demo: Three Bidders, Three Outcomes")
|
| 333 |
+
bidders = [
|
| 334 |
+
("Bidder A — Apex Constructions Pvt. Ltd.", IGRN, "ELIGIBLE",
|
| 335 |
+
["C1 Turnover: INR 6.37 Cr avg (threshold 5 Cr) — PASS",
|
| 336 |
+
"C2 Projects: 5 completed incl. CRPF barracks — PASS",
|
| 337 |
+
"C3 GST: GSTIN 27AABCA1234F1Z5, Active — PASS",
|
| 338 |
+
"C4 ISO 9001:2015: Valid June 2027 — PASS",
|
| 339 |
+
"All typed PDFs, confidence ≥ 93%"]),
|
| 340 |
+
("Bidder B — BuildRight Enterprises", RGBColor(0xB9, 0x1C, 0x1C), "NOT ELIGIBLE",
|
| 341 |
+
["C1 Turnover: INR 1.5 Cr avg (threshold 5 Cr) — FAIL",
|
| 342 |
+
"Reason: INR 1.5 Cr is below required INR 5 Cr",
|
| 343 |
+
"C2–C4: All pass",
|
| 344 |
+
"Auto-disqualified at high confidence (95%)"]),
|
| 345 |
+
("Bidder C — Shree Constructions & Services", SAF, "UNDER REVIEW",
|
| 346 |
+
["C1 Turnover: Submitted as blurry scan",
|
| 347 |
+
"Tesseract ~55% → Vision LLM: INR 5.4 Cr",
|
| 348 |
+
"Combined confidence 0.58 → safety rule triggered",
|
| 349 |
+
"C2: Exactly 3 projects (borderline)",
|
| 350 |
+
"C3–C4: Pass"]),
|
| 351 |
+
]
|
| 352 |
+
cw, ch = Inches(3.85), Inches(3.5)
|
| 353 |
+
for i, (name, color, verdict, bullets) in enumerate(bidders):
|
| 354 |
+
sx = Inches(0.6) + i * Inches(4.25)
|
| 355 |
+
rect(slide, sx, Inches(1.1), cw, ch, WHITE, BLUE, Pt(1))
|
| 356 |
+
rect(slide, sx, Inches(1.1), cw, Inches(0.08), color)
|
| 357 |
+
txt(slide, name, sx + Inches(0.15), Inches(1.22), cw - Inches(0.3), Inches(0.5),
|
| 358 |
+
sz=12, bold=True, clr=BLUE, font=FONT_H)
|
| 359 |
+
rect(slide, sx + Inches(0.15), Inches(1.8), Inches(3.4), Inches(0.35),
|
| 360 |
+
RGBColor(0xEB, 0xF0, 0xFF), color, Pt(1))
|
| 361 |
+
txt(slide, verdict, sx + Inches(0.15), Inches(1.8), Inches(3.4), Inches(0.35),
|
| 362 |
+
sz=12, bold=True, clr=color, align=PP_ALIGN.CENTER)
|
| 363 |
+
for j, b in enumerate(bullets):
|
| 364 |
+
txt(slide, b, sx + Inches(0.15), Inches(2.25) + Inches(0.37 * j),
|
| 365 |
+
cw - Inches(0.3), Inches(0.33), sz=11, clr=TXT)
|
| 366 |
+
|
| 367 |
+
# Metric strip
|
| 368 |
+
metrics = [
|
| 369 |
+
("Criteria extracted", "5"),
|
| 370 |
+
("Bidder docs processed", "15"),
|
| 371 |
+
("LLM evaluation calls", "15"),
|
| 372 |
+
("Vision OCR invocations", "1"),
|
| 373 |
+
("Human review items", "1"),
|
| 374 |
+
("Total audit entries", "20+"),
|
| 375 |
+
]
|
| 376 |
+
rect(slide, Inches(0.6), Inches(4.9), Inches(12.13), Inches(1.0),
|
| 377 |
+
LTBLUE, BLUE, Pt(1))
|
| 378 |
+
mw = Inches(2.0)
|
| 379 |
+
for i, (label, val) in enumerate(metrics):
|
| 380 |
+
mx = Inches(0.75) + i * Inches(2.0)
|
| 381 |
+
txt(slide, val, mx, Inches(4.97), mw, Inches(0.38),
|
| 382 |
+
sz=22, bold=True, clr=BLUE, align=PP_ALIGN.CENTER, font=FONT_H)
|
| 383 |
+
txt(slide, label, mx, Inches(5.42), mw, Inches(0.35),
|
| 384 |
+
sz=11, clr=TXT, align=PP_ALIGN.CENTER)
|
| 385 |
+
|
| 386 |
+
|
| 387 |
+
# ── Slide 8 ─────────────────────────────────────────────────────────────────
|
| 388 |
+
def s8(prs):
|
| 389 |
+
slide = blank(prs)
|
| 390 |
+
bg(slide)
|
| 391 |
+
header_bar(slide, "Stack, Impact & What's Next")
|
| 392 |
+
stack = [
|
| 393 |
+
("UI & orchestration", "Streamlit 1.39"),
|
| 394 |
+
("LLM", "DeepSeek API (OpenAI-compatible)"),
|
| 395 |
+
("OCR Tier 1", "PyMuPDF 1.24"),
|
| 396 |
+
("OCR Tier 2", "Tesseract"),
|
| 397 |
+
("OCR Tier 3", "DeepSeek Vision LLM"),
|
| 398 |
+
("Semantic retrieval", "sentence-transformers all-MiniLM-L6-v2"),
|
| 399 |
+
("Data validation", "Pydantic v2"),
|
| 400 |
+
("Audit log", "SQLite"),
|
| 401 |
+
("Deployment", "Streamlit Cloud / HuggingFace Spaces"),
|
| 402 |
+
]
|
| 403 |
+
rect(slide, Inches(0.6), Inches(1.05), Inches(6.0), Inches(0.4),
|
| 404 |
+
BLUE)
|
| 405 |
+
txt(slide, "Component", Inches(0.7), Inches(1.1), Inches(2.7), Inches(0.3),
|
| 406 |
+
sz=13, bold=True, clr=WHITE, font=FONT_H)
|
| 407 |
+
txt(slide, "Technology", Inches(3.55), Inches(1.1), Inches(2.9), Inches(0.3),
|
| 408 |
+
sz=13, bold=True, clr=WHITE, font=FONT_H)
|
| 409 |
+
for i, (comp, tech) in enumerate(stack):
|
| 410 |
+
ry = Inches(1.45) + Inches(0.38 * i)
|
| 411 |
+
bg_r = WHITE if i % 2 == 0 else RGBColor(0xF0, 0xF4, 0xFF)
|
| 412 |
+
rect(slide, Inches(0.6), ry, Inches(6.0), Inches(0.38), bg_r,
|
| 413 |
+
RGBColor(0xCB, 0xD5, 0xE1), Pt(0))
|
| 414 |
+
txt(slide, comp, Inches(0.7), ry + Inches(0.06), Inches(2.7), Inches(0.3),
|
| 415 |
+
sz=12, clr=TXT)
|
| 416 |
+
txt(slide, tech, Inches(3.55), ry + Inches(0.06), Inches(2.9), Inches(0.3),
|
| 417 |
+
sz=12, clr=TXT)
|
| 418 |
+
|
| 419 |
+
txt(slide, "Future Work", Inches(7.2), Inches(1.05), Inches(5.7), Inches(0.42),
|
| 420 |
+
sz=15, bold=True, clr=BLUE, font=FONT_H)
|
| 421 |
+
future = [
|
| 422 |
+
"Multi-tender workspace — same bidder pool, multiple tenders",
|
| 423 |
+
"GeM portal API integration — live tender ingestion",
|
| 424 |
+
"Automated bidder ranking with weighted scoring",
|
| 425 |
+
"LayoutLM for complex financial tables in scanned statements",
|
| 426 |
+
"Multi-evaluator workflow with role-based approval",
|
| 427 |
+
"Review queue email/SMS notifications",
|
| 428 |
+
"Audit PDF export for procurement oversight submissions",
|
| 429 |
+
]
|
| 430 |
+
for i, f in enumerate(future):
|
| 431 |
+
txt(slide, f"• {f}", Inches(7.2), Inches(1.55) + Inches(0.47 * i),
|
| 432 |
+
Inches(5.8), Inches(0.42), sz=13, clr=TXT)
|
| 433 |
+
|
| 434 |
+
rect(slide, Inches(0.6), Inches(6.18), Inches(12.13), Inches(0.95),
|
| 435 |
+
LTBLUE, BLUE, Pt(2))
|
| 436 |
+
txt(slide, ("3–5 days → minutes. Every verdict traceable to a document, page, and model version."
|
| 437 |
+
" Built in one hackathon session. Deployable today."),
|
| 438 |
+
Inches(0.8), Inches(6.3), Inches(11.9), Inches(0.75),
|
| 439 |
+
sz=13, bold=True, clr=BLUE, align=PP_ALIGN.CENTER, font=FONT_H)
|
| 440 |
+
|
| 441 |
+
|
| 442 |
+
def main():
|
| 443 |
+
os.makedirs("deck", exist_ok=True)
|
| 444 |
+
prs = new_prs()
|
| 445 |
+
s1(prs); s2(prs); s3(prs); s4(prs)
|
| 446 |
+
s5(prs); s6(prs); s7(prs); s8(prs)
|
| 447 |
+
out = "deck/TenderIQ_v3_government_official.pptx"
|
| 448 |
+
prs.save(out)
|
| 449 |
+
print(f"OK: Saved {out} ({len(prs.slides)} slides)")
|
| 450 |
+
|
| 451 |
+
|
| 452 |
+
if __name__ == "__main__":
|
| 453 |
+
main()
|
scripts/make_v4.py
ADDED
|
@@ -0,0 +1,575 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""TenderIQ v4 — Modern Gradient (PDF via reportlab)"""
|
| 3 |
+
import os
|
| 4 |
+
from reportlab.pdfgen.canvas import Canvas
|
| 5 |
+
from reportlab.lib.pagesizes import A4, landscape
|
| 6 |
+
from reportlab.lib import colors
|
| 7 |
+
from reportlab.lib.units import cm, mm
|
| 8 |
+
from reportlab.platypus import Paragraph
|
| 9 |
+
from reportlab.lib.styles import ParagraphStyle
|
| 10 |
+
|
| 11 |
+
W, H = landscape(A4) # 841.89 × 595.28 points
|
| 12 |
+
|
| 13 |
+
# Palette
|
| 14 |
+
C_PUR1 = colors.HexColor("#667EEA")
|
| 15 |
+
C_PUR2 = colors.HexColor("#764BA2")
|
| 16 |
+
C_BLU1 = colors.HexColor("#0EA5E9")
|
| 17 |
+
C_BLU2 = colors.HexColor("#2563EB")
|
| 18 |
+
C_DARK = colors.HexColor("#0F172A")
|
| 19 |
+
C_WHITE = colors.white
|
| 20 |
+
C_GRY = colors.HexColor("#64748B")
|
| 21 |
+
C_GRY2 = colors.HexColor("#E2E8F0")
|
| 22 |
+
C_GRN = colors.HexColor("#10B981")
|
| 23 |
+
C_RED = colors.HexColor("#F43F5E")
|
| 24 |
+
C_AMB = colors.HexColor("#FBBF24")
|
| 25 |
+
C_CARD = colors.HexColor("#FFFFFF")
|
| 26 |
+
|
| 27 |
+
M = 1.8 * cm # margin
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def grad_rect(c, x, y, w, h, col1, col2, steps=40):
|
| 31 |
+
"""Approximate horizontal gradient with filled rects."""
|
| 32 |
+
sw = w / steps
|
| 33 |
+
from reportlab.lib.colors import Color
|
| 34 |
+
r1, g1, b1 = col1.red, col1.green, col1.blue
|
| 35 |
+
r2, g2, b2 = col2.red, col2.green, col2.blue
|
| 36 |
+
for i in range(steps):
|
| 37 |
+
t = i / (steps - 1)
|
| 38 |
+
r = r1 + t * (r2 - r1)
|
| 39 |
+
g = g1 + t * (g2 - g1)
|
| 40 |
+
b = b1 + t * (b2 - b1)
|
| 41 |
+
c.setFillColor(Color(r, g, b))
|
| 42 |
+
c.rect(x + i * sw, y, sw + 1, h, fill=1, stroke=0)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def page_num(c, n):
|
| 46 |
+
c.setFont("Helvetica", 9)
|
| 47 |
+
c.setFillColor(C_GRY)
|
| 48 |
+
c.drawRightString(W - M, 0.8 * cm, str(n))
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def header_band(c, title, n):
|
| 52 |
+
"""Gradient header band for content slides."""
|
| 53 |
+
bh = H * 0.18
|
| 54 |
+
grad_rect(c, 0, H - bh, W, bh, C_BLU1, C_BLU2)
|
| 55 |
+
c.setFillColor(C_WHITE)
|
| 56 |
+
c.setFont("Helvetica-Bold", 22)
|
| 57 |
+
c.drawString(M, H - bh + 0.55 * cm, title)
|
| 58 |
+
page_num(c, n)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def card(c, x, y, w, h, accent=None):
|
| 62 |
+
"""White card with optional top accent border."""
|
| 63 |
+
c.setFillColor(C_CARD)
|
| 64 |
+
c.setStrokeColor(C_GRY2)
|
| 65 |
+
c.setLineWidth(0.5)
|
| 66 |
+
c.roundRect(x, y, w, h, 4, fill=1, stroke=1)
|
| 67 |
+
if accent:
|
| 68 |
+
c.setFillColor(accent)
|
| 69 |
+
c.setStrokeColor(accent)
|
| 70 |
+
c.roundRect(x, y + h - 4, w, 4, 2, fill=1, stroke=0)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def wrapped(c, text, x, y, width, font="Helvetica", size=12, color=C_DARK, leading=16):
|
| 74 |
+
style = ParagraphStyle('s', fontName=font, fontSize=size, leading=leading,
|
| 75 |
+
textColor=color)
|
| 76 |
+
p = Paragraph(text.replace("\n", "<br/>"), style)
|
| 77 |
+
_, used = p.wrapOn(c, width, 999)
|
| 78 |
+
p.drawOn(c, x, y - used)
|
| 79 |
+
return used
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def label(c, text, x, y, sz=10, font="Helvetica", color=C_GRY, align="left"):
|
| 83 |
+
c.setFont(font, sz)
|
| 84 |
+
c.setFillColor(color)
|
| 85 |
+
if align == "center":
|
| 86 |
+
c.drawCentredString(x, y, text)
|
| 87 |
+
elif align == "right":
|
| 88 |
+
c.drawRightString(x, y, text)
|
| 89 |
+
else:
|
| 90 |
+
c.drawString(x, y, text)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
# ── Slide 1 ─────────────────────────────────────────────────────────────────
|
| 94 |
+
def s1(c):
|
| 95 |
+
grad_rect(c, 0, 0, W, H, C_PUR1, C_PUR2)
|
| 96 |
+
c.setFillColor(colors.white)
|
| 97 |
+
c.setFont("Helvetica-Bold", 72)
|
| 98 |
+
c.drawCentredString(W / 2, H / 2 + 1.5 * cm, "TenderIQ")
|
| 99 |
+
c.setFont("Helvetica", 22)
|
| 100 |
+
c.drawCentredString(W / 2, H / 2 - 0.5 * cm,
|
| 101 |
+
"Explainable AI for Government Tender Evaluation")
|
| 102 |
+
c.setFont("Helvetica", 14)
|
| 103 |
+
c.setFillColor(colors.HexColor("#DDD6FE"))
|
| 104 |
+
c.drawCentredString(W / 2, H / 2 - 1.6 * cm, "CRPF Hackathon · Theme 3")
|
| 105 |
+
# divider
|
| 106 |
+
c.setStrokeColor(colors.HexColor("#A78BFA"))
|
| 107 |
+
c.setLineWidth(1.5)
|
| 108 |
+
c.line(W / 2 - 5 * cm, H / 2 - 2.2 * cm, W / 2 + 5 * cm, H / 2 - 2.2 * cm)
|
| 109 |
+
c.setFont("Helvetica-Oblique", 13)
|
| 110 |
+
c.setFillColor(colors.white)
|
| 111 |
+
c.drawCentredString(W / 2, H / 2 - 3.0 * cm,
|
| 112 |
+
"From days to minutes. Every decision traceable.")
|
| 113 |
+
page_num(c, 1)
|
| 114 |
+
c.showPage()
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
# ── Slide 2 ─────────────────────────────────────────────────────────────────
|
| 118 |
+
def s2(c):
|
| 119 |
+
c.setFillColor(colors.white)
|
| 120 |
+
c.rect(0, 0, W, H, fill=1, stroke=0)
|
| 121 |
+
header_band(c, "The Problem with Manual Tender Evaluation", 2)
|
| 122 |
+
|
| 123 |
+
callouts = [
|
| 124 |
+
("3–5", "Days per\ntender evaluation", C_PUR1),
|
| 125 |
+
("≠", "Inconsistent verdicts\nfrom evaluators", C_BLU2),
|
| 126 |
+
("0", "Zero audit trail\nfor decisions", C_GRN),
|
| 127 |
+
]
|
| 128 |
+
bw = (W - 2 * M - 1.5 * cm) / 3
|
| 129 |
+
top = H * 0.72
|
| 130 |
+
for i, (big, sub, acc) in enumerate(callouts):
|
| 131 |
+
bx = M + i * (bw + 0.75 * cm)
|
| 132 |
+
by = top - 3.8 * cm
|
| 133 |
+
card(c, bx, by, bw, 3.8 * cm, acc)
|
| 134 |
+
c.setFont("Helvetica-Bold", 52)
|
| 135 |
+
c.setFillColor(acc)
|
| 136 |
+
c.drawCentredString(bx + bw / 2, by + 2.4 * cm, big)
|
| 137 |
+
c.setFont("Helvetica", 12)
|
| 138 |
+
c.setFillColor(C_DARK)
|
| 139 |
+
lines = sub.split("\n")
|
| 140 |
+
for li, line in enumerate(lines):
|
| 141 |
+
c.drawCentredString(bx + bw / 2, by + 1.4 * cm - li * 0.45 * cm, line)
|
| 142 |
+
|
| 143 |
+
bullets = [
|
| 144 |
+
"• Mixed document formats: typed PDFs, scanned certificates, phone photographs",
|
| 145 |
+
"• Government procurement worth ₹50 lakh crore+ annually in India",
|
| 146 |
+
"• Project execution delays traced directly to procurement bottlenecks",
|
| 147 |
+
]
|
| 148 |
+
by2 = top - 5.5 * cm
|
| 149 |
+
c.setFont("Helvetica", 12)
|
| 150 |
+
c.setFillColor(C_GRY)
|
| 151 |
+
for i, b in enumerate(bullets):
|
| 152 |
+
c.drawString(M, by2 - i * 0.55 * cm, b)
|
| 153 |
+
page_num(c, 2)
|
| 154 |
+
c.showPage()
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
# ── Slide 3 ─────────────────────────────────────────────────────────────────
|
| 158 |
+
def s3(c):
|
| 159 |
+
c.setFillColor(colors.white)
|
| 160 |
+
c.rect(0, 0, W, H, fill=1, stroke=0)
|
| 161 |
+
header_band(c, "TenderIQ — Four Stages, End to End", 3)
|
| 162 |
+
|
| 163 |
+
stages = [
|
| 164 |
+
("1 — Extract",
|
| 165 |
+
"DeepSeek LLM reads the tender PDF and returns each criterion as structured JSON: "
|
| 166 |
+
"category, mandatory flag, threshold rule, source clause, query hints.",
|
| 167 |
+
C_PUR1),
|
| 168 |
+
("2 — OCR & Index",
|
| 169 |
+
"Three-tier pipeline handles any document format. All text chunked and indexed "
|
| 170 |
+
"for semantic retrieval.",
|
| 171 |
+
C_BLU2),
|
| 172 |
+
("3 — Evaluate",
|
| 173 |
+
"Vector search finds relevant evidence. LLM produces a verdict with confidence. "
|
| 174 |
+
"Safety rule prevents silent disqualification.",
|
| 175 |
+
C_GRN),
|
| 176 |
+
("4 — Review & Audit",
|
| 177 |
+
"Borderline cases go to human review queue. Every action logged. "
|
| 178 |
+
"Full audit trail exportable as CSV.",
|
| 179 |
+
C_AMB),
|
| 180 |
+
]
|
| 181 |
+
cw = (W - 2 * M - 1.5 * cm) / 4
|
| 182 |
+
top = H * 0.77
|
| 183 |
+
for i, (title, body, acc) in enumerate(stages):
|
| 184 |
+
cx = M + i * (cw + 0.5 * cm)
|
| 185 |
+
cy = top - 3.5 * cm
|
| 186 |
+
card(c, cx, cy, cw, 3.5 * cm, acc)
|
| 187 |
+
c.setFont("Helvetica-Bold", 13)
|
| 188 |
+
c.setFillColor(acc)
|
| 189 |
+
c.drawString(cx + 0.3 * cm, cy + 3.0 * cm, title)
|
| 190 |
+
wrapped(c, body, cx + 0.3 * cm, cy + 2.8 * cm, cw - 0.6 * cm, size=11, color=C_DARK, leading=15)
|
| 191 |
+
|
| 192 |
+
# Callout
|
| 193 |
+
by = top - 5.2 * cm
|
| 194 |
+
grad_rect(c, M, by, W - 2 * M, 1.2 * cm, C_BLU1, C_BLU2)
|
| 195 |
+
c.setFillColor(C_WHITE)
|
| 196 |
+
c.setFont("Helvetica-BoldOblique", 13)
|
| 197 |
+
c.drawCentredString(W / 2, by + 0.38 * cm,
|
| 198 |
+
'"Minutes, not days. Every verdict traceable to a document and page."')
|
| 199 |
+
page_num(c, 3)
|
| 200 |
+
c.showPage()
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
# ── Slide 4 ─────────────────────────────────────────────────────────────────
|
| 204 |
+
def s4(c):
|
| 205 |
+
c.setFillColor(colors.white)
|
| 206 |
+
c.rect(0, 0, W, H, fill=1, stroke=0)
|
| 207 |
+
header_band(c, "System Architecture", 4)
|
| 208 |
+
|
| 209 |
+
bw, bh = 5.5 * cm, 1.1 * cm
|
| 210 |
+
top = H * 0.74
|
| 211 |
+
|
| 212 |
+
def abox(x, y, text, acc=C_BLU2):
|
| 213 |
+
card(c, x, y, bw, bh, acc)
|
| 214 |
+
c.setFont("Helvetica", 10)
|
| 215 |
+
c.setFillColor(C_DARK)
|
| 216 |
+
lines = text.split("\n")
|
| 217 |
+
for li, ln in enumerate(lines):
|
| 218 |
+
c.drawCentredString(x + bw / 2, y + bh - 0.4 * cm - li * 0.35 * cm, ln)
|
| 219 |
+
|
| 220 |
+
def arr(x, y):
|
| 221 |
+
c.setStrokeColor(C_BLU2)
|
| 222 |
+
c.setLineWidth(1.5)
|
| 223 |
+
c.line(x + bw / 2, y, x + bw / 2, y - 0.6 * cm)
|
| 224 |
+
# arrowhead
|
| 225 |
+
ax = x + bw / 2
|
| 226 |
+
ay = y - 0.6 * cm
|
| 227 |
+
c.setFillColor(C_BLU2)
|
| 228 |
+
p = c.beginPath()
|
| 229 |
+
p.moveTo(ax - 0.15 * cm, ay)
|
| 230 |
+
p.lineTo(ax + 0.15 * cm, ay)
|
| 231 |
+
p.lineTo(ax, ay - 0.25 * cm)
|
| 232 |
+
p.close()
|
| 233 |
+
c.drawPath(p, fill=1, stroke=0)
|
| 234 |
+
|
| 235 |
+
step = 1.7 * cm
|
| 236 |
+
lx = M
|
| 237 |
+
abox(lx, top, "Tender PDF")
|
| 238 |
+
arr(lx, top)
|
| 239 |
+
abox(lx, top - step, "DeepSeek LLM\n(Extract Criteria)")
|
| 240 |
+
arr(lx, top - step)
|
| 241 |
+
abox(lx, top - 2 * step, "Criteria JSON\n(C1–C5 structured)")
|
| 242 |
+
|
| 243 |
+
rx = M + 7 * cm
|
| 244 |
+
abox(rx, top, "Bidder Docs\n(PDFs · scans · photos)")
|
| 245 |
+
arr(rx, top)
|
| 246 |
+
abox(rx, top - step, "3-Tier OCR Pipeline\n① PyMuPDF ② Tesseract ③ Vision LLM")
|
| 247 |
+
arr(rx, top - step)
|
| 248 |
+
abox(rx, top - 2 * step, "Vector Index\n(all-MiniLM-L6-v2 embeddings)")
|
| 249 |
+
|
| 250 |
+
# Converge
|
| 251 |
+
mid_y = top - 2 * step - bh
|
| 252 |
+
mx = M + 3.5 * cm + bw / 2
|
| 253 |
+
c.setStrokeColor(C_BLU2)
|
| 254 |
+
c.setLineWidth(1.5)
|
| 255 |
+
c.line(lx + bw / 2, mid_y, mx, mid_y - 0.6 * cm)
|
| 256 |
+
c.line(rx + bw / 2, mid_y, mx, mid_y - 0.6 * cm)
|
| 257 |
+
abox(M + 3.5 * cm, mid_y - 1.7 * cm, "DeepSeek LLM — Evaluate\neach criterion")
|
| 258 |
+
|
| 259 |
+
eval_y = mid_y - 1.7 * cm
|
| 260 |
+
arr(M + 3.5 * cm, eval_y)
|
| 261 |
+
|
| 262 |
+
# Outputs
|
| 263 |
+
out_y = eval_y - step
|
| 264 |
+
c.setFillColor(colors.HexColor("#D1FAE5"))
|
| 265 |
+
c.setStrokeColor(C_GRN)
|
| 266 |
+
c.setLineWidth(1)
|
| 267 |
+
c.roundRect(M + 1 * cm, out_y, 3.5 * cm, bh, 3, fill=1, stroke=1)
|
| 268 |
+
c.setFont("Helvetica-Bold", 10)
|
| 269 |
+
c.setFillColor(C_GRN)
|
| 270 |
+
c.drawCentredString(M + 2.75 * cm, out_y + 0.38 * cm, "eligible / not_eligible")
|
| 271 |
+
|
| 272 |
+
c.setFillColor(colors.HexColor("#FEF3C7"))
|
| 273 |
+
c.setStrokeColor(C_AMB)
|
| 274 |
+
c.roundRect(M + 5.5 * cm, out_y, 3.8 * cm, bh, 3, fill=1, stroke=1)
|
| 275 |
+
c.setFont("Helvetica-Bold", 10)
|
| 276 |
+
c.setFillColor(C_AMB)
|
| 277 |
+
c.drawCentredString(M + 7.4 * cm, out_y + 0.38 * cm, "needs_review / Review Queue")
|
| 278 |
+
|
| 279 |
+
# SQLite
|
| 280 |
+
abox(M + 3.5 * cm, out_y - step, "SQLite Audit Log")
|
| 281 |
+
|
| 282 |
+
# Key facts panel
|
| 283 |
+
fx = W - M - 8.5 * cm
|
| 284 |
+
c.setFillColor(colors.HexColor("#EFF6FF"))
|
| 285 |
+
c.setStrokeColor(C_BLU2)
|
| 286 |
+
c.setLineWidth(1)
|
| 287 |
+
c.roundRect(fx, H * 0.18, 8.5 * cm, H * 0.56, 5, fill=1, stroke=1)
|
| 288 |
+
c.setFont("Helvetica-Bold", 13)
|
| 289 |
+
c.setFillColor(C_BLU2)
|
| 290 |
+
c.drawString(fx + 0.4 * cm, H * 0.7, "Key Technical Facts")
|
| 291 |
+
facts = [
|
| 292 |
+
"Single-process Streamlit app",
|
| 293 |
+
"Deployable: Streamlit Cloud / HuggingFace",
|
| 294 |
+
"All storage local: SQLite + vector index",
|
| 295 |
+
"Only external dep: DeepSeek API",
|
| 296 |
+
]
|
| 297 |
+
c.setFont("Helvetica", 11)
|
| 298 |
+
c.setFillColor(C_DARK)
|
| 299 |
+
for i, f in enumerate(facts):
|
| 300 |
+
c.drawString(fx + 0.4 * cm, H * 0.63 - i * 0.65 * cm, f"• {f}")
|
| 301 |
+
|
| 302 |
+
page_num(c, 4)
|
| 303 |
+
c.showPage()
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
# ── Slide 5 ─────────────────────────────────────────────────────────────────
|
| 307 |
+
def s5(c):
|
| 308 |
+
c.setFillColor(colors.white)
|
| 309 |
+
c.rect(0, 0, W, H, fill=1, stroke=0)
|
| 310 |
+
header_band(c, "Three-Tier OCR — Handling Any Document Format", 5)
|
| 311 |
+
|
| 312 |
+
tiers = [
|
| 313 |
+
("Tier 1 — PyMuPDF",
|
| 314 |
+
"Trigger: Typed / digital PDF\nCost: Free, instant\nConfidence: 1.0 (lossless)\nLabel: Typed PDF",
|
| 315 |
+
C_BLU2),
|
| 316 |
+
("Tier 2 — Tesseract",
|
| 317 |
+
"Trigger: Scanned PDF or image\nCost: Free, local, fast\nConfidence: Mean per-word OCR\nLabel: Tesseract",
|
| 318 |
+
colors.HexColor("#7C3AED")),
|
| 319 |
+
("Tier 3 — DeepSeek Vision LLM",
|
| 320 |
+
"Trigger: Tesseract confidence < 65%\nCost: One API call\nConfidence: 0.95\nLabel: Vision LLM\nLogged: vision_ocr_invoked",
|
| 321 |
+
colors.HexColor("#EA580C")),
|
| 322 |
+
]
|
| 323 |
+
cw = (W - 2 * M - 2 * cm) / 3
|
| 324 |
+
top = H * 0.74
|
| 325 |
+
for i, (title, body, acc) in enumerate(tiers):
|
| 326 |
+
cx = M + i * (cw + 1 * cm)
|
| 327 |
+
cy = top - 3.2 * cm
|
| 328 |
+
card(c, cx, cy, cw, 3.2 * cm, acc)
|
| 329 |
+
c.setFont("Helvetica-Bold", 13)
|
| 330 |
+
c.setFillColor(acc)
|
| 331 |
+
c.drawString(cx + 0.35 * cm, cy + 2.75 * cm, title)
|
| 332 |
+
wrapped(c, body.replace("\n", "<br/>"), cx + 0.35 * cm, cy + 2.55 * cm,
|
| 333 |
+
cw - 0.7 * cm, size=11, color=C_DARK, leading=15)
|
| 334 |
+
|
| 335 |
+
# Demo callout
|
| 336 |
+
by = H * 0.27
|
| 337 |
+
grad_rect(c, M, by, W - 2 * M, 1.95 * cm, C_BLU1, C_BLU2, steps=30)
|
| 338 |
+
c.setFillColor(C_WHITE)
|
| 339 |
+
c.setFont("Helvetica-Bold", 12)
|
| 340 |
+
c.drawString(M + 0.4 * cm, by + 1.58 * cm, "Demo Scenario")
|
| 341 |
+
c.setFont("Helvetica", 11)
|
| 342 |
+
demo = ("Bidder C submits a blurry, rotated CA certificate scan. Tesseract reads it at ~55% confidence. "
|
| 343 |
+
"Vision LLM transcribes the turnover figure correctly. Combined confidence = 0.58 → routed to "
|
| 344 |
+
"human review. This is intentional — borderline evidence requires a human.")
|
| 345 |
+
wrapped(c, demo, M + 0.4 * cm, by + 1.35 * cm, W - 2 * M - 0.8 * cm,
|
| 346 |
+
size=11, color=C_WHITE, leading=15)
|
| 347 |
+
page_num(c, 5)
|
| 348 |
+
c.showPage()
|
| 349 |
+
|
| 350 |
+
|
| 351 |
+
# ── Slide 6 ─────────────────────────────────────────────────────────────────
|
| 352 |
+
def s6(c):
|
| 353 |
+
c.setFillColor(colors.white)
|
| 354 |
+
c.rect(0, 0, W, H, fill=1, stroke=0)
|
| 355 |
+
header_band(c, "Every Decision is Explainable and Auditable", 6)
|
| 356 |
+
|
| 357 |
+
half = (W - 2 * M - 0.8 * cm) / 2
|
| 358 |
+
lx = M
|
| 359 |
+
rx = M + half + 0.8 * cm
|
| 360 |
+
top = H * 0.76
|
| 361 |
+
ch = 3.4 * cm
|
| 362 |
+
|
| 363 |
+
card(c, lx, top - ch, half, ch, C_BLU2)
|
| 364 |
+
c.setFont("Helvetica-Bold", 13)
|
| 365 |
+
c.setFillColor(C_BLU2)
|
| 366 |
+
c.drawString(lx + 0.35 * cm, top - 0.45 * cm, "Criterion-Level Verdicts")
|
| 367 |
+
left_lines = [
|
| 368 |
+
"Each (bidder × criterion) pair shows:",
|
| 369 |
+
"• Which criterion was checked",
|
| 370 |
+
"• Document and page providing evidence",
|
| 371 |
+
"• Value extracted (e.g. 'INR 6.2 Cr')",
|
| 372 |
+
"• OCR tier that read the document",
|
| 373 |
+
"• Combined confidence score (0–100%)",
|
| 374 |
+
"• Plain-English reason",
|
| 375 |
+
]
|
| 376 |
+
c.setFont("Helvetica", 11)
|
| 377 |
+
c.setFillColor(C_DARK)
|
| 378 |
+
for i, ln in enumerate(left_lines):
|
| 379 |
+
c.drawString(lx + 0.35 * cm, top - 0.9 * cm - i * 0.42 * cm, ln)
|
| 380 |
+
|
| 381 |
+
card(c, rx, top - ch, half, ch, C_BLU2)
|
| 382 |
+
c.setFont("Helvetica-Bold", 13)
|
| 383 |
+
c.setFillColor(C_BLU2)
|
| 384 |
+
c.drawString(rx + 0.35 * cm, top - 0.45 * cm, "Audit Trail")
|
| 385 |
+
right_lines = [
|
| 386 |
+
"Every action logged with:",
|
| 387 |
+
"• UTC timestamp",
|
| 388 |
+
"• Action type (criteria_extracted /",
|
| 389 |
+
" bidder_processed / criterion_evaluated /",
|
| 390 |
+
" vision_ocr_invoked / precomputed_fallback)",
|
| 391 |
+
"• Model version & Actor",
|
| 392 |
+
"• Full payload JSON — Exportable as CSV",
|
| 393 |
+
]
|
| 394 |
+
c.setFont("Helvetica", 11)
|
| 395 |
+
c.setFillColor(C_DARK)
|
| 396 |
+
for i, ln in enumerate(right_lines):
|
| 397 |
+
c.drawString(rx + 0.35 * cm, top - 0.9 * cm - i * 0.42 * cm, ln)
|
| 398 |
+
|
| 399 |
+
# Safety rule
|
| 400 |
+
sy = top - ch - 0.4 * cm - 1.8 * cm
|
| 401 |
+
c.setFillColor(colors.HexColor("#FEF3C7"))
|
| 402 |
+
c.setStrokeColor(C_AMB)
|
| 403 |
+
c.setLineWidth(2)
|
| 404 |
+
c.roundRect(M, sy, W - 2 * M, 1.8 * cm, 4, fill=1, stroke=1)
|
| 405 |
+
c.setFont("Helvetica-Bold", 13)
|
| 406 |
+
c.setFillColor(colors.HexColor("#92400E"))
|
| 407 |
+
c.drawString(M + 0.4 * cm, sy + 1.35 * cm, "The Safety Rule:")
|
| 408 |
+
c.setFont("Helvetica", 12)
|
| 409 |
+
c.setFillColor(C_DARK)
|
| 410 |
+
rule = ("If combined confidence is 0.55–0.80 AND verdict is not_eligible, the verdict is "
|
| 411 |
+
"automatically downgraded to needs_review. A bidder is NEVER silently disqualified at medium confidence.")
|
| 412 |
+
wrapped(c, rule, M + 0.4 * cm, sy + 1.1 * cm, W - 2 * M - 0.8 * cm, size=11, color=C_DARK, leading=15)
|
| 413 |
+
page_num(c, 6)
|
| 414 |
+
c.showPage()
|
| 415 |
+
|
| 416 |
+
|
| 417 |
+
# ── Slide 7 ─────────────────────────────────────────────────────────────────
|
| 418 |
+
def s7(c):
|
| 419 |
+
c.setFillColor(colors.white)
|
| 420 |
+
c.rect(0, 0, W, H, fill=1, stroke=0)
|
| 421 |
+
header_band(c, "Demo: Three Bidders, Three Outcomes", 7)
|
| 422 |
+
|
| 423 |
+
bidders = [
|
| 424 |
+
("Bidder A\nApex Constructions Pvt. Ltd.", C_GRN, "ELIGIBLE",
|
| 425 |
+
colors.HexColor("#D1FAE5"),
|
| 426 |
+
["C1 Turnover: INR 6.37 Cr avg — PASS",
|
| 427 |
+
"C2 Projects: 5 completed (CRPF) — PASS",
|
| 428 |
+
"C3 GST: GSTIN active — PASS",
|
| 429 |
+
"C4 ISO 9001:2015 valid June 2027 — PASS",
|
| 430 |
+
"Confidence ≥ 93% on all criteria"]),
|
| 431 |
+
("Bidder B\nBuildRight Enterprises", C_RED, "NOT ELIGIBLE",
|
| 432 |
+
colors.HexColor("#FEE2E2"),
|
| 433 |
+
["C1 Turnover: INR 1.5 Cr — FAIL",
|
| 434 |
+
"Below required minimum of INR 5 Cr",
|
| 435 |
+
"C2–C4: All pass",
|
| 436 |
+
"Disqualified at high confidence (95%)"]),
|
| 437 |
+
("Bidder C\nShree Constructions & Services", C_AMB, "NEEDS REVIEW",
|
| 438 |
+
colors.HexColor("#FEF3C7"),
|
| 439 |
+
["C1 Turnover: Blurry scan submitted",
|
| 440 |
+
"Tesseract ~55% → Vision LLM: INR 5.4 Cr",
|
| 441 |
+
"Combined confidence 0.58 → safety rule",
|
| 442 |
+
"C2: Exactly 3 projects (borderline)",
|
| 443 |
+
"C3–C4: Pass"]),
|
| 444 |
+
]
|
| 445 |
+
cw = (W - 2 * M - 2 * cm) / 3
|
| 446 |
+
top = H * 0.76
|
| 447 |
+
for i, (name, color, verdict, vbg, bullets) in enumerate(bidders):
|
| 448 |
+
cx = M + i * (cw + 1 * cm)
|
| 449 |
+
cy = top - 3.5 * cm
|
| 450 |
+
card(c, cx, cy, cw, 3.5 * cm, color)
|
| 451 |
+
c.setFont("Helvetica-Bold", 12)
|
| 452 |
+
c.setFillColor(C_DARK)
|
| 453 |
+
for li, nl in enumerate(name.split("\n")):
|
| 454 |
+
c.drawString(cx + 0.35 * cm, cy + 3.2 * cm - li * 0.4 * cm, nl)
|
| 455 |
+
# verdict chip
|
| 456 |
+
c.setFillColor(vbg)
|
| 457 |
+
c.setStrokeColor(color)
|
| 458 |
+
c.setLineWidth(1)
|
| 459 |
+
c.roundRect(cx + 0.3 * cm, cy + 2.5 * cm, cw - 0.6 * cm, 0.5 * cm, 3, fill=1, stroke=1)
|
| 460 |
+
c.setFont("Helvetica-Bold", 11)
|
| 461 |
+
c.setFillColor(color)
|
| 462 |
+
c.drawCentredString(cx + cw / 2, cy + 2.65 * cm, verdict)
|
| 463 |
+
c.setFont("Helvetica", 10)
|
| 464 |
+
c.setFillColor(C_DARK)
|
| 465 |
+
for j, b in enumerate(bullets):
|
| 466 |
+
c.drawString(cx + 0.35 * cm, cy + 2.15 * cm - j * 0.42 * cm, b)
|
| 467 |
+
|
| 468 |
+
# Metric strip
|
| 469 |
+
metrics = [
|
| 470 |
+
("Criteria extracted", "5"),
|
| 471 |
+
("Bidder docs processed", "15"),
|
| 472 |
+
("LLM evaluation calls", "15"),
|
| 473 |
+
("Vision OCR invocations", "1"),
|
| 474 |
+
("Human review items", "1"),
|
| 475 |
+
("Total audit entries", "20+"),
|
| 476 |
+
]
|
| 477 |
+
sy = H * 0.18
|
| 478 |
+
grad_rect(c, M, sy, W - 2 * M, 1.5 * cm, C_BLU1, C_BLU2, steps=30)
|
| 479 |
+
mw = (W - 2 * M) / len(metrics)
|
| 480 |
+
for i, (lbl, val) in enumerate(metrics):
|
| 481 |
+
mx = M + i * mw + mw / 2
|
| 482 |
+
c.setFont("Helvetica-Bold", 20)
|
| 483 |
+
c.setFillColor(C_WHITE)
|
| 484 |
+
c.drawCentredString(mx, sy + 0.85 * cm, val)
|
| 485 |
+
c.setFont("Helvetica", 9)
|
| 486 |
+
c.setFillColor(colors.HexColor("#BFDBFE"))
|
| 487 |
+
c.drawCentredString(mx, sy + 0.3 * cm, lbl)
|
| 488 |
+
|
| 489 |
+
page_num(c, 7)
|
| 490 |
+
c.showPage()
|
| 491 |
+
|
| 492 |
+
|
| 493 |
+
# ── Slide 8 ─────────────────────────────────────────────────────────────────
|
| 494 |
+
def s8(c):
|
| 495 |
+
c.setFillColor(colors.white)
|
| 496 |
+
c.rect(0, 0, W, H, fill=1, stroke=0)
|
| 497 |
+
header_band(c, "Stack, Impact & What's Next", 8)
|
| 498 |
+
|
| 499 |
+
stack = [
|
| 500 |
+
("UI & orchestration", "Streamlit 1.39"),
|
| 501 |
+
("LLM", "DeepSeek API (OpenAI-compatible)"),
|
| 502 |
+
("OCR Tier 1", "PyMuPDF 1.24"),
|
| 503 |
+
("OCR Tier 2", "Tesseract"),
|
| 504 |
+
("OCR Tier 3", "DeepSeek Vision LLM"),
|
| 505 |
+
("Semantic retrieval", "sentence-transformers all-MiniLM-L6-v2"),
|
| 506 |
+
("Data validation", "Pydantic v2"),
|
| 507 |
+
("Audit log", "SQLite"),
|
| 508 |
+
("Deployment", "Streamlit Cloud / HuggingFace Spaces"),
|
| 509 |
+
]
|
| 510 |
+
lx = M
|
| 511 |
+
row_h = 0.5 * cm
|
| 512 |
+
top = H * 0.76
|
| 513 |
+
# Header
|
| 514 |
+
grad_rect(c, lx, top - row_h, 12.5 * cm, row_h, C_BLU1, C_BLU2, steps=20)
|
| 515 |
+
c.setFont("Helvetica-Bold", 11)
|
| 516 |
+
c.setFillColor(C_WHITE)
|
| 517 |
+
c.drawString(lx + 0.2 * cm, top - 0.38 * cm, "Component")
|
| 518 |
+
c.drawString(lx + 5.5 * cm, top - 0.38 * cm, "Technology")
|
| 519 |
+
for i, (comp, tech) in enumerate(stack):
|
| 520 |
+
ry = top - row_h - (i + 1) * row_h
|
| 521 |
+
bg_c = colors.white if i % 2 == 0 else colors.HexColor("#EFF6FF")
|
| 522 |
+
c.setFillColor(bg_c)
|
| 523 |
+
c.rect(lx, ry, 12.5 * cm, row_h, fill=1, stroke=0)
|
| 524 |
+
c.setFont("Helvetica", 10)
|
| 525 |
+
c.setFillColor(C_GRY)
|
| 526 |
+
c.drawString(lx + 0.2 * cm, ry + 0.13 * cm, comp)
|
| 527 |
+
c.setFillColor(C_DARK)
|
| 528 |
+
c.drawString(lx + 5.5 * cm, ry + 0.13 * cm, tech)
|
| 529 |
+
|
| 530 |
+
# Future work
|
| 531 |
+
fx = W - M - 11 * cm
|
| 532 |
+
c.setFont("Helvetica-Bold", 13)
|
| 533 |
+
c.setFillColor(C_BLU2)
|
| 534 |
+
c.drawString(fx, top - 0.4 * cm, "What's Next")
|
| 535 |
+
future = [
|
| 536 |
+
"Multi-tender workspace — same bidder pool, multiple tenders",
|
| 537 |
+
"GeM portal API integration — live tender ingestion",
|
| 538 |
+
"Automated bidder ranking with weighted scoring",
|
| 539 |
+
"LayoutLM for complex financial tables in scanned statements",
|
| 540 |
+
"Multi-evaluator workflow with role-based approval",
|
| 541 |
+
"Review queue email/SMS notifications",
|
| 542 |
+
"Audit PDF export for procurement oversight submissions",
|
| 543 |
+
]
|
| 544 |
+
c.setFont("Helvetica", 11)
|
| 545 |
+
c.setFillColor(C_DARK)
|
| 546 |
+
for i, f in enumerate(future):
|
| 547 |
+
c.drawString(fx, top - 0.9 * cm - i * 0.52 * cm, f"• {f}")
|
| 548 |
+
|
| 549 |
+
# Impact
|
| 550 |
+
iy = H * 0.19
|
| 551 |
+
grad_rect(c, M, iy, W - 2 * M, 1.2 * cm, C_BLU1, C_BLU2, steps=30)
|
| 552 |
+
c.setFont("Helvetica-Bold", 13)
|
| 553 |
+
c.setFillColor(C_WHITE)
|
| 554 |
+
c.drawCentredString(W / 2, iy + 0.65 * cm,
|
| 555 |
+
"3–5 days → minutes. Every verdict traceable to a document, page, and model version.")
|
| 556 |
+
c.setFont("Helvetica", 11)
|
| 557 |
+
c.drawCentredString(W / 2, iy + 0.28 * cm,
|
| 558 |
+
"Built in one hackathon session. Deployable today.")
|
| 559 |
+
page_num(c, 8)
|
| 560 |
+
c.showPage()
|
| 561 |
+
|
| 562 |
+
|
| 563 |
+
def main():
|
| 564 |
+
os.makedirs("deck", exist_ok=True)
|
| 565 |
+
out = "deck/TenderIQ_v4_modern_gradient.pdf"
|
| 566 |
+
c = Canvas(out, pagesize=landscape(A4))
|
| 567 |
+
s1(c); s2(c); s3(c); s4(c)
|
| 568 |
+
s5(c); s6(c); s7(c); s8(c)
|
| 569 |
+
c.save()
|
| 570 |
+
size = os.path.getsize(out)
|
| 571 |
+
print(f"OK: Saved {out} ({size:,} bytes, 8 slides)")
|
| 572 |
+
|
| 573 |
+
|
| 574 |
+
if __name__ == "__main__":
|
| 575 |
+
main()
|
scripts/make_v5.py
ADDED
|
@@ -0,0 +1,552 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""TenderIQ v5 — Data Forward (PPTX with embedded matplotlib charts)"""
|
| 3 |
+
import os
|
| 4 |
+
import io
|
| 5 |
+
import matplotlib
|
| 6 |
+
matplotlib.use("Agg")
|
| 7 |
+
import matplotlib.pyplot as plt
|
| 8 |
+
import matplotlib.patches as mpatches
|
| 9 |
+
import numpy as np
|
| 10 |
+
from pptx import Presentation
|
| 11 |
+
from pptx.util import Inches, Pt
|
| 12 |
+
from pptx.dml.color import RGBColor
|
| 13 |
+
from pptx.enum.text import PP_ALIGN
|
| 14 |
+
|
| 15 |
+
# Palette
|
| 16 |
+
BG = RGBColor(0xFA, 0xFA, 0xFA)
|
| 17 |
+
PRI = RGBColor(0x1E, 0x29, 0x3B)
|
| 18 |
+
ACC = RGBColor(0x63, 0x66, 0xF1) # indigo
|
| 19 |
+
TXT = RGBColor(0x33, 0x41, 0x55)
|
| 20 |
+
TXT2 = RGBColor(0x64, 0x74, 0x8B)
|
| 21 |
+
GRD = RGBColor(0xE2, 0xE8, 0xF0)
|
| 22 |
+
GRN = RGBColor(0x22, 0xC5, 0x5E)
|
| 23 |
+
RED_C = RGBColor(0xEF, 0x44, 0x44)
|
| 24 |
+
AMB = RGBColor(0xF5, 0x9E, 0x0B)
|
| 25 |
+
BLU = RGBColor(0x3B, 0x82, 0xF6)
|
| 26 |
+
PUR = RGBColor(0x8B, 0x5C, 0xF6)
|
| 27 |
+
FONT = "Calibri"
|
| 28 |
+
|
| 29 |
+
HEX = {
|
| 30 |
+
"bg": "#FAFAFA",
|
| 31 |
+
"pri": "#1E293B",
|
| 32 |
+
"acc": "#6366F1",
|
| 33 |
+
"txt": "#334155",
|
| 34 |
+
"txt2": "#64748B",
|
| 35 |
+
"grd": "#E2E8F0",
|
| 36 |
+
"grn": "#22C55E",
|
| 37 |
+
"red": "#EF4444",
|
| 38 |
+
"amb": "#F59E0B",
|
| 39 |
+
"blu": "#3B82F6",
|
| 40 |
+
"pur": "#8B5CF6",
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def new_prs():
|
| 45 |
+
prs = Presentation()
|
| 46 |
+
prs.slide_width = Inches(13.33)
|
| 47 |
+
prs.slide_height = Inches(7.5)
|
| 48 |
+
return prs
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def blank(prs):
|
| 52 |
+
return prs.slides.add_slide(prs.slide_layouts[6])
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def rect(slide, x, y, w, h, fill, line=None, lw=Pt(1)):
|
| 56 |
+
s = slide.shapes.add_shape(1, x, y, w, h)
|
| 57 |
+
s.fill.solid()
|
| 58 |
+
s.fill.fore_color.rgb = fill
|
| 59 |
+
if line:
|
| 60 |
+
s.line.color.rgb = line
|
| 61 |
+
s.line.width = lw
|
| 62 |
+
else:
|
| 63 |
+
s.line.fill.background()
|
| 64 |
+
return s
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def txt(slide, text, x, y, w, h, sz=14, bold=False, clr=None, align=PP_ALIGN.LEFT):
|
| 68 |
+
tb = slide.shapes.add_textbox(x, y, w, h)
|
| 69 |
+
tf = tb.text_frame
|
| 70 |
+
tf.word_wrap = True
|
| 71 |
+
p = tf.paragraphs[0]
|
| 72 |
+
p.alignment = align
|
| 73 |
+
r = p.add_run()
|
| 74 |
+
r.text = text
|
| 75 |
+
r.font.size = Pt(sz)
|
| 76 |
+
r.font.bold = bold
|
| 77 |
+
r.font.name = FONT
|
| 78 |
+
if clr:
|
| 79 |
+
r.font.color.rgb = clr
|
| 80 |
+
return tb
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def bg(slide):
|
| 84 |
+
rect(slide, 0, 0, Inches(13.33), Inches(7.5), BG)
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def heading(slide, text, y=Inches(0.3)):
|
| 88 |
+
rect(slide, Inches(0.6), y + Inches(0.08), Inches(0.06), Inches(0.44), ACC)
|
| 89 |
+
txt(slide, text, Inches(0.8), y, Inches(12.3), Inches(0.6),
|
| 90 |
+
sz=24, bold=True, clr=ACC)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def png_to_slide(slide, fig, x, y, w, h):
|
| 94 |
+
buf = io.BytesIO()
|
| 95 |
+
fig.savefig(buf, format="png", dpi=150, bbox_inches="tight",
|
| 96 |
+
facecolor=HEX["bg"], transparent=True)
|
| 97 |
+
plt.close(fig)
|
| 98 |
+
buf.seek(0)
|
| 99 |
+
slide.shapes.add_picture(buf, x, y, w, h)
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def mpl_style():
|
| 103 |
+
plt.rcParams.update({
|
| 104 |
+
"axes.facecolor": HEX["bg"],
|
| 105 |
+
"figure.facecolor": HEX["bg"],
|
| 106 |
+
"axes.edgecolor": HEX["grd"],
|
| 107 |
+
"axes.grid": True,
|
| 108 |
+
"grid.color": HEX["grd"],
|
| 109 |
+
"grid.linewidth": 0.7,
|
| 110 |
+
"text.color": HEX["pri"],
|
| 111 |
+
"xtick.color": HEX["txt2"],
|
| 112 |
+
"ytick.color": HEX["txt2"],
|
| 113 |
+
"font.family": "DejaVu Sans",
|
| 114 |
+
})
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
# ── Slide 1 ─────────────────────────────────────────────────────────────────
|
| 118 |
+
def s1(prs):
|
| 119 |
+
slide = blank(prs)
|
| 120 |
+
bg(slide)
|
| 121 |
+
rect(slide, 0, 0, Inches(0.5), Inches(7.5), ACC)
|
| 122 |
+
txt(slide, "TenderIQ", Inches(0.7), Inches(1.8), Inches(11.5), Inches(1.5),
|
| 123 |
+
sz=64, bold=True, clr=PRI, align=PP_ALIGN.LEFT)
|
| 124 |
+
txt(slide, "Explainable AI for Government Tender Evaluation",
|
| 125 |
+
Inches(0.7), Inches(3.3), Inches(11.5), Inches(0.65),
|
| 126 |
+
sz=22, clr=ACC)
|
| 127 |
+
rect(slide, Inches(0.7), Inches(4.1), Inches(8), Inches(0.05), GRD)
|
| 128 |
+
txt(slide, "CRPF Hackathon · Theme 3",
|
| 129 |
+
Inches(0.7), Inches(4.3), Inches(8), Inches(0.5),
|
| 130 |
+
sz=16, clr=TXT2)
|
| 131 |
+
txt(slide, "From days to minutes. Every decision traceable.",
|
| 132 |
+
Inches(0.7), Inches(4.95), Inches(9), Inches(0.5),
|
| 133 |
+
sz=15, clr=TXT)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
# ── Slide 2 ─────────────────────────────────────────────────────────────────
|
| 137 |
+
def s2(prs):
|
| 138 |
+
slide = blank(prs)
|
| 139 |
+
bg(slide)
|
| 140 |
+
heading(slide, "The Problem with Manual Tender Evaluation")
|
| 141 |
+
mpl_style()
|
| 142 |
+
fig, ax = plt.subplots(figsize=(5.5, 3.8))
|
| 143 |
+
categories = ["Manual\nProcess", "TenderIQ"]
|
| 144 |
+
values = [4.0, 0.02]
|
| 145 |
+
colors_ = [HEX["red"], HEX["grn"]]
|
| 146 |
+
bars = ax.bar(categories, values, color=colors_, width=0.45, zorder=3)
|
| 147 |
+
ax.set_ylabel("Days per tender", color=HEX["txt2"], fontsize=11)
|
| 148 |
+
ax.set_ylim(0, 5.5)
|
| 149 |
+
ax.set_title("Evaluation time comparison", color=HEX["pri"], fontsize=12, pad=8)
|
| 150 |
+
for bar, val in zip(bars, values):
|
| 151 |
+
label = f"{val} days" if val >= 0.1 else "Minutes"
|
| 152 |
+
ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.1,
|
| 153 |
+
label, ha="center", va="bottom", fontsize=12,
|
| 154 |
+
fontweight="bold", color=HEX["pri"])
|
| 155 |
+
ax.spines["top"].set_visible(False)
|
| 156 |
+
ax.spines["right"].set_visible(False)
|
| 157 |
+
ax.tick_params(axis="x", labelsize=12)
|
| 158 |
+
png_to_slide(slide, fig, Inches(0.6), Inches(1.0), Inches(5.8), Inches(4.5))
|
| 159 |
+
|
| 160 |
+
bullets = [
|
| 161 |
+
("3–5 Days", "per tender evaluation by committee"),
|
| 162 |
+
("Inconsistent", "two evaluators, two conclusions"),
|
| 163 |
+
("No Audit Trail", "decisions made untraceably"),
|
| 164 |
+
]
|
| 165 |
+
for i, (big, sub) in enumerate(bullets):
|
| 166 |
+
rect(slide, Inches(7.3), Inches(1.2) + Inches(1.55 * i),
|
| 167 |
+
Inches(5.5), Inches(1.3), RGBColor(0xF1, 0xF5, 0xF9), GRD, Pt(1))
|
| 168 |
+
txt(slide, big, Inches(7.5), Inches(1.3) + Inches(1.55 * i),
|
| 169 |
+
Inches(5.0), Inches(0.55), sz=22, bold=True, clr=ACC)
|
| 170 |
+
txt(slide, sub, Inches(7.5), Inches(1.85) + Inches(1.55 * i),
|
| 171 |
+
Inches(5.0), Inches(0.45), sz=13, clr=TXT)
|
| 172 |
+
txt(slide, ("• Mixed document formats: typed PDFs, scans, phone photographs\n"
|
| 173 |
+
"• Government procurement worth ₹50 lakh crore+ annually in India"),
|
| 174 |
+
Inches(7.3), Inches(6.0), Inches(5.5), Inches(0.9), sz=12, clr=TXT2)
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
# ── Slide 3 ─────────────────────────────────────────────────────────────────
|
| 178 |
+
def s3(prs):
|
| 179 |
+
slide = blank(prs)
|
| 180 |
+
bg(slide)
|
| 181 |
+
heading(slide, "TenderIQ — Four Stages, End to End")
|
| 182 |
+
stages = [
|
| 183 |
+
("1", "Extract",
|
| 184 |
+
"DeepSeek LLM reads tender PDF → structured JSON criteria"),
|
| 185 |
+
("2", "OCR & Index",
|
| 186 |
+
"3-tier pipeline handles any format → vector index"),
|
| 187 |
+
("3", "Evaluate",
|
| 188 |
+
"Vector search + LLM verdict + safety rule"),
|
| 189 |
+
("4", "Review & Audit",
|
| 190 |
+
"Human review queue + full exportable audit trail"),
|
| 191 |
+
]
|
| 192 |
+
cw = Inches(2.8)
|
| 193 |
+
for i, (num, title, body) in enumerate(stages):
|
| 194 |
+
sx = Inches(0.6) + i * Inches(3.18)
|
| 195 |
+
rect(slide, sx, Inches(1.1), cw, Inches(3.5), RGBColor(0xF1, 0xF5, 0xF9), GRD, Pt(1))
|
| 196 |
+
# Number circle (drawn as square for simplicity)
|
| 197 |
+
rect(slide, sx + Inches(0.15), Inches(1.25), Inches(0.65), Inches(0.65),
|
| 198 |
+
ACC)
|
| 199 |
+
txt(slide, num, sx + Inches(0.15), Inches(1.25), Inches(0.65), Inches(0.65),
|
| 200 |
+
sz=22, bold=True, clr=RGBColor(0xFF, 0xFF, 0xFF), align=PP_ALIGN.CENTER)
|
| 201 |
+
txt(slide, title, sx + Inches(0.9), Inches(1.3), cw - Inches(1.05), Inches(0.5),
|
| 202 |
+
sz=17, bold=True, clr=PRI)
|
| 203 |
+
txt(slide, body, sx + Inches(0.15), Inches(2.0), cw - Inches(0.3), Inches(2.4),
|
| 204 |
+
sz=13, clr=TXT)
|
| 205 |
+
# Connector arrow (not last)
|
| 206 |
+
if i < 3:
|
| 207 |
+
txt(slide, "→", sx + cw + Inches(0.05), Inches(2.5), Inches(0.3), Inches(0.5),
|
| 208 |
+
sz=22, bold=True, clr=ACC, align=PP_ALIGN.CENTER)
|
| 209 |
+
|
| 210 |
+
rect(slide, Inches(0.6), Inches(5.0), Inches(12.13), Inches(0.85),
|
| 211 |
+
RGBColor(0xEE, 0xF2, 0xFF), ACC, Pt(1))
|
| 212 |
+
txt(slide, '"Minutes, not days. Every verdict traceable to a document and page."',
|
| 213 |
+
Inches(0.8), Inches(5.12), Inches(11.9), Inches(0.65),
|
| 214 |
+
sz=15, bold=True, clr=ACC, align=PP_ALIGN.CENTER)
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
# ── Slide 4 ─────────────────────────────────────────────────────────────────
|
| 218 |
+
def s4(prs):
|
| 219 |
+
slide = blank(prs)
|
| 220 |
+
bg(slide)
|
| 221 |
+
heading(slide, "System Architecture")
|
| 222 |
+
|
| 223 |
+
bw, bh = Inches(2.5), Inches(0.52)
|
| 224 |
+
|
| 225 |
+
def abox(x, y, label, clr=RGBColor(0xF1, 0xF5, 0xF9)):
|
| 226 |
+
rect(slide, x, y, bw, bh, clr, GRD, Pt(1))
|
| 227 |
+
txt(slide, label, x + Inches(0.1), y + Inches(0.06),
|
| 228 |
+
bw - Inches(0.2), bh - Inches(0.1), sz=12, clr=TXT, align=PP_ALIGN.CENTER)
|
| 229 |
+
|
| 230 |
+
def arr(x, y):
|
| 231 |
+
rect(slide, x + Inches(1.15), y, Inches(0.2), Inches(0.28), ACC)
|
| 232 |
+
|
| 233 |
+
lx = Inches(0.6)
|
| 234 |
+
abox(lx, Inches(1.0), "Tender PDF")
|
| 235 |
+
arr(lx, Inches(1.52))
|
| 236 |
+
abox(lx, Inches(1.8), "DeepSeek LLM\n(Extract Criteria)")
|
| 237 |
+
arr(lx, Inches(2.32))
|
| 238 |
+
abox(lx, Inches(2.6), "Criteria JSON\n(C1–C5 structured)")
|
| 239 |
+
|
| 240 |
+
rx = Inches(4.0)
|
| 241 |
+
abox(rx, Inches(1.0), "Bidder Docs\n(PDFs · scans · photos)")
|
| 242 |
+
arr(rx, Inches(1.52))
|
| 243 |
+
abox(rx, Inches(1.8), "3-Tier OCR\n① PyMuPDF ② Tesseract ③ Vision LLM")
|
| 244 |
+
arr(rx, Inches(2.32))
|
| 245 |
+
abox(rx, Inches(2.6), "Vector Index\n(all-MiniLM-L6-v2)")
|
| 246 |
+
|
| 247 |
+
cx = Inches(3.15)
|
| 248 |
+
arr(cx, Inches(3.12))
|
| 249 |
+
abox(Inches(2.1), Inches(3.4), "DeepSeek LLM — Evaluate each criterion",
|
| 250 |
+
RGBColor(0xEE, 0xF2, 0xFF))
|
| 251 |
+
|
| 252 |
+
rect(slide, Inches(0.9), Inches(4.15), Inches(2.0), Inches(0.52),
|
| 253 |
+
RGBColor(0xDC, 0xFC, 0xE7), GRN, Pt(1))
|
| 254 |
+
txt(slide, "eligible / not_eligible", Inches(0.9), Inches(4.15), Inches(2.0), Inches(0.52),
|
| 255 |
+
sz=11, clr=GRN, align=PP_ALIGN.CENTER)
|
| 256 |
+
|
| 257 |
+
rect(slide, Inches(3.5), Inches(4.15), Inches(2.2), Inches(0.52),
|
| 258 |
+
RGBColor(0xFE, 0xF3, 0xC7), AMB, Pt(1))
|
| 259 |
+
txt(slide, "needs_review\nHuman Review Queue", Inches(3.5), Inches(4.15), Inches(2.2), Inches(0.52),
|
| 260 |
+
sz=11, clr=AMB, align=PP_ALIGN.CENTER)
|
| 261 |
+
|
| 262 |
+
abox(Inches(2.1), Inches(5.0), "SQLite Audit Log")
|
| 263 |
+
|
| 264 |
+
rect(slide, Inches(7.2), Inches(1.0), Inches(5.7), Inches(5.0),
|
| 265 |
+
RGBColor(0xF8, 0xF9, 0xFF), ACC, Pt(1))
|
| 266 |
+
txt(slide, "Key Technical Facts", Inches(7.35), Inches(1.1), Inches(5.4), Inches(0.4),
|
| 267 |
+
sz=14, bold=True, clr=ACC)
|
| 268 |
+
facts = [
|
| 269 |
+
"Single-process Streamlit app — no separate backend",
|
| 270 |
+
"Deployable to Streamlit Cloud or HuggingFace Spaces",
|
| 271 |
+
"All storage is local: SQLite + in-memory vector index",
|
| 272 |
+
"Only external dependency: DeepSeek API",
|
| 273 |
+
]
|
| 274 |
+
for i, f in enumerate(facts):
|
| 275 |
+
txt(slide, f"• {f}", Inches(7.35), Inches(1.65) + Inches(0.65 * i),
|
| 276 |
+
Inches(5.4), Inches(0.55), sz=13, clr=TXT)
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
# ── Slide 5 ─────────────────────────────────────────────────────────────────
|
| 280 |
+
def s5(prs):
|
| 281 |
+
slide = blank(prs)
|
| 282 |
+
bg(slide)
|
| 283 |
+
heading(slide, "Three-Tier OCR — Handling Any Document Format")
|
| 284 |
+
mpl_style()
|
| 285 |
+
|
| 286 |
+
# OCR confidence bar chart
|
| 287 |
+
fig, ax = plt.subplots(figsize=(4.8, 3.2))
|
| 288 |
+
tiers = ["Tier 1\nPyMuPDF", "Tier 2\nTesseract", "Tier 3\nVision LLM"]
|
| 289 |
+
vals = [100, 60, 95]
|
| 290 |
+
colors_ = [HEX["blu"], HEX["pur"], HEX["amb"]]
|
| 291 |
+
bars = ax.bar(tiers, vals, color=colors_, width=0.5, zorder=3)
|
| 292 |
+
ax.set_ylabel("Confidence (%)", color=HEX["txt2"], fontsize=10)
|
| 293 |
+
ax.set_ylim(0, 110)
|
| 294 |
+
ax.set_title("OCR confidence by tier", color=HEX["pri"], fontsize=11, pad=6)
|
| 295 |
+
for bar, val in zip(bars, vals):
|
| 296 |
+
ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 1,
|
| 297 |
+
f"{val}%", ha="center", va="bottom", fontsize=11,
|
| 298 |
+
fontweight="bold", color=HEX["pri"])
|
| 299 |
+
ax.spines["top"].set_visible(False)
|
| 300 |
+
ax.spines["right"].set_visible(False)
|
| 301 |
+
png_to_slide(slide, fig, Inches(0.6), Inches(1.0), Inches(5.0), Inches(3.8))
|
| 302 |
+
|
| 303 |
+
tiers_info = [
|
| 304 |
+
("Tier 1 — PyMuPDF", "Trigger: Typed / digital PDF\nCost: Free, instant\nConf: 1.0 (lossless)\nLabel: Typed PDF",
|
| 305 |
+
RGBColor(0x3B, 0x82, 0xF6)),
|
| 306 |
+
("Tier 2 — Tesseract", "Trigger: Scanned PDF / image\nCost: Free, local\nConf: Mean per-word scores\nLabel: Tesseract",
|
| 307 |
+
RGBColor(0x8B, 0x5C, 0xF6)),
|
| 308 |
+
("Tier 3 — Vision LLM", "Trigger: Tesseract conf < 65%\nCost: One API call\nConf: 0.95\nLabel: Vision LLM",
|
| 309 |
+
AMB),
|
| 310 |
+
]
|
| 311 |
+
cw = Inches(2.55)
|
| 312 |
+
for i, (title, body, accent) in enumerate(tiers_info):
|
| 313 |
+
sx = Inches(6.1) + i * Inches(2.35)
|
| 314 |
+
rect(slide, sx, Inches(1.0), cw, Inches(3.5), RGBColor(0xF8, 0xF9, 0xFF), accent, Pt(2))
|
| 315 |
+
rect(slide, sx, Inches(1.0), cw, Inches(0.08), accent)
|
| 316 |
+
txt(slide, title, sx + Inches(0.12), Inches(1.12), cw - Inches(0.24), Inches(0.45),
|
| 317 |
+
sz=13, bold=True, clr=accent)
|
| 318 |
+
txt(slide, body, sx + Inches(0.12), Inches(1.62), cw - Inches(0.24), Inches(2.5),
|
| 319 |
+
sz=12, clr=TXT)
|
| 320 |
+
|
| 321 |
+
rect(slide, Inches(0.6), Inches(5.0), Inches(12.13), Inches(1.75),
|
| 322 |
+
RGBColor(0xEE, 0xF2, 0xFF), ACC, Pt(1))
|
| 323 |
+
txt(slide, "Demo Scenario",
|
| 324 |
+
Inches(0.8), Inches(5.1), Inches(8), Inches(0.35),
|
| 325 |
+
sz=13, bold=True, clr=ACC)
|
| 326 |
+
demo = ("Bidder C submits a blurry, rotated CA certificate scan. "
|
| 327 |
+
"Tesseract reads it at ~55% confidence. Vision LLM transcribes the turnover figure correctly. "
|
| 328 |
+
"Combined confidence = 0.58 → routed to human review. "
|
| 329 |
+
"Borderline evidence requires a human — this is intentional.")
|
| 330 |
+
txt(slide, demo, Inches(0.8), Inches(5.52), Inches(12.0), Inches(1.1), sz=13, clr=TXT)
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
# ── Slide 6 ─────────────────────────────────────────────────────────────────
|
| 334 |
+
def s6(prs):
|
| 335 |
+
slide = blank(prs)
|
| 336 |
+
bg(slide)
|
| 337 |
+
heading(slide, "Every Decision is Explainable and Auditable")
|
| 338 |
+
|
| 339 |
+
lx = Inches(0.6)
|
| 340 |
+
rect(slide, lx, Inches(1.1), Inches(5.85), Inches(4.0),
|
| 341 |
+
RGBColor(0xF8, 0xF9, 0xFF), ACC, Pt(1))
|
| 342 |
+
rect(slide, lx, Inches(1.1), Inches(0.06), Inches(4.0), ACC)
|
| 343 |
+
txt(slide, "Criterion-Level Verdicts", lx + Inches(0.2), Inches(1.2),
|
| 344 |
+
Inches(5.5), Inches(0.42), sz=15, bold=True, clr=ACC)
|
| 345 |
+
left_lines = [
|
| 346 |
+
"Each (bidder × criterion) pair shows:",
|
| 347 |
+
"• Which criterion was checked",
|
| 348 |
+
"• Which document and page provided evidence",
|
| 349 |
+
"• What value was extracted (e.g. 'INR 6.2 Cr')",
|
| 350 |
+
"• Which OCR tier read the document",
|
| 351 |
+
"• Combined confidence score (0–100%)",
|
| 352 |
+
"• Plain-English reason",
|
| 353 |
+
]
|
| 354 |
+
for i, line in enumerate(left_lines):
|
| 355 |
+
c2 = TXT2 if i == 0 else TXT
|
| 356 |
+
txt(slide, line, lx + Inches(0.2), Inches(1.68) + Inches(0.42 * i),
|
| 357 |
+
Inches(5.5), Inches(0.38), sz=13, clr=c2)
|
| 358 |
+
|
| 359 |
+
rx = Inches(6.98)
|
| 360 |
+
rect(slide, rx, Inches(1.1), Inches(5.85), Inches(4.0),
|
| 361 |
+
RGBColor(0xF8, 0xF9, 0xFF), ACC, Pt(1))
|
| 362 |
+
rect(slide, rx, Inches(1.1), Inches(0.06), Inches(4.0), ACC)
|
| 363 |
+
txt(slide, "Audit Trail", rx + Inches(0.2), Inches(1.2),
|
| 364 |
+
Inches(5.5), Inches(0.42), sz=15, bold=True, clr=ACC)
|
| 365 |
+
right_lines = [
|
| 366 |
+
"Every action logged with:",
|
| 367 |
+
"• UTC timestamp",
|
| 368 |
+
"• Action type: criteria_extracted / bidder_processed",
|
| 369 |
+
" criterion_evaluated / human_review_action",
|
| 370 |
+
" vision_ocr_invoked / precomputed_fallback_used",
|
| 371 |
+
"• Model version & Actor (system / officer)",
|
| 372 |
+
"• Full payload JSON — Exportable as CSV",
|
| 373 |
+
]
|
| 374 |
+
for i, line in enumerate(right_lines):
|
| 375 |
+
c2 = TXT2 if i == 0 else TXT
|
| 376 |
+
txt(slide, line, rx + Inches(0.2), Inches(1.68) + Inches(0.42 * i),
|
| 377 |
+
Inches(5.5), Inches(0.38), sz=13, clr=c2)
|
| 378 |
+
|
| 379 |
+
rect(slide, Inches(0.6), Inches(5.38), Inches(12.13), Inches(1.22),
|
| 380 |
+
RGBColor(0xFE, 0xF3, 0xC7), AMB, Pt(2))
|
| 381 |
+
txt(slide, "The Safety Rule:",
|
| 382 |
+
Inches(0.8), Inches(5.45), Inches(5), Inches(0.38),
|
| 383 |
+
sz=15, bold=True, clr=AMB)
|
| 384 |
+
txt(slide, ("If combined confidence is 0.55–0.80 AND verdict is not_eligible, "
|
| 385 |
+
"the verdict is automatically downgraded to needs_review. "
|
| 386 |
+
"A bidder is NEVER silently disqualified at medium confidence."),
|
| 387 |
+
Inches(0.8), Inches(5.88), Inches(12), Inches(0.65),
|
| 388 |
+
sz=13, clr=TXT)
|
| 389 |
+
|
| 390 |
+
|
| 391 |
+
# ── Slide 7 ─────────────────────────────────────────────────────────────────
|
| 392 |
+
def s7(prs):
|
| 393 |
+
slide = blank(prs)
|
| 394 |
+
bg(slide)
|
| 395 |
+
heading(slide, "Demo: Three Bidders, Three Outcomes")
|
| 396 |
+
mpl_style()
|
| 397 |
+
|
| 398 |
+
# Verdicts matrix: 3 bidders × 5 criteria
|
| 399 |
+
criteria = ["C1\nTurnover", "C2\nProjects", "C3\nGST", "C4\nISO", "C5\nExperience"]
|
| 400 |
+
bidders = ["Bidder A\n(Apex)", "Bidder B\n(BuildRight)", "Bidder C\n(Shree)"]
|
| 401 |
+
verdicts = [
|
| 402 |
+
["E", "E", "E", "E", "E"],
|
| 403 |
+
["N", "E", "E", "E", "E"],
|
| 404 |
+
["R", "R", "E", "E", "E"],
|
| 405 |
+
]
|
| 406 |
+
color_map = {"E": HEX["grn"], "N": HEX["red"], "R": HEX["amb"]}
|
| 407 |
+
label_map = {"E": "E", "N": "N", "R": "R"}
|
| 408 |
+
|
| 409 |
+
fig, ax = plt.subplots(figsize=(5.2, 3.2))
|
| 410 |
+
for r, row in enumerate(verdicts):
|
| 411 |
+
for col, v in enumerate(row):
|
| 412 |
+
c_ = color_map[v]
|
| 413 |
+
ax.add_patch(mpatches.FancyBboxPatch(
|
| 414 |
+
(col + 0.05, r + 0.05), 0.9, 0.9,
|
| 415 |
+
boxstyle="round,pad=0.05", facecolor=c_, edgecolor="white", lw=2))
|
| 416 |
+
ax.text(col + 0.5, r + 0.5, label_map[v],
|
| 417 |
+
ha="center", va="center", fontsize=16,
|
| 418 |
+
fontweight="bold", color="white")
|
| 419 |
+
ax.set_xlim(0, 5)
|
| 420 |
+
ax.set_ylim(0, 3)
|
| 421 |
+
ax.set_xticks([i + 0.5 for i in range(5)])
|
| 422 |
+
ax.set_xticklabels(criteria, fontsize=9)
|
| 423 |
+
ax.set_yticks([i + 0.5 for i in range(3)])
|
| 424 |
+
ax.set_yticklabels(bidders, fontsize=9)
|
| 425 |
+
ax.set_title("Verdict Matrix (E=Eligible, N=Not Eligible, R=Review)",
|
| 426 |
+
color=HEX["pri"], fontsize=10, pad=6)
|
| 427 |
+
ax.grid(False)
|
| 428 |
+
ax.spines["top"].set_visible(False)
|
| 429 |
+
ax.spines["right"].set_visible(False)
|
| 430 |
+
ax.spines["bottom"].set_visible(False)
|
| 431 |
+
ax.spines["left"].set_visible(False)
|
| 432 |
+
png_to_slide(slide, fig, Inches(0.6), Inches(1.0), Inches(5.8), Inches(4.0))
|
| 433 |
+
|
| 434 |
+
bidders_info = [
|
| 435 |
+
("Bidder A — Apex Constructions", GRN, "ELIGIBLE",
|
| 436 |
+
["C1 INR 6.37 Cr avg — PASS",
|
| 437 |
+
"C2 5 completed projects — PASS",
|
| 438 |
+
"C3 GST active — PASS",
|
| 439 |
+
"C4 ISO 9001:2015 valid — PASS",
|
| 440 |
+
"Confidence ≥ 93% all criteria"]),
|
| 441 |
+
("Bidder B — BuildRight Enterprises", RED_C, "NOT ELIGIBLE",
|
| 442 |
+
["C1 INR 1.5 Cr avg — FAIL",
|
| 443 |
+
"Below required INR 5 Cr",
|
| 444 |
+
"C2–C4: All pass",
|
| 445 |
+
"Disqualified, high conf (95%)"]),
|
| 446 |
+
("Bidder C — Shree Constructions", AMB, "NEEDS REVIEW",
|
| 447 |
+
["C1 Blurry scan: Vision LLM",
|
| 448 |
+
"INR 5.4 Cr, conf 0.58 → review",
|
| 449 |
+
"C2: 3 projects (borderline)",
|
| 450 |
+
"C3–C4: Pass"]),
|
| 451 |
+
]
|
| 452 |
+
cw = Inches(2.2)
|
| 453 |
+
for i, (name, color, verdict, bullets) in enumerate(bidders_info):
|
| 454 |
+
sx = Inches(6.9) + i * Inches(2.15)
|
| 455 |
+
rect(slide, sx, Inches(1.0), cw, Inches(4.0),
|
| 456 |
+
RGBColor(0xF8, 0xF9, 0xFF), color, Pt(2))
|
| 457 |
+
rect(slide, sx, Inches(1.0), cw, Inches(0.08), color)
|
| 458 |
+
txt(slide, name, sx + Inches(0.1), Inches(1.1), cw - Inches(0.2), Inches(0.45),
|
| 459 |
+
sz=11, bold=True, clr=PRI)
|
| 460 |
+
rect(slide, sx + Inches(0.1), Inches(1.62), cw - Inches(0.2), Inches(0.32), color)
|
| 461 |
+
txt(slide, verdict, sx + Inches(0.1), Inches(1.62), cw - Inches(0.2), Inches(0.32),
|
| 462 |
+
sz=11, bold=True, clr=RGBColor(0xFF, 0xFF, 0xFF), align=PP_ALIGN.CENTER)
|
| 463 |
+
for j, b in enumerate(bullets):
|
| 464 |
+
txt(slide, b, sx + Inches(0.1), Inches(2.05) + Inches(0.38 * j),
|
| 465 |
+
cw - Inches(0.2), Inches(0.34), sz=11, clr=TXT)
|
| 466 |
+
|
| 467 |
+
# Metric strip
|
| 468 |
+
metrics = [
|
| 469 |
+
("Criteria extracted", "5"),
|
| 470 |
+
("Bidder docs processed", "15"),
|
| 471 |
+
("LLM evaluation calls", "15"),
|
| 472 |
+
("Vision OCR invocations", "1"),
|
| 473 |
+
("Human review items", "1"),
|
| 474 |
+
("Total audit entries", "20+"),
|
| 475 |
+
]
|
| 476 |
+
rect(slide, Inches(0.6), Inches(5.2), Inches(12.13), Inches(0.9),
|
| 477 |
+
RGBColor(0xEE, 0xF2, 0xFF), ACC, Pt(1))
|
| 478 |
+
mw = Inches(2.0)
|
| 479 |
+
for i, (lbl, val) in enumerate(metrics):
|
| 480 |
+
mx = Inches(0.7) + i * Inches(2.0)
|
| 481 |
+
txt(slide, val, mx, Inches(5.27), mw, Inches(0.38),
|
| 482 |
+
sz=20, bold=True, clr=ACC, align=PP_ALIGN.CENTER)
|
| 483 |
+
txt(slide, lbl, mx, Inches(5.7), mw, Inches(0.32),
|
| 484 |
+
sz=11, clr=TXT2, align=PP_ALIGN.CENTER)
|
| 485 |
+
|
| 486 |
+
|
| 487 |
+
# ── Slide 8 ─────────────────────────────────────────────────────────────────
|
| 488 |
+
def s8(prs):
|
| 489 |
+
slide = blank(prs)
|
| 490 |
+
bg(slide)
|
| 491 |
+
heading(slide, "Stack, Impact & What's Next")
|
| 492 |
+
stack = [
|
| 493 |
+
("UI & orchestration", "Streamlit 1.39"),
|
| 494 |
+
("LLM", "DeepSeek API (OpenAI-compatible)"),
|
| 495 |
+
("OCR Tier 1", "PyMuPDF 1.24"),
|
| 496 |
+
("OCR Tier 2", "Tesseract"),
|
| 497 |
+
("OCR Tier 3", "DeepSeek Vision LLM"),
|
| 498 |
+
("Semantic retrieval", "sentence-transformers all-MiniLM-L6-v2"),
|
| 499 |
+
("Data validation", "Pydantic v2"),
|
| 500 |
+
("Audit log", "SQLite"),
|
| 501 |
+
("Deployment", "Streamlit Cloud / HuggingFace Spaces"),
|
| 502 |
+
]
|
| 503 |
+
rect(slide, Inches(0.6), Inches(1.05), Inches(6.1), Inches(0.4),
|
| 504 |
+
RGBColor(0xEE, 0xF2, 0xFF), ACC, Pt(1))
|
| 505 |
+
txt(slide, "Component", Inches(0.7), Inches(1.1), Inches(2.7), Inches(0.3),
|
| 506 |
+
sz=13, bold=True, clr=ACC)
|
| 507 |
+
txt(slide, "Technology", Inches(3.55), Inches(1.1), Inches(2.9), Inches(0.3),
|
| 508 |
+
sz=13, bold=True, clr=ACC)
|
| 509 |
+
for i, (comp, tech) in enumerate(stack):
|
| 510 |
+
ry = Inches(1.45) + Inches(0.38 * i)
|
| 511 |
+
bg_r = RGBColor(0xF8, 0xF9, 0xFF) if i % 2 == 0 else BG
|
| 512 |
+
rect(slide, Inches(0.6), ry, Inches(6.1), Inches(0.38), bg_r)
|
| 513 |
+
txt(slide, comp, Inches(0.7), ry + Inches(0.06), Inches(2.7), Inches(0.3),
|
| 514 |
+
sz=12, clr=TXT2)
|
| 515 |
+
txt(slide, tech, Inches(3.55), ry + Inches(0.06), Inches(2.9), Inches(0.3),
|
| 516 |
+
sz=12, clr=TXT)
|
| 517 |
+
|
| 518 |
+
txt(slide, "What's Next", Inches(7.3), Inches(1.05), Inches(5.6), Inches(0.42),
|
| 519 |
+
sz=16, bold=True, clr=ACC)
|
| 520 |
+
future = [
|
| 521 |
+
"Multi-tender workspace — same bidder pool, multiple tenders",
|
| 522 |
+
"GeM portal API integration — live tender ingestion",
|
| 523 |
+
"Automated bidder ranking with weighted scoring",
|
| 524 |
+
"LayoutLM for complex financial tables in scanned statements",
|
| 525 |
+
"Multi-evaluator workflow with role-based approval",
|
| 526 |
+
"Review queue email/SMS notifications",
|
| 527 |
+
"Audit PDF export for procurement oversight submissions",
|
| 528 |
+
]
|
| 529 |
+
for i, f in enumerate(future):
|
| 530 |
+
txt(slide, f"• {f}", Inches(7.3), Inches(1.55) + Inches(0.47 * i),
|
| 531 |
+
Inches(5.8), Inches(0.42), sz=13, clr=TXT)
|
| 532 |
+
|
| 533 |
+
rect(slide, Inches(0.6), Inches(6.2), Inches(12.13), Inches(0.92),
|
| 534 |
+
RGBColor(0xEE, 0xF2, 0xFF), ACC, Pt(2))
|
| 535 |
+
txt(slide, ("3–5 days → minutes. Every verdict traceable to a document, page, and model version."
|
| 536 |
+
" Built in one hackathon session. Deployable today."),
|
| 537 |
+
Inches(0.8), Inches(6.3), Inches(11.9), Inches(0.75),
|
| 538 |
+
sz=14, bold=True, clr=ACC, align=PP_ALIGN.CENTER)
|
| 539 |
+
|
| 540 |
+
|
| 541 |
+
def main():
|
| 542 |
+
os.makedirs("deck", exist_ok=True)
|
| 543 |
+
prs = new_prs()
|
| 544 |
+
s1(prs); s2(prs); s3(prs); s4(prs)
|
| 545 |
+
s5(prs); s6(prs); s7(prs); s8(prs)
|
| 546 |
+
out = "deck/TenderIQ_v5_data_forward.pptx"
|
| 547 |
+
prs.save(out)
|
| 548 |
+
print(f"OK: Saved {out} ({len(prs.slides)} slides)")
|
| 549 |
+
|
| 550 |
+
|
| 551 |
+
if __name__ == "__main__":
|
| 552 |
+
main()
|
scripts/make_v6.py
ADDED
|
@@ -0,0 +1,547 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""TenderIQ v6 — Infographic (PDF via reportlab)"""
|
| 3 |
+
import os
|
| 4 |
+
import math
|
| 5 |
+
from reportlab.pdfgen.canvas import Canvas
|
| 6 |
+
from reportlab.lib.pagesizes import A4, landscape
|
| 7 |
+
from reportlab.lib import colors
|
| 8 |
+
from reportlab.lib.units import cm
|
| 9 |
+
from reportlab.platypus import Paragraph
|
| 10 |
+
from reportlab.lib.styles import ParagraphStyle
|
| 11 |
+
|
| 12 |
+
W, H = landscape(A4)
|
| 13 |
+
|
| 14 |
+
# Palette
|
| 15 |
+
C_BG = colors.white
|
| 16 |
+
C_STR = colors.HexColor("#F8FAFC")
|
| 17 |
+
C_BLU = colors.HexColor("#2563EB")
|
| 18 |
+
C_GRN = colors.HexColor("#22C55E")
|
| 19 |
+
C_RED = colors.HexColor("#EF4444")
|
| 20 |
+
C_AMB = colors.HexColor("#F59E0B")
|
| 21 |
+
C_PUR = colors.HexColor("#8B5CF6")
|
| 22 |
+
C_TXT = colors.HexColor("#0F172A")
|
| 23 |
+
C_SUB = colors.HexColor("#64748B")
|
| 24 |
+
C_DIV = colors.HexColor("#E2E8F0")
|
| 25 |
+
|
| 26 |
+
M = 1.8 * cm
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def para(c, text, x, y, width, font="Helvetica", size=12, color=C_TXT, leading=16, align="left"):
|
| 30 |
+
style = ParagraphStyle(
|
| 31 |
+
'p', fontName=font, fontSize=size, leading=leading, textColor=color,
|
| 32 |
+
alignment={"left": 0, "center": 1, "right": 2}.get(align, 0))
|
| 33 |
+
p = Paragraph(text.replace("\n", "<br/>"), style)
|
| 34 |
+
_, used = p.wrapOn(c, width, 1000)
|
| 35 |
+
p.drawOn(c, x, y - used)
|
| 36 |
+
return used
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def circ_progress(c, cx, cy, r, pct, fg, bg=C_DIV, thickness=8):
|
| 40 |
+
"""Draw a circular progress arc."""
|
| 41 |
+
c.setStrokeColor(bg)
|
| 42 |
+
c.setLineWidth(thickness)
|
| 43 |
+
c.circle(cx, cy, r, fill=0, stroke=1)
|
| 44 |
+
c.setStrokeColor(fg)
|
| 45 |
+
c.setLineWidth(thickness)
|
| 46 |
+
extent = 360 * pct
|
| 47 |
+
c.arc(cx - r, cy - r, cx + r, cy + r, startAng=90, extent=-extent)
|
| 48 |
+
c.setFillColor(fg)
|
| 49 |
+
c.setFont("Helvetica-Bold", int(r * 0.5))
|
| 50 |
+
c.drawCentredString(cx, cy - r * 0.18, f"{int(pct * 100)}%")
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def header_strip(c, title, n):
|
| 54 |
+
c.setFillColor(C_STR)
|
| 55 |
+
c.rect(0, H - 1.3 * cm, W, 1.3 * cm, fill=1, stroke=0)
|
| 56 |
+
c.setStrokeColor(C_BLU)
|
| 57 |
+
c.setLineWidth(2)
|
| 58 |
+
c.line(0, H - 1.3 * cm, W, H - 1.3 * cm)
|
| 59 |
+
c.setFont("Helvetica-Bold", 16)
|
| 60 |
+
c.setFillColor(C_BLU)
|
| 61 |
+
c.drawString(M, H - 1.0 * cm, title)
|
| 62 |
+
c.setFont("Helvetica", 10)
|
| 63 |
+
c.setFillColor(C_SUB)
|
| 64 |
+
c.drawRightString(W - M, H - 1.0 * cm, f"TenderIQ · CRPF Hackathon · {n} / 8")
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def footer_line(c):
|
| 68 |
+
c.setStrokeColor(C_DIV)
|
| 69 |
+
c.setLineWidth(0.5)
|
| 70 |
+
c.line(M, 0.9 * cm, W - M, 0.9 * cm)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def big_num(c, val, label, cx, cy, color=C_BLU):
|
| 74 |
+
c.setFont("Helvetica-Bold", 52)
|
| 75 |
+
c.setFillColor(color)
|
| 76 |
+
c.drawCentredString(cx, cy, val)
|
| 77 |
+
c.setFont("Helvetica", 11)
|
| 78 |
+
c.setFillColor(C_SUB)
|
| 79 |
+
# All-caps label
|
| 80 |
+
for i, line in enumerate(label.split("\n")):
|
| 81 |
+
c.drawCentredString(cx, cy - 1.3 * cm - i * 0.45 * cm, line.upper())
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def icon_label(c, icon, label, cx, cy, color=C_BLU, icon_sz=36, lbl_sz=11):
|
| 85 |
+
c.setFont("Helvetica-Bold", icon_sz)
|
| 86 |
+
c.setFillColor(color)
|
| 87 |
+
c.drawCentredString(cx, cy, icon)
|
| 88 |
+
c.setFont("Helvetica", lbl_sz)
|
| 89 |
+
c.setFillColor(C_TXT)
|
| 90 |
+
c.drawCentredString(cx, cy - 1.2 * cm, label)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def stage_row(c, icon, title, desc, y, color=C_BLU):
|
| 94 |
+
# Icon circle
|
| 95 |
+
c.setFillColor(color)
|
| 96 |
+
c.circle(M + 0.9 * cm, y + 0.45 * cm, 0.45 * cm, fill=1, stroke=0)
|
| 97 |
+
c.setFont("Helvetica-Bold", 14)
|
| 98 |
+
c.setFillColor(colors.white)
|
| 99 |
+
c.drawCentredString(M + 0.9 * cm, y + 0.3 * cm, icon)
|
| 100 |
+
c.setFont("Helvetica-Bold", 13)
|
| 101 |
+
c.setFillColor(color)
|
| 102 |
+
c.drawString(M + 1.8 * cm, y + 0.55 * cm, title)
|
| 103 |
+
c.setFont("Helvetica", 11)
|
| 104 |
+
c.setFillColor(C_TXT)
|
| 105 |
+
c.drawString(M + 1.8 * cm, y + 0.18 * cm, desc)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
# ── Slide 1 ─────────────────────────────────────────────────────────────────
|
| 109 |
+
def s1(c):
|
| 110 |
+
c.setFillColor(C_BG)
|
| 111 |
+
c.rect(0, 0, W, H, fill=1, stroke=0)
|
| 112 |
+
# Left accent bar
|
| 113 |
+
c.setFillColor(C_BLU)
|
| 114 |
+
c.rect(0, 0, 0.6 * cm, H, fill=1, stroke=0)
|
| 115 |
+
# Large icon
|
| 116 |
+
c.setFont("Helvetica-Bold", 80)
|
| 117 |
+
c.setFillColor(C_BLU)
|
| 118 |
+
c.drawCentredString(W / 2, H / 2 + 2.2 * cm, "TenderIQ")
|
| 119 |
+
# Divider
|
| 120 |
+
c.setStrokeColor(C_DIV)
|
| 121 |
+
c.setLineWidth(1.5)
|
| 122 |
+
c.line(W / 2 - 6 * cm, H / 2 + 1.3 * cm, W / 2 + 6 * cm, H / 2 + 1.3 * cm)
|
| 123 |
+
c.setFont("Helvetica", 20)
|
| 124 |
+
c.setFillColor(C_TXT)
|
| 125 |
+
c.drawCentredString(W / 2, H / 2 + 0.55 * cm,
|
| 126 |
+
"Explainable AI for Government Tender Evaluation")
|
| 127 |
+
c.setFont("Helvetica", 13)
|
| 128 |
+
c.setFillColor(C_SUB)
|
| 129 |
+
c.drawCentredString(W / 2, H / 2 - 0.4 * cm, "CRPF HACKATHON · THEME 3")
|
| 130 |
+
c.setFont("Helvetica-Oblique", 13)
|
| 131 |
+
c.setFillColor(C_BLU)
|
| 132 |
+
c.drawCentredString(W / 2, H / 2 - 1.3 * cm,
|
| 133 |
+
"From days to minutes. Every decision traceable.")
|
| 134 |
+
footer_line(c)
|
| 135 |
+
c.showPage()
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
# ── Slide 2 ─────────────────────────────────────────────────────────────────
|
| 139 |
+
def s2(c):
|
| 140 |
+
c.setFillColor(C_BG)
|
| 141 |
+
c.rect(0, 0, W, H, fill=1, stroke=0)
|
| 142 |
+
header_strip(c, "The Problem with Manual Tender Evaluation", 2)
|
| 143 |
+
cy = H * 0.5
|
| 144 |
+
third = (W - 2 * M) / 3
|
| 145 |
+
callouts = [
|
| 146 |
+
("3–5", "days per tender\nevaluation", C_RED),
|
| 147 |
+
("≠", "inconsistent verdicts\nfrom evaluators", C_AMB),
|
| 148 |
+
("?", "no audit trail\nfor decisions", C_PUR),
|
| 149 |
+
]
|
| 150 |
+
for i, (val, lbl, col) in enumerate(callouts):
|
| 151 |
+
cx = M + i * third + third / 2
|
| 152 |
+
big_num(c, val, lbl, cx, cy, col)
|
| 153 |
+
# Divider
|
| 154 |
+
c.setStrokeColor(C_DIV)
|
| 155 |
+
c.setLineWidth(1)
|
| 156 |
+
c.line(M, cy - 2.2 * cm, W - M, cy - 2.2 * cm)
|
| 157 |
+
bullets = [
|
| 158 |
+
"• Mixed document formats: typed PDFs, scanned certificates, phone photographs",
|
| 159 |
+
"• Government procurement worth ₹50 lakh crore+ annually in India",
|
| 160 |
+
"• Project execution delays traced directly to procurement bottlenecks",
|
| 161 |
+
]
|
| 162 |
+
c.setFont("Helvetica", 12)
|
| 163 |
+
c.setFillColor(C_SUB)
|
| 164 |
+
for i, b in enumerate(bullets):
|
| 165 |
+
c.drawString(M, cy - 2.8 * cm - i * 0.55 * cm, b)
|
| 166 |
+
footer_line(c)
|
| 167 |
+
c.showPage()
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
# ── Slide 3 ─────────────────────────────────────────────────────────────────
|
| 171 |
+
def s3(c):
|
| 172 |
+
c.setFillColor(C_BG)
|
| 173 |
+
c.rect(0, 0, W, H, fill=1, stroke=0)
|
| 174 |
+
header_strip(c, "TenderIQ — Four Stages, End to End", 3)
|
| 175 |
+
# Four icons in a row with arrows
|
| 176 |
+
icons = ["PDF", "OCR", "EVAL", "LOG"]
|
| 177 |
+
labels = ["Extract", "OCR & Index", "Evaluate", "Review & Audit"]
|
| 178 |
+
descs = [
|
| 179 |
+
"DeepSeek LLM reads tender PDF\n→ structured JSON criteria",
|
| 180 |
+
"3-tier pipeline handles any\ndoc format → vector index",
|
| 181 |
+
"Vector search + LLM verdict\n+ safety rule",
|
| 182 |
+
"Human review queue\n+ full audit trail (CSV)",
|
| 183 |
+
]
|
| 184 |
+
colors_ = [C_BLU, C_PUR, C_GRN, C_AMB]
|
| 185 |
+
step = (W - 2 * M) / 4
|
| 186 |
+
cy = H * 0.52
|
| 187 |
+
for i, (ico, lbl, desc, col) in enumerate(zip(icons, labels, descs, colors_)):
|
| 188 |
+
cx = M + i * step + step / 2
|
| 189 |
+
# Icon circle
|
| 190 |
+
c.setFillColor(col)
|
| 191 |
+
c.circle(cx, cy + 1.0 * cm, 0.9 * cm, fill=1, stroke=0)
|
| 192 |
+
c.setFont("Helvetica-Bold", 9)
|
| 193 |
+
c.setFillColor(colors.white)
|
| 194 |
+
c.drawCentredString(cx, cy + 0.88 * cm, ico)
|
| 195 |
+
c.setFont("Helvetica-Bold", 14)
|
| 196 |
+
c.setFillColor(col)
|
| 197 |
+
c.drawCentredString(cx, cy - 0.25 * cm, lbl)
|
| 198 |
+
c.setFont("Helvetica", 11)
|
| 199 |
+
c.setFillColor(C_TXT)
|
| 200 |
+
for j, dl in enumerate(desc.split("\n")):
|
| 201 |
+
c.drawCentredString(cx, cy - 0.82 * cm - j * 0.45 * cm, dl)
|
| 202 |
+
if i < 3:
|
| 203 |
+
c.setFont("Helvetica-Bold", 18)
|
| 204 |
+
c.setFillColor(C_SUB)
|
| 205 |
+
c.drawCentredString(M + (i + 1) * step, cy + 0.9 * cm, "→")
|
| 206 |
+
|
| 207 |
+
# Callout
|
| 208 |
+
by = H * 0.19
|
| 209 |
+
c.setFillColor(C_STR)
|
| 210 |
+
c.setStrokeColor(C_BLU)
|
| 211 |
+
c.setLineWidth(1.5)
|
| 212 |
+
c.roundRect(M, by, W - 2 * M, 1.1 * cm, 4, fill=1, stroke=1)
|
| 213 |
+
c.setFont("Helvetica-BoldOblique", 13)
|
| 214 |
+
c.setFillColor(C_BLU)
|
| 215 |
+
c.drawCentredString(W / 2, by + 0.38 * cm,
|
| 216 |
+
'"Minutes, not days. Every verdict traceable to a document and page."')
|
| 217 |
+
footer_line(c)
|
| 218 |
+
c.showPage()
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
# ── Slide 4 ─────────────────────────────────────────────────────────────────
|
| 222 |
+
def s4(c):
|
| 223 |
+
c.setFillColor(C_BG)
|
| 224 |
+
c.rect(0, 0, W, H, fill=1, stroke=0)
|
| 225 |
+
header_strip(c, "System Architecture", 4)
|
| 226 |
+
# Vertical flow infographic
|
| 227 |
+
stages = [
|
| 228 |
+
("PDF", "Tender PDF + Bidder Documents (PDFs · scans · photos)", C_BLU),
|
| 229 |
+
("LLM", "DeepSeek LLM — Extract Criteria | 3-Tier OCR → Vector Index", C_PUR),
|
| 230 |
+
("EVAL", "DeepSeek LLM — Evaluate each criterion with combined confidence", C_GRN),
|
| 231 |
+
("AUD", "eligible / not_eligible | needs_review → Human Review Queue", C_AMB),
|
| 232 |
+
("DB", "SQLite Audit Log — every action with timestamp + payload", C_BLU),
|
| 233 |
+
]
|
| 234 |
+
top_y = H - 2.2 * cm
|
| 235 |
+
row_h = 1.2 * cm
|
| 236 |
+
for i, (ico, desc, col) in enumerate(stages):
|
| 237 |
+
y = top_y - i * row_h
|
| 238 |
+
stage_row(c, ico, "", desc, y - 0.1 * cm, col)
|
| 239 |
+
if i < len(stages) - 1:
|
| 240 |
+
# Connector
|
| 241 |
+
c.setStrokeColor(C_DIV)
|
| 242 |
+
c.setLineWidth(1.5)
|
| 243 |
+
c.line(M + 0.9 * cm, y - 0.1 * cm, M + 0.9 * cm, y - row_h + 0.5 * cm)
|
| 244 |
+
|
| 245 |
+
# Key facts side panel
|
| 246 |
+
fx = W / 2 + 1 * cm
|
| 247 |
+
c.setFillColor(C_STR)
|
| 248 |
+
c.setStrokeColor(C_DIV)
|
| 249 |
+
c.setLineWidth(1)
|
| 250 |
+
c.roundRect(fx, H * 0.2, W - M - fx, H * 0.6, 5, fill=1, stroke=1)
|
| 251 |
+
c.setFont("Helvetica-Bold", 13)
|
| 252 |
+
c.setFillColor(C_BLU)
|
| 253 |
+
c.drawString(fx + 0.4 * cm, H * 0.76, "Key Technical Facts")
|
| 254 |
+
facts = [
|
| 255 |
+
"Single-process Streamlit app",
|
| 256 |
+
"Streamlit Cloud / HuggingFace deployment",
|
| 257 |
+
"Local storage: SQLite + vector index",
|
| 258 |
+
"Only external dep: DeepSeek API",
|
| 259 |
+
]
|
| 260 |
+
c.setFont("Helvetica", 11)
|
| 261 |
+
c.setFillColor(C_TXT)
|
| 262 |
+
for i, f in enumerate(facts):
|
| 263 |
+
c.drawString(fx + 0.4 * cm, H * 0.68 - i * 0.65 * cm, f"�� {f}")
|
| 264 |
+
footer_line(c)
|
| 265 |
+
c.showPage()
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
# ── Slide 5 ─────────────────────────────────────────────────────────────────
|
| 269 |
+
def s5(c):
|
| 270 |
+
c.setFillColor(C_BG)
|
| 271 |
+
c.rect(0, 0, W, H, fill=1, stroke=0)
|
| 272 |
+
header_strip(c, "Three-Tier OCR — Handling Any Document Format", 5)
|
| 273 |
+
tiers = [
|
| 274 |
+
("Tier 1\nPyMuPDF", "Typed PDF → Free, instant", 1.0, C_BLU),
|
| 275 |
+
("Tier 2\nTesseract", "Scan / image → Free, local", 0.60, C_PUR),
|
| 276 |
+
("Tier 3\nVision LLM", "Low confidence → API call", 0.95, C_AMB),
|
| 277 |
+
]
|
| 278 |
+
step = (W - 2 * M) / 3
|
| 279 |
+
cy = H * 0.55
|
| 280 |
+
for i, (tier, desc, pct, col) in enumerate(tiers):
|
| 281 |
+
cx = M + i * step + step / 2
|
| 282 |
+
# Circular progress
|
| 283 |
+
circ_progress(c, cx, cy + 0.5 * cm, 1.3 * cm, pct, col)
|
| 284 |
+
# Tier label
|
| 285 |
+
c.setFont("Helvetica-Bold", 13)
|
| 286 |
+
c.setFillColor(col)
|
| 287 |
+
for li, ln in enumerate(tier.split("\n")):
|
| 288 |
+
c.drawCentredString(cx, cy - 1.35 * cm - li * 0.45 * cm, ln)
|
| 289 |
+
c.setFont("Helvetica", 11)
|
| 290 |
+
c.setFillColor(C_TXT)
|
| 291 |
+
c.drawCentredString(cx, cy - 2.55 * cm, desc)
|
| 292 |
+
if i < 2:
|
| 293 |
+
c.setFont("Helvetica-Bold", 18)
|
| 294 |
+
c.setFillColor(C_SUB)
|
| 295 |
+
c.drawCentredString(M + (i + 1) * step, cy + 0.5 * cm, "→")
|
| 296 |
+
|
| 297 |
+
# Demo callout
|
| 298 |
+
dy = H * 0.19
|
| 299 |
+
c.setFillColor(C_STR)
|
| 300 |
+
c.setStrokeColor(C_AMB)
|
| 301 |
+
c.setLineWidth(2)
|
| 302 |
+
c.roundRect(M, dy, W - 2 * M, 2.2 * cm, 4, fill=1, stroke=1)
|
| 303 |
+
c.setFont("Helvetica-Bold", 12)
|
| 304 |
+
c.setFillColor(C_AMB)
|
| 305 |
+
c.drawString(M + 0.5 * cm, dy + 1.78 * cm, "Demo Scenario")
|
| 306 |
+
c.setFont("Helvetica", 11)
|
| 307 |
+
c.setFillColor(C_TXT)
|
| 308 |
+
demo = ("Bidder C submits a blurry, rotated CA certificate scan. "
|
| 309 |
+
"Tesseract reads it at ~55% confidence. Vision LLM transcribes the turnover correctly. "
|
| 310 |
+
"Combined confidence = 0.58 → routed to human review. "
|
| 311 |
+
"Borderline evidence requires a human — this is intentional.")
|
| 312 |
+
para(c, demo, M + 0.5 * cm, dy + 1.55 * cm, W - 2 * M - 1 * cm, size=11, color=C_TXT, leading=16)
|
| 313 |
+
footer_line(c)
|
| 314 |
+
c.showPage()
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
# ── Slide 6 ─────────────────────────────────────────────────────────────────
|
| 318 |
+
def s6(c):
|
| 319 |
+
c.setFillColor(C_BG)
|
| 320 |
+
c.rect(0, 0, W, H, fill=1, stroke=0)
|
| 321 |
+
header_strip(c, "Every Decision is Explainable and Auditable", 6)
|
| 322 |
+
|
| 323 |
+
half = (W - 2 * M - 0.8 * cm) / 2
|
| 324 |
+
lx = M
|
| 325 |
+
rx = M + half + 0.8 * cm
|
| 326 |
+
top = H - 2.0 * cm
|
| 327 |
+
panel_h = 4.2 * cm
|
| 328 |
+
|
| 329 |
+
for px, title, lines, col in [
|
| 330 |
+
(lx, "Criterion-Level Verdicts", [
|
| 331 |
+
"Each (bidder × criterion) pair shows:",
|
| 332 |
+
"• Which criterion was checked",
|
| 333 |
+
"• Document and page providing evidence",
|
| 334 |
+
"• Value extracted (e.g. 'INR 6.2 Cr')",
|
| 335 |
+
"• OCR tier that read the document",
|
| 336 |
+
"• Combined confidence score (0–100%)",
|
| 337 |
+
"• Plain-English reason",
|
| 338 |
+
], C_BLU),
|
| 339 |
+
(rx, "Audit Trail", [
|
| 340 |
+
"Every action logged with:",
|
| 341 |
+
"• UTC timestamp",
|
| 342 |
+
"• Action type (6 types incl. criteria_extracted,",
|
| 343 |
+
" vision_ocr_invoked, precomputed_fallback_used)",
|
| 344 |
+
"• Model version & Actor",
|
| 345 |
+
"• Full payload JSON",
|
| 346 |
+
"• Exportable as CSV",
|
| 347 |
+
], C_BLU),
|
| 348 |
+
]:
|
| 349 |
+
c.setFillColor(C_STR)
|
| 350 |
+
c.setStrokeColor(col)
|
| 351 |
+
c.setLineWidth(1.5)
|
| 352 |
+
c.roundRect(px, top - panel_h, half, panel_h, 4, fill=1, stroke=1)
|
| 353 |
+
c.setFont("Helvetica-Bold", 13)
|
| 354 |
+
c.setFillColor(col)
|
| 355 |
+
c.drawString(px + 0.4 * cm, top - 0.5 * cm, title)
|
| 356 |
+
c.setFont("Helvetica", 11)
|
| 357 |
+
c.setFillColor(C_TXT)
|
| 358 |
+
for i, ln in enumerate(lines):
|
| 359 |
+
c.setFillColor(C_SUB if i == 0 else C_TXT)
|
| 360 |
+
c.drawString(px + 0.4 * cm, top - 1.0 * cm - i * 0.45 * cm, ln)
|
| 361 |
+
|
| 362 |
+
# Safety rule
|
| 363 |
+
sy = top - panel_h - 0.5 * cm - 1.9 * cm
|
| 364 |
+
c.setFillColor(colors.HexColor("#FFFBEB"))
|
| 365 |
+
c.setStrokeColor(C_AMB)
|
| 366 |
+
c.setLineWidth(2.5)
|
| 367 |
+
c.roundRect(M, sy, W - 2 * M, 1.9 * cm, 4, fill=1, stroke=1)
|
| 368 |
+
c.setFont("Helvetica-Bold", 14)
|
| 369 |
+
c.setFillColor(colors.HexColor("#92400E"))
|
| 370 |
+
c.drawString(M + 0.5 * cm, sy + 1.52 * cm, "The Safety Rule:")
|
| 371 |
+
c.setFont("Helvetica", 12)
|
| 372 |
+
c.setFillColor(C_TXT)
|
| 373 |
+
rule = ("If combined confidence is 0.55–0.80 AND verdict is not_eligible, "
|
| 374 |
+
"the verdict is automatically downgraded to needs_review. "
|
| 375 |
+
"A bidder is NEVER silently disqualified at medium confidence.")
|
| 376 |
+
para(c, rule, M + 0.5 * cm, sy + 1.3 * cm, W - 2 * M - 1 * cm, size=12, color=C_TXT, leading=16)
|
| 377 |
+
footer_line(c)
|
| 378 |
+
c.showPage()
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
# ── Slide 7 ──────────────────────────────────────────���──────────────────────
|
| 382 |
+
def s7(c):
|
| 383 |
+
c.setFillColor(C_BG)
|
| 384 |
+
c.rect(0, 0, W, H, fill=1, stroke=0)
|
| 385 |
+
header_strip(c, "Demo: Three Bidders, Three Outcomes", 7)
|
| 386 |
+
|
| 387 |
+
bidders = [
|
| 388 |
+
("ELIGIBLE", C_GRN, colors.HexColor("#D1FAE5"),
|
| 389 |
+
"Bidder A\nApex Constructions Pvt. Ltd.",
|
| 390 |
+
["C1 Turnover: INR 6.37 Cr avg — PASS",
|
| 391 |
+
"C2 Projects: 5 completed (CRPF) — PASS",
|
| 392 |
+
"C3 GST: Active GSTIN — PASS",
|
| 393 |
+
"C4 ISO 9001:2015: Valid June 2027 — PASS",
|
| 394 |
+
"All typed PDFs, confidence ≥ 93%"]),
|
| 395 |
+
("NOT ELIGIBLE", C_RED, colors.HexColor("#FEE2E2"),
|
| 396 |
+
"Bidder B\nBuildRight Enterprises",
|
| 397 |
+
["C1 Turnover: INR 1.5 Cr avg — FAIL",
|
| 398 |
+
"Below required minimum of INR 5 Cr",
|
| 399 |
+
"C2–C4: All pass",
|
| 400 |
+
"Auto-disqualified, conf 95%"]),
|
| 401 |
+
("NEEDS REVIEW", C_AMB, colors.HexColor("#FEF3C7"),
|
| 402 |
+
"Bidder C\nShree Constructions & Services",
|
| 403 |
+
["C1: Blurry scan → Vision LLM",
|
| 404 |
+
"INR 5.4 Cr, combined conf 0.58",
|
| 405 |
+
"Safety rule → needs_review",
|
| 406 |
+
"C2: 3 projects (borderline)",
|
| 407 |
+
"C3–C4: Pass"]),
|
| 408 |
+
]
|
| 409 |
+
step = (W - 2 * M - 2 * cm) / 3
|
| 410 |
+
top = H - 2.0 * cm
|
| 411 |
+
panel_h = 4.3 * cm
|
| 412 |
+
for i, (verdict, col, bgc, name, bullets) in enumerate(bidders):
|
| 413 |
+
cx = M + i * (step + 1 * cm)
|
| 414 |
+
c.setFillColor(bgc)
|
| 415 |
+
c.setStrokeColor(col)
|
| 416 |
+
c.setLineWidth(2)
|
| 417 |
+
c.roundRect(cx, top - panel_h, step, panel_h, 5, fill=1, stroke=1)
|
| 418 |
+
# Outcome icon text
|
| 419 |
+
c.setFont("Helvetica-Bold", 24)
|
| 420 |
+
c.setFillColor(col)
|
| 421 |
+
c.drawCentredString(cx + step / 2, top - 0.6 * cm, verdict)
|
| 422 |
+
# Divider
|
| 423 |
+
c.setStrokeColor(col)
|
| 424 |
+
c.setLineWidth(1)
|
| 425 |
+
c.line(cx + 0.3 * cm, top - 0.9 * cm, cx + step - 0.3 * cm, top - 0.9 * cm)
|
| 426 |
+
# Name
|
| 427 |
+
c.setFont("Helvetica-Bold", 11)
|
| 428 |
+
c.setFillColor(C_TXT)
|
| 429 |
+
for li, nl in enumerate(name.split("\n")):
|
| 430 |
+
c.drawCentredString(cx + step / 2, top - 1.3 * cm - li * 0.38 * cm, nl)
|
| 431 |
+
# Bullets
|
| 432 |
+
c.setFont("Helvetica", 11)
|
| 433 |
+
c.setFillColor(C_TXT)
|
| 434 |
+
for j, b in enumerate(bullets):
|
| 435 |
+
c.drawString(cx + 0.3 * cm, top - 2.2 * cm - j * 0.42 * cm, b)
|
| 436 |
+
|
| 437 |
+
# Metric strip
|
| 438 |
+
metrics = [("5", "Criteria\nextracted"), ("15", "Bidder docs\nprocessed"),
|
| 439 |
+
("15", "LLM eval\ncalls"), ("1", "Vision OCR\ninvocations"),
|
| 440 |
+
("1", "Human review\nitems"), ("20+", "Audit\nentries")]
|
| 441 |
+
sy = H * 0.16
|
| 442 |
+
c.setFillColor(C_STR)
|
| 443 |
+
c.setStrokeColor(C_DIV)
|
| 444 |
+
c.setLineWidth(1)
|
| 445 |
+
c.roundRect(M, sy, W - 2 * M, 1.5 * cm, 4, fill=1, stroke=1)
|
| 446 |
+
mstep = (W - 2 * M) / len(metrics)
|
| 447 |
+
for i, (val, lbl) in enumerate(metrics):
|
| 448 |
+
mx = M + i * mstep + mstep / 2
|
| 449 |
+
c.setFont("Helvetica-Bold", 20)
|
| 450 |
+
c.setFillColor(C_BLU)
|
| 451 |
+
c.drawCentredString(mx, sy + 0.9 * cm, val)
|
| 452 |
+
c.setFont("Helvetica", 9)
|
| 453 |
+
c.setFillColor(C_SUB)
|
| 454 |
+
for li, ln in enumerate(lbl.split("\n")):
|
| 455 |
+
c.drawCentredString(mx, sy + 0.42 * cm - li * 0.28 * cm, ln.upper())
|
| 456 |
+
footer_line(c)
|
| 457 |
+
c.showPage()
|
| 458 |
+
|
| 459 |
+
|
| 460 |
+
# ── Slide 8 ─────────────────────────────────────────────────────────────────
|
| 461 |
+
def s8(c):
|
| 462 |
+
c.setFillColor(C_BG)
|
| 463 |
+
c.rect(0, 0, W, H, fill=1, stroke=0)
|
| 464 |
+
header_strip(c, "Stack, Impact & What's Next", 8)
|
| 465 |
+
|
| 466 |
+
# Icon grid for future work (6 items, 3×2)
|
| 467 |
+
future = [
|
| 468 |
+
("MULTI", "Multi-tender workspace\nsame bidder pool, multiple tenders"),
|
| 469 |
+
("GEM", "GeM portal API integration\nlive tender ingestion"),
|
| 470 |
+
("RANK", "Automated bidder ranking\nwith weighted scoring"),
|
| 471 |
+
("LLM+", "LayoutLM for complex\nfinancial tables in scans"),
|
| 472 |
+
("TEAM", "Multi-evaluator workflow\nwith role-based approval"),
|
| 473 |
+
("AUDIT", "Audit PDF export for\nprocurement oversight"),
|
| 474 |
+
]
|
| 475 |
+
colors_f = [C_BLU, C_GRN, C_PUR, C_AMB, C_RED, C_BLU]
|
| 476 |
+
gw = (W / 2 - M - 1 * cm) / 3
|
| 477 |
+
gh = 1.8 * cm
|
| 478 |
+
for i, ((ico, desc), col) in enumerate(zip(future, colors_f)):
|
| 479 |
+
row = i // 3
|
| 480 |
+
col_i = i % 3
|
| 481 |
+
gx = M + col_i * (gw + 0.4 * cm)
|
| 482 |
+
gy = H - 2.4 * cm - row * (gh + 0.5 * cm)
|
| 483 |
+
c.setFillColor(C_STR)
|
| 484 |
+
c.setStrokeColor(col)
|
| 485 |
+
c.setLineWidth(1)
|
| 486 |
+
c.roundRect(gx, gy - gh, gw, gh, 3, fill=1, stroke=1)
|
| 487 |
+
c.setFont("Helvetica-Bold", 9)
|
| 488 |
+
c.setFillColor(col)
|
| 489 |
+
c.drawString(gx + 0.25 * cm, gy - 0.5 * cm, ico)
|
| 490 |
+
c.setFont("Helvetica", 10)
|
| 491 |
+
c.setFillColor(C_TXT)
|
| 492 |
+
for li, ln in enumerate(desc.split("\n")):
|
| 493 |
+
c.drawString(gx + 0.25 * cm, gy - 0.95 * cm - li * 0.38 * cm, ln)
|
| 494 |
+
|
| 495 |
+
# Tech stack (right half)
|
| 496 |
+
stack = [
|
| 497 |
+
("UI / Orchestration", "Streamlit 1.39"),
|
| 498 |
+
("LLM", "DeepSeek API"),
|
| 499 |
+
("OCR Tiers", "PyMuPDF · Tesseract · Vision LLM"),
|
| 500 |
+
("Retrieval", "all-MiniLM-L6-v2"),
|
| 501 |
+
("Validation", "Pydantic v2"),
|
| 502 |
+
("Audit", "SQLite — Exportable CSV"),
|
| 503 |
+
("Deploy", "Streamlit Cloud / HuggingFace"),
|
| 504 |
+
]
|
| 505 |
+
sx = W / 2 + 0.5 * cm
|
| 506 |
+
sw = W - M - sx
|
| 507 |
+
c.setFont("Helvetica-Bold", 13)
|
| 508 |
+
c.setFillColor(C_BLU)
|
| 509 |
+
c.drawString(sx, H - 2.1 * cm, "Technology Stack")
|
| 510 |
+
for i, (comp, tech) in enumerate(stack):
|
| 511 |
+
ry = H - 2.75 * cm - i * 0.55 * cm
|
| 512 |
+
c.setFillColor(C_STR if i % 2 == 0 else C_BG)
|
| 513 |
+
c.rect(sx, ry - 0.38 * cm, sw, 0.5 * cm, fill=1, stroke=0)
|
| 514 |
+
c.setFont("Helvetica", 10)
|
| 515 |
+
c.setFillColor(C_SUB)
|
| 516 |
+
c.drawString(sx + 0.2 * cm, ry, comp)
|
| 517 |
+
c.setFillColor(C_TXT)
|
| 518 |
+
c.drawString(sx + sw * 0.42, ry, tech)
|
| 519 |
+
|
| 520 |
+
# Impact callout
|
| 521 |
+
iy = H * 0.15
|
| 522 |
+
c.setFillColor(C_BLU)
|
| 523 |
+
c.roundRect(M, iy, W - 2 * M, 1.3 * cm, 5, fill=1, stroke=0)
|
| 524 |
+
c.setFont("Helvetica-Bold", 13)
|
| 525 |
+
c.setFillColor(colors.white)
|
| 526 |
+
c.drawCentredString(W / 2, iy + 0.82 * cm,
|
| 527 |
+
"3–5 days → minutes. Every verdict traceable to a document, page, and model version.")
|
| 528 |
+
c.setFont("Helvetica", 11)
|
| 529 |
+
c.drawCentredString(W / 2, iy + 0.38 * cm,
|
| 530 |
+
"Built in one hackathon session. Deployable today.")
|
| 531 |
+
footer_line(c)
|
| 532 |
+
c.showPage()
|
| 533 |
+
|
| 534 |
+
|
| 535 |
+
def main():
|
| 536 |
+
os.makedirs("deck", exist_ok=True)
|
| 537 |
+
out = "deck/TenderIQ_v6_infographic.pdf"
|
| 538 |
+
c = Canvas(out, pagesize=landscape(A4))
|
| 539 |
+
s1(c); s2(c); s3(c); s4(c)
|
| 540 |
+
s5(c); s6(c); s7(c); s8(c)
|
| 541 |
+
c.save()
|
| 542 |
+
size = os.path.getsize(out)
|
| 543 |
+
print(f"OK: Saved {out} ({size:,} bytes, 8 slides)")
|
| 544 |
+
|
| 545 |
+
|
| 546 |
+
if __name__ == "__main__":
|
| 547 |
+
main()
|