Text-to-speech turns application content into usable audio. You can narrate articles, read notifications aloud, guide language learners, or add voice interfaces to existing products.
The Laravel AI SDK gives you one fluent API for this work. It supports text-to-speech through OpenAI, ElevenLabs, and Gemini. It also connects audio generation to Laravel storage, queues, API resources, validation, and testing.
This tutorial builds a practical TTS API from the ground up.
The examples use the current Laravel AI SDK documentation. Check the Laravel AI SDK documentation for provider-specific model updates.
What we are building
Our API will accept text and voice preferences through a REST endpoint:
POST /api/audio
The request will support:
- Text input
- Male or female voice helpers
- Provider-specific voice identifiers
- Delivery instructions
- Public audio storage
- JSON API resource responses
- Optional background generation
Laravel is a strong choice when you want to build a REST API with PHP without assembling every backend feature yourself. Authentication, validation, queues, filesystem support, and testing are already part of the ecosystem.
Install the Laravel AI SDK
Install the package with Composer:
composer require laravel/ai
Publish the SDK configuration and migrations:
php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"
php artisan migrate
The published files include config/ai.php. The SDK uses this file for provider credentials and default models.
Add the provider keys you plan to use to .env:
OPENAI_API_KEY=
ELEVENLABS_API_KEY=
GEMINI_API_KEY=
Keep these values outside source control. Laravel loads them through the configuration system.
Configure OpenAI, ElevenLabs, and Gemini

The AI SDK supports TTS through OpenAI, ElevenLabs, and Gemini. Configure each provider in config/ai.php:
return [
'default' => env('AI_PROVIDER', 'openai'),
'providers' => [
'openai' => [
'driver' => 'openai',
'key' => env('OPENAI_API_KEY'),
'models' => [
'audio' => [
'default' => env('OPENAI_AUDIO_MODEL'),
],
],
],
'elevenlabs' => [
'driver' => 'elevenlabs',
'key' => env('ELEVENLABS_API_KEY'),
'models' => [
'audio' => [
'default' => env('ELEVENLABS_AUDIO_MODEL'),
],
],
],
'gemini' => [
'driver' => 'gemini',
'key' => env('GEMINI_API_KEY'),
'models' => [
'audio' => [
'default' => env('GEMINI_AUDIO_MODEL'),
],
],
],
],
];
The exact model names depend on the provider and SDK version. Use the defaults in the published configuration when you create your application.
Set the provider you want to use by default:
AI_PROVIDER=openai
This lets your application call Audio::of() without scattering provider-specific configuration throughout your controllers.
You can also route requests through custom base URLs when your infrastructure uses a proxy or gateway. The AI SDK configuration documentation covers that setup.
Generate audio with the fluent API
The basic TTS operation is short:
use Laravel\Ai\Audio;
$audio = Audio::of('Welcome to our Laravel application.')->generate();
$rawContent = (string) $audio;
The returned audio response contains the generated binary content. You can stream it directly, store it, or pass it to another service.
Laravel also adds a toAudio() method to Stringable:
use Illuminate\Support\Str;
$audio = Str::of('Your report is ready.')->toAudio();
$rawContent = (string) $audio;
This is useful when your text already comes from a string transformation pipeline:
$audio = Str::of($article->body)
->stripTags()
->limit(3000)
->toAudio();
For production applications, validate and limit the input before sending it to a provider. Long text increases latency and cost.
Choose a voice and delivery style
The SDK includes simple voice helpers:
use Laravel\Ai\Audio;
$femaleVoice = Audio::of('Your order has shipped.')
->female()
->generate();
$maleVoice = Audio::of('Your verification code is 482913.')
->male()
->generate();
$specificVoice = Audio::of('Welcome back.')
->voice('voice-id-or-name')
->generate();
Use voice() when your provider exposes a specific voice ID or name. Store that value in your own application configuration rather than accepting arbitrary provider values from every request.
You can also coach the delivery with instructions():
$audio = Audio::of(
'Your appointment begins in fifteen minutes.'
)
->female()
->instructions('Speak clearly, calmly, and at a moderate pace.')
->generate();
Instructions work well for different product contexts:
$audio = Audio::of($lesson->content)
->voice($lesson->voice_id)
->instructions(
'Pronounce technical terms carefully. Pause briefly between examples.'
)
->generate();
The text controls what is said. The instructions control how it should sound.
Store generated audio
Generated audio integrates with Laravel’s filesystem API. Store it privately when only your application should access it:
$audio = Audio::of('Your invoice is ready.')->generate();
$path = $audio->store();
Use a predictable filename when you need to save the path in a database:
$path = $audio->storeAs('audio/invoices/invoice-123.mp3');
For a browser-accessible file, use the public storage methods:
$path = $audio->storePublicly();
$path = $audio->storePubliclyAs(
'audio/notifications/welcome.mp3'
);
These methods use the default disk configured in config/filesystems.php. You can use local storage during development and S3-compatible storage in production. Laravel Cloud also provides managed object storage for applications that need a hosted deployment path.
Save the generated path, provider, voice, and original text in an audio_generations table. That gives you an audit trail and lets clients retrieve existing files without generating them again.
Expose TTS through a REST API

