fix: release local preview object URLs

This commit is contained in:
mARTin-B78 2026-09-04 08:11:32 +02:00
parent fc0361bc71
commit 47d6a5d247
5 changed files with 23 additions and 8 deletions

View File

@ -55,6 +55,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
original temporary result; reusing a key for different request data is rejected.
- **Casting Idempotency.** Character-to-voice matching now applies the same bounded, expiring idempotency contract,
preventing repeated clicks from issuing duplicate LLM casting requests.
- **Preview Blob Lifecycle.** Character-library and Rehearser local voice previews now revoke replaced or completed
object URLs, reducing retained browser memory during repeated auditioning.
- **Security Regression Coverage.** Added a Python test gate for character metadata validation, private-download URL
rejection and oversized JSON request rejection.
- **Navigation Reliability.** Rehearser phase requests now survive the feature-module initialization boundary.

View File

@ -111,7 +111,8 @@ the first block before investing in architecture or polish.
- [ ] Update only affected table rows instead of rebuilding entire tables.
- [ ] Add an LRU cache for audio and images and remove old entries automatically. Persisted line-audio is now bounded;
image-cache eviction remains.
- [x] Revoke discarded object URLs with `URL.revokeObjectURL(objectUrl)`; completed synthesized voice previews now release their Blob URLs when playback ends.
- [x] Revoke discarded object URLs with `URL.revokeObjectURL(objectUrl)`; completed synthesized voice previews and
character/Rehearser local auditions now release their Blob URLs when playback ends or is replaced.
- [x] Add low-memory mode using `navigator.deviceMemory` where available; shared worker pools cap at two tasks on devices reporting approximately 4 GB RAM or less.
- [ ] Disable image preloading and reduce table/cache sizes on devices with approximately 4 GB RAM.

File diff suppressed because one or more lines are too long

View File

@ -2705,6 +2705,7 @@ function _confirmVoiceReuse(rec, reuse) {
+ '</div>';
document.body.appendChild(ov);
let audioEl = null;
let audioObjectUrl = null;
ov.querySelector('#cvr-play').addEventListener('click', async function (e) {
const btn = e.currentTarget;
const icon = btn.querySelector('.mdi');
@ -2720,8 +2721,16 @@ function _confirmVoiceReuse(rec, reuse) {
const rv = (window._voices || []).find(x => x.id === reuse.voiceId);
const rBackend = (rv && !rv.has_ref) ? 'voice_design' : 'voice_clone';
const blob = await fetchTtsPreviewBlob(reuse.voiceId, text, 'wav', '', rBackend);
if (!audioEl) { audioEl = new Audio(); audioEl.addEventListener('ended', function () { icon.className = 'mdi mdi-play'; }); }
audioEl.src = URL.createObjectURL(blob);
if (!audioEl) {
audioEl = new Audio();
audioEl.addEventListener('ended', function () {
icon.className = 'mdi mdi-play';
if (audioObjectUrl) { URL.revokeObjectURL(audioObjectUrl); audioObjectUrl = null; }
});
}
if (audioObjectUrl) URL.revokeObjectURL(audioObjectUrl);
audioObjectUrl = URL.createObjectURL(blob);
audioEl.src = audioObjectUrl;
await audioEl.play();
icon.className = 'mdi mdi-pause';
} catch (err) { toast('Could not play sample: ' + (err.message || err), 'error'); icon.className = 'mdi mdi-play'; }
@ -2729,6 +2738,7 @@ function _confirmVoiceReuse(rec, reuse) {
});
const cleanup = function (result) {
if (audioEl) audioEl.pause();
if (audioObjectUrl) { URL.revokeObjectURL(audioObjectUrl); audioObjectUrl = null; }
ov.remove();
resolve(result);
};

View File

@ -1064,9 +1064,10 @@ function castAvatarHtml(sp) {
}
// ── Online voice picker: audition a sample, browse alternatives, agree/switch ──
let _rehAudEl = null, _rehAudBtn = null;
let _rehAudEl = null, _rehAudBtn = null, _rehAudObjectUrl = null;
function _rehStopAudition() {
if (_rehAudEl) { try { _rehAudEl.pause(); } catch (_) {} }
if (_rehAudObjectUrl) { URL.revokeObjectURL(_rehAudObjectUrl); _rehAudObjectUrl = null; }
if (_rehAudBtn) { _rehAudBtn.classList.remove('playing'); const i = _rehAudBtn.querySelector('.mdi'); if (i) i.className = 'mdi mdi-play'; }
_rehAudBtn = null;
}
@ -1151,7 +1152,8 @@ async function _rehPreviewLocal(voiceId, btn) {
try {
const blob = await fetchTtsPreviewBlob(voiceId, _REH_PREVIEW_TEXT, 'wav', '', backend);
if (!_rehAudEl) { _rehAudEl = new Audio(); _rehAudEl.addEventListener('ended', _rehStopAudition); }
_rehAudEl.src = URL.createObjectURL(blob);
_rehAudObjectUrl = URL.createObjectURL(blob);
_rehAudEl.src = _rehAudObjectUrl;
await _rehAudEl.play();
btn.classList.remove('loading'); _rehAudBtn = btn; btn.classList.add('playing');
if (icon) icon.className = 'mdi mdi-stop';