generate_magazine.py (view raw)
1#!/usr/bin/env python3
2"""
3Newspaper Magazine Generator
4Proof of concept: generates a magazine-style PDF from RSS feeds
5"""
6
7import re
8import subprocess
9from concurrent.futures import ThreadPoolExecutor
10from pathlib import Path
11
12SCRIPT_DIR = Path(__file__).parent
13LLM_AGGREGATOR = SCRIPT_DIR.parent / "llm_aggregator" / "llm_aggregator"
14OUTPUT_DIR = SCRIPT_DIR / "output"
15TEX_FILE = OUTPUT_DIR / "magazine.tex"
16PDF_FILE = OUTPUT_DIR / "magazine.pdf"
17
18# Strip think tags
19STRIP_TAG_START = "<think>"
20STRIP_TAG_END = "</think>"
21STRIP_TAG_REPLACEMENT = ""
22
23# Base prompt ending
24PROMPT_ENDING = r"""Output in LaTeX format suitable for an editorial section.
25Use asterisk section headers (\section*{...}) to prevent numbering. Use the
26correct 1st January 2001 date format. Use English; not American dialect (color
27-> colour, formalize -> formalise, etc.). Use ``...'' for quotation marks,
28\emph{...} for italics and \textbf{...} for bold text. Do not attempt
29citations."""
30
31CATEGORIES = [
32 {
33 "name": "LINUX",
34 "icon": r"\faLinux",
35 "feed": SCRIPT_DIR / "feeds" / "linux.txt",
36 "prompt": f"""Summarise the latest Linux kernel discussions and
37 technical developments from the LKML feed. Focus on significant
38 patches, kernel updates, and developer discussions. {PROMPT_ENDING}""",
39 },
40 {
41 "name": "FREE SOFTWARE",
42 "icon": r"\faCogs",
43 "feed": SCRIPT_DIR / "feeds" / "free_software.txt",
44 "prompt": f"""Summarise the latest news from the free and open source
45 software world. Cover new releases, project updates, and community
46 news. {PROMPT_ENDING}""",
47 },
48 {
49 "name": r"AI \& SCIENCE",
50 "icon": r"\faFlask",
51 "feed": SCRIPT_DIR / "feeds" / "ai_science.txt",
52 "prompt": f"""Summarise the latest news regarding science and AI. Pay
53 attention to esoteric and difficult topics, rather than what is likely
54 to be easy pop-science without substance. {PROMPT_ENDING}""",
55 },
56 {
57 "name": "POLAND",
58 "icon": r"\faFlag",
59 "feed": SCRIPT_DIR / "feeds" / "poland.txt",
60 "prompt": f"""Summarise today's top news from Poland covering politics,
61 finance, and current events. Include major stories and their
62 significance. {PROMPT_ENDING}""",
63 },
64 {
65 "name": "ESOTERIC",
66 "icon": r"\faUserSecret",
67 "feed": SCRIPT_DIR / "feeds" / "esoteric.txt",
68 "prompt": f"""Summarise news from sources that are not mainstream,
69 regarding topics not often discussed in the public square. The readers
70 enjoy this section, so maintain a supportive tone, rather than
71 accusative. Seek out topics that specifically challenge popular
72 preconceptions. Do not write disclaimers or observations.
73 {PROMPT_ENDING}""",
74 },
75]
76
77
78def strip_tags(content: str) -> str:
79 """Strip content between (and including) two tags. Leaves placeholder."""
80 if STRIP_TAG_START and STRIP_TAG_END:
81 pattern = f"{re.escape(STRIP_TAG_START)}.*?{re.escape(STRIP_TAG_END)}"
82 return re.sub(pattern, STRIP_TAG_REPLACEMENT, content, flags=re.DOTALL)
83 return content
84
85
86def run_llm_aggregator(category: dict) -> dict:
87 """Run llm_aggregator for a category and return the result."""
88 output_file = OUTPUT_DIR / f"{category['name'].lower()}.txt"
89
90 cmd = [
91 str(LLM_AGGREGATOR),
92 "-f", str(category["feed"]),
93 "-p", category["prompt"],
94 "-P",
95 "--output-file", str(output_file),
96 "-v",
97 ]
98
99 print(f"📰 Processing: {category['name']}")
100 result = subprocess.run(cmd, capture_output=True, text=True)
101
102 if result.returncode != 0:
103 print(f" ❌ Error: {result.stderr}")
104 raise RuntimeError(f"llm_aggregator failed for {category['name']}")
105
106 content = output_file.read_text()
107 content = strip_tags(content)
108
109 print(f" ✓ Done: {category['name']}")
110 return {"name": category["name"], "icon": category["icon"], "content": content}
111
112
113def generate_latex(content_files: list[dict]) -> None:
114 """Generate the LaTeX document."""
115 latex = r"""\documentclass[12pt,a4paper]{article}
116\usepackage{fancyhdr, fontawesome, fontspec, graphicx, geometry, sectsty, setspace, parskip}
117\usepackage[UKenglish]{isodate}
118\setmainfont[Ligatures=TeX]{Liberation Sans}
119\geometry{margin=2.5cm}
120\sectionfont{\centering\bfseries\scshape}
121\subsectionfont{\large\bfseries}
122\pagestyle{fancy}
123\fancyhf{}
124\fancyhead[C]{\today}
125\fancyfoot[C]{\thepage}
126
127\begin{document}
128\onehalfspacing
129
130\vspace*{2cm}
131{\LARGE\bfseries The Daily Digest}\\[0.5em]
132{\Large \today}\\[2em]
133\hrule
134\vspace{2em}
135"""
136
137 for item in content_files:
138 name = item["name"]
139 icon = item["icon"]
140 content = item["content"]
141
142 latex += f"{{\\centering\\fontsize{{64}}{{12}}{icon}\\par}}\n"
143 latex += f"\\section*{{{name}}}\n"
144 latex += content
145 latex += "\n\n\\vspace{1.5em}\n\n"
146
147 latex += r"\end{document}"
148
149 with open(TEX_FILE, "w") as f:
150 f.write(latex)
151
152 print(f" Created: {TEX_FILE}")
153
154
155def compile_pdf() -> bool:
156 """Compile LaTeX to PDF."""
157 print()
158 print("🔄 Compiling PDF...")
159
160 result = subprocess.run(
161 ["xelatex", "-interaction=batchmode", "magazine.tex"],
162 cwd=OUTPUT_DIR,
163 capture_output=True,
164 )
165
166 return PDF_FILE.exists()
167
168
169def main():
170 OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
171
172 print("🗞️ Newspaper Magazine Generator")
173 print("===================================")
174 print()
175
176 # Run all feeds through llm_aggregator in parallel
177 print("📰 Processing feeds in parallel...")
178 print()
179 with ThreadPoolExecutor(max_workers=8) as executor:
180 content_files = list(executor.map(run_llm_aggregator, CATEGORIES))
181
182 print()
183 print("📄 Generating LaTeX magazine...")
184 print()
185
186 generate_latex(content_files)
187 print()
188
189 if compile_pdf():
190 print("✅ Magazine generated successfully!")
191 print()
192 print(f" PDF: {PDF_FILE}")
193 print()
194 print("📖 Opening PDF viewer...")
195 subprocess.run(["xdg-open", str(PDF_FILE)])
196 else:
197 print("❌ PDF compilation failed")
198 return 1
199
200 return 0
201
202
203if __name__ == "__main__":
204 exit(main())