Create a request class for consistent validation:
php artisan make:request GenerateAudioRequest
Define the accepted input:
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class GenerateAudioRequest extends FormRequest
{
public function rules(): array
{
return [
'text' => ['required', 'string', 'max:5000'],
'voice' => [
'nullable',
Rule::in(['male', 'female']),
],
'voice_id' => ['nullable', 'string', 'max:255'],
'instructions' => ['nullable', 'string', 'max:500'],
];
}
}
Create a resource for a stable JSON response:
php artisan make:resource AudioGenerationResource
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Storage;
class AudioGenerationResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'status' => $this->status,
'text' => $this->text,
'voice' => $this->voice,
'audio_url' => $this->path
? Storage::url($this->path)
: null,
'created_at' => $this->created_at?->toIso8601String(),
];
}
}
Now implement the controller:
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\GenerateAudioRequest;
use App\Http\Resources\AudioGenerationResource;
use App\Models\AudioGeneration;
use Laravel\Ai\Audio;
class AudioController extends Controller
{
public function store(GenerateAudioRequest $request)
{
$data = $request->validated();
$builder = Audio::of($data['text']);
$builder = match ($data['voice'] ?? null) {
'male' => $builder->male(),
'female' => $builder->female(),
default => $builder,
};
if (! empty($data['voice_id'])) {
$builder = $builder->voice($data['voice_id']);
}
if (! empty($data['instructions'])) {
$builder = $builder->instructions($data['instructions']);
}
$audio = $builder->generate();
$generation = AudioGeneration::create([
'text' => $data['text'],
'voice' => $data['voice'] ?? $data['voice_id'] ?? null,
'status' => 'complete',
'path' => $audio->storePubliclyAs(
'audio/'.str()->uuid().'.mp3'
),
]);
return (new AudioGenerationResource($generation))
->response()
->setStatusCode(201);
}
}
Register the route:
use App\Http\Controllers\Api\AudioController;
use Illuminate\Support\Facades\Route;
Route::post('/audio', [AudioController::class, 'store']);
A request now looks like this:
{
"text": "Welcome to our language course.",
"voice": "female",
"instructions": "Speak slowly and clearly."
}
For private APIs, protect the route with Laravel Sanctum:
Route::middleware('auth:sanctum')->group(function () {
Route::post('/audio', [AudioController::class, 'store']);
});
Sanctum supports token authentication for mobile clients, external integrations, and third-party consumers.
Queue longer audio jobs
Synchronous generation works for short messages. Longer narration should run in the background.
The AI SDK supports queueing directly:
use Laravel\Ai\Audio;
use Laravel\Ai\Responses\AudioResponse;
Audio::of($data['text'])
->female()
->instructions('Use a clear educational tone.')
->queue()
->then(function (AudioResponse $audio) use ($generation) {
$generation->update([
'status' => 'complete',
'path' => $audio->storePubliclyAs(
"audio/{$generation->id}.mp3"
),
]);
});
Return a 202 Accepted response while the job is processing:
return response()->json([
'id' => $generation->id,
'status' => 'processing',
], 202);
Your client can poll a GET /api/audio/{audioGeneration} endpoint. You can also broadcast completion events through Laravel Reverb when the frontend needs an immediate update.
Configure a queue connection in .env:
QUEUE_CONNECTION=database
Then create the database queue table if your application does not already have it:
php artisan make:queue-table
php artisan migrate
php artisan queue:work
Laravel’s queue system supports Redis, Amazon SQS, database queues, retries, rate limiting, and queue monitoring. These are useful php developer tools when TTS requests become part of a larger media workflow.

Test TTS without calling a provider
The AI SDK includes audio fakes for unit and feature tests:
use Laravel\Ai\Audio;
use Laravel\Ai\Prompts\AudioPrompt;
test('audio is generated with a female voice', function () {
Audio::fake();
$audio = Audio::of('Hello from the test suite.')
->female()
->generate();
Audio::assertGenerated(function (AudioPrompt $prompt) {
return $prompt->contains('Hello from the test suite.')
&& $prompt->isFemale();
});
});
You can test the API endpoint without external network requests:
use Laravel\Ai\Audio;
test('audio endpoint validates and generates speech', function () {
Audio::fake();
$response = $this->postJson('/api/audio', [
'text' => 'Your account is ready.',
'voice' => 'female',
]);
$response
->assertCreated()
->assertJsonPath('data.status', 'complete');
Audio::assertGenerated(fn ($prompt) =>
$prompt->contains('Your account is ready.')
&& $prompt->isFemale()
);
});
For queued generation, assert that the request was queued:
Audio::fake();
Audio::of('This will be generated later.')
->male()
->queue();
Audio::assertQueued(fn ($prompt) =>
$prompt->contains('This will be generated later.')
);
Use preventStrayAudio() when every audio call in a test must have an explicit fake response:
Audio::fake()->preventStrayAudio();
These tests verify your application logic without spending provider credits or depending on network availability.
Practical TTS use cases
Text-to-speech fits several application patterns:
- Narration: Convert articles, documentation, and product updates into audio.
- Accessibility: Offer spoken versions of important content.
- Notifications: Read order updates, reminders, or account alerts aloud.
- Language learning: Combine written lessons with pronunciation and listening exercises.
- Customer support: Turn approved responses into consistent voice messages.
- Voice interfaces: Pair TTS with the SDK’s transcription features for conversational workflows.
Laravel’s AI SDK keeps these use cases close to familiar framework code. You can validate input with Form Requests, store output with the filesystem, process expensive work through queues, and return stable responses with API Resources.
That combination makes Laravel a practical PHP web framework for audio-enabled products. Start with synchronous generation for short messages, then add storage, authentication, queues, and monitoring as your usage grows.
For more examples, explore the Laravel AI SDK documentation, the Laravel framework documentation, and the Laravel AI SDK repository on GitHub.