| <!doctype html> |
| <html lang="en"> |
| <head> |
| <meta charset="utf-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> |
| <title>ADI Voice Persona Lab</title> |
| <style> |
| :root { color-scheme: dark; --bg:#080b10; --panel:#151922; --line:#303745; --text:#f5f7fb; --muted:#aab2c0; --accent:#12b981; --orange:#f97316; } |
| * { box-sizing:border-box; } |
| body { margin:0; font:15px/1.45 system-ui, -apple-system, Segoe UI, sans-serif; background:var(--bg); color:var(--text); } |
| main { max-width:1180px; margin:0 auto; padding:28px 18px 40px; } |
| h1 { margin:0 0 18px; font-size:30px; } |
| textarea, select { width:100%; border:1px solid var(--line); border-radius:8px; background:#0d1118; color:var(--text); padding:12px; font:inherit; } |
| textarea { min-height:130px; resize:vertical; } |
| label { display:block; margin:0 0 8px; font-weight:800; } |
| .panel { background:var(--panel); border:1px solid var(--line); border-radius:8px; padding:16px; margin-bottom:16px; } |
| .grid { display:grid; grid-template-columns:repeat(3, minmax(240px, 1fr)); gap:14px; } |
| .row { display:grid; grid-template-columns:repeat(3, minmax(160px, 1fr)); gap:12px; } |
| button { min-height:42px; border:0; border-radius:8px; padding:0 16px; font-weight:900; cursor:pointer; color:#100701; background:var(--orange); } |
| .take { display:flex; flex-direction:column; gap:10px; } |
| .script { min-height:150px; color:#d8ffe9; } |
| .muted { color:var(--muted); } |
| @media (max-width:900px) { .grid, .row { grid-template-columns:1fr; } } |
| </style> |
| </head> |
| <body> |
| <main> |
| <h1>ADI Voice Persona Lab</h1> |
| <section class="panel"> |
| <label for="base">Base ADI response</label> |
| <textarea id="base">Hello, I am ADI. I can help inspect models, compare prompts, generate training data, and turn your ideas into working demos on Hugging Face Spaces.</textarea> |
| <div class="row" style="margin-top:14px"> |
| <div><label for="length">Script length</label><select id="length"><option>short</option><option selected>normal</option><option>expanded</option></select></div> |
| <div><label for="rate">Rate</label><select id="rate"><option value="0.85">slower</option><option value="1" selected>normal</option><option value="1.15">faster</option></select></div> |
| <div><label> </label><button id="generate">Generate Takes</button></div> |
| </div> |
| <p class="muted">Uses your browser's built-in speech voices, so it does not consume Hugging Face CPU quota.</p> |
| </section> |
| <section class="grid"> |
| <div class="panel take"> |
| <label>Persona A</label><select class="persona"><option>ADI calm</option><option>Technical mentor</option><option>Operator mode</option><option>Warm companion</option><option>Launch demo</option></select> |
| <label>Voice A</label><select class="voice"></select> |
| <textarea class="script"></textarea> |
| <button class="speak">Speak A</button> |
| </div> |
| <div class="panel take"> |
| <label>Persona B</label><select class="persona"><option>Technical mentor</option><option>ADI calm</option><option>Operator mode</option><option>Warm companion</option><option>Launch demo</option></select> |
| <label>Voice B</label><select class="voice"></select> |
| <textarea class="script"></textarea> |
| <button class="speak">Speak B</button> |
| </div> |
| <div class="panel take"> |
| <label>Persona C</label><select class="persona"><option>Launch demo</option><option>ADI calm</option><option>Technical mentor</option><option>Operator mode</option><option>Warm companion</option></select> |
| <label>Voice C</label><select class="voice"></select> |
| <textarea class="script"></textarea> |
| <button class="speak">Speak C</button> |
| </div> |
| </section> |
| </main> |
| <script> |
| const personaStyles = { |
| "ADI calm": ["Here is the calm version.", "steady, concise, and reassuring"], |
| "Technical mentor": ["Here is the technical mentor version.", "precise, capable, and lightly explanatory"], |
| "Operator mode": ["Status: ready.", "brief, action-oriented, and status-aware"], |
| "Warm companion": ["Here is the warm companion version.", "friendly, grounded, and conversational"], |
| "Launch demo": ["Here is the launch demo version.", "polished, confident, and presentation-ready"] |
| }; |
| const clean = text => (text || "").trim().replace(/\s+/g, " "); |
| function rewrite(text, persona, length) { |
| const [prefix, style] = personaStyles[persona]; |
| let body = clean(text); |
| const sentences = body.split(/(?<=[.!?])\s+/).filter(Boolean); |
| if (length === "short") body = sentences.slice(0, 2).join(" "); |
| if (length === "expanded") body += ` The intended delivery is ${style}, with enough detail to feel useful.`; |
| if (persona === "Operator mode") body = `${prefix} ${body} Next step: tell me what you want to run.`; |
| else if (persona === "Technical mentor") body = `${prefix} ${body} The key idea is to make the next action obvious.`; |
| else if (persona === "Warm companion") body = `${prefix} ${body} I can stay with you while we tune it.`; |
| else if (persona === "Launch demo") body = `${prefix} ${body} This is ready to show as a live ADI capability.`; |
| else body = `${prefix} ${body}`; |
| return body; |
| } |
| function loadVoices() { |
| const voices = speechSynthesis.getVoices(); |
| document.querySelectorAll(".voice").forEach((select, idx) => { |
| const chosen = select.value; |
| select.innerHTML = ""; |
| voices.forEach((voice, i) => { |
| const option = document.createElement("option"); |
| option.value = i; |
| option.textContent = `${voice.name} (${voice.lang})`; |
| select.appendChild(option); |
| }); |
| if (chosen) select.value = chosen; |
| else select.selectedIndex = Math.min(idx, Math.max(voices.length - 1, 0)); |
| }); |
| } |
| function generate() { |
| const text = document.querySelector("#base").value; |
| const length = document.querySelector("#length").value; |
| document.querySelectorAll(".take").forEach(take => { |
| take.querySelector(".script").value = rewrite(text, take.querySelector(".persona").value, length); |
| }); |
| } |
| function speak(take) { |
| speechSynthesis.cancel(); |
| const utterance = new SpeechSynthesisUtterance(take.querySelector(".script").value); |
| const voices = speechSynthesis.getVoices(); |
| const voice = voices[Number(take.querySelector(".voice").value)]; |
| if (voice) utterance.voice = voice; |
| utterance.rate = Number(document.querySelector("#rate").value); |
| speechSynthesis.speak(utterance); |
| } |
| document.querySelector("#generate").addEventListener("click", generate); |
| document.querySelectorAll(".speak").forEach(button => button.addEventListener("click", e => speak(e.target.closest(".take")))); |
| speechSynthesis.onvoiceschanged = loadVoices; |
| loadVoices(); |
| generate(); |
| </script> |
| </body> |
| </html> |
|
|