From bfd1bfb9390564a16cfc7e15ed170a2077a096db Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Wed, 8 Jul 2026 21:05:45 +0200 Subject: [PATCH] Add routing voice-picker, restore voice filters, fix pedalboard (v1.14.3) - App Routing's output-voice field gets the same searchable avatar-thumbnail dropdown used elsewhere, as a browse button alongside the existing free-text input (which must stay editable to target vd_ Voice Design presets not in the voice library). - My Voices table: Gender and Rating filter dropdowns existed in the JS (populateLibraryFilters, libraryFilterMatch) but their ` elements had been left out of the visible layout after an earlier redesign (silently no-op'ing the whole filter-population function, which also broke the already-visible Language/Type dropdowns). Added the missing controls plus new Tag and Group dropdowns wired to the same filter state the sidebar's group/tag chips already use. + +### Fixed +- **Audio effects (reverb/compressor/chorus/pitch) failed with "pedalboard is not installed"** even though it was — `pedalboard`'s native extension links against `libatomic.so.1`, an OS-level shared library missing from the container image. Added `libatomic1` to the Dockerfile and rebuilt. + +### Changed +- **"edit ID" relabeled to make clear it's literally the filename** — the voice inspector's ID rename control already renamed the underlying `.wav`/`.meta.json`/`.reference.txt`/picture files on disk (via the existing `/api/voice/rename` endpoint); it just wasn't obvious "ID" meant "filename." Relabeled to "rename filename" / "copy filename" with clearer tooltips. + +--- + ## [1.14.2] — 2026-07-06 ### Fixed diff --git a/Dockerfile b/Dockerfile index b936a75..7fd1f95 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,10 +10,14 @@ ENV DEBIAN_FRONTEND=noninteractive ENV PYTHONUNBUFFERED=1 # ── System packages ─────────────────────────────────────────────────────────── +# libatomic1: pedalboard's native extension (pedalboard_native) links against +# libatomic.so.1 and fails to import without it — pip installs the Python +# package fine, but the C extension silently can't load at runtime. RUN apt-get update && apt-get install -y --no-install-recommends \ ffmpeg \ curl \ ca-certificates \ + libatomic1 \ && rm -rf /var/lib/apt/lists/* # ── Python dependencies ─────────────────────────────────────────────────────── diff --git a/VERSION b/VERSION index a4cc557..4ea8ad8 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.14.2 +1.14.3 diff --git a/static/index.html b/static/index.html index 2d4a10d..2d5a6e4 100644 --- a/static/index.html +++ b/static/index.html @@ -10,7 +10,7 @@ - + @@ -27,7 +27,7 @@ - + @@ -365,7 +365,7 @@ window.toggleNavTree = function(treeId, chevronId) { - + diff --git a/static/js/routing.js b/static/js/routing.js index 0e2dfef..ff0463a 100644 --- a/static/js/routing.js +++ b/static/js/routing.js @@ -200,7 +200,7 @@ function renderRoutingList() { - +
@@ -214,6 +214,12 @@ function renderRoutingList() {
`).join(''); + // Same searchable avatar dropdown used for voice selection elsewhere in the + // app — the field itself stays a free-text input (routes can also target + // vd_ Voice Design presets, which aren't part of the voice library). + if (typeof VoicePicker !== 'undefined') { + document.querySelectorAll('.route-output').forEach(inp => VoicePicker.attachTextPicker(inp)); + } } function readRoutingForm() { diff --git a/static/js/voice-inspector.js b/static/js/voice-inspector.js index d371344..a0142a0 100644 --- a/static/js/voice-inspector.js +++ b/static/js/voice-inspector.js @@ -98,9 +98,9 @@ function selectVoice(wrap) {
- ${escHtml(voiceId)} - - + ${escHtml(voiceId)} + +
diff --git a/static/js/voice-library.js b/static/js/voice-library.js index 2a41e34..5ea0f80 100644 --- a/static/js/voice-library.js +++ b/static/js/voice-library.js @@ -308,32 +308,45 @@ function populateLibraryFilters() { const langSel = $('library-filter-lang'); const sexSel = $('library-filter-sex'); const typeSel = $('library-filter-type'); + const tagSel = $('library-filter-tag'); + const groupSel = $('library-filter-group'); if (!langSel || !sexSel || !typeSel) return; const langSet = new Set(); const sexSet = new Set(); const typeSet = new Set(); + const tagSet = new Set(); + const groupSet = new Set(); (_voices || []).forEach(v => { const lang = libraryVoiceLang(v); if (lang) langSet.add(lang); if (v.gender) sexSet.add(v.gender); const type = voiceFileType(v); if (type) typeSet.add(type); + String(v.tag || '').split(',').map(t => t.trim()).filter(Boolean).forEach(t => tagSet.add(t)); + const g = (v.group || '').trim(); + if (g) groupSet.add(g); }); const langs = [...langSet].sort((a,b) => libraryLanguageLabel(a).localeCompare(libraryLanguageLabel(b))); const sexOrder = ['F','M','N']; const sexes = [...sexSet] .sort((a, b) => (sexOrder.indexOf(a) < 0 ? 99 : sexOrder.indexOf(a)) - (sexOrder.indexOf(b) < 0 ? 99 : sexOrder.indexOf(b))); const types = [...typeSet].sort(); - const sig = JSON.stringify([langs, sexes, types]); + const tags = [...tagSet].sort((a, b) => a.localeCompare(b)); + const groups = [...groupSet].sort((a, b) => a.localeCompare(b)); + const sig = JSON.stringify([langs, sexes, types, tags, groups]); if (sig === _libraryFilterOptionsSig) return; _libraryFilterOptionsSig = sig; - const keep = {lang: langSel.value, sex: sexSel.value, type: typeSel.value}; + const keep = {lang: langSel.value, sex: sexSel.value, type: typeSel.value, tag: tagSel?.value, group: groupSel?.value}; langSel.innerHTML = '' + langs.map(x => ``).join(''); sexSel.innerHTML = '' + sexes.map(x => ``).join(''); typeSel.innerHTML = '' + types.map(x => ``).join(''); + if (tagSel) tagSel.innerHTML = '' + tags.map(x => ``).join(''); + if (groupSel) groupSel.innerHTML = '' + groups.map(x => ``).join(''); langSel.value = langs.includes(keep.lang) ? keep.lang : ''; sexSel.value = sexes.includes(keep.sex) ? keep.sex : ''; typeSel.value = types.includes(keep.type) ? keep.type : ''; + if (tagSel) tagSel.value = tags.includes(keep.tag) ? keep.tag : ''; + if (groupSel) groupSel.value = groups.includes(keep.group) ? keep.group : ''; } function readLibraryFilters() { @@ -1644,7 +1657,16 @@ $('show-disabled-cb').addEventListener('change', () => { $(id)?.addEventListener('change', () => { readLibraryFilters(); renderVoiceList(); }); }); $('library-filter-text')?.addEventListener('input', debounce(() => { readLibraryFilters(); renderVoiceList(); }, 180)); -$('library-clear-filters')?.addEventListener('click', clearLibraryFilters); +// Tag/group use the same globals the sidebar chips already set (window._voiceTagFilter / +// _voiceGroupFilter), so both entry points stay in sync with whichever was used last. +$('library-filter-tag')?.addEventListener('change', function () { window._voiceTagFilter = this.value || null; renderVoiceList(); }); +$('library-filter-group')?.addEventListener('change', function () { window._voiceGroupFilter = this.value || null; renderVoiceList(); }); +$('library-clear-filters')?.addEventListener('click', () => { + window._voiceTagFilter = null; window._voiceGroupFilter = null; + const tagSel = $('library-filter-tag'), groupSel = $('library-filter-group'); + if (tagSel) tagSel.value = ''; if (groupSel) groupSel.value = ''; + clearLibraryFilters(); +}); $('library-tts-backend-select')?.addEventListener('change', () => { status('Library TTS engine: ' + (backendById(libraryTtsBackend())?.label || libraryTtsBackend())); }); function renderVoiceList() { @@ -3231,6 +3253,7 @@ function renderVoiceGroupsBar() { if (e.target.closest('.vg-del')) return; const g = chip.dataset.group; window._voiceGroupFilter = (window._voiceGroupFilter === g) ? '' : g; + const groupSel = $('library-filter-group'); if (groupSel) groupSel.value = window._voiceGroupFilter || ''; renderVoiceList(); }); }); @@ -3238,7 +3261,9 @@ function renderVoiceGroupsBar() { btn.addEventListener('click', e => { e.stopPropagation(); deleteVoiceGroup(btn.dataset.group); }); }); bar.querySelector('.vg-clear')?.addEventListener('click', () => { - window._voiceGroupFilter = ''; renderVoiceList(); + window._voiceGroupFilter = ''; + const groupSel = $('library-filter-group'); if (groupSel) groupSel.value = ''; + renderVoiceList(); }); } diff --git a/static/js/voice-picker.js b/static/js/voice-picker.js index 7f99fa4..dca33b8 100644 --- a/static/js/voice-picker.js +++ b/static/js/voice-picker.js @@ -266,5 +266,71 @@ _pickers[selectId]?.syncTrigger(); } - window.VoicePicker = { upgrade, populate, getValue, setValue }; + // Same searchable avatar dropdown as upgrade(), but anchored to a plain + // text instead of replacing a
`; + document.body.appendChild(drop); + const searchInp = drop.querySelector('.vp-search'); + const list = drop.querySelector('.vp-list'); + + function _voices() { return (window._voices || []).filter(v => v.enabled !== false); } + + function _renderList(filter) { + const q = (filter || '').toLowerCase(); + const filtered = q ? _voices().filter(v => v.id.toLowerCase().includes(q)) : _voices(); + if (!filtered.length) { list.innerHTML = '
No voices found
'; return; } + list.innerHTML = filtered.map(v => _buildItem(v.id, v.id)).join(''); + list.querySelectorAll('.vp-item').forEach(el => el.classList.toggle('selected', el.dataset.value === input.value)); + } + + function _position() { + const r = input.getBoundingClientRect(); + drop.style.left = r.left + 'px'; + drop.style.top = (r.bottom + 4) + 'px'; + drop.style.width = Math.max(240, r.width + 32) + 'px'; + } + + function _open() { + _position(); + drop.hidden = false; + searchInp.value = ''; + _renderList(''); + searchInp.focus(); + } + function _close() { drop.hidden = true; } + + function _select(value) { + input.value = value; + input.dispatchEvent(new Event('input', { bubbles: true })); + input.dispatchEvent(new Event('change', { bubbles: true })); + _close(); + } + + btn.addEventListener('click', (e) => { e.stopPropagation(); drop.hidden ? _open() : _close(); }); + searchInp.addEventListener('input', () => _renderList(searchInp.value)); + searchInp.addEventListener('keydown', e => { if (e.key === 'Escape') { e.stopPropagation(); _close(); } }); + list.addEventListener('click', e => { const item = e.target.closest('.vp-item'); if (item) _select(item.dataset.value); }); + document.addEventListener('click', e => { if (!drop.contains(e.target) && e.target !== btn) _close(); }, true); + window.addEventListener('resize', () => { if (!drop.hidden) _position(); }); + } + + window.VoicePicker = { upgrade, populate, getValue, setValue, attachTextPicker }; })(); diff --git a/static/sections/s-voices.html b/static/sections/s-voices.html index d60db09..394ec13 100644 --- a/static/sections/s-voices.html +++ b/static/sections/s-voices.html @@ -51,7 +51,20 @@
+ + + + +
@@ -151,9 +164,6 @@