Skip to main content
تستطيع Venice أن تسمعك وتردّ عليك. لا يوجد مقبس فوري للكلام إلى كلام تتصل به، وهذا يبدو قيدًا إلى أن تلاحظ أن الوكيل الصوتي ليس في حقيقته سوى ثلاثة استدعاءات HTTP عادية في حلقة: انسخ ما قاله المستخدم نصًا، ولّد ردًا، ثم انطق الرد. في هذا الدليل، سنبني تلك الحلقة كتطبيق طرفية بلغة Python. اضغط Enter، وتكلّم، ثم اضغط Enter مرة أخرى، فتُشغَّل الإجابة عبر مكبرات الصوت لديك. ويمكنك كتابة سطر بدلًا من ذلك إن كنت لا تفضّل استخدام الميكروفون. هذا هو الشكل نفسه STT ← LLM ← TTS الموجود في دليل LiveKit Agents، لكن من دون LiveKit وكلمات التنبيه والأدوات. تجريد إطار العمل هو المقصد: بنهاية الدليل ستعرف بالضبط أيّ ثلاثة طلبات تؤدّي العمل، ولماذا نجعل اثنين منها متدفّقين. قبل أن نواصل: ستحتاج إلى مفتاح Venice API. صدّره كمتغيّر بيئة:
مهتم بالتنفيذ الكامل للشيفرة؟ اطّلع على مستودع GitHub.

المتطلبات المسبقة

  • Python 3.11 أو أحدث، وuv
  • مفتاح Venice API من venice.ai
  • ميكروفون ومكبرات صوت، إن أردت حلقة الصوت الكاملة
يمرّ التسجيل والتشغيل عبر sounddevice الذي يغلّف PortAudio. يُثبّت uv sync حزمة Python، وعلى Windows هذا كل ما تحتاجه. أما macOS وLinux فيحتاجان أيضًا إلى مكتبة PortAudio:
لا شيء من هذا يخصّ Venice — إنه فقط الطريقة التي تدخل بها العيّنات إلى جهازك وتخرج منه. يقبل التطبيق راية --text-only التي تتخطّى الميكروفون كليًا وتظل تُشغّل الدردشة وTTS، لتتمكّن من المتابعة على جهاز بلا عتاد صوتي إطلاقًا.

ما الذي نبنيه

الدور الواحد من المحادثة هو ثلاثة طلبات: معرّفات النماذج هذه نقطة انطلاق لا قائمة ثابتة. تُبدّل Venice الكتالوج دوريًا، لذا حدّدها وقت التشغيل من GET /models?type=... وGET /models/traits قبل أن تشحن أي شيء. راجع الإهمال التدريجي لمعرفة كيف يجري ذلك. سنُبقي شجرة المصدر صغيرة عمدًا:
هذا التقسيم أهم مما يبدو. venice.py هو الجزء الذي يمكنك نقله كما هو إلى تطبيق ويب أو بوت Discord أو تكامل هاتفي. audio.py هو الملف الوحيد الذي يهتم بالجهاز الذي يعمل عليه، وVenice لا ترى شيئًا منه أبدًا — الـ API لا يستقبل سوى كتلة WAV في طريق الدخول ويعيد PCM خامًا في طريق الخروج.

الإعداد

أنشئ المشروع وأضف الاعتماديات. تتولّى OpenAI SDK كل عمل HTTP، ويُبقي python-dotenv المفتاح خارج سجل الصدفة (shell)، ويتحدّث sounddevice إلى الميكروفون ومكبرات الصوت:
ثم أنشئ .env.example كي تكون خيارات النماذج إعدادات لا شيئًا مدفونًا في الشيفرة:
انسخه إلى .env وألصق مفتاحك فيه.

توجيه SDK نحو Venice

واجهة Venice API متوافقة مع OpenAI، لذا نستخدم عميل openai الرسمي ونغيّر عنوان URL الأساسي. هذا هو التكامل بأكمله. أنشئ venice.py وابدأ بالعميل:
لاحظ أننا نتحقّق من المفتاح بأنفسنا بدلًا من ترك os.environ["VENICE_API_KEY"] يرمي استثناء. فتتبّع مكدّس KeyError تجربة أولى سيئة لشيء اعتيادي كمفتاح مفقود. قطعة تدبير أخرى ونحن هنا. ترفع SDK أصنافًا فرعية من OpenAIError، والتفصيلة المفيدة مدفونة في جسم الاستجابة، لذا يستحق الأمر فكّ تغليفها مرة واحدة:
كل استدعاء أدناه يمرّر إخفاقاته عبر هذه الدالة، فيظهر معرّف صوت خاطئ أو مفتاح منتهي كسطر واحد مقروء بدلًا من تتبّع مكدّس.

