---
url: https://docs.nemo12.com/reference/dictation-game.md
description: >-
  Sân luyện chép chính tả learn.nemo12.com/dictation: chọn bậc, chủ đề, số câu,
  giọng; Cloudflare Workflow sinh đoạn và tiếng (02.09.2026).
---

# Sân luyện chép chính tả (SRC-669)

Chỉ đạo 2026-09-02: learner chọn **bậc 1-5**, chọn một **chủ đề** (phim · thương hiệu · vĩ nhân ·
quốc gia · sách), chọn **3-10 câu** và **giọng đọc**, bấm một nút; **Cloudflare Workflow** sinh đoạn
tiếng Anh rồi sinh tiếng, lưu kèm metadata, và learner luyện trên đó.

**Độc lập hoàn toàn** với [bài nghe trong lesson](dictation.md). Chung bộ nghe-gõ-chấm và chung bộ
đọc, khác nhau ở nguồn ngữ liệu và ở chủ sở hữu: bài trong lesson là ngữ liệu đã duyệt mọi learner
thấy như nhau; bài ở đây do chính learner đặt sinh ra và chỉ mình họ thấy.

## Năm bậc đo ĐỘ KHÓ NGHE, không đo chủ đề

Một đoạn về vật lý lượng tử viết bằng câu bảy từ vẫn dễ chép hơn một đoạn về bữa sáng viết bằng câu
hai mươi lăm từ có ba mệnh đề. Xếp bậc theo chủ đề thì learner chọn bậc 5 vì thấy nó "người lớn" rồi
vỡ trận ở chỗ không ai lường.

| Bậc | Từ/câu (mục tiêu) | Dung sai nhẹ | Cú pháp | Tốc độ |
| --- | --- | --- | --- | --- |
| 1 | 6-9 | ±2 → 4-11 | một mệnh đề, từ vựng đời thường | 0,85× |
| 2 | 9-12 | ±2 → 7-14 | có and/but/because | 0,92× |
| 3 | 12-16 | ±2 → 10-18 | một mệnh đề phụ | 1× |
| 4 | 16-20 | ±3 → 13-23 | từ học thuật, bị động | 1,05× |
| 5 | 20-26 | ±4 → 16-30 | nhiều mệnh đề lồng, có tên riêng | 1,15× |

Bảng này là **một nguồn** (`workers/api/src/modules/dictation/levels.ts`): prompt gửi model đọc nó,
TTS đọc nó, và bảng giải thích sau nút **?** trên màn hình cũng đọc nó. Ba chỗ chép tay ba bản là ba
chỗ để lệch.

### Số từ là MỤC TIÊU, không phải lời hứa từng từ

Màn hình nói với learner là "câu **khoảng** 20-26 từ", và chữ *khoảng* ấy có thật sau lưng nó.

**Model không đếm được từ.** Đo ngày 2026-09-03 bằng binding AI thật (`wrangler dev --remote`, không
deploy gì): xin bậc 5 thì Llama 3.3 trả về câu 15-23 từ, và **thử lại kèm lời chê không hội tụ** —
số lỗi nhảy 4 → 6 → 4 qua ba lượt. Bắt ±1 từ nghĩa là bậc 4-5 gần như không bao giờ sinh được bài.

Nên cổng kiểm chia làm ba mức, theo đúng thứ nó đáng bảo vệ:

| Mức | Xử lý | Vì sao |
| --- | --- | --- |
| Thiếu dấu chấm cuối, khoảng trắng thừa | **Tự dọn** | Learner không bao giờ thấy dấu câu (bộ chấm bỏ hết) và TTS đọc y nhau. Từ chối vì một dấu chấm là đốt ba lượt gọi model cho một ký tự mình tự thêm được — đo thật: **4/4 lượt trượt chỉ vì lý do này** |
| Lệch nhẹ khỏi biên, tới ⅓ số câu | **Bỏ qua** | Learner không đếm từ; thứ họ cảm thấy là đoạn này dài hơn đoạn kia. Quá ⅓ thì lệch nhẹ cộng lại thành lệch hẳn, và cả đoạn không còn đúng bậc |
| Lệch hẳn (dưới ½ biên dưới, trên 1,5× biên trên), sai số câu, có chữ số, có ký tự tai không nghe được | **Chặn, thử lại** | Đây mới là chỗ bậc mất nghĩa, hoặc bài tập hỏng |

