Fix Apply effects button staying disabled after streaming preview (v1.14.4)

Streaming playback never produces an in-memory blob, but the enable
logic required one. Now enables unconditionally and lazily
re-synthesizes a buffered blob on demand if effects are applied before
one exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-07-08 21:26:23 +02:00
parent bfd1bfb939
commit 939d33f7cc
5 changed files with 28 additions and 9 deletions

View File

@ -9,6 +9,13 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
--- ---
## [1.14.4] — 2026-07-08
### Fixed
- **"Apply effects" button stayed disabled after generating a preview** — streaming playback (the default, lower-latency mode) never produces an in-memory audio blob, and the button's enable logic required one. It now enables regardless of playback mode; if effects are applied before a buffered blob exists, one is synthesized on demand from the same voice/text/instruct/backend used for the preview.
---
## [1.14.3] — 2026-07-06 ## [1.14.3] — 2026-07-06
### Added ### Added

View File

@ -1 +1 @@
1.14.3 1.14.4

View File

@ -10,7 +10,7 @@
<meta name="format-detection" content="telephone=no"> <meta name="format-detection" content="telephone=no">
<meta name="color-scheme" content="light dark"> <meta name="color-scheme" content="light dark">
<meta name="theme-color" content="#2563EB"> <meta name="theme-color" content="#2563EB">
<meta name="app-version" content="1.14.3"> <meta name="app-version" content="1.14.4">
<link rel="manifest" href="/manifest.webmanifest"> <link rel="manifest" href="/manifest.webmanifest">
<link rel="icon" href="/static/icon.svg" type="image/svg+xml"> <link rel="icon" href="/static/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/static/icon.svg"> <link rel="apple-touch-icon" href="/static/icon.svg">
@ -27,7 +27,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.14.3"> <link rel="stylesheet" href="/static/style.css?v=1.14.4">
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── --> <!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
@ -365,7 +365,7 @@ window.toggleNavTree = function(treeId, chevronId) {
</script> </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.14.3"></script> <script src="/static/loader.js?v=1.14.4"></script>
</body> </body>
</html> </html>

View File

@ -339,8 +339,6 @@ $('effects-reset-btn')?.addEventListener('click', () => {
let _effectsSourceBlob = null; let _effectsSourceBlob = null;
$('effects-apply-btn')?.addEventListener('click', async () => { $('effects-apply-btn')?.addEventListener('click', async () => {
const blob = previewBlob || _effectsSourceBlob;
if (!blob) { toast('Generate audio first', 'error'); return; }
const chain = []; const chain = [];
if ($('fx-reverb-on')?.checked) chain.push({ type:'reverb', params: { room_size: +$('fx-reverb-room').value, wet: +$('fx-reverb-wet').value, dry: 1 - +$('fx-reverb-wet').value } }); if ($('fx-reverb-on')?.checked) chain.push({ type:'reverb', params: { room_size: +$('fx-reverb-room').value, wet: +$('fx-reverb-wet').value, dry: 1 - +$('fx-reverb-wet').value } });
if ($('fx-compressor-on')?.checked) chain.push({ type:'compressor', params: { threshold_db: +$('fx-comp-thresh').value, ratio: +$('fx-comp-ratio').value } }); if ($('fx-compressor-on')?.checked) chain.push({ type:'compressor', params: { threshold_db: +$('fx-comp-thresh').value, ratio: +$('fx-comp-ratio').value } });
@ -350,6 +348,13 @@ $('effects-apply-btn')?.addEventListener('click', async () => {
const btn = $('effects-apply-btn'), st = $('effects-status'); const btn = $('effects-apply-btn'), st = $('effects-status');
btn.disabled = true; if (st) st.textContent = 'Processing…'; btn.disabled = true; if (st) st.textContent = 'Processing…';
try { try {
let blob = previewBlob || _effectsSourceBlob;
if (!blob) {
const args = window._effectsSynthArgs;
if (!args || typeof fetchTtsPreviewBlob !== 'function') { toast('Generate audio first', 'error'); return; }
if (st) st.textContent = 'Synthesizing audio…';
blob = await fetchTtsPreviewBlob(args.voice, args.text, 'wav', args.instruct, args.backend);
}
const fd = new FormData(); const fd = new FormData();
fd.append('audio', blob, 'audio.wav'); fd.append('audio', blob, 'audio.wav');
fd.append('effects', JSON.stringify(chain)); fd.append('effects', JSON.stringify(chain));

View File

@ -239,9 +239,16 @@ $('preview-text-area').addEventListener('focus', () => {
if ($('preview-text-area').value === PREVIEW_SAMPLE_TEXT) $('preview-text-area').value = ''; if ($('preview-text-area').value === PREVIEW_SAMPLE_TEXT) $('preview-text-area').value = '';
}, { once:true }); }, { once:true });
function _onPreviewGenerated(source, voice, text, backend) { function _onPreviewGenerated(source, voice, text, backend, instruct) {
if (typeof effectsSourceBlob !== 'undefined') window._effectsSourceBlob = null; if (typeof effectsSourceBlob !== 'undefined') window._effectsSourceBlob = null;
const ea = $('effects-apply-btn'); if (ea && source.blob) ea.disabled = false; // Streaming playback (the default, lower-latency path) never produces a
// blob — audio plays via MSE without ever being fully buffered client-side
// — so gating "Apply effects" on source.blob left it permanently disabled
// whenever streaming succeeded, which is the common case. Enable it
// regardless; the effects handler re-fetches a buffered blob on demand
// (see window._effectsSynthArgs) if one isn't already sitting in memory.
window._effectsSynthArgs = { voice, text, instruct: instruct || '', backend };
const ea = $('effects-apply-btn'); if (ea) ea.disabled = false;
const ap = $('add-to-playlist-btn'); if (ap && source.blob) ap.disabled = false; const ap = $('add-to-playlist-btn'); if (ap && source.blob) ap.disabled = false;
if (typeof historyPush === 'function' && source.blob) historyPush(voice, text, backend, source.blob, source.url); if (typeof historyPush === 'function' && source.blob) historyPush(voice, text, backend, source.blob, source.url);
} }
@ -279,7 +286,7 @@ $('preview-btn').addEventListener('click', async () => {
await audio.play(); await audio.play();
$('save-preview-mp3-btn').disabled = false; $('save-preview-mp3-btn').disabled = false;
$('save-preview-btn').disabled = source.streaming; $('save-preview-btn').disabled = source.streaming;
_onPreviewGenerated(source, voice, text, backend); _onPreviewGenerated(source, voice, text, backend, instruct);
toast(source.streaming ? 'Streaming preview playing' : source.label === 'chunked' ? `Chunked (${text.length} chars) playing` : 'Preview playing', 'success'); toast(source.streaming ? 'Streaming preview playing' : source.label === 'chunked' ? `Chunked (${text.length} chars) playing` : 'Preview playing', 'success');
} catch(e) { toast('TTS failed: '+e.message,'error'); } } catch(e) { toast('TTS failed: '+e.message,'error'); }
finally { $('preview-btn').disabled=false; } finally { $('preview-btn').disabled=false; }