Aniruddh commited on
Commit
8608e55
·
0 Parent(s):

clean new branch

Browse files
.dockerignore ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Node & Python cache
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.pyd
6
+ *.db
7
+ *.sqlite3
8
+ *.log
9
+ *.pid
10
+
11
+ # Virtual envs
12
+ venv/
13
+ env/
14
+ .venv/
15
+
16
+ # Node modules
17
+ node_modules/
18
+ app/node_modules/
19
+
20
+ # Git & local config
21
+ .git/
22
+ .gitignore
23
+ .gitattributes
24
+ .lfsconfig
25
+
26
+ # OS / IDE files
27
+ .DS_Store
28
+ Thumbs.db
29
+ .vscode/
30
+ .idea/
31
+ *.swp
32
+ *.swo
33
+
34
+ # HF build logs
35
+ output.log
.gitattributes ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
3
+ *.pptx filter=lfs diff=lfs merge=lfs -text
4
+ *.png filter=lfs diff=lfs merge=lfs -text
5
+ *.jpg filter=lfs diff=lfs merge=lfs -text
6
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # Frontend (Vite in app/)
3
+ app/node_modules/
4
+ app/dist/
5
+ app/.vite/
6
+ app/.env
7
+ app/.env.local
8
+ app/.env.*.local
9
+
10
+ # Backend
11
+ backend/.env
12
+ backend/__pycache__/
13
+ backend/*.pyc
14
+
15
+ stem/
16
+
17
+ .venv
18
+
19
+ .DS_Store
20
+
21
+
22
+
23
+ # Logs
24
+
25
+ logs
26
+ *.log
27
+ npm-debug.log*
28
+ yarn-debug.log*
29
+ yarn-error.log*
30
+ pnpm-debug.log*
31
+
32
+
33
+ Images/
34
+ app/public/images/
35
+ *.pptx
.idea/.gitignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # Default ignored files
2
+ /shelf/
3
+ /workspace.xml
4
+ # Editor-based HTTP Client requests
5
+ /httpRequests/
6
+ # Datasource local storage ignored files
7
+ /dataSources/
8
+ /dataSources.local.xml
.idea/inspectionProfiles/Project_Default.xml ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <component name="InspectionProjectProfileManager">
2
+ <profile version="1.0">
3
+ <option name="myName" value="Project Default" />
4
+ <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
5
+ <inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
6
+ <option name="ignoredErrors">
7
+ <list>
8
+ <option value="N806" />
9
+ <option value="N802" />
10
+ </list>
11
+ </option>
12
+ </inspection_tool>
13
+ <inspection_tool class="PyStubPackagesAdvertiser" enabled="true" level="WARNING" enabled_by_default="true">
14
+ <option name="ignoredPackages">
15
+ <list>
16
+ <option value="pandas-stubs==2.3.2.250827" />
17
+ </list>
18
+ </option>
19
+ </inspection_tool>
20
+ </profile>
21
+ </component>
.idea/inspectionProfiles/profiles_settings.xml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ <component name="InspectionProjectProfileManager">
2
+ <settings>
3
+ <option name="USE_PROJECT_PROFILE" value="false" />
4
+ <version value="1.0" />
5
+ </settings>
6
+ </component>
Dockerfile ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ---------- Stage 1: build React (under app/) ----------
2
+ FROM node:20-alpine AS fe
3
+ WORKDIR /fe
4
+
5
+ # Copy manifests explicitly from app/
6
+ COPY app/package.json ./
7
+ COPY app/package-lock.json ./
8
+
9
+ # Debug: verify files arrived
10
+ RUN echo "LIST /fe after copying manifests:" && ls -la
11
+
12
+ # Use lockfile if present; otherwise fallback to npm install
13
+ RUN if [ -f package-lock.json ]; then npm ci --no-audit; else npm install; fi
14
+
15
+ # Copy the rest of the frontend source
16
+ COPY app/ .
17
+
18
+ # Build Vite app -> /fe/dist
19
+ RUN npm run build
20
+ RUN echo "LIST /fe/dist after build:" && ls -la /fe/dist
21
+
22
+ # ---------- Stage 2: FastAPI runtime ----------
23
+ FROM python:3.10-slim
24
+
25
+ ENV PYTHONDONTWRITEBYTECODE=1 \
26
+ PYTHONUNBUFFERED=1
27
+
28
+ # System libs for OpenCV/EasyOCR
29
+ RUN apt-get update && apt-get install -y --no-install-recommends \
30
+ build-essential libgl1 libglib2.0-0 ca-certificates \
31
+ && rm -rf /var/lib/apt/lists/*
32
+
33
+ WORKDIR /app
34
+
35
+ # Copy backend code
36
+ COPY backend/ /app/backend/
37
+
38
+ # Copy built frontend into the folder FastAPI serves
39
+ COPY --from=fe /fe/dist/ /app/backend/frontend/
40
+
41
+ # Python deps (Torch CPU first)
42
+ RUN pip install --no-cache-dir --upgrade pip \
43
+ && pip install --no-cache-dir torch --index-url https://download.pytorch.org/whl/cpu \
44
+ && pip install --no-cache-dir -r backend/requirements.txt \
45
+ && pip install --no-cache-dir uvicorn==0.30.1
46
+
47
+
48
+ # Hugging Face port + persistent DB path
49
+ ENV PORT=7860
50
+ ENV DB_PATH=/data/sessions.db
51
+
52
+ CMD ["uvicorn", "backend.app:app", "--host", "0.0.0.0", "--port", "7860"]
Readme.md ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Gen AI for STEM Education
3
+ emoji: 🚀
4
+ colorFrom: indigo
5
+ colorTo: blue
6
+ sdk: docker
7
+ app_file: Dockerfile
8
+ pinned: false
9
+ ---
app/.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
app/README.md ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # React + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9
+
10
+ ## Expanding the ESLint configuration
11
+
12
+ If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
app/eslint.config.js ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import { defineConfig, globalIgnores } from 'eslint/config'
6
+
7
+ export default defineConfig([
8
+ globalIgnores(['dist']),
9
+ {
10
+ files: ['**/*.{js,jsx}'],
11
+ extends: [
12
+ js.configs.recommended,
13
+ reactHooks.configs['recommended-latest'],
14
+ reactRefresh.configs.vite,
15
+ ],
16
+ languageOptions: {
17
+ ecmaVersion: 2020,
18
+ globals: globals.browser,
19
+ parserOptions: {
20
+ ecmaVersion: 'latest',
21
+ ecmaFeatures: { jsx: true },
22
+ sourceType: 'module',
23
+ },
24
+ },
25
+ rules: {
26
+ 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
27
+ },
28
+ },
29
+ ])
app/index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Vite + React</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.jsx"></script>
12
+ </body>
13
+ </html>
app/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
app/package.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "app",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "axios": "^1.11.0",
14
+ "react": "^19.1.1",
15
+ "react-dom": "^19.1.1",
16
+ "react-router-dom": "^7.9.1"
17
+ },
18
+ "devDependencies": {
19
+ "@eslint/js": "^9.33.0",
20
+ "@types/react": "^19.1.10",
21
+ "@types/react-dom": "^19.1.7",
22
+ "@vitejs/plugin-react": "^5.0.0",
23
+ "eslint": "^9.33.0",
24
+ "eslint-plugin-react-hooks": "^5.2.0",
25
+ "eslint-plugin-react-refresh": "^0.4.20",
26
+ "globals": "^16.3.0",
27
+ "vite": "^7.1.2"
28
+ }
29
+ }
app/public/vite.svg ADDED
app/src/App.jsx ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useRef } from "react";
2
+ import { BrowserRouter, Routes, Route } from "react-router-dom";
3
+ import ImageUploader from "./components/ImageUploader";
4
+ import ImageCanvas from "./components/ImageCanvas";
5
+ import LoginForm from "./components/Loginform";
6
+ import Page from "./components/Page";
7
+ import useLogout from "./hooks/useLogout";
8
+ import useautoLogout from "./hooks/useautoLogout";
9
+ import "./styles/App.css";
10
+
11
+ function MainPage() {
12
+ const [imageUrl, setImageUrl] = useState(null);
13
+ const [rawCircles, setRawCircles] = useState([]);
14
+ const [rawTexts, setRawTexts] = useState([]);
15
+ const [circles, setCircles] = useState([]);
16
+ const [texts, setTexts] = useState([]);
17
+ const [selectedShape, setSelectedShape] = useState(null);
18
+ const [loaded, setLoaded] = useState(false);
19
+ const [error, setError] = useState(null);
20
+ const [imageInfo, setImageInfo] = useState(null);
21
+
22
+ const [user, setUser] = useState(null);
23
+ const [sessionId, setSessionId] = useState(null);
24
+
25
+ const imgRef = useRef(null);
26
+
27
+ // Shared logout hook
28
+ const handleLogout = useLogout(sessionId, setUser, setSessionId, setImageUrl);
29
+
30
+ // Auto logout (inactivity + tab close)
31
+ useautoLogout(sessionId, handleLogout, 10 * 1000);
32
+
33
+ return (
34
+ <div className="container">
35
+ <h1 className="heading">Generative AI for Stem Education</h1>
36
+
37
+ {!user ? (
38
+ <LoginForm setUser={setUser} setSessionId={setSessionId} />
39
+ ) : (
40
+ <>
41
+ <ImageUploader
42
+ setImageUrl={setImageUrl}
43
+ resetStates={() => {
44
+ setLoaded(false);
45
+ setError(null);
46
+ setRawCircles([]);
47
+ setRawTexts([]);
48
+ setCircles([]);
49
+ setTexts([]);
50
+ setSelectedShape(null);
51
+ }}
52
+ />
53
+
54
+ {error && <div className="error">{error}</div>}
55
+
56
+ {imageUrl && (
57
+ <ImageCanvas
58
+ imageUrl={imageUrl}
59
+ imgRef={imgRef}
60
+ setLoaded={setLoaded}
61
+ setError={setError}
62
+ setImageInfo={setImageInfo}
63
+ setCircles={setCircles}
64
+ setTexts={setTexts}
65
+ setRawCircles={setRawCircles}
66
+ setRawTexts={setRawTexts}
67
+ loaded={loaded}
68
+ imageInfo={imageInfo}
69
+ circles={circles}
70
+ texts={texts}
71
+ setSelectedShape={setSelectedShape}
72
+ selectedShape={selectedShape}
73
+ sessionId={sessionId}
74
+ />
75
+ )}
76
+
77
+ {selectedShape && (
78
+ <>
79
+ <div
80
+ style={{
81
+ position: "fixed",
82
+ top: 0,
83
+ left: 0,
84
+ width: "100%",
85
+ height: "100%",
86
+ zIndex: 999,
87
+ }}
88
+ onClick={() => setSelectedShape(null)}
89
+ />
90
+ </>
91
+ )}
92
+ </>
93
+ )}
94
+
95
+ {user && (
96
+ <button onClick={handleLogout} className="logout-button">
97
+ Logout
98
+ </button>
99
+ )}
100
+ </div>
101
+ );
102
+ }
103
+
104
+ function App() {
105
+ return (
106
+ <BrowserRouter>
107
+ <Routes>
108
+ <Route path="/" element={<MainPage />} />
109
+ <Route path="/page" element={<Page />} />
110
+ </Routes>
111
+ </BrowserRouter>
112
+ );
113
+ }
114
+
115
+ export default App;
app/src/assets/react.svg ADDED
app/src/components/ImageCanvas.jsx ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * ImageCanvas.jsx
3
+ *
4
+ * This component is responsible for:
5
+ * - Displaying an uploaded image
6
+ * - Sending the image to a backend for text/shape detection
7
+ * - Scaling the returned coordinates to match the displayed image
8
+ * - Rendering overlays for detected circles/texts
9
+ * - Allowing zoom in/out (via buttons and scroll wheel)
10
+ * - Showing a popup when a shape is selected
11
+ */
12
+
13
+
14
+ import React, { useRef } from "react";
15
+ import ShapeOverlay from "./ShapeOverlay";
16
+ import Popup from "./Popup";
17
+ import ZoomControls from "./ZoomControls";
18
+ import useZoom from "../hooks/useZoom";
19
+
20
+ function ImageCanvas({
21
+ imageUrl,
22
+ imgRef,
23
+ setLoaded,
24
+ setError,
25
+ setImageInfo,
26
+ setCircles,
27
+ setTexts,
28
+ setRawCircles,
29
+ setRawTexts,
30
+ loaded,
31
+ imageInfo,
32
+ circles,
33
+ texts,
34
+ setSelectedShape,
35
+ selectedShape,
36
+ }) {
37
+ const wrapperRef = useRef(null);
38
+ const { zoom, zoomIn, zoomOut, handleWheel } = useZoom({ min: 1, max: 3, step: 0.25 });
39
+
40
+ const handleImageLoad = async () => {
41
+ if (!imgRef.current) return;
42
+
43
+ const info = {
44
+ naturalWidth: imgRef.current.naturalWidth,
45
+ naturalHeight: imgRef.current.naturalHeight,
46
+ clientWidth: imgRef.current.clientWidth,
47
+ clientHeight: imgRef.current.clientHeight,
48
+ scaleX: imgRef.current.clientWidth / imgRef.current.naturalWidth,
49
+ scaleY: imgRef.current.clientHeight / imgRef.current.naturalHeight,
50
+ };
51
+ setImageInfo(info);
52
+ setLoaded(true);
53
+
54
+ try {
55
+ const blob = await fetch(imageUrl).then((r) => r.blob());
56
+ const formData = new FormData();
57
+ formData.append("file", blob, "image.png");
58
+
59
+ const res = await fetch("/detect/", {
60
+ method: "POST",
61
+ body: formData,
62
+ });
63
+
64
+ if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
65
+
66
+ const data = await res.json();
67
+ const rawCircles = data.circles || [];
68
+ const rawTexts = data.texts || [];
69
+
70
+ setRawCircles(rawCircles);
71
+ setRawTexts(rawTexts);
72
+
73
+ const scaledCircles = rawCircles.map((c) => ({
74
+ ...c,
75
+ x: c.x * info.scaleX,
76
+ y: c.y * info.scaleY,
77
+ r: c.r * Math.min(info.scaleX, info.scaleY),
78
+ }));
79
+
80
+ const scaledTexts = rawTexts.map((t) => ({
81
+ ...t,
82
+ x1: t.x1 * info.scaleX,
83
+ y1: t.y1 * info.scaleY,
84
+ x2: t.x2 * info.scaleX,
85
+ y2: t.y2 * info.scaleY,
86
+ }));
87
+
88
+ setCircles(scaledCircles);
89
+ setTexts(scaledTexts);
90
+ } catch (err) {
91
+ setError(`Failed to detect shapes: ${err.message}`);
92
+ console.error(err);
93
+ }
94
+ };
95
+
96
+ return (
97
+ <div style={{ position: "relative", width: "100%", height: "100%" }}>
98
+ <ZoomControls zoom={zoom} zoomIn={zoomIn} zoomOut={zoomOut} />
99
+
100
+ <div
101
+ className="image-container"
102
+ style={{ position: "relative", display: "inline-block", overflow: "auto" }}
103
+ onWheel={handleWheel}
104
+ >
105
+ <div
106
+ ref={wrapperRef}
107
+ className="zoom-wrapper"
108
+ style={{
109
+ position: "relative",
110
+ width: imageInfo ? imageInfo.clientWidth : "auto",
111
+ height: imageInfo ? imageInfo.clientHeight : "auto",
112
+ transform: `scale(${zoom})`,
113
+ transformOrigin: "0 0",
114
+ transition: "transform 120ms ease-out",
115
+ }}
116
+ >
117
+ <img
118
+ ref={imgRef}
119
+ src={imageUrl}
120
+ alt="uploaded"
121
+ onLoad={handleImageLoad}
122
+ style={{
123
+ width: imageInfo ? imageInfo.clientWidth : "100%",
124
+ height: imageInfo ? imageInfo.clientHeight : "auto",
125
+ display: "block",
126
+ userSelect: "none",
127
+ }}
128
+ />
129
+ {loaded && imageInfo && (
130
+ <ShapeOverlay
131
+ imageInfo={imageInfo}
132
+ circles={circles}
133
+ texts={texts}
134
+ setSelectedShape={setSelectedShape}
135
+ />
136
+ )}
137
+ </div>
138
+ </div>
139
+
140
+ {selectedShape && imageInfo && (
141
+ <Popup selectedShape={selectedShape} onClose={() => setSelectedShape(null)} zoom={zoom} />
142
+ )}
143
+ </div>
144
+ );
145
+ }
146
+
147
+ export default ImageCanvas;
app/src/components/ImageUploader.jsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * ImageUploader.jsx
3
+ *
4
+ * This component provides a file input to upload an image.
5
+ * - When a user selects an image, it creates a temporary object URL
6
+ * - Updates the parent component with the new image URL
7
+ * - Resets any existing detection/overlay states to start fresh
8
+ */
9
+ function ImageUploader({ setImageUrl, resetStates }) {
10
+ const handleUpload = (e) => {
11
+ const file = e.target.files[0];
12
+ if (!file) return;
13
+
14
+ const url = URL.createObjectURL(file);
15
+ setImageUrl(url);
16
+ resetStates();
17
+ };
18
+
19
+ return (
20
+ <input
21
+ type="file"
22
+ accept="image/*"
23
+ onChange={handleUpload}
24
+ className="file-input"
25
+ />
26
+ );
27
+ }
28
+
29
+ export default ImageUploader;
app/src/components/Loginform.jsx ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * LoginForm.jsx
3
+ *
4
+ * This component renders a login form where a user enters their name and email.
5
+ * - On submit, it sends the credentials to the backend login API.
6
+ * - If successful, it stores the user info and session ID in the parent state.
7
+ * - Handles loading state and displays error messages if login fails.
8
+ */
9
+
10
+
11
+ import { useState } from "react";
12
+ import "../styles/LoginForm.css";
13
+
14
+ function LoginForm({ setUser, setSessionId }) {
15
+ const [name, setName] = useState("");
16
+ const [email, setEmail] = useState("");
17
+ const [loading, setLoading] = useState(false);
18
+ const [error, setError] = useState("");
19
+
20
+ const handleSubmit = async (e) => {
21
+ e.preventDefault();
22
+ setLoading(true);
23
+ setError("");
24
+
25
+ try {
26
+ const response = await fetch("/auth/login", {
27
+ method: "POST",
28
+ body: new URLSearchParams({ name, email }),
29
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
30
+ });
31
+
32
+ const data = await response.json();
33
+
34
+ if (response.ok && data.session_id) {
35
+ setUser({ name, email });
36
+ setSessionId(data.session_id);
37
+ } else {
38
+ setError(data.message || "Login failed. Please try again.");
39
+ }
40
+ } catch (err) {
41
+ console.error("Login error:", err);
42
+ setError("Network error. Please check your connection and try again.");
43
+ } finally {
44
+ setLoading(false);
45
+ }
46
+ };
47
+
48
+ return (
49
+ <div className="login-container">
50
+ <h2>Login</h2>
51
+ <form onSubmit={handleSubmit} className="login-form">
52
+ <input
53
+ type="text"
54
+ placeholder="Enter Name"
55
+ value={name}
56
+ onChange={(e) => setName(e.target.value)}
57
+ required
58
+ disabled={loading}
59
+ />
60
+ <input
61
+ type="email"
62
+ placeholder="Enter Email"
63
+ value={email}
64
+ onChange={(e) => setEmail(e.target.value)}
65
+ required
66
+ disabled={loading}
67
+ />
68
+ {error && <div className="error-message" style={{color: 'red', margin: '10px 0'}}>{error}</div>}
69
+ <button type="submit" disabled={loading}>
70
+ {loading ? "Logging in..." : "Login"}
71
+ </button>
72
+ </form>
73
+ </div>
74
+ );
75
+ }
76
+
77
+ export default LoginForm;
app/src/components/Page.jsx ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Page.jsx
3
+ *
4
+ * This component displays a single page image and highlights a specific circle on it.
5
+ * - Reads query parameters `image` (page image URL) and `circle` (circle text to highlight)
6
+ * - Loads the image and calculates scale info for proper overlays
7
+ * - Sends the image to the backend detection API to get all circles
8
+ * - Finds the circle that matches the target text and highlights it
9
+ * - Supports zooming via buttons and mouse wheel
10
+ */
11
+
12
+
13
+ import { useState, useEffect, useRef } from "react";
14
+ import { useSearchParams } from "react-router-dom";
15
+ import ZoomControls from "./ZoomControls";
16
+ import useZoom from "../hooks/useZoom";
17
+ import "../styles/zoom.css";
18
+
19
+ function Page() {
20
+ const [searchParams] = useSearchParams();
21
+ const targetCircleText = searchParams.get("circle");
22
+ const pageImage = searchParams.get("image");
23
+
24
+ const [highlightCircle, setHighlightCircle] = useState(null);
25
+ const [imageInfo, setImageInfo] = useState(null);
26
+
27
+ const imgRef = useRef(null);
28
+ const wrapperRef = useRef(null);
29
+
30
+ const { zoom, zoomIn, zoomOut, handleWheel } = useZoom({ min: 1, max: 3, step: 0.25 });
31
+
32
+
33
+ const handleImageLoad = () => {
34
+ if (!imgRef.current) return;
35
+
36
+ const info = {
37
+ naturalWidth: imgRef.current.naturalWidth,
38
+ naturalHeight: imgRef.current.naturalHeight,
39
+ clientWidth: imgRef.current.clientWidth,
40
+ clientHeight: imgRef.current.clientHeight,
41
+ scaleX: imgRef.current.clientWidth / imgRef.current.naturalWidth,
42
+ scaleY: imgRef.current.clientHeight / imgRef.current.naturalHeight,
43
+ };
44
+ setImageInfo(info);
45
+ };
46
+
47
+ useEffect(() => {
48
+ if (!pageImage || !targetCircleText) {
49
+ setHighlightCircle(null);
50
+ return;
51
+ }
52
+
53
+ const detect = async () => {
54
+ try {
55
+ const blob = await fetch(pageImage).then((res) => res.blob());
56
+ const formData = new FormData();
57
+ formData.append("file", blob, "page.png");
58
+
59
+ const resp = await fetch("/detect/", {
60
+ method: "POST",
61
+ body: formData,
62
+ });
63
+
64
+ if (!resp.ok) throw new Error(`Detection failed: ${await resp.text()}`);
65
+
66
+ const data = await resp.json();
67
+ const circles = data.circles || [];
68
+
69
+ const targetCircle = circles.find(
70
+ (c) =>
71
+ c.circle_text &&
72
+ c.circle_text.trim().toLowerCase() === targetCircleText.trim().toLowerCase()
73
+ );
74
+
75
+ setHighlightCircle(targetCircle || null);
76
+ } catch (err) {
77
+ console.error("Detection error:", err);
78
+ setHighlightCircle(null);
79
+ }
80
+ };
81
+
82
+ detect();
83
+ }, [pageImage, targetCircleText]);
84
+
85
+ const getScaledCircle = () => {
86
+ if (!highlightCircle || !imageInfo) return null;
87
+
88
+ return {
89
+ cx: highlightCircle.x * imageInfo.scaleX,
90
+ cy: highlightCircle.y * imageInfo.scaleY,
91
+ r: highlightCircle.r * Math.min(imageInfo.scaleX, imageInfo.scaleY),
92
+ };
93
+ };
94
+
95
+ const scaledCircle = getScaledCircle();
96
+
97
+ return (
98
+ <div style={{ position: "relative", width: "100%", height: "100%" }}>
99
+ <ZoomControls zoom={zoom} zoomIn={zoomIn} zoomOut={zoomOut} />
100
+
101
+ {pageImage && (
102
+ <div
103
+ className="image-container"
104
+ style={{ position: "relative", display: "inline-block", overflow: "auto" }}
105
+ onWheel={handleWheel}
106
+ >
107
+ <div
108
+ ref={wrapperRef}
109
+ className="zoom-wrapper"
110
+ style={{
111
+ position: "relative",
112
+ width: imageInfo ? imageInfo.clientWidth : "auto",
113
+ height: imageInfo ? imageInfo.clientHeight : "auto",
114
+ transform: `scale(${zoom})`,
115
+ transformOrigin: "0 0",
116
+ transition: "transform 120ms ease-out",
117
+ }}
118
+ >
119
+ <img
120
+ ref={imgRef}
121
+ src={pageImage}
122
+ alt="Page"
123
+ onLoad={handleImageLoad}
124
+ style={{
125
+ width: imageInfo ? imageInfo.clientWidth : "100%",
126
+ height: imageInfo ? imageInfo.clientHeight : "auto",
127
+ display: "block",
128
+ userSelect: "none",
129
+ }}
130
+ onError={(e) => {
131
+ console.error("Image failed to load:", pageImage);
132
+ e.target.alt = "Failed to load image";
133
+ }}
134
+ />
135
+
136
+ {/* Circle overlay */}
137
+ {scaledCircle && (
138
+ <svg
139
+ width={imageInfo?.clientWidth || 0}
140
+ height={imageInfo?.clientHeight || 0}
141
+ style={{ position: "absolute", top: 0, left: 0, pointerEvents: "none" }}
142
+ >
143
+ <circle
144
+ cx={scaledCircle.cx}
145
+ cy={scaledCircle.cy}
146
+ r={scaledCircle.r}
147
+ stroke="blue"
148
+ strokeWidth="3"
149
+ fill="none"
150
+ />
151
+ </svg>
152
+ )}
153
+ </div>
154
+ </div>
155
+ )}
156
+ </div>
157
+ );
158
+ }
159
+
160
+ export default Page;
app/src/components/Popup.jsx ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Popup.jsx
3
+ *
4
+ * This component displays a popup box with information about a selected shape (circle or text).
5
+ * - For circles: shows page number and circle text, with a button to navigate to the corresponding page.
6
+ * - For text: fetches additional info from the LLM backend and displays it.
7
+ * - Supports zoomed image coordinates without scaling the popup itself.
8
+ * - Provides a close button to dismiss the popup.
9
+ */
10
+
11
+
12
+ import { useState, useEffect } from "react";
13
+
14
+ function Popup({ selectedShape, onClose, zoom = 1 }) {
15
+ const [info, setInfo] = useState(null);
16
+
17
+ useEffect(() => {
18
+ if (!selectedShape || selectedShape.r) return;
19
+ setInfo("Loading...");
20
+ fetch("/llm/generate_info/", {
21
+ method: "POST",
22
+ headers: { "Content-Type": "application/json" },
23
+ body: JSON.stringify({ content: selectedShape.text || "Unlabeled text" }),
24
+ })
25
+ .then((res) => {
26
+ if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
27
+ return res.json();
28
+ })
29
+ .then((data) => setInfo(data.info))
30
+ .catch((error) => {
31
+ console.error("LLM fetch error:", error);
32
+ setInfo("Error generating info");
33
+ });
34
+ }, [selectedShape]);
35
+
36
+ if (!selectedShape) return null;
37
+
38
+ // ✅ Apply zoom only to coordinates, not to popup size
39
+ const left =
40
+ (selectedShape.r
41
+ ? selectedShape.x + selectedShape.r + 10
42
+ : selectedShape.x2 + 10) * zoom;
43
+
44
+ const top =
45
+ (selectedShape.r
46
+ ? selectedShape.y - selectedShape.r / 2
47
+ : selectedShape.y1) * zoom;
48
+
49
+ // Redirect handler
50
+ const handleRedirect = (e, pageNumber, circleText) => {
51
+ e.preventDefault();
52
+ e.stopPropagation();
53
+
54
+ if (!pageNumber) {
55
+ console.log("Missing navigation data:", { pageNumber });
56
+ return;
57
+ }
58
+
59
+ const pageImageUrl = `/images/${pageNumber}.png`;
60
+ let targetUrl = `/page?image=${encodeURIComponent(pageImageUrl)}`;
61
+ if (circleText) {
62
+ targetUrl += `&circle=${encodeURIComponent(circleText)}`;
63
+ }
64
+
65
+ window.open(targetUrl, "_blank", "noopener,noreferrer");
66
+ onClose();
67
+ };
68
+
69
+ return (
70
+ <div
71
+ className="popup-box"
72
+ onClick={(e) => e.stopPropagation()}
73
+ style={{
74
+ position: "absolute",
75
+ left: `${left}px`,
76
+ top: `${top}px`,
77
+ zIndex: 1001,
78
+ backgroundColor: "orange",
79
+ border: "1px solid #ccc",
80
+ borderRadius: "8px",
81
+ padding: "10px",
82
+ boxShadow: "0 2px 10px rgba(0,0,0,0.1)",
83
+ minWidth: "200px",
84
+ transform: "scale(1)",
85
+ }}
86
+ >
87
+ {selectedShape.r ? (
88
+ <div>
89
+ <h4>Circle Information</h4>
90
+ {selectedShape.page_number ? (
91
+ <ul style={{ listStyle: "none", padding: 0 }}>
92
+ <li style={{ marginBottom: "8px" }}>
93
+ <strong>Page Number:</strong> {selectedShape.page_number}
94
+ </li>
95
+ {selectedShape.circle_text && (
96
+ <li>
97
+ <strong>Circle Text:</strong> {selectedShape.circle_text}
98
+ </li>
99
+ )}
100
+ <li style={{ marginTop: "5px" }}>
101
+ <button
102
+ style={{
103
+ cursor: "pointer",
104
+ color: "white",
105
+ backgroundColor: "#007bff",
106
+ border: "none",
107
+ padding: "5px 10px",
108
+ borderRadius: "4px",
109
+ }}
110
+ onClick={(e) =>
111
+ handleRedirect(
112
+ e,
113
+ selectedShape.page_number,
114
+ selectedShape.circle_text
115
+ )
116
+ }
117
+ >
118
+ Go to page {selectedShape.page_number}
119
+ </button>
120
+ </li>
121
+ </ul>
122
+ ) : (
123
+ <p>No page number available for navigation.</p>
124
+ )}
125
+ <button
126
+ onClick={onClose}
127
+ style={{
128
+ marginTop: "10px",
129
+ padding: "5px 10px",
130
+ backgroundColor: "#6c757d",
131
+ color: "white",
132
+ border: "none",
133
+ borderRadius: "4px",
134
+ cursor: "pointer",
135
+ }}
136
+ >
137
+ Close
138
+ </button>
139
+ </div>
140
+ ) : (
141
+ <div>
142
+ <h4>Detected Text</h4>
143
+ <p>
144
+ <strong>Text:</strong> {selectedShape.text || "Text"}
145
+ </p>
146
+ <p>
147
+ <strong>Info:</strong> {info || "Click to generate info"}
148
+ </p>
149
+ <button
150
+ onClick={onClose}
151
+ style={{
152
+ marginTop: "10px",
153
+ padding: "5px 10px",
154
+ backgroundColor: "#6c757d",
155
+ color: "white",
156
+ border: "none",
157
+ borderRadius: "4px",
158
+ cursor: "pointer",
159
+ }}
160
+ >
161
+ Close
162
+ </button>
163
+ </div>
164
+ )}
165
+ </div>
166
+ );
167
+ }
168
+
169
+ export default Popup;
170
+
app/src/components/ShapeOverlay.jsx ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * ShapeOverlay.jsx
3
+ *
4
+ * This component renders an SVG overlay on top of an image to visually highlight
5
+ * detected circles and text regions. Each shape is clickable, allowing users
6
+ * to select a shape and view detailed information via a popup.
7
+ */
8
+
9
+
10
+ import React from "react";
11
+
12
+ function ShapeOverlay({ imageInfo, circles, texts, setSelectedShape }) {
13
+ const handleShapeClick = (e, shape) => {
14
+ e.preventDefault();
15
+ e.stopPropagation();
16
+ console.log("Shape clicked:", shape);
17
+ setSelectedShape(shape);
18
+ };
19
+
20
+ return (
21
+ <svg
22
+ className="overlay-svg"
23
+ width={imageInfo.clientWidth}
24
+ height={imageInfo.clientHeight}
25
+ viewBox={`0 0 ${imageInfo.clientWidth} ${imageInfo.clientHeight}`}
26
+ style={{
27
+ position: "absolute",
28
+ top: 0,
29
+ left: 0,
30
+ pointerEvents: "auto",
31
+ zIndex: 10,
32
+ transformOrigin: "0 0",
33
+ }}
34
+ >
35
+ {circles.map((c) => (
36
+ <circle
37
+ key={`circle-${c.id}`}
38
+ cx={c.x}
39
+ cy={c.y}
40
+ r={c.r}
41
+ fill="rgba(255, 0, 0, 0.22)"
42
+ stroke="red"
43
+ strokeWidth="2"
44
+ onClick={(e) => handleShapeClick(e, c)}
45
+ style={{ cursor: "pointer", pointerEvents: "all" }}
46
+ />
47
+ ))}
48
+
49
+ {texts.map((t) => (
50
+ <rect
51
+ key={`text-${t.id}`}
52
+ x={t.x1}
53
+ y={t.y1}
54
+ width={t.x2 - t.x1}
55
+ height={t.y2 - t.y1}
56
+ fill="rgba(0,255,0,0.18)"
57
+ stroke="green"
58
+ strokeWidth="2"
59
+ onClick={(e) => handleShapeClick(e, t)}
60
+ style={{ cursor: "pointer", pointerEvents: "all" }}
61
+ />
62
+ ))}
63
+ </svg>
64
+ );
65
+ }
66
+
67
+ export default ShapeOverlay;
app/src/components/ZoomControls.jsx ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import "../styles/Zoom.css";
3
+
4
+ export default function ZoomControls({ zoom, zoomIn, zoomOut }) {
5
+ return (
6
+ <div className="zoom-controls">
7
+ <button onClick={zoomIn} title="Zoom in" className="zoom-btn">+</button>
8
+ <button onClick={zoomOut} title="Zoom out" className="zoom-btn">−</button>
9
+ </div>
10
+ );
11
+ }
app/src/hooks/useLogout.jsx ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * useLogout.js
3
+ *
4
+ * This custom React hook returns a memoized logout function.
5
+ * - Sends a logout request to the backend API with the current session ID.
6
+ * - Clears user-related state (user info, session ID, and uploaded image) on logout.
7
+ * - Ensures the function reference is stable with useCallback for performance.
8
+ */
9
+
10
+
11
+ import { useCallback } from "react";
12
+
13
+ export default function useLogout(sessionId, setUser, setSessionId, setImageUrl) {
14
+ return useCallback(async () => {
15
+ if (!sessionId) return;
16
+
17
+ try {
18
+ await fetch("http://localhost:8001/logout", {
19
+ method: "POST",
20
+ headers: { "Content-Type": "application/json" },
21
+ body: JSON.stringify({ session_id: sessionId }),
22
+ });
23
+ } catch (err) {
24
+ console.error("Logout error:", err);
25
+ }
26
+
27
+ setUser(null);
28
+ setSessionId(null);
29
+ setImageUrl(null);
30
+ }, [sessionId, setUser, setSessionId, setImageUrl]);
31
+ }
app/src/hooks/useZoom.jsx ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * useZoom.js
3
+ *
4
+ * This custom React hook manages zoom functionality for UI elements such as images.
5
+ * - Allows zooming in, zooming out, and setting exact zoom levels.
6
+ * - Supports zoom via buttons, keyboard shortcuts (Ctrl/Cmd + +, -, 0), and mouse wheel.
7
+ * - Enforces minimum and maximum zoom limits and configurable zoom step.
8
+ */
9
+
10
+
11
+ import { useState, useEffect } from "react";
12
+
13
+ export default function useZoom({ min = 1, max = 3, step = 0.25 }) {
14
+ const [zoom, setZoom] = useState(1);
15
+
16
+ const clamp = (v) => Math.min(Math.max(v, min), max);
17
+
18
+ const zoomIn = () => setZoom((z) => clamp(Number((z + step).toFixed(2))));
19
+ const zoomOut = () => setZoom((z) => clamp(Number((z - step).toFixed(2))));
20
+ const setZoomExact = (v) => setZoom(clamp(Number(v)));
21
+
22
+ useEffect(() => {
23
+ const handleKeyDown = (e) => {
24
+ if (e.ctrlKey || e.metaKey) {
25
+ if (e.key === "+" || e.key === "=") {
26
+ e.preventDefault();
27
+ zoomIn();
28
+ } else if (e.key === "-") {
29
+ e.preventDefault();
30
+ zoomOut();
31
+ } else if (e.key === "0") {
32
+ e.preventDefault();
33
+ setZoomExact(1);
34
+ }
35
+ }
36
+ };
37
+
38
+ window.addEventListener("keydown", handleKeyDown);
39
+ return () => window.removeEventListener("keydown", handleKeyDown);
40
+ }, []);
41
+
42
+ const handleWheel = (e) => {
43
+ if (e.ctrlKey || e.metaKey) {
44
+ e.preventDefault();
45
+ const delta = -e.deltaY;
46
+ const factor = delta > 0 ? 1 + step : 1 - step;
47
+ setZoom((z) => clamp(Math.round(z * factor * 100) / 100));
48
+ }
49
+ };
50
+
51
+ return { zoom, zoomIn, zoomOut, handleWheel };
52
+ }
app/src/hooks/useautoLogout.jsx ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * useAutoLogout.js
3
+ *
4
+ * This custom React hook automatically logs out a user after a period of inactivity.
5
+ * - Tracks user activity (mouse movement, clicks, key presses, scrolling).
6
+ * - Resets the inactivity timer on any interaction.
7
+ * - Calls the provided handleLogout function if the user is inactive for the specified timeout.
8
+ * - Also logs out the user when the window is closed or refreshed.
9
+ */
10
+
11
+ import { useEffect } from "react";
12
+
13
+ export default function useAutoLogout(sessionId, handleLogout, timeout) {
14
+ useEffect(() => {
15
+ if (!sessionId) return;
16
+
17
+ let timer;
18
+
19
+ const resetTimer = () => {
20
+ clearTimeout(timer);
21
+ timer = setTimeout(() => {
22
+ console.log("Auto logout: inactive for 5 mins");
23
+ handleLogout();
24
+ }, timeout);
25
+ };
26
+
27
+ window.addEventListener("mousemove", resetTimer);
28
+ window.addEventListener("keydown", resetTimer);
29
+ window.addEventListener("click", resetTimer);
30
+ window.addEventListener("scroll", resetTimer);
31
+
32
+ const handleUnload = () => handleLogout();
33
+ window.addEventListener("beforeunload", handleUnload);
34
+
35
+ resetTimer();
36
+
37
+ return () => {
38
+ clearTimeout(timer);
39
+ window.removeEventListener("mousemove", resetTimer);
40
+ window.removeEventListener("keydown", resetTimer);
41
+ window.removeEventListener("click", resetTimer);
42
+ window.removeEventListener("scroll", resetTimer);
43
+ window.removeEventListener("beforeunload", handleUnload);
44
+ };
45
+ }, [sessionId, handleLogout, timeout]);
46
+ }
app/src/index.css ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
3
+ line-height: 1.5;
4
+ font-weight: 400;
5
+
6
+ color-scheme: light dark;
7
+ color: rgba(255, 255, 255, 0.87);
8
+ background-color: #242424;
9
+
10
+ font-synthesis: none;
11
+ text-rendering: optimizeLegibility;
12
+ -webkit-font-smoothing: antialiased;
13
+ -moz-osx-font-smoothing: grayscale;
14
+ }
15
+
16
+ a {
17
+ font-weight: 500;
18
+ color: #646cff;
19
+ text-decoration: inherit;
20
+ }
21
+ a:hover {
22
+ color: #535bf2;
23
+ }
24
+
25
+ body {
26
+ margin: 0;
27
+ display: flex;
28
+ place-items: center;
29
+ min-width: 320px;
30
+ min-height: 100vh;
31
+ }
32
+
33
+ h1 {
34
+ font-size: 3.2em;
35
+ line-height: 1.1;
36
+ }
37
+
38
+ button {
39
+ border-radius: 8px;
40
+ border: 1px solid transparent;
41
+ padding: 0.6em 1.2em;
42
+ font-size: 1em;
43
+ font-weight: 500;
44
+ font-family: inherit;
45
+ background-color: #1a1a1a;
46
+ cursor: pointer;
47
+ transition: border-color 0.25s;
48
+ }
49
+ button:hover {
50
+ border-color: #646cff;
51
+ }
52
+ button:focus,
53
+ button:focus-visible {
54
+ outline: 4px auto -webkit-focus-ring-color;
55
+ }
56
+
57
+ @media (prefers-color-scheme: light) {
58
+ :root {
59
+ color: #213547;
60
+ background-color: #ffffff;
61
+ }
62
+ a:hover {
63
+ color: #747bff;
64
+ }
65
+ button {
66
+ background-color: #f9f9f9;
67
+ }
68
+ }
app/src/main.jsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.jsx'
5
+
6
+ createRoot(document.getElementById('root')).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
app/src/styles/App.css ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .container {
2
+ position: relative;
3
+ width: 100vw;
4
+ height: 100vh;
5
+ margin: 0;
6
+ padding: 20px;
7
+ box-sizing: border-box;
8
+ text-align: center;
9
+ }
10
+ .logout-button {
11
+ position: absolute;
12
+ top: 20px;
13
+ right: 20px;
14
+ cursor: pointer;
15
+ padding: 5px 10px;
16
+ border-radius: 4px;
17
+ border: none;
18
+ background-color: #007bff;
19
+ color: white;
20
+ transform: none;
21
+ }
22
+
23
+ .heading {
24
+ font-size: 50px;
25
+ font-weight: bold;
26
+ margin-bottom: 20px;
27
+ }
28
+
29
+ .file-input {
30
+ margin-bottom: 20px;
31
+ width: 100%;
32
+ padding: 6px;
33
+ }
34
+
35
+ .error {
36
+ margin-bottom: 15px;
37
+ padding: 10px;
38
+ background-color: #fee2e2;
39
+ border: 1px solid #fca5a5;
40
+ color: #b91c1c;
41
+ border-radius: 6px;
42
+ }
43
+ .image-container {
44
+ position: relative;
45
+ display: inline-block;
46
+ border: 2px solid #d1d5db;
47
+ border-radius: 8px;
48
+ }
49
+
50
+ .uploaded-image {
51
+ max-width: 100%;
52
+ max-height: 100%;
53
+ display: block;
54
+ }
55
+
56
+ .overlay-svg {
57
+ position: absolute;
58
+ top: 0;
59
+ left: 0;
60
+ width: 100%;
61
+ height: 100%;
62
+ }
63
+
64
+ .circle, rect {
65
+ cursor: pointer;
66
+ }
67
+
68
+ .popup-overlay {
69
+ position: absolute;
70
+ top: 0;
71
+ left: 0;
72
+ width: 100%;
73
+ height: 100%;
74
+ z-index: 1000;
75
+ }
76
+
77
+ .popup-box {
78
+ list-style-type: none;
79
+ position: absolute;
80
+ background-color: #3b82f6;
81
+ color: white;
82
+ padding: 6px 10px;
83
+ border-radius: 6px;
84
+ min-width: 50px;
85
+ max-width: 100px;
86
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
87
+ font-size: 15px;
88
+ line-height: 1.2;
89
+ }
90
+
91
+ .popup-box ul {
92
+ list-style-type: none;
93
+ padding-left: 0;
94
+ margin: 0;
95
+ }
app/src/styles/LoginForm.css ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .login-container {
2
+ max-width: 300px;
3
+ margin: 0.5rem auto;
4
+ padding: 1rem;
5
+ border: 1px solid #ddd;
6
+ border-radius: 8px;
7
+ background: #fafafa;
8
+ color: #333;
9
+ font-size: 1.2rem;
10
+ }
11
+
12
+ .login-form {
13
+ display: flex;
14
+ flex-direction: column;
15
+ gap: 0.75rem;
16
+ }
17
+
18
+ .login-form input {
19
+ padding: 0.5rem;
20
+ font-size: 1rem;
21
+ background-color: #080b0da1;
22
+ font-size: 1.2rem;
23
+ }
24
+
25
+ .login-form button {
26
+ padding: 0.5rem;
27
+ background: #007bff;
28
+ color: white;
29
+ border: none;
30
+ border-radius: 4px;
31
+ cursor: pointer;
32
+ }
33
+
34
+ .login-form button:hover {
35
+ background: #0056b3;
36
+ }
37
+
38
+ .logout-button {
39
+ position: fixed;
40
+ top: 1rem;
41
+ right: 1rem;
42
+ padding: 0.5rem 1rem;
43
+ background: #007bff;
44
+ color: white;
45
+ border: none;
46
+ border-radius: 4px;
47
+ cursor: pointer;
48
+ z-index: 1000;
49
+ }
50
+
51
+ .logout-button:hover {
52
+ background: #0056b3;
53
+ }
app/src/styles/Zoom.css ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .zoom-controls {
2
+ position: fixed;
3
+ top: 100px;
4
+ right: 16px;
5
+ z-index: 2000;
6
+ display: flex;
7
+ flex-direction: row;
8
+ gap: 8px;
9
+ background: rgba(255, 255, 255, 0.9);
10
+ padding: 8px;
11
+ border-radius: 8px;
12
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
13
+ }
14
+
15
+ .zoom-btn {
16
+ width: 36px;
17
+ height: 36px;
18
+ font-size: 20px;
19
+ line-height: 36px;
20
+ text-align: center;
21
+ padding: 0;
22
+ cursor: pointer;
23
+ }
24
+
25
+ .zoom-label {
26
+ font-size: 12px;
27
+ display: flex;
28
+ align-items: center;
29
+ justify-content: center;
30
+ }
app/vite.config.js ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ // https://vite.dev/config/
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ })
backend/__init__.py ADDED
File without changes
backend/app.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from starlette.responses import JSONResponse, FileResponse
4
+
5
+ from fastapi.staticfiles import StaticFiles
6
+
7
+ import os
8
+ from pathlib import Path
9
+
10
+ from backend.routes import detect, llm,auth, regions_detect
11
+
12
+ app = FastAPI()
13
+
14
+ app.add_middleware(
15
+ CORSMiddleware,
16
+ allow_origins=["*"],
17
+ allow_credentials=True,
18
+ allow_methods=["*"],
19
+ allow_headers=["*"],
20
+ )
21
+
22
+ app.include_router(detect.router, prefix="/detect", tags=["Detection"])
23
+ app.include_router(llm.router, prefix="/llm", tags=["LLM"])
24
+ app.include_router(auth.router, prefix="/auth", tags=["Auth"])
25
+ app.include_router(regions_detect.router, prefix="/detect", tags=["Region Detection"])
26
+
27
+
28
+
29
+ @app.get("/healthz")
30
+ def health():
31
+ return {"ok": True}
32
+
33
+
34
+
35
+ # @app.get("/", tags=["Root"])
36
+ # def read_root():
37
+ # return JSONResponse({"message": "Backend is running!"})
38
+
39
+
40
+ HERE = Path(__file__).resolve().parent
41
+ FE_DIR = HERE / "frontend"
42
+
43
+ app.mount("/assets", StaticFiles(directory=str(FE_DIR / "assets")), name="assets")
44
+ app.mount("/images", StaticFiles(directory=str(FE_DIR / "images")), name="images")
45
+
46
+ @app.get("/")
47
+ def index():
48
+ return FileResponse(FE_DIR / "index.html")
49
+
50
+ @app.get("/favicon.co")
51
+ def favicon():
52
+ return JSONResponse({"detail": "No favicon configured"}, status_code=404)
53
+
54
+
55
+ # FRONTEND_DIR = os.path.join(os.path.dirname(__file__), "frontend")
56
+ # if os.path.isdir(FRONTEND_DIR):
57
+ # app.mount("/", StaticFiles(directory=FRONTEND_DIR, html=True), name="frontend")
58
+
backend/requirements.txt ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ annotated-types==0.7.0
2
+ anyio==4.11.0
3
+ certifi==2025.10.5
4
+ click==8.1.8
5
+ distro==1.9.0
6
+ dnspython==2.7.0
7
+ easyocr==1.7.0
8
+ email-validator==2.3.0
9
+ exceptiongroup==1.3.0
10
+ fastapi==0.111.1
11
+ fastapi-cli==0.0.14
12
+ filelock==3.19.1
13
+ fsspec==2025.9.0
14
+ groq==0.33.0
15
+ h11==0.16.0
16
+ httpcore==1.0.9
17
+ httptools==0.7.1
18
+ httpx==0.28.1
19
+ idna==3.11
20
+ imageio==2.37.0
21
+ Jinja2==3.1.6
22
+ lazy_loader==0.4
23
+ markdown-it-py==3.0.0
24
+ MarkupSafe==3.0.3
25
+ mdurl==0.1.2
26
+ mpmath==1.3.0
27
+ networkx==3.2.1
28
+ ninja==1.13.0
29
+ numpy==1.26.4
30
+ #opencv-python==4.8.1.78
31
+ opencv-python-headless==4.8.1.78
32
+ packaging==25.0
33
+ Pillow==9.5.0
34
+ pyclipper==1.3.0.post6
35
+ pydantic==2.7.1
36
+ pydantic_core==2.18.2
37
+ Pygments==2.19.2
38
+ python-bidi==0.6.7
39
+ python-dotenv==1.0.1
40
+ python-multipart==0.0.20
41
+ PyYAML==6.0.3
42
+ rich==14.2.0
43
+ rich-toolkit==0.15.1
44
+ scikit-image==0.24.0
45
+ scipy==1.13.1
46
+ shapely==2.0.7
47
+ shellingham==1.5.4
48
+ sniffio==1.3.1
49
+ starlette==0.37.2
50
+ sympy==1.14.0
51
+ tifffile==2024.8.30
52
+ #torch==2.8.0
53
+ #torchvision==0.23.0
54
+ typer==0.20.0
55
+ typing_extensions==4.15.0
56
+ uvicorn==0.38.0
57
+ uvloop==0.22.1
58
+ watchfiles==1.1.1
59
+ websockets==15.0.1
backend/routes/auth.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ auth.py
3
+
4
+ This module defines authentication routes for handling user login and logout sessions
5
+ using FastAPI. It manages session creation and termination by storing session details
6
+ (name, email, session ID, start time, and end time) in a local SQLite database (`sessions.db`).
7
+ Each session is uniquely identified by a UUID.
8
+ """
9
+
10
+ from fastapi import APIRouter, Form
11
+ from pydantic import BaseModel
12
+ from datetime import datetime
13
+ import sqlite3
14
+ import uuid
15
+ import os
16
+
17
+
18
+ # Create a FastAPI router instance for handling authentication routes
19
+ router = APIRouter()
20
+
21
+
22
+
23
+
24
+ DB_PATH = os.environ.get("DB_PATH", "sessions.db")
25
+
26
+ def get_conn():
27
+ conn = sqlite3.connect(DB_PATH, check_same_thread=False)
28
+
29
+ conn.execute("""
30
+ CREATE TABLE IF NOT EXISTS sessions (
31
+ id TEXT PRIMARY KEY,
32
+ name TEXT,
33
+ email TEXT,
34
+ start_time TEXT,
35
+ end_time TEXT
36
+ );
37
+ """)
38
+
39
+ return conn
40
+
41
+
42
+ @router.post("/login")
43
+ def login(name: str = Form(...), email: str = Form(...)):
44
+
45
+ sid = str(uuid.uuid4())
46
+ with get_conn() as conn:
47
+ conn.execute(
48
+ "INSERT INTO sessions (id, name, email, start_time, end_time) VALUES (?, ?, ?, ?, ?)",
49
+ (sid, name, email, datetime.now().isoformat(), None)
50
+ )
51
+
52
+ return {"session_id": sid, "status": "ok"}
53
+
54
+
55
+ @router.post("/logout")
56
+ def logout(session_id: str = Form(...)):
57
+ with get_conn() as conn:
58
+ conn.execute(
59
+ "UPDATE sessions SET end_time = ? WHERE id = ? AND end_time is NULL",
60
+ (datetime.now().isoformat(), session_id)
61
+
62
+ )
63
+ return {"status": "ok"}
64
+
65
+
66
+
67
+
68
+
69
+
70
+
71
+
72
+ # def init_db():
73
+ # """
74
+ # Initializes the SQLite database and creates the `sessions` table if it does not already exist.
75
+ # The `sessions` table stores:
76
+ # - id (str): Unique identifier for the session (UUID)
77
+ # - name (str): Name of the user
78
+ # - email (str): Email of the user
79
+ # - start_time (str): ISO formatted string marking when the session started
80
+ # - end_time (str): ISO formatted string marking when the session ended (nullable)
81
+ # """
82
+ # conn = sqlite3.connect("sessions.db")
83
+ # cursor = conn.cursor()
84
+ # cursor.execute("""
85
+ # CREATE TABLE IF NOT EXISTS sessions (
86
+ # id TEXT PRIMARY KEY,
87
+ # name TEXT,
88
+ # email TEXT,
89
+ # start_time TEXT,
90
+ # end_time TEXT
91
+ # )
92
+ # """)
93
+ # conn.commit()
94
+ # conn.close()
95
+ #
96
+ #
97
+ # # Initialize the database on module load
98
+ # init_db()
99
+ #
100
+ #
101
+ # class LogoutRequest(BaseModel):
102
+ # """
103
+ # Request model for logging out a session.
104
+ # Expects:
105
+ # - session_id (str): The unique identifier of the session to be terminated.
106
+ # """
107
+ # session_id: str
108
+ #
109
+ #
110
+ # @router.post("/login")
111
+ # async def login(name: str = Form(...), email: str = Form(...)):
112
+ # """
113
+ # Handles user login.
114
+ #
115
+ # - Accepts user `name` and `email` as form data.
116
+ # - Generates a unique session ID using UUID.
117
+ # - Captures the session's start time in UTC (ISO format).
118
+ # - Stores the session details in the SQLite database (`sessions` table).
119
+ # - Returns the generated session ID and the session start time.
120
+ #
121
+ # This function essentially begins a new user session.
122
+ # """
123
+ # session_id = str(uuid.uuid4())
124
+ # start_time = datetime.utcnow().isoformat()
125
+ #
126
+ # conn = sqlite3.connect("sessions.db")
127
+ # cursor = conn.cursor()
128
+ # cursor.execute(
129
+ # "INSERT INTO sessions (id, name, email, start_time, end_time) VALUES (?, ?, ?, ?, ?)",
130
+ # (session_id, name, email, start_time, None),
131
+ # )
132
+ # conn.commit()
133
+ # conn.close()
134
+ #
135
+ # return {"session_id": session_id, "start_time": start_time}
136
+ #
137
+ #
138
+ # @router.post("/logout")
139
+ # async def logout(request: LogoutRequest):
140
+ # """
141
+ # Handles user logout.
142
+ #
143
+ # - Accepts a `LogoutRequest` object containing the session ID.
144
+ # - Records the current UTC time as the session's end time (ISO format).
145
+ # - Updates the corresponding session record in the database by setting its `end_time`.
146
+ # - Returns a confirmation message along with the recorded end time.
147
+ #
148
+ # This function effectively ends a user session.
149
+ # """
150
+ # end_time = datetime.utcnow().isoformat()
151
+ #
152
+ # conn = sqlite3.connect("sessions.db")
153
+ # cursor = conn.cursor()
154
+ # cursor.execute(
155
+ # "UPDATE sessions SET end_time = ? WHERE id = ?",
156
+ # (end_time, request.session_id),
157
+ # )
158
+ # conn.commit()
159
+ # conn.close()
160
+ #
161
+ # return {"message": "Session ended", "end_time": end_time}
backend/routes/detect.py ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ detect.py
3
+
4
+ This module defines image-processing routes and functions for detecting circles and text
5
+ from uploaded images using OpenCV and EasyOCR.
6
+
7
+ Key functionalities:
8
+ - Detect circular regions in an image and extract text inside/near them.
9
+ - Detect textual regions across the entire image (excluding numeric-only text and quotes).
10
+ - Provide a FastAPI endpoint (`POST /`) that accepts an image file and returns
11
+ detected circles with text plus extracted non-numeric text regions.
12
+ """
13
+
14
+ from fastapi import APIRouter, File, UploadFile
15
+ import cv2
16
+ import numpy as np
17
+ import easyocr
18
+ import re
19
+
20
+ # Initialize FastAPI router for detection-related endpoints
21
+ router = APIRouter()
22
+
23
+ # Initialize EasyOCR reader (supports English by default)
24
+ reader = easyocr.Reader(['en'])
25
+
26
+
27
+ def detect_circles_with_text_from_image_bytes(image_bytes):
28
+ """
29
+ Detects circular shapes in the given image and extracts text within/around each circle.
30
+
31
+ Steps:
32
+ 1. Convert image bytes into an OpenCV image.
33
+ 2. Convert to grayscale for circle detection.
34
+ 3. Use Hough Circle Transform to detect circles.
35
+ 4. For each detected circle:
36
+ - Crop the circular region with some padding.
37
+ - Perform OCR (EasyOCR) on the cropped region.
38
+ - Identify possible `page_number` (format: a<digits>.<digits>) and
39
+ `circle_text` (purely numeric).
40
+ - Collect raw texts recognized in that region.
41
+ 5. Return a structured list of circles with metadata.
42
+
43
+ Returns:
44
+ List of dictionaries containing:
45
+ - id (int): Circle index
46
+ - x, y (int): Circle center coordinates
47
+ - r (int): Circle radius
48
+ - page_number (str): Extracted page number if detected
49
+ - circle_text (str): Extracted numeric text if detected
50
+ - raw_texts (list): All OCR results from that circle region
51
+ """
52
+ try:
53
+ nparr = np.frombuffer(image_bytes, np.uint8)
54
+ img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
55
+
56
+ if img is None:
57
+ print("Failed to decode image")
58
+ return []
59
+
60
+ gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
61
+
62
+ # Detect circles using Hough Circle Transform
63
+ circles = cv2.HoughCircles(
64
+ gray,
65
+ cv2.HOUGH_GRADIENT,
66
+ dp=1.2,
67
+ minDist=20,
68
+ param1=50,
69
+ param2=100,
70
+ minRadius=50,
71
+ maxRadius=100
72
+ )
73
+
74
+ results = []
75
+ if circles is not None:
76
+ circles = np.round(circles[0, :]).astype("int")
77
+
78
+ for i, (x, y, r) in enumerate(circles):
79
+ # Crop region around circle with padding
80
+ top = max(y - r - 20, 0)
81
+ bottom = min(y + r + 20, img.shape[0])
82
+ left = max(x - r - 20, 0)
83
+ right = min(x + r + 20, img.shape[1])
84
+ crop = img[top:bottom, left:right]
85
+
86
+ try:
87
+ ocr_result = reader.readtext(crop)
88
+ texts = [res[1].strip() for res in ocr_result]
89
+ except Exception as e:
90
+ print(f"OCR error for circle {i}: {e}")
91
+ texts = []
92
+
93
+ # Extract structured info
94
+ page_number, circle_text = "", ""
95
+ for t in texts:
96
+ t_clean = t.strip()
97
+
98
+ if re.match(r"^a\d+\.\d+$", t_clean, re.IGNORECASE):
99
+ page_number = t_clean
100
+
101
+ elif re.match(r"^\d+$", t_clean):
102
+ circle_text = t_clean
103
+
104
+ results.append({
105
+ "id": i + 1,
106
+ "x": int(x),
107
+ "y": int(y),
108
+ "r": int(r),
109
+ "page_number": page_number,
110
+ "circle_text": circle_text,
111
+ "raw_texts": texts
112
+ })
113
+
114
+ return results
115
+
116
+ except Exception as e:
117
+ print(f"Circle detection error: {e}")
118
+ return []
119
+
120
+
121
+ def detect_text_from_image_bytes(image_bytes):
122
+ """
123
+ Detects text regions from the entire image, excluding numeric-only text and
124
+ strings with quotes.
125
+
126
+ Steps:
127
+ 1. Convert image bytes to an OpenCV image.
128
+ 2. Run EasyOCR to detect text with bounding boxes.
129
+ 3. Skip text if it:
130
+ - Contains quotes (single/double).
131
+ - Contains any digits.
132
+ 4. Collect bounding box coordinates and the cleaned text.
133
+
134
+ Returns:
135
+ List of dictionaries containing:
136
+ - id (int): Text index
137
+ - x1, y1, x2, y2 (int): Bounding box coordinates
138
+ - text (str): Extracted text string
139
+ """
140
+ try:
141
+ nparr = np.frombuffer(image_bytes, np.uint8)
142
+ img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
143
+
144
+ if img is None:
145
+ print("Failed to decode image for text detection")
146
+ return []
147
+
148
+ results = reader.readtext(img)
149
+ text_boxes = []
150
+
151
+ for i, (bbox, text, confidence) in enumerate(results):
152
+ # Skip text containing quotes or numbers
153
+ if "'" in text or '"' in text:
154
+ continue
155
+
156
+ if any(char.isdigit() for char in text):
157
+ continue
158
+
159
+ try:
160
+ # Extract bounding box coordinates
161
+ x_coords = [point[0] for point in bbox]
162
+ y_coords = [point[1] for point in bbox]
163
+
164
+ x1, x2 = int(min(x_coords)), int(max(x_coords))
165
+ y1, y2 = int(min(y_coords)), int(max(y_coords))
166
+
167
+ text_boxes.append({
168
+ "id": i + 1,
169
+ "x1": x1,
170
+ "y1": y1,
171
+ "x2": x2,
172
+ "y2": y2,
173
+ "text": text.strip()
174
+ })
175
+ except Exception as e:
176
+ print(f"Error processing text box {i}: {e}")
177
+ continue
178
+
179
+ return text_boxes
180
+
181
+ except Exception as e:
182
+ print(f"Text detection error: {e}")
183
+ return []
184
+
185
+
186
+ @router.post("/")
187
+ async def detect_circles(file: UploadFile = File(...)):
188
+ """
189
+ FastAPI endpoint to detect circles and text from an uploaded image.
190
+
191
+ Steps:
192
+ 1. Accepts an image file via POST request.
193
+ 2. Reads image bytes.
194
+ 3. Runs circle detection (with OCR inside circles).
195
+ 4. Runs general text detection across the entire image.
196
+ 5. Returns results as a JSON response containing:
197
+ - circles: List of detected circles with text info
198
+ - texts: List of detected text regions outside circles
199
+ """
200
+ try:
201
+ image_bytes = await file.read()
202
+ circles_with_text = detect_circles_with_text_from_image_bytes(image_bytes)
203
+ texts = detect_text_from_image_bytes(image_bytes)
204
+
205
+ return {"circles": circles_with_text, "texts": texts}
206
+
207
+ except Exception as e:
208
+ print(f"Detection endpoint error: {e}")
209
+ return {"error": str(e), "circles": [], "texts": []}
backend/routes/llm.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ llm.py
3
+
4
+ This module defines a FastAPI router for interacting with a Large Language Model (LLM)
5
+ using the Groq API.
6
+
7
+ Key functionalities:
8
+ - Accept user-provided text.
9
+ - Query an LLM (LLaMA-3.1-8b-instant) via Groq.
10
+ - Return a simplified explanation of the input text in less than 100 words.
11
+ """
12
+
13
+ from fastapi import APIRouter, HTTPException
14
+ from pydantic import BaseModel
15
+ from groq import Groq
16
+ import os
17
+ from dotenv import load_dotenv
18
+
19
+ # Load environment variables from .env file
20
+ load_dotenv()
21
+ api_key = os.getenv("GROQ_API_KEY")
22
+
23
+ # Initialize Groq client with API key
24
+ client = Groq(api_key=api_key)
25
+
26
+ # Initialize FastAPI router for LLM endpoints
27
+ router = APIRouter()
28
+
29
+
30
+ class LLMRequest(BaseModel):
31
+ """
32
+ Request model for generating simplified information from text.
33
+
34
+ Fields:
35
+ - content (str): The raw text content provided by the user.
36
+ """
37
+ content: str
38
+
39
+
40
+ def generate_info_from_llm(text: str) -> str:
41
+ """
42
+ Sends the given text to the Groq LLM (LLaMA-3.1-8b-instant) and requests a
43
+ simplified explanation.
44
+
45
+ Steps:
46
+ 1. Create a chat completion request with the model.
47
+ 2. Prompt the LLM to explain the input text in simple terms (under 100 words).
48
+ 3. Extract and return the model's response as a string.
49
+
50
+ Args:
51
+ text (str): The text to be explained.
52
+
53
+ Returns:
54
+ str: Simplified explanation of the input text.
55
+ """
56
+ completion = client.chat.completions.create(
57
+ model="llama-3.1-8b-instant",
58
+ messages=[
59
+ {
60
+ "role": "user",
61
+ "content": f"Explain in simple terms the meaning of the content {text} in less than 100 words"
62
+ }
63
+ ]
64
+ )
65
+ return completion.choices[0].message.content.strip()
66
+
67
+
68
+ @router.post("/generate_info")
69
+ async def generate_info_endpoint(request: LLMRequest):
70
+ """
71
+ FastAPI endpoint to generate simplified information from user text.
72
+
73
+ Steps:
74
+ 1. Accept a POST request with JSON containing `content`.
75
+ 2. Pass the content to the `generate_info_from_llm` function.
76
+ 3. Return the simplified explanation in JSON format.
77
+ """
78
+ try:
79
+ info = generate_info_from_llm(request.content)
80
+ return {"info": info}
81
+ except Exception as e:
82
+ raise HTTPException(status_code=500, detail=str(e))
backend/routes/regions_detect.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ region_detection.py
3
+
4
+ This module provides functionality to detect text inside a specific region
5
+ of an uploaded image using OpenCV and EasyOCR.
6
+
7
+ Key functionalities:
8
+ - Extract a specified rectangular region from an image.
9
+ - Perform OCR (Optical Character Recognition) on the cropped region.
10
+ - Return detected text boxes along with their coordinates.
11
+ - Return the cropped region as a base64-encoded image.
12
+ """
13
+
14
+ from fastapi import APIRouter, File, UploadFile, Form
15
+ import cv2
16
+ import numpy as np
17
+ import easyocr
18
+ import base64
19
+
20
+ # Initialize FastAPI router
21
+ router = APIRouter()
22
+
23
+ # Initialize EasyOCR reader (English language)
24
+ reader = easyocr.Reader(['en'])
25
+
26
+
27
+ def detect_text_in_region(img, region):
28
+ """
29
+ Detects text within a specified rectangular region of an image.
30
+
31
+ Steps:
32
+ 1. Crop the region of interest (ROI) from the original image.
33
+ 2. Run EasyOCR to detect text inside the cropped region.
34
+ 3. Adjust bounding box coordinates relative to the original image.
35
+ 4. Convert the cropped region to base64 for return.
36
+
37
+ Args:
38
+ img (numpy.ndarray): The original OpenCV image.
39
+ region (tuple): A tuple (x, y, w, h) specifying the top-left
40
+ coordinates, width, and height of the region.
41
+
42
+ Returns:
43
+ tuple:
44
+ - text_boxes (list of dict): Each dict contains:
45
+ - id (int): Box index
46
+ - x1, y1 (int): Top-left coordinates
47
+ - x2, y2 (int): Bottom-right coordinates
48
+ - text (str): Detected text
49
+ - crop_base64 (str): Base64-encoded cropped image.
50
+ """
51
+ x, y, w, h = region
52
+ crop = img[y:y+h, x:x+w]
53
+ results = reader.readtext(crop)
54
+ text_boxes = []
55
+
56
+ for i, (bbox, text, prob) in enumerate(results):
57
+ (top_left, _, bottom_right, _) = bbox
58
+ top_left = [int(top_left[0] + x), int(top_left[1] + y)]
59
+ bottom_right = [int(bottom_right[0] + x), int(bottom_right[1] + y)]
60
+ text_boxes.append({
61
+ "id": i+1,
62
+ "x1": top_left[0],
63
+ "y1": top_left[1],
64
+ "x2": bottom_right[0],
65
+ "y2": bottom_right[1],
66
+ "text": text
67
+ })
68
+
69
+ # Convert cropped region to base64 string
70
+ _, buffer = cv2.imencode(".jpg", crop)
71
+ crop_base64 = base64.b64encode(buffer).decode("utf-8")
72
+
73
+ return text_boxes, crop_base64
74
+
75
+
76
+ @router.post("/region-detect")
77
+ async def detect_in_region(
78
+ file: UploadFile = File(...),
79
+ x: int = Form(...),
80
+ y: int = Form(...),
81
+ w: int = Form(...),
82
+ h: int = Form(...)
83
+ ):
84
+ """
85
+ FastAPI endpoint to detect text within a user-specified region of an uploaded image.
86
+
87
+ Steps:
88
+ 1. Accepts an image file and region coordinates (x, y, w, h).
89
+ 2. Decodes the image into an OpenCV format.
90
+ 3. Calls `detect_text_in_region` to extract text and crop region.
91
+ 4. Returns:
92
+ - Detected text boxes with coordinates and recognized text.
93
+ - Cropped image region as a base64 string.
94
+
95
+
96
+ """
97
+ image_bytes = await file.read()
98
+ nparr = np.frombuffer(image_bytes, np.uint8)
99
+ img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
100
+
101
+ detections, crop_base64 = detect_text_in_region(img, (x, y, w, h))
102
+ return {
103
+ "detections": detections,
104
+ "cropped_image": f"data:image/jpeg;base64,{crop_base64}"
105
+ }
backend/sessions.db ADDED
Binary file (32.8 kB). View file