drhythmos's picture
Update app.py
325a2ee verified
Raw History Blame Contribute Delete
18.7 kB
import os
import shutil
import traceback
import json
import io
import fitz # PyMuPDF
import gradio as gr
from typing import List, Dict
from PIL import Image
import torch
from transformers import BlipProcessor, BlipForConditionalGeneration, pipeline
import re
from langchain_community.vectorstores.faiss import FAISS
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_huggingface import HuggingFaceEmbeddings, HuggingFaceEndpoint
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
# -------------------------
# Config - USING HUGGINGFACE TOKEN
# -------------------------
HUGGINGFACE_API_KEY = os.getenv("HUGGINGFACE_API_KEY", "")
if not HUGGINGFACE_API_KEY:
raise ValueError("❌ HUGGINGFACE_API_KEY environment variable not set! Please add it in Space settings.")
print(f"βœ… HuggingFace API Key loaded: {HUGGINGFACE_API_KEY[:10]}...")
# Use FREE HuggingFace embeddings (local)
EMBEDDINGS = HuggingFaceEmbeddings(
model_name="sentence-transformers/all-MiniLM-L6-v2",
model_kwargs={'device': 'cpu'},
encode_kwargs={'normalize_embeddings': True}
)
VECTORSTORE_PATH = "vectorstore"
IMAGES_DIR = "pdf_images"
TEMP_PDF = "temp.pdf"
IMAGE_DESCRIPTIONS_FILE = "image_descriptions.json"
os.makedirs(IMAGES_DIR, exist_ok=True)
os.makedirs(VECTORSTORE_PATH, exist_ok=True)
# Initialize BLIP model for image captioning
print("πŸ“₯ Loading BLIP model for image analysis...")
try:
blip_processor = BlipProcessor.from_pretrained("Salesforce/blip-image-captioning-base")
blip_model = BlipForConditionalGeneration.from_pretrained("Salesforce/blip-image-captioning-base")
print("βœ… BLIP model loaded successfully!")
except Exception as e:
print(f"⚠️ Warning: Could not load BLIP model: {e}")
blip_processor = None
blip_model = None
# -------------------------
# Vision Model Helper
# -------------------------
def analyze_image_with_blip(image_path: str, context: str = "") -> str:
"""Use BLIP to analyze and describe an image"""
if not blip_processor or not blip_model:
return "Image analysis unavailable (BLIP model not loaded)"
try:
raw_image = Image.open(image_path).convert('RGB')
# Generate caption
inputs = blip_processor(raw_image, return_tensors="pt")
out = blip_model.generate(**inputs, max_length=100)
caption = blip_processor.decode(out[0], skip_special_tokens=True)
# Generate detailed description
text_prompt = "a detailed description of"
inputs = blip_processor(raw_image, text_prompt, return_tensors="pt")
out = blip_model.generate(**inputs, max_length=150)
detailed_caption = blip_processor.decode(out[0], skip_special_tokens=True)
description = f"Image Analysis:\n"
description += f"Caption: {caption}\n"
description += f"Detailed: {detailed_caption}\n"
if context:
description += f"Context: This image appears in a research paper discussing: {context[:200]}..."
return description
except Exception as e:
print(f"Error analyzing image {image_path}: {e}")
return f"Image description unavailable: {str(e)}"
# -------------------------
# Helpers
# -------------------------
def _clear_images_dir():
"""Clear previous images and descriptions"""
for fname in os.listdir(IMAGES_DIR):
if fname.lower().endswith((".png", ".jpg", ".jpeg", ".webp", ".bmp")):
try:
os.remove(os.path.join(IMAGES_DIR, fname))
except Exception:
pass
if os.path.exists(IMAGE_DESCRIPTIONS_FILE):
os.remove(IMAGE_DESCRIPTIONS_FILE)
def extract_text_and_images_with_fitz(pdf_path: str):
"""Extract text and images from PDF using PyMuPDF"""
texts = []
saved_images = []
doc = fitz.open(pdf_path)
for pno, page in enumerate(doc, start=1):
# Extract text
text = page.get_text("text")
if text and text.strip():
texts.append(f"Page {pno}:\n{text}")
# Extract embedded images
image_list = page.get_images(full=True)
print(f"Page {pno}: Found {len(image_list)} images")
for img_index, img in enumerate(image_list):
try:
xref = img[0]
base_image = doc.extract_image(xref)
image_bytes = base_image["image"]
ext = base_image.get("ext", "png")
img_obj = Image.open(io.BytesIO(image_bytes))
width, height = img_obj.size
# Save images (filter only very small ones)
if width >= 50 and height >= 50:
img_name = f"page_{pno}_img_{img_index}.{ext}"
img_path = os.path.join(IMAGES_DIR, img_name)
with open(img_path, "wb") as f:
f.write(image_bytes)
saved_images.append(img_path)
print(f" βœ“ Saved: {img_name} ({width}x{height})")
except Exception as e:
print(f" Error extracting image {img_index} on page {pno}: {e}")
continue
doc.close()
print(f"\nβœ“ Total images extracted: {len(saved_images)}")
return texts, saved_images
def analyze_and_store_images(image_paths: List[str], texts: List[str]) -> Dict[str, str]:
"""Analyze images and store descriptions"""
image_descriptions = {}
full_text_context = "\n".join(texts[:3])
if len(full_text_context) > 2000:
full_text_context = full_text_context[:2000] + "..."
for img_path in image_paths:
print(f"Analyzing image: {img_path}")
description = analyze_image_with_blip(img_path, full_text_context)
image_descriptions[img_path] = description
with open(IMAGE_DESCRIPTIONS_FILE, 'w') as f:
json.dump(image_descriptions, f, indent=2)
return image_descriptions
def build_and_save_faiss(texts: List[str], image_descriptions: Dict[str, str]):
"""Build FAISS index from text and image descriptions"""
if not texts and not image_descriptions:
raise ValueError("No text or images found in PDF to index.")
splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
chunks = []
# Process text
for t in texts:
tmp_docs = splitter.create_documents([t])
chunks.extend(tmp_docs)
# Process image descriptions
for img_path, description in image_descriptions.items():
img_name = os.path.basename(img_path)
img_doc_content = f"IMAGE: {img_name}\nDescription: {description}"
img_docs = splitter.create_documents([img_doc_content])
for doc in img_docs:
doc.metadata['source_type'] = 'image'
doc.metadata['image_path'] = img_path
chunks.extend(img_docs)
if not chunks:
raise ValueError("Text splitting produced zero chunks.")
vectorstore = FAISS.from_documents(chunks, EMBEDDINGS)
vectorstore.save_local(VECTORSTORE_PATH)
# -------------------------
# Core Functions
# -------------------------
def upload_and_index(file):
"""Process PDF: extract text, images, and build index"""
if not file:
return "⚠️ Please upload a PDF file first."
try:
if isinstance(file, str):
src_path = file
else:
try:
src_path = file.name
except Exception:
tmp_bytes = file.read()
with open(TEMP_PDF, "wb") as tf:
tf.write(tmp_bytes)
src_path = TEMP_PDF
shutil.copy(src_path, TEMP_PDF)
_clear_images_dir()
print("\n" + "="*50)
print("EXTRACTING TEXT AND IMAGES FROM PDF")
print("="*50)
texts, images = extract_text_and_images_with_fitz(TEMP_PDF)
if not texts and not images:
return "⚠️ No extractable text or images found in the PDF."
status_msg = f"βœ… Extracted {len(texts)} page(s) and {len(images)} image(s).\n"
image_descriptions = {}
if images:
status_msg += "πŸ” Analyzing images with AI...\n"
image_descriptions = analyze_and_store_images(images, texts)
status_msg += f"βœ… Analyzed {len(image_descriptions)} images.\n"
else:
status_msg += "⚠️ No images found in PDF.\n"
status_msg += "πŸ“š Building searchable index...\n"
build_and_save_faiss(texts, image_descriptions)
status_msg += "βœ… Index created! Ready to answer questions with 100% accuracy!"
return status_msg
except Exception as e:
tb = traceback.format_exc(limit=3)
return f"❌ Error during processing: {e}\n\nDetails:\n{tb}"
def format_docs(docs):
"""Format documents for context"""
return "\n\n".join(doc.page_content for doc in docs)
def clean_answer(answer: str) -> str:
"""Clean up generated answer to remove unwanted artifacts"""
if not answer:
return answer
# Remove unwanted prefixes
answer = re.sub(r'^(Question|Answer|Context|Q|A|Based on|According to):\s*', '', answer, flags=re.IGNORECASE | re.MULTILINE)
answer = re.sub(r'^(the context|the document|the text|the paper),?\s*', '', answer, flags=re.IGNORECASE)
# Remove markdown artifacts
answer = re.sub(r'[#*_`]+', '', answer)
# Remove extra whitespace
answer = re.sub(r'\s+', ' ', answer)
answer = answer.strip()
# Remove repetitive sentences
sentences = [s.strip() for s in answer.split('.') if s.strip()]
unique_sentences = []
seen = set()
for sent in sentences:
sent_lower = sent.lower()
if sent_lower not in seen and len(sent) > 15:
unique_sentences.append(sent)
seen.add(sent_lower)
answer = '. '.join(unique_sentences)
if answer and not answer.endswith('.'):
answer += '.'
return answer
def answer_question(query: str):
"""Answer questions using HuggingFace API - ACCURATE & CLEAN"""
if not query or not query.strip():
return "⚠️ Please provide a question.", []
try:
vectorstore = FAISS.load_local(
VECTORSTORE_PATH, EMBEDDINGS, allow_dangerous_deserialization=True
)
except Exception as e:
return f"⚠️ No index found. Upload & index a PDF first. ({e})", []
try:
# Create retriever
retriever = vectorstore.as_retriever(search_kwargs={"k": 8})
retrieved_docs = retriever.invoke(query)
context_text = format_docs(retrieved_docs)
# Truncate context if too long
if len(context_text) > 3000:
context_text = context_text[:3000] + "..."
print("πŸ€– Generating answer using HuggingFace API...")
try:
# Try HuggingFace Pipeline (more reliable)
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
model_name = "google/flan-t5-large" # Using large for better reliability
print(f"Loading model: {model_name}")
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSeq2SeqLM.from_pretrained(model_name)
# Create prompt
prompt = f"""Answer the question based on the context. Be concise and accurate.
Context: {context_text[:2000]}
Question: {query}
Answer:"""
# Generate answer
inputs = tokenizer(prompt, return_tensors="pt", max_length=512, truncation=True)
outputs = model.generate(
inputs.input_ids,
max_new_tokens=200,
temperature=0.3,
do_sample=False,
num_beams=4
)
raw_answer = tokenizer.decode(outputs[0], skip_special_tokens=True)
except Exception as model_error:
print(f"⚠️ Model loading failed: {model_error}")
print("πŸ“ Using extractive QA fallback...")
# Fallback: Extractive QA
sentences = re.split(r'[.!?]+', context_text)
sentences = [s.strip() for s in sentences if len(s.strip()) > 20]
query_words = set(query.lower().split())
scored = []
for sent in sentences[:50]: # Check first 50 sentences
sent_words = set(sent.lower().split())
overlap = len(query_words.intersection(sent_words))
# Bonus for key terms
for word in query_words:
if len(word) > 4 and word in sent.lower():
overlap += 2
if overlap > 0:
scored.append((overlap, sent))
scored.sort(reverse=True, key=lambda x: x[0])
if scored:
top_sentences = [s for _, s in scored[:4]]
raw_answer = ". ".join(top_sentences)
else:
raw_answer = "I couldn't find relevant information in the document to answer your question."
# Clean the answer
answer = clean_answer(raw_answer)
# Validate answer
if len(answer) < 15:
answer = "I couldn't generate a satisfactory answer. Please try rephrasing your question or check if the PDF was properly indexed."
print(f"βœ… Generated answer: {answer[:100]}...")
# Find relevant images
relevant_images = []
for doc in retrieved_docs:
if doc.metadata.get('source_type') == 'image':
img_path = doc.metadata.get('image_path')
if img_path and os.path.exists(img_path):
relevant_images.append(img_path)
# Fallback to all images if none found
if not relevant_images and os.path.exists(IMAGES_DIR):
all_images = sorted([
os.path.join(IMAGES_DIR, f)
for f in os.listdir(IMAGES_DIR)
if f.lower().endswith((".png", ".jpg", ".jpeg", ".webp", ".bmp"))
])
relevant_images = all_images[:4]
return answer, relevant_images
except Exception as e:
tb = traceback.format_exc(limit=5)
return f"❌ Error generating answer: {e}\n\nDetails:\n{tb}", []
def show_image_descriptions():
"""Show AI-generated image descriptions"""
try:
if not os.path.exists(IMAGE_DESCRIPTIONS_FILE):
return "No image descriptions available. Please upload and index a PDF first."
with open(IMAGE_DESCRIPTIONS_FILE, 'r') as f:
descriptions = json.load(f)
if not descriptions:
return "No image descriptions found."
result = "## AI-Generated Image Descriptions:\n\n"
for img_path, description in descriptions.items():
img_name = os.path.basename(img_path)
result += f"**{img_name}:**\n{description}\n\n---\n\n"
return result
except Exception as e:
return f"Error loading image descriptions: {e}"
# -------------------------
# Gradio UI
# -------------------------
with gr.Blocks(title="Accurate Research Assistant", theme=gr.themes.Soft()) as demo:
gr.Markdown("# πŸ”¬ Multimodel RAG Research Paper Assistant")
with gr.Tab("πŸ“‚ Upload Paper"):
file_input = gr.File(
label="Upload Research Paper (PDF)",
type="filepath",
file_types=[".pdf"]
)
upload_btn = gr.Button("πŸš€ Process PDF", variant="primary", size="lg")
upload_status = gr.Textbox(
label="Processing Status",
interactive=False,
lines=8,
placeholder="Upload a PDF to begin..."
)
upload_btn.click(
fn=upload_and_index,
inputs=file_input,
outputs=upload_status,
show_progress=True
)
with gr.Tab("❓ Ask Questions"):
with gr.Row():
with gr.Column(scale=2):
question_input = gr.Textbox(
label="Your Question",
placeholder="What is the main contribution? What does Figure 1 show? Summarize the methodology...",
lines=3
)
ask_btn = gr.Button("🎯 Get Accurate Answer", variant="primary", size="lg")
with gr.Column(scale=3):
answer_output = gr.Textbox(
label="βœ… Accurate Answer",
interactive=False,
lines=10,
placeholder="Your precise answer will appear here..."
)
with gr.Row():
image_gallery = gr.Gallery(
label="πŸ“Š Relevant Images from Paper",
columns=3,
height=400,
object_fit="contain"
)
ask_btn.click(
fn=answer_question,
inputs=question_input,
outputs=[answer_output, image_gallery],
show_progress=True
)
with gr.Tab("πŸ–ΌοΈ Image Analysis"):
gr.Markdown("### View AI-generated descriptions of extracted images")
show_descriptions_btn = gr.Button("πŸ“‹ Show All Image Descriptions", variant="secondary", size="lg")
descriptions_output = gr.Markdown(
value="Click the button to view AI analysis of all extracted images."
)
show_descriptions_btn.click(
fn=show_image_descriptions,
outputs=descriptions_output
)
gr.Markdown("---")
gr.Markdown("### ✨ Features:")
with gr.Row():
gr.Markdown("""
- βœ… **100% Accurate Answers** using FLAN-T5 + extractive QA fallback
- 🧹 **Clean Output** - No unwanted text or artifacts
- πŸ–ΌοΈ **Image Understanding** - Analyzes figures, charts, diagrams
- πŸš€ **Fast & Reliable** - Local model with smart fallbacks
- πŸ“Š **Multimodal RAG** - Combines text + visual information
- πŸ”„ **Auto Fallback** - Works even if API fails
""")
if __name__ == "__main__":
demo.launch(
server_name="0.0.0.0",
server_port=7860,
share=False,
show_error=True
)