Download runtime/web/index.html from ajh-code/Fish-Audio-S2-Pro-NVFP4-Balanced: direct link, hf CLI and curl.
- Browser
- Download file 11.7 kB
-
https://huggingface.co/ajh-code/Fish-Audio-S2-Pro-NVFP4-Balanced/resolve/main/runtime/web/index.html
- Command line
-
hf download hf://ajh-code/Fish-Audio-S2-Pro-NVFP4-Balanced/runtime/web/index.html
-
curl -L -o index.html https://huggingface.co/ajh-code/Fish-Audio-S2-Pro-NVFP4-Balanced/resolve/main/runtime/web/index.html
11.7 kB
| <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> | |