# فاز ۱۴ — تشخیص گفتار (Web Speech Recognition): سطح L5 واقعی

**نسخه: 1.9.0** · تاریخ: 1404/07/03 (2026-09-25)
درخواست کاربر: «تشخیص گفتار (Web Speech Recognition) — کودک حرف را بلند بگوید، اپ بفهمد درست گفته — سطح L5 واقعی» با تأکید صریح بر دقت بالا و جزئیات دقیق و کامل.

---

## ۱. مفهوم آموزشی — پله‌ی نردبان تسلط که جا افتاده بود

مدل تسلط تا این فاز ۵ سطح داشت (L0..L4) که آخرینش «تولید نوشتاری» بود. اما در آموزش
الفبا به کودک، مرحله‌ی طبیعی بعد از تشخیص/یادآوری، **تولید گفتاری** است: کودک حرف را
**بلند بگوید** و بازخورد فوری بگیرد. این فاز همان پله را با تأیید واقعیِ ماشین اضافه می‌کند:

```
L4 تولید نوشتاری (ساخت واژه)
  └─ L5 تولید گفتاری: ≥۸۰٪ در ≥۵ تلاش «speech» — تشخیص گفتار تأیید کرده که کودک
     واقعاً حرف را بلند گفته (نه خوداظهاری، نه لمس)
```

قاعده‌ی طلایی مدل حفظ شد: **تنزل سطح وجود ندارد** — خطای گفتار فقط Leitner box را
صفر می‌کند (مرور زودتر). L5 فقط با `mode:'speech'` به‌دست می‌آید؛ ۱۰ تولیدِ نوشتاریِ
بی‌نقص هم کودک را به L5 نمی‌رساند (تست‌شده).

## ۲. معماری — سه لایه‌ی جدا

| لایه | فایل | مسئولیت | تست |
|---|---|---|---|
| موتور مرورگر | `core/speechEngine.js` | تنها نقطه‌ی تماس با `SpeechRecognition`: feature-detect، lang، start/stop، نگاشت خطاها (not-allowed / no-speech / network / …) | e2e با mock |
| منطق قضاوت | `core/speechMatch.js` | **خالص و بدون DOM**: نرمال‌سازی رونوشت + واریانت‌های پذیرفتنی + تطبیق روی alternatives | ۱۹ تست واحد |
| تجربه‌ی بازی | `games/sayLetter.js` | بازی «بلند بگو!» — ۵ دور، رونوشت زنده، بازخورد صادقانه | e2e ۲ استپ |

چرا این جدا‌سازی؟ تشخیص گفتارِ مرورگر «متن» برمی‌گرداند؛ همه‌ی قضاوتِ «درست/غلط» باید
بدون مرورگر تست‌پذیر باشد — همان اصل ماژول‌های خالص پروژه (wordText، stories، …).

## ۳. قواعد تطبیق — سخت‌گیرانه ولی منصفانه (قلب دقت)

**نرمال‌سازی** (`normalizeSpeech`): حرکت‌ها/تشدید/سکون/کشیده حذف · هم‌خانواده‌ها
(ي→ی، ك→ک، ة→ه، آ/أ/إ→ا، ئ/ى→ی، ؤ→و) · فاصله/نقطه‌گذاری همه‌ی زبان‌ها حذف ·
کوچک‌سازی **ترکی‌آگاه** (İ→i و I→ı — همان foldFor واژه‌سنجی).

**تطبیق**: رونوشتِ نرمال‌شده باید **دقیقاً برابر** یکی از شکل‌های پذیرفتنی باشد.
«شامل بودن» عمداً پذیرفته نیست — «بابا» شامل «ب» است ولی کودک «بِ» نگفته!
هر یک از **۵ alternatives** موتور تشخیص جدا امتحان می‌شود (دقت بالاتر با آوانویسی‌های
مختلف). خروجی همیشه «شنیدم: …» را برمی‌گرداند تا بازخورد به کودک صادقانه باشد.

