File size: 20,164 Bytes
5125634
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
#!/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'<a href="{m["hf"]}" target="_blank" rel="noopener">{html.escape(m["name"])}</a>' if m["hf"] else f'<b>{html.escape(m["name"])}</b>'
        ours = " class=\"ours\"" if m.get("ours") else ""
        rows.append(
            f"<tr{ours}><td dir=\"auto\">{name}</td><td>{m['params']}</td>"
            f"<td class=\"main\">{pct(r['mean_cer_norm'])}</td><td>{pct(r['mean_cer'])}</td>"
            f"<td>{pct(r['mean_wer'])}</td><td>{r['mean_sim']:.3f}</td><td>{m['rtf']:.2f}</td>"
            f"<td dir=\"auto\"><a href=\"{m['lic_url']}\" target=\"_blank\" rel=\"noopener\">{html.escape(m['lic'])}</a></td></tr>"
        )
    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"""<div class="player">
  <div class="ph"><span class="pn">{html.escape(m['short'])}</span><span class="tag {badge_class(r['cer_norm'])}">{badge_text(r['cer_norm'])}</span></div>
  <audio controls preload="none" src="audio/{key}/sample_{i:02d}.wav"></audio>
</div>"""
            )
        hyp_rows = "\n".join(
            f"<tr><td>{html.escape(BY_KEY[k]['short'])}</td><td dir=\"rtl\">{html.escape(METRICS[k]['samples'][i]['hyp'])}</td></tr>"
            for k in ROW_ORDER
        )
        cards.append(
            f"""<article class="sentence">
  <div class="sh"><span class="num">{i + 1:02d}</span><span class="cat">{CATEGORIES[i]}</span></div>
  <p class="st">{html.escape(text)}</p>
  <div class="players">{''.join(players)}</div>
  <details class="hyp"><summary>ما سمعه Whisper من كل نظام</summary>
    <table><thead><tr><th>النظام</th><th>النص المُلتقط</th></tr></thead><tbody>{hyp_rows}</tbody></table>
  </details>
</article>"""
        )
    return "\n".join(cards)


