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