freddyaboulton HF Staff commited on
Commit
9a201dd
·
verified ·
1 Parent(s): 9f9afa9

Upload folder using huggingface_hub

Browse files
Files changed (4) hide show
  1. README.md +1 -1
  2. app.py +6 -4
  3. index.html +116 -29
  4. requirements.txt +1 -1
README.md CHANGED
@@ -9,7 +9,7 @@ app_file: app.py
9
  pinned: false
10
  license: mit
11
  short_description: Llama 3.2 - SambaNova API
12
- tags: [webrtc, websocket, gradio, secret|TWILIO_ACCOUNT_SID, secret|TWILIO_AUTH_TOKEN, secret|SAMBANOVA_API_KEY]
13
  ---
14
 
15
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
9
  pinned: false
10
  license: mit
11
  short_description: Llama 3.2 - SambaNova API
12
+ tags: [webrtc, websocket, gradio, secret|HF_TOKEN_ALT, secret|SAMBANOVA_API_KEY]
13
  ---
14
 
15
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app.py CHANGED
@@ -13,8 +13,8 @@ from fastrtc import (
13
  AdditionalOutputs,
14
  ReplyOnPause,
15
  Stream,
 
16
  get_stt_model,
17
- get_twilio_turn_credentials,
18
  )
19
  from gradio.utils import get_space
20
  from pydantic import BaseModel
@@ -38,6 +38,7 @@ def response(
38
  ):
39
  gradio_chatbot = gradio_chatbot or []
40
  conversation_state = conversation_state or []
 
41
 
42
  text = stt_model.stt(audio)
43
  sample_rate, array = audio
@@ -47,7 +48,6 @@ def response(
47
  yield AdditionalOutputs(gradio_chatbot, conversation_state)
48
 
49
  conversation_state.append({"role": "user", "content": text})
50
-
51
  request = client.chat.completions.create(
52
  model="meta-llama/Llama-3.2-3B-Instruct",
53
  messages=conversation_state, # type: ignore
@@ -75,7 +75,7 @@ stream = Stream(
75
  additional_outputs=[chatbot, state],
76
  additional_outputs_handler=lambda *a: (a[2], a[3]),
77
  concurrency_limit=20 if get_space() else None,
78
- rtc_configuration=get_twilio_turn_credentials() if get_space() else None,
79
  )
80
 
81
  app = FastAPI()
@@ -95,7 +95,9 @@ class InputData(BaseModel):
95
 
96
  @app.get("/")
97
  async def _():
98
- rtc_config = get_twilio_turn_credentials() if get_space() else None
 
 
99
  html_content = (curr_dir / "index.html").read_text()
100
  html_content = html_content.replace("__RTC_CONFIGURATION__", json.dumps(rtc_config))
101
  return HTMLResponse(content=html_content)
 
13
  AdditionalOutputs,
14
  ReplyOnPause,
15
  Stream,
16
+ get_cloudflare_turn_credentials_async,
17
  get_stt_model,
 
18
  )
19
  from gradio.utils import get_space
20
  from pydantic import BaseModel
 
38
  ):
39
  gradio_chatbot = gradio_chatbot or []
40
  conversation_state = conversation_state or []
41
+ print("chatbot", gradio_chatbot)
42
 
43
  text = stt_model.stt(audio)
44
  sample_rate, array = audio
 
48
  yield AdditionalOutputs(gradio_chatbot, conversation_state)
49
 
50
  conversation_state.append({"role": "user", "content": text})
 
51
  request = client.chat.completions.create(
52
  model="meta-llama/Llama-3.2-3B-Instruct",
53
  messages=conversation_state, # type: ignore
 
75
  additional_outputs=[chatbot, state],
76
  additional_outputs_handler=lambda *a: (a[2], a[3]),
77
  concurrency_limit=20 if get_space() else None,
78
+ rtc_configuration=get_cloudflare_turn_credentials_async,
79
  )
80
 
81
  app = FastAPI()
 
95
 
96
  @app.get("/")
97
  async def _():
