Arnavkumar01 commited on
Commit
f65990c
·
1 Parent(s): 18fe292

change in main.py to switch from wss to gradio

Browse files
Files changed (2) hide show
  1. main.py +144 -177
  2. requirements.txt +1 -0
main.py CHANGED
@@ -1,11 +1,14 @@
1
  import os
2
- import base64
3
- import logging
4
  import json
5
  import re
 
 
 
 
6
  from contextlib import asynccontextmanager
7
- from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request, status, Depends, Header, HTTPException, Query
8
- from fastapi.concurrency import run_in_threadpool # This line is corrected (no syntax error)
9
  from pydantic import BaseModel
10
  from dotenv import load_dotenv
11
  from openai import OpenAI
@@ -13,23 +16,22 @@ from elevenlabs.client import ElevenLabs
13
  from langchain_huggingface import HuggingFaceEmbeddings
14
  from langchain_postgres.vectorstores import PGVector
15
  from sqlalchemy import create_engine
16
- import asyncio
17
- import io
18
- from typing import Optional
19
 
20
  # --- SETUP ---
21
  os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
22
  logging.getLogger('tensorflow').setLevel(logging.ERROR)
23
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
24
 
25
- # Load environment variables
26
  load_dotenv()
27
  NEON_DATABASE_URL = os.getenv("NEON_DATABASE_URL")
28
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
29
  ELEVENLABS_API_KEY = os.getenv("ELEVENLABS_API_KEY")
30
  SHARED_SECRET = os.getenv("SHARED_SECRET")
31
 
32
- # --- CONFIGURATION ---
33
  COLLECTION_NAME = "real_estate_embeddings"
34
  EMBEDDING_MODEL = "hkunlp/instructor-large"
35
  ELEVENLABS_VOICE_NAME = "Leo"
@@ -44,102 +46,90 @@ TABLE_DESCRIPTIONS = """
44
  - "feedback_source": Customer feedback and ratings for projects.
45
  """
46
 
47
- # --- GLOBAL VARIABLES & CLIENTS ---
48
  embeddings = None
49
  vector_store = None
50
-
51
  client_openai = OpenAI(api_key=OPENAI_API_KEY)
52
  client_elevenlabs = ElevenLabs(api_key=ELEVENLABS_API_KEY)
53
 
54
 
55
- # --- FASTAPI LIFESPAN MANAGEMENT ---
56
  @asynccontextmanager
57
  async def lifespan(app: FastAPI):
58
- """Manages application startup and shutdown logic."""
59
  global embeddings, vector_store
60
- logging.info(f"Initializing embedding model: '{EMBEDDING_MODEL}'...")
61
  embeddings = HuggingFaceEmbeddings(model_name=EMBEDDING_MODEL)
62
- logging.info("Embedding model loaded successfully.")
63
 
64
- logging.info(f"Connecting to vector store '{COLLECTION_NAME}'...")
65
  engine = create_engine(NEON_DATABASE_URL, pool_pre_ping=True)
66
  vector_store = PGVector(
67
  connection=engine,
68
  collection_name=COLLECTION_NAME,
69
  embeddings=embeddings,
70
  )
71
- logging.info("Successfully connected to the vector store.")
72
  yield
73
- logging.info("Application shutting down.")
 
74
 
75
- # --- INITIALIZE FastAPI APP ---
76
  app = FastAPI(lifespan=lifespan)
77
 
 
78
  # --- PROMPTS ---
79
  QUERY_FORMULATION_PROMPT = f"""
80
- You are a query analysis agent. Your task is to transform a user's query into a precise search query for a vector database and determine the correct table to filter by.
81
  **Available Tables:**
82
  {TABLE_DESCRIPTIONS}
83
  **User's Query:** "{{user_query}}"
84
- **Your Task:**
85
- 1. Rephrase the user's query into a clear, keyword-focused English question suitable for a database search.
86
- 2. Analyze the user's query for keywords indicating project status (e.g., "ongoing", "under construction", "completed", "finished", "upcoming", "new launch").
87
- 3. If such status keywords are present, identify the single most relevant table from the list above to filter by.
88
- 4. If no specific status keywords are mentioned (e.g., the user asks generally about projects in a location), set the filter table to null.
89
- 5. Respond ONLY with a JSON object containing "search_query" and "filter_table" (which should be the table name string or null).
90
  """
 
