Add ttstoy cog

This commit is contained in:
2026-06-05 16:41:55 -05:00
parent 0e9f4a5224
commit 6739b2d6f1
975 changed files with 8216 additions and 0 deletions
+194
View File
@@ -0,0 +1,194 @@
{% 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 %}