سماع المستخدم

يستقبل POST /audio/transcriptions ملفًا صوتيًا ويعيد نصًا. نحن نسجّل محليًا بصيغة WAV أحادية القناة بتردد 16 كيلوهرتز، لكن نقطة النهاية تقبل الصيغ المعتادة، لذا نربط امتداد الملف بنوع MIME بدلًا من تثبيت واحد:
التفريغ النصي في Venice هو طلب/استجابة لا مقبس تدفّق، ولهذا للتسجيل نهاية محدّدة — نضغط Enter بدلًا من تشغيل كشف النشاط الصوتي. إن أردت تحديد نهاية الكلام اعتمادًا على VAD، فتلك هي المهمة التي يوكلها دليل LiveKit إلى Silero. التفريغ الفارغ نتيجة عادية لا خطأ. سيضغط أحدهم Enter مرتين بالخطأ، ورسالة ودّية «لم ألتقط ذلك» أفضل من استثناء في كل مرة.

بثّ الرد تدفّقًا

الآن استدعاء الدردشة. هناك إعدادان خاصان بـ Venice يُحدثان فرقًا حقيقيًا في الطريقة التي يبدو بها صوت الوكيل:
يمنع include_venice_system_prompt: False قيام Venice بإلحاق موجّه النظام الخاص بها قبل موجّهنا. إن تُرك مفعّلًا، فذلك نحو ألف وسبعمئة رمز إدخال إضافي لكل استدعاء وصوت ثانٍ يُملي على النموذج كيف يتصرف. أما disable_thinking: True (مع reasoning.enabled: False للنماذج التي تقرأ الحقل الأحدث) فيمنع GLM من إنفاق ميزانية رموزه على سلسلة تفكير خفية قبل أن يقول شيئًا — وذلك، حين تنتظر سماع الرد، وقت يمكنك أن تسمعه. الموجّه نفسه يستحق طوله. طلب عشرين كلمة يُبقي الإجابات ذات طابع منطوق لا مكتوب، وعبارة «احذف التفاصيل بدل أن تنتهي في منتصف الجملة» هي ما يمنع سقف max_tokens الصارم من بتر الرد في منتصف كلمة. وحظر ماركداون أهم مما تظن: نموذج TTS سيقرأ النجمات بصوت عالٍ بكل سرور.
تعليمة معاملة رسالة المستخدم كمدخل غير موثوق تؤدي عملًا حقيقيًا هنا. فالكلام المفرّغ نصيًا هو مدخل من المستخدم كأي مدخل آخر، وعبارة «تجاهل تعليماتك السابقة» يسهل قولها بصوت عالٍ بقدر ما يسهل كتابتها.
بوجود ذلك، يصبح الاستدعاء إكمالًا متدفّقًا عاديًا:
القرار التصميمي المهم أن هذه الدالة تُنتج جملًا لا رموزًا. يحتاج TTS إلى عبارة مكتملة لضبط النبرة الصوتية (prosody)، لذا نجمّع الدلتات في مخزن مؤقت حتى تكتمل جملة، ثم نسلّمها. هذا ما يسمح ببدء تشغيل الصوت بينما لا يزال النموذج يتكلم. يسمح حدث cancel للمستدعي بالتوقف عن استنزاف التدفّق حين يضغط المستخدم Ctrl+C، وإغلاق التدفّق داخل كتلة finally يحرّر الاتصال بدلًا من تركه معلّقًا حتى انتهاء المهلة.

تقسيم الجمل فور وصولها

التقسيم على . و! و؟ يوصلك إلى 90% من الطريق ثم يُحرجك أول مرة يقول فيها النموذج “Dr. Smith”. لذا نتحقّق مما إذا كان ما قبل النقطة اختصارًا قبل معاملتها كحدّ جملة:
لاحظ أن التعبير النمطي يشترط مسافة بيضاء بعد علامة الترقيم. هذا مقصود: في منتصف التدفّق، قد تكون "Hello." جملة مكتملة أو النصف الأول من "Hello.txt"، ولا نستطيع الجزم بعد. انتظار المسافة يعني أننا لا نقطع جملة قبل أوانها أبدًا، مقابل احتجاز الجملة الأخيرة حتى ينتهي التدفّق — وهو ما تعالجه iter_sentences بذلك التفريغ النهائي لـ leftover. هذا مُقسِّم ساذج وهو كافٍ. وهو أيضًا الجزء الوحيد من المنطق هنا الذي يرخص اختباره بوحدات، لذا يستحق ذلك:

