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)