| |
| """TenderIQ v6 — Infographic (PDF via reportlab)""" |
| import os |
| import math |
| from reportlab.pdfgen.canvas import Canvas |
| from reportlab.lib.pagesizes import A4, landscape |
| from reportlab.lib import colors |
| from reportlab.lib.units import cm |
| from reportlab.platypus import Paragraph |
| from reportlab.lib.styles import ParagraphStyle |
|
|
| W, H = landscape(A4) |
|
|
| |
| C_BG = colors.white |
| C_STR = colors.HexColor("#F8FAFC") |
| C_BLU = colors.HexColor("#2563EB") |
| C_GRN = colors.HexColor("#22C55E") |
| C_RED = colors.HexColor("#EF4444") |
| C_AMB = colors.HexColor("#F59E0B") |
| C_PUR = colors.HexColor("#8B5CF6") |
| C_TXT = colors.HexColor("#0F172A") |
| C_SUB = colors.HexColor("#64748B") |
| C_DIV = colors.HexColor("#E2E8F0") |
|
|
| M = 1.8 * cm |
|
|
|
|
| def para(c, text, x, y, width, font="Helvetica", size=12, color=C_TXT, leading=16, align="left"): |
| style = ParagraphStyle( |
| 'p', fontName=font, fontSize=size, leading=leading, textColor=color, |
| alignment={"left": 0, "center": 1, "right": 2}.get(align, 0)) |
| p = Paragraph(text.replace("\n", "<br/>"), style) |
| _, used = p.wrapOn(c, width, 1000) |
| p.drawOn(c, x, y - used) |
| return used |
|
|
|
|
| def circ_progress(c, cx, cy, r, pct, fg, bg=C_DIV, thickness=8): |
| """Draw a circular progress arc.""" |
| c.setStrokeColor(bg) |
| c.setLineWidth(thickness) |
| c.circle(cx, cy, r, fill=0, stroke=1) |
| c.setStrokeColor(fg) |
| c.setLineWidth(thickness) |
| extent = 360 * pct |
| c.arc(cx - r, cy - r, cx + r, cy + r, startAng=90, extent=-extent) |
| c.setFillColor(fg) |
| c.setFont("Helvetica-Bold", int(r * 0.5)) |
| c.drawCentredString(cx, cy - r * 0.18, f"{int(pct * 100)}%") |
|
|
|
|
| def header_strip(c, title, n): |
| c.setFillColor(C_STR) |
| c.rect(0, H - 1.3 * cm, W, 1.3 * cm, fill=1, stroke=0) |
| c.setStrokeColor(C_BLU) |
| c.setLineWidth(2) |
| c.line(0, H - 1.3 * cm, W, H - 1.3 * cm) |
| c.setFont("Helvetica-Bold", 16) |
| c.setFillColor(C_BLU) |
| c.drawString(M, H - 1.0 * cm, title) |
| c.setFont("Helvetica", 10) |
| c.setFillColor(C_SUB) |
| c.drawRightString(W - M, H - 1.0 * cm, f"TenderIQ · CRPF Hackathon · {n} / 8") |
|
|
|
|
| def footer_line(c): |
| c.setStrokeColor(C_DIV) |
| c.setLineWidth(0.5) |
| c.line(M, 0.9 * cm, W - M, 0.9 * cm) |
|
|
|
|
| def big_num(c, val, label, cx, cy, color=C_BLU): |
| c.setFont("Helvetica-Bold", 52) |
| c.setFillColor(color) |
| c.drawCentredString(cx, cy, val) |
| c.setFont("Helvetica", 11) |
| c.setFillColor(C_SUB) |
| |
| for i, line in enumerate(label.split("\n")): |
| c.drawCentredString(cx, cy - 1.3 * cm - i * 0.45 * cm, line.upper()) |
|
|
|
|
| def icon_label(c, icon, label, cx, cy, color=C_BLU, icon_sz=36, lbl_sz=11): |
| c.setFont("Helvetica-Bold", icon_sz) |
| c.setFillColor(color) |
| c.drawCentredString(cx, cy, icon) |
| c.setFont("Helvetica", lbl_sz) |
| c.setFillColor(C_TXT) |
| c.drawCentredString(cx, cy - 1.2 * cm, label) |
|
|
|
|
| def stage_row(c, icon, title, desc, y, color=C_BLU): |
| |
| c.setFillColor(color) |
| c.circle(M + 0.9 * cm, y + 0.45 * cm, 0.45 * cm, fill=1, stroke=0) |
| c.setFont("Helvetica-Bold", 14) |
| c.setFillColor(colors.white) |
| c.drawCentredString(M + 0.9 * cm, y + 0.3 * cm, icon) |
| c.setFont("Helvetica-Bold", 13) |
| c.setFillColor(color) |
| c.drawString(M + 1.8 * cm, y + 0.55 * cm, title) |
| c.setFont("Helvetica", 11) |
| c.setFillColor(C_TXT) |
| c.drawString(M + 1.8 * cm, y + 0.18 * cm, desc) |
|
|
|
|
| |
| def s1(c): |
| c.setFillColor(C_BG) |
| c.rect(0, 0, W, H, fill=1, stroke=0) |
| |
| c.setFillColor(C_BLU) |
| c.rect(0, 0, 0.6 * cm, H, fill=1, stroke=0) |
| |
| c.setFont("Helvetica-Bold", 80) |
| c.setFillColor(C_BLU) |
| c.drawCentredString(W / 2, H / 2 + 2.2 * cm, "TenderIQ") |
| |
| c.setStrokeColor(C_DIV) |
| c.setLineWidth(1.5) |
| c.line(W / 2 - 6 * cm, H / 2 + 1.3 * cm, W / 2 + 6 * cm, H / 2 + 1.3 * cm) |
| c.setFont("Helvetica", 20) |
| c.setFillColor(C_TXT) |
| c.drawCentredString(W / 2, H / 2 + 0.55 * cm, |
| "Explainable AI for Government Tender Evaluation") |
| c.setFont("Helvetica", 13) |
| c.setFillColor(C_SUB) |
| c.drawCentredString(W / 2, H / 2 - 0.4 * cm, "CRPF HACKATHON · THEME 3") |
| c.setFont("Helvetica-Oblique", 13) |
| c.setFillColor(C_BLU) |
| c.drawCentredString(W / 2, H / 2 - 1.3 * cm, |
| "From days to minutes. Every decision traceable.") |
| footer_line(c) |
| c.showPage() |
|
|
|
|
| |
| def s2(c): |
| c.setFillColor(C_BG) |
| c.rect(0, 0, W, H, fill=1, stroke=0) |
| header_strip(c, "The Problem with Manual Tender Evaluation", 2) |
| cy = H * 0.5 |
| third = (W - 2 * M) / 3 |
| callouts = [ |
| ("3–5", "days per tender\nevaluation", C_RED), |
| ("≠", "inconsistent verdicts\nfrom evaluators", C_AMB), |
| ("?", "no audit trail\nfor decisions", C_PUR), |
| ] |
| for i, (val, lbl, col) in enumerate(callouts): |
| cx = M + i * third + third / 2 |
| big_num(c, val, lbl, cx, cy, col) |
| |
| c.setStrokeColor(C_DIV) |
| c.setLineWidth(1) |
| c.line(M, cy - 2.2 * cm, W - M, cy - 2.2 * cm) |
| bullets = [ |
| "• Mixed document formats: typed PDFs, scanned certificates, phone photographs", |
| "• Government procurement worth ₹50 lakh crore+ annually in India", |
| "• Project execution delays traced directly to procurement bottlenecks", |
| ] |
| c.setFont("Helvetica", 12) |
| c.setFillColor(C_SUB) |
| for i, b in enumerate(bullets): |
| c.drawString(M, cy - 2.8 * cm - i * 0.55 * cm, b) |
| footer_line(c) |
| c.showPage() |
|
|
|
|
| |
| def s3(c): |
| c.setFillColor(C_BG) |
| c.rect(0, 0, W, H, fill=1, stroke=0) |
| header_strip(c, "TenderIQ — Four Stages, End to End", 3) |
| |
| icons = ["PDF", "OCR", "EVAL", "LOG"] |
| labels = ["Extract", "OCR & Index", "Evaluate", "Review & Audit"] |
| descs = [ |
| "DeepSeek LLM reads tender PDF\n→ structured JSON criteria", |
| "3-tier pipeline handles any\ndoc format → vector index", |
| "Vector search + LLM verdict\n+ safety rule", |
| "Human review queue\n+ full audit trail (CSV)", |
| ] |
| colors_ = [C_BLU, C_PUR, C_GRN, C_AMB] |
| step = (W - 2 * M) / 4 |
| cy = H * 0.52 |
| for i, (ico, lbl, desc, col) in enumerate(zip(icons, labels, descs, colors_)): |
| cx = M + i * step + step / 2 |
| |
| c.setFillColor(col) |
| c.circle(cx, cy + 1.0 * cm, 0.9 * cm, fill=1, stroke=0) |
| c.setFont("Helvetica-Bold", 9) |
| c.setFillColor(colors.white) |
| c.drawCentredString(cx, cy + 0.88 * cm, ico) |
| c.setFont("Helvetica-Bold", 14) |
| c.setFillColor(col) |
| c.drawCentredString(cx, cy - 0.25 * cm, lbl) |
| c.setFont("Helvetica", 11) |
| c.setFillColor(C_TXT) |
| for j, dl in enumerate(desc.split("\n")): |
| c.drawCentredString(cx, cy - 0.82 * cm - j * 0.45 * cm, dl) |
| if i < 3: |
| c.setFont("Helvetica-Bold", 18) |
| c.setFillColor(C_SUB) |
| c.drawCentredString(M + (i + 1) * step, cy + 0.9 * cm, "→") |
|
|
| |
| by = H * 0.19 |
| c.setFillColor(C_STR) |
| c.setStrokeColor(C_BLU) |
| c.setLineWidth(1.5) |
| c.roundRect(M, by, W - 2 * M, 1.1 * cm, 4, fill=1, stroke=1) |
| c.setFont("Helvetica-BoldOblique", 13) |
| c.setFillColor(C_BLU) |
| c.drawCentredString(W / 2, by + 0.38 * cm, |
| '"Minutes, not days. Every verdict traceable to a document and page."') |
| footer_line(c) |
| c.showPage() |
|
|
|
|
| |
| def s4(c): |
| c.setFillColor(C_BG) |
| c.rect(0, 0, W, H, fill=1, stroke=0) |
| header_strip(c, "System Architecture", 4) |
| |
| stages = [ |
| ("PDF", "Tender PDF + Bidder Documents (PDFs · scans · photos)", C_BLU), |
| ("LLM", "DeepSeek LLM — Extract Criteria | 3-Tier OCR → Vector Index", C_PUR), |
| ("EVAL", "DeepSeek LLM — Evaluate each criterion with combined confidence", C_GRN), |
| ("AUD", "eligible / not_eligible | needs_review → Human Review Queue", C_AMB), |
| ("DB", "SQLite Audit Log — every action with timestamp + payload", C_BLU), |
| ] |
| top_y = H - 2.2 * cm |
| row_h = 1.2 * cm |
| for i, (ico, desc, col) in enumerate(stages): |
| y = top_y - i * row_h |
| stage_row(c, ico, "", desc, y - 0.1 * cm, col) |
| if i < len(stages) - 1: |
| |
| c.setStrokeColor(C_DIV) |
| c.setLineWidth(1.5) |
| c.line(M + 0.9 * cm, y - 0.1 * cm, M + 0.9 * cm, y - row_h + 0.5 * cm) |
|
|
| |
| fx = W / 2 + 1 * cm |
| c.setFillColor(C_STR) |
| c.setStrokeColor(C_DIV) |
| c.setLineWidth(1) |
| c.roundRect(fx, H * 0.2, W - M - fx, H * 0.6, 5, fill=1, stroke=1) |
| c.setFont("Helvetica-Bold", 13) |
| c.setFillColor(C_BLU) |
| c.drawString(fx + 0.4 * cm, H * 0.76, "Key Technical Facts") |
| facts = [ |
| "Single-process Streamlit app", |
| "Streamlit Cloud / HuggingFace deployment", |
| "Local storage: SQLite + vector index", |
| "Only external dep: DeepSeek API", |
| ] |
| c.setFont("Helvetica", 11) |
| c.setFillColor(C_TXT) |
| for i, f in enumerate(facts): |
| c.drawString(fx + 0.4 * cm, H * 0.68 - i * 0.65 * cm, f"• {f}") |
| footer_line(c) |
| c.showPage() |
|
|
|
|
| |
| def s5(c): |
| c.setFillColor(C_BG) |
| c.rect(0, 0, W, H, fill=1, stroke=0) |
| header_strip(c, "Three-Tier OCR — Handling Any Document Format", 5) |
| tiers = [ |
| ("Tier 1\nPyMuPDF", "Typed PDF → Free, instant", 1.0, C_BLU), |
| ("Tier 2\nTesseract", "Scan / image → Free, local", 0.60, C_PUR), |
| ("Tier 3\nVision LLM", "Low confidence → API call", 0.95, C_AMB), |
| ] |
| step = (W - 2 * M) / 3 |
| cy = H * 0.55 |
| for i, (tier, desc, pct, col) in enumerate(tiers): |
| cx = M + i * step + step / 2 |
| |
| circ_progress(c, cx, cy + 0.5 * cm, 1.3 * cm, pct, col) |
| |
| c.setFont("Helvetica-Bold", 13) |
| c.setFillColor(col) |
| for li, ln in enumerate(tier.split("\n")): |
| c.drawCentredString(cx, cy - 1.35 * cm - li * 0.45 * cm, ln) |
| c.setFont("Helvetica", 11) |
| c.setFillColor(C_TXT) |
| c.drawCentredString(cx, cy - 2.55 * cm, desc) |
| if i < 2: |
| c.setFont("Helvetica-Bold", 18) |
| c.setFillColor(C_SUB) |
| c.drawCentredString(M + (i + 1) * step, cy + 0.5 * cm, "→") |
|
|
| |
| dy = H * 0.19 |
| c.setFillColor(C_STR) |
| c.setStrokeColor(C_AMB) |
| c.setLineWidth(2) |
| c.roundRect(M, dy, W - 2 * M, 2.2 * cm, 4, fill=1, stroke=1) |
| c.setFont("Helvetica-Bold", 12) |
| c.setFillColor(C_AMB) |
| c.drawString(M + 0.5 * cm, dy + 1.78 * cm, "Demo Scenario") |
| c.setFont("Helvetica", 11) |
| c.setFillColor(C_TXT) |
| demo = ("Bidder C submits a blurry, rotated CA certificate scan. " |
| "Tesseract reads it at ~55% confidence. Vision LLM transcribes the turnover correctly. " |
| "Combined confidence = 0.58 → routed to human review. " |
| "Borderline evidence requires a human — this is intentional.") |
| para(c, demo, M + 0.5 * cm, dy + 1.55 * cm, W - 2 * M - 1 * cm, size=11, color=C_TXT, leading=16) |
| footer_line(c) |
| c.showPage() |
|
|
|
|
| |
| def s6(c): |
| c.setFillColor(C_BG) |
| c.rect(0, 0, W, H, fill=1, stroke=0) |
| header_strip(c, "Every Decision is Explainable and Auditable", 6) |
|
|
| half = (W - 2 * M - 0.8 * cm) / 2 |
| lx = M |
| rx = M + half + 0.8 * cm |
| top = H - 2.0 * cm |
| panel_h = 4.2 * cm |
|
|
| for px, title, lines, col in [ |
| (lx, "Criterion-Level Verdicts", [ |
| "Each (bidder × criterion) pair shows:", |
| "• Which criterion was checked", |
| "• Document and page providing evidence", |
| "• Value extracted (e.g. 'INR 6.2 Cr')", |
| "• OCR tier that read the document", |
| "• Combined confidence score (0–100%)", |
| "• Plain-English reason", |
| ], C_BLU), |
| (rx, "Audit Trail", [ |
| "Every action logged with:", |
| "• UTC timestamp", |
| "• Action type (6 types incl. criteria_extracted,", |
| " vision_ocr_invoked, precomputed_fallback_used)", |
| "• Model version & Actor", |
| "• Full payload JSON", |
| "• Exportable as CSV", |
| ], C_BLU), |
| ]: |
| c.setFillColor(C_STR) |
| c.setStrokeColor(col) |
| c.setLineWidth(1.5) |
| c.roundRect(px, top - panel_h, half, panel_h, 4, fill=1, stroke=1) |
| c.setFont("Helvetica-Bold", 13) |
| c.setFillColor(col) |
| c.drawString(px + 0.4 * cm, top - 0.5 * cm, title) |
| c.setFont("Helvetica", 11) |
| c.setFillColor(C_TXT) |
| for i, ln in enumerate(lines): |
| c.setFillColor(C_SUB if i == 0 else C_TXT) |
| c.drawString(px + 0.4 * cm, top - 1.0 * cm - i * 0.45 * cm, ln) |
|
|
| |
| sy = top - panel_h - 0.5 * cm - 1.9 * cm |
| c.setFillColor(colors.HexColor("#FFFBEB")) |
| c.setStrokeColor(C_AMB) |
| c.setLineWidth(2.5) |
| c.roundRect(M, sy, W - 2 * M, 1.9 * cm, 4, fill=1, stroke=1) |
| c.setFont("Helvetica-Bold", 14) |
| c.setFillColor(colors.HexColor("#92400E")) |
| c.drawString(M + 0.5 * cm, sy + 1.52 * cm, "The Safety Rule:") |
| c.setFont("Helvetica", 12) |
| c.setFillColor(C_TXT) |
| rule = ("If combined confidence is 0.55–0.80 AND verdict is not_eligible, " |
| "the verdict is automatically downgraded to needs_review. " |
| "A bidder is NEVER silently disqualified at medium confidence.") |
| para(c, rule, M + 0.5 * cm, sy + 1.3 * cm, W - 2 * M - 1 * cm, size=12, color=C_TXT, leading=16) |
| footer_line(c) |
| c.showPage() |
|
|
|
|
| |
| def s7(c): |
| c.setFillColor(C_BG) |
| c.rect(0, 0, W, H, fill=1, stroke=0) |
| header_strip(c, "Demo: Three Bidders, Three Outcomes", 7) |
|
|
| bidders = [ |
| ("ELIGIBLE", C_GRN, colors.HexColor("#D1FAE5"), |
| "Bidder A\nApex Constructions Pvt. Ltd.", |
| ["C1 Turnover: INR 6.37 Cr avg — PASS", |
| "C2 Projects: 5 completed (CRPF) — PASS", |
| "C3 GST: Active GSTIN — PASS", |
| "C4 ISO 9001:2015: Valid June 2027 — PASS", |
| "All typed PDFs, confidence ≥ 93%"]), |
| ("NOT ELIGIBLE", C_RED, colors.HexColor("#FEE2E2"), |
| "Bidder B\nBuildRight Enterprises", |
| ["C1 Turnover: INR 1.5 Cr avg — FAIL", |
| "Below required minimum of INR 5 Cr", |
| "C2–C4: All pass", |
| "Auto-disqualified, conf 95%"]), |
| ("NEEDS REVIEW", C_AMB, colors.HexColor("#FEF3C7"), |
| "Bidder C\nShree Constructions & Services", |
| ["C1: Blurry scan → Vision LLM", |
| "INR 5.4 Cr, combined conf 0.58", |
| "Safety rule → needs_review", |
| "C2: 3 projects (borderline)", |
| "C3–C4: Pass"]), |
| ] |
| step = (W - 2 * M - 2 * cm) / 3 |
| top = H - 2.0 * cm |
| panel_h = 4.3 * cm |
| for i, (verdict, col, bgc, name, bullets) in enumerate(bidders): |
| cx = M + i * (step + 1 * cm) |
| c.setFillColor(bgc) |
| c.setStrokeColor(col) |
| c.setLineWidth(2) |
| c.roundRect(cx, top - panel_h, step, panel_h, 5, fill=1, stroke=1) |
| |
| c.setFont("Helvetica-Bold", 24) |
| c.setFillColor(col) |
| c.drawCentredString(cx + step / 2, top - 0.6 * cm, verdict) |
| |
| c.setStrokeColor(col) |
| c.setLineWidth(1) |
| c.line(cx + 0.3 * cm, top - 0.9 * cm, cx + step - 0.3 * cm, top - 0.9 * cm) |
| |
| c.setFont("Helvetica-Bold", 11) |
| c.setFillColor(C_TXT) |
| for li, nl in enumerate(name.split("\n")): |
| c.drawCentredString(cx + step / 2, top - 1.3 * cm - li * 0.38 * cm, nl) |
| |
| c.setFont("Helvetica", 11) |
| c.setFillColor(C_TXT) |
| for j, b in enumerate(bullets): |
| c.drawString(cx + 0.3 * cm, top - 2.2 * cm - j * 0.42 * cm, b) |
|
|
| |
| metrics = [("5", "Criteria\nextracted"), ("15", "Bidder docs\nprocessed"), |
| ("15", "LLM eval\ncalls"), ("1", "Vision OCR\ninvocations"), |
| ("1", "Human review\nitems"), ("20+", "Audit\nentries")] |
| sy = H * 0.16 |
| c.setFillColor(C_STR) |
| c.setStrokeColor(C_DIV) |
| c.setLineWidth(1) |
| c.roundRect(M, sy, W - 2 * M, 1.5 * cm, 4, fill=1, stroke=1) |
| mstep = (W - 2 * M) / len(metrics) |
| for i, (val, lbl) in enumerate(metrics): |
| mx = M + i * mstep + mstep / 2 |
| c.setFont("Helvetica-Bold", 20) |
| c.setFillColor(C_BLU) |
| c.drawCentredString(mx, sy + 0.9 * cm, val) |
| c.setFont("Helvetica", 9) |
| c.setFillColor(C_SUB) |
| for li, ln in enumerate(lbl.split("\n")): |
| c.drawCentredString(mx, sy + 0.42 * cm - li * 0.28 * cm, ln.upper()) |
| footer_line(c) |
| c.showPage() |
|
|
|
|
| |
| def s8(c): |
| c.setFillColor(C_BG) |
| c.rect(0, 0, W, H, fill=1, stroke=0) |
| header_strip(c, "Stack, Impact & What's Next", 8) |
|
|
| |
| future = [ |
| ("MULTI", "Multi-tender workspace\nsame bidder pool, multiple tenders"), |
| ("GEM", "GeM portal API integration\nlive tender ingestion"), |
| ("RANK", "Automated bidder ranking\nwith weighted scoring"), |
| ("LLM+", "LayoutLM for complex\nfinancial tables in scans"), |
| ("TEAM", "Multi-evaluator workflow\nwith role-based approval"), |
| ("AUDIT", "Audit PDF export for\nprocurement oversight"), |
| ] |
| colors_f = [C_BLU, C_GRN, C_PUR, C_AMB, C_RED, C_BLU] |
| gw = (W / 2 - M - 1 * cm) / 3 |
| gh = 1.8 * cm |
| for i, ((ico, desc), col) in enumerate(zip(future, colors_f)): |
| row = i // 3 |
| col_i = i % 3 |
| gx = M + col_i * (gw + 0.4 * cm) |
| gy = H - 2.4 * cm - row * (gh + 0.5 * cm) |
| c.setFillColor(C_STR) |
| c.setStrokeColor(col) |
| c.setLineWidth(1) |
| c.roundRect(gx, gy - gh, gw, gh, 3, fill=1, stroke=1) |
| c.setFont("Helvetica-Bold", 9) |
| c.setFillColor(col) |
| c.drawString(gx + 0.25 * cm, gy - 0.5 * cm, ico) |
| c.setFont("Helvetica", 10) |
| c.setFillColor(C_TXT) |
| for li, ln in enumerate(desc.split("\n")): |
| c.drawString(gx + 0.25 * cm, gy - 0.95 * cm - li * 0.38 * cm, ln) |
|
|
| |
| stack = [ |
| ("UI / Orchestration", "Streamlit 1.39"), |
| ("LLM", "DeepSeek API"), |
| ("OCR Tiers", "PyMuPDF · Tesseract · Vision LLM"), |
| ("Retrieval", "all-MiniLM-L6-v2"), |
| ("Validation", "Pydantic v2"), |
| ("Audit", "SQLite — Exportable CSV"), |
| ("Deploy", "Streamlit Cloud / HuggingFace"), |
| ] |
| sx = W / 2 + 0.5 * cm |
| sw = W - M - sx |
| c.setFont("Helvetica-Bold", 13) |
| c.setFillColor(C_BLU) |
| c.drawString(sx, H - 2.1 * cm, "Technology Stack") |
| for i, (comp, tech) in enumerate(stack): |
| ry = H - 2.75 * cm - i * 0.55 * cm |
| c.setFillColor(C_STR if i % 2 == 0 else C_BG) |
| c.rect(sx, ry - 0.38 * cm, sw, 0.5 * cm, fill=1, stroke=0) |
| c.setFont("Helvetica", 10) |
| c.setFillColor(C_SUB) |
| c.drawString(sx + 0.2 * cm, ry, comp) |
| c.setFillColor(C_TXT) |
| c.drawString(sx + sw * 0.42, ry, tech) |
|
|
| |
| iy = H * 0.15 |
| c.setFillColor(C_BLU) |
| c.roundRect(M, iy, W - 2 * M, 1.3 * cm, 5, fill=1, stroke=0) |
| c.setFont("Helvetica-Bold", 13) |
| c.setFillColor(colors.white) |
| c.drawCentredString(W / 2, iy + 0.82 * cm, |
| "3–5 days → minutes. Every verdict traceable to a document, page, and model version.") |
| c.setFont("Helvetica", 11) |
| c.drawCentredString(W / 2, iy + 0.38 * cm, |
| "Built in one hackathon session. Deployable today.") |
| footer_line(c) |
| c.showPage() |
|
|
|
|
| def main(): |
| os.makedirs("deck", exist_ok=True) |
| out = "deck/TenderIQ_v6_infographic.pdf" |
| c = Canvas(out, pagesize=landscape(A4)) |
| s1(c); s2(c); s3(c); s4(c) |
| s5(c); s6(c); s7(c); s8(c) |
| c.save() |
| size = os.path.getsize(out) |
| print(f"OK: Saved {out} ({size:,} bytes, 8 slides)") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|