0
0
Lập trình
Admin Team
Admin Teamtechmely

Biến Raspberry Pi thành trợ lý tiếng Nhật với Open JTalk

Đăng vào 1 tháng trước

• 8 phút đọc

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:

Copy
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):

Copy
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:

  1. Lấy nguồn Tải xuống và giải nén:
  • mecab
  • mecab-ipadic-utf8
  • hts_engine_API
  • open_jtalk
  1. 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ật config.guess).

  2. 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ể.

  3. Đóng gói CLI + Tài nguyên
    Đặt mọi thứ vào priv/, 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:

Copy
configure: error: cannot guess build type

Vì vậy, tôi tiêm config.subconfig.guess mới trước mỗi lần xây dựng.

Những điều kỳ quặc của macOS

  • BSD install khô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:

Copy
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:

Copy
# 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:

  1. Biến môi trường (OPENJTALK_CLI, OPENJTALK_DIC_DIR, OPENJTALK_VOICE)
  2. Tài nguyên đã đóng gói: các tệp được đặt trong priv/ tại thời gian biên dịch
  3. 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:

Copy
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:

Copy
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.guessconfig.sub thườ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é.

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào