Skip to content

piper tts guide

SalehGNUTUX edited this page Jul 20, 2026 · 1 revision

دليلُ Piper TTS — مرجعٌ عمليٌّ للمطوّرين

خلاصةُ تجربةٍ حقيقيّةٍ في بناءِ نطقٍ عربيٍّ (وإنجليزيٍّ وفرنسيٍّ) يعملُ دون إنترنت لتطبيقِ أطفال. كلُّ ما هنا مُجرَّبٌ في مشروعِ مِشكاةُ الطفلِ والآليّ — بما فيه الأخطاءُ التي كلّفتْنا وقتًا. نُشارِكُه لأنّ المشروعَ حرٌّ مفتوحُ المصدر، ولأنّ توثيقَ النطقِ العربيِّ العصبيِّ نادرٌ عمليًّا.

الرخصة: هذا الدليلُ جزءٌ من المشروع (GPL-3.0). انسخْه واقتبسْ منه بحرّيّة.


1) لماذا Piper؟ والمبدأُ الحاكم

Piper محرّكُ تركيبِ كلامٍ عصبيٌّ (VITS) خفيفٌ يعملُ محلّيًّا بنماذجِ ONNX.

قرارُنا المعماريُّ الحاسم:

Piper يُستعمَلُ وقتَ التطويرِ فقط. يُنتِجُ ملفّاتِ MP3 ثابتةً تُشحَنُ مع التطبيق. لا نموذجَ ولا استدلالَ وقتَ التشغيل.

لماذا؟

  • الخصوصيّة: لا شيءَ يُرسَل؛ التطبيقُ يعملُ بلا إنترنتٍ ولا أذوناتٍ إضافيّة.
  • الأداء: الهواتفُ الضعيفةُ لا تحتملُ استدلالَ VITS لحظيًّا؛ تشغيلُ MP3 مجّانيٌّ تقريبًا.
  • الثبات: الصوتُ نفسُه على كلِّ جهازٍ — لا مفاجآتٍ من أصواتِ النظام.
  • الحجم: النموذجُ الواحدُ 63 ميغابايت؛ ثلاثةُ نماذجَ = 190م. أمّا المقاطعُ المُولَّدةُ فـ9م بترميزٍ مناسب.

الثمن: كلُّ نصٍّ منطوقٍ يجبُ أن يُعرَفَ وقتَ البناء. لا نطقَ لنصٍّ يُركَّبُ لحظيًّا (انظرْ §7).


2) الإعدادُ من الصفر

التثبيت

python3 -m venv .piper-venv
.piper-venv/bin/pip install piper-tts imageio-ffmpeg

imageio-ffmpeg يجلبُ ثنائيَّ ffmpeg تلقائيًّا — أوفقُ من الاعتمادِ على ffmpeg النظام.

تنزيلُ النماذج

النماذجُ من Hugging Face — rhasspy/piper-voices. كلُّ صوتٍ ملفّان: .onnx (النموذج) و.onnx.json (الإعداد). كلاهما إلزاميّ.

mkdir -p tools/piper-voices && cd tools/piper-voices
# مثال: العربيّة (الأردنّ)
curl -LO https://huggingface.co/rhasspy/piper-voices/resolve/main/ar/ar_JO/kareem/medium/ar_JO-kareem-medium.onnx
curl -LO https://huggingface.co/rhasspy/piper-voices/resolve/main/ar/ar_JO/kareem/medium/ar_JO-kareem-medium.onnx.json

أصواتُنا الثلاثة:

اللغة الصوت المعرّف الاستعمال
العربيّة ar_JO-kareem-medium tts-kareem الجُمَلُ والقصصُ وكلامُ الشخصيّة
الإنجليزيّة en_GB-alba-medium tts-alba قسمُ اللغاتِ (حروف/كلمات/جُمَل)
الفرنسيّة fr_FR-tom-medium tts-tom قسمُ اللغات

مواصفاتُ النموذجِ العربيِّ (من .onnx.json): sample_rate: 22050 · phoneme_type: espeak · espeak.voice: ar · num_speakers: 1 · quality: medium · inference: {noise_scale: 0.667, length_scale: 1.0, noise_w: 0.8}.

.gitignore: لا تُودِعِ *.onnx* ولا .piper-venv/ (قابلةٌ لإعادةِ التنزيل). أودِعِ المقاطعَ المُولَّدةَ (public/tts/) فهي مُخرَجُ البناءِ الذي يُشحَن.

