mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-19 22:00:28 +02:00
feat: Implement voice mixer UI and functionality
- Added new styles for the voice mixer components in styles.css. - Updated base.html to include a block for scripts. - Refactored voices.html to create a structured voice mixer interface with profile management features. - Introduced voices.js to handle voice mixer logic, including profile creation, editing, and previewing. - Implemented actions for importing and exporting voice profiles. - Enhanced user experience with loading states and status messages.
This commit is contained in:
@@ -27,5 +27,6 @@
|
||||
<span>Need help?</span>
|
||||
<a href="https://github.com/denizsafak/abogen" target="_blank" rel="noopener">Read the docs</a>
|
||||
</footer>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,64 +3,79 @@
|
||||
{% block title %}abogen · Voice mixer{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="card">
|
||||
<h1 class="card__title">Voice mixer</h1>
|
||||
<p class="tag">Blend multiple voices, store them as reusable profiles, and reuse them in the dashboard form.</p>
|
||||
<div class="grid grid--two">
|
||||
<div class="grid">
|
||||
<h2>Saved profiles</h2>
|
||||
{% if profiles %}
|
||||
<ul class="list">
|
||||
{% for profile in profiles %}
|
||||
<li class="list__item">
|
||||
<div>
|
||||
<strong>{{ profile.name }}</strong>
|
||||
<p class="tag">Language: {{ languages.get(profile.language, profile.language|upper) }}</p>
|
||||
<p class="tag">Formula: {{ profile.formula }}</p>
|
||||
</div>
|
||||
<form method="post" action="{{ url_for('web.delete_voice_profile_route', name=profile.name) }}">
|
||||
<button type="submit" class="button button--danger">Delete</button>
|
||||
</form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>No saved profiles yet. Use the form to create one.</p>
|
||||
{% endif %}
|
||||
<section class="card voice-mixer">
|
||||
<div class="voice-mixer__header">
|
||||
<div>
|
||||
<h1 class="card__title">Voice mixer</h1>
|
||||
<p class="tag">Blend multiple Kokoro voices, audition the mix instantly, and keep reusable presets.</p>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<h2>Create or update</h2>
|
||||
<form method="post" action="{{ url_for('web.save_voice_profile_route') }}" class="grid">
|
||||
<div class="field">
|
||||
<label for="name">Profile name</label>
|
||||
<input type="text" id="name" name="name" required placeholder="Narrator Mix">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="language">Language</label>
|
||||
<select id="language" name="language">
|
||||
{% for key, label in languages.items() %}
|
||||
<option value="{{ key }}">{{ label }} ({{ key|upper }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="formula">Voice formula</label>
|
||||
<input type="text" id="formula" name="formula" required placeholder="af_nova*0.4+am_liam*0.6">
|
||||
<p class="tag">Use <code>voice_name*weight</code> segments joined with <code>+</code>. Weights will be normalised automatically.</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="voices">Available voices</label>
|
||||
<div class="pill-grid">
|
||||
{% for voice in voices %}
|
||||
<span class="pill">{{ voice }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<button type="submit" class="button">Save profile</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="voice-mixer__header-actions">
|
||||
<button type="button" class="button button--ghost" data-action="new-profile">New profile</button>
|
||||
<button type="button" class="button button--ghost" data-action="import-profiles">Import</button>
|
||||
<button type="button" class="button button--ghost" data-action="export-profiles">Export</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="voice-mixer-app" class="voice-mixer__layout" data-state="loading">
|
||||
<aside class="voice-mixer__profiles" data-role="profile-list">
|
||||
<p class="tag">Loading profiles…</p>
|
||||
</aside>
|
||||
<section class="voice-mixer__editor" data-role="editor">
|
||||
<noscript>
|
||||
<p class="tag">JavaScript is required for the mixer. Please enable it to edit profiles.</p>
|
||||
</noscript>
|
||||
<form id="voice-profile-form" class="voice-editor" autocomplete="off">
|
||||
<div class="voice-status" data-role="status"></div>
|
||||
<div class="voice-editor__meta">
|
||||
<div class="field">
|
||||
<label for="profile-name">Profile name</label>
|
||||
<input id="profile-name" name="name" type="text" placeholder="Narrator blend" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="profile-language">Language</label>
|
||||
<select id="profile-language" name="language">
|
||||
{% for key, label in options.languages.items() %}
|
||||
<option value="{{ key }}">{{ label }} ({{ key|upper }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="preview-speed">Preview speed</label>
|
||||
<input id="preview-speed" type="number" step="0.05" min="0.7" max="1.3" value="1.0">
|
||||
</div>
|
||||
</div>
|
||||
<div class="voice-editor__summary">
|
||||
<span data-role="profile-summary">Select or create a profile to begin.</span>
|
||||
<span class="tag" data-role="mix-total">Total weight: 0.00</span>
|
||||
</div>
|
||||
<div class="voice-editor__grid" data-role="voice-grid"></div>
|
||||
<div class="voice-editor__actions">
|
||||
<div class="button-row">
|
||||
<button type="submit" class="button" data-role="save-profile" disabled>Save profile</button>
|
||||
<button type="button" class="button button--ghost" data-role="duplicate-profile">Duplicate</button>
|
||||
<button type="button" class="button button--danger" data-role="delete-profile">Delete</button>
|
||||
</div>
|
||||
<div class="voice-preview">
|
||||
<div class="field">
|
||||
<label for="preview-text">Preview text</label>
|
||||
<textarea id="preview-text" rows="3" data-role="preview-text" placeholder="Paste a short line to audition your mix."></textarea>
|
||||
</div>
|
||||
<div class="voice-preview__controls">
|
||||
<button type="button" class="button" data-role="preview-button">Preview mix</button>
|
||||
<button type="button" class="button button--ghost" data-role="load-sample">Use sample text</button>
|
||||
</div>
|
||||
<audio data-role="preview-audio" controls preload="none"></audio>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
<input id="voice-import-input" type="file" accept="application/json" hidden>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
window.ABOGEN_VOICE_MIXER_DATA = {{ options | tojson | safe }};
|
||||
</script>
|
||||
<script type="module" src="{{ url_for('static', filename='voices.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user