المتطلبات المسبقة
- Python 3.11 أو أحدث، وuv
- مفتاح Venice API من venice.ai
- ميكروفون ومكبرات صوت، إن أردت حلقة الصوت الكاملة
uv sync حزمة Python، وعلى Windows هذا كل ما تحتاجه. أما macOS وLinux فيحتاجان أيضًا إلى مكتبة PortAudio:
--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 يُحدثان فرقًا حقيقيًا في الطريقة التي يبدو بها صوت الوكيل:include_venice_system_prompt: False قيام Venice بإلحاق موجّه النظام الخاص بها قبل موجّهنا. إن تُرك مفعّلًا، فذلك نحو ألف وسبعمئة رمز إدخال إضافي لكل استدعاء وصوت ثانٍ يُملي على النموذج كيف يتصرف. أما disable_thinking: True (مع reasoning.enabled: False للنماذج التي تقرأ الحقل الأحدث) فيمنع GLM من إنفاق ميزانية رموزه على سلسلة تفكير خفية قبل أن يقول شيئًا — وذلك، حين تنتظر سماع الرد، وقت يمكنك أن تسمعه.
الموجّه نفسه يستحق طوله. طلب عشرين كلمة يُبقي الإجابات ذات طابع منطوق لا مكتوب، وعبارة «احذف التفاصيل بدل أن تنتهي في منتصف الجملة» هي ما يمنع سقف max_tokens الصارم من بتر الرد في منتصف كلمة. وحظر ماركداون أهم مما تظن: نموذج TTS سيقرأ النجمات بصوت عالٍ بكل سرور.
تعليمة معاملة رسالة المستخدم كمدخل غير موثوق تؤدي عملًا حقيقيًا هنا. فالكلام المفرّغ نصيًا هو مدخل من المستخدم كأي مدخل آخر، وعبارة «تجاهل تعليماتك السابقة» يسهل قولها بصوت عالٍ بقدر ما يسهل كتابتها.
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 بسيطة عمدًا — تشذّب السلسلة وتعود إلى القيمة الافتراضية من البيئة، ولا تتحقّق من قائمة:
تحقّق قبل التشغيل
إليك المطبّ الوحيد الذي سيجعلك تقفز من كرسيّك. الـ PCM الخام بلا ترويسة ولا بايتات سحرية، فإذا كُتبت استجابة خطأ في أنبوب الصوت، شغّل مكبر الصوت الـ JSON بأمانة كموجة ضجيج بأقصى مستوى. لذا تحقّق من الحالة ونوع المحتوى قبل معاملة الجسم كصوت، وتشمّم أول قطعة كخط دفاع أخير:RIFF استجابة WAV ويلتقط ID3 استجابة MP3، وكلاهما يعني أن response_format لم يُفعَّل. وفحص JSON يلتقط جسم خطأ. لا شيء من هذا ذكي، وكله هو الفرق بين خطأ مقروء ومستخدم مذعور.
التسجيل والتشغيل
هذا الجزء لا يخصّ 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 الخاص بها إلى المشغّل فور وصوله.
raise_on_error=not failed يعني أنه حين يكون الدور فاشلًا أصلًا نفكّك التشغيل بهدوء بدلًا من تكديس خطأ ثانٍ فوق الخطأ الحقيقي.
طباعة زمن أول صوت شيء صغير لكنه مفيد فعلًا أثناء الضبط. إنه الرقم الذي يشعر به المستخدم.
حلقة الموجّه
كل ما تبقّى هوwhile True حول input():
warmup ذاك يستحق مكانه أيضًا. فهو يسرد النماذج ويرسل مسبار TTS من كلمة واحدة، مما يُنشئ اتصال TLS ويتحقّق من المفتاح والصوت قبل الدور الحقيقي الأول للمستخدم لا أثناءه:
التشغيل
reset لبدء محادثة جديدة، وq للخروج. الضغط على Ctrl+C أثناء رد يوقف التشغيل ويعيدك إلى الموجّه بدلًا من الخروج.
بعض التنويعات:
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 والمقاطعة والمكالمات متعددة المشاركين.