كل المقالات

· Casimiro Ferreira· 8 دقيقة قراءة

عائلة من مكتبات الكلام القائمة على ONNX الخالص

  • ONNX
  • TTS
  • voice cloning
  • VAD
  • self-hosted
  • phoonnx

ONNX صيغة ملف لشبكة عصبية مدرَّبة: الأوزان ورسم الحساب، مُجمَّدين، دون أي اعتماد على الإطار الذي دربهما. يمكن لنموذج مُصدَّر إلى ONNX أن يعمل عبر ONNX Runtime، محرك استدلال صغير لا يفعل شيئًا سوى تنفيذ ذلك الرسم. فهو لا يعرف كيف دُرِّب النموذج، ولا يدعم التدريب، ولا يحتاج إلى تثبيت PyTorch أو TensorFlow.

تلتزم عدة من مكتباتنا بقاعدة واحدة: في وقت التشغيل، الاعتماديات الوحيدة هي onnxruntime وnumpy. ليس “في الغالب” — فاستيراد الحزمة نفسها لا يجرّ أبدًا إطار تدريب. تتبع هذا audiosronnx (توسيع النطاق الترددي وإزالة الضوضاء)، وvoiceclonnx (استنساخ الصوت)، وspeakeronnx (تضمينات المتحدث)، وspeechonnxmetrics (التقييم)، وstressonnx (نبر الكلمات)، وvadonnx (كشف النشاط الصوتي)، وphoonnx (التصويت وتحويل النص إلى كلام)، كلٌّ في حزمة PyPI خاصة به. واثنتان أخريان، phoonnx.js وprecise-onnx-js، تطبّقان الفكرة نفسها في المتصفح باستخدام onnxruntime-web بدلًا من ذلك.

لماذا نكلّف أنفسنا العناء

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

  • حجم التثبيت. تثبيت PyTorch + CUDA يقفز إلى غيغابايتات قبل أن تحمّل نموذجًا واحدًا. أما onnxruntime وnumpy معًا فبضع عشرات من الميغابايتات.
  • دون إدارة CUDA. مطابقة تعريف وحدة معالجة رسومات، وإصدار مجموعة أدوات CUDA، وبنية إطار عمل هي مصدر متكرر للأعطال. يتجاوز ONNX Runtime المخصَّص لوحدة المعالجة المركزية ذلك تمامًا، ومع ذلك يُشغِّل الرسم نفسه على وحدة معالجة رسومات حيث تتوفر واحدة.
  • يعمل على عتاد متواضع. يستطيع Raspberry Pi أو حاسوب محمول عمره عشر سنوات تشغيل onnxruntime بارتياح. وعادةً لا يستطيع تشغيل حزمة PyTorch الكاملة بسرعة قابلة للاستخدام، ولا حتى تثبيتها على لوحة 32 بت أو محدودة الذاكرة.
  • قطعة أثرية واحدة، لكل منصّة. يعمل ملف .onnx نفسه دون تعديل على Linux و macOS و Windows — وعبر onnxruntime-web، داخل تبويب متصفح. لا خطوة تصدير منفصلة لكل هدف.
  • دون تعارضات إصدارات بين التدريب والخدمة. حزمة التدريب تثبّت إصدارات محددة من الإطار و CUDA. أما حزمة الخدمة فتريد أصغر مجموعة اعتماديات وأكثرها استقرارًا. فصلهما يعني ترقية إحداهما دون كسر الأخرى.

ما الذي يكلّفه ذلك

القيد حقيقي، وليس بلا ثمن.

لا يمكنك الضبط الدقيق أثناء التشغيل. رسم ONNX لا يمتلك أمثِّلًا (optimizer) ولا تمريرة خلفية. تعامل كل مكتبة من هذه المكتبات النماذج بوصفها قطعًا أثرية ثابتة: تحمّلها وتشغّلها. يحدث التدريب أو الضبط الدقيق بمعزل، بالإطار الأصلي، وتُصدَّر النتيجة إلى ONNX لاحقًا. تحتفظ كلٌّ من stressonnx وspeechonnxmetrics بإضافة اختيارية باسم export تجرّ torch فقط من أجل خطوة التحويل غير المتصلة تلك — أبدًا من أجل الاستدلال.