نطق الرد

POST /audio/speech هو الاستدعاء الثالث والأخير. خياران يجعلانه يبدو سريعًا:
يمنحنا response_format="pcm" عيّنات خام مُوقَّعة بعمق 16 بت بترتيب little-endian عند 24 كيلوهرتز أحادية القناة، يمكننا تمريرها مباشرة إلى مكبر الصوت دون خطوة فكّ ترميز. وإلا فإن tts-kokoro يعود افتراضيًا إلى MP3، وفكّ ترميز MP3 يعني انتظار وصول جزء كافٍ من الملف قبل أن تتمكن من تشغيل أي شيء منه. أما streaming: True فهي راية Venice التي تبدأ إرسال الصوت أثناء توليفه بدلًا من الانتظار حتى اكتمال المقطع كله. resolve_voice بسيطة عمدًا — تشذّب السلسلة وتعود إلى القيمة الافتراضية من البيئة، ولا تتحقّق من قائمة:
معرّف الصوت غير المعروف يفشل عند الـ API برسالة واضحة، وهذا أفضل من قائمة سماح محلية تتقادم بصمت مع إضافة Venice أصواتًا جديدة. لكن الأصوات خاصة بكل نموذج، فصوت Kokoro مع نموذج TTS مختلف لن يعمل — راجع نماذج تحويل النص إلى كلام للاقترانات.

تحقّق قبل التشغيل

إليك المطبّ الوحيد الذي سيجعلك تقفز من كرسيّك. الـ PCM الخام بلا ترويسة ولا بايتات سحرية، فإذا كُتبت استجابة خطأ في أنبوب الصوت، شغّل مكبر الصوت الـ JSON بأمانة كموجة ضجيج بأقصى مستوى. لذا تحقّق من الحالة ونوع المحتوى قبل معاملة الجسم كصوت، وتشمّم أول قطعة كخط دفاع أخير:
يلتقط RIFF استجابة WAV ويلتقط ID3 استجابة MP3، وكلاهما يعني أن response_format لم يُفعَّل. وفحص JSON يلتقط جسم خطأ. لا شيء من هذا ذكي، وكله هو الفرق بين خطأ مقروء ومستخدم مذعور.
لا تمرّر أبدًا جسم HTTP غير مفحوص إلى مخرج صوت خام. لا توجد مفاوضة صيغة في جانب التشغيل لتنقذك — أيّ بايتات تصل تُشغَّل كعيّنات.

التسجيل والتشغيل

