Ryadg commited on
Commit
22e60a6
Β·
1 Parent(s): b10f33b

feat: switch to Docker SDK with custom FastAPI frontend at /

Browse files
Files changed (5) hide show
  1. Dockerfile +17 -0
  2. README.md +2 -5
  3. app.py +0 -81
  4. main.py +128 -0
  5. requirements.txt +1 -0
Dockerfile ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.12-slim
2
+
3
+ WORKDIR /app
4
+
5
+ RUN apt-get update && apt-get install -y git curl && rm -rf /var/lib/apt/lists/*
6
+
7
+ COPY requirements.txt .
8
+ RUN pip install --no-cache-dir -r requirements.txt
9
+
10
+ COPY . .
11
+
12
+ RUN useradd -m -u 1000 user && chown -R user /app
13
+ USER user
14
+
15
+ EXPOSE 7860
16
+
17
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "1"]
README.md CHANGED
@@ -1,12 +1,9 @@
1
  ---
2
  title: PaperProf
3
- emoji: 😻 cat
4
  colorFrom: purple
5
  colorTo: blue
6
- sdk: gradio
7
- sdk_version: 6.16.0
8
- python_version: '3.12'
9
- app_file: app.py
10
  pinned: false
11
  ---
12
 
 
1
  ---
2
  title: PaperProf
3
+ emoji: πŸ“„
4
  colorFrom: purple
5
  colorTo: blue
6
+ sdk: docker
 
 
 
7
  pinned: false
8
  ---
9
 
app.py CHANGED
@@ -236,86 +236,5 @@ with gr.Blocks(title="PaperProf") as demo:
236
  outputs=[feedback_box, correct_state, total_state, score_box],
237
  )
238
 
239
- # ---------------------------------------------------------------------------
240
- # Custom frontend & stateless API β€” mounted on Gradio's internal FastAPI app
241
- # (demo.app is the FastAPI instance created when the Blocks context exits)
242
- # ---------------------------------------------------------------------------
243
-
244
- import os as _os
245
- import pathlib as _pathlib
246
- import tempfile as _tempfile
247
- from fastapi import File as _File, HTTPException as _HTTPException, UploadFile as _UploadFile
248
- from fastapi.responses import HTMLResponse as _HTMLResponse
249
- from pydantic import BaseModel as _BaseModel
250
-
251
-
252
- @demo.app.get("/paperprof-ui", response_class=_HTMLResponse)
253
- async def _serve_ui():
254
- return (_pathlib.Path(__file__).parent / "ui" / "index.html").read_text(encoding="utf-8")
255
-
256
-
257
- @demo.app.post("/api/load")
258
- async def _api_load(file: _UploadFile = _File(...)):
259
- if not (file.filename or "").lower().endswith(".pdf"):
260
- raise _HTTPException(400, "Only PDF files are supported.")
261
- content = await file.read()
262
- with _tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp:
263
- tmp.write(content)
264
- tmp_path = tmp.name
265
- try:
266
- text = extract_text(tmp_path)
267
- chunks = chunk_text(text)
268
- if not chunks:
269
- raise _HTTPException(400, "No text found in PDF (scanned or too short?).")
270
- return {"chunks": chunks, "count": len(chunks)}
271
- except _HTTPException:
272
- raise
273
- except ValueError as exc:
274
- raise _HTTPException(400, str(exc))
275
- except Exception as exc:
276
- raise _HTTPException(500, f"Unexpected error: {exc}")
277
- finally:
278
- _os.unlink(tmp_path)
279
-
280
-
281
- class _QuestionReq(_BaseModel):
282
- chunk: str
283
- language: str = "English"
284
- difficulty: str = "Normal"
285
-
286
-
287
- @spaces.GPU(duration=60)
288
- def _api_gen_question(chunk: str, language: str, difficulty: str) -> str:
289
- return generate_question(chunk, language=language, difficulty=difficulty)
290
-
291
-
292
- @demo.app.post("/api/question")
293
- def _api_question(req: _QuestionReq):
294
- try:
295
- return {"question": _api_gen_question(req.chunk, req.language, req.difficulty)}
296
- except Exception as exc:
297
- raise _HTTPException(500, str(exc))
298
-
299
-
300
- class _EvalReq(_BaseModel):
301
- question: str
302
- chunk: str
303
- answer: str
304
- language: str = "English"
305
-
306
-
307
- @spaces.GPU(duration=120)
308
- def _api_eval_answer(question: str, chunk: str, answer: str, language: str) -> str:
309
- return evaluate_answer(question, chunk, answer, language=language)
310
-
311
-
312
- @demo.app.post("/api/evaluate")
313
- def _api_evaluate(req: _EvalReq):
314
- try:
315
- return {"feedback": _api_eval_answer(req.question, req.chunk, req.answer, req.language)}
316
- except Exception as exc:
317
- raise _HTTPException(500, str(exc))
318
-
319
-
320
  if __name__ == "__main__":
321
  pass
 
236
  outputs=[feedback_box, correct_state, total_state, score_box],
237
  )
238
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  if __name__ == "__main__":
240
  pass
main.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ main.py β€” FastAPI entry point for PaperProf (Docker / HuggingFace Spaces).
3
+
4
+ Routes:
5
+ GET / β†’ custom HTML frontend
6
+ POST /api/load β†’ parse PDF, return chunks (CPU)
7
+ POST /api/question β†’ generate question from chunk (GPU via @spaces.GPU)
8
+ POST /api/evaluate β†’ evaluate student answer (GPU via @spaces.GPU)
9
+ * /gradio/* β†’ Gradio interface as fallback
10
+ """
11
+
12
+ import os
13
+ import pathlib
14
+ import tempfile
15
+
16
+ import gradio as gr
17
+ from fastapi import FastAPI, File, HTTPException, Request, UploadFile
18
+ from fastapi.responses import HTMLResponse
19
+
20
+ from app import demo
21
+ from core.parser import extract_text
22
+ from core.chunker import chunk_text
23
+ from core.questioner import generate_question
24
+ from core.evaluator import evaluate_answer
25
+
26
+ try:
27
+ import spaces
28
+ except ImportError:
29
+ class spaces: # noqa: N801 β€” local dev fallback
30
+ @staticmethod
31
+ def GPU(duration=60):
32
+ def wrap(fn): return fn
33
+ return wrap
34
+
35
+ # ─────────────────────────────────────────────────────────────────────────────
36
+ # FastAPI app
37
+ # ─────────────────────────────────────────────────────────────────────────────
38
+
39
+ fastapi_app = FastAPI(title="PaperProf", docs_url=None, redoc_url=None)
40
+
41
+
42
+ @fastapi_app.get("/", response_class=HTMLResponse)
43
+ async def root():
44
+ return pathlib.Path("ui/index.html").read_text(encoding="utf-8")
45
+
46
+
47
+ # ─────────────────────────────────────────────────────────────────────────────
48
+ # /api/load β€” parse uploaded PDF into text chunks (no GPU needed)
49
+ # ─────────────────────────────────────────────────────────────────────────────
50
+
51
+ @fastapi_app.post("/api/load")
52
+ async def api_load(file: UploadFile = File(...)):
53
+ if not (file.filename or "").lower().endswith(".pdf"):
54
+ raise HTTPException(400, "Only PDF files are supported.")
55
+ content = await file.read()
56
+ with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp:
57
+ tmp.write(content)
58
+ tmp_path = tmp.name
59
+ try:
60
+ text = extract_text(tmp_path)
61
+ chunks = chunk_text(text)
62
+ if not chunks:
63
+ raise HTTPException(400, "No text found in PDF (scanned or too short?).")
64
+ return {"chunks": chunks, "count": len(chunks)}
65
+ except HTTPException:
66
+ raise
67
+ except ValueError as exc:
68
+ raise HTTPException(400, str(exc))
69
+ except Exception as exc:
70
+ raise HTTPException(500, f"Unexpected error: {exc}")
71
+ finally:
72
+ os.unlink(tmp_path)
73
+
74
+
75
+ # ─────────────────────────────────────────────────────────────────────────────
76
+ # /api/question β€” generate a study question from a chunk (GPU)
77
+ # ─────────────────────────────────────────────────────────────────────────────
78
+
79
+ @spaces.GPU(duration=60)
80
+ def _gen_question(chunk: str, language: str, difficulty: str) -> str:
81
+ return generate_question(chunk, language, difficulty)
82
+
83
+
84
+ @fastapi_app.post("/api/question")
85
+ async def api_question(request: Request):
86
+ body = await request.json()
87
+ chunk = body.get("chunk", "")
88
+ language = body.get("language", "English")
89
+ difficulty = body.get("difficulty", "Normal")
90
+ if not chunk:
91
+ raise HTTPException(400, "chunk is required.")
92
+ try:
93
+ question = _gen_question(chunk, language, difficulty)
94
+ return {"question": question}
95
+ except Exception as exc:
96
+ raise HTTPException(500, str(exc))
97
+
98
+
99
+ # ─────────────────────────────────────────────────────────────────────────────
100
+ # /api/evaluate β€” evaluate student answer against source chunk (GPU)
101
+ # ─────────────────────────────────────────────────────────────────────────────
102
+
103
+ @spaces.GPU(duration=120)
104
+ def _eval_answer(question: str, chunk: str, answer: str, language: str) -> str:
105
+ return evaluate_answer(question, chunk, answer, language)
106
+
107
+
108
+ @fastapi_app.post("/api/evaluate")
109
+ async def api_evaluate(request: Request):
110
+ body = await request.json()
111
+ question = body.get("question", "")
112
+ chunk = body.get("chunk", "")
113
+ answer = body.get("answer", "")
114
+ language = body.get("language", "English")
115
+ if not (question and chunk and answer):
116
+ raise HTTPException(400, "question, chunk, and answer are required.")
117
+ try:
118
+ feedback = _eval_answer(question, chunk, answer, language)
119
+ return {"feedback": feedback}
120
+ except Exception as exc:
121
+ raise HTTPException(500, str(exc))
122
+
123
+
124
+ # ─────────────────────────────────────────────────────────────────────────────
125
+ # Mount Gradio at /gradio as fallback and export the combined ASGI app
126
+ # ─────────────────────────────────────────────────────────────────────────────
127
+
128
+ app = gr.mount_gradio_app(fastapi_app, demo, path="/gradio")
requirements.txt CHANGED
@@ -5,6 +5,7 @@ gradio>=4.36.0
5
  fastapi>=0.100.0
6
  uvicorn>=0.23.0
7
  python-multipart>=0.0.6
 
8
 
9
  # PDF extraction
10
  PyMuPDF>=1.24.0
 
5
  fastapi>=0.100.0
6
  uvicorn>=0.23.0
7
  python-multipart>=0.0.6
8
+ spaces>=0.50.0
9
 
10
  # PDF extraction
11
  PyMuPDF>=1.24.0