#!/usr/bin/env python3 """Build compare_models/index.html from metrics.json and the audio folders.""" from __future__ import annotations import html import json from pathlib import Path ROOT = Path(__file__).resolve().parent.parent METRICS = json.loads((ROOT / "metrics.json").read_text(encoding="utf-8")) TEXTS = [ln.strip() for ln in (ROOT / "texts.txt").read_text(encoding="utf-8").splitlines() if ln.strip()] CATEGORIES = ["أخبار", "علوم", "سؤال", "أرقام", "فصحى طويلة"] MODELS = [ dict(key="bayan", name="Bayan (صوت مراد)", short="Bayan", params="357M", arabic="فصحى + مصرية", clone="مدمج (تدريب)", lic="NVIDIA Open", lic_url="../NOTICE.md", rtf=0.99, hf="", tested=True, ours=True), dict(key="chatterbox", name="Chatterbox Multilingual v2", short="Chatterbox", params="0.5B", arabic="23 لغة", clone="zero-shot", lic="MIT", lic_url="https://huggingface.co/ResembleAI/chatterbox", rtf=1.06, hf="https://huggingface.co/ResembleAI/chatterbox", tested=True), dict(key="xtts", name="XTTS-v2", short="XTTS-v2", params="~467M", arabic="17 لغة", clone="zero-shot", lic="CPML (غير تجاري)", lic_url="https://coqui.ai/cpml", rtf=0.31, hf="https://huggingface.co/coqui/XTTS-v2", tested=True), dict(key="mms", name="MMS-TTS Arabic", short="MMS", params="~36M", arabic="فصحى", clone="لا", lic="CC-BY-NC 4.0", lic_url="https://huggingface.co/facebook/mms-tts-ara", rtf=0.03, hf="https://huggingface.co/facebook/mms-tts-ara", tested=True), dict(key="magpie", name="MagpieTTS الأصلي (Jason)", short="MagpieTTS", params="357M", arabic="فصحى + مصرية", clone="أصوات مدمجة", lic="NVIDIA Open", lic_url="https://huggingface.co/nvidia/magpie_tts_multilingual_357m", rtf=1.03, hf="https://huggingface.co/nvidia/magpie_tts_multilingual_357m", tested=True), ] BY_KEY = {m["key"]: m for m in MODELS} ROW_ORDER = ["bayan", "chatterbox", "xtts", "mms", "magpie"] POPULAR = [ ("hexgrad/Kokoro-82M", "Kokoro-82M", "82M", "10.5M", "7,243", "لا — 8 لغات", "Apache-2.0", False), ("coqui/XTTS-v2", "XTTS-v2", "~467M", "6.3M", "3,901", "نعم — 17 لغة", "CPML", True), ("Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice", "Qwen3-TTS 1.7B", "1.7B", "2.0M", "2,154", "لا — 10 لغات", "Apache-2.0", False), ("ResembleAI/chatterbox", "Chatterbox Multilingual", "0.5B", "1.77M", "1,858", "نعم — 23 لغة", "MIT", True), ("openbmb/VoxCPM2", "VoxCPM2", "2B", "902k", "1,705", "نعم — 30 لغة", "Apache-2.0", False), ("SWivid/F5-TTS", "F5-TTS", "336M", "887k", "1,219", "لا رسمياً — en/zh", "CC-BY-NC 4.0", False), ("microsoft/VibeVoice-1.5B", "VibeVoice-1.5B", "1.5B", "483k", "2,502", "لا — en/zh", "MIT", False), ("sesame/csm-1b", "CSM-1B", "1B", "129k", "2,481", "لا — إنجليزي", "Apache-2.0", False), ("nvidia/magpie_tts_multilingual_357m", "MagpieTTS 357M", "357M", "10.7k", "231", "نعم", "NVIDIA Open", True), ("facebook/mms-tts-ara", "MMS-TTS Arabic", "~36M", "6.5k", "24", "نعم", "CC-BY-NC 4.0", True), ("fishaudio/fish-speech-1.5", "Fish Speech 1.5", "~0.5B", "3.9k", "779", "نعم — 13 لغة", "CC-BY-NC-SA", False), ] def pct(x: float) -> str: return f"{x * 100:.2f}%" def badge_class(cer_norm: float) -> str: if cer_norm < 0.005: return "ok" if cer_norm < 0.03: return "warn" return "bad" def badge_text(cer_norm: float) -> str: if cer_norm < 0.005: return "مطابق" return f"CER* {cer_norm * 100:.1f}%" def aggregate_table() -> str: rows = [] for key in ROW_ORDER: m = BY_KEY[key] r = METRICS[key] name = f'{html.escape(m["name"])}' if m["hf"] else f'{html.escape(m["name"])}' ours = " class=\"ours\"" if m.get("ours") else "" rows.append( f"{name}{m['params']}" f"{pct(r['mean_cer_norm'])}{pct(r['mean_cer'])}" f"{pct(r['mean_wer'])}{r['mean_sim']:.3f}{m['rtf']:.2f}" f"{html.escape(m['lic'])}" ) return "\n".join(rows) def sentence_cards() -> str: cards = [] for i, text in enumerate(TEXTS): players = [] for key in ROW_ORDER: m = BY_KEY[key] r = METRICS[key]["samples"][i] players.append( f"""
{html.escape(m['short'])}{badge_text(r['cer_norm'])}
""" ) hyp_rows = "\n".join( f"{html.escape(BY_KEY[k]['short'])}{html.escape(METRICS[k]['samples'][i]['hyp'])}" for k in ROW_ORDER ) cards.append( f"""
{i + 1:02d}{CATEGORIES[i]}

