talas-group/09_MODELE_ECONOMIQUE/Subventions/generate_pdf1_hardware_v2.py
senke 66471934af Initial commit: Talas Group project management & documentation
Knowledge base of ~80+ markdown files across 14 domains (00-13),
Logseq graph, hardware design files (KiCAD), infrastructure configs,
and talas-wiki static site.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 20:10:41 +02:00

398 lines
16 KiB
Python

#!/usr/bin/env python3
"""
Generate PDF 1 (V2): Talas Microphone — Hardware Evidence
For NGI Zero Commons Fund application (25K EUR, personal tone).
PHOTOS: Place your own photographs in the photos/ directory:
photos/prototype_assembled.jpg — The assembled microphone (full view)
photos/mic_body_detached.jpg — Microphone body disassembled (modularity)
photos/capsule_sc_600.jpg — SC-600 capsule close-up
photos/pcb_preamp.jpg — Your preamp PCB (close-up)
photos/pcb_hex.jpg — Your hex inverter PCB (close-up)
photos/lab_setup.jpg — Your workbench with oscilloscope, tools
If a photo is missing, the script prints a warning and skips it.
KiCAD screenshots are used as-is (they're your own exports).
"""
from fpdf import FPDF
from PIL import Image
import os
BASE = "/home/senke/Documents/TG__Talas_Group"
PHOTOS = f"{BASE}/09_MODELE_ECONOMIQUE/Subventions/photos"
CONCEPTION = f"{BASE}/02_PRODUITS_PHYSIQUES/Microphone/Conception"
OUT = f"{BASE}/09_MODELE_ECONOMIQUE/Subventions/talas-microphone-hardware-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 1: Hardware", 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 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):
"""Italic personal note — first person."""
self.set_font("Sans", "I", 10)
self.set_text_color(60, 60, 60)
self.multi_cell(0, 5, text)
self.ln(2)
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 try_image(self, path, max_w=170, max_h=100, caption=None):
"""Add image if it exists, skip with warning if not."""
if not os.path.exists(path):
name = os.path.basename(path)
print(f" [SKIP] Photo missing: {name} — add it to photos/ and regenerate")
self.set_font("Sans", "I", 9)
self.set_text_color(180, 80, 80)
self.cell(0, 6, f"[Photo placeholder: {name}]",
new_x="LMARGIN", new_y="NEXT")
self.set_text_color(40, 40, 40)
self.ln(2)
return
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)
def try_image_pair(self, path_l, path_r, w=60, max_h=120, cap_l="", cap_r=""):
"""Two images side by side, height-constrained."""
exists_l = os.path.exists(path_l)
exists_r = os.path.exists(path_r)
if not exists_l and not exists_r:
self.try_image(path_l) # will show placeholder
return
y_start = self.get_y()
h_max = 0
def fit(path, target_w, target_max_h):
img = Image.open(path)
w_px, h_px = img.size
ratio = min(target_w / w_px, target_max_h / h_px)
return target_w if (target_w / w_px) <= (target_max_h / h_px) else w_px * ratio, h_px * ratio
if exists_l:
fw, fh = fit(path_l, w, max_h)
h_max = max(h_max, fh)
if y_start + fh + 20 > self.h - 20:
self.add_page()
y_start = self.get_y()
self.image(path_l, x=12, y=y_start, w=fw, h=fh)
if exists_r:
fw, fh = fit(path_r, w, max_h)
h_max = max(h_max, fh)
self.image(path_r, x=105, y=y_start, w=fw, h=fh)
self.set_y(y_start + h_max + 2)
self.set_font("Sans", "I", 8)
self.set_text_color(100, 100, 100)
if cap_l or cap_r:
self.cell(95, 5, cap_l, align="C")
self.cell(95, 5, cap_r, align="C", new_x="LMARGIN", new_y="NEXT")
self.ln(3)
def main():
pdf = TalasPDF()
pdf.alias_nb_pages()
pdf.set_auto_page_break(auto=True, margin=20)
# =========================================================
# PAGE 1 — Title + Personal intro + Prototype photo
# =========================================================
pdf.add_page()
pdf.set_font("Sans", "B", 20)
pdf.set_text_color(20, 20, 20)
pdf.cell(0, 12, "Talas One — Open Hardware Condenser Microphone",
align="C", new_x="LMARGIN", new_y="NEXT")
pdf.set_font("Sans", "", 11)
pdf.set_text_color(80, 80, 80)
pdf.cell(0, 7, "Hardware Evidence — March 2026 — Nikola Milovanovic",
align="C", new_x="LMARGIN", new_y="NEXT")
pdf.ln(6)
# Personal intro
pdf.section_title("1. What I Built")
pdf.personal_text(
"I designed and assembled this microphone from scratch. The circuit topology "
"is inspired by the Alice OPA design by DJJules (Instructables / JLI Electronics) "
"— a proven hex inverter + OPA preamp approach from the DIY audio community. "
"I modularized it into two independent PCBs in KiCAD and had them fabricated."
)
pdf.body_text(
"The prototype does not produce audio output yet — that is where this grant "
"starts. Debugging the circuit is Milestone 1."
)
# Prototype photo
pdf.try_image(
f"{PHOTOS}/prototype_assembled.jpg",
max_w=150, max_h=100,
caption="Fig. 1 — Assembled Talas One prototype (my photograph)"
)
# Detached body — shows modularity / repairability
pdf.personal_text(
"The body is designed for disassembly. Each internal module (capsule, "
"preamp board, hex board) can be removed, inspected, or replaced individually — "
"this is core to the repairable, modular philosophy."
)
pdf.try_image(
f"{PHOTOS}/mic_body_detached.jpg",
max_w=140, max_h=90,
caption="Fig. 2 — Disassembled view of the microphone body"
)
# =========================================================
# Specs
# =========================================================
pdf.section_title("2. Technical Specifications")
specs = [
("Type:", "Large-diaphragm condenser"),
("Capsule:", "t.bone SC-600 (34mm large-diaphragm, Thomann)"),
("Preamp:", "OPA1642AID (TI) — JFET-input, low-noise"),
("Circuit:", "AliceOPA Rev3 (inspired by DJJules / JLI Electronics)"),
("Architecture:", "2 independent PCBs (preamp + hex inverter)"),
("Power:", "Phantom 48V via XLR"),
("Connector:", "XLR 5-pin, balanced output"),
("Body:", "Aluminum (reconditioned)"),
("PCB design:", "KiCAD 8 — full project files available"),
("License:", "CERN-OHL-W-2.0 (irrevocable)"),
("Status:", "Assembled, awaiting debugging (Milestone 1)"),
]
for label, value in specs:
pdf.label_value(label, value)
pdf.ln(4)
# Capsule close-up
pdf.try_image(
f"{PHOTOS}/capsule_sc_600.jpg",
max_w=130, max_h=85,
caption="Fig. 3 — t.bone SC-600 capsule (34mm large-diaphragm condenser)"
)
# =========================================================
# PCB Photos (user's own)
# =========================================================
pdf.section_title("3. My PCBs")
pdf.personal_text(
"Both boards were designed in KiCAD, fabricated by PCBWay, and hand-assembled "
"by me with SMD and through-hole components. The dual-PCB architecture means "
"if one component fails, you replace that component — not the whole microphone."
)
pdf.try_image(
f"{PHOTOS}/pcb_preamp.jpg",
max_w=100, max_h=100,
caption="Preamp board — OPA1642AID (my photograph)"
)
pdf.try_image(
f"{PHOTOS}/pcb_hex.jpg",
max_w=130, max_h=70,
caption="Hex inverter board — TC4584BF (my photograph)"
)
# =========================================================
# KiCAD schematics (user's own exports)
# =========================================================
pdf.section_title("4. Circuit Design (KiCAD)")
# KiCAD screenshots — these ARE Nikola's own work
kicad_screens = [
(f"{CONCEPTION}/Screenshot From 2026-03-29 16-54-27.png",
"Fig. 4 — KiCAD PCB layouts: preamp board + hex inverter board"),
(f"{CONCEPTION}/mic_hex_inverter_pcb_prototype_p1/Screenshot From 2026-03-29 16-54-55.png",
"Fig. 5 — Hex inverter circuit schematic (KiCAD)"),
(f"{CONCEPTION}/mic_preamp_pcb_prototype_p1/Screenshot From 2026-03-29 16-55-17.png",
"Fig. 6 — Preamp circuit schematic (KiCAD)"),
]
for path, caption in kicad_screens:
pdf.try_image(path, max_w=170, max_h=90, caption=caption)
pdf.ln(1)
# =========================================================
# Lab setup
# =========================================================
pdf.section_title("5. My Lab (~3,000 EUR Self-Funded)")
pdf.personal_text(
"I built the complete measurement and fabrication lab with my own money. "
"Every instrument listed below was purchased specifically for this project. "
"This grant funds the work, not the tools — the tools are already here."
)
equipment = [
("Oscilloscope:", "Rigol DHO814 (12-bit, 100 MHz, 4-channel)"),
("Audio interface 1:", "Audient iD14 (reference-grade preamps)"),
("Audio interface 2:", "Behringer UMC-202HD (budget comparison)"),
("Reference mic 1:", "Rode NT1-A (studio standard)"),
("Reference mic 2:", "Power Edge Vibe C1 (budget baseline)"),
("Multimeter:", "Voltcraft VC-23"),
("Soldering:", "Toolcraft ST-100D (temperature controlled)"),
("Microscope:", "Perfex Sciences stereo (SMD inspection)"),
("Consumables:", "Lead-free solder, flux, desoldering — all RoHS"),
]
for label, value in equipment:
pdf.label_value(label, value)
pdf.ln(2)
pdf.try_image(
f"{PHOTOS}/lab_setup.jpg",
max_w=170, max_h=90,
caption="Fig. 7 — My workbench (oscilloscope, soldering station, microscope)"
)
# =========================================================
# BOM summary
# =========================================================
pdf.section_title("6. Bill of Materials (Key Components)")
bom_items = [
("OPA1642AID", "Dual JFET op-amp (TI)", "SOIC-8", "Mouser"),
("TC4584BF", "Hex inverter (Toshiba)", "SOIC-14", "Mouser"),
("t.bone SC-600", "34mm LDC capsule", "Through-hole", "Thomann"),
("47\u03bcF/63V", "Electrolytic (Panasonic)", "Radial", "Mouser"),
("1G\u03A9", "Capsule bias resistor", "Axial", "Mouser"),
("TZX12D-TR", "12V Zener (Vishay)", "SOD-123", "Mouser"),
("1N4148", "Signal diode (onsemi)", "SOD-323", "Mouser"),
("Neutrik NC5MAV", "XLR 5-pin connector", "Panel mount", "Mouser/Thomann"),
]
pdf.set_font("Sans", "B", 8)
pdf.set_fill_color(240, 240, 240)
pdf.cell(38, 6, "Part", border=1, fill=True)
pdf.cell(48, 6, "Description", border=1, fill=True)
pdf.cell(25, 6, "Package", border=1, fill=True)
pdf.cell(35, 6, "Sourcing", border=1, fill=True, new_x="LMARGIN", new_y="NEXT")
pdf.set_font("Sans", "", 8)
for part, desc, pkg, src in bom_items:
pdf.cell(38, 5, part, border=1)
pdf.cell(48, 5, desc, border=1)
pdf.cell(25, 5, pkg, border=1)
pdf.cell(35, 5, src, border=1, new_x="LMARGIN", new_y="NEXT")
pdf.ln(2)
pdf.set_font("Sans", "I", 8)
pdf.set_text_color(100, 100, 100)
pdf.multi_cell(0, 4,
"Full BOM with exact quantities, prices, and sourcing links maintained as "
"structured spreadsheet. All components available from EU distributors."
)
pdf.ln(3)
# =========================================================
# What grant funds
# =========================================================
pdf.section_title("7. What This Grant Funds (25,000 EUR)")
pdf.personal_text(
"Everything above this line I did on my own time and money. "
"The grant funds the path from assembled prototype to verified, "
"measured, and independently reproducible open commons."
)
steps = [
("DONE", "Circuit design (AliceOPA Rev3, inspired by DJJules)"),
("DONE", "KiCAD schematics and PCB layout (2 boards)"),
("DONE", "PCB fabrication (PCBWay) and hand-assembly"),
("DONE", "Component sourcing with complete BOM"),
("DONE", "Lab + infra purchased (~3K EUR self-funded)"),
("GRANT", "Circuit debugging and PCB revision if needed (M1)"),
("GRANT", "Capsule characterisation: multiple units + evaluate alternatives (M2)"),
("GRANT", "Open-source calibration toolkit in Rust (M3)"),
("GRANT", "Complete hardware publication under CERN-OHL-W (M4)"),
("GRANT", "Self-rebuild test from published files (M5)"),
("GRANT", "Reproducibility testing by 1-2 independent builders (M6)"),
("GRANT", "OSHWA registration"),
]
for status, desc in steps:
if status == "DONE":
pdf.set_font("Sans", "B", 9)
pdf.set_text_color(40, 140, 40)
pdf.cell(16, 5, "[DONE]")
else:
pdf.set_font("Sans", "B", 9)
pdf.set_text_color(180, 100, 20)
pdf.cell(16, 5, "[GRANT]")
pdf.set_font("Sans", "", 9)
pdf.set_text_color(40, 40, 40)
pdf.cell(0, 5, desc, new_x="LMARGIN", new_y="NEXT")
# =========================================================
# Output
# =========================================================
pdf.output(OUT)
print(f"\nPDF generated: {OUT}")
print(f"Pages: {pdf.pages_count}")
# Check for missing photos
expected = [
"prototype_assembled.jpg",
"mic_body_detached.jpg",
"capsule_sc_600.jpg",
"pcb_preamp.jpg",
"pcb_hex.jpg",
"lab_setup.jpg",
]
missing = [f for f in expected if not os.path.exists(f"{PHOTOS}/{f}")]
if missing:
print(f"\nMISSING PHOTOS ({len(missing)}):")
for f in missing:
print(f" photos/{f}")
print("Add these photos and run again for the complete PDF.")
if __name__ == "__main__":
main()