latexee / app.py
Samarth Naik
init: latex v2
6bf1ab9
Raw
History Blame Contribute Delete
4.63 kB
import os
import shutil
import subprocess
import tempfile
import threading
from pathlib import Path
from typing import Iterable
import gradio as gr
PORT = int(os.environ.get("PORT", "7860"))
OUTPUT_TTL_SECONDS = 300
def _copy_inputs(paths: Iterable[str], destination: Path) -> None:
for raw_path in paths:
if not raw_path:
continue
source_path = Path(raw_path)
shutil.copy(source_path, destination / source_path.name)
def _run_command(command: list[str], cwd: Path, timeout: int) -> subprocess.CompletedProcess[str]:
return subprocess.run(
command,
cwd=cwd,
capture_output=True,
text=True,
check=True,
timeout=timeout,
)
def _schedule_cleanup(file_path: Path) -> None:
def _cleanup() -> None:
try:
file_path.unlink(missing_ok=True)
file_path.parent.rmdir()
except OSError:
pass
threading.Timer(OUTPUT_TTL_SECONDS, _cleanup).start()
def compile_latex(
tex_file_path: str,
attachment_paths: list[str] | None,
double_render: bool,
):
if not tex_file_path:
raise gr.Error("Upload a .tex file to compile.")
source_path = Path(tex_file_path)
if source_path.suffix.lower() != ".tex":
raise gr.Error("Only .tex files are supported.")
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
working_tex = temp_path / source_path.name
_copy_inputs([tex_file_path], temp_path)
_copy_inputs(attachment_paths or [], temp_path)
latexmk_command = [
"latexmk",
"-pdf",
"-interaction=nonstopmode",
"-halt-on-error",
working_tex.name,
]
pdflatex_command = [
"pdflatex",
"-interaction=nonstopmode",
"-halt-on-error",
"-recorder",
working_tex.name,
]
try:
result = _run_command(latexmk_command, temp_path, 120)
if double_render:
extra_result = _run_command(pdflatex_command, temp_path, 120)
result = subprocess.CompletedProcess(
args=pdflatex_command,
returncode=extra_result.returncode,
stdout=(result.stdout or "") + "\n\n" + (extra_result.stdout or ""),
stderr=(result.stderr or "") + "\n\n" + (extra_result.stderr or ""),
)
except subprocess.CalledProcessError as exc:
log_output = (exc.stdout or "") + "\n" + (exc.stderr or "")
raise gr.Error(
"LaTeX compilation failed.\n\n"
f"{log_output.strip() or 'No compiler output was captured.'}"
) from exc
except subprocess.TimeoutExpired as exc:
raise gr.Error("LaTeX compilation timed out after 120 seconds.") from exc
pdf_path = working_tex.with_suffix(".pdf")
if not pdf_path.exists():
compiler_output = (result.stdout or "") + "\n" + (result.stderr or "")
raise gr.Error(
"Compilation finished without producing a PDF.\n\n"
f"{compiler_output.strip() or 'No compiler output was captured.'}"
)
final_output = Path(tempfile.mkdtemp()) / f"rendered-{source_path.stem}.pdf"
shutil.copy(pdf_path, final_output)
_schedule_cleanup(final_output)
return str(final_output)
with gr.Blocks(title="Latexee") as demo:
gr.Markdown(
"""
# Latexee
Upload a main `.tex` file plus any supporting assets like `.jpg`, `.png`,
`.bib`, `.sty`, or extra TeX files, and get back the rendered PDF.
"""
)
with gr.Row():
tex_input = gr.File(
label="LaTeX source",
file_count="single",
file_types=[".tex"],
type="filepath",
)
attachment_input = gr.File(
label="Supporting files",
file_count="multiple",
type="filepath",
)
double_render_input = gr.Checkbox(
label="Double render",
info="Run one extra pdflatex pass after the normal compile.",
value=False,
)
pdf_output = gr.File(label="Rendered PDF")
compile_button = gr.Button("Render PDF", variant="primary")
compile_button.click(
fn=compile_latex,
inputs=[tex_input, attachment_input, double_render_input],
outputs=pdf_output,
)
if __name__ == "__main__":
demo.launch(server_name="0.0.0.0", server_port=PORT)