91
  ANSWER_SYSTEM_PROMPT = """
92
  You are an expert AI assistant for a premier real estate developer.
93
- ## YOUR PERSONA
94
- - You are professional, helpful, and highly knowledgeable. Your tone should be polite and articulate.
95
- ## CORE BUSINESS KNOWLEDGE
96
- - **Operational Cities:** We are currently operational in Pune, Mumbai, Bengaluru, Delhi, Chennai, Hyderabad, Goa, Gurgaon, Kolkata.
97
- - **Property Types:** We offer luxury apartments, villas, and commercial properties.
98
- - **Budget Range:** Our residential properties typically range from 45 lakhs to 5 crores.
99
- ## CORE RULES
100
- 1. **Language Adaptation:** If the user's original query was in Hinglish, respond in Hinglish. If in English, respond in English.
101
- 2. **Fact-Based Answers:** Use the provided CONTEXT to answer the user's question. If the context is empty, use your Core Business Knowledge.
102
- 3. **Stay on Topic:** Only answer questions related to real estate.
103
  """
104
 
105
 
106
- # --- HELPER FUNCTIONS (to be run in threadpool) ---
107
-
108
  def transcribe_audio(audio_bytes: bytes) -> str:
109
- """
110
- Transcribes any audio format (WAV, MP3, WebM, Opus) from raw bytes.
111
- Whisper will auto-detect the format.
112
- """
113
  for attempt in range(3):
114
  try:
115
  audio_file = io.BytesIO(audio_bytes)
116
- # Give it a "name" hint for the API, but format is auto-detected
117
- audio_file.name = "input.audio"
118
-
119
  transcript = client_openai.audio.transcriptions.create(
120
- model="whisper-1",
121
  file=audio_file
122
  )
123
- text = transcript.text
124
 
125
- # Check for Hindi script and transliterate
126
  if re.search(r'[\u0900-\u097F]', text):
127
- translit_prompt = f"Transliterate this Hindi text to Roman script (Hinglish style): {text}"
128
  response = client_openai.chat.completions.create(
129
  model="gpt-4o-mini",
130
- messages=[{"role": "user", "content": translit_prompt}],
131
  temperature=0.0
132
  )
133
- text = response.choices[0].message.content
134
 
135
- return text.strip()
136
  except Exception as e:
137
- logging.error(f"Error during transcription (attempt {attempt+1}): {e}", exc_info=True)
138
  if attempt == 2:
139
  return ""
 
140
 
141
  def generate_elevenlabs_sync(text: str, voice: str) -> bytes:
142
- """Synchronous ElevenLabs generation wrapper for run_in_threadpool."""
143
  for attempt in range(3):
144
  try:
145
  return client_elevenlabs.generate(
@@ -149,15 +139,13 @@ def generate_elevenlabs_sync(text: str, voice: str) -> bytes:
149
  output_format="mp3_44100_128"
150
  )
151
  except Exception as e:
152
- logging.error(f"Error in ElevenLabs generate (attempt {attempt+1}): {e}", exc_info=True)
153
  if attempt == 2:
154
  return b''
 
155
 
156
 
157
- # --- RAG/LLM FUNCTIONS (async) ---
158
-
159
  async def formulate_search_plan(user_query: str) -> dict:
160
- logging.info("Formulating search plan with Planner LLM...")
161
  for attempt in range(3):
162
  try:
163
  response = await run_in_threadpool(
@@ -167,154 +155,133 @@ async def formulate_search_plan(user_query: str) -> dict:
167
  response_format={"type": "json_object"},
168
  temperature=0.0
169
  )
170
- plan = json.loads(response.choices[0].message.content)
171
- logging.info(f"Search plan received: {plan}")
172
- return plan
173
  except Exception as e:
174
- logging.error(f"Error in Planner LLM call (attempt {attempt+1}): {e}", exc_info=True)
175
  if attempt == 2:
176
  return {"search_query": user_query, "filter_table": None}
 
 
177
 
178
  async def get_agent_response(user_text: str) -> str:
179
- """Runs RAG and generation logic for a given text query with retries."""
180
  for attempt in range(3):
181
  try:
182
- search_plan = await formulate_search_plan(user_text)
183
- search_query = search_plan.get("search_query", user_text)
184
- filter_table = search_plan.get("filter_table")
185
-
186
  search_filter = {"source_table": filter_table} if filter_table else {}
