Show generated voice file and preview in character profiles

This commit is contained in:
mARTin-B78 2026-08-19 17:12:02 +02:00
parent 8cc92ca02b
commit aa15265021
5 changed files with 51 additions and 4 deletions

View File

@ -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

View File

@ -1 +1 @@
1.20.39
1.20.40

View File

@ -10,7 +10,7 @@
<meta name="format-detection" content="telephone=no">
<meta name="color-scheme" content="light dark">
<meta name="theme-color" content="#2563EB">
<meta name="app-version" content="1.20.39">
<meta name="app-version" content="1.20.40">
<link rel="manifest" href="/manifest.webmanifest">
<link rel="icon" href="/static/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/static/icon.svg">
@ -27,7 +27,7 @@
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
<link rel="stylesheet" href="/static/style.css?v=1.20.39">
<link rel="stylesheet" href="/static/style.css?v=1.20.40">
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
@ -378,7 +378,7 @@ window.toggleNavTree = function(treeId, chevronId) {
</script>
<!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
<script src="/static/loader.js?v=1.20.39"></script>
<script src="/static/loader.js?v=1.20.40"></script>
</body>
</html>

View File

@ -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 = '<div class="lcd-section-full lcd-prompts-section">'
+ '<div class="lcd-section-label"><span class="mdi mdi-script-text-outline"></span> Generation Prompts</div>'
+ _lcdPromptBox('Voice Design Prompt', sh.voice_design_prompt, 'voice_design_prompt')
+ (function () {
const fileName = _libGeneratedVoiceFileName(rec);
return fileName
? '<div class="lcd-generated-voice-file">'
+ '<button type="button" class="lcd-generated-voice-play" title="Stimme anhören" aria-label="Stimme anhören"><span class="mdi mdi-play"></span></button>'
+ '<span class="lcd-generated-voice-file-label"><span class="mdi mdi-file-music-outline"></span> Voice file: <strong>' + escHtml(fileName) + '</strong></span>'
+ '</div>'
: '';
}())
+ _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)

View File

@ -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