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. 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àPlayertrongapps/learn/src/Dictation.tsx.