118 lines
5.4 KiB
Python
118 lines
5.4 KiB
Python
from pathlib import Path
|
|
import re, html
|
|
from reportlab.lib.pagesizes import letter
|
|
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
|
from reportlab.lib import colors
|
|
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Preformatted, ListFlowable, ListItem
|
|
from reportlab.lib.units import inch
|
|
|
|
base = Path.home()/'.timmy'/'reports'/'notebooklm'/'2026-03-27-hermes-openclaw'
|
|
|
|
styles = getSampleStyleSheet()
|
|
styles.add(ParagraphStyle(name='Body2', parent=styles['BodyText'], fontName='Helvetica', fontSize=10.5, leading=14, spaceAfter=6))
|
|
styles.add(ParagraphStyle(name='H1x', parent=styles['Heading1'], fontName='Helvetica-Bold', fontSize=20, leading=24, spaceAfter=12, textColor=colors.HexColor('#111111')))
|
|
styles.add(ParagraphStyle(name='H2x', parent=styles['Heading2'], fontName='Helvetica-Bold', fontSize=15, leading=19, spaceBefore=8, spaceAfter=8, textColor=colors.HexColor('#222222')))
|
|
styles.add(ParagraphStyle(name='H3x', parent=styles['Heading3'], fontName='Helvetica-Bold', fontSize=12.5, leading=16, spaceBefore=6, spaceAfter=6))
|
|
styles.add(ParagraphStyle(name='CodeBlockX', parent=styles['Code'], fontName='Courier', fontSize=8.5, leading=10.5, leftIndent=12, rightIndent=12, backColor=colors.HexColor('#F4F4F4'), borderPadding=6, spaceAfter=8))
|
|
styles.add(ParagraphStyle(name='MetaX', parent=styles['BodyText'], fontName='Helvetica-Oblique', fontSize=9.5, leading=12, textColor=colors.HexColor('#555555'), spaceAfter=6))
|
|
url_re = re.compile(r'(https?://[^\s)]+)')
|
|
|
|
def linkify(text: str) -> str:
|
|
esc = html.escape(text)
|
|
return url_re.sub(lambda m: f'<link href="{m.group(1)}" color="blue">{m.group(1)}</link>', esc)
|
|
|
|
def markdown_to_story(md_path: Path):
|
|
text = md_path.read_text()
|
|
lines = text.splitlines()
|
|
story = []
|
|
i = 0
|
|
while i < len(lines):
|
|
line = lines[i]
|
|
if not line.strip():
|
|
i += 1
|
|
continue
|
|
if line.startswith('```'):
|
|
i += 1
|
|
buf = []
|
|
while i < len(lines) and not lines[i].startswith('```'):
|
|
buf.append(lines[i])
|
|
i += 1
|
|
if i < len(lines) and lines[i].startswith('```'):
|
|
i += 1
|
|
story.append(Preformatted('\n'.join(buf), styles['CodeBlockX']))
|
|
continue
|
|
if line.startswith('# '):
|
|
story.append(Paragraph(linkify(line[2:].strip()), styles['H1x']))
|
|
i += 1
|
|
continue
|
|
if line.startswith('## '):
|
|
story.append(Paragraph(linkify(line[3:].strip()), styles['H2x']))
|
|
i += 1
|
|
continue
|
|
if line.startswith('### '):
|
|
story.append(Paragraph(linkify(line[4:].strip()), styles['H3x']))
|
|
i += 1
|
|
continue
|
|
if re.match(r'^-\s+', line):
|
|
items = []
|
|
while i < len(lines) and re.match(r'^-\s+', lines[i]):
|
|
item_text = re.sub(r'^-\s+', '', lines[i]).strip()
|
|
items.append(ListItem(Paragraph(linkify(item_text), styles['Body2'])))
|
|
i += 1
|
|
story.append(ListFlowable(items, bulletType='bullet', leftIndent=18))
|
|
story.append(Spacer(1, 6))
|
|
continue
|
|
if re.match(r'^\d+\.\s+', line):
|
|
items = []
|
|
while i < len(lines) and re.match(r'^\d+\.\s+', lines[i]):
|
|
item_text = re.sub(r'^\d+\.\s+', '', lines[i]).strip()
|
|
items.append(ListItem(Paragraph(linkify(item_text), styles['Body2'])))
|
|
i += 1
|
|
story.append(ListFlowable(items, bulletType='1', leftIndent=18))
|
|
story.append(Spacer(1, 6))
|
|
continue
|
|
buf = [line.strip()]
|
|
i += 1
|
|
while i < len(lines) and lines[i].strip() and not any([
|
|
lines[i].startswith('# '), lines[i].startswith('## '), lines[i].startswith('### '),
|
|
lines[i].startswith('```'), re.match(r'^-\s+', lines[i]), re.match(r'^\d+\.\s+', lines[i])
|
|
]):
|
|
buf.append(lines[i].strip())
|
|
i += 1
|
|
paragraph = ' '.join(buf)
|
|
style = styles['MetaX'] if paragraph.startswith('Date:') or paragraph.startswith('Audience:') or paragraph.startswith('Subject:') else styles['Body2']
|
|
story.append(Paragraph(linkify(paragraph), style))
|
|
return story
|
|
|
|
def add_footer(canvas, doc):
|
|
canvas.saveState()
|
|
canvas.setFont('Helvetica', 8)
|
|
canvas.setFillColor(colors.HexColor('#666666'))
|
|
canvas.drawString(doc.leftMargin, 0.45*inch, f'{doc.title}')
|
|
canvas.drawRightString(letter[0]-doc.rightMargin, 0.45*inch, f'Page {doc.page}')
|
|
canvas.restoreState()
|
|
|
|
files = [
|
|
('hermes-agent-backlog-vision-report.md', 'Hermes Agent Research Report'),
|
|
('openclaw-backlog-vision-report.md', 'OpenClaw Research Report'),
|
|
]
|
|
for md_name, title in files:
|
|
md_path = base/md_name
|
|
pdf_path = base/md_name.replace('.md', '.pdf')
|
|
story = markdown_to_story(md_path)
|
|
doc = SimpleDocTemplate(str(pdf_path), pagesize=letter, leftMargin=0.7*inch, rightMargin=0.7*inch, topMargin=0.75*inch, bottomMargin=0.7*inch, title=title)
|
|
doc.build(story, onFirstPage=add_footer, onLaterPages=add_footer)
|
|
|
|
(base/'README.txt').write_text(
|
|
'NotebookLM packet\n\n'
|
|
'Generated files:\n'
|
|
'- hermes-agent-backlog-vision-report.pdf\n'
|
|
'- openclaw-backlog-vision-report.pdf\n\n'
|
|
'Source markdown:\n'
|
|
'- hermes-agent-backlog-vision-report.md\n'
|
|
'- openclaw-backlog-vision-report.md\n'
|
|
)
|
|
|
|
for p in sorted(base.iterdir()):
|
|
print(f'{p.name}\t{p.stat().st_size}')
|