fix: release local preview object URLs
This commit is contained in:
parent
fc0361bc71
commit
47d6a5d247
@ -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.
|
||||
|
||||
3
TODO.md
3
TODO.md
@ -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.
|
||||
|
||||
|
||||
6
static/dist/main.min.js
vendored
6
static/dist/main.min.js
vendored
File diff suppressed because one or more lines are too long
@ -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);
|
||||
};
|
||||
|
||||
@ -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';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user