[epub-tts] Merge aedocw backend vendor integration into main
This commit is contained in:
+12
@@ -0,0 +1,12 @@
|
||||
[submodule "src/vendor/epub2tts"]
|
||||
path = src/vendor/epub2tts
|
||||
url = https://github.com/aedocw/epub2tts
|
||||
[submodule "src/vendor/epub2tts-edge"]
|
||||
path = src/vendor/epub2tts-edge
|
||||
url = https://github.com/aedocw/epub2tts-edge
|
||||
[submodule "src/vendor/epub2tts-chatterbox"]
|
||||
path = src/vendor/epub2tts-chatterbox
|
||||
url = https://github.com/aedocw/epub2tts-chatterbox
|
||||
[submodule "src/vendor/epub2tts-kokoro"]
|
||||
path = src/vendor/epub2tts-kokoro
|
||||
url = https://github.com/aedocw/epub2tts-kokoro
|
||||
@@ -45,6 +45,7 @@ epub-tts = "epub_tts.__main__:main"
|
||||
# SETUPTOOLS
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
exclude = ["vendor*"]
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
#version = {attr = "epub_tts.__version__"}
|
||||
|
||||
@@ -7,13 +7,19 @@
|
||||
import sys
|
||||
from argparse import ArgumentParser
|
||||
|
||||
from epub_tts import tts_backend
|
||||
from epub_tts import tts_edge
|
||||
from epub_tts import aedocw_backend, tts_backend, tts_edge
|
||||
|
||||
backend = {
|
||||
# keys are the backend names, values are the corresponding TTS classes
|
||||
"default": tts_backend.GenericTTSBackend,
|
||||
"edge": tts_edge.TTSEdge,
|
||||
# aedocw-backed implementations — these expect the corresponding
|
||||
# package "console scripts"/entrypoints to be available in the
|
||||
# environment (or an explicit backend_cmd to be provided).
|
||||
"epub2tts": aedocw_backend.AedocwEpub2TTS,
|
||||
"epub2tts-edge": aedocw_backend.AedocwEpub2TTSEdge,
|
||||
"epub2tts-chatterbox": aedocw_backend.AedocwChatterbox,
|
||||
"epub2tts-kokoro": aedocw_backend.AedocwKokoro,
|
||||
}
|
||||
|
||||
|
||||
@@ -29,10 +35,13 @@ def main(args=None):
|
||||
p.add_argument("-o", "--output", help="Output audio file", required=True)
|
||||
p.add_argument("-l", "--language", help="Language to use for TTS")
|
||||
p.add_argument("-v", "--voice", help="Voice to use for TTS")
|
||||
p.add_argument("-b", "--backend",
|
||||
help="Backend to use for TTS",
|
||||
default="default",
|
||||
choices=backend.keys())
|
||||
p.add_argument(
|
||||
"-b",
|
||||
"--backend",
|
||||
help="Backend to use for TTS",
|
||||
default="default",
|
||||
choices=backend.keys(),
|
||||
)
|
||||
args = p.parse_args(args or sys.argv[1:])
|
||||
print(
|
||||
f"Converting {args.input} to {args.output} using voice "
|
||||
@@ -40,6 +49,14 @@ def main(args=None):
|
||||
f"backend {args.backend}, ..."
|
||||
)
|
||||
|
||||
backend_cls = backend[args.backend]
|
||||
backend_instance = backend_cls(
|
||||
backend_cmd=None,
|
||||
language=args.language,
|
||||
voice=args.voice,
|
||||
)
|
||||
backend_instance.run(args.input, args.output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2025-2026 Renato Xavier da Silveira Rosa
|
||||
# See [LICENSE](./LICENSE) or [BSD-3-Clause-Clear](https://spdx.org/licenses/BSD-3-Clause-Clear.html)
|
||||
"""Backends for the aedocw collection of EPUB->TTS repositories.
|
||||
|
||||
This module defines subclasses of GenericTTSBackend that provide sane
|
||||
default backend command names for the aedocw repositories available on
|
||||
GitHub: epub2tts, epub2tts-edge, epub2tts-chatterbox and epub2tts-kokoro.
|
||||
|
||||
The vendored packages are run in isolated virtual environments when the
|
||||
corresponding console script is not available in the current environment.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from . import epub2tts as epub2tts_adapter
|
||||
from . import epub2tts_chatterbox as epub2tts_chatterbox_adapter
|
||||
from . import epub2tts_edge as epub2tts_edge_adapter
|
||||
from . import epub2tts_kokoro as epub2tts_kokoro_adapter
|
||||
from .tts_backend import GenericTTSBackend
|
||||
|
||||
|
||||
class AedocwBackend(GenericTTSBackend):
|
||||
"""Generic aedocw backend wrapper.
|
||||
|
||||
Parameters:
|
||||
repo: repository name (one of the keys in _CMD_MAP)
|
||||
backend_cmd: explicit command/executable to use (overrides repo mapping)
|
||||
language: optional language code
|
||||
voice: optional voice name
|
||||
|
||||
The GenericTTSBackend stores the default command name in self.backend_cmd,
|
||||
but the vendored adapters may still run the package in their own venv if
|
||||
the console script is not present.
|
||||
"""
|
||||
|
||||
_CMD_MAP = {
|
||||
"epub2tts": "epub2tts",
|
||||
"epub2tts-edge": "epub2tts-edge",
|
||||
"epub2tts-chatterbox": "epub2tts-chatterbox",
|
||||
"epub2tts-kokoro": "epub2tts-kokoro",
|
||||
}
|
||||
|
||||
_RUNNER_MAP = {
|
||||
"epub2tts": epub2tts_adapter.run,
|
||||
"epub2tts-edge": epub2tts_edge_adapter.run,
|
||||
"epub2tts-chatterbox": epub2tts_chatterbox_adapter.run,
|
||||
"epub2tts-kokoro": epub2tts_kokoro_adapter.run,
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
repo: str = "epub2tts",
|
||||
backend_cmd: Optional[str] = None,
|
||||
language: Optional[str] = None,
|
||||
voice: Optional[str] = None,
|
||||
):
|
||||
if backend_cmd is None:
|
||||
backend_cmd = self._CMD_MAP.get(repo, "epub2tts")
|
||||
super().__init__(backend_cmd=backend_cmd, language=language, voice=voice)
|
||||
self.repo = repo
|
||||
|
||||
def run(self, input_source, output_dest):
|
||||
runner = self._RUNNER_MAP.get(self.repo)
|
||||
if runner is None:
|
||||
raise ValueError(f"Unknown repository backend: {self.repo}")
|
||||
|
||||
return runner(
|
||||
input_source,
|
||||
output_dest,
|
||||
language=self.language,
|
||||
voice=self.voice,
|
||||
backend_cmd=self.backend_cmd,
|
||||
)
|
||||
|
||||
|
||||
class AedocwEpub2TTS(AedocwBackend):
|
||||
"""Backend configured for github.com/aedocw/epub2tts."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
backend_cmd: Optional[str] = None,
|
||||
language: Optional[str] = None,
|
||||
voice: Optional[str] = None,
|
||||
):
|
||||
super().__init__(
|
||||
repo="epub2tts", backend_cmd=backend_cmd, language=language, voice=voice
|
||||
)
|
||||
|
||||
|
||||
class AedocwEpub2TTSEdge(AedocwBackend):
|
||||
"""Backend configured for github.com/aedocw/epub2tts-edge."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
backend_cmd: Optional[str] = None,
|
||||
language: Optional[str] = None,
|
||||
voice: Optional[str] = None,
|
||||
):
|
||||
super().__init__(
|
||||
repo="epub2tts-edge",
|
||||
backend_cmd=backend_cmd,
|
||||
language=language,
|
||||
voice=voice,
|
||||
)
|
||||
|
||||
|
||||
class AedocwChatterbox(AedocwBackend):
|
||||
"""Backend configured for github.com/aedocw/epub2tts-chatterbox."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
backend_cmd: Optional[str] = None,
|
||||
language: Optional[str] = None,
|
||||
voice: Optional[str] = None,
|
||||
):
|
||||
super().__init__(
|
||||
repo="epub2tts-chatterbox",
|
||||
backend_cmd=backend_cmd,
|
||||
language=language,
|
||||
voice=voice,
|
||||
)
|
||||
|
||||
|
||||
class AedocwKokoro(AedocwBackend):
|
||||
"""Backend configured for github.com/aedocw/epub2tts-kokoro."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
backend_cmd: Optional[str] = None,
|
||||
language: Optional[str] = None,
|
||||
voice: Optional[str] = None,
|
||||
):
|
||||
super().__init__(
|
||||
repo="epub2tts-kokoro",
|
||||
backend_cmd=backend_cmd,
|
||||
language=language,
|
||||
voice=voice,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AedocwBackend",
|
||||
"AedocwEpub2TTS",
|
||||
"AedocwEpub2TTSEdge",
|
||||
"AedocwChatterbox",
|
||||
"AedocwKokoro",
|
||||
]
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2025-2026 Renato Xavier da Silveira Rosa
|
||||
# See [LICENSE](./LICENSE) or [BSD-3-Clause-Clear](https://spdx.org/licenses/BSD-3-Clause-Clear.html)
|
||||
"""Adapter for the vendored github.com/aedocw/epub2tts repository."""
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from .vendor_utils import build_run_command
|
||||
|
||||
SUPPORTED_AUDIO_FORMATS = {
|
||||
".m4b": "m4b",
|
||||
".wav": "wav",
|
||||
".flac": "flac",
|
||||
}
|
||||
|
||||
|
||||
def _normalize_path(path_input: Path | str) -> Path:
|
||||
return Path(path_input)
|
||||
|
||||
|
||||
def _build_command(
|
||||
input_source: Path,
|
||||
output_dest: Path,
|
||||
language: Optional[str] = None,
|
||||
voice: Optional[str] = None,
|
||||
backend_cmd: Optional[str] = None,
|
||||
) -> list[str]:
|
||||
command = build_run_command("epub2tts", "epub2tts", backend_cmd=backend_cmd)
|
||||
command.append(str(input_source))
|
||||
|
||||
if language:
|
||||
command.extend(["--language", language])
|
||||
|
||||
if voice:
|
||||
command.extend(["--speaker", voice])
|
||||
|
||||
output_format = SUPPORTED_AUDIO_FORMATS.get(output_dest.suffix.lower(), "m4b")
|
||||
if output_format != "m4b":
|
||||
command.extend(["--audioformat", output_format])
|
||||
|
||||
return command
|
||||
|
||||
|
||||
def run(
|
||||
input_source: Path | str,
|
||||
output_dest: Path | str,
|
||||
language: Optional[str] = None,
|
||||
voice: Optional[str] = None,
|
||||
backend_cmd: Optional[str] = None,
|
||||
) -> subprocess.CompletedProcess:
|
||||
input_path = _normalize_path(input_source)
|
||||
output_path = _normalize_path(output_dest)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
command = _build_command(
|
||||
input_path,
|
||||
output_path,
|
||||
language=language,
|
||||
voice=voice,
|
||||
backend_cmd=backend_cmd,
|
||||
)
|
||||
|
||||
completed = subprocess.run(command, cwd=str(input_path.parent), check=True)
|
||||
|
||||
generated_path = input_path.with_suffix(
|
||||
SUPPORTED_AUDIO_FORMATS.get(output_path.suffix.lower(), "m4b")
|
||||
)
|
||||
if generated_path != output_path:
|
||||
generated_path.replace(output_path)
|
||||
|
||||
return completed
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2025-2026 Renato Xavier da Silveira Rosa
|
||||
# See [LICENSE](./LICENSE) or [BSD-3-Clause-Clear](https://spdx.org/licenses/BSD-3-Clause-Clear.html)
|
||||
"""Adapter for the vendored github.com/aedocw/epub2tts-chatterbox repository."""
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from .vendor_utils import build_run_command
|
||||
|
||||
DEFAULT_SAMPLE = "none"
|
||||
|
||||
|
||||
def _normalize_path(path_input: Path | str) -> Path:
|
||||
return Path(path_input)
|
||||
|
||||
|
||||
def _build_command(
|
||||
input_source: Path,
|
||||
sample: Optional[str] = None,
|
||||
backend_cmd: Optional[str] = None,
|
||||
) -> list[str]:
|
||||
command = build_run_command(
|
||||
"epub2tts-chatterbox",
|
||||
"epub2tts_chatterbox",
|
||||
backend_cmd=backend_cmd,
|
||||
)
|
||||
command.append(str(input_source))
|
||||
if sample and sample != DEFAULT_SAMPLE:
|
||||
command.extend(["--sample", sample])
|
||||
return command
|
||||
|
||||
|
||||
def run(
|
||||
input_source: Path | str,
|
||||
output_dest: Path | str,
|
||||
voice: Optional[str] = None,
|
||||
backend_cmd: Optional[str] = None,
|
||||
) -> subprocess.CompletedProcess:
|
||||
input_path = _normalize_path(input_source)
|
||||
output_path = _normalize_path(output_dest)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
sample_text = DEFAULT_SAMPLE
|
||||
cwd = input_path.parent
|
||||
if voice:
|
||||
voice_path = Path(voice)
|
||||
sample_text = voice_path.name
|
||||
if voice_path.parent != Path(""):
|
||||
cwd = str(voice_path.parent)
|
||||
|
||||
command = _build_command(input_path, sample=sample_text, backend_cmd=backend_cmd)
|
||||
completed = subprocess.run(command, cwd=cwd, check=True)
|
||||
|
||||
generated_path = Path(cwd) / f"{input_path.stem} ({sample_text}).m4b"
|
||||
if generated_path != output_path:
|
||||
generated_path.replace(output_path)
|
||||
|
||||
return completed
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2025-2026 Renato Xavier da Silveira Rosa
|
||||
# See [LICENSE](./LICENSE) or [BSD-3-Clause-Clear](https://spdx.org/licenses/BSD-3-Clause-Clear.html)
|
||||
"""Adapter for the vendored github.com/aedocw/epub2tts-edge repository."""
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from .vendor_utils import build_run_command
|
||||
|
||||
DEFAULT_SPEAKER = "en-US-AndrewNeural"
|
||||
|
||||
|
||||
def _normalize_path(path_input: Path | str) -> Path:
|
||||
return Path(path_input)
|
||||
|
||||
|
||||
def _build_command(
|
||||
input_source: Path,
|
||||
voice: Optional[str] = None,
|
||||
backend_cmd: Optional[str] = None,
|
||||
) -> list[str]:
|
||||
command = build_run_command(
|
||||
"epub2tts-edge", "epub2tts_edge", backend_cmd=backend_cmd
|
||||
)
|
||||
command.append(str(input_source))
|
||||
if voice:
|
||||
command.extend(["--speaker", voice])
|
||||
return command
|
||||
|
||||
|
||||
def run(
|
||||
input_source: Path | str,
|
||||
output_dest: Path | str,
|
||||
voice: Optional[str] = None,
|
||||
backend_cmd: Optional[str] = None,
|
||||
) -> subprocess.CompletedProcess:
|
||||
input_path = _normalize_path(input_source)
|
||||
output_path = _normalize_path(output_dest)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
speaker = voice or DEFAULT_SPEAKER
|
||||
command = _build_command(input_path, voice=speaker, backend_cmd=backend_cmd)
|
||||
completed = subprocess.run(command, cwd=str(input_path.parent), check=True)
|
||||
|
||||
generated_path = input_path.parent / f"{input_path.stem} ({speaker}).m4b"
|
||||
if generated_path != output_path:
|
||||
generated_path.replace(output_path)
|
||||
|
||||
return completed
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2025-2026 Renato Xavier da Silveira Rosa
|
||||
# See [LICENSE](./LICENSE) or [BSD-3-Clause-Clear](https://spdx.org/licenses/BSD-3-Clause-Clear.html)
|
||||
"""Adapter for the vendored github.com/aedocw/epub2tts-kokoro repository."""
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from .vendor_utils import build_run_command
|
||||
|
||||
DEFAULT_SPEAKER = "af_heart"
|
||||
|
||||
|
||||
def _normalize_path(path_input: Path | str) -> Path:
|
||||
return Path(path_input)
|
||||
|
||||
|
||||
def _build_command(
|
||||
input_source: Path,
|
||||
voice: Optional[str] = None,
|
||||
backend_cmd: Optional[str] = None,
|
||||
) -> list[str]:
|
||||
command = build_run_command(
|
||||
"epub2tts-kokoro", "epub2tts_kokoro", backend_cmd=backend_cmd
|
||||
)
|
||||
command.append(str(input_source))
|
||||
if voice:
|
||||
command.extend(["--speaker", voice])
|
||||
return command
|
||||
|
||||
|
||||
def run(
|
||||
input_source: Path | str,
|
||||
output_dest: Path | str,
|
||||
voice: Optional[str] = None,
|
||||
backend_cmd: Optional[str] = None,
|
||||
) -> subprocess.CompletedProcess:
|
||||
input_path = _normalize_path(input_source)
|
||||
output_path = _normalize_path(output_dest)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
speaker = voice or DEFAULT_SPEAKER
|
||||
command = _build_command(input_path, voice=speaker, backend_cmd=backend_cmd)
|
||||
completed = subprocess.run(command, cwd=str(input_path.parent), check=True)
|
||||
|
||||
generated_path = input_path.parent / f"{input_path.stem} ({speaker}).m4b"
|
||||
if generated_path != output_path:
|
||||
generated_path.replace(output_path)
|
||||
|
||||
return completed
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2025-2026 Renato Xavier da Silveira Rosa
|
||||
# See [LICENSE](./LICENSE) or [BSD-3-Clause-Clear](https://spdx.org/licenses/BSD-3-Clause-Clear.html)
|
||||
"""Compatibility shim for the edge backend in epub-tts."""
|
||||
|
||||
from .aedocw_backend import AedocwEpub2TTSEdge
|
||||
|
||||
|
||||
class TTSEdge(AedocwEpub2TTSEdge):
|
||||
"""Backward-compatible edge backend wrapper."""
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2025-2026 Renato Xavier da Silveira Rosa
|
||||
# See [LICENSE](./LICENSE) or [BSD-3-Clause-Clear](https://spdx.org/licenses/BSD-3-Clause-Clear.html)
|
||||
"""Utilities for running vendored EPUB->TTS repositories in isolated venvs."""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import venv
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def get_vendor_root() -> Path:
|
||||
return Path(__file__).resolve().parents[1] / "vendor"
|
||||
|
||||
|
||||
def get_repo_path(repo_name: str) -> Path:
|
||||
repo_path = get_vendor_root() / repo_name
|
||||
if not repo_path.exists():
|
||||
raise FileNotFoundError(
|
||||
f"Vendored repository '{repo_name}' not found under {get_vendor_root()}"
|
||||
)
|
||||
return repo_path
|
||||
|
||||
|
||||
def get_venv_path(repo_name: str) -> Path:
|
||||
return get_repo_path(repo_name) / ".venv"
|
||||
|
||||
|
||||
def get_venv_python(repo_name: str) -> Path:
|
||||
venv_path = get_venv_path(repo_name)
|
||||
python_executable = (
|
||||
venv_path / "Scripts" / "python.exe"
|
||||
if sys.platform == "win32"
|
||||
else venv_path / "bin" / "python"
|
||||
)
|
||||
if not python_executable.exists():
|
||||
raise FileNotFoundError(
|
||||
f"Python executable not found in virtualenv at {python_executable}"
|
||||
)
|
||||
return python_executable
|
||||
|
||||
|
||||
def ensure_venv(repo_name: str) -> Path:
|
||||
repo_path = get_repo_path(repo_name)
|
||||
venv_path = get_venv_path(repo_name)
|
||||
if not venv_path.exists():
|
||||
venv.EnvBuilder(with_pip=True).create(venv_path)
|
||||
python_executable = get_venv_python(repo_name)
|
||||
subprocess.run(
|
||||
[str(python_executable), "-m", "pip", "install", "-e", str(repo_path)],
|
||||
check=True,
|
||||
)
|
||||
return get_venv_python(repo_name)
|
||||
|
||||
|
||||
def build_run_command(
|
||||
repo_name: str,
|
||||
module_name: str,
|
||||
backend_cmd: Optional[str] = None,
|
||||
) -> list[str]:
|
||||
if backend_cmd is None or backend_cmd == repo_name:
|
||||
python_executable = ensure_venv(repo_name)
|
||||
return [str(python_executable), "-m", module_name]
|
||||
return [backend_cmd]
|
||||
+1
Submodule src/vendor/epub2tts added at 286d12975f
+1
Submodule src/vendor/epub2tts-chatterbox added at 5dead2e9e5
+1
Submodule src/vendor/epub2tts-edge added at 6fb7a0f125
+1
Submodule src/vendor/epub2tts-kokoro added at dd27e5721a
Reference in New Issue
Block a user