blitzline 0.1.1__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.
- blitzline/__init__.py +9 -0
- blitzline/__main__.py +12 -0
- blitzline/cli.py +324 -0
- blitzline/config.py +492 -0
- blitzline/core.py +672 -0
- blitzline/exports/__init__.py +7 -0
- blitzline/exports/anki.py +191 -0
- blitzline/exports/common.py +96 -0
- blitzline/exports/model.py +315 -0
- blitzline/exports/package.py +110 -0
- blitzline/exports/presets/blitzer.json +33 -0
- blitzline/exports/tsv.py +107 -0
- blitzline/integrations/__init__.py +7 -0
- blitzline/integrations/asr.py +166 -0
- blitzline/integrations/blitzer.py +142 -0
- blitzline/integrations/llm.py +165 -0
- blitzline/integrations/remote_asr.py +212 -0
- blitzline/media.py +170 -0
- blitzline/records.py +161 -0
- blitzline/review.py +265 -0
- blitzline/storage.py +189 -0
- blitzline/transcript.py +269 -0
- blitzline/vocabulary.py +296 -0
- blitzline-0.1.1.dist-info/METADATA +117 -0
- blitzline-0.1.1.dist-info/RECORD +28 -0
- blitzline-0.1.1.dist-info/WHEEL +5 -0
- blitzline-0.1.1.dist-info/entry_points.txt +2 -0
- blitzline-0.1.1.dist-info/top_level.txt +1 -0
blitzline/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Identify the Blitzline package without starting the application.
|
|
2
|
+
|
|
3
|
+
Scope statement: expose package metadata only.
|
|
4
|
+
Included: the installed application version.
|
|
5
|
+
Excluded: CLI startup (cli.py), pipeline execution (core.py), and I/O.
|
|
6
|
+
Start here: core.run_pipeline provides the application API.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.1"
|
blitzline/__main__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Start the CLI when Python runs the Blitzline package as a module.
|
|
2
|
+
|
|
3
|
+
Scope statement: forward module execution to Click.
|
|
4
|
+
Included: the guarded console entry.
|
|
5
|
+
Excluded: command definitions (cli.py) and pipeline logic (core.py).
|
|
6
|
+
Start here: run `python -m blitzline --help` to see available commands.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from blitzline.cli import cli
|
|
10
|
+
|
|
11
|
+
if __name__ == "__main__":
|
|
12
|
+
cli()
|
blitzline/cli.py
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"""Present the recording-to-Anki pipeline as a small command-line interface.
|
|
2
|
+
|
|
3
|
+
Scope statement: translate explicit terminal choices into application calls.
|
|
4
|
+
Included: run/resume/export/check commands, option validation, concise progress,
|
|
5
|
+
artifact locations, dependency diagnostics, and documented exit statuses.
|
|
6
|
+
Excluded: stage algorithms (core.py/transcript.py/vocabulary.py), provider calls
|
|
7
|
+
(integrations/), persistence (storage.py), and export construction (exports/).
|
|
8
|
+
Start here: cli is the console entry; run starts the main pipeline.
|
|
9
|
+
Exit statuses: 0 success/intentional stop, 1 failed stage, 2 invalid input,
|
|
10
|
+
3 awaiting review. Commands never prompt during processing.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import shutil
|
|
15
|
+
from contextlib import contextmanager
|
|
16
|
+
from importlib.util import find_spec
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import click
|
|
20
|
+
|
|
21
|
+
from blitzline import __version__
|
|
22
|
+
from blitzline.config import get_config, llm_profile, resolve_language
|
|
23
|
+
from blitzline.core import STAGES, export_run, resume_pipeline, run_pipeline
|
|
24
|
+
from blitzline.integrations.blitzer import preflight
|
|
25
|
+
from blitzline.records import NeedsReview, PipelineError
|
|
26
|
+
from blitzline.storage import read_json
|
|
27
|
+
|
|
28
|
+
CONTEXT = {"help_option_names": ["-h", "--help"]}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@contextmanager
|
|
32
|
+
def errors():
|
|
33
|
+
"""Convert expected failures into concise CLI messages and stable exit codes."""
|
|
34
|
+
try:
|
|
35
|
+
yield
|
|
36
|
+
except NeedsReview as error:
|
|
37
|
+
click.echo(str(error), err=True)
|
|
38
|
+
raise click.exceptions.Exit(3) from None
|
|
39
|
+
except (ValueError, KeyError, TypeError) as error:
|
|
40
|
+
raise click.UsageError(str(error)) from None
|
|
41
|
+
except (PipelineError, OSError) as error:
|
|
42
|
+
raise click.ClickException(str(error)) from None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def common_options(function):
|
|
46
|
+
"""Attach shared explicit configuration and delivery overrides to commands."""
|
|
47
|
+
options = [
|
|
48
|
+
click.option(
|
|
49
|
+
"--deck-profile", help="Configured language/deck/card-type profile."
|
|
50
|
+
),
|
|
51
|
+
click.option(
|
|
52
|
+
"--config", type=click.Path(exists=True, dir_okay=False, path_type=Path)
|
|
53
|
+
),
|
|
54
|
+
click.option(
|
|
55
|
+
"--format", "output_format", type=click.Choice(["tsv", "apkg", "anki"])
|
|
56
|
+
),
|
|
57
|
+
click.option("--clips", type=click.Choice(["audio", "video"])),
|
|
58
|
+
click.option("--proofread/--no-proofread", default=None),
|
|
59
|
+
click.option("--translate/--no-translate", default=None),
|
|
60
|
+
click.option("--allow-partial/--no-allow-partial", default=None),
|
|
61
|
+
]
|
|
62
|
+
for option in options:
|
|
63
|
+
function = option(function)
|
|
64
|
+
return function
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def transcript_options(function):
|
|
68
|
+
"""Attach import, review, and stage-stop choices to run/resume commands."""
|
|
69
|
+
options = [
|
|
70
|
+
click.option(
|
|
71
|
+
"--asr-backend",
|
|
72
|
+
type=click.Choice(["local", "modal", "http"]),
|
|
73
|
+
help="Transcription backend; remote endpoints are configured in TOML.",
|
|
74
|
+
),
|
|
75
|
+
click.option(
|
|
76
|
+
"--srt", type=click.Path(exists=True, dir_okay=False, path_type=Path)
|
|
77
|
+
),
|
|
78
|
+
click.option(
|
|
79
|
+
"--asr-json", type=click.Path(exists=True, dir_okay=False, path_type=Path)
|
|
80
|
+
),
|
|
81
|
+
click.option(
|
|
82
|
+
"--review",
|
|
83
|
+
"review_file",
|
|
84
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
85
|
+
),
|
|
86
|
+
click.option("--stop-after", type=click.Choice(STAGES)),
|
|
87
|
+
]
|
|
88
|
+
for option in options:
|
|
89
|
+
function = option(function)
|
|
90
|
+
return function
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def overrides(
|
|
94
|
+
output_format=None,
|
|
95
|
+
clips=None,
|
|
96
|
+
proofread=None,
|
|
97
|
+
translate=None,
|
|
98
|
+
allow_partial=None,
|
|
99
|
+
asr_backend=None,
|
|
100
|
+
deck_profile=None,
|
|
101
|
+
):
|
|
102
|
+
"""Build nested non-None CLI choices for the configuration/application APIs."""
|
|
103
|
+
return {
|
|
104
|
+
"export": {"format": output_format, "deck_profile": deck_profile},
|
|
105
|
+
"asr": {"backend": asr_backend},
|
|
106
|
+
"media": {"kind": clips},
|
|
107
|
+
"run": {
|
|
108
|
+
"proofread": proofread,
|
|
109
|
+
"translate": translate,
|
|
110
|
+
"allow_partial": allow_partial,
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def progress(message: str) -> None:
|
|
116
|
+
"""Show a brief stage message on stderr, leaving stdout for results."""
|
|
117
|
+
click.echo(message, err=True)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def show_result(directory: Path) -> None:
|
|
121
|
+
"""Print the run directory, state, card count, and completed artifact locations."""
|
|
122
|
+
manifest = read_json(directory / "manifest.json")
|
|
123
|
+
click.echo(f"Run: {directory}")
|
|
124
|
+
click.echo(f"State: {manifest['state']}; cards: {manifest.get('cards', 0)}")
|
|
125
|
+
if (directory / "artifacts" / "source.srt").is_file():
|
|
126
|
+
click.echo(f"Subtitles: {directory / 'artifacts' / 'source.srt'}")
|
|
127
|
+
if manifest["state"] == "complete":
|
|
128
|
+
click.echo(
|
|
129
|
+
f"Exports: {directory / 'exports' / manifest['settings']['export']['format']}"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@click.group(context_settings=CONTEXT)
|
|
134
|
+
@click.version_option(__version__)
|
|
135
|
+
def cli():
|
|
136
|
+
"""Turn a foreign-language recording into subtitles and Anki vocabulary."""
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@cli.command()
|
|
140
|
+
@click.argument("media", type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
|
141
|
+
@click.option(
|
|
142
|
+
"--language",
|
|
143
|
+
"-l",
|
|
144
|
+
help="Blitzer three-letter language code; inferred from --deck-profile when omitted.",
|
|
145
|
+
)
|
|
146
|
+
@click.option("--output", "directory", type=click.Path(file_okay=False, path_type=Path))
|
|
147
|
+
@common_options
|
|
148
|
+
@transcript_options
|
|
149
|
+
def run(
|
|
150
|
+
media,
|
|
151
|
+
language,
|
|
152
|
+
directory,
|
|
153
|
+
config,
|
|
154
|
+
deck_profile,
|
|
155
|
+
output_format,
|
|
156
|
+
clips,
|
|
157
|
+
proofread,
|
|
158
|
+
translate,
|
|
159
|
+
allow_partial,
|
|
160
|
+
asr_backend,
|
|
161
|
+
srt,
|
|
162
|
+
asr_json,
|
|
163
|
+
review_file,
|
|
164
|
+
stop_after,
|
|
165
|
+
):
|
|
166
|
+
"""Process MEDIA, optionally starting from an existing subtitle/transcript."""
|
|
167
|
+
with errors():
|
|
168
|
+
settings = get_config(
|
|
169
|
+
config,
|
|
170
|
+
overrides(
|
|
171
|
+
output_format,
|
|
172
|
+
clips,
|
|
173
|
+
proofread,
|
|
174
|
+
translate,
|
|
175
|
+
allow_partial,
|
|
176
|
+
asr_backend,
|
|
177
|
+
deck_profile,
|
|
178
|
+
),
|
|
179
|
+
)
|
|
180
|
+
result = run_pipeline(
|
|
181
|
+
media,
|
|
182
|
+
resolve_language(settings, language),
|
|
183
|
+
settings,
|
|
184
|
+
directory=directory,
|
|
185
|
+
srt=srt,
|
|
186
|
+
asr_json=asr_json,
|
|
187
|
+
review_file=review_file,
|
|
188
|
+
stop_after=stop_after,
|
|
189
|
+
progress=progress,
|
|
190
|
+
)
|
|
191
|
+
show_result(result)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@cli.command()
|
|
195
|
+
@click.argument(
|
|
196
|
+
"run_dir", type=click.Path(exists=True, file_okay=False, path_type=Path)
|
|
197
|
+
)
|
|
198
|
+
@common_options
|
|
199
|
+
@transcript_options
|
|
200
|
+
def resume(
|
|
201
|
+
run_dir,
|
|
202
|
+
config,
|
|
203
|
+
deck_profile,
|
|
204
|
+
output_format,
|
|
205
|
+
clips,
|
|
206
|
+
proofread,
|
|
207
|
+
translate,
|
|
208
|
+
allow_partial,
|
|
209
|
+
asr_backend,
|
|
210
|
+
srt,
|
|
211
|
+
asr_json,
|
|
212
|
+
review_file,
|
|
213
|
+
stop_after,
|
|
214
|
+
):
|
|
215
|
+
"""Continue a saved run with optional edited review or transcript inputs."""
|
|
216
|
+
with errors():
|
|
217
|
+
selected = get_config(config) if config else None
|
|
218
|
+
result = resume_pipeline(
|
|
219
|
+
run_dir,
|
|
220
|
+
settings=selected,
|
|
221
|
+
overrides=overrides(
|
|
222
|
+
output_format,
|
|
223
|
+
clips,
|
|
224
|
+
proofread,
|
|
225
|
+
translate,
|
|
226
|
+
allow_partial,
|
|
227
|
+
asr_backend,
|
|
228
|
+
deck_profile,
|
|
229
|
+
),
|
|
230
|
+
srt=srt,
|
|
231
|
+
asr_json=asr_json,
|
|
232
|
+
review_file=review_file,
|
|
233
|
+
stop_after=stop_after,
|
|
234
|
+
progress=progress,
|
|
235
|
+
)
|
|
236
|
+
show_result(result)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@cli.command("export")
|
|
240
|
+
@click.argument(
|
|
241
|
+
"run_dir", type=click.Path(exists=True, file_okay=False, path_type=Path)
|
|
242
|
+
)
|
|
243
|
+
@click.option("--config", type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
|
244
|
+
@click.option(
|
|
245
|
+
"--format",
|
|
246
|
+
"output_format",
|
|
247
|
+
type=click.Choice(["tsv", "apkg", "anki"]),
|
|
248
|
+
required=True,
|
|
249
|
+
)
|
|
250
|
+
@click.option("--allow-partial/--no-allow-partial", default=None)
|
|
251
|
+
@click.option(
|
|
252
|
+
"--deck-profile",
|
|
253
|
+
help="Configured destination deck profile for this run's language.",
|
|
254
|
+
)
|
|
255
|
+
def export_command(run_dir, config, output_format, allow_partial, deck_profile):
|
|
256
|
+
"""Deliver existing validated cards as TSV, APKG, or directly to Anki."""
|
|
257
|
+
with errors():
|
|
258
|
+
result = export_run(
|
|
259
|
+
run_dir,
|
|
260
|
+
settings=get_config(config) if config else None,
|
|
261
|
+
overrides=overrides(
|
|
262
|
+
output_format=output_format,
|
|
263
|
+
allow_partial=allow_partial,
|
|
264
|
+
deck_profile=deck_profile,
|
|
265
|
+
),
|
|
266
|
+
)
|
|
267
|
+
for path in result["paths"]:
|
|
268
|
+
click.echo(path)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@cli.command("check")
|
|
272
|
+
@click.option(
|
|
273
|
+
"--language", "-l", help="Also verify this Blitzer language pack and config."
|
|
274
|
+
)
|
|
275
|
+
@click.option("--config", type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
|
276
|
+
def check_command(config, language):
|
|
277
|
+
"""Show configured tools/providers and missing prerequisites without requests."""
|
|
278
|
+
with errors():
|
|
279
|
+
settings = get_config(config)
|
|
280
|
+
missing = []
|
|
281
|
+
for label, executable in (
|
|
282
|
+
("Blitzer", settings["blitzer"]["executable"]),
|
|
283
|
+
("FFmpeg", settings["media"]["ffmpeg"]),
|
|
284
|
+
("FFprobe", settings["media"]["ffprobe"]),
|
|
285
|
+
):
|
|
286
|
+
path = shutil.which(executable)
|
|
287
|
+
click.echo(f"{label}: {path or 'missing'}")
|
|
288
|
+
if not path and not label.startswith("ASR"):
|
|
289
|
+
missing.append(label)
|
|
290
|
+
for name, profile in settings["decks"].items():
|
|
291
|
+
click.echo(
|
|
292
|
+
f"Deck profile {name}: {profile['language']} -> {profile['deck']}; card types: {', '.join(profile['card_types'])}"
|
|
293
|
+
)
|
|
294
|
+
asr = settings["asr"]
|
|
295
|
+
click.echo(f"ASR backend: {asr['backend']} (not needed for imported SRT/JSON)")
|
|
296
|
+
if asr["backend"] == "local":
|
|
297
|
+
click.echo(f"WhisperX: {shutil.which(asr['executable']) or 'missing'}")
|
|
298
|
+
elif asr["backend"] == "modal":
|
|
299
|
+
click.echo(
|
|
300
|
+
f"Modal: {asr['modal_app']}/{asr['modal_function']}; SDK: {'available' if find_spec('modal') else 'install blitzline[modal]'}"
|
|
301
|
+
)
|
|
302
|
+
else:
|
|
303
|
+
click.echo(f"Remote ASR endpoint: {asr['endpoint']}")
|
|
304
|
+
if asr["api_key_env"]:
|
|
305
|
+
click.echo(
|
|
306
|
+
f"{asr['api_key_env']}: {'set' if os.environ.get(asr['api_key_env']) else 'missing'}"
|
|
307
|
+
)
|
|
308
|
+
click.echo(
|
|
309
|
+
f"APKG writer: {'available' if find_spec('genanki') else 'install blitzline[apkg]'}"
|
|
310
|
+
)
|
|
311
|
+
for stage in ("proofread", "translate", "review"):
|
|
312
|
+
profile = llm_profile(settings, stage)
|
|
313
|
+
if not profile:
|
|
314
|
+
click.echo(f"{stage}: no LLM profile")
|
|
315
|
+
continue
|
|
316
|
+
present = bool(os.environ.get(profile["api_key_env"]))
|
|
317
|
+
click.echo(
|
|
318
|
+
f"{stage}: {profile['provider']} / {profile['model']}; {profile['api_key_env']}: {'set' if present else 'missing'}"
|
|
319
|
+
)
|
|
320
|
+
if missing:
|
|
321
|
+
raise PipelineError("Missing required tools: " + ", ".join(missing))
|
|
322
|
+
if language:
|
|
323
|
+
preflight(language, settings["blitzer"])
|
|
324
|
+
click.echo(f"Blitzer {language}: ready")
|