Làm bot Discord đọc chat tiếng Việt trong kênh thoại: xử lý teencode, số và tên người trước khi đưa vào TTS

Chào mọi người,

Mình là Khánh Duy, làm dev ở TP.HCM. Thời gian qua mình làm một mình một con bot Discord tên là Voxy: vào kênh thoại, gõ /join, từ đó tin nhắn ai gõ trong chat của kênh thoại đó sẽ được đọc to bằng giọng tiếng Việt cho cả phòng nghe. Bot dành cho mấy bạn không bật mic được (ở ký túc xá, chơi khuya sợ ồn, mic hỏng…).

Mình đăng bài này chủ yếu để chia sẻ phần xử lý văn bản trước khi đưa vào text-to-speech, vì phần này tốn công hơn mình tưởng rất nhiều, và mong nhận góp ý từ mọi người.

1. Chat không phải tiếng Việt chuẩn

Một tin nhắn bình thường trong phòng game:

ae ơi tối nay 8h30 vô ko, ai thiếu 100k thì ib t nha kkkkk

Đưa thẳng cho engine TTS thì nó đọc kiểu “a e ơi… tám hát ba mươi… ka ô… một trăm ka”. Vì vậy mỗi tin đi qua một chuỗi bước chuyển chữ sang lời nói, theo thứ tự:

  1. Từ điển phát âm của server (khởi tạo bằng 82 từ teencode có sẵn: ko thành “không”, mn thành “mọi người”, dc thành “được”…).
  2. Bỏ emoji.
  3. Số điện thoại 9 đến 15 chữ số đọc từng chữ số một.
  4. Giờ: 8h30 thành “8 giờ 30”.
  5. Số kèm đơn vị: 100k thành “100 nghìn”, 2tr thành “2 triệu”, 15p thành “15 phút”.
  6. Tiếng cười: kkkkk thành “ha ha”, và một âm tiết lặp nhiều lần như “hahahahaha” chỉ đọc 3 lần.

Link và code block được thay bằng “đường dẫn” và “đoạn code”. Timestamp của Discord dạng <t:1792774733:R> được đọc thành giờ và ngày theo giờ Việt Nam.

2. Vấn đề ranh giới từ

Tiếng Việt có dấu nên \b của regex không dùng được: \b chỉ hiểu [A-Za-z0-9_], gặp chữ “ơ” hay “đ” là coi như hết từ. Mình dùng lookbehind và lookahead với Unicode property:

const NOT_WORD = "(?<![\\p{L}\\p{M}\\p{N}_])";
const END_WORD = "(?![\\p{L}\\p{M}\\p{N}_])";
const HOUR_PATTERN = new RegExp(`${NOT_WORD}(\\d{1,2})h(\\d{1,2})?(?:p)?${END_WORD}`, "giu");

Có \p{M} để không cắt giữa chữ khi dấu được gõ ở dạng tổ hợp (chữ cái + dấu rời).

Cái bẫy mình gặp: teencode có k nghĩa là “không”. Nếu chạy từ điển kiểu thay chuỗi thì 100k và kkkk sẽ bị phá. Vì từ trong từ điển chỉ khớp nguyên từ (có ranh giới hai đầu), nên k đứng một mình mới thành “không”, còn 100k để bước đơn vị xử lý, kkkk để bước tiếng cười xử lý.

Một cái bẫy khác: kết quả của từ điển không được bị các bước sau đọc lại. Ví dụ server định nghĩa lmht thành “liên minh huyền thoại”, rồi bước sau lại thấy chữ trong đó khớp một mục khác. Cách mình làm là thay mỗi mục khớp bằng một ký tự trong vùng Private Use của Unicode (U+E100 trở đi), chạy hết các bước, cuối cùng mới đổi ký tự đó về cách đọc thật. Như vậy cách đọc của server không bao giờ bị mở rộng lần hai.

3. Không có bộ teencode nào đúng cho mọi server