187
- if search_filter:
188
- logging.info(f"Applying initial filter: {search_filter}")
189
 
190
- # Run blocking DB call in threadpool
191
- retrieved_docs = await run_in_threadpool(
192
  vector_store.similarity_search,
193
  search_query, k=3, filter=search_filter
194
  )
 
 
195
 
196
- if not retrieved_docs:
197
- logging.info("Initial search returned no results. Performing a broader fallback search.")
198
- retrieved_docs = await run_in_threadpool(
199
- vector_store.similarity_search,
200
- search_query, k=3
201
- )
202
 
203
- context_text = "\n\n".join([doc.page_content for doc in retrieved_docs])
204
- logging.info(f"Retrieved Context (preview): {context_text[:500]}...")
205
-
206
- final_prompt_messages = [
207
- {"role": "system", "content": ANSWER_SYSTEM_PROMPT},
208
- {"role": "system", "content": f"Use the following CONTEXT to answer:\n{context_text}"},
209
- {"role": "user", "content": f"My original question was: '{user_text}'"}
210
- ]
211
-
212
- # Run blocking OpenAI call in threadpool
213
- final_response = await run_in_threadpool(
214
  client_openai.chat.completions.create,
215
  model=ANSWERER_MODEL,
216
- messages=final_prompt_messages
 
 
 
 
217
  )
218
- # --- TYPO FIX WAS HERE ---
219
- return final_response.choices[0].message.content
220
  except Exception as e:
221
- logging.error(f"Error in get_agent_response (attempt {attempt+1}): {e}", exc_info=True)
222
  if attempt == 2:
223
- return "Sorry, I couldn't generate a response. Please try again."
 
224
 
225
- # --- AUTH / TEST ENDPOINT HELPERS ---
226
 
 
227
  class TextQuery(BaseModel):
228
  query: str
229
 
230
  async def verify_token(x_auth_token: str = Header(...)):
231
- """Dependency to verify the shared secret token."""
232
  if not SHARED_SECRET or x_auth_token != SHARED_SECRET:
233
- logging.warning("Authentication failed for /test-text-query.")
234
- raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid or missing authentication token")
235
- logging.info("Authentication successful for /test-text-query.")
236
-
237
-
238
- # --- API ENDPOINTS ---
239
 
240
  @app.post("/test-text-query", dependencies=[Depends(verify_token)])
241
  async def test_text_query_endpoint(query: TextQuery):
242
- """Endpoint for text-based testing via Swagger UI."""
243
- logging.info(f"Received text query: {query.query}")
244
- response_text = await get_agent_response(query.query)
245
- logging.info(f"Generated text response: {response_text}")
246
- return {"response": response_text}
247
-
248
-
249
- @app.websocket("/browser-listen")
250
- async def browser_websocket_endpoint(
251
- websocket: WebSocket,
252
- token: Optional[str] = Query(None) # Get token from query param
253
- ):
254
- """
255
- Main WebSocket endpoint for browser-based audio.
256
- Authenticates using a query parameter.
257
- """
258
- # Authentication block
259
- if not token or token != SHARED_SECRET:
260
- logging.warning(f"Browser auth failed: Invalid token '{token}'")
261
- await websocket.accept() # Accept briefly to send error
262
- await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
263
- return
264
-
265
- await websocket.accept()
266
- logging.info("Browser client connected and authenticated.")
267
 
268
  try:
269
- while True:
270
- # 1. Receive JSON message from browser
271
- message = await websocket.receive_json()
272
- audio_base64 = message.get("audio")
273
-
274
- if not audio_base64:
275
- continue
276
-
277
- logging.info("Received audio blob from browser.")
278
- audio_bytes = base64.b64decode(audio_base64)
279
-
280
- # 2. Transcribe (Shared logic)
281
- user_text = await run_in_threadpool(transcribe_audio, audio_bytes)
282
- if not user_text:
283
- logging.info("Transcription empty; skipping.")
284
- continue
285
- logging.info(f"User said: {user_text}")
286
-
287
- # 3. Get AI response (Shared logic)
288
- agent_response_text = await get_agent_response(user_text)
289
- if not agent_response_text:
290
- logging.warning("Agent generated empty response.")
291
- continue
292
- logging.info(f"AI Responded (preview): {agent_response_text[:100]}...")
293
-
294
- # 4. Generate AI speech (Shared logic)
295
- ai_audio_bytes = await run_in_threadpool(
296
- generate_elevenlabs_sync,
297
- agent_response_text,
298
- ELEVENLABS_VOICE_NAME
299
- )
300
- if not ai_audio_bytes:
301
- continue
302
-
303
- # 5. Send audio and text back to browser
304
- response_audio_base64 = base64.b64encode(ai_audio_bytes).decode('utf-8')
305
-
306
- await websocket.send_json({
307
- "text": agent_response_text,
308
- "audio": response_audio_base64
309
- })
310
- logging.info("Sent AI audio response back to browser.")
311
-
312
- except WebSocketDisconnect:
313
- logging.info("Browser client disconnected.")
314
  except Exception as e:
