Files
scrapyard-cogworks/ttstoy/webui/templates/chatterbox.html
T
2026-06-05 16:41:55 -05:00

195 lines
9.0 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}Chatterbox — TtsToy{% endblock %}
{% block content %}
<div style="margin-bottom:1.25rem;">
<h1 style="font-size:1rem;font-weight:300;color:#e0ecff;text-transform:uppercase;letter-spacing:2px;text-shadow:0 0 16px rgba(80,150,255,0.2);">Chatterbox Voices</h1>
</div>
<div class="card">
<h2>Upload New Voice</h2>
<div class="muted" style="margin-bottom:0.9rem;">Upload a 5–15 second .wav or .mp3 clip of a single speaker. Short clips are auto-looped.</div>
<div class="row">
<div class="col form-group">
<label>Voice Name</label>
<input type="text" id="upload-name" placeholder="e.g. MyVoice">
</div>
<div class="col form-group">
<label>Audio File (.wav / .mp3)</label>
<input type="file" id="upload-file" accept=".wav,.mp3">
</div>
</div>
<div style="display:flex;gap:0.5rem;align-items:center;">
<button class="btn btn-primary btn-sm" onclick="uploadVoice()">Upload</button>
<span id="upload-status" class="muted"></span>
</div>
</div>
<div class="card">
<h2>Your Voices</h2>
<ul class="voice-list" id="voice-list"><li class="muted">Loading...</li></ul>
</div>
<div class="card" id="edit-card" style="display:none;">
<h2>Edit Voice: <span id="edit-voice-name" style="color:#c8daf0;font-weight:300;"></span></h2>
<input type="hidden" id="edit-voice-filename">
<div class="row">
<div class="col form-group">
<label>Temperature (0.0–1.5)</label>
<input type="number" id="edit-temp" step="0.05" min="0" max="1.5" placeholder="server default">
</div>
<div class="col form-group">
<label>Exaggeration (0.25–2.0)</label>
<input type="number" id="edit-exag" step="0.05" min="0.25" max="2.0" placeholder="server default">
</div>
<div class="col form-group">
<label>Volume dB (-20–+20)</label>
<input type="number" id="edit-vol" step="0.5" min="-20" max="20" placeholder="0">
</div>
<div class="col form-group">
<label>Speed (0.25–4.0)</label>
<input type="number" id="edit-speed" step="0.05" min="0.25" max="4.0" placeholder="1.0">
</div>
</div>
<div style="display:flex;gap:0.5rem;flex-wrap:wrap;align-items:center;">
<button class="btn btn-primary btn-sm" id="save-btn">Save</button>
<button class="btn btn-secondary btn-sm" onclick="setActiveVoice()">Set as Active</button>
<button class="btn btn-danger btn-sm" onclick="deleteVoice()">Delete</button>
<button class="btn btn-secondary btn-sm" onclick="closeEdit()">Cancel</button>
<span id="edit-status" class="muted"></span>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let voices = [];
let settings = {};
async function loadVoices() {
const [vr, sr] = await Promise.all([apiFetch('/api/chatterbox/voices'), apiFetch('/api/settings')]);
voices = await vr.json();
settings = await sr.json();
renderVoices();
}
function renderVoices() {
const ul = document.getElementById('voice-list');
if (!voices.length) { ul.innerHTML = '<li class="muted">No voices uploaded yet.</li>'; return; }
ul.innerHTML = voices.map(v => {
const active = v.filename === settings.user_voice;
return `<li>
<span class="name">${v.display} ${active ? '<span class="badge badge-active">active</span>' : ''}</span>
<button class="btn btn-secondary btn-sm" onclick="openEdit('${v.filename}','${v.display}')">Edit</button>
</li>`;
}).join('');
}
function openEdit(filename, display) {
const alreadyOpen = document.getElementById('edit-card').style.display !== 'none'
&& document.getElementById('edit-voice-filename').value === filename;
document.getElementById('edit-card').style.display = 'block';
document.getElementById('edit-voice-name').textContent = display;
document.getElementById('edit-voice-filename').value = filename;
// Don't overwrite fields if this voice is already open (e.g. after a save re-render)
if (!alreadyOpen) {
document.getElementById('edit-status').textContent = '';
document.getElementById('edit-temp').value = settings.chatterbox_temperature_offsets?.[filename] ?? '';
document.getElementById('edit-exag').value = settings.chatterbox_exaggeration_offsets?.[filename] ?? '';
document.getElementById('edit-vol').value = settings.chatterbox_volume_offsets?.[filename] ?? '';
document.getElementById('edit-speed').value = settings.chatterbox_speed_offsets?.[filename] ?? '';
document.getElementById('edit-card').scrollIntoView({behavior:'smooth'});
}
}
function closeEdit() { document.getElementById('edit-card').style.display = 'none'; }
// Capture input values on mousedown, before the button click causes blur+snap on number inputs
let _pendingSave = null;
document.getElementById('save-btn').addEventListener('mousedown', () => {
_pendingSave = {
t: document.getElementById('edit-temp').value,
e: document.getElementById('edit-exag').value,
v: document.getElementById('edit-vol').value,
s: document.getElementById('edit-speed').value,
};
});
document.getElementById('save-btn').addEventListener('click', saveVoiceParams);
async function saveVoiceParams() {
const filename = document.getElementById('edit-voice-filename').value;
// Use values captured on mousedown (before blur snapped them), fall back to current
const raw = _pendingSave || {
t: document.getElementById('edit-temp').value,
e: document.getElementById('edit-exag').value,
v: document.getElementById('edit-vol').value,
s: document.getElementById('edit-speed').value,
};
_pendingSave = null;
const params = {};
if (raw.t !== '') params.temperature = parseFloat(raw.t);
if (raw.e !== '') params.exaggeration = parseFloat(raw.e);
if (raw.v !== '') params.volume_db = parseFloat(raw.v);
if (raw.s !== '') params.speed = parseFloat(raw.s);
const r = await apiFetch('/api/settings/chatterbox_params', {method:'POST', body: JSON.stringify({voice: filename, params})});
const d = await r.json();
if (d.ok) {
// Update local settings cache so re-render doesn't clobber the form with stale disk values
if (!settings.chatterbox_temperature_offsets) settings.chatterbox_temperature_offsets = {};
if (!settings.chatterbox_exaggeration_offsets) settings.chatterbox_exaggeration_offsets = {};
if (!settings.chatterbox_volume_offsets) settings.chatterbox_volume_offsets = {};
if (!settings.chatterbox_speed_offsets) settings.chatterbox_speed_offsets = {};
if (params.temperature !== undefined) settings.chatterbox_temperature_offsets[filename] = params.temperature;
if (params.exaggeration !== undefined) settings.chatterbox_exaggeration_offsets[filename] = params.exaggeration;
if (params.volume_db !== undefined) settings.chatterbox_volume_offsets[filename] = params.volume_db;
if (params.speed !== undefined) settings.chatterbox_speed_offsets[filename] = params.speed;
// Restore field values to what was actually saved (blur may have snapped them)
if (raw.t !== '') document.getElementById('edit-temp').value = raw.t;
if (raw.e !== '') document.getElementById('edit-exag').value = raw.e;
if (raw.v !== '') document.getElementById('edit-vol').value = raw.v;
if (raw.s !== '') document.getElementById('edit-speed').value = raw.s;
document.getElementById('edit-status').textContent = 'Saved.';
} else {
document.getElementById('edit-status').textContent = 'Error: ' + d.error;
}
}
async function setActiveVoice() {
const filename = document.getElementById('edit-voice-filename').value;
const r = await apiFetch('/api/settings/voice', {method:'POST', body: JSON.stringify({voice: filename})});
const d = await r.json();
document.getElementById('edit-status').textContent = d.ok ? 'Set as active voice.' : 'Error: ' + d.error;
if (d.ok) await loadVoices();
}
async function deleteVoice() {
const filename = document.getElementById('edit-voice-filename').value;
if (!confirm('Delete this voice? This cannot be undone.')) return;
const r = await fetch(`/api/chatterbox/voices/${encodeURIComponent(filename)}`, {method:'DELETE'});
const d = await r.json();
if (d.ok) { closeEdit(); await loadVoices(); }
else document.getElementById('edit-status').textContent = 'Error: ' + d.error;
}
async function uploadVoice() {
const name = document.getElementById('upload-name').value.trim();
const file = document.getElementById('upload-file').files[0];
if (!file) { document.getElementById('upload-status').textContent = 'Select a file first.'; return; }
const fd = new FormData();
fd.append('file', file);
if (name) fd.append('name', name);
document.getElementById('upload-status').textContent = 'Uploading...';
const r = await fetch('/api/chatterbox/voices/upload', {method:'POST', body: fd});
const d = await r.json();
if (d.ok) {
document.getElementById('upload-status').textContent = 'Uploaded.';
document.getElementById('upload-name').value = '';
document.getElementById('upload-file').value = '';
await loadVoices();
} else {
document.getElementById('upload-status').textContent = 'Error: ' + d.error;
}
}
loadVoices();
</script>
{% endblock %}