**Dung sai tỉ lệ với độ dài câu**, không phải một con số cố định: lệch 2 từ trên câu 7 từ là 30%,
trên câu 23 từ chỉ là 9% — một biên độ tuyệt đối cho cả hai là khắt khe với bậc cao và dễ dãi với
bậc thấp. Công thức ở `mildBand` trong `forge.ts`.

### Hai thứ trong prompt, cũng từ đo mà ra

**Một câu mẫu đúng độ dài của từng bậc.** Model bắt chước giỏi hơn hẳn việc đếm, nên cho nó thứ để
bắt chước thay vì một con số để đếm.

**Tổng số từ của cả đoạn.** Xin nhiều câu ở bậc cao thì model tự rút ngắn từng câu — đo được: bậc 5
xin 8 câu ra 12-15 từ mỗi câu, trong khi bậc 5 xin 3 câu ra 20-23. Tổng là thứ model xử lý tốt hơn.

**Lượt thử lại mang theo lời chê của lượt trước**, đọc từ D1 chứ không từ biến cục bộ: `step.do`
chạy lại là chạy lại TỪ ĐẦU, mọi biến trong closure đã mất.

Kết quả đo sau ba sửa trên: **0/4 → 9/10** lượt sinh được bài. Lượt hỏng còn lại hiện thông báo bằng
tiếng người kèm nút Xoá, không để learner nhìn một dòng log.

## Kho hạt giống chủ đề

Bảng `dictation_seeds`, nạp 48 hạt lần đầu bằng migration 0195, sửa/thêm ở admin. `group_slug` là
chuỗi **tự do** chứ không phải enum: chủ dự án nói rõ *"có thể có nhiều nhóm khác nữa, tôi liệt kê
nhanh 4 nhóm đó thôi"*, và đóng cứng danh sách nhóm ở tầng schema thì thêm một nhóm là một lần
deploy — mà thêm nhóm chính là việc người vận hành làm thường xuyên nhất.

`brief_en` là **lời dẫn cho model**, không phải nội dung learner đọc: đoạn văn vẫn do máy sinh ra
lúc chạy (SDD-027).

## Vì sao Workflow, và vì sao nó sống ở `nemo12-api`

Một lượt là sinh chữ → kiểm → đọc 3-10 câu → ghép → đẩy R2 → ghi D1, mất 15-40 giây. Trong một
request thì đứt giữa chừng là mất trắng và không ai biết đã tới đâu; trong Workflow thì mỗi `step.do`
chạy lại được, và learner đóng tab cũng không sao.

Nó **không** ở `nemo12-foundry` như hai workflow kia vì xưởng nội dung chưa có binding R2 (token CI
thiếu quyền), mà bài này bắt buộc phải ghi audio vào R2. Worker `nemo12-api` đã có sẵn cả ba thứ
cần: `AI`, `CONTENT`, `DB`.

Đọc–ghép–đóng gói dùng chung `tts.ts` với bài trong lesson: vẫn là **đọc từng câu rồi nối ở mức PCM
thô**, nên mốc từng câu chính xác đến từng mẫu.

## Hạn mức ba bài

Chỉ đạo: *"mỗi learner tối đa 3 audio thôi, ghi rõ cho họ trước khi họ bấm tạo. Và họ cần nghe xong
xuôi 1 trong 3 audio kia, đáp ứng 100% điểm, thì họ sẽ được tạo tiếp."*

