Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Backend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ instance/

# Scrapy stuff:
.scrapy

myenv/
# Sphinx documentation
docs/_build/

Expand Down
3 changes: 3 additions & 0 deletions Backend/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,8 @@ class Config:
GROQ_BANK_ROWS_PER_CALL = int(os.getenv("GROQ_BANK_ROWS_PER_CALL", "20"))
GROQ_TRANSACTION_ROWS_PER_CALL = int(os.getenv("GROQ_TRANSACTION_ROWS_PER_CALL", "25"))
GROQ_OCR_MAX_INPUT_CHARS = int(os.getenv("GROQ_OCR_MAX_INPUT_CHARS", "12000"))
HF_TOKEN = os.getenv("HF_TOKEN", "")
HF_OCR_MODEL = os.getenv("HF_OCR_MODEL", "zai-org/GLM-OCR")
HF_OCR_PROVIDER = os.getenv("HF_OCR_PROVIDER", "zai-org")
TESSERACT_CMD = os.getenv("TESSERACT_CMD", "")
PDF_EXPORT_DIR = os.getenv("PDF_EXPORT_DIR", "generated_pdfs")
2 changes: 1 addition & 1 deletion Backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ groq
PyJWT
Pillow
pandas
pytesseract
python-dotenv
python-multipart
SQLAlchemy
psycopg[binary]
uvicorn
Werkzeug
huggingface_hub
79 changes: 41 additions & 38 deletions Backend/services/image_ocr.py
Original file line number Diff line number Diff line change
@@ -1,73 +1,73 @@
from __future__ import annotations

import os
from io import BytesIO
import tempfile
from typing import Any

from PIL import Image, ImageOps
from huggingface_hub import InferenceClient

from runtime import get_runtime_config
from services.document_ingestion import DocumentValidationError, parse_document
from services.groq_ai import extract_csv_from_ocr_text, groq_status
from services.tax_constants import SUPPORTED_DOCUMENTS

try:
import pytesseract
except ImportError: # pragma: no cover
pytesseract = None


IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp", ".tiff", ".tif", ".bmp"}


class OcrConversionError(ValueError):
pass


def ocr_status() -> dict[str, Any]:
config = get_runtime_config()
tesseract_cmd = str(getattr(config, "TESSERACT_CMD", "")).strip()
hf_token = str(getattr(config, "HF_TOKEN", "")).strip()
return {
"pytesseract_installed": pytesseract is not None,
"tesseract_cmd_configured": bool(tesseract_cmd),
"provider": getattr(config, "HF_OCR_PROVIDER", "zai-org"),
"model": getattr(config, "HF_OCR_MODEL", "zai-org/GLM-OCR"),
"has_hf_token": bool(hf_token),
"groq": groq_status(),
}
Comment on lines 22 to 30

Copilot AI Apr 3, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ocr_status() reports "glm_ocr_configured": True unconditionally, even if transformers/torch aren’t installed or the model can’t be loaded/downloaded. This can mislead the /options API and the frontend. Consider making this flag reflect reality (e.g., attempt a lightweight import and/or _load_glm_ocr() in a try/except and return configured: False plus an error detail when it fails).

Copilot uses AI. Check for mistakes.


def _is_image_filename(filename: str) -> bool:
return os.path.splitext(filename.lower())[1] in IMAGE_EXTENSIONS


def _extract_text_from_image(image_bytes: bytes) -> str:
if pytesseract is None:
raise OcrConversionError(
"pytesseract is not installed. Install it and ensure the Tesseract engine is available."
)

def _extract_text_from_image(image_bytes: bytes, source_name: str) -> str:
config = get_runtime_config()
tesseract_cmd = str(getattr(config, "TESSERACT_CMD", "")).strip()
if tesseract_cmd:
pytesseract.pytesseract.tesseract_cmd = tesseract_cmd
hf_token = str(getattr(config, "HF_TOKEN", "")).strip()
if not hf_token:
raise OcrConversionError("HF_TOKEN is not configured. Set HF_TOKEN to use Hugging Face OCR.")

try:
image = Image.open(BytesIO(image_bytes))
except Exception as exc:
raise OcrConversionError(f"Uploaded file is not a readable image: {exc}") from exc
client = InferenceClient(
provider=str(getattr(config, "HF_OCR_PROVIDER", "zai-org")).strip() or "zai-org",
api_key=hf_token,
)