ليست كل بنية تُصدَّر بسلاسة. التحكم الديناميكي في التدفق، أو نوى CUDA المخصّصة، أو العمليات التي لا مقابل لها في ONNX يمكن أن تعطّل تصديرًا مباشرًا. يوثّق ملف README الخاص بـ audiosronnx هذا صراحةً: فهو يحتفظ بقائمة النماذج غير المشحونة التي قيّمها ورفضها، مع الأسباب، بدلًا من التظاهر بأن كل نموذج بحثي يُنقَل بسلام.

يجب إعادة تنفيذ المعالجة المسبقة يدويًا. يشحن إطار مثل PyTorch أو Kaldi تنفيذات سريعة ومُختبَرة لـ STFT (تحويل موجة صوتية إلى مخطط طيفي)، وسمات مصفاة مِل، وإعادة أخذ العينات. وبمجرد ألا يعتمد النموذج نفسه على ذلك الإطار، لا يمكن لمعالجته المسبقة أن تعتمد عليه أيضًا — تُعيد speakeronnx تنفيذ مصفاة لوغاريتمية-مِل بـ 80 نطاقًا بلغة NumPy الخالصة لهذا السبب بالضبط، وتفعل audiosronnx الشيء نفسه لـ STFT وإعادة أخذ العينات. هذا شيفرة أكثر ينبغي إتقانها، وتحتاج اختبارات تكافؤ خاصة بها مقابل الأصل.

مهمة واحدة، محركات عدة، واجهة واحدة

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

تختار كل مكتبة في هذه العائلة مهمة واحدة وتغلّف عدة نماذج منشورة مستقلة خلف واجهة واحدة، بحيث يصير تبديل المحرك تغييرًا بسطر واحد بدلًا من إعادة كتابة.

تفصل audiosronnx مهمتيها — إزالة الضوضاء وتوسيع النطاق الترددي (تحويل تسجيل ضيق النطاق، كصوت هاتفي بـ 8 كيلوهرتز، إلى إشارة أكمل صوتًا وأعلى معدّل أخذ عينات) — خلف مُحمِّلَين، مدعومًا كلٌّ منهما بعدة محركات:

from audiosronnx import load_denoise, load_sr

clean, rate = load_denoise("dpdfnet").denoise("noisy_call.wav")   # remove noise
wide, _ = load_sr("lavasr").upscale(clean, rate)                  # extend to 48 kHz

يسجّل load_denoise حاليًا عشرة مزيلات ضوضاء (dpdfnet، وmossformer2، وfrcrn، وmpsenet، وgtcrn، وcmgan، وmetadenoiser، وmossformergan، وvoicefixer، وdeepfilternet)، من نموذج بحجم 0.54 ميغابايت إلى آخر بحجم 415 ميغابايت، برخص مختلفة. ويسجّل load_sr سبعة موسِّعات نطاق ترددي (lavasr، وnovasr، وflowhigh، وhifiganbwe، وapbwe، وsidon، وcallenhancer). النماذج المهيمَن عليها — التي يتفوق عليها محرك آخر على كل محور مقيس — تبقى في السجل رغم ذلك، كي تبقى نتيجة معيارية منشورة قابلة لإعادة الإنتاج عند الطلب.

تسلك voiceclonnx النهج نفسه لاستنساخ الصوت — تحويل الصوت في تسجيل موجود ليبدو كمتحدث مرجعي مختلف، دون المرور عبر النص:

from voiceclonnx import VoiceCloner

cloner = VoiceCloner(engine="facodec")
out = cloner.clone_voice("source.wav", "reference.wav", "out.wav")

