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 = [
|
||||
'voice-picker', 'benchmark-voice-picker', 'voice-inspector', 'seed-finder', 'voice-sources', 'fishaudio-browser',
|
||||
'integrations', 'routing', 'voice-clone', 'voice-library', 'tts-preview',
|
||||
'generation', 'benchmark', 'stt', 'rehearser-parse', 'rehearser', 'reader', 'audiobook', 'character-sheets',
|
||||
'characters-library', 'sillytavern', 'library', 'library-characters', 'studio',
|
||||
'generation', 'benchmark', 'stt', 'rehearser-parse', 'rehearser', 'reader', 'audiobook', 'audiobook/network', 'character-sheets',
|
||||
'characters-library', 'sillytavern', 'library', 'character-columns', 'library-characters', 'studio',
|
||||
].map(n => join(jsDir, n + '.js'));
|
||||
|
||||
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 _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) {
|
||||
return String(text || '')
|
||||
.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>';
|
||||
}
|
||||
|
||||
// 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 —
|
||||
// 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
|
||||
|
||||
@ -145,10 +145,12 @@
|
||||
'/static/js/rehearser.js',
|
||||
'/static/js/reader.js',
|
||||
'/static/js/audiobook.js',
|
||||
'/static/js/audiobook/network.js',
|
||||
'/static/js/character-sheets.js',
|
||||
'/static/js/characters-library.js',
|
||||
'/static/js/sillytavern.js',
|
||||
'/static/js/library.js',
|
||||
'/static/js/character-columns.js',
|
||||
'/static/js/library-characters.js',
|
||||
'/static/js/studio.js',
|
||||
];
|
||||
|
||||
Loading…
Reference in New Issue
Block a user