Spaces:
Paused
Paused
Upload 132 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +27 -0
- controller/__init__.py +0 -0
- controller/__pycache__/__init__.cpython-311.pyc +0 -0
- controller/__pycache__/__init__.cpython-313.pyc +0 -0
- controller/__pycache__/auth_controller.cpython-311.pyc +0 -0
- controller/__pycache__/auth_controller.cpython-313.pyc +0 -0
- controller/__pycache__/bbbb.cpython-311.pyc +0 -0
- controller/__pycache__/chat_controller.cpython-313.pyc +0 -0
- controller/__pycache__/graph_controller.cpython-311.pyc +0 -0
- controller/__pycache__/graph_controller.cpython-313.pyc +0 -0
- controller/__pycache__/latex_to_text_controller.cpython-313.pyc +0 -0
- controller/__pycache__/mathcamera_controller.cpython-311.pyc +0 -0
- controller/__pycache__/pdf_controller.cpython-311.pyc +0 -0
- controller/__pycache__/pdf_controller.cpython-313.pyc +0 -0
- controller/__pycache__/pdffly_controller.cpython-311.pyc +0 -0
- controller/__pycache__/pdffly_controller.cpython-313.pyc +0 -0
- controller/__pycache__/pdflly_controller.cpython-311.pyc +0 -0
- controller/__pycache__/pdflly_controller.cpython-313.pyc +0 -0
- controller/__pycache__/pix2text_controller.cpython-311.pyc +0 -0
- controller/__pycache__/pix2text_controller.cpython-313.pyc +0 -0
- controller/__pycache__/scribble_controller.cpython-311.pyc +0 -0
- controller/__pycache__/scribble_controller.cpython-313.pyc +0 -0
- controller/__pycache__/table_controller.cpython-311.pyc +0 -0
- controller/__pycache__/table_controller.cpython-313.pyc +0 -0
- controller/auth_controller.py +97 -0
- controller/graph_controller.py +189 -0
- controller/models/__init__.py +0 -0
- controller/models/__pycache__/__init__.cpython-311.pyc +0 -0
- controller/models/__pycache__/__init__.cpython-313.pyc +0 -0
- controller/models/__pycache__/camera_to_latex.cpython-311.pyc +0 -0
- controller/models/__pycache__/camera_to_latex.cpython-313.pyc +0 -0
- controller/models/__pycache__/math_equation.cpython-311.pyc +0 -0
- controller/models/__pycache__/math_equation.cpython-313.pyc +0 -0
- controller/models/__pycache__/scribble_to_latex.cpython-311.pyc +0 -0
- controller/models/__pycache__/scribble_to_latex.cpython-313.pyc +0 -0
- controller/models/camera_to_latex.py +29 -0
- controller/models/math_equation.py +37 -0
- controller/models/scribble_to_latex.py +22 -0
- controller/pdf_controller.py +166 -0
- controller/pdffly_controller.py +274 -0
- controller/pdflly_controller.py +166 -0
- controller/pix2text_controller.py +223 -0
- controller/scribble_controller.py +159 -0
- controller/table_controller.py +192 -0
- data/users.json +74 -0
- models/__init__.py +1 -0
- models/__pycache__/__init__.cpython-311.pyc +0 -0
- models/__pycache__/__init__.cpython-313.pyc +0 -0
- models/__pycache__/user.cpython-311.pyc +0 -0
- models/__pycache__/user.cpython-313.pyc +0 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,30 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
static/uploads/Assignment_1_Solution.pdf filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
static/uploads/b0d6064f-3245-40bc-87b5-bfbb459979fd.jpeg filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
static/uploads/capture.jpg filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
static/uploads/CSE331.pdf filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
static/uploads/input2.png filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
static/uploads/math_formula_sheet.pdf filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
static/uploads/page_10.png filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
static/uploads/page_11.png filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
static/uploads/page_2.png filter=lfs diff=lfs merge=lfs -text
|
| 45 |
+
static/uploads/page_3.png filter=lfs diff=lfs merge=lfs -text
|
| 46 |
+
static/uploads/page_4.png filter=lfs diff=lfs merge=lfs -text
|
| 47 |
+
static/uploads/page_5.png filter=lfs diff=lfs merge=lfs -text
|
| 48 |
+
static/uploads/page_6.png filter=lfs diff=lfs merge=lfs -text
|
| 49 |
+
static/uploads/page_7.png filter=lfs diff=lfs merge=lfs -text
|
| 50 |
+
static/uploads/page_8.png filter=lfs diff=lfs merge=lfs -text
|
| 51 |
+
static/uploads/page_9.png filter=lfs diff=lfs merge=lfs -text
|
| 52 |
+
static/uploads/table_26.png filter=lfs diff=lfs merge=lfs -text
|
| 53 |
+
static/uploads/table_27.png filter=lfs diff=lfs merge=lfs -text
|
| 54 |
+
static/uploads/table_28.png filter=lfs diff=lfs merge=lfs -text
|
| 55 |
+
static/uploads/table_29.png filter=lfs diff=lfs merge=lfs -text
|
| 56 |
+
static/uploads/table_30.png filter=lfs diff=lfs merge=lfs -text
|
| 57 |
+
static/uploads/table_31.png filter=lfs diff=lfs merge=lfs -text
|
| 58 |
+
static/uploads/table_32.png filter=lfs diff=lfs merge=lfs -text
|
| 59 |
+
static/uploads/table_33.png filter=lfs diff=lfs merge=lfs -text
|
| 60 |
+
static/uploads/table_34.png filter=lfs diff=lfs merge=lfs -text
|
| 61 |
+
static/uploads/table_35.png filter=lfs diff=lfs merge=lfs -text
|
| 62 |
+
static/uploads/WhatsApp[[:space:]]Image[[:space:]]2025-10-26[[:space:]]at[[:space:]]01.38.22_3d87e144.jpg filter=lfs diff=lfs merge=lfs -text
|
controller/__init__.py
ADDED
|
File without changes
|
controller/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (165 Bytes). View file
|
|
|
controller/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (136 Bytes). View file
|
|
|
controller/__pycache__/auth_controller.cpython-311.pyc
ADDED
|
Binary file (4.73 kB). View file
|
|
|
controller/__pycache__/auth_controller.cpython-313.pyc
ADDED
|
Binary file (4.2 kB). View file
|
|
|
controller/__pycache__/bbbb.cpython-311.pyc
ADDED
|
Binary file (5.48 kB). View file
|
|
|
controller/__pycache__/chat_controller.cpython-313.pyc
ADDED
|
Binary file (4.22 kB). View file
|
|
|
controller/__pycache__/graph_controller.cpython-311.pyc
ADDED
|
Binary file (7.84 kB). View file
|
|
|
controller/__pycache__/graph_controller.cpython-313.pyc
ADDED
|
Binary file (7.05 kB). View file
|
|
|
controller/__pycache__/latex_to_text_controller.cpython-313.pyc
ADDED
|
Binary file (4.65 kB). View file
|
|
|
controller/__pycache__/mathcamera_controller.cpython-311.pyc
ADDED
|
Binary file (5.49 kB). View file
|
|
|
controller/__pycache__/pdf_controller.cpython-311.pyc
ADDED
|
Binary file (8.25 kB). View file
|
|
|
controller/__pycache__/pdf_controller.cpython-313.pyc
ADDED
|
Binary file (7.09 kB). View file
|
|
|
controller/__pycache__/pdffly_controller.cpython-311.pyc
ADDED
|
Binary file (12.8 kB). View file
|
|
|
controller/__pycache__/pdffly_controller.cpython-313.pyc
ADDED
|
Binary file (11.3 kB). View file
|
|
|
controller/__pycache__/pdflly_controller.cpython-311.pyc
ADDED
|
Binary file (3.19 kB). View file
|
|
|
controller/__pycache__/pdflly_controller.cpython-313.pyc
ADDED
|
Binary file (7.1 kB). View file
|
|
|
controller/__pycache__/pix2text_controller.cpython-311.pyc
ADDED
|
Binary file (10.2 kB). View file
|
|
|
controller/__pycache__/pix2text_controller.cpython-313.pyc
ADDED
|
Binary file (9.03 kB). View file
|
|
|
controller/__pycache__/scribble_controller.cpython-311.pyc
ADDED
|
Binary file (7.97 kB). View file
|
|
|
controller/__pycache__/scribble_controller.cpython-313.pyc
ADDED
|
Binary file (7.01 kB). View file
|
|
|
controller/__pycache__/table_controller.cpython-311.pyc
ADDED
|
Binary file (9.45 kB). View file
|
|
|
controller/__pycache__/table_controller.cpython-313.pyc
ADDED
|
Binary file (8.3 kB). View file
|
|
|
controller/auth_controller.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import time
|
| 3 |
+
from flask import Blueprint, render_template, request, jsonify, redirect, url_for, session, current_app
|
| 4 |
+
from flask_login import LoginManager, login_user, logout_user, login_required, current_user
|
| 5 |
+
import requests
|
| 6 |
+
from models.user import User
|
| 7 |
+
|
| 8 |
+
auth_bp = Blueprint('auth', __name__)
|
| 9 |
+
|
| 10 |
+
# Google OAuth configuration (these would normally come from environment variables)
|
| 11 |
+
GOOGLE_CLIENT_ID = os.environ.get('GOOGLE_CLIENT_ID', 'your-google-client-id')
|
| 12 |
+
GOOGLE_CLIENT_SECRET = os.environ.get('GOOGLE_CLIENT_SECRET', 'your-google-client-secret')
|
| 13 |
+
GOOGLE_REDIRECT_URI = os.environ.get('GOOGLE_REDIRECT_URI', 'http://localhost:5000/auth/google/callback')
|
| 14 |
+
|
| 15 |
+
# Initialize login manager
|
| 16 |
+
login_manager = LoginManager()
|
| 17 |
+
|
| 18 |
+
def init_app(app):
|
| 19 |
+
"""Initialize the login manager with the app"""
|
| 20 |
+
login_manager.init_app(app)
|
| 21 |
+
# Set login view - using setattr to avoid type checking issues
|
| 22 |
+
setattr(login_manager, 'login_view', 'auth.login')
|
| 23 |
+
return login_manager
|
| 24 |
+
|
| 25 |
+
@login_manager.user_loader
|
| 26 |
+
def load_user(user_id):
|
| 27 |
+
return User.get(user_id)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@auth_bp.route("/login")
|
| 31 |
+
def login():
|
| 32 |
+
"""Display the login page"""
|
| 33 |
+
return render_template('login.html')
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
@auth_bp.route("/google/login")
|
| 37 |
+
def google_login():
|
| 38 |
+
"""Redirect to Google OAuth login"""
|
| 39 |
+
# In a real implementation, you would redirect to Google's OAuth endpoint
|
| 40 |
+
# For now, we'll create a dummy user for testing
|
| 41 |
+
user = User.create_or_update(
|
| 42 |
+
id="google_user_123",
|
| 43 |
+
email="user@gmail.com",
|
| 44 |
+
name="Google User",
|
| 45 |
+
picture=None
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
if user:
|
| 49 |
+
login_user(user)
|
| 50 |
+
return redirect(url_for('index'))
|
| 51 |
+
else:
|
| 52 |
+
return "Failed to create user", 500
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
@auth_bp.route("/guest/login", methods=['POST'])
|
| 56 |
+
def guest_login():
|
| 57 |
+
"""Login as a guest user"""
|
| 58 |
+
name = request.form.get('name')
|
| 59 |
+
if not name:
|
| 60 |
+
return "Name is required", 400
|
| 61 |
+
|
| 62 |
+
# Create a guest user
|
| 63 |
+
user = User.create_or_update(
|
| 64 |
+
id=f"guest_{name.lower().replace(' ', '_')}_{int(time.time())}",
|
| 65 |
+
email=f"{name.lower().replace(' ', '.')}@guest.texlab.com",
|
| 66 |
+
name=name,
|
| 67 |
+
picture=None
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
if user:
|
| 71 |
+
login_user(user)
|
| 72 |
+
return redirect(url_for('index'))
|
| 73 |
+
else:
|
| 74 |
+
return "Failed to create user", 500
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
@auth_bp.route("/logout")
|
| 78 |
+
@login_required
|
| 79 |
+
def logout():
|
| 80 |
+
logout_user()
|
| 81 |
+
return redirect(url_for('auth.login'))
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
@auth_bp.route("/user")
|
| 85 |
+
def get_current_user():
|
| 86 |
+
if current_user.is_authenticated:
|
| 87 |
+
return jsonify({
|
| 88 |
+
'authenticated': True,
|
| 89 |
+
'user': {
|
| 90 |
+
'id': current_user.id,
|
| 91 |
+
'email': current_user.email,
|
| 92 |
+
'name': current_user.name,
|
| 93 |
+
'picture': current_user.picture
|
| 94 |
+
}
|
| 95 |
+
})
|
| 96 |
+
else:
|
| 97 |
+
return jsonify({'authenticated': False})
|
controller/graph_controller.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from flask import Blueprint, render_template, request, jsonify
|
| 3 |
+
import numpy as np
|
| 4 |
+
import matplotlib
|
| 5 |
+
matplotlib.use('Agg') # Use non-interactive backend
|
| 6 |
+
import matplotlib.pyplot as plt
|
| 7 |
+
import base64
|
| 8 |
+
from io import BytesIO
|
| 9 |
+
import re
|
| 10 |
+
|
| 11 |
+
graph_bp = Blueprint('graph_bp', __name__)
|
| 12 |
+
|
| 13 |
+
UPLOAD_FOLDER = 'static/uploads'
|
| 14 |
+
GRAPHS_FOLDER = 'static/graphs'
|
| 15 |
+
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
| 16 |
+
os.makedirs(GRAPHS_FOLDER, exist_ok=True)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def parse_plot_command(command):
|
| 20 |
+
"""Parse plot command and extract function and range"""
|
| 21 |
+
# Remove extra spaces
|
| 22 |
+
command = command.strip()
|
| 23 |
+
|
| 24 |
+
# Match patterns like "plot x^2 from -5 to 5" or "plot sin(x) from 0 to 2*pi"
|
| 25 |
+
pattern = r"plot\s+(.+?)\s+from\s+(-?[\d\*\.pi]+)\s+to\s+(-?[\d\*\.pi]+)"
|
| 26 |
+
match = re.match(pattern, command, re.IGNORECASE)
|
| 27 |
+
|
| 28 |
+
if match:
|
| 29 |
+
function = match.group(1)
|
| 30 |
+
x_min = match.group(2)
|
| 31 |
+
x_max = match.group(3)
|
| 32 |
+
|
| 33 |
+
# Convert pi expressions
|
| 34 |
+
x_min = x_min.replace('pi', 'np.pi')
|
| 35 |
+
x_max = x_max.replace('pi', 'np.pi')
|
| 36 |
+
|
| 37 |
+
try:
|
| 38 |
+
x_min_val = eval(x_min)
|
| 39 |
+
x_max_val = eval(x_max)
|
| 40 |
+
return function, x_min_val, x_max_val
|
| 41 |
+
except:
|
| 42 |
+
raise ValueError("Invalid range values")
|
| 43 |
+
|
| 44 |
+
# Match simpler patterns like "plot x^2"
|
| 45 |
+
simple_pattern = r"plot\s+(.+)"
|
| 46 |
+
simple_match = re.match(simple_pattern, command, re.IGNORECASE)
|
| 47 |
+
|
| 48 |
+
if simple_match:
|
| 49 |
+
function = simple_match.group(1)
|
| 50 |
+
return function, -10, 10 # Default range
|
| 51 |
+
|
| 52 |
+
raise ValueError("Invalid plot command format")
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def evaluate_function(func_str, x):
|
| 56 |
+
"""Safely evaluate mathematical function"""
|
| 57 |
+
# Replace common math functions
|
| 58 |
+
func_str = func_str.replace('^', '**')
|
| 59 |
+
func_str = func_str.replace('sin', 'np.sin')
|
| 60 |
+
func_str = func_str.replace('cos', 'np.cos')
|
| 61 |
+
func_str = func_str.replace('tan', 'np.tan')
|
| 62 |
+
func_str = func_str.replace('log', 'np.log')
|
| 63 |
+
func_str = func_str.replace('exp', 'np.exp')
|
| 64 |
+
func_str = func_str.replace('sqrt', 'np.sqrt')
|
| 65 |
+
func_str = func_str.replace('abs', 'np.abs')
|
| 66 |
+
|
| 67 |
+
# Replace x with the actual value
|
| 68 |
+
func_str = func_str.replace('x', f'({x})')
|
| 69 |
+
|
| 70 |
+
try:
|
| 71 |
+
return eval(func_str)
|
| 72 |
+
except:
|
| 73 |
+
raise ValueError(f"Error evaluating function: {func_str}")
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def generate_tikz_latex(function, x_min, x_max):
|
| 77 |
+
"""Generate TikZ/pgfplots LaTeX code for the function"""
|
| 78 |
+
# Convert function to pgfplots format
|
| 79 |
+
pgf_function = function.replace('^', '^')
|
| 80 |
+
pgf_function = pgf_function.replace('sqrt', 'sqrt')
|
| 81 |
+
|
| 82 |
+
# Create complete LaTeX document with TikZ/pgfplots
|
| 83 |
+
latex_code = r'''\documentclass{article}
|
| 84 |
+
\usepackage{pgfplots}
|
| 85 |
+
\usepackage{amsmath}
|
| 86 |
+
\pgfplotsset{compat=1.18}
|
| 87 |
+
|
| 88 |
+
\begin{document}
|
| 89 |
+
|
| 90 |
+
\begin{figure}[h]
|
| 91 |
+
\centering
|
| 92 |
+
\begin{tikzpicture}
|
| 93 |
+
\begin{axis}[
|
| 94 |
+
xlabel={$x$},
|
| 95 |
+
ylabel={$y$},
|
| 96 |
+
grid=major,
|
| 97 |
+
width=12cm,
|
| 98 |
+
height=8cm,
|
| 99 |
+
samples=200,
|
| 100 |
+
domain=''' + f"{x_min}:{x_max}" + r''',
|
| 101 |
+
legend pos=north west,
|
| 102 |
+
axis lines=middle,
|
| 103 |
+
]
|
| 104 |
+
\addplot[blue, thick] {''' + pgf_function + r'''};
|
| 105 |
+
\legend{$f(x)=''' + function + r'''$}
|
| 106 |
+
\end{axis}
|
| 107 |
+
\end{tikzpicture}
|
| 108 |
+
\caption{Graph of $f(x) = ''' + function + r'''$}
|
| 109 |
+
\label{fig:graph}
|
| 110 |
+
\end{figure}
|
| 111 |
+
|
| 112 |
+
\end{document}'''
|
| 113 |
+
|
| 114 |
+
return latex_code
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def generate_plot(function, x_min, x_max):
|
| 118 |
+
"""Generate plot and return base64 image"""
|
| 119 |
+
try:
|
| 120 |
+
# Create x values
|
| 121 |
+
x = np.linspace(x_min, x_max, 1000)
|
| 122 |
+
|
| 123 |
+
# Evaluate function for all x values
|
| 124 |
+
y = []
|
| 125 |
+
for xi in x:
|
| 126 |
+
try:
|
| 127 |
+
yi = evaluate_function(function, xi)
|
| 128 |
+
y.append(yi)
|
| 129 |
+
except:
|
| 130 |
+
y.append(np.nan) # Handle undefined points
|
| 131 |
+
|
| 132 |
+
y = np.array(y)
|
| 133 |
+
|
| 134 |
+
# Create plot
|
| 135 |
+
plt.figure(figsize=(10, 6))
|
| 136 |
+
plt.plot(x, y, linewidth=2, color='#667eea')
|
| 137 |
+
plt.grid(True, alpha=0.3)
|
| 138 |
+
plt.xlabel('x')
|
| 139 |
+
plt.ylabel('y')
|
| 140 |
+
plt.title(f'Graph of f(x) = {function}')
|
| 141 |
+
|
| 142 |
+
# Save to base64 string
|
| 143 |
+
buffer = BytesIO()
|
| 144 |
+
plt.savefig(buffer, format='png', dpi=150, bbox_inches='tight')
|
| 145 |
+
buffer.seek(0)
|
| 146 |
+
image_base64 = base64.b64encode(buffer.getvalue()).decode()
|
| 147 |
+
plt.close()
|
| 148 |
+
|
| 149 |
+
return image_base64
|
| 150 |
+
except Exception as e:
|
| 151 |
+
raise ValueError(f"Error generating plot: {str(e)}")
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
@graph_bp.route("/graph")
|
| 155 |
+
def graph_page():
|
| 156 |
+
return render_template("graph.html")
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
@graph_bp.route("/graph/generate", methods=["POST"])
|
| 160 |
+
def generate_graph():
|
| 161 |
+
"""Generate graph from user input"""
|
| 162 |
+
try:
|
| 163 |
+
data = request.get_json()
|
| 164 |
+
if not data:
|
| 165 |
+
return jsonify({'error': 'No data provided'}), 400
|
| 166 |
+
|
| 167 |
+
command = data.get('command', '')
|
| 168 |
+
if not command:
|
| 169 |
+
return jsonify({'error': 'No command provided'}), 400
|
| 170 |
+
|
| 171 |
+
# Parse the command
|
| 172 |
+
function, x_min, x_max = parse_plot_command(command)
|
| 173 |
+
|
| 174 |
+
# Generate the plot image
|
| 175 |
+
image_base64 = generate_plot(function, x_min, x_max)
|
| 176 |
+
|
| 177 |
+
# Generate TikZ/pgfplots LaTeX code
|
| 178 |
+
latex_code = generate_tikz_latex(function, x_min, x_max)
|
| 179 |
+
|
| 180 |
+
return jsonify({
|
| 181 |
+
'success': True,
|
| 182 |
+
'image': image_base64,
|
| 183 |
+
'function': function,
|
| 184 |
+
'range': f'[{x_min}, {x_max}]',
|
| 185 |
+
'latex': latex_code
|
| 186 |
+
})
|
| 187 |
+
|
| 188 |
+
except Exception as e:
|
| 189 |
+
return jsonify({'error': str(e)}), 500
|
controller/models/__init__.py
ADDED
|
File without changes
|
controller/models/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (172 Bytes). View file
|
|
|
controller/models/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (143 Bytes). View file
|
|
|
controller/models/__pycache__/camera_to_latex.cpython-311.pyc
ADDED
|
Binary file (2 kB). View file
|
|
|
controller/models/__pycache__/camera_to_latex.cpython-313.pyc
ADDED
|
Binary file (1.76 kB). View file
|
|
|
controller/models/__pycache__/math_equation.cpython-311.pyc
ADDED
|
Binary file (1.92 kB). View file
|
|
|
controller/models/__pycache__/math_equation.cpython-313.pyc
ADDED
|
Binary file (1.69 kB). View file
|
|
|
controller/models/__pycache__/scribble_to_latex.cpython-311.pyc
ADDED
|
Binary file (1.37 kB). View file
|
|
|
controller/models/__pycache__/scribble_to_latex.cpython-313.pyc
ADDED
|
Binary file (1.16 kB). View file
|
|
|
controller/models/camera_to_latex.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# controller/models/camera_to_latex.py
|
| 2 |
+
|
| 3 |
+
import base64
|
| 4 |
+
import io
|
| 5 |
+
from PIL import Image
|
| 6 |
+
from transformers import TrOCRProcessor
|
| 7 |
+
from optimum.onnxruntime import ORTModelForVision2Seq
|
| 8 |
+
|
| 9 |
+
print("🔹 Loading Pix2Text model for Camera → LaTeX...")
|
| 10 |
+
|
| 11 |
+
processor = TrOCRProcessor.from_pretrained("breezedeus/pix2text-mfr")
|
| 12 |
+
model = ORTModelForVision2Seq.from_pretrained("breezedeus/pix2text-mfr", use_cache=False)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def camera_to_latex(image_base64: str) -> str:
|
| 16 |
+
try:
|
| 17 |
+
# Remove header (e.g., "data:image/png;base64,")
|
| 18 |
+
image_data = image_base64.split(",")[1]
|
| 19 |
+
image_bytes = base64.b64decode(image_data)
|
| 20 |
+
image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
|
| 21 |
+
|
| 22 |
+
pixel_values = processor(images=image, return_tensors="pt").pixel_values
|
| 23 |
+
generated_ids = model.generate(pixel_values)
|
| 24 |
+
text = processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
|
| 25 |
+
|
| 26 |
+
return text.strip()
|
| 27 |
+
except Exception as e:
|
| 28 |
+
print(f"❌ Error in camera_to_latex: {e}")
|
| 29 |
+
return "⚠️ Error generating LaTeX"
|
controller/models/math_equation.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask, request, jsonify, render_template
|
| 2 |
+
import os
|
| 3 |
+
from werkzeug.utils import secure_filename
|
| 4 |
+
from pix2text import Pix2Text # Make sure you installed it
|
| 5 |
+
from PIL import Image
|
| 6 |
+
|
| 7 |
+
app = Flask(__name__)
|
| 8 |
+
app.config['UPLOAD_FOLDER'] = 'static/uploads'
|
| 9 |
+
app.config['ALLOWED_EXTENSIONS'] = {'png', 'jpg', 'jpeg', 'gif'}
|
| 10 |
+
|
| 11 |
+
p2t = Pix2Text() # Load Pix2Text model
|
| 12 |
+
|
| 13 |
+
def allowed_file(filename):
|
| 14 |
+
return '.' in filename and filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS']
|
| 15 |
+
|
| 16 |
+
@app.route('/math/process', methods=['POST'])
|
| 17 |
+
def process_math():
|
| 18 |
+
if 'image' not in request.files:
|
| 19 |
+
return jsonify({'success': False, 'error': 'No file part'})
|
| 20 |
+
|
| 21 |
+
file = request.files['image']
|
| 22 |
+
if file.filename == '':
|
| 23 |
+
return jsonify({'success': False, 'error': 'No selected file'})
|
| 24 |
+
|
| 25 |
+
if file and allowed_file(file.filename):
|
| 26 |
+
filename = secure_filename(file.filename)
|
| 27 |
+
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
| 28 |
+
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
| 29 |
+
file.save(filepath)
|
| 30 |
+
|
| 31 |
+
try:
|
| 32 |
+
result = p2t(Image.open(filepath)) # Convert image → LaTeX
|
| 33 |
+
return jsonify({'success': True, 'latex': result, 'image_path': filepath})
|
| 34 |
+
except Exception as e:
|
| 35 |
+
return jsonify({'success': False, 'error': str(e)})
|
| 36 |
+
else:
|
| 37 |
+
return jsonify({'success': False, 'error': 'Invalid file type'})
|
controller/models/scribble_to_latex.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# controller/models/scribble_to_latex.py
|
| 2 |
+
import base64
|
| 3 |
+
from io import BytesIO
|
| 4 |
+
from PIL import Image
|
| 5 |
+
from .math_equation import image_to_latex # reuse your existing pix2tex model
|
| 6 |
+
|
| 7 |
+
def scribble_to_latex(image_data: str):
|
| 8 |
+
"""
|
| 9 |
+
Convert scribble (base64 PNG) to LaTeX using Pix2Text model.
|
| 10 |
+
"""
|
| 11 |
+
try:
|
| 12 |
+
# Decode base64 image
|
| 13 |
+
image_bytes = base64.b64decode(image_data.split(',')[1])
|
| 14 |
+
image = Image.open(BytesIO(image_bytes)).convert("RGB")
|
| 15 |
+
|
| 16 |
+
# Call your existing model
|
| 17 |
+
latex_code = image_to_latex(image)
|
| 18 |
+
|
| 19 |
+
return latex_code.strip()
|
| 20 |
+
except Exception as e:
|
| 21 |
+
print(f"❌ Error in scribble_to_latex: {e}")
|
| 22 |
+
return "⚠️ Failed to process scribble"
|
controller/pdf_controller.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import base64
|
| 4 |
+
import io
|
| 5 |
+
from flask import Blueprint, request, render_template, jsonify, current_app
|
| 6 |
+
import fitz # PyMuPDF
|
| 7 |
+
import PyPDF2
|
| 8 |
+
from PIL import Image
|
| 9 |
+
import tempfile
|
| 10 |
+
|
| 11 |
+
pdf_bp = Blueprint('pdf', __name__, url_prefix='/pdf')
|
| 12 |
+
|
| 13 |
+
def extract_text_from_pdf(filepath, page_number=None, coordinates=None):
|
| 14 |
+
"""Extract text from PDF using PyMuPDF for better accuracy"""
|
| 15 |
+
try:
|
| 16 |
+
doc = fitz.open(filepath)
|
| 17 |
+
|
| 18 |
+
if page_number is not None:
|
| 19 |
+
# Extract from specific page
|
| 20 |
+
page = doc[page_number]
|
| 21 |
+
|
| 22 |
+
if coordinates:
|
| 23 |
+
# Extract text from specific area
|
| 24 |
+
x, y, width, height = coordinates
|
| 25 |
+
rect = fitz.Rect(x, y, x + width, y + height)
|
| 26 |
+
text = page.get_text("text", clip=rect)
|
| 27 |
+
else:
|
| 28 |
+
# Extract text from entire page
|
| 29 |
+
text = page.get_text("text")
|
| 30 |
+
else:
|
| 31 |
+
# Extract from entire document
|
| 32 |
+
text = ""
|
| 33 |
+
for page in doc:
|
| 34 |
+
text += page.get_text("text")
|
| 35 |
+
|
| 36 |
+
doc.close()
|
| 37 |
+
return text.strip()
|
| 38 |
+
except Exception as e:
|
| 39 |
+
print(f"Error extracting text with PyMuPDF: {e}")
|
| 40 |
+
# Fallback to PyPDF2
|
| 41 |
+
return extract_text_with_pypdf2(filepath, page_number, coordinates)
|
| 42 |
+
|
| 43 |
+
def extract_text_with_pypdf2(filepath, page_number=None, coordinates=None):
|
| 44 |
+
"""Fallback text extraction using PyPDF2"""
|
| 45 |
+
try:
|
| 46 |
+
with open(filepath, 'rb') as file:
|
| 47 |
+
reader = PyPDF2.PdfReader(file)
|
| 48 |
+
|
| 49 |
+
if page_number is not None and page_number < len(reader.pages):
|
| 50 |
+
page = reader.pages[page_number]
|
| 51 |
+
return page.extract_text()
|
| 52 |
+
else:
|
| 53 |
+
text = ""
|
| 54 |
+
for page in reader.pages:
|
| 55 |
+
text += page.extract_text()
|
| 56 |
+
return text
|
| 57 |
+
except Exception as e:
|
| 58 |
+
return f"Error extracting text: {str(e)}"
|
| 59 |
+
|
| 60 |
+
def convert_text_to_latex(text):
|
| 61 |
+
"""Simple conversion of text to LaTeX format"""
|
| 62 |
+
# This is a placeholder implementation
|
| 63 |
+
# In a real application, you would use an AI model to convert text to LaTeX
|
| 64 |
+
return text.replace('\\', '\\\\').replace('_', '\\_').replace('^', '\\^').replace('&', '\\&')
|
| 65 |
+
|
| 66 |
+
@pdf_bp.route('/')
|
| 67 |
+
def pdf_converter():
|
| 68 |
+
"""Render the PDF converter page"""
|
| 69 |
+
return render_template('pdf.html')
|
| 70 |
+
|
| 71 |
+
@pdf_bp.route('/upload', methods=['POST'])
|
| 72 |
+
def upload_pdf():
|
| 73 |
+
"""Handle PDF file upload"""
|
| 74 |
+
if 'pdf_file' not in request.files:
|
| 75 |
+
return jsonify({'success': False, 'error': 'No file provided'})
|
| 76 |
+
|
| 77 |
+
file = request.files['pdf_file']
|
| 78 |
+
|
| 79 |
+
if file.filename == '':
|
| 80 |
+
return jsonify({'success': False, 'error': 'No file selected'})
|
| 81 |
+
|
| 82 |
+
if file and file.filename.lower().endswith('.pdf'):
|
| 83 |
+
try:
|
| 84 |
+
# Save file temporarily
|
| 85 |
+
temp_dir = tempfile.gettempdir()
|
| 86 |
+
filename = file.filename
|
| 87 |
+
filepath = os.path.join(temp_dir, filename)
|
| 88 |
+
file.save(filepath)
|
| 89 |
+
|
| 90 |
+
# Get page count
|
| 91 |
+
with open(filepath, 'rb') as f:
|
| 92 |
+
reader = PyPDF2.PdfReader(f)
|
| 93 |
+
page_count = len(reader.pages)
|
| 94 |
+
|
| 95 |
+
return jsonify({
|
| 96 |
+
'success': True,
|
| 97 |
+
'filename': filename,
|
| 98 |
+
'filepath': filepath,
|
| 99 |
+
'pages': page_count
|
| 100 |
+
})
|
| 101 |
+
except Exception as e:
|
| 102 |
+
return jsonify({'success': False, 'error': f'Upload failed: {str(e)}'})
|
| 103 |
+
|
| 104 |
+
return jsonify({'success': False, 'error': 'Invalid file type. Please upload a PDF file.'})
|
| 105 |
+
|
| 106 |
+
@pdf_bp.route('/process', methods=['POST'])
|
| 107 |
+
def process_pdf():
|
| 108 |
+
"""Process PDF and convert to LaTeX"""
|
| 109 |
+
try:
|
| 110 |
+
data = request.get_json()
|
| 111 |
+
filename = data.get('filename')
|
| 112 |
+
coordinates = data.get('coordinates')
|
| 113 |
+
page = data.get('page', 0)
|
| 114 |
+
convert_all = data.get('convert_all', False)
|
| 115 |
+
|
| 116 |
+
if not filename:
|
| 117 |
+
return jsonify({'success': False, 'error': 'No filename provided'})
|
| 118 |
+
|
| 119 |
+
# Get file path
|
| 120 |
+
temp_dir = tempfile.gettempdir()
|
| 121 |
+
filepath = os.path.join(temp_dir, filename)
|
| 122 |
+
|
| 123 |
+
if not os.path.exists(filepath):
|
| 124 |
+
return jsonify({'success': False, 'error': 'File not found'})
|
| 125 |
+
|
| 126 |
+
if convert_all:
|
| 127 |
+
# Convert entire PDF
|
| 128 |
+
text = extract_text_from_pdf(filepath)
|
| 129 |
+
elif coordinates:
|
| 130 |
+
# Convert selected area
|
| 131 |
+
text = extract_text_from_pdf(filepath, page, coordinates)
|
| 132 |
+
else:
|
| 133 |
+
# Convert specific page
|
| 134 |
+
text = extract_text_from_pdf(filepath, page)
|
| 135 |
+
|
| 136 |
+
# Convert to LaTeX (placeholder)
|
| 137 |
+
latex = convert_text_to_latex(text)
|
| 138 |
+
|
| 139 |
+
return jsonify({
|
| 140 |
+
'success': True,
|
| 141 |
+
'latex': latex,
|
| 142 |
+
'text': text
|
| 143 |
+
})
|
| 144 |
+
except Exception as e:
|
| 145 |
+
return jsonify({'success': False, 'error': f'Processing failed: {str(e)}'})
|
| 146 |
+
|
| 147 |
+
@pdf_bp.route('/solve', methods=['POST'])
|
| 148 |
+
def solve_equation():
|
| 149 |
+
"""Solve mathematical equations in LaTeX"""
|
| 150 |
+
try:
|
| 151 |
+
data = request.get_json()
|
| 152 |
+
latex = data.get('latex', '')
|
| 153 |
+
|
| 154 |
+
# This is a placeholder implementation
|
| 155 |
+
# In a real application, you would use SymPy or similar library
|
| 156 |
+
solution = {
|
| 157 |
+
'type': 'expression',
|
| 158 |
+
'result': f"Simplified: {latex}"
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
return jsonify({
|
| 162 |
+
'success': True,
|
| 163 |
+
'solution': solution
|
| 164 |
+
})
|
| 165 |
+
except Exception as e:
|
| 166 |
+
return jsonify({'success': False, 'error': f'Solving failed: {str(e)}'})
|
controller/pdffly_controller.py
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Blueprint, request, jsonify, render_template
|
| 2 |
+
import os
|
| 3 |
+
import re
|
| 4 |
+
import fitz # PyMuPDF
|
| 5 |
+
from PIL import Image
|
| 6 |
+
from werkzeug.utils import secure_filename
|
| 7 |
+
try:
|
| 8 |
+
from pix2text import Pix2Text
|
| 9 |
+
PIX2TEXT_AVAILABLE = True
|
| 10 |
+
except ImportError:
|
| 11 |
+
PIX2TEXT_AVAILABLE = False
|
| 12 |
+
print("⚠️ Pix2Text not available. Install with: pip install pix2text")
|
| 13 |
+
|
| 14 |
+
pdffly_bp = Blueprint('pdffly', __name__)
|
| 15 |
+
UPLOAD_FOLDER = 'static/uploads'
|
| 16 |
+
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
| 17 |
+
|
| 18 |
+
# Load Pix2Text model once (for efficiency)
|
| 19 |
+
if PIX2TEXT_AVAILABLE:
|
| 20 |
+
print("🔹 Loading Pix2Text model for PDF → LaTeX...")
|
| 21 |
+
try:
|
| 22 |
+
p2t = Pix2Text()
|
| 23 |
+
print("✅ Pix2Text model loaded successfully")
|
| 24 |
+
except Exception as e:
|
| 25 |
+
print(f"⚠️ Error loading Pix2Text: {e}")
|
| 26 |
+
p2t = None
|
| 27 |
+
else:
|
| 28 |
+
p2t = None
|
| 29 |
+
|
| 30 |
+
def pdf_to_images(pdf_path):
|
| 31 |
+
"""Convert PDF pages to images"""
|
| 32 |
+
doc = fitz.open(pdf_path)
|
| 33 |
+
image_paths = []
|
| 34 |
+
for i, page in enumerate(doc):
|
| 35 |
+
pix = page.get_pixmap(dpi=200)
|
| 36 |
+
img_path = os.path.join(UPLOAD_FOLDER, f"page_{i+1}.png")
|
| 37 |
+
pix.save(img_path)
|
| 38 |
+
image_paths.append(img_path)
|
| 39 |
+
doc.close()
|
| 40 |
+
return image_paths
|
| 41 |
+
|
| 42 |
+
def extract_text_from_pdf(pdf_path):
|
| 43 |
+
"""Extract raw text from PDF (fallback method)"""
|
| 44 |
+
doc = fitz.open(pdf_path)
|
| 45 |
+
all_text = []
|
| 46 |
+
for page_num, page in enumerate(doc):
|
| 47 |
+
text = page.get_text()
|
| 48 |
+
all_text.append(f"Page {page_num + 1}:\n{text}\n")
|
| 49 |
+
doc.close()
|
| 50 |
+
return "\n".join(all_text)
|
| 51 |
+
|
| 52 |
+
def clean_latex_code(latex_str):
|
| 53 |
+
"""Clean and format LaTeX code for Overleaf compilation"""
|
| 54 |
+
if not latex_str or not isinstance(latex_str, str):
|
| 55 |
+
return ""
|
| 56 |
+
|
| 57 |
+
# Remove common OCR artifacts and spaces in commands
|
| 58 |
+
latex_str = re.sub(r'\\operatorname\*?\s*\{\s*([a-z])\s+([a-z])\s+([a-z])\s*\}',
|
| 59 |
+
lambda m: f'\\{m.group(1)}{m.group(2)}{m.group(3)}', latex_str)
|
| 60 |
+
|
| 61 |
+
# Fix common math operators with spaces
|
| 62 |
+
replacements = {
|
| 63 |
+
r'\\operatorname\s*\{\s*l\s+i\s+m\s*\}': r'\\lim',
|
| 64 |
+
r'\\operatorname\s*\{\s*s\s+i\s+n\s*\}': r'\\sin',
|
| 65 |
+
r'\\operatorname\s*\{\s*c\s+o\s+s\s*\}': r'\\cos',
|
| 66 |
+
r'\\operatorname\s*\{\s*t\s+a\s+n\s*\}': r'\\tan',
|
| 67 |
+
r'\\operatorname\s*\{\s*l\s+o\s+g\s*\}': r'\\log',
|
| 68 |
+
r'\\operatorname\s*\{\s*l\s+n\s*\}': r'\\ln',
|
| 69 |
+
r'\\operatorname\s*\{\s*e\s+x\s+p\s*\}': r'\\exp',
|
| 70 |
+
r'\\operatorname\s*\{\s*m\s+a\s+x\s*\}': r'\\max',
|
| 71 |
+
r'\\operatorname\s*\{\s*m\s+i\s+n\s*\}': r'\\min',
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
for pattern, replacement in replacements.items():
|
| 75 |
+
latex_str = re.sub(pattern, replacement, latex_str, flags=re.IGNORECASE)
|
| 76 |
+
|
| 77 |
+
# Remove spaces inside any remaining \operatorname commands
|
| 78 |
+
latex_str = re.sub(r'\\operatorname\*?\s*\{([^}]+)\}',
|
| 79 |
+
lambda m: f'\\operatorname{{{m.group(1).replace(" ", "")}}}', latex_str)
|
| 80 |
+
|
| 81 |
+
# Replace $$ with \[ \]
|
| 82 |
+
latex_str = re.sub(r'\$\$([^$]+)\$\$', r'\\[\1\\]', latex_str)
|
| 83 |
+
|
| 84 |
+
# Remove obvious OCR gibberish (sequences of random chars/symbols)
|
| 85 |
+
latex_str = re.sub(r'[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f-\xff]+', '', latex_str)
|
| 86 |
+
|
| 87 |
+
# Balance braces and brackets
|
| 88 |
+
open_braces = latex_str.count('{')
|
| 89 |
+
close_braces = latex_str.count('}')
|
| 90 |
+
if open_braces > close_braces:
|
| 91 |
+
latex_str += '}' * (open_braces - close_braces)
|
| 92 |
+
elif close_braces > open_braces:
|
| 93 |
+
latex_str = '{' * (close_braces - open_braces) + latex_str
|
| 94 |
+
|
| 95 |
+
# Balance brackets
|
| 96 |
+
open_brackets = latex_str.count('[')
|
| 97 |
+
close_brackets = latex_str.count(']')
|
| 98 |
+
if open_brackets > close_brackets:
|
| 99 |
+
latex_str += ']' * (open_brackets - close_brackets)
|
| 100 |
+
elif close_brackets > open_brackets:
|
| 101 |
+
latex_str = '[' * (close_brackets - open_brackets) + latex_str
|
| 102 |
+
|
| 103 |
+
# Remove invalid math syntax patterns
|
| 104 |
+
latex_str = re.sub(r'\\\\+', r'\\\\', latex_str) # Multiple backslashes
|
| 105 |
+
latex_str = re.sub(r'\s+', ' ', latex_str) # Multiple spaces
|
| 106 |
+
|
| 107 |
+
return latex_str.strip()
|
| 108 |
+
|
| 109 |
+
def create_complete_latex_document(latex_content, title="PDF to LaTeX Conversion"):
|
| 110 |
+
"""Wrap LaTeX content in a complete compilable document"""
|
| 111 |
+
document = r'''\documentclass{article}
|
| 112 |
+
\usepackage{amsmath}
|
| 113 |
+
\usepackage{amssymb}
|
| 114 |
+
\usepackage{amsfonts}
|
| 115 |
+
\usepackage{graphicx}
|
| 116 |
+
|
| 117 |
+
\title{''' + title + r'''}
|
| 118 |
+
\author{PDFly}
|
| 119 |
+
\date{\today}
|
| 120 |
+
|
| 121 |
+
\begin{document}
|
| 122 |
+
|
| 123 |
+
\maketitle
|
| 124 |
+
|
| 125 |
+
\begin{center}
|
| 126 |
+
\textit{This document was automatically generated from a PDF file using OCR and LaTeX conversion.}
|
| 127 |
+
\end{center}
|
| 128 |
+
|
| 129 |
+
\section*{Content}
|
| 130 |
+
|
| 131 |
+
''' + latex_content + r'''
|
| 132 |
+
|
| 133 |
+
\end{document}'''
|
| 134 |
+
|
| 135 |
+
return document
|
| 136 |
+
|
| 137 |
+
@pdffly_bp.route("/", methods=["GET"])
|
| 138 |
+
def pdffly_page():
|
| 139 |
+
"""Render the main PDFfly page."""
|
| 140 |
+
return render_template("pdffly.html")
|
| 141 |
+
|
| 142 |
+
@pdffly_bp.route('/upload', methods=['POST'])
|
| 143 |
+
def upload_and_convert_pdf():
|
| 144 |
+
"""Upload PDF and convert to LaTeX"""
|
| 145 |
+
if 'file' not in request.files:
|
| 146 |
+
return jsonify({'error': 'No file found'}), 400
|
| 147 |
+
|
| 148 |
+
file = request.files['file']
|
| 149 |
+
if not file or file.filename == '':
|
| 150 |
+
return jsonify({'error': 'No file selected'}), 400
|
| 151 |
+
|
| 152 |
+
if not file.filename.lower().endswith('.pdf'):
|
| 153 |
+
return jsonify({'error': 'Only PDF files are allowed'}), 400
|
| 154 |
+
|
| 155 |
+
filename = secure_filename(file.filename)
|
| 156 |
+
pdf_path = os.path.join(UPLOAD_FOLDER, filename)
|
| 157 |
+
file.save(pdf_path)
|
| 158 |
+
|
| 159 |
+
try:
|
| 160 |
+
# Get page count
|
| 161 |
+
doc = fitz.open(pdf_path)
|
| 162 |
+
page_count = len(doc)
|
| 163 |
+
doc.close()
|
| 164 |
+
|
| 165 |
+
# Convert PDF → images
|
| 166 |
+
images = pdf_to_images(pdf_path)
|
| 167 |
+
|
| 168 |
+
# Run LaTeX recognition for each image
|
| 169 |
+
latex_results = []
|
| 170 |
+
all_latex_pages = []
|
| 171 |
+
for i, img_path in enumerate(images):
|
| 172 |
+
try:
|
| 173 |
+
if p2t:
|
| 174 |
+
result = p2t.recognize(img_path, resized_shape=768)
|
| 175 |
+
latex_code = result if isinstance(result, str) else str(result)
|
| 176 |
+
# Clean the LaTeX code
|
| 177 |
+
latex_code = clean_latex_code(latex_code)
|
| 178 |
+
all_latex_pages.append(f"% Page {i + 1}\n{latex_code}")
|
| 179 |
+
else:
|
| 180 |
+
# Fallback: extract text
|
| 181 |
+
latex_code = f"Text extraction (Pix2Text not available)"
|
| 182 |
+
all_latex_pages.append(latex_code)
|
| 183 |
+
|
| 184 |
+
latex_results.append({
|
| 185 |
+
'page': i + 1,
|
| 186 |
+
'image': img_path.replace('static/', '/static/'),
|
| 187 |
+
'latex': latex_code
|
| 188 |
+
})
|
| 189 |
+
except Exception as e:
|
| 190 |
+
latex_results.append({
|
| 191 |
+
'page': i + 1,
|
| 192 |
+
'image': img_path.replace('static/', '/static/'),
|
| 193 |
+
'error': str(e)
|
| 194 |
+
})
|
| 195 |
+
all_latex_pages.append(f"% Page {i + 1}: Error - {str(e)}")
|
| 196 |
+
|
| 197 |
+
# Create complete document
|
| 198 |
+
combined_latex = "\n\n".join(all_latex_pages)
|
| 199 |
+
complete_document = create_complete_latex_document(combined_latex, filename)
|
| 200 |
+
|
| 201 |
+
return jsonify({
|
| 202 |
+
'success': True,
|
| 203 |
+
'message': 'PDF converted successfully!',
|
| 204 |
+
'pdf_path': pdf_path.replace('static/', '/static/'),
|
| 205 |
+
'filename': filename,
|
| 206 |
+
'pages': page_count,
|
| 207 |
+
'results': latex_results,
|
| 208 |
+
'complete_document': complete_document
|
| 209 |
+
})
|
| 210 |
+
|
| 211 |
+
except Exception as e:
|
| 212 |
+
return jsonify({
|
| 213 |
+
'success': False,
|
| 214 |
+
'error': f'Error processing PDF: {str(e)}'
|
| 215 |
+
}), 500
|
| 216 |
+
|
| 217 |
+
@pdffly_bp.route('/process', methods=['POST'])
|
| 218 |
+
def process_pdf():
|
| 219 |
+
"""Process specific area or entire PDF"""
|
| 220 |
+
data = request.get_json()
|
| 221 |
+
filename = data.get('filename')
|
| 222 |
+
convert_all = data.get('convert_all', False)
|
| 223 |
+
page_num = data.get('page', 0)
|
| 224 |
+
coordinates = data.get('coordinates')
|
| 225 |
+
|
| 226 |
+
if not filename:
|
| 227 |
+
return jsonify({'success': False, 'error': 'No filename provided'}), 400
|
| 228 |
+
|
| 229 |
+
pdf_path = os.path.join(UPLOAD_FOLDER, filename)
|
| 230 |
+
|
| 231 |
+
if not os.path.exists(pdf_path):
|
| 232 |
+
return jsonify({'success': False, 'error': 'PDF file not found'}), 404
|
| 233 |
+
|
| 234 |
+
try:
|
| 235 |
+
if convert_all:
|
| 236 |
+
# Extract text from entire PDF
|
| 237 |
+
text = extract_text_from_pdf(pdf_path)
|
| 238 |
+
latex = f"\\text{{{text}}}"
|
| 239 |
+
else:
|
| 240 |
+
# Extract from specific page
|
| 241 |
+
doc = fitz.open(pdf_path)
|
| 242 |
+
if page_num < len(doc):
|
| 243 |
+
page = doc[page_num]
|
| 244 |
+
text = page.get_text()
|
| 245 |
+
latex = f"\\text{{{text}}}"
|
| 246 |
+
else:
|
| 247 |
+
latex = "Page not found"
|
| 248 |
+
doc.close()
|
| 249 |
+
|
| 250 |
+
return jsonify({
|
| 251 |
+
'success': True,
|
| 252 |
+
'latex': latex
|
| 253 |
+
})
|
| 254 |
+
|
| 255 |
+
except Exception as e:
|
| 256 |
+
return jsonify({
|
| 257 |
+
'success': False,
|
| 258 |
+
'error': str(e)
|
| 259 |
+
}), 500
|
| 260 |
+
|
| 261 |
+
@pdffly_bp.route('/solve', methods=['POST'])
|
| 262 |
+
def solve_latex():
|
| 263 |
+
"""Solve mathematical content"""
|
| 264 |
+
data = request.get_json()
|
| 265 |
+
latex = data.get('latex', '')
|
| 266 |
+
|
| 267 |
+
# Simple solver response
|
| 268 |
+
return jsonify({
|
| 269 |
+
'success': True,
|
| 270 |
+
'solution': {
|
| 271 |
+
'type': 'info',
|
| 272 |
+
'message': 'Math solver integration pending'
|
| 273 |
+
}
|
| 274 |
+
})
|
controller/pdflly_controller.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import base64
|
| 4 |
+
import io
|
| 5 |
+
from flask import Blueprint, request, render_template, jsonify, current_app
|
| 6 |
+
import fitz # PyMuPDF
|
| 7 |
+
import PyPDF2
|
| 8 |
+
from PIL import Image
|
| 9 |
+
import tempfile
|
| 10 |
+
|
| 11 |
+
pdflly_bp = Blueprint('pdflly', __name__, url_prefix='/pdflly')
|
| 12 |
+
|
| 13 |
+
def extract_text_from_pdf(filepath, page_number=None, coordinates=None):
|
| 14 |
+
"""Extract text from PDF using PyMuPDF for better accuracy"""
|
| 15 |
+
try:
|
| 16 |
+
doc = fitz.open(filepath)
|
| 17 |
+
|
| 18 |
+
if page_number is not None:
|
| 19 |
+
# Extract from specific page
|
| 20 |
+
page = doc[page_number]
|
| 21 |
+
|
| 22 |
+
if coordinates:
|
| 23 |
+
# Extract text from specific area
|
| 24 |
+
x, y, width, height = coordinates
|
| 25 |
+
rect = fitz.Rect(x, y, x + width, y + height)
|
| 26 |
+
text = page.get_text("text", clip=rect)
|
| 27 |
+
else:
|
| 28 |
+
# Extract text from entire page
|
| 29 |
+
text = page.get_text("text")
|
| 30 |
+
else:
|
| 31 |
+
# Extract from entire document
|
| 32 |
+
text = ""
|
| 33 |
+
for page in doc:
|
| 34 |
+
text += page.get_text("text")
|
| 35 |
+
|
| 36 |
+
doc.close()
|
| 37 |
+
return text.strip()
|
| 38 |
+
except Exception as e:
|
| 39 |
+
print(f"Error extracting text with PyMuPDF: {e}")
|
| 40 |
+
# Fallback to PyPDF2
|
| 41 |
+
return extract_text_with_pypdf2(filepath, page_number, coordinates)
|
| 42 |
+
|
| 43 |
+
def extract_text_with_pypdf2(filepath, page_number=None, coordinates=None):
|
| 44 |
+
"""Fallback text extraction using PyPDF2"""
|
| 45 |
+
try:
|
| 46 |
+
with open(filepath, 'rb') as file:
|
| 47 |
+
reader = PyPDF2.PdfReader(file)
|
| 48 |
+
|
| 49 |
+
if page_number is not None and page_number < len(reader.pages):
|
| 50 |
+
page = reader.pages[page_number]
|
| 51 |
+
return page.extract_text()
|
| 52 |
+
else:
|
| 53 |
+
text = ""
|
| 54 |
+
for page in reader.pages:
|
| 55 |
+
text += page.extract_text()
|
| 56 |
+
return text
|
| 57 |
+
except Exception as e:
|
| 58 |
+
return f"Error extracting text: {str(e)}"
|
| 59 |
+
|
| 60 |
+
def convert_text_to_latex(text):
|
| 61 |
+
"""Simple conversion of text to LaTeX format"""
|
| 62 |
+
# This is a placeholder implementation
|
| 63 |
+
# In a real application, you would use an AI model to convert text to LaTeX
|
| 64 |
+
return text.replace('\\', '\\\\').replace('_', '\\_').replace('^', '\\^').replace('&', '\\&')
|
| 65 |
+
|
| 66 |
+
@pdflly_bp.route('/')
|
| 67 |
+
def pdflly_converter():
|
| 68 |
+
"""Render the PDFly converter page"""
|
| 69 |
+
return render_template('pdflly.html')
|
| 70 |
+
|
| 71 |
+
@pdflly_bp.route('/upload', methods=['POST'])
|
| 72 |
+
def upload_pdf():
|
| 73 |
+
"""Handle PDF file upload"""
|
| 74 |
+
if 'pdf_file' not in request.files:
|
| 75 |
+
return jsonify({'success': False, 'error': 'No file provided'})
|
| 76 |
+
|
| 77 |
+
file = request.files['pdf_file']
|
| 78 |
+
|
| 79 |
+
if file.filename == '':
|
| 80 |
+
return jsonify({'success': False, 'error': 'No file selected'})
|
| 81 |
+
|
| 82 |
+
if file and file.filename.lower().endswith('.pdf'):
|
| 83 |
+
try:
|
| 84 |
+
# Save file temporarily
|
| 85 |
+
temp_dir = tempfile.gettempdir()
|
| 86 |
+
filename = file.filename
|
| 87 |
+
filepath = os.path.join(temp_dir, filename)
|
| 88 |
+
file.save(filepath)
|
| 89 |
+
|
| 90 |
+
# Get page count
|
| 91 |
+
with open(filepath, 'rb') as f:
|
| 92 |
+
reader = PyPDF2.PdfReader(f)
|
| 93 |
+
page_count = len(reader.pages)
|
| 94 |
+
|
| 95 |
+
return jsonify({
|
| 96 |
+
'success': True,
|
| 97 |
+
'filename': filename,
|
| 98 |
+
'filepath': filepath,
|
| 99 |
+
'pages': page_count
|
| 100 |
+
})
|
| 101 |
+
except Exception as e:
|
| 102 |
+
return jsonify({'success': False, 'error': f'Upload failed: {str(e)}'})
|
| 103 |
+
|
| 104 |
+
return jsonify({'success': False, 'error': 'Invalid file type. Please upload a PDF file.'})
|
| 105 |
+
|
| 106 |
+
@pdflly_bp.route('/process', methods=['POST'])
|
| 107 |
+
def process_pdf():
|
| 108 |
+
"""Process PDF and convert to LaTeX"""
|
| 109 |
+
try:
|
| 110 |
+
data = request.get_json()
|
| 111 |
+
filename = data.get('filename')
|
| 112 |
+
coordinates = data.get('coordinates')
|
| 113 |
+
page = data.get('page', 0)
|
| 114 |
+
convert_all = data.get('convert_all', False)
|
| 115 |
+
|
| 116 |
+
if not filename:
|
| 117 |
+
return jsonify({'success': False, 'error': 'No filename provided'})
|
| 118 |
+
|
| 119 |
+
# Get file path
|
| 120 |
+
temp_dir = tempfile.gettempdir()
|
| 121 |
+
filepath = os.path.join(temp_dir, filename)
|
| 122 |
+
|
| 123 |
+
if not os.path.exists(filepath):
|
| 124 |
+
return jsonify({'success': False, 'error': 'File not found'})
|
| 125 |
+
|
| 126 |
+
if convert_all:
|
| 127 |
+
# Convert entire PDF
|
| 128 |
+
text = extract_text_from_pdf(filepath)
|
| 129 |
+
elif coordinates:
|
| 130 |
+
# Convert selected area
|
| 131 |
+
text = extract_text_from_pdf(filepath, page, coordinates)
|
| 132 |
+
else:
|
| 133 |
+
# Convert specific page
|
| 134 |
+
text = extract_text_from_pdf(filepath, page)
|
| 135 |
+
|
| 136 |
+
# Convert to LaTeX (placeholder)
|
| 137 |
+
latex = convert_text_to_latex(text)
|
| 138 |
+
|
| 139 |
+
return jsonify({
|
| 140 |
+
'success': True,
|
| 141 |
+
'latex': latex,
|
| 142 |
+
'text': text
|
| 143 |
+
})
|
| 144 |
+
except Exception as e:
|
| 145 |
+
return jsonify({'success': False, 'error': f'Processing failed: {str(e)}'})
|
| 146 |
+
|
| 147 |
+
@pdflly_bp.route('/solve', methods=['POST'])
|
| 148 |
+
def solve_equation():
|
| 149 |
+
"""Solve mathematical equations in LaTeX"""
|
| 150 |
+
try:
|
| 151 |
+
data = request.get_json()
|
| 152 |
+
latex = data.get('latex', '')
|
| 153 |
+
|
| 154 |
+
# This is a placeholder implementation
|
| 155 |
+
# In a real application, you would use SymPy or similar library
|
| 156 |
+
solution = {
|
| 157 |
+
'type': 'expression',
|
| 158 |
+
'result': f"Simplified: {latex}"
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
return jsonify({
|
| 162 |
+
'success': True,
|
| 163 |
+
'solution': solution
|
| 164 |
+
})
|
| 165 |
+
except Exception as e:
|
| 166 |
+
return jsonify({'success': False, 'error': f'Solving failed: {str(e)}'})
|
controller/pix2text_controller.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# controller/pix2text_bp.py
|
| 2 |
+
import os
|
| 3 |
+
import cv2
|
| 4 |
+
from flask import Blueprint, render_template, request, jsonify
|
| 5 |
+
from pix2text import Pix2Text
|
| 6 |
+
from utils.math_solver import solve_equation
|
| 7 |
+
from controller.models.camera_to_latex import camera_to_latex
|
| 8 |
+
|
| 9 |
+
# Initialize Pix2Text globally once
|
| 10 |
+
print("🔹 Loading Pix2Text model (mfd)...")
|
| 11 |
+
try:
|
| 12 |
+
p2t = Pix2Text(analyzer_config=dict(model_name='mfd'))
|
| 13 |
+
print("✅ Pix2Text model loaded successfully.")
|
| 14 |
+
except Exception as e:
|
| 15 |
+
print(f"❌ Pix2Text failed to initialize: {e}")
|
| 16 |
+
p2t = None
|
| 17 |
+
|
| 18 |
+
# Flask blueprint
|
| 19 |
+
pix2text_bp = Blueprint('pix2text_bp', __name__)
|
| 20 |
+
|
| 21 |
+
UPLOAD_FOLDER = 'static/uploads'
|
| 22 |
+
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
| 23 |
+
|
| 24 |
+
# Optional preprocessing
|
| 25 |
+
def preprocess_image(image_path):
|
| 26 |
+
"""Preprocess image for better OCR results"""
|
| 27 |
+
try:
|
| 28 |
+
# Read image
|
| 29 |
+
img = cv2.imread(image_path)
|
| 30 |
+
if img is None:
|
| 31 |
+
raise ValueError("Could not read image")
|
| 32 |
+
|
| 33 |
+
# Convert to grayscale
|
| 34 |
+
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
| 35 |
+
|
| 36 |
+
# Apply mild Gaussian blur to reduce noise while preserving edges
|
| 37 |
+
blurred = cv2.GaussianBlur(gray, (3, 3), 0)
|
| 38 |
+
|
| 39 |
+
# Apply adaptive thresholding with parameters better suited for text
|
| 40 |
+
thresh = cv2.adaptiveThreshold(
|
| 41 |
+
blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 15, 2
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
# Save processed image
|
| 45 |
+
processed_path = os.path.join(
|
| 46 |
+
PROCESSED_FOLDER,
|
| 47 |
+
os.path.basename(image_path).replace('.', '_processed.')
|
| 48 |
+
)
|
| 49 |
+
cv2.imwrite(processed_path, thresh)
|
| 50 |
+
|
| 51 |
+
return processed_path
|
| 52 |
+
except Exception as e:
|
| 53 |
+
print(f"Preprocessing error: {e}")
|
| 54 |
+
return image_path # Return original if preprocessing fails
|
| 55 |
+
|
| 56 |
+
# -----------------------------
|
| 57 |
+
# Math Routes
|
| 58 |
+
# -----------------------------
|
| 59 |
+
@pix2text_bp.route("/math")
|
| 60 |
+
def math_page():
|
| 61 |
+
return render_template("math.html")
|
| 62 |
+
|
| 63 |
+
@pix2text_bp.route("/math/process", methods=["POST"])
|
| 64 |
+
def process_math_image():
|
| 65 |
+
try:
|
| 66 |
+
if 'image' not in request.files:
|
| 67 |
+
return jsonify({'error': 'No image file provided'}), 400
|
| 68 |
+
|
| 69 |
+
file = request.files['image']
|
| 70 |
+
if not file.filename:
|
| 71 |
+
return jsonify({'error': 'No file selected'}), 400
|
| 72 |
+
|
| 73 |
+
filename = file.filename
|
| 74 |
+
filepath = os.path.join(UPLOAD_FOLDER, filename)
|
| 75 |
+
file.save(filepath)
|
| 76 |
+
|
| 77 |
+
# Preprocess (optional)
|
| 78 |
+
processed_path = preprocess_image(filepath)
|
| 79 |
+
|
| 80 |
+
# Run Pix2Text
|
| 81 |
+
if p2t:
|
| 82 |
+
result = p2t.recognize(processed_path)
|
| 83 |
+
if isinstance(result, dict):
|
| 84 |
+
latex = result.get('text', '')
|
| 85 |
+
elif isinstance(result, list) and result and isinstance(result[0], dict):
|
| 86 |
+
latex = result[0].get('text', '')
|
| 87 |
+
else:
|
| 88 |
+
latex = str(result)
|
| 89 |
+
else:
|
| 90 |
+
latex = "\\text{Pix2Text not initialized}"
|
| 91 |
+
|
| 92 |
+
return jsonify({
|
| 93 |
+
'success': True,
|
| 94 |
+
'latex': latex,
|
| 95 |
+
'image_path': filepath
|
| 96 |
+
})
|
| 97 |
+
|
| 98 |
+
except Exception as e:
|
| 99 |
+
print(f"❌ Error in /math/process: {e}")
|
| 100 |
+
return jsonify({'error': str(e)}), 500
|
| 101 |
+
|
| 102 |
+
@pix2text_bp.route("/math/solve", methods=["POST"])
|
| 103 |
+
def solve_math_equation():
|
| 104 |
+
try:
|
| 105 |
+
data = request.get_json()
|
| 106 |
+
if not data or 'latex' not in data:
|
| 107 |
+
return jsonify({'error': 'No equation provided'}), 400
|
| 108 |
+
|
| 109 |
+
solution = solve_equation(data['latex'])
|
| 110 |
+
return jsonify({'success': True, 'solution': solution})
|
| 111 |
+
|
| 112 |
+
except Exception as e:
|
| 113 |
+
print(f"❌ Error in /math/solve: {e}")
|
| 114 |
+
return jsonify({'error': str(e)}), 500
|
| 115 |
+
|
| 116 |
+
# -----------------------------
|
| 117 |
+
# Camera Routes
|
| 118 |
+
# -----------------------------
|
| 119 |
+
# @pix2text_bp.route("/camera")
|
| 120 |
+
# def camera_page():
|
| 121 |
+
# return render_template("camera.html")
|
| 122 |
+
|
| 123 |
+
@pix2text_bp.route("/camera")
|
| 124 |
+
def camera_page():
|
| 125 |
+
"""Render the camera capture page"""
|
| 126 |
+
return render_template("camera.html")
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
@pix2text_bp.route("/camera/solve", methods=["POST"])
|
| 130 |
+
def solve_camera_equation():
|
| 131 |
+
"""Solve a LaTeX equation from camera input"""
|
| 132 |
+
try:
|
| 133 |
+
data = request.get_json()
|
| 134 |
+
if not data:
|
| 135 |
+
return jsonify({'error': 'No data provided'}), 400
|
| 136 |
+
|
| 137 |
+
latex_equation = data.get('latex', '')
|
| 138 |
+
if not latex_equation:
|
| 139 |
+
return jsonify({'error': 'No equation provided'}), 400
|
| 140 |
+
|
| 141 |
+
# Solve the equation
|
| 142 |
+
solution = solve_equation(latex_equation)
|
| 143 |
+
|
| 144 |
+
return jsonify({
|
| 145 |
+
'success': True,
|
| 146 |
+
'solution': solution
|
| 147 |
+
})
|
| 148 |
+
|
| 149 |
+
except Exception as e:
|
| 150 |
+
return jsonify({'error': str(e)}), 500
|
| 151 |
+
return jsonify({'error': 'Unknown error'}), 500
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
@pix2text_bp.route("/camera/process", methods=["POST"])
|
| 155 |
+
def process_camera_image():
|
| 156 |
+
"""Process camera captured image using Pix2Text"""
|
| 157 |
+
try:
|
| 158 |
+
if 'image' not in request.files:
|
| 159 |
+
return jsonify({'error': 'No image file provided'}), 400
|
| 160 |
+
|
| 161 |
+
file = request.files['image']
|
| 162 |
+
if file.filename == '':
|
| 163 |
+
return jsonify({'error': 'No image file selected'}), 400
|
| 164 |
+
|
| 165 |
+
if file and file.filename:
|
| 166 |
+
# Save original image
|
| 167 |
+
filename = file.filename
|
| 168 |
+
filepath = os.path.join(UPLOAD_FOLDER, filename)
|
| 169 |
+
file.save(filepath)
|
| 170 |
+
|
| 171 |
+
# For camera captures, try processing the original image first
|
| 172 |
+
# as preprocessing might distort mathematical symbols
|
| 173 |
+
processed_path = filepath
|
| 174 |
+
|
| 175 |
+
# Process with Pix2Text if available
|
| 176 |
+
if p2t:
|
| 177 |
+
print(f"Processing image: {processed_path}")
|
| 178 |
+
result = p2t.recognize(processed_path)
|
| 179 |
+
print(f"Raw result: {result}")
|
| 180 |
+
|
| 181 |
+
# Handle different result types
|
| 182 |
+
if isinstance(result, dict):
|
| 183 |
+
latex_code = result.get('text', '')
|
| 184 |
+
elif isinstance(result, list):
|
| 185 |
+
# If result is a list, extract text from first item
|
| 186 |
+
if result and isinstance(result[0], dict):
|
| 187 |
+
latex_code = result[0].get('text', '')
|
| 188 |
+
else:
|
| 189 |
+
latex_code = str(result)
|
| 190 |
+
else:
|
| 191 |
+
latex_code = str(result)
|
| 192 |
+
|
| 193 |
+
# If we get no result or very short result, try with preprocessing
|
| 194 |
+
if len(latex_code.strip()) < 2:
|
| 195 |
+
print("Result too short, trying with preprocessing...")
|
| 196 |
+
processed_path = preprocess_image(filepath)
|
| 197 |
+
result = p2t.recognize(processed_path)
|
| 198 |
+
print(f"Preprocessed result: {result}")
|
| 199 |
+
|
| 200 |
+
if isinstance(result, dict):
|
| 201 |
+
latex_code = result.get('text', '')
|
| 202 |
+
elif isinstance(result, list):
|
| 203 |
+
if result and isinstance(result[0], dict):
|
| 204 |
+
latex_code = result[0].get('text', '')
|
| 205 |
+
else:
|
| 206 |
+
latex_code = str(result)
|
| 207 |
+
else:
|
| 208 |
+
latex_code = str(result)
|
| 209 |
+
|
| 210 |
+
print(f"Final extracted LaTeX: {latex_code}")
|
| 211 |
+
else:
|
| 212 |
+
latex_code = "\\text{Pix2Text not available}"
|
| 213 |
+
|
| 214 |
+
return jsonify({
|
| 215 |
+
'success': True,
|
| 216 |
+
'latex': latex_code,
|
| 217 |
+
'image_path': filepath
|
| 218 |
+
})
|
| 219 |
+
|
| 220 |
+
except Exception as e:
|
| 221 |
+
print(f"Error processing camera image: {e}")
|
| 222 |
+
return jsonify({'error': str(e)}), 500
|
| 223 |
+
return jsonify({'error': 'Unknown error'}), 500
|
controller/scribble_controller.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from flask import Blueprint, render_template, request, jsonify
|
| 3 |
+
import base64
|
| 4 |
+
from io import BytesIO
|
| 5 |
+
from PIL import Image
|
| 6 |
+
import numpy as np
|
| 7 |
+
import cv2
|
| 8 |
+
from utils.math_solver import solve_equation
|
| 9 |
+
from typing import Union, Tuple, Any
|
| 10 |
+
|
| 11 |
+
# Initialize Pix2Text with MFD (Mathematical Formula Detection) model for better accuracy
|
| 12 |
+
try:
|
| 13 |
+
from pix2text import Pix2Text
|
| 14 |
+
p2t = Pix2Text(analyzer_config=dict(model_name='mfd'))
|
| 15 |
+
except Exception as e:
|
| 16 |
+
print(f"Warning: Could not initialize Pix2Text: {e}")
|
| 17 |
+
p2t = None
|
| 18 |
+
|
| 19 |
+
scribble_bp = Blueprint('scribble_bp', __name__)
|
| 20 |
+
|
| 21 |
+
UPLOAD_FOLDER = 'static/uploads'
|
| 22 |
+
PROCESSED_FOLDER = 'static/processed'
|
| 23 |
+
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
| 24 |
+
os.makedirs(PROCESSED_FOLDER, exist_ok=True)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def preprocess_image(image_data):
|
| 28 |
+
"""Preprocess image for better OCR results"""
|
| 29 |
+
try:
|
| 30 |
+
# Convert base64 to image
|
| 31 |
+
image_data = image_data.split(',')[1] # Remove data URL prefix
|
| 32 |
+
image = Image.open(BytesIO(base64.b64decode(image_data)))
|
| 33 |
+
|
| 34 |
+
# Convert to OpenCV format
|
| 35 |
+
opencv_image = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
|
| 36 |
+
|
| 37 |
+
# Convert to grayscale
|
| 38 |
+
gray = cv2.cvtColor(opencv_image, cv2.COLOR_BGR2GRAY)
|
| 39 |
+
|
| 40 |
+
# Apply mild Gaussian blur to reduce noise while preserving edges
|
| 41 |
+
blurred = cv2.GaussianBlur(gray, (3, 3), 0)
|
| 42 |
+
|
| 43 |
+
# Apply adaptive thresholding with parameters better suited for handwritten text
|
| 44 |
+
thresh = cv2.adaptiveThreshold(
|
| 45 |
+
blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 15, 2
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
# Save processed image
|
| 49 |
+
processed_path = os.path.join(PROCESSED_FOLDER, 'scribble_processed.png')
|
| 50 |
+
cv2.imwrite(processed_path, thresh)
|
| 51 |
+
|
| 52 |
+
return processed_path
|
| 53 |
+
except Exception as e:
|
| 54 |
+
print(f"Preprocessing error: {e}")
|
| 55 |
+
# Save original if preprocessing fails
|
| 56 |
+
image_path = os.path.join(UPLOAD_FOLDER, 'scribble.png')
|
| 57 |
+
# Reopen image to save it
|
| 58 |
+
image = Image.open(BytesIO(base64.b64decode(image_data.split(',')[1])))
|
| 59 |
+
image.save(image_path)
|
| 60 |
+
return image_path
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
@scribble_bp.route("/scribble")
|
| 64 |
+
def scribble_page():
|
| 65 |
+
return render_template("scribble.html")
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
@scribble_bp.route("/scribble/process", methods=["POST"])
|
| 69 |
+
def process_scribble():
|
| 70 |
+
try:
|
| 71 |
+
data = request.get_json()
|
| 72 |
+
if not data:
|
| 73 |
+
return jsonify({'error': 'No data provided'}), 400
|
| 74 |
+
|
| 75 |
+
image_data = data.get('image', '')
|
| 76 |
+
if not image_data:
|
| 77 |
+
return jsonify({'error': 'No image data provided'}), 400
|
| 78 |
+
|
| 79 |
+
# Save original image first
|
| 80 |
+
image_path = os.path.join(UPLOAD_FOLDER, 'scribble_original.png')
|
| 81 |
+
image_data_content = image_data.split(',')[1]
|
| 82 |
+
image = Image.open(BytesIO(base64.b64decode(image_data_content)))
|
| 83 |
+
image.save(image_path)
|
| 84 |
+
|
| 85 |
+
# Process with Pix2Text if available
|
| 86 |
+
if p2t:
|
| 87 |
+
print(f"Processing scribble with MFD model: {image_path}")
|
| 88 |
+
|
| 89 |
+
# Try with original image first (works better for handwritten math)
|
| 90 |
+
result = p2t.recognize(image_path)
|
| 91 |
+
print(f"Original image result: {result}")
|
| 92 |
+
|
| 93 |
+
# Handle different result types
|
| 94 |
+
if isinstance(result, dict):
|
| 95 |
+
latex_code = result.get('text', '')
|
| 96 |
+
elif isinstance(result, list):
|
| 97 |
+
# If result is a list, extract text from first item
|
| 98 |
+
if result and isinstance(result[0], dict):
|
| 99 |
+
latex_code = result[0].get('text', '')
|
| 100 |
+
else:
|
| 101 |
+
latex_code = str(result)
|
| 102 |
+
else:
|
| 103 |
+
latex_code = str(result)
|
| 104 |
+
|
| 105 |
+
# If we get no result or very short result, try with preprocessing
|
| 106 |
+
if len(latex_code.strip()) < 2:
|
| 107 |
+
print("Result too short, trying with preprocessing...")
|
| 108 |
+
processed_path = preprocess_image(image_data)
|
| 109 |
+
result = p2t.recognize(processed_path)
|
| 110 |
+
print(f"Preprocessed image result: {result}")
|
| 111 |
+
|
| 112 |
+
if isinstance(result, dict):
|
| 113 |
+
latex_code = result.get('text', '')
|
| 114 |
+
elif isinstance(result, list):
|
| 115 |
+
if result and isinstance(result[0], dict):
|
| 116 |
+
latex_code = result[0].get('text', '')
|
| 117 |
+
else:
|
| 118 |
+
latex_code = str(result)
|
| 119 |
+
else:
|
| 120 |
+
latex_code = str(result)
|
| 121 |
+
|
| 122 |
+
print(f"Final extracted LaTeX: {latex_code}")
|
| 123 |
+
else:
|
| 124 |
+
latex_code = "\\text{Pix2Text not available}"
|
| 125 |
+
|
| 126 |
+
return jsonify({
|
| 127 |
+
'success': True,
|
| 128 |
+
'latex': latex_code
|
| 129 |
+
})
|
| 130 |
+
|
| 131 |
+
except Exception as e:
|
| 132 |
+
print(f"Error processing scribble: {e}")
|
| 133 |
+
return jsonify({'error': str(e)}), 500
|
| 134 |
+
return jsonify({'error': 'Unknown error'}), 500
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
@scribble_bp.route("/scribble/solve", methods=["POST"])
|
| 138 |
+
def solve_scribble_equation():
|
| 139 |
+
"""Solve a LaTeX equation from scribble"""
|
| 140 |
+
try:
|
| 141 |
+
data = request.get_json()
|
| 142 |
+
if not data:
|
| 143 |
+
return jsonify({'error': 'No data provided'}), 400
|
| 144 |
+
|
| 145 |
+
latex_equation = data.get('latex', '')
|
| 146 |
+
if not latex_equation:
|
| 147 |
+
return jsonify({'error': 'No equation provided'}), 400
|
| 148 |
+
|
| 149 |
+
# Solve the equation
|
| 150 |
+
solution = solve_equation(latex_equation)
|
| 151 |
+
|
| 152 |
+
return jsonify({
|
| 153 |
+
'success': True,
|
| 154 |
+
'solution': solution
|
| 155 |
+
})
|
| 156 |
+
|
| 157 |
+
except Exception as e:
|
| 158 |
+
return jsonify({'error': str(e)}), 500
|
| 159 |
+
return jsonify({'error': 'Unknown error'}), 500
|
controller/table_controller.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import cv2
|
| 3 |
+
import numpy as np
|
| 4 |
+
import base64
|
| 5 |
+
import io
|
| 6 |
+
from flask import Blueprint, render_template, request, jsonify
|
| 7 |
+
from PIL import Image
|
| 8 |
+
from utils.math_solver import solve_equation # optional, if you use math solving
|
| 9 |
+
|
| 10 |
+
# -------------------------------
|
| 11 |
+
# Blueprint setup
|
| 12 |
+
# -------------------------------
|
| 13 |
+
table_bp = Blueprint('table_bp', __name__)
|
| 14 |
+
|
| 15 |
+
UPLOAD_FOLDER = 'static/uploads'
|
| 16 |
+
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
# -------------------------------
|
| 20 |
+
# Core Table Detection Logic
|
| 21 |
+
# -------------------------------
|
| 22 |
+
def detect_table(image_path):
|
| 23 |
+
"""Detect rows and columns from table image using Hough line detection."""
|
| 24 |
+
img = cv2.imread(image_path)
|
| 25 |
+
if img is None:
|
| 26 |
+
raise ValueError(f"Cannot read image at {image_path}")
|
| 27 |
+
|
| 28 |
+
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
| 29 |
+
gray = cv2.GaussianBlur(gray, (5, 5), 0)
|
| 30 |
+
edges = cv2.Canny(gray, 50, 150, apertureSize=3)
|
| 31 |
+
|
| 32 |
+
lines = cv2.HoughLinesP(
|
| 33 |
+
edges,
|
| 34 |
+
rho=1,
|
| 35 |
+
theta=np.pi / 180,
|
| 36 |
+
threshold=80,
|
| 37 |
+
minLineLength=60,
|
| 38 |
+
maxLineGap=15
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
if lines is None:
|
| 42 |
+
return 0, 0
|
| 43 |
+
|
| 44 |
+
horizontal, vertical = [], []
|
| 45 |
+
|
| 46 |
+
for x1, y1, x2, y2 in lines[:, 0]:
|
| 47 |
+
if abs(y2 - y1) < abs(x2 - x1): # horizontal line
|
| 48 |
+
horizontal.append((y1 + y2) / 2)
|
| 49 |
+
elif abs(x2 - x1) < abs(y2 - y1): # vertical line
|
| 50 |
+
vertical.append((x1 + x2) / 2)
|
| 51 |
+
|
| 52 |
+
def merge_lines(coords, gap=25):
|
| 53 |
+
coords = sorted(coords)
|
| 54 |
+
merged = []
|
| 55 |
+
if not coords:
|
| 56 |
+
return merged
|
| 57 |
+
group = [coords[0]]
|
| 58 |
+
for c in coords[1:]:
|
| 59 |
+
if abs(c - group[-1]) < gap:
|
| 60 |
+
group.append(c)
|
| 61 |
+
else:
|
| 62 |
+
merged.append(np.mean(group))
|
| 63 |
+
group = [c]
|
| 64 |
+
merged.append(np.mean(group))
|
| 65 |
+
return merged
|
| 66 |
+
|
| 67 |
+
horizontal = merge_lines(horizontal)
|
| 68 |
+
vertical = merge_lines(vertical)
|
| 69 |
+
|
| 70 |
+
num_rows = max(len(horizontal) - 1, 1)
|
| 71 |
+
num_cols = max(len(vertical) - 1, 1)
|
| 72 |
+
return num_rows, num_cols
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
# -------------------------------
|
| 76 |
+
# Generate LaTeX Table Code
|
| 77 |
+
# -------------------------------
|
| 78 |
+
def generate_latex_table(rows, cols):
|
| 79 |
+
col_format = '|' + '|'.join(['c'] * cols) + '|'
|
| 80 |
+
latex = f"\\begin{{tabular}}{{{col_format}}}\n\\hline\n"
|
| 81 |
+
for _ in range(rows):
|
| 82 |
+
latex += ' & '.join([''] * cols) + " \\\\ \\hline\n"
|
| 83 |
+
latex += "\\end{tabular}"
|
| 84 |
+
return latex
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
# -------------------------------
|
| 88 |
+
# Helpers
|
| 89 |
+
# -------------------------------
|
| 90 |
+
def save_base64_image(image_base64):
|
| 91 |
+
"""Convert base64 image string to file and return the saved path."""
|
| 92 |
+
try:
|
| 93 |
+
if ',' in image_base64:
|
| 94 |
+
image_base64 = image_base64.split(',')[1]
|
| 95 |
+
image_bytes = base64.b64decode(image_base64)
|
| 96 |
+
image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
|
| 97 |
+
filename = f"table_{len(os.listdir(UPLOAD_FOLDER)) + 1}.png"
|
| 98 |
+
filepath = os.path.join(UPLOAD_FOLDER, filename)
|
| 99 |
+
image.save(filepath)
|
| 100 |
+
return filepath
|
| 101 |
+
except Exception as e:
|
| 102 |
+
raise ValueError(f"Failed to decode base64 image: {e}")
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
# -------------------------------
|
| 106 |
+
# Routes
|
| 107 |
+
# -------------------------------
|
| 108 |
+
|
| 109 |
+
@table_bp.route("/table", methods=["GET"])
|
| 110 |
+
def table_page():
|
| 111 |
+
"""Render the main table-to-LaTeX page."""
|
| 112 |
+
return render_template("table.html")
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
@table_bp.route("/table/process", methods=["POST"])
|
| 116 |
+
def process_table_image():
|
| 117 |
+
"""
|
| 118 |
+
Handles both:
|
| 119 |
+
- Uploaded image file (FormData)
|
| 120 |
+
- Base64 image (drawn or camera)
|
| 121 |
+
"""
|
| 122 |
+
try:
|
| 123 |
+
# 🟢 Case 1: File upload (from drag & drop or browse)
|
| 124 |
+
if 'image' in request.files:
|
| 125 |
+
file = request.files['image']
|
| 126 |
+
if not file.filename:
|
| 127 |
+
return jsonify({'success': False, 'error': 'No file selected.'}), 400
|
| 128 |
+
|
| 129 |
+
filename = file.filename
|
| 130 |
+
filepath = os.path.join(UPLOAD_FOLDER, filename)
|
| 131 |
+
file.save(filepath)
|
| 132 |
+
|
| 133 |
+
# 🟢 Case 2: Base64 image (from canvas draw)
|
| 134 |
+
elif request.is_json:
|
| 135 |
+
data = request.get_json()
|
| 136 |
+
if 'image' not in data:
|
| 137 |
+
return jsonify({'success': False, 'error': 'No image data provided'}), 400
|
| 138 |
+
filepath = save_base64_image(data['image'])
|
| 139 |
+
|
| 140 |
+
else:
|
| 141 |
+
return jsonify({'success': False, 'error': 'Invalid image input.'}), 400
|
| 142 |
+
|
| 143 |
+
# Detect table structure
|
| 144 |
+
rows, cols = detect_table(filepath)
|
| 145 |
+
latex_code = generate_latex_table(rows, cols)
|
| 146 |
+
|
| 147 |
+
return jsonify({
|
| 148 |
+
'success': True,
|
| 149 |
+
'latex': latex_code,
|
| 150 |
+
'rows': rows,
|
| 151 |
+
'cols': cols,
|
| 152 |
+
'image_path': filepath
|
| 153 |
+
})
|
| 154 |
+
|
| 155 |
+
except Exception as e:
|
| 156 |
+
print(f"❌ Error in /table/process: {e}")
|
| 157 |
+
return jsonify({'success': False, 'error': str(e)}), 500
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
@table_bp.route("/table/generate", methods=["POST"])
|
| 161 |
+
def generate_table_from_input():
|
| 162 |
+
"""Generate LaTeX manually from user-input rows/cols."""
|
| 163 |
+
try:
|
| 164 |
+
data = request.get_json()
|
| 165 |
+
rows = int(data.get('rows', 0))
|
| 166 |
+
cols = int(data.get('cols', 0))
|
| 167 |
+
|
| 168 |
+
if rows <= 0 or cols <= 0:
|
| 169 |
+
return jsonify({'success': False, 'error': 'Invalid rows or columns'}), 400
|
| 170 |
+
|
| 171 |
+
latex_code = generate_latex_table(rows, cols)
|
| 172 |
+
|
| 173 |
+
return jsonify({'success': True, 'latex': latex_code})
|
| 174 |
+
except Exception as e:
|
| 175 |
+
print(f"❌ Error in /table/generate: {e}")
|
| 176 |
+
return jsonify({'success': False, 'error': str(e)}), 500
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
@table_bp.route("/table/solve", methods=["POST"])
|
| 180 |
+
def solve_table_content():
|
| 181 |
+
"""Optional: solve math expressions inside the LaTeX table (if supported)."""
|
| 182 |
+
try:
|
| 183 |
+
data = request.get_json()
|
| 184 |
+
if not data or 'latex' not in data:
|
| 185 |
+
return jsonify({'success': False, 'error': 'No LaTeX data provided'}), 400
|
| 186 |
+
|
| 187 |
+
solution = solve_equation(data['latex'])
|
| 188 |
+
return jsonify({'success': True, 'solution': solution})
|
| 189 |
+
|
| 190 |
+
except Exception as e:
|
| 191 |
+
print(f"❌ Error in /table/solve: {e}")
|
| 192 |
+
return jsonify({'success': False, 'error': str(e)}), 500
|
data/users.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"test_user_123": {
|
| 3 |
+
"id": "test_user_123",
|
| 4 |
+
"email": "test@example.com",
|
| 5 |
+
"name": "Test User",
|
| 6 |
+
"picture": null
|
| 7 |
+
},
|
| 8 |
+
"google_user_123": {
|
| 9 |
+
"id": "google_user_123",
|
| 10 |
+
"email": "user@gmail.com",
|
| 11 |
+
"name": "Google User",
|
| 12 |
+
"picture": null
|
| 13 |
+
},
|
| 14 |
+
"guest_gg_1762329533": {
|
| 15 |
+
"id": "guest_gg_1762329533",
|
| 16 |
+
"email": "gg@guest.texlab.com",
|
| 17 |
+
"name": "gg",
|
| 18 |
+
"picture": null
|
| 19 |
+
},
|
| 20 |
+
"guest_gg_1762330083": {
|
| 21 |
+
"id": "guest_gg_1762330083",
|
| 22 |
+
"email": "gg@guest.texlab.com",
|
| 23 |
+
"name": "gg",
|
| 24 |
+
"picture": null
|
| 25 |
+
},
|
| 26 |
+
"guest_syk_1762332308": {
|
| 27 |
+
"id": "guest_syk_1762332308",
|
| 28 |
+
"email": "syk@guest.texlab.com",
|
| 29 |
+
"name": "syk",
|
| 30 |
+
"picture": null
|
| 31 |
+
},
|
| 32 |
+
"guest_gg_1762333085": {
|
| 33 |
+
"id": "guest_gg_1762333085",
|
| 34 |
+
"email": "gg@guest.texlab.com",
|
| 35 |
+
"name": "gg",
|
| 36 |
+
"picture": null
|
| 37 |
+
},
|
| 38 |
+
"guest_syk_1762333094": {
|
| 39 |
+
"id": "guest_syk_1762333094",
|
| 40 |
+
"email": "syk@guest.texlab.com",
|
| 41 |
+
"name": "syk",
|
| 42 |
+
"picture": null
|
| 43 |
+
},
|
| 44 |
+
"guest_syk_1762333160": {
|
| 45 |
+
"id": "guest_syk_1762333160",
|
| 46 |
+
"email": "syk@guest.texlab.com",
|
| 47 |
+
"name": "syk",
|
| 48 |
+
"picture": null
|
| 49 |
+
},
|
| 50 |
+
"guest_syk_1762333181": {
|
| 51 |
+
"id": "guest_syk_1762333181",
|
| 52 |
+
"email": "syk@guest.texlab.com",
|
| 53 |
+
"name": "syk",
|
| 54 |
+
"picture": null
|
| 55 |
+
},
|
| 56 |
+
"guest_ali_1762333646": {
|
| 57 |
+
"id": "guest_ali_1762333646",
|
| 58 |
+
"email": "ali@guest.texlab.com",
|
| 59 |
+
"name": "ali",
|
| 60 |
+
"picture": null
|
| 61 |
+
},
|
| 62 |
+
"guest_ali_ashraf_1762334613": {
|
| 63 |
+
"id": "guest_ali_ashraf_1762334613",
|
| 64 |
+
"email": "ali.ashraf@guest.texlab.com",
|
| 65 |
+
"name": "ALI ASHRAF",
|
| 66 |
+
"picture": null
|
| 67 |
+
},
|
| 68 |
+
"guest_ashraf_1762608609": {
|
| 69 |
+
"id": "guest_ashraf_1762608609",
|
| 70 |
+
"email": "ashraf@guest.texlab.com",
|
| 71 |
+
"name": "Ashraf",
|
| 72 |
+
"picture": null
|
| 73 |
+
}
|
| 74 |
+
}
|
models/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Models package
|
models/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (176 Bytes). View file
|
|
|
models/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (147 Bytes). View file
|
|
|
models/__pycache__/user.cpython-311.pyc
ADDED
|
Binary file (3.64 kB). View file
|
|
|
models/__pycache__/user.cpython-313.pyc
ADDED
|
Binary file (3.01 kB). View file
|
|
|