diff options
| author | historia <historiavg@proton.me> | 2026-08-24 02:59:26 -0400 |
|---|---|---|
| committer | historia <historiavg@proton.me> | 2026-08-24 02:59:26 -0400 |
| commit | f00249db9d1ea051d29aa1bcca869fc4b88e83eb (patch) | |
| tree | a75f076fac1b63e0b4bf2eb8f54affbcc681a891 | |
| parent | 9dd4f9595be3b1d76a3a07dc3eca90cfaf8a3f97 (diff) | |
| download | tts-audiobook-generator-f00249db9d1ea051d29aa1bcca869fc4b88e83eb.tar.gz | |
refactor: add app directory, dir structure change
| -rw-r--r-- | .gitignore | 6 | ||||
| -rw-r--r-- | README.md | 18 | ||||
| -rw-r--r-- | app/backends/__init__.py (renamed from backends/__init__.py) | 2 | ||||
| -rwxr-xr-x | app/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-x | app/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) | bin | 6801 -> 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-x | audiobook.py | 23 |
41 files changed, 123 insertions, 106 deletions
@@ -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 @@ -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 Binary files differindex 0c252db..0c252db 100644 --- a/tests/cover_test.png +++ b/app/tests/cover_test.png 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) |
