Archime commited on
Commit
75c9c9a
·
1 Parent(s): aaaa3df

impl fastrtc receive

Browse files
Files changed (5) hide show
  1. .gitattributes +2 -0
  2. app.py +198 -45
  3. app/utils.py +42 -36
  4. gpu_compute.py +61 -0
  5. requirements.txt +2 -1
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.mp4 filter=lfs diff=lfs merge=lfs -text
37
+ *.wav filter=lfs diff=lfs merge=lfs -text
app.py CHANGED
@@ -1,63 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
1
  from app.logger_config import logger as logging
2
  from app.utils import (
3
- debug_current_device,
4
- get_current_device
5
  )
6
- import os
7
- import gradio as gr
8
- import spaces
9
- import torch
10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
- logging.info("-----------info------------")
14
- logging.debug("-----------debug------------")
 
 
 
 
 
 
 
 
15
 
16
- @spaces.GPU
17
- def gpu_compute(name):
18
- logging.debug("=== Start of gpu_compute() ===")
19
- debug_current_device()
20
- tensor,device_name = compute(name)
21
- logging.debug("=== End of gpu_compute() ===")
22
- return f"Tensor: {tensor.cpu().numpy()} | Device: {device_name}"
 
 
 
 
 
 
23
 
24
- def cpu_compute(name):
25
- logging.debug("=== Start of cpu_compute() ===")
26
- debug_current_device()
27
 
28
- tensor,device_name = compute(name)
 
 
 
 
 
 
 
29
 
30
- logging.debug("=== End of cpu_compute() ===")
31
- return f"Tensor: {tensor.cpu().numpy()} | Device: {device_name}"
 
 
 
 
 
 
 
 
 
 
 
32
 
33
- def compute(name) :
34
- # Get device info
35
- device, device_name = get_current_device()
36
- # Create a tensor
37
- tensor = torch.tensor([len(name)], dtype=torch.float32, device=device)
38
- logging.debug(f"Tensor created: {tensor}")
39
- # Optional: free GPU memory
40
- if torch.cuda.is_available():
41
- torch.cuda.empty_cache()
42
- logging.debug("GPU cache cleared")
43
- return tensor, device_name
44
 
 
 
 
 
45
 
46
- block = gr.Blocks()
 
 
 
 
 
 
 
47
 
48
- with block as demo:
49
- with gr.Row():
50
- input_text = gr.Text()
51
- output_text = gr.Text()
52
- with gr.Row():
53
- gpu_button = gr.Button("GPU compute")
54
- cpu_button = gr.Button("CPU compute")
55
 
56
- gpu_button.click(fn=gpu_compute, inputs=[input_text],outputs=[output_text])
57
- cpu_button.click(fn=cpu_compute, inputs=[input_text],outputs=[output_text])
 
 
 
 
 
 
 
58
 
59
- with gr.Blocks() as demo:
60
- block.render()
61
 
62
  if __name__ == "__main__":
63
- demo.queue(max_size=10, api_open=False).launch(show_api=False)
 
1
+ from app.logger_config import logger as logging
2
+ import numpy as np
3
+ import gradio as gr
4
+ import asyncio
5
+ from fastrtc.webrtc import WebRTC
6
+ from pydub import AudioSegment
7
+ import time
8
+ import threading
9
+ import os # Added to check if file exists
10
+ from gradio.utils import get_space
11
+
12
  from app.logger_config import logger as logging
13
  from app.utils import (
14
+ generate_coturn_config
 
15
  )
 
 
 
 
16
 
