Imaginez un agent IA capable de rejoindre vos appels Google Meet, écouter les participants et répondre avec une voix humaine naturelle — le tout de manière autonome sur un serveur cloud. C'est exactement ce que nous allons construire dans ce tutoriel.
À la fin, vous aurez :
Un bot basé sur Selenium qui rejoint Google Meet en tant que participant invité
Un routage audio virtuel PulseAudio qui achemine l'audio de Meet vers une IA et inversement
Un pont ElevenLabs Conversational AI qui écoute, réfléchit et parle
Un déploiement sur serveur cloud GPU sur Vast.ai avec un script de configuration en une commande
C'est la même configuration que nous avons testée en interne chez Noqta — et ça fonctionne.
Prérequis
Avant de commencer, assurez-vous d'avoir :
Un compte ElevenLabs avec accès à Conversational AI
Un compte Vast.ai (ou tout serveur Linux avec accès root)
Des connaissances de base en Python et en ligne de commande Linux
Un lien Google Meet pour tester
Vue d'ensemble de l'architecture
Le système comporte trois composants principaux qui fonctionnent ensemble :
Composant
Rôle
Outil
Meet Joiner
Ouvre Chrome, navigue vers Meet, rejoint en tant qu'invité
Selenium + Xvfb
Routeur audio
Crée des périphériques audio virtuels, route l'audio entre Meet et l'IA
PulseAudio
Pont IA
Capture l'audio de Meet, l'envoie à ElevenLabs, joue la réponse en retour
SDK ElevenLabs ConvAI
Les trois s'exécutent sur la même machine. L'idée clé est d'utiliser des null sinks et des sources virtuelles PulseAudio pour créer un pont audio bidirectionnel entre Chrome et l'API ElevenLabs — aucun microphone physique ou haut-parleur nécessaire.
Étape 1 : Provisionner un serveur cloud
Vous avez besoin d'un serveur Linux avec un environnement de bureau (pour Chrome). Nous avons utilisé Vast.ai car c'est bon marché, rapide à démarrer et donne un accès root.
Étape 2 : Configurer l'affichage virtuel et l'audio
Comme il n'y a pas de moniteur physique ni de carte son sur un serveur cloud, nous utilisons Xvfb (framebuffer virtuel) pour l'affichage et PulseAudio pour les périphériques audio virtuels.
C'est la partie cruciale. Nous avons besoin de deux périphériques audio virtuels :
# 1. meet_capture — La sortie audio de Chrome va ici# Nous lirons depuis meet_capture.monitor pour entendre ce que disent les participantspactl load-module module-null-sink \ sink_name=meet_capture \ sink_properties=device.description=MeetCapture# 2. atlas_out — Les réponses de l'IA sont jouées ici# atlas_mic lit depuis atlas_out.monitor et agit comme entrée micro de Chromepactl load-module module-null-sink \ sink_name=atlas_out \ sink_properties=device.description=AtlasOutput# 3. atlas_mic — Source microphone virtuelle alimentée par atlas_outpactl load-module module-virtual-source \ source_name=atlas_mic \ master=atlas_out.monitor \ source_properties=device.description=AtlasMic# Définir les valeurs par défaut pour que Chrome les utilisepactl set-default-source atlas_mic # Micro de Chrome = sortie IApactl set-default-sink meet_capture # Haut-parleurs de Chrome = notre point de capture
Vérifier la configuration
# Vérifier les sinks (devrait afficher meet_capture et atlas_out)pactl list short sinks# Vérifier les sources (devrait afficher atlas_mic et meet_capture.monitor)pactl list short sources
Le bot utilise Selenium pour ouvrir Chrome, naviguer vers Google Meet, entrer un nom et cliquer sur « Demander à rejoindre » :
#!/usr/bin/env python3"""meet_bot.py — Rejoint un appel Google Meet en tant qu'invité via Selenium."""import osimport sysimport timefrom selenium import webdriverfrom selenium.webdriver.chrome.service import Servicefrom selenium.webdriver.chrome.options import Optionsfrom selenium.webdriver.common.by import Byfrom selenium.common.exceptions import NoSuchElementExceptionMEET_URL = sys.argv[1] if len(sys.argv) > 1 else "https://meet.google.com/abc-defg-hij"DISPLAY = os.environ.get("DISPLAY", ":99")BOT_NAME = "Atlas"def make_driver(): opts = Options() opts.binary_location = "/usr/bin/google-chrome" opts.add_argument("--no-sandbox") opts.add_argument("--disable-dev-shm-usage") opts.add_argument("--disable-gpu") opts.add_argument("--use-fake-ui-for-media-stream") # auto-allow mic/cam # Important : NE PAS utiliser --use-fake-device-for-media-stream # Nous voulons que Chrome utilise les vrais périphériques PulseAudio opts.add_argument("--disable-blink-features=AutomationControlled") opts.add_argument("--window-size=1280,720") opts.add_argument("--autoplay-policy=no-user-gesture-required") opts.add_experimental_option("excludeSwitches", ["enable-automation"]) svc = Service("/usr/local/bin/chromedriver") driver = webdriver.Chrome(service=svc, options=opts) driver.execute_script( "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})" ) return driverdef find_any(driver, xpaths, timeout=10): """Essaie plusieurs sélecteurs XPath, retourne la première correspondance visible.""" deadline = time.time() + timeout while time.time() < deadline: for xpath in xpaths: try: el = driver.find_element(By.XPATH, xpath) if el.is_displayed(): return el except NoSuchElementException: pass time.sleep(0.5) return Nonedef join_meet(driver, url): print(f"Ouverture de {url}") driver.get(url) time.sleep(4) # Étape 1 : Choisir le mode invité (sans compte Google) guest = find_any(driver, [ "//span[contains(text(),'Use without an account')]", "//button[contains(text(),'Use without')]", "//span[contains(text(),'Continue without')]", ], timeout=8) if guest: guest.click() print(" Mode invité sélectionné") time.sleep(3) # Étape 2 : Entrer le nom du bot name_el = find_any(driver, [ "//input[contains(@aria-label,'name') or contains(@aria-label,'Name')]", "//input[@placeholder='Your name']", "//input[@type='text']", ], timeout=8) if name_el: name_el.clear() name_el.send_keys(BOT_NAME) print(f" Nom saisi : {BOT_NAME}") time.sleep(1) # Étape 3 : Cliquer sur rejoindre join_btn = find_any(driver, [ "//span[contains(text(),'Ask to join')]", "//span[contains(text(),'Join now')]", "//button[contains(text(),'Ask to join')]", "//button[contains(text(),'Join now')]", ], timeout=15) if join_btn: join_btn.click() print(" Demande de participation envoyée — en attente d'admission...") else: print(" ERREUR : Bouton de participation non trouvé") driver.save_screenshot("/tmp/meet_debug.png") return False # Étape 4 : Attendre l'admission (jusqu'à 5 minutes) for i in range(60): mute_btns = driver.find_elements(By.XPATH, "//button[contains(@aria-label,'microphone') or contains(@aria-label,'mic')]" ) if mute_btns: print(f" DANS L'APPEL !") return True time.sleep(5) print(" Délai d'attente dépassé pour l'admission") return Falseif __name__ == "__main__": driver = make_driver() try: if join_meet(driver, MEET_URL): print("\nLe bot est dans l'appel. Appuyez sur Ctrl+C pour quitter.") while True: time.sleep(10) except KeyboardInterrupt: print("\nDéconnexion de l'appel...") finally: driver.quit()
Explication des flags Chrome importants
Flag
Pourquoi
--use-fake-ui-for-media-stream
Accepte automatiquement les popups de permission micro/caméra
--no-sandbox
Requis pour l'exécution en root sur les serveurs cloud
--disable-blink-features=AutomationControlled
Empêche Meet de détecter Selenium
Pas de --use-fake-device-for-media-stream
Garantit que Chrome utilise PulseAudio (vrais périphériques audio)
Important : Après le démarrage de Chrome, vous devrez peut-être déplacer manuellement son flux audio vers le sink meet_capture. Nous automatiserons cela dans le pont.
Étape 4 : Construire le pont ElevenLabs ConvAI
C'est le composant principal. Il capture l'audio depuis meet_capture.monitor (ce que disent les participants), l'envoie à ElevenLabs Conversational AI et joue la réponse de l'IA dans atlas_out (qui alimente le micro de Chrome).
#!/usr/bin/env python3"""meet_elevenlabs_bridge.py — Pont ElevenLabs ConvAI <-> Google Meet via PulseAudio.Flux audio : Audio sortant Meet -> meet_capture.monitor -> Agent ElevenLabs Agent ElevenLabs -> atlas_out -> atlas_out.monitor -> atlas_mic -> Micro Meet"""import argparseimport queueimport subprocessimport sysimport threadingimport signalimport timefrom typing import Callablefrom elevenlabs.client import ElevenLabsfrom elevenlabs.conversational_ai.conversation import ( AudioInterface, Conversation,)# ── Configuration ─────────────────────────────────────────────────API_KEY = "votre_clé_api_elevenlabs_ici"INPUT_SOURCE = "meet_capture.monitor" # ce que Meet joueOUTPUT_SINK = "atlas_out" # alimente atlas_mic -> micro MeetSAMPLE_RATE = 16000CHANNELS = 1FORMAT = "s16le" # PCM 16-bit signé little-endianCHUNK_SAMPLES = 4000 # chunks de 250ms (recommandé par le SDK)CHUNK_BYTES = CHUNK_SAMPLES * CHANNELS * 2# ── Interface audio PulseAudio personnalisée ─────────────────────class PulseAudioInterface(AudioInterface): """Route l'audio via PulseAudio en utilisant des sous-processus parec/pacat.""" def start(self, input_callback: Callable[[bytes], None]): self.input_callback = input_callback self.output_queue: queue.Queue[bytes] = queue.Queue() self.should_stop = threading.Event() # Capture depuis la sortie audio de Meet self._rec_proc = subprocess.Popen( [ "parec", f"--device={INPUT_SOURCE}", f"--format={FORMAT}", f"--rate={SAMPLE_RATE}", f"--channels={CHANNELS}", "--latency-msec=50", ], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, ) # Jouer les réponses de l'IA dans atlas_out -> micro de Meet self._play_proc = subprocess.Popen( [ "pacat", "--playback", f"--device={OUTPUT_SINK}", f"--format={FORMAT}", f"--rate={SAMPLE_RATE}", f"--channels={CHANNELS}", "--latency-msec=50", ], stdin=subprocess.PIPE, stderr=subprocess.DEVNULL, ) self._input_thread = threading.Thread( target=self._read_input, daemon=True ) self._output_thread = threading.Thread( target=self._write_output, daemon=True ) self._input_thread.start() self._output_thread.start() print(f"[audio] capture depuis {INPUT_SOURCE}") print(f"[audio] lecture vers {OUTPUT_SINK}") def stop(self): self.should_stop.set() if self._rec_proc: self._rec_proc.terminate() if self._play_proc: try: self._play_proc.stdin.close() except Exception: pass self._play_proc.terminate() def output(self, audio: bytes): self.output_queue.put(audio) def interrupt(self): # Vider la file d'attente quand l'utilisateur interrompt l'IA try: while True: self.output_queue.get_nowait() except queue.Empty: pass def _read_input(self): while not self.should_stop.is_set(): chunk = self._rec_proc.stdout.read(CHUNK_BYTES) if not chunk: break self.input_callback(chunk) def _write_output(self): while not self.should_stop.is_set(): try: audio = self.output_queue.get(timeout=0.25) self._play_proc.stdin.write(audio) self._play_proc.stdin.flush() except queue.Empty: pass except BrokenPipeError: break# ── Principal ─────────────────────────────────────────────────────def run_bridge(client: ElevenLabs, agent_id: str): print(f"\n[pont] Démarrage du pont ElevenLabs ConvAI") print(f"[pont] Agent : {agent_id}") print(f"[pont] Appuyez sur Ctrl+C pour arrêter\n") quit_event = threading.Event() signal.signal(signal.SIGTERM, lambda s, f: quit_event.set()) signal.signal(signal.SIGINT, lambda s, f: quit_event.set()) while not quit_event.is_set(): print("[pont] Démarrage d'une nouvelle session...") try: conversation = Conversation( client=client, agent_id=agent_id, requires_auth=False, audio_interface=PulseAudioInterface(), callback_agent_response=lambda t: print(f"[agent] {t}"), callback_user_transcript=lambda t: print(f"[user] {t}"), callback_latency_measurement=lambda ms: print( f"[latence] {ms}ms" ), ) conversation.start_session() conversation.wait_for_session_end() except Exception as e: print(f"[pont] Erreur de session : {e}") if not quit_event.is_set(): print("[pont] Session terminée, redémarrage dans 2s...") time.sleep(2) print("[pont] Terminé.")def main(): parser = argparse.ArgumentParser( description="Pont ElevenLabs ConvAI <-> Google Meet" ) parser.add_argument("--agent-id", required=True, help="ID de l'agent ElevenLabs") parser.add_argument( "--api-key", default=API_KEY, help="Clé API ElevenLabs", ) args = parser.parse_args() client = ElevenLabs(api_key=args.api_key) run_bridge(client, args.agent_id)if __name__ == "__main__": main()
Comment fonctionne le PulseAudioInterface
Le SDK ElevenLabs attend un AudioInterface avec quatre méthodes :
Méthode
Ce qu'elle fait
start(callback)
Lance les sous-processus parec (capture) et pacat (lecture)
output(audio)
Met en file d'attente les octets audio générés par l'IA pour la lecture
interrupt()
Vide la file quand l'utilisateur commence à parler (interruption)
stop()
Termine les sous-processus audio
Utiliser parec et pacat directement (au lieu de PyAudio ou sounddevice) est l'approche la plus fiable sur les serveurs Linux headless — pas de conflits ALSA/JACK, pas de problèmes d'énumération de périphériques.
Étape 5 : Tout connecter ensemble
Maintenant, exécutons les trois composants. Ouvrez trois sessions de terminal (ou utilisez tmux) :
Après que Chrome a rejoint l'appel, déplacez sa sortie audio vers le sink de capture :
# Lister les flux audio de Chromepactl list short sink-inputs# Déplacer chaque flux vers meet_capture (remplacez INDEX par le numéro réel)pactl move-sink-input INDEX meet_capture
Vous pouvez automatiser cela avec une fonction d'aide :
def move_chrome_audio(): """Déplace tous les flux audio Chrome vers le sink meet_capture.""" import time time.sleep(6) # attendre que Chrome commence à jouer de l'audio result = subprocess.run( ["pactl", "list", "short", "sink-inputs"], capture_output=True, text=True, ) for line in result.stdout.strip().splitlines(): parts = line.split() if parts: subprocess.run( ["pactl", "move-sink-input", parts[0], "meet_capture"], capture_output=True, ) print(f"Flux audio {parts[0]} déplacé vers meet_capture")
Étape 6 : Créer votre agent ElevenLabs
Avant de lancer le pont, vous avez besoin d'un agent Conversational AI sur ElevenLabs :
Allez dans Tableau de bord ElevenLabs > Conversational AI
Cliquez sur Créer un agent
Configurez votre agent :
Nom : Le nom de votre bot (ex. « Atlas »)
Voix : Choisissez une voix dans la bibliothèque
Prompt système : Définissez la personnalité et les connaissances de l'agent
Langue : Réglez sur votre langue préférée
Copiez l'ID de l'agent depuis la page de paramètres de l'agent
Exemple de prompt système
Tu es Atlas, un assistant IA serviable qui participe à un appel Google Meet.
Tu écoutes ce que disent les participants et tu réponds naturellement.
Garde tes réponses concises — c'est une conversation en direct, pas un chat textuel.
Si tu n'es pas sûr de quelque chose, demande des précisions.
Étape 7 : Déployer en un seul script
Pour une utilisation en production, combinez tout en un seul script :
#!/usr/bin/env python3"""voice_meet_bot.py — Agent vocal IA complet pour Google Meet."""import osimport subprocessimport sysimport threadingimport time# ... (combiner meet_bot.py + meet_elevenlabs_bridge.py)# Voir le script combiné complet dans le dépôt du projetdef main(): # 1. Configurer les périphériques audio setup_audio() # 2. Démarrer Chrome et rejoindre Meet driver = make_driver() join_thread = threading.Thread(target=join_meet, args=(driver, MEET_URL)) join_thread.start() # 3. Déplacer l'audio de Chrome après qu'il a rejoint time.sleep(10) move_chrome_audio() # 4. Démarrer le pont ElevenLabs client = ElevenLabs(api_key=API_KEY) run_bridge(client, AGENT_ID)
Dépannage
« Pas d'audio des participants Meet »
L'audio de Chrome n'est peut-être pas routé vers meet_capture. Exécutez :
pactl list short sink-inputs
Si vous voyez le flux Chrome sur un autre sink, déplacez-le :
pactl move-sink-input <INDEX> meet_capture
« L'IA répond mais les participants Meet ne l'entendent pas »
Vérifiez que atlas_mic est la source d'entrée de Chrome :
pactl list short source-outputs
Déplacez l'entrée source de Chrome si nécessaire :
pactl move-source-output <INDEX> atlas_mic
« Chrome ne démarre pas »
Assurez-vous que Xvfb est en cours d'exécution : export DISPLAY=:99
Vérifiez que la version de ChromeDriver correspond à Chrome : google-chrome --version
« La session ElevenLabs redémarre sans cesse »
Vérifiez que votre clé API est valide
Assurez-vous qu'il y a bien de l'audio entrant (le silence peut causer des timeouts de session)
Essayez d'augmenter CHUNK_SAMPLES à 8000 (chunks de 500ms)
« Meet détecte le bot comme automatisé »
Le flag --disable-blink-features=AutomationControlled aide
La surcharge de la propriété webdriver dans make_driver() aide aussi
Évitez de rejoindre trop d'appels en succession rapide
Pour les tests et le développement, vous pouvez faire tourner l'ensemble pour moins de 1$/jour.
Et ensuite
Une fois la configuration de base fonctionnelle, voici quelques idées pour l'étendre :
Ajouter des bases de connaissances à votre agent ElevenLabs pour des conversations spécifiques à un domaine
Enregistrer les transcriptions en utilisant les fonctions de rappel pour des notes de réunion automatisées
Support multilingue en configurant les paramètres de langue de l'agent
Outils personnalisés — Les agents ElevenLabs supportent l'appel de fonctions, votre bot peut donc interroger des bases de données, appeler des API ou déclencher des actions en pleine conversation
Plusieurs bots dans le même appel — chacun avec un rôle différent (secrétaire, traducteur, expert métier)
Conclusion
Construire un agent vocal IA pour Google Meet est étonnamment réalisable avec la bonne configuration de routage audio. La combinaison de Selenium pour l'automatisation du navigateur, PulseAudio pour les périphériques audio virtuels et ElevenLabs pour l'IA conversationnelle crée un pipeline robuste qui fonctionne de manière fiable sur les serveurs cloud headless.
La partie la plus difficile n'est pas l'IA — c'est la plomberie audio. Une fois que vous comprenez le flux meet_capture -> ElevenLabs -> atlas_out -> atlas_mic, le reste est simple.
Lancez une instance Vast.ai, suivez les étapes et faites rejoindre votre IA à des appels en moins d'une heure. Dites-nous ce que vous construisez avec !
Construit et testé par l'équipe d'ingénierie Noqta. Des questions ? Contactez-nous sur noqta.tn.
Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.
Apprenez à utiliser les dictionnaires de prononciation avec le SDK Python ElevenLabs pour contrôler la prononciation des mots dans les applications de synthèse vocale.
Apprenez a utiliser le modele ALLaM-7B-Instruct-preview avec Python, et comment interagir avec lui depuis JavaScript via une API hebergee (ex: sur Hugging Face Spaces).