تخيّل وكيلاً ذكياً يمكنه الانضمام إلى مكالمات Google Meet، الاستماع إلى المشاركين، والرد بصوت بشري طبيعي — كل ذلك يعمل بشكل مستقل على خادم سحابي. هذا بالضبط ما سنبنيه في هذا الدليل.
في النهاية، ستحصل على:
بوت مبني على Selenium ينضم إلى Google Meet كمشارك ضيف
توجيه صوت افتراضي PulseAudio ينقل صوت Meet إلى الذكاء الاصطناعي والعكس
جسر ElevenLabs Conversational AI يستمع ويفكر ويتحدث
نشر على خادم سحابي GPU على Vast.ai مع سكريبت إعداد بأمر واحد
هكذا يتدفق الصوت:
المشاركون في Meet يتحدثون
← Chrome يلتقط الصوت ← sink meet_capture
← ElevenLabs ConvAI (STT ← LLM ← TTS)
← sink atlas_out ← مصدر افتراضي atlas_mic
← مدخل ميكروفون Chrome ← Meet يسمع رد الذكاء الاصطناعي
هذا نفس الإعداد الذي اختبرناه داخلياً في نقطة — وهو يعمل.
المتطلبات المسبقة
قبل البدء، تأكد من أن لديك:
حساب ElevenLabs مع إمكانية الوصول إلى Conversational AI
ينشئ أجهزة صوت افتراضية، يوجه الصوت بين Meet والذكاء الاصطناعي
PulseAudio
جسر الذكاء الاصطناعي
يلتقط صوت Meet، يرسله إلى ElevenLabs، يشغّل الرد
SDK ElevenLabs ConvAI
الثلاثة يعملون على نفس الجهاز. الفكرة الأساسية هي استخدام null sinks ومصادر افتراضية PulseAudio لإنشاء جسر صوتي ثنائي الاتجاه بين Chrome وواجهة برمجة ElevenLabs — لا حاجة لميكروفون أو مكبر صوت فعلي.
الخطوة 1: توفير خادم سحابي
تحتاج إلى خادم Linux مع بيئة سطح مكتب (لـ Chrome). استخدمنا Vast.ai لأنه رخيص وسريع التشغيل ويمنحك صلاحيات root.
بما أنه لا يوجد شاشة فعلية أو بطاقة صوت على الخادم السحابي، نستخدم Xvfb (مخزن إطارات افتراضي) للعرض وPulseAudio لأجهزة الصوت الافتراضية.
تشغيل Xvfb وPulseAudio
# تشغيل العرض الافتراضيXvfb :99 -screen 0 1280x720x24 &export DISPLAY=:99# تشغيل PulseAudio في وضع النظامpulseaudio --start --exit-idle-time=-1
إنشاء أجهزة الصوت الافتراضية
هذا هو الجزء الحاسم. نحتاج إلى جهازي صوت افتراضيين:
# 1. meet_capture — خرج صوت Chrome يذهب هنا# سنقرأ من meet_capture.monitor لسماع ما يقوله المشاركونpactl load-module module-null-sink \ sink_name=meet_capture \ sink_properties=device.description=MeetCapture# 2. atlas_out — ردود الذكاء الاصطناعي تُشغَّل هنا# atlas_mic يقرأ من atlas_out.monitor ويعمل كمدخل ميكروفون Chromepactl load-module module-null-sink \ sink_name=atlas_out \ sink_properties=device.description=AtlasOutput# 3. atlas_mic — مصدر ميكروفون افتراضي يتغذى من atlas_outpactl load-module module-virtual-source \ source_name=atlas_mic \ master=atlas_out.monitor \ source_properties=device.description=AtlasMic# تعيين الافتراضيات ليستخدمها Chromepactl set-default-source atlas_mic # ميكروفون Chrome = خرج الذكاء الاصطناعيpactl set-default-sink meet_capture # مكبرات صوت Chrome = نقطة الالتقاط
التحقق من الإعداد
# التحقق من sinks (يجب أن ترى meet_capture وatlas_out)pactl list short sinks# التحقق من المصادر (يجب أن ترى atlas_mic وmeet_capture.monitor)pactl list short sources
يستخدم البوت Selenium لفتح Chrome، التنقل إلى Google Meet، إدخال اسم، والنقر على "طلب الانضمام":
#!/usr/bin/env python3"""meet_bot.py — ينضم إلى مكالمة Google Meet كضيف عبر 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") # السماح تلقائياً بالميكروفون/الكاميرا # مهم: لا تستخدم --use-fake-device-for-media-stream # نريد أن يستخدم Chrome أجهزة 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): """يجرب عدة محددات XPath، يعيد أول تطابق مرئي.""" 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"فتح {url}") driver.get(url) time.sleep(4) # الخطوة 1: اختيار وضع الضيف (بدون حساب 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(" تم اختيار وضع الضيف") time.sleep(3) # الخطوة 2: إدخال اسم البوت 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" تم إدخال الاسم: {BOT_NAME}") time.sleep(1) # الخطوة 3: النقر على الانضمام 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(" تم إرسال طلب الانضمام — في انتظار القبول...") else: print(" خطأ: لم يتم العثور على زر الانضمام") driver.save_screenshot("/tmp/meet_debug.png") return False # الخطوة 4: انتظار القبول (حتى 5 دقائق) 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" في المكالمة!") return True time.sleep(5) print(" انتهت مهلة انتظار القبول") return Falseif __name__ == "__main__": driver = make_driver() try: if join_meet(driver, MEET_URL): print("\nالبوت في المكالمة. اضغط Ctrl+C للمغادرة.") while True: time.sleep(10) except KeyboardInterrupt: print("\nمغادرة المكالمة...") finally: driver.quit()
شرح أعلام Chrome المهمة
العلم
لماذا
--use-fake-ui-for-media-stream
يقبل تلقائياً نوافذ أذونات الميكروفون/الكاميرا
--no-sandbox
مطلوب للتشغيل كـ root على الخوادم السحابية
--disable-blink-features=AutomationControlled
يمنع Meet من اكتشاف Selenium
عدم استخدام --use-fake-device-for-media-stream
يضمن أن Chrome يستخدم PulseAudio (أجهزة صوت حقيقية)
مهم: بعد بدء Chrome، قد تحتاج إلى نقل تدفق الصوت يدوياً إلى sink meet_capture. سنقوم بأتمتة هذا في الجسر.
الخطوة 4: بناء جسر ElevenLabs ConvAI
هذا هو المكون الأساسي. يلتقط الصوت من meet_capture.monitor (ما يقوله المشاركون)، يرسله إلى ElevenLabs Conversational AI، ويشغّل رد الذكاء الاصطناعي في atlas_out (الذي يغذي ميكروفون Chrome).
#!/usr/bin/env python3"""meet_elevenlabs_bridge.py — جسر ElevenLabs ConvAI <-> Google Meet عبر PulseAudio.تدفق الصوت: خرج صوت Meet -> meet_capture.monitor -> وكيل ElevenLabs وكيل ElevenLabs -> atlas_out -> atlas_out.monitor -> atlas_mic -> ميكروفون Meet"""import argparseimport queueimport subprocessimport sysimport threadingimport signalimport timefrom typing import Callablefrom elevenlabs.client import ElevenLabsfrom elevenlabs.conversational_ai.conversation import ( AudioInterface, Conversation,)# ── الإعدادات ─────────────────────────────────────────────────────API_KEY = "مفتاح_api_elevenlabs_الخاص_بك"INPUT_SOURCE = "meet_capture.monitor" # ما يشغله MeetOUTPUT_SINK = "atlas_out" # يغذي atlas_mic -> ميكروفون MeetSAMPLE_RATE = 16000CHANNELS = 1FORMAT = "s16le" # PCM 16-بت موقّع little-endianCHUNK_SAMPLES = 4000 # أجزاء 250 مللي ثانية (موصى بها من SDK)CHUNK_BYTES = CHUNK_SAMPLES * CHANNELS * 2# ── واجهة صوت PulseAudio مخصصة ──────────────────────────────────class PulseAudioInterface(AudioInterface): """يوجه الصوت عبر PulseAudio باستخدام عمليات فرعية 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() # الالتقاط من خرج صوت 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, ) # تشغيل ردود الذكاء الاصطناعي في atlas_out -> ميكروفون 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"[صوت] الالتقاط من {INPUT_SOURCE}") print(f"[صوت] التشغيل إلى {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): # تفريغ قائمة الانتظار عند مقاطعة المستخدم للذكاء الاصطناعي 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# ── الرئيسي ───────────────────────────────────────────────────────def run_bridge(client: ElevenLabs, agent_id: str): print(f"\n[جسر] بدء جسر ElevenLabs ConvAI") print(f"[جسر] الوكيل: {agent_id}") print(f"[جسر] اضغط Ctrl+C للإيقاف\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("[جسر] بدء جلسة جديدة...") try: conversation = Conversation( client=client, agent_id=agent_id, requires_auth=False, audio_interface=PulseAudioInterface(), callback_agent_response=lambda t: print(f"[وكيل] {t}"), callback_user_transcript=lambda t: print(f"[مستخدم] {t}"), callback_latency_measurement=lambda ms: print( f"[زمن الاستجابة] {ms}ms" ), ) conversation.start_session() conversation.wait_for_session_end() except Exception as e: print(f"[جسر] خطأ في الجلسة: {e}") if not quit_event.is_set(): print("[جسر] انتهت الجلسة، إعادة التشغيل خلال 2 ثانية...") time.sleep(2) print("[جسر] تم.")def main(): parser = argparse.ArgumentParser( description="جسر ElevenLabs ConvAI <-> Google Meet" ) parser.add_argument("--agent-id", required=True, help="معرّف وكيل ElevenLabs") parser.add_argument( "--api-key", default=API_KEY, help="مفتاح API ElevenLabs", ) args = parser.parse_args() client = ElevenLabs(api_key=args.api_key) run_bridge(client, args.agent_id)if __name__ == "__main__": main()
تضع بايتات الصوت المُنتجة بالذكاء الاصطناعي في قائمة الانتظار للتشغيل
interrupt()
تفرّغ قائمة الانتظار عندما يبدأ المستخدم بالتحدث (مقاطعة)
stop()
تنهي العمليات الفرعية للصوت
استخدام parec وpacat مباشرة (بدلاً من PyAudio أو sounddevice) هو النهج الأكثر موثوقية على خوادم Linux بدون واجهة — لا تعارضات ALSA/JACK، لا مشاكل في تعداد الأجهزة.
الخطوة 5: ربط كل شيء معاً
الآن لنشغّل المكونات الثلاثة. افتح ثلاث جلسات طرفية (أو استخدم tmux):
الطرفية 1: تشغيل العرض والصوت
# تشغيل XvfbXvfb :99 -screen 0 1280x720x24 &export DISPLAY=:99# تشغيل PulseAudiopulseaudio --start --exit-idle-time=-1# إنشاء أجهزة الصوت الافتراضيةpactl load-module module-null-sink sink_name=meet_capture \ sink_properties=device.description=MeetCapturepactl load-module module-null-sink sink_name=atlas_out \ sink_properties=device.description=AtlasOutputpactl load-module module-virtual-source source_name=atlas_mic \ master=atlas_out.monitor \ source_properties=device.description=AtlasMicpactl set-default-source atlas_micpactl set-default-sink meet_capture
بعد انضمام Chrome إلى المكالمة، انقل خرج الصوت إلى sink الالتقاط:
# عرض تدفقات صوت Chromepactl list short sink-inputs# نقل كل تدفق إلى meet_capture (استبدل INDEX بالرقم الفعلي)pactl move-sink-input INDEX meet_capture
يمكنك أتمتة هذا بدالة مساعدة:
def move_chrome_audio(): """نقل جميع تدفقات صوت Chrome إلى sink meet_capture.""" import time time.sleep(6) # انتظار بدء Chrome بتشغيل الصوت 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"تم نقل تدفق الصوت {parts[0]} إلى meet_capture")
الخطوة 6: إنشاء وكيل ElevenLabs
قبل تشغيل الجسر، تحتاج إلى وكيل Conversational AI على ElevenLabs:
اذهب إلى لوحة تحكم ElevenLabs > Conversational AI
انقر على إنشاء وكيل
قم بتكوين وكيلك:
الاسم: اسم البوت (مثلاً "Atlas")
الصوت: اختر أي صوت من المكتبة
موجه النظام: حدد شخصية الوكيل ومعرفته
اللغة: اضبط على لغتك المفضلة
انسخ معرّف الوكيل من صفحة إعدادات الوكيل
مثال على موجه النظام
أنت Atlas، مساعد ذكاء اصطناعي مفيد يشارك في مكالمة Google Meet.
تستمع إلى ما يقوله المشاركون وترد بشكل طبيعي.
اجعل ردودك موجزة — هذه محادثة مباشرة، وليست دردشة نصية.
إذا لم تكن متأكداً من شيء، اطلب التوضيح.
الخطوة 7: النشر كسكريبت واحد
للاستخدام في الإنتاج، ادمج كل شيء في سكريبت واحد:
#!/usr/bin/env python3"""voice_meet_bot.py — وكيل صوتي ذكي كامل لـ Google Meet."""import osimport subprocessimport sysimport threadingimport time# ... (دمج meet_bot.py + meet_elevenlabs_bridge.py)# انظر السكريبت المدمج الكامل في مستودع المشروعdef main(): # 1. إعداد أجهزة الصوت setup_audio() # 2. تشغيل Chrome والانضمام إلى Meet driver = make_driver() join_thread = threading.Thread(target=join_meet, args=(driver, MEET_URL)) join_thread.start() # 3. نقل صوت Chrome بعد الانضمام time.sleep(10) move_chrome_audio() # 4. تشغيل جسر ElevenLabs client = ElevenLabs(api_key=API_KEY) run_bridge(client, AGENT_ID)
استكشاف الأخطاء وإصلاحها
"لا يوجد صوت من مشاركي Meet"
قد لا يكون صوت Chrome موجهاً إلى meet_capture. شغّل:
pactl list short sink-inputs
إذا رأيت تدفق Chrome على sink مختلف، انقله:
pactl move-sink-input <INDEX> meet_capture
"الذكاء الاصطناعي يرد لكن مشاركي Meet لا يسمعونه"
تحقق أن atlas_mic هو مصدر إدخال Chrome:
pactl list short source-outputs
انقل مدخل مصدر Chrome إذا لزم الأمر:
pactl move-source-output <INDEX> atlas_mic
"Chrome لا يبدأ"
تأكد أن Xvfb يعمل: export DISPLAY=:99
تحقق أن إصدار ChromeDriver يتطابق مع Chrome: google-chrome --version
"جلسة ElevenLabs تستمر في إعادة التشغيل"
تحقق أن مفتاح API صالح
تأكد أن هناك صوتاً فعلياً يدخل (الصمت قد يسبب انتهاء مهلة الجلسة)
جرّب زيادة CHUNK_SAMPLES إلى 8000 (أجزاء 500 مللي ثانية)
"Meet يكتشف البوت كآلي"
علم --disable-blink-features=AutomationControlled يساعد
تجاوز خاصية webdriver في make_driver() يساعد أيضاً
للاختبار والتطوير، يمكنك تشغيل كامل النظام بأقل من 1$/يوم.
ما التالي
بمجرد أن يعمل الإعداد الأساسي، إليك بعض الأفكار لتوسيعه:
إضافة قواعد معرفة لوكيل ElevenLabs لمحادثات متخصصة بمجال معين
تسجيل النصوص باستخدام دوال الاستدعاء لملاحظات اجتماعات آلية
دعم متعدد اللغات من خلال تكوين إعدادات لغة الوكيل
أدوات مخصصة — وكلاء ElevenLabs يدعمون استدعاء الدوال، لذا يمكن لبوتك الاستعلام من قواعد البيانات، استدعاء APIs، أو تنفيذ إجراءات أثناء المحادثة
عدة بوتات في نفس المكالمة — كل واحد بدور مختلف (مدوّن ملاحظات، مترجم، خبير متخصص)
الخلاصة
بناء وكيل صوتي ذكي لـ Google Meet أمر ممكن بشكل مدهش مع الإعداد الصحيح لتوجيه الصوت. الجمع بين Selenium لأتمتة المتصفح، PulseAudio لأجهزة الصوت الافتراضية، وElevenLabs للذكاء الاصطناعي التحادثي ينشئ خط أنابيب قوي يعمل بشكل موثوق على الخوادم السحابية بدون واجهة.
الجزء الأصعب ليس الذكاء الاصطناعي — إنه توصيلات الصوت. بمجرد فهم تدفق meet_capture -> ElevenLabs -> atlas_out -> atlas_mic، يصبح الباقي بسيطاً.
شغّل مثيل Vast.ai، اتبع الخطوات، واجعل ذكاءك الاصطناعي ينضم إلى المكالمات في أقل من ساعة. أخبرنا بما تبنيه!
بُني واختُبر بواسطة فريق هندسة نقطة. أسئلة؟ تواصل معنا على noqta.tn.