- Add cross-project voice/name library (library.py + webUI library tabs) - chapter_builder: embedding-mode generation + auto-load TTS base model in preflight - qwen_tts: vmatrix capture, embedding/voice-prompt refinements - registry: vmatrix_path column + migration for approved voices - webUI: speakers list, chapter audio detail, proper names updates - docs: update voice generation and tts_server README
72 lines
3.2 KiB
HTML
72 lines
3.2 KiB
HTML
<div class="card">
|
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem">
|
|
<h3 style="margin:0">Library Speakers ({{ speakers|length }})</h3>
|
|
<span style="color:#64748b; font-size:0.85rem">Add speakers from the project Speakers page using the "Lib" button.</span>
|
|
</div>
|
|
{% if not speakers %}
|
|
<p style="color:#64748b">No speakers in the library yet.</p>
|
|
{% else %}
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Demographics</th>
|
|
<th>Voice</th>
|
|
<th>Source Project</th>
|
|
<th>Reference</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for s in speakers %}
|
|
<tr id="lib-speaker-{{ s.id }}">
|
|
<td>
|
|
<strong>{{ s.primary_name }}</strong>
|
|
{% if s.is_narrator %}<span class="badge badge-blue" style="margin-left:0.3rem">Narrator</span>{% endif %}
|
|
{% if s.is_group %}<span class="badge badge-yellow" style="margin-left:0.3rem">Group</span>{% endif %}
|
|
{% if s.aliases and s.aliases not in ('[]','') %}
|
|
<br><small style="color:#94a3b8">aliases: {{ s.aliases }}</small>
|
|
{% endif %}
|
|
</td>
|
|
<td style="font-size:0.8rem; color:#94a3b8">
|
|
{{ s.gender or '—' }}{% if s.accent %} · {{ s.accent }}{% endif %}
|
|
{% if s.vocal_traits and s.vocal_traits not in ('[]','') %}
|
|
<br><small>{{ s.vocal_traits }}</small>
|
|
{% endif %}
|
|
</td>
|
|
<td style="font-size:0.8rem; color:#94a3b8">
|
|
{% if s.distortion_effects and s.distortion_effects not in ('[]','') %}
|
|
<span style="color:#f59e0b">{{ s.distortion_effects }}</span>
|
|
{% else %}standard{% endif %}
|
|
</td>
|
|
<td style="font-size:0.8rem; color:#94a3b8">{{ s.source_project or 'manual' }}</td>
|
|
<td>
|
|
{% if s.voice_ref_path %}
|
|
{% set ref_name = s.voice_ref_path.split('/')[-1] %}
|
|
<audio controls preload="none" style="height:32px">
|
|
<source src="/library-audio/{{ s.id }}/{{ ref_name }}" type="audio/wav">
|
|
</audio>
|
|
{% else %}
|
|
<span style="color:#64748b">No ref</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<button class="btn btn-reject" style="font-size:0.7rem; padding:0.2rem 0.5rem"
|
|
onclick="removeLibSpeaker({{ s.id }}, '{{ s.primary_name|e }}')">✕</button>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% endif %}
|
|
</div>
|
|
<script>
|
|
function removeLibSpeaker(id, name) {
|
|
if (!confirm('Remove "' + name + '" from the library?')) return;
|
|
fetch('/api/library/speakers/' + id, {method: 'DELETE'})
|
|
.then(r => r.json()).then(d => {
|
|
htmx.ajax('GET', '/library/speakers', {target: '#lib-tab', swap: 'innerHTML'});
|
|
});
|
|
}
|
|
</script>
|