جودةُ النموذج (x_low / low / medium / high)

جرّبْنا medium واستقرّينا عليه: high يُضاعِفُ الحجمَ وزمنَ التوليدِ بفارقٍ لا يُدرِكُه طفلٌ عبرَ سمّاعةِ لوحيّة، وlow يُسمَعُ معدنيًّا.


3) البنيةُ العمليّة

tools/
├─ piper_synth.py       # عاملُ بايثون: نصّ → WAV → MP3 (يُستدعى من Node)
├─ gen-piper.mjs        # يجمعُ نصوصَ العربيّةِ ويبني قائمةَ المهامّ
└─ gen-piper-lang.mjs   # المِثلُ للغاتِ الأجنبيّة (en|fr)
public/tts/voices/<setHash>/<textHash>.mp3
src/voices-bundled.json # {sets:[…], files:{"<setId> <النصّ>": "المسار"}}

المفتاحُ هو النصُّ نفسُه: "tts-kareem مَرْحَبًا". لا معرّفاتٍ ولا فهارس. فتغييرُ حرفٍ واحدٍ في النصِّ = مقطعٌ جديد، والقديمُ يصيرُ يتيمًا.

اسمُ الملفِّ = sha1(النصّ).slice(0,12) — يتجنّبُ حدودَ أسماءِ الملفّاتِ ومشاكلَ اليونيكود.

المسارُ من نصٍّ إلى صوت

نصٌّ في content/ أو robo-phrases
   ↓ gen-piper.mjs يجمعُه ويُوحّدُه (uniq)
   ↓ forSynthesis(t) — تصحيحُ التشكيلِ (§5)
   ↓ piper_synth.py → WAV (22.05kHz)
   ↓ ffmpeg → MP3 (libmp3lame -q:a 6)
   ↓ voices-bundled.json (مفتاحُه النصُّ الأصليّ)
   ↓ وقتَ التشغيل: بحثٌ حرفيٌّ بالنصّ → تشغيلُ MP3

4) خصائصُ كلِّ لغة

العربيّة — التشكيلُ هو كلُّ شيء

Piper العربيُّ يُفنِّمُ عبرَ espeak-ng، وespeak لا يُخمّنُ الحركات. فالنصُّ غيرُ المشكولِ يُنطَقُ عشوائيًّا.

  • شكّلِ النصَّ المنطوقَ بالكامل. الجزئيُّ أسوأُ من العدم أحيانًا: يُضلّلُ المُفنِّمَ فيُخطئُ حيثُ كان يُصيب.
  • مزلقٌ خبيث: كلماتٌ تحملُ شدّةً فقط (معلّمي, أنّ, ثمّ) تبدو مشكولةً لأيِّ فحصٍ يبحثُ عن [ً-ْ]، وهي بلا حركاتٍ قصيرة فتُنطَقُ خطأً. افحصْها صراحةً.
  • لفظُ الجلالة «الله»: اتركْه مجرَّدًا. المشكَّلُ يُشوَّه؛ والمجرَّدُ يُنطَقُ صحيحًا لأنّ espeak يعرفُه استثناءً. forSynthesis يجرّدُه تلقائيًّا.
  • «من» لا تُشكَّلُ آليًّا: مِنْ (جارّة) ≠ مَنْ (موصولة). السياقُ وحدَه يحسم — وفحصُنا وجدها ~50/50.
  • الكلماتُ المفردةُ القصيرةُ تُشوَّه: النموذجُ العصبيُّ يقضمُ حوافَّ الوحداتِ القصيرة. ذيّلْها بنقطة (t + " .") في نصِّ التركيبِ فقط — يُعطيها خاتمةً جمليّةً فيستقرُّ النطق.
  • الحروفُ المفردةُ: لا تستعملْ Piper لها إطلاقًا (§8).

الإنجليزيّة والفرنسيّة — الإملاءُ الصوتيُّ هو المفتاح

أسماءُ الحروفِ تُخزَّنُ كما تُنطَق لا كما تُكتَب، لأنّ المفتاحَ نفسَه يُغذّي المُفنِّم:

الحرف خطأ صواب لماذا
A (en) "ay" "eigh" ay → /aɪ/ = تُنطَقُ I
H (en) "h" "aitch" الاسمُ لا الصوت
H (fr) "ache" الاسمُ الفرنسيّ

