wenyi-cli 1.4.2__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.
- wenyi_cli/__init__.py +9 -0
- wenyi_cli/__main__.py +3 -0
- wenyi_cli/cli.py +45 -0
- wenyi_cli/commands/__init__.py +1 -0
- wenyi_cli/commands/bootstrap.py +108 -0
- wenyi_cli/commands/context.py +50 -0
- wenyi_cli/commands/glossary.py +104 -0
- wenyi_cli/commands/inspection.py +179 -0
- wenyi_cli/commands/machine_output.py +102 -0
- wenyi_cli/commands/presentation.py +139 -0
- wenyi_cli/commands/progress.py +92 -0
- wenyi_cli/commands/run_report.py +103 -0
- wenyi_cli/commands/validation.py +97 -0
- wenyi_cli/commands/workflows.py +393 -0
- wenyi_cli/main.py +5 -0
- wenyi_cli/model_commands.py +165 -0
- wenyi_cli-1.4.2.dist-info/METADATA +38 -0
- wenyi_cli-1.4.2.dist-info/RECORD +20 -0
- wenyi_cli-1.4.2.dist-info/WHEEL +4 -0
- wenyi_cli-1.4.2.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
"""Register workflows commands with an invocation context accessor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.progress import Progress
|
|
9
|
+
from wenyi_core.config import PipelineConfig
|
|
10
|
+
from wenyi_core.errors import WenyiError
|
|
11
|
+
from wenyi_core.timing import load_timing
|
|
12
|
+
|
|
13
|
+
from wenyi_cli.commands import presentation
|
|
14
|
+
from wenyi_cli.commands import progress as progress_view
|
|
15
|
+
|
|
16
|
+
from .context import ContextAccessor
|
|
17
|
+
from .machine_output import MachineOutput
|
|
18
|
+
from .validation import (
|
|
19
|
+
require_input_file,
|
|
20
|
+
resolve_output_format,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TranslationMode(str, Enum):
|
|
25
|
+
standard = "standard"
|
|
26
|
+
best_of_three = "best_of_three"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def register_workflows_commands(app: typer.Typer, context: ContextAccessor) -> None:
|
|
30
|
+
@app.command(rich_help_panel="Main workflow")
|
|
31
|
+
def parse(
|
|
32
|
+
input_path: str = typer.Argument(..., help="Source book to parse without AI"),
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Parse source chapters and list the contents without AI initialization."""
|
|
35
|
+
from rich.table import Table
|
|
36
|
+
from wenyi_core.pipeline.prepare.input_preparation import (
|
|
37
|
+
locate_input_storage,
|
|
38
|
+
parse_document,
|
|
39
|
+
)
|
|
40
|
+
from wenyi_core.storage.file import FileStorage
|
|
41
|
+
|
|
42
|
+
console = context().console
|
|
43
|
+
try:
|
|
44
|
+
require_input_file(input_path, console=console)
|
|
45
|
+
config = context().load_config()
|
|
46
|
+
store, digest = locate_input_storage(input_path, config, FileStorage)
|
|
47
|
+
try:
|
|
48
|
+
with store.lock():
|
|
49
|
+
document = parse_document(store, input_path, config, expected_sha256=digest)
|
|
50
|
+
table = Table("Chapter", "Title", "Paragraphs")
|
|
51
|
+
for chapter in document.chapters:
|
|
52
|
+
table.add_row(
|
|
53
|
+
str(chapter.index), chapter.title, str(len(chapter.text_segments))
|
|
54
|
+
)
|
|
55
|
+
console.print(table)
|
|
56
|
+
console.print(f"State directory: {store.run_dir}", markup=False)
|
|
57
|
+
console.print("Source parsed. AI preparation has not been run by this command.")
|
|
58
|
+
finally:
|
|
59
|
+
store.close()
|
|
60
|
+
except (WenyiError, ImportError, OSError, ValueError, RuntimeError) as error:
|
|
61
|
+
console.print(f"[red]Error: {error}[/]")
|
|
62
|
+
raise typer.Exit(1) from None
|
|
63
|
+
|
|
64
|
+
def _translate_impl(
|
|
65
|
+
input_path: str,
|
|
66
|
+
*,
|
|
67
|
+
chapter: int | None = None,
|
|
68
|
+
fmt: str | None = None,
|
|
69
|
+
out: str | None = None,
|
|
70
|
+
polish: bool | None = None,
|
|
71
|
+
translation_mode: TranslationMode | None = None,
|
|
72
|
+
review: bool | None = None,
|
|
73
|
+
mono: bool | None = None,
|
|
74
|
+
bilingual: bool | None = None,
|
|
75
|
+
progress_json: str | None = None,
|
|
76
|
+
result_json: str | None = None,
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Run translation and report expected input or configuration errors concisely."""
|
|
79
|
+
console = context().console
|
|
80
|
+
machine = None
|
|
81
|
+
try:
|
|
82
|
+
machine = MachineOutput(
|
|
83
|
+
progress_json,
|
|
84
|
+
result_json,
|
|
85
|
+
source=input_path,
|
|
86
|
+
config=context().config_path,
|
|
87
|
+
output=out,
|
|
88
|
+
)
|
|
89
|
+
machine.start()
|
|
90
|
+
_translate_impl_or_raise(
|
|
91
|
+
input_path,
|
|
92
|
+
chapter=chapter,
|
|
93
|
+
fmt=fmt,
|
|
94
|
+
out=out,
|
|
95
|
+
polish=polish,
|
|
96
|
+
translation_mode=translation_mode,
|
|
97
|
+
review=review,
|
|
98
|
+
mono=mono,
|
|
99
|
+
bilingual=bilingual,
|
|
100
|
+
machine=machine,
|
|
101
|
+
)
|
|
102
|
+
except typer.Exit:
|
|
103
|
+
raise
|
|
104
|
+
except (WenyiError, ImportError, OSError, ValueError, RuntimeError) as error:
|
|
105
|
+
console.print(f"[red]Error: {error}[/]")
|
|
106
|
+
raise typer.Exit(1) from None
|
|
107
|
+
finally:
|
|
108
|
+
# Includes cancellation and unexpected exceptions. Never leave a stale success
|
|
109
|
+
# receipt, and never replace the original error with a reporting failure.
|
|
110
|
+
if machine is not None and not machine.completed:
|
|
111
|
+
try:
|
|
112
|
+
machine.failed()
|
|
113
|
+
except (OSError, WenyiError):
|
|
114
|
+
console.print("[red]Could not publish the machine failure receipt.[/]")
|
|
115
|
+
|
|
116
|
+
def _translate_impl_or_raise(
|
|
117
|
+
input_path: str,
|
|
118
|
+
*,
|
|
119
|
+
chapter: int | None = None,
|
|
120
|
+
fmt: str | None = None,
|
|
121
|
+
out: str | None = None,
|
|
122
|
+
polish: bool | None = None,
|
|
123
|
+
translation_mode: TranslationMode | None = None,
|
|
124
|
+
review: bool | None = None,
|
|
125
|
+
mono: bool | None = None,
|
|
126
|
+
bilingual: bool | None = None,
|
|
127
|
+
machine: MachineOutput,
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Run translation; let ``_translate_impl`` convert exceptions to CLI errors."""
|
|
130
|
+
console = context().console
|
|
131
|
+
from wenyi_core.pipeline.orchestrator import Orchestrator
|
|
132
|
+
|
|
133
|
+
fmt = resolve_output_format(input_path, fmt, console=console)
|
|
134
|
+
config = context().load_config()
|
|
135
|
+
pipeline_values = config.pipeline.model_dump()
|
|
136
|
+
if polish is not None:
|
|
137
|
+
pipeline_values["polish"] = polish
|
|
138
|
+
if translation_mode is not None:
|
|
139
|
+
pipeline_values["translation_mode"] = translation_mode.value
|
|
140
|
+
try:
|
|
141
|
+
config.pipeline = PipelineConfig.model_validate(pipeline_values)
|
|
142
|
+
except ValueError:
|
|
143
|
+
if (
|
|
144
|
+
pipeline_values["translation_mode"] == "best_of_three"
|
|
145
|
+
and not pipeline_values["polish"]
|
|
146
|
+
):
|
|
147
|
+
raise ValueError(
|
|
148
|
+
"best_of_three translation mode requires pipeline.polish=true; remove --no-polish"
|
|
149
|
+
) from None
|
|
150
|
+
raise
|
|
151
|
+
if review is not None:
|
|
152
|
+
config.pipeline.review = review
|
|
153
|
+
if mono is not None:
|
|
154
|
+
config.output.mono = mono
|
|
155
|
+
if bilingual is not None:
|
|
156
|
+
config.output.bilingual = bilingual
|
|
157
|
+
if chapter is not None:
|
|
158
|
+
ignored: list[str] = []
|
|
159
|
+
if fmt not in {None, "epub"}:
|
|
160
|
+
ignored.append("--format")
|
|
161
|
+
if out is not None:
|
|
162
|
+
ignored.append("--out")
|
|
163
|
+
if review is not None:
|
|
164
|
+
ignored.append("--review/--no-review")
|
|
165
|
+
if mono is not None:
|
|
166
|
+
ignored.append("--mono/--no-mono")
|
|
167
|
+
if bilingual is not None:
|
|
168
|
+
ignored.append("--bilingual/--no-bilingual")
|
|
169
|
+
if ignored:
|
|
170
|
+
raise ValueError(
|
|
171
|
+
"--chapter only translates and saves the selected chapter; incompatible finalization options: "
|
|
172
|
+
+ ", ".join(ignored)
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if chapter is not None:
|
|
176
|
+
config.pipeline.review = False
|
|
177
|
+
context().validate_api_configuration(config, "translate")
|
|
178
|
+
require_input_file(input_path, console=console)
|
|
179
|
+
orch = Orchestrator(config, allow_terminology_context=True)
|
|
180
|
+
|
|
181
|
+
with (
|
|
182
|
+
orch.client.interrupt_scope(),
|
|
183
|
+
Progress(
|
|
184
|
+
*progress_view.progress_columns(),
|
|
185
|
+
console=console,
|
|
186
|
+
) as prog,
|
|
187
|
+
):
|
|
188
|
+
rich_progress = progress_view.RichProgressBridge(prog, "Preparing…")
|
|
189
|
+
|
|
190
|
+
def cb(done: int, total: int, label: str) -> None:
|
|
191
|
+
rich_progress(done, total, label)
|
|
192
|
+
machine(done, total, label)
|
|
193
|
+
|
|
194
|
+
if chapter is not None:
|
|
195
|
+
try:
|
|
196
|
+
store = orch.run(input_path, only_chapter=chapter, progress=cb)
|
|
197
|
+
except ValueError as error:
|
|
198
|
+
console.print(f"[red]{error}[/]")
|
|
199
|
+
raise typer.Exit(2) from error
|
|
200
|
+
console.print(
|
|
201
|
+
f"[green]Translated chapter {chapter}[/], State directory: {store.run_dir}"
|
|
202
|
+
)
|
|
203
|
+
presentation.print_usage(console, store.load_usage() or {})
|
|
204
|
+
presentation.print_timing(console, load_timing(store.run_dir))
|
|
205
|
+
machine.success([], store.run_dir)
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
result = orch.run_all(
|
|
209
|
+
input_path,
|
|
210
|
+
progress=cb,
|
|
211
|
+
out_format=fmt,
|
|
212
|
+
out_path=out,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
store = result["store"]
|
|
216
|
+
with store.lock(), store.state_lock():
|
|
217
|
+
chapters = store.load_manifest()["chapters"]
|
|
218
|
+
chapters_done = sum(chapter["status"] == "done" for chapter in chapters)
|
|
219
|
+
term_count = store.stats()["terms"]
|
|
220
|
+
presentation.print_translation_summary(
|
|
221
|
+
console,
|
|
222
|
+
chapters_done=chapters_done,
|
|
223
|
+
chapters_total=len(chapters),
|
|
224
|
+
terms=term_count,
|
|
225
|
+
)
|
|
226
|
+
presentation.print_usage(console, store.load_usage() or {})
|
|
227
|
+
presentation.print_timing(console, load_timing(store.run_dir))
|
|
228
|
+
for path in result.get("outputs") or [result["output"]]:
|
|
229
|
+
console.print(f"Translation: [bold]{path}[/]")
|
|
230
|
+
if result.get("review_dir"):
|
|
231
|
+
presentation.print_review_details(
|
|
232
|
+
console, result.get("review_result"), result["review_dir"]
|
|
233
|
+
)
|
|
234
|
+
machine.success(result.get("outputs") or [result["output"]], store.run_dir)
|
|
235
|
+
|
|
236
|
+
def _prepare_impl(input_path: str) -> None:
|
|
237
|
+
"""Complete preparation without translating body text or exporting files."""
|
|
238
|
+
console = context().console
|
|
239
|
+
from wenyi_core.pipeline.orchestrator import Orchestrator
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
config = context().load_config()
|
|
243
|
+
context().validate_api_configuration(config, "prepare")
|
|
244
|
+
require_input_file(input_path, console=console)
|
|
245
|
+
orch = Orchestrator(config, allow_terminology_context=True)
|
|
246
|
+
with (
|
|
247
|
+
orch.client.interrupt_scope(),
|
|
248
|
+
Progress(
|
|
249
|
+
*progress_view.progress_columns(),
|
|
250
|
+
console=console,
|
|
251
|
+
) as prog,
|
|
252
|
+
):
|
|
253
|
+
cb = progress_view.RichProgressBridge(prog, "Preparing…")
|
|
254
|
+
store = orch.prepare_for_translation(input_path, progress=cb)
|
|
255
|
+
except typer.Exit:
|
|
256
|
+
raise
|
|
257
|
+
except (WenyiError, ImportError, OSError, ValueError, RuntimeError) as error:
|
|
258
|
+
console.print(f"[red]Error: {error}[/]")
|
|
259
|
+
raise typer.Exit(1) from None
|
|
260
|
+
|
|
261
|
+
manifest = store.load_manifest()
|
|
262
|
+
chapters = manifest.get("chapters", [])
|
|
263
|
+
analysis = store.load_analysis() or {}
|
|
264
|
+
digests = sum(
|
|
265
|
+
bool(store.load_chapter(item["index"]).meta.get("source_digest")) for item in chapters
|
|
266
|
+
)
|
|
267
|
+
presentation.print_preparation_summary(
|
|
268
|
+
console,
|
|
269
|
+
chapter_count=len(chapters),
|
|
270
|
+
digest_count=digests,
|
|
271
|
+
has_synopsis=bool(analysis.get("book_synopsis")),
|
|
272
|
+
run_dir=store.run_dir,
|
|
273
|
+
)
|
|
274
|
+
presentation.print_usage(console, store.load_usage() or {})
|
|
275
|
+
presentation.print_timing(console, load_timing(store.run_dir))
|
|
276
|
+
|
|
277
|
+
@app.command(rich_help_panel="Main workflow")
|
|
278
|
+
def translate(
|
|
279
|
+
input: str = typer.Argument(
|
|
280
|
+
...,
|
|
281
|
+
help="Book to translate (EPUB / FB2 / TXT / Markdown / HTML / PDF / DOCX)",
|
|
282
|
+
),
|
|
283
|
+
chapter: int | None = typer.Option(
|
|
284
|
+
None,
|
|
285
|
+
"--chapter",
|
|
286
|
+
min=0,
|
|
287
|
+
help="Translate and save one chapter (zero-based); skip review and export",
|
|
288
|
+
),
|
|
289
|
+
fmt: str | None = typer.Option(
|
|
290
|
+
None,
|
|
291
|
+
"--format",
|
|
292
|
+
help="Output format: epub / txt / html / markdown / docx / pdf (BabelDOC only); default: pdf for BabelDOC PDF state, docx for .docx input, epub otherwise",
|
|
293
|
+
),
|
|
294
|
+
out: str | None = typer.Option(
|
|
295
|
+
None,
|
|
296
|
+
"--out",
|
|
297
|
+
help="Monolingual output path; defaults to the output directory beside the source",
|
|
298
|
+
),
|
|
299
|
+
polish: bool | None = typer.Option(
|
|
300
|
+
None,
|
|
301
|
+
"--polish/--no-polish",
|
|
302
|
+
help="Override pipeline.polish to enable or disable polishing",
|
|
303
|
+
),
|
|
304
|
+
translation_mode: TranslationMode | None = typer.Option(
|
|
305
|
+
None,
|
|
306
|
+
"--translation-mode",
|
|
307
|
+
help="Book translation mode; best_of_three requires polishing",
|
|
308
|
+
),
|
|
309
|
+
review: bool | None = typer.Option(
|
|
310
|
+
None,
|
|
311
|
+
"--review/--no-review",
|
|
312
|
+
help="Override pipeline.review to enable or disable final whole-book review",
|
|
313
|
+
),
|
|
314
|
+
mono: bool | None = typer.Option(
|
|
315
|
+
None,
|
|
316
|
+
"--mono/--no-mono",
|
|
317
|
+
help="Override output.mono to enable or disable monolingual output",
|
|
318
|
+
),
|
|
319
|
+
bilingual: bool | None = typer.Option(
|
|
320
|
+
None,
|
|
321
|
+
"--bilingual/--no-bilingual",
|
|
322
|
+
help="Override output.bilingual to enable or disable bilingual output",
|
|
323
|
+
),
|
|
324
|
+
progress_json: str | None = typer.Option(
|
|
325
|
+
None, "--progress-json", help="Atomically publish protocol-1 progress JSON"
|
|
326
|
+
),
|
|
327
|
+
result_json: str | None = typer.Option(
|
|
328
|
+
None, "--result-json", help="Atomically publish a protocol-1 output receipt"
|
|
329
|
+
),
|
|
330
|
+
):
|
|
331
|
+
"""Prepare, translate, optionally review and export. Repeat to resume."""
|
|
332
|
+
_translate_impl(
|
|
333
|
+
input,
|
|
334
|
+
chapter=chapter,
|
|
335
|
+
fmt=fmt,
|
|
336
|
+
out=out,
|
|
337
|
+
polish=polish,
|
|
338
|
+
translation_mode=translation_mode,
|
|
339
|
+
review=review,
|
|
340
|
+
mono=mono,
|
|
341
|
+
bilingual=bilingual,
|
|
342
|
+
progress_json=progress_json,
|
|
343
|
+
result_json=result_json,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
@app.command(rich_help_panel="Main workflow")
|
|
347
|
+
def prepare(
|
|
348
|
+
input: str = typer.Argument(
|
|
349
|
+
...,
|
|
350
|
+
help="Book to prepare (EPUB / FB2 / TXT / Markdown / HTML / PDF / DOCX)",
|
|
351
|
+
),
|
|
352
|
+
) -> None:
|
|
353
|
+
"""Parse, detect language, analyze style and terms, and prescan the book."""
|
|
354
|
+
_prepare_impl(input)
|
|
355
|
+
|
|
356
|
+
@app.command(rich_help_panel="Quality checks")
|
|
357
|
+
def review(
|
|
358
|
+
input: str = typer.Argument(..., help="Source file whose entire body has been translated"),
|
|
359
|
+
autofix: bool | None = typer.Option(
|
|
360
|
+
None,
|
|
361
|
+
"--autofix/--no-autofix",
|
|
362
|
+
help="Override pipeline.review_autofix to publish revisions to formal chapters",
|
|
363
|
+
),
|
|
364
|
+
):
|
|
365
|
+
"""Run evidence review, shadow revisions and blind rechecks, with optional autofix."""
|
|
366
|
+
console = context().console
|
|
367
|
+
from wenyi_core.pipeline.orchestrator import Orchestrator
|
|
368
|
+
|
|
369
|
+
try:
|
|
370
|
+
config = context().load_config()
|
|
371
|
+
if autofix is not None:
|
|
372
|
+
config.pipeline.review_autofix = autofix
|
|
373
|
+
context().validate_api_configuration(config, "review")
|
|
374
|
+
require_input_file(input, console=console)
|
|
375
|
+
orch = Orchestrator(config)
|
|
376
|
+
|
|
377
|
+
with (
|
|
378
|
+
orch.client.interrupt_scope(),
|
|
379
|
+
Progress(
|
|
380
|
+
*progress_view.progress_columns(),
|
|
381
|
+
console=console,
|
|
382
|
+
) as prog,
|
|
383
|
+
):
|
|
384
|
+
cb = progress_view.RichProgressBridge(prog, "Preparing whole-book review…")
|
|
385
|
+
result = orch.run_review(input, progress=cb)
|
|
386
|
+
except typer.Exit:
|
|
387
|
+
raise
|
|
388
|
+
except (WenyiError, ImportError, OSError, ValueError, RuntimeError) as error:
|
|
389
|
+
console.print(f"[red]Error: {error}[/]")
|
|
390
|
+
raise typer.Exit(1) from None
|
|
391
|
+
|
|
392
|
+
presentation.print_review_summary(console, result["review_result"], result["review_dir"])
|
|
393
|
+
presentation.print_timing(console, load_timing(result["store"].run_dir))
|
wenyi_cli/main.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Model configuration previews and explicit migration commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
import yaml
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
from wenyi_core.config import Config
|
|
14
|
+
from wenyi_core.llm.operations import configured_operations, require_operation
|
|
15
|
+
from wenyi_core.llm.routing import resolve_routes
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def register_model_commands(
|
|
19
|
+
app: typer.Typer, load_config: Callable[[], Config], console: Console
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Register commands once; operation and provider details come from their registries."""
|
|
22
|
+
models = typer.Typer(
|
|
23
|
+
no_args_is_help=True, help="Inspect model routes and manage model settings."
|
|
24
|
+
)
|
|
25
|
+
app.add_typer(models, name="models", rich_help_panel="Configuration")
|
|
26
|
+
|
|
27
|
+
@models.command("list")
|
|
28
|
+
def list_models(
|
|
29
|
+
as_json: bool = typer.Option(False, "--json", help="Print structured routing data"),
|
|
30
|
+
):
|
|
31
|
+
"""Show all effective routes without credentials or network requests."""
|
|
32
|
+
config = load_config()
|
|
33
|
+
routes = resolve_routes(config.llm)
|
|
34
|
+
if as_json:
|
|
35
|
+
typer.echo(
|
|
36
|
+
json.dumps({key: value.describe() for key, value in routes.items()}, indent=2)
|
|
37
|
+
)
|
|
38
|
+
return
|
|
39
|
+
table = Table(
|
|
40
|
+
"Operation",
|
|
41
|
+
"Selection",
|
|
42
|
+
"Tier",
|
|
43
|
+
"Profile",
|
|
44
|
+
"Connection / adapter",
|
|
45
|
+
"Model",
|
|
46
|
+
"Output",
|
|
47
|
+
"Concurrency",
|
|
48
|
+
)
|
|
49
|
+
for operation, route in routes.items():
|
|
50
|
+
connection = config.llm.providers[route.provider]
|
|
51
|
+
table.add_row(
|
|
52
|
+
operation,
|
|
53
|
+
route.origin,
|
|
54
|
+
route.tier or "direct",
|
|
55
|
+
route.profile,
|
|
56
|
+
f"{route.provider} / {route.provider_kind}",
|
|
57
|
+
route.model,
|
|
58
|
+
str(route.max_output_tokens or "provider default"),
|
|
59
|
+
str(connection.max_concurrency or "unlimited"),
|
|
60
|
+
)
|
|
61
|
+
console.print(table)
|
|
62
|
+
|
|
63
|
+
@models.command("explain")
|
|
64
|
+
def explain(operation: str = typer.Option(..., "--operation")):
|
|
65
|
+
"""Explain an operation's defaults, effective request and selection origin."""
|
|
66
|
+
try:
|
|
67
|
+
spec = require_operation(operation)
|
|
68
|
+
route = resolve_routes(load_config().llm)[operation]
|
|
69
|
+
typer.echo(json.dumps({"description": spec.description, **route.describe()}, indent=2))
|
|
70
|
+
except ValueError as error:
|
|
71
|
+
raise typer.BadParameter(str(error)) from None
|
|
72
|
+
|
|
73
|
+
@models.command("check")
|
|
74
|
+
def check(
|
|
75
|
+
workflow: str = typer.Option("translate", "--for", help="prepare, translate or review"),
|
|
76
|
+
):
|
|
77
|
+
"""Validate reachable credentials locally; never send a test request."""
|
|
78
|
+
try:
|
|
79
|
+
config = load_config()
|
|
80
|
+
operations = configured_operations(config, workflow)
|
|
81
|
+
# Imported here so ``--help`` never pays for the provider SDK.
|
|
82
|
+
from wenyi_core.llm.router import RoutedLLMClient
|
|
83
|
+
|
|
84
|
+
RoutedLLMClient(config.llm).validate_credentials(operations)
|
|
85
|
+
console.print(
|
|
86
|
+
f"Model configuration valid for {workflow}: {len(operations)} operations."
|
|
87
|
+
)
|
|
88
|
+
except (ValueError, RuntimeError) as error:
|
|
89
|
+
console.print(f"[red]Error: {error}[/]")
|
|
90
|
+
raise typer.Exit(1) from None
|
|
91
|
+
|
|
92
|
+
@models.command("ping")
|
|
93
|
+
def ping(
|
|
94
|
+
workflow: str = typer.Option("translate", "--for", help="prepare, translate or review"),
|
|
95
|
+
timeout: float = typer.Option(
|
|
96
|
+
30.0, "--timeout", min=0.1, help="Seconds to wait for each provider"
|
|
97
|
+
),
|
|
98
|
+
as_json: bool = typer.Option(False, "--json", help="Print structured probe results"),
|
|
99
|
+
):
|
|
100
|
+
"""Send one short live request per reachable model profile; uses a few tokens."""
|
|
101
|
+
from wenyi_core.llm.connectivity import probe_connections
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
config = load_config()
|
|
105
|
+
operations = configured_operations(config, workflow)
|
|
106
|
+
results = probe_connections(config.llm, operations, timeout=timeout)
|
|
107
|
+
except (ValueError, RuntimeError) as error:
|
|
108
|
+
console.print(f"[red]Error: {error}[/]")
|
|
109
|
+
raise typer.Exit(1) from None
|
|
110
|
+
if as_json:
|
|
111
|
+
typer.echo(json.dumps([result.describe() for result in results], indent=2))
|
|
112
|
+
else:
|
|
113
|
+
table = Table("Profile", "Connection / adapter", "Model", "Status", "Latency", "Detail")
|
|
114
|
+
for result in results:
|
|
115
|
+
table.add_row(
|
|
116
|
+
result.profile,
|
|
117
|
+
f"{result.connection} / {result.provider}",
|
|
118
|
+
result.model,
|
|
119
|
+
"[green]ok[/]" if result.ok else f"[red]{result.category}[/]",
|
|
120
|
+
"-" if result.latency_ms is None else f"{result.latency_ms} ms",
|
|
121
|
+
result.message,
|
|
122
|
+
)
|
|
123
|
+
console.print(table)
|
|
124
|
+
failed = sum(not result.ok for result in results)
|
|
125
|
+
if failed:
|
|
126
|
+
if not as_json:
|
|
127
|
+
console.print(f"[red]{failed} of {len(results)} model profiles unreachable.[/]")
|
|
128
|
+
raise typer.Exit(1)
|
|
129
|
+
if not as_json:
|
|
130
|
+
console.print(f"All {len(results)} model profiles reachable for {workflow}.")
|
|
131
|
+
|
|
132
|
+
@models.command("migrate-config")
|
|
133
|
+
def migrate_config(
|
|
134
|
+
source: Path = typer.Argument(..., exists=True, dir_okay=False),
|
|
135
|
+
out: Path = typer.Option(..., "--out", help="New YAML file; must not exist"),
|
|
136
|
+
):
|
|
137
|
+
"""Convert a retired configuration into a separate, reviewable YAML file."""
|
|
138
|
+
from wenyi_core.llm.migration import convert_config
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
raw = yaml.safe_load(source.read_text(encoding="utf-8"))
|
|
142
|
+
if not isinstance(raw, dict):
|
|
143
|
+
raise ValueError("Configuration must be a mapping")
|
|
144
|
+
converted = convert_config(raw)
|
|
145
|
+
Config.from_dict(converted)
|
|
146
|
+
with out.open("x", encoding="utf-8") as file:
|
|
147
|
+
yaml.safe_dump(converted, file, allow_unicode=True, sort_keys=False)
|
|
148
|
+
console.print(f"Converted configuration: {out}")
|
|
149
|
+
except (OSError, ValueError, yaml.YAMLError) as error:
|
|
150
|
+
console.print(f"[red]Error: {error}[/]")
|
|
151
|
+
raise typer.Exit(1) from None
|
|
152
|
+
|
|
153
|
+
@models.command("migrate-usage")
|
|
154
|
+
def migrate_usage(run_dir: Path = typer.Argument(..., exists=True, file_okay=False)):
|
|
155
|
+
"""Back up and convert usage ledgers in one selected target run directory."""
|
|
156
|
+
from wenyi_core.storage.file_migrations import migrate_usage_ledgers
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
count = migrate_usage_ledgers(run_dir)
|
|
160
|
+
console.print(
|
|
161
|
+
f"Converted {count} usage ledgers; original files backed up alongside them."
|
|
162
|
+
)
|
|
163
|
+
except (OSError, ValueError) as error:
|
|
164
|
+
console.print(f"[red]Error: {error}[/]")
|
|
165
|
+
raise typer.Exit(1) from None
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: wenyi-cli
|
|
3
|
+
Version: 1.4.2
|
|
4
|
+
Summary: Local command-line interface for Wenyi's translation engine.
|
|
5
|
+
Project-URL: Repository, https://github.com/BigDawnGhost/wenyi
|
|
6
|
+
Project-URL: Documentation, https://github.com/BigDawnGhost/wenyi/blob/main/docs/cli.md
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: rich>=13.7
|
|
9
|
+
Requires-Dist: typer>=0.12
|
|
10
|
+
Requires-Dist: wenyi-core
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# Wenyi CLI
|
|
14
|
+
|
|
15
|
+
Local command-line application for multilingual long-form translation, powered
|
|
16
|
+
by `wenyi-core`. It supports terminology management, contextual translation,
|
|
17
|
+
polishing, whole-book review, resumable workflows, and multiple document formats.
|
|
18
|
+
|
|
19
|
+
Install into an activated Python 3.10+ virtual environment:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uv pip install wenyi-cli
|
|
23
|
+
wenyi --version
|
|
24
|
+
wenyi --help
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The executable is `wenyi`; the module entry point is `python -m wenyi_cli`.
|
|
28
|
+
The translation engine is installed automatically. API keys are read from
|
|
29
|
+
environment variables; do not put credentials in configuration files.
|
|
30
|
+
|
|
31
|
+
PDF layout preservation additionally requires an independently installed
|
|
32
|
+
`wenyi-babeldoc-bridge` HTTP service. This package does not install BabelDOC,
|
|
33
|
+
the Web API, or the Desktop application.
|
|
34
|
+
|
|
35
|
+
- [CLI guide](https://github.com/BigDawnGhost/wenyi/blob/main/docs/cli.md)
|
|
36
|
+
- [Configuration](https://github.com/BigDawnGhost/wenyi/blob/main/docs/configuration.md)
|
|
37
|
+
- [Source and license](https://github.com/BigDawnGhost/wenyi)
|
|
38
|
+
- [简体中文](https://github.com/BigDawnGhost/wenyi/blob/main/docs/zh/cli.md)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
wenyi_cli/__init__.py,sha256=WOmaRQbKRo7QSfUoy-CGriiK5WrC8qmUkhVAk4XljXI,274
|
|
2
|
+
wenyi_cli/__main__.py,sha256=vBQ82334kX06ImDbFlPFgiBRiLIinwNk3z8Khs6hd74,31
|
|
3
|
+
wenyi_cli/cli.py,sha256=tKpOwEx-pMzf6F8diAoP935qx3fsZZ9y3E6xWmEiaNc,1552
|
|
4
|
+
wenyi_cli/main.py,sha256=YphRUYMjqX4Mjw-Wu8r3XYQwA09DbtQ1xhgAA29WeNc,98
|
|
5
|
+
wenyi_cli/model_commands.py,sha256=nmpO6R0e7aHPkfCsk40KOYIvWmJO7XIbsUMJQOncTrA,6955
|
|
6
|
+
wenyi_cli/commands/__init__.py,sha256=f9cLnOA-FXA1RuQedQ37Y4u4XIk2Gj2tMEKAWBPdeMQ,62
|
|
7
|
+
wenyi_cli/commands/bootstrap.py,sha256=v2RTEi8f-RCbCecaWT5QgdiUFmSpqUafmMBsx4aMunA,3697
|
|
8
|
+
wenyi_cli/commands/context.py,sha256=a0QySybZDGVCVQA2AxNm_IHVPHEqP-1ooumJW7Qiw9E,1739
|
|
9
|
+
wenyi_cli/commands/glossary.py,sha256=w7PpV0OUHmKvpFhFxXlnCgAVHe0Oommx-HvlxBkRr3Q,4265
|
|
10
|
+
wenyi_cli/commands/inspection.py,sha256=by9C3p7LuYSAevaRapKQC_IvMFNCGHOBgkU1sLrbQDg,7415
|
|
11
|
+
wenyi_cli/commands/machine_output.py,sha256=_BJ0G1QV_FsC7ZZ3uASkNYS-Jj4cx09zBnf4HDmmhEg,3519
|
|
12
|
+
wenyi_cli/commands/presentation.py,sha256=um7irPa6f1Enk8UgONUbFj0vau8CjG8GfQkUFwUcq6M,5890
|
|
13
|
+
wenyi_cli/commands/progress.py,sha256=5UnWFSZgljhvW0eut8BwnKY7yL_Qb9iMHP073B6191Y,3272
|
|
14
|
+
wenyi_cli/commands/run_report.py,sha256=ZhLwG0cs-1-rpydylb7RuzRJ0rNe303yPJJq1fB6YOY,3820
|
|
15
|
+
wenyi_cli/commands/validation.py,sha256=FRGfaVHf2MAxluluvVYKveV8ifVVGZiw81AKz-YEZos,3777
|
|
16
|
+
wenyi_cli/commands/workflows.py,sha256=q8xGFIzh1KXRK02x-eMM10q2njxNQmgQdn8y1ISkx6U,15538
|
|
17
|
+
wenyi_cli-1.4.2.dist-info/METADATA,sha256=R3ICtNpttA3j6UfDqOBJy02u7luMXKedRxaKwUlZLbQ,1519
|
|
18
|
+
wenyi_cli-1.4.2.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
19
|
+
wenyi_cli-1.4.2.dist-info/entry_points.txt,sha256=UOuMOGDjQXGrG-03A1dxl2LWBWhdmOHjkqHrZs95m4c,46
|
|
20
|
+
wenyi_cli-1.4.2.dist-info/RECORD,,
|