JaydeepR Claude Sonnet 4.6 commited on
Commit
1b26bd8
·
1 Parent(s): d5d2cb4

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 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()