2025, Oct 03 23:00
Why PyScript Can’t Run Tesseract/OpenCV OCR in the Browser and the Right Server-Side Approach
Learn why PyScript can’t call the Tesseract binary or list local files, and how to run reliable OCR with OpenCV+pytesseract on a Flask/FastAPI server.
Running a local OCR script that combines OpenCV, pytesseract and pandas is straightforward, but attempting to move the same logic into the browser with PyScript tends to fail in non-obvious ways. The sticking points usually surface around calling the native Tesseract binary and reading files from the local filesystem. Below is a compact walkthrough that shows why the original approach breaks in PyScript, what the browser sandbox allows, and how to restructure the solution so OCR still happens reliably with a web front end.
Minimal example that works locally but breaks in PyScript
The following snippet mirrors the original behavior: it scans a directory for images, OCRs each one with Tesseract via pytesseract, and reshapes the output into a DataFrame. The identifiers are intentionally different while preserving the exact logic.
# main.py
import pytesseract as ocr
ocr.pytesseract.tesseract_cmd = r"Tesseract-OCR\tesseract.exe"
import os
import cv2
import pandas as pd
image_bucket = []
def harvest_text(idx):
img = cv2.imread(image_bucket[idx], 0)
binarized = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
raw = ocr.image_to_string(binarized, lang='eng', config='--psm 6')
condensed = "\n".join([ln.rstrip() for ln in raw.splitlines() if ln.strip()])
return condensed.split('\n')
def run_batch():
base_dir = f'SQL_NOTES\\'
entries = os.listdir(base_dir)
for entry in entries:
if entry.startswith("imagename"):
image_bucket.append(base_dir + entry)
idx_map = dict({0: 'image_bucket[0]', 1: 'image_bucket[1]', 2: 'image_bucket[2]', 3: 'image_bucket[3]'})
rows = ([harvest_text(k) for k in idx_map])
return pd.DataFrame(rows).T
print(run_batch())
In the browser, it’s invoked through PyScript as shown here.
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<title>Empty Grass</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="stylesheet" href="https://pyscript.net/releases/2025.7.3/core.css">
<script type="module" src="https://pyscript.net/releases/2025.7.3/core.js"></script>
</head>
<body>
<button id="go_ocr">Run</button>
<script type="py" config="./pyscript.toml" terminal>
from pyscript import when
@when("click", "#go_ocr")
def on_go(evt):
from main import run_batch
run_batch()
</script>
</body>
</html>
# pyscript.toml
packages = [ "pytesseract", "opencv-python", "pandas" ]
[files]
"main.py" = "main.py"
What actually goes wrong and why
The browser security model is the root cause. PyScript runs in the browser and inherits the same sandboxing rules as JavaScript. That affects both the external binary and the filesystem.
PyScript can't run external program .exe because browsers don't allow for this for security reason.
In practice, the line that points pytesseract to a local executable does not work in PyScript. There is no way to spawn a native process like Tesseract from inside the browser. The same restrictions apply to native binaries behind Python packages such as OpenCV and pytesseract themselves.
Direct filesystem access is also restricted. Reading local directories with os.listdir is not allowed. PyScript documents how its virtual filesystem works and describes mounting options. On some Chromium-based browsers you can mount a local directory via a user-driven dialog with fs.mount, but even then you still cannot execute local .exe programs. The documentation is here: https://docs.pyscript.net/2025.8.1/user-guide/filesystem/
The combined effect is clear. The browser cannot enumerate your disks freely, and it cannot launch native executables. The two lines that look harmless on desktop Python, the tesseract_cmd assignment and the directory traversal, are not viable inside PyScript.
The practical way forward: move OCR to the server side
The resolution is to keep the web UI in the browser and move OCR execution to a www server. The browser sends image data, the server runs Tesseract and returns the result. Popular choices for the server piece include Flask or FastAPI. A higher-level option is to use a framework that already wires UI and a Python backend together. The following example uses NiceGUI and demonstrates the same OCR logic on the server, including rendering a pandas DataFrame as a table.
# server_app.py
from nicegui import ui
import os
import cv2
import pytesseract
import pandas as pd
# If needed on your system, configure the Tesseract path here as you would locally
# pytesseract.pytesseract.tesseract_cmd = r"Tesseract-OCR\tesseract.exe"
def parse_image(img_path):
gray = cv2.imread(img_path, 0)
mask = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
text = pytesseract.image_to_string(mask, lang='eng', config='--psm 6')
compact = "\n".join([ln.rstrip() for ln in text.splitlines() if ln.strip()])
return compact.split('\n')
def build_df():
folder = 'SQL_NOTES'
folder = '.' # folder containing images
collected = []
for name in os.listdir(folder):
if name.startswith("image"):
collected.append(os.path.join(folder, name))
records = [parse_image(p) for p in collected]
return pd.DataFrame(records).T
def on_press(e):
df = build_df()
status_label.set_text(df.to_string())
ui.table.from_pandas(df)
ui.button("Press to run", on_click=on_press)
status_label = ui.label("Waiting for result...")
ui.run()
If you prefer to let users upload images instead of scanning a server folder, NiceGUI also provides an upload element. The OCR still runs on the server and sends results back to the page.
# server_upload_app.py
from nicegui import ui
import cv2
import pytesseract
import pandas as pd
import numpy as np
def on_upload(e):
payload = e.content.read()
arr = np.frombuffer(payload, np.uint8)
mat = cv2.imdecode(arr, 0)
mask = cv2.threshold(mat, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
text = pytesseract.image_to_string(mask, lang='eng', config='--psm 6')
compact = "\n".join([ln.rstrip() for ln in text.splitlines() if ln.strip()])
rows = [compact.split('\n')]
df = pd.DataFrame(rows).T
outcome.set_text(df.to_string())
ui.table.from_pandas(df)
ui.upload(on_upload=on_upload)
outcome = ui.label("Waiting for result...")
ui.run()
Why this distinction matters
Understanding the browser sandbox saves time and avoids fragile hacks. PyScript inherits the same constraints as JavaScript, which means no arbitrary local file enumeration and no launching of native binaries from a web page. While Chromium-based browsers can expose a user-approved directory via mounting, that does not change the restriction on executing local programs. OCR, being dependent on Tesseract and native extensions, belongs on the server side when you build a web experience.
Takeaways and closing notes
Keep the OCR runtime on a Python backend and expose only a thin UI in the browser. If you already have a working desktop script, move that logic to a server endpoint using a familiar tool such as Flask or FastAPI, or rely on an integrated stack like NiceGUI that can display a pandas DataFrame directly. Do not expect pytesseract.pytesseract.tesseract_cmd to work in PyScript, and don’t rely on os.listdir for local folders in the browser. When in doubt, refer to the PyScript filesystem guide at https://docs.pyscript.net/2025.8.1/user-guide/filesystem/ and treat the browser as an untrusted, sandboxed client that exchanges data with your server, where the actual OCR takes place.
The article is based on a question from StackOverflow by nasrin begum pathan and an answer by furas.