Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b515b8b722 | ||
|
|
fcb4d8884d | ||
|
|
a2adafe9e8 | ||
|
|
ffbf2f21be |
@@ -136,6 +136,7 @@ venv/
|
|||||||
ENV/
|
ENV/
|
||||||
env.bak/
|
env.bak/
|
||||||
venv.bak/
|
venv.bak/
|
||||||
|
.tmp-samples/
|
||||||
|
|
||||||
# Spyder project settings
|
# Spyder project settings
|
||||||
.spyderproject
|
.spyderproject
|
||||||
|
|||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
[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
|
||||||
|
[submodule "src/vendor/epub3-samples"]
|
||||||
|
path = src/vendor/epub3-samples
|
||||||
|
url = https://github.com/IDPF/epub3-samples
|
||||||
|
[submodule "src/vendor/sample-epub-minimal"]
|
||||||
|
path = src/vendor/sample-epub-minimal
|
||||||
|
url = https://github.com/thansen0/sample-epub-minimal
|
||||||
@@ -18,6 +18,18 @@ Recent Python (>=3.11), pip (>=22.3), some sort of virtula environment recommend
|
|||||||
3. Test install running `epub-tts --version`
|
3. Test install running `epub-tts --version`
|
||||||
- Make sure the venv is activated or that pip installed the package in your PATH
|
- Make sure the venv is activated or that pip installed the package in your PATH
|
||||||
|
|
||||||
|
# Sample EPUB download
|
||||||
|
|
||||||
|
After cloning with submodules, the repository now includes the EPUB 3 sample collection under `src/vendor/epub3-samples`.
|
||||||
|
|
||||||
|
Use the new console entrypoints to export the Moby Dick samples as EPUB files:
|
||||||
|
|
||||||
|
- `download-moby-dick` creates `.tmp-samples/moby-dick.epub`
|
||||||
|
- `download-moby-dick-media-overlays` creates `.tmp-samples/moby-dick-mo.epub`
|
||||||
|
- `download-epub-minimal` creates `.tmp-samples/minimal.epub`
|
||||||
|
|
||||||
|
The staging directory `.tmp-samples/` is included in `.gitignore` so generated artifacts stay out of version control.
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|
||||||
# Citation
|
# Citation
|
||||||
|
|||||||
@@ -41,10 +41,14 @@ Issues = "https://git.silveirarosa.com/renatoxsr/epub-tts/issues"
|
|||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
epub-tts = "epub_tts.__main__:main"
|
epub-tts = "epub_tts.__main__:main"
|
||||||
|
download-moby-dick = "epub_tts.sample_download:main"
|
||||||
|
download-moby-dick-media-overlays = "epub_tts.sample_download:main_with_media_overlay"
|
||||||
|
download-epub-minimal = "epub_tts.sample_download:main_minimal_epub"
|
||||||
|
|
||||||
# SETUPTOOLS
|
# SETUPTOOLS
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["src"]
|
where = ["src"]
|
||||||
|
exclude = ["vendor*"]
|
||||||
|
|
||||||
[tool.setuptools.dynamic]
|
[tool.setuptools.dynamic]
|
||||||
#version = {attr = "epub_tts.__version__"}
|
#version = {attr = "epub_tts.__version__"}
|
||||||
|
|||||||
@@ -7,13 +7,19 @@
|
|||||||
import sys
|
import sys
|
||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
|
|
||||||
from epub_tts import tts_backend
|
from epub_tts import aedocw_backend, tts_backend, tts_edge
|
||||||
from epub_tts import tts_edge
|
|
||||||
|
|
||||||
backend = {
|
backend = {
|
||||||
# keys are the backend names, values are the corresponding TTS classes
|
# keys are the backend names, values are the corresponding TTS classes
|
||||||
"default": tts_backend.GenericTTSBackend,
|
"default": tts_backend.GenericTTSBackend,
|
||||||
"edge": tts_edge.TTSEdge,
|
"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("-o", "--output", help="Output audio file", required=True)
|
||||||
p.add_argument("-l", "--language", help="Language to use for TTS")
|
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("-v", "--voice", help="Voice to use for TTS")
|
||||||
p.add_argument("-b", "--backend",
|
p.add_argument(
|
||||||
help="Backend to use for TTS",
|
"-b",
|
||||||
default="default",
|
"--backend",
|
||||||
choices=backend.keys())
|
help="Backend to use for TTS",
|
||||||
|
default="default",
|
||||||
|
choices=backend.keys(),
|
||||||
|
)
|
||||||
args = p.parse_args(args or sys.argv[1:])
|
args = p.parse_args(args or sys.argv[1:])
|
||||||
print(
|
print(
|
||||||
f"Converting {args.input} to {args.output} using voice "
|
f"Converting {args.input} to {args.output} using voice "
|
||||||
@@ -40,6 +49,14 @@ def main(args=None):
|
|||||||
f"backend {args.backend}, ..."
|
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__":
|
if __name__ == "__main__":
|
||||||
sys.exit(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,160 @@
|
|||||||
|
#!/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)
|
||||||
|
|
||||||
|
"""Download EPUB sample files from a vendored samples repository and package them as EPUB archives."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import shutil
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from epub_tts.vendor_utils import get_repo_path
|
||||||
|
|
||||||
|
|
||||||
|
def get_project_root() -> Path:
|
||||||
|
return Path(__file__).resolve().parents[1].parent
|
||||||
|
|
||||||
|
|
||||||
|
def get_temp_sample_root() -> Path:
|
||||||
|
return get_project_root() / ".tmp-samples"
|
||||||
|
|
||||||
|
|
||||||
|
def get_sample_directory(with_media_overlay: bool) -> Path:
|
||||||
|
sample_name = "moby-dick-mo" if with_media_overlay else "moby-dick"
|
||||||
|
sample_dir = get_repo_path("epub3-samples") / "30" / sample_name
|
||||||
|
if not sample_dir.exists():
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"Sample directory not found: {sample_dir}. Ensure the epub3-samples submodule is initialized."
|
||||||
|
)
|
||||||
|
return sample_dir
|
||||||
|
|
||||||
|
|
||||||
|
def get_minimal_sample_file() -> Path:
|
||||||
|
sample_file = get_repo_path("sample-epub-minimal") / "minimal.epub"
|
||||||
|
if not sample_file.exists():
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"Sample EPUB file not found: {sample_file}. Ensure the sample-epub-minimal submodule is initialized."
|
||||||
|
)
|
||||||
|
return sample_file
|
||||||
|
|
||||||
|
|
||||||
|
def copy_sample_to_temp(sample_dir: Path) -> Path:
|
||||||
|
temp_root = get_temp_sample_root()
|
||||||
|
temp_root.mkdir(parents=True, exist_ok=True)
|
||||||
|
dest_dir = temp_root / sample_dir.name
|
||||||
|
if dest_dir.exists():
|
||||||
|
shutil.rmtree(dest_dir)
|
||||||
|
shutil.copytree(sample_dir, dest_dir)
|
||||||
|
return dest_dir
|
||||||
|
|
||||||
|
|
||||||
|
def copy_file_to_temp(sample_file: Path) -> Path:
|
||||||
|
temp_root = get_temp_sample_root()
|
||||||
|
temp_root.mkdir(parents=True, exist_ok=True)
|
||||||
|
dest_file = temp_root / sample_file.name
|
||||||
|
shutil.copy2(sample_file, dest_file)
|
||||||
|
return dest_file
|
||||||
|
|
||||||
|
|
||||||
|
def zip_sample_directory(sample_dir: Path, output_path: Path) -> Path:
|
||||||
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
if output_path.exists():
|
||||||
|
output_path.unlink()
|
||||||
|
|
||||||
|
mimetype_file = sample_dir / "mimetype"
|
||||||
|
with zipfile.ZipFile(output_path, mode="w", compression=zipfile.ZIP_DEFLATED) as archive:
|
||||||
|
if mimetype_file.exists():
|
||||||
|
archive.write(mimetype_file, "mimetype", compress_type=zipfile.ZIP_STORED)
|
||||||
|
for path in sorted(sample_dir.rglob("*")):
|
||||||
|
if path.is_file() and path != mimetype_file:
|
||||||
|
archive.write(path, path.relative_to(sample_dir).as_posix())
|
||||||
|
|
||||||
|
return output_path
|
||||||
|
|
||||||
|
|
||||||
|
def download_sample_moby_dick(
|
||||||
|
output: Optional[str] = None,
|
||||||
|
with_media_overlay: bool = False,
|
||||||
|
) -> Path:
|
||||||
|
sample_dir = get_sample_directory(with_media_overlay=with_media_overlay)
|
||||||
|
temp_sample_dir = copy_sample_to_temp(sample_dir)
|
||||||
|
default_output = get_temp_sample_root() / f"{sample_dir.name}.epub"
|
||||||
|
output_path = Path(output) if output else default_output
|
||||||
|
output_path = output_path.with_suffix(".epub")
|
||||||
|
return zip_sample_directory(temp_sample_dir, output_path)
|
||||||
|
|
||||||
|
|
||||||
|
def download_minimal_epub_sample(output: Optional[str] = None) -> Path:
|
||||||
|
sample_file = get_minimal_sample_file()
|
||||||
|
temp_sample_file = copy_file_to_temp(sample_file)
|
||||||
|
default_output = get_temp_sample_root() / sample_file.name
|
||||||
|
output_path = Path(output) if output else default_output
|
||||||
|
output_path = output_path.with_suffix(".epub")
|
||||||
|
if temp_sample_file != output_path:
|
||||||
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copy2(temp_sample_file, output_path)
|
||||||
|
return output_path
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: Optional[list[str]] = None) -> int:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Download the Moby Dick EPUB sample and package it as an EPUB file."
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-o",
|
||||||
|
"--output",
|
||||||
|
help="Output EPUB file path. Defaults to .tmp-samples/moby-dick.epub or .tmp-samples/moby-dick-mo.epub.",
|
||||||
|
)
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
output_path = download_sample_moby_dick(
|
||||||
|
output=args.output,
|
||||||
|
with_media_overlay=False,
|
||||||
|
)
|
||||||
|
print(f"Created EPUB sample archive: {output_path}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def main_with_media_overlay(argv: Optional[list[str]] = None) -> int:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description=(
|
||||||
|
"Download the Moby Dick EPUB sample with media overlay and package it as an EPUB file."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-o",
|
||||||
|
"--output",
|
||||||
|
help="Output EPUB file path. Defaults to .tmp-samples/moby-dick-mo.epub.",
|
||||||
|
)
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
output_path = download_sample_moby_dick(
|
||||||
|
output=args.output,
|
||||||
|
with_media_overlay=True,
|
||||||
|
)
|
||||||
|
print(f"Created EPUB sample archive: {output_path}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def main_minimal_epub(argv: Optional[list[str]] = None) -> int:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description=(
|
||||||
|
"Download the minimal EPUB sample and copy it to an EPUB file."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-o",
|
||||||
|
"--output",
|
||||||
|
help="Output EPUB file path. Defaults to .tmp-samples/minimal.epub.",
|
||||||
|
)
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
output_path = download_minimal_epub_sample(output=args.output)
|
||||||
|
print(f"Created minimal EPUB sample archive: {output_path}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -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
+1
Submodule src/vendor/epub3-samples added at 7651e2002b
+1
Submodule src/vendor/sample-epub-minimal added at 8e7d6cdd91
Reference in New Issue
Block a user