عشرة محركات مسجّلة (facodec، وopenvoice، وchatterbox، وtriaan، وcosyvoice، وbicodec، وknnvc، وfocalcodec، وlscodec، وrvc)، تمتد عبر ست عائلات نماذج متمايزة — تبديل السمات بأقرب جار، والمُرمِّز المُفكَّك، ومطابقة التدفق، ونقل اللون النغمي، ونموذج اللغة المُرمِّز الانحداري الذاتي، والمُرمِّز المنفصل عن المتحدث. يأتي كل واحد خلف الكواليس بأرقام منشورة للوضوح وتشابه المتحدث، بحيث يصير اختيار محرك مقارنةً، لا رمية عملة.

تطبّق vadonnx النمط نفسه على كشف النشاط الصوتي — تقرير أي أجزاء من تدفق صوتي تحتوي كلامًا أصلًا:

from vadonnx import load_vad

vad = load_vad("silero")
segments = vad.get_speech_segments(audio, sample_rate=16000)
# -> [SpeechSegment(start=0.32, end=2.27), SpeechSegment(start=3.27, end=4.45), ...]

ست عائلات نماذج مسجّلة (silero، وmarblenet، وpyannote، وfsmn، وspeechbrain، وten)، ويتيح IOSignature تصريحي لمحرك عام واحد تشغيل معظمها، أو الإشارة إلى أي ملف .onnx مخصّص لكشف النشاط الصوتي.

تستخرج speakeronnx تضمين متحدث — متجهًا ثابت الطول يلخّص من يتحدث، بمعزل عمّا قاله — وتقارن تضمينين بتشابه جيب التمام للتحقق مما إذا كان مقطعان صوتيان للمتحدث نفسه:

from speakeronnx import SpeakerEmbedder, cosine

embedder = SpeakerEmbedder(model="wespeaker-resnet34")
alice1 = embedder.embed("alice_clip1.wav")
alice2 = embedder.embed("alice_clip2.wav")
print(cosine(alice1, alice2))   # e.g. 0.82 - same speaker

تسجّل تسعة نماذج عبر أربع عائلات بنيوية (WeSpeaker وCAM++ وERes2Net وReDimNet)، بأبعاد تضمين ورخص منشورة.

تختار stressonnx نبر الكلمات لواجهات تحويل النص إلى كلام — أي مقطع من الكلمة يحمل التشديد، وهي معلومة لا تكتبها لغات كثيرة صراحةً (الروسية за́мок، قلعة، مقابل замо́к، قفل، تتشاركان الأحرف نفسها). تسجّل خط أنابيب عصبيًا للروسية، وثانيًا للأوكرانية والبيلاروسية، وواجهة خلفية قائمة على القواعد والمعجم تغطي 26 لغة دون أي استدلال عصبي إطلاقًا:

from stressonnx import stress

stress("старинный замок стоит на горе", "ru")
# 'стари́нный за́мок сто́ит на горе́'

تصوّت phoonnx النص (تحوّل الكلمات المهجّاة إلى وحدات الصوت التي يستهلكها نموذج TTS) وتشغّل تحويل النص إلى كلام عبر 17 محرك تخليق مسجّلًا وأصواتًا مُصدَّرة من عدة منظومات (phoonnx الأصلية، وPiper، وMimic3، وCoqui، وMMS، وTransformers):

import wave
from phoonnx.voice import TTSVoice

voice = TTSVoice.load("model.onnx", "model.json")
with wave.open("hello.wav", "wb") as wav_file:
    voice.synthesize_wav("Hello world!", wav_file)

تحمل phoonnx.js مسارات المُقطِّع نفسها إلى المتصفح باستخدام onnxruntime-web، وتنقل precise-onnx-js كشف كلمة التنبيه (استخراج سمات MFCC مع مصنِّف ONNX، متوافق مع نماذج Mycroft Precise) إلى JavaScript، كلاهما دون خادم:

import { loadVoice, synthesizeWav } from "phoonnx";
import { getVoice } from "phoonnx/voices";

const voice = await loadVoice(getVoice("phoonnx_eu-ES_dii_unicode")!);
const blob = await synthesizeWav(voice, "Kaixo mundua!");