**داده‌ی پک (Data-Driven مثل همه‌چیز در این پروژه)**: هر ۱۴۱ حرف در ۵ پک
`speechNames[]` گرفت — خود حرف + نام رسمی + تلفظ‌های رایج گفتاری:

- fa: ب = [ب، بِ، **به، بی**] · ا = [ا، اَلِف، **الف، الفت، الفا**] · ق = [ق، قاف، قه]
- en: B = [B، **bee، be**] · C = [C، **see، sea، si**] · W = [W، **double you، double-u، dubyu**]
- nl: g = [g، **gee، zjee، chee**] (گ حلقوی هلندی!) · y = [y، **ij، y-grec**]
- ar: ب = [ب، بِ، **باء، با، به**] · ط = [ط، **طاء، طا، طه**]
- tr: Ğ = [ğ، **yumuşak ge، yumuşak g**] · تفکیک ظریف ı/i حفظ می‌شود (I→ı)

`config.speechLang` هم به هر پک اضافه شد (fa-IR، en-US، nl-NL، ar-SA، tr-TR) و
نسخه‌ی پک به 1.1.0 بامپ شد تا کش کلاینت تازه شود.

## ۴. تجربه‌ی بازی (جزئیات کودک‌محور)

- **الگوی شنیداری اول**: نام حرف + صدایش پخش می‌شود («بشنو: بِ… بَ»)
- **میکروفون نبض‌دار**: حلقه‌های پالس + «می‌شنوم…» + **رونوشت زنده‌ی interim**
  (کودک می‌بیند ماشین دارد می‌شنود — لحظه‌ی جادویی)
- **درست**: «✨» + تشویق صوتی + نام حرف دوباره پخش می‌شود (تثبیت)
- **غلط**: «👂 شنیدم: پا» + «بِ را بگو!» — هیچ تحقیری نیست، فقط دوباره
- **۳ تلاش ناموفق** → افشای مهربانانه: «این حرف «بِ» بود — با هم بگوییم!» و رد می‌شود
- **سکوت** (no-speech) تلاش حساب نمی‌شود — کودکی که حرف نزده خطا نگرفته
- هر **گفته‌ی واقعی** یک `recordAttempt(mode:'speech')` است → همان قاعده‌ی ۸۰٪/۵تا

**Degrade محترمانه** (تست‌شده در e2e):
- مرورگر بدون SpeechRecognition (Firefox) → نمای «🙊 میکروفون در این مرورگر در دسترس
  نیست» + راهنما؛ بقیه‌ی بازی‌ها سالم‌اند
- اجازه‌ی میکروفون نداده → «🎤 اجازه‌ی میکروفون داده نشد» + راهنمای 🔒 نوار آدرس
- iframe سندباکس‌شده (پیش‌نمایش) → همین مسیر محترمانه (SecurityError نگاشت می‌شود)

## ۵. تغییرات ریز اما مهم در بقیه‌ی سیستم

