MohitGupta41 commited on
Commit
4390dd3
·
1 Parent(s): 87e65bc

Final Commit

Browse files
Files changed (4) hide show
  1. .streamlit/config.toml +4 -2
  2. Dockerfile +1 -15
  3. app.py +240 -121
  4. requirements.txt +3 -1
.streamlit/config.toml CHANGED
@@ -3,6 +3,8 @@ gatherUsageStats = false
3
 
4
  [server]
5
  headless = true
6
- enableCORS = true
7
- enableXsrfProtection = true
8
  fileWatcherType = "auto"
 
 
 
3
 
4
  [server]
5
  headless = true
6
+ enableCORS = false
7
+ enableXsrfProtection = false
8
  fileWatcherType = "auto"
9
+
10
+ maxUploadSize = 2000
Dockerfile CHANGED
@@ -1,18 +1,4 @@
1
- # FROM python:3.12-slim
2
-
3
- # RUN apt-get update && apt-get install -y --no-install-recommends \
4
- # libgl1 libglib2.0-0 && \
5
- # rm -rf /var/lib/apt/lists/*
6
-
7
- # WORKDIR /app
8
- # COPY requirements.txt .
9
- # RUN pip install --no-cache-dir -r requirements.txt
10
-
11
- # COPY app.py .
12
- # ENV PORT=7860
13
- # CMD ["streamlit", "run", "app.py", "--server.port=7860", "--server.address=0.0.0.0"]
14
-
15
- FROM python:3.11-slim
16
 
17
  # System deps
18
  RUN apt-get update && apt-get install -y --no-install-recommends \
 
1
+ FROM python:3.12-slim
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  # System deps
4
  RUN apt-get update && apt-get install -y --no-install-recommends \
app.py CHANGED
@@ -3,6 +3,8 @@ from typing import Optional, Tuple
3
  import requests
4
  from PIL import Image, ImageOps, ImageDraw
5
  import streamlit as st
 
 
6
 
7
  # -----------------------
8
  # Config
@@ -23,7 +25,6 @@ with st.sidebar:
23
  if _tok.strip():
24
  HF_TOKEN = _tok.strip()
25
 
26
- # Helpers
27
  def _headers():
28
  h = {"Accept": "application/json"}
29
  if HF_TOKEN:
@@ -50,19 +51,15 @@ def pil_from_upload(file) -> Optional[Image.Image]:
50
  return None
51
 
52
  def compress_and_b64(img: Image.Image, max_side: int = 1280, quality: int = 85):
53
- img = ImageOps.exif_transpose(img) # same orientation as backend
54
  w0, h0 = img.size
55
  scale = max(w0, h0) / max_side if max(w0, h0) > max_side else 1.0
56
- if scale > 1.0:
57
- img_proc = img.resize((int(w0/scale), int(h0/scale)))
58
- else:
59
- img_proc = img
60
 
61
  buf = io.BytesIO()
62
  img_proc.save(buf, format="JPEG", quality=quality, optimize=True)
63
  b64 = base64.b64encode(buf.getvalue()).decode()
64
- return b64, img_proc, (w0, h0), img_proc.size # also return sizes if you ever want to rescale back
65
-
66
 
67
  def draw_bbox(img: Image.Image, bbox: list[int], color=(0, 255, 0), width: int = 4) -> Image.Image:
68
  out = img.copy()
