Archime commited on
Commit
1dd92bc
·
1 Parent(s): d1f7785

clean new app

Browse files
Files changed (1) hide show
  1. new_app.py +142 -173
new_app.py CHANGED
@@ -6,12 +6,12 @@ from fastrtc.webrtc import WebRTC
6
  from fastrtc.utils import AdditionalOutputs
7
  from pydub import AudioSegment
8
  import time
9
- import os
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
  from app.new_session_utils import (
17
  on_load,
@@ -19,137 +19,161 @@ from app.new_session_utils import (
19
  get_active_sessions,
20
  reset_all_active_sessions,
21
  )
 
 
 
 
22
  reset_all_active_sessions()
23
  EXAMPLE_FILES = ["data/bonjour.wav", "data/bonjour2.wav"]
24
- DEFAULT_FILE = EXAMPLE_FILES[0]
 
25
 
 
 
 
26
  def _is_stop_requested(stop_streaming_flags: dict) -> bool:
 
27
  if not isinstance(stop_streaming_flags, dict):
28
  return False
29
  return bool(stop_streaming_flags.get("stop", False))
30
 
31
- def read_and_stream_audio(filepath_to_stream: str, session_id: str, stop_streaming_flags: dict):
 
32
  """
33
- Un générateur synchrone qui lit un fichier audio (via filepath_to_stream)
34
- et le streame chunk par chunk d'1 seconde.
 
 
35
  """
 
 
 
 
36
 
37
- if not session_id:
38
- logging.warning( "Aucun session_id fourni, arrêt du stream par sécurité.")
39
- return
40
 
41
  if isinstance(stop_streaming_flags, dict):
42
  stop_streaming_flags["stop"] = False
43
- else:
44
- logging.warning(f" [{session_id}] Stop stop_streaming_flags non initialisés, le stream continuera sans contrôle d'arrêt.")
45
 
46
- if not filepath_to_stream or not os.path.exists(filepath_to_stream):
47
- logging.error(f"[{session_id}] Fichier audio non trouvé ou non spécifié : {filepath_to_stream}")
48
- # Tenter d'utiliser le fichier par défaut en cas de problème
49
- if os.path.exists(DEFAULT_FILE):
50
- logging.warning(f"[{session_id}] Utilisation du fichier par défaut : {DEFAULT_FILE}")
51
- filepath_to_stream = DEFAULT_FILE
52
- else:
53
- logging.error(f"[{session_id}] Fichier par défaut non trouvé. Arrêt du stream.")
54
- return
55
 
56
- logging.info(f"[{session_id}] Préparation du segment audio depuis : {filepath_to_stream}")
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
  try:
59
  segment = AudioSegment.from_file(filepath_to_stream)
60
- chunk_duree_ms = 1000
61
- total_chunks = len(segment) // chunk_duree_ms + 1
62
- logging.info(f"[{session_id}] Début du streaming en chunks de {chunk_duree_ms}ms...")
63
-
64
- for i, chunk in enumerate(segment[::chunk_duree_ms]):
65
- iter_start_time = time.perf_counter()
66
- logging.info(f"Envoi du chunk {i+1}...")
67
 
 
68
  if _is_stop_requested(stop_streaming_flags):
69
- logging.info(f"[{session_id}]Signal d'arrêt reçu, arrêt de la boucle.")
70
  break
71
 
72
- output_chunk = (
73
- chunk.frame_rate,
74
- np.array(chunk.get_array_of_samples()).reshape(1, -1),
75
- )
76
- # Calcul du pourcentage de progression
77
  progress = round(((i + 1) / total_chunks) * 100, 2)
78
- logging.debug(f"[{session_id}] Progression: {progress}%")
79
-
80
- # Envoi du chunk et de la progression numérique
81
- yield (output_chunk, AdditionalOutputs(progress))
82
-
83
- iter_end_time = time.perf_counter()
84
- processing_duration_ms = (iter_end_time - iter_start_time) * 1000
85
-
86
- sleep_duration = (chunk_duree_ms / 1000.0) - (processing_duration_ms / 1000.0) - 0.1
87
- if sleep_duration < 0:
88
- sleep_duration = 0.01 # Éviter un temps de sommeil négatif
89
-
90
- logging.debug(f"[{session_id}]Temps de traitement: {processing_duration_ms:.2f}ms, Sommeil: {sleep_duration:.2f}s")
91
-
92
- elapsed = 0.0
93
- interval = 0.05
94
- while elapsed < sleep_duration:
95
- if _is_stop_requested(stop_streaming_flags):
96
- logging.info(f"[{session_id}]Signal d'arrêt reçu pendant l'attente.")
97
- break
98
- wait_chunk = min(interval, sleep_duration - elapsed)
99
- time.sleep(wait_chunk)
100
- elapsed += wait_chunk
101
- if _is_stop_requested(stop_streaming_flags):
102
- break
103
 
104
- logging.info(f"[{session_id}]Streaming terminé.")
 
 
 
 
 
 
105
 
106
  except asyncio.CancelledError:
107
- logging.info(f"[{session_id}]Stream arrêté par l'utilisateur (CancelledError).")
108
- raise
109
- except FileNotFoundError:
110
- logging.error(f"[{session_id}] Erreur critique : Fichier non trouvé : {filepath_to_stream}")
111
  except Exception as e:
112
- logging.error(f"[{session_id}] Erreur pendant le stream: {e}", exc_info=True)
113
- raise
114
  finally:
115
  if isinstance(stop_streaming_flags, dict):
116
  stop_streaming_flags["stop"] = False
117
- logging.info(f"[{session_id}]Signal d'arrêt nettoyé.")
118
- yield (None, AdditionalOutputs({"progress": 100}))
119
-
120
 
121
 
122
  def stop_streaming(session_id: str, stop_streaming_flags: dict):
123
- """Active le signal d'arrêt pour le générateur."""
124
- logging.info("Bouton Stop cliqué: envoi du signal d'arrêt.")
125
  if not isinstance(stop_streaming_flags, dict):
126
  stop_streaming_flags = {"stop": True}
127
  else:
128
  stop_streaming_flags["stop"] = True
129
  return stop_streaming_flags
130
 
131
- def handle_additional_outputs(status_slider,progress_value):
132
- """Met à jour le slider selon la valeur reçue et gère sa visibilité."""
133
- logging.debug(f"📡 Additional output received: {progress_value}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
 
135
  try:
136
  progress = float(progress_value)
137
  except (ValueError, TypeError):
138
  progress = 0
139
- # status_slider = gr.update(interactive=True,visible=True, value=max(0, min(progress, 100)))
140
- # return status_slider
 
 
 
 
 
 
 
 
 
141
  if progress >= 100:
142
- return gr.update(visible=False, value=100)
143
- elif progress <= 0:
144
- return gr.update(visible=False, value=0)
145
- else:
146
- return gr.update(visible=True, value=progress)
 
 
 
 
 
 
 
 
 
147
 
148
- # --- Interface Gradio ---
149
 
 
 
 
150
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
151
 
152
- session_hash = gr.State()
153
  session_hash_box = gr.Textbox(label="Session ID", interactive=False)
154
  demo.load(fn=on_load, inputs=None, outputs=[session_hash, session_hash_box])
155
  demo.unload(on_unload)
@@ -157,135 +181,80 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
157
  stop_streaming_flags = gr.State(value={"stop": False})
158
 
159
  gr.Markdown(
160
- "## Application 'Streamer' WebRTC (Serveur -> Client)\n"
161
- "Utilisez l'exemple fourni, uploadez un fichier ou enregistrez depuis votre micro, "
162
- "puis cliquez sur 'Start' pour écouter le stream."
163
  )
164
 
165
- # 1. État pour stocker le chemin du fichier à lire
166
  active_filepath = gr.State(value=DEFAULT_FILE)
167
 
168
  with gr.Row(equal_height=True):
169
  with gr.Column(elem_id="column_source", scale=1):
170
  with gr.Group(elem_id="centered_content"):
171
  main_audio = gr.Audio(
172
- label="Source Audio",
173
- sources=["upload", "microphone"], # Combine les deux sources
174
  type="filepath",
175
- value=DEFAULT_FILE, # Défaut au premier exemple
 
 
 
176
  )
177
- status_slider = gr.Slider(0, 100, value=0, label="Progression du streaming", interactive=False, visible=False )
178
 
179
  with gr.Column():
180
  webrtc_stream = WebRTC(
181
- label="Stream Audio",
182
  mode="receive",
183
  modality="audio",
184
  rtc_configuration=generate_coturn_config(),
185
- visible=True, # Caché par défaut
186
- height = 200,
187
  )
188
- # 4. Boutons de contrôle
189
  with gr.Row():
190
  with gr.Column():
191
  start_button = gr.Button("Start Streaming", variant="primary")
192
  stop_button = gr.Button("Stop Streaming", variant="stop", interactive=False)
193
- with gr.Column():
194
- gr.Text()
195
 
196
  def set_new_file(filepath):
197
- """Met à jour l'état avec le nouveau chemin, ou revient au défaut si None."""
198
  if filepath is None:
199
- logging.info("Audio effacé, retour au fichier d'exemple par défaut.")
200
  new_path = DEFAULT_FILE
201
  else:
202
- logging.info(f"Nouvelle source audio sélectionnée : {filepath}")
203
  new_path = filepath
204
- # Retourne la valeur à mettre dans le gr.State
205
  return new_path
206
 
207
- # Mettre à jour le chemin si l'utilisateur upload, efface, ou change le fichier
208
- main_audio.change(
209
- fn=set_new_file,
210
- inputs=[main_audio],
211
- outputs=[active_filepath]
212
- )
213
-
214
- # Mettre à jour le chemin si l'utilisateur termine un enregistrement
215
- main_audio.stop_recording(
216
- fn=set_new_file,
217
- inputs=[main_audio],
218
- outputs=[active_filepath]
219
- )
220
-
221
 
222
- # Fonctions pour mettre à jour l'état de l'interface
223
- def start_streaming_ui(session_id: str, flags: dict):
224
- logging.info("UI : Démarrage du streaming. Désactivation des contrôles.")
225
- if not isinstance(flags, dict):
226
- flags = {"stop": False}
227
- else:
228
- flags["stop"] = False
229
- return (
230
- gr.Button(interactive=False),
231
- gr.Button(interactive=True),
232
- gr.Audio(visible=False),
233
- flags,
234
- )
235
-
236
- def stop_streaming_ui(flags: dict):
237
- logging.info("UI : Arrêt du streaming. Réactivation des contrôles.")
238
- return (
239
- gr.Button(interactive=True),
240
- gr.Button(interactive=False),
241
- gr.Audio(
242
- label="Source Audio",
243
- sources=["upload", "microphone"],
244
- type="filepath",
245
- value=active_filepath.value,
246
- visible=True,
247
- ),
248
- )
249
-
250
-
251
- ui_components = [
252
- start_button, stop_button,
253
- main_audio,
254
- ]
255
 
256
  stream_event = webrtc_stream.stream(
257
  fn=read_and_stream_audio,
258
- inputs=[active_filepath, session_hash, stop_streaming_flags],
259
  outputs=[webrtc_stream],
260
  trigger=start_button.click,
261
- concurrency_id="audio_stream",
262
- concurrency_limit=10
263
  )
 
264
  webrtc_stream.on_additional_outputs(
265
  fn=handle_additional_outputs,
266
- inputs=[status_slider],
267
- outputs=[status_slider],
268
  concurrency_id="additional_outputs_audio_stream",
269
- concurrency_limit=10
270
- )
271
- # Mettre à jour l'interface au clic sur START
272
- start_button.click(
273
- fn=start_streaming_ui,
274
- inputs=[session_hash, stop_streaming_flags],
275
- outputs=ui_components + [stop_streaming_flags]
276
  )
277
 
278
- # Correction : S'assurer que le stream est bien annulé
 
279
  stop_button.click(
280
- fn=stop_streaming,
281
  inputs=[session_hash, stop_streaming_flags],
282
- outputs=[stop_streaming_flags],
283
- ).then(
284
- fn=stop_streaming_ui, # ENSUITE, mettre à jour l'interface
285
- inputs=[stop_streaming_flags],
286
- outputs=ui_components
287
  )
288
- # --- Active sessions ---
289
  with gr.Accordion("📊 Active Sessions", open=False):
290
  sessions_table = gr.DataFrame(
291
  headers=["session_hash", "file", "start_time", "status"],
@@ -297,9 +266,8 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
297
  gr.Timer(3.0).tick(fn=get_active_sessions, outputs=sessions_table)
298
 
299
 
300
-
301
  # --------------------------------------------------------
302
- # CSS
303
  # --------------------------------------------------------
304
  custom_css = """
305
  #column_source {
@@ -317,9 +285,10 @@ custom_css = """
317
  }
318
  """
319
  demo.css = custom_css
 
 
320
  # --------------------------------------------------------
321
- # MAIN
322
  # --------------------------------------------------------
323
-
324
  if __name__ == "__main__":
325
  demo.queue(max_size=10, api_open=False).launch(show_api=False, debug=True)
 
6
  from fastrtc.utils import AdditionalOutputs
7
  from pydub import AudioSegment
8
  import time
9
+ import os
10
  from gradio.utils import get_space
11
 
 
12
  from app.utils import (
13
+ generate_coturn_config,
14
+ raise_function
15
  )
16
  from app.new_session_utils import (
17
  on_load,
 
19
  get_active_sessions,
20
  reset_all_active_sessions,
21
  )
22
+
23
+ # --------------------------------------------------------
24
+ # Initialization
25
+ # --------------------------------------------------------
26
  reset_all_active_sessions()
27
  EXAMPLE_FILES = ["data/bonjour.wav", "data/bonjour2.wav"]
28
+ DEFAULT_FILE = EXAMPLE_FILES[0]
29
+
30
 
31
+ # --------------------------------------------------------
32
+ # Utility functions
33
+ # --------------------------------------------------------
34
  def _is_stop_requested(stop_streaming_flags: dict) -> bool:
35
+ """Check if the stop signal was requested."""
36
  if not isinstance(stop_streaming_flags, dict):
37
  return False
38
  return bool(stop_streaming_flags.get("stop", False))
39
 
40
+
41
+ def handle_stream_error(session_id: str, error: Exception | str, stop_streaming_flags: dict | None = None):
42
  """
43
+ Handle streaming errors:
44
+ - Log the error
45
+ - Send structured info to client
46
+ - Reset stop flag
47
  """
48
+ if isinstance(error, Exception):
49
+ msg = f"{type(error).__name__}: {str(error)}"
50
+ else:
51
+ msg = str(error)
52
 
53
+ logging.error(f"[{session_id}] Streaming error: {msg}", exc_info=isinstance(error, Exception))
 
 
54
 
55
  if isinstance(stop_streaming_flags, dict):
56
  stop_streaming_flags["stop"] = False
 
 
57
 
58
+ yield (None, AdditionalOutputs({"error": True, "message": msg}))
59
+ yield (None, AdditionalOutputs("STREAM_DONE"))
 
 
 
 
 
 
 
60
 
61
+
62
+ def read_and_stream_audio(filepath_to_stream: str, session_id: str, stop_streaming_flags: dict):
63
+ """
64
+ Read an audio file and stream it chunk by chunk (1s per chunk).
65
+ Handles errors safely and reports structured messages to the client.
66
+ """
67
+ if not session_id:
68
+ yield from handle_stream_error("unknown", "No session_id provided.", stop_streaming_flags)
69
+ return
70
+
71
+ if not filepath_to_stream or not os.path.exists(filepath_to_stream):
72
+ yield from handle_stream_error(session_id, f"Audio file not found: {filepath_to_stream}", stop_streaming_flags)
73
+ return
74
 
75
  try:
76
  segment = AudioSegment.from_file(filepath_to_stream)
77
+ chunk_duration_ms = 1000
78
+ total_chunks = len(segment) // chunk_duration_ms + 1
79
+ logging.info(f"[{session_id}] Starting audio streaming ({total_chunks} chunks).")
 
 
 
 
80
 
81
+ for i, chunk in enumerate(segment[::chunk_duration_ms]):
82
  if _is_stop_requested(stop_streaming_flags):
83
+ logging.info(f"[{session_id}] Stop signal received. Terminating stream.")
84
  break
85
 
86
+ frame_rate = chunk.frame_rate
87
+ samples = np.array(chunk.get_array_of_samples()).reshape(1, -1)
 
 
 
88
  progress = round(((i + 1) / total_chunks) * 100, 2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
+ yield ((frame_rate, samples), AdditionalOutputs(progress))
91
+ logging.debug(f"[{session_id}] Sent chunk {i+1}/{total_chunks} ({progress}%).")
92
+
93
+ time.sleep(0.9)
94
+ raise_function() # Optional injected test exception
95
+
96
+ logging.info(f"[{session_id}] Audio streaming completed successfully.")
97
 
98
  except asyncio.CancelledError:
99
+ yield from handle_stream_error(session_id, "Streaming cancelled by user.", stop_streaming_flags)
100
+ except FileNotFoundError as e:
101
+ yield from handle_stream_error(session_id, e, stop_streaming_flags)
 
102
  except Exception as e:
103
+ yield from handle_stream_error(session_id, e, stop_streaming_flags)
 
104
  finally:
105
  if isinstance(stop_streaming_flags, dict):
106
  stop_streaming_flags["stop"] = False
107
+ logging.info(f"[{session_id}] Stop flag reset.")
108
+ yield (None, AdditionalOutputs("STREAM_DONE"))
 
109
 
110
 
111
  def stop_streaming(session_id: str, stop_streaming_flags: dict):
112
+ """Trigger the stop flag for active streaming."""
113
+ logging.info(f"[{session_id}] Stop button clicked sending stop signal.")
114
  if not isinstance(stop_streaming_flags, dict):
115
  stop_streaming_flags = {"stop": True}
116
  else:
117
  stop_streaming_flags["stop"] = True
118
  return stop_streaming_flags
119
 
120
+
121
+ def handle_additional_outputs(start_button, stop_button, main_audio, status_slider, progress_value):
122
+ """
123
+ Update UI elements based on streaming progress or errors.
124
+ Controls button states, audio visibility, and progress slider.
125
+ """
126
+ logging.debug(f"Additional output received: {progress_value}")
127
+
128
+ # Handle structured error message
129
+ if isinstance(progress_value, dict) and progress_value.get("error"):
130
+ msg = progress_value.get("message", "Unknown error.")
131
+ logging.error(f"[stream_ui] Client-side error: {msg}")
132
+ return (
133
+ gr.update(interactive=True), # start_button enabled
134
+ gr.update(interactive=False), # stop_button disabled
135
+ gr.update(visible=True), # audio re-shown
136
+ gr.update(visible=False, value=0), # slider hidden
137
+ )
138
 
139
  try:
140
  progress = float(progress_value)
141
  except (ValueError, TypeError):
142
  progress = 0
143
+
144
+ # --- Stream not started ---
145
+ if progress <= 0:
146
+ return (
147
+ gr.update(interactive=True), # start_button enabled
148
+ gr.update(interactive=False), # stop_button disabled
149
+ gr.update(visible=True), # audio visible
150
+ gr.update(visible=False, value=0), # slider hidden
151
+ )
152
+
153
+ # --- Stream finished ---
154
  if progress >= 100:
155
+ return (
156
+ gr.update(interactive=True), # start_button re-enabled
157
+ gr.update(interactive=False), # stop_button disabled
158
+ gr.update(visible=True), # audio visible
159
+ gr.update(visible=False, value=100), # slider hidden
160
+ )
161
+
162
+ # --- Stream in progress ---
163
+ return (
164
+ gr.update(interactive=False), # start_button disabled
165
+ gr.update(interactive=True), # stop_button enabled
166
+ gr.update(visible=False), # hide audio
167
+ gr.update(visible=True, value=progress), # show progress
168
+ )
169
 
 
170
 
171
+ # --------------------------------------------------------
172
+ # Gradio Interface
173
+ # --------------------------------------------------------
174
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
175
 
176
+ session_hash = gr.State()
177
  session_hash_box = gr.Textbox(label="Session ID", interactive=False)
178
  demo.load(fn=on_load, inputs=None, outputs=[session_hash, session_hash_box])
179
  demo.unload(on_unload)
 
181
  stop_streaming_flags = gr.State(value={"stop": False})
182
 
183
  gr.Markdown(
184
+ "## WebRTC Audio Streamer (Server Client)\n"
185
+ "Upload or record an audio file, then click **Start** to listen to the streamed audio."
 
186
  )
187
 
 
188
  active_filepath = gr.State(value=DEFAULT_FILE)
189
 
190
  with gr.Row(equal_height=True):
191
  with gr.Column(elem_id="column_source", scale=1):
192
  with gr.Group(elem_id="centered_content"):
193
  main_audio = gr.Audio(
194
+ label="Audio File",
195
+ sources=["upload", "microphone"],
196
  type="filepath",
197
+ value=DEFAULT_FILE,
198
+ )
199
+ status_slider = gr.Slider(
200
+ 0, 100, value=0, label="Streaming Progress", interactive=False, visible=False
201
  )
 
202
 
203
  with gr.Column():
204
  webrtc_stream = WebRTC(
205
+ label="Live",
206
  mode="receive",
207
  modality="audio",
208
  rtc_configuration=generate_coturn_config(),
209
+ visible=True,
210
+ height=200,
211
  )
212
+
213
  with gr.Row():
214
  with gr.Column():
215
  start_button = gr.Button("Start Streaming", variant="primary")
216
  stop_button = gr.Button("Stop Streaming", variant="stop", interactive=False)
 
 
217
 
218
  def set_new_file(filepath):
219
+ """Update active audio path or reset to default if empty."""
220
  if filepath is None:
221
+ logging.info("[ui] Audio cleared reverting to default example file.")
222
  new_path = DEFAULT_FILE
223
  else:
224
+ logging.info(f"[ui] New audio source selected: {filepath}")
225
  new_path = filepath
 
226
  return new_path
227
 
228
+ main_audio.change(fn=set_new_file, inputs=[main_audio], outputs=[active_filepath])
229
+ main_audio.stop_recording(fn=set_new_file, inputs=[main_audio], outputs=[active_filepath])
 
 
 
 
 
 
 
 
 
 
 
 
230
 
231
+ ui_components = [start_button, stop_button, main_audio, status_slider]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
 
233
  stream_event = webrtc_stream.stream(
234
  fn=read_and_stream_audio,
235
+ inputs=[active_filepath, session_hash, stop_streaming_flags],
236
  outputs=[webrtc_stream],
237
  trigger=start_button.click,
238
+ concurrency_id="audio_stream",
239
+ concurrency_limit=10,
240
  )
241
+
242
  webrtc_stream.on_additional_outputs(
243
  fn=handle_additional_outputs,
244
+ inputs=ui_components,
245
+ outputs=ui_components,
246
  concurrency_id="additional_outputs_audio_stream",
247
+ concurrency_limit=10,
 
 
 
 
 
 
248
  )
249
 
250
+ start_button.click(fn=None, inputs=None, outputs=None)
251
+
252
  stop_button.click(
253
+ fn=stop_streaming,
254
  inputs=[session_hash, stop_streaming_flags],
255
+ outputs=[stop_streaming_flags],
 
 
 
 
256
  )
257
+
258
  with gr.Accordion("📊 Active Sessions", open=False):
259
  sessions_table = gr.DataFrame(
260
  headers=["session_hash", "file", "start_time", "status"],
 
266
  gr.Timer(3.0).tick(fn=get_active_sessions, outputs=sessions_table)
267
 
268
 
 
269
  # --------------------------------------------------------
270
+ # Custom CSS
271
  # --------------------------------------------------------
272
  custom_css = """
273
  #column_source {
 
285
  }
286
  """
287
  demo.css = custom_css
288
+
289
+
290
  # --------------------------------------------------------
291
+ # Main
292
  # --------------------------------------------------------
 
293
  if __name__ == "__main__":
294
  demo.queue(max_size=10, api_open=False).launch(show_api=False, debug=True)