Spaces:
Build error
Build error
| from __future__ import annotations | |
| import hashlib | |
| import json | |
| import time | |
| import uuid | |
| from pathlib import Path | |
| from fastapi import FastAPI, File, Request, UploadFile | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import HTMLResponse | |
| from fastapi.staticfiles import StaticFiles | |
| from pageparse.config import settings | |
| from pageparse.store import Store | |
| from pageparse.telemetry import get_telemetry | |
| app = FastAPI(title="PageParse") | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| HERE = Path(__file__).resolve().parent.parent.parent | |
| web_static = HERE / "web" / "static" | |
| web_templates = HERE / "web" / "templates" | |
| if not web_static.exists(): | |
| cwd_static = Path.cwd() / "web" / "static" | |
| if cwd_static.exists(): | |
| web_static = cwd_static | |
| if not web_templates.exists(): | |
| cwd_templates = Path.cwd() / "web" / "templates" | |
| if cwd_templates.exists(): | |
| web_templates = cwd_templates | |
| if web_static.exists(): | |
| app.mount("/static", StaticFiles(directory=str(web_static)), name="static") | |
| _store_instance: Store | None = None | |
| def _get_store() -> Store: | |
| global _store_instance | |
| if _store_instance is None: | |
| _store_instance = Store() | |
| _store_instance.init_db() | |
| return _store_instance | |
| def _get_ocr(): | |
| from pageparse.ocr.handwriting import HandwritingOCR | |
| return HandwritingOCR() | |
| def _get_printed_ocr(): | |
| from pageparse.ocr.printed import PrintedOCR | |
| return PrintedOCR() | |
| def _get_extractor(): | |
| from pageparse.extract import Extractor | |
| return Extractor() | |
| # Structured logging | |
| import structlog | |
| logger = structlog.get_logger() | |
| # Request tracing | |
| TRACING_ENABLED = settings.tracing_enabled | |
| # Prometheus metrics | |
| if settings.prometheus_enabled: | |
| try: | |
| from prometheus_client import Counter, Histogram, generate_latest, REGISTRY, CONTENT_TYPE_LATEST | |
| from starlette.responses import Response | |
| HTTP_REQUESTS = Counter("pageparse_http_requests_total", "Total HTTP requests", ["method", "endpoint", "status"]) | |
| HTTP_REQUEST_DURATION = Histogram("pageparse_http_request_duration_seconds", "HTTP request duration", ["method", "endpoint"]) | |
| PROCESSED_SOURCES = Counter("pageparse_processed_sources_total", "Total processed sources", ["source_type", "status"]) | |
| OCR_INFERENCES = Counter("pageparse_ocr_inferences_total", "Total OCR inferences", ["ocr_type"]) | |
| except ImportError: | |
| settings.prometheus_enabled = False | |
| async def add_tracing_and_metrics(request: Request, call_next): | |
| request_id = str(uuid.uuid4())[:8] | |
| request.state.request_id = request_id | |
| start_time = time.time() | |
| response = await call_next(request) | |
| duration = time.time() - start_time | |
| status_code = response.status_code | |
| if settings.prometheus_enabled: | |
| try: | |
| HTTP_REQUESTS.labels(method=request.method, endpoint=request.url.path, status=status_code).inc() | |
| HTTP_REQUEST_DURATION.labels(method=request.method, endpoint=request.url.path).observe(duration) | |
| except Exception: | |
| pass | |
| logger.info( | |
| "request", | |
| request_id=request_id, | |
| method=request.method, | |
| path=request.url.path, | |
| status=status_code, | |
| duration_ms=round(duration * 1000), | |
| ) | |
| response.headers["X-Request-ID"] = request_id | |
| return response | |
| if settings.prometheus_enabled: | |
| async def metrics(): | |
| return Response(content=generate_latest(REGISTRY), media_type=CONTENT_TYPE_LATEST) | |
| async def index() -> str: | |
| static_index = web_static / "index.html" | |
| if static_index.exists(): | |
| html = static_index.read_text(encoding="utf-8") | |
| btn = ( | |
| '<a href="/file-explorer" style="position:fixed;bottom:24px;right:24px;z-index:9999;' | |
| 'display:inline-flex;align-items:center;gap:8px;padding:12px 20px;' | |
| 'background:linear-gradient(135deg,#238636,#2ea043);color:#fff;' | |
| 'border-radius:12px;text-decoration:none;font-size:14px;font-weight:600;' | |
| 'box-shadow:0 4px 16px rgba(35,134,54,0.4);' | |
| 'transition:transform .2s,box-shadow .2s;"' | |
| 'onmouseover="this.style.transform=\'scale(1.05)\';this.style.boxShadow=\'0 6px 24px rgba(35,134,54,0.6)\'"' | |
| 'onmouseout="this.style.transform=\'scale(1)\';this.style.boxShadow=\'0 4px 16px rgba(35,134,54,0.4)\'">' | |
| '📊 DSA Visualizer</a>' | |
| ) | |
| close_tag = '</body>' | |
| if close_tag in html: | |
| return html.replace(close_tag, btn + close_tag) | |
| return html + btn | |
| index_path = web_templates / "index.html" | |
| if index_path.exists(): | |
| return index_path.read_text(encoding="utf-8") | |
| return "<html><body><h1>PageParse</h1></body></html>" | |
| async def upload( | |
| file: UploadFile = File(...), language: str = "English", schema_type: str = "auto" | |
| ) -> dict: | |
| import tempfile | |
| from pageparse import pipelines | |
| contents = await file.read() | |
| filename = file.filename or "upload" | |
| content_hash = hashlib.sha256(contents).hexdigest() | |
| store = _get_store() | |
| if settings.diff_sync_enabled: | |
| existing = store.get_source_by_hash(content_hash) | |
| if existing: | |
| records = store.get_records(source_id=existing["id"]) | |
| return { | |
| "source_id": existing["id"], | |
| "page_id": existing["id"], | |
| "message": "Duplicate content skipped (already ingested)", | |
| "duplicate": True, | |
| "raw_text": existing.get("raw_text", ""), | |
| "image_path": existing.get("image_path", ""), | |
| "cleaned_image_path": existing.get("cleaned_image_path", ""), | |
| "result": { | |
| "source_file": existing.get("filename", ""), | |
| "source_type": existing.get("source_type", ""), | |
| "captured_date": existing.get("captured_date", ""), | |
| "title": existing.get("title", ""), | |
| "summary": existing.get("summary", ""), | |
| "records": [ | |
| { | |
| "type": r.get("type", "task"), | |
| "content": r.get("content", ""), | |
| "due_date": r.get("due_date"), | |
| "priority": r.get("priority"), | |
| "category": r.get("category"), | |
| "speaker": r.get("speaker"), | |
| "timestamp": r.get("timestamp"), | |
| "status": r.get("status"), | |
| "confidence": r.get("confidence", 0.9), | |
| } | |
| for r in records | |
| ], | |
| }, | |
| } | |
| temp_dir = Path(tempfile.gettempdir()) | |
| temp_path = temp_dir / filename | |
| temp_path.write_bytes(contents) | |
| uploads_dir = web_static / "uploads" | |
| uploads_dir.mkdir(exist_ok=True, parents=True) | |
| timestamp = int(time.time()) | |
| original_filename = f"original_{timestamp}_{filename}" | |
| cleaned_filename = f"cleaned_{timestamp}_{filename}" | |
| original_path = uploads_dir / original_filename | |
| cleaned_path = uploads_dir / cleaned_filename | |
| original_path.write_bytes(contents) | |
| filename_lower = filename.lower() | |
| ext = Path(filename_lower).suffix | |
| image_exts = {".jpg", ".jpeg", ".png", ".bmp", ".tiff", ".tif", ".webp"} | |
| audio_exts = {".mp3", ".wav", ".m4a", ".ogg", ".flac"} | |
| video_exts = {".mp4", ".mkv", ".mov", ".avi"} | |
| doc_exts = {".pdf", ".docx", ".txt"} | |
| sheet_exts = {".csv", ".xlsx", ".xls"} | |
| source_type = "document" | |
| cleaned_image_url = None | |
| barcodes = [] | |
| tables = [] | |
| if ext in image_exts: | |
| source_type = "image" | |
| raw_text, image_url, cleaned_image_url, barcodes, tables = pipelines.process_image(temp_path, language) | |
| clean_temp = temp_dir / f"cleaned_{file.filename}" | |
| if clean_temp.exists(): | |
| cleaned_path.write_bytes(clean_temp.read_bytes()) | |
| if settings.prometheus_enabled: | |
| try: | |
| OCR_INFERENCES.labels(ocr_type="image").inc() | |
| except Exception: | |
| pass | |
| elif ext in audio_exts: | |
| source_type = "audio" | |
| raw_text, image_url = pipelines.process_audio(temp_path, language) | |
| if settings.prometheus_enabled: | |
| try: | |
| OCR_INFERENCES.labels(ocr_type="audio").inc() | |
| except Exception: | |
| pass | |
| elif ext in video_exts: | |
| source_type = "video" | |
| raw_text, image_url = pipelines.process_video(temp_path, language) | |
| elif ext in doc_exts: | |
| source_type = "document" | |
| raw_text, image_url = pipelines.process_document(temp_path, language) | |
| elif ext in sheet_exts: | |
| source_type = "spreadsheet" | |
| raw_text, image_url = pipelines.process_spreadsheet(temp_path, language) | |
| else: | |
| source_type = "document" | |
| raw_text, image_url = pipelines.process_document(temp_path, language) | |
| if settings.prometheus_enabled: | |
| try: | |
| PROCESSED_SOURCES.labels(source_type=source_type, status="processed").inc() | |
| except Exception: | |
| pass | |
| english_raw_text = raw_text | |
| has_non_ascii = not all(ord(c) < 128 for c in raw_text) | |
| if language != "English" and has_non_ascii: | |
| from pageparse.translation import translate_text | |
| try: | |
| english_raw_text = translate_text(raw_text, "English", source_lang=language) | |
| except Exception as e: | |
| print(f"Failed to translate native text to English for extraction: {e}") | |
| extractor = _get_extractor() | |
| if schema_type == "auto": | |
| schema_type = extractor.detect_schema_type(english_raw_text) | |
| result = extractor.extract(english_raw_text, file.filename, schema_type) | |
| result.source_type = source_type | |
| result.barcodes = barcodes | |
| result.tables = tables | |
| if language != "English": | |
| from pageparse.translation import translate_text | |
| for record in result.records: | |
| if record.content: | |
| record.content = translate_text(record.content, language, source_lang="English") | |
| if not has_non_ascii: | |
| raw_text = translate_text(raw_text, language) | |
| final_image_url = f"/static/uploads/{original_filename}" | |
| final_cleaned_image_url = ( | |
| f"/static/uploads/{cleaned_filename}" if cleaned_image_url or (ext in image_exts) else None | |
| ) | |
| source_id = store.save( | |
| result, | |
| raw_text, | |
| image_path=final_image_url, | |
| cleaned_image_path=final_cleaned_image_url, | |
| content_hash=content_hash, | |
| ) | |
| return { | |
| "source_id": source_id, | |
| "page_id": source_id, | |
| "result": result.model_dump(mode="json"), | |
| "raw_text": raw_text, | |
| "image_path": final_image_url, | |
| "cleaned_image_path": final_cleaned_image_url, | |
| "barcodes": barcodes, | |
| "tables": tables, | |
| "detected_schema": schema_type if schema_type != "todo" else None, | |
| } | |
| async def upload_batch(files: list[UploadFile] = File(...), language: str = "English", schema_type: str = "auto") -> list[dict]: | |
| results = [] | |
| for file in files: | |
| try: | |
| single = await upload(file, language, schema_type) | |
| results.append(single) | |
| except Exception as e: | |
| results.append({"filename": file.filename, "error": str(e)}) | |
| return results | |
| async def telemetry() -> dict: | |
| return get_telemetry() | |
| async def stats() -> dict: | |
| store = _get_store() | |
| return store.get_stats() | |
| async def get_airgap() -> dict: | |
| return {"airgap": settings.airgap} | |
| async def set_airgap(request: Request) -> dict: | |
| data = await request.json() | |
| val = data.get("airgap", False) | |
| settings.airgap = val | |
| return {"airgap": settings.airgap} | |
| async def translate(request: Request) -> dict: | |
| from pageparse.translation import translate_text | |
| data = await request.json() | |
| text = data.get("text", "") | |
| target_lang = data.get("target_lang", "English") | |
| source_lang = data.get("source_lang", "auto") | |
| translated_text = translate_text(text, target_lang, source_lang) | |
| return {"translated_text": translated_text} | |
| async def get_sources() -> list[dict]: | |
| return _get_store().list_sources() | |
| async def get_records(priority: str | None = None, type: str | None = None) -> list[dict]: | |
| return _get_store().get_records(priority=priority, type=type) | |
| async def patch_record(record_id: int, request: Request) -> dict: | |
| updates = await request.json() | |
| success = _get_store().update_record(record_id, updates) | |
| return {"success": success} | |
| async def delete_record(record_id: int) -> dict: | |
| success = _get_store().delete_record(record_id) | |
| return {"success": success} | |
| async def summarize_source(source_id: int) -> dict: | |
| store = _get_store() | |
| sources = store.list_sources() | |
| source = next((s for s in sources if s["id"] == source_id), None) | |
| if not source: | |
| return {"error": "Source not found"} | |
| raw_text = source.get("raw_text", "") | |
| if not raw_text.strip() or raw_text in ("[UNCLEAR]", "[inaudible]"): | |
| return {"summary": "No text available to summarize."} | |
| summary = _llm_summarize(raw_text) | |
| if not summary.strip(): | |
| summary = f"Summary of {source.get('filename')}: Contains extracted records and raw text." | |
| store.update_source_summary(source_id, summary) | |
| return {"summary": summary} | |
| async def text_to_speech(request: Request) -> dict: | |
| data = await request.json() | |
| text = data.get("text", "") | |
| if not text.strip(): | |
| return {"error": "No text provided"} | |
| try: | |
| import tempfile | |
| import pyttsx3 | |
| engine = pyttsx3.init() | |
| engine.setProperty("rate", 150) | |
| engine.setProperty("volume", 0.9) | |
| tts_dir = web_static / "tts" | |
| tts_dir.mkdir(exist_ok=True) | |
| tts_path = tts_dir / f"tts_{int(time.time())}.wav" | |
| engine.save_to_file(text, str(tts_path)) | |
| engine.runAndWait() | |
| return {"tts_url": f"/static/tts/{tts_path.name}"} | |
| except ImportError: | |
| return {"error": "TTS not available (pyttsx3 not installed)"} | |
| except Exception as e: | |
| return {"error": f"TTS failed: {e}"} | |
| async def set_webhook(request: Request) -> dict: | |
| data = await request.json() | |
| url = data.get("url", "") | |
| secret = data.get("secret", "") | |
| settings.webhook_url = url | |
| settings.webhook_secret = secret | |
| return {"webhook_url": url, "configured": bool(url)} | |
| async def get_webhook() -> dict: | |
| return { | |
| "webhook_url": settings.webhook_url or "", | |
| "configured": bool(settings.webhook_url), | |
| } | |
| async def analyze_code(request: Request) -> dict: | |
| from fastapi.responses import JSONResponse | |
| data = await request.json() | |
| code = data.get("code", "").strip() | |
| language_hint = data.get("language", "auto") | |
| if not code: | |
| return JSONResponse(status_code=400, content={"error": "No code provided"}) | |
| # Check Ollama connectivity first | |
| ollama_ok = _check_ollama_connection() | |
| if not ollama_ok: | |
| return JSONResponse( | |
| status_code=503, | |
| content={ | |
| "error": f"SLM (Ollama) not reachable at {settings.ollama_url}. Make sure Ollama is running and the model '{settings.slm_model}' is available.", | |
| "language": language_hint if language_hint != "auto" else "unknown", | |
| "explanation": "", | |
| "summary": "", | |
| }, | |
| ) | |
| lang = language_hint if language_hint != "auto" else _detect_language(code) | |
| prompt = ( | |
| "You are a code analysis assistant. Analyze the following code and provide:\n" | |
| "1. EXPLANATION: A clear explanation of what this code does (2-3 sentences)\n" | |
| "2. SUMMARY: A one-line summary of the code's purpose\n\n" | |
| f"Language: {lang}\n\n" | |
| f"Code:\n```{lang}\n{code}\n```\n\n" | |
| "Format your response as:\n" | |
| "EXPLANATION:\n<explanation>\n\nSUMMARY:\n<summary>" | |
| ) | |
| response = _llm_complete(prompt, system="You are a precise code analyst.", max_tokens=512) | |
| if not response: | |
| return JSONResponse( | |
| status_code=500, | |
| content={ | |
| "error": f"SLM returned empty response. Check that Ollama model '{settings.slm_model}' is downloaded and running at {settings.ollama_url}.", | |
| "language": lang, | |
| "explanation": "", | |
| "summary": "", | |
| }, | |
| ) | |
| explanation = "" | |
| summary = "" | |
| if "EXPLANATION:" in response and "SUMMARY:" in response: | |
| parts = response.split("SUMMARY:", 1) | |
| summary = parts[1].strip() | |
| expl_part = parts[0].replace("EXPLANATION:", "").strip() | |
| explanation = expl_part | |
| else: | |
| explanation = response | |
| summary = response.split(".")[0] + "." | |
| return { | |
| "language": lang, | |
| "explanation": explanation, | |
| "summary": summary, | |
| } | |
| def _check_ollama_connection() -> bool: | |
| import urllib.request | |
| try: | |
| req = urllib.request.Request(f"{settings.ollama_url}/api/tags") | |
| with urllib.request.urlopen(req, timeout=5) as resp: | |
| return resp.status == 200 | |
| except Exception: | |
| return False | |
| def _detect_language(code: str) -> str: | |
| import re | |
| code_stripped = code.strip() | |
| if not code_stripped: | |
| return "unknown" | |
| # Python | |
| if re.search(r'^\s*(import |from |def |class |print\(|if __name__)', code_stripped, re.MULTILINE): | |
| return "python" | |
| # JavaScript/TypeScript | |
| if re.search(r'^\s*(import |export |function |const |let |var |console\.log|document\.)', code_stripped, re.MULTILINE): | |
| if re.search(r':\s*(string|number|boolean|void|any)\s*[=;),]', code_stripped): | |
| return "typescript" | |
| return "javascript" | |
| # Java | |
| if re.search(r'^\s*(public |private |protected |class |import java\.)', code_stripped, re.MULTILINE): | |
| return "java" | |
| # C/C++ | |
| if re.search(r'^\s*(#include|int main|void main|#define)', code_stripped, re.MULTILINE): | |
| return "c" if re.search(r'printf\(|scanf\(', code_stripped) else "cpp" | |
| # Rust | |
| if re.search(r'^\s*(fn |let mut|use std|impl |pub )', code_stripped, re.MULTILINE): | |
| return "rust" | |
| # Go | |
| if re.search(r'^\s*(package main|func |import \()', code_stripped, re.MULTILINE): | |
| return "go" | |
| # HTML | |
| if re.search(r'^\s*<!DOCTYPE|<html|<div|<body|<head', code_stripped, re.MULTILINE): | |
| return "html" | |
| # CSS | |
| if re.search(r'^\s*[a-zA-Z-]+\s*\{', code_stripped, re.MULTILINE): | |
| return "css" | |
| # SQL | |
| if re.search(r'^\s*(SELECT|INSERT|UPDATE|DELETE|CREATE TABLE|ALTER|DROP)\s', code_stripped, re.MULTILINE): | |
| return "sql" | |
| # Ruby | |
| if re.search(r'^\s*(def |class |require |module |end\s*$)', code_stripped, re.MULTILINE): | |
| return "ruby" | |
| # Shell/Bash | |
| if re.search(r'^\s*(#!/bin/|#!|\. )', code_stripped, re.MULTILINE) or re.search(r'\b(if\s+\[|fi\s*$|then\s*$|esac\s*$)', code_stripped, re.MULTILINE): | |
| return "bash" | |
| return language_hint if language_hint != "auto" else "unknown" | |
| def _llm_complete(prompt: str, system: str = "", temperature: float = 0.3, max_tokens: int = 256) -> str: | |
| import urllib.request | |
| payload = { | |
| "model": settings.slm_model, | |
| "prompt": f"{system}\n\n{prompt}" if system else prompt, | |
| "stream": False, | |
| "options": {"temperature": temperature, "num_predict": max_tokens}, | |
| } | |
| url = f"{settings.ollama_url}/api/generate" | |
| try: | |
| data = json.dumps(payload).encode("utf-8") | |
| req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"}) | |
| with urllib.request.urlopen(req, timeout=90) as response: | |
| res = json.loads(response.read().decode("utf-8")) | |
| return res.get("response", "").strip() | |
| except Exception as e: | |
| print(f"Ollama request failed: {e}") | |
| return "" | |
| def _llm_summarize(text: str) -> str: | |
| prompt = ( | |
| "Summarize the following raw extracted text concisely in 2-3 sentences. " | |
| "Focus on the main topics and action items.\n\n" | |
| f"Text:\n{text}\n\n" | |
| "Summary:" | |
| ) | |
| return _llm_complete(prompt, system="You are a concise summarizer.") | |
| async def chat(request: Request) -> dict: | |
| data = await request.json() | |
| prompt = data.get("prompt", "") | |
| source_id = data.get("source_id", None) | |
| store = _get_store() | |
| records = [] | |
| try: | |
| if source_id: | |
| records = store.get_records(source_id=int(source_id)) | |
| else: | |
| from pageparse.search import SemanticSearch | |
| searcher = SemanticSearch() | |
| records = searcher.search(prompt, top_k=7) | |
| except Exception: | |
| all_recs = store.get_records(source_id=int(source_id) if source_id else None) | |
| keywords = prompt.lower().split() | |
| matched = [] | |
| for r in all_recs: | |
| if any( | |
| k in r["content"].lower() or (r["category"] and k in r["category"].lower()) | |
| for k in keywords | |
| ): | |
| matched.append(r) | |
| records = matched[:7] if matched else all_recs[:10] | |
| context = "\n".join( | |
| [ | |
| f"- [{r['type']}] {r['content']} (Due: {r['due_date']}, Priority: {r['priority']}, Category: {r['category']})" | |
| for r in records | |
| ] | |
| ) | |
| llm_prompt = ( | |
| "You are PageParse AI, a local notes and task assistant. " | |
| "Use the following user records context to answer the user's question. " | |
| "Answer concisely and clearly in English. If the information is not in the context, " | |
| "use your general knowledge but state clearly if it's not present in the user's records.\n\n" | |
| f"Context:\n{context}\n\n" | |
| f"User Question: {prompt}\n\n" | |
| "Response:" | |
| ) | |
| response_text = _llm_complete(llm_prompt, system="You are a helpful task assistant.", max_tokens=256) | |
| if not response_text: | |
| ollama_ok = _check_ollama_connection() | |
| if not ollama_ok: | |
| response_text = ( | |
| "The AI assistant requires Ollama to be running locally.\n\n" | |
| "To enable AI features:\n" | |
| "1. Install Ollama from https://ollama.com\n" | |
| "2. Pull a model: `ollama pull llama3.2:1b`\n" | |
| "3. Start Ollama and refresh this page\n\n" | |
| "Meanwhile, you can still upload files or explore DSA templates." | |
| ) | |
| elif context: | |
| response_text = f"Here is what I found in your uploaded files:\n\n{context}" | |
| else: | |
| response_text = ( | |
| "I don't have enough context from your files to answer that.\n\n" | |
| "Try:\n" | |
| "- Upload a file first and ask about it\n" | |
| "- Ask about the DSA visualizer templates\n" | |
| "- Check Ollama is running with a compatible model" | |
| ) | |
| return {"response": response_text} | |
| async def ollama_status() -> dict: | |
| import urllib.request | |
| url = f"{settings.ollama_url}/api/tags" | |
| connected = False | |
| models: list[str] = [] | |
| model_available = False | |
| version = "" | |
| try: | |
| req = urllib.request.Request(url) | |
| with urllib.request.urlopen(req, timeout=5) as response: | |
| data = json.loads(response.read().decode("utf-8")) | |
| models = [m["name"] for m in data.get("models", [])] | |
| connected = True | |
| model_available = settings.slm_model in models | |
| except Exception: | |
| pass | |
| # Also try getting version | |
| try: | |
| ver_req = urllib.request.Request(f"{settings.ollama_url}/api/version") | |
| with urllib.request.urlopen(ver_req, timeout=3) as response: | |
| ver_data = json.loads(response.read().decode("utf-8")) | |
| version = ver_data.get("version", "") | |
| except Exception: | |
| pass | |
| return { | |
| "connected": connected, | |
| "url": settings.ollama_url, | |
| "model": settings.slm_model, | |
| "models": models, | |
| "model_available": model_available, | |
| "version": version, | |
| } | |
| async def ollama_configure(request: Request) -> dict: | |
| data = await request.json() | |
| url = data.get("url", "").strip() | |
| model = data.get("model", "").strip() | |
| if url: | |
| settings.ollama_url = url.rstrip("/") | |
| if model: | |
| settings.slm_model = model | |
| return {"url": settings.ollama_url, "model": settings.slm_model} | |
| CODE_EXTENSIONS = { | |
| ".py", ".js", ".ts", ".jsx", ".tsx", ".html", ".css", ".scss", ".less", | |
| ".c", ".cpp", ".cc", ".cxx", ".h", ".hpp", ".java", ".kt", ".scala", | |
| ".rs", ".go", ".rb", ".php", ".swift", ".m", ".mm", | |
| ".sql", ".r", ".m", ".sh", ".bash", ".zsh", ".ps1", | |
| ".yaml", ".yml", ".json", ".xml", ".toml", ".ini", ".cfg", | |
| ".md", ".rst", ".tex", ".txt", | |
| ".vue", ".svelte", ".lua", ".pl", ".pm", ".hs", ".ex", ".exs", | |
| } | |
| IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".bmp", ".tiff", ".tif", ".webp", ".gif", ".svg"} | |
| AUDIO_EXTS = {".mp3", ".wav", ".m4a", ".ogg", ".flac", ".aac", ".wma"} | |
| VIDEO_EXTS = {".mp4", ".mkv", ".mov", ".avi", ".webm", ".flv", ".wmv"} | |
| DOC_EXTS = {".pdf", ".docx", ".txt"} | |
| SHEET_EXTS = {".csv", ".xlsx", ".xls"} | |
| async def analyze_file(file: UploadFile = File(...)) -> dict: | |
| import tempfile | |
| contents = await file.read() | |
| filename = file.filename or "upload" | |
| ext = Path(filename).suffix.lower() | |
| size_kb = len(contents) / 1024 | |
| result = { | |
| "filename": filename, | |
| "extension": ext, | |
| "size_kb": round(size_kb, 2), | |
| "file_type": "unknown", | |
| "content_preview": "", | |
| "explanation": "", | |
| "summary": "", | |
| "step_by_step": [], | |
| } | |
| if ext in CODE_EXTENSIONS: | |
| result["file_type"] = "code" | |
| try: | |
| code_text = contents.decode("utf-8") | |
| except UnicodeDecodeError: | |
| try: | |
| code_text = contents.decode("latin-1") | |
| except Exception: | |
| code_text = "[Binary or non-text content]" | |
| preview_lines = code_text.split("\n")[:50] | |
| result["content_preview"] = "\n".join(preview_lines) | |
| result["total_lines"] = code_text.count("\n") + 1 | |
| lang = ext.lstrip(".") | |
| lang_map = { | |
| "py": "python", "js": "javascript", "ts": "typescript", | |
| "html": "html", "css": "css", "c": "c", "cpp": "cpp", | |
| "java": "java", "rs": "rust", "go": "go", "rb": "ruby", | |
| "kt": "kotlin", "swift": "swift", "sh": "bash", "bash": "bash", | |
| "php": "php", "sql": "sql", "r": "r", "lua": "lua", | |
| "vue": "vue", "svelte": "svelte", "scala": "scala", | |
| } | |
| language = lang_map.get(lang, lang) | |
| from pageparse import dsa_visualizer | |
| analysis = dsa_visualizer.analyze_code(code_text, language) | |
| result["explanation"] = analysis.get("explanation", "") | |
| result["summary"] = analysis.get("summary", "") | |
| result["dsa_type"] = analysis.get("dsa_type", "general") | |
| result["time_complexity"] = analysis.get("time_complexity", "") | |
| result["space_complexity"] = analysis.get("space_complexity", "") | |
| step_by_step = dsa_visualizer.explain_code_step_by_step(code_text, language) | |
| raw_steps = step_by_step.get("explanation", "").split("\n") | |
| result["step_by_step"] = [s.strip() for s in raw_steps if s.strip()] | |
| if not result["explanation"]: | |
| lines = code_text.split("\n") | |
| result["explanation"] = f"This {language} file has {result['total_lines']} lines. " | |
| result["step_by_step"] = [f"Line {i+1}: {lines[i][:100]}" for i in range(min(20, len(lines)))] | |
| elif ext in IMAGE_EXTS: | |
| result["file_type"] = "image" | |
| import base64 | |
| b64 = base64.b64encode(contents).decode("utf-8") | |
| mime_key = {"jpg": "jpeg", "jpeg": "jpeg"}.get(ext.lstrip("."), ext.lstrip(".")) | |
| result["image_data_url"] = f"data:image/{mime_key};base64,{b64}" | |
| result["mime"] = f"image/{mime_key}" | |
| temp_dir = Path(tempfile.gettempdir()) | |
| temp_path = temp_dir / filename | |
| temp_path.write_bytes(contents) | |
| try: | |
| from PIL import Image as PILImage | |
| pil_img = PILImage.open(temp_path) | |
| img_w, img_h = pil_img.size | |
| img_mode = pil_img.mode | |
| img_format = pil_img.format or ext.lstrip(".").upper() | |
| meta_desc = f"Image dimensions: {img_w}x{img_h} pixels, Format: {img_format}, Color mode: {img_mode}, File size: {size_kb:.1f} KB" | |
| result["image_metadata"] = {"width": img_w, "height": img_h, "format": img_format, "mode": img_mode} | |
| result["content_preview"] = f"[{img_format} Image: {img_w}x{img_h}, {size_kb:.1f} KB]" | |
| from pageparse import pipelines | |
| raw_text, image_url, cleaned_url, barcodes, tables = pipelines.process_image(temp_path) | |
| text = raw_text.strip() | |
| if text and text not in ("[UNCLEAR]", "[inaudible]", ""): | |
| result["explanation"] = text | |
| result["barcodes"] = barcodes | |
| result["tables"] = tables | |
| result["step_by_step"] = text.split("\n")[:30] | |
| sm = _llm_summarize(text) | |
| result["summary"] = sm if sm else f"Extracted {len(text)} characters of text from this image." | |
| else: | |
| result["explanation"] = f"This is a {img_format} image ({img_w}x{img_h} pixels, {img_mode} mode, {size_kb:.1f} KB). No text was detected in the image." | |
| result["summary"] = f"{img_format} image — {img_w}x{img_h}px, {size_kb:.1f}KB" | |
| except Exception as e: | |
| result["explanation"] = f"Image file: {filename} ({ext}, {size_kb:.1f} KB). Details: {e}" | |
| result["summary"] = f"Image: {filename}" | |
| result["content_preview"] = f"[Image: {filename}, {size_kb:.1f} KB]" | |
| elif ext in AUDIO_EXTS: | |
| result["file_type"] = "audio" | |
| temp_dir = Path(tempfile.gettempdir()) | |
| temp_path = temp_dir / filename | |
| temp_path.write_bytes(contents) | |
| audio_info = f"Audio file: {filename} ({ext}, {size_kb:.1f} KB)" | |
| try: | |
| from pageparse import pipelines | |
| transcript, audio_url = pipelines.process_audio(temp_path) | |
| text = transcript.strip() | |
| if text and text not in ("[UNCLEAR]", "[inaudible]", ""): | |
| result["content_preview"] = text[:2000] | |
| result["explanation"] = text | |
| result["step_by_step"] = [s.strip() + "." for s in text.replace("?", ".").replace("!", ".").split(".") if len(s.strip()) > 20][:30] | |
| sm = _llm_summarize(text) | |
| result["summary"] = sm if sm else f"Transcribed {len(text)} characters from audio." | |
| else: | |
| result["content_preview"] = audio_info | |
| result["explanation"] = f"{audio_info}. The audio was processed but no speech could be transcribed (may be silence, music, or an unsupported format)." | |
| result["summary"] = f"Audio — no speech detected" | |
| except Exception as e: | |
| result["content_preview"] = audio_info | |
| result["explanation"] = f"{audio_info}. Could not transcribe: {e}" | |
| result["summary"] = f"Audio: {filename}" | |
| elif ext in VIDEO_EXTS: | |
| result["file_type"] = "video" | |
| temp_dir = Path(tempfile.gettempdir()) | |
| temp_path = temp_dir / filename | |
| temp_path.write_bytes(contents) | |
| video_info = f"Video file: {filename} ({ext}, {size_kb:.1f} KB)" | |
| try: | |
| import cv2 | |
| cap = cv2.VideoCapture(str(temp_path)) | |
| v_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) | |
| v_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) | |
| v_fps = cap.get(cv2.CAP_PROP_FPS) | |
| v_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) | |
| v_duration = v_frames / v_fps if v_fps > 0 else 0 | |
| v_duration_str = f"{int(v_duration // 60)}m {int(v_duration % 60)}s" if v_duration > 0 else "unknown" | |
| cap.release() | |
| video_info = f"Video: {v_width}x{v_height}, {v_fps:.1f} fps, {v_duration_str}, {size_kb:.1f} KB" | |
| result["video_metadata"] = {"width": v_width, "height": v_height, "fps": round(v_fps, 1), "frames": v_frames, "duration_sec": round(v_duration, 1)} | |
| from pageparse import pipelines | |
| combined_text, video_url = pipelines.process_video(temp_path) | |
| text = combined_text.strip() | |
| if text and text not in ("[UNCLEAR]", "[inaudible]", ""): | |
| result["content_preview"] = text[:2000] | |
| result["explanation"] = text | |
| result["step_by_step"] = text.split("\n\n")[:30] | |
| sm = _llm_summarize(text) | |
| result["summary"] = sm if sm else f"Extracted {len(text)} characters from video." | |
| else: | |
| result["content_preview"] = video_info | |
| result["explanation"] = f"This video ({v_width}x{v_height}, {v_fps:.1f} fps, {v_duration_str}, {size_kb:.1f} KB). No text or speech could be extracted." | |
| result["summary"] = f"Video — {v_width}x{v_height}, {v_duration_str}" | |
| except Exception as e: | |
| result["content_preview"] = video_info | |
| result["explanation"] = f"{video_info}. Could not process: {e}" | |
| result["summary"] = f"Video: {filename}" | |
| elif ext in DOC_EXTS: | |
| result["file_type"] = "document" | |
| try: | |
| text_content = contents.decode("utf-8") | |
| except Exception: | |
| text_content = "" | |
| result["content_preview"] = text_content[:2000] if text_content else f"[Document: {filename}]" | |
| result["explanation"] = f"This is a document file ({ext}). " | |
| result["summary"] = f"Document: {filename}" | |
| if text_content: | |
| result["step_by_step"] = text_content.split("\n")[:30] | |
| elif ext in SHEET_EXTS: | |
| result["file_type"] = "spreadsheet" | |
| result["content_preview"] = f"[Spreadsheet file: {filename}, {size_kb:.1f} KB]" | |
| result["explanation"] = f"This is a spreadsheet file ({ext}). Upload to /upload for data extraction." | |
| result["summary"] = f"Spreadsheet: {filename}" | |
| else: | |
| result["file_type"] = "binary" if ext else "unknown" | |
| result["content_preview"] = f"[{size_kb:.1f} KB binary file]" | |
| result["explanation"] = f"File type '{ext}' is not recognized. Try uploading via the standard /upload endpoint." | |
| result["summary"] = f"Unknown file type: {filename}" | |
| return result | |
| async def dsa_analyze_code(request: Request) -> dict: | |
| from fastapi.responses import JSONResponse | |
| data = await request.json() | |
| code = data.get("code", "").strip() | |
| language = data.get("language", "auto") | |
| if not code: | |
| return JSONResponse(status_code=400, content={"error": "No code provided"}) | |
| from pageparse import dsa_visualizer | |
| result = dsa_visualizer.analyze_code(code, language) | |
| return result | |
| async def dsa_explain_code(request: Request) -> dict: | |
| from fastapi.responses import JSONResponse | |
| data = await request.json() | |
| code = data.get("code", "").strip() | |
| language = data.get("language", "auto") | |
| if not code: | |
| return JSONResponse(status_code=400, content={"error": "No code provided"}) | |
| from pageparse import dsa_visualizer | |
| result = dsa_visualizer.explain_code_step_by_step(code, language) | |
| return result | |
| async def dsa_visualize(request: Request) -> dict: | |
| from fastapi.responses import JSONResponse | |
| data = await request.json() | |
| code = data.get("code", "").strip() | |
| language = data.get("language", "auto") | |
| input_data = data.get("input_data", None) | |
| if not code: | |
| return JSONResponse(status_code=400, content={"error": "No code provided"}) | |
| from pageparse import dsa_visualizer | |
| result = dsa_visualizer.analyze_code(code, language) | |
| if input_data and isinstance(input_data, list): | |
| dsa_type = result.get("dsa_type", "") | |
| if dsa_type in ("bubble_sort", "selection_sort", "insertion_sort"): | |
| result["steps"] = dsa_visualizer.generate_sorting_steps(input_data, dsa_type) | |
| elif dsa_type == "binary_tree": | |
| result["steps"] = dsa_visualizer.generate_bst_steps(input_data) | |
| elif dsa_type == "linked_list": | |
| result["steps"] = dsa_visualizer.generate_linked_list_steps(input_data) | |
| return result | |
| async def dsa_templates() -> dict: | |
| from pageparse import dsa_visualizer | |
| return {"templates": dsa_visualizer.list_templates()} | |
| async def dsa_visualize_template(request: Request) -> dict: | |
| from fastapi.responses import JSONResponse | |
| data = await request.json() | |
| template_name = data.get("template", "").strip() | |
| input_data = data.get("input_data", None) | |
| from pageparse import dsa_visualizer | |
| template = dsa_visualizer.get_template(template_name) | |
| if not template: | |
| return JSONResponse(status_code=404, content={"error": f"Template '{template_name}' not found"}) | |
| result = dsa_visualizer.analyze_code(template["code"], "python") | |
| if input_data and isinstance(input_data, list): | |
| dsa_type = result.get("dsa_type", "") | |
| if dsa_type in ("bubble_sort", "selection_sort", "insertion_sort"): | |
| result["steps"] = dsa_visualizer.generate_sorting_steps(input_data, dsa_type) | |
| elif dsa_type == "binary_tree": | |
| result["steps"] = dsa_visualizer.generate_bst_steps(input_data) | |
| elif dsa_type == "linked_list": | |
| result["steps"] = dsa_visualizer.generate_linked_list_steps(input_data) | |
| return result | |
| async def file_explorer_page() -> HTMLResponse: | |
| fe_path = web_templates / "file_explorer.html" | |
| if fe_path.exists(): | |
| return HTMLResponse(content=fe_path.read_text(encoding="utf-8")) | |
| return HTMLResponse(content="<html><body><h1>Page not found</h1></body></html>") | |