Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a68a6b652 | |||
| f0ea5b282b | |||
| 32e4770ac7 |
@ -3,6 +3,8 @@ from flasgger import Swagger
|
|||||||
from app.database import db
|
from app.database import db
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.routes.user_routes import user_bp
|
from app.routes.user_routes import user_bp
|
||||||
|
from app.routes.login_form import login_bp
|
||||||
|
from app.routes.dashboard import dashboard_bp
|
||||||
from app.routes.invoice_routes import invoice_bp
|
from app.routes.invoice_routes import invoice_bp
|
||||||
from werkzeug.security import generate_password_hash
|
from werkzeug.security import generate_password_hash
|
||||||
|
|
||||||
@ -54,5 +56,7 @@ def create_app():
|
|||||||
# Registrar Blueprints
|
# Registrar Blueprints
|
||||||
app.register_blueprint(user_bp, url_prefix="/users")
|
app.register_blueprint(user_bp, url_prefix="/users")
|
||||||
app.register_blueprint(invoice_bp, url_prefix="/invoices")
|
app.register_blueprint(invoice_bp, url_prefix="/invoices")
|
||||||
|
app.register_blueprint(login_bp, url_prefix="/login_bp")
|
||||||
|
app.register_blueprint(dashboard_bp, url_prefix="/dashboard")
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|||||||
49
app/app.py
Normal file
49
app/app.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
from flask import Flask, render_template, request, redirect, url_for, session, flash
|
||||||
|
import os
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.secret_key = 'segredo-super-seguro'
|
||||||
|
|
||||||
|
# Rota de login
|
||||||
|
@app.route('/', methods=['GET', 'POST'])
|
||||||
|
@app.route('/login', methods=['GET', 'POST'])
|
||||||
|
def login():
|
||||||
|
if request.method == 'POST':
|
||||||
|
email = request.form['email']
|
||||||
|
password = request.form['password']
|
||||||
|
if email == 'admin@admin.com' and password == '123456':
|
||||||
|
session['user'] = 'Admin'
|
||||||
|
return redirect(url_for('dashboard'))
|
||||||
|
else:
|
||||||
|
flash('Credenciais inválidas.', 'danger')
|
||||||
|
return render_template('login.html')
|
||||||
|
|
||||||
|
# Rota protegida
|
||||||
|
@app.route('/dashboard')
|
||||||
|
def dashboard():
|
||||||
|
if 'user' not in session:
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
return render_template('dashboard.html', user_name=session['user'])
|
||||||
|
|
||||||
|
# Rota de upload
|
||||||
|
@app.route('/upload', methods=['GET', 'POST'])
|
||||||
|
def upload():
|
||||||
|
if 'user' not in session:
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
if request.method == 'POST':
|
||||||
|
file = request.files['file']
|
||||||
|
if file:
|
||||||
|
filepath = os.path.join('uploads', file.filename)
|
||||||
|
os.makedirs('uploads', exist_ok=True)
|
||||||
|
file.save(filepath)
|
||||||
|
flash('Arquivo enviado com sucesso!', 'success')
|
||||||
|
return render_template('upload.html')
|
||||||
|
|
||||||
|
# Logout
|
||||||
|
@app.route('/logout')
|
||||||
|
def logout():
|
||||||
|
session.clear()
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(debug=True, port=5003)
|
||||||
0
app/controllers/__init__.py
Normal file
0
app/controllers/__init__.py
Normal file
13
app/controllers/auth_controller.py
Normal file
13
app/controllers/auth_controller.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from app.auth import generate_token
|
||||||
|
from app.models.user import User
|
||||||
|
from werkzeug.security import check_password_hash
|
||||||
|
from flask import jsonify
|
||||||
|
|
||||||
|
def authenticate_user(email, password):
|
||||||
|
user = User.query.filter_by(email=email).first()
|
||||||
|
|
||||||
|
if not user or not check_password_hash(user.password, password):
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
token = generate_token(user)
|
||||||
|
return user, token
|
||||||
12
app/routes/dashboard.py
Normal file
12
app/routes/dashboard.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
from flask import Blueprint, render_template, session, redirect, url_for, flash
|
||||||
|
|
||||||
|
dashboard_bp = Blueprint("dashboard", __name__, template_folder="templates")
|
||||||
|
|
||||||
|
@dashboard_bp.route("/")
|
||||||
|
def dashboard():
|
||||||
|
"""Protege a rota do dashboard"""
|
||||||
|
if "user_id" not in session:
|
||||||
|
flash("Faça login para acessar o dashboard.", "warning")
|
||||||
|
return redirect(url_for("login_form.login"))
|
||||||
|
|
||||||
|
return render_template("dashboard.html", user_email=session.get("user_email"))
|
||||||
30
app/routes/login_form.py
Normal file
30
app/routes/login_form.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
from flask import Blueprint, render_template, request, redirect, url_for, flash, session
|
||||||
|
from app.controllers.auth_controller import authenticate_user
|
||||||
|
|
||||||
|
login_bp = Blueprint("login_form", __name__, template_folder="templates")
|
||||||
|
|
||||||
|
@login_bp.route("/", methods=["GET", "POST"])
|
||||||
|
def login():
|
||||||
|
"""Rota de login"""
|
||||||
|
if request.method == "POST":
|
||||||
|
email = request.form.get("email")
|
||||||
|
password = request.form.get("password")
|
||||||
|
|
||||||
|
user, token = authenticate_user(email, password)
|
||||||
|
|
||||||
|
if user:
|
||||||
|
session["user_id"] = user.id
|
||||||
|
session["user_email"] = user.email
|
||||||
|
flash("Login bem-sucedido!", "success")
|
||||||
|
return redirect(url_for("dashboard.dashboard"))
|
||||||
|
|
||||||
|
flash("Credenciais inválidas!", "danger")
|
||||||
|
|
||||||
|
return render_template("login.html")
|
||||||
|
|
||||||
|
@login_bp.route("/logout")
|
||||||
|
def logout():
|
||||||
|
"""Rota de logout"""
|
||||||
|
session.clear()
|
||||||
|
flash("Você saiu da conta.", "info")
|
||||||
|
return redirect(url_for("login_form.login"))
|
||||||
72
app/services/ocr_service.py
Normal file
72
app/services/ocr_service.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
from typing import Dict
|
||||||
|
from PIL import Image
|
||||||
|
from pdf2image import convert_from_path
|
||||||
|
from app.services.correction_service import correct_text
|
||||||
|
from app.services.engines.tesseract_ocr import ocr_tesseract
|
||||||
|
from app.services.engines.easyocr_ocr import ocr_easyocr
|
||||||
|
from app.services.engines.paddleocr_ocr import ocr_paddleocr
|
||||||
|
# from app.services.engines.mmocr_ocr import ocr_mmocr # Opcional
|
||||||
|
from app.utils import save_text_file
|
||||||
|
|
||||||
|
UPLOAD_DIR = "app/static/uploads"
|
||||||
|
TEXTS_DIR = "app/static/texts"
|
||||||
|
|
||||||
|
def process_document(file_path: str) -> Dict[str, str]:
|
||||||
|
"""
|
||||||
|
Processa um arquivo PDF ou imagem, aplica OCR com diferentes engines e salva os textos extraídos.
|
||||||
|
Retorna os textos por engine e o texto corrigido.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path (str): Caminho do arquivo a ser processado.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, str]: Dicionário com os textos por engine e o texto corrigido.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
raise FileNotFoundError(f"Arquivo não encontrado: {file_path}")
|
||||||
|
|
||||||
|
filename = os.path.basename(file_path)
|
||||||
|
base_name = os.path.splitext(filename)[0]
|
||||||
|
output_folder = os.path.join(TEXTS_DIR, base_name)
|
||||||
|
os.makedirs(output_folder, exist_ok=True)
|
||||||
|
|
||||||
|
# Converte PDF para imagens ou carrega imagem única
|
||||||
|
images = convert_from_path(file_path) if file_path.lower().endswith(".pdf") else [Image.open(file_path)]
|
||||||
|
|
||||||
|
results = {
|
||||||
|
"tesseract": "",
|
||||||
|
"easyocr": "",
|
||||||
|
"paddleocr": "",
|
||||||
|
# "mmocr": ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Processa cada página/imagem
|
||||||
|
for i, image in enumerate(images):
|
||||||
|
temp_img_path = os.path.join(output_folder, f"page_{i}.png")
|
||||||
|
image.save(temp_img_path)
|
||||||
|
|
||||||
|
results["tesseract"] += ocr_tesseract(image) + "\n"
|
||||||
|
results["easyocr"] += ocr_easyocr(temp_img_path) + "\n"
|
||||||
|
results["paddleocr"] += ocr_paddleocr(temp_img_path) + "\n"
|
||||||
|
# results["mmocr"] += ocr_mmocr(temp_img_path) + "\n"
|
||||||
|
|
||||||
|
os.remove(temp_img_path)
|
||||||
|
|
||||||
|
# Salva os textos extraídos
|
||||||
|
for engine, text in results.items():
|
||||||
|
save_text_file(text, os.path.join(output_folder, f"{engine}.txt"))
|
||||||
|
|
||||||
|
# Aplica correção no texto do tesseract
|
||||||
|
corrected = correct_text(results["tesseract"])
|
||||||
|
save_text_file(corrected, os.path.join(output_folder, "corrigido.txt"))
|
||||||
|
results["corrigido"] = corrected
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERRO] Falha ao processar documento: {e}")
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
27
app/statics/css/style.css
Normal file
27
app/statics/css/style.css
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
body {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
color: #343a40;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
background: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0px 3px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.btn {
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.btn:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
21
app/templates/base.html
Normal file
21
app/templates/base.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pt-br">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>{% block title %}OCR App{% endblock %}</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark px-4">
|
||||||
|
<a class="navbar-brand" href="#">OCR App</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container mt-4">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
9
app/templates/dashboard.html
Normal file
9
app/templates/dashboard.html
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Dashboard - OCR App{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h3>Bem-vindo(a), {{ user_name }}</h3>
|
||||||
|
<p>Escolha uma ação no menu ou envie um novo documento para OCR.</p>
|
||||||
|
<a href="/upload" class="btn btn-outline-primary">Enviar novo documento</a>
|
||||||
|
{% endblock %}
|
||||||
22
app/templates/login.html
Normal file
22
app/templates/login.html
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Login - OCR App{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<h3 class="mb-4">Login</h3>
|
||||||
|
<form method="POST" action="/login">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label>Email:</label>
|
||||||
|
<input type="email" name="email" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label>Senha:</label>
|
||||||
|
<input type="password" name="password" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary w-100" type="submit">Entrar</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
14
app/templates/upload.html
Normal file
14
app/templates/upload.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Upload de Arquivo - OCR App{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h3>Upload de Documento</h3>
|
||||||
|
<form method="POST" action="/upload" enctype="multipart/form-data">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="file" class="form-label">Selecionar Arquivo</label>
|
||||||
|
<input class="form-control" type="file" id="file" name="file" accept=".pdf,.jpg,.png,.jpeg" required>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-success" type="submit">Enviar</button>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
@ -1,13 +1,16 @@
|
|||||||
attrs==25.1.0
|
attrs==25.1.0
|
||||||
blinker==1.9.0
|
blinker==1.9.0
|
||||||
click==8.1.8
|
click==8.1.8
|
||||||
|
exceptiongroup==1.2.2
|
||||||
factory_boy==3.3.3
|
factory_boy==3.3.3
|
||||||
Faker==36.2.2
|
Faker==36.2.2
|
||||||
flasgger==0.9.7.1
|
flasgger==0.9.7.1
|
||||||
Flask==3.1.0
|
Flask==3.1.0
|
||||||
|
Flask-JWT-Extended==4.7.1
|
||||||
Flask-SQLAlchemy==3.1.1
|
Flask-SQLAlchemy==3.1.1
|
||||||
Flask-Testing==0.8.1
|
Flask-Testing==0.8.1
|
||||||
greenlet==3.1.1
|
greenlet==3.1.1
|
||||||
|
importlib_metadata==8.6.1
|
||||||
iniconfig==2.0.0
|
iniconfig==2.0.0
|
||||||
itsdangerous==2.2.0
|
itsdangerous==2.2.0
|
||||||
Jinja2==3.1.6
|
Jinja2==3.1.6
|
||||||
@ -17,12 +20,15 @@ MarkupSafe==3.0.2
|
|||||||
mistune==3.1.2
|
mistune==3.1.2
|
||||||
packaging==24.2
|
packaging==24.2
|
||||||
pluggy==1.5.0
|
pluggy==1.5.0
|
||||||
|
PyJWT==2.10.1
|
||||||
pytest==8.3.5
|
pytest==8.3.5
|
||||||
PyYAML==6.0.2
|
PyYAML==6.0.2
|
||||||
referencing==0.36.2
|
referencing==0.36.2
|
||||||
rpds-py==0.23.1
|
rpds-py==0.23.1
|
||||||
six==1.17.0
|
six==1.17.0
|
||||||
SQLAlchemy==2.0.38
|
SQLAlchemy==2.0.38
|
||||||
|
tomli==2.2.1
|
||||||
typing_extensions==4.12.2
|
typing_extensions==4.12.2
|
||||||
tzdata==2025.1
|
tzdata==2025.1
|
||||||
Werkzeug==3.1.3
|
Werkzeug==3.1.3
|
||||||
|
zipp==3.21.0
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user