| <!doctype html> |
| <html lang="en"> |
| <head> |
| <meta charset="utf-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> |
| <meta name="theme-color" content="#faf3e7" /> |
| <meta |
| name="description" |
| content="Chorus — an AI voice studio for writing and generating natural multi-speaker dialogue, powered by VibeVoice." |
| /> |
| <title>Chorus — AI Voice Studio</title> |
| <link |
| rel="icon" |
| href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect x='2' y='11' width='4' height='10' rx='2' fill='%23e2582a'/%3E%3Crect x='9' y='6' width='4' height='20' rx='2' fill='%232f6f63'/%3E%3Crect x='16' y='2' width='4' height='28' rx='2' fill='%23e2582a'/%3E%3Crect x='23' y='8' width='4' height='16' rx='2' fill='%23cc8a2e'/%3E%3C/svg%3E" |
| /> |
| <link rel="preconnect" href="https://fonts.googleapis.com" /> |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> |
| <link |
| href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" |
| rel="stylesheet" |
| /> |
| <link rel="stylesheet" href="/static/styles.css" /> |
| <script src="/static/vendor/wavesurfer.min.js" defer></script> |
| <script src="/static/vendor/wavesurfer.timeline.min.js" defer></script> |
| <script src="/static/app.js" defer></script> |
| </head> |
| <body> |
| <header class="topbar"> |
| <a class="wordmark" href="/"> |
| <svg class="logo-mark" viewBox="0 0 34 30" aria-hidden="true"> |
| <rect x="0" y="10" width="4.5" height="10" rx="2.25" fill="#e2582a" /> |
| <rect x="7.5" y="5" width="4.5" height="20" rx="2.25" fill="#2f6f63" /> |
| <rect x="15" y="0" width="4.5" height="30" rx="2.25" fill="#e2582a" /> |
| <rect x="22.5" y="7" width="4.5" height="16" rx="2.25" fill="#cc8a2e" /> |
| <rect x="30" y="11.5" width="4.5" height="7" rx="2.25" fill="#7b4b94" /> |
| </svg> |
| <span class="mark">Chorus</span> |
| <span class="brand-pill">VOICE STUDIO</span> |
| </a> |
| <nav class="topbar-right"> |
| <button class="link-quiet" type="button" id="browseVoicesBtn">Browse voices</button> |
| <button class="link-quiet" type="button" id="aboutBtn">How it works</button> |
| <div class="runtime-status" id="runtimeStatus" aria-live="polite"> |
| <span class="status-dot"></span> |
| <span id="runtimeLabel">Connecting</span> |
| </div> |
| </nav> |
| </header> |
|
|
| <div class="app-shell"> |
| |
| <aside class="pane sidebar"> |
| <div class="side-section"> |
| <span class="side-label">Your cast</span> |
| <div class="seg" id="speakerStepper"> |
| <button type="button" data-count="1"><strong>1</strong><span>Solo</span></button> |
| <button type="button" data-count="2"><strong>2</strong><span>Duo</span></button> |
| <button type="button" data-count="3"><strong>3</strong><span>Trio</span></button> |
| <button type="button" data-count="4"><strong>4</strong><span>Cast</span></button> |
| </div> |
| <div id="voiceRows"></div> |
| <button id="cloneVoiceBtn" class="clone-voice-btn" type="button">🎙️ Clone a voice</button> |
| <div id="voiceConsentRow" class="voice-consent-row" hidden> |
| <label> |
| <input type="checkbox" id="voiceConsentCheckbox" /> |
| I have the right to use each uploaded voice and will disclose this audio as synthetic. |
| </label> |
| </div> |
| </div> |
|
|
| <div class="side-section"> |
| <span class="side-label">Quality</span> |
| <div class="seg" id="qualityPills"></div> |
| </div> |
|
|
| <div class="side-section"> |
| <div class="side-label-row"> |
| <span class="side-label">Expressiveness</span> |
| <span id="cfgScaleValue" class="side-label-value">2.00 · Dynamic</span> |
| </div> |
| <input id="cfgScale" type="range" min="1.0" max="2.0" step="0.05" value="2.0" /> |
| <div class="slider-ends"><span>Calm & steady</span><span>Big & dynamic</span></div> |
| </div> |
|
|
| <div class="sidebar-footer"> |
| Multi-speaker synthesis by <strong>VibeVoice</strong> (Microsoft Research), running on |
| Modal GPU workers. Scripts are written by Qwen2.5-Coder-32B. |
| </div> |
| </aside> |
|
|
| |
| <main class="pane-col canvas"> |
| <div class="card composer-card"> |
| <button class="composer-collapsed" id="composerCollapsedStrip" type="button" hidden> |
| <span class="collapsed-label">Scene</span> |
| <span class="collapsed-summary" id="collapsedSummary"></span> |
| <span class="collapsed-cta">Edit</span> |
| </button> |
| <div id="composerBody"> |
| <h2 class="card-heading">What's the scene?</h2> |
| <div class="composer"> |
| <textarea |
| id="scriptPrompt" |
| class="composer-input" |
| rows="3" |
| placeholder="Describe a scenario — a wizard and an orc debating battle strategy..." |
| ></textarea> |
| <div class="composer-toolbar"> |
| <div class="composer-toolbar-left"> |
| <select id="durationSelect" class="toolbar-select" aria-label="Target length"></select> |
| <button class="toolbar-btn" id="openImportBtn" type="button"> |
| <span aria-hidden="true">📋</span> Import script |
| </button> |
| </div> |
| <button id="generateScriptBtn" class="btn btn-accent" type="button">Write with AI</button> |
| </div> |
| </div> |
| <div class="example-chips" id="examplePills"></div> |
| <div id="scriptGenStatus" class="gen-status"></div> |
| </div> |
| </div> |
|
|
| <div class="card script-card"> |
| <div class="transcript-header"> |
| <h2 id="scriptTitle">Untitled conversation</h2> |
| <span class="transcript-meta" id="scriptDuration"></span> |
| </div> |
|
|
| <div id="turnsList"> |
| <div class="empty-transcript" id="emptyTurns"> |
| <div class="empty-title">No scene yet</div> |
| Describe a scenario above and click <strong>Write with AI</strong>, import a script, |
| or start typing your own line below. |
| </div> |
| </div> |
| <button class="add-turn" id="addTurnBtn" type="button">+ Add a line</button> |
| </div> |
|
|
| <div class="generate-bar"> |
| <span class="generate-bar-meta" id="generateBarMeta">Add dialogue to begin</span> |
| <button id="generateBtn" class="btn btn-ink" type="button">Generate Audio</button> |
| </div> |
| </main> |
|
|
| |
| <aside class="pane dock"> |
| <div class="side-label dock-label">Your recording</div> |
|
|
| <div id="statusCard" class="status-card"> |
| <div class="status-spinner" id="statusSpinner"></div> |
| <div class="status-title"><span class="status-dot-pulse" id="statusDot"></span><span id="statusTitle">Working</span></div> |
| <div class="status-desc" id="statusDesc"></div> |
| <div class="progress-track" id="progressTrack" hidden><div class="progress-fill" id="progressFill"></div></div> |
| <div class="progress-meta" id="progressMeta" hidden></div> |
| <button id="stopGenBtn" class="btn stop-gen-btn" type="button" hidden>Stop generation</button> |
| </div> |
|
|
| <div id="dockEmpty" class="dock-empty">Your generated audio will appear here once you record a take.</div> |
|
|
| <div id="resultBlock" class="result-block"> |
| <div class="player-top"> |
| <button id="playBtn" class="play-btn" type="button" aria-label="Play">►</button> |
| <div id="resultWaveform" class="wave-box"></div> |
| <button id="openPlayerBtn" class="icon-btn" type="button" title="Open player" aria-label="Open player"> |
| <svg width="13" height="13" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M8.5 1.5h4v4"/><path d="M12.5 1.5 7.8 6.2"/><path d="M5.5 12.5h-4v-4"/><path d="M1.5 12.5 6.2 7.8"/></svg> |
| </button> |
| </div> |
| <div class="player-time" id="playerTime">0:00 / 0:00</div> |
|
|
| <div class="synced-transcript" id="syncedTranscript"></div> |
|
|
| <div class="result-metrics"> |
| <div><span>Generation time</span> <strong id="generationTime">--</strong></div> |
| <div id="realtimeRow" hidden><span>Speed</span> <strong id="realtimeFactor">--</strong></div> |
| <div id="warmupRow" hidden><span>GPU warm-up</span> <strong id="warmupTime">--</strong></div> |
| <div><span>Audio length</span> <strong id="audioDuration">--</strong></div> |
| <div><span>Model</span> <strong id="resultModel">--</strong></div> |
| </div> |
| <a id="downloadBtn" class="btn btn-pill-outline" download="chorus-audio.wav">Download WAV</a> |
| <a id="downloadMp3Btn" class="btn btn-pill-outline" download="chorus-audio.mp3" |
| title="Much smaller file — the server encodes it on first click, which can take a minute or two for long takes">Download MP3 (smaller)</a> |
| <audio id="resultAudio" hidden></audio> |
| </div> |
|
|
| <button class="log-toggle" id="logToggleBtn" type="button" hidden>View generation log</button> |
| <div class="log-box" id="logBox"></div> |
| </aside> |
| </div> |
|
|
| |
| <dialog id="playerStage" class="player-stage"> |
| <div class="stage-body"> |
| <button id="stageCloseBtn" class="icon-btn stage-close" type="button" title="Close (Esc)" aria-label="Close player"> |
| <svg width="13" height="13" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"><path d="M2 2l10 10M12 2L2 12"/></svg> |
| </button> |
| <div id="stageGenPane" hidden> |
| <div class="stage-eyebrow">GENERATING · <span id="genStageTitle"></span></div> |
| <div class="gen-pct" id="genStagePct">Starting…</div> |
| <div class="progress-track gen-track" id="genStageTrack" hidden> |
| <div class="progress-fill" id="genStageFill"></div> |
| </div> |
| <div class="gen-stage-meta" id="genStageMeta"></div> |
| <div class="gen-stage-desc" id="genStageDesc"></div> |
| <div class="gen-stage-log" id="genStageLog"></div> |
| <div class="stage-actions"> |
| <button class="btn stop-gen-btn" type="button" id="genStageStopBtn">Stop generation</button> |
| </div> |
| </div> |
|
|
| <div id="stagePlayPane"> |
| <div class="stage-eyebrow">NOW PLAYING · <span id="stageTitle"></span></div> |
| <div class="stage-play-row"> |
| <button id="stagePlayBtn" class="play-btn play-btn-lg" type="button" aria-label="Play">►</button> |
| </div> |
| <div id="stageWaveform" class="wave-box stage-wave"></div> |
| <div class="player-time stage-time" id="stageTime">0:00 / 0:00</div> |
| <div class="stage-caption"> |
| <span class="sync-dot stage-dot" id="stageDot"></span> |
| <div class="stage-line" id="stageLine"></div> |
| <div class="stage-speaker" id="stageSpeaker"></div> |
| </div> |
| <div class="stage-actions"> |
| <a class="btn btn-ink" id="stageDownloadBtn" download="chorus-audio.wav">Download WAV</a> |
| <a class="btn btn-pill-outline" id="stageDownloadMp3Btn" download="chorus-audio.mp3" |
| title="Much smaller file — encoded on the server on first click">MP3</a> |
| <button class="btn btn-pill-outline" id="stagePolishToggle" type="button">Polish</button> |
| <button class="btn btn-pill-outline" id="stageScriptToggle" type="button">View full script</button> |
| </div> |
|
|
| <div class="polish-panel" id="polishPanel" hidden> |
| <div class="polish-row"> |
| <span class="polish-label">Speed</span> |
| <input type="range" id="polishSpeed" min="0.8" max="1.25" step="0.05" value="1" /> |
| <span class="polish-value" id="polishSpeedValue">1.00×</span> |
| </div> |
| <div class="polish-row"> |
| <span class="polish-label">Tone</span> |
| <div class="seg polish-seg" id="polishTone"> |
| <button type="button" class="single-line" data-tone="neutral"><span>Neutral</span></button> |
| <button type="button" class="single-line" data-tone="warm"><span>Warm</span></button> |
| <button type="button" class="single-line" data-tone="bright"><span>Bright</span></button> |
| </div> |
| </div> |
| <label class="polish-check"> |
| <input type="checkbox" id="polishBoost" /> Lift quiet speech |
| </label> |
| <div class="polish-status" id="polishStatus" hidden></div> |
| <div class="polish-note"> |
| <span id="polishNote">Preview only until you change something — then downloads are rendered with these settings.</span> |
| <button type="button" class="polish-reset" id="polishReset">Reset</button> |
| </div> |
| </div> |
| <div class="synced-transcript stage-transcript" id="stageTranscript" hidden></div> |
| </div> |
| </div> |
| </dialog> |
|
|
| |
| <dialog id="voiceLibraryDialog" class="library-dialog"> |
| <div class="dialog-body"> |
| <div class="library-head"> |
| <h3 id="libraryTitle">Choose your voices</h3> |
| <button class="dialog-close" type="button" id="closeLibraryBtn" aria-label="Close">×</button> |
| </div> |
| <input id="librarySearch" class="library-search" placeholder="Search voices by name..." /> |
| <div class="filter-chips" id="libraryFilters"></div> |
| <div class="library-grid" id="libraryGrid"></div> |
| </div> |
| </dialog> |
|
|
| |
| <dialog id="cloneDialog"> |
| <div class="dialog-body"> |
| <div class="library-head"> |
| <h3>Clone a voice</h3> |
| <button class="dialog-close" type="button" id="closeCloneBtn" aria-label="Close">×</button> |
| </div> |
| <p> |
| Record or upload <strong>15–30 seconds</strong> of one person speaking clearly, with no |
| background noise or music. |
| </p> |
| <div class="clone-section-label">Voice name</div> |
| <input id="cloneNameInput" class="clone-name-input" maxlength="40" placeholder="e.g. Josh, Grandma Ruth, Movie Trailer Guy" /> |
| <div class="clone-capture"> |
| <button id="recordBtn" class="btn btn-accent" type="button">● Record</button> |
| <label class="btn clone-upload-label">Upload file<input id="cloneFileInput" type="file" accept="audio/*" hidden /></label> |
| <span id="recordTimer" class="record-timer" hidden>0:00</span> |
| </div> |
| <button id="readScriptToggle" class="read-script-toggle" type="button">Not sure what to say? Show a script to read →</button> |
| <div id="readScriptCard" class="read-script-card" hidden> |
| <div class="read-script-text" id="readScriptText"></div> |
| <div class="read-script-foot"> |
| <span id="readScriptMeta"></span> |
| <button id="readScriptShuffle" type="button">↻ Try another</button> |
| </div> |
| </div> |
| <div id="clonePreview" class="clone-preview" hidden> |
| <audio id="cloneAudio" controls></audio> |
| <div id="cloneMeta" class="clone-meta"></div> |
| </div> |
| <label class="clone-optimize"> |
| <input type="checkbox" id="optimizeCheckbox" checked /> |
| Optimize audio — suppress mic noise, trim silence, remove rumble, level the volume |
| </label> |
| <div class="clone-section-label">Use as</div> |
| <div id="cloneSlotRow" class="clone-slots"></div> |
| <div id="cloneReplaceRow" hidden> |
| <div class="clone-section-label">Voice library full — choose one to replace</div> |
| <div id="cloneReplacePills" class="clone-slots"></div> |
| </div> |
| <label class="clone-consent"> |
| <input type="checkbox" id="cloneConsentCheckbox" /> |
| I have the right to use this voice and will disclose this audio as synthetic. |
| </label> |
| <div class="dialog-actions" style="justify-content: flex-end;"> |
| <button class="btn" type="button" id="cancelCloneBtn">Cancel</button> |
| <button class="btn btn-ink" type="button" id="useCloneBtn" disabled>Use this voice</button> |
| </div> |
| </div> |
| </dialog> |
|
|
| |
| <dialog id="importDialog"> |
| <div class="dialog-body"> |
| <h3>Import a script</h3> |
| <p> |
| Use <code>Speaker 1:</code> … <code>Speaker 4:</code> tags, one turn per line. Untagged |
| text becomes Speaker 1. Named characters (<code>Host:</code>, <code>Mom:</code>) are |
| auto-mapped to speaker slots. |
| </p> |
| <textarea id="pastedScript" rows="7" placeholder="Speaker 1: Welcome to the show... Speaker 2: Thanks for having me..."></textarea> |
| <div class="dialog-actions"> |
| <div class="file-pick"> |
| <label class="btn">Upload .txt<input id="scriptFileUpload" type="file" accept=".txt,.md" hidden /></label> |
| <span class="file-pick-name" id="scriptFileName">No file chosen</span> |
| </div> |
| <div class="row" style="display:flex; gap:8px;"> |
| <button class="btn" type="button" id="cancelImportBtn">Cancel</button> |
| <button class="btn btn-ink" type="button" id="loadScriptBtn">Load</button> |
| </div> |
| </div> |
| </div> |
| </dialog> |
|
|
| |
| <dialog id="aboutDialog"> |
| <div class="dialog-body"> |
| <h3>How Chorus works</h3> |
| <p> |
| VibeVoice generates expressive, long-form, multi-speaker conversational audio from text. |
| It uses continuous speech tokenizers at an ultra-low 7.5 Hz frame rate and a |
| next-token diffusion framework to synthesize up to 90 minutes of speech with up to 4 |
| distinct speakers. |
| </p> |
| <ul> |
| <li><strong>Multi-speaker</strong> — up to 4 distinct voices in one take</li> |
| <li><strong>Long-form</strong> — up to 90 minutes of continuous audio</li> |
| <li><strong>Natural flow</strong> — turn-taking, filler words, natural pacing</li> |
| <li><strong>Two model sizes</strong> — Fast (1.5B) for iteration, Best (7B) for fidelity</li> |
| </ul> |
| <img src="/public/images/diagram.jpg" alt="VibeVoice architecture diagram" /> |
| <div class="dialog-actions" style="justify-content: flex-end;"> |
| <button class="btn btn-ink" type="button" id="closeAboutBtn">Close</button> |
| </div> |
| </div> |
| </dialog> |
| </body> |
| </html> |
|
|