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 @app.middleware("http") 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: @app.get("/metrics") async def metrics(): return Response(content=generate_latest(REGISTRY), media_type=CONTENT_TYPE_LATEST) @app.get("/", response_class=HTMLResponse) async def index() -> str: static_index = web_static / "index.html" if static_index.exists(): html = static_index.read_text(encoding="utf-8") btn = ( '' '📊 DSA Visualizer' ) close_tag = '' 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 "

PageParse

" @app.post("/upload") 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, } @app.post("/upload/batch") 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 @app.get("/telemetry") async def telemetry() -> dict: return get_telemetry() @app.get("/stats") async def stats() -> dict: store = _get_store() return store.get_stats() @app.get("/airgap") async def get_airgap() -> dict: return {"airgap": settings.airgap} @app.post("/airgap") async def set_airgap(request: Request) -> dict: data = await request.json() val = data.get("airgap", False) settings.airgap = val return {"airgap": settings.airgap} @app.post("/translate") 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} @app.get("/sources") @app.get("/pages") async def get_sources() -> list[dict]: return _get_store().list_sources() @app.get("/records") @app.get("/tasks") async def get_records(priority: str | None = None, type: str | None = None) -> list[dict]: return _get_store().get_records(priority=priority, type=type) @app.patch("/records/{record_id}") @app.patch("/tasks/{record_id}") 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} @app.delete("/records/{record_id}") @app.delete("/tasks/{record_id}") async def delete_record(record_id: int) -> dict: success = _get_store().delete_record(record_id) return {"success": success} @app.post("/sources/{source_id}/summarize") @app.post("/pages/{source_id}/summarize") 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} @app.post("/tts") 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}"} @app.post("/webhook") 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)} @app.get("/webhook") async def get_webhook() -> dict: return { "webhook_url": settings.webhook_url or "", "configured": bool(settings.webhook_url), } @app.post("/analyze-code") 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\n\nSUMMARY:\n" ) 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* 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.") @app.post("/chat") 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} @app.get("/ollama/status") 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, } @app.post("/ollama/configure") 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"} @app.post("/analyze-file") 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 @app.post("/dsa/analyze-code") 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 @app.post("/dsa/explain-code") 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 @app.post("/dsa/visualize") 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 @app.get("/dsa/templates") async def dsa_templates() -> dict: from pageparse import dsa_visualizer return {"templates": dsa_visualizer.list_templates()} @app.post("/dsa/visualize-template") 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 @app.get("/file-explorer") 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="

Page not found

")