تعيش أوزان audiosronnx (18 نموذجًا منشورًا) وvoiceclonnx (10 نماذج منشورة) كتنزيلات منفصلة على منظمة TigreGótico على Hugging Face، تُجلَب عند أول استخدام وتُخزَّن محليًا، بحيث يصير اختيار محرك مختلف تغييرًا في الإعداد، لا إعادة نشر.

إغلاق الحلقة: الحكم على المحركات بدلًا من التخمين

تسجيل محركات كثيرة خلف واجهة واحدة لا يؤتي ثماره إلا إذا استطعت معرفة أيّها أفضل فعلًا لمدخلك. لهذا وُجدت speechonnxmetrics: مكتبة مقاييس مبنية على القيد نفسه، numpy + onnxruntime، بحيث لا يكلّف تقييم نموذج تثبيتًا إضافيًا.

تجمّع المقاييس في ثلاثة أنواع. تُقدِّر مُقدِّرات MOS بلا مرجع — UTMOS، وDNSMOS، وNISQA، وSIGMOS — درجة الرأي المتوسط، وهي التقييم من 1 إلى 5 لدرجة الطبيعية الذي قد تمنحه لجنة مستمعين بشرية لمقطع، دون الحاجة إلى مرجع نظيف للمقارنة. أما المقاييس التدخّلية — STOI (الوضوح الموضوعي قصير المدى)، وSI-SDR (نسبة الإشارة إلى التشويه غير المتغيرة بالمقياس)، وMCD (تشويه مِل-الطيفي) — فتحتاج إلى مرجع نظيف مطابق وتقيس مدى قرب المخرجات منه. أما المقاييس النصية القائمة على ASR — WER (معدّل خطأ الكلمات) وCER (معدّل خطأ الأحرف) — فتُشغِّل أداة تعرّف على الكلام فوق المخرجات وتقارن النص المفرَّغ بالنص المتوقَّع، فتلتقط حالات ينتج فيها نموذج صوتًا يبدو جيدًا لكنه يقول كلمات خاطئة.

import speechonnxmetrics as s

print(s.score("degraded.wav", ["utmos"]))
# -> {'utmos': 4.41...}

print(s.score("clone_output.wav", ["stoi", "mcd", "si_sdr"], ref="source.wav"))
# -> {'stoi': 0.662..., 'mcd': 10.459..., 'si_sdr': -26.937...}

هذا يحوّل اختيار المحرك من اختبار استماع إلى جدول. تنشر voiceclonnx بالضبط تلك المقارنة لمحركات الاستنساخ العشرة الخاصة بها — معدّل خطأ الكلمات مقابل النص المصدر إضافةً إلى درجة تشابه متحدث منفصلة لكل واحد، بحيث تصير عبارات مثل “facodec يعطي معدّل خطأ كلمات 0%” أو “lscodec يضحّي بمعدّل خطأ الكلمات مقابل نقل جرس نغمي أقوى” ادّعاءات مقيسة، لا انطباعات. وحين تُضرَب تلك المقارنة عبر اللغات وحالات التسجيل يتوقف التقييم اليدوي عن كونه واقعيًا؛ والمقياس الموضوعي هو ما يجعل سجلًا من عشرة محركات قابلًا للاستخدام بدلًا من مُرهِق.

أين يفيد هذا

إن كنت تحتاج معالجة صوت دون اتصال — تنظيف تسجيل، أو استنساخ صوت، أو كشف من يتحدث، أو تخليق أحدهم — على عتاد لن يرى وحدة معالجة رسومات أبدًا، فهذا هو الشكل الذي ينبغي البحث عنه: اعتمادية تشغيل صغيرة، واختيار من نماذج منشورة بدلًا من افتراضي واحد ثابت، وطريقة لقياس أيّها يعمل فعلًا لحالتك. كل مكتبة أعلاه على بُعد pip install، مرخّصة بـ MIT أو Apache على مستوى الشيفرة (تحمل أوزان النماذج الفردية رخصها الخاصة من المنبع، موثَّقة لكل محرك)، وتعمل بالطريقة نفسها على حاسوب محمول، أو خادم، أو Raspberry Pi.

تواصل معنا عبر /contact أو اطّلع على ما نبنيه أيضًا في /services.