Spaces:
Sleeping
Sleeping
Fix email validation, re-index on delete, lazy-load DocumentIndex
Browse files- backend/main.py +87 -0
- backend/vectorstore/document_index.py +8 -2
backend/main.py
CHANGED
|
@@ -150,6 +150,13 @@ class UserRegister(BaseModel):
|
|
| 150 |
|
| 151 |
@app.post("/register")
|
| 152 |
async def register(user: UserRegister):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
conn = sqlite3.connect(DB_PATH)
|
| 154 |
cursor = conn.cursor()
|
| 155 |
|
|
@@ -481,8 +488,10 @@ async def delete_document(filename: str, username: str = Depends(get_current_use
|
|
| 481 |
if not os.path.exists(file_path):
|
| 482 |
raise HTTPException(status_code=404, detail="File not found")
|
| 483 |
|
|
|
|
| 484 |
os.remove(file_path)
|
| 485 |
|
|
|
|
| 486 |
indices_dir = os.path.join(user_dir, "indices")
|
| 487 |
processed_dir = os.path.join(user_dir, "processed")
|
| 488 |
if os.path.exists(indices_dir):
|
|
@@ -490,6 +499,84 @@ async def delete_document(filename: str, username: str = Depends(get_current_use
|
|
| 490 |
if os.path.exists(processed_dir):
|
| 491 |
shutil.rmtree(processed_dir)
|
| 492 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 493 |
return {"message": f"Deleted {filename}"}
|
| 494 |
|
| 495 |
# --- Conversation Management ---
|
|
|
|
| 150 |
|
| 151 |
@app.post("/register")
|
| 152 |
async def register(user: UserRegister):
|
| 153 |
+
# Validate email format
|
| 154 |
+
if user.email:
|
| 155 |
+
import re
|
| 156 |
+
email_pattern = re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
|
| 157 |
+
if not email_pattern.match(user.email):
|
| 158 |
+
raise HTTPException(status_code=400, detail="Invalid email address")
|
| 159 |
+
|
| 160 |
conn = sqlite3.connect(DB_PATH)
|
| 161 |
cursor = conn.cursor()
|
| 162 |
|
|
|
|
| 488 |
if not os.path.exists(file_path):
|
| 489 |
raise HTTPException(status_code=404, detail="File not found")
|
| 490 |
|
| 491 |
+
# Remove the target file
|
| 492 |
os.remove(file_path)
|
| 493 |
|
| 494 |
+
# Wipe old indices and processed images
|
| 495 |
indices_dir = os.path.join(user_dir, "indices")
|
| 496 |
processed_dir = os.path.join(user_dir, "processed")
|
| 497 |
if os.path.exists(indices_dir):
|
|
|
|
| 499 |
if os.path.exists(processed_dir):
|
| 500 |
shutil.rmtree(processed_dir)
|
| 501 |
|
| 502 |
+
# Rebuild indices from remaining files
|
| 503 |
+
remaining_files = []
|
| 504 |
+
if os.path.exists(user_raw_dir):
|
| 505 |
+
remaining_files = [f for f in os.listdir(user_raw_dir) if os.path.isfile(os.path.join(user_raw_dir, f))]
|
| 506 |
+
|
| 507 |
+
if remaining_files:
|
| 508 |
+
USER_PROCESSED_DIR = os.path.join(user_dir, "processed", "images")
|
| 509 |
+
USER_INDICES_DIR = os.path.join(user_dir, "indices")
|
| 510 |
+
USER_CHUNK_INDEX_PATH = os.path.join(USER_INDICES_DIR, "chunks")
|
| 511 |
+
USER_DOC_INDEX_PATH = os.path.join(USER_INDICES_DIR, "docs")
|
| 512 |
+
USER_IMAGE_INDEX_PATH = os.path.join(USER_INDICES_DIR, "images")
|
| 513 |
+
|
| 514 |
+
rebuild_chunk_index = ChunkIndex()
|
| 515 |
+
rebuild_doc_index = DocumentIndex()
|
| 516 |
+
rebuild_image_store = ImageVectorStore()
|
| 517 |
+
|
| 518 |
+
AUDIO_EXTENSIONS = {".mp3", ".wav", ".m4a", ".ogg", ".flac"}
|
| 519 |
+
|
| 520 |
+
for remaining_file in remaining_files:
|
| 521 |
+
remaining_path = os.path.join(user_raw_dir, remaining_file)
|
| 522 |
+
file_ext = os.path.splitext(remaining_file)[1].lower()
|
| 523 |
+
|
| 524 |
+
try:
|
| 525 |
+
if file_ext in AUDIO_EXTENSIONS:
|
| 526 |
+
transcript = ingest_audio(remaining_path)
|
| 527 |
+
if transcript and len(transcript.strip()) > 50:
|
| 528 |
+
chunk_size = 500
|
| 529 |
+
words = transcript.split()
|
| 530 |
+
current_chunk = ""
|
| 531 |
+
audio_chunks = []
|
| 532 |
+
for word in words:
|
| 533 |
+
if len(current_chunk) + len(word) + 1 > chunk_size and current_chunk:
|
| 534 |
+
audio_chunks.append(current_chunk.strip())
|
| 535 |
+
current_chunk = word
|
| 536 |
+
else:
|
| 537 |
+
current_chunk += " " + word
|
| 538 |
+
if current_chunk.strip():
|
| 539 |
+
audio_chunks.append(current_chunk.strip())
|
| 540 |
+
rebuild_doc_index.add_document(transcript, remaining_file)
|
| 541 |
+
rebuild_chunk_index.add_chunks(remaining_file, audio_chunks)
|
| 542 |
+
else:
|
| 543 |
+
text_chunks, _ = ingest_pdf(remaining_path, USER_PROCESSED_DIR)
|
| 544 |
+
from collections import defaultdict
|
| 545 |
+
chunks_by_source = defaultdict(list)
|
| 546 |
+
for chunk in text_chunks:
|
| 547 |
+
chunks_by_source[chunk["source"]].append(chunk["text"])
|
| 548 |
+
for source, chunks in chunks_by_source.items():
|
| 549 |
+
slide_chunks = []
|
| 550 |
+
full_text = ""
|
| 551 |
+
for text in chunks:
|
| 552 |
+
text = text.strip()
|
| 553 |
+
if len(text) > 50:
|
| 554 |
+
full_text += text + "\n"
|
| 555 |
+
slide_chunks.append(text)
|
| 556 |
+
if slide_chunks:
|
| 557 |
+
rebuild_doc_index.add_document(full_text, source)
|
| 558 |
+
rebuild_chunk_index.add_chunks(source, slide_chunks)
|
| 559 |
+
|
| 560 |
+
all_images = glob.glob(os.path.join(USER_PROCESSED_DIR, "*.png"))
|
| 561 |
+
pdf_basename = os.path.basename(remaining_path)
|
| 562 |
+
new_images = [img for img in all_images if pdf_basename in os.path.basename(img)]
|
| 563 |
+
image_metadata = []
|
| 564 |
+
for p in new_images:
|
| 565 |
+
try:
|
| 566 |
+
parts = p.split("_page_")
|
| 567 |
+
page_num = int(parts[1].split("_img_")[0]) if len(parts) > 1 else 0
|
| 568 |
+
except:
|
| 569 |
+
page_num = 0
|
| 570 |
+
image_metadata.append({"image_path": p, "page": page_num})
|
| 571 |
+
if new_images:
|
| 572 |
+
rebuild_image_store.add_images(new_images, image_metadata)
|
| 573 |
+
except Exception as e:
|
| 574 |
+
print(f"[{username}] Warning: failed to re-index {remaining_file}: {e}")
|
| 575 |
+
|
| 576 |
+
rebuild_chunk_index.save_local(USER_CHUNK_INDEX_PATH)
|
| 577 |
+
rebuild_doc_index.save_local(USER_DOC_INDEX_PATH)
|
| 578 |
+
rebuild_image_store.save_local(USER_IMAGE_INDEX_PATH)
|
| 579 |
+
|
| 580 |
return {"message": f"Deleted {filename}"}
|
| 581 |
|
| 582 |
# --- Conversation Management ---
|
backend/vectorstore/document_index.py
CHANGED
|
@@ -1,15 +1,21 @@
|
|
| 1 |
import faiss
|
| 2 |
import numpy as np
|
| 3 |
-
from sentence_transformers import SentenceTransformer
|
| 4 |
import os
|
| 5 |
import pickle
|
| 6 |
|
| 7 |
class DocumentIndex:
|
| 8 |
def __init__(self):
|
| 9 |
-
self.
|
| 10 |
self.index = faiss.IndexFlatIP(384)
|
| 11 |
self.metadata = []
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
def add_document(self, full_text, source):
|
| 14 |
embedding = self.model.encode([full_text])
|
| 15 |
embedding = np.array(embedding).astype("float32")
|
|
|
|
| 1 |
import faiss
|
| 2 |
import numpy as np
|
|
|
|
| 3 |
import os
|
| 4 |
import pickle
|
| 5 |
|
| 6 |
class DocumentIndex:
|
| 7 |
def __init__(self):
|
| 8 |
+
self._model = None
|
| 9 |
self.index = faiss.IndexFlatIP(384)
|
| 10 |
self.metadata = []
|
| 11 |
|
| 12 |
+
@property
|
| 13 |
+
def model(self):
|
| 14 |
+
if self._model is None:
|
| 15 |
+
from sentence_transformers import SentenceTransformer
|
| 16 |
+
self._model = SentenceTransformer("all-MiniLM-L6-v2")
|
| 17 |
+
return self._model
|
| 18 |
+
|
| 19 |
def add_document(self, full_text, source):
|
| 20 |
embedding = self.model.encode([full_text])
|
| 21 |
embedding = np.array(embedding).astype("float32")
|