Spaces:
Running
on
Zero
Running
on
Zero
| from app.logger_config import logger as logging | |
| import numpy as np | |
| import gradio as gr | |
| import asyncio | |
| from fastrtc.webrtc import WebRTC | |
| from pydub import AudioSegment | |
| import time | |
| import os | |
| from gradio.utils import get_space | |
| from app.logger_config import logger as logging | |
| from app.utils import ( | |
| generate_coturn_config | |
| ) | |
| EXAMPLE_FILES = ["data/bonjour.wav", "data/bonjour2.wav"] | |
| DEFAULT_FILE = EXAMPLE_FILES[0] | |
| # Utilisé pour signaler l'arrêt du streaming à l'intérieur du générateur | |
| stop_stream_state = gr.State(value=False) | |
| def read_and_stream_audio(filepath_to_stream: str): | |
| """ | |
| Un générateur synchrone qui lit un fichier audio (via filepath_to_stream) | |
| et le streame chunk par chunk d'1 seconde. | |
| """ | |
| if not filepath_to_stream or not os.path.exists(filepath_to_stream): | |
| logging.error(f"Fichier audio non trouvé ou non spécifié : {filepath_to_stream}") | |
| # Tenter d'utiliser le fichier par défaut en cas de problème | |
| if os.path.exists(DEFAULT_FILE): | |
| logging.warning(f"Utilisation du fichier par défaut : {DEFAULT_FILE}") | |
| filepath_to_stream = DEFAULT_FILE | |
| else: | |
| logging.error("Fichier par défaut non trouvé. Arrêt du stream.") | |
| return | |
| logging.info(f"Préparation du segment audio depuis : {filepath_to_stream}") | |
| # Réinitialiser le signal d'arrêt à chaque lancement | |
| stop_stream_state.value = False | |
| try: | |
| segment = AudioSegment.from_file(filepath_to_stream) | |
| chunk_duree_ms = 1000 | |
| logging.info(f"Début du streaming en chunks de {chunk_duree_ms}ms...") | |
| for i, chunk in enumerate(segment[::chunk_duree_ms]): | |
| iter_start_time = time.perf_counter() | |
| logging.info(f"Envoi du chunk {i+1}...") | |
| if stop_stream_state.value: | |
| logging.info("Signal d'arrêt reçu, arrêt de la boucle.") | |
| break | |
| output_chunk = ( | |
| chunk.frame_rate, | |
| np.array(chunk.get_array_of_samples()).reshape(1, -1), | |
| ) | |
| yield output_chunk | |
| iter_end_time = time.perf_counter() | |
| processing_duration_ms = (iter_end_time - iter_start_time) * 1000 | |
| sleep_duration = (chunk_duree_ms / 1000.0) - (processing_duration_ms / 1000.0) - 0.1 | |
| if sleep_duration < 0: | |
| sleep_duration = 0.01 # Éviter un temps de sommeil négatif | |
| logging.debug(f"Temps de traitement: {processing_duration_ms:.2f}ms, Sommeil: {sleep_duration:.2f}s") | |
| elapsed = 0.0 | |
| interval = 0.05 | |
| while elapsed < sleep_duration: | |
| if stop_stream_state.value: | |
| logging.info("Signal d'arrêt reçu pendant l'attente.") | |
| break | |
| wait_chunk = min(interval, sleep_duration - elapsed) | |
| time.sleep(wait_chunk) | |
| elapsed += wait_chunk | |
| if stop_stream_state.value: | |
| break | |
| logging.info("Streaming terminé.") | |
| except asyncio.CancelledError: | |
| logging.info("Stream arrêté par l'utilisateur (CancelledError).") | |
| raise | |
| except FileNotFoundError: | |
| logging.error(f"Erreur critique : Fichier non trouvé : {filepath_to_stream}") | |
| except Exception as e: | |
| logging.error(f"Erreur pendant le stream: {e}", exc_info=True) | |
| raise | |
| finally: | |
| stop_stream_state.value = False | |
| logging.info("Signal d'arrêt nettoyé.") | |
| def stop_streaming(): | |
| """Active le signal d'arrêt pour le générateur.""" | |
| logging.info("Bouton Stop cliqué: envoi du signal d'arrêt.") | |
| stop_stream_state.value = True | |
| return None | |
| # --- Interface Gradio --- | |
| with gr.Blocks(theme=gr.themes.Soft()) as demo: | |
| gr.Markdown( | |
| "## Application 'Streamer' WebRTC (Serveur -> Client)\n" | |
| "Utilisez l'exemple fourni, uploadez un fichier ou enregistrez depuis votre micro, " | |
| "puis cliquez sur 'Start' pour écouter le stream." | |
| ) | |
| # 1. État pour stocker le chemin du fichier à lire | |
| active_filepath = gr.State(value=DEFAULT_FILE) | |
| with gr.Row(): | |
| with gr.Column(): | |
| main_audio = gr.Audio( | |
| label="Source Audio", | |
| sources=["upload", "microphone"], # Combine les deux sources | |
| type="filepath", | |
| value=DEFAULT_FILE, # Défaut au premier exemple | |
| ) | |
| with gr.Column(): | |
| webrtc_stream = WebRTC( | |
| label="Stream Audio", | |
| mode="receive", | |
| modality="audio", | |
| rtc_configuration=generate_coturn_config(), | |
| visible=True, # Caché par défaut | |
| height = 200, | |
| ) | |
| # 4. Boutons de contrôle | |
| with gr.Row(): | |
| with gr.Column(): | |
| start_button = gr.Button("Start Streaming", variant="primary") | |
| stop_button = gr.Button("Stop Streaming", variant="stop", interactive=False) | |
| with gr.Column(): | |
| gr.Text() | |
| def set_new_file(filepath): | |
| """Met à jour l'état avec le nouveau chemin, ou revient au défaut si None.""" | |
| if filepath is None: | |
| logging.info("Audio effacé, retour au fichier d'exemple par défaut.") | |
| new_path = DEFAULT_FILE | |
| else: | |
| logging.info(f"Nouvelle source audio sélectionnée : {filepath}") | |
| new_path = filepath | |
| # Retourne la valeur à mettre dans le gr.State | |
| return new_path | |
| # Mettre à jour le chemin si l'utilisateur upload, efface, ou change le fichier | |
| main_audio.change( | |
| fn=set_new_file, | |
| inputs=[main_audio], | |
| outputs=[active_filepath] | |
| ) | |
| # Mettre à jour le chemin si l'utilisateur termine un enregistrement | |
| main_audio.stop_recording( | |
| fn=set_new_file, | |
| inputs=[main_audio], | |
| outputs=[active_filepath] | |
| ) | |
| # Fonctions pour mettre à jour l'état de l'interface | |
| def start_streaming_ui(): | |
| logging.info("UI : Démarrage du streaming. Désactivation des contrôles.") | |
| return { | |
| start_button: gr.Button(interactive=False), | |
| stop_button: gr.Button(interactive=True), | |
| main_audio: gr.Audio(visible=False), | |
| } | |
| def stop_streaming_ui(): | |
| logging.info("UI : Arrêt du streaming. Réactivation des contrôles.") | |
| return { | |
| start_button: gr.Button(interactive=True), | |
| stop_button: gr.Button(interactive=False), | |
| main_audio: gr.Audio( | |
| label="Source Audio", | |
| sources=["upload", "microphone"], # Combine les deux sources | |
| type="filepath", | |
| value=active_filepath.value, | |
| visible=True | |
| ), | |
| } | |
| ui_components = [ | |
| start_button, stop_button, | |
| main_audio, | |
| ] | |
| stream_event = webrtc_stream.stream( | |
| fn=read_and_stream_audio, | |
| inputs=[active_filepath], | |
| outputs=[webrtc_stream], | |
| trigger=start_button.click, | |
| concurrency_id="audio_stream", # ID de concurrence | |
| concurrency_limit=10 | |
| ) | |
| # Mettre à jour l'interface au clic sur START | |
| start_button.click( | |
| fn=start_streaming_ui, | |
| outputs=ui_components | |
| ) | |
| # Correction : S'assurer que le stream est bien annulé | |
| stop_button.click( | |
| fn=stop_streaming, | |
| outputs=[webrtc_stream], | |
| ).then( | |
| fn=stop_streaming_ui, # ENSUITE, mettre à jour l'interface | |
| inputs=None, | |
| outputs=ui_components | |
| ) | |
| if __name__ == "__main__": | |
| demo.queue(max_size=10, api_open=False).launch(show_api=False, debug=True) | |