Builtin Library
README.md
audio_gen (service)
Text-to-audio generation via the ElevenLabs API
— speech, sound effects, and music. A .service asset — a plain Luau module,
resolvable as @builtin::services.audio_gen, exposing three generators that
run through the credit-metered service path.
Operations
speech({ text })— text-to-speech. Returns spoken audio, read in the offering's reading voice.sfx({ prompt, duration? })— text-to-sound-effect.durationis the target length in seconds (optional). Returns a short sound effect.music({ prompt, duration?, instrumental? })— text-to-music.durationis the target length in seconds (optional);instrumentalforces a track with no vocals (optional). Returns a musical track.
Those field names are the whole surface: an operation refuses a field it does
not declare, by name, before any credit is spent. :operations() is the
machine-readable form of the list above.
Speech reads in one voice
speech renders every line in the same reading voice, and the offering takes
no voice selection — so a cast of several characters is separated by what they
say and by how their clips are played back, not by picking a speaker per line.
Both of those are yours to set:
- Writing and delivery — vocabulary, sentence length and punctuation move a read further than a voice swap does.
- Playback treatment — the
Audiocomponent'spitchshifts a speaker up or down;audio.decodeplusasset.create("soundClip", …, { pcm, sampleRate, channels })re-renders a clip, so resampling, band-limiting and a reverb tail are all authorable per character.
Using it
A generation runs in the background: the call hands back a handle straight away, and the audio arrives on the run's status row a few seconds later.
local audio = asset.resolve("audio_gen", "service")
print(audio:cost("sfx"), audio:balance()) -- budget before spending
local g = audio:invoke({ operation = "sfx", prompt = "a heavy wooden door slamming", duration = 2 })
-- Watch the run until it settles. `status` moves through the run's own stages
-- (`submitted`, `writing`, `importing`) to `completed` or `failed`; a completed
-- row's `asset` is the finished, spawnable `.soundClip`.
local row = tools.use("services", "status", { id = g.id })
while row ~= nil and row.status ~= "completed" and row.status ~= "failed" do
task.wait(1)
row = tools.use("services", "status", { id = g.id })
end
print(row.status, row.asset)
audio_gen declares no primary operation (there are three siblings, not
one default) — always pass operation.
The handle's asset_path is where the run directs the raw generated file, not
the asset it becomes: the importer turns those bytes into a .soundClip
container, and that container is what the completed status row names as
asset. Read the produced asset from the row. The same holds for an
asset_path you pass: asset_path = "/zero/source/game/audio/door.mp3" lands
the clip /zero/source/game/audio/door.soundClip, and so does
asset_path = "/zero/source/game/audio/door.soundClip", which names the clip
itself.
Cost
Each operation costs credits (see :cost(operation)); the exact amount
charged is returned on the generation handle. Check :balance() first. A call
refused for an input the operation does not declare is refused before the
charge, so a wrong field name costs a message rather than a generation.