تحقّقْ قبلَ أن تُصدّق:

espeak-ng -v en-gb -q --ipa -- "eigh"    # المتوقَّع: eɪ
espeak-ng -v fr    -q --ipa -- "ache"
espeak-ng -v ar    -q --ipa -- "بِسْمِ اللَّهِ"

قاعدةٌ تعلّمناها بالتجربة: حين يُخطئُ نطقُ حرفٍ أجنبيّ، لا تُبدّلِ المحرّك — أصلِحِ الإملاءَ الصوتيَّ للمفتاح. جرّبْنا الرجوعَ إلى espeak فكانت النتيجةُ أسوأ.


5) إصلاحُ النطق — ثلاثُ أدواتٍ بترتيبِ الأولويّة

(أ) forSynthesis() — التطبيعُ البنيويّ (تلقائيّ)

يُطبَّقُ على نصِّ التركيبِ فقط والمفتاحُ يبقى الأصل:

  1. إعادةُ ترتيبِ الشدّة: المحرّكاتُ تتوقّعُ «حرف + شدّة + حركة». وNFC يُنتِجُ أحيانًا «حرف + حركة + شدّة» فتُهمَلُ الشدّةُ وتُسمَعُ كالسكون (أوّل → «أَوْل»).
  2. تجريدُ لفظِ الجلالة من الحركات.
  3. تاءُ التأنيثِ وقفًا (ة → ه) للمفردةِ المشكولة.

(ب) SYNTH_FIX — تصحيحٌ يدويٌّ لحالةٍ بعينها

حين يكونُ النصُّ صحيحًا لغويًّا والنطقُ خاطئًا:

const SYNTH_FIX = {
  "مُمْتَاز!": "مُمْتَااز!",                    // مدُّ الألفِ صراحةً (كان يُنطَقُ قصيرًا)
  "اِنْتَهَى الوَقْت!…": "اِنْتَهَى الوَقَتُ.…", // «الوَقْت» كان يُسمَعُ «الوَخْت»
};

المفتاحُ (والملفُّ) يبقى النصَّ الأصليَّ — فلا يتغيّرُ ما يُعرَضُ للمستخدمِ ولا تنكسرُ المطابقة. هذه أهمُّ خاصّيّةٍ في الآليّة.

(ج) إعادةُ الصياغة — الملاذُ الأخير

إن استعصى النطقُ، غيّرِ العبارةَ نفسَها. أحيانًا يكونُ ذلك أصحَّ تربويًّا أيضًا.


6) التوليدُ التفاضليّ (وفّرْ 25 دقيقةً في كلِّ مرّة)

توليدُ ~1000 مقطعٍ يستغرقُ ~25 دقيقة. لا معنى لإعادتِه لأجلِ عبارةٍ واحدة:

force = os.environ.get("PIPER_FORCE") == "1"
if not force and os.path.exists(out) and os.path.getsize(out) > 0:
    manifest[key] = file      # ⚠️ أضِفْه للبيان رغمَ التخطّي
    skip += 1
    continue

مزلقانِ وقعْنا فيهما:

  1. لا تنسَ manifest[key] = file عند التخطّي — وإلّا سقطتِ المقاطعُ الموجودةُ من الحزمة.
  2. صحّحْ شرطَ الخروج: كان sys.exit(0 if ok > 0 else 1)، وبعدَ التخطّي صارَ «صفرُ جديدٍ» يعني محدَّثٌ لا فشلًا:
    sys.exit(0 if (ok + skip) > 0 and fail == 0 else 1)
    قبلَ الإصلاحِ كان البناءُ يفشلُ بسببٍ زائفٍ كلّما لم يتغيّرْ نصّ.

PIPER_FORCE=1 يُعيدُ توليدَ الكلِّ — استعمِلْه عند تغييرِ النموذجِ أو إعداداتِ التركيب.

تنظيفُ اليتامى

تغييرُ نصٍّ يُنتِجُ مقطعًا جديدًا ويترُكُ القديمَ يتيمًا:

const ref = new Set(Object.values(bundle.files));
// كلُّ ملفٍّ تحتَ public/tts/voices لا يُشيرُ إليه البيان ⇒ احذفْه

نصيحة: تحقّقْ دائمًا أنّ عددَ الملفّاتِ على القرصِ = عددَ مراجعِ البيان. عندنا 3831 = 3831.