17
+ # --- Constants and Global State ---
18
+ EXAMPLE_FILES = ["data/bonjour.wav", "data/bonjour2.wav"]
19
+ # The default file is the first in the list
20
+ DEFAULT_FILE = EXAMPLE_FILES[0]
21
+ streaming_should_stop = threading.Event()
22
+
23
+ def read_and_stream_audio(filepath_to_stream: str):
24
+ """
25
+ A synchronous generator that reads an audio file (via filepath_to_stream)
26
+ and streams it in 1-second chunks.
27
+ """
28
+
29
+ if not filepath_to_stream or not os.path.exists(filepath_to_stream):
30
+ logging.error(f"Audio file not found or not specified: {filepath_to_stream}")
31
+ # Attempt to use the default file as a fallback
32
+ if os.path.exists(DEFAULT_FILE):
33
+ logging.warning(f"Using default file: {DEFAULT_FILE}")
34
+ filepath_to_stream = DEFAULT_FILE
35
+ else:
36
+ logging.error("Default file not found. Stopping stream.")
37
+ return
38
+
39
+ logging.info(f"Preparing audio segment from: {filepath_to_stream}")
40
+ streaming_should_stop.clear()
41
+
42
+ try:
43
+ segment = AudioSegment.from_file(filepath_to_stream)
44
+ chunk_duree_ms = 1000
45
+ logging.info(f"Starting streaming in {chunk_duree_ms}ms chunks...")
46
+
47
+ for i, chunk in enumerate(segment[::chunk_duree_ms]):
48
+ iter_start_time = time.perf_counter()
49
+ logging.info(f"Sending chunk {i+1}...")
50
+
51
+ if streaming_should_stop.is_set():
52
+ logging.info("Stop signal received, breaking loop.")
53
+ break
54
+
55
+ output_chunk = (
56
+ chunk.frame_rate,
57
+ np.array(chunk.get_array_of_samples()).reshape(1, -1),
58
+ )
59
+
60
+ yield output_chunk
61
+
62
+ iter_end_time = time.perf_counter()
63
+ processing_duration_ms = (iter_end_time - iter_start_time) * 1000
64
+
65
+ sleep_duration = (chunk_duree_ms / 1000.0) - (processing_duration_ms / 1000.0) - 0.1
66
+ if sleep_duration < 0:
67
+ sleep_duration = 0.01 # Avoid negative sleep time
68
+
69
+ logging.debug(f"Processing time: {processing_duration_ms:.2f}ms, Sleep: {sleep_duration:.2f}s")
70
+
71
+ # Using wait() allows the thread to wake up if the signal is received
72
+ if streaming_should_stop.wait(timeout=sleep_duration):
73
+ logging.info("Stop signal received while waiting.")
74
+ break
75
+
76
+ logging.info("Streaming finished.")
77
+
78
+ except asyncio.CancelledError:
79
+ logging.info("Stream stopped by user (CancelledError).")
80
+ raise
81
+ except FileNotFoundError:
82
+ logging.error(f"Critical error: File not found: {filepath_to_stream}")
83
+ except Exception as e:
84
+ logging.error(f"Error during stream: {e}", exc_info=True)
85
+ raise
86
+ finally:
87
+ streaming_should_stop.clear()
88
+ logging.info("Stop signal cleared.")
89
+
90
+
91
+ def stop_streaming():
92
+ """Activates the stop signal for the generator."""
93
+ logging.info("Stop button clicked: sending stop signal.")
94
+ streaming_should_stop.set()
95
+ return None
96
+
97
+ # --- Gradio Interface ---
98
+
99
+ with gr.Blocks(theme=gr.themes.Soft()) as demo:
100
+ gr.Markdown(
101
+ "## Application 'Streamer' WebRTC (Serveur -> Client)\n"
102
+ "Utilisez l'exemple fourni, uploadez un fichier ou enregistrez depuis votre micro, "
103
+ "puis cliquez sur 'Start' pour écouter le stream."
104
+ )
105
+
106
+ # 1. State to store the path of the file to be read
107
+ active_filepath = gr.State(value=DEFAULT_FILE)
108
 
109
+ with gr.Row():
110
+ with gr.Column():
111
+ main_audio = gr.Audio(
112
+ label="Source Audio",
113
+ sources=["upload", "microphone"], # Combine both sources
114
+ type="filepath",
115
+ value=DEFAULT_FILE, # Default to the first example
116
+ )
117
+ with gr.Column():
118
+ webrtc_stream = WebRTC(
119
+ label="Stream Audio",
120
+ mode="receive",
121
+ modality="audio",
122
+ rtc_configuration=generate_coturn_config(),
123
+ visible=True,
124
+ height = 200,
125
+ )
126
+ # 4. Control buttons
127
+ with gr.Row():
128
+ with gr.Column():
129
+ start_button = gr.Button("Start Streaming", variant="primary")
130
+ stop_button = gr.Button("Stop Streaming", variant="stop", interactive=False)
131
+ with gr.Column():
132
+ gr.Text()
133
 
134
+ def set_new_file(filepath):
135
+ """Updates the state with the new path, or reverts to default if None."""
136
+ if filepath is None:
137
+ logging.info("Audio cleared, reverting to default example file.")
138
+ new_path = DEFAULT_FILE
139
+ else:
140
+ logging.info(f"New audio source selected: {filepath}")
141
+ new_path = filepath
142
+ # Returns the value to be put in the gr.State
143
+ return new_path
144
 
