blitzline 0.1.1__tar.gz

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.
Files changed (45) hide show
  1. blitzline-0.1.1/PKG-INFO +117 -0
  2. blitzline-0.1.1/README.md +97 -0
  3. blitzline-0.1.1/blitzline/__init__.py +9 -0
  4. blitzline-0.1.1/blitzline/__main__.py +12 -0
  5. blitzline-0.1.1/blitzline/cli.py +324 -0
  6. blitzline-0.1.1/blitzline/config.py +492 -0
  7. blitzline-0.1.1/blitzline/core.py +672 -0
  8. blitzline-0.1.1/blitzline/exports/__init__.py +7 -0
  9. blitzline-0.1.1/blitzline/exports/anki.py +191 -0
  10. blitzline-0.1.1/blitzline/exports/common.py +96 -0
  11. blitzline-0.1.1/blitzline/exports/model.py +315 -0
  12. blitzline-0.1.1/blitzline/exports/package.py +110 -0
  13. blitzline-0.1.1/blitzline/exports/presets/blitzer.json +33 -0
  14. blitzline-0.1.1/blitzline/exports/tsv.py +107 -0
  15. blitzline-0.1.1/blitzline/integrations/__init__.py +7 -0
  16. blitzline-0.1.1/blitzline/integrations/asr.py +166 -0
  17. blitzline-0.1.1/blitzline/integrations/blitzer.py +142 -0
  18. blitzline-0.1.1/blitzline/integrations/llm.py +165 -0
  19. blitzline-0.1.1/blitzline/integrations/remote_asr.py +212 -0
  20. blitzline-0.1.1/blitzline/media.py +170 -0
  21. blitzline-0.1.1/blitzline/records.py +161 -0
  22. blitzline-0.1.1/blitzline/review.py +265 -0
  23. blitzline-0.1.1/blitzline/storage.py +189 -0
  24. blitzline-0.1.1/blitzline/transcript.py +269 -0
  25. blitzline-0.1.1/blitzline/vocabulary.py +296 -0
  26. blitzline-0.1.1/blitzline.egg-info/PKG-INFO +117 -0
  27. blitzline-0.1.1/blitzline.egg-info/SOURCES.txt +43 -0
  28. blitzline-0.1.1/blitzline.egg-info/dependency_links.txt +1 -0
  29. blitzline-0.1.1/blitzline.egg-info/entry_points.txt +2 -0
  30. blitzline-0.1.1/blitzline.egg-info/requires.txt +16 -0
  31. blitzline-0.1.1/blitzline.egg-info/top_level.txt +1 -0
  32. blitzline-0.1.1/pyproject.toml +50 -0
  33. blitzline-0.1.1/setup.cfg +4 -0
  34. blitzline-0.1.1/tests/test_adapters.py +170 -0
  35. blitzline-0.1.1/tests/test_blitzer.py +248 -0
  36. blitzline-0.1.1/tests/test_config_llm.py +185 -0
  37. blitzline-0.1.1/tests/test_deck_profiles.py +251 -0
  38. blitzline-0.1.1/tests/test_exports.py +187 -0
  39. blitzline-0.1.1/tests/test_note_models.py +335 -0
  40. blitzline-0.1.1/tests/test_pipeline.py +202 -0
  41. blitzline-0.1.1/tests/test_release.py +99 -0
  42. blitzline-0.1.1/tests/test_remote_asr.py +324 -0
  43. blitzline-0.1.1/tests/test_storage_cli.py +88 -0
  44. blitzline-0.1.1/tests/test_transcript.py +132 -0
  45. blitzline-0.1.1/tests/test_vocabulary.py +132 -0
