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>
334 lines
13 KiB
Python
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()
|