feat: add visual pair review workflow
This commit is contained in:
parent
8ed1f122a3
commit
a7a6bb1cdc
@ -35,6 +35,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
||||
job status/cancellation endpoints while reusing the existing casting validation, retries, and idempotency behavior.
|
||||
- **Attribution Fixture Coverage.** Added a neutral multi-speaker Book 2-style fixture regression to verify parser
|
||||
normalization across named speakers and narration without requiring a live provider.
|
||||
- **Visual Pair Review Workflow.** The character library now exposes the read-only visual-pair audit directly in its
|
||||
toolbar and displays actionable per-character findings without changing saved assets.
|
||||
- **STT Cancellation Safety.** Queued transcription cancels before consuming the single STT slot, and timed-out
|
||||
requests keep the device slot held until the underlying worker actually exits.
|
||||
- **Credential Boundary Review.** Provider credentials are now documented as server-only; only the explicitly
|
||||
|
||||
2
static/dist/main.min.js
vendored
2
static/dist/main.min.js
vendored
File diff suppressed because one or more lines are too long
@ -6,6 +6,40 @@
|
||||
|
||||
let _libCharsRenderSeq = 0;
|
||||
|
||||
async function _libReviewVisualPairs(button, container) {
|
||||
if (!button) return;
|
||||
button.disabled = true;
|
||||
const oldLabel = button.innerHTML;
|
||||
button.innerHTML = '<span class="mdi mdi-loading mdi-spin"></span> Reviewing…';
|
||||
try {
|
||||
const response = await fetch('/api/characters/visual-pair-review');
|
||||
const payload = await response.json();
|
||||
if (!response.ok) throw new Error(payload.detail || 'Visual-pair review failed');
|
||||
let panel = container.querySelector('#lib-chars-visual-review');
|
||||
if (!panel) {
|
||||
panel = document.createElement('div');
|
||||
panel.id = 'lib-chars-visual-review';
|
||||
panel.className = 'lib-chars-visual-review';
|
||||
container.insertBefore(panel, container.firstChild);
|
||||
}
|
||||
if (!payload.issues || !payload.issues.length) {
|
||||
panel.innerHTML = '<span class="mdi mdi-check-circle-outline"></span> Visual pairs passed the metadata and asset-reuse checks (' + payload.checked + ' characters reviewed).';
|
||||
if (typeof toast === 'function') toast('Visual pair review passed', 'success');
|
||||
} else {
|
||||
panel.innerHTML = '<strong><span class="mdi mdi-alert-outline"></span> Review ' + payload.issues.length + ' character pair' + (payload.issues.length === 1 ? '' : 's') + ':</strong> '
|
||||
+ payload.issues.map(function (item) {
|
||||
return '<span class="lib-chars-review-item"><b>' + escHtml(item.name || item.id) + '</b> — ' + escHtml(item.issues.join(', ')) + '</span>';
|
||||
}).join('');
|
||||
if (typeof toast === 'function') toast(payload.issues.length + ' visual pair(s) need review', 'warning');
|
||||
}
|
||||
} catch (error) {
|
||||
if (typeof toast === 'function') toast(error.message || 'Visual-pair review failed', 'error');
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
button.innerHTML = oldLabel;
|
||||
}
|
||||
}
|
||||
|
||||
async function libraryRenderCharacters() {
|
||||
const container = document.getElementById('lib-chars-list');
|
||||
if (!container) return;
|
||||
@ -120,6 +154,7 @@ async function libraryRenderCharacters() {
|
||||
? '<button class="btn-secondary btn-sm" id="lib-chars-back-reader" title="Return to Read Aloud"><span class="mdi mdi-arrow-left"></span> Back to reader</button>'
|
||||
: '')
|
||||
+ '<button class="btn-secondary btn-sm" id="lib-chars-import" title="Import character cards from SillyTavern (.json or .png)"><span class="mdi mdi-import"></span> Import from SillyTavern</button>'
|
||||
+ '<button class="btn-secondary btn-sm" id="lib-chars-review-visual" title="Check profile and reference assets for stale metadata, duplicates, or reused images"><span class="mdi mdi-image-search-outline"></span> Review visual pairs</button>'
|
||||
+ '<label class="lib-chars-sort"><span class="mdi mdi-sort"></span> Sort'
|
||||
+ '<select id="lib-chars-sort-sel">' + SORT_OPTIONS.map(function (o) {
|
||||
return '<option value="' + o[0] + '"' + (o[0] === sortMode ? ' selected' : '') + '>' + o[1] + '</option>';
|
||||
@ -132,6 +167,9 @@ async function libraryRenderCharacters() {
|
||||
bar.querySelector('#lib-chars-import').addEventListener('click', function () {
|
||||
if (typeof stImportDialog === 'function') stImportDialog('', function () { libraryRenderCharacters(); });
|
||||
});
|
||||
bar.querySelector('#lib-chars-review-visual').addEventListener('click', function () {
|
||||
_libReviewVisualPairs(this, container);
|
||||
});
|
||||
bar.querySelector('#lib-chars-back-reader')?.addEventListener('click', function () {
|
||||
try { sessionStorage.removeItem('ttsvc_cast_return'); } catch (_) {}
|
||||
if (typeof navTo === 'function') navTo('s-reader');
|
||||
|
||||
@ -599,6 +599,8 @@ audio { width: 100%; }
|
||||
/* ── Characters / Cast view ─────────────────────────────────────────────────── */
|
||||
.lib-chars-toolbar { display:flex; gap:8px; margin-bottom:16px; flex-wrap:wrap; align-items:center; }
|
||||
.lib-chars-toolbar #lib-chars-back-reader { margin-right:4px; }
|
||||
.lib-chars-visual-review { margin:0 0 12px; padding:10px 12px; border:1px solid color-mix(in srgb, var(--accent) 35%, var(--border)); border-radius:8px; color:var(--subtext); line-height:1.5; }
|
||||
.lib-chars-review-item { display:inline-block; margin-left:10px; }
|
||||
.lib-chars-sort { display:flex; align-items:center; gap:6px; font-size:12.5px; color:var(--subtext); font-weight:600; }
|
||||
.lib-chars-sort select {
|
||||
padding:5px 8px; border-radius:6px; border:1px solid var(--border); background:var(--surface);
|
||||
|
||||
@ -15,6 +15,7 @@ const CHARACTERS = [
|
||||
|
||||
test('Characters / Cast supports profile sorting, column visibility, and voice rating', async ({ page }) => {
|
||||
await page.route('**/api/characters*', route => route.fulfill({ json: { characters: CHARACTERS } }));
|
||||
await page.route('**/api/characters/visual-pair-review', route => route.fulfill({ json: { ok: true, checked: 2, issues: [] } }));
|
||||
await page.route('**/api/voices*', route => route.fulfill({ json: VOICES }));
|
||||
await page.route('**/api/settings*', route => route.fulfill({ json: {} }));
|
||||
await page.route('**/api/voice/meta', route => route.fulfill({ json: { ok: true } }));
|
||||
@ -32,6 +33,8 @@ test('Characters / Cast supports profile sorting, column visibility, and voice r
|
||||
await expect(page.locator('#s-library')).toHaveClass(/is-active/, { timeout: 15_000 });
|
||||
const tableToggle = page.locator('.lib-chars-view-toggle button[data-view="table"]');
|
||||
await expect(tableToggle).toBeVisible({ timeout: 15_000 });
|
||||
await page.locator('#lib-chars-review-visual').click();
|
||||
await expect(page.locator('#lib-chars-visual-review')).toContainText('passed');
|
||||
// Switch through the same persisted view state used by the UI, then wait
|
||||
// for the asynchronous character rebuild to finish before asserting rows.
|
||||
await page.evaluate(() => { localStorage.setItem('ttsvc_libchars_view', 'table'); return window.libraryRenderCharacters(); });
|
||||
|
||||
Loading…
Reference in New Issue
Block a user