@@ -0,0 +1,117 @@
1
+ Metadata-Version: 2.4
2
+ Name: blitzline
3
+ Version: 0.1.1
4
+ Summary: Turn foreign-language recordings into subtitle-linked Anki vocabulary.
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: click>=8.1
8
+ Requires-Dist: platformdirs>=4
9
+ Provides-Extra: apkg
10
+ Requires-Dist: genanki<0.14,>=0.13.1; extra == "apkg"
11
+ Provides-Extra: modal
12
+ Requires-Dist: modal<2,>=1.0; extra == "modal"
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest>=8; extra == "dev"
15
+ Requires-Dist: ruff>=0.11; extra == "dev"
16
+ Requires-Dist: genanki<0.14,>=0.13.1; extra == "dev"
17
+ Requires-Dist: bump-my-version>=1; extra == "dev"
18
+ Requires-Dist: build>=1.2; extra == "dev"
19
+ Requires-Dist: twine>=6; extra == "dev"
20
+
21
+ # Blitzline
22
+
23
+ Turn a foreign-language audio/video file into subtitles and Anki vocabulary
24
+ with matching audio or video examples. Blitzline uses WhisperX for transcription,
25
+ `bltzr` for unknown-word extraction, and an optional LLM for proofreading,
26
+ English subtitles, and lemma review.
27
+
28
+ **LLM providers:** OpenRouter, DeepSeek, and custom chat-compatible endpoints.
29
+ Each stage can use a different provider/model. Internally, responses are validated
30
+ JSON; Blitzline generates timestamps, bold examples, media references, and TSV.
31
+
32
+ Start with the [short setup guide](docs/setup.md) and
33
+ [example configuration](docs/config.example.toml).
34
+
35
+ ```sh
36
+ python3 -m venv .venv
37
+ .venv/bin/python -m pip install -e '.[apkg]'
38
+ .venv/bin/blitzline check --config docs/config.example.toml
39
+ .venv/bin/blitzline run recording.mp4 --language slv --config docs/config.example.toml
40
+ ```
41
+
42
+ Install FFmpeg and Blitzer with your language pack separately. Transcribe locally
43
+ with WhisperX or [offload to Modal or an HTTP worker](docs/transcription.md).
44
+ Choose your model and set its API key before using the example config.
45
+ WhisperX can run in another environment via `asr.executable`.
46
+
47
+ [Blitzer](https://github.com/samiddhi/blitzer-py/) can be installed from pip with:
48
+
49
+ ```sh
50
+ pip install bltzr
51
+ ```
52
+
53
+ Use a Blitzer build whose `bltzr blitz --help` lists
54
+ `--skip-exact-words-file` and `--skip-word-families-file` for the new lists.
55
+ If working from the updated Blitzer checkout, `blitzer.executable` can point
56
+ to its `.venv/bin/bltzr`.
57
+
58
+ Choose [which words to skip](docs/word-lists.md): **exact words (word form)**
59
+ skip only listed spellings, while **word families (lexeme/lemma)** skip a word
60
+ and its dictionary-connected forms. For **to be**, an exact list containing
61
+ **be** and **am** leaves **are** counted; a family list containing **be** skips
62
+ its recognized forms. Blitzline uses your Blitzer lists automatically, or you
63
+ can override either file in the `[blitzer]` section of your Blitzline config.
64
+
65
+ Already have subtitles? Skip transcription:
66
+
67
+ ```sh
68
+ blitzline run recording.mp4 --language slv --srt recording.srt --config my-config.toml
69
+ # Or: --asr-json whisper-output.json
70
+ ```
71
+
72
+ Without an LLM, the run stops with an editable `artifacts/review.json` containing
73
+ one decision per candidate. Fill its meanings and decisions using
74
+ `artifacts/04-vocabulary.json`, then continue:
75
+
76
+ ```sh
77
+ blitzline resume RUN_DIR --review RUN_DIR/artifacts/review.json
78
+ blitzline export RUN_DIR --format apkg
79
+ # Direct delivery requires Anki running with AnkiConnect:
80
+ blitzline export RUN_DIR --format anki
81
+ ```
82
+
83
+ The default [Anki note preset](docs/anki.md) matches `ref/example_note.apkg`:
84
+ clickable sentence cards, example audio, and optional typed recall. Fields,
85
+ mappings, multiple templates, CSS/JavaScript, tags, and assets are configurable.
86
+ The legacy format remains available. Configure a language and card types for each
87
+ [deck profile](docs/anki.md#multiple-languages-and-explicit-deck-profiles), then use
88
+ `--deck-profile slovenian` to select its deck and infer its source language. One
89
+ configuration supports multiple languages and decks.
90
+
91
+ Use `--clips video` for video examples, `--translate` for English subtitles, and
92
+ `--proofread` for transcript review. Optional `asr.relisten` performs a second ASR
93
+ pass on questionable audio; a text-only model is never presented as listening.
94
+ Unresolved items stop delivery; `--allow-partial` explicitly exports accepted
95
+ cards and keeps a report of omissions.
96
+
97
+ Runs contain `artifacts/`, `media/`, `exports/`, `logs/`, and `manifest.json`.
98
+ The command prints their location. Resume reuses checksum-valid stages and
99
+ invalidates downstream work when transcript/config/skip-list inputs change.
100
+ Blitzer's skip lists and history are never updated by this pipeline.
101
+
102
+ The original requirements and prompt are retained in [the vision](docs/vision.md).
103
+ Publish releases with `make release`; see the [one-time GitHub/PyPI setup and
104
+ release steps](maintenance/RELEASING.md). The helper bumps the patch version and
105
+ publishes a GitHub release, which triggers the PyPI workflow.
106
+
107
+ See [the implementation checklist](docs/plan.md) and
108
+ [developer/configuration guide](docs/development.md) for stage contracts,
109
+ limitations, and verification. Every Python module documents its scope and
110
+ navigation; domain transformations are functions independent of I/O.
111
+
112
+ ```sh
113
+ python -m pip install -e '.[dev]'
114
+ python -m pytest -q
115
+ python -m ruff check blitzline tests
116
+ python -m ruff format --check blitzline tests
117
+ ```
@@ -0,0 +1,97 @@
1
+ # Blitzline
2
+
3
+ Turn a foreign-language audio/video file into subtitles and Anki vocabulary
4
+ with matching audio or video examples. Blitzline uses WhisperX for transcription,
5
+ `bltzr` for unknown-word extraction, and an optional LLM for proofreading,
6
+ English subtitles, and lemma review.
7
+
8
+ **LLM providers:** OpenRouter, DeepSeek, and custom chat-compatible endpoints.
9
+ Each stage can use a different provider/model. Internally, responses are validated
10
+ JSON; Blitzline generates timestamps, bold examples, media references, and TSV.
11
+
12
+ Start with the [short setup guide](docs/setup.md) and
13
+ [example configuration](docs/config.example.toml).
14
+
15
+ ```sh
16
+ python3 -m venv .venv
17
+ .venv/bin/python -m pip install -e '.[apkg]'
18
+ .venv/bin/blitzline check --config docs/config.example.toml
19
+ .venv/bin/blitzline run recording.mp4 --language slv --config docs/config.example.toml
20
+ ```
21
+
22
+ Install FFmpeg and Blitzer with your language pack separately. Transcribe locally
23
+ with WhisperX or [offload to Modal or an HTTP worker](docs/transcription.md).
24
+ Choose your model and set its API key before using the example config.
25
+ WhisperX can run in another environment via `asr.executable`.
26
+
27
+ [Blitzer](https://github.com/samiddhi/blitzer-py/) can be installed from pip with:
28
+
29
+ ```sh
30
+ pip install bltzr
31
+ ```
32
+
33
+ Use a Blitzer build whose `bltzr blitz --help` lists
34
+ `--skip-exact-words-file` and `--skip-word-families-file` for the new lists.
35
+ If working from the updated Blitzer checkout, `blitzer.executable` can point
36
+ to its `.venv/bin/bltzr`.
37
+
38
+ Choose [which words to skip](docs/word-lists.md): **exact words (word form)**
39
+ skip only listed spellings, while **word families (lexeme/lemma)** skip a word
40
+ and its dictionary-connected forms. For **to be**, an exact list containing
41
+ **be** and **am** leaves **are** counted; a family list containing **be** skips
42
+ its recognized forms. Blitzline uses your Blitzer lists automatically, or you
43
+ can override either file in the `[blitzer]` section of your Blitzline config.
44
+
45
+ Already have subtitles? Skip transcription:
46
+
47
+ ```sh
48
+ blitzline run recording.mp4 --language slv --srt recording.srt --config my-config.toml
49
+ # Or: --asr-json whisper-output.json
50
+ ```
51
+
52
+ Without an LLM, the run stops with an editable `artifacts/review.json` containing
53
+ one decision per candidate. Fill its meanings and decisions using
54
+ `artifacts/04-vocabulary.json`, then continue:
55
+
56
+ ```sh
57
+ blitzline resume RUN_DIR --review RUN_DIR/artifacts/review.json
58
+ blitzline export RUN_DIR --format apkg
59
+ # Direct delivery requires Anki running with AnkiConnect:
60
+ blitzline export RUN_DIR --format anki
61
+ ```
62
+
63
+ The default [Anki note preset](docs/anki.md) matches `ref/example_note.apkg`:
64
+ clickable sentence cards, example audio, and optional typed recall. Fields,
65
+ mappings, multiple templates, CSS/JavaScript, tags, and assets are configurable.
66
+ The legacy format remains available. Configure a language and card types for each
67
+ [deck profile](docs/anki.md#multiple-languages-and-explicit-deck-profiles), then use
68
+ `--deck-profile slovenian` to select its deck and infer its source language. One
69
+ configuration supports multiple languages and decks.
70
+
71
+ Use `--clips video` for video examples, `--translate` for English subtitles, and
72
+ `--proofread` for transcript review. Optional `asr.relisten` performs a second ASR
73
+ pass on questionable audio; a text-only model is never presented as listening.
74
+ Unresolved items stop delivery; `--allow-partial` explicitly exports accepted
75
+ cards and keeps a report of omissions.
76
+
77
+ Runs contain `artifacts/`, `media/`, `exports/`, `logs/`, and `manifest.json`.
78
+ The command prints their location. Resume reuses checksum-valid stages and
79
+ invalidates downstream work when transcript/config/skip-list inputs change.
80
+ Blitzer's skip lists and history are never updated by this pipeline.
81
+
82
+ The original requirements and prompt are retained in [the vision](docs/vision.md).
83
+ Publish releases with `make release`; see the [one-time GitHub/PyPI setup and
84
+ release steps](maintenance/RELEASING.md). The helper bumps the patch version and
85
+ publishes a GitHub release, which triggers the PyPI workflow.
86
+
87
+ See [the implementation checklist](docs/plan.md) and
88
+ [developer/configuration guide](docs/development.md) for stage contracts,
89
+ limitations, and verification. Every Python module documents its scope and
90
+ navigation; domain transformations are functions independent of I/O.
91
+
92
+ ```sh
93
+ python -m pip install -e '.[dev]'
94
+ python -m pytest -q
95
+ python -m ruff check blitzline tests
96
+ python -m ruff format --check blitzline tests
97
+ ```
@@ -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"
@@ -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()
@@ -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")