guyba commited on
Commit
145743e
ยท
verified ยท
1 Parent(s): a9e7096

Upload 4 files

Browse files
Files changed (4) hide show
  1. Dockerfile.txt +25 -0
  2. README.md +111 -6
  3. app.py +278 -0
  4. requirements.txt +4 -0
Dockerfile.txt ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ # Install system dependencies
4
+ RUN apt-get update && apt-get install -y \
5
+ build-essential \
6
+ curl \
7
+ && rm -rf /var/lib/apt/lists/*
8
+
9
+ # Set working directory
10
+ WORKDIR /app
11
+
12
+ # Copy requirements first (for better caching)
13
+ COPY requirements.txt .
14
+
15
+ # Install Python dependencies
16
+ RUN pip install --no-cache-dir -r requirements.txt
17
+
18
+ # Copy application code
19
+ COPY app.py .
20
+
21
+ # Expose port 7860 (HuggingFace Spaces default)
22
+ EXPOSE 7860
23
+
24
+ # Run the application
25
+ CMD ["python", "app.py"]
README.md CHANGED
@@ -1,12 +1,117 @@
1
  ---
2
- title: Marker
3
- emoji: โšก
4
- colorFrom: yellow
5
- colorTo: blue
6
  sdk: docker
 
 
7
  pinned: false
8
  license: mit
9
- short_description: Use Marker to convert PDFs to Markdown
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Marker PDF Converter
3
+ emoji: ๐Ÿ“„
4
+ colorFrom: blue
5
+ colorTo: purple
6
  sdk: docker
7
+ sdk_version: "3.10"
8
+ app_file: app.py
9
  pinned: false
10
  license: mit
 
11
  ---
12
 
13
+ # Marker PDF Converter
14
+
15
+ Convert PDF files to clean, LLM-optimized Markdown using [Marker AI](https://github.com/VikParuchuri/marker).
16
+
17
+ This Space provides a **FREE REST API** powered by HuggingFace's GPU infrastructure.
18
+
19
+ ## Usage
20
+
21
+ ### API Endpoints
22
+
23
+ **Convert PDF:**
24
+ ```bash
25
+ POST https://YOUR-USERNAME-marker-pdf-converter.hf.space/marker
26
+ ```
27
+
28
+ **Check Status:**
29
+ ```bash
30
+ GET https://YOUR-USERNAME-marker-pdf-converter.hf.space/status/{request_id}
31
+ ```
32
+
33
+ ### Example
34
+
35
+ ```python
36
+ import requests
37
+
38
+ # Upload PDF
39
+ with open("document.pdf", "rb") as f:
40
+ response = requests.post(
41
+ "https://YOUR-USERNAME-marker-pdf-converter.hf.space/marker",
42
+ files={"file": f},
43
+ data={
44
+ "output_format": "markdown",
45
+ "paginate": "false"
46
+ }
47
+ )
48
+
49
+ request_id = response.json()["request_id"]
50
+
51
+ # Poll for result
52
+ import time
53
+ while True:
54
+ status_response = requests.get(
55
+ f"https://YOUR-USERNAME-marker-pdf-converter.hf.space/status/{request_id}"
56
+ )
57
+ data = status_response.json()
58
+
59
+ if data["status"] == "complete":
60
+ markdown = data["markdown"]
61
+ print(markdown)
62
+ break
63
+ elif data["status"] == "error":
64
+ print(f"Error: {data['error']}")
65
+ break
66
+
67
+ time.sleep(2)
68
+ ```
69
+
70
+ ### JavaScript Example
71
+
72
+ ```javascript
73
+ // Upload PDF
74
+ const formData = new FormData();
75
+ formData.append('file', pdfFile);
76
+ formData.append('output_format', 'markdown');
77
+
78
+ const response = await fetch(
79
+ 'https://YOUR-USERNAME-marker-pdf-converter.hf.space/marker',
80
+ {
81
+ method: 'POST',
82
+ body: formData
83
+ }
84
+ );
85
+
86
+ const { request_id } = await response.json();
87
+
88
+ // Poll for result
89
+ while (true) {
90
+ const statusResponse = await fetch(
91
+ `https://YOUR-USERNAME-marker-pdf-converter.hf.space/status/${request_id}`
92
+ );
93
+ const data = await statusResponse.json();
94
+
95
+ if (data.status === 'complete') {
96
+ console.log(data.markdown);
97
+ break;
98
+ } else if (data.status === 'error') {
99
+ console.error(data.error);
100
+ break;
101
+ }
102
+
103
+ await new Promise(resolve => setTimeout(resolve, 2000));
104
+ }
105
+ ```
106
+
107
+ ## Parameters
108
+
109
+ | Parameter | Type | Default | Description |
110
+ |-----------|------|---------|-------------|
111
+ | `file` | File | Required | PDF file to convert |
112
+ | `output_format` | String | "markdown" | Output format (currently only markdown) |
113
+ | `langs` | String | null | Language hints (e.g., "English,Spanish") |
114
+ | `paginate` | Boolean | false | Add page separators |
115
+ | `disable_image_extraction` | Boolean | false | Skip extracting images |
116
+ | `use_llm` | Boolean | false | Use LLM for better quality (slower, requires `api_key`) |
117
+ | `api_key` | String | null | Gemini API key (required if `use_llm=true`) |
app.py ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Marker PDF Converter - HuggingFace Space
3
+ Free GPU-powered PDF to Markdown conversion
4
+
5
+ This Space runs on HuggingFace's free GPU tier (NVIDIA T4)
6
+ and provides a REST API for the AI Doc Prep website.
7
+ """
8
+
9
+ import os
10
+ import uuid
11
+ import subprocess
12
+ import tempfile
13
+ import shutil
14
+ from pathlib import Path
15
+ from typing import Optional, Dict, Any
16
+ from fastapi import FastAPI, File, UploadFile, Form, HTTPException
17
+ from fastapi.middleware.cors import CORSMiddleware
18
+ from fastapi.responses import JSONResponse
19
+
20
+ # Initialize FastAPI
21
+ app = FastAPI(
22
+ title="Marker PDF Converter",
23
+ description="Free GPU-powered PDF to Markdown conversion using Marker AI",
24
+ version="1.0.0"
25
+ )
26
+
27
+ # Get base URL from environment or use default
28
+ BASE_URL = os.environ.get("BASE_URL", "https://huggingface.co/spaces/YOUR-USERNAME/marker-pdf-converter")
29
+
30
+ # Configure CORS - allow all origins for public API
31
+ # You can restrict this to your domain later: ["https://ai-doc-prep.com"]
32
+ allowed_origins_str = os.environ.get("ALLOWED_ORIGINS", "*")
33
+ if allowed_origins_str == "*":
34
+ allowed_origins = ["*"]
35
+ else:
36
+ allowed_origins = [origin.strip() for origin in allowed_origins_str.split(",") if origin.strip()]
37
+
38
+ app.add_middleware(
39
+ CORSMiddleware,
40
+ allow_origins=allowed_origins,
41
+ allow_credentials=True,
42
+ allow_methods=["*"],
43
+ allow_headers=["*"],
44
+ )
45
+
46
+ # File size limit (200MB to match Marker API)
47
+ MAX_PDF_FILE_SIZE = 200 * 1024 * 1024 # 200MB in bytes
48
+
49
+ # In-memory job storage
50
+ jobs: Dict[str, Dict[str, Any]] = {}
51
+
52
+ # Temp directories
53
+ UPLOAD_DIR = Path(tempfile.gettempdir()) / "marker_uploads"
54
+ OUTPUT_DIR = Path(tempfile.gettempdir()) / "marker_outputs"
55
+ UPLOAD_DIR.mkdir(exist_ok=True)
56
+ OUTPUT_DIR.mkdir(exist_ok=True)
57
+
58
+
59
+ def str_to_bool(value: str) -> bool:
60
+ """Convert string to boolean."""
61
+ if value is None:
62
+ return False
63
+ return value.lower() in ('true', '1', 'yes', 'on')
64
+
65
+
66
+ @app.get("/")
67
+ async def root():
68
+ """Health check and info endpoint."""
69
+ return {
70
+ "status": "online",
71
+ "service": "Marker PDF Converter",
72
+ "gpu": "NVIDIA T4" if os.path.exists("/dev/nvidia0") else "CPU",
73
+ "mode": "HuggingFace Space (Free)",
74
+ "active_jobs": len(jobs),
75
+ "docs": f"{BASE_URL}/docs"
76
+ }
77
+
78
+
79
+ @app.post("/marker")
80
+ async def convert_pdf(
81
+ file: UploadFile = File(...),
82
+ output_format: str = Form("markdown"),
83
+ langs: Optional[str] = Form(None),
84
+ paginate: str = Form("false"),
85
+ format_lines: str = Form("false"),
86
+ use_llm: str = Form("false"),
87
+ disable_image_extraction: str = Form("false"),
88
+ redo_inline_math: str = Form("false"),
89
+ api_key: Optional[str] = Form(None),
90
+ ):
91
+ """
92
+ Convert PDF to markdown using Marker.
93
+
94
+ This endpoint receives a PDF, runs marker_single CLI command,
95
+ and returns a request_id for polling status.
96
+ """
97
+ # Validate file
98
+ if not file.filename or not file.filename.lower().endswith('.pdf'):
99
+ raise HTTPException(status_code=400, detail="Only PDF files are supported")
100
+
101
+ # Read file content and validate size
102
+ content = await file.read()
103
+ if len(content) > MAX_PDF_FILE_SIZE:
104
+ raise HTTPException(
105
+ status_code=413,
106
+ detail=f"PDF file size exceeds the maximum allowed size of {MAX_PDF_FILE_SIZE // (1024 * 1024)} MB"
107
+ )
108
+
109
+ if len(content) == 0:
110
+ raise HTTPException(status_code=400, detail="File is empty")
111
+
112
+ # Generate unique request ID
113
+ request_id = str(uuid.uuid4())
114
+
115
+ # Create temp directories for this job
116
+ job_upload_dir = UPLOAD_DIR / request_id
117
+ job_output_dir = OUTPUT_DIR / request_id
118
+ job_upload_dir.mkdir(exist_ok=True)
119
+ job_output_dir.mkdir(exist_ok=True)
120
+
121
+ # Save uploaded PDF
122
+ pdf_path = job_upload_dir / file.filename
123
+ with open(pdf_path, "wb") as f:
124
+ f.write(content)
125
+
126
+ # Parse boolean options
127
+ options = {
128
+ "paginate": str_to_bool(paginate),
129
+ "format_lines": str_to_bool(format_lines),
130
+ "use_llm": str_to_bool(use_llm),
131
+ "disable_image_extraction": str_to_bool(disable_image_extraction),
132
+ "redo_inline_math": str_to_bool(redo_inline_math),
133
+ }
134
+
135
+ # Build marker_single CLI command
136
+ cmd = [
137
+ "marker_single",
138
+ str(pdf_path),
139
+ str(job_output_dir),
140
+ "--output_format", output_format,
141
+ ]
142
+
143
+ # Add optional flags
144
+ if langs:
145
+ cmd.extend(["--langs", langs])
146
+ if options["paginate"]:
147
+ cmd.append("--paginate")
148
+ if options["disable_image_extraction"]:
149
+ cmd.append("--disable_image_extraction")
150
+
151
+ # Initialize job
152
+ jobs[request_id] = {
153
+ "status": "processing",
154
+ "pdf_path": str(pdf_path),
155
+ "output_dir": str(job_output_dir),
156
+ "upload_dir": str(job_upload_dir),
157
+ "command": " ".join(cmd),
158
+ "markdown": None,
159
+ "error": None,
160
+ }
161
+
162
+ # Start conversion in background (non-blocking)
163
+ import asyncio
164
+ asyncio.create_task(run_conversion(request_id, cmd, options, api_key, pdf_path, job_output_dir, job_upload_dir))
165
+
166
+ # Return response immediately
167
+ return JSONResponse(content={
168
+ "success": True,
169
+ "request_id": request_id,
170
+ "request_check_url": f"{BASE_URL}/status/{request_id}",
171
+ })
172
+
173
+
174
+ async def run_conversion(request_id: str, cmd: list, options: dict, api_key: Optional[str], pdf_path: Path, output_dir: Path, upload_dir: Path):
175
+ """Run the marker_single conversion in background."""
176
+ import asyncio
177
+
178
+ try:
179
+ # Set environment for LLM
180
+ env = os.environ.copy()
181
+ if options["use_llm"] and api_key:
182
+ env["GEMINI_API_KEY"] = api_key
183
+
184
+ print(f"[{request_id}] Starting conversion: {' '.join(cmd)}")
185
+
186
+ # Run marker_single command
187
+ process = await asyncio.create_subprocess_exec(
188
+ *cmd,
189
+ env=env,
190
+ stdout=asyncio.subprocess.PIPE,
191
+ stderr=asyncio.subprocess.PIPE
192
+ )
193
+
194
+ stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=300) # 5 min timeout
195
+
196
+ if process.returncode == 0:
197
+ # Find the output markdown file
198
+ markdown_files = list(output_dir.glob("*.md"))
199
+ if markdown_files:
200
+ with open(markdown_files[0], "r", encoding="utf-8") as f:
201
+ markdown = f.read()
202
+ jobs[request_id]["status"] = "complete"
203
+ jobs[request_id]["markdown"] = markdown
204
+ print(f"[{request_id}] Success! ({len(markdown)} chars)")
205
+ else:
206
+ jobs[request_id]["status"] = "error"
207
+ jobs[request_id]["error"] = "No markdown file generated"
208
+ print(f"[{request_id}] Error: No markdown output")
209
+ else:
210
+ error_msg = stderr.decode() if stderr else "Unknown error"
211
+ jobs[request_id]["status"] = "error"
212
+ jobs[request_id]["error"] = f"Marker failed: {error_msg}"
213
+ print(f"[{request_id}] Error: {error_msg}")
214
+
215
+ except asyncio.TimeoutError:
216
+ jobs[request_id]["status"] = "error"
217
+ jobs[request_id]["error"] = "Conversion timed out (5 minutes)"
218
+ print(f"[{request_id}] Timeout!")
219
+ except Exception as e:
220
+ jobs[request_id]["status"] = "error"
221
+ jobs[request_id]["error"] = str(e)
222
+ print(f"[{request_id}] Exception: {e}")
223
+ finally:
224
+ # Cleanup temp files
225
+ try:
226
+ shutil.rmtree(upload_dir)
227
+ except:
228
+ pass
229
+ try:
230
+ shutil.rmtree(output_dir)
231
+ except:
232
+ pass
233
+
234
+
235
+ @app.get("/status/{request_id}")
236
+ async def check_status(request_id: str):
237
+ """
238
+ Check conversion status.
239
+
240
+ Returns:
241
+ - status: "processing" | "complete" | "error"
242
+ - markdown: converted markdown text (if complete)
243
+ - error: error message (if error)
244
+ """
245
+ if request_id not in jobs:
246
+ raise HTTPException(status_code=404, detail="Request ID not found")
247
+
248
+ job = jobs[request_id]
249
+ response = {"status": job["status"]}
250
+
251
+ if job["status"] == "complete":
252
+ response["markdown"] = job["markdown"]
253
+ # Clean up job after successful retrieval
254
+ del jobs[request_id]
255
+ elif job["status"] == "error":
256
+ response["error"] = job["error"]
257
+ # Clean up job after error retrieval
258
+ del jobs[request_id]
259
+
260
+ return JSONResponse(content=response)
261
+
262
+
263
+ @app.get("/health")
264
+ async def health_check():
265
+ """Health check for monitoring."""
266
+ return {
267
+ "status": "healthy",
268
+ "active_jobs": len(jobs),
269
+ "gpu_available": os.path.exists("/dev/nvidia0")
270
+ }
271
+
272
+
273
+ # For HuggingFace Spaces gradio interface (optional)
274
+ if __name__ == "__main__":
275
+ import uvicorn
276
+ print("๐Ÿš€ Starting Marker PDF Converter on HuggingFace Space...")
277
+ print(f"๐Ÿ“ GPU: {'NVIDIA T4' if os.path.exists('/dev/nvidia0') else 'CPU (waiting for GPU)'}")
278
+ uvicorn.run(app, host="0.0.0.0", port=7860)
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ marker-pdf
2
+ fastapi
3
+ uvicorn[standard]
4
+ python-multipart