Ban đầu mình hard-code danh sách. Chạy thật một thời gian thì có từ đọc sai nghĩa tùy ngữ cảnh và phải gỡ ra. Giờ mỗi server có từ điển riêng (lệnh /dictionary, tối đa 300 từ):

  • Lần đầu dùng, 82 từ có sẵn được chép vào từ điển của server, sửa hay xóa từ nào cũng được.
  • Từ có sẵn đã xóa thì không tự quay lại khi mình cập nhật danh sách.
  • Teencode có sẵn không áp vào tên người nói. Không thì bạn tên “Ny” sẽ được giới thiệu là “người yêu nói: …”, còn bạn tên “K” thành “không nói”. Từ do server tự thêm thì vẫn áp vào tên, vì đó thường là biệt danh mà server muốn bot đọc đúng.

4. Chống spam mà không làm mất ý

Phòng game kiểu gì cũng có người dán 10 cái emoji hoặc gõ “gg gg gg gg gg”. Luật hiện tại:

  • Một ký tự lặp quá 3 lần thì cắt bớt.
  • Một cụm lặp quá 2 lần thì gộp lại.
  • Emoji không được đọc, chỉ đọc chữ.
  • Mỗi tin đọc tối đa 40 từ. Đếm từ bằng Intl.Segmenter("vi", { granularity: "word" }) có sẵn trong JavaScript, tin dài hơn bị cắt và đọc thêm “, còn nữa”.

5. Discord voice

Vài điều mình học được khi làm phần thoại với discord.js và @discordjs/voice:

  • Discord chỉ cho một bot ở một kênh thoại trong mỗi server. Muốn đọc nhiều phòng cùng lúc thì phải có nhiều bot, nên mình làm thêm Voxy 2, 3, 4 (tối đa 4 phòng một server).
  • Trong lúc đang phát câu hiện tại thì câu kế tiếp đã được gửi đi tổng hợp giọng, để giảm độ trễ khi phòng đông.
  • Khi deploy hay khởi động lại máy, bot tự vào lại các phòng còn người trong vòng 30 phút, tin nhắn gửi lúc đang kết nối lại được giữ và đọc đúng thứ tự.

Giọng đọc dùng Google Cloud Text-to-Speech: giọng thường (Standard) miễn phí 20.000 ký tự mỗi ngày cho mỗi server, 10 giọng HD tính theo credit. Bot viết bằng TypeScript, chạy Node, Docker trên một VPS.

Ai muốn thử hoặc nghe các giọng thì ở đây: https://voxybot.app

Mình muốn hỏi thêm mọi người hai chuyện:

  1. Có ai từng xử lý chuẩn hóa văn bản tiếng Việt cho TTS (số, ngày tháng, viết tắt) mà có thư viện hay bộ dữ liệu nào tốt không? Hiện mình đang tự viết bằng regex.
  2. Với từ mơ hồ như k (không hay nghìn), ngoài luật ranh giới từ thì mọi người có cách nào hay hơn không?

Cảm ơn mọi người đã đọc.

Bạn phải chấp nhận là không thể cover hết 100% được, có thể cải thiện dần dần sau. Ví dụ với trường hợp k:

  • 100k -> (\d+)k cái này thì dễ ha
  • kkkk -> k{3,} cái này mình nghĩ là 3 chữ k trở lên, vì nếu 2 chữ k thì giải kk có thể được hiểu là giải haha.
  • thằng K nó bảo nó mới đi chơi với ny -> K là thằng nào? Kiên, Khánh, hay 1 thằng có nickname là Kevin, Kroos trong nhóm chat? Chỗ này có thể để nguyên K không xử lý nè.

Dùng được, bạn dựng đầy đủ nguyên âm + phụ âm/vần là được.

Tham khảo 1 số thư viện:


Mình mới lụm được trên mạng 1 thư viện normalize text luôn nè (keyword vietnamese text normalization):

repo:

paper:

https://arxiv.org/html/2603.04145v1

83% thành viên diễn đàn không hỏi bài tập, còn bạn thì sao?