Show generated voice file and preview in character profiles
This commit is contained in:
parent
8cc92ca02b
commit
aa15265021
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user