7) وقتَ التشغيل — المطابقةُ حرفيّةٌ تمامًا

const url = bundledFiles["tts-kareem" + " " + text];   // تطابقٌ بايتًا ببايت

النتيجةُ العمليّة: أيُّ نصٍّ يُركَّبُ لحظيًّا لا مقطعَ له.

// ❌ خطأٌ كلّفَنا إصدارًا كاملًا: أربعةُ نصوصٍ لكلٍّ مقطعُه،
//    لكنّ السلسلةَ المدموجةَ نصٌّ جديدٌ بلا مقطع ⇒ ارتدادٌ لصوتِ النظام
roboSay(`${cheer} ${reaction} ${praise} ${tadabbur}`);

// ✅ صواب: كلُّ جزءٍ بمقطعِه، متتابعةً
roboSayChain([cheer, reaction, praise, tadabbur]);

قائمةُ تحقّقٍ لأيِّ نصٍّ منطوقٍ جديد:

  1. هل هو مشكولٌ بالكامل؟
  2. هل مصدرُه يقرؤُه المولّد؟ (وليس سلسلةً مضمَّنةً في الشيفرة)
  3. هل شُغِّلَ gen:piper بعد إضافتِه؟
  4. هل هو نصٌّ ثابتٌ لا مُركَّبٌ لحظيًّا؟

مزلقٌ في المولّد: ما يستوردُه gen-piper.mjs يجبُ أن يخلوَ من DOM. استيرادُ وحدةِ واجهةٍ يجرُّ سلسلةً تنتهي بـvoices-bundled.json فيفشلُ في Node. ضعِ النصوصَ في content/.


8) حدودُ Piper — متى لا تستعملُه

الحالة الحكم البديل
جُمَلٌ ونصوصٌ وقصص ✅ ممتاز
ردودُ شخصيّةٍ وحوار ✅ ممتاز
حروفٌ مفردة ❌ فاشل espeak (أوضحُ للتعليم) أو تسجيلٌ بشريّ
مفرداتٌ معزولة ⚠️ متوسّط تسجيلٌ بشريٌّ إن توفّر
تلاوةُ القرآن ❌ لا يجوز تسجيلُ قارئٍ مُجاز

لماذا يفشلُ في الحروف؟ النموذجُ مُدرَّبٌ على كلامٍ متّصل؛ الوحدةُ المعزولةُ تخرجُ بحوافَّ مبتورةٍ وتشكيلٍ مطموس — وهو ما يحتاجُه الطفلُ بالضبط.

استراتيجيّتُنا: أولويّةٌ ذكيّة — بشريٌّ مسجَّل ← عصبيٌّ (Piper) ← آليٌّ (espeak) — لكلِّ نوعٍ على حدة (حرف/كلمة/جملة). فالعصبيُّ للجُمَل، وespeak يضمنُ تغطيةً كاملةً للحروف.


9) الترميزُ والحجم

ffmpeg -i in.wav -codec:a libmp3lame -q:a 6 out.mp3

-q:a 6 (~48kbps VBR) — الخيارُ الذي استقرّينا عليه بعدَ تجريب: كلامٌ واضحٌ بحجمٍ يسمحُ بشحنِ آلافِ المقاطع. (كلامٌ لا موسيقى، فالتردّداتُ العاليةُ غيرُ مهمّة.)

أرقامٌ من مشروعِنا: 1042 مقطعًا عربيًّا · 714 إنجليزيًّا · 1057 فرنسيًّا ≈ 9 ميغابايت.

استثناء: لا تضغطْ تلاوةَ القرآنِ — تُترَكُ بجودتِها الأصليّة.


10) قائمةُ تحقّقٍ قبلَ الإصدار

  • كلُّ نصٍّ منطوقٍ جديدٍ مشكولٌ ومُدرَجٌ في مصدرٍ يقرؤُه المولّد
  • npm run gen:piper (تفاضليٌّ فسريع)
  • صفرُ يتيم: ملفّاتُ القرصِ = مراجعُ البيان
  • استماعٌ فعليٌّ للعباراتِ الجديدة — لا تفترضْ
  • npm run build وحجمُ precache معقول

11) مصادر

الرخص: Piper و espeak-ng — GPL-3.0 · النماذجُ لكلٍّ رخصتُه (راجِعْ بطاقةَ الصوتِ على Hugging Face) · هذا الدليل — GPL-3.0.

Clone this wiki locally