const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
async function synthesize(body: object, attempts = 4): Promise<ArrayBuffer> {
for (let attempt = 1; ; attempt++) {
const res = await fetch('https://api.lokutor.com/tts/synthesize', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LOKUTOR_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (res.ok) return res.arrayBuffer();
const err = await res.json().catch(() => ({}));
if (res.status === 402) {
throw new Error(`Top up needed: ${err.message ?? err.code}`);
}
// The synthesis 503 has {error, voice, hint} and Retry-After; other errors have {code, retryable}.
const retryable = res.status === 503 || res.status === 429 || err.retryable === true;
if (!retryable || attempt >= attempts) {
throw new Error(`${res.status} ${err.code ?? err.error ?? ''}: ${err.message ?? err.hint ?? ''}`);
}
const retryAfter = Number(res.headers.get('Retry-After'));
await sleep(retryAfter > 0 ? retryAfter * 1000 : Math.min(1000 * 2 ** attempt, 10000));
}
}