Spaces:
Running on Zero
Running on Zero
Commit ·
e9d083e
1
Parent(s): 0382c60
Relay Space WebRTC through short-lived TURN
Browse files- README.md +5 -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 |
-
-
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=
|
| 524 |
-
server_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(
|