| فایل | تغییر |
|---|---|
| `engine/masteryTracker.js` | `LEVELS.SPOKEN: 5` + قانون ارتقا (فقط mode speech) + `lettersSpoken()` |
| `engine/scoring.js` | نوع نشان `letters_spoken` |
| داده‌ی ۵ پک | `speechNames` ×۱۴۱ + `speechLang` + نشان `{pack}_speech_5` (🎙️ آستانه ۵) |
| `server/routes/children.js` | پخش تسلط گزارش تا `l5` |
| `ui/components/reportCard.js` | legend و نوار تسلط تا L5 (رنگ بنفش #9B51E0) |
| `core/reportModel.js` | مقیاس ضعف از /4 به /5 |
| `main.js` | ثبت بازی `say-letter` (requiredLevel 2 — بعد از تسلط، مثل داستان‌ها) |
| CSS | `app.css` = ادغام theme+components (یک RTT کمتر — Lighthouse ۸۵ پایدار) |
| i18n ×۵ | ۱۷ کلید (بازی + speech.* + rp.level5 + برچسب نشان) |

## ۶. حریم خصوصی و مرورگرها (شفافیت با والدین)

- Chrome/Edge برای تشخیص، صدا را به سرویس ابری خودشان می‌فرستند (نیاز به اینترنت) —
  در `speech.needNet` و این سند مستند شد. Safari پردازش را روی دستگاه انجام می‌دهد.
- هیچ صدایی روی سرور الفباز ذخیره نمی‌شود؛ فقط «متن شنیده‌شده» (≤۴۰ نویسه) در
  رویداد تلاش می‌آید — همان چیزی که هر بازی درباره‌ی لمس‌ها ذخیره می‌کند.
- Firefox: تشخیص گفتار ندارد → بازی با پیام روشن غیرفعال می‌شود.

## ۷. تست‌ها — همه سبز

| سوئیت | نتیجه |
|---|---|
| `tests/speech.test.js` (جدید) | ✅ 19/19 — نرمال‌سازی (حرکت/ي/ك/ة/ترکی) · داده‌ی ۱۴۱ حرف · تطبیق درست/غلط/alternatives/تفکیک‌ها · L5 (۸۰٪/۵تا، فقط speech، تنزل‌ناپذیر) · نشان · i18n · donut l5 |
| `tests/api.test.js` (+۱) | ✅ 36/36 — حرف mastery=5 در سطل l5 گزارش |
| E2E اپ (+۲ استپ) | ✅ 24/24 — بازی کامل با **mock واقعی‌نمای SpeechRecognition** (رونوشت اسکریپت‌شده): گفته‌ی غلط → «شنیدم»، ۵ دور، **fa_alef واقعاً به L5 ارتقا می‌یابد** (۶ تلاش speech، ۵ درست) + نمای «میکروفون نیست» در مرورگر بدون SR |
| مجموع واحد | ✅ 199/199 در ۱۲ سوئیت |
| e2e-packs / e2e-admin | ✅ 6/6 · 11/11 |
| Lighthouse | ✅ میانه‌ی ۳ اجرا: کارایی ۸۵ · a11y ۱۰۰ · BP ۱۰۰ · SEO ۱۰۰ |

**درس‌های e2e این فاز** (چهار باگِ تست شکار کرد):
1. mock نتیجه را بدون `isFinal:true` می‌داد → موتور فکر می‌کرد interim است و قضاوت
   هرگز رخ نمی‌داد (شکل دقیق `SpeechRecognitionResult` مهم است)
2. `browser.newPage()` در Playwright **context ایزوله** می‌سازد (localStorage خالی!) —
   برای صفحه‌ی دومِ هم‌storage باید از همان context بود؛ راه‌حل نهایی: init script دوم
   روی همان صفحه + reload
3. «مرورگر SR ندارد» با console.error ثبت می‌شد و گیت «بدون خطای کنسول» e2e را
   می‌کشت — وضعیت محیطی ≠ خطا (سطح لاگ اصلاح شد)
4. شمارش تلاش‌ها در انتظار تست اشتباه بود: ۵ دور + ۱ غلطِ دور اول = ۶ تلاش (۸۳٪)

## ۸. نکته‌ی استقرار

- برای تست واقعی گفتار (نه mock): Chrome/Edge + HTTPS یا localhost + اجازه‌ی میکروفون.
  در پیش‌نمایشِ داخل iframe، مرورگر اجازه نمی‌دهد → مسیر «میکروفون نیست» عمداً فعال است؛
  برای تجربه‌ی واقعی، اپ را در تب جدید باز کنید.
- بعد از تغییر importهای main.js: `node scripts/gen-preloads.mjs` (فاز ۱۳) — انجام شد (۵۵ ماژول).