{html.escape(text)}

{''.join(players)}
ما سمعه Whisper من كل نظام {hyp_rows}
النظامالنص المُلتقط
""" ) return "\n".join(cards) def popular_table() -> str: rows = [] for repo, name, params, dl, likes, ar, lic, tested in POPULAR: mark = '✓' if tested else '—' rows.append( f"{html.escape(name)}" f"{params}{dl}{likes}{ar}{html.escape(lic)}{mark}" ) return "\n".join(rows) CSS = """ :root{--bg:#0b1020;--panel:#141b31;--panel2:#1b2440;--text:#e8ecf7;--muted:#9aa7c7; --accent:#5ee0b5;--accent2:#7aa2ff;--line:#26304d;--warn:#ffcf6e;--bad:#ff7a90} *{box-sizing:border-box} body{margin:0;background:radial-gradient(1200px 600px at 80% -10%,#1d2a55 0%,var(--bg) 55%); color:var(--text);font-family:"Noto Naskh Arabic","Segoe UI",Tahoma,Arial,sans-serif;line-height:1.8} a,a:visited{color:var(--accent2);text-decoration:none}a:hover{text-decoration:underline} .wrap{max-width:1100px;margin:0 auto;padding:40px 22px 70px} header{border:1px solid var(--line);background:linear-gradient(160deg,#182242,#101830 70%); border-radius:22px;padding:34px 30px;position:relative;overflow:hidden} header:before{content:"";position:absolute;inset:auto -60px -120px auto;width:320px;height:320px; background:radial-gradient(circle,#5ee0b544,transparent 65%);filter:blur(10px)} .eyebrow{color:var(--accent);font-size:13px;letter-spacing:.4px;margin:0 0 8px} h1{margin:0 0 8px;font-size:clamp(27px,4vw,42px);letter-spacing:-.5px} .sub{color:var(--muted);margin:0 0 18px;font-size:17.5px;max-width:850px} .badges{display:flex;flex-wrap:wrap;gap:8px;position:relative;z-index:1} .badge{border:1px solid var(--line);background:#0f1730;color:var(--muted);padding:5px 12px;border-radius:999px;font-size:13px} .badge b{color:var(--text);font-weight:600} section{margin-top:34px} h2{font-size:22px;margin:0 0 14px;display:flex;align-items:center;gap:10px} h2:before{content:"";width:7px;height:22px;border-radius:4px;background:linear-gradient(180deg,var(--accent),var(--accent2))} .grid{display:grid;gap:14px} .g4{grid-template-columns:repeat(auto-fit,minmax(190px,1fr))} .card{background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:18px} .metric{font-size:30px;font-weight:700;color:var(--accent);letter-spacing:-.5px} .metric.blue{color:var(--accent2)} .label{color:var(--muted);font-size:14px} .note{color:var(--muted);font-size:13.5px;margin-top:8px} .scroll{overflow-x:auto;border:1px solid var(--line);border-radius:16px;background:var(--panel)} table{width:100%;border-collapse:collapse;font-size:14.5px;min-width:760px} th,td{padding:11px 12px;border-bottom:1px solid var(--line);text-align:right;white-space:normal} th{color:var(--muted);font-weight:600;font-size:12.5px;background:#111a33} tr:last-child td{border-bottom:none} td.main{color:var(--accent);font-weight:700;white-space:nowrap} tr.ours td{background:#123a3055} .sentence{background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:16px 18px;margin:14px 0} .sh{display:flex;align-items:center;gap:10px;margin-bottom:4px} .num{color:var(--muted);font-size:13px;font-variant-numeric:tabular-nums} .cat{font-size:12px;color:var(--accent2);border:1px solid #2a3a66;background:#151f3a;border-radius:999px;padding:1px 10px} .st{font-size:16px;margin:4px 0 12px} .players{display:grid;grid-template-columns:repeat(auto-fit,minmax(185px,1fr));gap:10px} .player{background:var(--panel2);border:1px solid var(--line);border-radius:12px;padding:10px 11px} .ph{display:flex;align-items:center;justify-content:space-between;gap:6px;margin-bottom:2px} .pn{font-size:13.5px;font-weight:600} .tag{font-size:11px;border-radius:999px;padding:1px 8px;border:1px solid var(--line);color:var(--muted)} .tag.ok{color:var(--accent);border-color:#1f5a4a;background:#123a30} .tag.warn{color:var(--warn);border-color:#5a4a1f;background:#3a3012} .tag.bad{color:var(--bad);border-color:#5a1f2a;background:#3a121a} audio{width:100%;height:34px;margin-top:4px;filter:invert(.92) hue-rotate(180deg)} details.hyp{margin-top:10px;color:var(--muted);font-size:13px} details.hyp summary{cursor:pointer;color:var(--accent2)} details.hyp table{min-width:0;margin-top:8px;font-size:13px} details.hyp th,details.hyp td{white-space:normal;padding:6px 8px} .yes{color:var(--accent);font-weight:700}.no{color:var(--muted)} .callout{background:var(--panel2);border:1px solid var(--line);border-radius:14px;padding:16px 18px;font-size:14.5px;color:var(--muted)} .callout b{color:var(--text)} ul{margin:8px 0 0;padding-right:20px;color:var(--muted)}li{margin:6px 0} footer{margin-top:44px;color:var(--muted);font-size:13px;border-top:1px solid var(--line);padding-top:18px} .links{display:flex;gap:16px;flex-wrap:wrap;margin-top:8px} @media(max-width:640px){.players{grid-template-columns:1fr 1fr}.st{font-size:15px}} @media(max-width:420px){.players{grid-template-columns:1fr}} """ def main() -> int: bayan = METRICS["bayan"] chatter = METRICS["chatterbox"] xtts = METRICS["xtts"] html_doc = f""" Bayan (صوت مراد) مقابل النماذج المشهورة على Hugging Face

مقارنة مفتوحة · 5 جمل فصحى · 5 أنظمة

Bayan (صوت مراد) مقابل النماذج المشهورة على Hugging Face

نفس النصوص، نفس البطاقة الصوتية، ونفس أدوات القياس. لا نختار الجمل التي ننجح فيها فقط، ونعرض أيضاً ما لم يكن في صالحنا: التعادل النصي مع Chatterbox، وتفوق XTTS في السرعة.

5 جمل من اختبار الـ32 Whisper large-v3 TitaNet لتشابه الصوت RX 6700 XT · ROCm آخر تحديث: 21 أكتوبر 2026

الخلاصة في أربع بطاقات

{pct(bayan['mean_cer_norm'])}
CER* لـBayan — 5/5 بلا خطأ نطقي
بعد تطبيع الأرقام والوحدات
{bayan['mean_sim']:.3f}
تشابه الصوت مع مراد — الأعلى
Chatterbox 0.830 · XTTS 0.718
{pct(chatter['mean_cer_norm'])}
CER* لـChatterbox — تعادل نصي
أقوى منافس مفتوح في هذه العينة
0.31
RTF لـXTTS — الأسرع بين الجودة العالية
Bayan 0.99 · Chatterbox 1.06

النتائج المجمّعة

{aggregate_table()}
النظامالمعاملاتCER* ↓CER خام (سياقي)WER ↓التشابه ↑RTF ↓الترخيص

CER* هو المقياس العادل للنطق: يستبدل الأرقام والوحدات برمز واحد في الطرفين. السبب أن Whisper يكتب «110 كم» بينما النص «مئة وعشرة كيلومترات»؛ لذلك يبدو CER الخام أسوأ مما هو. Bayan وChatterbox كلاهما 0.00% بعد التطبيع.

استمع وقارن — الجمل الخمس

الترتيب: Bayan، Chatterbox، XTTS-v2، MMS، ثم MagpieTTS الأصلي بصوت Jason. كل المشغلات موحّدة الجهارة (−20 LUFS) لتحكيم عادل؛ القياسات على الملفات الخام.

{sentence_cards()}

خريطة النماذج المشهورة على Hugging Face

{popular_table()}
النموذجالمعاملاتالتنزيلاتالإعجاباتالعربيةالترخيصجُرّب؟

الأرقام لحظة 21 أكتوبر 2026 وقد تتغير. «جُرّب هنا» تعني أننا ولّدنا به نفس الجمل الخمس؛ غيره مذكور للسياق فقط. Kokoro وQwen3-TTS وVibeVoice وCSM لا تدعم العربية، وF5-TTS أساسه إنجليزي/صيني.

لماذا هذه النماذج تحديداً؟

Chatterbox
0.5B، MIT، 23 لغة، استنساخ فوري. أوزان v2 المتاحة عبر الحزمة 0.1.7. أقوى منافس في هذه العينة: 0.00% CER* وتشابه 0.830.
XTTS-v2
~467M، 17 لغة، استنساخ من 6 ثوانٍ. الأسرع بين النماذج عالية الجودة (RTF 0.31)، لكنه غير تجاري (CPML) وارتكب خطأين بسيطين.
MMS-TTS Arabic
~36M فقط من Meta، بلا استنساخ، وأسرع نظام (RTF 0.03). جودته أقل بوضوح: WER 15.8% وتشابه 0.407.
MagpieTTS الأصلي
النموذج الأساسي بصوت Jason. يوضح أن الفرق ليس في النص فقط: التشابه مع مراد 0.299 مقابل 0.867 لنسختنا.

المنهجية والعدالة

  • النصوص: خمس جمل من اختبار الـ32 الحصري (فهارس 0، 1، 8، 19، 30): أخبار، علوم، سؤال، أرقام، وجملة فصحى طويلة. مشكّلة بالكامل.
  • Bayan: 357M إجمالاً، دُرّب منها 13.9M فقط (LoRA + صف صوت) ثم دُمجت في الملف المنشور.
  • الاستنساخ: XTTS وChatterbox استلما نفس المقطع المرجعي (9.04 ثانية) من بيانات مراد. Bayan نفسه تدرب على 71.6 دقيقة — عدم تماثل واضح، وليس في مصلحة المنافسين.
  • الإعدادات: XTTS وChatterbox وMMS على إعداداتها الافتراضية؛ Bayan على cfg 2.5 / temperature 0.6 / top-k 80 / seed 1234. لم نُعد ضبط أي نموذج خصيصاً لهذه الجمل.
  • القياس: Whisper large-v3 للـCER/WER بعد تطبيع عربي (إزالة التشكيل والترقيم وتوحيد الألف والتاء المربوطة). التشابه = cosine بين TitaNet ومتوسط بصمة مراد من 12 مقطعاً تدريبياً.
  • RTF: زمن التوليد ÷ زمن الصوت، بدون تحميل النموذج، على RX 6700 XT عبر ROCm 6.3. الأرقام في الجدول من تشغيل الـ5 جمل (وBayan/Jason من تشغيل الـ32).
  • العينات: XTTS وChatterbox يخرجان 24 kHz، وMMS 16 kHz، وBayan 22.05 kHz. وُحّدت كلها إلى 22.05 kHz في مشغلات الصفحة فقط؛ القياسات على الملفات الأصلية.
  • الشفافية: أخطاء XTTS: «للمتجددته» و«الميتوكندرية». MMS: «ألن»، «الميت كوندريا»، «بدقا»، «الساعد»، وجملة طويلة مشوّشة. Chatterbox: لا أخطاء نصية. Bayan: لا أخطاء بعد تطبيع الأرقام.

الحكم النهائي

Bayan ليس الأفضل في كل شيء، لكنه الأفضل في ما صُنع من أجله: بصمة صوت «مراد» المصرية (0.867)، وفصحى سليمة 5/5 بعد تطبيع الأرقام، بحجم 357M يعمل محلياً داخل NeMo. في المقابل، Chatterbox منافس قوي جداً للفصحى (0.00% CER*، تشابه 0.830، رخصة MIT)، وXTTS-v2 أسرع (0.31) لكنه غير تجاري وأقل دقة قليلاً. MMS خفيف وسريع لكنه ليس في نفس المستوى، وJason يثبت أن الصوت الأصلي بعيد عن هدفنا.
""" (ROOT / "index.html").write_text(html_doc, encoding="utf-8") print("wrote", ROOT / "index.html", len(html_doc)) return 0 if __name__ == "__main__": raise SystemExit(main())