vuthaihoc / laravel-tts-client
Laravel client for the Piper TTS API (laravel-tts-api): signs temporary tokens locally, synthesizes and streams speech.
Requires
- php: ^8.2
- firebase/php-jwt: ^6.10|^7.0
- guzzlehttp/guzzle: ^7.8
- illuminate/contracts: ^11.0|^12.0|^13.0
- illuminate/http: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^9.0|^10.0|^11.0
- phpunit/phpunit: ^11.0|^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Client Laravel cho laravel-tts-api, dịch vụ chuyển văn bản thành giọng nói chạy Piper.
- Token tạm được ký ngay trong app bằng client secret, không phải gọi lên server để xin token. Token được dùng lại cho tới khi gần hết hạn.
Tts::synthesize()trả về một file WAV hoàn chỉnh.Tts::stream()trả audio theo từng câu, có thể chuyển tiếp thẳng xuống trình duyệt.Tts::url()/Tts::token()cấp URL hoặc token cho frontend tự gọi server, để audio không phải đi qua app.- Hỗ trợ Laravel 11–13, PHP 8.2+.
Cài đặt
composer require vuthaihoc/laravel-tts-client
php artisan vendor:publish --tag=tts-config # không bắt buộc
.env:
TTS_BASE_URL=https://tts.example.com TTS_CLIENT_ID=acme.k3f9 TTS_CLIENT_SECRET=... # do admin server cấp bằng `php artisan tts:client acme` # TTS_VOICE=vi_VN-csa-voice-piper-v3-medium # voice mặc định, bỏ trống thì server tự chọn # TTS_TOKEN_TTL=300 # không được vượt giới hạn của server (mặc định 300) # TTS_TIMEOUT=120
TTS_CLIENT_SECRET chỉ nằm ở backend: ai có nó đều ký được token. Trình duyệt chỉ nhận token đã ký sẵn hoặc URL có gắn token.
Sử dụng
use Vuthaihoc\TtsClient\Facades\Tts; // Danh sách voice và speaker (cache 5 phút) $voices = Tts::voices(); // Một file WAV $audio = Tts::synthesize('Xin chào!', voice: 'vi_VN-csa-voice-piper-v3-medium', speaker: 'yen_nhi'); $audio->content; // bytes WAV $audio->duration; // giây $audio->save('tts/hello.wav', 's3'); // Trả thẳng từ controller Route::get('/hello.wav', fn () => Tts::synthesize('Xin chào!'));
Tuỳ chỉnh âm thanh
synthesize(), stream() và url() nhận thêm các named argument sau. Để null (mặc định) thì lấy giá trị trong config; config cũng không đặt thì server dùng mặc định của nó:
| Tham số | Server mặc định | Ý nghĩa |
|---|---|---|
normalize |
true |
Đưa đỉnh của mỗi câu về mức tối đa rồi mới nhân volume |
volume |
1.0 |
0.1–2 |
sentenceSilence |
0.2 |
Giây lặng giữa các câu (0–3) |
noiseScale |
theo voice (~0.667) |
Độ biến thiên của giọng; thấp hơn thì đều hơn nhưng phẳng hơn |
noiseW |
theo voice (~0.8) |
Độ biến thiên nhịp; noiseScale: 0, noiseW: 0 cho kết quả lặp lại y hệt |
Tts::synthesize('Xin chào!', volume: 0.8, sentenceSilence: 0.4); Tts::stream($text, format: 'pcm', noiseScale: 0.5, noiseW: 0.6);
Đặt mặc định cho cả app trong .env: TTS_NORMALIZE, TTS_VOLUME, TTS_SENTENCE_SILENCE, TTS_NOISE_SCALE, TTS_NOISE_W.
IPA
Phần nằm giữa [[ ... ]] được đọc đúng theo âm vị IPA, dùng để sửa cách đọc tên riêng, từ viết tắt, hoặc chọn cách đọc Anh-Anh hay Anh-Mỹ cho một từ:
Tts::synthesize('I like [[təmˈɑːtəʊ]] sauce.', voice: 'en_US-amy-medium'); Tts::synthesize('Chào mừng quý khách đến [[tˈe-2ɲ fˈoɜ]] Hồ Chí Minh.');
Ký hiệu phải theo đúng quy ước của espeak-ng cho voice đó. Lấy IPA mẫu trên server bằng php artisan piper:ipa "thành phố" --voice=.... Cần server có libpiper build từ scripts/build-libpiper.sh (image Docker đã có sẵn), xem docs/api.md của server.
Lấy phiên âm (IPA)
Tts::ipa() gọi /api/tts/ipa: đưa vào một từ, một câu hoặc nhiều câu, nhận lại IPA theo từng câu.
$ipa = Tts::ipa('either', 'en-GB'); $ipa->cambridge(); // "/ˈaɪ.ðə/" array_column($ipa->alternatives(), 'cambridge'); // ["/ˈaɪ.ðə/", "/ˈiː.ðə/"] (chỉ có với từ lẻ) $ipa->ipa(); // "ˈaɪðɐ", dán được vào [[ ]] $ipa = Tts::ipa('The vegetable recipe.', 'en-US', engine: 'dictionary'); $ipa->cambridge(); // "/ðə ˈvedʒ.tə.bəl ˈres.ə.pi/" $ipa->words(); // từng từ: nguồn (cmudict/britfone/espeak) và mọi cách đọc $ipa->sentences; // dữ liệu gốc theo từng câu
language:en-GB,en-UShoặcvi-VN. Để trống thì lấyTTS_IPA_LANGUAGE(mặc địnhen-US).engine: 'espeak'(mặc định) cho IPA đúng như voice sẽ đọc.engine: 'dictionary'tra từng từ trong CMUdict (Anh-Mỹ) hoặc Britfone (Anh-Anh), từ nào không có thì dùng espeak; chỉ dùng được với tiếng Anh,vi-VNsẽ némTtsValidationException.- Bản ghi kiểu Cambridge (
cambridge()) lànullvới tiếng Việt.
Stream
// Chuyển tiếp xuống trình duyệt theo từng câu Route::get('/speak', fn (Request $r) => Tts::stream($r->input('text'), voice: 'en_US-amy-medium')); // Tự đọc từng chunk foreach (Tts::stream($text, format: 'pcm') as $chunk) { // s16le mono, Tts::stream(...)->sampleRate Hz } // NDJSON: mỗi câu một mảng ['i', 'sample_rate', 'duration', 'last', 'pcm' => base64] foreach (Tts::stream($text, format: 'ndjson')->sentences() as $sentence) { // ... }
Lỗi (voice sai, token bị từ chối, ...) được ném ra ngay khi gọi stream(), trước khi gửi byte audio nào xuống trình duyệt. Việc chuyển tiếp dùng echo + flush, nên stream được trên PHP-FPM và Octane/Swoole. Riêng Octane/RoadRunner sẽ gom hết rồi mới gửi.
Để frontend gọi server trực tiếp
{{-- URL dùng được trong token_ttl giây (mặc định 5 phút) --}} <audio controls src="{{ Tts::url('Xin chào các bạn.', speaker: 'quang_huy') }}"></audio>
// Hoặc trả token cho JavaScript, để JS tự gọi fetch(`${base}/api/tts/stream`, { headers: { Authorization: `Bearer ${token}` } }) return ['token' => Tts::token(), 'expires_at' => Tts::tokenExpiresAt()];
Xử lý lỗi
Mọi lỗi đều kế thừa Vuthaihoc\TtsClient\Exceptions\TtsException (getCode() là HTTP status, ->response là response gốc):
| Exception | Khi nào |
|---|---|
TtsAuthenticationException |
401: sai client secret, server đã đổi TTS_SECRET, đồng hồ lệch quá 60 s, TTS_TOKEN_TTL lớn hơn giới hạn của server |
TtsValidationException |
422: ->errors() trả lỗi theo từng trường (text quá dài, voice hoặc speaker không tồn tại, ...) |
TtsRateLimitException |
429: ->retryAfter() là số giây phải chờ |
TtsConnectionException |
Không kết nối được server hoặc quá TTS_TIMEOUT |
TtsConfigurationException |
Thiếu TTS_BASE_URL, TTS_CLIENT_ID hoặc TTS_CLIENT_SECRET |
Test
composer test # unit test, dùng Http::fake TTS_LIVE_BASE_URL=http://127.0.0.1:8088 TTS_LIVE_CLIENT_ID=... TTS_LIVE_CLIENT_SECRET=... \ vendor/bin/phpunit --testsuite Live # chạy với server thật
Trong app của bạn, chỉ cần Http::fake(['*/api/tts*' => Http::response('RIFF...')]) là gọi được Tts mà không cần server.
License
MIT