Author SHA1 Message Date
renatoxsr fcb4d8884d [epub-tts] Add EPUB sample submodules and download entrypoints 2026-08-06 16:33:59 -03:00
renatoxsr a2adafe9e8 [epub-tts] Merge aedocw backend vendor integration into main 2026-08-06 16:05:04 -03:00
renatoxsrandCopilot ffbf2f21be [epub-tts] Vendor aedocw backends and add adapter subclasses
- Add git submodules under src/vendor for the aedocw repositories:
  - epub2tts, epub2tts-edge, epub2tts-chatterbox, epub2tts-kokoro
- Add adapter modules in src/epub_tts to run vendored packages in isolated venvs:
  - vendor_utils.py (creates/uses per-repo .venv and installs package)
  - epub2tts.py, epub2tts_edge.py, epub2tts_chatterbox.py, epub2tts_kokoro.py
- Add aedocw_backend.py providing AedocwBackend and concrete subclasses that
  dispatch to the vendored adapters when selected via the CLI.
- Add tts_edge shim for backward compatibility.
- Register new backends in src/epub_tts/__main__.py and invoke the selected
  backend at runtime.
- Update pyproject.toml to exclude vendored packages from setuptools package
  discovery.

This change vendors upstream tools while providing thin adapters that invoke
their CLIs inside isolated virtualenvs, and integrates them into the
epub-tts backend registry.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-06 15:56:27 -03:00
19 changed files with 691 additions and 6 deletions
+1
View File
@@ -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
View File
@@ -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
+12
View File
@@ -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
+4
View File
@@ -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__"}
+23 -6
View File
@@ -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())
+149
View File
@@ -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",
]
+74
View File
@@ -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
+61
View File
@@ -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
+52
View File
@@ -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
+52
View File
@@ -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
+160
View File
@@ -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())
+13
View File
@@ -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
+66
View File
@@ -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]
Vendored Submodule
+1
Submodule src/vendor/epub2tts added at 286d12975f
Vendored Submodule
+1
Vendored Submodule
+1
Vendored Submodule
+1