98
+ rtc_config = await get_cloudflare_turn_credentials_async(
99
+ hf_token=os.getenv("HF_TOKEN_ALT")
100
+ )
101
  html_content = (curr_dir / "index.html").read_text()
102
  html_content = html_content.replace("__RTC_CONFIGURATION__", json.dumps(rtc_config))
103
  return HTMLResponse(content=html_content)
index.html CHANGED
@@ -72,13 +72,17 @@
72
  background-color: #0066cc;
73
  color: white;
74
  border: none;
75
- padding: 12px 24px;
76
  font-family: inherit;
77
  font-size: 14px;
78
  cursor: pointer;
79
  transition: all 0.3s;
80
  border-radius: 4px;
81
  font-weight: 500;
 
 
 
 
82
  }
83
 
84
  button:hover {
@@ -94,7 +98,6 @@
94
  align-items: center;
95
  justify-content: center;
96
  gap: 12px;
97
- min-width: 180px;
98
  }
99
 
100
  .spinner {
@@ -118,7 +121,6 @@
118
  align-items: center;
119
  justify-content: center;
120
  gap: 12px;
121
- min-width: 180px;
122
  }
123
 
124
  .pulse-circle {
@@ -200,6 +202,23 @@
200
  background-color: #ffd700;
201
  color: black;
202
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  </style>
204
  </head>
205
 
@@ -239,28 +258,82 @@
239
  let audioContext, analyser, audioSource;
240
  let messages = [];
241
  let eventSource;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
 
243
  function updateButtonState() {
244
- const button = document.getElementById('start-button');
 
 
 
 
 
245
  if (peerConnection && (peerConnection.connectionState === 'connecting' || peerConnection.connectionState === 'new')) {
246
- button.innerHTML = `
247
  <div class="icon-with-spinner">
248
  <div class="spinner"></div>
249
  <span>Connecting...</span>
250
  </div>
251
  `;
 
252
  } else if (peerConnection && peerConnection.connectionState === 'connected') {
253
- button.innerHTML = `
254
- <div class="pulse-container">
255
- <div class="pulse-circle"></div>
256
- <span>Stop Conversation</span>
257
- </div>
258
  `;
 
 
 
 
 
 
 
 
 
 
 
259
  } else {
260
- button.innerHTML = 'Start Conversation';
 
261
  }
262
  }
263
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  function setupAudioVisualization(stream) {
265
  audioContext = new (window.AudioContext || window.webkitAudioContext)();
266
  analyser = audioContext.createAnalyser();
@@ -357,20 +430,20 @@
357
  const offer = await peerConnection.createOffer();
358
  await peerConnection.setLocalDescription(offer);
359
 
360
- peerConnection.onicecandidate = ({ candidate }) => {
361
- if (candidate) {
362
- console.debug("Sending ICE candidate", candidate);
363
- fetch('/webrtc/offer', {
364
  method: 'POST',
365
  headers: { 'Content-Type': 'application/json' },
366
  body: JSON.stringify({
367
  candidate: candidate.toJSON(),
368
  webrtc_id: webrtc_id,
369
  type: "ice-candidate",
370
- })
371
- })
372
- }
373
- };
374
 
375
  peerConnection.addEventListener('connectionstatechange', () => {
376
  console.log('connectionstatechange', peerConnection.connectionState);
@@ -378,6 +451,8 @@
378
  clearTimeout(timeoutId);
379
  const toast = document.getElementById('error-toast');
380
  toast.style.display = 'none';
 
 
381
  }
382
  updateButtonState();
383
  });
@@ -448,9 +523,10 @@
448
 
449
  if (animationFrame) {
450
  cancelAnimationFrame(animationFrame);
 
451
  }
452
  if (audioContext) {
453
- audioContext.close();
454
  audioContext = null;
455
  analyser = null;
456
  audioSource = null;
@@ -464,22 +540,33 @@
464
  });
465
  }
466
 
467
- if (peerConnection.getSenders) {
468
- peerConnection.getSenders().forEach(sender => {
469
- if (sender.track && sender.track.stop) sender.track.stop();
470
- });
471
- }
472
  peerConnection.close();
 
 
