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
+399
View File
@@ -0,0 +1,399 @@
{% extends "base.html" %}
{% block title %}Dashboard — TtsToy{% endblock %}
{% block content %}
<div style="margin-bottom:1.25rem;display:flex;align-items:baseline;gap:1rem;">
<h1 style="font-size:1rem;font-weight:300;color:#e0ecff;text-shadow:0 0 16px rgba(80,150,255,0.2);text-transform:uppercase;letter-spacing:2px;">Dashboard</h1>
<span class="muted">{{ session.get('discord_name','') }}</span>
</div>
<div class="row">
<div class="col">
<div class="card">
<h2>Active Mode</h2>
<div style="font-size:1rem;margin-bottom:0.4rem;color:#c8daf0;">
<span class="badge badge-mode">{{ mode.upper() }}</span>
</div>
<div class="muted">Global bot mode: <strong style="color:rgba(200,220,255,0.7);">{{ global_mode }}</strong></div>
</div>
</div>
<div class="col">
<div class="card">
<h2>Active Voice</h2>
<div style="font-size:0.95rem;margin-bottom:0.4rem;color:#c8daf0;" id="active-voice-display">{{ active_voice_display }}</div>
<div class="muted">
{% if mode == "chatterbox" %}<a href="{{ url_for('page_chatterbox') }}">Manage voices</a>
{% elif mode == "minimax" %}<a href="{{ url_for('page_minimax') }}">Browse voices</a>
{% elif mode == "dectalk" %}<a href="{{ url_for('page_dectalk') }}">Voice commands</a>
{% elif mode == "vox" %}<a href="{{ url_for('page_vox') }}">Word list</a>
{% endif %}
</div>
</div>
</div>
</div>
<div class="card">
<h2>Submit TTS Message</h2>
<div class="muted" style="margin-bottom:1rem;"><strong>Speak in VC</strong> plays in your voice channel. <strong>Generate Only</strong> creates the audio for playback here — no VC needed.</div>
<div class="row" style="margin-bottom:0.75rem;">
<div class="col form-group" style="max-width:180px;">
<label>Mode</label>
<select id="tts-mode-select" onchange="onModeChange()">
{% for m in ["minimax","chatterbox","dectalk","morshu","vox"] %}
<option value="{{ m }}" {% if m == mode %}selected{% endif %}>{{ m }}</option>
{% endfor %}
</select>
</div>
<div class="col form-group" id="voice-select-group">
<label>Voice</label>
<select id="tts-voice-select" onchange="onVoiceChange()">
<option value="">— use my saved voice —</option>
</select>
</div>
</div>
<!-- Chatterbox knobs -->
<div id="cb-knobs" style="display:none;margin-bottom:1rem;">
<div style="background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.10);border-radius:4px;padding:0.9rem;">
<div style="font-size:0.72rem;color:rgba(140,180,230,0.4);margin-bottom:0.75rem;text-transform:uppercase;letter-spacing:.05em;">Voice Settings</div>
<div class="row">
<div class="col form-group">
<label>Temperature <span class="muted">0–1.5</span></label>
<input type="range" id="cb-temp" min="0" max="1.5" step="0.05" style="width:100%;" oninput="updateKnobLabel('cb-temp','cb-temp-val')">
<div style="display:flex;justify-content:space-between;font-size:0.72rem;color:rgba(140,180,230,0.4);margin-top:2px;">
<span>consistent</span><span id="cb-temp-val" style="color:rgba(200,220,255,0.7);">—</span><span>varied</span>
</div>
</div>
<div class="col form-group">
<label>Exaggeration <span class="muted">0.25–2.0</span></label>
<input type="range" id="cb-exag" min="0.25" max="2.0" step="0.05" style="width:100%;" oninput="updateKnobLabel('cb-exag','cb-exag-val')">
<div style="display:flex;justify-content:space-between;font-size:0.72rem;color:rgba(140,180,230,0.4);margin-top:2px;">
<span>flat</span><span id="cb-exag-val" style="color:rgba(200,220,255,0.7);">—</span><span>dramatic</span>
</div>
</div>
<div class="col form-group">
<label>Volume dB <span class="muted">-20–+20</span></label>
<input type="range" id="cb-vol" min="-20" max="20" step="0.5" style="width:100%;" oninput="updateKnobLabel('cb-vol','cb-vol-val')">
<div style="display:flex;justify-content:space-between;font-size:0.72rem;color:rgba(140,180,230,0.4);margin-top:2px;">
<span>-20</span><span id="cb-vol-val" style="color:rgba(200,220,255,0.7);">0</span><span>+20</span>
</div>
</div>
<div class="col form-group">
<label>Speed <span class="muted">0.25–4.0</span></label>
<input type="range" id="cb-speed" min="0.25" max="4.0" step="0.05" style="width:100%;" oninput="updateKnobLabel('cb-speed','cb-speed-val')">
<div style="display:flex;justify-content:space-between;font-size:0.72rem;color:rgba(140,180,230,0.4);margin-top:2px;">
<span>slow</span><span id="cb-speed-val" style="color:rgba(200,220,255,0.7);">1.0</span><span>fast</span>
</div>
</div>
</div>
<div style="display:flex;gap:0.5rem;align-items:center;margin-top:0.25rem;">
<button class="btn btn-secondary btn-sm" onclick="saveKnobs()">Save for this voice</button>
<button class="btn btn-secondary btn-sm" onclick="resetKnobs()">Reset</button>
<span id="knob-status" class="muted"></span>
</div>
</div>
</div>
<div class="form-group">
<label for="tts-text">Message</label>
<div style="position:relative;">
<textarea id="tts-text" placeholder="Type something to say..." style="padding-right:2.5rem;"></textarea>
<button type="button" id="sfx-toggle-btn" onclick="toggleSfxDrawer()" title="Insert SFX emoji"
style="position:absolute;right:6px;top:6px;background:none;border:1px solid rgba(255,255,255,0.12);
border-radius:4px;padding:2px 6px;cursor:pointer;font-size:1.1rem;line-height:1;
color:rgba(200,220,255,0.6);transition:background 0.12s;"
onmouseenter="this.style.background='rgba(255,255,255,0.06)'" onmouseleave="this.style.background='none'">🔊</button>
</div>
<div id="sfx-drawer" style="display:none;margin-top:0.5rem;background:rgba(255,255,255,0.04);
border:1px solid rgba(255,255,255,0.10);border-radius:4px;padding:0.6rem 0.75rem;">
<div style="font-size:0.72rem;color:rgba(140,180,230,0.4);text-transform:uppercase;letter-spacing:.05em;margin-bottom:0.5rem;">Sound Effects — click to insert</div>
<div id="sfx-grid" style="display:flex;flex-wrap:wrap;gap:0.35rem;"></div>
</div>
</div>
<div style="display:flex;gap:0.75rem;align-items:center;flex-wrap:wrap;">
<button class="btn btn-primary" onclick="submitTTS()">
<span id="spinner" class="spin" style="display:none;"></span> Speak in VC
</button>
<button class="btn btn-secondary" id="gen-only-btn" onclick="generateOnly()" style="display:none;">
<span id="gen-spinner" class="spin" style="display:none;"></span> Generate Only
</button>
<span id="tts-status" class="muted"></span>
</div>
<div id="tts-result" style="margin-top:0.75rem;display:none;">
<audio id="tts-audio" controls></audio>
</div>
</div>
<div class="card">
<h2>Mode Preference</h2>
<div class="muted" style="margin-bottom:0.75rem;">Your personal mode override — does not affect other users.</div>
<div style="display:flex;gap:0.5rem;align-items:center;">
<button class="btn btn-secondary btn-sm" onclick="saveMode()">Save current mode selection</button>
<span id="mode-status" class="muted"></span>
</div>
</div>
<div class="card">
<h2>TTS Tips</h2>
<table>
<tr>
<td style="color:rgba(140,180,230,0.4);width:35%;vertical-align:top;">Voice switching</td>
<td>Use <code>[mode|voice]</code> tags to change voice mid-message. Everything after the tag uses that voice until the next tag. The voice part is optional for modes that don't need it.<br>
<span class="muted" style="font-size:0.78rem;">Example: <code>[minimax|robotnik] I am the eggman [dectalk] [:nh]now I'm Harry</code></span>
</td>
</tr>
<tr>
<td style="color:rgba(140,180,230,0.4);vertical-align:top;">Available modes</td>
<td>
<code>minimax</code> · <code>chatterbox</code> · <code>dectalk</code> · <code>morshu</code> · <code>vox</code>
</td>
</tr>
<tr>
<td style="color:rgba(140,180,230,0.4);vertical-align:top;">Voice names</td>
<td>
MiniMax: <code>[minimax|Robotnik]</code>, <code>[minimax|Gnome]</code>, etc.<br>
Chatterbox: <code>[chatterbox|YourVoiceName]</code> (uploaded filename)<br>
DECTalk: just <code>[dectalk]</code> — control voices in the text with <code>[:np]</code> Paul, <code>[:nb]</code> Betty, <code>[:nh]</code> Harry, etc.<br>
Morshu / VOX: <code>[morshu]</code> or <code>[vox]</code> — no voice selection needed
</td>
</tr>
<tr>
<td style="color:rgba(140,180,230,0.4);vertical-align:top;">Sound effects</td>
<td>Insert SFX emoji anywhere in your text — click the 🔊 button on the message box to browse available sounds.</td>
</tr>
<tr>
<td style="color:rgba(140,180,230,0.4);vertical-align:top;">Chatterbox tokens</td>
<td><code>[laugh]</code> <code>[chuckle]</code> <code>[sigh]</code> <code>[gasp]</code> <code>[cough]</code> <code>[clear throat]</code> <code>[sniff]</code> <code>[groan]</code> <code>[shush]</code></td>
</tr>
</table>
</div>
{% endblock %}
{% block scripts %}
<script>
let settings = {};
async function loadSettings() {
// Remember current voice selection so it survives the re-render
const prevVoice = document.getElementById('tts-voice-select')?.value || null;
const r = await apiFetch('/api/settings');
settings = await r.json();
populateVoiceSelect(document.getElementById('tts-mode-select').value, prevVoice);
}
function updateKnobLabel(sliderId, labelId) {
document.getElementById(labelId).textContent = parseFloat(document.getElementById(sliderId).value).toFixed(2);
}
function loadKnobsForVoice(filename) {
const t = settings.chatterbox_temperature_offsets?.[filename];
const e = settings.chatterbox_exaggeration_offsets?.[filename];
const v = settings.chatterbox_volume_offsets?.[filename] ?? 0;
const s = settings.chatterbox_speed_offsets?.[filename] ?? 1.0;
document.getElementById('cb-temp').value = t ?? 0.5;
document.getElementById('cb-exag').value = e ?? 1.0;
document.getElementById('cb-vol').value = v;
document.getElementById('cb-speed').value = s;
document.getElementById('cb-temp-val').textContent = t != null ? parseFloat(t).toFixed(2) : '—';
document.getElementById('cb-exag-val').textContent = e != null ? parseFloat(e).toFixed(2) : '—';
document.getElementById('cb-vol-val').textContent = parseFloat(v).toFixed(1);
document.getElementById('cb-speed-val').textContent = parseFloat(s).toFixed(2);
document.getElementById('knob-status').textContent = '';
}
function populateVoiceSelect(mode, restoreVoice) {
const sel = document.getElementById('tts-voice-select');
const group = document.getElementById('voice-select-group');
const knobs = document.getElementById('cb-knobs');
sel.innerHTML = '<option value="">— use my saved voice —</option>';
knobs.style.display = 'none';
if (mode === 'chatterbox') {
group.style.display = '';
(settings.chatterbox_voices || []).forEach(filename => {
const parts = filename.split('_');
const display = parts.length > 1 ? parts.slice(1).join('_').replace(/\.[^.]+$/, '') : filename;
const opt = document.createElement('option');
opt.value = filename; opt.textContent = display;
sel.appendChild(opt);
});
// Restore previous selection, fall back to saved active voice
const target = restoreVoice || settings.user_voice || '';
if (target) sel.value = target;
// If the target wasn't in the list, sel.value will be "" (default option) — that's fine
const active = sel.value || settings.user_voice;
if (active) { knobs.style.display = ''; loadKnobsForVoice(active); }
if (settings.user_voice) {
const parts = settings.user_voice.split('_');
document.getElementById('active-voice-display').textContent =
parts.length > 1 ? parts.slice(1).join('_').replace(/\.[^.]+$/, '') : settings.user_voice;
}
} else if (mode === 'minimax') {
group.style.display = '';
const voices = settings.minimax_voices || {};
Object.entries(voices).forEach(([name, id]) => {
const opt = document.createElement('option');
opt.value = id; opt.textContent = name;
sel.appendChild(opt);
});
const target = restoreVoice || settings.user_voice || '';
if (target) sel.value = target;
} else {
group.style.display = 'none';
}
}
function onVoiceChange() {
const mode = document.getElementById('tts-mode-select').value;
const voice = document.getElementById('tts-voice-select').value;
const knobs = document.getElementById('cb-knobs');
if (mode === 'chatterbox' && voice) { knobs.style.display = ''; loadKnobsForVoice(voice); }
else knobs.style.display = 'none';
}
function onModeChange() {
populateVoiceSelect(document.getElementById('tts-mode-select').value);
document.getElementById('gen-only-btn').style.display =
document.getElementById('tts-mode-select').value === 'chatterbox' ? '' : 'none';
}
async function saveKnobs() {
const voice = document.getElementById('tts-voice-select').value || settings.user_voice;
if (!voice) return;
const params = {
temperature: parseFloat(document.getElementById('cb-temp').value),
exaggeration: parseFloat(document.getElementById('cb-exag').value),
volume_db: parseFloat(document.getElementById('cb-vol').value),
speed: parseFloat(document.getElementById('cb-speed').value),
};
const r = await apiFetch('/api/settings/chatterbox_params', {method:'POST', body: JSON.stringify({voice, params})});
const d = await r.json();
document.getElementById('knob-status').textContent = d.ok ? 'Saved.' : 'Error: ' + d.error;
if (d.ok) await loadSettings();
}
async function resetKnobs() {
const voice = document.getElementById('tts-voice-select').value || settings.user_voice;
if (!voice) return;
const r = await apiFetch('/api/settings/chatterbox_params', {method:'POST', body: JSON.stringify({voice, params:{temperature:null,exaggeration:null,volume_db:0,speed:1.0}})});
const d = await r.json();
document.getElementById('knob-status').textContent = d.ok ? 'Reset to defaults.' : 'Error: ' + d.error;
if (d.ok) await loadSettings();
}
async function submitTTS() {
const text = document.getElementById('tts-text').value.trim();
if (!text) return;
const mode = document.getElementById('tts-mode-select').value;
const voice = document.getElementById('tts-voice-select').value || null;
const spinner = document.getElementById('spinner');
const status = document.getElementById('tts-status');
const result = document.getElementById('tts-result');
spinner.style.display = 'inline-block';
status.textContent = 'Sending to bot...';
result.style.display = 'none';
const r = await apiFetch('/api/tts', {method:'POST', body: JSON.stringify({text, mode, voice})});
const {job_id, error} = await r.json();
if (error) { spinner.style.display='none'; status.textContent = 'Error: ' + error; return; }
const poll = setInterval(async () => {
const s = await apiFetch(`/api/tts/${job_id}/status`);
const job = await s.json();
if (job.status === 'done') {
clearInterval(poll);
spinner.style.display = 'none';
status.textContent = 'Playing in VC — posted to Discord channel.';
document.getElementById('tts-audio').src = `/api/tts/${job_id}/audio`;
result.style.display = 'block';
} else if (job.status === 'error') {
clearInterval(poll);
spinner.style.display = 'none';
status.textContent = 'Error: ' + job.error;
}
}, 800);
}
async function generateOnly() {
const text = document.getElementById('tts-text').value.trim();
if (!text) return;
const mode = document.getElementById('tts-mode-select').value;
const voice = document.getElementById('tts-voice-select').value || null;
const spinner = document.getElementById('gen-spinner');
const status = document.getElementById('tts-status');
const result = document.getElementById('tts-result');
spinner.style.display = 'inline-block';
status.textContent = 'Generating audio...';
result.style.display = 'none';
const r = await apiFetch('/api/tts/generate', {method:'POST', body: JSON.stringify({text, mode, voice})});
const {job_id, error} = await r.json();
if (error) { spinner.style.display='none'; status.textContent = 'Error: ' + error; return; }
const poll = setInterval(async () => {
const s = await apiFetch(`/api/tts/${job_id}/status`);
const job = await s.json();
if (job.status === 'done') {
clearInterval(poll);
spinner.style.display = 'none';
status.textContent = 'Audio ready — posted to Discord channel.';
document.getElementById('tts-audio').src = `/api/tts/${job_id}/audio`;
result.style.display = 'block';
} else if (job.status === 'error') {
clearInterval(poll);
spinner.style.display = 'none';
status.textContent = 'Error: ' + job.error;
}
}, 800);
}
async function saveMode() {
const mode = document.getElementById('tts-mode-select').value;
const r = await apiFetch('/api/settings/mode', {method:'POST', body: JSON.stringify({mode})});
const d = await r.json();
document.getElementById('mode-status').textContent = d.ok ? 'Saved: ' + mode : 'Error: ' + d.error;
}
loadSettings();
loadSfxEmojis();
// Show generate-only button if initial mode is chatterbox
document.getElementById('gen-only-btn').style.display =
document.getElementById('tts-mode-select').value === 'chatterbox' ? '' : 'none';
let sfxEmojis = [];
let sfxDrawerOpen = false;
async function loadSfxEmojis() {
try {
const r = await apiFetch('/api/sfx_emojis');
sfxEmojis = await r.json();
renderSfxGrid();
} catch(e) { console.warn('Could not load SFX emojis', e); }
}
function renderSfxGrid() {
const grid = document.getElementById('sfx-grid');
if (!sfxEmojis.length) { grid.innerHTML = '<span class="muted">No SFX available.</span>'; return; }
grid.innerHTML = sfxEmojis.map(s =>
`<button type="button" onclick="insertSfx('${s.emoji}')" title="${s.name}"
style="background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.10);border-radius:4px;
padding:0.3rem 0.5rem;cursor:pointer;font-size:1rem;line-height:1.2;display:flex;
flex-direction:column;align-items:center;gap:2px;min-width:52px;transition:background 0.12s;"
onmouseenter="this.style.background='rgba(255,255,255,0.08)'" onmouseleave="this.style.background='rgba(255,255,255,0.04)'">
<span style="font-size:1.25rem;">${s.emoji}</span>
<span style="font-size:0.62rem;color:rgba(140,180,230,0.4);">${s.name}</span>
</button>`
).join('');
}
function toggleSfxDrawer() {
sfxDrawerOpen = !sfxDrawerOpen;
document.getElementById('sfx-drawer').style.display = sfxDrawerOpen ? '' : 'none';
}
function insertSfx(emoji) {
const ta = document.getElementById('tts-text');
const start = ta.selectionStart;
const end = ta.selectionEnd;
ta.value = ta.value.substring(0, start) + emoji + ta.value.substring(end);
ta.selectionStart = ta.selectionEnd = start + emoji.length;
ta.focus();
}
loadSfxEmojis();
</script>
{% endblock %}