#!/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()