Vik Paruchuri
commited on
Commit
·
853977e
1
Parent(s):
ca8c244
Add simple API server
Browse files- README.md +20 -1
- marker_server.py +171 -0
- poetry.lock +0 -0
- pyproject.toml +9 -4
- run_marker_app.py +2 -3
README.md
CHANGED
|
@@ -50,7 +50,7 @@ There's a hosted API for marker available [here](https://www.datalab.to/):
|
|
| 50 |
|
| 51 |
- Supports PDFs, word documents, and powerpoints
|
| 52 |
- 1/4th the price of leading cloud-based competitors
|
| 53 |
-
-
|
| 54 |
|
| 55 |
# Community
|
| 56 |
|
|
@@ -191,6 +191,25 @@ The output will be a markdown file, but there will also be a metadata json file
|
|
| 191 |
}
|
| 192 |
```
|
| 193 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
# Troubleshooting
|
| 195 |
|
| 196 |
There are some settings that you may find useful if things aren't working the way you expect:
|
|
|
|
| 50 |
|
| 51 |
- Supports PDFs, word documents, and powerpoints
|
| 52 |
- 1/4th the price of leading cloud-based competitors
|
| 53 |
+
- High uptime (99.99%), quality, and speed (.25s/page for 50 page doc)
|
| 54 |
|
| 55 |
# Community
|
| 56 |
|
|
|
|
| 191 |
}
|
| 192 |
```
|
| 193 |
|
| 194 |
+
## API server
|
| 195 |
+
|
| 196 |
+
There is a very simple API server you can run like this:
|
| 197 |
+
|
| 198 |
+
```shell
|
| 199 |
+
pip install -U uvicorn fastapi python-multipart
|
| 200 |
+
marker_server --port 8001
|
| 201 |
+
```
|
| 202 |
+
|
| 203 |
+
This will start a fastapi server that you can access at `localhost:8001`. You can go to `localhost:8001/docs` to see the endpoint options.
|
| 204 |
+
|
| 205 |
+
Note that this is not a very robust API, and is only intended for small-scale use. If you want to use this server, but want a more robust conversion option, you can run against the hosted [Datalab API](https://www.datalab.to/plans). You'll need to register and get an API key, then run:
|
| 206 |
+
|
| 207 |
+
```shell
|
| 208 |
+
marker_server --port 8001 --api_key API_KEY
|
| 209 |
+
```
|
| 210 |
+
|
| 211 |
+
Note: This is not the recommended way to use the Datalab API - it's only provided as a convenience for people wrapping the marker repo. The recommended way is to make a post request to the endpoint directly from your code vs proxying through this server.
|
| 212 |
+
|
| 213 |
# Troubleshooting
|
| 214 |
|
| 215 |
There are some settings that you may find useful if things aren't working the way you expect:
|
marker_server.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import asyncio
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
import requests
|
| 6 |
+
import uvicorn
|
| 7 |
+
from starlette.responses import HTMLResponse
|
| 8 |
+
|
| 9 |
+
os.environ["PDFTEXT_CPU_WORKERS"] = "1"
|
| 10 |
+
|
| 11 |
+
import base64
|
| 12 |
+
from contextlib import asynccontextmanager
|
| 13 |
+
from typing import Optional
|
| 14 |
+
import io
|
| 15 |
+
|
| 16 |
+
from fastapi import FastAPI, Form
|
| 17 |
+
from marker.convert import convert_single_pdf
|
| 18 |
+
from marker.models import load_all_models
|
| 19 |
+
|
| 20 |
+
app_data = {}
|
| 21 |
+
|
| 22 |
+
@asynccontextmanager
|
| 23 |
+
async def lifespan(app: FastAPI):
|
| 24 |
+
if app.state.LOCAL:
|
| 25 |
+
app_data["models"] = load_all_models()
|
| 26 |
+
|
| 27 |
+
yield
|
| 28 |
+
|
| 29 |
+
if "models" in app_data:
|
| 30 |
+
del app_data["models"]
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
app = FastAPI(lifespan=lifespan)
|
| 34 |
+
|
| 35 |
+
@app.get("/")
|
| 36 |
+
async def root():
|
| 37 |
+
return HTMLResponse(
|
| 38 |
+
"""
|
| 39 |
+
<h1>Marker API</h1>
|
| 40 |
+
<ul>
|
| 41 |
+
<li><a href="/docs">API Documentation</a></li>
|
| 42 |
+
<li><a href="/local">Run marker locally (post request only)</a></li>
|
| 43 |
+
<li><a href="/remote">Run marker remotely (post request only)</a></li>
|
| 44 |
+
</ul>
|
| 45 |
+
"""
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
@app.post("/remote")
|
| 50 |
+
async def convert_pdf_remote(
|
| 51 |
+
filepath: str = Form(
|
| 52 |
+
...,
|
| 53 |
+
description="The path to the PDF file, word document, or powerpoint to convert."
|
| 54 |
+
),
|
| 55 |
+
max_pages: Optional[int] = Form(
|
| 56 |
+
None,
|
| 57 |
+
description="The maximum number of pages in the document to convert."
|
| 58 |
+
),
|
| 59 |
+
langs: Optional[str] = Form(
|
| 60 |
+
None,
|
| 61 |
+
description="The optional languages to use if OCR is needed, comma separated. Must be either the names or codes from https://github.com/VikParuchuri/surya/blob/master/surya/languages.py."
|
| 62 |
+
),
|
| 63 |
+
force_ocr: bool = Form(
|
| 64 |
+
False,
|
| 65 |
+
description="Force OCR on all pages of the PDF. Defaults to False. This can lead to worse results if you have good text in your PDFs (which is true in most cases)."
|
| 66 |
+
),
|
| 67 |
+
paginate: bool = Form(False,
|
| 68 |
+
description="Whether to paginate the output. Defaults to False. If set to True, each page of the output will be separated by a horizontal rule that contains the page number (2 newlines, {PAGE_NUMBER}, 48 - characters, 2 newlines)."),
|
| 69 |
+
extract_images: bool = Form(True, description="Whether to extract images from the PDF. Defaults to True. If set to False, no images will be extracted from the PDF."),
|
| 70 |
+
):
|
| 71 |
+
with open(filepath, "rb") as f:
|
| 72 |
+
filedata = f.read()
|
| 73 |
+
|
| 74 |
+
filename = os.path.basename(filepath)
|
| 75 |
+
form_data = {
|
| 76 |
+
'file': (filename, filedata, 'application/pdf'),
|
| 77 |
+
'max_pages': (None, max_pages),
|
| 78 |
+
'langs': (None, langs),
|
| 79 |
+
'force_ocr': (None, force_ocr),
|
| 80 |
+
'paginate': (None, paginate),
|
| 81 |
+
'extract_images': (None, extract_images),
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
headers = {"X-API-Key": app.state.API_KEY}
|
| 85 |
+
|
| 86 |
+
response = requests.post(app.state.DATALAB_URL, files=form_data, headers=headers)
|
| 87 |
+
data = response.json()
|
| 88 |
+
|
| 89 |
+
check_url = data["request_check_url"]
|
| 90 |
+
|
| 91 |
+
for i in range(300):
|
| 92 |
+
await asyncio.sleep(2)
|
| 93 |
+
response = requests.get(check_url, headers=headers)
|
| 94 |
+
data = response.json()
|
| 95 |
+
|
| 96 |
+
if data["status"] == "complete":
|
| 97 |
+
break
|
| 98 |
+
|
| 99 |
+
return data
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
@app.post("/local")
|
| 103 |
+
async def convert_pdf_local(
|
| 104 |
+
filepath: str = Form(
|
| 105 |
+
...,
|
| 106 |
+
description="The path to the PDF file to convert."
|
| 107 |
+
),
|
| 108 |
+
max_pages: Optional[int] = Form(
|
| 109 |
+
None,
|
| 110 |
+
description="The maximum number of pages in the PDF to convert."
|
| 111 |
+
),
|
| 112 |
+
langs: Optional[str] = Form(
|
| 113 |
+
None,
|
| 114 |
+
description="The optional languages to use if OCR is needed, comma separated. Must be either the names or codes from https://github.com/VikParuchuri/surya/blob/master/surya/languages.py."
|
| 115 |
+
),
|
| 116 |
+
force_ocr: bool = Form(
|
| 117 |
+
False,
|
| 118 |
+
description="Force OCR on all pages of the PDF. Defaults to False. This can lead to worse results if you have good text in your PDFs (which is true in most cases)."
|
| 119 |
+
)
|
| 120 |
+
):
|
| 121 |
+
try:
|
| 122 |
+
full_text, images, metadata = convert_single_pdf(
|
| 123 |
+
filepath,
|
| 124 |
+
app_data["models"],
|
| 125 |
+
max_pages=max_pages,
|
| 126 |
+
langs=langs,
|
| 127 |
+
ocr_all_pages=force_ocr
|
| 128 |
+
)
|
| 129 |
+
except Exception as e:
|
| 130 |
+
return {
|
| 131 |
+
"success": False,
|
| 132 |
+
"error": str(e),
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
encoded = {}
|
| 136 |
+
for k, v in images.items():
|
| 137 |
+
byte_stream = io.BytesIO()
|
| 138 |
+
v.save(byte_stream, format="PNG")
|
| 139 |
+
encoded[k] = base64.b64encode(byte_stream.getvalue()).decode("utf-8")
|
| 140 |
+
|
| 141 |
+
return {
|
| 142 |
+
"markdown": full_text,
|
| 143 |
+
"images": encoded,
|
| 144 |
+
"metadata": metadata,
|
| 145 |
+
"success": True
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def main():
|
| 150 |
+
parser = argparse.ArgumentParser(description='Convert PDFs to markdown.')
|
| 151 |
+
parser.add_argument('--port', type=int, default=8000, help='Port to run the server on')
|
| 152 |
+
parser.add_argument('--host', type=str, default="127.0.0.1", help='Host to run the server on')
|
| 153 |
+
parser.add_argument('--api_key', type=str, default=None, help='API key for the Datalab API. If not specified, API will run locally.')
|
| 154 |
+
parser.add_argument("--datalab_url", type=str, default="https://api.datalab.to/api/v1/marker", help="The URL for the Datalab API")
|
| 155 |
+
|
| 156 |
+
args = parser.parse_args()
|
| 157 |
+
|
| 158 |
+
app.state.API_KEY = args.api_key
|
| 159 |
+
app.state.LOCAL = args.api_key is None
|
| 160 |
+
app.state.DATALAB_URL = args.datalab_url
|
| 161 |
+
|
| 162 |
+
# Run the server
|
| 163 |
+
uvicorn.run(
|
| 164 |
+
app,
|
| 165 |
+
host=args.host,
|
| 166 |
+
port=args.port,
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
if __name__ == "__main__":
|
| 171 |
+
main()
|
poetry.lock
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
pyproject.toml
CHANGED
|
@@ -16,7 +16,8 @@ include = [
|
|
| 16 |
"chunk_convert.sh",
|
| 17 |
"chunk_convert.py",
|
| 18 |
"marker_app.py",
|
| 19 |
-
"run_marker_app.py"
|
|
|
|
| 20 |
]
|
| 21 |
|
| 22 |
[tool.poetry.dependencies]
|
|
@@ -32,22 +33,26 @@ tabulate = "^0.9.0"
|
|
| 32 |
ftfy = "^6.1.1"
|
| 33 |
texify = "^0.2.0"
|
| 34 |
rapidfuzz = "^3.8.1"
|
| 35 |
-
surya-ocr = "^0.6.
|
| 36 |
filetype = "^1.2.0"
|
| 37 |
regex = "^2024.4.28"
|
| 38 |
-
pdftext = "^0.3.
|
| 39 |
tabled-pdf = "^0.1.4"
|
| 40 |
|
| 41 |
[tool.poetry.group.dev.dependencies]
|
| 42 |
jupyter = "^1.0.0"
|
| 43 |
datasets = "^2.21.0"
|
| 44 |
streamlit = "^1.37.1"
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
[tool.poetry.scripts]
|
| 47 |
marker = "convert:main"
|
| 48 |
marker_single = "convert_single:main"
|
| 49 |
marker_chunk_convert = "chunk_convert:main"
|
| 50 |
-
marker_gui = "run_marker_app:
|
|
|
|
| 51 |
|
| 52 |
[build-system]
|
| 53 |
requires = ["poetry-core"]
|
|
|
|
| 16 |
"chunk_convert.sh",
|
| 17 |
"chunk_convert.py",
|
| 18 |
"marker_app.py",
|
| 19 |
+
"run_marker_app.py",
|
| 20 |
+
"marker_server.py",
|
| 21 |
]
|
| 22 |
|
| 23 |
[tool.poetry.dependencies]
|
|
|
|
| 33 |
ftfy = "^6.1.1"
|
| 34 |
texify = "^0.2.0"
|
| 35 |
rapidfuzz = "^3.8.1"
|
| 36 |
+
surya-ocr = "^0.6.13"
|
| 37 |
filetype = "^1.2.0"
|
| 38 |
regex = "^2024.4.28"
|
| 39 |
+
pdftext = "^0.3.18"
|
| 40 |
tabled-pdf = "^0.1.4"
|
| 41 |
|
| 42 |
[tool.poetry.group.dev.dependencies]
|
| 43 |
jupyter = "^1.0.0"
|
| 44 |
datasets = "^2.21.0"
|
| 45 |
streamlit = "^1.37.1"
|
| 46 |
+
fastapi = "^0.115.4"
|
| 47 |
+
uvicorn = "^0.32.0"
|
| 48 |
+
python-multipart = "^0.0.16"
|
| 49 |
|
| 50 |
[tool.poetry.scripts]
|
| 51 |
marker = "convert:main"
|
| 52 |
marker_single = "convert_single:main"
|
| 53 |
marker_chunk_convert = "chunk_convert:main"
|
| 54 |
+
marker_gui = "run_marker_app:run"
|
| 55 |
+
marker_server = "marker_server:main"
|
| 56 |
|
| 57 |
[build-system]
|
| 58 |
requires = ["poetry-core"]
|
run_marker_app.py
CHANGED
|
@@ -1,9 +1,8 @@
|
|
| 1 |
-
import argparse
|
| 2 |
import subprocess
|
| 3 |
import os
|
| 4 |
|
| 5 |
|
| 6 |
-
def
|
| 7 |
cur_dir = os.path.dirname(os.path.abspath(__file__))
|
| 8 |
app_path = os.path.join(cur_dir, "marker_app.py")
|
| 9 |
cmd = ["streamlit", "run", app_path]
|
|
@@ -11,4 +10,4 @@ def run_app():
|
|
| 11 |
|
| 12 |
|
| 13 |
if __name__ == "__main__":
|
| 14 |
-
|
|
|
|
|
|
|
| 1 |
import subprocess
|
| 2 |
import os
|
| 3 |
|
| 4 |
|
| 5 |
+
def run():
|
| 6 |
cur_dir = os.path.dirname(os.path.abspath(__file__))
|
| 7 |
app_path = os.path.join(cur_dir, "marker_app.py")
|
| 8 |
cmd = ["streamlit", "run", app_path]
|
|
|
|
| 10 |
|
| 11 |
|
| 12 |
if __name__ == "__main__":
|
| 13 |
+
run()
|