feat: editable Source field + fish-audio fallback + bulk set source (v1.12.7)
- Add Source input to voice inspector panel (saves to origin in meta.json) - Auto-detect fish-audio voices via tag — SOURCE column now shows "fish-audio" for tagged voices even without an explicit origin field - Bulk "Set source" action in the multi-select toolbar - Source cell in table reflects the live-displayed value and updates immediately when changed via the inspector Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
04dee87710
commit
0bc3acc2ee
@ -9,6 +9,15 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [1.12.7] — 2026-06-27
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Editable Source field** in the voice inspector panel (below Note) — type a source label (e.g. `fish-audio`, `cloned`) and it saves immediately to the voice's meta.json. Changes now persist across refreshes.
|
||||||
|
- **Bulk "Set source"** button in the multi-select toolbar — select any number of voices and apply a source label to all at once.
|
||||||
|
- **Fish-audio tag fallback** in the SOURCE column: voices tagged `fish-audio` automatically show `fish-audio` as their source even without an explicit origin field, so the column is populated correctly without having to edit every voice.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [1.12.6] — 2026-06-27
|
## [1.12.6] — 2026-06-27
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
|
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
|
||||||
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
|
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
|
||||||
<link rel="stylesheet" href="/static/style.css?v=1.12.6">
|
<link rel="stylesheet" href="/static/style.css?v=1.12.7">
|
||||||
|
|
||||||
|
|
||||||
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
||||||
@ -336,7 +336,7 @@
|
|||||||
<script src="/static/vendor/wavesurfer-regions.min.js"></script>
|
<script src="/static/vendor/wavesurfer-regions.min.js"></script>
|
||||||
|
|
||||||
<!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
|
<!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
|
||||||
<script src="/static/loader.js?v=1.12.6"></script>
|
<script src="/static/loader.js?v=1.12.7"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -72,6 +72,17 @@ function initBenchmarkSampleControls() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns the display-level source label for a voice.
|
||||||
|
// Explicit origin field wins; fallback to any recognised tag value.
|
||||||
|
function _displaySource(v) {
|
||||||
|
if (v.origin) return v.origin;
|
||||||
|
if (v.tag) {
|
||||||
|
const tags = v.tag.split(',').map(t => t.trim().toLowerCase());
|
||||||
|
if (tags.includes('fish-audio') || tags.includes('fishaudio')) return 'fish-audio';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
function getSortValue(v, field) {
|
function getSortValue(v, field) {
|
||||||
switch(field) {
|
switch(field) {
|
||||||
case 'has_picture': return v.has_picture ? 1 : 0;
|
case 'has_picture': return v.has_picture ? 1 : 0;
|
||||||
@ -84,7 +95,7 @@ function getSortValue(v, field) {
|
|||||||
case 'benchmark': return voiceBenchmarkElapsed(v) ?? 999999;
|
case 'benchmark': return voiceBenchmarkElapsed(v) ?? 999999;
|
||||||
case 'transcript': return (v.transcript || '').toLowerCase();
|
case 'transcript': return (v.transcript || '').toLowerCase();
|
||||||
case 'note': return (v.note || '').toLowerCase();
|
case 'note': return (v.note || '').toLowerCase();
|
||||||
case 'source': return (v.origin || '').toLowerCase();
|
case 'source': return (_displaySource(v) || '').toLowerCase();
|
||||||
case 'seed': return v.seed != null ? v.seed : 9999999;
|
case 'seed': return v.seed != null ? v.seed : 9999999;
|
||||||
case 'tag': return (v.tag || '').toLowerCase();
|
case 'tag': return (v.tag || '').toLowerCase();
|
||||||
case 'rating': return v.rating || 0;
|
case 'rating': return v.rating || 0;
|
||||||
@ -1940,7 +1951,7 @@ function makeVoiceRow(v) {
|
|||||||
? `<input type="number" min="0" max="5" class="vl-inline-edit" data-field="rating" value="${v.rating||0}" style="width:40px">`
|
? `<input type="number" min="0" max="5" class="vl-inline-edit" data-field="rating" value="${v.rating||0}" style="width:40px">`
|
||||||
: starsHtml}
|
: starsHtml}
|
||||||
</div>
|
</div>
|
||||||
<div class="vl-tbl-source">${escHtml(v.origin || '-')}</div>
|
<div class="vl-tbl-source" title="Click to edit source">${escHtml(_displaySource(v) || '-')}</div>
|
||||||
<div class="vl-tbl-seed">${v.seed != null ? '#' + v.seed : 'Auto'}</div>
|
<div class="vl-tbl-seed">${v.seed != null ? '#' + v.seed : 'Auto'}</div>
|
||||||
<div class="vl-tbl-note" title="${escHtml(v.note||'')}">
|
<div class="vl-tbl-note" title="${escHtml(v.note||'')}">
|
||||||
${window._voiceTableEditMode
|
${window._voiceTableEditMode
|
||||||
@ -2035,6 +2046,13 @@ function makeVoiceRow(v) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="vr-source">
|
||||||
|
<label>Source</label>
|
||||||
|
<div class="vr-inline">
|
||||||
|
<input type="text" placeholder="e.g. fish-audio, cloned, designed…" value="${escHtml(v.origin||'')}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="vr-detail-active">
|
<div class="vr-detail-active">
|
||||||
<label>Active</label>
|
<label>Active</label>
|
||||||
<div class="vr-active-tools">
|
<div class="vr-active-tools">
|
||||||
@ -2839,6 +2857,16 @@ function makeVoiceRow(v) {
|
|||||||
await saveMeta(v.id, { note: v.note });
|
await saveMeta(v.id, { note: v.note });
|
||||||
}, 800));
|
}, 800));
|
||||||
|
|
||||||
|
// Source / origin (debounced save — also updates the table cell live)
|
||||||
|
const sourceInput = wrap.querySelector('.vr-source input');
|
||||||
|
sourceInput.addEventListener('input', debounce(async () => {
|
||||||
|
v.origin = sourceInput.value.trim();
|
||||||
|
await saveMeta(v.id, { origin: v.origin });
|
||||||
|
// Reflect in table row immediately
|
||||||
|
const tblCell = document.querySelector(`.vl-row[data-id="${CSS.escape(v.id)}"] .vl-tbl-source`);
|
||||||
|
if (tblCell) tblCell.textContent = _displaySource(v) || '-';
|
||||||
|
}, 800));
|
||||||
|
|
||||||
// Stars
|
// Stars
|
||||||
const starSpans = wrap.querySelectorAll('.star');
|
const starSpans = wrap.querySelectorAll('.star');
|
||||||
starSpans.forEach(s => {
|
starSpans.forEach(s => {
|
||||||
@ -3223,6 +3251,38 @@ $('vl-bulk-tag')?.addEventListener('click', () => {
|
|||||||
inp.focus();
|
inp.focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('vl-bulk-source')?.addEventListener('click', () => {
|
||||||
|
if (!_bulkSelected.size) return;
|
||||||
|
const ids = [..._bulkSelected];
|
||||||
|
const bar = $('vl-bulk-bar');
|
||||||
|
let inp = bar.querySelector('.vl-bulk-source-inp');
|
||||||
|
if (!inp) {
|
||||||
|
inp = document.createElement('input');
|
||||||
|
inp.type = 'text';
|
||||||
|
inp.className = 'vl-bulk-source-inp';
|
||||||
|
inp.placeholder = 'e.g. fish-audio, cloned…';
|
||||||
|
inp.autocomplete = 'off';
|
||||||
|
const applyBtn = document.createElement('button');
|
||||||
|
applyBtn.className = 'vl-bulk-btn';
|
||||||
|
applyBtn.innerHTML = '<span class="mdi mdi-check"></span> Apply';
|
||||||
|
applyBtn.addEventListener('click', async () => {
|
||||||
|
const val = inp.value.trim();
|
||||||
|
let done = 0;
|
||||||
|
for (const id of ids) {
|
||||||
|
await saveMeta(id, { origin: val }).catch(() => {});
|
||||||
|
done++;
|
||||||
|
}
|
||||||
|
toast(`Source set on ${done} voice${done !== 1 ? 's' : ''}`, 'success');
|
||||||
|
inp.remove(); applyBtn.remove();
|
||||||
|
_bulkSelected.clear();
|
||||||
|
await loadVoiceLibrary();
|
||||||
|
});
|
||||||
|
bar.appendChild(inp);
|
||||||
|
bar.appendChild(applyBtn);
|
||||||
|
}
|
||||||
|
inp.focus();
|
||||||
|
});
|
||||||
|
|
||||||
$('vl-bulk-rating')?.addEventListener('click', () => {
|
$('vl-bulk-rating')?.addEventListener('click', () => {
|
||||||
if (!_bulkSelected.size) return;
|
if (!_bulkSelected.size) return;
|
||||||
const ids = [..._bulkSelected];
|
const ids = [..._bulkSelected];
|
||||||
|
|||||||
@ -93,6 +93,7 @@
|
|||||||
<button class="vl-bulk-btn" id="vl-bulk-deselect" title="Clear selection"><span class="mdi mdi-checkbox-multiple-blank-outline"></span> None</button>
|
<button class="vl-bulk-btn" id="vl-bulk-deselect" title="Clear selection"><span class="mdi mdi-checkbox-multiple-blank-outline"></span> None</button>
|
||||||
<div class="vl-bulk-sep"></div>
|
<div class="vl-bulk-sep"></div>
|
||||||
<button class="vl-bulk-btn" id="vl-bulk-tag" title="Add / replace tag on selected voices"><span class="mdi mdi-tag-outline"></span> Set tag</button>
|
<button class="vl-bulk-btn" id="vl-bulk-tag" title="Add / replace tag on selected voices"><span class="mdi mdi-tag-outline"></span> Set tag</button>
|
||||||
|
<button class="vl-bulk-btn" id="vl-bulk-source" title="Set source / origin on selected voices"><span class="mdi mdi-database-outline"></span> Set source</button>
|
||||||
<button class="vl-bulk-btn" id="vl-bulk-hide" title="Disable (hide) selected voices"><span class="mdi mdi-eye-off-outline"></span> Hide</button>
|
<button class="vl-bulk-btn" id="vl-bulk-hide" title="Disable (hide) selected voices"><span class="mdi mdi-eye-off-outline"></span> Hide</button>
|
||||||
<button class="vl-bulk-btn" id="vl-bulk-unhide" title="Enable (unhide) selected voices"><span class="mdi mdi-eye-outline"></span> Unhide</button>
|
<button class="vl-bulk-btn" id="vl-bulk-unhide" title="Enable (unhide) selected voices"><span class="mdi mdi-eye-outline"></span> Unhide</button>
|
||||||
<button class="vl-bulk-btn" id="vl-bulk-rating" title="Set star rating on selected"><span class="mdi mdi-star-outline"></span> Rate</button>
|
<button class="vl-bulk-btn" id="vl-bulk-rating" title="Set star rating on selected"><span class="mdi mdi-star-outline"></span> Rate</button>
|
||||||
|
|||||||
@ -802,16 +802,16 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
|
|||||||
.vr-detail-active .vr-active-tools { justify-content: flex-end; width: 100%; }
|
.vr-detail-active .vr-active-tools { justify-content: flex-end; width: 100%; }
|
||||||
.vr-detail-delete { display: flex; align-items: start; justify-content: flex-end; min-height: 31px; padding-top: 18px; }
|
.vr-detail-delete { display: flex; align-items: start; justify-content: flex-end; min-height: 31px; padding-top: 18px; }
|
||||||
.vr-active-tools { display: flex; gap: 10px; align-items: center; }
|
.vr-active-tools { display: flex; gap: 10px; align-items: center; }
|
||||||
.vr-ref, .vr-note { display: flex; flex-direction: column; align-items: stretch; gap: 2px; }
|
.vr-ref, .vr-note, .vr-source { display: flex; flex-direction: column; align-items: stretch; gap: 2px; }
|
||||||
.vr-ref .vr-inline, .vr-note .vr-inline { display: flex; gap: 6px; align-items: center; min-width: 0; }
|
.vr-ref .vr-inline, .vr-note .vr-inline, .vr-source .vr-inline { display: flex; gap: 6px; align-items: center; min-width: 0; }
|
||||||
.vr-ref input { width: 100%; min-width: 0; background: transparent; border: none; border-bottom: 1px solid transparent; color: var(--subtext); font-size: 13px; padding: 3px 5px; font-family: inherit; outline: none; }
|
.vr-ref input { width: 100%; min-width: 0; background: transparent; border: none; border-bottom: 1px solid transparent; color: var(--subtext); font-size: 13px; padding: 3px 5px; font-family: inherit; outline: none; }
|
||||||
.vr-ref input:focus { border-bottom-color: var(--accent); color: var(--text); }
|
.vr-ref input:focus { border-bottom-color: var(--accent); color: var(--text); }
|
||||||
.vr-ref input::placeholder { color: var(--placehld); font-style: italic; }
|
.vr-ref input::placeholder { color: var(--placehld); font-style: italic; }
|
||||||
.ref-transcribe-btn { width: 28px; height: 28px; padding: 0; border-radius: 6px; background: none; border: 1px solid transparent; color: var(--subtext); flex-shrink: 0; font-size: 14px; }
|
.ref-transcribe-btn { width: 28px; height: 28px; padding: 0; border-radius: 6px; background: none; border: 1px solid transparent; color: var(--subtext); flex-shrink: 0; font-size: 14px; }
|
||||||
.ref-transcribe-btn:hover { background: var(--border); color: var(--text); filter: none; }
|
.ref-transcribe-btn:hover { background: var(--border); color: var(--text); filter: none; }
|
||||||
.vr-note input { width: 100%; background: transparent; border: none; border-bottom: 1px solid var(--border); color: var(--text); font-size: 13px; padding: 3px 5px; font-family: inherit; outline: none; }
|
.vr-note input, .vr-source input { width: 100%; background: transparent; border: none; border-bottom: 1px solid var(--border); color: var(--text); font-size: 13px; padding: 3px 5px; font-family: inherit; outline: none; }
|
||||||
.vr-note input:focus { border-bottom-color: var(--accent); }
|
.vr-note input:focus, .vr-source input:focus { border-bottom-color: var(--accent); }
|
||||||
.vr-note input::placeholder { color: var(--placehld); }
|
.vr-note input::placeholder, .vr-source input::placeholder { color: var(--placehld); }
|
||||||
.vr-row-subtle { color: var(--subtext); font-size: 14px; font-family: monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
.vr-row-subtle { color: var(--subtext); font-size: 14px; font-family: monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
.vr-optimizer { grid-column: auto; display: none; margin: 6px 0 0; width: 100%; box-sizing: border-box; }
|
.vr-optimizer { grid-column: auto; display: none; margin: 6px 0 0; width: 100%; box-sizing: border-box; }
|
||||||
.vl-row.edit-open .vr-optimizer { display: block; }
|
.vl-row.edit-open .vr-optimizer { display: block; }
|
||||||
@ -1952,12 +1952,12 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
|
|||||||
.inspector-body .vr-detail-row .vr-detail-active,
|
.inspector-body .vr-detail-row .vr-detail-active,
|
||||||
.inspector-body .vr-detail-row .vr-detail-delete { display: none !important; }
|
.inspector-body .vr-detail-row .vr-detail-delete { display: none !important; }
|
||||||
.inspector-body .vr-detail-row .vr-ref { display: none !important; }
|
.inspector-body .vr-detail-row .vr-ref { display: none !important; }
|
||||||
.inspector-body .vr-note { padding: 10px 14px; display: flex; flex-direction: column; gap: 5px; }
|
.inspector-body .vr-note, .inspector-body .vr-source { padding: 10px 14px; display: flex; flex-direction: column; gap: 5px; }
|
||||||
.inspector-body .vr-note label {
|
.inspector-body .vr-note label, .inspector-body .vr-source label {
|
||||||
font-size: 10px; font-weight: 700; color: var(--subtext);
|
font-size: 10px; font-weight: 700; color: var(--subtext);
|
||||||
text-transform: uppercase; letter-spacing: .07em;
|
text-transform: uppercase; letter-spacing: .07em;
|
||||||
}
|
}
|
||||||
.inspector-body .vr-note input {
|
.inspector-body .vr-note input, .inspector-body .vr-source input {
|
||||||
background: transparent; border: none; outline: none;
|
background: transparent; border: none; outline: none;
|
||||||
font-size: 13.5px; color: var(--text); width: 100%; padding: 0;
|
font-size: 13.5px; color: var(--text); width: 100%; padding: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user