File size: 7,320 Bytes
7996370
4fd45b0
6db8576
7547a54
6db8576
b824d13
 
7996370
7547a54
b824d13
7996370
7547a54
 
 
 
 
 
 
6401b0a
4e85813
b824d13
 
 
6db8576
b824d13
 
 
d93d7bb
6db8576
b824d13
7996370
7547a54
 
 
 
d93d7bb
 
 
 
 
 
 
7547a54
 
 
 
 
d93d7bb
 
7547a54
d93d7bb
 
7547a54
d93d7bb
7547a54
 
d93d7bb
 
 
 
 
 
 
 
 
 
 
 
 
 
7996370
d93d7bb
 
 
6db8576
d93d7bb
 
 
 
 
 
 
 
 
 
 
 
6db8576
 
d93d7bb
 
 
6db8576
d93d7bb
6db8576
7996370
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6db8576
d93d7bb
 
 
 
 
 
 
 
 
 
 
 
7996370
d93d7bb
 
 
 
 
 
 
6401b0a
d93d7bb
6401b0a
d93d7bb
 
 
 
7996370
d93d7bb
7996370
d93d7bb
 
 
 
6401b0a
d93d7bb
 
 
b824d13
d93d7bb
 
 
7996370
7547a54
7996370
 
7547a54
7996370
7547a54
6401b0a
d93d7bb
 
 
 
 
 
 
 
 
 
 
 
 
 
7996370
 
 
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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# app.py - Fully Local and Free
import os
import re
import tempfile
import requests
import streamlit as st
from PyPDF2 import PdfReader
from typing import List
import traceback

# Local TTS library (requires system dependencies)
try:
    import pyttsx3
    HAS_PYTTSX3 = True
except Exception:
    HAS_PYTTSX3 = False

# ============ CONFIG ============
HUGGINGFACE_KEY = os.getenv("HUGGINGFACE_API_KEY", st.secrets.get("HUGGINGFACE_API_KEY"))
HF_MERMAID_MODEL = os.getenv("HF_MERMAID_MODEL", "TroyDoesAI/MermaidStable3B")

# ============ HELPERS ============
def clean_text(text: str) -> str:
    return re.sub(r"\s+", " ", text or "").strip()

def extract_text_from_pdf(uploaded_file) -> str:
    reader = PdfReader(uploaded_file)
    parts = [page.extract_text() for page in reader.pages if page.extract_text()]
    return clean_text(" ".join(parts))

def local_summary(text: str, num_sentences: int = 6) -> str:
    if not text:
        return ""
    sentences = re.split(r'(?<=[.!?])\s+', text)
    words = re.findall(r'\w+', text.lower())
    stopwords = set(["the", "and", "is", "in", "to", "of", "a", "that", "it", "for"])
    freq = {w: words.count(w) for w in words if w not in stopwords and len(w) > 1}
    sent_scores = [(sum(freq.get(w, 0) for w in re.findall(r'\w+', s.lower())), s) for s in sentences]
    sent_scores.sort(reverse=True)
    chosen_sentences = sorted([s for _, s in sent_scores[:num_sentences]], key=text.find)
    return "\n".join(f"- {clean_text(s)}" for s in chosen_sentences if s.strip())

def pyttsx3_tts_file(text: str):
    if not HAS_PYTTSX3:
        return False, "pyttsx3 not installed"
    try:
        engine = pyttsx3.init()
        temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".wav")
        engine.save_to_file(text, temp_file.name)
        engine.runAndWait()
        with open(temp_file.name, "rb") as f:
            return True, f.read()
    except Exception as e:
        return False, f"pyttsx3 TTS failed: {e}"

def generate_mermaid_from_summary(summary: str):
    prompt = ("Create a concise Mermaid flowchart ('flowchart TD') from the following summary. "
              "Output only the Mermaid code block. Summary:\n" + summary)
    if HUGGINGFACE_KEY:
        url = f"https://api-inference.huggingface.co/models/{HF_MERMAID_MODEL}"
        headers = {"Authorization": f"Bearer {HUGGINGFACE_KEY}"}
        payload = {"inputs": prompt, "parameters": {"max_new_tokens": 512}}
        try:
            response = requests.post(url, headers=headers, json=payload, timeout=40)
            if response.ok and response.json():
                text = response.json()[0]['generated_text']
                match = re.search(r"```(?:mermaid)?\n([\s\S]+?)```", text)
                if match:
                    return match.group(1).strip()
        except Exception:
            pass
    
    # Local fallback logic
    nodes = [re.sub(r'^- ', '', line).strip() for line in summary.split('\n') if line.strip()]
    if not nodes:
        return "graph TD\n  A[Summary Empty]"
    mermaid_code = "graph TD\n"
    for i, node_text in enumerate(nodes[:8]):
        mermaid_code += f'  A{i}["{node_text.replace('"', "'")[:60]}"]\n'
    for i in range(len(nodes[:8]) - 1):
        mermaid_code += f"  A{i} --> A{i+1}\n"
    return mermaid_code

