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.
|
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,
|
- **Casting Idempotency.** Character-to-voice matching now applies the same bounded, expiring idempotency contract,
|
||||||
preventing repeated clicks from issuing duplicate LLM casting requests.
|
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
|
- **Security Regression Coverage.** Added a Python test gate for character metadata validation, private-download URL
|
||||||
rejection and oversized JSON request rejection.
|
rejection and oversized JSON request rejection.
|
||||||
- **Navigation Reliability.** Rehearser phase requests now survive the feature-module initialization boundary.
|
- **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.
|
- [ ] 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;
|
- [ ] Add an LRU cache for audio and images and remove old entries automatically. Persisted line-audio is now bounded;
|
||||||
image-cache eviction remains.
|
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.
|
- [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.
|
- [ ] 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>';
|
+ '</div>';
|
||||||
document.body.appendChild(ov);
|
document.body.appendChild(ov);
|
||||||
let audioEl = null;
|
let audioEl = null;
|
||||||
|
let audioObjectUrl = null;
|
||||||
ov.querySelector('#cvr-play').addEventListener('click', async function (e) {
|
ov.querySelector('#cvr-play').addEventListener('click', async function (e) {
|
||||||
const btn = e.currentTarget;
|
const btn = e.currentTarget;
|
||||||
const icon = btn.querySelector('.mdi');
|
const icon = btn.querySelector('.mdi');
|
||||||
@ -2720,8 +2721,16 @@ function _confirmVoiceReuse(rec, reuse) {
|
|||||||
const rv = (window._voices || []).find(x => x.id === reuse.voiceId);
|
const rv = (window._voices || []).find(x => x.id === reuse.voiceId);
|
||||||
const rBackend = (rv && !rv.has_ref) ? 'voice_design' : 'voice_clone';
|
const rBackend = (rv && !rv.has_ref) ? 'voice_design' : 'voice_clone';
|
||||||
const blob = await fetchTtsPreviewBlob(reuse.voiceId, text, 'wav', '', rBackend);
|
const blob = await fetchTtsPreviewBlob(reuse.voiceId, text, 'wav', '', rBackend);
|
||||||
if (!audioEl) { audioEl = new Audio(); audioEl.addEventListener('ended', function () { icon.className = 'mdi mdi-play'; }); }
|
if (!audioEl) {
|
||||||
audioEl.src = URL.createObjectURL(blob);
|
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();
|
await audioEl.play();
|
||||||
icon.className = 'mdi mdi-pause';
|
icon.className = 'mdi mdi-pause';
|
||||||
} catch (err) { toast('Could not play sample: ' + (err.message || err), 'error'); icon.className = 'mdi mdi-play'; }
|
} 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) {
|
const cleanup = function (result) {
|
||||||
if (audioEl) audioEl.pause();
|
if (audioEl) audioEl.pause();
|
||||||
|
if (audioObjectUrl) { URL.revokeObjectURL(audioObjectUrl); audioObjectUrl = null; }
|
||||||
ov.remove();
|
ov.remove();
|
||||||
resolve(result);
|
resolve(result);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1064,9 +1064,10 @@ function castAvatarHtml(sp) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Online voice picker: audition a sample, browse alternatives, agree/switch ──
|
// ── Online voice picker: audition a sample, browse alternatives, agree/switch ──
|
||||||
let _rehAudEl = null, _rehAudBtn = null;
|
let _rehAudEl = null, _rehAudBtn = null, _rehAudObjectUrl = null;
|
||||||
function _rehStopAudition() {
|
function _rehStopAudition() {
|
||||||
if (_rehAudEl) { try { _rehAudEl.pause(); } catch (_) {} }
|
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'; }
|
if (_rehAudBtn) { _rehAudBtn.classList.remove('playing'); const i = _rehAudBtn.querySelector('.mdi'); if (i) i.className = 'mdi mdi-play'; }
|
||||||
_rehAudBtn = null;
|
_rehAudBtn = null;
|
||||||
}
|
}
|
||||||
@ -1151,7 +1152,8 @@ async function _rehPreviewLocal(voiceId, btn) {
|
|||||||
try {
|
try {
|
||||||
const blob = await fetchTtsPreviewBlob(voiceId, _REH_PREVIEW_TEXT, 'wav', '', backend);
|
const blob = await fetchTtsPreviewBlob(voiceId, _REH_PREVIEW_TEXT, 'wav', '', backend);
|
||||||
if (!_rehAudEl) { _rehAudEl = new Audio(); _rehAudEl.addEventListener('ended', _rehStopAudition); }
|
if (!_rehAudEl) { _rehAudEl = new Audio(); _rehAudEl.addEventListener('ended', _rehStopAudition); }
|
||||||
_rehAudEl.src = URL.createObjectURL(blob);
|
_rehAudObjectUrl = URL.createObjectURL(blob);
|
||||||
|
_rehAudEl.src = _rehAudObjectUrl;
|
||||||
await _rehAudEl.play();
|
await _rehAudEl.play();
|
||||||
btn.classList.remove('loading'); _rehAudBtn = btn; btn.classList.add('playing');
|
btn.classList.remove('loading'); _rehAudBtn = btn; btn.classList.add('playing');
|
||||||
if (icon) icon.className = 'mdi mdi-stop';
|
if (icon) icon.className = 'mdi mdi-stop';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user