AgentNewTwo commited on
Commit
e9d083e
·
1 Parent(s): 0382c60

Relay Space WebRTC through short-lived TURN

Browse files
Files changed (2) hide show
  1. README.md +5 -2
  2. streaming_app.py +46 -5
README.md CHANGED
@@ -50,8 +50,11 @@ ZeroGPU, the approach is invalidated rather than disguised as turn-based chat.
50
  - Gated model files are requested with the signed-in user's short-lived Hugging Face OAuth token; no broad
51
  repository-write token is stored in the Space.
52
  - Audio queues, transcript text, and scene state are session-only and are discarded when the session ends.
53
- - WebRTC uses Cloudflare's public STUN service for connection discovery. STUN receives network metadata but is not
54
- an audio relay; no TURN relay or third-party inference/media API is configured in this spike.
 
 
 
55
  - Version 1 provides only NVIDIA's bundled synthetic voice prompts. It does not clone uploaded voices.
56
  - Player audio is treated as untrusted dialogue, never as a system or director instruction.
57
  - Prompt-leak detection is best-effort and the fictional secret is deliberately harmless test data.
 
50
  - Gated model files are requested with the signed-in user's short-lived Hugging Face OAuth token; no broad
51
  repository-write token is stored in the Space.
52
  - Audio queues, transcript text, and scene state are session-only and are discarded when the session ends.
53
+ - Hugging Face Spaces requires a TURN relay for reliable WebRTC through its cloud firewall. When the signed-in user
54
+ connects the microphone, FastRTC exchanges that user's short-lived Hugging Face OAuth token for temporary
55
+ Cloudflare TURN credentials. Cloudflare can relay encrypted WebRTC traffic and receives connection metadata; it
56
+ does not receive plaintext prompts or model inference requests. No persistent Hugging Face token is stored as a
57
+ Space secret.
58
  - Version 1 provides only NVIDIA's bundled synthetic voice prompts. It does not clone uploaded voices.
59
  - Player audio is treated as untrusted dialogue, never as a system or director instruction.
60
  - Prompt-leak detection is best-effort and the fictional secret is deliberately harmless test data.
streaming_app.py CHANGED
@@ -5,6 +5,7 @@ import json
5
  import multiprocessing
6
  import queue
7
  import tarfile
 
8
  import time
9
  import uuid
10
  from pathlib import Path
@@ -14,7 +15,7 @@ import numpy as np
14
  import sentencepiece
15
  import spaces
16
  import torch
17
- from fastrtc import StreamHandler, WebRTC
18
  from huggingface_hub import hf_hub_download
19
 
20
 
@@ -28,9 +29,10 @@ INPUT_QUEUE_FRAMES = 12
28
  OUTPUT_QUEUE_FRAMES = 20
29
  WEBRTC_FRAME_RATE = 50
30
  OUTPUT_SAMPLE_RATE = 24_000
31
- RTC_CONFIGURATION = {
32
  "iceServers": [{"urls": ["stun:stun.cloudflare.com:3478"]}],
33
  }
 
34
 
