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 +11 -0
- kentui/__main__.py +269 -0
- kentui/cli/__init__.py +1 -0
- kentui/cli/add.py +1918 -0
- kentui/cli/add_profile.py +60 -0
- kentui/cli/cache.py +48 -0
- kentui/cli/config.py +635 -0
- kentui/cli/steps.py +124 -0
- kentui/cli/system.py +0 -0
- kentui/cli/voices.py +525 -0
- kentui-0.1.0.dist-info/METADATA +363 -0
- kentui-0.1.0.dist-info/RECORD +16 -0
- kentui-0.1.0.dist-info/WHEEL +5 -0
- kentui-0.1.0.dist-info/entry_points.txt +2 -0
- kentui-0.1.0.dist-info/licenses/LICENSE +674 -0
- kentui-0.1.0.dist-info/top_level.txt +1 -0
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."""
|