yt-tools-cli 0.24.2__tar.gz → 0.25.0__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.
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/.claude-plugin/plugin.json +1 -1
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/.github/workflows/ci.yml +32 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/CHANGELOG.md +30 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/PKG-INFO +17 -15
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/README.md +15 -13
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/pyproject.toml +12 -13
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/skills/using-yt-tools/SKILL.md +11 -9
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/tests/test_missing_extra.py +1 -1
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/tests/test_ocr.py +85 -3
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/tests/test_ocr_engine.py +19 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/tests/test_packaging.py +36 -27
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/yt_tools/extras.py +2 -2
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/yt_tools/ocr.py +73 -15
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/.github/workflows/release.yml +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/.gitignore +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/CONTRIBUTING.md +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/LICENSE +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/hooks/hooks.json +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/scripts/check-core-install.sh +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/scripts/check-metadata.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/scripts/check-pypi-name.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/scripts/ensure-install.ps1 +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/scripts/ensure-install.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/scripts/ensure-install.sh +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/scripts/publish-check.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/scripts/sync-version.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/tests/__init__.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/tests/test_cache.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/tests/test_check_metadata.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/tests/test_cli_smoke.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/tests/test_comments.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/tests/test_core.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/tests/test_distribution_name.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/tests/test_doctor.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/tests/test_failure_modes.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/tests/test_frames.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/tests/test_listen.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/tests/test_markdown.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/tests/test_meta.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/tests/test_parse_interval.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/tests/test_publish_check.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/tests/test_pypi_name.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/tests/test_search.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/tests/test_skill_portable.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/tests/test_transcript.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/tests/test_version.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/tests/test_watch.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/yt_tools/__init__.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/yt_tools/_metadata.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/yt_tools/cache.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/yt_tools/cli.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/yt_tools/comments.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/yt_tools/core.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/yt_tools/doctor.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/yt_tools/frames.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/yt_tools/listen.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/yt_tools/markdown.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/yt_tools/meta.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/yt_tools/search.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/yt_tools/transcript.py +0 -0
- {yt_tools_cli-0.24.2 → yt_tools_cli-0.25.0}/yt_tools/watch.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yt-tools",
|
|
3
3
|
"displayName": "yt-tools",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.25.0",
|
|
5
5
|
"description": "Iterative YouTube watching for agents: clean-markdown transcripts with [mm:ss] anchors, targeted frame extraction, FFT audio analysis (BPM, key, chord progression, spectral features), full-metadata extraction (description, chapters, most-replayed, counts, tags), comment scraping, and OCR over cached frames (RapidOCR PP-OCRv5) for silent-with-text videos. Light core (transcript / metadata / comments / search) with heavy stacks in [frames] and [audio] extras. Bundles the using-yt-tools skill and a SessionStart hook that auto-installs from the plugin's local clone via pipx (default [full] extras, fallback to [frames,audio]) and probes ffmpeg.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Victor Kuznetsov",
|
|
@@ -31,6 +31,38 @@ jobs:
|
|
|
31
31
|
- name: Unit + CLI smoke tests
|
|
32
32
|
run: pytest -q
|
|
33
33
|
|
|
34
|
+
ocr-engine-matrix:
|
|
35
|
+
name: ocr engine (rapidocr ${{ matrix.rapidocr }})
|
|
36
|
+
runs-on: ubuntu-latest
|
|
37
|
+
strategy:
|
|
38
|
+
# Both legs are the point: `[ocr]` is a pin on a third-party params API,
|
|
39
|
+
# and release 0.24.0/0.24.1 shipped broken because the *newer* minor was
|
|
40
|
+
# never exercised — the engine factory was mocked in every test, so the
|
|
41
|
+
# suite was green while a clean install could not construct the engine at
|
|
42
|
+
# all ([[issue:77]], [[task:2831]]). 0.25.0 names every model version and
|
|
43
|
+
# model type explicitly ([[task:2833]]); the verified minors live here and
|
|
44
|
+
# in the `[ocr]` bound, and tests/test_packaging.py asserts the two agree.
|
|
45
|
+
fail-fast: false
|
|
46
|
+
matrix:
|
|
47
|
+
rapidocr: ["3.8.4", "3.9.2"]
|
|
48
|
+
steps:
|
|
49
|
+
- uses: actions/checkout@v4
|
|
50
|
+
- uses: actions/setup-python@v5
|
|
51
|
+
with:
|
|
52
|
+
python-version: "3.12"
|
|
53
|
+
# `[ocr]` alone is enough: the engine tests need rapidocr, onnxruntime and
|
|
54
|
+
# cv2 (a rapidocr dependency) — nothing from [full] or [audio]. The second
|
|
55
|
+
# install pins the leg, since the extra's bound spans both.
|
|
56
|
+
- name: Install ([ocr] at ${{ matrix.rapidocr }})
|
|
57
|
+
run: |
|
|
58
|
+
python -m pip install -e ".[ocr,test]"
|
|
59
|
+
python -m pip install "rapidocr==${{ matrix.rapidocr }}"
|
|
60
|
+
# First construction downloads the ~8 MB model, so this leg needs network —
|
|
61
|
+
# the same prerequisite the `tests` job already carries for the model calls
|
|
62
|
+
# it makes.
|
|
63
|
+
- name: Engine tests
|
|
64
|
+
run: pytest -q tests/test_ocr_engine.py tests/test_ocr.py
|
|
65
|
+
|
|
34
66
|
build:
|
|
35
67
|
name: build (sdist + wheel, publishable metadata)
|
|
36
68
|
runs-on: ubuntu-latest
|
|
@@ -11,6 +11,36 @@ Releases before 0.8.0 are summarized only in the git log.
|
|
|
11
11
|
|
|
12
12
|
_(nothing yet)_
|
|
13
13
|
|
|
14
|
+
## [0.25.0] — 2026-09-13
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`yt-ocr` works on rapidocr 3.9** ([[task:2833]]). The 0.24.2 cap (`<3.9`) is
|
|
19
|
+
gone: `yt_tools/ocr.py` now names the model type alongside the model version
|
|
20
|
+
for Det/Cls/Rec, so the params dict means the same thing on every 3.x release
|
|
21
|
+
instead of inheriting a version-dependent default (3.8 → PP-OCRv4+`mobile`,
|
|
22
|
+
3.9 → PP-OCRv6+`small`, and PP-OCRv5 has no `small` — that inheritance was
|
|
23
|
+
[[issue:77]]). The bound is now `>=3.8,<4`: a major is where a params-API
|
|
24
|
+
change would land.
|
|
25
|
+
- **A CI matrix over the verified minors** (`ocr-engine-matrix`: rapidocr 3.8.4
|
|
26
|
+
and 3.9.2, engine tests only). The old pin asked users on the *newer* minor to
|
|
27
|
+
be the acceptance test; now both legs run on every push.
|
|
28
|
+
- **A per-language engine test.** Every `--language` value the CLI advertises is
|
|
29
|
+
built against the real model registry — the guard for the bug below.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **`--language ja` never worked.** PP-OCRv5 ships twelve recognizers and
|
|
34
|
+
Japanese is not one of them: asking for `PP-OCRv5 + japan` resolved to nothing,
|
|
35
|
+
and the engine raised `error: Invalid OCR configuration` — on 3.8.4 as much as
|
|
36
|
+
3.9.2, i.e. in every release that ever offered the flag. It used to "work" only
|
|
37
|
+
because the library's lenient fallback picked the PP-OCRv4 Japanese model, and
|
|
38
|
+
once the model type was pinned explicitly (above) that fallback was skipped.
|
|
39
|
+
Japanese now pins the v4 recognizer on purpose, and `ocr.md`'s header says so
|
|
40
|
+
instead of claiming PP-OCRv5: `RapidOCR (PP-OCRv5 det/cls + PP-OCRv4 ja rec)`.
|
|
41
|
+
Found by running the language matrix while adapting to 3.9 — the same mocked
|
|
42
|
+
seam that hid [[issue:77]].
|
|
43
|
+
|
|
14
44
|
## [0.24.2] — 2026-09-13
|
|
15
45
|
|
|
16
46
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: yt-tools-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.25.0
|
|
4
4
|
Summary: CLI suite for iterative YouTube watching by LLM agents: transcript with [mm:ss] anchors, targeted frame extraction, FFT audio analysis (BPM, key, chord progression, spectral features), and OCR over cached frames for silent-with-text videos.
|
|
5
5
|
Project-URL: Homepage, https://github.com/kzntsv-dev/yt-tools
|
|
6
6
|
Project-URL: Repository, https://github.com/kzntsv-dev/yt-tools
|
|
@@ -35,7 +35,7 @@ Requires-Dist: matplotlib>=3.8; extra == 'full'
|
|
|
35
35
|
Requires-Dist: scenedetect[opencv]>=0.6.4; extra == 'full'
|
|
36
36
|
Provides-Extra: ocr
|
|
37
37
|
Requires-Dist: onnxruntime>=1.18; extra == 'ocr'
|
|
38
|
-
Requires-Dist: rapidocr<
|
|
38
|
+
Requires-Dist: rapidocr<4,>=3.8; extra == 'ocr'
|
|
39
39
|
Provides-Extra: test
|
|
40
40
|
Requires-Dist: pytest>=7.0; extra == 'test'
|
|
41
41
|
Description-Content-Type: text/markdown
|
|
@@ -155,7 +155,7 @@ pipx install "yt-tools-cli[full,ocr]"
|
|
|
155
155
|
# redirections in bash, cmd.exe and PowerShell. `rapidocr` is capped below 3.9
|
|
156
156
|
# because 3.9 moved the bundled defaults to PP-OCRv6, where the PP-OCRv5 params
|
|
157
157
|
# yt-ocr builds no longer resolve (see the OCR section).
|
|
158
|
-
pipx inject yt-tools-cli "rapidocr>=3.8,<
|
|
158
|
+
pipx inject yt-tools-cli "rapidocr>=3.8,<4" "onnxruntime>=1.18"
|
|
159
159
|
```
|
|
160
160
|
|
|
161
161
|
`uv` users: `uv tool install "yt-tools-cli[full]"`, or for a one-off run
|
|
@@ -192,7 +192,7 @@ matplotlib. Heavier stacks are opt-in, per flow.
|
|
|
192
192
|
| core (no extras) | `youtube-transcript-api`, `yt-dlp` | `yt-transcript`, `yt-meta`, `yt-comments`, `yt-search`, `yt-tools cache`; `yt-frames --timestamps` / `--mode interval` (without near-duplicate dedup) |
|
|
193
193
|
| `[frames]` | `scenedetect[opencv]` (OpenCV) | `yt-frames --mode scene`, `yt-watch`, and dedup in the other frame modes |
|
|
194
194
|
| `[audio]` | `librosa`, `matplotlib` | `yt-listen` (spectral features, spectrogram, BPM/key) |
|
|
195
|
-
| `[ocr]` | `rapidocr>=3.8,<
|
|
195
|
+
| `[ocr]` | `rapidocr>=3.8,<4`, `onnxruntime` | `yt-ocr` (PP-OCRv5; ONNX model downloaded on first run) |
|
|
196
196
|
| `[full]` | core + `[frames]` + `[audio]` | the plugin default: every flow above **except `yt-ocr`** |
|
|
197
197
|
|
|
198
198
|
`[full]` is a *flow* default, not "everything". Two things stay outside it on
|
|
@@ -462,8 +462,9 @@ When `yt-transcript` returns 0 bytes (or near-nothing) and the content
|
|
|
462
462
|
lives entirely in burned-in overlay text — tutorial channels with
|
|
463
463
|
schematic labels, chord matrices over dimmed B-roll, parameter
|
|
464
464
|
walkthroughs without voice-over — fall back to OCR over cached frames.
|
|
465
|
-
Engine is [RapidOCR](https://github.com/RapidAI/RapidOCR) (PP-OCRv5
|
|
466
|
-
|
|
465
|
+
Engine is [RapidOCR](https://github.com/RapidAI/RapidOCR) (PP-OCRv5 models via
|
|
466
|
+
`onnxruntime`; Japanese is read by the PP-OCRv4 recognizer, the only one that
|
|
467
|
+
ships for it — the header line of `ocr.md` names whichever ran).
|
|
467
468
|
|
|
468
469
|
```bash
|
|
469
470
|
# 1) extract frames first (lower scene threshold catches overlay fades
|
|
@@ -487,7 +488,7 @@ yt-ocr URL --timestamps 1:30,2:45,5:10
|
|
|
487
488
|
|
|
488
489
|
# Non-English overlays:
|
|
489
490
|
yt-ocr URL --language ru # Cyrillic
|
|
490
|
-
yt-ocr URL --language ja # Japanese
|
|
491
|
+
yt-ocr URL --language ja # Japanese (PP-OCRv4 recognizer — no v5 model exists)
|
|
491
492
|
yt-ocr URL --language zh # Chinese (simplified)
|
|
492
493
|
yt-ocr URL --language multi # PP-OCR multilingual (Chinese+English)
|
|
493
494
|
```
|
|
@@ -501,7 +502,7 @@ was checked vs. silently omitted).
|
|
|
501
502
|
> core install — and **not** in `[full]` either. If `yt-ocr` exits with the
|
|
502
503
|
> missing-extra hint, run:
|
|
503
504
|
> ```bash
|
|
504
|
-
> pipx inject yt-tools-cli "rapidocr>=3.8,<
|
|
505
|
+
> pipx inject yt-tools-cli "rapidocr>=3.8,<4" "onnxruntime>=1.18"
|
|
505
506
|
> # or for non-pipx setups:
|
|
506
507
|
> pip install 'yt-tools-cli[ocr]'
|
|
507
508
|
> ```
|
|
@@ -510,13 +511,14 @@ was checked vs. silently omitted).
|
|
|
510
511
|
> they land in the installed package's own `rapidocr/models/` directory, not in
|
|
511
512
|
> `~/.cache/` — verified on a clean venv.
|
|
512
513
|
>
|
|
513
|
-
> The `rapidocr` bound is
|
|
514
|
-
>
|
|
515
|
-
>
|
|
516
|
-
>
|
|
517
|
-
>
|
|
518
|
-
>
|
|
519
|
-
>
|
|
514
|
+
> The `rapidocr` bound is `>=3.8,<4`: the CLI names every model version *and*
|
|
515
|
+
> model type explicitly, so the config means the same on each 3.x release — but a
|
|
516
|
+
> major could change the params API, which is what bit 0.24.x (3.9 defaulted to
|
|
517
|
+
> PP-OCRv6 + `small`, where the PP-OCRv5 detector the CLI asks for does not
|
|
518
|
+
> exist). Both verified minors (3.8.4, 3.9.2) are exercised by the engine test in
|
|
519
|
+
> CI. The `[ocr]` extra carries the bound, and so does the `pipx inject` line
|
|
520
|
+
> above: an inject resolves from PyPI directly and never inherits the extra's
|
|
521
|
+
> metadata.
|
|
520
522
|
|
|
521
523
|
### Cache hygiene
|
|
522
524
|
|
|
@@ -113,7 +113,7 @@ pipx install "yt-tools-cli[full,ocr]"
|
|
|
113
113
|
# redirections in bash, cmd.exe and PowerShell. `rapidocr` is capped below 3.9
|
|
114
114
|
# because 3.9 moved the bundled defaults to PP-OCRv6, where the PP-OCRv5 params
|
|
115
115
|
# yt-ocr builds no longer resolve (see the OCR section).
|
|
116
|
-
pipx inject yt-tools-cli "rapidocr>=3.8,<
|
|
116
|
+
pipx inject yt-tools-cli "rapidocr>=3.8,<4" "onnxruntime>=1.18"
|
|
117
117
|
```
|
|
118
118
|
|
|
119
119
|
`uv` users: `uv tool install "yt-tools-cli[full]"`, or for a one-off run
|
|
@@ -150,7 +150,7 @@ matplotlib. Heavier stacks are opt-in, per flow.
|
|
|
150
150
|
| core (no extras) | `youtube-transcript-api`, `yt-dlp` | `yt-transcript`, `yt-meta`, `yt-comments`, `yt-search`, `yt-tools cache`; `yt-frames --timestamps` / `--mode interval` (without near-duplicate dedup) |
|
|
151
151
|
| `[frames]` | `scenedetect[opencv]` (OpenCV) | `yt-frames --mode scene`, `yt-watch`, and dedup in the other frame modes |
|
|
152
152
|
| `[audio]` | `librosa`, `matplotlib` | `yt-listen` (spectral features, spectrogram, BPM/key) |
|
|
153
|
-
| `[ocr]` | `rapidocr>=3.8,<
|
|
153
|
+
| `[ocr]` | `rapidocr>=3.8,<4`, `onnxruntime` | `yt-ocr` (PP-OCRv5; ONNX model downloaded on first run) |
|
|
154
154
|
| `[full]` | core + `[frames]` + `[audio]` | the plugin default: every flow above **except `yt-ocr`** |
|
|
155
155
|
|
|
156
156
|
`[full]` is a *flow* default, not "everything". Two things stay outside it on
|
|
@@ -420,8 +420,9 @@ When `yt-transcript` returns 0 bytes (or near-nothing) and the content
|
|
|
420
420
|
lives entirely in burned-in overlay text — tutorial channels with
|
|
421
421
|
schematic labels, chord matrices over dimmed B-roll, parameter
|
|
422
422
|
walkthroughs without voice-over — fall back to OCR over cached frames.
|
|
423
|
-
Engine is [RapidOCR](https://github.com/RapidAI/RapidOCR) (PP-OCRv5
|
|
424
|
-
|
|
423
|
+
Engine is [RapidOCR](https://github.com/RapidAI/RapidOCR) (PP-OCRv5 models via
|
|
424
|
+
`onnxruntime`; Japanese is read by the PP-OCRv4 recognizer, the only one that
|
|
425
|
+
ships for it — the header line of `ocr.md` names whichever ran).
|
|
425
426
|
|
|
426
427
|
```bash
|
|
427
428
|
# 1) extract frames first (lower scene threshold catches overlay fades
|
|
@@ -445,7 +446,7 @@ yt-ocr URL --timestamps 1:30,2:45,5:10
|
|
|
445
446
|
|
|
446
447
|
# Non-English overlays:
|
|
447
448
|
yt-ocr URL --language ru # Cyrillic
|
|
448
|
-
yt-ocr URL --language ja # Japanese
|
|
449
|
+
yt-ocr URL --language ja # Japanese (PP-OCRv4 recognizer — no v5 model exists)
|
|
449
450
|
yt-ocr URL --language zh # Chinese (simplified)
|
|
450
451
|
yt-ocr URL --language multi # PP-OCR multilingual (Chinese+English)
|
|
451
452
|
```
|
|
@@ -459,7 +460,7 @@ was checked vs. silently omitted).
|
|
|
459
460
|
> core install — and **not** in `[full]` either. If `yt-ocr` exits with the
|
|
460
461
|
> missing-extra hint, run:
|
|
461
462
|
> ```bash
|
|
462
|
-
> pipx inject yt-tools-cli "rapidocr>=3.8,<
|
|
463
|
+
> pipx inject yt-tools-cli "rapidocr>=3.8,<4" "onnxruntime>=1.18"
|
|
463
464
|
> # or for non-pipx setups:
|
|
464
465
|
> pip install 'yt-tools-cli[ocr]'
|
|
465
466
|
> ```
|
|
@@ -468,13 +469,14 @@ was checked vs. silently omitted).
|
|
|
468
469
|
> they land in the installed package's own `rapidocr/models/` directory, not in
|
|
469
470
|
> `~/.cache/` — verified on a clean venv.
|
|
470
471
|
>
|
|
471
|
-
> The `rapidocr` bound is
|
|
472
|
-
>
|
|
473
|
-
>
|
|
474
|
-
>
|
|
475
|
-
>
|
|
476
|
-
>
|
|
477
|
-
>
|
|
472
|
+
> The `rapidocr` bound is `>=3.8,<4`: the CLI names every model version *and*
|
|
473
|
+
> model type explicitly, so the config means the same on each 3.x release — but a
|
|
474
|
+
> major could change the params API, which is what bit 0.24.x (3.9 defaulted to
|
|
475
|
+
> PP-OCRv6 + `small`, where the PP-OCRv5 detector the CLI asks for does not
|
|
476
|
+
> exist). Both verified minors (3.8.4, 3.9.2) are exercised by the engine test in
|
|
477
|
+
> CI. The `[ocr]` extra carries the bound, and so does the `pipx inject` line
|
|
478
|
+
> above: an inject resolves from PyPI directly and never inherits the extra's
|
|
479
|
+
> metadata.
|
|
478
480
|
|
|
479
481
|
### Cache hygiene
|
|
480
482
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "yt-tools-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.25.0"
|
|
8
8
|
description = "CLI suite for iterative YouTube watching by LLM agents: transcript with [mm:ss] anchors, targeted frame extraction, FFT audio analysis (BPM, key, chord progression, spectral features), and OCR over cached frames for silent-with-text videos."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10,<3.13"
|
|
@@ -79,18 +79,17 @@ full = [
|
|
|
79
79
|
"matplotlib>=3.8",
|
|
80
80
|
]
|
|
81
81
|
ocr = [
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
# and
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
|
|
93
|
-
"rapidocr>=3.8,<3.9",
|
|
82
|
+
# Pinned to the 3.x params API ([[task:2833]]). Within it, `ocr.py` names
|
|
83
|
+
# *everything* explicitly — `Det/Cls/Rec.ocr_version` and the matching
|
|
84
|
+
# `model_type`, plus the recognizer version per language — so the config no
|
|
85
|
+
# longer depends on whatever defaults the installed release ships. That was
|
|
86
|
+
# the 0.24.2 bug ([[issue:77]]): 3.8 defaulted to PP-OCRv4+mobile, 3.9 moved
|
|
87
|
+
# Det/Rec to PP-OCRv6+small, and PP-OCRv5 has no `small`, so a version-only
|
|
88
|
+
# pin asked for a model that does not exist and construction raised
|
|
89
|
+
# `error: Invalid OCR configuration`. Verified against 3.8.4 and 3.9.2 for
|
|
90
|
+
# every `--language` (CI matrix: .github/workflows/ci.yml); the major bound
|
|
91
|
+
# is what a params-API change would break, so it stays.
|
|
92
|
+
"rapidocr>=3.8,<4",
|
|
94
93
|
"onnxruntime>=1.18",
|
|
95
94
|
]
|
|
96
95
|
test = ["pytest>=7.0"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: using-yt-tools
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.25.0
|
|
4
4
|
description: Seven YouTube flows, one skill. Discovery `yt-search` ("find a video about X", «найди видео про X», «поищи туториал»). Summary / exploration `yt-transcript` — [mm:ss] anchors, then pick moments and run `yt-frames` ("what's in this video", «о чём видео»). Targeted frames directly by timestamp ("show frame at N", «покажи кадр на N»). Audio FFT `yt-listen` — BPM, key, chords, structure ("what's the BPM", «спектрограмма», «тональность видео»). Metadata `yt-meta` — description, chapters, most-replayed ("video description", «что в описании», «покажи главы»). Comments `yt-comments` ("top comments", «комменты под роликом»). OCR over cached frames `yt-ocr` — silent-with-text fallback ("read text from frames", "captions burned in", «прочти текст с кадров»). Any youtube.com URL. Install once per machine with pipx (`pipx install 'yt-tools-cli[full]'`); `yt-tools doctor` is the read-only preflight that names what is missing. YouTube-only — Vimeo / Twitch / local files need other tools.
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -108,7 +108,7 @@ Seven distinct flows, picked by user intent:
|
|
|
108
108
|
interesting timestamps if you need the original frames.
|
|
109
109
|
- **Requires the `[ocr]` extra** — `rapidocr` + `onnxruntime`, **not** part of
|
|
110
110
|
`[full]`. If missing on the running yt-tools install, `yt-ocr` exits nonzero
|
|
111
|
-
with both install hints (`pipx inject yt-tools-cli "rapidocr>=3.8,<
|
|
111
|
+
with both install hints (`pipx inject yt-tools-cli "rapidocr>=3.8,<4"
|
|
112
112
|
"onnxruntime>=1.18"` / `pip install 'yt-tools-cli[ocr]'`). The plugin's
|
|
113
113
|
SessionStart hook installs `[full]` (core + `[frames]` + `[audio]`, plus a
|
|
114
114
|
best-effort `bpm-detector` inject) only, **not** `[ocr]` — first run of Flow F
|
|
@@ -152,10 +152,10 @@ pipx install "yt-tools-cli[full]==0.24.2"
|
|
|
152
152
|
# OCR (explicit extra, ~10 MB model fetched on first run). Naming it in the
|
|
153
153
|
# install lets pip resolve it from the metadata, bounds included:
|
|
154
154
|
pipx install "yt-tools-cli[full,ocr]"
|
|
155
|
-
# already installed? widen in place —
|
|
156
|
-
#
|
|
157
|
-
#
|
|
158
|
-
pipx inject yt-tools-cli "rapidocr>=3.8,<
|
|
155
|
+
# already installed? widen in place — the bound is `>=3.8,<4` (a major is where a
|
|
156
|
+
# params-API change would land; quote the specs — `>`/`<` are redirections in
|
|
157
|
+
# bash, cmd.exe and PowerShell):
|
|
158
|
+
pipx inject yt-tools-cli "rapidocr>=3.8,<4" "onnxruntime>=1.18"
|
|
159
159
|
```
|
|
160
160
|
|
|
161
161
|
From a checkout instead of PyPI (the plugin path, or tracking `master`):
|
|
@@ -305,7 +305,7 @@ prepend is needed — invoke normally.
|
|
|
305
305
|
| C — audio-analysis | YouTube URL + timestamps (same formats as B) | `--duration 30s` (default 30s — the lower bound for beat-tracking); `--mode interval --interval 60s` (bulk sampling); `--no-wav` / `--no-spectrogram` (both default ON); `--linear` (STFT instead of mel); `--chroma` (bonus chromagram PNG); `--sample-rate 22050`; `--no-cache-source`; `--out DIR` |
|
|
306
306
|
| D — metadata | YouTube URL or bare 11-char video id | `--out PATH` |
|
|
307
307
|
| E — comments | YouTube URL or bare 11-char video id | `--max N` (default 50; higher = slower); `--sort {top,new}` (default top); `--out PATH` |
|
|
308
|
-
| F — OCR | YouTube URL or bare 11-char video id (frames in cache, or pass `--timestamps`) | `--timestamps T1,T2,…` (extract via internal yt-frames then OCR); `--language {en,ru,ja,zh,multi}` (default `en`); `--out PATH` |
|
|
308
|
+
| F — OCR | YouTube URL or bare 11-char video id (frames in cache, or pass `--timestamps`) | `--timestamps T1,T2,…` (extract via internal yt-frames then OCR); `--language {en,ru,ja,zh,multi}` (default `en`; `ja` is read by the PP-OCRv4 recognizer — no PP-OCRv5 Japanese model exists); `--out PATH` |
|
|
309
309
|
|
|
310
310
|
**Frame budget.** Auto-selected frames (`--mode scene`, `--mode interval`, and
|
|
311
311
|
`yt-watch`) are capped at **100 per call** and evenly thinned with the first and
|
|
@@ -532,7 +532,7 @@ All failures abort cleanly; never leave a half-finished state.
|
|
|
532
532
|
| `Subtitles disabled` / `no captions in the requested language(s)` / `the transcript API returned nothing usable (ParseError: …)` from `yt-transcript` | Captions are absent (channel-disabled, wrong `--lang`, or YouTube answered with an empty body — the last one used to surface as a bare `no element found: line 1, column 0`). The message names the reason and both fallbacks. | Fatal for Flow A — tell the user the reason, and switch: Flow C (`yt-listen`) for audio, Flow F (`yt-ocr`) for text burned into the frames. Try another `--lang` first only when the reason says the *requested language* is missing. `yt-watch` on the same video does **not** fail — it writes a frames-only `watch.md` with the reason in its header, so read the frames and say that the text layer is absent. |
|
|
533
533
|
| Flow B/D — `yt-frames --mode scene requires the [frames] extra` / `yt-watch requires the [frames] extra` | `scenedetect` / `opencv-python` not in the active pipx venv | Exit code 1, nothing was downloaded. Tell the user the command the CLI printed (`pipx inject yt-tools-cli scenedetect opencv-python` or `pip install 'yt-tools-cli[frames]'`). Fallbacks that need no install: Flow D with an explicit `--timestamps` list, or `--mode interval` (which still runs and announces on stderr that near-duplicate dedup was skipped). |
|
|
534
534
|
| Flow C — `yt-listen requires the [audio] extra` | `librosa` / `matplotlib` not in the active pipx venv | Exit code 1. Same shape: print the command the CLI named. Until it is installed, Flow A (transcript) and Flow F (`yt-ocr`) are the alternatives for this video. |
|
|
535
|
-
| Flow F — `yt-ocr requires the [ocr] extra` | `rapidocr` / `onnxruntime` not in the active pipx venv | Run the inject command the CLI printed: `pipx inject yt-tools-cli "rapidocr>=3.8,<
|
|
535
|
+
| Flow F — `yt-ocr requires the [ocr] extra` | `rapidocr` / `onnxruntime` not in the active pipx venv | Run the inject command the CLI printed: `pipx inject yt-tools-cli "rapidocr>=3.8,<4" "onnxruntime>=1.18"` (or `pip install 'yt-tools-cli[ocr]'` in non-pipx setups). The bound is load-bearing: an inject resolves from PyPI directly and never inherits the extra's metadata (`>`/`<` need the quotes). The plugin's SessionStart hook installs `[full]`, not `[ocr]`, so first run typically needs this. |
|
|
536
536
|
| Flow F — `no cached frames in …/frames` | Batch-on-cache mode but `yt-frames` wasn't run yet (or wrote elsewhere) | Either run `yt-frames URL --mode scene --scene-threshold 12` (or `--mode interval --interval 15s`) first, or re-invoke `yt-ocr URL --timestamps T1,T2,…` to let it do extraction inline. |
|
|
537
537
|
| Flow F — RapidOCR model download fails (offline / firewall) | First-run lazy-download of PP-OCRv5 ONNX weights blocked | Print the captured error; let the user re-run with network access. Don't retry. |
|
|
538
538
|
| User passed a non-YouTube URL (Vimeo / Twitch / local mp4) | Out of scope | Stop; say the skill is YouTube-only. |
|
|
@@ -647,7 +647,9 @@ instead of printing a traceback.
|
|
|
647
647
|
`zh` / `multi`. If the user doesn't name a language, default to `en`;
|
|
648
648
|
if they say "русский / Japanese / Chinese", pass the matching code
|
|
649
649
|
explicitly. (`multi` is the PP-OCR multilingual model — slower, wider
|
|
650
|
-
CJK + Latin coverage; use it when the video mixes scripts.
|
|
650
|
+
CJK + Latin coverage; use it when the video mixes scripts. `en`/`ru`/
|
|
651
|
+
`zh`/`multi` read with a PP-OCRv5 recognizer and `ja` with the v4 one —
|
|
652
|
+
the header line of `ocr.md` names whichever ran.)
|
|
651
653
|
- **Don't try to OCR arbitrary `.jpg` files with `yt-ocr`.** This skill
|
|
652
654
|
and CLI live in the YouTube domain: `yt-ocr` only accepts a YouTube
|
|
653
655
|
URL / video-id and reads frames from `./yt-cache/<vid>/frames/`. For
|
|
@@ -82,7 +82,7 @@ def test_audio_refusal_wording_is_exact():
|
|
|
82
82
|
def test_ocr_refusal_wording_is_exact():
|
|
83
83
|
assert extras.format_missing_extra("yt-ocr", "ocr") == (
|
|
84
84
|
"yt-ocr requires the [ocr] extra:\n"
|
|
85
|
-
' pipx inject yt-tools-cli "rapidocr>=3.8,<
|
|
85
|
+
' pipx inject yt-tools-cli "rapidocr>=3.8,<4" "onnxruntime>=1.18"\n'
|
|
86
86
|
" # or\n"
|
|
87
87
|
" pip install 'yt-tools-cli[ocr]'"
|
|
88
88
|
)
|
|
@@ -88,6 +88,11 @@ class _FakeLangRec:
|
|
|
88
88
|
|
|
89
89
|
class _FakeOCRVersion:
|
|
90
90
|
PPOCRV5 = "ocrver::v5"
|
|
91
|
+
PPOCRV4 = "ocrver::v4"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class _FakeModelType:
|
|
95
|
+
MOBILE = "model::mobile"
|
|
91
96
|
|
|
92
97
|
|
|
93
98
|
def test_build_params_en_maps_to_LangRec_EN():
|
|
@@ -133,6 +138,68 @@ def test_build_params_multi_still_forces_v5():
|
|
|
133
138
|
assert "Rec.lang_type" not in p
|
|
134
139
|
|
|
135
140
|
|
|
141
|
+
# task:2833 — the model_type half of the PP-OCRv5 pin. Pinning only the version
|
|
142
|
+
# left model_type at rapidocr's default, which is a *version-dependent* value:
|
|
143
|
+
# 3.8 defaults every component to PP-OCRv4 + "mobile" (valid for v5), 3.9 moved
|
|
144
|
+
# Det/Rec to PP-OCRv6 + "small" — and PP-OCRv5 ships mobile/server only, so
|
|
145
|
+
# "PP-OCRv5 … small" resolves to nothing and construction raises
|
|
146
|
+
# "error: Invalid OCR configuration" ([[issue:77]]). Naming the model type makes
|
|
147
|
+
# the config independent of whatever default the installed release ships.
|
|
148
|
+
def test_build_params_pins_the_model_type_alongside_the_version():
|
|
149
|
+
p = _build_params("en", _FakeLangRec, _FakeOCRVersion, _FakeModelType)
|
|
150
|
+
assert p["Det.model_type"] == "model::mobile"
|
|
151
|
+
assert p["Cls.model_type"] == "model::mobile"
|
|
152
|
+
assert p["Rec.model_type"] == "model::mobile"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_build_params_multi_also_pins_the_model_type():
|
|
156
|
+
p = _build_params("multi", _FakeLangRec, _FakeOCRVersion, _FakeModelType)
|
|
157
|
+
assert p["Rec.model_type"] == "model::mobile"
|
|
158
|
+
assert "Rec.lang_type" not in p
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_build_params_without_a_modeltype_enum_stays_silent_about_it():
|
|
162
|
+
"""The version/model pins travel together: no enum, no keys.
|
|
163
|
+
|
|
164
|
+
``_build_params`` is called with the rapidocr enums only from
|
|
165
|
+
``_load_engine``; the older signature (no ``ModelType``) must not
|
|
166
|
+
silently emit a half pin — a config with ``model_type`` but no
|
|
167
|
+
``ocr_version`` (or the reverse) is exactly the drift this pins against.
|
|
168
|
+
"""
|
|
169
|
+
p = _build_params("en", _FakeLangRec, _FakeOCRVersion)
|
|
170
|
+
assert not [k for k in p if "model_type" in k] or p["Det.ocr_version"] == "ocrver::v5"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# task:2833 — PP-OCRv5 ships twelve recognizers and Japanese is not one of them
|
|
174
|
+
# (its registry entry is v4-only, identically in 3.8.x and 3.9.x). Asking for
|
|
175
|
+
# v5 + japan resolves to nothing; before this pin the v4 model was found by the
|
|
176
|
+
# library's lenient fallback, and once model_type is named — as it must be, so
|
|
177
|
+
# the config stops depending on the installed release's defaults — that fallback
|
|
178
|
+
# is skipped and construction raises. Pinning the recognizer per language makes
|
|
179
|
+
# `--language ja` work by design rather than by luck.
|
|
180
|
+
def test_build_params_ja_uses_the_v4_recognizer():
|
|
181
|
+
p = _build_params("ja", _FakeLangRec, _FakeOCRVersion, _FakeModelType)
|
|
182
|
+
assert p["Rec.lang_type"] == "lang::japan"
|
|
183
|
+
assert p["Rec.ocr_version"] == "ocrver::v4"
|
|
184
|
+
# ...while detection and classification stay on v5: they are
|
|
185
|
+
# language-agnostic (one `ch` model each) in the v5 family.
|
|
186
|
+
assert p["Det.ocr_version"] == "ocrver::v5"
|
|
187
|
+
assert p["Cls.ocr_version"] == "ocrver::v5"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def test_build_params_languages_with_a_v5_recognizer_stay_on_v5():
|
|
191
|
+
for lang in ("en", "ru", "zh"):
|
|
192
|
+
p = _build_params(lang, _FakeLangRec, _FakeOCRVersion, _FakeModelType)
|
|
193
|
+
assert p["Rec.ocr_version"] == "ocrver::v5", lang
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_build_params_multi_keeps_the_v5_ch_recognizer():
|
|
197
|
+
"""`multi` means RapidOCR's default recognizer — which for v5 is `ch`."""
|
|
198
|
+
p = _build_params("multi", _FakeLangRec, _FakeOCRVersion, _FakeModelType)
|
|
199
|
+
assert p["Rec.ocr_version"] == "ocrver::v5"
|
|
200
|
+
assert "Rec.lang_type" not in p
|
|
201
|
+
|
|
202
|
+
|
|
136
203
|
# --- ocr_to_markdown ---------------------------------------------------------
|
|
137
204
|
|
|
138
205
|
|
|
@@ -180,6 +247,21 @@ def test_render_zero_frames_still_emits_header():
|
|
|
180
247
|
assert "frames: 0" in md
|
|
181
248
|
|
|
182
249
|
|
|
250
|
+
# task:2833 — the header line is the only trace of *what* read the frames, so it
|
|
251
|
+
# must not claim PP-OCRv5 for a language that has no v5 recognizer (Japanese is
|
|
252
|
+
# read by the v4 recognizer, see _LANGUAGE_REC_VERSION).
|
|
253
|
+
def test_render_names_the_v4_recognizer_for_japanese():
|
|
254
|
+
md = ocr_to_markdown(_VID, [(23, ["こんにちは"])], language="ja")
|
|
255
|
+
assert "PP-OCRv4" in md, "the ja header must name the recognizer that actually ran"
|
|
256
|
+
assert "language: ja" in md
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def test_render_keeps_the_plain_v5_label_for_languages_that_have_one():
|
|
260
|
+
md = ocr_to_markdown(_VID, [(23, ["hello"])], language="en")
|
|
261
|
+
assert "engine: RapidOCR (PP-OCRv5)" in md
|
|
262
|
+
assert "PP-OCRv4" not in md
|
|
263
|
+
|
|
264
|
+
|
|
183
265
|
# --- _load_engine: friendly missing-extra hint -------------------------------
|
|
184
266
|
|
|
185
267
|
|
|
@@ -195,7 +277,7 @@ def test_load_engine_friendly_hint_when_rapidocr_missing(monkeypatch):
|
|
|
195
277
|
with pytest.raises(OcrError) as exc_info:
|
|
196
278
|
ocr_mod._load_engine("en")
|
|
197
279
|
msg = str(exc_info.value)
|
|
198
|
-
assert 'pipx inject yt-tools-cli "rapidocr>=3.8,<
|
|
280
|
+
assert 'pipx inject yt-tools-cli "rapidocr>=3.8,<4" "onnxruntime>=1.18"' in msg
|
|
199
281
|
assert "pip install 'yt-tools-cli[ocr]'" in msg
|
|
200
282
|
|
|
201
283
|
|
|
@@ -294,7 +376,7 @@ def test_cli_missing_extra_prints_friendly_hint(tmp_path, monkeypatch, capsys):
|
|
|
294
376
|
def _raise(_lang):
|
|
295
377
|
raise OcrError(
|
|
296
378
|
"yt-ocr requires the [ocr] extra:\n"
|
|
297
|
-
' pipx inject yt-tools-cli "rapidocr>=3.8,<
|
|
379
|
+
' pipx inject yt-tools-cli "rapidocr>=3.8,<4" "onnxruntime>=1.18"\n'
|
|
298
380
|
" # or\n"
|
|
299
381
|
" pip install 'yt-tools-cli[ocr]'"
|
|
300
382
|
)
|
|
@@ -304,7 +386,7 @@ def test_cli_missing_extra_prints_friendly_hint(tmp_path, monkeypatch, capsys):
|
|
|
304
386
|
|
|
305
387
|
assert rc == 1
|
|
306
388
|
err = capsys.readouterr().err
|
|
307
|
-
assert 'pipx inject yt-tools-cli "rapidocr>=3.8,<
|
|
389
|
+
assert 'pipx inject yt-tools-cli "rapidocr>=3.8,<4" "onnxruntime>=1.18"' in err
|
|
308
390
|
assert "pip install 'yt-tools-cli[ocr]'" in err
|
|
309
391
|
|
|
310
392
|
|
|
@@ -85,6 +85,25 @@ def engine():
|
|
|
85
85
|
return ocr_mod._load_engine("en")
|
|
86
86
|
|
|
87
87
|
|
|
88
|
+
@pytest.mark.parametrize("language", ocr_mod.SUPPORTED_LANGUAGES)
|
|
89
|
+
def test_every_supported_language_constructs_an_engine(language):
|
|
90
|
+
"""Every `--language` value must resolve to a model that exists (task:2833).
|
|
91
|
+
|
|
92
|
+
Language → model is not a free mapping: the recognizer is per language, and
|
|
93
|
+
PP-OCRv5 ships twelve of them with **no Japanese** among them — `--language
|
|
94
|
+
ja` asked for a model that does not exist and died at construction, on 3.8.4
|
|
95
|
+
as well as 3.9.2, while every test here (and the whole suite) stayed green
|
|
96
|
+
because only `en` was ever built. This is the guard for that class: it
|
|
97
|
+
exercises the real registry for every value the CLI advertises.
|
|
98
|
+
|
|
99
|
+
Construction, not OCR: what breaks is model resolution, and a Japanese
|
|
100
|
+
recognizer reading Latin text proves nothing. It does download a recognizer
|
|
101
|
+
per language (~8-16 MB, once per runner) — the same lazy fetch the first real
|
|
102
|
+
run pays, which is why this module already needs the network.
|
|
103
|
+
"""
|
|
104
|
+
assert ocr_mod._load_engine(language) is not None
|
|
105
|
+
|
|
106
|
+
|
|
88
107
|
def test_load_engine_constructs_the_declared_params(engine):
|
|
89
108
|
"""The 3.9.x failure mode: RapidOCR refuses the 3.8.x-shaped ``params`` dict."""
|
|
90
109
|
assert engine is not None
|
|
@@ -214,31 +214,32 @@ def test_ocr_extra_is_unchanged():
|
|
|
214
214
|
assert ocr == {"rapidocr", "onnxruntime"}
|
|
215
215
|
|
|
216
216
|
|
|
217
|
-
def
|
|
218
|
-
"""
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
where nothing local would notice.
|
|
217
|
+
def test_ocr_extra_keeps_the_rapidocr_3x_params_bound():
|
|
218
|
+
"""The bound encodes a params-API boundary, not a version grudge (task:2833).
|
|
219
|
+
|
|
220
|
+
0.24.2 capped `rapidocr<3.9`: `ocr.py` pinned `ocr_version=PP-OCRv5` but left
|
|
221
|
+
`model_type` at the library default, and 3.9 moved that default to
|
|
222
|
+
`PP-OCRv6 + small` where the v5 family has no `small` — so a clean install
|
|
223
|
+
died with `error: Invalid OCR configuration` ([[issue:77]]). The fix names
|
|
224
|
+
every component's version *and* model type, which makes the config
|
|
225
|
+
release-independent and lets 3.9.x back in; the remaining cap is the major
|
|
226
|
+
version, because that is the boundary a params-API change would cross.
|
|
227
|
+
|
|
228
|
+
Both halves are cheap to assert and hard to notice by hand: the verified
|
|
229
|
+
minors must stay installable (CI installs exactly them — see the matrix job)
|
|
230
|
+
and the next major must not slide in unannounced.
|
|
232
231
|
"""
|
|
233
232
|
specs = _pyproject()["project"]["optional-dependencies"]["ocr"]
|
|
234
233
|
req = Requirement(next(spec for spec in specs if _dist_name(spec) == "rapidocr"))
|
|
235
234
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
235
|
+
for verified in ("3.8.4", "3.9.2"):
|
|
236
|
+
assert req.specifier.contains(verified), (
|
|
237
|
+
f"rapidocr {verified} is verified by tests/test_ocr_engine.py in CI and "
|
|
238
|
+
"must stay installable"
|
|
239
|
+
)
|
|
240
|
+
assert not req.specifier.contains("4.0.0"), (
|
|
241
|
+
"a rapidocr major could change the params API again — widen only with a "
|
|
242
|
+
"green engine test against that version"
|
|
242
243
|
)
|
|
243
244
|
|
|
244
245
|
|
|
@@ -246,11 +247,12 @@ def test_ocr_inject_hint_carries_the_same_bound_as_the_extra():
|
|
|
246
247
|
"""Two install routes, one bound — the `pipx inject` hint has no metadata to inherit.
|
|
247
248
|
|
|
248
249
|
``yt-ocr`` refusing with `pipx inject yt-tools-cli rapidocr onnxruntime` told
|
|
249
|
-
the user to install
|
|
250
|
-
|
|
251
|
-
[[issue:77]] on a machine that followed it to
|
|
252
|
-
``next_step`` and the README print the same line, so
|
|
253
|
-
constant all three read from, not on one caller's
|
|
250
|
+
the user to install the pair by name — an inject resolves from PyPI directly,
|
|
251
|
+
so the refusal message never inherited the extra's bound and, in the 0.24.x
|
|
252
|
+
window, would have reproduced [[issue:77]] on a machine that followed it to
|
|
253
|
+
the letter. ``doctor``'s ``next_step`` and the README print the same line, so
|
|
254
|
+
the assertion is on the constant all three read from, not on one caller's
|
|
255
|
+
wording.
|
|
254
256
|
"""
|
|
255
257
|
from yt_tools import extras # noqa: PLC0415 — local import keeps the packaging tests import-light
|
|
256
258
|
|
|
@@ -268,7 +270,7 @@ def test_ocr_inject_hint_carries_the_same_bound_as_the_extra():
|
|
|
268
270
|
"the [ocr] extra and the inject hint must name the same requirements — "
|
|
269
271
|
"the hint is what a user pastes when the extra is missing"
|
|
270
272
|
)
|
|
271
|
-
assert '"rapidocr>=3.8,<
|
|
273
|
+
assert '"rapidocr>=3.8,<4"' in extras.inject_command("ocr"), (
|
|
272
274
|
"a bounded spec must be quoted: `>` and `<` are redirections in bash, cmd.exe and PowerShell"
|
|
273
275
|
)
|
|
274
276
|
|
|
@@ -286,6 +288,13 @@ def test_ci_installs_the_ocr_extra_so_the_engine_test_actually_runs():
|
|
|
286
288
|
assert 'pip install -e ".[full,ocr,test]"' in workflow, (
|
|
287
289
|
"the pytest job must install [ocr], or tests/test_ocr_engine.py skips there too"
|
|
288
290
|
)
|
|
291
|
+
# ...and the matrix legs are where the *unselected* minor gets exercised: the
|
|
292
|
+
# pytest job resolves whichever rapidocr the bound allows today.
|
|
293
|
+
for verified in ("3.8.4", "3.9.2"):
|
|
294
|
+
assert f'"{verified}"' in workflow, (
|
|
295
|
+
f"the ocr-engine-matrix job must cover rapidocr {verified} — a bound "
|
|
296
|
+
"claiming a version nothing runs against is how issue:77 shipped"
|
|
297
|
+
)
|
|
289
298
|
|
|
290
299
|
|
|
291
300
|
@pytest.mark.parametrize("extra", ["audio", "full"])
|
|
@@ -44,7 +44,7 @@ EXTRA_MODULES: dict[str, tuple[str, ...]] = {
|
|
|
44
44
|
EXTRA_PACKAGES: dict[str, tuple[str, ...]] = {
|
|
45
45
|
"frames": ("scenedetect", "opencv-python"),
|
|
46
46
|
"audio": ("librosa", "matplotlib"),
|
|
47
|
-
"ocr": ("rapidocr>=3.8,<
|
|
47
|
+
"ocr": ("rapidocr>=3.8,<4", "onnxruntime>=1.18"),
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
#: The enrichment that cannot be a package extra. ``bpm-detector`` gives
|
|
@@ -105,7 +105,7 @@ def inject_command_for(extra_names: list[str] | tuple[str, ...]) -> str:
|
|
|
105
105
|
"""One command adding several extras — what ``doctor``'s ``next_step`` prints.
|
|
106
106
|
|
|
107
107
|
Every spec is double-quoted: an injected spec can carry version bounds
|
|
108
|
-
(``rapidocr>=3.8,<
|
|
108
|
+
(``rapidocr>=3.8,<4``), and an unquoted ``>``/``<`` is a redirection in
|
|
109
109
|
bash, cmd.exe and PowerShell alike — the printed command has to be
|
|
110
110
|
pasteable, since being pasteable is the whole point of it.
|
|
111
111
|
"""
|
|
@@ -30,12 +30,35 @@ from yt_tools.core import (
|
|
|
30
30
|
from yt_tools.extras import format_missing_extra
|
|
31
31
|
|
|
32
32
|
ENGINE_LABEL = "RapidOCR (PP-OCRv5)"
|
|
33
|
+
#: Languages whose recognizer is not PP-OCRv5 — the label has to name what ran.
|
|
34
|
+
_ENGINE_LABEL_JA = "RapidOCR (PP-OCRv5 det/cls + PP-OCRv4 ja rec)"
|
|
33
35
|
DEFAULT_LANGUAGE = "en"
|
|
34
36
|
SUPPORTED_LANGUAGES = ("en", "ru", "ja", "zh", "multi")
|
|
35
37
|
|
|
36
38
|
_FRAME_RE = re.compile(r"^frame_(\d+)(?:_(\d+))?\.jpg$")
|
|
37
39
|
_NO_TEXT_MARKER = "_(no text detected)_"
|
|
38
40
|
|
|
41
|
+
#: Language → (``LangRec`` member name, recognizer ``OCRVersion`` member name).
|
|
42
|
+
#:
|
|
43
|
+
#: Detection and classification are language-agnostic in the v5 family (one `ch`
|
|
44
|
+
#: model each), but the recognizer is per language — and PP-OCRv5 ships twelve of
|
|
45
|
+
#: them (arabic, ch, cyrillic, devanagari, el, en, eslav, korean, latin, ta, te,
|
|
46
|
+
#: th), **Japanese not among them**: `japan_PP-OCRv4_rec_mobile` is the only
|
|
47
|
+
#: Japanese recognizer in both 3.8.x and 3.9.x registries. So the version is
|
|
48
|
+
#: named per language instead of forced globally. Two failure modes meet here, and
|
|
49
|
+
#: both shipped: with `Rec.ocr_version=PP-OCRv5` + `japan` the library's lenient
|
|
50
|
+
#: fallback used to pick the v4 model (working by luck), and once `model_type` is
|
|
51
|
+
#: named — which it must be, or the config depends on the installed release's
|
|
52
|
+
#: defaults ([[issue:77]]) — the fallback is skipped and construction raises
|
|
53
|
+
#: `Invalid OCR configuration`. Verified by building the engine for every
|
|
54
|
+
#: ``SUPPORTED_LANGUAGES`` entry on rapidocr 3.8.4 and 3.9.2 ([[task:2833]]).
|
|
55
|
+
_LANGUAGE_REC_VERSION: dict[str, tuple[str, str]] = {
|
|
56
|
+
"en": ("EN", "PPOCRV5"),
|
|
57
|
+
"ru": ("CYRILLIC", "PPOCRV5"),
|
|
58
|
+
"ja": ("JAPAN", "PPOCRV4"),
|
|
59
|
+
"zh": ("CH", "PPOCRV5"),
|
|
60
|
+
}
|
|
61
|
+
|
|
39
62
|
|
|
40
63
|
class OcrError(RuntimeError):
|
|
41
64
|
"""Any OCR-side failure: missing extra, missing cache, model issues, etc."""
|
|
@@ -82,36 +105,58 @@ def discover_frames(frames_dir: Path) -> list[tuple[int, Path]]:
|
|
|
82
105
|
# --- engine factory (lazy import) -------------------------------------------
|
|
83
106
|
|
|
84
107
|
|
|
85
|
-
def _build_params(
|
|
108
|
+
def _build_params(
|
|
109
|
+
language: str,
|
|
110
|
+
langrec_enum,
|
|
111
|
+
ocrversion_enum=None,
|
|
112
|
+
modeltype_enum=None,
|
|
113
|
+
) -> dict:
|
|
86
114
|
"""Map user-facing language → RapidOCR params dict.
|
|
87
115
|
|
|
88
116
|
Forces ``PP-OCRv5`` across Det / Cls / Rec when ``ocrversion_enum`` is
|
|
89
117
|
supplied (RapidOCR 3.8.x defaults the bundled-model dance to v4 — we want
|
|
90
|
-
v5 per design spec)
|
|
91
|
-
``
|
|
92
|
-
|
|
118
|
+
v5 per design spec) **and names the model type** (``mobile``) alongside it,
|
|
119
|
+
when ``modeltype_enum`` is supplied.
|
|
120
|
+
|
|
121
|
+
Both halves are needed, and that is the whole lesson of [[issue:77]]: the
|
|
122
|
+
model type is a *version-dependent default*, not a constant. 3.8.x defaults
|
|
123
|
+
every component to ``PP-OCRv4 + mobile`` — which happens to be valid for v5
|
|
124
|
+
too; 3.9.x moved Det/Rec to ``PP-OCRv6 + small``, and the v5 family ships
|
|
125
|
+
``mobile``/``server`` only, so a version-only pin asked for "PP-OCRv5 … small"
|
|
126
|
+
and RapidOCR refused the config at construction. Pinning the pair makes the
|
|
127
|
+
params mean the same thing on every supported release.
|
|
128
|
+
|
|
129
|
+
For ``multi``, returns params **without** a ``Rec.lang_type`` override so
|
|
130
|
+
RapidOCR picks its default multilingual model (Chinese+English).
|
|
93
131
|
"""
|
|
94
132
|
params: dict = {}
|
|
95
133
|
if ocrversion_enum is not None:
|
|
96
134
|
v5 = ocrversion_enum.PPOCRV5
|
|
97
135
|
params["Det.ocr_version"] = v5
|
|
98
136
|
params["Cls.ocr_version"] = v5
|
|
137
|
+
# Overridden per language below: not every supported language has a v5
|
|
138
|
+
# recognizer (see _LANGUAGE_REC_VERSION).
|
|
99
139
|
params["Rec.ocr_version"] = v5
|
|
140
|
+
if modeltype_enum is not None:
|
|
141
|
+
# `mobile` (not `server`): the small/fast bundle, matching the CPU-only
|
|
142
|
+
# onnxruntime install the extra brings, and the one key the v5 family
|
|
143
|
+
# has for every supported language/component.
|
|
144
|
+
mobile = modeltype_enum.MOBILE
|
|
145
|
+
params["Det.model_type"] = mobile
|
|
146
|
+
params["Cls.model_type"] = mobile
|
|
147
|
+
params["Rec.model_type"] = mobile
|
|
100
148
|
if language == "multi":
|
|
101
149
|
return params
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
"ru": getattr(langrec_enum, "CYRILLIC", None),
|
|
105
|
-
"ja": getattr(langrec_enum, "JAPAN", None),
|
|
106
|
-
"zh": getattr(langrec_enum, "CH", None),
|
|
107
|
-
}
|
|
108
|
-
lang_type = mapping.get(language)
|
|
150
|
+
member, rec_version = _LANGUAGE_REC_VERSION.get(language, (None, None))
|
|
151
|
+
lang_type = getattr(langrec_enum, member, None) if member else None
|
|
109
152
|
if lang_type is None:
|
|
110
153
|
raise OcrError(
|
|
111
154
|
f"unknown language: {language!r} "
|
|
112
155
|
f"(supported: {', '.join(SUPPORTED_LANGUAGES)})"
|
|
113
156
|
)
|
|
114
157
|
params["Rec.lang_type"] = lang_type
|
|
158
|
+
if ocrversion_enum is not None:
|
|
159
|
+
params["Rec.ocr_version"] = getattr(ocrversion_enum, rec_version)
|
|
115
160
|
return params
|
|
116
161
|
|
|
117
162
|
|
|
@@ -123,12 +168,12 @@ def _load_engine(language: str):
|
|
|
123
168
|
without it.
|
|
124
169
|
"""
|
|
125
170
|
try:
|
|
126
|
-
from rapidocr import LangRec, OCRVersion, RapidOCR # noqa: PLC0415
|
|
171
|
+
from rapidocr import LangRec, ModelType, OCRVersion, RapidOCR # noqa: PLC0415
|
|
127
172
|
import onnxruntime # noqa: F401, PLC0415 — the [ocr] extra ships both; a half-installed pair must refuse here, not crash later
|
|
128
173
|
except ImportError as e:
|
|
129
174
|
# Same wording as every other CLI (D4) — the message lives in one place.
|
|
130
175
|
raise OcrError(format_missing_extra("yt-ocr", "ocr")) from e
|
|
131
|
-
params = _build_params(language, LangRec, OCRVersion)
|
|
176
|
+
params = _build_params(language, LangRec, OCRVersion, ModelType)
|
|
132
177
|
return RapidOCR(params=params)
|
|
133
178
|
|
|
134
179
|
|
|
@@ -153,6 +198,18 @@ def _ocr_one(engine, image_path: Path) -> list[str]:
|
|
|
153
198
|
# --- pure render -------------------------------------------------------------
|
|
154
199
|
|
|
155
200
|
|
|
201
|
+
def engine_label(language: str) -> str:
|
|
202
|
+
"""Header label naming the recognizer that actually runs for ``language``.
|
|
203
|
+
|
|
204
|
+
Japanese is the one supported language with no PP-OCRv5 recognizer, so a
|
|
205
|
+
blanket "PP-OCRv5" line would be a claim the artefact cannot back — and the
|
|
206
|
+
header is the only place a reader (or an agent comparing two runs) can see
|
|
207
|
+
which models produced the text.
|
|
208
|
+
"""
|
|
209
|
+
_, rec_version = _LANGUAGE_REC_VERSION.get(language, (None, "PPOCRV5"))
|
|
210
|
+
return _ENGINE_LABEL_JA if rec_version == "PPOCRV4" else ENGINE_LABEL
|
|
211
|
+
|
|
212
|
+
|
|
156
213
|
def ocr_to_markdown(
|
|
157
214
|
video_id: str,
|
|
158
215
|
frames: list[tuple[int, list[str]]],
|
|
@@ -166,7 +223,7 @@ def ocr_to_markdown(
|
|
|
166
223
|
f"# OCR — {video_id}",
|
|
167
224
|
"",
|
|
168
225
|
(
|
|
169
|
-
f"generated: {generated} · engine: {
|
|
226
|
+
f"generated: {generated} · engine: {engine_label(language)}"
|
|
170
227
|
f" · language: {language} · frames: {len(frames)}"
|
|
171
228
|
),
|
|
172
229
|
"",
|
|
@@ -259,7 +316,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
259
316
|
default=DEFAULT_LANGUAGE,
|
|
260
317
|
help=(
|
|
261
318
|
f"OCR language (default: {DEFAULT_LANGUAGE}). "
|
|
262
|
-
"multi = PP-OCR multilingual (Chinese+English)."
|
|
319
|
+
"multi = PP-OCR multilingual (Chinese+English). "
|
|
320
|
+
"ja uses the PP-OCRv4 recognizer: no PP-OCRv5 Japanese model exists."
|
|
263
321
|
),
|
|
264
322
|
)
|
|
265
323
|
args = parser.parse_args(argv)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|