35
  ALL_VOICES = [
36
  "NATF0", "NATF1", "NATF2", "NATF3",
@@ -180,9 +182,46 @@ def get_models(token):
180
 
181
  _manager = multiprocessing.Manager()
182
  _bridge_registry = _manager.dict()
 
 
 
183
 
184
 
185
- def new_bridge():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  input_queue = _manager.Queue(INPUT_QUEUE_FRAMES)
187
  output_queue = _manager.Queue(OUTPUT_QUEUE_FRAMES)
188
  stop_event = _manager.Event()
@@ -227,6 +266,7 @@ def stop_session(session_id):
227
  bridge = _bridge_registry.get(session_id) if session_id else None
228
  if bridge is not None:
229
  bridge[2].set()
 
230
  return "Session stop requested. The microphone can now be disconnected."
231
 
232
 
@@ -460,6 +500,7 @@ def run_streaming_session(
460
 
461
  ready_event.clear()
462
  stop_event.set()
 
463
  final_metrics = session_metrics(
464
  "complete",
465
  session_started,
@@ -520,8 +561,8 @@ with gr.Blocks(title="RoleForge Streaming Spike", theme=gr.themes.Soft(), css=CS
520
  label="2. Connect microphone after Engine ready",
521
  modality="audio",
522
  mode="send-receive",
523
- rtc_configuration=RTC_CONFIGURATION,
524
- server_rtc_configuration=RTC_CONFIGURATION,
525
  full_screen=False,
526
  )
527
  gr.Markdown(
 
5
  import multiprocessing
6
  import queue
7
  import tarfile
8
+ import threading
9
  import time
10
  import uuid
11
  from pathlib import Path
 
15
  import sentencepiece
16
  import spaces
17
  import torch
18
+ from fastrtc import StreamHandler, WebRTC, get_cloudflare_turn_credentials_async
19
  from huggingface_hub import hf_hub_download
20
 
21
 
 
29
  OUTPUT_QUEUE_FRAMES = 20
30
  WEBRTC_FRAME_RATE = 50
31
  OUTPUT_SAMPLE_RATE = 24_000
32
+ SERVER_RTC_CONFIGURATION = {
33
  "iceServers": [{"urls": ["stun:stun.cloudflare.com:3478"]}],
34
  }
35
+ TURN_TOKEN_TTL_SECONDS = 10 * 60
36
 
37
  ALL_VOICES = [
38
  "NATF0", "NATF1", "NATF2", "NATF3",
 
182
 
183
  _manager = multiprocessing.Manager()
184
  _bridge_registry = _manager.dict()
185
+ _turn_token_lock = threading.Lock()
186
+ _turn_oauth_token = None
187
+ _turn_oauth_deadline = 0.0
188
 
189
 
190
+ def remember_turn_token(oauth_token):
191
+ """Keep the signed-in user's short-lived token only long enough to mint TURN credentials."""
192
+ global _turn_oauth_token, _turn_oauth_deadline
193
+ token = getattr(oauth_token, "token", None)
194
+ if not token:
195
+ raise gr.Error("Sign in with Hugging Face before starting the live engine.")
196
+ with _turn_token_lock:
197
+ _turn_oauth_token = token
198
+ _turn_oauth_deadline = time.monotonic() + TURN_TOKEN_TTL_SECONDS
199
+
200
+
201
+ def clear_turn_token():
202
+ global _turn_oauth_token, _turn_oauth_deadline
203
+ with _turn_token_lock:
204
+ _turn_oauth_token = None
205
+ _turn_oauth_deadline = 0.0
206
+
207
+
208
+ async def get_turn_configuration():
209
+ """Mint relay credentials at microphone-connect time without a persistent Space secret."""
210
+ with _turn_token_lock:
211
+ token = _turn_oauth_token
212
+ deadline = _turn_oauth_deadline
213
+ if not token or time.monotonic() >= deadline:
214
+ raise RuntimeError("Start the live engine before connecting the microphone.")
215
+ configuration = await get_cloudflare_turn_credentials_async(
216
+ hf_token=token,
217
+ ttl=TURN_TOKEN_TTL_SECONDS,
218
+ )
219
+ clear_turn_token()
220
+ return configuration
221
+
222
+
223
+ def new_bridge(oauth_token: gr.OAuthToken | None):
224
+ remember_turn_token(oauth_token)
225
  input_queue = _manager.Queue(INPUT_QUEUE_FRAMES)
226
  output_queue = _manager.Queue(OUTPUT_QUEUE_FRAMES)
227
  stop_event = _manager.Event()
 
266
  bridge = _bridge_registry.get(session_id) if session_id else None
267
  if bridge is not None:
268
  bridge[2].set()
269
+ clear_turn_token()
270
  return "Session stop requested. The microphone can now be disconnected."
271
 
272
 
 
500
 
501
  ready_event.clear()
502
  stop_event.set()
503
+ clear_turn_token()
504
  final_metrics = session_metrics(
505
  "complete",
506
  session_started,
 
561
  label="2. Connect microphone after Engine ready",
562
  modality="audio",
563
  mode="send-receive",
564
+ rtc_configuration=get_turn_configuration,
565
+ server_rtc_configuration=SERVER_RTC_CONFIGURATION,
566
  full_screen=False,
567
  )
568
  gr.Markdown(