Spaces:
Sleeping
Sleeping
File size: 7,080 Bytes
ca4cce7 e565aa7 ca4cce7 e565aa7 ca4cce7 e565aa7 ca4cce7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 | from __future__ import annotations
import json
import logging
from threading import Thread
from pathlib import Path
from typing import Optional
from fastapi import FastAPI, HTTPException
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.gzip import GZipMiddleware
from pydantic import BaseModel, Field
from backend.ai_definition_service import AIDefinitionService
from backend.document_service import DocumentService
from backend.feedback_service import FeedbackService
from backend.glossary_service import GlossaryService
from backend.image_utils import decode_data_url, image_fingerprint, image_quality
from backend.insight_service import InsightService
from backend.ocr_service import OCRService
from backend.progress_service import ProgressService
from backend.vlm_service import VLMService
ROOT = Path(__file__).parent
DATA = ROOT / "data"
logging.basicConfig(level=logging.INFO)
class CacheControlledStaticFiles(StaticFiles):
def file_response(self, *args, **kwargs):
response = super().file_response(*args, **kwargs)
response.headers["Cache-Control"] = "public, max-age=86400"
return response
app = FastAPI(title="FalconScan", version="1.0.0", description="CPU-first customs terminology camera assistant")
app.add_middleware(GZipMiddleware, minimum_size=1000)
app.mount("/static", CacheControlledStaticFiles(directory=ROOT / "frontend"), name="static")
ocr = OCRService()
Thread(target=ocr.warmup, daemon=True, name="falconscan-ocr-warmup").start()
glossary = GlossaryService(DATA / "glossary.json", DATA / "user_corrections.json", DATA / "sme_approved_definitions.json")
feedback = FeedbackService(DATA / "user_corrections.json", DATA / "feedback_history.json", DATA / "sme_approved_definitions.json")
ai = AIDefinitionService()
vlm = VLMService()
progress = ProgressService(DATA / "project_progress.json")
documents = DocumentService(ocr, glossary)
insights = InsightService(glossary)
class FrameRequest(BaseModel):
image_base64: str
frame_width: int = Field(gt=0, le=10000)
frame_height: int = Field(gt=0, le=10000)
language_preference: str = Field(default="en", pattern="^(en|ar)$")
class FeedbackRequest(BaseModel):
term: str = Field(min_length=1, max_length=200)
old_definition: str = Field(default="", max_length=2000)
corrected_definition: Optional[str] = Field(default=None, max_length=2000)
feedback_type: str = Field(pattern="^(thumbs_up|thumbs_down)$")
suggested_by: str = Field(default="anonymous", max_length=100)
class ReviewRequest(BaseModel):
term: str
action: str = Field(pattern="^(approve|reject)$")
reviewer: str = "SME"
class VLMRequest(BaseModel):
image_base64: Optional[str] = None
user_requested: bool = True
class DocumentRequest(BaseModel):
file_base64: str
filename: str = Field(min_length=1, max_length=255)
language_preference: str = Field(default="en", pattern="^(en|ar)$")
class SelectionRequest(BaseModel):
text: str = Field(min_length=1, max_length=5000)
language_preference: str = Field(default="en", pattern="^(en|ar)$")
@app.get("/", include_in_schema=False)
def index():
return FileResponse(ROOT / "frontend" / "index.html")
@app.get("/health")
def health():
return {"status": "ok", "ocr": ocr.status(), "ai_enabled": ai.enabled,
"vlm_enabled": vlm.enabled, "storage": "local_json"}
@app.post("/analyze-frame")
def analyze_frame(request: FrameRequest):
try:
image = decode_data_url(request.image_base64)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
fingerprint = image_fingerprint(image)
cache_path = DATA / "ocr_cache.json"
cache = json.loads(cache_path.read_text(encoding="utf-8"))
cached = cache.get(fingerprint)
if cached:
return {**cached, "cached": True}
quality = image_quality(image)
try:
detections = ocr.extract(image, request.language_preference)
except RuntimeError as exc:
# Honest degraded mode: camera works and reports setup rather than fabricating OCR.
return {"detected_terms": [], "unknown_terms": [], "ocr_items": [], "cached": False,
"quality": quality, "ocr_available": False, "message": str(exc),
"suggest_vlm": False, "vlm_reasons": []}
terms = glossary.match_regions(detections)
if len(terms) < 3:
terms.extend(glossary.contextual_fallbacks(detections, 3 - len(terms), terms))
_, unknown = glossary.match_ocr(detections)
mean_confidence = sum(x["confidence"] for x in detections) / len(detections) if detections else 0.0
suggest_vlm, reasons = vlm.should_suggest(mean_confidence, len(unknown))
result = {"detected_terms": terms, "unknown_terms": unknown[:10], "ocr_items": detections,
"cached": False, "ocr_available": True, "quality": quality,
"mean_ocr_confidence": round(mean_confidence, 3), "suggest_vlm": suggest_vlm,
"vlm_reasons": reasons}
cache[fingerprint] = result
# Bounded local cache: images are never persisted.
if len(cache) > 100:
cache.pop(next(iter(cache)))
cache_path.write_text(json.dumps(cache, ensure_ascii=False, indent=2), encoding="utf-8")
return result
@app.post("/analyze-document")
def analyze_document(request: DocumentRequest):
try:
return documents.analyze(request.file_base64, request.filename, request.language_preference)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except RuntimeError as exc:
raise HTTPException(status_code=503, detail=str(exc)) from exc
@app.post("/explain-selection")
def explain_selection(request: SelectionRequest):
try:
return insights.explain(request.text, request.language_preference)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@app.get("/definition/{term:path}")
def get_definition(term: str, language: str = "en"):
result = glossary.definition(term)
return result or ai.define(term, language=language)
@app.post("/submit-feedback")
def submit_feedback(request: FeedbackRequest):
if request.feedback_type == "thumbs_down" and not request.corrected_definition:
raise HTTPException(status_code=400, detail="A corrected definition is required for thumbs down")
return feedback.submit(**request.model_dump())
@app.get("/admin/corrections")
def pending_corrections():
return {"items": feedback.pending()}
@app.post("/admin/review")
def review_correction(request: ReviewRequest):
try:
return feedback.review(request.term, request.action, request.reviewer)
except KeyError as exc:
raise HTTPException(status_code=404, detail="Correction not found") from exc
@app.post("/analyze-document-vlm")
def analyze_document_vlm(request: VLMRequest):
return vlm.analyze(request.user_requested)
@app.get("/progress")
def project_progress():
return progress.get()
|