-
Notifications
You must be signed in to change notification settings - Fork 0
piper tts guide
خلاصةُ تجربةٍ حقيقيّةٍ في بناءِ نطقٍ عربيٍّ (وإنجليزيٍّ وفرنسيٍّ) يعملُ دون إنترنت لتطبيقِ أطفال. كلُّ ما هنا مُجرَّبٌ في مشروعِ مِشكاةُ الطفلِ والآليّ — بما فيه الأخطاءُ التي كلّفتْنا وقتًا. نُشارِكُه لأنّ المشروعَ حرٌّ مفتوحُ المصدر، ولأنّ توثيقَ النطقِ العربيِّ العصبيِّ نادرٌ عمليًّا.
الرخصة: هذا الدليلُ جزءٌ من المشروع (GPL-3.0). انسخْه واقتبسْ منه بحرّيّة.
Piper محرّكُ تركيبِ كلامٍ عصبيٌّ (VITS) خفيفٌ يعملُ محلّيًّا بنماذجِ ONNX.
قرارُنا المعماريُّ الحاسم:
Piper يُستعمَلُ وقتَ التطويرِ فقط. يُنتِجُ ملفّاتِ MP3 ثابتةً تُشحَنُ مع التطبيق. لا نموذجَ ولا استدلالَ وقتَ التشغيل.
لماذا؟
- الخصوصيّة: لا شيءَ يُرسَل؛ التطبيقُ يعملُ بلا إنترنتٍ ولا أذوناتٍ إضافيّة.
- الأداء: الهواتفُ الضعيفةُ لا تحتملُ استدلالَ VITS لحظيًّا؛ تشغيلُ MP3 مجّانيٌّ تقريبًا.
- الثبات: الصوتُ نفسُه على كلِّ جهازٍ — لا مفاجآتٍ من أصواتِ النظام.
-
الحجم: النموذجُ الواحدُ
63 ميغابايت؛ ثلاثةُ نماذجَ = 190م. أمّا المقاطعُ المُولَّدةُ فـ9م بترميزٍ مناسب.
الثمن: كلُّ نصٍّ منطوقٍ يجبُ أن يُعرَفَ وقتَ البناء. لا نطقَ لنصٍّ يُركَّبُ لحظيًّا (انظرْ §7).
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/) فهي مُخرَجُ البناءِ الذي يُشحَن.
جرّبْنا medium واستقرّينا عليه: high يُضاعِفُ الحجمَ وزمنَ التوليدِ بفارقٍ لا يُدرِكُه طفلٌ عبرَ سمّاعةِ لوحيّة، وlow يُسمَعُ معدنيًّا.
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
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 فكانت النتيجةُ أسوأ.
يُطبَّقُ على نصِّ التركيبِ فقط والمفتاحُ يبقى الأصل:
-
إعادةُ ترتيبِ الشدّة: المحرّكاتُ تتوقّعُ «حرف + شدّة + حركة». وNFC يُنتِجُ أحيانًا «حرف + حركة + شدّة» فتُهمَلُ الشدّةُ وتُسمَعُ كالسكون (
أوّل→ «أَوْل»). - تجريدُ لفظِ الجلالة من الحركات.
- تاءُ التأنيثِ وقفًا (ة → ه) للمفردةِ المشكولة.
حين يكونُ النصُّ صحيحًا لغويًّا والنطقُ خاطئًا:
const SYNTH_FIX = {
"مُمْتَاز!": "مُمْتَااز!", // مدُّ الألفِ صراحةً (كان يُنطَقُ قصيرًا)
"اِنْتَهَى الوَقْت!…": "اِنْتَهَى الوَقَتُ.…", // «الوَقْت» كان يُسمَعُ «الوَخْت»
};المفتاحُ (والملفُّ) يبقى النصَّ الأصليَّ — فلا يتغيّرُ ما يُعرَضُ للمستخدمِ ولا تنكسرُ المطابقة. هذه أهمُّ خاصّيّةٍ في الآليّة.
إن استعصى النطقُ، غيّرِ العبارةَ نفسَها. أحيانًا يكونُ ذلك أصحَّ تربويًّا أيضًا.
توليدُ ~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مزلقانِ وقعْنا فيهما:
-
لا تنسَ
manifest[key] = fileعند التخطّي — وإلّا سقطتِ المقاطعُ الموجودةُ من الحزمة. -
صحّحْ شرطَ الخروج: كان
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.
const url = bundledFiles["tts-kareem" + " " + text]; // تطابقٌ بايتًا ببايتالنتيجةُ العمليّة: أيُّ نصٍّ يُركَّبُ لحظيًّا لا مقطعَ له.
// ❌ خطأٌ كلّفَنا إصدارًا كاملًا: أربعةُ نصوصٍ لكلٍّ مقطعُه،
// لكنّ السلسلةَ المدموجةَ نصٌّ جديدٌ بلا مقطع ⇒ ارتدادٌ لصوتِ النظام
roboSay(`${cheer} ${reaction} ${praise} ${tadabbur}`);
// ✅ صواب: كلُّ جزءٍ بمقطعِه، متتابعةً
roboSayChain([cheer, reaction, praise, tadabbur]);قائمةُ تحقّقٍ لأيِّ نصٍّ منطوقٍ جديد:
- هل هو مشكولٌ بالكامل؟
- هل مصدرُه يقرؤُه المولّد؟ (وليس سلسلةً مضمَّنةً في الشيفرة)
- هل شُغِّلَ
gen:piperبعد إضافتِه؟ - هل هو نصٌّ ثابتٌ لا مُركَّبٌ لحظيًّا؟
مزلقٌ في المولّد: ما يستوردُه
gen-piper.mjsيجبُ أن يخلوَ من DOM. استيرادُ وحدةِ واجهةٍ يجرُّ سلسلةً تنتهي بـvoices-bundled.jsonفيفشلُ في Node. ضعِ النصوصَ فيcontent/.
| الحالة | الحكم | البديل |
|---|---|---|
| جُمَلٌ ونصوصٌ وقصص | ✅ ممتاز | — |
| ردودُ شخصيّةٍ وحوار | ✅ ممتاز | — |
| حروفٌ مفردة | ❌ فاشل | espeak (أوضحُ للتعليم) أو تسجيلٌ بشريّ |
| مفرداتٌ معزولة | تسجيلٌ بشريٌّ إن توفّر | |
| تلاوةُ القرآن | ❌ لا يجوز | تسجيلُ قارئٍ مُجاز |
لماذا يفشلُ في الحروف؟ النموذجُ مُدرَّبٌ على كلامٍ متّصل؛ الوحدةُ المعزولةُ تخرجُ بحوافَّ مبتورةٍ وتشكيلٍ مطموس — وهو ما يحتاجُه الطفلُ بالضبط.
استراتيجيّتُنا: أولويّةٌ ذكيّة — بشريٌّ مسجَّل ← عصبيٌّ (Piper) ← آليٌّ (espeak) — لكلِّ نوعٍ على حدة (حرف/كلمة/جملة). فالعصبيُّ للجُمَل، وespeak يضمنُ تغطيةً كاملةً للحروف.
ffmpeg -i in.wav -codec:a libmp3lame -q:a 6 out.mp3-q:a 6 (~48kbps VBR) — الخيارُ الذي استقرّينا عليه بعدَ تجريب: كلامٌ واضحٌ بحجمٍ يسمحُ بشحنِ آلافِ المقاطع. (كلامٌ لا موسيقى، فالتردّداتُ العاليةُ غيرُ مهمّة.)
أرقامٌ من مشروعِنا: 1042 مقطعًا عربيًّا · 714 إنجليزيًّا · 1057 فرنسيًّا ≈ 9 ميغابايت.
استثناء: لا تضغطْ تلاوةَ القرآنِ — تُترَكُ بجودتِها الأصليّة.
- كلُّ نصٍّ منطوقٍ جديدٍ مشكولٌ ومُدرَجٌ في مصدرٍ يقرؤُه المولّد
-
npm run gen:piper(تفاضليٌّ فسريع) - صفرُ يتيم: ملفّاتُ القرصِ = مراجعُ البيان
- استماعٌ فعليٌّ للعباراتِ الجديدة — لا تفترضْ
-
npm run buildوحجمُ precache معقول
- Piper (rhasspy/piper) — المحرّك
- النماذج على Hugging Face
- espeak-ng — المُفنِّمُ خلفَ Piper
- في هذا المستودع:
audio-pronunciation.md·pitfalls-and-solutions.md·tools/gen-piper.mjs·tools/piper_synth.py
الرخص: Piper و espeak-ng — GPL-3.0 · النماذجُ لكلٍّ رخصتُه (راجِعْ بطاقةَ الصوتِ على Hugging Face) · هذا الدليل — GPL-3.0.
📖 هذا الويكي مرآةٌ مولَّدةٌ من GT-MK/docs/ في المستودع — حرّرِ الملفَّ هناك ثمّ شغّلْ bash tools/publish-wiki.sh.
رخصةُ المشروعِ والوثائق: GPL-3.0.
البداية
المرجعُ التقنيّ
الخبرةُ المتراكمة