Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Text To Speech Laravel Package

cion/laravel-text-to-speech

A Laravel package for converting text to speech with a simple API, configurable voices, and support for multiple TTS providers. Easily generate audio files from text in your Laravel apps.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and verify compatibility first. The package has not shipped a release since December 2023 and has zero dependents, so before writing any code confirm it installs cleanly on your Laravel version:

composer require cion/laravel-text-to-speech
composer show cion/laravel-text-to-speech   # check requires vs. your laravel/framework constraint

If Composer resolves to an old framework constraint, either pin the package and stay on that Laravel line, or fork it — this is the single biggest "day one" risk for a package this small.

Publish the config and register credentials. The package follows the standard Laravel driver-manager shape (a facade over a manager that resolves provider drivers from config), so the config file is the source of truth for which providers exist and which one is the default:

php artisan vendor:publish --tag=text-to-speech-config

Then set the provider keys in .env (ELEVENLABS_API_KEY, OPENAI_API_KEY, AWS_*, etc. — use whatever names the published config/text-to-speech.php references) and set your default driver there.

Where to read first, in order:

  1. README.md — the only usage documentation; treat the method names below as illustrative and match them to the README.
  2. config/text-to-speech.php — the list of supported drivers and their options (voice, model, format, sample rate).
  3. src/ — the manager class and the Drivers/ directory. Each driver is a thin adapter, so reading one tells you exactly what the shared API supports and what it silently drops.
  4. composer.json — for the Laravel/PHP constraint and the autoloaded facade alias.

First use case: synthesize a short string to a file on a disk. This is the 20-line path that proves credentials, driver selection, and storage all work:

use Cion\TextToSpeech\Facades\TextToSpeech;

$path = TextToSpeech::driver('openai')          // or your configured default
    ->convert('Welcome to the show.')
    ->save(storage_path('app/public/hello.mp3'));

Once that file plays, you have validated the whole chain: config → driver resolution → HTTP call → binary handling → disk write. Everything else is scaling that pattern.

Implementation Patterns

Wrap the facade in your own service, never call it from controllers. The facade is a static entry point to a manager; injecting it directly makes tests brittle and couples your app to a driver API that may change. Bind a small interface instead:

interface SpeechSynthesizer {
    public function synthesize(string $text, array $options = []): string; // returns storage path
}

class ProviderSpeechSynthesizer implements SpeechSynthesizer
{
    public function synthesize(string $text, array $options = []): string
    {
        $driver = $options['driver'] ?? config('text-to-speech.default');
        return TextToSpeech::driver($driver)
            ->convert($text)
            ->save($this->pathFor($text, $options));
    }
}

This gives you one seam for swapping providers, faking in tests, and adding logging/cost tracking.

Always run synthesis in a queued job. TTS calls are 1–10+ seconds for a paragraph and longer for anything substantial. A job with $timeout, $tries = 3, and backoff() is the correct default:

class GenerateSpeech implements ShouldQueue
{
    public int $tries = 3;
    public array $backoff = [5, 30, 120];

    public function __construct(
        public string $text,
        public string $diskPath,
        public array $options = [],
    ) {}

    public function handle(SpeechSynthesizer $tts): void
    {
        $tts->synthesize($this->text, $this->options);
    }
}

Dispatch on a dedicated queue so TTS latency doesn't starve your transactional jobs.

Cache by content hash, not by request. TTS is billed per character. Keying on a hash of (text, voice, model, format) and storing the resulting file on a shared disk means repeat content is free:

$key = 'tts:'.md5(json_encode([$text, $voice, $format]));
$path = Cache::remember($key, now()->addDays(30), fn () => $tts->synthesize($text, $options));

Serve audio through a controller with a signed URL, not a public disk. Audio files are user-generated and often large; Storage::disk('tts')->temporaryUrl(...) or a signedRoute keeps them off the public web and works fine with S3/R2.

Use the default driver as a switch, not a hardcode. Put provider selection in config so dev/staging/prod can differ (cheap provider in CI, premium voice in prod), and pass an explicit driver() only where a specific voice genuinely matters.

Chunk long text yourself. The shared API almost certainly does not implement provider-specific chunking or SSML splitting. Split on sentence boundaries at roughly the provider's character limit, synthesize each chunk, and concatenate — or better, store segments and concatenate at playback to avoid re-billing on retry.

For streaming APIs, don't write to disk at all. If you're returning audio to a browser, stream the provider response directly (response()->streamDownload(...)) so you skip a round trip through the filesystem; keep the file-write path for pre-generated/podcast-style content.

Gotchas and Tips

The package is effectively unmaintained. Last release December 2023, 43 stars, zero dependents. Pin the exact version in composer.json, and budget for the likelihood that you own bug fixes. Check whether it supports the current Laravel major before adopting — if it doesn't, fork and bump the constraint yourself rather than waiting.

config:cache will break naive env usage. If the package reads env() directly inside a driver instead of through config(), credentials silently become null in production. Audit src/ for env( calls and move any you find into the published config file. Same class of bug applies to app()->environment() checks.

Driver method surfaces are not identical. The promise of a "shared API" is the lowest common denominator. Voice cloning, SSML break/emphasis tags, word-level timestamps, and output formats will be supported by some providers and silently ignored — or throw — by others. Read each driver before assuming an option is portable; isolate provider-specific options in your own service so a provider swap is a diff in one file.

Output format and sample rate differ per provider. MP3 vs. WAV vs. Opus, 22.05kHz vs. 44.1kHz. Browsers and downstream tools (ffmpeg, podcast platforms) care. Normalize at the storage layer or constrain format in config per driver and document it.

Character limits cause silent truncation or 400s. Providers cap per-request characters differently. Validate text length before dispatch and fail loudly in your job; a truncated podcast intro that "worked" is worse than an exception.

Rate limits and 429s need backoff, not retries-in-a-loop. Use Laravel's job backoff() and retryUntil(); also consider RateLimited middleware on the queue by provider so a batch of 500 dispatches doesn't burn your quota in ten seconds.

Costs are per character, and retries are double-billed. A failed job that re-runs after a partial success still costs money. Persist a synthesis record (text_hash, provider, characters, cost_estimate, status) so you can see spend and avoid duplicate work; idempotency on the content hash is the cheapest defense.

Testing: the package likely has no fake(). Check src/ for a testing helper before assuming one exists. If not, bind your SpeechSynthesizer interface to a stub in the container, or use Http::fake() against the provider's endpoint and assert the driver wrote the expected file. This is the strongest argument for the service-wrapper pattern above.

Facade alias/namespace drift. Because the Packagist name (cion/...) and the GitHub repo (meemalabs/...) differ, verify the actual namespace and facade alias from composer.json autoload and the service provider rather than copying snippets from a blog post — a wrong use statement is the most common first-hour failure with small packages.

Extension point is the manager, not the driver. If you need a provider the package doesn't ship, look at how the manager resolves drivers (extend() / TextToSpeech::extend(...) if it follows Laravel's manager convention). Registering a custom driver in a service provider is far cleaner than forking — and it's the right way to survive the package's slow maintenance.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast