kanhaiyaML commited on
Commit
c04034a
Β·
verified Β·
1 Parent(s): 3dc9027

adding voice feature (tts - sts)

Browse files
Files changed (4) hide show
  1. api.py +18 -1
  2. index.html +195 -2
  3. speak.py +28 -0
  4. transcribe.py +26 -0
api.py CHANGED
@@ -19,6 +19,8 @@ from fastapi.responses import StreamingResponse
19
  from chatbot import stream_chat,generate_title,chat
20
  import json
21
  from sqlalchemy import delete
 
 
22
 
23
 
24
  @asynccontextmanager
@@ -146,7 +148,6 @@ async def chat_stream_route(data: MessageSend, db: AsyncSession = Depends(get_db
146
  messages = result.scalars().all()
147
  history = [{"role":m.role,"content":m.content} for m in messages]
148
 
149
-
150
  if len(history) == 0:
151
  generated_title = await generate_title(data.message)
152
  await db.execute(
@@ -190,3 +191,19 @@ async def chat_stream_route(data: MessageSend, db: AsyncSession = Depends(get_db
190
 
191
  return StreamingResponse(generate(),media_type="text/event-stream",background=background_tasks)
192
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  from chatbot import stream_chat,generate_title,chat
20
  import json
21
  from sqlalchemy import delete
22
+ from fastapi import UploadFile,File,Response
23
+ from transcribe import transcribe_audio
24
 
25
 
26
  @asynccontextmanager
 
148
  messages = result.scalars().all()
149
  history = [{"role":m.role,"content":m.content} for m in messages]
150
 
 
151
  if len(history) == 0:
152
  generated_title = await generate_title(data.message)
153
  await db.execute(
 
191
 
192
  return StreamingResponse(generate(),media_type="text/event-stream",background=background_tasks)
193
 
194
+
195
+ @app.post("/transcribe")
196
+ async def transcribe_endpoint(audio: UploadFile = File(...)):
197
+ audio_bytes = await audio.read()
198
+ text = await transcribe_audio(audio_bytes,audio.filename)
199
+ return {"text": text}
200
+
201
+
202
+ from speak import text_to_speach
203
+
204
+ class SpeakRequest(BaseModel):
205
+ text: str
206
+
207
+ @app.post("/speak")
208
+ async def speak(request: SpeakRequest):
209
+ return await text_to_speach(request.text)
index.html CHANGED
@@ -1147,6 +1147,45 @@
1147
  color: white;
1148
  border-color: var(--accent);
1149
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1150
  </style>
1151
  </head>
1152
  <body>
@@ -1203,6 +1242,9 @@
1203
  <textarea id="messageInput" placeholder="Message KAI..." rows="1"
1204
  oninput="autoResize(this); toggleSend()"
1205
  onkeydown="handleKey(event)"></textarea>
 
 
 
1206
  <button id="sendBtn" onclick="sendMessage()">
1207
  <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
1208
  <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
@@ -1252,8 +1294,8 @@
1252
  </main>
1253
 
1254
  <script>
1255
- const API = 'https://kanhaiyaml-chat-bot.hf.space';
1256
- const REVIEW_API = 'https://kanhaiyaml-chat-bot.hf.space/review';
1257
  let currentConvId = null;
1258
  let isTyping = false;
1259
  let webSearchOn = false;
@@ -1261,6 +1303,96 @@
1261
  let currentMode = 'chat';
1262
  let sidebarCollapsed = false;
1263
  let conversationToDelete = null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1264
 
1265
  // ── Sidebar Toggle ──────────────────────────────────
1266
  function toggleSidebar() {
@@ -1560,6 +1692,8 @@
1560
  if (streamDone) break;
1561
  }
1562
  msgEl.innerHTML = marked.parse(fullText);
 
 
1563
  msgEl.style.display = 'none';
1564
  msgEl.offsetHeight;
1565
  msgEl.style.display = '';
@@ -1590,6 +1724,7 @@
1590
  </div>
1591
  `;
1592
  area.appendChild(row);
 
1593
  scrollToBottom();
1594
  }
1595
 
@@ -1674,6 +1809,64 @@
1674
  }
1675
  }
1676
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1677
  // ── CODE REVIEW LOGIC ─────────────────────────────────
1678
  const codeInput = document.getElementById('codeInput');
1679
  const reviewQuestionInput = document.getElementById('reviewQuestionInput');
 
1147
  color: white;
1148
  border-color: var(--accent);
1149
  }
1150
+ .mic-btn {
1151
+ width: 34px;
1152
+ height: 34px;
1153
+ background: transparent;
1154
+ border: 1px solid var(--border);
1155
+ border-radius: 9px;
1156
+ cursor: pointer;
1157
+ display: flex;
1158
+ align-items: center;
1159
+ justify-content: center;
1160
+ flex-shrink: 0;
1161
+ transition: all 0.2s;
1162
+ color: var(--text-muted);
1163
+ font-size: 16px;
1164
+ }
1165
+
1166
+ .mic-btn.recording {
1167
+ background: var(--red);
1168
+ border-color: var(--red);
1169
+ color: white;
1170
+ animation: pulse 1.5s infinite;
1171
+ }
1172
+
1173
+ .speak-btn {
1174
+ background: transparent;
1175
+ border: 1px solid var(--border);
1176
+ border-radius: 6px;
1177
+ cursor: pointer;
1178
+ padding: 4px 8px;
1179
+ color: var(--text-muted);
1180
+ font-size: 14px;
1181
+ margin-top: 6px;
1182
+ transition: all 0.2s;
1183
+ }
1184
+
1185
+ .speak-btn:hover {
1186
+ background: var(--surface2);
1187
+ color: var(--accent);
1188
+ }
1189
  </style>
1190
  </head>
1191
  <body>
 
1242
  <textarea id="messageInput" placeholder="Message KAI..." rows="1"
1243
  oninput="autoResize(this); toggleSend()"
1244
  onkeydown="handleKey(event)"></textarea>
1245
+ <button class="mic-btn" id="micBtn" onclick="toggleRecording()" title="Voice input">
1246
+ 🎀
1247
+ </button>
1248
  <button id="sendBtn" onclick="sendMessage()">
1249
  <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
1250
  <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
 
1294
  </main>
1295
 
1296
  <script>
1297
+ const API = 'http://localhost:8000';
1298
+ const REVIEW_API = 'http://localhost:8000/review';
1299
  let currentConvId = null;
1300
  let isTyping = false;
1301
  let webSearchOn = false;
 
1303
  let currentMode = 'chat';
1304
  let sidebarCollapsed = false;
1305
  let conversationToDelete = null;
1306
+ let mediaRecorder = null;
1307
+ let audioChunks = [];
1308
+ let isRecording = false;
1309
+
1310
+ async function toggleRecording() {
1311
+ if (isRecording) {
1312
+ await stopRecording();
1313
+ } else {
1314
+ await startRecording();
1315
+ }
1316
+ }
1317
+
1318
+ async function startRecording() {
1319
+ try {
1320
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
1321
+ mediaRecorder = new MediaRecorder(stream);
1322
+ audioChunks = [];
1323
+
1324
+ mediaRecorder.ondataavailable = (e) => {
1325
+ audioChunks.push(e.data);
1326
+ };
1327
+
1328
+ mediaRecorder.onstop = async () => {
1329
+ const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
1330
+ await sendAudioToBackend(audioBlob);
1331
+ stream.getTracks().forEach(track => track.stop());
1332
+ };
1333
+
1334
+ mediaRecorder.start();
1335
+ isRecording = true;
1336
+ document.getElementById('micBtn').classList.add('recording');
1337
+ document.getElementById('micBtn').innerHTML = 'πŸ”΄';
1338
+ } catch (err) {
1339
+ alert('Microphone access denied. Please allow microphone access.');
1340
+ }
1341
+ }
1342
+
1343
+ async function stopRecording() {
1344
+ if (mediaRecorder) {
1345
+ mediaRecorder.stop();
1346
+ isRecording = false;
1347
+ document.getElementById('micBtn').classList.remove('recording');
1348
+ document.getElementById('micBtn').innerHTML = '🎀';
1349
+ }
1350
+ }
1351
+
1352
+ async function sendAudioToBackend(audioBlob) {
1353
+ const formData = new FormData();
1354
+ formData.append('audio', audioBlob, 'recording.webm');
1355
+
1356
+ console.log("Sending audio, size:", audioBlob.size);
1357
+
1358
+ try {
1359
+ const res = await fetch(`${API}/transcribe`, {
1360
+ method: 'POST',
1361
+ body: formData
1362
+ });
1363
+
1364
+ console.log("Response status:", res.status);
1365
+
1366
+ if (!res.ok) {
1367
+ const errorText = await res.text();
1368
+ console.error("Transcription error:", errorText);
1369
+ alert('Transcription failed: ' + errorText);
1370
+ return;
1371
+ }
1372
+
1373
+ const data = await res.json();
1374
+ console.log("Transcription result:", data);
1375
+
1376
+ const transcribedText = data.text.text; // ← fixed
1377
+
1378
+ if (!transcribedText || transcribedText.trim() === '') {
1379
+ alert('No text detected. Please try again.');
1380
+ return;
1381
+ }
1382
+
1383
+ document.getElementById('messageInput').value = transcribedText;
1384
+ toggleSend();
1385
+ autoResize(document.getElementById('messageInput'));
1386
+
1387
+ if (transcribedText.trim()) {
1388
+ sendMessage();
1389
+ }
1390
+
1391
+ } catch (err) {
1392
+ console.error('Network error:', err);
1393
+ alert('Failed to connect to server.');
1394
+ }
1395
+ }
1396
 
1397
  // ── Sidebar Toggle ──────────────────────────────────
1398
  function toggleSidebar() {
 
1692
  if (streamDone) break;
1693
  }
1694
  msgEl.innerHTML = marked.parse(fullText);
1695
+ addSpeakButton(msgRow);
1696
+ speakText(fullText);
1697
  msgEl.style.display = 'none';
1698
  msgEl.offsetHeight;
1699
  msgEl.style.display = '';
 
1724
  </div>
1725
  `;
1726
  area.appendChild(row);
1727
+ if (!isUser) addSpeakButton(row);
1728
  scrollToBottom();
1729
  }
1730
 
 
1809
  }
1810
  }
1811
 
1812
+ // function speakText(text) {
1813
+ // if ('speechSynthesis' in window) {
1814
+ // window.speechSynthesis.cancel();
1815
+ // const utterance = new SpeechSynthesisUtterance(text);
1816
+ // utterance.lang = 'hi-IN'
1817
+ // utterance.rate = 1;
1818
+ // utterance.pitch = 1;
1819
+ // window.speechSynthesis.speak(utterance);
1820
+ // }
1821
+ // }
1822
+
1823
+ function addSpeakButton(row) {
1824
+ const msgText = row.querySelector('.message-text');
1825
+ const text = msgText.textContent;
1826
+
1827
+ const speakBtn = document.createElement('button');
1828
+ speakBtn.className = 'speak-btn';
1829
+ speakBtn.innerHTML = 'πŸ”Š';
1830
+ speakBtn.title = 'Listen';
1831
+ speakBtn.onclick = () => speakText(text);
1832
+
1833
+ row.querySelector('.message-content').appendChild(speakBtn);
1834
+ }
1835
+
1836
+ async function speakText(text) {
1837
+ try {
1838
+ const res = await fetch(`${API}/speak`, {
1839
+ method: 'POST',
1840
+ headers: { 'Content-Type': 'application/json' },
1841
+ body: JSON.stringify({ text: text })
1842
+ });
1843
+
1844
+ const audioBlob = await res.blob();
1845
+
1846
+ // Check if blob is valid
1847
+ if (audioBlob.size < 100) {
1848
+ console.error("Audio too small, something wrong");
1849
+ return;
1850
+ }
1851
+
1852
+ const audioUrl = URL.createObjectURL(audioBlob);
1853
+ const audio = new Audio(audioUrl);
1854
+
1855
+ // Must be triggered by user click - already is since speakText is called from onclick
1856
+ audio.load();
1857
+ await audio.play();
1858
+
1859
+ } catch (err) {
1860
+ console.error('Failed to speak:', err);
1861
+ // Fallback to browser TTS
1862
+ if ('speechSynthesis' in window) {
1863
+ const utterance = new SpeechSynthesisUtterance(text);
1864
+ utterance.lang = 'hi-IN';
1865
+ window.speechSynthesis.speak(utterance);
1866
+ }
1867
+ }
1868
+ }
1869
+
1870
  // ── CODE REVIEW LOGIC ─────────────────────────────────
1871
  const codeInput = document.getElementById('codeInput');
1872
  const reviewQuestionInput = document.getElementById('reviewQuestionInput');
speak.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+ from fastapi.responses import Response
3
+ import os
4
+ from dotenv import load_dotenv
5
+ load_dotenv()
6
+
7
+
8
+ ELEVENLABS_API_KEY = os.getenv("ELEVENLABS_API_KEY")
9
+ ELEVENLABS_VOICE_ID = os.getenv("ELEVENLABS_VOICE_ID")
10
+
11
+ async def text_to_speach(text: str):
12
+ url = f"https://api.elevenlabs.io/v1/text-to-speech/{ELEVENLABS_VOICE_ID}"
13
+
14
+ async with httpx.AsyncClient() as client:
15
+ response = await client.post(
16
+ url,
17
+ headers={"xi-api-key": ELEVENLABS_API_KEY},
18
+ json={
19
+ "text": text,
20
+ "model_id": "eleven_multilingual_v2",
21
+ "voice_settings": {
22
+ "stability": 0.5,
23
+ "similarity_boost": 0.75
24
+ }
25
+ }
26
+ )
27
+
28
+ return Response(content=response.content, media_type="audio/mpeg")
transcribe.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import tempfile
2
+ from groq import Groq
3
+ import os
4
+ from dotenv import load_dotenv
5
+ load_dotenv()
6
+
7
+
8
+ client = Groq(api_key=os.getenv("GROQ_API_KEY"))
9
+
10
+ async def transcribe_audio(audio_file: bytes, filename: str):
11
+ suffix = "." + filename.split(".")[-1]
12
+
13
+ with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
14
+ tmp.write(audio_file)
15
+ tmp_path = tmp.name
16
+
17
+ try:
18
+ with open(tmp_path,"rb") as f:
19
+ transcription = client.audio.transcriptions.create(
20
+ model="whisper-large-v3-turbo",
21
+ file=("audio.webm",f)
22
+ )
23
+ return {"text": transcription.text}
24
+ finally:
25
+ os.remove(tmp_path)
26
+