هذا الجزء لا يخصّ Venice، لذا سنمرّ به سريعًا. يفتح audio.py تدفّق إدخال PortAudio أثناء كلام المستخدم وتدفّق إخراج PortAudio لتشغيل الرد، وكلاهما عبر sounddevice. نستورده بكسل كي يصبح غياب مكتبة أصلية جملةً بدلًا من OSError عند بدء التشغيل:
هذان إخفاقان مختلفان فعلًا بحلّين مختلفين، ويبلّغ sounddevice عن الثاني كـ OSError مجرّد من الاستيراد نفسه. التقاطهما معًا هنا هو ما يسمح لـ --text-only بالعمل على جهاز لا يستطيع تحميل PortAudio إطلاقًا. التسجيل هو نداء رجعي (callback) يُلحق في قائمة، مع سقف صارم كي لا تنمو جلسة منسيّة بلا حدود:
الـ try/finally المتداخل مقصود. الداخلي يحوّل الإلغاء إلى AudioError ودّي، والخارجي يوقف التدفّق ويغلقه في كل مسار خروج — بما في ذلك الإلغاء — لأن RawInputStream الذي لا يُغلق أبدًا يظل ممسكًا بالميكروفون بعد انتهاء الدور. وbytes(indata) تنسخ بدلًا من أن تُشير إلى الأصل، لأن PortAudio يعيد استخدام ذلك المخزن المؤقت في النداء التالي. لاحظ أن العيّنات لا تلمس القرص أبدًا. تحتاج /audio/transcriptions إلى رفع بهيئة ملف، لكن «بهيئة ملف» يعني فقط أنه يحتاج إلى ترويسة WAV، ويمكننا وضعها في الذاكرة:
أربعة عشر سطرًا لتجنّب كتابة تسجيل لصوت أحدهم إلى مجلد مؤقت، وهذه تبدو صفقة جيدة. wave في المكتبة القياسية، والبايتات تذهب مباشرة إلى الوسيط file= الذي أعددناه سابقًا. التشغيل تدفّق واحد لكل رد، فتجري الجمل المتتابعة ككلام متصل بدلًا من إعادة تشغيل الجهاز في كل مرة:
مخزن _pending هو التفصيلة الوحيدة هنا التي ستؤذيك إن تخطّيتها. حدود قطع HTTP لا علاقة لها بحدود العيّنات، فقراءة 4096 بايت قد تسلّمك عددًا فرديًا من البايتات وتشطر عيّنة 16 بت من منتصفها. اكتب ذلك إلى الجهاز وستنزاح كل عيّنة لاحقة بايتًا واحدًا، وهو ما يبدو كالمكافئ الصوتي للتشويش. لذا لا نكتب أبدًا سوى عدد زوجي من البايتات ونرحّل البايت الفائض إلى النداء التالي. الصنف الكامل في المستودع يتضمن أيضًا abort() لأجل Ctrl+C — أوقف الجهاز فورًا وتخلّص مما في المخزن — وclose() للمسار العادي، الذي يفرّغ آخر عيّنة جزئية (محشوّة ببايت صفري) ثم ينتظر انتهاء الجهاز من تشغيل ما لديه. عكس الاثنين يعني إما قصّ الكلمة الأخيرة من كل رد أو العجز عن مقاطعة أحدها.
PortAudio هو طبقة النقل بين المنصات هنا، لذا يعمل audio.py نفسه على macOS وWindows وLinux. لا شيء في venice.py يعلم أو يهتم بأيّها.

مزامنة التدفّق مع التشغيل

هنا يؤتي التدفّق ثماره فعلًا. إذا استنزفنا تدفّق الدردشة وشغّلنا الصوت على الخيط نفسه، حجب التشغيل الحلقة وبقيت رموز النموذج المتبقية غير مقروءة في مخزن مقبس. لذا نستنزف التدفّق على خيط جانبي ونسلّم الجمل عبر طابور:
وضع الاستثناء في الطابور وإعادة رفعه في جانب المستهلك هو ما يُبقي معالجة الأخطاء أمينة. فخيط خلفي يموت بصمت يمنحك تعليقًا بدلًا من رسالة، واستخدام BaseException بدلًا من Exception يعني أن KeyboardInterrupt داخل التدفّق يظل يصل إلى المستدعي. الآن الدور نفسه: اسحب الجمل، واطبع كل واحدة، وغذِّ الـ PCM الخاص بها إلى المشغّل فور وصوله.
يُنشأ المشغّل بكسل عند أول قطعة صوت بدلًا من إنشائه مسبقًا، فلا يترك فشل TTS تدفّق إخراج خاملًا ممسكًا بمكبرات الصوت. وraise_on_error=not failed يعني أنه حين يكون الدور فاشلًا أصلًا نفكّك التشغيل بهدوء بدلًا من تكديس خطأ ثانٍ فوق الخطأ الحقيقي. طباعة زمن أول صوت شيء صغير لكنه مفيد فعلًا أثناء الضبط. إنه الرقم الذي يشعر به المستخدم.

حلقة الموجّه

كل ما تبقّى هو while True حول input():
السطر الفارغ يعني «استمع»؛ وأي شيء آخر يُعامَل كمدخل مكتوب. يُشذَّب التاريخ إلى آخر ثمانية تبادلات، وهو أكثر من كافٍ لمحادثة منطوقة ويُبقي عدد رموز الإدخال ثابتًا بدلًا من نموّه إلى أن يشتكي شيء ما. معالجة الأخطاء ذات المستويين تستحق الإشارة. إخفاقات الإعداد تُخرج من البرنامج — لا جدوى من بدء REPL لا تستطيع استخدامه. أما إخفاقات كل دور فتُطبع ويُعاد المستخدم إلى الموجّه، لأن حد المعدّل أو تسجيلًا مرتبكًا لا ينبغي أن ينهي الجلسة. استدعاء warmup ذاك يستحق مكانه أيضًا. فهو يسرد النماذج ويرسل مسبار TTS من كلمة واحدة، مما يُنشئ اتصال TLS ويتحقّق من المفتاح والصوت قبل الدور الحقيقي الأول للمستخدم لا أثناءه:

