ajh-code's picture
Add files using upload-large-folder tool
c41750d verified
Raw
History Blame Contribute Delete
11.7 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>S2-Pro Quantized TTS</title>
<style>
:root {
color-scheme: dark;
font-family: Inter, ui-sans-serif, system-ui, sans-serif;
background: #0b1014;
color: #edf3f6;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(circle at 15% 10%, #18384a 0, transparent 34rem),
linear-gradient(145deg, #080c10, #111b21);
}
main { width: min(920px, calc(100% - 32px)); margin: 0 auto; padding: 48px 0 72px; }
header { margin-bottom: 28px; }
.eyebrow { color: #68d6c0; font-size: 0.78rem; font-weight: 800; letter-spacing: 0.15em; text-transform: uppercase; }
h1 { margin: 8px 0; font-size: clamp(2rem, 6vw, 4rem); line-height: 1; letter-spacing: -0.05em; }
header p { max-width: 700px; color: #aab9c2; line-height: 1.65; }
.card { background: rgba(18, 28, 34, 0.9); border: 1px solid #2b3d47; border-radius: 18px; padding: 24px; box-shadow: 0 22px 70px #0008; }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
.full { grid-column: 1 / -1; }
label { display: block; margin-bottom: 8px; color: #c9d5da; font-size: 0.88rem; font-weight: 700; }
textarea, input, select {
width: 100%; border: 1px solid #354b56; border-radius: 11px; padding: 12px 13px;
background: #0a1217; color: #f4fafc; font: inherit; outline: none;
}
textarea { min-height: 108px; resize: vertical; line-height: 1.5; }
textarea:focus, input:focus, select:focus { border-color: #68d6c0; box-shadow: 0 0 0 3px #68d6c022; }
.hint { margin: 7px 0 0; color: #82939d; font-size: 0.79rem; line-height: 1.45; }
.actions { display: flex; align-items: center; gap: 14px; margin-top: 22px; flex-wrap: wrap; }
button {
appearance: none; border: 0; border-radius: 999px; padding: 12px 22px;
color: #06100e; background: #68d6c0; font: inherit; font-weight: 850; cursor: pointer;
}
button:hover { background: #8be6d4; }
button:disabled { opacity: 0.45; cursor: wait; }
#status { color: #9fb0b9; font-size: 0.88rem; }
#status.error { color: #ff9f9f; }
.result { display: none; margin-top: 22px; padding-top: 20px; border-top: 1px solid #2b3d47; }
audio { width: 100%; }
.download { display: inline-block; margin-top: 12px; color: #68d6c0; font-weight: 700; }
.badge { display: inline-flex; align-items: center; gap: 7px; padding: 6px 10px; border: 1px solid #2b3d47; border-radius: 999px; color: #aab9c2; font-size: 0.76rem; }
.dot { width: 8px; height: 8px; border-radius: 50%; background: #e2b34a; }
.dot.ok { background: #68d6c0; box-shadow: 0 0 12px #68d6c0; }
footer { margin-top: 22px; color: #6f8089; font-size: 0.76rem; line-height: 1.5; }
@media (max-width: 680px) { .grid { grid-template-columns: 1fr; } .full { grid-column: auto; } main { padding-top: 28px; } }
</style>
</head>
<body>
<main>
<header>
<div class="eyebrow">V1 · English balanced release</div>
<h1 id="model-title">Fish Audio S2-Pro NVFP4 Balanced</h1>
<p id="model-description">Loading the active checkpoint information…</p>
<div class="badge"><span id="health-dot" class="dot"></span><span id="health-text">Checking service…</span></div>
</header>
<section class="card">
<form id="tts-form">
<div class="grid">
<div class="full">
<label for="text">Text to speak</label>
<textarea id="text" required>A clear voice can make even a short message feel personal and memorable.</textarea>
<p class="hint">The base S2-Pro model can sound flatter than highly expressive TTS systems. Inline instructions may help some prompts, but V1 does not claim to solve that inherited limitation.</p>
</div>
<div>
<label for="reference-audio">Zero-shot reference audio</label>
<input id="reference-audio" type="file" accept="audio/*" required>
<p class="hint">Use a clean, consented 10–30 second sample with one speaker and little background noise.</p>
</div>
<div>
<label for="reference-text">Exact reference transcript</label>
<textarea id="reference-text" required placeholder="Transcribe the reference audio verbatim."></textarea>
<p class="hint">The transcript is required for reliable zero-shot conditioning.</p>
</div>
<div>
<label for="seed">Seed (optional)</label>
<input id="seed" type="number" placeholder="Random when blank">
</div>
<div>
<label for="sampling-preset">Voice-clone sampling</label>
<select id="sampling-preset">
<option value="natural" selected>Natural (recommended)</option>
<option value="api">Original API default</option>
<option value="stable">Stable identity</option>
<option value="expressive">Higher randomness (not an emotion tag)</option>
</select>
<p class="hint">Use the same seed to A/B sampling. For emotional delivery, put explicit [tag] instructions in the text.</p>
</div>
<div>
<label for="temperature">Temperature</label>
<input id="temperature" type="number" min="0.1" max="1" step="0.05" value="0.9">
</div>
<div>
<label for="top-p">Top-p</label>
<input id="top-p" type="number" min="0.1" max="1" step="0.05" value="0.9">
</div>
</div>
<div class="actions">
<button id="generate" type="submit">Generate speech</button>
<span id="status">Ready.</span>
</div>
</form>
<div id="result" class="result">
<audio id="player" controls></audio>
<a id="download" class="download" download="s2-pro-tts.wav">Download WAV</a>
</div>
</section>
<footer><strong>Built with Fish Audio.</strong> Use only voices you have permission to clone. This derivative is under the Fish Audio Research License; commercial use requires a separate written license from Fish Audio.</footer>
</main>
<script>
const form = document.querySelector('#tts-form');
const button = document.querySelector('#generate');
const status = document.querySelector('#status');
const result = document.querySelector('#result');
const player = document.querySelector('#player');
const download = document.querySelector('#download');
const samplingPreset = document.querySelector('#sampling-preset');
const temperature = document.querySelector('#temperature');
const topP = document.querySelector('#top-p');
let currentUrl = null;
const samplingProfiles = {
natural: { temperature: 0.9, topP: 0.9 },
api: { temperature: 0.8, topP: 0.8 },
stable: { temperature: 0.7, topP: 0.7 },
expressive: { temperature: 0.95, topP: 0.95 }
};
samplingPreset.addEventListener('change', () => {
const profile = samplingProfiles[samplingPreset.value];
temperature.value = profile.temperature;
topP.value = profile.topP;
});
function toBase64(buffer) {
const bytes = new Uint8Array(buffer);
const chunkSize = 32768;
let binary = '';
for (let offset = 0; offset < bytes.length; offset += chunkSize) {
binary += String.fromCharCode(...bytes.subarray(offset, offset + chunkSize));
}
return btoa(binary);
}
function setStatus(message, isError = false) {
status.textContent = message;
status.classList.toggle('error', isError);
}
async function checkHealth() {
try {
const response = await fetch('/v1/health');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const modelResponse = await fetch('/v1/model');
if (!modelResponse.ok) throw new Error(`Model HTTP ${modelResponse.status}`);
const model = await modelResponse.json();
const isNvfp4 = model.policy === 'w4a16_gate_up_middle30_mxfp8_rest';
const isBf16 = model.policy === 'bf16_control';
document.querySelector('#model-title').textContent = model.model;
document.querySelector('#model-description').textContent = `${model.quantization}. Checkpoint: ${model.checkpoint}`;
document.querySelector('#health-dot').classList.add('ok');
document.querySelector('#health-text').textContent = isNvfp4 ? 'Checkpointed NVFP4 service online' : (isBf16 ? 'Untouched BF16 base online' : 'MXFP8 service online');
if (isNvfp4) {
samplingProfiles.natural = { temperature: 1.0, topP: 0.85 };
temperature.value = 1.0;
topP.value = 0.85;
}
} catch (error) {
document.querySelector('#health-text').textContent = 'Service unavailable';
}
}
form.addEventListener('submit', async (event) => {
event.preventDefault();
const audio = document.querySelector('#reference-audio').files[0];
const referenceText = document.querySelector('#reference-text').value.trim();
if (!audio || !referenceText) {
setStatus('Reference audio and its exact transcript are required.', true);
return;
}
button.disabled = true;
result.style.display = 'none';
setStatus('Encoding reference and generating… first use may take longer.');
try {
const seedValue = document.querySelector('#seed').value;
const payload = {
text: document.querySelector('#text').value,
references: [{ audio: toBase64(await audio.arrayBuffer()), text: referenceText }],
reference_id: null,
format: 'wav',
streaming: false,
normalize: false,
max_new_tokens: 2048,
chunk_length: 1000,
top_p: Number(topP.value),
repetition_penalty: 1.05,
temperature: Number(temperature.value),
seed: seedValue === '' ? null : Number(seedValue),
// The pinned server caches transcript text by audio hash alone.
// A fresh load prevents a prior transcript for the same WAV from
// silently conditioning this request.
use_memory_cache: 'off'
};
const headers = { 'Content-Type': 'application/json', 'Accept': 'audio/wav' };
const response = await fetch('/v1/tts', { method: 'POST', headers, body: JSON.stringify(payload) });
if (!response.ok) throw new Error((await response.text()) || `HTTP ${response.status}`);
const blob = await response.blob();
if (currentUrl) URL.revokeObjectURL(currentUrl);
currentUrl = URL.createObjectURL(blob);
player.src = currentUrl;
download.href = currentUrl;
result.style.display = 'block';
setStatus(`Complete — ${(blob.size / 1024).toFixed(0)} KiB WAV.`);
} catch (error) {
setStatus(`Generation failed: ${error.message}`, true);
} finally {
button.disabled = false;
}
});
checkHealth();
</script>
</body>
</html>