# Grayscale + auto-contrast usually improves OCR quality on scans.
processed = ImageOps.autocontrast(ImageOps.grayscale(image))
suffix = os.path.splitext(source_name)[1].lower() or ".png"
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as temp_file:
temp_file.write(image_bytes)
temp_path = temp_file.name

try:
text = pytesseract.image_to_string(processed)
except Exception as exc:
raise OcrConversionError(
"OCR failed. Verify Tesseract is installed and TESSERACT_CMD is configured if needed."
) from exc

normalized = "\n".join(line.rstrip() for line in text.splitlines())
if len(normalized.strip()) < 20:
raise OcrConversionError(
"OCR extracted too little text from the image. Upload a clearer image or use CSV."
output = client.image_to_text(
temp_path,
model=str(getattr(config, "HF_OCR_MODEL", "zai-org/GLM-OCR")).strip() or "zai-org/GLM-OCR",
)
return normalized
normalized = "\n".join(line.rstrip() for line in str(output or "").splitlines())
if len(normalized.strip()) < 20:
raise OcrConversionError(
"OCR extracted too little text from the image. Upload a clearer image or use CSV."
)
return normalized
except OcrConversionError:
raise
except Exception as exc:
raise OcrConversionError(f"Hugging Face OCR inference failed: {exc}") from exc
finally:
if os.path.exists(temp_path):
os.remove(temp_path)


def convert_image_to_csv_document(
Expand All @@ -76,12 +76,13 @@ def convert_image_to_csv_document(
source_name: str,
image_bytes: bytes,
) -> dict[str, Any]:
config = get_runtime_config()
if document_type not in SUPPORTED_DOCUMENTS:
raise OcrConversionError(f"Unsupported document type: {document_type}")
if not _is_image_filename(source_name):
raise OcrConversionError(f"{source_name} is not a supported image format.")

ocr_text = _extract_text_from_image(image_bytes)
ocr_text = _extract_text_from_image(image_bytes, source_name)
schema = SUPPORTED_DOCUMENTS[document_type]
llm_result = extract_csv_from_ocr_text(
document_type=document_type,
Expand Down Expand Up @@ -122,7 +123,9 @@ def convert_image_to_csv_document(
"metadata": {
"source": "OCR",
"conversion_meta": {
"origin": "image_ocr",
"origin": "huggingface_glm_ocr",
"provider": getattr(config, "HF_OCR_PROVIDER", "zai-org"),
"model": getattr(config, "HF_OCR_MODEL", "zai-org/GLM-OCR"),
"original_source": source_name,
"ocr_chars": len(ocr_text),
"llm_meta": llm_result.get("meta", {}),
Expand Down
2 changes: 1 addition & 1 deletion Frontend/.env
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# Backend API URL
VITE_API_URL=https://taxai-77xc.onrender.com
VITE_API_URL=http://127.0.0.1:8000

Copilot AI Apr 3, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Committing Frontend/.env with VITE_API_URL=http://127.0.0.1:8000 will bake a localhost API URL into any production build that uses repository .env defaults, breaking deployments (and overriding the fallback onrender URL in src/utils/api.js). Typically .env should be untracked and .env.example used for documentation; consider removing Frontend/.env from version control (and adding it to Frontend/.gitignore) or restoring the deployed API URL here.

Suggested change
VITE_API_URL=http://127.0.0.1:8000
# Leave VITE_API_URL unset here so the app can use its built-in fallback URL.
# For local development, define VITE_API_URL in an untracked local env file instead.

Copilot uses AI. Check for mistakes.
2 changes: 1 addition & 1 deletion Frontend/.env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# Backend API URL
VITE_API_URL=http://127.0.0.1:5000
VITE_API_URL=http://127.0.0.1:8000
7 changes: 3 additions & 4 deletions Frontend/src/pages/AIAssistant.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useState } from 'react';
import AppLayout from '../components/AppLayout';

const API_URL = 'https://taxai-77xc.onrender.com';
import { API_BASE_URL } from '../utils/api';

export default function AIAssistant() {
const [activeTab, setActiveTab] = useState('tax');
Expand Down Expand Up @@ -120,7 +119,7 @@ export default function AIAssistant() {
});
}

const response = await fetch(`${API_URL}/api/tax-assistant/analyze`, {
const response = await fetch(`${API_BASE_URL}/api/tax-assistant/analyze`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand Down Expand Up @@ -161,7 +160,7 @@ export default function AIAssistant() {
// For personal Q&A, call the API
if (activeTab === 'personal' && taxAnalysis) {
try {
const response = await fetch(`${API_URL}/api/tax-assistant/ask`, {
const response = await fetch(`${API_BASE_URL}/api/tax-assistant/ask`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand Down
5 changes: 2 additions & 3 deletions Frontend/src/pages/Dashboard.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useState, useEffect } from 'react';
import AppLayout from '../components/AppLayout';

const API_URL = 'https://taxai-77xc.onrender.com';
import { API_BASE_URL } from '../utils/api';

export default function Dashboard() {
const [dashboardData, setDashboardData] = useState(null);
Expand All @@ -12,7 +11,7 @@ export default function Dashboard() {
const fetchDashboardData = async () => {
try {
const token = localStorage.getItem('access_token');
const response = await fetch(`${API_URL}/api/tax-assistant/dashboard/financial-data`, {
const response = await fetch(`${API_BASE_URL}/api/tax-assistant/dashboard/financial-data`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
Expand Down
23 changes: 11 additions & 12 deletions Frontend/src/pages/Filing.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { useState, useEffect } from 'react';
import { useAuth } from '../context/AuthContext';
import AppLayout from '../components/AppLayout';

const API_URL = 'https://taxai-77xc.onrender.com';
import { API_BASE_URL } from '../utils/api';

export default function Filing() {
const { user } = useAuth();
Expand All @@ -29,7 +28,7 @@ export default function Filing() {
useEffect(() => {
const fetchOptions = async () => {
try {
const response = await fetch(`${API_URL}/api/tax-assistant/options`, {
const response = await fetch(`${API_BASE_URL}/api/tax-assistant/options`, {
headers: { Authorization: `Bearer ${localStorage.getItem('access_token')}` },
});
const data = await response.json();
Expand All @@ -56,7 +55,7 @@ export default function Filing() {
const fetchJobs = async () => {
try {
setLoading(true);
const response = await fetch(`${API_URL}/api/tax-assistant/jobs`, {
const response = await fetch(`${API_BASE_URL}/api/tax-assistant/jobs`, {
headers: { Authorization: `Bearer ${localStorage.getItem('access_token')}` },
});
const data = await response.json();
Expand All @@ -82,7 +81,7 @@ export default function Filing() {
if (formData.deduction_80d) payload.deduction_80d = parseFloat(formData.deduction_80d);
if (formData.advance_tax) payload.advance_tax = parseFloat(formData.advance_tax);

const response = await fetch(`${API_URL}/api/tax-assistant/jobs`, {
const response = await fetch(`${API_BASE_URL}/api/tax-assistant/jobs`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand Down Expand Up @@ -590,7 +589,7 @@ function JobDetail({ job, onBack, onJobUpdate }) {
const token = localStorage.getItem('access_token');

// Fetch options
const optResponse = await fetch(`${API_URL}/api/tax-assistant/options`, {
const optResponse = await fetch(`${API_BASE_URL}/api/tax-assistant/options`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!optResponse.ok) throw new Error('Failed to fetch options');
Expand All @@ -611,7 +610,7 @@ function JobDetail({ job, onBack, onJobUpdate }) {
// Fetch review state if status is review or approved
if (job.status === 'review' || job.status === 'approved') {
const revResponse = await fetch(
`${API_URL}/api/tax-assistant/jobs/${jobId}/review`,
`${API_BASE_URL}/api/tax-assistant/jobs/${jobId}/review`,
{
headers: { Authorization: `Bearer ${token}` },
}
Expand Down Expand Up @@ -651,7 +650,7 @@ function JobDetail({ job, onBack, onJobUpdate }) {
docFormData.append('files', file);
});

const uploadUrl = `${API_URL}/api/tax-assistant/jobs/${jobId}/documents`;
const uploadUrl = `${API_BASE_URL}/api/tax-assistant/jobs/${jobId}/documents`;
console.log('Uploading to:', uploadUrl, 'Document type:', docItem.documentType);

const response = await fetch(uploadUrl, {
Expand Down Expand Up @@ -706,7 +705,7 @@ function JobDetail({ job, onBack, onJobUpdate }) {
try {
setProcessLoading(true);
const jobId = currentJob.job_id || currentJob.id;
const response = await fetch(`${API_URL}/api/tax-assistant/jobs/${jobId}/process`, {
const response = await fetch(`${API_BASE_URL}/api/tax-assistant/jobs/${jobId}/process`, {
method: 'POST',
headers: {
Authorization: `Bearer ${localStorage.getItem('access_token')}`,
Expand All @@ -721,7 +720,7 @@ function JobDetail({ job, onBack, onJobUpdate }) {
if (data.job?.status === 'review' || data.job?.status === 'approved') {
try {
const revResponse = await fetch(
`${API_URL}/api/tax-assistant/jobs/${jobId}/review`,
`${API_BASE_URL}/api/tax-assistant/jobs/${jobId}/review`,
{
headers: { Authorization: `Bearer ${localStorage.getItem('access_token')}` },
}
Expand Down Expand Up @@ -754,7 +753,7 @@ function JobDetail({ job, onBack, onJobUpdate }) {
try {
setApproveLoading(true);
const jobId = currentJob.job_id || currentJob.id;
const response = await fetch(`${API_URL}/api/tax-assistant/jobs/${jobId}/approve`, {
const response = await fetch(`${API_BASE_URL}/api/tax-assistant/jobs/${jobId}/approve`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand Down Expand Up @@ -786,7 +785,7 @@ function JobDetail({ job, onBack, onJobUpdate }) {
try {
setExportLoading(true);
const jobId = currentJob.job_id || currentJob.id;
const response = await fetch(`${API_URL}/api/tax-assistant/jobs/${jobId}/export/itr-pdf`, {
const response = await fetch(`${API_BASE_URL}/api/tax-assistant/jobs/${jobId}/export/itr-pdf`, {
headers: {
Authorization: `Bearer ${localStorage.getItem('access_token')}`,
},
Expand Down
13 changes: 6 additions & 7 deletions Frontend/src/pages/Upload.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useState, useEffect } from 'react';
import AppLayout from '../components/AppLayout';

const API_URL = 'https://taxai-77xc.onrender.com';
import { API_BASE_URL } from '../utils/api';

export default function Upload() {
const [mode, setMode] = useState('analyze'); // 'analyze' or 'filing'
Expand Down Expand Up @@ -35,15 +34,15 @@ export default function Upload() {
const token = localStorage.getItem('access_token');

// Fetch options
const optResponse = await fetch(`${API_URL}/api/tax-assistant/options`, {
const optResponse = await fetch(`${API_BASE_URL}/api/tax-assistant/options`, {
headers: { Authorization: `Bearer ${token}` },
});
const optData = await optResponse.json();
setOptions(optData);
setAnalyzeOptions(optData);

// Fetch jobs
const jobResponse = await fetch(`${API_URL}/api/tax-assistant/jobs`, {
const jobResponse = await fetch(`${API_BASE_URL}/api/tax-assistant/jobs`, {
headers: { Authorization: `Bearer ${token}` },
});
const jobData = await jobResponse.json();
Expand All @@ -63,7 +62,7 @@ export default function Upload() {
try {
const jobId = selectedJob.job_id || selectedJob.id;
const response = await fetch(
`${API_URL}/api/tax-assistant/jobs/${jobId}/documents`,
`${API_BASE_URL}/api/tax-assistant/jobs/${jobId}/documents`,
{
headers: { Authorization: `Bearer ${localStorage.getItem('access_token')}` },
}
Expand Down Expand Up @@ -142,7 +141,7 @@ export default function Upload() {
});

const response = await fetch(
`${API_URL}/api/tax-assistant/jobs/${jobId}/documents`,
`${API_BASE_URL}/api/tax-assistant/jobs/${jobId}/documents`,
{
method: 'POST',
headers: {
Expand Down Expand Up @@ -218,7 +217,7 @@ export default function Upload() {
formData.append('files', file);
});

const response = await fetch(`${API_URL}/api/tax-assistant/analyze-files`, {
const response = await fetch(`${API_BASE_URL}/api/tax-assistant/analyze-files`, {
method: 'POST',
headers: {
Authorization: `Bearer ${localStorage.getItem('access_token')}`,
Expand Down
2 changes: 1 addition & 1 deletion Frontend/src/utils/api.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const rawBaseUrl = import.meta.env.VITE_API_URL || 'https://taxai-77xc.onrender.com';
const rawBaseUrl = import.meta.env.VITE_API_URL || 'http://127.0.0.1:8000';

export const API_BASE_URL = rawBaseUrl.replace(/\/+$/, '');
export const API_PREFIX = `${API_BASE_URL}/api`;
Expand Down
Loading