tts-voice-creator-clone-and.../static/js/audiobook/network.js

96 lines
3.3 KiB
JavaScript

// Network and streaming helpers for audiobook attribution.
// Loaded after audiobook.js; the functions are called from user actions and
// therefore do not need to execute while the main audiobook module initializes.
async function audiobookFetchWithTimeout(url, options = {}, timeoutMs = AUDIOBOOK_ATTRIBUTION_TIMEOUT_MS) {
const parentSignal = options.signal;
const ac = new AbortController();
let timedOut = false;
const timer = setTimeout(() => {
timedOut = true;
ac.abort();
}, timeoutMs);
const onParentAbort = () => ac.abort(parentSignal?.reason);
if (parentSignal) {
if (parentSignal.aborted) onParentAbort();
else parentSignal.addEventListener('abort', onParentAbort, { once: true });
}
try {
return await fetch(url, { ...options, signal: ac.signal });
} catch (err) {
if (timedOut) {
const timeoutErr = new Error(`Timed out after ${Math.ceil(timeoutMs / 1000)}s`);
timeoutErr.name = 'TimeoutError';
throw timeoutErr;
}
throw err;
} finally {
clearTimeout(timer);
if (parentSignal) parentSignal.removeEventListener('abort', onParentAbort);
}
}
function audiobookTimeoutSeconds(timeoutMs) {
return Math.max(5, Math.round(timeoutMs / 1000));
}
async function audiobookAttributeStream(body, view, outerSignal, idleTimeoutMs = AUDIOBOOK_ATTRIBUTION_TIMEOUT_MS) {
const ctl = new AbortController();
const onAbort = () => ctl.abort();
if (outerSignal) {
if (outerSignal.aborted) ctl.abort();
else outerSignal.addEventListener('abort', onAbort, { once: true });
}
let idleTimer = null;
const armIdle = () => {
clearTimeout(idleTimer);
idleTimer = setTimeout(() => ctl.abort(), idleTimeoutMs);
};
try {
armIdle();
const r = await fetch('/api/attribute-dialogue/stream', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
signal: ctl.signal, body: JSON.stringify({ ...body, want_reasoning: true }),
});
if (!r.ok || !r.body) throw new Error('stream HTTP ' + r.status);
const reader = r.body.getReader();
const dec = new TextDecoder();
let buf = '', result = null;
for (;;) {
const { done, value } = await reader.read();
if (done) break;
armIdle();
buf += dec.decode(value, { stream: true });
let at;
while ((at = buf.indexOf('\n\n')) >= 0) {
const line = buf.slice(0, at).trim();
buf = buf.slice(at + 2);
if (!line.startsWith('data:')) continue;
let d;
try { d = JSON.parse(line.slice(5)); } catch (_) { continue; }
if (d.t && view?.thinking) view.thinking(d.t);
if (d.error) throw new Error(d.error);
if (d.done) result = d.result || null;
}
}
if (!result) throw new Error('stream ended without result');
return result;
} catch (err) {
if (err?.name === 'AbortError' && !(outerSignal && outerSignal.aborted)) {
throw new Error('stream idle timeout');
}
throw err;
} finally {
clearTimeout(idleTimer);
if (outerSignal) outerSignal.removeEventListener('abort', onAbort);
}
}
window.TTVC = window.TTVC || {};
window.TTVC.audiobook = window.TTVC.audiobook || {};
Object.assign(window.TTVC.audiobook, {
fetchWithTimeout: audiobookFetchWithTimeout,
attributeStream: audiobookAttributeStream,
timeoutSeconds: audiobookTimeoutSeconds,
});