Add voice management functionality and voice synthesis preview

- Implemented voice management routes in `voices.py` for listing, saving, and deleting speaker configurations.
- Added a test endpoint for voice synthesis preview, allowing users to test voice settings with provided text and speed.
- Introduced utility functions in `voice.py` for building voice catalogs, resolving voice settings, and synthesizing audio from normalized text.
- Enhanced speaker roster building and configuration application logic to support dynamic voice settings.
This commit is contained in:
JB
2025-11-28 14:57:23 -08:00
parent 0a2b3533f4
commit d08cbcfdc9
23 changed files with 4780 additions and 5463 deletions
+3 -3
View File
@@ -39,13 +39,13 @@ def test_prepare_project_layout_uses_timestamped_folder(monkeypatch: pytest.Monk
project_root, audio_dir, subtitle_dir, metadata_dir = _prepare_project_layout(job, tmp_path)
assert project_root.name.startswith("20250101-120000_sample_title"), project_root.name
assert project_root.name.startswith("20250101-120000_Sample_Title"), project_root.name
assert audio_dir == project_root
assert subtitle_dir == project_root
assert metadata_dir is None
output_path = _build_output_path(audio_dir, job.original_filename, "mp3")
assert output_path == project_root / "Sample Title.mp3"
assert output_path == project_root / "Sample_Title.mp3"
def test_prepare_project_layout_creates_project_subdirs(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
@@ -66,4 +66,4 @@ def test_prepare_project_layout_creates_project_subdirs(monkeypatch: pytest.Monk
assert metadata_dir is not None and metadata_dir.is_dir()
output_path = _build_output_path(audio_dir, job.original_filename, "wav")
assert output_path == audio_dir / "Sample Title.wav"
assert output_path == audio_dir / "Sample_Title.wav"
+5 -4
View File
@@ -2,7 +2,8 @@ from pathlib import Path
from werkzeug.datastructures import MultiDict
from abogen.web.routes import _apply_prepare_form, _resolve_voice_setting
from abogen.web.routes.utils.form import apply_prepare_form
from abogen.web.routes.utils.voice import resolve_voice_setting
from abogen.web.service import PendingJob
@@ -55,7 +56,7 @@ def test_apply_prepare_form_handles_custom_mix_for_speakers():
}
)
_, _, _, errors, *_ = _apply_prepare_form(pending, form)
_, _, _, errors, *_ = apply_prepare_form(pending, form)
assert not errors
hero = pending.speakers["hero"]
@@ -75,7 +76,7 @@ def test_resolve_voice_setting_handles_profile_reference():
}
}
voice, profile_name, language = _resolve_voice_setting("profile:Blend", profiles=profiles)
voice, profile_name, language = resolve_voice_setting("profile:Blend", profiles=profiles)
assert voice == "af_nova*0.5+am_liam*0.5"
assert profile_name == "Blend"
@@ -89,6 +90,6 @@ def test_apply_prepare_form_updates_closing_outro_flag():
"read_closing_outro": "false",
})
_apply_prepare_form(pending, form)
apply_prepare_form(pending, form)
assert pending.read_closing_outro is False
+5 -5
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
import io
import time
from abogen.web.service import Job, JobStatus, build_service, _JOB_LOGGER, _build_audiobookshelf_metadata
from abogen.web.service import Job, JobStatus, build_service, _JOB_LOGGER, build_audiobookshelf_metadata
def test_service_processes_job(tmp_path):
@@ -223,7 +223,7 @@ def test_audiobookshelf_metadata_uses_book_number(tmp_path):
},
)
metadata = _build_audiobookshelf_metadata(job)
metadata = build_audiobookshelf_metadata(job)
assert metadata["seriesName"] == "Example Saga"
assert metadata["seriesSequence"] == "7"
@@ -254,7 +254,7 @@ def test_audiobookshelf_metadata_normalizes_sequence_value(tmp_path):
},
)
metadata = _build_audiobookshelf_metadata(job)
metadata = build_audiobookshelf_metadata(job)
assert metadata["seriesName"] == "Example Saga"
assert metadata["seriesSequence"] == "7"
@@ -285,7 +285,7 @@ def test_audiobookshelf_metadata_allows_decimal_sequence(tmp_path):
},
)
metadata = _build_audiobookshelf_metadata(job)
metadata = build_audiobookshelf_metadata(job)
assert metadata["seriesSequence"] == "4.5"
@@ -316,7 +316,7 @@ def test_audiobookshelf_metadata_ignores_author_series_collision(tmp_path):
},
)
metadata = _build_audiobookshelf_metadata(job)
metadata = build_audiobookshelf_metadata(job)
assert "seriesName" not in metadata
assert "seriesSequence" not in metadata