Add LaTeX compile support with templates (memoir, academic)

This commit is contained in:
Solaria
2026-03-14 13:16:09 +00:00
parent c42556d147
commit 8693d2ffa9
5 changed files with 748 additions and 0 deletions
+15
View File
@@ -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}")
+323
View File
@@ -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)
@@ -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}
+123
View File
@@ -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}
@@ -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}