aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-24 02:59:26 -0400
committerhistoria <historiavg@proton.me>2026-08-24 02:59:26 -0400
commitf00249db9d1ea051d29aa1bcca869fc4b88e83eb (patch)
treea75f076fac1b63e0b4bf2eb8f54affbcc681a891
parent9dd4f9595be3b1d76a3a07dc3eca90cfaf8a3f97 (diff)
downloadtts-audiobook-generator-f00249db9d1ea051d29aa1bcca869fc4b88e83eb.tar.gz
refactor: add app directory, dir structure change
-rw-r--r--.gitignore6
-rw-r--r--README.md18
-rw-r--r--app/backends/__init__.py (renamed from backends/__init__.py)2
-rwxr-xr-xapp/backends/audiocpp.py (renamed from backends/audiocpp.py)66
-rw-r--r--app/backends/common.py (renamed from backends/common.py)18
-rw-r--r--app/backends/envs.py (renamed from backends/envs.py)8
-rwxr-xr-xapp/backends/faster.py (renamed from backends/faster.py)24
-rw-r--r--app/backends/qwen.py (renamed from backends/qwen.py)12
-rw-r--r--app/backends/servers.py (renamed from backends/servers.py)8
-rw-r--r--app/converter/__init__.py (renamed from converter/__init__.py)0
-rw-r--r--app/converter/audio.py (renamed from converter/audio.py)2
-rw-r--r--app/converter/chunking.py (renamed from converter/chunking.py)2
-rw-r--r--app/converter/config.py (renamed from converter/config.py)0
-rw-r--r--app/converter/converter.py (renamed from converter/converter.py)12
-rw-r--r--app/converter/cover.py (renamed from converter/cover.py)0
-rw-r--r--app/converter/extractors.py (renamed from converter/extractors.py)0
-rw-r--r--app/converter/tts.py (renamed from converter/tts.py)12
-rw-r--r--app/docs/backend-audiocpp.md (renamed from docs/backend-audiocpp.md)2
-rw-r--r--app/docs/backend-faster.md (renamed from docs/backend-faster.md)2
-rw-r--r--app/docs/backend-qwen.md (renamed from docs/backend-qwen.md)4
-rw-r--r--app/tests/__init__.py (renamed from tests/__init__.py)0
-rw-r--r--app/tests/cover_test.png (renamed from tests/cover_test.png)bin6801 -> 6801 bytes
-rw-r--r--app/tests/gen_test_cover.py (renamed from tests/gen_test_cover.py)0
-rw-r--r--app/tests/test_audio.py (renamed from tests/test_audio.py)0
-rw-r--r--app/tests/test_backends.py (renamed from tests/test_backends.py)0
-rw-r--r--app/tests/test_backends_audiocpp.py (renamed from tests/test_backends_audiocpp.py)0
-rw-r--r--app/tests/test_backends_envs.py (renamed from tests/test_backends_envs.py)0
-rw-r--r--app/tests/test_backends_faster.py (renamed from tests/test_backends_faster.py)0
-rw-r--r--app/tests/test_backends_servers.py (renamed from tests/test_backends_servers.py)0
-rw-r--r--app/tests/test_chunking.py (renamed from tests/test_chunking.py)0
-rw-r--r--app/tests/test_cleaning.py (renamed from tests/test_cleaning.py)0
-rw-r--r--app/tests/test_converter.py (renamed from tests/test_converter.py)0
-rw-r--r--app/tests/test_cover.py (renamed from tests/test_cover.py)0
-rw-r--r--app/tests/test_extractors.py (renamed from tests/test_extractors.py)0
-rw-r--r--app/tests/test_hub.py (renamed from tests/test_hub.py)0
-rw-r--r--app/tests/test_tts.py (renamed from tests/test_tts.py)0
-rw-r--r--app/tests/test_tui.py (renamed from tests/test_tui.py)0
-rw-r--r--app/ui/__init__.py (renamed from ui/__init__.py)0
-rw-r--r--app/ui/hub.py (renamed from ui/hub.py)8
-rw-r--r--app/ui/tui.py (renamed from ui/tui.py)0
-rwxr-xr-xaudiobook.py23
41 files changed, 123 insertions, 106 deletions
diff --git a/.gitignore b/.gitignore
index ca8252f..313ac60 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,12 +10,12 @@ voices/*
!voices/.gitkeep
# Backend checkouts cloned by the setup wizards (backends.audiocpp / .faster)
-/audio.cpp/
-/faster-qwen3-tts/
+/app/audio.cpp/
+/app/faster-qwen3-tts/
# Managed Python environment created by audiobook.py (envs.py) for the app
# requirements and the backend TTS packages (qwen-tts, faster-qwen3-tts).
-/envs/
+/app/envs/
*.epub
input/*.txt
diff --git a/README.md b/README.md
index 3f61655..5d96ff8 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@ The converter sends text extracted from your books to a locally running TTS serv
## Installation
Clone the repo. No manual environment setup is needed — `audiobook.py`
-creates and manages its own virtual environment (`envs/tts`) the first time
+creates and manages its own virtual environment (`app/envs/tts`) the first time
it runs, installing its requirements and any backend TTS packages into it.
```bash
@@ -30,7 +30,7 @@ cd tts-audiobook-generator
python audiobook.py
```
-On first launch `audiobook.py` creates `envs/tts` (via `python -m venv`),
+On first launch `audiobook.py` creates `app/envs/tts` (via `python -m venv`),
installs `requirements.txt` into it, and re-launches itself inside that
environment. Backend packages (`qwen-tts`, `faster-qwen3-tts[demo]`) are
pip-installed into the same venv by their setup wizards.
@@ -39,7 +39,7 @@ To add extras manually (e.g. FlashAttention), pip-install into the managed
venv directly:
```bash
-envs/tts/bin/python -m pip install flash-attn
+app/envs/tts/bin/python -m pip install flash-attn
```
Put your book files (epub, etc.) in the `input/` directory. The output goes to `output/`.
@@ -61,7 +61,7 @@ A full-screen TUI opens and shows each backend's status in a table — **unavail
**Configure a backend…** and **Server…** only appear once at least one backend is installed or running.
-Everything the TUI does can also be scripted with flags: `python audiobook.py --backend audiocpp --model higgs --voice narrator`, or `python -m backends.audiocpp --families higgs_audio_tts --clone --build-backend cuda`.
+Everything the TUI does can also be scripted with flags: `python audiobook.py --backend audiocpp --model higgs --voice narrator`, or `python app/backends/audiocpp.py --families higgs_audio_tts --clone --build-backend cuda`.
You need one of the following backends (the TUI sets them up for you; manual steps below):
@@ -90,15 +90,15 @@ You need one of the following backends (the TUI sets them up for you; manual ste
| `--transcription "..."` | `qwen`: Override whisper auto-transcription with manual audio transcript. |
| `--no-transcription` | `qwen`: Skip auto-transcription of the reference audio. |
-Other options including backend server URLs/ports are configured in `converter/config.py`
+Other options including backend server URLs/ports are configured in `app/converter/config.py`
## TTS Backend Setup
-Installation and usage documentation for each supported TTS backend is in the `docs/` directory:
+Installation and usage documentation for each supported TTS backend is in the `app/docs/` directory:
-- [audio.cpp instructions](docs/backend-audiocpp.md)
-- [qwen-tts instructions](docs/backend-qwen.md)
-- [faster-qwen-tts instructions](docs/backend-faster.md)
+- [audio.cpp instructions](app/docs/backend-audiocpp.md)
+- [qwen-tts instructions](app/docs/backend-qwen.md)
+- [faster-qwen-tts instructions](app/docs/backend-faster.md)
## Tips
diff --git a/backends/__init__.py b/app/backends/__init__.py
index 6a23ab7..ed772d4 100644
--- a/backends/__init__.py
+++ b/app/backends/__init__.py
@@ -47,7 +47,7 @@ class BackendStatus:
INSTALLED means the backend itself is present (a cloned + built
checkout, or a pip package). CONFIGURED means the supporting files are
- in place (a server.json / voices.json and a converter/config.py that
+ in place (a server.json / voices.json and an app/converter/config.py that
points at the right port). RUNNING means an external server is
currently accepting connections on the configured port (probed by
``backends.common.server_running``). DETAILS are short status lines for
diff --git a/backends/audiocpp.py b/app/backends/audiocpp.py
index 486017f..cc67efc 100755
--- a/backends/audiocpp.py
+++ b/app/backends/audiocpp.py
@@ -2,10 +2,10 @@
"""Set up the audio.cpp TTS backend for the audiobook generator.
This does the whole audio.cpp setup end-to-end as a full-screen DOS-style
-TUI: locate or clone an audio.cpp checkout into ``./audio.cpp``, optionally
+TUI: locate or clone an audio.cpp checkout into ``app/audio.cpp``, optionally
build ``audiocpp_server``, pick model families/packages from the checkout's
``model_specs`` catalog, transcribe reference .wav voices, write
-``server.json``, sync ``converter/config.py``, download the models, and
+``server.json``, sync ``app/converter/config.py``, download the models, and
print the exact command to start the server. It is driven by
``audiobook.py``'s TUI hub (``backends.REGISTRY``) but can also be run
directly for scripting — every value has a flag, and a non-interactive run
@@ -16,7 +16,7 @@ entry from ``GET /v1/models`` at startup), so any TTS family listed in the
catalog works without further changes.
Usage:
- python -m backends.audiocpp [--wavs WAV_DIR] [--output PATH]
+ python app/backends/audiocpp.py [--wavs WAV_DIR] [--output PATH]
[--audiocpp-dir PATH] [--clone] [--families FAM1,FAM2]
[--all-packages] [--host HOST] [--port PORT]
[--build-backend {cuda,vulkan,hip,cpu}] [--backend {cuda,vulkan,hip,cpu}]
@@ -38,7 +38,7 @@ import urllib.parse
from pathlib import Path
from typing import Callable, Dict, List, Optional, Set, Tuple
-# Allow running directly (python backends/audiocpp.py) from any cwd.
+# Allow running directly (python app/backends/audiocpp.py) from any cwd.
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from backends import (
@@ -49,6 +49,7 @@ from backends import (
format_launch_hint,
)
from backends.common import (
+ APP_DIR,
CONFIG_PATH,
PROMPT_TEXT_FILENAME,
TTS_ROOT,
@@ -213,7 +214,7 @@ def _backend_options(detected: Optional[str] = None
def config_port() -> int:
- """Return the port of AUDIOCPP_API_URL in converter/config.py."""
+ """Return the port of AUDIOCPP_API_URL in app/converter/config.py."""
try:
return urllib.parse.urlsplit(config.AUDIOCPP_API_URL).port or FALLBACK_PORT
except ValueError:
@@ -228,7 +229,7 @@ def _url_with_port(url: str, port: int) -> str:
def update_config_api_url_port(port: int, config_path: Optional[Path] = None) -> bool:
- """Rewrite the port inside AUDIOCPP_API_URL in converter/config.py.
+ """Rewrite the port inside AUDIOCPP_API_URL in app/converter/config.py.
Only the quoted URL literal is replaced; surrounding lines and the
trailing comment are preserved. Returns True when the file was changed.
@@ -296,14 +297,16 @@ def default_model_id(family: str) -> str:
def detect_audiocpp_dir() -> Optional[Path]:
"""Best-effort location of a local audio.cpp checkout with model_specs.
- Checks the AUDIOCPP_DIR environment variable, then an ``audio.cpp``
- directory in or above the current working directory. Returns the path
- only when it contains a ``model_specs`` directory.
+ Checks the AUDIOCPP_DIR environment variable, then ``app/audio.cpp`` in
+ the tts-audiobook-generator root, then an ``audio.cpp`` directory in or
+ above the current working directory. Returns the path only when it
+ contains a ``model_specs`` directory.
"""
candidates: List[Path] = []
env_dir = os.environ.get("AUDIOCPP_DIR")
if env_dir:
candidates.append(Path(os.path.expanduser(env_dir)))
+ candidates.append(APP_DIR / AUDIOCPP_DIR_NAME)
cwd = Path.cwd()
candidates.append(cwd / "audio.cpp")
candidates.append(cwd.parent / "audio.cpp")
@@ -553,7 +556,7 @@ def print_empty_transcript_warning(transcripts: Dict[str, str]) -> None:
def _apply_port_sync(port: int, accepted: bool) -> None:
- """Write the port into converter/config.py, or report when declined."""
+ """Write the port into app/converter/config.py, or report when declined."""
if accepted:
if not update_config_api_url_port(port):
print(f"[WARNING] Could not update {CONFIG_PATH}; edit "
@@ -657,7 +660,7 @@ def _flag_plan(wav_files: list, prompt_path: Path, force: bool) -> dict:
def _offer_config_model_id_sync(model_id: str, accepted: Optional[bool]) -> None:
- """Point converter/config.py at a single hosted model entry.
+ """Point app/converter/config.py at a single hosted model entry.
The converter requests the model id configured in AUDIOCPP_MODEL_ID,
and single-model servers use the same id for the clone entry, so both
@@ -905,20 +908,21 @@ def _wizard(stdscr, args: argparse.Namespace, parser: argparse.ArgumentParser
audiocpp_dir = do_browse()
else:
# No checkout found anywhere: offer to clone one into
- # ./audio.cpp or browse for an existing checkout. Esc
- # on this first menu aborts the wizard.
+ # ./app/audio.cpp or browse for an existing checkout.
+ # Esc on this first menu aborts the wizard.
choice = tui.menu(
stdscr, "No audio.cpp checkout found",
- [(f"Clone into ./{AUDIOCPP_DIR_NAME} "
+ [(f"Clone into ./app/{AUDIOCPP_DIR_NAME} "
f"(from {AUDIOCPP_GIT_URL})", "clone"),
("Browse for an existing checkout", "browse")],
help_lines=[
"audio.cpp hosts the TTS model families "
"this generator uses.",
- "Clone it into the project directory, or "
- "point at an existing checkout."])
+ "Clone it into the project's app "
+ "directory, or point at an existing "
+ "checkout."])
if choice == "clone":
- target = TTS_ROOT / AUDIOCPP_DIR_NAME
+ target = APP_DIR / AUDIOCPP_DIR_NAME
with tui.suspend(stdscr):
rc = common.git_clone(AUDIOCPP_GIT_URL,
target)
@@ -1098,7 +1102,7 @@ def _wizard(stdscr, args: argparse.Namespace, parser: argparse.ArgumentParser
sync_port: Optional[bool] = None
if port != config_port():
sync_port = tui.confirm(
- stdscr, f"Update AUDIOCPP_API_URL in converter/config.py "
+ stdscr, f"Update AUDIOCPP_API_URL in app/converter/config.py "
f"to port {port} so audiobook.py talks to this server",
default=True, cancel_value=_GO_BACK)
if sync_port is _GO_BACK:
@@ -1194,7 +1198,7 @@ def _wizard(stdscr, args: argparse.Namespace, parser: argparse.ArgumentParser
and config.AUDIOCPP_CLONE_MODEL_ID == entry_ids[0]):
sync_model_ids = tui.confirm(
stdscr, "Update AUDIOCPP_MODEL_ID and "
- "AUDIOCPP_CLONE_MODEL_ID in converter/config.py to "
+ "AUDIOCPP_CLONE_MODEL_ID in app/converter/config.py to "
f"'{entry_ids[0]}' so audiobook.py uses this model",
default=True, cancel_value=_GO_BACK)
if sync_model_ids is _GO_BACK:
@@ -1239,16 +1243,16 @@ def _wizard(stdscr, args: argparse.Namespace, parser: argparse.ArgumentParser
def find_local_checkout() -> Optional[Path]:
"""Best-effort location of an audio.cpp checkout with model_specs.
- Checks the AUDIOCPP_DIR environment variable, then ``./audio.cpp`` inside
- the tts-audiobook-generator root, then an ``audio.cpp`` directory in or
- above the current working directory. Returns the path only when it
+ Checks the AUDIOCPP_DIR environment variable, then ``app/audio.cpp``
+ inside the tts-audiobook-generator root, then an ``audio.cpp`` directory
+ in or above the current working directory. Returns the path only when it
contains a ``model_specs`` directory.
"""
candidates: List[Path] = []
env_dir = os.environ.get("AUDIOCPP_DIR")
if env_dir:
candidates.append(Path(os.path.expanduser(env_dir)))
- candidates.append(TTS_ROOT / AUDIOCPP_DIR_NAME)
+ candidates.append(APP_DIR / AUDIOCPP_DIR_NAME)
cwd = Path.cwd()
candidates.append(cwd / AUDIOCPP_DIR_NAME)
candidates.append(cwd.parent / AUDIOCPP_DIR_NAME)
@@ -1446,7 +1450,7 @@ def _collect_from_flags(args: argparse.Namespace,
if audiocpp_dir is None:
audiocpp_dir = find_local_checkout()
if audiocpp_dir is None and args.clone:
- target = TTS_ROOT / AUDIOCPP_DIR_NAME
+ target = APP_DIR / AUDIOCPP_DIR_NAME
rc = common.git_clone(AUDIOCPP_GIT_URL, target)
if rc != 0:
parser.error(f"git clone failed (exit {rc}); clone audio.cpp "
@@ -1455,7 +1459,7 @@ def _collect_from_flags(args: argparse.Namespace,
if audiocpp_dir is None:
parser.error(
"An audio.cpp checkout is required. Pass --audiocpp-dir PATH, "
- "or --clone to clone ./audio.cpp, or run without flags for the "
+ "or --clone to clone app/audio.cpp, or run without flags for the "
"TUI wizard.")
audiocpp_dir = Path(audiocpp_dir).resolve()
if not audiocpp_dir.is_dir():
@@ -1588,7 +1592,7 @@ def build_parser() -> argparse.ArgumentParser:
"""The audio.cpp setup CLI (also used to build a default namespace)."""
parser = argparse.ArgumentParser(
description="Set up the audio.cpp TTS backend: clone/build, pick "
- "models, write server.json, and sync converter/config.py.")
+ "models, write server.json, and sync app/converter/config.py.")
parser.add_argument("--wavs", type=resolve_wav_dir_arg, default=None,
dest="input_dir", metavar="WAV_DIR",
help="Directory with .wav reference files to publish as "
@@ -1603,11 +1607,11 @@ def build_parser() -> argparse.ArgumentParser:
parser.add_argument("--audiocpp-dir", type=normalize_dir_arg, default=None,
help="Path to a local audio.cpp checkout containing a "
"model_specs/ directory (default: detected from "
- "AUDIOCPP_DIR or ./audio.cpp; in the TUI you can "
+ "AUDIOCPP_DIR or ./app/audio.cpp; in the TUI you can "
"clone one instead)")
parser.add_argument("--clone", action="store_true",
help="Non-interactive: clone audio.cpp into "
- "./audio.cpp when no checkout is found")
+ "./app/audio.cpp when no checkout is found")
parser.add_argument("--families", type=str, default=None,
help="Comma-separated model families to host, as named "
"in the audio.cpp catalog (e.g. "
@@ -1623,7 +1627,7 @@ def build_parser() -> argparse.ArgumentParser:
help="Bind host for the server (default: 127.0.0.1)")
parser.add_argument("--port", type=int, default=None,
help="Port for the server (default: the port in "
- "AUDIOCPP_API_URL from converter/config.py)")
+ "AUDIOCPP_API_URL from app/converter/config.py)")
parser.add_argument("--backend", choices=BACKENDS, default=None,
help="Inference backend recorded in server.json "
"(default: auto-detected from the checkout's "
@@ -1646,7 +1650,7 @@ def build_parser() -> argparse.ArgumentParser:
"only)")
parser.add_argument("--no-sync-port", action="store_true",
help="Do not rewrite AUDIOCPP_API_URL in "
- "converter/config.py when --port differs")
+ "app/converter/config.py when --port differs")
parser.add_argument("--no-sync-model-ids", action="store_true",
help="Do not rewrite AUDIOCPP_MODEL_ID/"
"AUDIOCPP_CLONE_MODEL_ID for a single-entry server")
@@ -1665,7 +1669,7 @@ def detect() -> BackendStatus:
return BackendStatus("audiocpp", "audio.cpp", installed=False,
configured=False, running=running,
details=["not cloned — run setup to clone "
- "./audio.cpp"])
+ "./app/audio.cpp"])
details.append(f"checkout: {checkout}")
binary = find_audiocpp_server_bin(checkout)
built = binary is not None
diff --git a/backends/common.py b/app/backends/common.py
index 2529a8f..42faa7e 100644
--- a/backends/common.py
+++ b/app/backends/common.py
@@ -3,7 +3,7 @@
Every TTS backend setup wizard (audio.cpp, qwen, faster) lives in its own
module under ``backends``; this module holds the pieces more than one of
them needs: .wav discovery, path normalization, and the regex edit that
-keeps ``converter/config.py`` in sync with the choices made in a wizard.
+keeps ``app/converter/config.py`` in sync with the choices made in a wizard.
It deliberately imports nothing from the other backend modules (or the
TUI) so it can be reused without pulling curses into a non-interactive
run.
@@ -16,18 +16,22 @@ from pathlib import Path
from typing import Dict, List, Optional, Set, Tuple
# The tts-audiobook-generator checkout root (where audiobook.py lives).
-# Backend checkouts are cloned into subdirectories of this root
-# (./audio.cpp, ./faster-qwen3-tts) so a single tree holds everything.
-TTS_ROOT = Path(__file__).resolve().parent.parent
+# Everything non-user-facing lives under ./app: the source packages
+# (backends, converter, ui), the generated dirs (envs, chunks, logs, debug),
+# and the backend checkouts (app/audio.cpp, app/faster-qwen3-tts).
+TTS_ROOT = Path(__file__).resolve().parent.parent.parent
+
+# The single "everything else" directory under TTS_ROOT.
+APP_DIR = TTS_ROOT / "app"
# The project's sample-voice directory: .wav files dropped here are offered
# as the default source when a setup/configure wizard asks for a wav
# directory (both the TUI browser start and the --wavs flag default).
VOICES_DIR = TTS_ROOT / "voices"
-# converter/config.py — rewritten in place by update_config_value so the
+# app/converter/config.py — rewritten in place by update_config_value so the
# converter picks up the host/port/voice a wizard configured.
-CONFIG_PATH = TTS_ROOT / "converter" / "config.py"
+CONFIG_PATH = APP_DIR / "converter" / "config.py"
# Output directory of tts-audiobook-generator; never offered as a .wav
# source by detect_wav_dir.
@@ -173,7 +177,7 @@ def server_running(url: str, timeout: float = 0.3) -> bool:
def update_config_value(key: str, value: str,
config_path: Optional[Path] = None) -> bool:
- """Rewrite a ``KEY = "value"`` line in converter/config.py.
+ """Rewrite a ``KEY = "value"`` line in app/converter/config.py.
Only the quoted literal is replaced; surrounding lines and the trailing
comment are preserved. Returns True when the file was changed. Used by
diff --git a/backends/envs.py b/app/backends/envs.py
index 7596db6..6e5b6cc 100644
--- a/backends/envs.py
+++ b/app/backends/envs.py
@@ -2,7 +2,7 @@
audiobook.py is meant to be launched from any Python (a bare system interpreter
is fine): on startup it bootstraps a single tool-managed venv at
-``envs/tts`` and re-execs itself inside it. That venv holds both the
+``app/envs/tts`` and re-execs itself inside it. That venv holds both the
audiobook app's own ``requirements.txt`` dependencies and the backend TTS
packages (``qwen-tts``, ``faster-qwen3-tts[demo]``) the setup wizards pip
install, so nothing is ever installed into the launching interpreter's
@@ -11,7 +11,7 @@ environment.
A parent process never needs to "activate" an environment — activation is
just a shell convenience that puts an env's ``bin`` on PATH. Instead every
helper here resolves the env's binaries by absolute path
-(``envs/tts/bin/python``, ``envs/tts/bin/qwen-tts-demo``), so the hub can
+(``app/envs/tts/bin/python``, ``app/envs/tts/bin/qwen-tts-demo``), so the hub can
spawn servers in this env from any parent environment.
This module is imported before audiobook.py's third-party dependencies, so
@@ -28,10 +28,10 @@ from typing import List
from backends import common
# The tts-audiobook-generator checkout root (where audiobook.py lives).
-TTS_ROOT = Path(__file__).resolve().parent.parent
+TTS_ROOT = Path(__file__).resolve().parent.parent.parent
# One shared venv for the app requirements and every pip-installed backend.
-ENV_DIR = TTS_ROOT / "envs" / "tts"
+ENV_DIR = TTS_ROOT / "app" / "envs" / "tts"
REQUIREMENTS_PATH = TTS_ROOT / "requirements.txt"
# Marker file recording the requirements.txt hash last installed into the env,
diff --git a/backends/faster.py b/app/backends/faster.py
index 50c6102..0d34a0f 100755
--- a/backends/faster.py
+++ b/app/backends/faster.py
@@ -7,11 +7,11 @@ reference voice configured on the server through a ``voices.json``. This
module sets the whole backend up end-to-end as a TUI: pip-install the
package, clone the repo (for ``examples/openai_server.py``), build a
``voices.json`` from a directory of .wav references (transcribed with
-Whisper), sync ``converter/config.py``, and print the launch command. It is
+Whisper), sync ``app/converter/config.py``, and print the launch command. It is
driven by ``audiobook.py``'s hub but can also be run directly with flags.
Usage:
- python -m backends.faster [--wavs WAV_DIR] [--output PATH]
+ python app/backends/faster.py [--wavs WAV_DIR] [--output PATH]
[--language LANG] [--whisper-model NAME] [--force]
[--port PORT] [--voice NAME] [--skip-install] [--skip-clone]
"""
@@ -33,7 +33,7 @@ from backends import (
format_launch_hint,
)
from backends.common import (
- TTS_ROOT,
+ APP_DIR,
VOICES_DIR,
find_wav_files,
normalize_dir_arg,
@@ -53,7 +53,7 @@ WHISPER_MODELS = ("tiny", "base", "small", "medium", "large-v3")
def _checkout() -> Path:
- return TTS_ROOT / FASTER_DIR_NAME
+ return APP_DIR / FASTER_DIR_NAME
def _is_installed() -> bool:
@@ -135,7 +135,7 @@ def _wizard(stdscr, args: argparse.Namespace) -> Optional[dict]:
do_clone = False
if not _is_cloned() and not args.skip_clone:
choice = confirm(f"faster-qwen3-tts repo not cloned. Clone it into "
- f"./{FASTER_DIR_NAME}?", default=True)
+ f"./app/{FASTER_DIR_NAME}?", default=True)
if choice is None:
return None
do_clone = choice
@@ -228,7 +228,7 @@ def _execute(settings: dict) -> int:
if voices is None:
return 1
- # Sync converter/config.py port + default voice.
+ # Sync app/converter/config.py port + default voice.
port = settings["port"]
new_url = common.url_with_port(config.FASTER_API_URL, port)
if new_url != config.FASTER_API_URL:
@@ -236,14 +236,14 @@ def _execute(settings: dict) -> int:
print(f"[OK] Updated FASTER_API_URL to {new_url}")
else:
print("[WARNING] Could not update FASTER_API_URL; edit "
- "converter/config.py by hand")
+ "app/converter/config.py by hand")
default_voice = next(iter(voices))
if default_voice != config.FASTER_VOICE:
if common.update_config_value("FASTER_VOICE", default_voice):
print(f"[OK] Updated FASTER_VOICE to {default_voice}")
else:
print("[WARNING] Could not update FASTER_VOICE; edit "
- "converter/config.py by hand")
+ "app/converter/config.py by hand")
_print_launch_hint(settings["output_path"], port)
return 0
@@ -312,14 +312,14 @@ def _collect_from_flags(args: argparse.Namespace,
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Set up the faster-qwen3-tts backend: pip install, clone, "
- "build voices.json, and sync converter/config.py.")
+ "build voices.json, and sync app/converter/config.py.")
parser.add_argument("input_dir", type=normalize_dir_arg, nargs="?",
default=None, metavar="WAV_DIR",
help="Directory with .wav reference files "
f"(default: {VOICES_DIR}; browsed for in the TUI)")
parser.add_argument("--output", type=Path, default=None,
help="Output path for voices.json (default: "
- "./faster-qwen3-tts/voices.json, or "
+ "./app/faster-qwen3-tts/voices.json, or "
"WAV_DIR/voices.json when not cloned)")
parser.add_argument("--language", type=str, default=None,
help="Language for all voices (default: English; "
@@ -332,7 +332,7 @@ def build_parser() -> argparse.ArgumentParser:
help="Overwrite an existing voices.json without "
"prompting")
parser.add_argument("--port", type=int, default=None,
- help="Server port to record in converter/config.py "
+ help="Server port to record in app/converter/config.py "
"(default: the port in FASTER_API_URL)")
parser.add_argument("--skip-install", action="store_true",
help="Do not pip install faster-qwen3-tts[demo]")
@@ -352,7 +352,7 @@ def detect() -> BackendStatus:
details.append("pip: installed" if installed else
"not installed — run setup to pip install")
details.append(f"checkout: {_checkout()}" if cloned else
- f"not cloned — run setup to clone ./{FASTER_DIR_NAME}")
+ f"not cloned — run setup to clone ./app/{FASTER_DIR_NAME}")
details.append(f"voices: {voices_json}" if voices_json.exists() else
"no voices.json — run setup to create one")
launch = ""
diff --git a/backends/qwen.py b/app/backends/qwen.py
index 52f7a3f..21280a0 100644
--- a/backends/qwen.py
+++ b/app/backends/qwen.py
@@ -5,11 +5,11 @@ qwen-tts is a pip package providing the ``qwen-tts-demo`` server, which
hosts the Qwen3-TTS CustomVoice (built-in speakers) and Base (voice
cloning) models on separate ports. This module sets it up end-to-end as a
TUI: pip-install the package, configure the two ports and the built-in
-speaker in ``converter/config.py``, and print the launch commands. It is
+speaker in ``app/converter/config.py``, and print the launch commands. It is
driven by ``audiobook.py``'s hub but can also be run directly with flags.
Usage:
- python -m backends.qwen [--port-custom PORT] [--port-clone PORT]
+ python app/backends/qwen.py [--port-custom PORT] [--port-clone PORT]
[--speaker NAME] [--skip-install]
"""
@@ -37,7 +37,7 @@ QWEN_BASE_MODEL = "Qwen/Qwen3-TTS-12Hz-1.7B-Base"
DEFAULT_CUSTOM_PORT = 7860
DEFAULT_CLONE_PORT = 7861
-# Built-in CustomVoice speakers (see converter/config.py SPEAKER).
+# Built-in CustomVoice speakers (see app/converter/config.py SPEAKER).
QWEN_SPEAKERS = ("Vivian", "Serena", "Uncle_Fu", "Dylan", "Eric", "Ryan",
"Aiden", "Ono_Anna", "Sohee")
@@ -128,20 +128,20 @@ def _execute(settings: dict) -> int:
print(f"[OK] Updated QWEN_API_URL to {custom_url}")
else:
print("[WARNING] Could not update QWEN_API_URL; edit "
- "converter/config.py by hand")
+ "app/converter/config.py by hand")
clone_url = common.url_with_port(config.CLONE_API_URL, settings["clone_port"])
if clone_url != config.CLONE_API_URL:
if common.update_config_value("CLONE_API_URL", clone_url):
print(f"[OK] Updated CLONE_API_URL to {clone_url}")
else:
print("[WARNING] Could not update CLONE_API_URL; edit "
- "converter/config.py by hand")
+ "app/converter/config.py by hand")
if settings["speaker"] != config.SPEAKER:
if common.update_config_value("SPEAKER", settings["speaker"]):
print(f"[OK] Updated SPEAKER to {settings['speaker']}")
else:
print("[WARNING] Could not update SPEAKER; edit "
- "converter/config.py by hand")
+ "app/converter/config.py by hand")
_print_launch_hint(settings["custom_port"], settings["clone_port"])
return 0
diff --git a/backends/servers.py b/app/backends/servers.py
index 12846f0..a5a6829 100644
--- a/backends/servers.py
+++ b/app/backends/servers.py
@@ -4,13 +4,13 @@ Each backend's ``detect()`` returns a list of ``ServerSpec`` — the exact argv
(absolute binaries in the managed venv, no shell activation needed) and the
URL to probe for readiness. This module turns those specs into running
processes: ``start`` spawns the server, streams its output to
-``logs/<name>-server.log``, records its pid, and polls the URL until it
+``app/logs/<name>-server.log``, records its pid, and polls the URL until it
accepts connections (model loads are slow, so the timeout is generous);
``stop`` terminates the process group the hub started.
Everything here runs in the plain console tail after the curses TUI returns
(matching the wizards' build/pip streaming), so progress and log tails appear
-normally. Pid/log files live under ``logs/`` which is already gitignored.
+normally. Pid/log files live under ``app/logs/`` which is already gitignored.
"""
import os
@@ -22,9 +22,9 @@ from pathlib import Path
from typing import List
from backends import common
-from backends.common import TTS_ROOT
+from backends.common import APP_DIR
-LOG_DIR = TTS_ROOT / "logs"
+LOG_DIR = APP_DIR / "logs"
# How long to wait for a server to accept connections on its URL. First-time
# model loads (especially qwen-tts / faster-qwen3-tts pulling weights into
diff --git a/converter/__init__.py b/app/converter/__init__.py
index 86a827f..86a827f 100644
--- a/converter/__init__.py
+++ b/app/converter/__init__.py
diff --git a/converter/audio.py b/app/converter/audio.py
index d1dc28b..81431cb 100644
--- a/converter/audio.py
+++ b/app/converter/audio.py
@@ -13,7 +13,7 @@ from . import config
logger = logging.getLogger(__name__)
-CHUNKS_FOLDER = Path(__file__).resolve().parent.parent / "chunks"
+CHUNKS_FOLDER = Path(__file__).resolve().parent.parent.parent / "app" / "chunks"
def atempo_filters(speed: float) -> str:
diff --git a/converter/chunking.py b/app/converter/chunking.py
index 800b76a..9ef6a5d 100644
--- a/converter/chunking.py
+++ b/app/converter/chunking.py
@@ -16,7 +16,7 @@ def split_into_chunks(text: str, max_words: Optional[int] = None) -> List[str]:
There is no ceiling beyond that setting, but note that the TTS
servers silently truncate audio when a single generation runs too
long without reporting an error, so very large values are at your
- own risk (see CHUNK_SIZE in converter/config.py).
+ own risk (see CHUNK_SIZE in app/converter/config.py).
Splits on sentence boundaries. Sentences longer than the limit are
split further at clause punctuation (which is kept attached for TTS
diff --git a/converter/config.py b/app/converter/config.py
index 6a98136..6a98136 100644
--- a/converter/config.py
+++ b/app/converter/config.py
diff --git a/converter/converter.py b/app/converter/converter.py
index b24ac09..cef4808 100644
--- a/converter/converter.py
+++ b/app/converter/converter.py
@@ -33,14 +33,16 @@ from .tts import (
logger = logging.getLogger(__name__)
# Folders, resolved from the project root so the converter runs from any
-# working directory.
-BASE_DIR = Path(__file__).resolve().parent.parent
+# working directory. User-facing dirs (input/, output/) stay at the root;
+# scratch/log dirs live under the app/ container.
+BASE_DIR = Path(__file__).resolve().parent.parent.parent
+APP_DIR = BASE_DIR / "app"
BOOKS_FOLDER = BASE_DIR / "input"
AUDIOBOOKS_FOLDER = BASE_DIR / "output"
-CHUNKS_FOLDER = BASE_DIR / "chunks" # Per-chunk scratch audio, cleaned per book
-LOGS_FOLDER = BASE_DIR / "logs"
-DEBUG_FOLDER = BASE_DIR / "debug" # --debug dumps, kept across runs
+CHUNKS_FOLDER = APP_DIR / "chunks" # Per-chunk scratch audio, cleaned per book
+LOGS_FOLDER = APP_DIR / "logs"
+DEBUG_FOLDER = APP_DIR / "debug" # --debug dumps, kept across runs
# Output containers and supported input formats.
AUDIO_FORMATS = ("mp3", "m4b", "ogg", "flac")
diff --git a/converter/cover.py b/app/converter/cover.py
index b2d3cb5..b2d3cb5 100644
--- a/converter/cover.py
+++ b/app/converter/cover.py
diff --git a/converter/extractors.py b/app/converter/extractors.py
index a564333..a564333 100644
--- a/converter/extractors.py
+++ b/app/converter/extractors.py
diff --git a/converter/tts.py b/app/converter/tts.py
index f5d54e2..6ccef56 100644
--- a/converter/tts.py
+++ b/app/converter/tts.py
@@ -180,7 +180,7 @@ MODEL_SIZE = "1.7B"
CUSTOM_VOICE_MODEL_ID = "Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice"
SAMPLE_RATE = 24000
-CHUNKS_FOLDER = Path(__file__).resolve().parent.parent / "chunks"
+CHUNKS_FOLDER = Path(__file__).resolve().parent.parent.parent / "app" / "chunks"
def _resolve_request_seed() -> int:
@@ -990,7 +990,7 @@ class AudioCppTTSClient(_BaseTTSClient):
"""Resolve an empty model id when the server hosts exactly one entry.
Multi-model servers generated with several lazily-loaded entries can
- be used without editing converter/config.py: leave AUDIOCPP_MODEL_ID
+ be used without editing app/converter/config.py: leave AUDIOCPP_MODEL_ID
(and ``--model``) unset, and the single hosted entry is chosen
automatically. With more than one entry an explicit choice is required
(via ``--model`` or AUDIOCPP_MODEL_ID), since guessing would risk
@@ -1025,7 +1025,7 @@ class AudioCppTTSClient(_BaseTTSClient):
f"The audio.cpp server at {self.api_url} hosts {len(model_ids)} "
f"model entries ({configured}); audiobook.py needs to know which "
"one to use. Pass --model <id> when converting, or set "
- "AUDIOCPP_MODEL_ID in converter/config.py to one of them "
+ "AUDIOCPP_MODEL_ID in app/converter/config.py to one of them "
"(see README)."
)
if self.preset_mode:
@@ -1035,14 +1035,14 @@ class AudioCppTTSClient(_BaseTTSClient):
f"'{config.AUDIOCPP_CLONE_MODEL_ID}' (configured: {configured}). "
"Add a TTS model entry for the family you want to the server "
"config and match AUDIOCPP_MODEL_ID / AUDIOCPP_CLONE_MODEL_ID "
- "in converter/config.py to its id, or select it per run with "
+ "in app/converter/config.py to its id, or select it per run with "
"--model (see README)."
)
raise RuntimeError(
f"The audio.cpp server at {self.api_url} has no model id "
f"'{self.model_id}' (configured: {configured}). Speaker mode needs "
"the Qwen3-TTS CustomVoice model: add a qwen3_tts model entry to "
- "the server config and match AUDIOCPP_MODEL_ID in converter/config.py to its "
+ "the server config and match AUDIOCPP_MODEL_ID in app/converter/config.py to its "
"id (or pass --model), or rerun with --voice to use a voice preset "
"on any TTS model (see README)."
)
@@ -1088,7 +1088,7 @@ class AudioCppTTSClient(_BaseTTSClient):
"AUDIOCPP_CLONE_MODEL_ID %r hosts family %r, but "
"AUDIOCPP_MODEL_ID %r hosts %r; preset requests stay on "
"'%s'. Point both ids at the same model entry in "
- "converter/config.py (single-model servers use the same id "
+ "app/converter/config.py (single-model servers use the same id "
"for both)",
clone_model_id, clone_family, self.model_id, primary_family,
self.model_id)
diff --git a/docs/backend-audiocpp.md b/app/docs/backend-audiocpp.md
index ee511bb..271c9b6 100644
--- a/docs/backend-audiocpp.md
+++ b/app/docs/backend-audiocpp.md
@@ -2,7 +2,7 @@
`--backend audiocpp` talks to `audiocpp_server` from [audio.cpp](https://github.com/0xShug0/audio.cpp), which hosts numerous TTS model families.
-The easiest way is the TUI: run `python audiobook.py`, choose **Set up a backend… → audio.cpp**, and it clones `audio.cpp` into `./audio.cpp` (or reuses an existing checkout), builds `audiocpp_server`, lets you pick model families/packages from an expandable checkbox tree (reading the checkout's `model_specs/`), transcribes `.wav` voices with `whisper`, writes `server.json` into the checkout, syncs `converter/config.py`, and prints the launch command (the hub can also start the server for you via the **Server** menu or automatically when converting). Run it directly with `python -m backends.audiocpp` (flags like `--wavs`, `--families`, `--build-backend`, `--clone` skip the corresponding screens for scripting). The TUI runs in the managed `envs/tts` venv, which includes `whisper` via `requirements.txt`; for a manual setup, make sure `whisper` (or `faster_whisper`) is installed in the environment you run the wizard from. The Qwen3-TTS model tree also offers hosting the VoiceDesign package as a `vdes` entry.
+The easiest way is the TUI: run `python audiobook.py`, choose **Set up a backend… → audio.cpp**, and it clones `audio.cpp` into `app/audio.cpp` (or reuses an existing checkout), builds `audiocpp_server`, lets you pick model families/packages from an expandable checkbox tree (reading the checkout's `model_specs/`), transcribes `.wav` voices with `whisper`, writes `server.json` into the checkout, syncs `app/converter/config.py`, and prints the launch command (the hub can also start the server for you via the **Server** menu or automatically when converting). Run it directly with `python app/backends/audiocpp.py` (flags like `--wavs`, `--families`, `--build-backend`, `--clone` skip the corresponding screens for scripting). The TUI runs in the managed `app/envs/tts` venv, which includes `whisper` via `requirements.txt`; for a manual setup, make sure `whisper` (or `faster_whisper`) is installed in the environment you run the wizard from. The Qwen3-TTS model tree also offers hosting the VoiceDesign package as a `vdes` entry.
If you prefer to install the backend yourself (in your own environment, not the managed venv), the manual steps are below. Either way the hub detects a running server by its port, so a manually-installed backend works once its server is up.
diff --git a/docs/backend-faster.md b/app/docs/backend-faster.md
index c407a70..b08e057 100644
--- a/docs/backend-faster.md
+++ b/app/docs/backend-faster.md
@@ -2,7 +2,7 @@
`--backend faster` talks to the OpenAI-compatible server from [faster-qwen3-tts](https://github.com/andimarafioti/faster-qwen3-tts), which uses CUDA graph capture for roughly 5-10x faster inference with the same models. **It requires an NVIDIA GPU**.
-The easiest way is to run `python audiobook.py` → **Set up a backend… → faster-qwen3-tts** (or `python -m backends.faster path/to/clone/wavs`): the TUI pip-installs `faster-qwen3-tts[demo]` into its managed venv (`envs/tts`), clones the repo, transcribes the `.wav` files with `whisper`, and writes `voices.json` for you. You can also start the server from the hub's **Server** menu, or let a conversion start it automatically.
+The easiest way is to run `python audiobook.py` → **Set up a backend… → faster-qwen3-tts** (or `python app/backends/faster.py path/to/clone/wavs`): the TUI pip-installs `faster-qwen3-tts[demo]` into its managed venv (`app/envs/tts`), clones the repo, transcribes the `.wav` files with `whisper`, and writes `voices.json` for you. You can also start the server from the hub's **Server** menu, or let a conversion start it automatically.
If you prefer to install the backend yourself (in your own environment, not the managed venv), the manual steps are below. Either way the hub detects a running server by its port, so a manually-installed backend works once its server is up.
diff --git a/docs/backend-qwen.md b/app/docs/backend-qwen.md
index 028d6f3..074004c 100644
--- a/docs/backend-qwen.md
+++ b/app/docs/backend-qwen.md
@@ -1,6 +1,6 @@
# Backend Option 2: Qwen3-TTS
-The easiest way is to run `python audiobook.py` → **Set up a backend… → qwen-tts** (or `python -m backends.qwen`): the TUI pip-installs `qwen-tts` into its managed venv (`envs/tts`), configures the two ports and the built-in speaker in `converter/config.py`, and prints the launch commands. You can also start the server from the hub's **Server** menu, or let a conversion start it automatically.
+The easiest way is to run `python audiobook.py` → **Set up a backend… → qwen-tts** (or `python app/backends/qwen.py`): the TUI pip-installs `qwen-tts` into its managed venv (`app/envs/tts`), configures the two ports and the built-in speaker in `app/converter/config.py`, and prints the launch commands. You can also start the server from the hub's **Server** menu, or let a conversion start it automatically.
If you prefer to install the backend yourself (in your own environment, not the managed venv), the manual steps are below. Either way the hub detects a running server by its port, so a manually-installed backend works once its server is up.
@@ -40,7 +40,7 @@ qwen-tts-demo Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice --ip 127.0.0.1 --port 7860 [-
python audiobook.py --backend qwen
```
-Change the voice settings in `converter/config.py`.
+Change the voice settings in `app/converter/config.py`.
## Optional: FlashAttention for qwen-tts-demo server
diff --git a/tests/__init__.py b/app/tests/__init__.py
index e69de29..e69de29 100644
--- a/tests/__init__.py
+++ b/app/tests/__init__.py
diff --git a/tests/cover_test.png b/app/tests/cover_test.png
index 0c252db..0c252db 100644
--- a/tests/cover_test.png
+++ b/app/tests/cover_test.png
Binary files differ
diff --git a/tests/gen_test_cover.py b/app/tests/gen_test_cover.py
index 292469a..292469a 100644
--- a/tests/gen_test_cover.py
+++ b/app/tests/gen_test_cover.py
diff --git a/tests/test_audio.py b/app/tests/test_audio.py
index ef5e92a..ef5e92a 100644
--- a/tests/test_audio.py
+++ b/app/tests/test_audio.py
diff --git a/tests/test_backends.py b/app/tests/test_backends.py
index c0e8d4a..c0e8d4a 100644
--- a/tests/test_backends.py
+++ b/app/tests/test_backends.py
diff --git a/tests/test_backends_audiocpp.py b/app/tests/test_backends_audiocpp.py
index 9882ce1..9882ce1 100644
--- a/tests/test_backends_audiocpp.py
+++ b/app/tests/test_backends_audiocpp.py
diff --git a/tests/test_backends_envs.py b/app/tests/test_backends_envs.py
index cf4ecc6..cf4ecc6 100644
--- a/tests/test_backends_envs.py
+++ b/app/tests/test_backends_envs.py
diff --git a/tests/test_backends_faster.py b/app/tests/test_backends_faster.py
index 641f6ee..641f6ee 100644
--- a/tests/test_backends_faster.py
+++ b/app/tests/test_backends_faster.py
diff --git a/tests/test_backends_servers.py b/app/tests/test_backends_servers.py
index 02b65e6..02b65e6 100644
--- a/tests/test_backends_servers.py
+++ b/app/tests/test_backends_servers.py
diff --git a/tests/test_chunking.py b/app/tests/test_chunking.py
index 2904e40..2904e40 100644
--- a/tests/test_chunking.py
+++ b/app/tests/test_chunking.py
diff --git a/tests/test_cleaning.py b/app/tests/test_cleaning.py
index 41f4ed7..41f4ed7 100644
--- a/tests/test_cleaning.py
+++ b/app/tests/test_cleaning.py
diff --git a/tests/test_converter.py b/app/tests/test_converter.py
index 2fe0f5d..2fe0f5d 100644
--- a/tests/test_converter.py
+++ b/app/tests/test_converter.py
diff --git a/tests/test_cover.py b/app/tests/test_cover.py
index f19db5a..f19db5a 100644
--- a/tests/test_cover.py
+++ b/app/tests/test_cover.py
diff --git a/tests/test_extractors.py b/app/tests/test_extractors.py
index ae1794c..ae1794c 100644
--- a/tests/test_extractors.py
+++ b/app/tests/test_extractors.py
diff --git a/tests/test_hub.py b/app/tests/test_hub.py
index b3a17e8..b3a17e8 100644
--- a/tests/test_hub.py
+++ b/app/tests/test_hub.py
diff --git a/tests/test_tts.py b/app/tests/test_tts.py
index a2df07f..a2df07f 100644
--- a/tests/test_tts.py
+++ b/app/tests/test_tts.py
diff --git a/tests/test_tui.py b/app/tests/test_tui.py
index c121d55..c121d55 100644
--- a/tests/test_tui.py
+++ b/app/tests/test_tui.py
diff --git a/ui/__init__.py b/app/ui/__init__.py
index 4986a10..4986a10 100644
--- a/ui/__init__.py
+++ b/app/ui/__init__.py
diff --git a/ui/hub.py b/app/ui/hub.py
index b2a2b08..6a94096 100644
--- a/ui/hub.py
+++ b/app/ui/hub.py
@@ -200,7 +200,7 @@ def _convert_menu(stdscr, statuses) -> Optional[tuple]:
def _convert_audiocpp(stdscr, statuses) -> Optional[tuple]:
- """Collect audio.cpp run settings by reading ./audio.cpp/server.json."""
+ """Collect audio.cpp run settings by reading app/audio.cpp/server.json."""
checkout = audiocpp_backend.find_local_checkout()
server_json = checkout / "server.json" if checkout else None
if not server_json or not server_json.exists():
@@ -379,7 +379,7 @@ def _common_options(stdscr) -> Optional[dict]:
# ---------------------------------------------------------------------------
-# Settings menu (global output options -> converter/config.py)
+# Settings menu (global output options -> app/converter/config.py)
# ---------------------------------------------------------------------------
def _settings_menu(stdscr) -> None:
@@ -434,7 +434,7 @@ def _validate_chunk_size(value: str) -> Optional[str]:
def _apply_settings(values: dict) -> None:
- """Write VALUES to converter/config.py and reload them in-memory."""
+ """Write VALUES to app/converter/config.py and reload them in-memory."""
chunk_size = int(values["chunk_size"].strip())
if chunk_size < 1:
raise ValueError("Chunk size must be at least 1")
@@ -455,7 +455,7 @@ def _apply_settings(values: dict) -> None:
def _write_config(updates: dict) -> None:
- """Rewrite the ``NAME = value`` lines for UPDATES in converter/config.py.
+ """Rewrite the ``NAME = value`` lines for UPDATES in app/converter/config.py.
Only the value of each named assignment changes: the indentation, the
quotes (double, matching the file's style) and any trailing comment on
diff --git a/ui/tui.py b/app/ui/tui.py
index 9047f36..9047f36 100644
--- a/ui/tui.py
+++ b/app/ui/tui.py
diff --git a/audiobook.py b/audiobook.py
index 792b274..6c075ea 100755
--- a/audiobook.py
+++ b/audiobook.py
@@ -5,12 +5,13 @@ Converts TXT, PDF and EPUB files into audiobooks using a local TTS server.
Run with no arguments in a terminal for the full TUI (set up backends,
process the input directory); pass flags to script a conversion directly.
-Edit converter/config.py to change voice and processing settings.
+Edit app/converter/config.py to change voice and processing settings.
"""
import argparse
import sys
import traceback
+from pathlib import Path
# Fix Windows console encoding for unicode output
if sys.platform == "win32":
@@ -20,6 +21,12 @@ if sys.platform == "win32":
except AttributeError:
pass
+# Everything non-user-facing (source packages, generated dirs, venv, backend
+# checkouts) lives under ./app so the checkout root stays clean. Put it on
+# sys.path before importing the packages below.
+APP_DIR = Path(__file__).resolve().parent / "app"
+sys.path.insert(0, str(APP_DIR))
+
# The managed-environment bootstrap (backends.envs) is stdlib-only and is
# imported here so main() can launch it before any third-party dependency is
# touched. It must NOT run at import time (importing this module must stay
@@ -174,7 +181,7 @@ Examples:
"With --backend audiocpp the language is adapted to the model "
"family: sent as a code (e.g. 'en') for families that take one, or "
"omitted when the model detects the language itself. Defaults to "
- "the LANGUAGE setting in converter/config.py (English).")
+ "the LANGUAGE setting in app/converter/config.py (English).")
)
parser.add_argument(
"--speed", type=float, default=1.0,
@@ -198,7 +205,7 @@ Examples:
"audio.cpp audiocpp_server (audiocpp) hosting any of its TTS "
"model families — Qwen3-TTS, Higgs Audio, VoxCPM2, IndexTTS2, "
"and more. Defaults to the BACKEND setting in "
- "converter/config.py (audiocpp).")
+ "app/converter/config.py (audiocpp).")
)
parser.add_argument(
"--voice", type=str, default=None, metavar="NAME",
@@ -207,7 +214,7 @@ Examples:
"with --ref-audio). audiocpp: a voice_preset or voice_dir entry "
"(cloning); required for audio.cpp families without built-in "
"speakers (everything except Qwen3-TTS CustomVoice). Not used by "
- "the qwen backend (use converter/config.py SPEAKER or --clone "
+ "the qwen backend (use app/converter/config.py SPEAKER or --clone "
"there).")
)
parser.add_argument(
@@ -219,7 +226,7 @@ Examples:
parser.add_argument(
"--chunk", action="store_true",
help=("Force client-side chunking into CHUNK_SIZE-word requests (see "
- "converter/config.py). Only matters for --backend audiocpp, which "
+ "app/converter/config.py). Only matters for --backend audiocpp, which "
"otherwise sends each chapter as one request and lets the server "
"chunk long text itself; the qwen and faster backends always "
"chunk.")
@@ -228,7 +235,7 @@ Examples:
"--model", type=str, default=None, metavar="ID",
help=("audio.cpp server model entry id to use for this run "
"(--backend audiocpp only). Overrides AUDIOCPP_MODEL_ID in "
- "converter/config.py, which is useful for a server hosting "
+ "app/converter/config.py, which is useful for a server hosting "
"several lazily-loaded models: generate one server.json with "
"backends.audiocpp, then pick the model per run with --model. "
"Leave unset to use the config id, or to auto-select when the "
@@ -243,7 +250,7 @@ Examples:
"'A warm adult female narrator with a British accent'. On "
"other families it acts as a style/delivery instruction when "
"the model supports one and is ignored otherwise. Defaults to "
- "AUDIOCPP_INSTRUCTIONS in converter/config.py (empty).")
+ "AUDIOCPP_INSTRUCTIONS in app/converter/config.py (empty).")
)
parser.add_argument(
"--option", action="append", type=str, default=None, metavar="KEY=VALUE",
@@ -306,7 +313,7 @@ Examples:
if args.voice is not None:
parser.error("--voice requires --backend faster or audiocpp; the "
"qwen backend uses built-in speakers "
- "(converter/config.py SPEAKER) or --clone")
+ "(app/converter/config.py SPEAKER) or --clone")
if args.language is not None:
try:
args.language = normalize_language(args.language)