Refactor table and audiobook helper modules
This commit is contained in:
parent
388fd601ab
commit
b953ba1a83
@ -22,8 +22,8 @@ mkdirSync(outDir, { recursive: true });
|
|||||||
const MAIN = [
|
const MAIN = [
|
||||||
'voice-picker', 'benchmark-voice-picker', 'voice-inspector', 'seed-finder', 'voice-sources', 'fishaudio-browser',
|
'voice-picker', 'benchmark-voice-picker', 'voice-inspector', 'seed-finder', 'voice-sources', 'fishaudio-browser',
|
||||||
'integrations', 'routing', 'voice-clone', 'voice-library', 'tts-preview',
|
'integrations', 'routing', 'voice-clone', 'voice-library', 'tts-preview',
|
||||||
'generation', 'benchmark', 'stt', 'rehearser-parse', 'rehearser', 'reader', 'audiobook', 'character-sheets',
|
'generation', 'benchmark', 'stt', 'rehearser-parse', 'rehearser', 'reader', 'audiobook', 'audiobook/network', 'character-sheets',
|
||||||
'characters-library', 'sillytavern', 'library', 'library-characters', 'studio',
|
'characters-library', 'sillytavern', 'library', 'character-columns', 'library-characters', 'studio',
|
||||||
].map(n => join(jsDir, n + '.js'));
|
].map(n => join(jsDir, n + '.js'));
|
||||||
|
|
||||||
const source = MAIN.map(f => `\n/* ==== ${f.split('/').pop()} ==== */\n` + readFileSync(f, 'utf8')).join('\n');
|
const source = MAIN.map(f => `\n/* ==== ${f.split('/').pop()} ==== */\n` + readFileSync(f, 'utf8')).join('\n');
|
||||||
|
|||||||
10
static/dist/main.min.js
vendored
10
static/dist/main.min.js
vendored
File diff suppressed because one or more lines are too long
@ -60,98 +60,6 @@ window._audiobook = _audiobook;
|
|||||||
let _abDraftAutosaveTimer = null;
|
let _abDraftAutosaveTimer = null;
|
||||||
let _abLastServerDraftErrorAt = 0;
|
let _abLastServerDraftErrorAt = 0;
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Streamed attribution: reads the SSE feed from /api/attribute-dialogue/stream,
|
|
||||||
// pushing each delta into view.thinking() so the user can watch the LLM work,
|
|
||||||
// and resolves with the final parsed result. Uses an INACTIVITY timeout (reset
|
|
||||||
// on every received chunk) rather than an overall one — a passage legitimately
|
|
||||||
// takes minutes on a slow model, but silence that long means the stream died.
|
|
||||||
// Throws on any failure; callers fall back to the blocking endpoint. A user
|
|
||||||
// Stop (outer signal) propagates as AbortError; a stalled stream does not.
|
|
||||||
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) {
|
|
||||||
// An idle-timeout abort is a stream failure (fall back), not a user Stop.
|
|
||||||
if (err?.name === 'AbortError' && !(outerSignal && outerSignal.aborted)) {
|
|
||||||
throw new Error('stream idle timeout');
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
} finally {
|
|
||||||
clearTimeout(idleTimer);
|
|
||||||
if (outerSignal) outerSignal.removeEventListener('abort', onAbort);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function audiobookDialogueKey(text) {
|
function audiobookDialogueKey(text) {
|
||||||
return String(text || '')
|
return String(text || '')
|
||||||
.normalize('NFKC')
|
.normalize('NFKC')
|
||||||
|
|||||||
87
static/js/audiobook/network.js
Normal file
87
static/js/audiobook/network.js
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
47
static/js/character-columns.js
Normal file
47
static/js/character-columns.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
// Character-table metadata and the persisted column-visibility controller.
|
||||||
|
// Kept separate from the character renderer so table layout can evolve
|
||||||
|
// without making library-characters.js responsible for every table concern.
|
||||||
|
|
||||||
|
const _CHAR_PROFILE_COMPLETENESS_FIELDS = [
|
||||||
|
'gender', 'age_estimate', 'race_species', 'languages', 'nationality_background',
|
||||||
|
'social_class', 'profession', 'title', 'archetype', 'physical', 'clothing',
|
||||||
|
'alignment', 'arc_direction', 'arc_note', 'attribute_high', 'attribute_low',
|
||||||
|
'skills', 'capabilities', 'backstory', 'relationships', 'motivation', 'fears',
|
||||||
|
'mannerisms', 'communication_style', 'voice_pattern', 'secret', 'conflict_style',
|
||||||
|
'win_condition', 'reputation', 'religious_beliefs', 'notes', 'inventory',
|
||||||
|
];
|
||||||
|
|
||||||
|
const _CHAR_TABLE_COLUMN_DEFS = [
|
||||||
|
['select', 'Select'], ['avatar', 'Avatar'], ['name', 'Name'], ['gender', 'Gender'],
|
||||||
|
['age', 'Age'], ['lines', 'Lines'], ['language', 'Language'], ['alignment', 'Good / Evil'],
|
||||||
|
['profile', 'Profile'], ['voice', 'Voice'], ['rating', 'Rating'], ['prompt', 'Voice Design Prompt'],
|
||||||
|
['notes', 'Notes'], ['tags', 'Tags'], ['book', 'Book / Script'], ['export', 'Export'],
|
||||||
|
];
|
||||||
|
|
||||||
|
function _charTableHiddenColumns() {
|
||||||
|
try {
|
||||||
|
const saved = JSON.parse(localStorage.getItem('ttsvc_libchars_columns') || '{}');
|
||||||
|
return saved && typeof saved === 'object' ? saved : {};
|
||||||
|
} catch (_) { return {}; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function _applyCharTableColumns(table, hidden) {
|
||||||
|
if (!table) return;
|
||||||
|
_CHAR_TABLE_COLUMN_DEFS.forEach(function (def, index) {
|
||||||
|
const isHidden = hidden[def[0]] === false;
|
||||||
|
table.querySelectorAll('tr').forEach(function (row) {
|
||||||
|
const cell = row.children[index];
|
||||||
|
if (cell) cell.classList.toggle('lib-chars-col-hidden', isHidden);
|
||||||
|
});
|
||||||
|
const col = table.querySelector('colgroup')?.children[index];
|
||||||
|
if (col) col.classList.toggle('lib-chars-col-hidden', isHidden);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _charProfileCompleteness(sheet) {
|
||||||
|
const sh = sheet || {};
|
||||||
|
const filled = _CHAR_PROFILE_COMPLETENESS_FIELDS.filter(function (key) {
|
||||||
|
return _libStr(sh[key]).trim().length > 0;
|
||||||
|
}).length;
|
||||||
|
return { filled: filled, total: _CHAR_PROFILE_COMPLETENESS_FIELDS.length };
|
||||||
|
}
|
||||||
@ -899,48 +899,6 @@ function _charCardHtml(rec, allChars) {
|
|||||||
+ '</div></div>';
|
+ '</div></div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// These are the 32 substantive profile fields shown in the character sheet.
|
|
||||||
// Identity/name and generated assets are intentionally excluded.
|
|
||||||
const _CHAR_PROFILE_COMPLETENESS_FIELDS = [
|
|
||||||
'gender', 'age_estimate', 'race_species', 'languages', 'nationality_background',
|
|
||||||
'social_class', 'profession', 'title', 'archetype', 'physical', 'clothing',
|
|
||||||
'alignment', 'arc_direction', 'arc_note', 'attribute_high', 'attribute_low',
|
|
||||||
'skills', 'capabilities', 'backstory', 'relationships', 'motivation', 'fears',
|
|
||||||
'mannerisms', 'communication_style', 'voice_pattern', 'secret', 'conflict_style',
|
|
||||||
'win_condition', 'reputation', 'religious_beliefs', 'notes', 'inventory',
|
|
||||||
];
|
|
||||||
const _CHAR_TABLE_COLUMN_DEFS = [
|
|
||||||
['select', 'Select'], ['avatar', 'Avatar'], ['name', 'Name'], ['gender', 'Gender'],
|
|
||||||
['age', 'Age'], ['lines', 'Lines'], ['language', 'Language'], ['alignment', 'Good / Evil'],
|
|
||||||
['profile', 'Profile'], ['voice', 'Voice'], ['rating', 'Rating'], ['prompt', 'Voice Design Prompt'],
|
|
||||||
['notes', 'Notes'], ['tags', 'Tags'], ['book', 'Book / Script'], ['export', 'Export'],
|
|
||||||
];
|
|
||||||
function _charTableHiddenColumns() {
|
|
||||||
try {
|
|
||||||
const saved = JSON.parse(localStorage.getItem('ttsvc_libchars_columns') || '{}');
|
|
||||||
return saved && typeof saved === 'object' ? saved : {};
|
|
||||||
} catch (_) { return {}; }
|
|
||||||
}
|
|
||||||
function _applyCharTableColumns(table, hidden) {
|
|
||||||
if (!table) return;
|
|
||||||
_CHAR_TABLE_COLUMN_DEFS.forEach(function (def, index) {
|
|
||||||
const isHidden = hidden[def[0]] === false;
|
|
||||||
table.querySelectorAll('tr').forEach(function (row) {
|
|
||||||
const cell = row.children[index];
|
|
||||||
if (cell) cell.classList.toggle('lib-chars-col-hidden', isHidden);
|
|
||||||
});
|
|
||||||
const col = table.querySelector('colgroup')?.children[index];
|
|
||||||
if (col) col.classList.toggle('lib-chars-col-hidden', isHidden);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function _charProfileCompleteness(sheet) {
|
|
||||||
const sh = sheet || {};
|
|
||||||
const filled = _CHAR_PROFILE_COMPLETENESS_FIELDS.filter(function (key) {
|
|
||||||
return _libStr(sh[key]).trim().length > 0;
|
|
||||||
}).length;
|
|
||||||
return { filled: filled, total: _CHAR_PROFILE_COMPLETENESS_FIELDS.length };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compact alternative to the card grid for scanning a large cast at once —
|
// Compact alternative to the card grid for scanning a large cast at once —
|
||||||
// one row per character with the fields that matter for casting decisions.
|
// one row per character with the fields that matter for casting decisions.
|
||||||
// Rows carry the "lib-char-card" class too so they're picked up by the same
|
// Rows carry the "lib-char-card" class too so they're picked up by the same
|
||||||
|
|||||||
@ -145,10 +145,12 @@
|
|||||||
'/static/js/rehearser.js',
|
'/static/js/rehearser.js',
|
||||||
'/static/js/reader.js',
|
'/static/js/reader.js',
|
||||||
'/static/js/audiobook.js',
|
'/static/js/audiobook.js',
|
||||||
|
'/static/js/audiobook/network.js',
|
||||||
'/static/js/character-sheets.js',
|
'/static/js/character-sheets.js',
|
||||||
'/static/js/characters-library.js',
|
'/static/js/characters-library.js',
|
||||||
'/static/js/sillytavern.js',
|
'/static/js/sillytavern.js',
|
||||||
'/static/js/library.js',
|
'/static/js/library.js',
|
||||||
|
'/static/js/character-columns.js',
|
||||||
'/static/js/library-characters.js',
|
'/static/js/library-characters.js',
|
||||||
'/static/js/studio.js',
|
'/static/js/studio.js',
|
||||||
];
|
];
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user