def render_mermaid(mermaid_code: str):
    html_code = f"""
    <div class="mermaid">
      {mermaid_code}
    </div>
    <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
    <style>
      .mermaid-container {{ height: 420px; border: 1px solid #ddd; padding: 10px; border-radius: 8px; }}
    </style>
    """
    st.components.v1.html(html_code, height=450, scrolling=True)

def local_qa(text: str, query: str) -> str:
    # A simple, local Q&A function based on keyword matching.
    sentences = re.split(r'(?<=[.!?])\s+', text)
    query_words = set(re.findall(r'\w+', query.lower()))
    
    ranked_sentences = []
    for s in sentences:
        s_words = set(re.findall(r'\w+', s.lower()))
        score = len(query_words.intersection(s_words))
        if score > 0:
            ranked_sentences.append((score, s))
            
    ranked_sentences.sort(key=lambda x: x[0], reverse=True)
    
    if ranked_sentences:
        top_sentences = [s[1] for s in ranked_sentences[:3]]
        return " ".join(top_sentences)
    else:
        return "I couldn't find a relevant answer in the document."

# ============ STREAMLIT UI ============
st.set_page_config(page_title="PDF Assistant", layout="wide")
st.title("πŸ“„ PDF Assistant: Summary, Diagram, Q&A")
st.markdown("---")

st.session_state.setdefault('raw_text', None)
st.session_state.setdefault('summary', None)
st.session_state.setdefault('mermaid_code', None)
st.session_state.setdefault('chat_history', [])

with st.sidebar:
    st.header("πŸ”‘ API Status")
    st.markdown(f"**Hugging Face:** {'βœ… Key present' if HUGGINGFACE_KEY else '❌ Key missing. Diagram will be local.'}")
    st.markdown(f"**Local TTS:** {'βœ… Active' if HAS_PYTTSX3 else '❌ Not available. Run `pip install pyttsx3`'}")

uploaded_file = st.file_uploader("1. Upload a PDF", type=["pdf"])
if uploaded_file and st.session_state.raw_text is None:
    with st.spinner("Extracting text..."):
        st.session_state.raw_text = extract_text_from_pdf(uploaded_file)
        if st.session_state.raw_text:
            st.success("Text extracted successfully!")
        else:
            st.warning("No text extracted from PDF. Is it a scanned image?")

if st.session_state.raw_text:
    st.markdown("---")
    if st.button("2. Generate Summary & Diagram"):
        with st.spinner("Generating summary and diagram..."):
            st.session_state.summary = local_summary(st.session_state.raw_text)
            st.session_state.mermaid_code = generate_mermaid_from_summary(st.session_state.summary)
            st.success("Summary and diagram generated!")
            
    if st.session_state.summary:
        st.header("πŸ“Œ Summary")
        st.markdown(st.session_state.summary)

        st.header("πŸ—ΊοΈ Diagram")
        render_mermaid(st.session_state.mermaid_code)
        st.code(st.session_state.mermaid_code, language="mermaid")

        st.header("πŸ”Š Audio")
        if st.button("Generate Audio"):
            with st.spinner("Generating audio..."):
                ok, out = pyttsx3_tts_file(st.session_state.summary)
                if ok:
                    st.audio(out, format="audio/wav")
                    st.info(f"Audio generated using: **pyttsx3**")
                else:
                    st.error("Audio generation failed.")

    st.markdown("---")
    st.header("πŸ’¬ Q&A Chatbot")
    for chat_message in st.session_state.chat_history:
        role, content = chat_message
        with st.chat_message(role):
            st.markdown(content)

    prompt = st.chat_input("Ask a question about the PDF")
    if prompt:
        st.session_state.chat_history.append(("user", prompt))
        with st.chat_message("user"):
            st.markdown(prompt)

        with st.chat_message("assistant"):
            with st.spinner("Thinking..."):
                answer = local_qa(st.session_state.raw_text, prompt)
                st.markdown(answer)
                st.session_state.chat_history.append(("assistant", answer))