315
- logging.error(f"An error occurred in browser websocket: {e}", exc_info=True)
316
- finally:
317
- try:
318
- await websocket.close()
319
- except Exception:
320
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
+ import io
 
3
  import json
4
  import re
5
+ import tempfile
6
+ import asyncio
7
+ from typing import Optional
8
+ import logging
9
  from contextlib import asynccontextmanager
10
+ from fastapi import FastAPI, Request, status, Depends, Header, HTTPException
11
+ from fastapi.concurrency import run_in_threadpool
12
  from pydantic import BaseModel
13
  from dotenv import load_dotenv
14
  from openai import OpenAI
 
16
  from langchain_huggingface import HuggingFaceEmbeddings
17
  from langchain_postgres.vectorstores import PGVector
18
  from sqlalchemy import create_engine
19
+
20
+ # --- GRADIO ---
21
+ import gradio as gr
22
 
23
  # --- SETUP ---
24
  os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
25
  logging.getLogger('tensorflow').setLevel(logging.ERROR)
26
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
27
 
 
28
  load_dotenv()
29
  NEON_DATABASE_URL = os.getenv("NEON_DATABASE_URL")
30
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
31
  ELEVENLABS_API_KEY = os.getenv("ELEVENLABS_API_KEY")
32
  SHARED_SECRET = os.getenv("SHARED_SECRET")
33
 
34
+ # --- CONFIG ---
35
  COLLECTION_NAME = "real_estate_embeddings"
36
  EMBEDDING_MODEL = "hkunlp/instructor-large"
37
  ELEVENLABS_VOICE_NAME = "Leo"
 
46
  - "feedback_source": Customer feedback and ratings for projects.
47
  """
48
 
49
+ # --- CLIENTS ---
50
  embeddings = None
51
  vector_store = None
 
52
  client_openai = OpenAI(api_key=OPENAI_API_KEY)
53
  client_elevenlabs = ElevenLabs(api_key=ELEVENLABS_API_KEY)
54
 
55
 
56
+ # --- LIFESPAN ---
57
  @asynccontextmanager
58
  async def lifespan(app: FastAPI):
 
59
  global embeddings, vector_store
60
+ logging.info(f"Loading embedding model: {EMBEDDING_MODEL}")
61
  embeddings = HuggingFaceEmbeddings(model_name=EMBEDDING_MODEL)
 
62
 
63
+ logging.info(f"Connecting to vector store: {COLLECTION_NAME}")
64
  engine = create_engine(NEON_DATABASE_URL, pool_pre_ping=True)
65
  vector_store = PGVector(
66
  connection=engine,
67
  collection_name=COLLECTION_NAME,
68
  embeddings=embeddings,
69
  )
70
+ logging.info("Vector store ready.")
71
  yield
72
+ logging.info("Shutting down.")
73
+
74
 
 
75
  app = FastAPI(lifespan=lifespan)
76
 
77
+
78
  # --- PROMPTS ---
79
  QUERY_FORMULATION_PROMPT = f"""
80
+ You are a query analysis agent. Transform the user's query into a precise search query and determine the correct table to filter by.
81
  **Available Tables:**
82
  {TABLE_DESCRIPTIONS}
83
  **User's Query:** "{{user_query}}"
84
+ **Task:**
85
+ 1. Rephrase into a clear, keyword-focused English search query.
86
+ 2. If status keywords (ongoing, completed, upcoming, etc.) are present, pick the matching table.
87
+ 3. If no status keyword, set filter_table to null.
88
+ 4. Return JSON: {{"search_query": "...", "filter_table": "table_name or null"}}
 
