Spaces:
Sleeping
Sleeping
File size: 4,736 Bytes
488d16f ed656e4 488d16f ed656e4 1b18489 ed656e4 1b18489 ed656e4 1b18489 ed656e4 1b18489 ed656e4 488d16f ed656e4 488d16f ed656e4 488d16f ed656e4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | """Reading one upload for the live demo: the shared upload checks, the pipeline's own
ingestion (layoutlmv3 tables, Qwen2.5 VL figures), the limits, and the per address daily cap. No Gradio here, so
it runs and is tested anywhere; app.py wraps it for the Space."""
import contextlib
import hashlib
import io
import ipaddress
import os
import shutil
import tempfile
import threading
import time
from typing import Dict, List, Optional, Tuple
from src.ingestion import upload_checks
from src.ingestion.upload_checks import UploadRefused
DAY_S = 24 * 3600
def limit(name: str, default: int) -> int:
"""A Space variable (MAX_MB, MAX_PAGES, MAX_CHUNKS, DAILY_READS_PER_IP), else the default."""
return int(os.environ.get(name, default))
def client_ip(forwarded_for: Optional[str], peer: Optional[str]) -> str:
"""The visitor's address: the rightmost public hop of X-Forwarded-For (what the first proxy we trust saw; the
leftmost hops are whatever the client claimed), else the socket peer."""
return client_hop(forwarded_for, peer)[0]
def client_hop(forwarded_for: Optional[str], peer: Optional[str]) -> Tuple[str, str]:
"""(address, where it came from, as "hop k of n" or "peer"); the second part is safe to log, the first is not."""
hops = [h.strip() for h in (forwarded_for or "").split(",") if h.strip()]
for k in range(len(hops) - 1, -1, -1):
try:
if ipaddress.ip_address(hops[k]).is_global:
return hops[k], f"hop {k + 1} of {len(hops)}"
except ValueError:
continue
return peer or "unknown", "peer"
class DailyCap:
"""Successful reads per address in a rolling 24 hours, in memory (lost on restart); safe across threads."""
def __init__(self):
self._reads: Dict[str, Tuple[float, int]] = {}
self._lock = threading.Lock()
def allowed(self, ip: str, now: float = None) -> bool:
now = time.time() if now is None else now
with self._lock:
start, count = self._reads.get(ip, (now, 0))
return now - start >= DAY_S or count < limit("DAILY_READS_PER_IP", 10)
def record(self, ip: str, now: float = None) -> None:
now = time.time() if now is None else now
with self._lock:
start, count = self._reads.get(ip, (now, 0))
self._reads[ip] = (now, 1) if now - start >= DAY_S else (start, count + 1)
def prepare(source: str, name: str) -> Tuple[str, Optional[int]]:
"""Checks the upload and copies it, under its own name, into a fresh private folder: (folder, PDF page count)."""
upload_checks.check_name(name)
max_mb = limit("MAX_MB", 10)
if os.path.getsize(source) > max_mb * 1024 * 1024:
raise UploadRefused("too_large", 413, f"{name} is larger than {max_mb} MB.")
folder = tempfile.mkdtemp(prefix="vrag-")
path = os.path.join(folder, name)
shutil.copyfile(source, path)
try:
pages = upload_checks.check_contents(path, name, limit("MAX_PAGES", 5))
except Exception:
shutil.rmtree(folder, ignore_errors=True)
raise
return folder, pages
def log_name(name: str) -> str:
"""A short fingerprint of a visitor's file name, so the log can tell uploads apart without naming them."""
return hashlib.sha256((name or "").encode("utf-8")).hexdigest()[:8]
def read(folder: str) -> List[Dict]:
"""The pipeline's own reading and chunking of the one file in `folder`, as the demo's chunk records. The
ingestion's own log lines name the file and quote reader errors, so they are discarded here."""
from src.ingestion.chunking import DocumentChunker
from src.ingestion.loader import DocumentLoader
with contextlib.redirect_stdout(io.StringIO()):
chunks = DocumentChunker().chunk_documents(DocumentLoader(raw_data_dir=folder).load_documents())
max_chunks = limit("MAX_CHUNKS", 2000)
if len(chunks) > max_chunks:
raise UploadRefused("too_many_chunks", 413, f"The document makes {len(chunks)} chunks; the limit is {max_chunks}.")
return [{"id": c["id"], "doc_hash": c["metadata"].get("doc_hash"), "chunk_index": c["metadata"].get("chunk_index"),
"page": c["metadata"].get("page"), "kind": c["metadata"].get("kind", "text"), "content": c["content"]}
for c in chunks]
def discard(*paths: Optional[str]) -> None:
"""Deletes the private copy and Gradio's cached upload; nothing of the visitor's file is kept."""
for path in paths:
if not path:
continue
if os.path.isdir(path):
shutil.rmtree(path, ignore_errors=True)
elif os.path.exists(path):
try:
os.remove(path)
except OSError:
pass
|