vipularya commited on
Commit
a3578ec
·
verified ·
1 Parent(s): 8c612bd

Update streamlit.py

Browse files
Files changed (1) hide show
  1. streamlit.py +634 -0
streamlit.py CHANGED
@@ -0,0 +1,634 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ——— Patch 1: Stop Streamlit watcher hitting torch._classes.__path__ ———
2
+ import torch
3
+ class _DummyPath:
4
+ def __init__(self):
5
+ self._path = []
6
+ def __getattr__(self, name):
7
+ return []
8
+ torch._classes.__path__ = _DummyPath()
9
+
10
+ # ——— Patch 2: Make SentenceTransformer.to() fall back to to_empty() on meta modules ———
11
+ import sentence_transformers as _st
12
+ _BaseST = _st.SentenceTransformer
13
+ class SentenceTransformer(_BaseST):
14
+ def to(self, *args, **kwargs):
15
+ try:
16
+ return super().to(*args, **kwargs)
17
+ except NotImplementedError:
18
+ return super().to_empty(*args, **kwargs)
19
+
20
+ # ——— Standard imports ———
21
+ import streamlit as st
22
+ import streamlit.components.v1 as components
23
+ import PyPDF2
24
+ import numpy as np
25
+ from typing import List, Dict
26
+ from langdetect import detect, detect_langs
27
+ from sklearn.metrics.pairwise import cosine_similarity
28
+ import google.generativeai as genai
29
+ from gtts import gTTS
30
+ import speech_recognition as sr
31
+ import tempfile, base64, os
32
+ import requests, time
33
+ import sqlite3
34
+ from datetime import datetime
35
+ import pandas as pd
36
+ import faiss # Import FAISS
37
+
38
+ # ——— Configuration ———
39
+ GENAI_API_KEY = "AIzaSyA5xtoT9HAjH-wsa7OHFXlBjRRcXwCFBMg"
40
+ DID_API_KEY = "a3Jpc2huYW12aXB1bEBnbWFpbC4Y29t:5DSNuJuWUBZQ0G44TfJlJ" # Replace with your actual D-ID API key
41
+ AVATAR_IMAGE_URL = "https://raw.githubusercontent.com/de-id/live-streaming-demo/main/alex_v2_idle_image.png"
42
+
43
+ # Ensure data directories exist
44
+ if not os.path.exists("data"):
45
+ os.makedirs("data")
46
+ if not os.path.exists("data/pdfs"):
47
+ os.makedirs("data/pdfs")
48
+ if not os.path.exists("data/faiss_indexes"):
49
+ os.makedirs("data/faiss_indexes")
50
+
51
+ # ——— SQLite DB Setup ———
52
+ def init_db():
53
+ conn = sqlite3.connect("interactions.db")
54
+ cursor = conn.cursor()
55
+ cursor.execute("""
56
+ CREATE TABLE IF NOT EXISTS users (
57
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
58
+ username TEXT UNIQUE NOT NULL,
59
+ password TEXT NOT NULL
60
+ )
61
+ """)
62
+ cursor.execute("""
63
+ CREATE TABLE IF NOT EXISTS interactions (
64
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
65
+ user_id INTEGER,
66
+ timestamp TEXT,
67
+ language TEXT,
68
+ question TEXT,
69
+ answer TEXT,
70
+ FOREIGN KEY (user_id) REFERENCES users (id)
71
+ )
72
+ """)
73
+ cursor.execute("""
74
+ CREATE TABLE IF NOT EXISTS documents (
75
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
76
+ user_id INTEGER,
77
+ filename TEXT NOT NULL,
78
+ filepath TEXT NOT NULL,
79
+ faiss_index_path TEXT NOT NULL,
80
+ language TEXT, -- Store the detected primary language of the document
81
+ FOREIGN KEY (user_id) REFERENCES users (id)
82
+ )
83
+ """)
84
+ conn.commit()
85
+ conn.close()
86
+
87
+ def add_user(username, password):
88
+ conn = sqlite3.connect("interactions.db")
89
+ cursor = conn.cursor()
90
+ try:
91
+ # NOTE: For production, use a strong hashing library like 'bcrypt' or 'passlib'
92
+ # For this example, a simple hash() is used, which is NOT SECURE for real applications.
93
+ cursor.execute("INSERT INTO users (username, password) VALUES (?, ?)", (username, hash(password)))
94
+ conn.commit()
95
+ return True
96
+ except sqlite3.IntegrityError:
97
+ return False # Username already exists
98
+ finally:
99
+ conn.close()
100
+
101
+ def verify_user(username, password):
102
+ conn = sqlite3.connect("interactions.db")
103
+ cursor = conn.cursor()
104
+ # NOTE: For production, use a strong hashing library like 'bcrypt' or 'passlib'
105
+ cursor.execute("SELECT id FROM users WHERE username = ? AND password = ?", (username, hash(password)))
106
+ user = cursor.fetchone()
107
+ conn.close()
108
+ return user[0] if user else None
109
+
110
+ def save_interaction(user_id: int, language: str, question: str, answer: str):
111
+ conn = sqlite3.connect("interactions.db")
112
+ cursor = conn.cursor()
113
+ cursor.execute("""
114
+ INSERT INTO interactions (user_id, timestamp, language, question, answer)
115
+ VALUES (?, ?, ?, ?, ?)
116
+ """, (user_id, datetime.now().isoformat(), language, question, answer))
117
+ conn.commit()
118
+ conn.close()
119
+
120
+ def save_document_metadata(user_id: int, filename: str, filepath: str, faiss_index_path: str, language: str):
121
+ conn = sqlite3.connect("interactions.db")
122
+ cursor = conn.cursor()
123
+ cursor.execute("""
124
+ INSERT INTO documents (user_id, filename, filepath, faiss_index_path, language)
125
+ VALUES (?, ?, ?, ?, ?)
126
+ """, (user_id, filename, filepath, faiss_index_path, language))
127
+ conn.commit()
128
+ conn.close()
129
+
130
+ def get_user_documents(user_id: int) -> List[Dict]:
131
+ conn = sqlite3.connect("interactions.db")
132
+ cursor = conn.cursor()
133
+ cursor.execute("SELECT id, filename, filepath, faiss_index_path, language FROM documents WHERE user_id = ?", (user_id,))
134
+ docs = [{"id": row[0], "filename": row[1], "filepath": row[2], "faiss_index_path": row[3], "language": row[4]} for row in cursor.fetchall()]
135
+ conn.close()
136
+ return docs
137
+
138
+ # ——— RAGSingleLanguage class ———
139
+ class RAGSingleLanguage:
140
+ def __init__(self, api_key: str):
141
+ genai.configure(api_key=api_key)
142
+ self.model = genai.GenerativeModel('gemini-1.5-flash')
143
+ self.embedder = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
144
+ self.chunks: List[str] = []
145
+ self.faiss_index = None
146
+ self.language: str = 'en' # Default language for translation if not explicitly set
147
+
148
+ def detect_languages(self, text: str) -> List[str]:
149
+ seg_size = 1000
150
+ probs = {}
151
+ for i in range(0, len(text), seg_size):
152
+ seg = text[i:i+seg_size]
153
+ try:
154
+ for lang in detect_langs(seg):
155
+ probs[lang.lang] = max(probs.get(lang.lang, 0.0), lang.prob)
156
+ except:
157
+ continue
158
+ # Only return languages with a probability >= 0.2
159
+ langs = [l for l,p in probs.items() if p >= 0.2]
160
+ # Fallback to English if no strong detection
161
+ return langs or ['en']
162
+
163
+ def translate(self, text: str, tgt: str) -> str:
164
+ try:
165
+ src = detect(text)
166
+ except:
167
+ src = 'en' # Assume English if detection fails
168
+ if src.lower() == tgt.lower():
169
+ return text
170
+ prompt = f"Translate to {tgt.upper()}:\n\n{text}"
171
+ try:
172
+ return self.model.generate_content(prompt).text.strip()
173
+ except Exception as e:
174
+ st.warning(f"Translation failed: {e}. Returning original text.")
175
+ return text
176
+
177
+ def process_document(self, pdf_file_path: str, chunk_size: int = 500) -> str:
178
+ reader = PyPDF2.PdfReader(pdf_file_path)
179
+ pages = [p.extract_text() or "" for p in reader.pages]
180
+ full_text = " ".join(pages)
181
+
182
+ # Detect dominant language of the document
183
+ detected_langs = self.detect_languages(full_text)
184
+ # We'll store the first detected language as the document's primary language
185
+ doc_language = detected_langs[0] if detected_langs else 'en'
186
+
187
+ full = full_text.split()
188
+ self.chunks = [
189
+ " ".join(full[i:i+chunk_size])
190
+ for i in range(0, len(full), chunk_size)
191
+ ]
192
+
193
+ # Generate embeddings
194
+ embeddings = self.embedder.encode(
195
+ self.chunks,
196
+ convert_to_numpy=True,
197
+ normalize_embeddings=True
198
+ )
199
+
200
+ # Create FAISS index
201
+ dimension = embeddings.shape[1]
202
+ self.faiss_index = faiss.IndexFlatL2(dimension)
203
+ self.faiss_index.add(embeddings)
204
+
205
+ return doc_language # Return the detected language for saving
206
+
207
+ def load_faiss_index(self, faiss_index_path: str, document_chunks: List[str]):
208
+ try:
209
+ self.faiss_index = faiss.read_index(faiss_index_path)
210
+ self.chunks = document_chunks # Load associated chunks
211
+ return True
212
+ except Exception as e:
213
+ st.error(f"Error loading FAISS index: {e}")
214
+ return False
215
+
216
+ def set_language(self, lang: str):
217
+ self.language = lang
218
+
219
+ def answer_question(self, question: str, top_k: int = 5) -> str:
220
+ if self.faiss_index is None or not self.chunks:
221
+ return "Please select a document to query from."
222
+
223
+ q_en = self.translate(question, 'en')
224
+ q_emb = self.embedder.encode([q_en], convert_to_numpy=True, normalize_embeddings=True)
225
+
226
+ # Search FAISS index
227
+ # D, I are distances and indices respectively.
228
+ # For normalized embeddings, L2 distance (d) is related to cosine similarity (s) by d^2 = 2(1-s)
229
+ distances, indices = self.faiss_index.search(q_emb, top_k)
230
+
231
+ contexts = []
232
+ for i, dist in zip(indices[0], distances[0]):
233
+ if i >= 0 and i < len(self.chunks): # Ensure index is valid
234
+ sim_score = 1 - (dist / 2) # Convert L2 distance to cosine similarity for display
235
+ contexts.append(f"[Score: {sim_score:.2f}]\n{self.chunks[i]}")
236
+
237
+ ctx = "\n\n".join(contexts)
238
+
239
+ prompt = (
240
+ "Answer the following question using only the provided context. "
241
+ "Be accurate and detailed. If the answer is not present, say: "
242
+ "'I apologize, but I cannot find this information in the documentation. "
243
+ "Please contact customer support for accurate assistance on this matter.'\n\n"
244
+ f"Context:\n{ctx}\n\nQuestion: {q_en}"
245
+ )
246
+
247
+ try:
248
+ out = self.model.generate_content(prompt).text.strip()
249
+ except Exception as e:
250
+ return f"Error generating answer: {e}"
251
+ return self.translate(out, self.language)
252
+
253
+ # ——— Voice Input ———
254
+ def recognize_voice(lang_code='en-IN') -> str:
255
+ r = sr.Recognizer()
256
+ with sr.Microphone() as src:
257
+ st.info("🎤 Adjusting for ambient noise…")
258
+ r.adjust_for_ambient_noise(src, duration=1)
259
+ st.info("Listening…")
260
+ try:
261
+ audio = r.listen(src, timeout=10, phrase_time_limit=10)
262
+ except sr.WaitTimeoutError:
263
+ st.warning("⏰ No speech detected.")
264
+ return ""
265
+ try:
266
+ return r.recognize_google(audio, language=lang_code)
267
+ except sr.UnknownValueError:
268
+ st.error("❗ Could not understand audio.")
269
+ except sr.RequestError as e:
270
+ st.error(f"🚫 Speech API error: {e}")
271
+ return ""
272
+
273
+ # ——— D-ID Avatar Generator ———
274
+ def generate_did_avatar_video(answer_text: str, image_url: str) -> str:
275
+ url = "https://api.d-id.com/talks"
276
+ headers = {
277
+ "Authorization": f"Basic {base64.b64encode(DID_API_KEY.encode()).decode()}",
278
+ "Content-Type": "application/json"
279
+ }
280
+ payload = {
281
+ "source_url": image_url,
282
+ "script": {
283
+ "type": "text",
284
+ "input": answer_text,
285
+ "provider": {
286
+ "type": "microsoft",
287
+ "voice_id": "en-US-GuyNeural", # Default English voice
288
+ "voice_config": {"style": "Cheerful"}
289
+ }
290
+ },
291
+ "config": {"stitch": True}
292
+ }
293
+ response = requests.post(url, json=payload, headers=headers)
294
+ if response.status_code not in [200, 201]:
295
+ st.error(f"❌ Avatar video request failed: {response.text}")
296
+ return ""
297
+ talk_id = response.json().get("id")
298
+ if not talk_id:
299
+ st.error("❌ Talk ID not found in response.")
300
+ return ""
301
+
302
+ # Poll for video status
303
+ for _ in range(30): # Try for up to 60 seconds (30 * 2 seconds)
304
+ time.sleep(2)
305
+ check = requests.get(f"https://api.d-id.com/talks/{talk_id}", headers=headers)
306
+ if check.status_code == 200:
307
+ data = check.json()
308
+ if data.get("status") == "done":
309
+ return data.get("result_url")
310
+ elif data.get("status") == "error":
311
+ st.error(f"❌ D-ID video generation error: {data.get('error')}")
312
+ return ""
313
+ st.warning("⚠️ Avatar video is still processing or timed out.")
314
+ return ""
315
+
316
+ # ——— Main App ———
317
+ def main():
318
+ init_db()
319
+ st.set_page_config(page_title="Voice‑Viz RAG", page_icon="🔊")
320
+ st.title("🔊 AI Helpdesk")
321
+
322
+ # Initialize all session state variables at the top
323
+ if 'rag' not in st.session_state:
324
+ st.session_state.rag = RAGSingleLanguage(GENAI_API_KEY)
325
+ if 'logged_in' not in st.session_state:
326
+ st.session_state.logged_in = False
327
+ st.session_state.user_id = None
328
+ st.session_state.username = None
329
+ if 'selected_doc_id' not in st.session_state:
330
+ st.session_state.selected_doc_id = None
331
+ st.session_state.selected_doc_chunks = []
332
+ if 'current_doc_language' not in st.session_state: # Stores the language of the currently loaded document
333
+ st.session_state.current_doc_language = 'en'
334
+ if 'interaction_language' not in st.session_state: # Stores the language chosen for interaction (can differ from doc lang)
335
+ st.session_state.interaction_language = 'en'
336
+ if 'voice_q' not in st.session_state: # THIS IS THE FIX FOR THE ATTRIBUTEERROR
337
+ st.session_state.voice_q = ""
338
+
339
+ st.sidebar.header("How to use")
340
+ st.sidebar.markdown("""
341
+ 1. Login or Sign Up.
342
+ 2. Upload PDF(s) to your account.
343
+ 3. Select a document from your uploads.
344
+ 4. Confirm or change the interaction language.
345
+ 5. Type or speak your question.
346
+ 6. Read or listen to the AI's answer.
347
+ """)
348
+
349
+ if not st.session_state.logged_in:
350
+ st.subheader("User Authentication")
351
+ auth_option = st.radio("Choose an option:", ("Login", "Sign Up"))
352
+
353
+ with st.form("auth_form"):
354
+ username = st.text_input("Username")
355
+ password = st.text_input("Password", type="password")
356
+ submitted = st.form_submit_button("Submit")
357
+
358
+ if submitted:
359
+ if auth_option == "Login":
360
+ user_id = verify_user(username, password)
361
+ if user_id:
362
+ st.session_state.logged_in = True
363
+ st.session_state.user_id = user_id
364
+ st.session_state.username = username
365
+ st.success(f"Welcome, {username}!")
366
+ st.rerun() # Rerun to switch to the main app view
367
+ else:
368
+ st.error("Invalid username or password.")
369
+ elif auth_option == "Sign Up":
370
+ if add_user(username, password):
371
+ st.success("Account created successfully! Please log in.")
372
+ else:
373
+ st.error("Username already exists. Please choose a different one.")
374
+ else:
375
+ st.sidebar.write(f"Logged in as: **{st.session_state.username}**")
376
+ if st.sidebar.button("Logout"):
377
+ st.session_state.logged_in = False
378
+ st.session_state.user_id = None
379
+ st.session_state.username = None
380
+ st.session_state.selected_doc_id = None
381
+ st.session_state.selected_doc_chunks = []
382
+ st.session_state.current_doc_language = 'en'
383
+ st.session_state.interaction_language = 'en'
384
+ st.session_state.voice_q = "" # Reset voice input
385
+ st.session_state.rag = RAGSingleLanguage(GENAI_API_KEY) # Reset RAG instance
386
+ st.rerun()
387
+
388
+ st.subheader("Document Management")
389
+ uploaded_file = st.file_uploader("Upload your PDF manual(s)", type="pdf", accept_multiple_files=True)
390
+
391
+ if uploaded_file:
392
+ for file in uploaded_file:
393
+ # Check if the file (by name) is already uploaded by this user
394
+ existing_docs = get_user_documents(st.session_state.user_id)
395
+ if file.name in [doc['filename'] for doc in existing_docs]:
396
+ st.info(f"Document '{file.name}' already uploaded by you.")
397
+ continue # Skip to the next file if already exists
398
+
399
+ with st.spinner(f"Processing {file.name}…"):
400
+ # Save PDF to disk
401
+ pdf_path = os.path.join("data", "pdfs", file.name)
402
+ with open(pdf_path, "wb") as f:
403
+ f.write(file.getbuffer())
404
+
405
+ # Process document and get its primary language
406
+ doc_language = st.session_state.rag.process_document(pdf_path)
407
+
408
+ # Save FAISS index
409
+ faiss_index_filename = f"{os.path.splitext(file.name)[0]}_{st.session_state.user_id}.faiss"
410
+ faiss_index_path = os.path.join("data", "faiss_indexes", faiss_index_filename)
411
+ faiss.write_index(st.session_state.rag.faiss_index, faiss_index_path)
412
+
413
+ # Save chunks separately (FAISS only stores embeddings, not the text chunks)
414
+ chunks_filename = f"{os.path.splitext(file.name)[0]}_{st.session_state.user_id}.chunks"
415
+ chunks_path = os.path.join("data", "faiss_indexes", chunks_filename)
416
+ with open(chunks_path, "w", encoding="utf-8") as f:
417
+ # Use a unique delimiter that is unlikely to appear in the text
418
+ f.write("\n--CHUNK_DELIMITER--\n".join(st.session_state.rag.chunks))
419
+
420
+ # Save document metadata to DB
421
+ save_document_metadata(st.session_state.user_id, file.name, pdf_path, faiss_index_path, doc_language)
422
+ st.success(f"✅ Document '{file.name}' processed and saved!")
423
+ st.rerun() # Rerun to refresh the document list
424
+
425
+ # Display and allow selection of user's uploaded documents
426
+ user_docs = get_user_documents(st.session_state.user_id)
427
+ if user_docs:
428
+ doc_options_display = {doc['filename']: doc for doc in user_docs}
429
+ # Add an empty option for "No document selected"
430
+ selected_filename = st.selectbox(
431
+ "Select a document to query:",
432
+ [""] + list(doc_options_display.keys()),
433
+ key="doc_selector" # Add a key to avoid potential widget errors
434
+ )
435
+
436
+ # Logic to load selected document's FAISS index and chunks
437
+ if selected_filename and selected_filename != "":
438
+ selected_doc_info = doc_options_display[selected_filename]
439
+
440
+ # Check if this document is already loaded
441
+ if st.session_state.selected_doc_id != selected_doc_info['id']:
442
+ st.session_state.selected_doc_id = selected_doc_info['id']
443
+
444
+ chunks_filename = f"{os.path.splitext(selected_doc_info['filename'])[0]}_{st.session_state.user_id}.chunks"
445
+ chunks_path = os.path.join("data", "faiss_indexes", chunks_filename)
446
+
447
+ if os.path.exists(chunks_path):
448
+ with open(chunks_path, "r", encoding="utf-8") as f:
449
+ st.session_state.selected_doc_chunks = f.read().split("\n--CHUNK_DELIMITER--\n")
450
+ else:
451
+ st.error("Error: Chunks file not found for this document.")
452
+ st.session_state.selected_doc_chunks = []
453
+ st.session_state.selected_doc_id = None # Invalidate selection
454
+
455
+ if st.session_state.selected_doc_id and \
456
+ st.session_state.rag.load_faiss_index(selected_doc_info['faiss_index_path'], st.session_state.selected_doc_chunks):
457
+ st.success(f"Selected document: '{selected_filename}'")
458
+ # Set the detected language of the document
459
+ st.session_state.current_doc_language = selected_doc_info['language']
460
+ st.session_state.interaction_language = selected_doc_info['language'] # Default interaction language to doc's
461
+ st.session_state.rag.set_language(st.session_state.interaction_language)
462
+ st.rerun() # Rerun to update language selector and clear old inputs
463
+ else:
464
+ st.error(f"Could not load FAISS index for '{selected_filename}'.")
465
+ st.session_state.selected_doc_id = None
466
+ st.session_state.rag.faiss_index = None
467
+ st.session_state.rag.chunks = []
468
+ st.session_state.current_doc_language = 'en'
469
+ st.session_state.interaction_language = 'en'
470
+
471
+ # If a document is selected and loaded, allow language choice for interaction
472
+ if st.session_state.selected_doc_id:
473
+ st.markdown("---") # Separator for clarity
474
+ st.markdown("**Choose Interaction Language**")
475
+
476
+ # You could fetch all detected languages from the processed document if desired
477
+ # For simplicity, we'll offer a few common ones, plus the detected document language
478
+ available_langs = sorted(list(set(['en', 'hi', 'fr', 'es', 'de', st.session_state.current_doc_language])))
479
+ # Remove duplicates and ensure the current_doc_language is an option
480
+
481
+ lang_selection = st.selectbox(
482
+ "Select the language for your question and the AI's answer:",
483
+ [lang.upper() for lang in available_langs],
484
+ index=available_langs.index(st.session_state.interaction_language) if st.session_state.interaction_language in available_langs else 0,
485
+ key="interaction_lang_selector"
486
+ )
487
+
488
+ if lang_selection:
489
+ new_lang = lang_selection.lower()
490
+ if new_lang != st.session_state.interaction_language:
491
+ st.session_state.interaction_language = new_lang
492
+ st.session_state.rag.set_language(new_lang)
493
+ st.rerun() # Rerun to update the question input field's language
494
+
495
+ st.markdown(f"**Asking in:** `{st.session_state.interaction_language.upper()}`")
496
+ st.markdown("---") # Separator
497
+
498
+ st.markdown("**Type your question**")
499
+ typed_question = st.text_input(f"Ask in {st.session_state.interaction_language.upper()}:", value=st.session_state.voice_q, key="typed_question_input")
500
+
501
+ st.markdown("**Or use voice input**")
502
+ if st.button("🎙️ Speak Your Question", key="speak_button"):
503
+ # Adjust language code for speech recognition based on interaction language
504
+ recognizer_lang_code = st.session_state.interaction_language
505
+ if recognizer_lang_code == "en":
506
+ recognizer_lang_code = "en-IN" # Default to Indian English for better recognition in some cases
507
+ elif recognizer_lang_code == "hi":
508
+ recognizer_lang_code = "hi-IN" # Hindi
509
+ # Add more specific regional codes if necessary for other languages
510
+
511
+ recd_speech = recognize_voice(recognizer_lang_code)
512
+ if recd_speech:
513
+ st.session_state.voice_q = recd_speech
514
+ st.success(f"🎤 You said: {recd_speech}")
515
+ st.rerun() # Rerun to populate the text input with spoken text
516
+ else:
517
+ st.warning("No speech recognized.")
518
+
519
+ # Use the typed input or the voice input if available
520
+ question_to_process = typed_question or st.session_state.voice_q
521
+
522
+ if st.button("Get Answer", key="get_answer_button") and question_to_process:
523
+ st.markdown(f"🔍 Question: `{question_to_process}`")
524
+ with st.spinner("Thinking…"):
525
+ answer = st.session_state.rag.answer_question(question_to_process)
526
+ st.markdown(f"**Answer ({st.session_state.interaction_language.upper()}):** {answer}")
527
+
528
+ # Save to DB
529
+ save_interaction(st.session_state.user_id, st.session_state.interaction_language, question_to_process, answer)
530
+
531
+ # Text-to-Speech (gTTS)
532
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as fp:
533
+ try:
534
+ gTTS(text=answer, lang=st.session_state.interaction_language).save(fp.name)
535
+ mp3_bytes = open(fp.name, "rb").read()
536
+ b64 = base64.b64encode(mp3_bytes).decode()
537
+
538
+ html = f"""
539
+ <audio id='player' controls>
540
+ <source src='data:audio/mp3;base64,{b64}' type='audio/mp3'/>
541
+ </audio>
542
+ <canvas id='canvas' width='300' height='100'></canvas>
543
+ <script>
544
+ const audio = document.getElementById('player');
545
+ const canvas = document.getElementById('canvas');
546
+ const ctx = canvas.getContext('2d');
547
+ const audioCtx = new (window.AudioContext||window.webkitAudioContext)();
548
+ const source = audioCtx.createMediaElementSource(audio);
549
+ const analyser = audioCtx.createAnalyser();
550
+ analyser.fftSize = 256;
551
+ source.connect(analyser);
552
+ analyser.connect(audioCtx.destination);
553
+ const data = new Uint8Array(analyser.frequencyBinCount);
554
+ function drawLine() {{
555
+ requestAnimationFrame(drawLine);
556
+ analyser.getByteTimeDomainData(data);
557
+ let sum = 0;
558
+ for (let i=0; i<data.length; i++) {{
559
+ const v = data[i] - 128;
560
+ sum += v*v;
561
+ }}
562
+ const rms = Math.sqrt(sum/data.length);
563
+ const maxLen = canvas.width / 2 * (rms/128);
564
+ const y = canvas.height / 2;
565
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
566
+ ctx.beginPath();
567
+ ctx.moveTo((canvas.width / 2) - maxLen, y);
568
+ ctx.lineTo((canvas.width / 2) + maxLen, y);
569
+ ctx.lineWidth = 4;
570
+ ctx.strokeStyle = '#4CAF50';
571
+ ctx.stroke();
572
+ }}
573
+ audio.onplay = () => {{
574
+ audioCtx.resume().then(() => drawLine());
575
+ }};
576
+ </script>
577
+ """
578
+ components.html(html, height=150)
579
+ except Exception as e:
580
+ st.error(f"Error generating audio: {e}. Please ensure gTTS supports '{st.session_state.interaction_language}'.")
581
+
582
+ # Clear voice_q after processing the answer
583
+ st.session_state.voice_q = ""
584
+
585
+ st.markdown("### 🧑‍💼 Speaking AI Avatar")
586
+ with st.spinner("Generating avatar video…"):
587
+ video_url = generate_did_avatar_video(answer, AVATAR_IMAGE_URL)
588
+ if video_url:
589
+ st.video(video_url)
590
+ else:
591
+ st.error("Failed to load avatar video.")
592
+ elif st.button("Get Answer") and not question_to_process:
593
+ st.warning("Please enter or speak a question.")
594
+ else:
595
+ st.info("Please select a document from your uploaded files to start querying.")
596
+ # Reset RAG if no document is selected
597
+ st.session_state.selected_doc_id = None
598
+ st.session_state.rag.faiss_index = None
599
+ st.session_state.rag.chunks = []
600
+ st.session_state.current_doc_language = 'en'
601
+ st.session_state.interaction_language = 'en'
602
+ st.session_state.rag.set_language('en') # Reset RAG's internal language
603
+ else:
604
+ st.info("No documents uploaded yet. Please upload a PDF to begin.")
605
+
606
+ # --- Optional: Admin View ---
607
+ st.sidebar.markdown("---")
608
+ st.sidebar.header("Admin Views")
609
+
610
+ if st.sidebar.checkbox("📜 Show Past Interactions"):
611
+ if st.session_state.logged_in:
612
+ conn = sqlite3.connect("interactions.db")
613
+ df = pd.read_sql_query(f"SELECT timestamp, language, question, answer FROM interactions WHERE user_id = {st.session_state.user_id} ORDER BY timestamp DESC", conn)
614
+ if not df.empty:
615
+ st.sidebar.dataframe(df)
616
+ else:
617
+ st.sidebar.info("No past interactions for this user.")
618
+ conn.close()
619
+ else:
620
+ st.sidebar.warning("Please log in to view past interactions.")
621
+
622
+ if st.sidebar.checkbox("📂 Show My Uploaded Documents"):
623
+ if st.session_state.logged_in:
624
+ user_docs = get_user_documents(st.session_state.user_id)
625
+ if user_docs:
626
+ df_docs = pd.DataFrame(user_docs)
627
+ st.sidebar.dataframe(df_docs[['filename', 'language']])
628
+ else:
629
+ st.sidebar.info("No documents uploaded yet.")
630
+ else:
631
+ st.sidebar.warning("Please log in to view your uploaded documents.")
632
+
633
+ if __name__ == "__main__":
634
+ main()