diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9713c4d..8d3d97f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,23 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
---
+## [1.12.19] — 2026-06-28
+
+### Fixed
+- **Voice list flicker on load** — engines.js (batch E) calls `loadVoiceLibrary()` after nav.js already rendered voices, causing the list to blank out and reload. Second call now runs silently (no skeleton, no status reset) when voices are already present.
+
+---
+
+## [1.12.18] — 2026-06-28
+
+### Fixed
+- **Diagnostic code removed** — temporary debug IIFE and renderVoiceList try-catch scaffolding cleaned out of voice-library.js; My Voices loads correctly.
+- **Settings load crash: renderSettingsAbout is not defined** — `loadSettings()` called `renderSettingsAbout()` unconditionally but that function lives in `conversation.js` (batch E, loaded after init). Guard added with `typeof` check; the About section still renders when opened (nav.js already guards the same call).
+- **Duplicate element IDs** — `s-library.html` Characters panel duplicated `cl-book-filter`, `cl-search`, `cl-grid` from `s-characters.html`, causing `getElementById` to return the wrong element. Characters tab in Library now shows a redirect placeholder instead.
+- **Infinite recursion in loadVoiceLibrary** — `window.loadVoiceLibrary = () => loadVoiceLibrary()` overwrites the global binding that the arrow function references, causing immediate stack overflow. Changed to direct assignment `window.loadVoiceLibrary = loadVoiceLibrary`.
+
+---
+
## [1.12.17] — 2026-06-28
### Fixed
diff --git a/VERSION b/VERSION
index 6dbba9c..ea90c61 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.12.17
+1.12.19
diff --git a/core/voice.py b/core/voice.py
index 2794690..1e9dcb5 100644
--- a/core/voice.py
+++ b/core/voice.py
@@ -65,11 +65,13 @@ def _is_sound_asset_file(path: Path) -> bool:
)
+_AUDIO_EXTS_SET = frozenset(_AUDIO_EXTS)
+
def _voice_audio_files(root: Path):
- for ext in _AUDIO_EXTS:
- for p in root.rglob(f"*{ext}"):
- if not _is_internal_voice_file(p) and not _is_sound_asset_file(p):
- yield p
+ # Single rglob pass instead of one per extension (6× faster directory scan)
+ for p in root.rglob("*"):
+ if p.suffix.lower() in _AUDIO_EXTS_SET and not _is_internal_voice_file(p) and not _is_sound_asset_file(p):
+ yield p
def _find_voice_audio(voice_id: str, scan_dir: Path) -> Path | None:
diff --git a/static/index.html b/static/index.html
index f39b4c6..f612459 100644
--- a/static/index.html
+++ b/static/index.html
@@ -26,7 +26,7 @@
-
+
@@ -336,7 +336,7 @@
-
+