473
  }
 
474
  updateButtonState();
475
  audioLevel = 0;
476
  }
477
 
478
- startButton.addEventListener('click', () => {
479
- if (!peerConnection || peerConnection.connectionState !== 'connected') {
480
- setupWebRTC();
481
- } else {
 
 
 
482
  stop();
 
 
 
 
 
 
483
  }
484
  });
485
  </script>
 
72
  background-color: #0066cc;
73
  color: white;
74
  border: none;
75
+ padding: 12px 18px;
76
  font-family: inherit;
77
  font-size: 14px;
78
  cursor: pointer;
79
  transition: all 0.3s;
80
  border-radius: 4px;
81
  font-weight: 500;
82
+ display: inline-flex;
83
+ align-items: center;
84
+ justify-content: center;
85
+ gap: 8px;
86
  }
87
 
88
  button:hover {
 
98
  align-items: center;
99
  justify-content: center;
100
  gap: 12px;
 
101
  }
102
 
103
  .spinner {
 
121
  align-items: center;
122
  justify-content: center;
123
  gap: 12px;
 
124
  }
125
 
126
  .pulse-circle {
 
202
  background-color: #ffd700;
203
  color: black;
204
  }
205
+
206
+ /* Styles for the mute toggle icon */
207
+ .mute-toggle {
208
+ width: 20px;
209
+ height: 20px;
210
+ cursor: pointer;
211
+ display: flex;
212
+ align-items: center;
213
+ justify-content: center;
214
+ flex-shrink: 0;
215
+ }
216
+
217
+ .mute-toggle svg {
218
+ width: 100%;
219
+ height: 100%;
220
+ stroke: white;
221
+ }
222
  </style>
223
  </head>
224
 
 
258
  let audioContext, analyser, audioSource;
259
  let messages = [];
260
  let eventSource;
261
+ let isMuted = false;
262
+
263
+ // SVG Icons
264
+ const micIconSVG = `
265
+ <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
266
+ <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
267
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
268
+ <line x1="12" y1="19" x2="12" y2="23"></line>
269
+ <line x1="8" y1="23" x2="16" y2="23"></line>
270
+ </svg>`;
271
+
272
+ const micMutedIconSVG = `
273
+ <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
274
+ <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
275
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
276
+ <line x1="12" y1="19" x2="12" y2="23"></line>
277
+ <line x1="8" y1="23" x2="16" y2="23"></line>
278
+ <line x1="1" y1="1" x2="23" y2="23"></line>
279
+ </svg>`;
280
 
281
  function updateButtonState() {
282
+ const existingMuteButton = startButton.querySelector('.mute-toggle');
283
+ if (existingMuteButton) {
284
+ existingMuteButton.removeEventListener('click', toggleMute);
285
+ }
286
+ startButton.innerHTML = '';
287
+
288
  if (peerConnection && (peerConnection.connectionState === 'connecting' || peerConnection.connectionState === 'new')) {
289
+ startButton.innerHTML = `
290
  <div class="icon-with-spinner">
291
  <div class="spinner"></div>
292
  <span>Connecting...</span>
293
  </div>
294
  `;
295
+ startButton.disabled = true;
296
  } else if (peerConnection && peerConnection.connectionState === 'connected') {
297
+ const pulseContainer = document.createElement('div');
298
+ pulseContainer.className = 'pulse-container';
299
+ pulseContainer.innerHTML = `
300
+ <div class="pulse-circle"></div>
301
+ <span>Stop Conversation</span>
302
  `;
303
+
304
+ const muteToggle = document.createElement('div');
305
+ muteToggle.className = 'mute-toggle';
306
+ muteToggle.title = isMuted ? 'Unmute' : 'Mute';
307
+ muteToggle.innerHTML = isMuted ? micMutedIconSVG : micIconSVG;
308
+ muteToggle.addEventListener('click', toggleMute);
309
+
310
+ startButton.appendChild(pulseContainer);
311
+ startButton.appendChild(muteToggle);
312
+ startButton.disabled = false;
313
+
314
  } else {
315
+ startButton.textContent = 'Start Conversation';
316
+ startButton.disabled = false;
317
  }
318
  }