145
+ # Update the path if the user uploads, clears, or changes the file
146
+ main_audio.change(
147
+ fn=set_new_file,
148
+ inputs=[main_audio],
149
+ outputs=[active_filepath]
150
+ )
151
+
152
+ # Update the path if the user finishes a recording
153
+ main_audio.stop_recording(
154
+ fn=set_new_file,
155
+ inputs=[main_audio],
156
+ outputs=[active_filepath]
157
+ )
158
 
 
 
 
159
 
160
+ # Functions to update the interface state
161
+ def start_streaming_ui():
162
+ logging.info("UI: Starting stream. Disabling controls.")
163
+ return {
164
+ start_button: gr.Button(interactive=False),
165
+ stop_button: gr.Button(interactive=True),
166
+ main_audio: gr.Audio(visible=False),
167
+ }
168
 
169
+ def stop_streaming_ui():
170
+ logging.info("UI: Stopping stream. Re-enabling controls.")
171
+ return {
172
+ start_button: gr.Button(interactive=True),
173
+ stop_button: gr.Button(interactive=False),
174
+ main_audio: gr.Audio(
175
+ label="Source Audio",
176
+ sources=["upload", "microphone"], # Combine both sources
177
+ type="filepath",
178
+ value=active_filepath.value,
179
+ visible=True
180
+ ),
181
+ }
182
 
 
 
 
 
 
 
 
 
 
 
 
183
 
184
+ ui_components = [
185
+ start_button, stop_button,
186
+ main_audio,
187
+ ]
188
 
189
+ stream_event = webrtc_stream.stream(
190
+ fn=read_and_stream_audio,
191
+ inputs=[active_filepath],
192
+ outputs=[webrtc_stream],
193
+ trigger=start_button.click,
194
+ concurrency_id="audio_stream", # Concurrency ID
195
+ concurrency_limit=10
196
+ )
197
 
198
+ # Update the interface on START click
199
+ start_button.click(
200
+ fn=start_streaming_ui,
201
+ outputs=ui_components
202
+ )
 
 
203
 
204
+ # Fix: Ensure the stream is properly cancelled
205
+ stop_button.click(
206
+ fn=stop_streaming,
207
+ outputs=[webrtc_stream],
208
+ ).then(
209
+ fn=stop_streaming_ui, # THEN, update the interface
210
+ inputs=None,
211
+ outputs=ui_components
212
+ )
213
 
 
 
214
 
215
  if __name__ == "__main__":
216
+ demo.queue(max_size=10, api_open=False).launch(show_api=False, debug=True)
app/utils.py CHANGED
@@ -1,40 +1,10 @@
1
  import torch
2
  from app.logger_config import logger as logging
3
-
4
- # def debug_current_device():
5
- # """Logs detailed information about the current GPU or CPU device."""
6
- # logging.debug("=== Debugging current device ===")
7
-
8
- # if torch.cuda.is_available():
9
- # device = torch.device("cuda")
10
- # device_name = torch.cuda.get_device_name(0)
11
- # memory_allocated = torch.cuda.memory_allocated(0) / (1024 ** 2)
12
- # memory_reserved = torch.cuda.memory_reserved(0) / (1024 ** 2)
13
- # memory_total = torch.cuda.get_device_properties(0).total_memory / (1024 ** 2)
14
- # capability = torch.cuda.get_device_capability(0)
15
- # current_device = torch.cuda.current_device()
16
-
17
- # logging.debug(f"GPU name : {device_name}")
18
- # logging.debug(f"Current device ID : {current_device}")
19
- # logging.debug(f"CUDA capability : {capability}")
20
- # logging.debug(f"Memory allocated : {memory_allocated:.2f} MB")
21
- # logging.debug(f"Memory reserved : {memory_reserved:.2f} MB")
22
- # logging.debug(f"Total memory : {memory_total:.2f} MB")
23
-
24
- # else:
25
- # logging.debug("No GPU detected, using CPU")
26
-
27
- # def get_current_device():
28
- # if torch.cuda.is_available():
29
- # device = torch.device("cuda")
30
- # device_name = torch.cuda.get_device_name(0)
31
- # return device, device_name
32
- # else:
33
- # return torch.device("cpu"), "CPU (no GPU detected)"
34
-
35
-
36
-
37
- import torch
38
 
39
  def debug_current_device():
40
  """Safely logs GPU or CPU information without crashing on stateless GPU."""