Nên trần đếm các bài **chưa đạt 100%**, không phải tổng số bài từng tạo. Bài đã đạt 100% vẫn giữ lại
để nghe lại và vẫn tính điểm, nhưng nhường chỗ cho bài mới. Số suất còn lại hiện **trên nút**, trước
khi bấm.

## Bằng chứng và bảng xếp hạng

Mỗi lượt bấm **Kiểm tra** phát `learner.evidence.recorded` với `subject_id = "english"` và
`node_id = "listening.dictation.L<bậc>"` — gắn với **bậc** chứ không gắn với chủ đề, vì thứ bài này
đo là kỹ năng nghe ở một mức khó, không phải kiến thức về nước Pháp hay phim Up.

**Năm bảng xếp hạng, một cho mỗi bậc**: một learner luyện bậc 1 và một learner luyện bậc 5 không so
được với nhau, mà gộp chung thì bậc thấp luôn thắng vì câu ngắn dễ đúng hơn.

**Và chỉ trong CÙNG LỚP** (chỉ đạo 2026-09-03). Đây là quyết định **riêng tư**, không phải quyết
định kỹ thuật: Nemo12 là sản phẩm cho trẻ em, và một bảng phơi tên mọi learner của mọi gia đình ra
cho nhau là thứ không ai trong nhà đồng ý nếu được hỏi. Cùng lớp thì tên đứa trẻ chỉ hiện ra trước
những người vốn đã biết nhau ngoài đời.

"Lớp" ở đây là **khối lớp** (`learners.grade`), không phải lớp có lịch: `grade` thì learner nào cũng
có, còn `course_enrollments` thì phần lớn learner chưa ghi danh — lấy cái sau thì hầu hết mọi người
mở ra một bảng trống. Learner chưa khai lớp thành một nhóm riêng, không gộp vào lớp nào: gộp họ vào
một lớp bất kỳ là đoán hộ một đứa trẻ nó học lớp mấy.

Màn hình **nói ra phạm vi** ("Chỉ các bạn lớp 9") — một hạng 12 mà không biết đang so với ai thì đọc
ra thành một lời chê.

Điểm là **bốn thành phần cộng lại, không nhân** — công thức nhân cho ra một con số không ai giải
thích nổi vì sao mình tụt hạng, còn công thức cộng thì mỗi phần trả lời một câu riêng và màn hình
bày ra được cả bốn:

| Thành phần | Cách tính | Trần |
| --- | --- | --- |
| Chép đúng | 10 điểm mỗi câu chép đúng | — |
| Trọn vẹn | 50 điểm mỗi bài đạt 100% | — |
| Nỗ lực | 2 điểm mỗi lượt kiểm tra | 3 lượt mỗi bài |
| Chăm nghe | 5 điểm mỗi phút đã nghe | 60 phút |

**Không thành phần nào trừ điểm.** Muốn mở thêm audio thì phải đạt 100%, tức là learner *bắt buộc*
phải chấm đi chấm lại — trừ điểm mỗi lần chấm là phạt đúng cái hành vi mà luật kia ép họ làm.

Trần ở hai thành phần dễ bơm nhất là bắt buộc: thời lượng nghe do **trình duyệt tự khai** (máy chủ
không biết tai learner đang nghe hay đang mở tab khác), nên một con số tự khai không có trần là một
cái nút "cho tôi hạng nhất". Có thêm trần thứ hai ở tầng route: 15 phút mỗi lượt gửi.

## Bảng và mã

* Bảng: `migrations/0195_dictation_practice.sql` — `dictation_seeds`, `dictation_practice`,
  `dictation_practice_lines`.
* Workflow: `workers/api/src/modules/dictation/forge.ts`, bậc ở `levels.ts`, điểm ở `board.ts`,
  nghiệp vụ ở `practice.ts`.
* Giao diện: `apps/learn/src/DictationGame.tsx`; bộ nghe-gõ-chấm dùng chung là `Player` trong
  `apps/learn/src/Dictation.tsx`.
