diff --git a/CHANGELOG.md b/CHANGELOG.md index 7850664..59f0b59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi --- +## [1.20.40] — 2026-08-19 + +### Added +- **Character profiles now show the generated voice filename and a play button** when the assigned voice has a saved local audio file. Playback reuses the existing voice-file preview and does not synthesize a new sample. + ## [1.20.39] — 2026-08-19 ### Changed diff --git a/VERSION b/VERSION index df40f00..3af842e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.20.39 +1.20.40 diff --git a/static/index.html b/static/index.html index 7cdb9f4..c80bc92 100644 --- a/static/index.html +++ b/static/index.html @@ -10,7 +10,7 @@ - + @@ -27,7 +27,7 @@ - + @@ -378,7 +378,7 @@ window.toggleNavTree = function(treeId, chevronId) { - + diff --git a/static/js/library-characters.js b/static/js/library-characters.js index 35b7fb4..45a4a18 100644 --- a/static/js/library-characters.js +++ b/static/js/library-characters.js @@ -787,6 +787,19 @@ async function _libPreviewCharVoice(rec, btn) { } } +// Return the actual saved audio filename for a character's assigned voice. +// A voice id alone is not enough: online/unresolved assignments can exist +// without a local WAV, so the profile only advertises a file when the library +// metadata confirms that one is available. +function _libGeneratedVoiceFileName(rec) { + const voiceId = rec?.voice ? (typeof rec.voice === 'object' ? (rec.voice.id || '') : String(rec.voice)) : ''; + if (!voiceId) return ''; + const voice = (window._voices || []).find(v => v && v.id === voiceId); + if (!voice?.path) return ''; + const raw = String(voice.path).split(/[\\/]/).pop().trim(); + return raw || ''; +} + // A character can keep pointing at a voice id that was since deleted // straight from the Voice Library (confirmed live: deleting broken designed // voices there left them still listed, un-flagged, on every character card). @@ -1210,6 +1223,15 @@ async function _charDetailPage(rec, allChars, opts) { const promptsHtml = '
' + '
Generation Prompts
' + _lcdPromptBox('Voice Design Prompt', sh.voice_design_prompt, 'voice_design_prompt') + + (function () { + const fileName = _libGeneratedVoiceFileName(rec); + return fileName + ? '
' + + '' + + ' Voice file: ' + escHtml(fileName) + '' + + '
' + : ''; + }()) + _lcdPromptBox('Character Image Prompt', sh.image_prompt, 'image_prompt') + _lcdPromptBox('SillyTavern Character Prompt', sh.silly_tavern_prompt, 'silly_tavern_prompt') + _lcdPromptBox('Concept Art Prompt', sh.concept_art_prompt, 'concept_art_prompt') @@ -1454,6 +1476,11 @@ async function _charDetailPage(rec, allChars, opts) { e.stopPropagation(); _libPreviewCharVoice(rec, e.currentTarget); }); + pg.querySelector('.lcd-generated-voice-play')?.addEventListener('click', function (e) { + e.preventDefault(); + e.stopPropagation(); + _libPreviewCharVoice(rec, e.currentTarget); + }); // Generation Prompts section: copy buttons + one-call generation of all four // external-tool prompts (Voice Design / Image / SillyTavern / Concept Art) diff --git a/static/style.css b/static/style.css index 751680b..ca2b247 100644 --- a/static/style.css +++ b/static/style.css @@ -1202,6 +1202,21 @@ tr.lib-char-card.lib-chars-tbl-row:hover { transform: none; box-shadow: none; ba } .lcd-prompt-text:focus { border-color: var(--accent); } .lcd-prompt-actions { display: flex; gap: 6px; justify-content: flex-end; } +.lcd-generated-voice-file { + display: flex; align-items: center; gap: 8px; margin: -2px 0 8px; padding: 7px 10px; + border: 1px solid var(--border); border-radius: 8px; background: var(--panel); + color: var(--subtext); font-size: 11.5px; +} +.lcd-generated-voice-file-label { min-width: 0; overflow-wrap: anywhere; } +.lcd-generated-voice-file-label strong { color: var(--text); font-weight: 600; } +.lcd-generated-voice-play { + display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; + width: 28px; height: 28px; padding: 0; border: 1px solid var(--accent); + border-radius: 50%; background: var(--accent); color: #fff; cursor: pointer; +} +.lcd-generated-voice-play:hover { filter: brightness(.92); } +.lcd-generated-voice-play.playing { background: #1d4ed8; } +.lcd-generated-voice-play.loading { opacity: .65; cursor: wait; } .lcd-field { margin-bottom:16px; } .lcd-field:last-child { margin-bottom:0; } /* Compact list row — label left, value right, thin separator — used inside