ACloudCenter's picture
Make polish real post-processing; fix distortion, underline and dead MP3 click
9d60340
Raw
History Blame Contribute Delete
19 kB
<!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">
<!-- SIDEBAR: cast & model -->
<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 &amp; steady</span><span>Big &amp; 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>
<!-- CANVAS: script composer + transcript -->
<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>
<!-- DOCK: generation status + output -->
<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>
<!-- Now Playing stage -->
<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>
<!-- Voice library 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>
<!-- Clone voice 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>
<!-- Import 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...&#10;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>
<!-- About 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&nbsp;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>