337 lines
13 KiB
Python
337 lines
13 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""
|
||
|
|
Generate PDF 2 (V2): Talas — Delivery Capacity Evidence
|
||
|
|
For NGI Zero Commons Fund application (25K EUR, personal tone).
|
||
|
|
|
||
|
|
No external images required — uses KiCAD screenshots and text metrics.
|
||
|
|
Optionally add photos/veza_screenshot.png for a Veza platform screenshot.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from fpdf import FPDF
|
||
|
|
import os
|
||
|
|
|
||
|
|
BASE = "/home/senke/Documents/TG__Talas_Group"
|
||
|
|
PHOTOS = f"{BASE}/09_MODELE_ECONOMIQUE/Subventions/photos"
|
||
|
|
OUT = f"{BASE}/09_MODELE_ECONOMIQUE/Subventions/talas-project-evidence-v2.pdf"
|
||
|
|
|
||
|
|
FONT_DIR = "/usr/share/fonts/liberation-sans-fonts"
|
||
|
|
|
||
|
|
|
||
|
|
class TalasPDF(FPDF):
|
||
|
|
def __init__(self):
|
||
|
|
super().__init__()
|
||
|
|
self.add_font("Sans", "", f"{FONT_DIR}/LiberationSans-Regular.ttf")
|
||
|
|
self.add_font("Sans", "B", f"{FONT_DIR}/LiberationSans-Bold.ttf")
|
||
|
|
self.add_font("Sans", "I", f"{FONT_DIR}/LiberationSans-Italic.ttf")
|
||
|
|
self.add_font("Sans", "BI", f"{FONT_DIR}/LiberationSans-BoldItalic.ttf")
|
||
|
|
|
||
|
|
def header(self):
|
||
|
|
self.set_font("Sans", "B", 9)
|
||
|
|
self.set_text_color(100, 100, 100)
|
||
|
|
self.cell(0, 5, "Talas — NGI Zero Commons Fund — Attachment 2: Delivery Evidence",
|
||
|
|
align="R")
|
||
|
|
self.ln(8)
|
||
|
|
|
||
|
|
def footer(self):
|
||
|
|
self.set_y(-15)
|
||
|
|
self.set_font("Sans", "I", 8)
|
||
|
|
self.set_text_color(150, 150, 150)
|
||
|
|
self.cell(0, 10, f"Page {self.page_no()}/{{nb}}", align="C")
|
||
|
|
|
||
|
|
def section_title(self, title):
|
||
|
|
self.set_font("Sans", "B", 13)
|
||
|
|
self.set_text_color(30, 30, 30)
|
||
|
|
self.cell(0, 10, title, new_x="LMARGIN", new_y="NEXT")
|
||
|
|
self.set_draw_color(60, 60, 60)
|
||
|
|
self.line(self.l_margin, self.get_y(), self.w - self.r_margin, self.get_y())
|
||
|
|
self.ln(4)
|
||
|
|
|
||
|
|
def sub_title(self, title):
|
||
|
|
self.set_font("Sans", "B", 11)
|
||
|
|
self.set_text_color(50, 50, 50)
|
||
|
|
self.cell(0, 8, title, new_x="LMARGIN", new_y="NEXT")
|
||
|
|
self.ln(1)
|
||
|
|
|
||
|
|
def body_text(self, text):
|
||
|
|
self.set_font("Sans", "", 10)
|
||
|
|
self.set_text_color(40, 40, 40)
|
||
|
|
self.multi_cell(0, 5, text)
|
||
|
|
self.ln(2)
|
||
|
|
|
||
|
|
def personal_text(self, text):
|
||
|
|
self.set_font("Sans", "I", 10)
|
||
|
|
self.set_text_color(60, 60, 60)
|
||
|
|
self.multi_cell(0, 5, text)
|
||
|
|
self.ln(2)
|
||
|
|
|
||
|
|
def metric_row(self, label, value, detail=""):
|
||
|
|
self.set_font("Sans", "B", 10)
|
||
|
|
self.set_text_color(40, 40, 40)
|
||
|
|
self.cell(55, 6, label)
|
||
|
|
self.set_font("Sans", "", 10)
|
||
|
|
self.set_text_color(30, 100, 30)
|
||
|
|
self.cell(50, 6, value)
|
||
|
|
self.set_text_color(120, 120, 120)
|
||
|
|
self.set_font("Sans", "I", 9)
|
||
|
|
self.cell(0, 6, detail, new_x="LMARGIN", new_y="NEXT")
|
||
|
|
|
||
|
|
def label_value(self, label, value):
|
||
|
|
self.set_font("Sans", "B", 10)
|
||
|
|
self.set_text_color(40, 40, 40)
|
||
|
|
self.cell(55, 6, label)
|
||
|
|
self.set_font("Sans", "", 10)
|
||
|
|
self.cell(0, 6, value, new_x="LMARGIN", new_y="NEXT")
|
||
|
|
|
||
|
|
def bullet(self, text, indent=10):
|
||
|
|
self.set_font("Sans", "", 9)
|
||
|
|
self.set_text_color(40, 40, 40)
|
||
|
|
self.cell(indent, 5, "\u2022")
|
||
|
|
self.multi_cell(self.w - self.r_margin - self.l_margin - indent, 5, text)
|
||
|
|
self.ln(1)
|
||
|
|
|
||
|
|
def try_image(self, path, max_w=170, max_h=100, caption=None):
|
||
|
|
if not os.path.exists(path):
|
||
|
|
return False
|
||
|
|
from PIL import Image
|
||
|
|
img = Image.open(path)
|
||
|
|
w_px, h_px = img.size
|
||
|
|
ratio = min(max_w / w_px, max_h / h_px)
|
||
|
|
w_mm = w_px * ratio
|
||
|
|
h_mm = h_px * ratio
|
||
|
|
x = (self.w - w_mm) / 2
|
||
|
|
if self.get_y() + h_mm + 15 > self.h - 20:
|
||
|
|
self.add_page()
|
||
|
|
self.image(path, x=x, w=w_mm)
|
||
|
|
if caption:
|
||
|
|
self.set_font("Sans", "I", 8)
|
||
|
|
self.set_text_color(100, 100, 100)
|
||
|
|
self.cell(0, 5, caption, align="C", new_x="LMARGIN", new_y="NEXT")
|
||
|
|
self.ln(3)
|
||
|
|
return True
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
pdf = TalasPDF()
|
||
|
|
pdf.alias_nb_pages()
|
||
|
|
pdf.set_auto_page_break(auto=True, margin=20)
|
||
|
|
|
||
|
|
# =========================================================
|
||
|
|
# PAGE 1 — Personal intro + Veza delivery proof
|
||
|
|
# =========================================================
|
||
|
|
pdf.add_page()
|
||
|
|
|
||
|
|
pdf.set_font("Sans", "B", 20)
|
||
|
|
pdf.set_text_color(20, 20, 20)
|
||
|
|
pdf.cell(0, 12, "Talas — Why I Can Deliver", align="C",
|
||
|
|
new_x="LMARGIN", new_y="NEXT")
|
||
|
|
pdf.set_font("Sans", "", 11)
|
||
|
|
pdf.set_text_color(80, 80, 80)
|
||
|
|
pdf.cell(0, 7, "Evidence of delivery capacity — Nikola Milovanovic — March 2026",
|
||
|
|
align="C", new_x="LMARGIN", new_y="NEXT")
|
||
|
|
pdf.ln(4)
|
||
|
|
|
||
|
|
pdf.personal_text(
|
||
|
|
"This grant asks you to trust that I can transform an assembled prototype into "
|
||
|
|
"a verified, measured, documented open commons. Here is the evidence that I "
|
||
|
|
"finish what I start."
|
||
|
|
)
|
||
|
|
|
||
|
|
# --- Veza ---
|
||
|
|
pdf.section_title("1. I Built Veza Solo — 38 Releases in 5 Months")
|
||
|
|
|
||
|
|
pdf.personal_text(
|
||
|
|
"Veza is a music platform I have been building on my own — Go + Rust + React, "
|
||
|
|
"with streaming, real-time chat, e-commerce, and community features. "
|
||
|
|
"It is currently proprietary and not related to this grant, but it shows "
|
||
|
|
"that I can ship complex software from scratch. Every line of code is mine."
|
||
|
|
)
|
||
|
|
|
||
|
|
pdf.sub_title("What I Shipped")
|
||
|
|
pdf.metric_row("Releases:", "38", "v0.1 through v1.0.2, over 5 months")
|
||
|
|
pdf.metric_row("API endpoints:", "500+", "REST API (Go/Gin)")
|
||
|
|
pdf.metric_row("Database:", "60+ tables", "115 SQL migrations (PostgreSQL)")
|
||
|
|
pdf.metric_row("Frontend:", "661 components", "React 18, TypeScript, 52+ routes")
|
||
|
|
pdf.metric_row("Languages:", "3", "English, French, Spanish")
|
||
|
|
pdf.metric_row("Streaming:", "HLS adaptive", "Rust/Axum, FFmpeg transcoding")
|
||
|
|
pdf.metric_row("Real-time:", "WebSocket", "Chat, notifications, co-listening")
|
||
|
|
pdf.ln(2)
|
||
|
|
|
||
|
|
# Veza repo screenshot
|
||
|
|
pdf.try_image(
|
||
|
|
f"{PHOTOS}/github_veza.png",
|
||
|
|
max_w=170, max_h=85,
|
||
|
|
caption="Veza private repository — 2,210 commits, 36 branches, 51 tags"
|
||
|
|
)
|
||
|
|
|
||
|
|
# Language breakdown
|
||
|
|
pdf.try_image(
|
||
|
|
f"{PHOTOS}/github_lang_prog_graph.png",
|
||
|
|
max_w=100, max_h=40,
|
||
|
|
caption="Codebase: TypeScript 38%, Go 30%, HTML 20%, Rust 6%"
|
||
|
|
)
|
||
|
|
|
||
|
|
# Activity heatmaps side by side
|
||
|
|
pdf.try_image(
|
||
|
|
f"{PHOTOS}/github_activity_2025.png",
|
||
|
|
max_w=170, max_h=45,
|
||
|
|
caption="3,415 contributions in 2025"
|
||
|
|
)
|
||
|
|
pdf.try_image(
|
||
|
|
f"{PHOTOS}/github_activity_2026.png",
|
||
|
|
max_w=170, max_h=45,
|
||
|
|
caption="2,708 contributions in 2026 (January\u2013March only)"
|
||
|
|
)
|
||
|
|
|
||
|
|
# --- Security ---
|
||
|
|
pdf.section_title("2. I Take Security Seriously")
|
||
|
|
|
||
|
|
pdf.personal_text(
|
||
|
|
"My academic background is cybersecurity (BSc EPITA, MSc OTERIA). "
|
||
|
|
"I ran a thorough security audit of Veza using AI-assisted analysis "
|
||
|
|
"(Claude, Anthropic) to systematically review every attack surface."
|
||
|
|
)
|
||
|
|
|
||
|
|
pdf.sub_title("Security Audit (AI-Assisted, October 2025)")
|
||
|
|
pdf.metric_row("Findings:", "36", "identified through systematic review")
|
||
|
|
pdf.metric_row("Remediated:", "36/36", "100% — zero open findings")
|
||
|
|
pdf.metric_row("Critical/High:", "0", "remaining after remediation")
|
||
|
|
pdf.ln(2)
|
||
|
|
|
||
|
|
pdf.sub_title("Security Stack I Implemented")
|
||
|
|
features = [
|
||
|
|
"JWT RS256 authentication (asymmetric key rotation)",
|
||
|
|
"WebAuthn / Passkeys (passwordless login)",
|
||
|
|
"Two-factor authentication (TOTP + backup codes)",
|
||
|
|
"OAuth 2.0 (Google, GitHub, Apple, Facebook)",
|
||
|
|
"CSRF tokens (Redis-backed), rate limiting (3-tier)",
|
||
|
|
"ClamAV antivirus on all uploads",
|
||
|
|
"Coraza WAF with OWASP Core Rule Set",
|
||
|
|
"GDPR: data export, account deletion, anonymization",
|
||
|
|
]
|
||
|
|
for f in features:
|
||
|
|
pdf.bullet(f)
|
||
|
|
pdf.ln(2)
|
||
|
|
|
||
|
|
# =========================================================
|
||
|
|
# PAGE 2 — Infrastructure + Investment + What grant funds
|
||
|
|
# =========================================================
|
||
|
|
|
||
|
|
pdf.section_title("3. I Built the Infrastructure Too")
|
||
|
|
|
||
|
|
pdf.personal_text(
|
||
|
|
"The entire stack runs on hardware I own. No AWS, no Cloudflare, no SaaS. "
|
||
|
|
"Two refurbished Dell servers in my home, managed by Ansible playbooks I wrote. "
|
||
|
|
"Monthly cost: 135 EUR in electricity — versus 800-1,500 EUR in cloud fees."
|
||
|
|
)
|
||
|
|
|
||
|
|
pdf.sub_title("Hardware I Own")
|
||
|
|
pdf.metric_row("Servers:", "2x Dell R720", "rack-mounted, purchased used")
|
||
|
|
pdf.metric_row("RAM:", "768 GB total", "384 GB per server")
|
||
|
|
pdf.metric_row("CPU:", "64 cores total", "2x Xeon E5-2670 per server")
|
||
|
|
pdf.metric_row("Network:", "10 GbE", "inter-server + 1 Gbps fiber")
|
||
|
|
pdf.metric_row("Storage:", "16 HDDs per server", "ZFS mirror pools, ~100 spare HDDs in reserve")
|
||
|
|
pdf.metric_row("Monthly cost:", "~135 EUR", "electricity only")
|
||
|
|
pdf.ln(2)
|
||
|
|
|
||
|
|
pdf.sub_title("Automation I Wrote")
|
||
|
|
pdf.metric_row("Ansible:", "Full automation", "infrastructure-as-code")
|
||
|
|
pdf.metric_row("Deployment:", "Blue-green", "zero-downtime via HAProxy")
|
||
|
|
pdf.metric_row("Monitoring:", "Prometheus + Grafana", "metrics + dashboards")
|
||
|
|
pdf.metric_row("Security:", "Coraza WAF", "OWASP rules, WireGuard VPN")
|
||
|
|
pdf.metric_row("Backup:", "PostgreSQL PITR", "ZFS snapshots, WAL archiving")
|
||
|
|
pdf.ln(4)
|
||
|
|
|
||
|
|
# =========================================================
|
||
|
|
# Personal investment
|
||
|
|
# =========================================================
|
||
|
|
pdf.section_title("4. What I Already Invested (~3,000 EUR)")
|
||
|
|
|
||
|
|
pdf.personal_text(
|
||
|
|
"Before writing this application, I spent approximately 3,000 EUR of my own "
|
||
|
|
"money on equipment, components, and infrastructure. As a student working "
|
||
|
|
"part-time, that is a significant personal investment."
|
||
|
|
)
|
||
|
|
|
||
|
|
investments = [
|
||
|
|
("Measurement lab:", "Rigol oscilloscope, 2 audio interfaces, 2 reference mics, multimeter"),
|
||
|
|
("Fabrication:", "Soldering station, stereo microscope, RoHS consumables"),
|
||
|
|
("Components:", "OPA1642, TC4584BF, passives, PCB fabrication (PCBWay), capsules"),
|
||
|
|
("Servers:", "2x Dell R720 (used), networking, drives"),
|
||
|
|
("Software dev:", "5 months full-time on Veza (unpaid)"),
|
||
|
|
]
|
||
|
|
for label, value in investments:
|
||
|
|
pdf.label_value(label, value)
|
||
|
|
pdf.ln(4)
|
||
|
|
|
||
|
|
# =========================================================
|
||
|
|
# Open-core model
|
||
|
|
# =========================================================
|
||
|
|
pdf.section_title("5. What Stays Open Forever")
|
||
|
|
|
||
|
|
pdf.body_text(
|
||
|
|
"The hardware funded by this grant is published under CERN-OHL-W-2.0. "
|
||
|
|
"This license is irrevocable — even I cannot close it. The commons exist "
|
||
|
|
"permanently, regardless of what happens to the company."
|
||
|
|
)
|
||
|
|
|
||
|
|
pdf.set_font("Sans", "B", 9)
|
||
|
|
pdf.set_fill_color(240, 240, 240)
|
||
|
|
pdf.cell(55, 6, "Artifact", border=1, fill=True)
|
||
|
|
pdf.cell(40, 6, "License", border=1, fill=True)
|
||
|
|
pdf.cell(55, 6, "Status", border=1, fill=True, new_x="LMARGIN", new_y="NEXT")
|
||
|
|
|
||
|
|
pdf.set_font("Sans", "", 9)
|
||
|
|
licenses = [
|
||
|
|
("Microphone schematics (KiCAD)", "CERN-OHL-W-2.0", "Designed, publication pending"),
|
||
|
|
("PCB layouts + Gerber files", "CERN-OHL-W-2.0", "Designed, publication pending"),
|
||
|
|
("Bill of Materials", "CERN-OHL-W-2.0", "Complete, maintained"),
|
||
|
|
("Assembly guide", "CC BY-SA 4.0", "Grant Milestone 4"),
|
||
|
|
("Calibration toolkit (Rust)", "AGPL v3", "Grant Milestone 3"),
|
||
|
|
("Acoustic measurements", "CC BY-SA 4.0", "Grant Milestone 2"),
|
||
|
|
("Veza platform", "Proprietary", "Revenue funds open HW"),
|
||
|
|
]
|
||
|
|
for comp, lic, status in licenses:
|
||
|
|
pdf.cell(55, 5, comp, border=1)
|
||
|
|
pdf.cell(40, 5, lic, border=1)
|
||
|
|
pdf.cell(55, 5, status, border=1, new_x="LMARGIN", new_y="NEXT")
|
||
|
|
pdf.ln(2)
|
||
|
|
|
||
|
|
pdf.personal_text(
|
||
|
|
"The model is simple: hardware knowledge is permanently open (commons). "
|
||
|
|
"Assembled microphones and the Veza platform are commercial products whose "
|
||
|
|
"revenue funds ongoing maintenance of the commons. This is the Arduino / Prusa "
|
||
|
|
"model, adapted for audio."
|
||
|
|
)
|
||
|
|
|
||
|
|
# =========================================================
|
||
|
|
# Summary
|
||
|
|
# =========================================================
|
||
|
|
pdf.section_title("6. Summary")
|
||
|
|
|
||
|
|
pdf.body_text(
|
||
|
|
"I am one person. I designed a microphone, built a music platform, set up "
|
||
|
|
"the infrastructure, wrote the documentation, and invested my own money — "
|
||
|
|
"all before asking for a single euro of funding."
|
||
|
|
)
|
||
|
|
pdf.body_text(
|
||
|
|
"This grant (25,000 EUR) funds the final step: transforming an assembled prototype "
|
||
|
|
"into a reproducible open-hardware commons with professional acoustic measurements "
|
||
|
|
"and complete documentation. If someone builds this from my files and it "
|
||
|
|
"doesn't work, I have failed."
|
||
|
|
)
|
||
|
|
|
||
|
|
# =========================================================
|
||
|
|
# Output
|
||
|
|
# =========================================================
|
||
|
|
pdf.output(OUT)
|
||
|
|
print(f"\nPDF generated: {OUT}")
|
||
|
|
print(f"Pages: {pdf.pages_count}")
|
||
|
|
|
||
|
|
# Check optional screenshot
|
||
|
|
if not os.path.exists(f"{PHOTOS}/veza_screenshot.png"):
|
||
|
|
print("\nOPTIONAL: Add photos/veza_screenshot.png for a Veza platform screenshot.")
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|