Files changed (8) hide show
  1. README.md +2 -2
  2. app.py +154 -133
  3. ingest.py +0 -55
  4. llm.py +0 -95
  5. rag_pipeline.py +15 -107
  6. requirements.txt +3 -11
  7. stt.py +0 -36
  8. tts.py +0 -63
README.md CHANGED
@@ -4,8 +4,8 @@ emoji: 🏦
4
  colorFrom: yellow
5
  colorTo: green
6
  sdk: gradio
7
- sdk_version: 5.16.1
8
- python_version: 3.11
9
  app_file: app.py
10
  pinned: true
11
  license: mit
 
4
  colorFrom: yellow
5
  colorTo: green
6
  sdk: gradio
7
+ sdk_version: 5.9.1
8
+ python: 13.1
9
  app_file: app.py
10
  pinned: true
11
  license: mit
app.py CHANGED
@@ -1,88 +1,101 @@
1
- # STARTUP:
2
- # 1. pip install -r requirements.txt
3
- # 2. python app.py ← start the app (auto-builds index)
 
 
 
 
 
4
 
5
  import os
6
  import io
7
  import numpy as np
8
  import soundfile as sf
9
  import gradio as gr
10
-
11
- import stt
12
- import llm
13
- import tts
14
- import rag_pipeline
 
 
 
 
15
 
16
  # ─────────────────────────────────────────────
17
- # AUTO INGEST — builds ChromaDB on first startup
18
- # Runs automatically if chroma_db folder not found
19
  # ─────────────────────────────────────────────
20
- import os
21
- if not os.path.exists("./chroma_db"):
22
- print("ChromaDB not found — building knowledge base index...")
23
- try:
24
- from knowledge_base import KNOWLEDGE_BASE
25
- from sentence_transformers import SentenceTransformer
26
- import chromadb
27
 
28
- print("Loading embedding model for ingest...")
29
- _embedder = SentenceTransformer(
30
- 'sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2'
31
- )
32
- _chroma = chromadb.PersistentClient(path="./chroma_db")
33
- _collection = _chroma.get_or_create_collection("banking_hindi")
34
-
35
- _documents = []
36
- _metadatas = []
37
- _ids = []
38
-
39
- for doc in KNOWLEDGE_BASE:
40
- full_text = f"{doc['title']}\n{doc['content']}"
41
- _documents.append(full_text)
42
- _metadatas.append({
43
- "id": doc["id"],
44
- "title": doc["title"],
45
- "category": doc["category"]
46
- })
47
- _ids.append(doc["id"])
48
-
49
- print(f"Embedding {len(_documents)} documents... (takes 2-3 min on first run)")
50
- _embeddings = _embedder.encode(
51
- _documents,
52
- show_progress_bar=True,
53
- batch_size=8
54
- ).tolist()
55
-
56
- _collection.add(
57
- documents=_documents,
58
- embeddings=_embeddings,
59
- metadatas=_metadatas,
60
- ids=_ids
61
- )
62
- print(f"✅ ChromaDB ready — {len(_documents)} documents indexed")
63
-
64
- # Cleanup temp variables
65
- del _embedder, _chroma, _collection
66
- del _documents, _metadatas, _ids, _embeddings
67
 
68
- except Exception as e:
69
- print(f"⚠️ Auto-ingest failed: {e}. Will use keyword retrieval fallback.")
70
- else:
71
- print("✅ ChromaDB found — skipping ingest")
72
 
73
  # ─────────────────────────────────────────────
74
- # CONFIG
75
  # ─────────────────────────────────────────────
76
- HF_TOKEN = (
77
- os.environ.get("HF_TOKEN") or
78
- os.environ.get("HF_API_TOKEN") or
79
- os.environ.get("HUGGINGFACE_TOKEN") or
80
- ""
81
- )
82
- print(f"DEBUG app.py: HF_TOKEN loaded = {bool(HF_TOKEN)}, length = {len(HF_TOKEN)}")
83
-
84
 
85
- # (STT, LLM, and TTS functions moved to separate modules)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
 
87
 
88
  # ─────────────────────────────────────────────
