| import streamlit as st |
| import fitz |
| import pandas as pd |
| import numpy as np |
| import re |
| import torch |
| from tqdm.auto import tqdm |
| from sentence_transformers import SentenceTransformer, util |
| import textwrap |
|
|
| |
| device = "cuda" if torch.cuda.is_available() else "cpu" |
|
|
| |
| @st.cache_data(show_spinner=True) |
| def process_pdf(pdf_path): |
| def text_formatter(text): |
| return text.replace("\n", " ").strip() |
|
|
| doc = fitz.open(pdf_path) |
| pages_and_texts = [] |
| for page_number, page in enumerate(doc): |
| text = text_formatter(page.get_text()) |
| pages_and_texts.append({ |
| "page_number": page_number - 15, |
| "text": text, |
| "page_char_count": len(text), |
| "page_word_count": len(text.split(" ")), |
| "page_sentence_count_raw": len(text.split(". ")), |
| "page_token_count": len(text)/4 |
| }) |
| return pages_and_texts |
|
|
| @st.cache_data(show_spinner=True) |
| def chunk_sentences(pages_and_texts, chunk_size=10): |
| import spacy |
| nlp = spacy.blank("en") |
| nlp.add_pipe("sentencizer") |
|
|
| def split_list(lst, size): |
| return [lst[i:i+size] for i in range(0, len(lst), size)] |
|
|
| for item in pages_and_texts: |
| item["sentences"] = [str(s) for s in nlp(item["text"]).sents] |
| item["sentence_chunks"] = split_list(item["sentences"], chunk_size) |
|
|
| pages_and_chunks = [] |
| for item in pages_and_texts: |
| for chunk in item["sentence_chunks"]: |
| joined = "".join(chunk).replace(" ", " ").strip() |
| joined = re.sub(r'\.([A-Z])', r'. \1', joined) |
| pages_and_chunks.append({ |
| "page_number": item["page_number"], |
| "sentence_chunk": joined, |
| "chunk_char_count": len(joined), |
| "chunk_word_count": len(joined.split(" ")), |
| "chunk_token_count": len(joined)/4 |
| }) |
| return pages_and_chunks |
|
|
| @st.cache_data(show_spinner=True) |
| def embed_chunks(pages_and_chunks): |
| model = SentenceTransformer("all-mpnet-base-v2", device=device) |
| filtered = [c for c in pages_and_chunks if c["chunk_token_count"] > 30] |
| texts = [item["sentence_chunk"] for item in filtered] |
| embeddings = model.encode(texts, convert_to_tensor=True) |
| for i, emb in enumerate(embeddings): |
| filtered[i]["embedding"] = emb.cpu().numpy() |
| return filtered, embeddings, model |
|
|
| @st.cache_data(show_spinner=False) |
| def semantic_search(query, embeddings, model, chunks, k=5): |
| query_emb = model.encode(query, convert_to_tensor=True).to(device) |
| scores = util.dot_score(query_emb, embeddings)[0] |
| top_k = torch.topk(scores, k) |
| results = [] |
| for score, idx in zip(top_k[0], top_k[1]): |
| results.append({ |
| "score": float(score), |
| "text": chunks[idx]["sentence_chunk"], |
| "page": chunks[idx]["page_number"] |
| }) |
| return results |
|
|
| st.title("Semantic Search App for Ian Goodfellow's Deep Learning Book") |
|
|
| uploaded_file = st.file_uploader("Upload the PDF", type="pdf") |
|
|
| if uploaded_file: |
| with st.spinner("Processing PDF..."): |
| with open("uploaded.pdf", "wb") as f: |
| f.write(uploaded_file.read()) |
| texts = process_pdf("uploaded.pdf") |
| chunks = chunk_sentences(texts) |
| chunk_data, chunk_embeddings, emb_model = embed_chunks(chunks) |
|
|
| query = st.text_input("Enter your query:") |
| if query: |
| with st.spinner("Searching..."): |
| results = semantic_search(query, chunk_embeddings, emb_model, chunk_data) |
| st.subheader("Top Results") |
| for res in results: |
| st.markdown(f"**Score:** {res['score']:.4f}") |
| st.markdown(f"**Page:** {res['page']}") |
| st.markdown(f"> {res['text']}") |
| st.markdown("---") |