@@ -80,4 +50,40 @@ def get_current_device():
80
  device_name = "CPU (stateless GPU mode)"
81
  # else:
82
  # raise
83
- return device, device_name
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import torch
2
  from app.logger_config import logger as logging
3
+ import hmac
4
+ import hashlib
5
+ import base64
6
+ import os
7
+ import time
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
  def debug_current_device():
10
  """Safely logs GPU or CPU information without crashing on stateless GPU."""
 
50
  device_name = "CPU (stateless GPU mode)"
51
  # else:
52
  # raise
53
+ return device, device_name
54
+
55
+
56
+
57
+ def generate_coturn_config():
58
+ """
59
+ Génère une configuration Coturn complète avec authentification dynamique (use-auth-secret).
60
+ Returns:
61
+ dict: Objet coturn_config prêt à être utilisé côté client WebRTC.
62
+ """
63
+
64
+ secret_key = os.getenv("TURN_SECRET_KEY", "your_secret_key")
65
+ ttl = int(os.getenv("TURN_TTL", 3600))
66
+ turn_url = os.getenv("TURN_URL", "turn:*******")
67
+ turn_s_url = os.getenv("TURN_S_URL", "turns:*****")
68
+ user = os.getenv("TURN_USER", "client")
69
+
70
+ timestamp = int(time.time()) + ttl
71
+ username = f"{timestamp}:{user}"
72
+ password = base64.b64encode(
73
+ hmac.new(secret_key.encode(), username.encode(), hashlib.sha1).digest()
74
+ ).decode()
75
+
76
+ coturn_config = {
77
+ "iceServers": [
78
+ {
79
+ "urls": [
80
+ f"{turn_url}",
81
+ f"{turn_s_url}",
82
+ ],
83
+ "username": username,
84
+ "credential": password,
85
+ }
86
+ ]
87
+ }
88
+ print(coturn_config)
89
+ return coturn_config
gpu_compute.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app.logger_config import logger as logging
2
+ from app.utils import (
3
+ debug_current_device,
4
+ get_current_device
5
+ )
6
+ import os
7
+ import gradio as gr
8
+ import spaces
9
+ import torch
10
+
11
+
12
+
13
+
14
+ @spaces.GPU
15
+ def gpu_compute(name):
16
+ logging.debug("=== Start of gpu_compute() ===")
17
+ debug_current_device()
18
+ tensor,device_name = compute(name)
19
+ logging.debug("=== End of gpu_compute() ===")
20
+ return f"Tensor: {tensor.cpu().numpy()} | Device: {device_name}"
21
+
22
+ def cpu_compute(name):
23
+ logging.debug("=== Start of cpu_compute() ===")
24
+ debug_current_device()
25
+
26
+ tensor,device_name = compute(name)
27
+
28
+ logging.debug("=== End of cpu_compute() ===")
29
+ return f"Tensor: {tensor.cpu().numpy()} | Device: {device_name}"
30
+
31
+ def compute(name) :
32
+ # Get device info
33
+ device, device_name = get_current_device()
34
+ # Create a tensor
35
+ tensor = torch.tensor([len(name)], dtype=torch.float32, device=device)
36
+ logging.debug(f"Tensor created: {tensor}")
37
+ # Optional: free GPU memory
38
+ if torch.cuda.is_available():
39
+ torch.cuda.empty_cache()
40
+ logging.debug("GPU cache cleared")
41
+ return tensor, device_name
42
+
43
+
44
+ block = gr.Blocks()
45
+
46
+ with block as demo:
47
+ with gr.Row():
48
+ input_text = gr.Text()
49
+ output_text = gr.Text()
50
+ with gr.Row():
51
+ gpu_button = gr.Button("GPU compute")
52
+ cpu_button = gr.Button("CPU compute")
53
+
54
+ gpu_button.click(fn=gpu_compute, inputs=[input_text],outputs=[output_text])
55
+ cpu_button.click(fn=cpu_compute, inputs=[input_text],outputs=[output_text])
56
+
57
+ with gr.Blocks() as demo:
58
+ block.render()
59
+
60
+ if __name__ == "__main__":
61
+ demo.queue(max_size=10, api_open=False).launch(show_api=False)
requirements.txt CHANGED
@@ -1,4 +1,5 @@
1
  gradio
2
  spaces
3
  torch
4
- python-dotenv
 
 
1
  gradio
2
  spaces
3
  torch
4
+ python-dotenv
5
+ fastrtc==0.0.33