Initial: Tesseract Web GUI with Flask + start.bat

This commit is contained in:
Sivarius
2026-07-03 16:59:26 +08:00
commit 330b79288b
6 changed files with 534 additions and 0 deletions
+59
View File
@@ -0,0 +1,59 @@
# Tesseract Web GUI
Простой локальный веб-сервис для OCR через Tesseract.
## Структура
```
tesseract-webgui/
├── app.py # Flask сервер
├── start.bat # Запуск на Windows
├── templates/
│ └── index.html # Веб-интерфейс
└── uploads/ # Сюда сохраняются загруженные и обработанные картинки
```
## Запуск
```bash
start.bat
```
Или вручную:
```bash
cd tesseract-webgui
python -m pip install flask pillow pytesseract
python app.py
```
Затем открывай в браузере: **http://127.0.0.1:5000/**
## Возможности
- Загрузка картинки drag-&-drop или через кнопку
- Настройка языка (`rus+eng`, `eng`, и т.д.)
- PSM режимы (авто, блок текста, одна строка и др.)
- Предобработка: увеличение, контраст, резкость, бинаризация
- Whitelist символов
- Предпросмотр загруженной картинки
- Сохранение всех параметров OCR в выводе
## Требования
- Python 3.10+
- Tesseract OCR (должен быть в `C:\Program Files\Tesseract-OCR\tesseract.exe`)
- Русский языковой пакет: `rus.traineddata` в `C:\Program Files\Tesseract-OCR\tessdata\`
## Установка Tesseract
```bash
winget install UB-Mannheim.TesseractOCR
```
Русский язык (если не установился):
```bash
curl -L -o "%TEMP%\rus.traineddata" "https://github.com/tesseract-ocr/tessdata/raw/main/rus.traineddata"
copy "%TEMP%\rus.traineddata" "C:\Program Files\Tesseract-OCR\tessdata\"
```
+148
View File
@@ -0,0 +1,148 @@
"""
Tesseract Web GUI
Локальный веб-сервис для распознавания текста с картинок.
Запуск: start.bat
"""
import os
import io
import uuid
import base64
from pathlib import Path
from datetime import datetime
from PIL import Image, ImageEnhance, ImageFilter, ImageOps
import pytesseract
from flask import Flask, render_template, request, jsonify, send_from_directory
from werkzeug.utils import secure_filename
# ── Конфиг ─────────────────────────────────────────────────────────────
BASE_DIR = Path(__file__).resolve().parent
UPLOAD_DIR = BASE_DIR / "uploads"
UPLOAD_DIR.mkdir(exist_ok=True)
# Путь к Tesseract (Windows)
TESSERACT_CMD = r"C:\Program Files\Tesseract-OCR\tesseract.exe"
if Path(TESSERACT_CMD).exists():
pytesseract.pytesseract.tesseract_cmd = TESSERACT_CMD
ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "bmp", "tiff", "webp", "gif"}
app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 * 1024 # 16 MB
# ── Хелперы ────────────────────────────────────────────────────────────
def allowed_file(filename: str) -> bool:
return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
def preprocess_image(img: Image.Image, scale: int, contrast: float, sharpen: bool, binary: bool, binary_thresh: int) -> Image.Image:
"""Предобработка перед OCR."""
if scale > 1:
w, h = img.size
img = img.resize((w * scale, h * scale), Image.LANCZOS)
img = img.convert("L")
if contrast != 1.0:
img = ImageEnhance.Contrast(img).enhance(contrast)
if sharpen:
img = img.filter(ImageFilter.SHARPEN)
if binary:
img = img.point(lambda x: 0 if x < binary_thresh else 255)
return img
def do_ocr(image_path: Path, lang: str, psm: int,
scale: int, contrast: float, sharpen: bool,
binary: bool, binary_thresh: int,
whitelist: str = "") -> dict:
"""Выполняет OCR и возвращает словарь с результатом."""
img = Image.open(image_path)
# Для preview сохраняем оригинал как base64
buf = io.BytesIO()
img.convert("RGB").save(buf, format="PNG")
preview_b64 = base64.b64encode(buf.getvalue()).decode("utf-8")
# Предобработка
proc = preprocess_image(img, scale, contrast, sharpen, binary, binary_thresh)
config = f"--psm {psm}"
if whitelist:
config += f' -c tessedit_char_whitelist="{whitelist}"'
text = pytesseract.image_to_string(proc, lang=lang, config=config)
# Сохраняем обработанную картинку
proc_id = uuid.uuid4().hex[:8]
proc_path = UPLOAD_DIR / f"proc_{proc_id}.png"
proc.save(proc_path)
return {
"text": text,
"preview": preview_b64,
"proc_path": str(proc_path),
"config_used": f"lang={lang}, psm={psm}, scale={scale}, contrast={contrast}, sharpen={sharpen}, binary={binary}, thresh={binary_thresh}, whitelist={whitelist or 'none'}",
}
# ── Роуты ──────────────────────────────────────────────────────────────
@app.route("/")
def index():
return render_template("index.html")
@app.route("/ocr", methods=["POST"])
def ocr():
if "file" not in request.files:
return jsonify({"error": "Файл не загружен"}), 400
file = request.files["file"]
if file.filename == "" or not allowed_file(file.filename):
return jsonify({"error": "Неверный или пустой файл (разрешены PNG, JPG, JPEG, BMP, TIFF, WEBP, GIF)"}), 400
# Параметры из формы
lang = request.form.get("lang", "rus+eng").strip()
psm = int(request.form.get("psm", 6))
scale = int(request.form.get("scale", 2))
contrast = float(request.form.get("contrast", 2.0))
sharpen = request.form.get("sharpen", "true").lower() in ("true", "1", "on")
binary = request.form.get("binary", "false").lower() in ("true", "1", "on")
binary_thresh = int(request.form.get("binary_thresh", 140))
whitelist = request.form.get("whitelist", "").strip()
# Сохраняем оригинал
ext = secure_filename(file.filename).rsplit(".", 1)[1].lower()
uid = uuid.uuid4().hex[:8]
original_path = UPLOAD_DIR / f"{uid}_{datetime.now():%Y%m%d_%H%M%S}.{ext}"
file.save(original_path)
try:
result = do_ocr(
original_path, lang, psm,
scale, contrast, sharpen,
binary, binary_thresh, whitelist
)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/uploads/<path:filename>")
def uploads(filename):
return send_from_directory(UPLOAD_DIR, filename)
# ── Запуск ─────────────────────────────────────────────────────────────
if __name__ == "__main__":
port = int(os.environ.get("PORT", 5000))
print(f"[Tesseract Web GUI] http://127.0.0.1:{port}/")
# debug=False, потому что иначе батник не закроется нормально
app.run(host="127.0.0.1", port=port, debug=False)
+36
View File
@@ -0,0 +1,36 @@
@echo off
chcp 65001 > nul
title Tesseract Web GUI
:: Путь к папке проекта
set APP_DIR=%~dp0
cd /d "%APP_DIR%"
:: Проверяем Python
where python > nul 2> nul
if errorlevel 1 (
echo [Ошибка] Python не найден. Установи Python 3.10+ и добавь в PATH.
pause
exit /b 1
)
:: Установка зависимостей (пропустит если уже стоят)
echo [1/3] Проверка зависимостей...
python -m pip install -q flask pillow pytesseract
:: Проверяем Tesseract
if not exist "C:\Program Files\Tesseract-OCR\tesseract.exe" (
echo [Предупреждение] Tesseract не найден по пути C:\Program Files\Tesseract-OCR\tesseract.exe
echo Установи: winget install UB-Mannheim.TesseractOCR
pause
exit /b 1
)
:: Запуск
echo [2/3] Запуск веб-сервиса...
echo [3/3] Открой браузер: http://127.0.0.1:5000/
echo.
python app.py
pause
+291
View File
@@ -0,0 +1,291 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tesseract Web GUI</title>
<style>
:root {
--bg: #1a1a2e;
--panel: #16213e;
--accent: #0f3460;
--text: #e0e0e0;
--muted: #a0a0a0;
--border: #2a3f5f;
--ok: #2ecc71;
--err: #e74c3c;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "Segoe UI", Arial, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.5;
}
.container {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
}
h1 {
margin: 0 0 10px;
font-size: 26px;
}
.subtitle {
color: var(--muted);
margin-bottom: 20px;
}
.grid {
display: grid;
grid-template-columns: 320px 1fr;
gap: 20px;
}
@media (max-width: 800px) {
.grid { grid-template-columns: 1fr; }
}
.panel {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 12px;
padding: 18px;
}
.panel h2 {
margin: 0 0 14px;
font-size: 16px;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
label {
display: block;
margin: 12px 0 5px;
font-size: 13px;
color: var(--muted);
}
input, select, textarea {
width: 100%;
padding: 8px 10px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text);
font-size: 14px;
}
input[type="checkbox"] {
width: auto;
margin-right: 6px;
}
.checkbox-row {
display: flex;
align-items: center;
margin-top: 12px;
}
.checkbox-row label {
margin: 0;
}
button {
width: 100%;
margin-top: 16px;
padding: 12px;
background: var(--accent);
border: none;
border-radius: 8px;
color: #fff;
font-size: 15px;
cursor: pointer;
transition: background 0.2s;
}
button:hover { background: #1a4a80; }
button:disabled { opacity: 0.6; cursor: wait; }
.dropzone {
border: 2px dashed var(--border);
border-radius: 10px;
padding: 30px;
text-align: center;
color: var(--muted);
cursor: pointer;
transition: border-color 0.2s;
}
.dropzone:hover, .dropzone.dragover { border-color: var(--accent); color: var(--text); }
.preview {
margin-top: 15px;
text-align: center;
}
.preview img {
max-width: 100%;
max-height: 220px;
border-radius: 8px;
border: 1px solid var(--border);
}
.result textarea {
min-height: 280px;
font-family: Consolas, monospace;
font-size: 13px;
resize: vertical;
}
.meta {
margin-top: 10px;
font-size: 12px;
color: var(--muted);
}
.status {
margin-top: 12px;
padding: 10px;
border-radius: 8px;
font-size: 13px;
display: none;
}
.status.ok { background: rgba(46, 204, 113, 0.15); color: var(--ok); display: block; }
.status.err { background: rgba(231, 76, 60, 0.15); color: var(--err); display: block; }
</style>
</head>
<body>
<div class="container">
<h1>🔍 Tesseract Web GUI</h1>
<div class="subtitle">Загрузи картинку — получи текст. Всё локально, без интернета.</div>
<div class="grid">
<div>
<div class="panel">
<h2>1. Картинка</h2>
<div class="dropzone" id="dropzone">
📁 Перетащи сюда файл или кликни для выбора
<input type="file" id="fileInput" accept="image/*" hidden>
</div>
<div class="preview" id="preview"></div>
</div>
<div class="panel" style="margin-top: 20px;">
<h2>2. Настройки OCR</h2>
<label>Язык</label>
<input type="text" id="lang" value="rus+eng">
<label>PSM режим</label>
<select id="psm">
<option value="3">3 — Авто</option>
<option value="4">4 — Одна колонка</option>
<option value="6" selected>6 — Блок текста (скриншоты)</option>
<option value="7">7 — Одна строка</option>
<option value="8">8 — Одно слово</option>
<option value="11">11 — Разреженный текст</option>
<option value="13">13 — Сырая строка</option>
</select>
<label>Увеличение (scale)</label>
<input type="number" id="scale" value="2" min="1" max="5">
<label>Контраст</label>
<input type="number" id="contrast" value="2.0" step="0.1" min="1" max="5">
<div class="checkbox-row">
<input type="checkbox" id="sharpen" checked>
<label for="sharpen">Резкость</label>
</div>
<div class="checkbox-row">
<input type="checkbox" id="binary">
<label for="binary">Бинаризация</label>
</div>
<label>Порог бинаризации</label>
<input type="number" id="binary_thresh" value="140" min="0" max="255">
<label>Whitelist символов (опционально)</label>
<input type="text" id="whitelist" placeholder="Например: 0123456789">
<button id="runBtn" disabled>🔮 Распознать</button>
<div class="status" id="status"></div>
</div>
</div>
<div class="panel result">
<h2>3. Результат</h2>
<textarea id="result" readonly placeholder="Тут появится распознанный текст..."></textarea>
<div class="meta" id="meta"></div>
</div>
</div>
</div>
<script>
const dropzone = document.getElementById('dropzone');
const fileInput = document.getElementById('fileInput');
const preview = document.getElementById('preview');
const runBtn = document.getElementById('runBtn');
const status = document.getElementById('status');
const resultArea = document.getElementById('result');
const meta = document.getElementById('meta');
let currentFile = null;
dropzone.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', (e) => handleFiles(e.target.files));
dropzone.addEventListener('dragover', (e) => {
e.preventDefault();
dropzone.classList.add('dragover');
});
dropzone.addEventListener('dragleave', () => dropzone.classList.remove('dragover'));
dropzone.addEventListener('drop', (e) => {
e.preventDefault();
dropzone.classList.remove('dragover');
handleFiles(e.dataTransfer.files);
});
function handleFiles(files) {
if (!files.length) return;
currentFile = files[0];
const url = URL.createObjectURL(currentFile);
preview.innerHTML = `<img src="${url}" alt="preview">`;
runBtn.disabled = false;
status.className = 'status';
status.textContent = '';
}
runBtn.addEventListener('click', async () => {
if (!currentFile) return;
runBtn.disabled = true;
status.className = 'status';
status.textContent = 'Распознаю...';
const form = new FormData();
form.append('file', currentFile);
form.append('lang', document.getElementById('lang').value);
form.append('psm', document.getElementById('psm').value);
form.append('scale', document.getElementById('scale').value);
form.append('contrast', document.getElementById('contrast').value);
form.append('sharpen', document.getElementById('sharpen').checked);
form.append('binary', document.getElementById('binary').checked);
form.append('binary_thresh', document.getElementById('binary_thresh').value);
form.append('whitelist', document.getElementById('whitelist').value);
try {
const resp = await fetch('/ocr', { method: 'POST', body: form });
const data = await resp.json();
if (data.error) {
status.className = 'status err';
status.textContent = 'Ошибка: ' + data.error;
return;
}
resultArea.value = data.text;
meta.textContent = 'Настройки: ' + data.config_used;
if (data.preview) {
preview.innerHTML = `<img src="data:image/png;base64,${data.preview}" alt="preview">`;
}
status.className = 'status ok';
status.textContent = 'Готово!';
} catch (e) {
status.className = 'status err';
status.textContent = 'Ошибка сети: ' + e.message;
} finally {
runBtn.disabled = false;
}
});
</script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB