mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
feat: Enhance voice mixer functionality with language filtering and improved directory management
This commit is contained in:
+9
-7
@@ -7,7 +7,7 @@ from typing import Any, Optional
|
||||
|
||||
from flask import Flask
|
||||
|
||||
from abogen.utils import get_user_cache_path
|
||||
from abogen.utils import get_user_cache_path, get_user_output_path
|
||||
|
||||
from .conversion_runner import run_conversion_job
|
||||
from .service import build_service
|
||||
@@ -17,13 +17,15 @@ def _default_dirs() -> tuple[Path, Path]:
|
||||
uploads_override = os.environ.get("ABOGEN_UPLOAD_ROOT")
|
||||
outputs_override = os.environ.get("ABOGEN_OUTPUT_ROOT")
|
||||
|
||||
if uploads_override and outputs_override:
|
||||
uploads = Path(uploads_override)
|
||||
outputs = Path(outputs_override)
|
||||
if uploads_override:
|
||||
uploads = Path(os.path.expanduser(uploads_override)).resolve()
|
||||
else:
|
||||
base = Path(get_user_cache_path("web"))
|
||||
uploads = base / "uploads"
|
||||
outputs = base / "outputs"
|
||||
uploads = Path(get_user_cache_path("web/uploads"))
|
||||
|
||||
if outputs_override:
|
||||
outputs = Path(os.path.expanduser(outputs_override)).resolve()
|
||||
else:
|
||||
outputs = Path(get_user_output_path("web"))
|
||||
|
||||
uploads.mkdir(parents=True, exist_ok=True)
|
||||
outputs.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -33,6 +33,9 @@ body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.brand {
|
||||
@@ -397,8 +400,8 @@ progress.progress::-moz-progress-bar {
|
||||
|
||||
.voice-mixer__layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(240px, 280px) 1fr;
|
||||
gap: 2rem;
|
||||
grid-template-columns: minmax(260px, 300px) 1fr;
|
||||
gap: 2.25rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
@@ -511,8 +514,8 @@ progress.progress::-moz-progress-bar {
|
||||
|
||||
.voice-editor__canvas {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(260px, 1fr) minmax(320px, 1fr);
|
||||
gap: 1.5rem;
|
||||
grid-template-columns: minmax(280px, 340px) minmax(320px, 1fr);
|
||||
gap: 2rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
@@ -526,20 +529,67 @@ progress.progress::-moz-progress-bar {
|
||||
.voice-available__header,
|
||||
.voice-mix__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.voice-available__title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.voice-filter {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.voice-filter label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.voice-filter select {
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||
background: rgba(15, 23, 42, 0.55);
|
||||
padding: 0.55rem 0.75rem;
|
||||
color: var(--text);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.voice-filter select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.15);
|
||||
}
|
||||
|
||||
.voice-available__list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 0.75rem;
|
||||
gap: 0.85rem;
|
||||
max-height: 55vh;
|
||||
overflow-y: auto;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
.voice-available__empty {
|
||||
margin: 0;
|
||||
padding: 1.25rem 1rem;
|
||||
text-align: center;
|
||||
border: 1px dashed rgba(148, 163, 184, 0.25);
|
||||
border-radius: 16px;
|
||||
color: var(--muted);
|
||||
background: rgba(15, 23, 42, 0.3);
|
||||
}
|
||||
|
||||
.voice-available__card {
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
border-radius: 18px;
|
||||
@@ -609,8 +659,8 @@ progress.progress::-moz-progress-bar {
|
||||
.voice-mix__dropzone {
|
||||
border: 2px dashed rgba(148, 163, 184, 0.25);
|
||||
border-radius: 20px;
|
||||
padding: 1.25rem;
|
||||
min-height: 260px;
|
||||
padding: 1.5rem;
|
||||
min-height: 320px;
|
||||
background: rgba(15, 23, 42, 0.25);
|
||||
transition: border 0.2s ease, background 0.2s ease;
|
||||
display: flex;
|
||||
@@ -640,7 +690,7 @@ progress.progress::-moz-progress-bar {
|
||||
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||
border-radius: 18px;
|
||||
background: rgba(15, 23, 42, 0.4);
|
||||
padding: 1rem 1.1rem;
|
||||
padding: 1.1rem 1.2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.9rem;
|
||||
@@ -803,4 +853,17 @@ progress.progress::-moz-progress-bar {
|
||||
max-height: none;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.voice-available__header,
|
||||
.voice-mix__header {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.voice-filter {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.voice-mix__dropzone {
|
||||
min-height: 220px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ const setupVoiceMixer = () => {
|
||||
const selectedListEl = app.querySelector('[data-role="selected-voices"]');
|
||||
const dropzoneEl = app.querySelector('[data-role="dropzone"]');
|
||||
const emptyStateEl = app.querySelector('[data-role="mix-empty"]');
|
||||
const voiceFilterSelect = app.querySelector('[data-role="voice-filter"]');
|
||||
|
||||
if (!profileListEl || !availableListEl || !selectedListEl) {
|
||||
return;
|
||||
@@ -55,6 +56,7 @@ const setupVoiceMixer = () => {
|
||||
language: "a",
|
||||
voices: new Map(),
|
||||
},
|
||||
languageFilter: voiceFilterSelect ? voiceFilterSelect.value : "",
|
||||
};
|
||||
|
||||
let statusTimeout = null;
|
||||
@@ -211,14 +213,16 @@ const setupVoiceMixer = () => {
|
||||
|
||||
const slider = document.createElement("input");
|
||||
slider.type = "range";
|
||||
slider.min = "0";
|
||||
slider.min = "5";
|
||||
slider.max = "100";
|
||||
slider.step = "1";
|
||||
slider.className = "mix-slider";
|
||||
slider.value = String(Math.round(clamp(weight, 0, 1) * 100));
|
||||
setSliderFill(slider, weight);
|
||||
const normalizedWeight = clamp(weight, 0.05, 1);
|
||||
slider.value = String(Math.round(normalizedWeight * 100));
|
||||
setSliderFill(slider, normalizedWeight);
|
||||
slider.addEventListener("input", () => {
|
||||
const value = clamp(Number(slider.value) / 100, 0, 1);
|
||||
const value = clamp(Number(slider.value) / 100, 0.05, 1);
|
||||
slider.value = String(Math.round(value * 100));
|
||||
state.draft.voices.set(voiceId, value);
|
||||
weightLabel.textContent = formatWeight(value);
|
||||
setSliderFill(slider, value);
|
||||
@@ -244,7 +248,24 @@ const setupVoiceMixer = () => {
|
||||
.slice()
|
||||
.sort((a, b) => (a.display_name || a.id).localeCompare(b.display_name || b.id));
|
||||
|
||||
sortedVoices.forEach((voice) => {
|
||||
const filteredVoices = sortedVoices.filter((voice) => {
|
||||
const languageCode = voice.language || voice.id?.charAt(0) || "a";
|
||||
return !state.languageFilter || state.languageFilter === languageCode;
|
||||
});
|
||||
|
||||
if (!filteredVoices.length) {
|
||||
const empty = document.createElement("p");
|
||||
empty.className = "voice-available__empty";
|
||||
const label = state.languageFilter
|
||||
? voiceLanguageLabel(state.languageFilter) || state.languageFilter.toUpperCase()
|
||||
: "All voices";
|
||||
empty.innerHTML = `No voices for <strong>${label}</strong> yet.`;
|
||||
availableListEl.appendChild(empty);
|
||||
updateAvailableState();
|
||||
return;
|
||||
}
|
||||
|
||||
filteredVoices.forEach((voice) => {
|
||||
if (!voice?.id) {
|
||||
return;
|
||||
}
|
||||
@@ -309,6 +330,7 @@ const setupVoiceMixer = () => {
|
||||
});
|
||||
|
||||
updateAvailableState();
|
||||
availableListEl.scrollTop = 0;
|
||||
};
|
||||
|
||||
const addVoiceToDraft = (voiceId, weight = 0.6) => {
|
||||
@@ -445,7 +467,8 @@ const setupVoiceMixer = () => {
|
||||
const [voiceId, weight] = entry;
|
||||
const value = clamp(parseFloat(weight), 0, 1);
|
||||
if (!Number.isNaN(value) && value > 0) {
|
||||
state.draft.voices.set(String(voiceId), value);
|
||||
const normalized = clamp(value, 0.05, 1);
|
||||
state.draft.voices.set(String(voiceId), normalized);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -697,6 +720,13 @@ const setupVoiceMixer = () => {
|
||||
});
|
||||
}
|
||||
|
||||
if (voiceFilterSelect) {
|
||||
voiceFilterSelect.addEventListener("change", () => {
|
||||
state.languageFilter = voiceFilterSelect.value;
|
||||
renderAvailableVoices();
|
||||
});
|
||||
}
|
||||
|
||||
if (nameInput) {
|
||||
nameInput.addEventListener("input", () => {
|
||||
state.draft.name = nameInput.value;
|
||||
|
||||
@@ -50,8 +50,19 @@
|
||||
<div class="voice-editor__canvas">
|
||||
<section class="voice-available">
|
||||
<header class="voice-available__header">
|
||||
<h2>Available voices</h2>
|
||||
<p class="tag">Drag into the mixer or click Add.</p>
|
||||
<div class="voice-available__title">
|
||||
<h2>Available voices</h2>
|
||||
<p class="tag">Drag into the mixer or click Add.</p>
|
||||
</div>
|
||||
<div class="voice-filter">
|
||||
<label for="voice-language-filter">Language</label>
|
||||
<select id="voice-language-filter" data-role="voice-filter">
|
||||
<option value="">All languages</option>
|
||||
{% for key, label in options.languages.items() %}
|
||||
<option value="{{ key }}">{{ label }} ({{ key|upper }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</header>
|
||||
<div class="voice-available__list" data-role="available-voices"></div>
|
||||
</section>
|
||||
@@ -92,8 +103,9 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script id="voice-mixer-options" type="application/json">{{ options | tojson | safe }}</script>
|
||||
<script>
|
||||
window.ABOGEN_VOICE_MIXER_DATA = {{ options | tojson | safe }};
|
||||
window.ABOGEN_VOICE_MIXER_DATA = JSON.parse(document.getElementById('voice-mixer-options').textContent);
|
||||
</script>
|
||||
<script type="module" src="{{ url_for('static', filename='voices.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user