From 8693d2ffa975fc18ff5d7059ab61992f999e0c71 Mon Sep 17 00:00:00 2001 From: Solaria Date: Sat, 14 Mar 2026 13:16:09 +0000 Subject: [PATCH] Add LaTeX compile support with templates (memoir, academic) --- opus_orchestrator/__init__.py | 15 + opus_orchestrator/latex_compile.py | 323 ++++++++++++++++++ .../templates/latex/academic.tex | 152 +++++++++ opus_orchestrator/templates/latex/base.tex | 123 +++++++ opus_orchestrator/templates/latex/memoir.tex | 135 ++++++++ 5 files changed, 748 insertions(+) create mode 100644 opus_orchestrator/latex_compile.py create mode 100644 opus_orchestrator/templates/latex/academic.tex create mode 100644 opus_orchestrator/templates/latex/base.tex create mode 100644 opus_orchestrator/templates/latex/memoir.tex diff --git a/opus_orchestrator/__init__.py b/opus_orchestrator/__init__.py index 1dfa1f9..6521919 100644 --- a/opus_orchestrator/__init__.py +++ b/opus_orchestrator/__init__.py @@ -159,3 +159,18 @@ __all__ = [ "ExportOptions", "ExportOptions", ] + +def __getattr__(name): + if name == "LaTeXExporter": + from opus_orchestrator.latex_compile import LaTeXExporter + return LaTeXExporter + if name == "CompileOptions": + from opus_orchestrator.latex_compile import CompileOptions + return CompileOptions + if name == "export_to_latex": + from opus_orchestrator.latex_compile import export_to_latex + return export_to_latex + if name == "compile_pdf": + from opus_orchestrator.latex_compile import compile_pdf + return compile_pdf + raise AttributeError(f"module has no attribute {name!r}") diff --git a/opus_orchestrator/latex_compile.py b/opus_orchestrator/latex_compile.py new file mode 100644 index 0000000..91e19d4 --- /dev/null +++ b/opus_orchestrator/latex_compile.py @@ -0,0 +1,323 @@ +"""LaTeX Compile for Opus Orchestrator. + +Handles conversion to LaTeX and PDF compilation. +""" + +import os +import re +import subprocess +import shutil +from pathlib import Path +from typing import Optional +from dataclasses import dataclass, field +from datetime import datetime + +from opus_orchestrator.schemas import Manuscript, Chapter + + +@dataclass +class CompileOptions: + """Options for LaTeX compilation.""" + template: str = "memoir" # memoir, academic, base + output_format: str = "pdf" # pdf, tex + theme: str = "light" # light, dark, sepia + font: str = "serif" # serif, sans, mono + include_toc: bool = True + include_index: bool = False + dedication: str = "" + epigraph: str = "" + acknowledgments: str = "" + abstract: str = "" + bibliography: str = "" + author: str = "Opus Orchestrator" + publisher: str = "" + isbn: str = "" + edition: str = "" + series: str = "" + date: str = "" + + def __post_init__(self): + if not self.date: + self.date = datetime.now().strftime("%Y") + + +class LaTeXExporter: + """Export manuscript to LaTeX and compile to PDF.""" + + TEMPLATES = { + "memoir": "memoir.tex", + "academic": "academic.tex", + "base": "base.tex", + } + + def __init__(self, template_dir: Optional[str] = None): + if template_dir: + self.template_dir = Path(template_dir) + else: + # Default to package templates + self.template_dir = Path(__file__).parent / "templates" / "latex" + + def export( + self, + manuscript: Manuscript, + book_title: str, + options: Optional[CompileOptions] = None, + ) -> dict: + """Export manuscript to LaTeX. + + Args: + manuscript: The Manuscript to export + book_title: Title for the book + options: CompileOptions + + Returns: + Export metadata with file paths + """ + opts = options or CompileOptions(template="memoir") + + # Get template + template_file = self.TEMPLATES.get(opts.template, "memoir.tex") + template_path = self.template_dir / template_file + + if not template_path.exists(): + raise FileNotFoundError(f"Template not found: {template_path}") + + # Read template + template_content = template_path.read_text() + + # Build body from chapters + body = self._build_body(manuscript) + + # Fill template + latex_content = self._fill_template( + template_content, + body, + book_title, + opts, + ) + + return { + "latex": latex_content, + "template": opts.template, + "options": opts, + } + + def export_to_file( + self, + manuscript: Manuscript, + book_title: str, + output_path: str, + options: Optional[CompileOptions] = None, + ) -> dict: + """Export to .tex file.""" + result = self.export(manuscript, book_title, options) + + output_file = Path(output_path) + output_file.parent.mkdir(parents=True, exist_ok=True) + output_file.write_text(result["latex"]) + + result["output_file"] = str(output_file) + return result + + def compile( + self, + manuscript: Manuscript, + book_title: str, + output_path: str, + options: Optional[CompileOptions] = None, + ) -> dict: + """Export and compile to PDF. + + Args: + manuscript: The Manuscript + book_title: Book title + output_path: Output .pdf path + options: CompileOptions + + Returns: + Compilation result with paths + """ + # First export to tex + tex_path = output_path.replace(".pdf", ".tex") + result = self.export_to_file(manuscript, book_title, tex_path, options) + + # Try to compile + compile_result = self._compile_latex(tex_path) + + result.update(compile_result) + result["pdf_path"] = output_path if compile_result.get("success") else None + + return result + + def _build_body(self, manuscript: Manuscript) -> str: + """Build LaTeX body from chapters.""" + parts = [] + + for chapter in manuscript.chapters: + # Chapter heading + parts.append(f"\\chapter{{{chapter.title}}}") + parts.append("") + + # Content (convert markdown to latex) + content = self._markdown_to_latex(chapter.content or "") + parts.append(content) + parts.append("") + + return "\n".join(parts) + + def _markdown_to_latex(self, text: str) -> str: + """Convert basic markdown to LaTeX.""" + # Headers + text = re.sub(r'^### (.+)$', r'\\subsection{\1}', text, flags=re.MULTILINE) + text = re.sub(r'^## (.+)$', r'\\section{\1}', text, flags=re.MULTILINE) + text = re.sub(r'^# (.+)$', r'\\chapter{\1}', text, flags=re.MULTILINE) + + # Bold/italic + text = re.sub(r'\*\*\*(.+?)\*\*\*', r'\\textbf{\\textit{\1}}', text) + text = re.sub(r'\*\*(.+?)\*\*', r'\\textbf{\1}', text) + text = re.sub(r'\*(.+?)\*', r'\\textit{\1}', text) + + # Code blocks + text = re.sub(r'```(\w+)?\n(.+?)```', r'\\begin{verbatim}\2\\end{verbatim}', text, flags=re.DOTALL) + + # Inline code + text = re.sub(r'`(.+?)`', r'\\texttt{\1}', text) + + # Lists + text = re.sub(r'^- (.+)$', r'\\item \1', text, flags=re.MULTILINE) + text = re.sub(r'^\d+\. (.+)$', r'\\item \1', text, flags=re.MULTILINE) + + # Blockquotes + text = re.sub(r'^> (.+)$', r'\\begin{quote}\1\\end{quote}', text, flags=re.MULTILINE) + + # Horizontal rule + text = re.sub(r'^---$', r'\\hrulefill', text, flags=re.MULTILINE) + + return text + + def _fill_template( + self, + template: str, + body: str, + book_title: str, + options: CompileOptions, + ) -> str: + """Fill template with content.""" + # Build replacements + replacements = { + "book_title": book_title, + "author": options.author, + "date": options.date, + "publisher": options.publisher, + "isbn": options.isbn, + "edition": options.edition, + "series": options.series, + "dedication": options.dedication, + "epigraph": options.epigraph, + "acknowledgments": options.acknowledgments, + "abstract": options.abstract, + "bibliography": options.bibliography, + "body": body, + } + + # Fill template - handle both ${var} and $var + content = template + for key, value in replacements.items(): + # Replace ${var} + content = content.replace(f"${{{key}}}", str(value)) + # Replace standalone $var + dollar_key = f"${key}" + content = content.replace(dollar_key, str(value)) + + return content + + def _compile_latex(self, tex_path: str) -> dict: + """Compile LaTeX to PDF.""" + tex_file = Path(tex_path) + if not tex_file.exists(): + return {"success": False, "error": "TeX file not found"} + + # Check for xelatex + xelatex = shutil.which("xelatex") + if not xelatex: + return { + "success": False, + "error": "xelatex not found. Install with: brew install texlab or apt install texlive-xelatex", + "tex_file": str(tex_file), + } + + # Compile + work_dir = tex_file.parent + + try: + # Run xelatex + result = subprocess.run( + [xelatex, "-interaction=nonstopmode", tex_file.name], + cwd=work_dir, + capture_output=True, + text=True, + timeout=120, + ) + + success = result.returncode == 0 + + return { + "success": success, + "returncode": result.returncode, + "stdout": result.stdout[-2000:] if result.stdout else "", + "stderr": result.stderr[-2000:] if result.stderr else "", + "tex_file": str(tex_file), + } + + except subprocess.TimeoutExpired: + return {"success": False, "error": "Compilation timeout"} + except Exception as e: + return {"success": False, "error": str(e)} + + +def export_to_latex( + manuscript: Manuscript, + book_title: str, + output_path: str, + template: str = "memoir", + **options, +) -> dict: + """Convenience function to export to LaTeX. + + Args: + manuscript: The Manuscript + book_title: Book title + output_path: Output .tex path + template: Template name (memoir, academic, base) + **options: Additional CompileOptions + + Returns: + Export result + """ + opts = CompileOptions(template=template, **options) + exporter = LaTeXExporter() + return exporter.export_to_file(manuscript, book_title, output_path, opts) + + +def compile_pdf( + manuscript: Manuscript, + book_title: str, + output_path: str, + template: str = "memoir", + **options, +) -> dict: + """Convenience function to compile to PDF. + + Args: + manuscript: The Manuscript + book_title: Book title + output_path: Output .pdf path + template: Template name + **options: Additional CompileOptions + + Returns: + Compilation result + """ + opts = CompileOptions(template=template, **options) + exporter = LaTeXExporter() + return exporter.compile(manuscript, book_title, output_path, opts) diff --git a/opus_orchestrator/templates/latex/academic.tex b/opus_orchestrator/templates/latex/academic.tex new file mode 100644 index 0000000..45e85f4 --- /dev/null +++ b/opus_orchestrator/templates/latex/academic.tex @@ -0,0 +1,152 @@ +% Opus Orchestrator - Academic/Textbook Template +% Technical, educational, academic works + +\documentclass[12pt,oneside]{book} + +% Packages +\usepackage[utf8]{inputenc} +\usepackage[T1]{fontenc} +\usepackage{amsmath,amssymb} +\usepackage{graphicx} +\usepackage{hyperref} +\usepackage{geometry} +\usepackage{makeidx} +\usepackage{natbib} +\usepackage{titlesec} +\usepackage{booktabs} +\usepackage{listings} +\usepackage{fancyvrb} +\usepackage{multicol} + +% Geometry +\geometry{ + paperwidth=8.5in, + paperheight=11in, + textwidth=6in, + textheight=9in, + left=1in, + right=1in, + top=1in, + bottom=1in, +} + +% Hyperref +\hypersetup{ + colorlinks=true, + linkcolor=blue, + citecolor=blue, + urlcolor=blue, +} + +% Chapter formatting +\titleformat{\chapter}[display] + {\normalfont\LARGE\bfseries} + {\chaptertitlename\ \thechapter}{20pt}{\LARGE\bfseries} + +\titleformat{\section} + {\normalfont\Large\bfseries} + {\thesection}{12pt}{\Large\bfseries} + +\titleformat{\subsection} + {\normalfont\large\bfseries} + {\thesubsection}{12pt}{\large\bfseries} + +% Headers +\usepackage{fancyhdr} +\pagestyle{fancy} +\fancyhf{} +\fancyhead[LE]{\thepage} +\fancyhead[RO]{\thepage} +\fancyhead[RE]{\leftmark} +\fancyhead[LO]{\rightmark} + +% Code listings +\lstset{ + basicstyle=\ttfamily\small, + frame=single, + backgroundcolor=\color{lightgray}, +} + +% Index +\makeindex + +% Book metadata +\def\booktitle{$book_title$} +\def\bookauthor{$author$} +\def\bookdate{$date$} +\def\bookpublisher{$publisher$} +\def\bookedition{$edition$} + +\begin{document} + +% Front matter +\frontmatter + +% Title page +\begin{titlepage} +\begin{center} +\vspace*{1in} +{\Huge\bfseries\booktitle\par} +\vspace{0.5in} +{\Large\bookauthor\par} +\vfill +{\large\bookedition}\\ +\bigskip +{\large\bookpublisher}\\ +{\large\bookdate} +\end{titlepage} + +% Copyright +\thispagestyle{empty} +\vspace*{\fill} +Copyright \textcopyright\ \bookdate\ \bookauthor\\ +All rights reserved. +\vfill + +% Dedication +$if(dedication)$ +\clearpage +\begin{center} +\textit{$dedication$} +\end{center} +$endif$ + +% Acknowledgments +$if(acknowledgments)$ +\chapter{Acknowledgments} +$acknowledgments$ +$endif$ + +% Abstract +$if(abstract)$ +\chapter{Abstract} +$abstract$ +$endif$ + +% Table of contents +\tableofcontents + +% List of figures +\listoffigures + +% List of tables +\listoftables + +% Main matter +\mainmatter + +$body$ + +% Back matter +\backmatter + +% Bibliography +$if(bibliography)$ +\bibliographystyle{plainnat} +\bibliography{$bibliography$} +$endif$ + +% Index +\printindex + +\end{document} diff --git a/opus_orchestrator/templates/latex/base.tex b/opus_orchestrator/templates/latex/base.tex new file mode 100644 index 0000000..c33b5f4 --- /dev/null +++ b/opus_orchestrator/templates/latex/base.tex @@ -0,0 +1,123 @@ +% Opus Orchestrator - Base LaTeX Template +% Auto-generated book template + +\documentclass[12pt,openany]{book} + +% Packages +\usepackage[utf8]{inputenc} +\usepackage[T1]{fontenc} +\usepackage{amsmath,amssymb} +\usepackage{graphicx} +\usepackage{hyperref} +\usepackage{geometry} +\usepackage{fancyhdr} +\usepackage{titlesec} +\usepackage{indentfirst} +\usepackage{epigraph} +\usepackage{changepage} + +% Geometry +\geometry{ + paperwidth=6in, + paperheight=9in, + textwidth=4.5in, + textheight=7in, + headheight=12pt, + headsep=12pt, + footskip=24pt, + marginparwidth=0pt, +} + +% Hyperref +\hypersetup{ + colorlinks=true, + linkcolor=black, + citecolor=black, + urlcolor=black, +} + +% Title formatting +\titleformat{\chapter}[display] + {\normalfont\LARGE\bfseries} + {\chaptertitlefont\thechapter}{1em}{\chaptertitlefont\titlefont} + +\titleformat{\section} + {\normalfont\Large\bfseries} + {\thesection}{1em}{} + +\titleformat{\subsection} + {\normalfont\large\bfseries} + {\thesubsection}{1em}{} + +% Headers +\fancyhf{} +\fancyhead[LE]{\nouppercase{\thechapter}} +\fancyhead[RO]{\nouppercase{\rightmark}} +\fancyfoot[C]{\thepage} +\pagestyle{fancy} + +% Commands +\newcommand{\chaptertitlefont}{\scshape} +\newcommand{\titlefont}{\scshape} + +% Book metadata (filled by Opus) +\def\booktitle{$book_title$} +\def\bookauthor{$author$} +\def\bookdate{$date$} +\def\bookpublisher{$publisher$} +\def\bookisbn{$isbn$} + +\begin{document} + +% Frontmatter +\frontmatter + +% Title page +\begin{titlepage} +\begin{center} +\vspace*{1in} +{\Huge\scshape\booktitle\par} +\vspace{2in} +{\Large\scshape\bookauthor\par} +\vspace{1in} +{\large\scshape\bookdate\par} +\vfill +\end{titlepage} + +% Copyright page +\thispagestyle{empty} +\vspace*{\fill} +\begin{center} +Copyright \textcopyright\ \bookdate\ \bookauthor\\ +All rights reserved.\\ +\bookisbn +\end{center} +\vspace*{\fill} + +% Dedication (if any) +$if(dedication)$ +\cleardoublepage +\thispagestyle{empty} +\vspace*{3in} +\begin{center} +\textit{$dedication$} +\end{center} +$endif$ + +% Table of contents +\tableofcontents + +% Main body +\mainmatter + +$body$ + +% Backmatter +\backmatter + +% Bibliography (if any) +$if(bibliography)$ +\printbibliography +$endif$ + +\end{document} diff --git a/opus_orchestrator/templates/latex/memoir.tex b/opus_orchestrator/templates/latex/memoir.tex new file mode 100644 index 0000000..8acd45e --- /dev/null +++ b/opus_orchestrator/templates/latex/memoir.tex @@ -0,0 +1,135 @@ +% Opus Orchestrator - Memoir Template +% Novel, memoir, personal narrative + +\documentclass[12pt,openany]{memoir} + +% Packages +\usepackage[utf8]{inputenc} +\usepackage[T1]{fontenc} +\usepackage{amsmath,amssymb} +\usepackage{graphicx} +\usepackage{hyperref} +\usepackage{geometry} +\usepackage{xcolor} +\usepackage{lettrine} + +% Geometry - trade book format +\geometry{ + paperwidth=5.5in, + paperheight=8.5in, + textwidth=4in, + textheight=7in, + inner=0.75in, + outer=0.5in, + top=0.75in, + bottom=0.75in, +} + +% Hyperref +\hypersetup{ + colorlinks=false, +} + +% Memoir settings +\chapterstyle{levado} +\sectionstyle{hang} +\subsectionstyle{runin} + +% Drop caps +\def\rubricum#1{{\let\A\firstchar #1}} +\let\firstchar= + +% Custom chapter style +\makechapterstyle{levado}{ + \def\chapterheadstart{} + \def\beforechaptitle{\centering\spacedallcaps} + \def\chapternumber{\chapnumfont\thechapter} + \def\afterchaptitle{\par\nobreak\vskip 1.5em} +} + +% Epigraphs +\epigraphwidth=3in +\epigraphsize\small + +% Headers +\nouppercaseheads +\headsep=12pt +\makepagestyle{opus} +\makepsmarks{opus}{ + \createmark{chapter}{left}{shownumber}{\chaptertitleformat}{\ } + + \createmark{section}{right}{shownumber}{\sectiontitleformat}{\ } + \nobreak\Cfoot{\thepage}\or + \nobreak\cefoot{\thepage}\or + \nobreak\cofoot{\thepage} +} +\pagestyle{opus} + +% Book metadata +\def\booktitle{$book_title$} +\def\bookauthor{$author$} +\def\bookdate{$date$} +\def\bookpublisher{$publisher$} +\def\bookseries{$series$} + +\begin{document} + +% Half title +\thispagestyle{empty} +\begin{center} +\vspace*{2in} +{\Huge\scshape\booktitle\par} +\end{center} + +% Title page +\clearpage +\thispagestyle{empty} +\begin{center} +\vspace*{1in} +{\Huge\scshape\booktitle\par} +\vspace{2in} +{\Large\scshape\bookauthor\par} +\vfill +{\large\scshape\bookpublisher\par} +{\large\scshape\bookdate\par} +\end{center} + +% Copyright +\clearpage +\thispagestyle{empty} +\vspace*{\fill} +\begin{center} +Copyright \textcopyright\ \bookdate\ \bookauthor\\ +All rights reserved.\\ +ISBN: $isbn$ +\end{center} +\vspace*{\fill} + +% Dedication +$if(dedication)$ +\clearpage +\thispagestyle{empty} +\vspace*{3in} +\begin{center} +\textit{$dedication$} +\end{center} +$endif$ + +% Epigraph +$if(epigraph)$ +\clearpage +\begin{center} +\textit{$epigraph$} +\end{center} +$endif$ + +% Contents +\clearpage +\tableofcontents* + +\clearpage +\mainmatter + +$body$ + +\end{document}