@@ -71,6 +68,173 @@ def draw_bbox(img: Image.Image, bbox: list[int], color=(0, 255, 0), width: int =
71
  draw.rectangle([x1, y1, x2, y2], outline=color, width=width)
72
  return out
73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  # -----------------------
75
  # UI
76
  # -----------------------
@@ -91,48 +255,9 @@ if not ok and info:
91
  if "user_name" not in st.session_state:
92
  st.session_state.user_name = "mohit"
93
 
94
- # # -----------------------
95
- # # 1) Enroll / Upsert face
96
- # # -----------------------
97
- # with st.expander("1) Enroll / Upsert face (optional)", expanded=False):
98
- # st.session_state.user_name = st.text_input("User name", value=st.session_state.user_name, key="user_name_input")
99
- # c1, c2 = st.columns(2)
100
- # with c1:
101
- # upload_img = st.file_uploader("Upload a face image (jpg/png)", type=["jpg","jpeg","png"])
102
- # with c2:
103
- # cam_img = st.camera_input("Or capture from camera")
104
-
105
- # chosen = None
106
- # if cam_img is not None:
107
- # chosen = pil_from_upload(cam_img)
108
- # elif upload_img is not None:
109
- # chosen = pil_from_upload(upload_img)
110
-
111
- # if st.button("Upsert to local index"):
112
- # if not chosen:
113
- # st.error("Please provide an image (upload or camera).")
114
- # elif not st.session_state.user_name.strip():
115
- # st.error("Please enter a user name.")
116
- # else:
117
- # buf = io.BytesIO()
118
- # chosen.save(buf, format="JPEG", quality=90)
119
- # buf.seek(0)
120
- # try:
121
- # with st.spinner("Upserting…"):
122
- # resp = post_multipart(
123
- # "/index/upsert_image",
124
- # files={"image": ("face.jpg", buf, "image/jpeg")},
125
- # params={"user": st.session_state.user_name.strip()},
126
- # )
127
- # if resp.ok:
128
- # st.success("Face vector upserted ✅")
129
- # st.json(resp.json())
130
- # else:
131
- # st.error(f"Upsert failed: {resp.status_code}")
132
- # st.text(resp.text)
133
- # except Exception as e:
134
- # st.error(f"Request error: {e}")
135
-
136
  with st.expander("1) Bulk enroll via ZIP (Images/<UserName>/*)", expanded=False):
137
  zip_up = st.file_uploader("Upload ZIP", type=["zip"], key="zip_enroll")
138
  if st.button("Enroll ZIP"):
@@ -152,6 +277,9 @@ with st.expander("1) Bulk enroll via ZIP (Images/<UserName>/*)", expanded=False)
152
  except Exception as e:
153
  st.error(f"Request error: {e}")
154
 
 
 
 
155
  with st.expander("2) Identify from image", expanded=False):
156
  col_u, col_c = st.columns(2)
157
  with col_u:
@@ -165,13 +293,7 @@ with st.expander("2) Identify from image", expanded=False):
165
  elif test_upload is not None:
166
  test_img = pil_from_upload(test_upload)
167
 
168
- # --- helpers -------------------------------------------------------------
169
  def encode_for_backend(img: Image.Image):
170
- """
171
- Returns (b64_str, sent_img) where sent_img is decoded from the exact
172
- bytes we POST to the backend so bbox coords align 1:1.
173
- Works whether compress_and_b64 returns a str or a tuple/list.
174
- """
175
  b64_out = compress_and_b64(img)
176
  if isinstance(b64_out, (tuple, list)):
177
  b64_str = b64_out[0]
@@ -189,7 +311,6 @@ with st.expander("2) Identify from image", expanded=False):
189
  return b64_str, sent_img
190
 
191
  def draw_many(img: Image.Image, dets: list[dict]) -> Image.Image:
192
- from PIL import ImageDraw
193
  out = img.copy()
194
  draw = ImageDraw.Draw(out)
195
  for d in dets:
@@ -197,15 +318,11 @@ with st.expander("2) Identify from image", expanded=False):
197
  name = d.get("decision", "Unknown")
198
  score = float(d.get("best_score", 0.0))
199
  label = f"{name} ({score:.3f})"
200
-
201
- # box
202
  draw.rectangle([x1, y1, x2, y2], outline=(0, 255, 0), width=3)
203
-
204
  try:
205
  tb = draw.textbbox((x1, y1), label)
206
  tw, th = tb[2] - tb[0], tb[3] - tb[1]
207
  except Exception:
208
- # fallback: approximate height
209
  tw, th = max(60, len(label) * 7), 14
210
  by1 = max(0, y1 - th - 6)
211
  draw.rectangle([x1, by1, x1 + tw + 6, y1], fill=(0, 0, 0))
@@ -217,7 +334,7 @@ with st.expander("2) Identify from image", expanded=False):
217
  st.warning("Please provide an image first.")
218
  else:
219
  try:
220
- b64, sent_img = encode_for_backend(test_img) # <- draw on sent image
221
  with st.spinner("Identifying…"):
222
  r = post_json("/identify_many", {"image_b64": b64, "top_k": 3})
223
  if not r.ok:
@@ -234,74 +351,76 @@ with st.expander("2) Identify from image", expanded=False):
234
  except Exception as e:
235
  st.error(f"Request error: {e}")
236
 
237
-
238
  # -----------------------
239
- # 3) Ask a BI question
240
  # -----------------------
241
- st.subheader("3) Ask a BI question")
 
 
 
242
 
243
- with st.expander("Examples", expanded=False):
244
- cols = st.columns(2)
245
- examples = [
246
- "What is total sales (revenue) of Ramesh?",
247
- "Revenue for BLR on 2025-09-06",
248
- "Monthly revenue for Electronics in BLR for 2025-09",
249
- "Top 5 SKUs by revenue in HYD on 2025-09-06 (include category)",
250
- "Ramesh's total sales in NCR on 2025-09-06",
251
- ]
252
- for i, ex in enumerate(examples):
253
- if cols[i % 2].button(ex, key=f"ex_{i}"):
254
- st.session_state.setdefault("q_text", ex)
255
-
256
- default_q = st.session_state.get("q_text", "What is total sales (revenue) of Ramesh?")
257
- q_text = st.text_area("Your question", value=default_q, height=100)
258
-
259
- with st.expander("Optional: visual context (JSON)", expanded=False):
260
- vis_str = st.text_area("visual_ctx", value="{}", height=80)
261
- try:
262
- visual_ctx = json.loads(vis_str) if vis_str.strip() else {}
263
- except Exception:
264
- visual_ctx = {}
265
- st.warning("`visual_ctx` is not valid JSON; ignored.")
266
-
267
- if st.button("Ask"):
268
- payload = {
269
- "user_id": st.session_state.user_name or None,
270
- "text": q_text.strip(),
271
- "visual_ctx": visual_ctx,
272
- }
273
- try:
274
- with st.spinner("Querying…"):
275
- r = post_json("/query", payload)
276
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
  if r.ok:
278
  resp = r.json()
279
- st.success(resp.get("answer_text", ""))
280
-
281
- sqls = [c[4:] for c in resp.get("citations", []) if isinstance(c, str) and c.startswith("sql:")]
282
- if sqls:
283
- with st.expander("SQL used", expanded=True):
284
- st.code(sqls[0], language="sql")
285
- if len(sqls) > 1:
286
- for s in sqls[1:]:
287
- st.code(s, language="sql")
288
-
289
- if resp.get("metrics"):
290
- with st.expander("Metrics", expanded=False):
291
- st.json(resp["metrics"])
292
- if resp.get("chart_refs"):
293
- with st.expander("Charts", expanded=False):
294
- st.json(resp["chart_refs"])
295
- if "uncertainty" in resp:
296
- st.caption(f"Uncertainty: {resp['uncertainty']:.2f}")
297
-
298
  else:
299
  try:
300
  err = r.json()
301
  except Exception:
302
  err = {"detail": r.text}
 
303
  st.error(f"Backend error {r.status_code}: {err.get('detail')}")
304
- if "SQLGenTool disabled" in str(err.get("detail", "")):
305
- st.info("Add your Hugging Face token in the sidebar (or set the HF_TOKEN env var).")
306
- except Exception as e:
307
- st.error(f"Request error: {e}")
 
 
 
 
 
3
  import requests
4
  from PIL import Image, ImageOps, ImageDraw
5
  import streamlit as st
6
+ from streamlit_mic_recorder import mic_recorder, speech_to_text
7
+ from gtts import gTTS
8
 
9
  # -----------------------
10
  # Config
 
25
  if _tok.strip():
26
  HF_TOKEN = _tok.strip()
27
 
 
28
  def _headers():
29
  h = {"Accept": "application/json"}
30
  if HF_TOKEN:
 
51
  return None
52
 
53
  def compress_and_b64(img: Image.Image, max_side: int = 1280, quality: int = 85):
54
+ img = ImageOps.exif_transpose(img)
55
  w0, h0 = img.size
56
  scale = max(w0, h0) / max_side if max(w0, h0) > max_side else 1.0
57
+ img_proc = img.resize((int(w0/scale), int(h0/scale))) if scale > 1.0 else img
 
 
 
58
 
59
  buf = io.BytesIO()
60
  img_proc.save(buf, format="JPEG", quality=quality, optimize=True)
61
  b64 = base64.b64encode(buf.getvalue()).decode()
62
+ return b64, img_proc, (w0, h0), img_proc.size
 
63
 
64
  def draw_bbox(img: Image.Image, bbox: list[int], color=(0, 255, 0), width: int = 4) -> Image.Image:
65
  out = img.copy()
 
68
  draw.rectangle([x1, y1, x2, y2], outline=color, width=width)
69
  return out
70
 
71
+ def tts_gtts_bytes(text: str, lang: str = "en", tld: str = "com", slow: bool = False) -> bytes:
72
+ buf = io.BytesIO()
73
+ gTTS(text=text, lang=lang, tld=tld, slow=slow).write_to_fp(buf)
74
+ return buf.getvalue()
75
+
76
+ # --- Chat state helpers ---
77
+ if "chat" not in st.session_state:
78
+ st.session_state.chat = [] # list of {"role": "user"|"assistant", "text": str}
79
+
80
+ def add_chat(role: str, text: str):
81
+ st.session_state.chat.append({"role": role, "text": text})
82
+
83
+ def render_chat_transcript():
84
+ st.subheader("🗨️ Conversation")
85
+ for m in st.session_state.chat[-100:]: # show last 100 turns
86
+ with st.chat_message("user" if m["role"]=="user" else "assistant"):
87
+ st.markdown(m["text"])
88
+
89
+ # -----------------------
90
+ # Small UI renderers (so we can reorder cleanly)
91
+ # -----------------------
92
+ def render_examples_buttons(key_prefix: str = "main"):
93
+ cols = st.columns(2)
94
+ examples = [
95
+ "What is total sales (revenue) of Ramesh?",
96
+ "Revenue for BLR on 2025-09-06",
97
+ "Monthly revenue for Electronics in BLR for 2025-09",
98
+ "Top 5 SKUs by revenue in HYD on 2025-09-06 (include category)",
99
+ "Ramesh's total sales in NCR on 2025-09-06",
100
+ ]
101
+ for i, ex in enumerate(examples):
102
+ if cols[i % 2].button(ex, key=f"{key_prefix}_ex_{i}"):
103
+ st.session_state["q_text"] = ex
104
+ st.rerun()
105
+
106
+
107
+ def render_bi_question_section(section_heading=True, key_prefix: str = "main"):
108
+ if section_heading:
109
+ st.subheader("3) Ask a BI question")
110
+
111
+ with st.expander("Examples", expanded=False):
112
+ render_examples_buttons(key_prefix=key_prefix)
113
+
114
+ # Use a unique key for the textarea.
115
+ default_q = st.session_state.get("q_text", "What is total sales (revenue) of Ramesh?")
116
+ q_text = st.text_area("Your question", value=default_q, height=100,
117
+ key=f"{key_prefix}_q_textarea")
118
+
119
+ with st.expander("Optional: visual context (JSON)", expanded=False):
120
+ vis_str = st.text_area("visual_ctx", value="{}", height=80,
121
+ key=f"{key_prefix}_vis_text")
122
+
123
+ try:
124
+ visual_ctx = json.loads(vis_str) if vis_str.strip() else {}
125
+ except Exception:
126
+ visual_ctx = {}
127
+ st.warning("`visual_ctx` is not valid JSON; ignored.")
128
+
129
+ if st.button("Ask", key=f"{key_prefix}_ask"):
130
+ payload = {
131
+ "user_id": st.session_state.user_name or None,
132
+ "text": q_text.strip(),
133
+ "visual_ctx": visual_ctx,
134
+ }
135
+ try:
136
+ with st.spinner("Querying…"):
137
+ r = post_json("/query", payload)
138
+
139
+ if r.ok:
140
+ resp = r.json()
141
+ answer = resp.get("answer_text", "")
142
+ st.success(answer)
143
+ st.session_state["last_answer_text"] = answer
144
+
145
+ sqls = [c[4:] for c in resp.get("citations", [])
146
+ if isinstance(c, str) and c.startswith("sql:")]
147
+ if sqls:
148
+ with st.expander("SQL used", expanded=True):
149
+ st.code(sqls[0], language="sql")
150
+ for s in sqls[1:]:
151
+ st.code(s, language="sql")
152
+
153
+ if resp.get("metrics"):
154
+ with st.expander("Metrics", expanded=False):
155
+ st.json(resp["metrics"])
156
+ if resp.get("chart_refs"):
157
+ with st.expander("Charts", expanded=False):
158
+ st.json(resp["chart_refs"])
159
+ if "uncertainty" in resp:
160
+ st.caption(f"Uncertainty: {resp['uncertainty']:.2f}")
161
+ else:
162
+ try:
163
+ err = r.json()
164
+ except Exception:
165
+ err = {"detail": r.text}
166
+ st.error(f"Backend error {r.status_code}: {err.get('detail')}")
167
+ if "SQLGenTool disabled" in str(err.get("detail", "")):
168
+ st.info("Add your Hugging Face token in the sidebar (or set the HF_TOKEN env var).")
169
+ except Exception as e:
170
+ st.error(f"Request error: {e}")
171
+
172
+ def render_voice_to_text():
173
+ st.caption("Voice → Text (browser STT)")
174
+ c1, c2 = st.columns([2, 1])
175
+ with c1:
176
+ st.write("Click to speak; recognized text will fill the question box.")
177
+ with c2:
178
+ stt_lang = st.selectbox("STT language", ["en", "hi"], index=0, key="stt_lang_dd")
179
+ if "prev_stt_lang" not in st.session_state:
180
+ st.session_state["prev_stt_lang"] = stt_lang
181
+ elif st.session_state["prev_stt_lang"] != stt_lang:
182
+ st.session_state["prev_stt_lang"] = stt_lang
183
+ st.rerun()
184
+
185
+ stt_text = speech_to_text(
186
+ language=st.session_state.get("stt_lang_dd", "en"),
187
+ use_container_width=True,
188
+ just_once=True,
189
+ start_prompt="🎙️ Start recording",
190
+ stop_prompt="⏹️ Stop recording",
191
+ key="stt_main_btn",
192
+ )
193
+
194
+ if stt_text:
195
+ st.session_state["q_text"] = stt_text
196
+ st.success(f"Recognized: {stt_text}")
197
+ st.rerun()
198
+
199
+ st.markdown("---")
200
+ st.caption("Optional: record & play raw audio (no transcription)")
201
+ rec = mic_recorder(
202
+ start_prompt="🎙️ Start",
203
+ stop_prompt="⏹️ Stop",
204
+ just_once=True,
205
+ key="mic_raw_btn",
206
+ )
207
+ if rec and rec.get("bytes"):
208
+ st.audio(rec["bytes"], format="audio/wav")
209
+
210
+ def render_tts_controls():
211
+ st.markdown("---")
212
+ st.caption("Text → Voice (gTTS)")
213
+ tts_lang = st.selectbox("TTS language", ["en", "hi"], index=0, key="tts_lang_dd")
214
+ tld_label = st.selectbox(
215
+ "Accent / region (tld)",
216
+ ["Default (.com)", "India (.co.in)", "US (.us)", "UK (.co.uk)"],
217
+ index=1,
218
+ key="tts_tld_dd"
219
+ )
220
+ tld_map = {
221
+ "Default (.com)": "com",
222
+ "India (.co.in)": "co.in",
223
+ "US (.us)": "us",
224
+ "UK (.co.uk)": "co.uk",
225
+ }
226
+
227
+ if st.button("🔊 Speak last answer", key="tts_speak_btn"):
228
+ ans = st.session_state.get("last_answer_text", "")
229
+ if not ans.strip():
230
+ st.warning("Ask a question first to generate an answer.")
231
+ else:
232
+ try:
233
+ mp3 = tts_gtts_bytes(ans, lang=tts_lang, tld=tld_map[tld_label], slow=False)
234
+ st.audio(mp3, format="audio/mp3")
235
+ except Exception as e:
236
+ st.error(f"TTS error: {e}")
237
+
238
  # -----------------------
239
  # UI
240
  # -----------------------
 
255
  if "user_name" not in st.session_state:
256
  st.session_state.user_name = "mohit"
257
 
258
+ # -----------------------
259
+ # 1) Bulk enroll via ZIP (Images/<UserName>/*)
260
+ # -----------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
  with st.expander("1) Bulk enroll via ZIP (Images/<UserName>/*)", expanded=False):
262
  zip_up = st.file_uploader("Upload ZIP", type=["zip"], key="zip_enroll")
263
  if st.button("Enroll ZIP"):
 
277
  except Exception as e:
278
  st.error(f"Request error: {e}")
279
 
280
+ # -----------------------
281
+ # 2) Identify from image
282
+ # -----------------------
283
  with st.expander("2) Identify from image", expanded=False):
284
  col_u, col_c = st.columns(2)
285
  with col_u:
 
293
  elif test_upload is not None:
294
  test_img = pil_from_upload(test_upload)
295
 
 
296
  def encode_for_backend(img: Image.Image):
 
 
 
 
 
297
  b64_out = compress_and_b64(img)
298
  if isinstance(b64_out, (tuple, list)):
299
  b64_str = b64_out[0]
 
311
  return b64_str, sent_img
312
 
313
  def draw_many(img: Image.Image, dets: list[dict]) -> Image.Image:
 
314
  out = img.copy()
315
  draw = ImageDraw.Draw(out)
316
  for d in dets:
 
318
  name = d.get("decision", "Unknown")
319
  score = float(d.get("best_score", 0.0))
320
  label = f"{name} ({score:.3f})"
 
 
321
  draw.rectangle([x1, y1, x2, y2], outline=(0, 255, 0), width=3)
 
322
  try:
323
  tb = draw.textbbox((x1, y1), label)
324
  tw, th = tb[2] - tb[0], tb[3] - tb[1]
325
  except Exception:
 
326
  tw, th = max(60, len(label) * 7), 14
327
  by1 = max(0, y1 - th - 6)
328
  draw.rectangle([x1, by1, x1 + tw + 6, y1], fill=(0, 0, 0))
 
334
  st.warning("Please provide an image first.")
335
  else:
336
  try:
337
+ b64, sent_img = encode_for_backend(test_img)
338
  with st.spinner("Identifying…"):
339
  r = post_json("/identify_many", {"image_b64": b64, "top_k": 3})
340
  if not r.ok:
 
351
  except Exception as e:
352
  st.error(f"Request error: {e}")
353
 
 
354
  # -----------------------
355
+ # 2.5) Voice mode (frontend-only) with requested order
356
  # -----------------------
357
+ st.subheader("🎙️ Voice mode (optional)")
358
+ with st.expander("Speak your question / hear the answer", expanded=True):
359
+ # (1) Voice → Text first
360
+ render_voice_to_text()
361
 
362
+ # (2) Ask a BI question (same logic as main section)
363
+ render_bi_question_section(section_heading=False, key_prefix="voice")
364
+
365
+ # (3) Listen response (TTS button)
366
+ render_tts_controls()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
 
368
+ # -----------------------
369
+ # 2.6) Talk → Ask → Speak (voice chat with transcript)
370
+ # -----------------------
371
+ st.subheader("🗣️ Talk → Ask → Speak")
372
+ c_left, c_right = st.columns([2, 3])
373
+ with c_left:
374
+ st.caption("Press to speak; we'll answer, speak back, and log the chat below.")
375
+ with c_right:
376
+ # voice settings reuse your TTS controls' state if present; else defaults
377
+ tts_lang = st.session_state.get("tts_lang_dd", "en")
378
+ tld_map = {"Default (.com)": "com", "India (.co.in)": "co.in", "US (.us)": "us", "UK (.co.uk)": "co.uk"}
379
+ tld_label = st.session_state.get("tts_tld_dd", "India (.co.in)")
380
+
381
+ # Mic widget (one utterance per click)
382
+ spoken = speech_to_text(
383
+ language=st.session_state.get("stt_lang_dd", "en"),
384
+ use_container_width=True,
385
+ just_once=True,
386
+ start_prompt="🎙️ Speak",
387
+ stop_prompt="⏹️ Stop",
388
+ key="stt_conv_btn",
389
+ )
390
+
391
+ if spoken:
392
+ user_text = spoken.strip()
393
+ if user_text:
394
+ add_chat("user", user_text)
395
+ payload = {"user_id": st.session_state.user_name or None, "text": user_text, "visual_ctx": {}}
396
+ with st.spinner("Thinking…"):
397
+ r = post_json("/query", payload)
398
  if r.ok:
399
  resp = r.json()
400
+ answer = resp.get("answer_text", "").strip()
401
+ add_chat("assistant", answer or "_(no rows)_")
402
+ st.session_state["last_answer_text"] = answer
403
+ # speak the answer
404
+ try:
405
+ mp3 = tts_gtts_bytes(answer or "I have no rows to report.",
406
+ lang=tts_lang,
407
+ tld=tld_map.get(tld_label, "co.in"),
408
+ slow=False)
409
+ st.audio(mp3, format="audio/mp3")
410
+ except Exception as e:
411
+ st.error(f"TTS error: {e}")
 
 
 
 
 
 
 
412
  else:
413
  try:
414
  err = r.json()
415
  except Exception:
416
  err = {"detail": r.text}
417
+ add_chat("assistant", f"Backend error {r.status_code}: {err.get('detail')}")
418
  st.error(f"Backend error {r.status_code}: {err.get('detail')}")
419
+
420
+ # Show running transcript
421
+ render_chat_transcript()
422
+
423
+ # -----------------------
424
+ # 3) Ask a BI question (also kept as a main section for non-voice users)
425
+ # -----------------------
426
+ # render_bi_question_section(section_heading=True, key_prefix="main")
requirements.txt CHANGED
@@ -1,3 +1,5 @@
1
  streamlit
2
  requests
3
- Pillow
 
 
 
1
  streamlit
2
  requests
3
+ Pillow
4
+ streamlit-mic-recorder
5
+ gTTS