talas-group/09_MODELE_ECONOMIQUE/Subventions/generate_pdf1_hardware.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

334 lines
13 KiB
Python

#!/usr/bin/env python3
"""
Generate PDF 1: Talas Microphone — Hardware Evidence
For NGI Zero Commons Fund application attachment.
"""
from fpdf import FPDF
from PIL import Image
import os
BASE = "/home/senke/Documents/TG__Talas_Group"
ALICE = f"{BASE}/02_PRODUITS_PHYSIQUES/Microphone/Conception/AliceOPA"
OUT = f"{BASE}/09_MODELE_ECONOMIQUE/Subventions/talas-microphone-hardware.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 Application — Attachment 1", 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 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 add_image_centered(self, path, max_w=170, max_h=100, caption=None):
if not os.path.exists(path):
self.body_text(f"[Image not found: {os.path.basename(path)}]")
return
is_pdf = path.lower().endswith(".pdf")
if is_pdf:
# For PDF files, just use a fixed width and let fpdf2 handle it
w_mm = min(max_w, 170)
x = (self.w - w_mm) / 2
if self.get_y() + max_h + 15 > self.h - 20:
self.add_page()
self.image(path, x=x, w=w_mm)
else:
img = Image.open(path)
w_px, h_px = img.size
# Calculate dimensions to fit within max bounds
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
# Check if we need a new page
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 main():
pdf = TalasPDF()
pdf.alias_nb_pages()
pdf.set_auto_page_break(auto=True, margin=20)
# =========================================================
# PAGE 1 — Title + Overview + PCB Photos
# =========================================================
pdf.add_page()
# Title
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", align="C",
new_x="LMARGIN", new_y="NEXT")
pdf.ln(6)
# Project summary
pdf.section_title("1. Project Overview")
pdf.body_text(
"Talas One is an open-hardware large-diaphragm condenser microphone designed for "
"independent musicians, podcasters, and makers. The design uses an OPA1642 low-noise "
"operational amplifier in a balanced output configuration, powered by standard 48V "
"phantom power via XLR 5-pin connector. The body is machined aluminum."
)
pdf.body_text(
"All hardware designs are published under CERN-OHL-W-2.0. The companion software "
"platform (Veza) is licensed under AGPL-3.0."
)
# Specs table
pdf.section_title("2. Technical Specifications")
specs = [
("Type:", "Large-diaphragm condenser"),
("Preamp IC:", "OPA1642 (Texas Instruments) — dual low-noise JFET op-amp"),
("Output:", "Balanced (differential, via IC1A + IC1B)"),
("Power:", "48V phantom power"),
("Connector:", "XLR 5-pin"),
("Body:", "Machined aluminum"),
("PCB design tool:", "KiCAD 8"),
("PCB count:", "2 boards (preamp + hex inverter / DC-DC)"),
("License:", "CERN-OHL-W-2.0"),
("Status:", "Functional prototype assembled"),
]
for label, value in specs:
pdf.label_value(label, value)
pdf.ln(4)
# PCB photos
pdf.section_title("3. Fabricated PCBs")
pdf.body_text(
"Custom PCBs fabricated by PCBWay. Two revisions exist in the design history "
"(14 retro-backups from January to February 2025, showing iterative refinement)."
)
# Dual channel front + back side by side
front_path = f"{ALICE}/front.jpg"
back_path = f"{ALICE}/back.jpg"
if os.path.exists(front_path) and os.path.exists(back_path):
y_start = pdf.get_y()
half_w = 60
# front
pdf.image(front_path, x=12, y=y_start, w=half_w)
# back
pdf.image(back_path, x=105, y=y_start, w=half_w)
# Get height of tallest image
front_img = Image.open(front_path)
ratio_f = half_w / front_img.size[0]
h_f = front_img.size[1] * ratio_f
pdf.set_y(y_start + h_f + 2)
pdf.set_font("Sans", "I", 8)
pdf.set_text_color(100, 100, 100)
pdf.cell(95, 5, "OPA Alice Dual Channel v1.0 — Front", align="C")
pdf.cell(95, 5, "OPA Alice Dual Channel v1.0 — Back (traces)", align="C",
new_x="LMARGIN", new_y="NEXT")
pdf.ln(3)
# Single channel (red board)
pdf.body_text("Single-channel variant (v1.0) — compact version for mono microphone:")
single_front = f"{ALICE}/single_front.jpg"
single_back = f"{ALICE}/single_back.jpg"
if os.path.exists(single_front) and os.path.exists(single_back):
y_start = pdf.get_y()
half_w = 42
pdf.image(single_front, x=25, y=y_start, w=half_w)
pdf.image(single_back, x=115, y=y_start, w=half_w)
s_img = Image.open(single_front)
ratio_s = half_w / s_img.size[0]
h_s = s_img.size[1] * ratio_s
pdf.set_y(y_start + h_s + 2)
pdf.set_font("Sans", "I", 8)
pdf.set_text_color(100, 100, 100)
pdf.cell(95, 5, "Single channel — Front", align="C")
pdf.cell(95, 5, "Single channel — Back", align="C",
new_x="LMARGIN", new_y="NEXT")
pdf.ln(5)
# =========================================================
# Schematics (continues naturally, no forced page break)
# =========================================================
pdf.section_title("4. Circuit Schematics")
pdf.body_text(
"Preamp circuit: OPA1642 dual JFET op-amp in balanced configuration. "
"IC1A handles the capsule signal (1G\u03A9 bias resistor R11), IC1B provides "
"the differential output. Phantom power filtering via R1/R2 (47\u03A9), "
"Zener voltage regulation (D1/D2/D3), and decoupling capacitors (C1-C7)."
)
pdf.add_image_centered(
f"{ALICE}/AliceOPA_single_channel_schematic_export.png",
max_w=175, max_h=110,
caption="Fig. 1 -- OPA Alice single-channel preamp schematic (KiCAD export)"
)
pdf.ln(3)
pdf.body_text(
"Full condenser microphone signal path — from capsule to XLR output, "
"showing the complete connection diagram:"
)
pdf.add_image_centered(
f"{ALICE}/full_cond.png",
max_w=120, max_h=80,
caption="Fig. 2 — Complete condenser microphone wiring diagram"
)
# =========================================================
# Hex inverter + BOM + What grant funds (natural flow)
# =========================================================
pdf.body_text(
"Hex inverter DC-DC converter board — provides bias voltage for the "
"condenser capsule using a CMOS hex inverter as voltage multiplier:"
)
pdf.add_image_centered(
f"{ALICE}/hex_sch.png",
max_w=170, max_h=70,
caption="Fig. 3 — Hex inverter DC-DC converter schematic"
)
pdf.ln(2)
# Assembled board photo
pdf.body_text("Assembled dual-channel board with components populated:")
pdf.add_image_centered(
f"{ALICE}/opa_alice_3.png",
max_w=170, max_h=65,
caption="Fig. 4 — Assembled OPA Alice boards (dual channel, components populated)"
)
pdf.ln(2)
pdf.section_title("5. Bill of Materials (Summary)")
pdf.body_text(
"Complete BOM maintained as a structured spreadsheet (inventaires_composants_bom_origin_project.ods) "
"with part numbers, quantities, supplier references, and pricing. Key components:"
)
bom_items = [
("OPA1642AIDR", "Dual JFET op-amp (TI)", "SOIC-8", "Mouser/Farnell"),
("TZX12D-TR", "12V Zener diode (Vishay)", "SOD-123", "Mouser"),
("TZX6V2D-TR", "6.2V Zener diode (Vishay)", "SOD-123", "Mouser"),
("47uF/63V", "Electrolytic capacitor", "Radial", "Mouser/Farnell"),
("47uF/50V", "Electrolytic capacitor", "Radial", "Mouser/Farnell"),
("0.1uF/63V", "Film capacitor (Wima MKS)", "Radial", "Mouser"),
("22nF/50V", "Ceramic capacitor (Vishay)", "Radial", "Mouser"),
("1G\u03A9", "Bias resistor", "Axial", "Mouser"),
("47\u03A9, 2K2, 47K, 200R", "Signal path resistors", "Various", "Mouser/Farnell"),
]
# Table header
pdf.set_font("Sans", "B", 8)
pdf.set_fill_color(240, 240, 240)
pdf.cell(35, 6, "Part", border=1, fill=True)
pdf.cell(50, 6, "Description", border=1, fill=True)
pdf.cell(25, 6, "Package", border=1, fill=True)
pdf.cell(40, 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(35, 5, part, border=1)
pdf.cell(50, 5, desc, border=1)
pdf.cell(25, 5, pkg, border=1)
pdf.cell(40, 5, src, border=1, new_x="LMARGIN", new_y="NEXT")
pdf.ln(3)
# =========================================================
# What the grant funds
# =========================================================
pdf.section_title("6. What This Grant Funds")
pdf.body_text(
"The prototype is functional but not yet reproducible as an open commons. "
"This grant funds the missing steps:"
)
steps = [
("DONE", "Circuit design (OPA1642 preamp, hex inverter DC-DC)"),
("DONE", "KiCAD schematics and PCB layout (2 boards)"),
("DONE", "PCB fabrication and assembly"),
("DONE", "Component sourcing with BOM documentation"),
("DONE", "Functional prototype verification"),
("GRANT", "Professional acoustic measurements (freq response, SNR, THD, SPL)"),
("GRANT", "Open-source calibration toolkit (Rust, FFT-based)"),
("GRANT", "Cleaned schematics + Gerber files for public release"),
("GRANT", "Step-by-step assembly guide with photos"),
("GRANT", "Reproducibility validation (independent test builds)"),
("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(14, 5, "[DONE]")
else:
pdf.set_font("Sans", "B", 9)
pdf.set_text_color(180, 100, 20)
pdf.cell(14, 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")
pdf.ln(2)
pdf.set_font("Sans", "I", 8)
pdf.set_text_color(100, 100, 100)
pdf.multi_cell(0, 4,
"Design history: 14 versioned KiCAD backups (Jan 11 - Feb 23, 2025) show iterative "
"refinement. Component datasheets archived for all critical parts."
)
# =========================================================
# Output
# =========================================================
pdf.output(OUT)
print(f"PDF generated: {OUT}")
print(f"Pages: {pdf.pages_count}")
if __name__ == "__main__":
main()