voice
Every spoken line in Sengoku Trance is generated once and kept as a `.soundClip` under `/source/story/vo/`, so a hot reload, a scene reload or a rebuild never re-buys the script.
story.voice — the voice cache
Every spoken line in Sengoku Trance is generated once and kept as a
.soundClip under /source/story/vo/, so a hot reload, a scene reload or a
rebuild never re-buys the script.
local Voice = require("story.voice")
Voice.load() -- read the duration manifest
Voice.have("vo_opening_01") -- is the line cached?
Voice.clip("vo_opening_01") -- AssetRef<soundClip>, or nil
Voice.seconds("vo_opening_01", "the text") -- how long the line runs
Voice.coverage() -- { total, cached, missing }
Voice.fill({}) -- build-time: generate what is missing
Reading a line's length
seconds answers from the manifest, then from the clip itself, and finally
from the words — about 2.7 a second with a floor, so a one-word line still
lands as a beat. A line whose clip is missing still runs: the plate
prints it and the beat holds for the estimate. Audio can go missing; a line
never does.
Filling the cache
fill is build-time. It is deliberately serial: several generations
started inside one frame come back with a backend transaction conflict that
reads exactly like a failed line, so this runs one at a time and waits.
The service names the file it writes after the prompt, truncated — two lines
that open with the same clause overwrite each other's asset — so every clip
is copied straight out of the service's own output directory into
/source/story/vo/ under a name derived from the beat and the line's index,
and the service's copy is swept up behind it.
The manifest at /source/story/vo/manifest.txt is name|durationMs, one
line each, so the beat's length is known without decoding anything.
Scoped to this part · feeds back into the world's score.