319
 
320
+ function toggleMute(event) {
321
+ event.stopPropagation();
322
+ if (!peerConnection || peerConnection.connectionState !== 'connected') return;
323
+
324
+ isMuted = !isMuted;
325
+ console.log("Mute toggled:", isMuted);
326
+
327
+ peerConnection.getSenders().forEach(sender => {
328
+ if (sender.track && sender.track.kind === 'audio') {
329
+ sender.track.enabled = !isMuted;
330
+ console.log(`Audio track ${sender.track.id} enabled: ${!isMuted}`);
331
+ }
332
+ });
333
+
334
+ updateButtonState();
335
+ }
336
+
337
  function setupAudioVisualization(stream) {
338
  audioContext = new (window.AudioContext || window.webkitAudioContext)();
339
  analyser = audioContext.createAnalyser();
 
430
  const offer = await peerConnection.createOffer();
431
  await peerConnection.setLocalDescription(offer);
432
 
433
+ peerConnection.onicecandidate = ({ candidate }) => {
434
+ if (candidate) {
435
+ console.debug("Sending ICE candidate", candidate);
436
+ fetch('/webrtc/offer', {
437
  method: 'POST',
438
  headers: { 'Content-Type': 'application/json' },
439
  body: JSON.stringify({
440
  candidate: candidate.toJSON(),
441
  webrtc_id: webrtc_id,
442
  type: "ice-candidate",
443
+ })
444
+ })
445
+ }
446
+ };
447
 
448
  peerConnection.addEventListener('connectionstatechange', () => {
449
  console.log('connectionstatechange', peerConnection.connectionState);
 
451
  clearTimeout(timeoutId);
452
  const toast = document.getElementById('error-toast');
453
  toast.style.display = 'none';
454
+ } else if (['closed', 'failed', 'disconnected'].includes(peerConnection.connectionState)) {
455
+ stop();
456
  }
457
  updateButtonState();
458
  });
 
523
 
524
  if (animationFrame) {
525
  cancelAnimationFrame(animationFrame);
526
+ animationFrame = null;
527
  }
528
  if (audioContext) {
529
+ audioContext.close().catch(e => console.error("Error closing AudioContext:", e));
530
  audioContext = null;
531
  analyser = null;
532
  audioSource = null;
 
540
  });
541
  }
542
 
543
+ peerConnection.onicecandidate = null;
544
+ peerConnection.ondatachannel = null;
545
+ peerConnection.onconnectionstatechange = null;
546
+
 
547
  peerConnection.close();
548
+ peerConnection = null;
549
+ console.log("Peer connection closed.");
550
  }
551
+ isMuted = false;
552
  updateButtonState();
553
  audioLevel = 0;
554
  }
555
 
556
+ startButton.addEventListener('click', (event) => {
557
+ if (event.target.closest('.mute-toggle')) {
558
+ return;
559
+ }
560
+
561
+ if (peerConnection && peerConnection.connectionState === 'connected') {
562
+ console.log("Stop button clicked");
563
  stop();
564
+ } else if (!peerConnection || ['new', 'closed', 'failed', 'disconnected'].includes(peerConnection.connectionState)) {
565
+ console.log("Start button clicked");
566
+ messages = [];
567
+ chatMessages.innerHTML = '';
568
+ setupWebRTC();
569
+ updateButtonState();
570
  }
571
  });
572
  </script>
requirements.txt CHANGED
@@ -1,4 +1,4 @@
1
- fastrtc[vad,stt]@https://huggingface.co/datasets/freddyaboulton/bucket/resolve/main/wheels/fastrtc/fastrtc-0.0.17rc3-py3-none-any.whl
2
  python-dotenv
3
  huggingface_hub>=0.29.0
4
  twilio
 
1
+ fastrtc[vad, stt]==0.0.20.rc2
2
  python-dotenv
3
  huggingface_hub>=0.29.0
4
  twilio