Varun2007's picture
initial clean deployment commit with compilers
8c3e275
Raw
History Blame Contribute Delete
39.1 kB
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 = (
'<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)\'">'
'&#x1F4CA; 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>"
@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<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.")
@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="<html><body><h1>Page not found</h1></body></html>")