maya2-native-studio / studio.html
akhaliq's picture
akhaliq HF Staff
WebAudio streaming playback with live waveform
eb88a5a verified
Raw History Blame Contribute Delete
40.6 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Maya 2 Native Studio</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/inter@5/index.css" />
<style>
:root {
--ink: #0b2a6b;
--deep: #123a9e;
--green: #35c14f;
--glass: rgba(255, 255, 255, 0.10);
--glass-strong: rgba(255, 255, 255, 0.16);
--stroke: rgba(255, 255, 255, 0.24);
--text: rgba(255, 255, 255, 0.95);
--muted: rgba(255, 255, 255, 0.68);
--dim: rgba(255, 255, 255, 0.45);
--radius: 18px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
[hidden] { display: none !important; }
html { height: 100%; }
body {
font-family: "Inter Variable", "Inter", ui-sans-serif, system-ui, sans-serif;
color: var(--text);
min-height: 100vh;
background: linear-gradient(180deg, #0f3aa8 0%, #1a55d6 34%, #4f8fe8 68%, #a9d3f7 100%);
background-attachment: fixed;
padding-bottom: 44px; /* status bar */
-webkit-font-smoothing: antialiased;
}
/* ------- sky + hills ------- */
.cloud {
position: fixed; border-radius: 50%; pointer-events: none; z-index: 0;
background: radial-gradient(ellipse at center, rgba(255,255,255,0.95) 0%, rgba(255,255,255,0) 68%);
animation: drift linear infinite;
}
.cloud.c1 { width: 420px; height: 130px; top: 9%; left: -6%; animation-duration: 90s; }
.cloud.c2 { width: 560px; height: 170px; top: 3%; right: -8%; animation-duration: 120s; animation-delay: -30s; }
.cloud.c3 { width: 320px; height: 100px; top: 34%; left: 12%; animation-duration: 105s; animation-delay: -60s; opacity: 0.7; }
.cloud.c4 { width: 380px; height: 120px; top: 28%; right: 6%; animation-duration: 80s; animation-delay: -15s; opacity: 0.75; }
@keyframes drift {
from { transform: translateX(-4vw); }
50% { transform: translateX(4vw); }
to { transform: translateX(-4vw); }
}
.hills {
position: fixed; left: 0; right: 0; bottom: 0; width: 100%; height: 32vh;
z-index: 0; pointer-events: none;
}
/* ------- top bar ------- */
.topbar {
position: relative; z-index: 10;
display: flex; align-items: center; gap: 14px; padding: 10px 22px;
background: rgba(10, 30, 84, 0.30);
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
border-bottom: 1px solid rgba(255, 255, 255, 0.14);
}
.brand { display: flex; align-items: center; gap: 9px; font-weight: 650; font-size: 0.95rem; white-space: nowrap; }
.brand .mark { width: 26px; height: 26px; border-radius: 8px; display: block; flex-shrink: 0; }
.brand .brand-sub { color: var(--muted); font-weight: 500; padding-left: 12px; margin-left: 4px; border-left: 1px solid rgba(255,255,255,0.22); }
.mode-tabs { display: flex; gap: 2px; background: rgba(8, 24, 64, 0.35); border: 1px solid rgba(255,255,255,0.16); border-radius: 999px; padding: 3px; margin-left: 10px; }
.mode-tabs button {
border: none; border-radius: 999px; padding: 6px 18px; font: 600 0.82rem/1 inherit; font-family: inherit;
background: transparent; color: var(--muted); cursor: pointer; transition: all 0.15s;
}
.mode-tabs button:hover { color: var(--text); }
.mode-tabs button.active { background: white; color: var(--ink); box-shadow: 0 2px 8px rgba(6, 26, 70, 0.35); }
.spacer { flex: 1; }
.toplink {
color: var(--muted); text-decoration: none; font-size: 0.82rem; font-weight: 550; white-space: nowrap;
transition: color 0.15s;
}
.toplink:hover { color: white; }
.toplink.cta {
color: white; border: 1px solid rgba(255,255,255,0.45); border-radius: 999px; padding: 6px 15px;
transition: background 0.15s, border-color 0.15s;
}
.toplink.cta:hover { background: rgba(255,255,255,0.14); border-color: white; }
/* ------- hero ------- */
main { position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; }
.hero { text-align: center; padding: 52px 20px 26px; max-width: 860px; }
.badge {
display: inline-flex; align-items: center; gap: 8px;
font-size: 0.78rem; font-weight: 550; color: rgba(255,255,255,0.9);
background: rgba(255, 255, 255, 0.14); border: 1px solid rgba(255, 255, 255, 0.30);
backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
border-radius: 999px; padding: 7px 16px; margin-bottom: 26px;
}
.badge-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--green); box-shadow: 0 0 8px var(--green); }
.hero h1 {
font-size: clamp(2.4rem, 6vw, 4.2rem); font-weight: 700; letter-spacing: -0.03em; line-height: 1.04;
text-shadow: 0 2px 24px rgba(9, 32, 88, 0.35);
}
.hero .sub {
margin-top: 18px; font-size: clamp(0.95rem, 1.6vw, 1.1rem); color: rgba(255,255,255,0.85);
font-weight: 450; line-height: 1.5;
}
/* ------- console ------- */
.console {
width: min(780px, calc(100vw - 32px));
background: var(--glass); border: 1px solid var(--stroke); border-radius: 24px;
backdrop-filter: blur(20px) saturate(1.25); -webkit-backdrop-filter: blur(20px) saturate(1.25);
box-shadow: 0 24px 60px rgba(6, 26, 70, 0.35);
padding: 20px;
}
textarea, select, input[type="number"] {
font-family: inherit; color: var(--text);
background: rgba(8, 24, 64, 0.30); border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 14px; outline: none;
}
textarea:focus, select:focus, input[type="number"]:focus { border-color: rgba(255,255,255,0.55); box-shadow: 0 0 0 3px rgba(255,255,255,0.12); }
textarea { width: 100%; padding: 14px 16px; font-size: 1rem; line-height: 1.55; resize: vertical; }
textarea::placeholder { color: var(--dim); }
.row { display: flex; gap: 12px; margin-top: 12px; align-items: flex-end; }
.field { display: flex; flex-direction: column; gap: 6px; }
.field.grow { flex: 1; min-width: 0; }
.field label { font-size: 0.72rem; font-weight: 650; letter-spacing: 0.09em; text-transform: uppercase; color: var(--dim); padding-left: 4px; }
select {
appearance: none; -webkit-appearance: none; width: 100%; padding: 11px 36px 11px 14px; font-size: 0.92rem; cursor: pointer;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3' stroke-linecap='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
background-repeat: no-repeat; background-position: right 14px center;
}
select option { color: #10245c; background: white; }
.persona { margin: 8px 4px 0; font-size: 0.8rem; color: var(--muted); min-height: 1.1em; }
/* clone panel */
.clone { margin-top: 14px; border: 1px solid rgba(255,255,255,0.16); border-radius: 16px; background: rgba(8, 24, 64, 0.18); }
.clone summary {
list-style: none; cursor: pointer; padding: 12px 16px; font-size: 0.88rem; font-weight: 600;
display: flex; align-items: center; gap: 8px; user-select: none;
}
.clone summary::-webkit-details-marker { display: none; }
.clone summary::after { content: "+"; margin-left: auto; color: var(--muted); font-weight: 500; }
.clone[open] summary::after { content: "-"; }
.clone-state {
font-size: 0.68rem; font-weight: 700; letter-spacing: 0.06em; color: #0d3d1c;
background: #7de08f; border-radius: 999px; padding: 3px 9px; text-transform: uppercase;
}
.clone-state:empty { display: none; }
.clone-body { padding: 0 14px 14px; display: flex; flex-direction: column; gap: 10px; }
.dropzone {
border: 1.5px dashed rgba(255,255,255,0.35); border-radius: 14px; padding: 18px;
text-align: center; font-size: 0.86rem; color: var(--muted); cursor: pointer; transition: all 0.15s;
}
.dropzone:hover, .dropzone.over { border-color: white; background: rgba(255,255,255,0.08); color: var(--text); }
.dropzone u { text-decoration-thickness: 1px; }
.dropzone button {
border: 1px solid rgba(255,255,255,0.4); background: transparent; color: white; font: 600 0.76rem/1 inherit;
font-family: inherit; border-radius: 999px; padding: 5px 12px; cursor: pointer; margin-left: 4px;
}
.dropzone button:hover { background: rgba(255,255,255,0.14); }
.clip-row { display: flex; gap: 8px; align-items: center; }
.clip-row audio { flex: 1; height: 36px; min-width: 0; }
.clip-row button {
border: 1px solid rgba(255,255,255,0.3); background: transparent; color: var(--muted);
width: 32px; height: 32px; border-radius: 50%; cursor: pointer; flex-shrink: 0;
}
.clip-row button:hover { color: white; border-color: white; }
#ref-text { font-size: 0.88rem; }
.hint { font-size: 0.76rem; color: var(--dim); line-height: 1.45; padding: 0 2px; }
/* advanced row */
.adv { align-items: center; flex-wrap: wrap; }
.switch { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; font-size: 0.82rem; color: var(--muted); }
.switch input { display: none; }
.switch .sw {
width: 34px; height: 20px; border-radius: 999px; background: rgba(8, 24, 64, 0.5);
border: 1px solid rgba(255,255,255,0.25); position: relative; transition: background 0.15s; flex-shrink: 0;
}
.switch .sw::after {
content: ""; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; border-radius: 50%;
background: white; transition: transform 0.15s;
}
.switch input:checked + .sw { background: var(--green); border-color: var(--green); }
.switch input:checked + .sw::after { transform: translateX(14px); }
.seed-box { display: inline-flex; align-items: center; gap: 8px; font-size: 0.82rem; color: var(--muted); }
.seed-box input { width: 92px; padding: 6px 10px; font-size: 0.82rem; border-radius: 10px; }
/* run button + result */
#run-btn {
width: 100%; margin-top: 16px; border: none; border-radius: 999px; padding: 15px;
background: white; color: var(--ink); font: 700 1rem/1 inherit; font-family: inherit;
cursor: pointer; transition: transform 0.12s, box-shadow 0.12s, opacity 0.15s;
box-shadow: 0 8px 24px rgba(6, 26, 70, 0.35);
}
#run-btn:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 12px 30px rgba(6, 26, 70, 0.45); }
#run-btn:disabled { opacity: 0.55; cursor: wait; }
.cold-hint { text-align: center; font-size: 0.74rem; color: var(--dim); margin-top: 9px; }
.result {
margin-top: 16px; border: 1px solid rgba(255,255,255,0.22); border-radius: 18px;
background: rgba(8, 24, 64, 0.28); padding: 14px 16px;
}
.result-top { display: flex; align-items: center; gap: 14px; }
#play-btn {
width: 46px; height: 46px; border-radius: 50%; border: none; flex-shrink: 0;
background: white; color: var(--ink); font-size: 1rem; cursor: pointer;
display: flex; align-items: center; justify-content: center;
box-shadow: 0 4px 14px rgba(6, 26, 70, 0.4); transition: transform 0.12s;
}
#play-btn:hover { transform: scale(1.05); }
#wave { flex: 1; height: 56px; min-width: 0; cursor: pointer; }
.result-meta { display: flex; align-items: center; gap: 10px; margin-top: 10px; font-size: 0.78rem; color: var(--muted); font-weight: 550; }
#result-time { font-variant-numeric: tabular-nums; color: var(--text); }
.result-meta a {
margin-left: auto; color: white; font-weight: 700; text-decoration: none;
border: 1px solid rgba(255,255,255,0.35); border-radius: 999px; padding: 4px 12px; font-size: 0.74rem;
transition: background 0.15s;
}
.result-meta a:hover { background: rgba(255,255,255,0.16); }
/* history */
.history { margin-top: 14px; display: flex; align-items: center; gap: 10px; }
.hlabel { font-size: 0.68rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; color: var(--dim); flex-shrink: 0; }
.history-strip { display: flex; gap: 6px; overflow-x: auto; padding-bottom: 2px; }
.hchip {
flex-shrink: 0; border: 1px solid rgba(255,255,255,0.25); background: rgba(255,255,255,0.10);
color: var(--text); border-radius: 999px; padding: 6px 13px; font: 550 0.76rem/1 inherit; font-family: inherit;
cursor: pointer; transition: background 0.15s;
}
.hchip:hover { background: rgba(255,255,255,0.22); }
/* examples */
.examples {
width: min(880px, calc(100vw - 32px)); margin-top: 26px;
display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; position: relative; z-index: 1;
}
.ex-chip {
border: 1px solid rgba(255,255,255,0.28); background: rgba(255,255,255,0.10);
backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
color: var(--text); border-radius: 999px; padding: 8px 15px; cursor: pointer;
font: 550 0.8rem/1.2 inherit; font-family: inherit; transition: background 0.15s, transform 0.12s;
max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ex-chip b { font-weight: 700; margin-right: 5px; }
.ex-chip:hover { background: rgba(255,255,255,0.22); transform: translateY(-1px); }
.foot { margin: 30px 0 6px; font-size: 0.74rem; color: var(--dim); text-align: center; position: relative; z-index: 1; }
.foot a { color: var(--muted); }
/* workflow view */
#workflow-view { position: fixed; inset: 47px 0 36px 0; z-index: 5; background: #0d1526; }
#workflow-view iframe { width: 100%; height: 100%; border: none; }
/* status bar */
.statusbar {
position: fixed; left: 0; right: 0; bottom: 0; z-index: 20;
display: flex; align-items: center; gap: 10px; padding: 8px 18px;
background: rgba(8, 22, 58, 0.75); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
border-top: 1px solid rgba(255,255,255,0.12); font-size: 0.78rem; color: var(--muted);
}
.pulse { width: 8px; height: 8px; border-radius: 50%; background: #6be07f; flex-shrink: 0; }
.statusbar.busy .pulse { background: #ffd166; animation: pulse 1s ease-in-out infinite; }
@keyframes pulse { 50% { opacity: 0.35; } }
#seed-out { font-variant-numeric: tabular-nums; }
@media (max-width: 640px) {
.brand .brand-sub { display: none; }
.toplink:not(.cta) { display: none; }
.hero { padding-top: 36px; }
}
</style>
</head>
<body>
<div class="cloud c1"></div>
<div class="cloud c2"></div>
<div class="cloud c3"></div>
<div class="cloud c4"></div>
<svg class="hills" viewBox="0 0 1440 360" preserveAspectRatio="none" aria-hidden="true">
<path d="M0,170 C240,110 430,195 720,160 C1010,128 1210,195 1440,150 L1440,360 L0,360 Z" fill="#84c96f"/>
<path d="M0,235 C210,185 480,255 760,225 C1040,195 1240,255 1440,215 L1440,360 L0,360 Z" fill="#52b25c"/>
<path d="M0,295 C260,255 520,315 820,285 C1120,255 1300,315 1440,285 L1440,360 L0,360 Z" fill="#37944b"/>
</svg>
<header class="topbar">
<div class="brand">
<svg class="mark" viewBox="0 0 26 26" aria-hidden="true">
<rect width="26" height="26" rx="7" fill="#35c14f"/>
<path d="M7 8h9M7 13h12M7 18h7" stroke="white" stroke-width="2.4" stroke-linecap="round"/>
</svg>
Maya Research
<span class="brand-sub">Maya 2 Native Studio</span>
</div>
<nav class="mode-tabs">
<button id="mode-studio" class="active">Studio</button>
<button id="mode-workflow">Workflow</button>
</nav>
<div class="spacer"></div>
<a class="toplink" href="https://huggingface.co/maya-research/maya2-native" target="_blank" rel="noopener">Model card ↗</a>
<a class="toplink" href="https://github.com/MayaResearch/maya2-native" target="_blank" rel="noopener">GitHub ↗</a>
<a class="toplink cta" href="/workflow/?ui=canvas" target="_blank" rel="noopener">Open canvas ↗</a>
</header>
<main id="studio-view">
<section class="hero">
<div class="badge"><span class="badge-dot"></span>Apache-2.0 · 12 languages · 20 voices · voice cloning</div>
<h1>Voice models<br />that sound native</h1>
<p class="sub">Real-time speech that carries the accent, rhythm, and emotion of a native speaker —<br />in 11 Indic languages and English.</p>
</section>
<section class="console">
<textarea id="text" rows="4" placeholder="Type something to say — in any of the 12 supported languages…"></textarea>
<div class="row">
<div class="field grow"><label for="lang">Language</label><select id="lang"></select></div>
<div class="field grow"><label for="voice">Voice</label><select id="voice"></select></div>
</div>
<div class="persona" id="persona"></div>
<details class="clone" id="clone-panel">
<summary>🎭 Clone a voice instead <span class="clone-state" id="clone-state"></span></summary>
<div class="clone-body">
<div class="dropzone" id="dropzone">
Drop a 3–10 s clip here, or <u>browse files</u> ·
<button id="sample-ref" type="button">try the Ritvik sample</button>
</div>
<div class="clip-row" id="clip-row" hidden>
<audio id="clip-player" controls></audio>
<button id="clip-clear" title="Remove reference clip">✕</button>
</div>
<textarea id="ref-text" rows="2" placeholder="Exact transcript of the clip — what is said, word for word…"></textarea>
<div class="row" style="margin-top:0">
<div class="field grow"><label for="ref-lang">Clip language</label><select id="ref-lang"></select></div>
</div>
<p class="hint">A reference clip overrides the named voice. The clip's language and the output
language can differ — a Hindi reference can speak Tamil. Only clone voices you have permission to use.</p>
</div>
</details>
<div class="row adv">
<label class="switch"><input type="checkbox" id="normalize" checked /><span class="sw"></span><span>Normalize numbers &amp; dates</span></label>
<div class="spacer"></div>
<div class="seed-box"><label for="seed">Seed</label><input type="number" id="seed" value="0" min="0" max="2147483647" /></div>
<label class="switch"><input type="checkbox" id="randomize" checked /><span class="sw"></span><span>Randomize</span></label>
</div>
<button id="run-btn">▶&nbsp; Generate speech</button>
<p class="cold-hint">The first run after idle wakes a GPU worker — about a minute. Warm runs take seconds.</p>
<div class="result" id="result" hidden>
<div class="result-top">
<button id="play-btn" title="Play / pause">▶</button>
<canvas id="wave"></canvas>
</div>
<div class="result-meta">
<span id="result-time">0:00</span>
<span id="result-meta">—</span>
<a id="download" download="maya2-native.wav">Download ↓</a>
</div>
<audio id="player" hidden></audio>
</div>
<div class="history" id="history" hidden>
<span class="hlabel">History</span>
<div class="history-strip" id="history-strip"></div>
</div>
</section>
<section class="examples" id="examples"></section>
<footer class="foot">
Unofficial studio · Model by <a href="https://huggingface.co/maya-research" target="_blank" rel="noopener">Maya Research</a> · Apache-2.0 ·
Disclose synthetic speech; clone only with permission
</footer>
</main>
<section id="workflow-view" hidden>
<iframe id="workflow-frame" title="Maya 2 Native workflow canvas"></iframe>
</section>
<footer class="statusbar" id="statusbar">
<div class="pulse"></div>
<span id="status">Idle</span>
<div class="spacer"></div>
<span id="seed-out"></span>
</footer>
<input type="file" id="file-input" accept="audio/*,.wav,.mp3,.flac,.ogg,.m4a" />
<script type="module">
import { client } from "https://cdn.jsdelivr.net/npm/@gradio/client@2.7.1/dist/index.min.js";
const $ = (id) => document.getElementById(id);
const state = { app: null, refPath: null };
const LANGS = [
["hi", "हिन्दी · Hindi"], ["en", "English"], ["as", "অসমীয়া · Assamese"],
["bn", "বাংলা · Bengali"], ["gu", "ગુજરાતી · Gujarati"], ["kn", "ಕನ್ನಡ · Kannada"],
["ml", "മലയാളം · Malayalam"], ["mr", "मराठी · Marathi"], ["or", "ଓଡ଼ିଆ · Odia"],
["pa", "ਪੰਜਾਬੀ · Punjabi"], ["ta", "தமிழ் · Tamil"], ["te", "తెలుగు · Telugu"],
];
const LANG_NATIVE = Object.fromEntries(LANGS.map(([c, n]) => [c, n.split(" · ")[0]]));
const VOICES = {
Ahana: ["Young female · bright, youthful", "all 11 Indic languages + English"],
Vaidehi: ["Adult female · calm, gentle", "all 11 Indic languages + English"],
Malini: ["Mature female · steady narrator", "all 11 Indic languages + English"],
Charvi: ["Young female · energetic, upbeat", "all 11 Indic languages + English"],
Jahnavi: ["Adult female · warm, friendly", "10 Indic languages + English"],
Anirban: ["Adult male · steady narrator", "all 11 Indic languages + English"],
Nakul: ["Young male · bright, youthful", "all 11 Indic languages + English"],
Vasant: ["Mature male · steady narrator", "10 Indic languages + English"],
Siddhant: ["Adult male · clear, professional", "all 11 Indic languages + English"],
Satvik: ["Young male · warm, friendly", "all 11 Indic languages + English"],
Abhay: ["Adult male · conversational", "English, Hindi"],
Nivedita: ["Adult female · conversational", "English, Hindi"],
Ishwari: ["Adult female · conversational", "Hindi"],
Ritvik: ["Adult male · conversational", "Hindi"],
Vrinda: ["Young female · conversational", "Hindi"],
Sreekar: ["Adult male · conversational", "English, Hindi, Telugu"],
Kayal: ["Adult female · conversational", "English, Tamil"],
Shaunak: ["Young male · conversational", "English, Hindi"],
Akshat: ["Adult male · conversational", "English, Hindi"],
Tanushree:["Adult female · conversational", "English, Hindi"],
};
const EXAMPLES = [
["क्या आपका वर्तमान पता 45, एमजी रोड, अंधेरी ईस्ट, मुंबई 400069 है? यदि हाँ, तो 1 दबाएं।", "hi", "Ritvik"],
["The EMI for your home loan is due by 12-10-24; please ensure your account has sufficient funds.", "en", "Siddhant"],
["এন ই এফ টি লেনদেনটো সম্পূৰ্ণ হ'বলৈ প্ৰায় 2 ঘণ্টা 30 মিনিট সময় লাগিব পাৰে।", "as", "Nakul"],
["12/08/2023 তারিখে আপনার 1,500 টাকার refund process হয়ে গেছে, status update চেক করুন।", "bn", "Jahnavi"],
["જી બિલકુલ, મને સિસ્ટમમાં દેખાય છે કે તમારું ₹2,500 નું પેમેન્ટ 15મી તારીખે સક્સેસફુલ થઈ ગયું છે.", "gu", "Vaidehi"],
["ಹೌದಾ... ಅದು ನಿಜಕ್ಕೂ ಕಷ್ಟ. ನಿಮ್ಮ ಅಂತಿಮ ಬಿಲ್ ₹45,000 ಆಗಿದೆ, ಅದರಲ್ಲಿ ಇನ್ಶೂರೆನ್ಸ್ ಕವರ್ ಆಗಿದೆಯಾ ಅಂತ ಚೆಕ್ ಮಾಡ್ತೀನಿ.", "kn", "Satvik"],
["റദ്ദാക്കിയ ടിക്കറ്റിന്റെ റീഫണ്ട് തുകയായ ₹1,800 മൂന്ന് ദിവസത്തിനുള്ളിൽ ലഭിക്കും.", "ml", "Anirban"],
["मला 98765 43210 या नंबरवर OTP आला नाही, कृपया पुन्हा resend करा.", "mr", "Charvi"],
["ଦୟାକରି ମନେରଖନ୍ତୁ ଯେ EMI ପ୍ରତ୍ୟେକ ମାସର 5 ତାରିଖରେ କଟିବ ଏବଂ ଏହା ଆପଣଙ୍କର 3ୟ କିସ୍ତି।", "or", "Ahana"],
["NEFT ਰਾਹੀਂ ਫੰਡ ਟਰਾਂਸਫਰ 00:30 ਤੋਂ 23:30 ਤੱਕ ਉਪਲਬਧ ਹਨ।", "pa", "Malini"],
["கடந்த நிதியாண்டில் வங்கியின் மொத்த லாபம் ₹3.5 கோடி, இதில் ₹45 லட்சம் முதலீட்டாளர்களுக்கு வழங்கப்பட்டது.", "ta", "Abhay"],
["సేవింగ్స్ తో పోలిస్తే ఫిక్స్డ్ డిపాజిట్ వడ్డీ నిష్పత్తి 3:2 గా ఉండి, 7.25% ఇస్తుంది.", "te", "Nivedita"],
];
// Bundled showcase clip from the model repo — handy for a first voice-cloning run.
const SAMPLE_REF = {
url: "https://huggingface.co/maya-research/maya2-native/resolve/main/samples/hi_ritvik.wav",
name: "hi_ritvik.wav",
text: "क्या आपका वर्तमान पता 45, एमजी रोड, अंधेरी ईस्ट, मुंबई 400069 है? यदि हाँ, तो 1 दबाएं।",
lang: "hi",
};
// ---------- setup ----------
for (const [code, name] of LANGS) {
$("lang").add(new Option(name, code));
$("ref-lang").add(new Option(name, code));
}
$("lang").value = "hi";
$("ref-lang").value = "hi";
for (const name of Object.keys(VOICES)) $("voice").add(new Option(name, name));
$("voice").value = "Ritvik";
function showPersona() {
const p = VOICES[$("voice").value];
$("persona").textContent = p ? `${p[0]} — trained on ${p[1]}` : "";
}
$("voice").onchange = showPersona;
showPersona();
for (const [text, lang, voice] of EXAMPLES) {
const chip = document.createElement("button");
chip.className = "ex-chip";
chip.innerHTML = `<b>${LANG_NATIVE[lang]}</b>${text}`;
chip.title = `${text} · voice: ${voice}`;
chip.onclick = () => {
$("text").value = text;
$("lang").value = lang;
$("voice").value = voice;
showPersona();
$("text").focus();
setStatus(`Example loaded — ${voice} reads ${LANG_NATIVE[lang]}. Hit Generate.`);
};
$("examples").appendChild(chip);
}
// ---------- gradio client ----------
async function getApp() {
if (!state.app) state.app = await client(location.origin);
return state.app;
}
async function uploadFile(file) {
const form = new FormData();
form.append("files", file);
const resp = await fetch("/gradio_api/upload", { method: "POST", body: form });
if (!resp.ok) throw new Error(`Upload failed (${resp.status})`);
const paths = await resp.json();
return Array.isArray(paths) ? paths[0] : paths;
}
function setStatus(text, busy = false) {
$("status").textContent = text;
$("statusbar").classList.toggle("busy", busy);
$("run-btn").disabled = busy;
}
// ---------- mode tabs ----------
let frameLoaded = false;
function setMode(mode) {
$("mode-studio").classList.toggle("active", mode === "studio");
$("mode-workflow").classList.toggle("active", mode === "workflow");
$("studio-view").style.display = mode === "workflow" ? "none" : "";
$("workflow-view").hidden = mode !== "workflow";
if (mode === "workflow" && !frameLoaded) {
$("workflow-frame").src = "/workflow/?ui=canvas";
frameLoaded = true;
}
}
$("mode-studio").onclick = () => setMode("studio");
$("mode-workflow").onclick = () => setMode("workflow");
// ---------- engine pre-warm: build the engine while the user types ----------
(async () => {
setStatus("Warming up the engine in the background…");
try {
const app = await getApp();
await app.predict("/warmup", {});
setStatus("Engine warm — hit Generate.");
document.querySelector(".cold-hint").textContent = "The engine is warm — runs take a few seconds.";
} catch {
setStatus("Idle — the first run builds the engine (~1 min).");
}
})();
// ---------- reference clip ----------
async function attachClip(file) {
setStatus("Uploading reference clip…", true);
try {
state.refPath = await uploadFile(file);
$("clip-player").src = URL.createObjectURL(file);
$("clip-row").hidden = false;
$("clone-state").textContent = "active";
$("clone-panel").open = true;
updateRunLabel();
setStatus("Reference clip ready — add its exact transcript.");
} catch (err) {
setStatus(String(err));
}
}
$("dropzone").onclick = () => $("file-input").click();
$("dropzone").addEventListener("dragover", (e) => { e.preventDefault(); $("dropzone").classList.add("over"); });
$("dropzone").addEventListener("dragleave", () => $("dropzone").classList.remove("over"));
$("dropzone").addEventListener("drop", (e) => {
e.preventDefault();
$("dropzone").classList.remove("over");
if (e.dataTransfer.files[0]) attachClip(e.dataTransfer.files[0]);
});
$("file-input").onchange = (e) => {
const file = e.target.files[0];
e.target.value = "";
if (file) attachClip(file);
};
$("sample-ref").onclick = async (e) => {
e.stopPropagation();
setStatus("Fetching the Ritvik sample clip…", true);
try {
const blob = await (await fetch(SAMPLE_REF.url)).blob();
await attachClip(new File([blob], SAMPLE_REF.name, { type: "audio/wav" }));
$("ref-text").value = SAMPLE_REF.text;
$("ref-lang").value = SAMPLE_REF.lang;
setStatus("Sample clip + transcript loaded — hit Generate.");
} catch (err) {
setStatus(String(err));
}
};
$("clip-clear").onclick = () => {
state.refPath = null;
$("clip-row").hidden = true;
$("clip-player").removeAttribute("src");
$("clone-state").textContent = "";
updateRunLabel();
};
function updateRunLabel() {
$("run-btn").innerHTML = state.refPath ? "▶&nbsp; Generate with cloned voice" : "▶&nbsp; Generate speech";
}
// ---------- history ----------
const historyItems = [];
function pushHistory(item) {
historyItems.unshift(item);
if (historyItems.length > 10) historyItems.pop();
$("history").hidden = false;
const strip = $("history-strip");
strip.innerHTML = "";
historyItems.forEach((h, i) => {
const chip = document.createElement("button");
chip.className = "hchip";
chip.textContent = `${h.voice} · ${h.lang} · ${h.duration}s`;
chip.title = h.text.slice(0, 120);
chip.onclick = () => showResult(h, false);
strip.appendChild(chip);
});
}
// ---------- generate ----------
function unwrap(result) {
const data = Array.isArray(result?.data) ? result.data[0] : result?.data;
return data ?? {};
}
// ---------- waveform player ----------
const wave = $("wave");
const wctx = wave.getContext("2d");
const player = $("player");
let wavePeaks = null;
let audioCtx = null;
async function buildWave(url) {
try {
audioCtx = audioCtx || new (window.AudioContext || window.webkitAudioContext)();
const buf = await audioCtx.decodeAudioData(await (await fetch(url)).arrayBuffer());
const data = buf.getChannelData(0);
const bars = 120;
const peaks = new Float32Array(bars);
const step = Math.floor(data.length / bars) || 1;
for (let i = 0; i < bars; i++) {
let peak = 0;
const start = i * step;
for (let j = start; j < Math.min(start + step, data.length); j += 8) {
const v = Math.abs(data[j]);
if (v > peak) peak = v;
}
peaks[i] = peak;
}
wavePeaks = peaks;
} catch {
wavePeaks = null; // decode failed: fall back to a flat progress bar
}
drawWave();
}
function drawWave() {
const dpr = window.devicePixelRatio || 1;
const w = wave.clientWidth, h = wave.clientHeight;
if (!w) return;
if (wave.width !== Math.round(w * dpr)) { wave.width = Math.round(w * dpr); wave.height = Math.round(h * dpr); }
wctx.setTransform(dpr, 0, 0, dpr, 0, 0);
wctx.clearRect(0, 0, w, h);
let frac = player.duration ? player.currentTime / player.duration : 0;
if (streamCtl && streamCtl.streamed > 0 && audioCtx) {
frac = Math.min(1, Math.max(0, (audioCtx.currentTime - streamCtl.t0) / streamCtl.streamed));
}
const bars = wavePeaks ? wavePeaks.length : 120;
const gap = 2;
const bw = Math.max(1.5, (w - gap * (bars - 1)) / bars);
for (let i = 0; i < bars; i++) {
const p = wavePeaks ? Math.max(0.06, wavePeaks[i]) : 0.5;
const bh = Math.max(3, p * (h - 6));
const x = i * (bw + gap);
wctx.fillStyle = i / bars <= frac ? "#ffffff" : "rgba(255,255,255,0.30)";
if (wctx.roundRect) {
wctx.beginPath();
wctx.roundRect(x, (h - bh) / 2, bw, bh, bw / 2);
wctx.fill();
} else {
wctx.fillRect(x, (h - bh) / 2, bw, bh);
}
}
}
function fmtTime(s) {
if (!isFinite(s)) s = 0;
return `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, "0")}`;
}
player.addEventListener("timeupdate", () => {
drawWave();
$("result-time").textContent = `${fmtTime(player.currentTime)} / ${fmtTime(player.duration)}`;
});
player.addEventListener("play", () => { $("play-btn").textContent = "⏸"; });
player.addEventListener("pause", () => { $("play-btn").textContent = "▶"; });
player.addEventListener("ended", () => { $("play-btn").textContent = "▶"; drawWave(); });
$("play-btn").onclick = () => {
if (streamCtl && audioCtx) { // a take is streaming: pause the whole context
if (audioCtx.state === "running") { audioCtx.suspend(); $("play-btn").textContent = "▶"; }
else { audioCtx.resume(); $("play-btn").textContent = "⏸"; }
return;
}
if (!player.src) return;
player.paused ? player.play().catch(() => {}) : player.pause();
};
wave.addEventListener("click", (e) => {
if (!player.duration) return;
const box = wave.getBoundingClientRect();
player.currentTime = Math.min(0.999, Math.max(0, (e.clientX - box.left) / box.width)) * player.duration;
});
window.addEventListener("resize", drawWave);
function showResult(item, isNew, autoplay = true) {
stopLive();
player.src = item.audio_url;
$("download").href = item.audio_url;
$("result-meta").textContent = `${item.voice} · ${item.langName} · ${item.duration}s`;
$("result-time").textContent = `0:00 / ${fmtTime(item.duration)}`;
$("result").hidden = false;
$("seed-out").textContent = "seed " + item.seed;
$("play-btn").textContent = "▶";
wavePeaks = null;
drawWave();
buildWave(item.audio_url);
if (autoplay) player.play().catch(() => {});
if (isNew) pushHistory(item);
}
// ---------- streaming generation (SSE of PCM chunks -> WebAudio) ----------
let streamCtl = null;
function stopLive() {
if (streamCtl) {
streamCtl.aborted = true;
try { streamCtl.ac.abort(); } catch {}
streamCtl = null;
if (audioCtx) { audioCtx.close().catch(() => {}); audioCtx = null; }
}
$("play-btn").textContent = "▶";
}
function base64ToInt16(b64) {
const bin = atob(b64);
const bytes = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
return new Int16Array(bytes.buffer);
}
function scheduleChunk(ctl, pcm) {
const buf = audioCtx.createBuffer(1, pcm.length, 24000);
const chan = buf.getChannelData(0);
for (let i = 0; i < pcm.length; i++) chan[i] = pcm[i] / 32768;
const src = audioCtx.createBufferSource();
src.buffer = buf;
src.connect(audioCtx.destination);
const start = Math.max(audioCtx.currentTime + 0.12, ctl.playTime);
src.start(start);
ctl.playTime = start + buf.duration;
ctl.streamed += buf.duration;
}
async function streamGenerate(payload) {
const ctl = { ac: new AbortController(), streamed: 0, playTime: 0, t0: 0, done: false, aborted: false };
streamCtl = ctl;
let resp;
try {
resp = await fetch("/api/stream", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
signal: ctl.ac.signal,
});
} catch (err) {
if (ctl.aborted) return null;
throw err;
}
if (ctl.aborted) return null;
if (!resp.ok || !resp.body) {
let msg = `Stream failed (${resp.status})`;
try { msg = (await resp.json()).detail || msg; } catch {}
throw new Error(msg);
}
audioCtx = audioCtx || new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 24000 });
await audioCtx.resume();
ctl.t0 = audioCtx.currentTime + 0.05;
$("result").hidden = false;
$("result-meta").textContent = `${payload.voice || "cloned voice"} · ${LANG_NATIVE[payload.lang] || payload.lang} · live`;
$("result-time").textContent = "0:00";
$("play-btn").textContent = "⏸";
wavePeaks = [];
drawWave();
const reader = resp.body.getReader();
const decoder = new TextDecoder();
let buf = "";
let finalItem = null;
try {
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
let idx;
while ((idx = buf.indexOf("\n\n")) >= 0) {
const block = buf.slice(0, idx);
buf = buf.slice(idx + 2);
for (const line of block.split("\n")) {
if (!line.startsWith("data: ")) continue;
const ev = JSON.parse(line.slice(6));
if (ev.type === "audio") {
const pcm = base64ToInt16(ev.audio);
scheduleChunk(ctl, pcm);
let peak = 0;
for (let i = 0; i < pcm.length; i += 16) {
const v = Math.abs(pcm[i]) / 32768;
if (v > peak) peak = v;
}
wavePeaks.push(peak);
drawWave();
$("result-time").textContent = `${fmtTime(Math.max(0, audioCtx.currentTime - ctl.t0))} / ${fmtTime(ctl.streamed)}`;
setStatus(`Streaming… ${ctl.streamed.toFixed(1)}s of audio`, true);
} else if (ev.type === "done") {
finalItem = ev;
} else if (ev.type === "error") {
throw new Error(ev.message);
}
}
}
}
} catch (err) {
if (ctl.aborted) return null;
throw err;
}
ctl.done = true;
if (ctl.aborted) return null;
return { item: finalItem, ctl };
}
// Swap the live stream for the file-backed player once playback has drained.
function watchDrain(ctl, item, meta) {
if (streamCtl !== ctl) return; // a newer take replaced this one
if (!audioCtx || audioCtx.currentTime + 0.05 >= ctl.playTime) {
streamCtl = null;
showResult({ ...item, ...meta }, true, false); // no autoplay: already heard
return;
}
drawWave();
$("result-time").textContent = `${fmtTime(Math.max(0, audioCtx.currentTime - ctl.t0))} / ${fmtTime(ctl.streamed)}`;
setTimeout(() => watchDrain(ctl, item, meta), 200);
}
$("run-btn").onclick = async () => {
const text = $("text").value.trim();
if (!text) { setStatus("Type something to say first."); $("text").focus(); return; }
const lang = $("lang").value;
const seed = parseInt($("seed").value, 10) || 0;
const randomize = $("randomize").checked;
const normalize = $("normalize").checked;
const cloning = state.refPath != null;
if (cloning && !$("ref-text").value.trim()) {
setStatus("Voice cloning needs the reference clip's exact transcript.");
$("clone-panel").open = true;
$("ref-text").focus();
return;
}
stopLive();
const payload = { text, lang, voice: $("voice").value, normalize, seed, randomize };
if (cloning) Object.assign(payload, {
ref_audio: state.refPath,
ref_text: $("ref-text").value.trim(),
ref_lang: $("ref-lang").value,
});
const meta = {
text, lang,
langName: LANG_NATIVE[lang] || lang,
voice: cloning ? "cloned voice" : $("voice").value,
};
try {
setStatus("Waking the GPU worker — first run can take about a minute…", true);
const outcome = await streamGenerate(payload);
if (!outcome) return; // aborted by a newer take
const { item, ctl } = outcome;
if (!item || !item.audio_url) { setStatus("Stream ended without audio (see console)."); return; }
watchDrain(ctl, item, meta);
setStatus("Done");
} catch (err) {
stopLive();
setStatus(err?.message || String(err));
}
};
// Ctrl/Cmd+Enter generates.
$("text").addEventListener("keydown", (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") $("run-btn").click();
});
</script>
</body>
</html>