التشغيل

اضغط Enter، وتكلّم، ثم اضغط Enter مرة أخرى. اكتب سطرًا إن كنت لا تفضّل الميكروفون، وreset لبدء محادثة جديدة، وq للخروج. الضغط على Ctrl+C أثناء رد يوقف التشغيل ويعيدك إلى الموجّه بدلًا من الخروج. بعض التنويعات:
إن التقط الميكروفون أو مكبرات الصوت الخطأ، اسأل PortAudio عمّا يراه وضع اسمًا أو فهرسًا في AUDIO_SOURCE / AUDIO_SINK:
والاختبارات:

ما تتوقعه من زمن الاستجابة

خط الأنابيب ثلاثة طلبات متتابعة، فتتراكم الأرقام تقريبًا هكذا: توقّع نحو ثانية حتى أول صوت على اتصال جيد. يهيمن على هذا الرقم أمران: هل يبدأ TTS عند أول جملة أم ينتظر الرد كاملًا، وهل يحرق النموذج رموزًا في التفكير قبل أن يتكلم. التدفّق على مستوى الجمل وdisable_thinking هما التغييران اللذان ستلاحظهما هنا لو أزلتهما. إن أردته أسرع، فأبقِ الردود قصيرة — الجملة الأولى هي ما يحكم الإحساس بالاستجابة — وجرّب نموذج دردشة من فئة flash. هناك المزيد عن هذا في ملاحظات زمن الاستجابة في LiveKit.

ملاحظات الخصوصية

يجدر التصريح بما يغادر الجهاز، فهذا مشروع فيه ميكروفون. يذهب الصوت إلى Venice ليُفرَّغ نصيًا ويعود النص ليُنطق؛ وكلاهما مشمول بسياسة Venice لعدم الاحتفاظ بالبيانات إطلاقًا، ولا يُخزَّن شيء في جانبهم بعد الطلب. محليًا، لا يُكتب أي شيء إلى القرص إطلاقًا — يُجمَّع التسجيل في قائمة، ويُغلَّف بترويسة WAV في الذاكرة، ويُسلَّم إلى الطلب، فلا يوجد ملف مؤقت ليتسرّب أو يُنظَّف. يُقرأ مفتاح API من البيئة ولا يُطبع أبدًا. وتاريخ المحادثة يعيش في الذاكرة فقط ويختفي حين تخرج أو تكتب reset. راجع الخصوصية للاطلاع على المستويات حسب كل نموذج إن احتجت إلى ضمان أقوى من عدم الاحتفاظ.

الختام

الخلاصة التي تأخذها معك: الوكيل الصوتي على Venice هو ثلاث نقاط نهاية متوافقة مع OpenAI، اثنتان منها متدفّقتان. وكل شيء آخر في هذا المشروع — مُقسِّم الجمل، وتدفّقات الصوت، والطابور — موجود لجعل تلك الاستدعاءات الثلاثة تبدو كمحادثة. venice.py هو الجزء الذي يستحق السرقة. استبدل app.py بمعالج ويب أو تكامل هاتفي وطبقة الـ API لا تتغيّر. بعض ما يستحقّ عمله لاحقًا:

أعطه أدوات

أضف استدعاء الدوال إلى خطوة الدردشة ويستطيع الوكيل البحث عن أشياء أثناء المحادثة.

دعه يبحث

اضبط enable_web_search في venice_parameters وتتوقف الإجابات عن الاقتصار على بيانات التدريب.

استنسخ صوتًا

استبدل معرّف صوت Kokoro بصوت استنسخته بنفسك.

ضعه في غرفة

سلّم المراحل الثلاث نفسها إلى LiveKit لأجل VAD والمقاطعة والمكالمات متعددة المشاركين.
شكرًا للقراءة! نأمل أن يكون هذا قد أزال بعض الغموض عن الوكلاء الصوتيين — فهم أقل غرابة بكثير مما يبدون حين ترى الطلبات الثلاثة تحتهم.

موارد ذات صلة