@@ -90,98 +103,108 @@ print(f"DEBUG app.py: HF_TOKEN loaded = {bool(HF_TOKEN)}, length = {len(HF_TOKEN
90
  # ─────────────────────────────────────────────
91
 
92
  def run_voice_pipeline(audio_input):
93
- """Voice mode: mic audio → transcript, answer, citations, audio, status."""
94
  if audio_input is None:
95
- yield "", "कृपया माइक्रोफोन बटन दबाकर अपना प्रश्न पूछें।", "", None, "🔴 *तैयार*"
96
  return
97
  if not HF_TOKEN:
98
- yield "", "⚠️ HF_TOKEN Secret नहीं मिला। Space Settings → Secrets में जोड़ें।", "", None, "❌ *त्रुटि*"
99
  return
100
 
101
  try:
102
- yield "", "", "", None, "🎙️ **सुन रहा हूँ...** (STT)"
103
  sample_rate, audio_array = audio_input
104
-
105
- # Ensure STT gets proper data
106
- transcript = stt.stt_whisper(audio_array, sample_rate)
107
-
108
- if not transcript.strip():
109
- yield "", "आवाज़ समझ नहीं आई। कृपया फिर से बोलें।", "", None, "⚠️ *फिर से प्रयास करें*"
 
 
 
 
 
110
  return
111
-
112
- yield transcript, "", "", None, "🔎 **जानकारी खोज रहूँ...** (RAG)"
113
- normalized = rag_pipeline.normalize_jargon(transcript)
114
- eng_query = rag_pipeline.translate_to_retrieval_query(normalized)
115
- context_docs = rag_pipeline.retrieve_with_embeddings(eng_query, top_k=3)
116
-
117
- citations = "\n".join([f"• {doc['title']}" for doc in context_docs]) if context_docs else ""
118
-
119
- yield transcript, "", "", None, "🤖 **उत्तर तैयार क रहा हूँ...** (LLM)"
120
-
 
121
  context = "\n\n".join(
122
  f"[{i+1}] {d['title']}\n{d['content'][:500]}"
123
- for i, d in enumerate(context_docs)
124
- ) if context_docs else "कोई प्रासंगिक जानकारी नहीं मिली।"
125
 
126
  full_prompt = (
127
  f"नीचे दी गई जानकारी के आधार पर प्रश्न का उत्तर दें:\n\n"
128
  f"{context}\n\n"
129
- f"प्रश्न: {transcript}"
130
  )
131
- answer = llm.llm_generate(full_prompt)
132
-
133
- yield transcript, answer, citations, None, "🔊 **आवाज़ बना रहा हूँ...** (TTS)"
134
- clean_text = rag_pipeline.format_response_for_tts(answer)
135
- audio_out = tts.tts_hindi(clean_text)
136
-
137
- yield transcript, answer, citations, audio_out, "✅ **पूरा हुआ**"
138
-
 
 
 
139
  except Exception as e:
140
  import traceback; traceback.print_exc()
141
- print(f"Pipeline error: {e}")
142
- yield "", f"⚠️ त्रुटि: {e}", "", None, "❌ *विफल*"
143
 
144
 
145
- def run_text_pipeline(text_input):
146
- """Text mode: typed question → answer, citations, audio, status."""
147
  if not text_input or not text_input.strip():
148
- yield text_input, "कृपया एक प्रश्न लिखें।", "", None, "🔴 *तैयार*"
149
  return
150
  if not HF_TOKEN:
151
- yield text_input, "⚠️ HF_TOKEN Secret नहीं मिला।", "", None, "❌ *त्रुटि*"
152
  return
153
 
154
  try:
155
- yield text_input, "", "", None, "🔎 **जानकारी खोज रहूँ...** (RAG)"
156
- normalized = rag_pipeline.normalize_jargon(text_input)
157
- eng_query = rag_pipeline.translate_to_retrieval_query(normalized)
158
- context_docs = rag_pipeline.retrieve_with_embeddings(eng_query, top_k=3)
159
-
160
- citations = "\n".join([f"• {doc['title']}" for doc in context_docs]) if context_docs else ""
161
-
162
- yield text_input, "", "", None, "🤖 **उत्तर तैयार क रहा हूँ...** (LLM)"
163
-
164
  context = "\n\n".join(
165
  f"[{i+1}] {d['title']}\n{d['content'][:500]}"
166
- for i, d in enumerate(context_docs)
167
- ) if context_docs else "कोई प्रासंगिक जानकारी नहीं मिली।"
168
 
169
  full_prompt = (
170
  f"नीचे दी गई जानकारी के आधार पर प्रश्न का उत्तर दें:\n\n"
171
  f"{context}\n\n"
172
  f"प्रश्न: {text_input}"
173
  )
174
- answer = llm.llm_generate(full_prompt)
175
-
176
- yield text_input, answer, citations, None, "🔊 **आवाज़ बना रहा हूँ...** (TTS)"
177
- clean_text = rag_pipeline.format_response_for_tts(answer)
178
- audio_out = tts.tts_hindi(clean_text)
179
-
180
- yield text_input, answer, citations, audio_out, "✅ **पूरा हुआ**"
181
-
 
 
 
182
  except Exception as e:
183
  import traceback; traceback.print_exc()
184
- yield text_input, f"⚠️ त्रुटि: {e}", "", None, "❌ *विफल*"
185
 
186
 
187
  # ─────────────────────────────────────────────
@@ -316,9 +339,8 @@ def build_ui():
316
  v_audio_out = gr.Audio(label="🔊 उत्तर सुनें", type="numpy", autoplay=True)
317
  v_citations = gr.Textbox(label="📚 स्रोत", interactive=False, lines=3, elem_classes="sources-area")
318
 
319
- v_status = gr.Markdown("🔴 *तैयार*", elem_classes="status-bar")
320
  v_btn.click(fn=run_voice_pipeline, inputs=[v_audio_in],
321
- outputs=[v_transcript, v_answer, v_citations, v_audio_out, v_status])
322
 
323
  with gr.Tab("⌨️ Text Mode"):
324
  with gr.Row():
@@ -332,11 +354,10 @@ def build_ui():
332
  t_audio_out = gr.Audio(label="🔊 उत्तर सुनें", type="numpy", autoplay=True)
333
  t_citations = gr.Textbox(label="📚 स्रोत", interactive=False, lines=3, elem_classes="sources-area")
334
 
335
- t_status = gr.Markdown("🔴 *तैयार*", elem_classes="status-bar")
336
  t_btn.click(fn=run_text_pipeline, inputs=[t_input],
337
- outputs=[t_transcript, t_answer, t_citations, t_audio_out, t_status])
338
  t_input.submit(fn=run_text_pipeline, inputs=[t_input],
339
- outputs=[t_transcript, t_answer, t_citations, t_audio_out, t_status])
340
 
341
  gr.HTML('<div style="padding:6px 10px 0;"><p style="color:var(--gold);font-size:11px;font-weight:600;letter-spacing:1px;text-transform:uppercase;margin:8px 0 4px 32px;">📌 उदाहरण प्रश्न</p></div>')
342
  gr.Examples(examples=[[q] for q in EXAMPLES], inputs=[t_input], label="", cache_examples=False)
@@ -378,4 +399,4 @@ def build_ui():
378
 
379
  if __name__ == "__main__":
380
  demo = build_ui()
381
- demo.queue().launch(show_api=False)
 
1
+ """
2
+ Hindi Banking Voice Assistant
3
+ CPU Basic safe uses HF InferenceClient with providers that actually work.
4
+
5
+ STT : openai/whisper-large-v3 (hf-inference, ASR)
6
+ LLM : mistralai/Mistral-7B-Instruct-v0.3 (featherless provider, free tier)
7
+ TTS : facebook/mms-tts-hin (hf-inference, Hindi TTS, small model)
8
+ """
9
 
10
  import os
11
  import io
12
  import numpy as np
13
  import soundfile as sf
14
  import gradio as gr
15
+ from huggingface_hub import InferenceClient
16
+
17
+ from rag_pipeline import (
18
+ normalize_jargon,
19
+ translate_to_retrieval_query,
20
+ build_rag_prompt,
21
+ format_response_for_tts,
22
+ get_retriever,
23
+ )
24
 
25
  # ─────────────────────────────────────────────
26
+ # CONFIG
 
27
  # ─────────────────────────────────────────────
28
+ HF_TOKEN = os.environ.get("HF_TOKEN", "")
 
 
 
 
 
 
29
 
30
+ STT_MODEL = "openai/whisper-large-v3"
31
+ LLM_MODEL = "mistralai/Mistral-7B-Instruct-v0.3"
32
+ TTS_MODEL = "facebook/mms-tts-hin"
33
+
34
+ retriever = get_retriever()
35
+ print(f"Knowledge base ready: {len(retriever.doc_ids)} documents")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
 
 
 
 
37
 
38
  # ─────────────────────────────────────────────
39
+ # INFERENCE FUNCTIONS
40
  # ─────────────────────────────────────────────
 
 
 
 
 
 
 
 
41
 
42
+ def get_client(provider: str) -> InferenceClient:
43
+ return InferenceClient(provider=provider, api_key=HF_TOKEN)
44
+
45
+
46
+ def stt_whisper(audio_array: np.ndarray, sample_rate: int) -> str:
47
+ """Convert Hindi audio to text using Whisper via hf-inference."""
48
+ buf = io.BytesIO()
49
+ sf.write(buf, audio_array, sample_rate, format="WAV", subtype="PCM_16")
50
+ buf.seek(0)
51
+
52
+ client = get_client("hf-inference")
53
+ result = client.automatic_speech_recognition(
54
+ audio=buf.read(),
55
+ model=STT_MODEL,
56
+ )
57
+ # result is an ASROutput object with .text
58
+ return result.text.strip() if hasattr(result, "text") else str(result).strip()
59
+
60
+
61
+ def llm_generate(prompt: str) -> str:
62
+ """Generate Hindi answer using Mistral via featherless (free tier)."""
63
+ client = get_client("featherless")
64
+
65
+ # Build messages in chat format
66
+ messages = [
67
+ {
68
+ "role": "system",
69
+ "content": (
70
+ "आप एक सहायक बैंकिंग सहायक हैं। केवल दी गई जानकारी के आधार पर "
71
+ "सरल हिंदी में 3-4 वाक्यों मे��� उत्तर दें। "
72
+ "यदि जानकारी नहीं है तो कहें: 'यह जानकारी मेरे पास नहीं है। "
73
+ "कृपया अपने बैंक से संपर्क करें।' हमेशा हिंदी में उत्तर दें।"
74
+ ),
75
+ },
76
+ {"role": "user", "content": prompt},
77
+ ]
78
+
79
+ response = client.chat.completions.create(
80
+ model=LLM_MODEL,
81
+ messages=messages,
82
+ max_tokens=350,
83
+ temperature=0.7,
84
+ )
85
+ return response.choices[0].message.content.strip()
86
+
87
+
88
+ def tts_hindi(text: str):
89
+ """Convert Hindi text to speech using MMS-TTS-HIN via hf-inference."""
90
+ client = get_client("hf-inference")
91
+ try:
92
+ audio_bytes = client.text_to_speech(text=text, model=TTS_MODEL)
93
+ buf = io.BytesIO(audio_bytes)
94
+ audio_array, sample_rate = sf.read(buf)
95
+ return int(sample_rate), audio_array.astype(np.float32)
96
+ except Exception as e:
97
+ print(f"TTS error: {e}")
98
+ return None
99
 
100
 
101
  # ─────────────────────────────────────────────
 
103
  # ─────────────────────────────────────────────
104
 
105
  def run_voice_pipeline(audio_input):
106
+ """Voice mode: mic audio → transcript, answer, citations, audio."""
107
  if audio_input is None:
108
+ yield "", "कृपया माइक्रोफोन बटन दबाकर अपना प्रश्न पूछें।", "", None
109
  return
110
  if not HF_TOKEN:
111
+ yield "", "⚠️ HF_TOKEN Secret नहीं मिला। Space Settings → Secrets में जोड़ें।", "", None
112
  return
113
 
114
  try:
 
115
  sample_rate, audio_array = audio_input
116
+ audio_float = audio_array.astype(np.float32)
117
+ if np.abs(audio_float).max() > 1.0:
118
+ audio_float /= 32768.0
119
+ if audio_float.ndim > 1:
120
+ audio_float = audio_float.mean(axis=1)
121
+
122
+ yield "⏳ आवाज़ पहचाना जा रहा है...", "", "", None
123
+
124
+ hindi_text = stt_whisper(audio_float, sample_rate)
125
+ if not hindi_text:
126
+ yield "", "आवाज़ स्पष्ट नहीं सुनाई दी। कृपया दोबारा कोशिश करें।", "", None
127
  return
128
+
129
+ yield hindi_text, " जानकारी खोज जा रह...", "", None
130
+
131
+ normalized = normalize_jargon(hindi_text)
132
+ eng_query = translate_to_retrieval_query(normalized)
133
+ docs = retriever.retrieve(eng_query, top_k=3) or retriever.retrieve(hindi_text, top_k=2)
134
+ citations = "\n".join(f"• {d['title']}" for d in docs) if docs else "कोई स्रोत नहीं मिला।"
135
+
136
+ yield hindi_text, " उत्तर तैयार किया जा रहा ह...", citations, None
137
+
138
+ # Build a concise context-augmented prompt for the chat model
139
  context = "\n\n".join(
140
  f"[{i+1}] {d['title']}\n{d['content'][:500]}"
141
+ for i, d in enumerate(docs)
142
+ ) if docs else "कोई प्रासंगिक जानकारी नहीं मिली।"
143
 
144
  full_prompt = (
145
  f"नीचे दी गई जानकारी के आधार पर प्रश्न का उत्तर दें:\n\n"
146
  f"{context}\n\n"
147
+ f"प्रश्न: {hindi_text}"
148
  )
149
+
150
+ answer = llm_generate(full_prompt)
151
+ clean_answer = format_response_for_tts(answer) or \
152
+ "यह जानकारी मेरे पास नहीं है। कृपया अपने बैंक से संपर्क करें।"
153
+
154
+ yield hindi_text, clean_answer, citations, None
155
+
156
+ yield hindi_text, clean_answer, "⏳ ऑडियो बनाया जा रहा है...", None
157
+ audio_out = tts_hindi(clean_answer)
158
+ yield hindi_text, clean_answer, citations, audio_out
159
+
160
  except Exception as e:
161
  import traceback; traceback.print_exc()
162
+ yield "", f"⚠️ त्रुटि: {e}", "", None
 
163
 
164
 
165
+ def run_text_pipeline(text_input: str):
166
+ """Text mode: typed question → answer, citations, audio."""
167
  if not text_input or not text_input.strip():
168
+ yield text_input, "कृपया एक प्रश्न लिखें।", "", None
169
  return
170
  if not HF_TOKEN:
171
+ yield text_input, "⚠️ HF_TOKEN Secret नहीं मिला।", "", None
172
  return
173
 
174
  try:
175
+ yield text_input, " जानकारी खोज जा रह...", "", None
176
+
177
+ normalized = normalize_jargon(text_input)
178
+ eng_query = translate_to_retrieval_query(normalized)
179
+ docs = retriever.retrieve(eng_query, top_k=3) or retriever.retrieve(text_input, top_k=2)
180
+ citations = "\n".join(f"• {d['title']}" for d in docs) if docs else "कोई स्रोत नहीं मिला।"
181
+
182
+ yield text_input, " उत्तर तैयार किया जा रहा ह...", citations, None
183
+
184
  context = "\n\n".join(
185
  f"[{i+1}] {d['title']}\n{d['content'][:500]}"
186
+ for i, d in enumerate(docs)
187
+ ) if docs else "कोई प्रासंगिक जानकारी नहीं मिली।"
188
 
189
  full_prompt = (
190
  f"नीचे दी गई जानकारी के आधार पर प्रश्न का उत्तर दें:\n\n"
191
  f"{context}\n\n"
192
  f"प्रश्न: {text_input}"
193
  )
194
+
195
+ answer = llm_generate(full_prompt)
196
+ clean_answer = format_response_for_tts(answer) or \
197
+ "यह जानकारी मेरे पास नहीं है। कृपया अपने बैंक से संपर्क करें।"
198
+
199
+ yield text_input, clean_answer, citations, None
200
+
201
+ yield text_input, clean_answer, "⏳ ऑडियो बन��या जा रहा है...", None
202
+ audio_out = tts_hindi(clean_answer)
203
+ yield text_input, clean_answer, citations, audio_out
204
+
205
  except Exception as e:
206
  import traceback; traceback.print_exc()
207
+ yield text_input, f"⚠️ त्रुटि: {e}", "", None
208
 
209
 
210
  # ─────────────────────────────────────────────
 
339
  v_audio_out = gr.Audio(label="🔊 उत्तर सुनें", type="numpy", autoplay=True)
340
  v_citations = gr.Textbox(label="📚 स्रोत", interactive=False, lines=3, elem_classes="sources-area")
341
 
 
342
  v_btn.click(fn=run_voice_pipeline, inputs=[v_audio_in],
343
+ outputs=[v_transcript, v_answer, v_citations, v_audio_out])
344
 
345
  with gr.Tab("⌨️ Text Mode"):
346
  with gr.Row():
 
354
  t_audio_out = gr.Audio(label="🔊 उत्तर सुनें", type="numpy", autoplay=True)
355
  t_citations = gr.Textbox(label="📚 स्रोत", interactive=False, lines=3, elem_classes="sources-area")
356
 
 
357
  t_btn.click(fn=run_text_pipeline, inputs=[t_input],
358
+ outputs=[t_transcript, t_answer, t_citations, t_audio_out])
359
  t_input.submit(fn=run_text_pipeline, inputs=[t_input],
360
+ outputs=[t_transcript, t_answer, t_citations, t_audio_out])
361
 
362
  gr.HTML('<div style="padding:6px 10px 0;"><p style="color:var(--gold);font-size:11px;font-weight:600;letter-spacing:1px;text-transform:uppercase;margin:8px 0 4px 32px;">📌 उदाहरण प्रश्न</p></div>')
363
  gr.Examples(examples=[[q] for q in EXAMPLES], inputs=[t_input], label="", cache_examples=False)
 
399
 
400
  if __name__ == "__main__":
401
  demo = build_ui()
402
+ demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True)
ingest.py DELETED
@@ -1,55 +0,0 @@
1
- """
2
- One-time script to build ChromaDB vector store from knowledge base.
3
- Run once before starting the app: python ingest.py
4
- Embeds all 37 knowledge base documents using multilingual MiniLM.
5
- """
6
- from knowledge_base import KNOWLEDGE_BASE
7
- from sentence_transformers import SentenceTransformer
8
- import chromadb
9
-
10
- print("Loading embedding model...")
11
- embedder = SentenceTransformer(
12
- 'sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2'
13
- )
14
-
15
- print("Setting up ChromaDB...")
16
- chroma_client = chromadb.PersistentClient(path="./chroma_db")
17
-
18
- # Delete existing collection to rebuild fresh
19
- try:
20
- chroma_client.delete_collection("banking_hindi")
21
- print("Deleted existing collection.")
22
- except:
23
- pass
24
-
25
- collection = chroma_client.get_or_create_collection("banking_hindi")
26
-
27
- documents = []
28
- metadatas = []
29
- ids = []
30
-
31
- for doc in KNOWLEDGE_BASE:
32
- full_text = f"{doc['title']}\n{doc['content']}"
33
- documents.append(full_text)
34
- metadatas.append({
35
- "id": doc["id"],
36
- "title": doc["title"],
37
- "category": doc["category"]
38
- })
39
- ids.append(doc["id"])
40
-
41
- print(f"Embedding {len(documents)} documents...")
42
- embeddings = embedder.encode(
43
- documents,
44
- show_progress_bar=True,
45
- batch_size=8
46
- ).tolist()
47
-
48
- collection.add(
49
- documents=documents,
50
- embeddings=embeddings,
51
- metadatas=metadatas,
52
- ids=ids
53
- )
54
- print(f"✅ Successfully ingested {len(documents)} documents into ChromaDB")
55
- print(f"Collection count: {collection.count()}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
llm.py DELETED
@@ -1,95 +0,0 @@
1
- """
2
- LLM Module - Llama 3.1 70B via HuggingFace Router
3
- Uses router.huggingface.co OpenAI-compatible endpoint.
4
- Same pattern as verified working production code.
5
- Includes retry logic for model cold starts.
6
- """
7
- import requests
8
- import time
9
- import os
10
-
11
- HF_TOKEN = (
12
- os.environ.get("HF_TOKEN") or
13
- os.environ.get("HF_API_TOKEN") or
14
- os.environ.get("HUGGINGFACE_TOKEN") or
15
- ""
16
- )
17
- print(f"DEBUG LLM: HF_TOKEN loaded = {bool(HF_TOKEN)}")
18
-
19
- LLM_ROUTER_URL = "https://router.huggingface.co/v1/chat/completions"
20
- LLM_MODEL = "meta-llama/Llama-3.1-70B-Instruct"
21
-
22
- SYSTEM_PROMPT = """आप एक भारतीय बैंकिंग सहायक हैं।
23
- आपका काम है ग्रामीण और शहरी भारतीय उपयोगकर्ताओं को
24
- बैंकिंग और वित्तीय जानकारी सरल हिंदी में देना।
25
-
26
- सख्त नियम:
27
- 1. केवल हिंदी में उत्तर दें — अंग्रेजी बिल्कुल नहीं
28
- 2. केवल दिए गए संदर्भ से उत्तर दें
29
- 3. यदि संदर्भ में जानकारी नहीं है तो कहें:
30
- यह जानकारी मेरे पास नहीं है। कृपया अपने बैंक से संपर्क करें।
31
- 4. ब्याज दर, EMI, या कोई भी राशि केवल संदर्भ से बताएं
32
- 5. उत्तर 3-4 वाक्यों में दें — TTS के लिए छोटा रखें
33
- 6. सरल भाषा — जैसे किसी गांव के व्यक्ति को समझाना हो"""
34
-
35
- def llm_generate(prompt: str) -> str:
36
- """
37
- Generate Hindi answer using Llama 3.1 70B.
38
- Keeps same function signature as existing llm_generate() in app.py.
39
- """
40
- if not HF_TOKEN:
41
- return ("HF_TOKEN नहीं मिला। "
42
- "Space Settings → Secrets में HF_TOKEN जोड़ें।")
43
-
44
- headers = {"Authorization": f"Bearer {HF_TOKEN}"}
45
- payload = {
46
- "model": LLM_MODEL,
47
- "messages": [
48
- {"role": "system", "content": SYSTEM_PROMPT},
49
- {"role": "user", "content": prompt}
50
- ],
51
- "max_tokens": 350,
52
- "temperature": 0.3
53
- }
54
-
55
- for i in range(3):
56
- try:
57
- res = requests.post(
58
- LLM_ROUTER_URL,
59
- headers=headers,
60
- json=payload,
61
- timeout=45
62
- )
63
- print(f"DEBUG LLM status: {res.status_code}")
64
-
65
- if res.status_code != 200:
66
- print(f"DEBUG LLM error body: {res.text[:300]}")
67
-
68
- if not res.text.strip():
69
- print(f"Empty response, retry {i+1}...")
70
- time.sleep(5)
71
- continue
72
-
73
- result = res.json()
74
-
75
- if isinstance(result, dict) and "choices" in result:
76
- answer = result["choices"][0]["message"]["content"].strip()
77
- print(f"DEBUG LLM answer preview: {answer[:100]}")
78
- return answer
79
-
80
- if isinstance(result, dict) and "error" in result:
81
- err = result.get("error", "")
82
- if isinstance(err, dict):
83
- err = err.get("message", str(err))
84
- if "loading" in str(err).lower():
85
- print(f"Model loading, retry {i+1} in 10s...")
86
- time.sleep(10)
87
- continue
88
- print(f"LLM API error: {err}")
89
- break
90
-
91
- except Exception as e:
92
- print(f"LLM exception retry {i+1}: {e}")
93
- time.sleep(2)
94
-
95
- return "माफ करें, अभी उत्तर देने में समस्या हो रही है। कृपया दोबारा प्रयास करें।"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
rag_pipeline.py CHANGED
@@ -106,8 +106,6 @@ JARGON_MAP = {
106
  "zameen": "land property",
107
  "ghar banana": "home construction loan",
108
  "flat": "apartment home loan",
109
- "awas yojana": "awas yojana housing scheme pmay subsidy",
110
- "awas": "housing pmay",
111
 
112
  # Grievance
113
  "shikayat": "complaint grievance",
@@ -127,15 +125,9 @@ def normalize_jargon(text: str) -> str:
127
 
128
 
129
  def translate_to_retrieval_query(normalized_text: str) -> str:
130
- """Extract English words and key Hindi terms from normalized text for retrieval."""
131
- # Keep English words (alpha) and common Hindi keywords that are in KB tags
132
- words = [str(w) for w in normalized_text.split() if any(c.isalpha() for c in w)]
133
- if not words:
134
- # Fallback to the original text if no normalization happened
135
- return str(normalized_text)
136
- # Standard slicing for list of strings
137
- result_words = words[0:20]
138
- return " ".join(result_words)
139
 
140
 
141
  # ─────────────────────────────────────────────
@@ -171,15 +163,10 @@ class SimpleRetriever:
171
  for i, doc_words in enumerate(self.doc_words):
172
  overlap = len(query_words & doc_words)
173
  score = overlap / (len(query_words) + 0.5)
174
-
175
- # Substantial bonus for exact phrase matching in document
176
- if query.lower() in self.documents[i]:
177
- score += 1.0
178
-
179
  # Bonus for longer exact word matches
180
  for qw in query_words:
181
- if len(qw) > 3 and qw in self.documents[i]:
182
- score += 0.2
183
  scores.append((score, i))
184
 
185
  scores.sort(reverse=True)
@@ -225,10 +212,10 @@ def build_rag_prompt(user_question: str, retrieved_docs: list) -> str:
225
  return f"""<|system|>
226
  आप एक सहायक बैंकिंग सहायक हैं जो भारतीय बैंकिंग, लोन, और सरकारी योजनाओं के बारे में सरल हिंदी में जानकारी देते हैं।
227
 
228
- नियम:
229
- 1. नीचे दी गई जानकारी का उपयोग करं। यदि जनका बिल्कुल स्ष्ट नहीं है, ो सामानय बैंकिंग ज्ञान का उपयोग कर सहाया करें लेकि "यह जानकारी ेरे पहीं है" कह से बचें यदि आप बेसिक सलाह दे सकते हैं।
230
  2. उत्तर छोटा, सरल और बोलने योग्य हो — 3-4 वाक्यों में।
231
- 3. यदि जानकारी बिल्कुल भी उपलब्ध नहीं है, तभी कहें: "यह जानकारी मेरे पास नहीं है। कृपया अपने बैंक से संपर्क करें।"
232
  4. अंत में केवल एक जरूरी follow-up प्रश्न पूछें (यदि आवश्यक हो)।
233
  5. हमेशा हिंदी में उत्तर दें।
234
 
@@ -251,89 +238,10 @@ def format_response_for_tts(text: str) -> str:
251
  return text.strip()
252
 
253
 
254
-
255
- # ─────────────────────────────────────────────
256
- # EMBEDDING RETRIEVER — multilingual, Hindi-aware
257
- # Uses paraphrase-multilingual-MiniLM-L12-v2
258
- # Falls back to SimpleRetriever if ChromaDB not ready
259
- # ─────────────────────────────────────────────
260
-
261
- try:
262
- from sentence_transformers import SentenceTransformer
263
- from FlagEmbedding import FlagReranker
264
- import chromadb
265
-
266
- print("Loading embedding model (multilingual MiniLM)...")
267
- embedder = SentenceTransformer(
268
- 'sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2'
269
- )
270
- print("Loading reranker (bge-reranker-v2-m3)...")
271
- reranker = FlagReranker('BAAI/bge-reranker-v2-m3', use_fp16=False)
272
-
273
- chroma_client = chromadb.PersistentClient(path="./chroma_db")
274
- EMBEDDING_READY = True
275
- print("Embedding retriever ready.")
276
-
277
- except Exception as e:
278
- print(f"Embedding retriever not available: {e}. Using keyword retriever.")
279
- EMBEDDING_READY = False
280
-
281
-
282
- def retrieve_with_embeddings(query: str, top_k: int = 3) -> list:
283
- """
284
- Two-stage retrieval:
285
- Stage 1 - ChromaDB embedding search (top 10)
286
- Stage 2 - bge-reranker picks best 3
287
- Falls back to SimpleRetriever if embeddings not ready.
288
- Critical for Hinglish queries like
289
- 'home loan ka interest kitna hai for salaried?'
290
- """
291
- if not EMBEDDING_READY:
292
- print("DEBUG RAG: falling back to keyword retriever")
293
- retriever = get_retriever()
294
- return retriever.retrieve(query, top_k=top_k)
295
-
296
- try:
297
- collection = chroma_client.get_collection("banking_hindi")
298
- except Exception:
299
- print("DEBUG RAG: ChromaDB collection not found, run ingest.py first")
300
- print("DEBUG RAG: falling back to keyword retriever")
301
- retriever = get_retriever()
302
- return retriever.retrieve(query, top_k=top_k)
303
-
304
- try:
305
- # Stage 1: embedding similarity search
306
- query_embedding = embedder.encode([query]).tolist()
307
- results = collection.query(
308
- query_embeddings=query_embedding,
309
- n_results=min(10, collection.count())
310
- )
311
- candidates = results['documents'][0]
312
- metadatas = results['metadatas'][0]
313
-
314
- print(f"DEBUG RAG: {len(candidates)} candidates from ChromaDB")
315
-
316
- # Stage 2: rerank
317
- pairs = [[query, doc] for doc in candidates]
318
- scores = reranker.compute_score(pairs)
319
- ranked = sorted(
320
- zip(scores, candidates, metadatas),
321
- reverse=True
322
- )
323
-
324
- top_results = [
325
- {
326
- "id": meta.get("id", ""),
327
- "title": meta.get("title", ""),
328
- "content": doc,
329
- "category": meta.get("category", "")
330
- }
331
- for _, doc, meta in ranked[:top_k]
332
- ]
333
- print(f"DEBUG RAG: top result = {top_results[0]['title'] if top_results else 'none'}")
334
- return top_results
335
-
336
- except Exception as e:
337
- print(f"DEBUG RAG: embedding retrieval error: {e}")
338
- retriever = get_retriever()
339
- return retriever.retrieve(query, top_k=top_k)
 
106
  "zameen": "land property",
107
  "ghar banana": "home construction loan",
108
  "flat": "apartment home loan",
 
 
109
 
110
  # Grievance
111
  "shikayat": "complaint grievance",
 
125
 
126
 
127
  def translate_to_retrieval_query(normalized_text: str) -> str:
128
+ """Extract English words from normalized text for retrieval."""
129
+ words = [w for w in normalized_text.split() if any(c.isalpha() for c in w)]
130
+ return " ".join(words[:20])
 
 
 
 
 
 
131
 
132
 
133
  # ─────────────────────────────────────────────
 
163
  for i, doc_words in enumerate(self.doc_words):
164
  overlap = len(query_words & doc_words)
165
  score = overlap / (len(query_words) + 0.5)
 
 
 
 
 
166
  # Bonus for longer exact word matches
167
  for qw in query_words:
168
+ if len(qw) > 4 and qw in self.documents[i]:
169
+ score += 0.3
170
  scores.append((score, i))
171
 
172
  scores.sort(reverse=True)
 
212
  return f"""<|system|>
213
  आप एक सहायक बैंकिंग सहायक हैं जो भारतीय बैंकिंग, लोन, और सरकारी योजनाओं के बारे में सरल हिंदी में जानकारी देते हैं।
214
 
215
+ नियम:
216
+ 1. केवल नीचे दी गई जानकारी के आधार प त्तरें मान न लं।
217
  2. उत्तर छोटा, सरल और बोलने योग्य हो — 3-4 वाक्यों में।
218
+ 3. यदि जानकारी उपलब्ध नहीं है, त कहें: "यह जानकारी मेरे पास नहीं है। कृपया अपने बैंक से संपर्क करें।"
219
  4. अंत में केवल एक जरूरी follow-up प्रश्न पूछें (यदि आवश्यक हो)।
220
  5. हमेशा हिंदी में उत्तर दें।
221
 
 
238
  return text.strip()
239
 
240
 
241
+ def get_tts_description(text: str) -> str:
242
+ """Speaker description for Indic-Parler-TTS."""
243
+ return (
244
+ "A calm, clear female voice speaking in Hindi. "
245
+ "The speech is measured and helpful, like a bank customer service representative. "
246
+ "Very clear pronunciation, moderate pace, friendly tone."
247
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -1,12 +1,4 @@
1
- # Stability Fixes (Deep Lock)
2
- pydantic==2.9.2
3
- fastapi==0.115.5
4
-
5
- # Core Logic Dependencies
6
  soundfile>=0.12.1
7
- faster-whisper
8
- chromadb
9
- sentence-transformers
10
- FlagEmbedding
11
- requests
12
- edge-tts
 
1
+ gradio>=5.9.1
2
+ huggingface_hub>=0.34.0
 
 
 
3
  soundfile>=0.12.1
4
+ numpy>=1.24.0
 
 
 
 
 
stt.py DELETED
@@ -1,36 +0,0 @@
1
- """
2
- STT Module - faster-whisper medium int8 on CPU
3
- Transcribes Hindi audio to text.
4
- Faster than openai/whisper via API, runs locally, no API cost.
5
- """
6
- from faster_whisper import WhisperModel
7
- import numpy as np
8
- import soundfile as sf
9
- import io
10
-
11
- print("Loading Whisper model (medium int8)... first run downloads ~500MB")
12
- whisper_model = WhisperModel("medium", device="cpu", compute_type="int8")
13
- print("Whisper model loaded.")
14
-
15
- def stt_whisper(audio_array: np.ndarray, sample_rate: int) -> str:
16
- """
17
- Convert Hindi audio array to text.
18
- Keeps same function signature as existing stt_whisper() in app.py.
19
- """
20
- try:
21
- buf = io.BytesIO()
22
- sf.write(buf, audio_array, sample_rate, format="WAV", subtype="PCM_16")
23
- buf.seek(0)
24
-
25
- segments, info = whisper_model.transcribe(
26
- buf,
27
- language="hi",
28
- beam_size=5
29
- )
30
- transcript = " ".join([s.text for s in segments]).strip()
31
- print(f"DEBUG STT transcript: {transcript}")
32
- print(f"DEBUG STT detected language: {info.language} confidence: {info.language_probability:.2f}")
33
- return transcript
34
- except Exception as e:
35
- print(f"STT error: {e}")
36
- return ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tts.py DELETED
@@ -1,63 +0,0 @@
1
- """
2
- TTS Module - Microsoft Edge TTS
3
- Uses hi-IN-SwaraNeural — natural Hindi female voice.
4
- No compilation needed, works on Python 3.14 + Windows.
5
- No API key required.
6
- Returns (sample_rate, audio_array) tuple — same as existing tts_hindi().
7
- """
8
- import edge_tts
9
- import asyncio
10
- import numpy as np
11
- import soundfile as sf
12
- import io
13
- import tempfile
14
- import os
15
-
16
- HINDI_VOICE = "hi-IN-SwaraNeural"
17
-
18
- def tts_hindi(text: str):
19
- """
20
- Convert Hindi text to audio using Edge TTS.
21
- Keeps same function signature as existing tts_hindi() in app.py.
22
- Returns (sample_rate, audio_array) tuple or None on failure.
23
- """
24
- try:
25
- if not text or not text.strip():
26
- return None
27
-
28
- # Limit text length
29
- if len(text) > 500:
30
- text = text[:500]
31
- print("DEBUG TTS: text truncated to 500 chars")
32
-
33
- # Edge TTS is async — run it synchronously
34
- async def _generate():
35
- communicate = edge_tts.Communicate(text, HINDI_VOICE)
36
- # Use tempfile to get a proper temp path
37
- fd, tmp_path = tempfile.mkstemp(suffix=".mp3")
38
- os.close(fd) # Close file descriptor immediately
39
- try:
40
- await communicate.save(tmp_path)
41
- return tmp_path
42
- except Exception as e:
43
- if os.path.exists(tmp_path):
44
- os.remove(tmp_path)
45
- raise e
46
-
47
- # Run async function
48
- tmp_path = asyncio.run(_generate())
49
-
50
- # Read audio file
51
- audio_array, sample_rate = sf.read(tmp_path)
52
- audio_array = audio_array.astype(np.float32)
53
-
54
- # Cleanup temp file
55
- if os.path.exists(tmp_path):
56
- os.remove(tmp_path)
57
-
58
- print(f"DEBUG TTS: generated audio at {sample_rate}Hz")
59
- return (int(sample_rate), audio_array)
60
-
61
- except Exception as e:
62
- print(f"TTS error: {e}")
63
- return None