def popular_table() -> str:
    rows = []
    for repo, name, params, dl, likes, ar, lic, tested in POPULAR:
        mark = '<span class="yes">✓</span>' if tested else '<span class="no">—</span>'
        rows.append(
            f"<tr><td dir=\"auto\"><a href=\"https://huggingface.co/{repo}\" target=\"_blank\" rel=\"noopener\">{html.escape(name)}</a></td>"
            f"<td>{params}</td><td dir=\"ltr\">{dl}</td><td dir=\"ltr\">{likes}</td><td>{ar}</td><td dir=\"auto\">{html.escape(lic)}</td><td>{mark}</td></tr>"
        )
    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"""<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bayan (صوت مراد) مقابل النماذج المشهورة على Hugging Face</title>
<style>{CSS}</style>
</head>
<body>
<div class="wrap">

<header>
  <p class="eyebrow">مقارنة مفتوحة · 5 جمل فصحى · 5 أنظمة</p>
  <h1>Bayan (صوت مراد) مقابل النماذج المشهورة على Hugging Face</h1>
  <p class="sub">نفس النصوص، نفس البطاقة الصوتية، ونفس أدوات القياس. لا نختار الجمل التي ننجح فيها فقط،
  ونعرض أيضاً ما لم يكن في صالحنا: التعادل النصي مع Chatterbox، وتفوق XTTS في السرعة.</p>
  <div class="badges">
    <span class="badge"><b>5</b> جمل من اختبار الـ32</span>
    <span class="badge"><b>Whisper</b> large-v3</span>
    <span class="badge"><b>TitaNet</b> لتشابه الصوت</span>
    <span class="badge"><b>RX 6700 XT</b> · ROCm</span>
    <span class="badge">آخر تحديث: <b>21 أكتوبر 2026</b></span>
  </div>
</header>

<section>
  <h2>الخلاصة في أربع بطاقات</h2>
  <div class="grid g4">
    <div class="card"><div class="metric">{pct(bayan['mean_cer_norm'])}</div><div class="label">CER* لـBayan — 5/5 بلا خطأ نطقي</div><div class="note">بعد تطبيع الأرقام والوحدات</div></div>
    <div class="card"><div class="metric">{bayan['mean_sim']:.3f}</div><div class="label">تشابه الصوت مع مراد — الأعلى</div><div class="note">Chatterbox 0.830 · XTTS 0.718</div></div>
    <div class="card"><div class="metric blue">{pct(chatter['mean_cer_norm'])}</div><div class="label">CER* لـChatterbox — تعادل نصي</div><div class="note">أقوى منافس مفتوح في هذه العينة</div></div>
    <div class="card"><div class="metric blue">0.31</div><div class="label">RTF لـXTTS — الأسرع بين الجودة العالية</div><div class="note">Bayan 0.99 · Chatterbox 1.06</div></div>
  </div>
</section>

<section>
  <h2>النتائج المجمّعة</h2>
  <div class="scroll">
    <table>
      <thead><tr><th>النظام</th><th>المعاملات</th><th>CER* ↓</th><th>CER خام (سياقي)</th><th>WER ↓</th><th>التشابه ↑</th><th>RTF ↓</th><th>الترخيص</th></tr></thead>
      <tbody>
        {aggregate_table()}
      </tbody>
    </table>
  </div>
  <p class="note"><b>CER*</b> هو المقياس العادل للنطق: يستبدل الأرقام والوحدات برمز واحد في الطرفين. السبب أن Whisper يكتب «110 كم» بينما النص «مئة وعشرة كيلومترات»؛ لذلك يبدو CER الخام أسوأ مما هو. Bayan وChatterbox كلاهما 0.00% بعد التطبيع.</p>
</section>

<section>
  <h2>استمع وقارن — الجمل الخمس</h2>
  <p class="note" style="margin-top:-4px">الترتيب: Bayan، Chatterbox، XTTS-v2، MMS، ثم MagpieTTS الأصلي بصوت Jason. كل المشغلات موحّدة الجهارة (−20 LUFS) لتحكيم عادل؛ القياسات على الملفات الخام.</p>
  {sentence_cards()}
</section>

<section>
  <h2>خريطة النماذج المشهورة على Hugging Face</h2>
  <div class="scroll">
    <table>
      <thead><tr><th>النموذج</th><th>المعاملات</th><th>التنزيلات</th><th>الإعجابات</th><th>العربية</th><th>الترخيص</th><th>جُرّب؟</th></tr></thead>
      <tbody>
        {popular_table()}
      </tbody>
    </table>
  </div>
  <p class="note">الأرقام لحظة 21 أكتوبر 2026 وقد تتغير. «جُرّب هنا» تعني أننا ولّدنا به نفس الجمل الخمس؛ غيره مذكور للسياق فقط. Kokoro وQwen3-TTS وVibeVoice وCSM لا تدعم العربية، وF5-TTS أساسه إنجليزي/صيني.</p>
</section>

<section>
  <h2>لماذا هذه النماذج تحديداً؟</h2>
  <div class="grid" style="grid-template-columns:repeat(auto-fit,minmax(240px,1fr))">
    <div class="card"><b>Chatterbox</b><div class="note">0.5B، MIT، 23 لغة، استنساخ فوري. أوزان v2 المتاحة عبر الحزمة 0.1.7. أقوى منافس في هذه العينة: 0.00% CER* وتشابه 0.830.</div></div>
    <div class="card"><b>XTTS-v2</b><div class="note">~467M، 17 لغة، استنساخ من 6 ثوانٍ. الأسرع بين النماذج عالية الجودة (RTF 0.31)، لكنه غير تجاري (CPML) وارتكب خطأين بسيطين.</div></div>
    <div class="card"><b>MMS-TTS Arabic</b><div class="note">~36M فقط من Meta، بلا استنساخ، وأسرع نظام (RTF 0.03). جودته أقل بوضوح: WER 15.8% وتشابه 0.407.</div></div>
    <div class="card"><b>MagpieTTS الأصلي</b><div class="note">النموذج الأساسي بصوت Jason. يوضح أن الفرق ليس في النص فقط: التشابه مع مراد 0.299 مقابل 0.867 لنسختنا.</div></div>
  </div>
</section>

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

<section>
  <h2>الحكم النهائي</h2>
  <div class="callout">
    <b>Bayan ليس الأفضل في كل شيء، لكنه الأفضل في ما صُنع من أجله:</b> بصمة صوت «مراد» المصرية
    (0.867)، وفصحى سليمة 5/5 بعد تطبيع الأرقام، بحجم 357M يعمل محلياً داخل NeMo. في المقابل،
    <b>Chatterbox</b> منافس قوي جداً للفصحى (0.00% CER*، تشابه 0.830، رخصة MIT)، و<b>XTTS-v2</b>
    أسرع (0.31) لكنه غير تجاري وأقل دقة قليلاً. <b>MMS</b> خفيف وسريع لكنه ليس في نفس المستوى،
    و<b>Jason</b> يثبت أن الصوت الأصلي بعيد عن هدفنا.
  </div>
</section>

<footer>
  <div>القياسات الخام: <a href="metrics.json" style="color:var(--accent2)">metrics.json</a> · <a href="results.csv" style="color:var(--accent2)">results.csv</a> · النصوص: <a href="texts.txt" style="color:var(--accent2)">texts.txt</a></div>
  <div class="links">
    <a href="../model_card.html" style="color:var(--accent2)">بطاقة النموذج</a>
    <a href="../README.md" style="color:var(--accent2)">README</a>
    <a href="../eval/msa_32/report.html" style="color:var(--accent2)">اختبار الـ32 جملة</a>
    <a href="../compare/index.html" style="color:var(--accent2)">مقارنة النص الطويل</a>
  </div>
  <div style="margin-top:10px">الصفحة مولّدة آلياً من <code>scripts/make_page.py</code>. لا تُستخدم عينات النماذج الأخرى خارج نطاق المقارنة دون مراجعة تراخيصها.</div>
</footer>

</div>
</body>
</html>
"""
    (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())