Giới thiệu
Một ngày nọ, người bạn kurokouji của tôi đã đặt ra một câu hỏi thú vị:
Liệu Raspberry Pi chạy Nerves của tôi có thể nói tiếng Nhật bằng Open JTalk không?
Thách thức đã được nhận.
Tôi nghĩ câu trả lời là có—chỉ cần tôi chưa biết cách làm. Như Antonio Inoki đã từng nói:
元氣が有れば何でもできる (Nếu bạn có tinh thần, bạn có thể làm bất cứ điều gì)
Vì vậy, không có lý do gì để không thử. Trường hợp xấu nhất, tôi sẽ học được điều gì đó. Trường hợp tốt nhất, Raspberry Pi sẽ nói tiếng Nhật. Nhờ vào các công cụ AI hiện đại, chúng ta giờ đây có thể khám phá những vấn đề thú vị mà không bị lạc đường.
Kết quả là open_jtalk_elixir—một wrapper Elixir di động cho Open JTalk, công cụ chuyển văn bản thành giọng nói cổ điển của Nhật Bản.
Thư viện này:
- Xây dựng một CLI Open JTalk native trong quá trình biên dịch
- Đóng gói các tài nguyên từ điển và giọng nói cần thiết theo mặc định
- Cung cấp một API Elixir rõ ràng như thế này:
Dù đích đến là rõ ràng, nhưng con đường đi đầy những bài học. Bài viết này ghi lại hành trình—những ngõ cụt, các quyết định thiết kế, và cách mà các mảnh ghép đã kết hợp lại với nhau.
Open JTalk là gì?
Open JTalk là một engine chuyển văn bản thành giọng nói (TTS) tiếng Nhật đã được thiết lập rộng rãi, thường được sử dụng trong nghiên cứu, hệ thống nhúng và các dự án hobby.
Nó được xây dựng từ các thành phần sau:
- MeCab 0.996 — Một bộ phân tích hình thái học phân tích văn bản tiếng Nhật thành từ với cách đọc và các đặc điểm ngữ pháp.
- HTS Engine API 1.10 — Một engine tổng hợp giọng nói tham số thống kê tạo ra sóng âm thực từ các đặc điểm.
- Open JTalk Dictionary 1.11 — Gói từ điển UTF-8 (
open_jtalk_dic_utf_8-1.11) được phân phối cùng với Open JTalk. Nó cung cấp dữ liệu từ vựng cần thiết cho MeCab để phân tích hình thái và được duy trì như một phần của dự án Open JTalk chính thức. - HTS Voice “Mei” (MMDAgent Example 1.8) — Một mô hình giọng nói tiếng Nhật rất phổ biến, nghe rõ ràng và tự nhiên.
- Open JTalk CLI 1.11 — Một công cụ dòng lệnh dựa trên C kết nối tất cả các thành phần lại với nhau và xuất âm thanh.
Mặc dù đã ra đời từ lâu, Open JTalk vẫn rất phù hợp—đặc biệt khi bạn cần TTS tiếng Nhật không cần internet trong các môi trường nhúng như Raspberry Pi hoặc Nerves.
Thách thức trong môi trường nhúng
Việc làm cho Open JTalk hoạt động bên trong firmware Elixir chạy Nerves không phải là điều dễ dàng.
Dưới đây là lý do:
1. Độ phức tạp trong việc xây dựng native
Open JTalk sử dụng autotools để biên dịch stack C của nó: MeCab, HTS Engine, và chính Open JTalk. Chúng không được xây dựng để biên dịch chéo ngay từ đầu.
2. Tài nguyên cần thiết là lớn
Open JTalk không hoạt động nếu không có các tệp từ điển và mô hình giọng nói của nó. Bạn cần phải cung cấp:
- Từ điển: ~100 MB
- Giọng Mei: ~2 MB
- Tệp nhị phân CLI: ~1 MB
Nếu bạn bỏ qua việc đóng gói chúng, ứng dụng của bạn có thể biên dịch tốt... nhưng sẽ im lặng thất bại trong việc tổng hợp giọng nói trừ khi người dùng tự tải các tài nguyên đó sau khi cài đặt.
Đó không phải là trải nghiệm "nhà phát triển" mà chúng ta mong muốn.
Cách tiếp cận hiệu quả: Vendor + Bundle + Shell Out
Sau khi khám phá nhiều cách tiếp cận, tôi nhận thấy cách đơn giản này là hiệu quả:
- Vendor và biên dịch tất cả các phụ thuộc C tại thời gian biên dịch Elixir (
mix compile) - Đóng gói các tệp giọng nói + từ điển vào thư mục
priv/ - Sử dụng System.cmd/3 để shell ra từ Elixir vào CLI native
Điều này có nghĩa là bạn sẽ có một thiết lập hoạt động hoàn toàn ngay lập tức, ngay cả trên các mục tiêu nhúng như Raspberry Pi chạy Nerves.
Nếu bạn lo ngại về kích thước firmware—như trong các triển khai Nerves tối thiểu—bạn có thể tắt các tài nguyên đã đóng gói như sau:
OPENJTALK_BUNDLE_ASSETS=0 \
OPENJTALK_DIC_DIR=/data/jdic \
OPENJTALK_VOICE=/data/mei.htsvoice \
mix compile
Bạn thậm chí có thể bỏ qua việc biên dịch hoàn toàn (ví dụ, nếu được xây dựng sẵn trong CI):
OPENJTALK_BIN=./bin/open_jtalk mix compile
Tất cả những gì chúng ta thực sự làm là để Elixir điều phối CLI, trong khi công việc nặng nhọc vẫn nằm trong mã native. Nó có thể không đẹp nhưng lại đáng tin cậy, di động và dễ dàng gỡ lỗi.
Những ý tưởng không thành công
Trước khi quyết định vào cách tiếp cận hiện tại, tôi đã thử một số cách khác. Đây là lý do tại sao chúng thất bại:
Hệ thống Nerves tùy chỉnh
Ý tưởng đầu tiên là xây dựng một hình ảnh hệ thống Nerves tùy chỉnh với Open JTalk đã tích hợp. Nghe có vẻ mạnh mẽ—mọi thứ đều được biên dịch sẵn và tích hợp vào firmware.
Nhưng trên thực tế, điều này làm cho việc chỉnh sửa chậm và phát triển khó khăn. Bất cứ khi nào bạn muốn điều chỉnh hoặc thử nghiệm điều gì đó, bạn đều phải xây dựng lại toàn bộ.
Thêm vào đó, ngay cả khi Open JTalk đã được bao gồm, bạn vẫn cần bọc CLI trong Elixir và xử lý việc tải tài nguyên.
Tôi đã loại trừ điều này sớm—đó không đáng để phải đối mặt với độ phức tạp cho thứ tôi đang cố gắng xây dựng.
Hàm được triển khai native trong Elixir (NIFs)
NIFs cho phép bạn gọi các hàm C trực tiếp từ Elixir—nhanh chóng, không cần shell ra.
Nhưng NIFs đi kèm với rủi ro: nếu có bất kỳ điều gì sai, chúng có thể làm sập toàn bộ BEAM VM. Tính di động giữa các CPU/OS khác nhau cũng khá khó khăn.
Vì vậy, tôi cũng bỏ qua lộ trình này. An toàn hơn cho tôi—và cho runtime của bạn.
Cổng Elixir
Tôi đã sử dụng Ports trước đây—ví dụ, trong thư viện cảm biến sgp40 của tôi. Nó hoạt động rất tốt khi đầu vào/đầu ra là đơn giản.
Open JTalk không đơn giản như vậy. Các vấn đề mã hóa, tệp tạm thời, đối số phức tạp... Port gây đau đầu hơn là lợi ích.
Chi tiết về hệ thống xây dựng
Stack native được xây dựng qua một Makefile + các script shell. Quy trình:
- Lấy nguồn Tải xuống và giải nén:
mecabmecab-ipadic-utf8hts_engine_APIopen_jtalk
-
Cấu hình & Sửa đổi
Áp dụng các bản vá cụ thể cho nền tảng (ví dụ, sửa lỗi macOS, cập nhậtconfig.guess). -
Biên dịch stack C
Mỗi thư viện được biên dịch bằng autotools và liên kết tĩnh khi có thể. -
Đóng gói CLI + Tài nguyên
Đặt mọi thứ vàopriv/, sẵn sàng cho Elixir sử dụng.
Tính di động đa nền tảng
Làm cho nó chạy trên:
- 🐧 Linux (x86_64 & ARM)
- 🍎 macOS (Apple Silicon)
- 🍓 Raspberry Pi qua Nerves
...là một cuộc phiêu lưu.
Cơn đau autotools
Autotools không thể phát hiện các nền tảng hiện đại mà không có sự trợ giúp:
configure: error: cannot guess build type
Vì vậy, tôi tiêm config.sub và config.guess mới trước mỗi lần xây dựng.
Những điều kỳ quặc của macOS
- BSD
installkhông hỗ trợ-D→ thêm script shim. - Không liên kết tĩnh → vô hiệu hóa trên Darwin.
Biên dịch chéo Nerves
Nerves xử lý các công cụ nếu bạn tôn trọng MIX_TARGET và truyền các env đúng:
export MIX_TARGET=rpi4
mix deps.get
mix compile
mix firmware
Thật tuyệt vời!
Cách sử dụng trong Elixir
Sau mix compile, việc sử dụng cực kỳ đơn giản:
# Nói ra bằng âm thanh hệ thống
OpenJTalk.say("元氣が有れば、何でもできる")
# Tổng hợp thành WAV
OpenJTalk.to_wav("こんにちは", out: "/tmp/test.wav")
# Lấy WAV dưới dạng nhị phân
{:ok, binary} = OpenJTalk.to_binary("おはようございます")
Các tùy chọn có sẵn
Tất cả các hàm chấp nhận cùng một tập hợp các tùy chọn, tương ứng với các cờ CLI của Open JTalk nhưng với tên hấp dẫn hơn:
| Tùy chọn | Ý nghĩa | Mặc định |
|---|---|---|
:pitch_shift |
Thay đổi nửa âm | 0 |
:rate |
Hệ số tốc độ nói | 1.0 |
:timbre |
Điều chỉnh chất lượng giọng nói | 0.0 |
:gain |
Tăng âm đầu ra (đơn vị dB) | 0 |
:voice |
Đường dẫn đến tệp .htsvoice |
(đã đóng gói) |
:dictionary |
Đường dẫn đến thư mục từ điển | (đã đóng gói) |
:timeout |
Thời gian tối đa chạy trong mili giây | 20000 |
Giải quyết tài nguyên
Thư viện tự động tìm các thành phần cần thiết (nhị phân, từ điển, giọng) theo thứ tự này:
- Biến môi trường (
OPENJTALK_CLI,OPENJTALK_DIC_DIR,OPENJTALK_VOICE) - Tài nguyên đã đóng gói: các tệp được đặt trong
priv/tại thời gian biên dịch - Vị trí hệ thống:
/usr/share,/usr/local/share, Homebrew, v.v.
Bạn có thể kiểm tra việc giải quyết tài nguyên:
OpenJTalk.info()
Nếu bạn từng hoán đổi tệp hoặc điều chỉnh biến môi trường tại runtime, bạn có thể đặt lại bộ nhớ cache với:
OpenJTalk.Assets.reset_cache()
Những bài học rút ra
- Đóng gói tài nguyên theo mặc định — Người dùng mong đợi mọi thứ sẽ hoạt động "ngay lập tức". Tùy chọn từ chối là ổn, nhưng tùy chọn tham gia tạo ra bất ngờ và thất vọng.
- Tôn trọng quy tắc Nerves — Biên dịch chéo hoạt động mượt mà nếu bạn để Nerves thiết lập công cụ. Đấu tranh với nó chỉ gây thêm đau đớn.
- Autotools thiếu hỗ trợ nền tảng hiện đại — Legacy
config.guessvàconfig.subthường thất bại; thay thế chúng là điều cần thiết cho tính di động. - Những điều kỳ quặc của macOS quan trọng — Ngay cả khi bạn không sử dụng macOS, người khác sẽ. Xử lý sự khác biệt về công cụ BSD và giới hạn liên kết tĩnh sớm.
- Elixir + shelling out là một lựa chọn vững chắc — Đối với các công cụ native phức tạp, nó đơn giản và an toàn hơn so với việc quản lý Ports.
- Giữ cho Makefiles nhàm chán — Tránh biến Makefiles thành một ngôn ngữ lập trình. Chuyển logic sang các script cho sự rõ ràng và bảo trì.
- Thử nghiệm trên ARM sớm — x86 không còn là thế giới mặc định nữa. Raspberry Pi, Apple Silicon và các runner ARM trên đám mây nên được xem như những công dân hàng đầu.
Những suy nghĩ cuối cùng
Tôi bắt đầu với một mục tiêu: làm cho Raspberry Pi của tôi nói tiếng Nhật bằng Elixir.
Nó đã trở thành một hệ thống xây dựng đa nền tảng hoàn chỉnh cho một thư viện C đã tồn tại hàng thập kỷ—với UX sạch sẽ và hỗ trợ Nerves ngay lập tức.
Nhưng kết quả cuối cùng?
Đó chính xác là những gì chúng tôi mong muốn.
Hãy thử open_jtalk_elixir, và để cho Raspberry Pi của bạn cũng nói lên nhé.