diff --git a/CHANGELOG.md b/CHANGELOG.md index 5aef859..28e1024 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 ### Added diff --git a/static/index.html b/static/index.html index aaca335..da15dfc 100644 --- a/static/index.html +++ b/static/index.html @@ -26,7 +26,7 @@ - + @@ -336,7 +336,7 @@ - + diff --git a/static/js/voice-library.js b/static/js/voice-library.js index 5a19434..537e79e 100644 --- a/static/js/voice-library.js +++ b/static/js/voice-library.js @@ -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) { switch(field) { case 'has_picture': return v.has_picture ? 1 : 0; @@ -84,7 +95,7 @@ function getSortValue(v, field) { case 'benchmark': return voiceBenchmarkElapsed(v) ?? 999999; case 'transcript': return (v.transcript || '').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 'tag': return (v.tag || '').toLowerCase(); case 'rating': return v.rating || 0; @@ -1940,7 +1951,7 @@ function makeVoiceRow(v) { ? `` : starsHtml} -
${escHtml(v.origin || '-')}
+
${escHtml(_displaySource(v) || '-')}
${v.seed != null ? '#' + v.seed : 'Auto'}
${window._voiceTableEditMode @@ -2035,6 +2046,13 @@ function makeVoiceRow(v) {
+
+ +
+ +
+
+
@@ -2839,6 +2857,16 @@ function makeVoiceRow(v) { await saveMeta(v.id, { note: v.note }); }, 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 const starSpans = wrap.querySelectorAll('.star'); starSpans.forEach(s => { @@ -3223,6 +3251,38 @@ $('vl-bulk-tag')?.addEventListener('click', () => { 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 = ' 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', () => { if (!_bulkSelected.size) return; const ids = [..._bulkSelected]; diff --git a/static/sections/s-voices.html b/static/sections/s-voices.html index f6d3474..e8e30be 100644 --- a/static/sections/s-voices.html +++ b/static/sections/s-voices.html @@ -93,6 +93,7 @@
+ diff --git a/static/style.css b/static/style.css index 9d72f11..091cc2f 100644 --- a/static/style.css +++ b/static/style.css @@ -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-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-ref, .vr-note { 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-note, .vr-source { display: flex; flex-direction: column; align-items: stretch; gap: 2px; } +.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:focus { border-bottom-color: var(--accent); color: var(--text); } .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: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:focus { border-bottom-color: var(--accent); } -.vr-note input::placeholder { color: var(--placehld); } +.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, .vr-source input:focus { border-bottom-color: var(--accent); } +.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-optimizer { grid-column: auto; display: none; margin: 6px 0 0; width: 100%; box-sizing: border-box; } .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-delete { 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 label { +.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-source label { font-size: 10px; font-weight: 700; color: var(--subtext); 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; font-size: 13.5px; color: var(--text); width: 100%; padding: 0; }