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.
- blitzline-0.1.1/PKG-INFO +117 -0
- blitzline-0.1.1/README.md +97 -0
- blitzline-0.1.1/blitzline/__init__.py +9 -0
- blitzline-0.1.1/blitzline/__main__.py +12 -0
- blitzline-0.1.1/blitzline/cli.py +324 -0
- blitzline-0.1.1/blitzline/config.py +492 -0
- blitzline-0.1.1/blitzline/core.py +672 -0
- blitzline-0.1.1/blitzline/exports/__init__.py +7 -0
- blitzline-0.1.1/blitzline/exports/anki.py +191 -0
- blitzline-0.1.1/blitzline/exports/common.py +96 -0
- blitzline-0.1.1/blitzline/exports/model.py +315 -0
- blitzline-0.1.1/blitzline/exports/package.py +110 -0
- blitzline-0.1.1/blitzline/exports/presets/blitzer.json +33 -0
- blitzline-0.1.1/blitzline/exports/tsv.py +107 -0
- blitzline-0.1.1/blitzline/integrations/__init__.py +7 -0
- blitzline-0.1.1/blitzline/integrations/asr.py +166 -0
- blitzline-0.1.1/blitzline/integrations/blitzer.py +142 -0
- blitzline-0.1.1/blitzline/integrations/llm.py +165 -0
- blitzline-0.1.1/blitzline/integrations/remote_asr.py +212 -0
- blitzline-0.1.1/blitzline/media.py +170 -0
- blitzline-0.1.1/blitzline/records.py +161 -0
- blitzline-0.1.1/blitzline/review.py +265 -0
- blitzline-0.1.1/blitzline/storage.py +189 -0
- blitzline-0.1.1/blitzline/transcript.py +269 -0
- blitzline-0.1.1/blitzline/vocabulary.py +296 -0
- blitzline-0.1.1/blitzline.egg-info/PKG-INFO +117 -0
- blitzline-0.1.1/blitzline.egg-info/SOURCES.txt +43 -0
- blitzline-0.1.1/blitzline.egg-info/dependency_links.txt +1 -0
- blitzline-0.1.1/blitzline.egg-info/entry_points.txt +2 -0
- blitzline-0.1.1/blitzline.egg-info/requires.txt +16 -0
- blitzline-0.1.1/blitzline.egg-info/top_level.txt +1 -0
- blitzline-0.1.1/pyproject.toml +50 -0
- blitzline-0.1.1/setup.cfg +4 -0
- blitzline-0.1.1/tests/test_adapters.py +170 -0
- blitzline-0.1.1/tests/test_blitzer.py +248 -0
- blitzline-0.1.1/tests/test_config_llm.py +185 -0
- blitzline-0.1.1/tests/test_deck_profiles.py +251 -0
- blitzline-0.1.1/tests/test_exports.py +187 -0
- blitzline-0.1.1/tests/test_note_models.py +335 -0
- blitzline-0.1.1/tests/test_pipeline.py +202 -0
- blitzline-0.1.1/tests/test_release.py +99 -0
- blitzline-0.1.1/tests/test_remote_asr.py +324 -0
- blitzline-0.1.1/tests/test_storage_cli.py +88 -0
- blitzline-0.1.1/tests/test_transcript.py +132 -0
- blitzline-0.1.1/tests/test_vocabulary.py +132 -0
blitzline-0.1.1/PKG-INFO
ADDED
|
@@ -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")
|