Add TeX Live API client and deployment configs
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
# TeX Live API
|
||||
|
||||
Lightweight LaTeX compilation API.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pip install flask
|
||||
|
||||
# Install LaTeX
|
||||
# macOS
|
||||
brew install texlive
|
||||
|
||||
# Ubuntu
|
||||
sudo apt install texlive-xelatex
|
||||
|
||||
# Run
|
||||
python texlive_api.py
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Compile
|
||||
curl -X POST http://localhost:8080/compile \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"tex": "\\documentclass{article}\\n\\begin{document}\\nHello\\n\\end{document}"}'
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
@@ -0,0 +1,18 @@
|
||||
# TeX Live API Service
|
||||
# Run with: docker-compose up -d
|
||||
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
texlive-api:
|
||||
image: alex11br/texlive-api
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- texlive-cache:/root/.texlive
|
||||
environment:
|
||||
- TEXLIVE_ENGINE=xelatex
|
||||
- MAX_TIMEOUT=180
|
||||
|
||||
volumes:
|
||||
texlive-cache:
|
||||
@@ -0,0 +1,74 @@
|
||||
# Simple TeX Live API Service
|
||||
# Run: python texlive_api.py
|
||||
|
||||
from flask import Flask, request, jsonify, send_file
|
||||
import subprocess
|
||||
import tempfile
|
||||
import os
|
||||
import base64
|
||||
from pathlib import Path
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
ALLOWED_ENGINES = ['xelatex', 'pdflatex', 'lualatex']
|
||||
MAX_TIMEOUT = 180
|
||||
|
||||
|
||||
@app.route('/health', methods=['GET'])
|
||||
def health():
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
|
||||
@app.route('/compile', methods=['POST'])
|
||||
def compile():
|
||||
data = request.get_json()
|
||||
|
||||
tex_content = data.get('tex')
|
||||
engine = data.get('engine', 'xelatex')
|
||||
timeout = min(data.get('timeout', MAX_TIMEOUT), MAX_TIMEOUT)
|
||||
|
||||
if not tex_content:
|
||||
return jsonify({"error": "No tex content provided"}), 400
|
||||
|
||||
if engine not in ALLOWED_ENGINES:
|
||||
return jsonify({"error": f"Invalid engine: {engine}"}), 400
|
||||
|
||||
# Create temp directory
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
tex_file = Path(tmpdir) / "input.tex"
|
||||
tex_file.write_text(tex_content)
|
||||
|
||||
# Compile
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[engine, "-interaction=nonstopmode", tex_file.name],
|
||||
cwd=tmpdir,
|
||||
capture_output=True,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
pdf_file = tex_file.with_suffix('.pdf')
|
||||
|
||||
if pdf_file.exists():
|
||||
pdf_data = base64.b64encode(pdf_file.read_bytes()).decode()
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"pdf": pdf_data,
|
||||
"engine": engine,
|
||||
})
|
||||
else:
|
||||
# Return error log
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "PDF not generated",
|
||||
"log": result.stderr.decode()[-2000:],
|
||||
}), 500
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
return jsonify({"error": "Compilation timeout"}), 504
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=8080)
|
||||
@@ -0,0 +1,100 @@
|
||||
"""TeX Live API Client for Opus Orchestrator.
|
||||
|
||||
Compiles LaTeX via remote TeX Live API service.
|
||||
"""
|
||||
|
||||
import json
|
||||
import base64
|
||||
from typing import Optional, Dict, Any
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class TeXLiveClient:
|
||||
"""Client for TeX Live API service."""
|
||||
|
||||
def __init__(self, base_url: str = "http://localhost:8080"):
|
||||
"""Initialize TeX Live client.
|
||||
|
||||
Args:
|
||||
base_url: Base URL of TeX Live API service
|
||||
"""
|
||||
self.base_url = base_url.rstrip("/")
|
||||
|
||||
def compile(
|
||||
self,
|
||||
tex_content: str,
|
||||
engine: str = "xelatex",
|
||||
timeout: int = 120,
|
||||
) -> Dict[str, Any]:
|
||||
"""Compile LaTeX via API.
|
||||
|
||||
Args:
|
||||
tex_content: LaTeX source code
|
||||
engine: LaTeX engine (xelatex, pdflatex, lualatex)
|
||||
timeout: Compilation timeout in seconds
|
||||
|
||||
Returns:
|
||||
Compilation result with PDF data
|
||||
"""
|
||||
import requests
|
||||
|
||||
response = requests.post(
|
||||
f"{self.base_url}/compile",
|
||||
json={
|
||||
"tex": tex_content,
|
||||
"engine": engine,
|
||||
"timeout": timeout,
|
||||
},
|
||||
timeout=timeout + 10,
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
raise RuntimeError(f"TeX Live API error: {response.text}")
|
||||
|
||||
result = response.json()
|
||||
|
||||
if result.get("error"):
|
||||
raise RuntimeError(f"LaTeX compilation failed: {result['error']}")
|
||||
|
||||
return result
|
||||
|
||||
def compile_file(
|
||||
self,
|
||||
tex_path: str,
|
||||
engine: str = "xelatex",
|
||||
) -> bytes:
|
||||
"""Compile LaTeX file via API.
|
||||
|
||||
Args:
|
||||
tex_path: Path to .tex file
|
||||
engine: LaTeX engine
|
||||
|
||||
Returns:
|
||||
Compiled PDF as bytes
|
||||
"""
|
||||
tex_content = Path(tex_path).read_text()
|
||||
result = self.compile(tex_content, engine)
|
||||
|
||||
# Decode PDF from base64
|
||||
pdf_data = base64.b64decode(result["pdf"])
|
||||
return pdf_data
|
||||
|
||||
|
||||
def compile_via_texlive(
|
||||
tex_content: str,
|
||||
base_url: str = "http://localhost:8080",
|
||||
engine: str = "xelatex",
|
||||
) -> bytes:
|
||||
"""Convenience function to compile LaTeX via TeX Live API.
|
||||
|
||||
Args:
|
||||
tex_content: LaTeX source
|
||||
base_url: TeX Live API URL
|
||||
engine: LaTeX engine
|
||||
|
||||
Returns:
|
||||
Compiled PDF bytes
|
||||
"""
|
||||
client = TeXLiveClient(base_url)
|
||||
result = client.compile(tex_content, engine)
|
||||
return base64.b64decode(result["pdf"])
|
||||
Reference in New Issue
Block a user