89
  """
90
+
91
  ANSWER_SYSTEM_PROMPT = """
92
  You are an expert AI assistant for a premier real estate developer.
93
+ ## CORE KNOWLEDGE
94
+ - Cities: Pune, Mumbai, Bengaluru, Delhi, Chennai, Hyderabad, Goa, Gurgaon, Kolkata.
95
+ - Properties: Luxury apartments, villas, commercial.
96
+ - Budget: 45 lakhs to 5 crores.
97
+ ## RULES
98
+ 1. Match user language (Hinglish Hinglish, English English).
99
+ 2. Use CONTEXT if available, else use core knowledge.
100
+ 3. Only answer real estate questions.
 
 
101
  """
102
 
103
 
104
+ # --- AUDIO & LLM HELPERS ---
 
105
  def transcribe_audio(audio_bytes: bytes) -> str:
 
 
 
 
106
  for attempt in range(3):
107
  try:
108
  audio_file = io.BytesIO(audio_bytes)
109
+ audio_file.name = "input.audio"
 
 
110
  transcript = client_openai.audio.transcriptions.create(
111
+ model="whisper-1",
112
  file=audio_file
113
  )
114
+ text = transcript.text.strip()
115
 
116
+ # Hinglish transliteration
117
  if re.search(r'[\u0900-\u097F]', text):
 
118
  response = client_openai.chat.completions.create(
119
  model="gpt-4o-mini",
120
+ messages=[{"role": "user", "content": f"Transliterate to Roman (Hinglish): {text}"}],
121
  temperature=0.0
122
  )
123
+ text = response.choices[0].message.content.strip()
124
 
125
+ return text
126
  except Exception as e:
127
+ logging.error(f"Transcription error (attempt {attempt+1}): {e}")
128
  if attempt == 2:
129
  return ""
130
+ return ""
131
 
132
  def generate_elevenlabs_sync(text: str, voice: str) -> bytes:
 
133
  for attempt in range(3):
134
  try:
135
  return client_elevenlabs.generate(
 
139
  output_format="mp3_44100_128"
140
  )
141
  except Exception as e:
142
+ logging.error(f"ElevenLabs error (attempt {attempt+1}): {e}")
143
  if attempt == 2:
144
  return b''
145
+ return b''
146
 
147
 
 
 
148
  async def formulate_search_plan(user_query: str) -> dict:
 
149
  for attempt in range(3):
150
  try:
151
  response = await run_in_threadpool(
 
155
  response_format={"type": "json_object"},
156
  temperature=0.0
157
  )
158
+ return json.loads(response.choices[0].message.content)
 
 
159
  except Exception as e:
160
+ logging.error(f"Planner error (attempt {attempt+1}): {e}")
161
  if attempt == 2:
162
  return {"search_query": user_query, "filter_table": None}
163
+ return {"search_query": user_query, "filter_table": None}
164
+
165
 
166
  async def get_agent_response(user_text: str) -> str:
 
167
  for attempt in range(3):
168
  try:
169
+ plan = await formulate_search_plan(user_text)
170
+ search_query = plan.get("search_query", user_text)
171
+ filter_table = plan.get("filter_table")
 
172
  search_filter = {"source_table": filter_table} if filter_table else {}
 
 
173
 
174
+ docs = await run_in_threadpool(
 
175
  vector_store.similarity_search,
176
  search_query, k=3, filter=search_filter
177
  )
178
+ if not docs:
179
+ docs = await run_in_threadpool(vector_store.similarity_search, search_query, k=3)
180
 
181
+ context = "\n\n".join([d.page_content for d in docs])
 
 
 
 
 
182
 
183
+ response = await run_in_threadpool(
 
 
 
 
 
 
 
 
 
 
184
  client_openai.chat.completions.create,
185
  model=ANSWERER_MODEL,
186
+ messages=[
187
+ {"role": "system", "content": ANSWER_SYSTEM_PROMPT},
188
+ {"role": "system", "content": f"CONTEXT:\n{context}"},
189
+ {"role": "user", "content": f"Question: {user_text}"}
190
+ ]
191
  )
192
+ return response.choices[0].message.content.strip()
 
193
  except Exception as e:
194
+ logging.error(f"RAG error (attempt {attempt+1}): {e}")
195
  if attempt == 2:
196
+ return "Sorry, I couldn't respond. Please try again."
197
+ return "Sorry, I couldn't respond."
198
 
 
199
 
200
+ # --- AUTH ENDPOINT ---
201
  class TextQuery(BaseModel):
202
  query: str
203
 
204
  async def verify_token(x_auth_token: str = Header(...)):
 
205
  if not SHARED_SECRET or x_auth_token != SHARED_SECRET:
206
+ logging.warning("Auth failed for /test-text-query")
207
+ raise HTTPException(status_code=401, detail="Invalid token")
208
+ logging.info("Auth passed")
 
 
 
209
 
210
  @app.post("/test-text-query", dependencies=[Depends(verify_token)])
211
  async def test_text_query_endpoint(query: TextQuery):
212
+ logging.info(f"Text query: {query.query}")
213
+ response = await get_agent_response(query.query)
214
+ return {"response": response}
215
+
216
+
217
+ # --- GRADIO AUDIO PROCESSING (BOSS'S FIX) ---
218
+ async def process_audio(audio_path):
219
+ if not audio_path:
220
+ return None, "No audio. Please speak."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
 
222
  try:
223
+ # BOSS'S GENIUS: Read raw bytes directly
224
+ with open(audio_path, "rb") as f:
225
+ audio_bytes = f.read()
226
+
227
+ if len(audio_bytes) == 0:
228
+ return None, "Empty audio."
229
+
230
+ # 1. Transcribe
231
+ user_text = await run_in_threadpool(transcribe_audio, audio_bytes)
232
+ if not user_text:
233
+ return None, "Couldn't understand. Try again."
234
+
235
+ logging.info(f"User: {user_text}")
236
+
237
+ # 2. AI Response
238
+ agent_response = await get_agent_response(user_text)
239
+ if not agent_response:
240
+ return None, "No response generated."
241
+
242
+ logging.info(f"AI: {agent_response[:100]}...")
243
+
244
+ # 3. Generate Speech
245
+ ai_audio_bytes = await run_in_threadpool(
246
+ generate_elevenlabs_sync, agent_response, ELEVENLABS_VOICE_NAME
247
+ )
248
+ if not ai_audio_bytes:
249
+ return None, "Failed to generate voice."
250
+
251
+ # Save to temp file
252
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as f:
253
+ f.write(ai_audio_bytes)
254
+ out_path = f.name
255
+
256
+ return out_path, f"**You:** {user_text}\n\n**AI:** {agent_response}"
257
+
 
 
 
 
 
 
 
 
 
 
258
  except Exception as e:
259
+ logging.error(f"Audio processing error: {e}", exc_info=True)
260
+ return None, f"Error: {str(e)}"
261
+
262
+
263
+ # --- GRADIO UI ---
264
+ with gr.Blocks(title="Real Estate AI") as demo:
265
+ gr.Markdown("# Real Estate Voice Assistant")
266
+ gr.Markdown("Ask about projects in Pune, Mumbai, Bengaluru, etc.")
267
+
268
+ with gr.Row():
269
+ inp = gr.Audio(sources=["microphone"], type="filepath", label="Speak")
270
+ out_audio = gr.Audio(label="AI Response", type="filepath")
271
+
272
+ out_text = gr.Textbox(label="Conversation", lines=8)
273
+
274
+ inp.change(process_audio, inp, [out_audio, out_text])
275
+
276
+ gr.Examples(
277
+ examples=[
278
+ ["Ongoing projects in Pune?"],
279
+ ["Mumbai mein upcoming flats?"],
280
+ ["Completed villas in Goa"]
281
+ ],
282
+ inputs=inp
283
+ )
284
+
285
+
286
+ # --- MOUNT GRADIO ---
287
+ app = gr.mount_gradio_app(app, demo, path="/")
requirements.txt CHANGED
@@ -1,5 +1,6 @@
1
  fastapi==0.115.13
2
  uvicorn==0.34.3
 
3
  websockets==15.0.1
4
  openai==2.3.0
5
  elevenlabs==2.17.0
 
1
  fastapi==0.115.13
2
  uvicorn==0.34.3
3
+ gradio==5.49.1
4
  websockets==15.0.1
5
  openai==2.3.0
6
  elevenlabs==2.17.0