Francisco Zanartu commited on
Commit ·
d7ffc69
1
Parent(s): 8aae565
feat: refactor document analysis and enhance Gradio interface for misinformation detection
Browse files
main.py
CHANGED
|
@@ -4,14 +4,26 @@ This is the minimal version for quick prototyping.
|
|
| 4 |
"""
|
| 5 |
|
| 6 |
import logging
|
| 7 |
-
import pprint
|
| 8 |
from pathlib import Path
|
| 9 |
import gradio as gr
|
| 10 |
-
from
|
|
|
|
|
|
|
| 11 |
from src.utils.chunking import get_base_chunks
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
from src.api.apis import classify_text
|
| 13 |
from src.api.rebuttal import RebuttalStructure
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
logging.basicConfig(
|
| 16 |
format="%(asctime)s %(levelname)s %(message)s",
|
| 17 |
datefmt="%m/%d/%Y %I:%M:%S %p",
|
|
@@ -20,125 +32,113 @@ logging.basicConfig(
|
|
| 20 |
logger = logging.getLogger(__name__)
|
| 21 |
|
| 22 |
|
| 23 |
-
def
|
| 24 |
-
""
|
| 25 |
-
Simple analysis function that processes PDF or URL input.
|
| 26 |
-
|
| 27 |
-
Returns formatted HTML with highlighted misinformation and rebuttals.
|
| 28 |
-
"""
|
| 29 |
-
# Step 1: Convert to markdown
|
| 30 |
-
md = MarkdownConverter()
|
| 31 |
-
|
| 32 |
-
if pdf_file is not None:
|
| 33 |
-
logger.info(f"📄 Processing PDF file: {pdf_file}")
|
| 34 |
-
document = md.run(pdf_file)
|
| 35 |
-
source = Path(pdf_file).name
|
| 36 |
-
elif url_text and url_text.strip():
|
| 37 |
-
logger.info(f"🔗 Processing URL: {url_text}")
|
| 38 |
-
document = md.run(url_text)
|
| 39 |
-
source = url_text
|
| 40 |
-
else:
|
| 41 |
-
return "<p style='color: red;'>❌ Please provide a PDF or URL</p>"
|
| 42 |
-
|
| 43 |
-
# Step 2: Chunk the document
|
| 44 |
-
chunks = get_base_chunks(document, chunk_size=1000, chunk_overlap=200)
|
| 45 |
-
logger.info(f"✅ Created {len(chunks)} chunks")
|
| 46 |
-
logger.info(f"✅ First chunk: {pprint.pformat(chunks[0], indent=4)}")
|
| 47 |
-
|
| 48 |
-
# Step 3: Classify chunks
|
| 49 |
-
# Handle both Document objects and dicts
|
| 50 |
-
responses = []
|
| 51 |
-
for chunk in chunks:
|
| 52 |
-
# Extract text from Document object or dict
|
| 53 |
-
if hasattr(chunk, "page_content"):
|
| 54 |
-
# LangChain Document object
|
| 55 |
-
chunk_text = chunk.page_content
|
| 56 |
-
elif isinstance(chunk, dict):
|
| 57 |
-
# Dictionary format
|
| 58 |
-
chunk_text = chunk.get("metadata", {}).get("chunk") or chunk.get("text", "")
|
| 59 |
-
else:
|
| 60 |
-
# Fallback: try to convert to string
|
| 61 |
-
chunk_text = str(chunk)
|
| 62 |
-
|
| 63 |
-
if chunk_text:
|
| 64 |
-
responses.append(classify_text(chunk_text))
|
| 65 |
-
|
| 66 |
-
# Step 4: Keep only positive misinformation detections
|
| 67 |
-
positive_responses = [r for r in responses if r.category != "0"]
|
| 68 |
-
|
| 69 |
-
# If no misinformation found
|
| 70 |
-
if not positive_responses:
|
| 71 |
-
return f"""
|
| 72 |
-
<div style='padding: 30px; background: #d4edda; border-radius: 8px; text-align: center;'>
|
| 73 |
-
<h2>✅ No Misinformation Detected</h2>
|
| 74 |
-
<p>Source: {source}</p>
|
| 75 |
-
</div>
|
| 76 |
-
"""
|
| 77 |
-
|
| 78 |
-
# Step 5: Generate rebuttals
|
| 79 |
rebuttal_gen = RebuttalStructure()
|
| 80 |
-
rebuttals = [rebuttal_gen.run(misinfo) for misinfo in positive_responses]
|
| 81 |
-
|
| 82 |
-
# Step 6: Build output HTML
|
| 83 |
-
output = f"""
|
| 84 |
-
<div style='max-width: 900px; margin: 0 auto; padding: 20px; font-family: Arial, sans-serif;'>
|
| 85 |
-
<h2>📋 Misinformation Analysis</h2>
|
| 86 |
-
<p><strong>Source:</strong> {source}</p>
|
| 87 |
-
<p><strong>Issues Found:</strong> {len(positive_responses)} out of {len(chunks)} sections</p>
|
| 88 |
-
<hr>
|
| 89 |
-
"""
|
| 90 |
-
|
| 91 |
-
# Map responses to chunks
|
| 92 |
-
response_index = 0
|
| 93 |
-
for i, chunk in enumerate(chunks):
|
| 94 |
-
# Extract text from Document object or dict
|
| 95 |
-
if hasattr(chunk, "page_content"):
|
| 96 |
-
chunk_text = chunk.page_content
|
| 97 |
-
elif isinstance(chunk, dict):
|
| 98 |
-
chunk_text = chunk.get("metadata", {}).get("chunk") or chunk.get("text", "")
|
| 99 |
-
else:
|
| 100 |
-
chunk_text = str(chunk)
|
| 101 |
-
|
| 102 |
-
# Check if this chunk has misinformation
|
| 103 |
-
if i < len(responses) and responses[i].category != "0":
|
| 104 |
-
# Highlighted section with rebuttal
|
| 105 |
-
output += f"""
|
| 106 |
-
<div style='background: #fff3cd; padding: 20px; margin: 20px 0; border-left: 4px solid #ff9800; border-radius: 4px;'>
|
| 107 |
-
<p style='margin: 0; font-size: 16px;'>{chunk_text}</p>
|
| 108 |
-
<div style='margin-top: 15px; padding: 15px; background: white; border-left: 3px solid #2196F3; border-radius: 4px;'>
|
| 109 |
-
<strong style='color: #2196F3;'>🔍 Fact Check:</strong>
|
| 110 |
-
<p style='margin: 5px 0 0 0;'>{rebuttals[response_index]}</p>
|
| 111 |
-
</div>
|
| 112 |
-
</div>
|
| 113 |
-
"""
|
| 114 |
-
response_index += 1
|
| 115 |
-
else:
|
| 116 |
-
# Normal section
|
| 117 |
-
output += f"""
|
| 118 |
-
<div style='padding: 15px; margin: 15px 0; background: #f5f5f5; border-radius: 4px;'>
|
| 119 |
-
<p style='margin: 0;'>{chunk_text}</p>
|
| 120 |
-
</div>
|
| 121 |
-
"""
|
| 122 |
-
|
| 123 |
-
output += "</div>"
|
| 124 |
-
return output
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
# Create simple Gradio interface
|
| 128 |
-
demo = gr.Interface(
|
| 129 |
-
fn=analyze_document,
|
| 130 |
-
inputs=[
|
| 131 |
-
gr.File(label="📄 Upload PDF (optional)", file_types=[".pdf"]),
|
| 132 |
-
gr.Textbox(
|
| 133 |
-
label="🔗 Or Enter URL (optional)",
|
| 134 |
-
placeholder="https://example.com/article",
|
| 135 |
-
),
|
| 136 |
-
],
|
| 137 |
-
outputs=gr.HTML(label="Analysis Results"),
|
| 138 |
-
title="🔍 Misinformation Detector",
|
| 139 |
-
description="Upload a PDF or enter a URL to detect and fact-check potential misinformation.",
|
| 140 |
-
)
|
| 141 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
|
| 143 |
if __name__ == "__main__":
|
| 144 |
demo.launch(
|
|
|
|
| 4 |
"""
|
| 5 |
|
| 6 |
import logging
|
|
|
|
| 7 |
from pathlib import Path
|
| 8 |
import gradio as gr
|
| 9 |
+
from langchain_core.messages import HumanMessage
|
| 10 |
+
from src.llm.llms import google_llm
|
| 11 |
+
from src.utils.parser_utils import clean_markdown, encode_pdf_to_base64
|
| 12 |
from src.utils.chunking import get_base_chunks
|
| 13 |
+
from src.utils.annotation_rendering import (
|
| 14 |
+
calculate_coverage,
|
| 15 |
+
create_end_markers,
|
| 16 |
+
highlight_text,
|
| 17 |
+
create_layout,
|
| 18 |
+
)
|
| 19 |
from src.api.apis import classify_text
|
| 20 |
from src.api.rebuttal import RebuttalStructure
|
| 21 |
|
| 22 |
+
|
| 23 |
+
transcription_prompt = Path("./src/prompts/md_transcript.md").read_text(
|
| 24 |
+
encoding="utf-8"
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
logging.basicConfig(
|
| 28 |
format="%(asctime)s %(levelname)s %(message)s",
|
| 29 |
datefmt="%m/%d/%Y %I:%M:%S %p",
|
|
|
|
| 32 |
logger = logging.getLogger(__name__)
|
| 33 |
|
| 34 |
|
| 35 |
+
def analyze_chunks(prev_state):
|
| 36 |
+
chunks = [c.copy() for c in prev_state["chunks"]]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
rebuttal_gen = RebuttalStructure()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
+
for chunk in chunks:
|
| 40 |
+
resp = classify_text(chunk["text"])
|
| 41 |
+
chunk["CARDS_code"] = resp.category
|
| 42 |
+
chunk["CARDS_category"] = resp.description
|
| 43 |
+
|
| 44 |
+
if resp.category != "0":
|
| 45 |
+
chunk["has_misinformation"] = True
|
| 46 |
+
chunk["rebuttal"] = rebuttal_gen.run(chunk["text"])
|
| 47 |
+
|
| 48 |
+
return {
|
| 49 |
+
"raw_markdown": prev_state["raw_markdown"],
|
| 50 |
+
"chunks": chunks,
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def render_document(state):
|
| 55 |
+
misleading = [c for c in state["chunks"] if c["has_misinformation"]]
|
| 56 |
+
|
| 57 |
+
coverage = calculate_coverage(misleading)
|
| 58 |
+
end_markers = create_end_markers(misleading)
|
| 59 |
+
|
| 60 |
+
annotated = highlight_text(state["raw_markdown"], coverage, end_markers)
|
| 61 |
+
|
| 62 |
+
return create_layout(annotated, misleading)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def transcribe_pdf(file_obj, prev_state):
|
| 66 |
+
if not file_obj:
|
| 67 |
+
yield "Please upload a PDF."
|
| 68 |
+
return
|
| 69 |
+
|
| 70 |
+
encoded_pdf = encode_pdf_to_base64(file_obj.name)
|
| 71 |
+
|
| 72 |
+
message = HumanMessage(
|
| 73 |
+
content=[
|
| 74 |
+
{"type": "text", "text": transcription_prompt},
|
| 75 |
+
{"type": "media", "mime_type": "application/pdf", "data": encoded_pdf},
|
| 76 |
+
]
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
markdown = ""
|
| 80 |
+
for chunk in google_llm.stream([message]):
|
| 81 |
+
markdown += chunk.content
|
| 82 |
+
yield markdown, prev_state
|
| 83 |
+
|
| 84 |
+
cleaned = clean_markdown(markdown)
|
| 85 |
+
|
| 86 |
+
chunks = [
|
| 87 |
+
{
|
| 88 |
+
"id": i,
|
| 89 |
+
"text": c.page_content,
|
| 90 |
+
"start": c.metadata["start_index"],
|
| 91 |
+
"end": c.metadata["start_index"] + len(c.page_content),
|
| 92 |
+
"has_misinformation": False,
|
| 93 |
+
"CARDS_code": None,
|
| 94 |
+
"CARDS_category": None,
|
| 95 |
+
"rebuttal": None,
|
| 96 |
+
}
|
| 97 |
+
for i, c in enumerate(
|
| 98 |
+
get_base_chunks(cleaned, chunk_size=1000, chunk_overlap=200)
|
| 99 |
+
)
|
| 100 |
+
]
|
| 101 |
+
|
| 102 |
+
new_state = {
|
| 103 |
+
"raw_markdown": cleaned,
|
| 104 |
+
"chunks": chunks,
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
yield cleaned, new_state
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
with gr.Blocks() as demo:
|
| 111 |
+
|
| 112 |
+
doc_state = gr.State(
|
| 113 |
+
{
|
| 114 |
+
"raw_markdown": "",
|
| 115 |
+
"chunks": [],
|
| 116 |
+
}
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
gr.Markdown("## Gemini Multimodal Chat (LangChain + Gradio)")
|
| 120 |
+
|
| 121 |
+
with gr.Row():
|
| 122 |
+
with gr.Row():
|
| 123 |
+
input_file = gr.File(label="Upload PDF file", file_types=[".pdf"])
|
| 124 |
+
submit_btn = gr.Button("Analyze", variant="primary")
|
| 125 |
+
|
| 126 |
+
with gr.Row():
|
| 127 |
+
output_text = gr.Markdown(label="Gemini's Response", line_breaks=True)
|
| 128 |
+
|
| 129 |
+
submit_btn.click(
|
| 130 |
+
fn=transcribe_pdf,
|
| 131 |
+
inputs=[input_file, doc_state],
|
| 132 |
+
outputs=[output_text, doc_state],
|
| 133 |
+
).then(
|
| 134 |
+
fn=analyze_chunks,
|
| 135 |
+
inputs=doc_state,
|
| 136 |
+
outputs=doc_state,
|
| 137 |
+
).then(
|
| 138 |
+
fn=render_document,
|
| 139 |
+
inputs=doc_state,
|
| 140 |
+
outputs=output_text,
|
| 141 |
+
)
|
| 142 |
|
| 143 |
if __name__ == "__main__":
|
| 144 |
demo.launch(
|