kentui 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
kentui/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """kentui — Interactive CLI for kenkui ebook-to-audiobook conversion."""
2
+
3
+ import importlib.metadata
4
+
5
+ try:
6
+ __version__ = importlib.metadata.version("kentui")
7
+ except importlib.metadata.PackageNotFoundError:
8
+ __version__ = "0.1.0"
9
+
10
+ __author__ = "Sumner MacArthur"
11
+ __license__ = "GPL-3.0"
kentui/__main__.py ADDED
@@ -0,0 +1,269 @@
1
+ """
2
+ kentui — Ebook to Audiobook Converter (local, in-process)
3
+ Entry point / sub-command dispatcher.
4
+
5
+ Sub-commands
6
+ ------------
7
+ kentui book.epub Interactive wizard → run_job() in-process
8
+ kentui book.epub -c config.toml Headless: run_job() → Rich progress → exit 0/1
9
+
10
+ kentui add book.epub Interactive wizard → run_job() in-process
11
+ kentui add book.epub -c config.toml Headless: run_job()
12
+
13
+ kentui config [name] Create/edit a named config
14
+ kentui voices Interactive voice TUI
15
+ kentui voices list / fetch / download / exclude / include / audition
16
+ kentui cache Clear cache files
17
+
18
+ kentui parse book.epub Stage 1-2 NLP only (entity scan)
19
+ kentui attribute book.epub Stage 3-4 NLP attribution only
20
+ kentui generate book.epub TTS + stitch only (requires prior NLP cache)
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import logging
26
+ import multiprocessing
27
+ import sys
28
+ from pathlib import Path
29
+
30
+ # MUST set multiprocessing start method BEFORE any multiprocessing usage.
31
+ if __name__ == "__main__":
32
+ try:
33
+ multiprocessing.set_start_method("spawn", force=True)
34
+ except RuntimeError:
35
+ pass
36
+
37
+ import argparse
38
+
39
+ DEFAULT_HOST = "127.0.0.1"
40
+ DEFAULT_PORT = 45365
41
+
42
+ EBOOK_EXTENSIONS = {".epub", ".mobi", ".fb2", ".azw", ".azw3", ".azw4"}
43
+
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # Argument parser
47
+ # ---------------------------------------------------------------------------
48
+
49
+
50
+ def _build_parser() -> argparse.ArgumentParser:
51
+ from . import __version__
52
+
53
+ parser = argparse.ArgumentParser(
54
+ prog="kentui",
55
+ description=(
56
+ "kentui — Ebook to Audiobook Converter (local, in-process).\n\n"
57
+ "Pass an ebook path directly to run the interactive wizard.\n\n"
58
+ "Sub-commands: add, config, voices, cache, parse, attribute, generate"
59
+ ),
60
+ formatter_class=argparse.RawDescriptionHelpFormatter,
61
+ )
62
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
63
+ parser.add_argument("--verbose", action="store_true", help="Enable DEBUG logging.")
64
+ parser.add_argument("--log-file", default=None, metavar="PATH")
65
+ parser.add_argument(
66
+ "-c",
67
+ "--config",
68
+ default=None,
69
+ metavar="PATH_OR_NAME",
70
+ help="Config file path or bare name. Triggers headless mode when combined with a book path.",
71
+ )
72
+ parser.add_argument("-o", "--output", default=None, metavar="DIR")
73
+
74
+ sub = parser.add_subparsers(dest="command")
75
+
76
+ # ---- kentui add --------------------------------------------------------
77
+ add_p = sub.add_parser("add", help="Convert a book (interactive wizard).")
78
+ add_p.add_argument("book", type=Path, help="Path to the ebook file.")
79
+ add_p.add_argument("-c", "--config", default=None, metavar="PATH_OR_NAME")
80
+ add_p.add_argument("-o", "--output", default=None, metavar="DIR")
81
+ add_p.add_argument(
82
+ "--apostrophe-mode",
83
+ choices=["keep", "always_remove", "remove_contractions", "expand_contractions"],
84
+ default=None,
85
+ dest="apostrophe_mode",
86
+ metavar="MODE",
87
+ )
88
+
89
+ # ---- kentui config -----------------------------------------------------
90
+ cfg_p = sub.add_parser("config", help="Create or edit a config file.")
91
+ cfg_p.add_argument(
92
+ "path",
93
+ nargs="?",
94
+ default=None,
95
+ metavar="PATH_OR_NAME",
96
+ help="Destination name (no .toml) or full path. Defaults to 'default'.",
97
+ )
98
+
99
+ # ---- kentui voices -----------------------------------------------------
100
+ voices_p = sub.add_parser("voices", help="List and manage available voices.")
101
+ voices_sub = voices_p.add_subparsers(dest="voices_command")
102
+
103
+ voices_list_p = voices_sub.add_parser("list", help="List available voices.")
104
+ voices_list_p.add_argument("--gender", default=None)
105
+ voices_list_p.add_argument("--accent", default=None)
106
+ voices_list_p.add_argument("--dataset", default=None)
107
+ voices_list_p.add_argument(
108
+ "--source", default=None, choices=["compiled", "builtin", "uncompiled"]
109
+ )
110
+
111
+ voices_sub.add_parser("fetch", help="Download uncompiled voices from HuggingFace.")
112
+
113
+ voices_dl = voices_sub.add_parser("download", help="Download compiled voices from HuggingFace.")
114
+ voices_dl.add_argument("--force", action="store_true")
115
+
116
+ voices_exc = voices_sub.add_parser("exclude", help="Exclude a voice from auto-assignment.")
117
+ voices_exc.add_argument("voice", help="Voice name to exclude.")
118
+
119
+ voices_inc = voices_sub.add_parser("include", help="Re-include a previously excluded voice.")
120
+ voices_inc.add_argument("voice", help="Voice name to re-include.")
121
+
122
+ voices_cast = voices_sub.add_parser("cast", help="Show character→voice cast for a book.")
123
+ voices_cast.add_argument("title", help="Book title (fuzzy matched).")
124
+
125
+ voices_aud = voices_sub.add_parser("audition", help="Synthesize a voice preview.")
126
+ voices_aud.add_argument("voice", help="Voice name to audition.")
127
+ voices_aud.add_argument("--text", default=None, metavar="TEXT")
128
+
129
+ # ---- kentui cache -------------------------------------------------------
130
+ cache_p = sub.add_parser("cache", help="Clear kenkui cache files.")
131
+ cache_p.add_argument("-y", "--yes", action="store_true", help="Skip confirmation prompt.")
132
+
133
+ # ---- kentui parse / attribute / generate --------------------------------
134
+ for cmd, help_text in [
135
+ ("parse", "Run Stage 1-2 NLP (entity scan) and cache the roster."),
136
+ ("attribute", "Run Stage 3-4 NLP attribution using a cached roster."),
137
+ ("generate", "Run TTS + stitch using a cached NLP result."),
138
+ ]:
139
+ step_p = sub.add_parser(cmd, help=help_text)
140
+ step_p.add_argument("book", type=Path, help="Path to the ebook file.")
141
+ step_p.add_argument("-c", "--config", default=None, metavar="PATH_OR_NAME")
142
+ step_p.add_argument("-o", "--output", default=None, metavar="DIR")
143
+
144
+ return parser
145
+
146
+
147
+ # ---------------------------------------------------------------------------
148
+ # Main
149
+ # ---------------------------------------------------------------------------
150
+
151
+
152
+ def _is_bare_book_invocation() -> bool:
153
+ for arg in sys.argv[1:]:
154
+ if arg.startswith("-"):
155
+ continue
156
+ return Path(arg).suffix.lower() in EBOOK_EXTENSIONS
157
+ return False
158
+
159
+
160
+ def _ensure_voices() -> None:
161
+ """Download compiled voices on first run if not present."""
162
+ from rich.console import Console
163
+ from rich.panel import Panel
164
+ from kenkui.voice_download import voices_are_present, download_voices
165
+
166
+ console = Console()
167
+ try:
168
+ if not voices_are_present():
169
+ console.print(Panel(
170
+ "[bold]Voice files not found.[/bold]\n"
171
+ "Downloading compiled voices from HuggingFace (~440 MB)…\n"
172
+ "[dim]This only happens once. Use 'kentui voices download' to re-download.[/dim]",
173
+ title="First Run Setup",
174
+ border_style="cyan",
175
+ ))
176
+ download_voices()
177
+ console.print("[green]✓ Voices downloaded successfully.[/green]")
178
+ except Exception as exc:
179
+ console.print(f"[yellow]Warning: Could not download voices: {exc}[/yellow]")
180
+ console.print("[dim]8 built-in voices are still available. Run 'kentui voices download' later.[/dim]")
181
+
182
+
183
+ def main() -> None:
184
+ if _is_bare_book_invocation():
185
+ # Treat as implicit `kentui add <book>`
186
+ parser = _build_parser()
187
+ args = parser.parse_args(["add"] + sys.argv[1:])
188
+ args.command = "add"
189
+ else:
190
+ parser = _build_parser()
191
+ args = parser.parse_args()
192
+
193
+ handlers: list[logging.Handler] = [logging.StreamHandler(sys.stderr)]
194
+ if getattr(args, "log_file", None):
195
+ handlers.append(logging.FileHandler(args.log_file))
196
+ logging.basicConfig(
197
+ level=logging.DEBUG if getattr(args, "verbose", False) else logging.WARNING,
198
+ format="%(levelname)s [%(name)s] %(message)s",
199
+ handlers=handlers,
200
+ )
201
+
202
+ command = getattr(args, "command", None)
203
+ voices_cmd = getattr(args, "voices_command", None)
204
+
205
+ if not (command == "voices" and voices_cmd == "download"):
206
+ _ensure_voices()
207
+
208
+ if command == "add" or command is None:
209
+ from .cli.add import cmd_add
210
+
211
+ book = getattr(args, "book", None)
212
+ if book is None:
213
+ parser.print_help()
214
+ sys.exit(0)
215
+ sys.exit(cmd_add(args))
216
+
217
+ elif command == "config":
218
+ from .cli.config import cmd_config
219
+
220
+ sys.exit(cmd_config(args))
221
+
222
+ elif command == "voices":
223
+ from .cli.voices import (
224
+ cmd_voices_list,
225
+ cmd_voices_fetch,
226
+ cmd_voices_download,
227
+ cmd_voices_exclude,
228
+ cmd_voices_include,
229
+ cmd_voices_cast,
230
+ cmd_voices_audition,
231
+ cmd_voices_tui,
232
+ )
233
+
234
+ if voices_cmd == "list":
235
+ cmd_voices_list(args)
236
+ elif voices_cmd == "fetch":
237
+ cmd_voices_fetch(args)
238
+ elif voices_cmd == "download":
239
+ sys.exit(cmd_voices_download(args))
240
+ elif voices_cmd == "exclude":
241
+ cmd_voices_exclude(args)
242
+ elif voices_cmd == "include":
243
+ cmd_voices_include(args)
244
+ elif voices_cmd == "cast":
245
+ cmd_voices_cast(args)
246
+ elif voices_cmd == "audition":
247
+ cmd_voices_audition(args)
248
+ else:
249
+ cmd_voices_tui(args)
250
+ sys.exit(0)
251
+
252
+ elif command == "cache":
253
+ from .cli.cache import cmd_cache
254
+
255
+ cmd_cache(args)
256
+ sys.exit(0)
257
+
258
+ elif command in ("parse", "attribute", "generate"):
259
+ from .cli.steps import cmd_step
260
+
261
+ sys.exit(cmd_step(command, args))
262
+
263
+ else:
264
+ parser.print_help()
265
+ sys.exit(0)
266
+
267
+
268
+ if __name__ == "__main__":
269
+ main()
kentui/cli/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """kenkui CLI sub-command implementations."""