Initial: Tesseract Web GUI with Flask + start.bat
This commit is contained in:
@@ -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\"
|
||||||
|
```
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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 |
Reference in New Issue
Block a user