ocr-software 0.1.0__tar.gz → 0.1.2__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 (25) hide show
  1. ocr_software-0.1.2/INSTALL.txt +106 -0
  2. ocr_software-0.1.2/PKG-INFO +123 -0
  3. ocr_software-0.1.2/README.md +106 -0
  4. {ocr_software-0.1.0 → ocr_software-0.1.2}/ocr_software/__init__.py +1 -1
  5. {ocr_software-0.1.0 → ocr_software-0.1.2}/ocr_software/advice.py +6 -0
  6. {ocr_software-0.1.0 → ocr_software-0.1.2}/ocr_software/cli.py +22 -6
  7. {ocr_software-0.1.0 → ocr_software-0.1.2}/ocr_software/client.py +10 -6
  8. ocr_software-0.1.2/ocr_software.egg-info/PKG-INFO +123 -0
  9. {ocr_software-0.1.0 → ocr_software-0.1.2}/pyproject.toml +3 -3
  10. {ocr_software-0.1.0 → ocr_software-0.1.2}/tests/test_cli.py +56 -0
  11. {ocr_software-0.1.0 → ocr_software-0.1.2}/tests/test_client.py +10 -0
  12. {ocr_software-0.1.0 → ocr_software-0.1.2}/tests/test_release_files.py +2 -0
  13. ocr_software-0.1.0/INSTALL.txt +0 -71
  14. ocr_software-0.1.0/PKG-INFO +0 -88
  15. ocr_software-0.1.0/README.md +0 -71
  16. ocr_software-0.1.0/ocr_software.egg-info/PKG-INFO +0 -88
  17. {ocr_software-0.1.0 → ocr_software-0.1.2}/LICENSE +0 -0
  18. {ocr_software-0.1.0 → ocr_software-0.1.2}/ocr_software/__main__.py +0 -0
  19. {ocr_software-0.1.0 → ocr_software-0.1.2}/ocr_software.egg-info/SOURCES.txt +0 -0
  20. {ocr_software-0.1.0 → ocr_software-0.1.2}/ocr_software.egg-info/dependency_links.txt +0 -0
  21. {ocr_software-0.1.0 → ocr_software-0.1.2}/ocr_software.egg-info/entry_points.txt +0 -0
  22. {ocr_software-0.1.0 → ocr_software-0.1.2}/ocr_software.egg-info/requires.txt +0 -0
  23. {ocr_software-0.1.0 → ocr_software-0.1.2}/ocr_software.egg-info/top_level.txt +0 -0
  24. {ocr_software-0.1.0 → ocr_software-0.1.2}/setup.cfg +0 -0
  25. {ocr_software-0.1.0 → ocr_software-0.1.2}/tests/test_advice.py +0 -0
@@ -0,0 +1,106 @@
1
+ # ocr-software
2
+
3
+ Python client and command line for the [ocr-software.com](https://ocr-software.com/) OCR API. MIT, standard library only, Python 3.9 or newer.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ pipx install ocr-software # command line only, in its own environment
9
+ pip install ocr-software # client library, inside a venv
10
+ uvx ocr-software --version # run once without installing (uv)
11
+ ```
12
+
13
+ First call (list the pipelines, then read one file):
14
+
15
+ ```sh
16
+ ocr-software --list-pipelines
17
+ ocr-software scan.pdf --pipeline scan-ctc@2 -o scan.txt
18
+ ```
19
+
20
+ ## Settings
21
+
22
+ Environment variables; the command line flag wins.
23
+
24
+ | Variable | Meaning |
25
+ |---|---|
26
+ | `OCR_SOFTWARE_API_KEY` | API key (starts with `ocr_`). `OCR_KEY` is read too, it is the name used in the API docs; if both are set, `OCR_SOFTWARE_API_KEY` wins. |
27
+ | `OCR_SOFTWARE_BASE_URL` | Default `https://api.ocr-software.com`; own server e.g. `http://127.0.0.1:8080` (also `--base-url`). `OCR_BASE_URL` is read too. |
28
+ | `OCR_SOFTWARE_ALLOWED_DIR` | Optional: read documents only from below this folder (symlinks and `..` do not escape). |
29
+
30
+ The key is never printed and never part of an error message. Without `--pipeline` the default is used: the fastest and cheapest available pipeline (no previews). It is named on stderr (`-q` silences that line for bulk runs; `-o NUL` or `-o /dev/null` discards the result); `--list-pipelines` marks it. Warnings of the service (`dpi_ignored`, `job_budget`, `free_limit`, `balance_too_low`) are printed as `warning CODE: text` lines on stderr by `--dry-run` and before a job that uses `--dpi` or `--job-budget` (that job asks once with a dry run first). In Python: `client.ocr(path, client.default_pipeline())`.
31
+
32
+ ## Command line
33
+
34
+ ```sh
35
+ ocr-software --list-pipelines # available pipelines, one per line: name@version, tab, what it delivers and costs; the default one on stderr (no key needed)
36
+ ocr-software scan.pdf --pipeline NAME@VERSION --dry-run # page count and price in euros, nothing runs, nothing is billed
37
+ ocr-software scan.pdf # text to stdout, with the default pipeline (named on stderr)
38
+ ocr-software scan.pdf --pipeline NAME@VERSION # text to stdout
39
+ ocr-software scan.pdf -o scan.md --pipeline NAME@VERSION --format md
40
+ ocr-software scan.pdf -o scan.pdf.txt --pipeline NAME@VERSION
41
+ ocr-software scan.pdf -o searchable.pdf --pipeline NAME@VERSION --format pdf # searchable PDF, -o is required
42
+ ocr-software scan.pdf --pipeline NAME@VERSION --format json > scan.json
43
+ ocr-software scan.pdf --pipeline NAME@VERSION --base-url http://10.0.0.5:8080 # your own server
44
+ python -m ocr_software ... # the same command without the script on PATH
45
+ ```
46
+
47
+ Options:
48
+ - `--format text` (default) `| md | json | hocr | pdf`
49
+ - `--dpi 150|200|300` (PDF render resolution)
50
+ - `--timeout SECONDS` (wait for the job, default 3600)
51
+ - `--job-budget MILLICENTS` (cap; ignored where billing is off)
52
+ - `-q`, `--dry-run`, `--version`
53
+
54
+ Without `-o` the result goes to stdout as UTF-8 (not for pdf). With `-o` the file appears whole or not at all, an existing file is replaced, and the input file itself is refused as output.
55
+
56
+ ### Exit codes
57
+
58
+ | Code | Meaning |
59
+ |---|---|
60
+ | 0 | done, every page read |
61
+ | 1 | error from the service or the network (message on stderr with the API error code, for example 401 unauthorized); also an address that does not answer: a typo and an outage look the same, so a script may retry this |
62
+ | 2 | wrong call: unreadable or unsupported input, output not writable, no key, malformed address (not http/https) |
63
+ | 3 | the job is done but a page failed or did not run, or the job stopped early: the result is written anyway, stderr says what is missing and names the job; treat the file as incomplete |
64
+ | 4 | the job did not finish within `--timeout`; it keeps running, stderr names the job |
65
+
66
+ ## Python
67
+
68
+ ```python
69
+ from ocr_software import Client, PagesUnread
70
+ client = Client() # key and address from the environment
71
+ text = client.ocr("scan.pdf", "NAME@VERSION", "md") # bytes (text.decode("utf-8") for a string); raises PagesUnread (with .data) if a page was not read
72
+ pdf = client.ocr("scan.pdf", "NAME@VERSION", "pdf") # searchable PDF
73
+ job = client.submit("scan.pdf", "NAME@VERSION") # step by step: submit, wait, result
74
+ client.wait(job["job_id"]); client.result(job["job_id"], "json")
75
+ q = client.quote("scan.pdf", "NAME@VERSION") # dry run: page count and price, nothing runs
76
+ from ocr_software import price_sentence; price_sentence(q) # the same sentence as --dry-run, in euros
77
+ ```
78
+
79
+ **Price:** the API counts money in millicents, 100,000 millicents = 1 EUR (a page of the cheapest pipeline costs 100 = EUR 0.001). Today nothing is charged ("you pay EUR 0.00 today"); the list price is shown for information. Free use has limits: [fair usage](https://ocr-software.com/legal/fair-usage/); for more volume write to info@ocr-software.com.
80
+
81
+ **Word boxes (json):** `box = [x0, y0, x1, y1]` in pixels of the page image, origin top left; `width`, `height` and `dpi` of the page say which image. In PDF points: pixels * 72 / dpi. Per word, `verified` is true only where a second reader confirmed it (`scan-vlm-ctc@2`); `safe_ok` is always false today. Judge by `confidence` (0 to 1). Every field: [reference](https://ocr-software.com/docs/reference/).
82
+
83
+ **Retries** follow the API rules: 429, 500, 502, 503, 504 and network errors up to 5 times (waits 1, 2, 4, 8, 16 s or `Retry-After`). An upload the service cuts off (it does that beyond 2 uploads at once per key, instead of answering 429) counts like 429. If no connection can be made at all (wrong address, host down) it gives up after 2 retries and names the address: a few seconds, up to about 10 s on Windows, where a refused port itself takes 2 s per try (connect timeout 10 s). A `Retry-After` over 60 s (`locked_out`) is not waited for. A submit carries an `Idempotency-Key`, so a retry never creates a second job.
84
+
85
+ Every document is checked locally first: PDF, PNG, JPEG or TIFF by its first bytes, at most 100 MiB.
86
+
87
+ ## Paperless-ngx
88
+
89
+ Pre-consume script (sketch, not tried against a real Paperless; a non-PDF input keeps its file name but becomes a PDF):
90
+
91
+ ```sh
92
+ #!/bin/sh
93
+ ocr-software "$DOCUMENT_WORKING_PATH" --pipeline NAME@VERSION --format pdf -o "$DOCUMENT_WORKING_PATH.ocr" && mv "$DOCUMENT_WORKING_PATH.ocr" "$DOCUMENT_WORKING_PATH"
94
+ ```
95
+
96
+ with `OCR_SOFTWARE_API_KEY` and `OCR_SOFTWARE_BASE_URL` set in the environment of the Paperless container.
97
+
98
+ ## Tests
99
+
100
+ Fake local service, no network, no key:
101
+
102
+ ```sh
103
+ python -m venv .venv
104
+ .venv/bin/pip install pytest # Windows: .venv\Scripts\pip install pytest
105
+ .venv/bin/python -m pytest -p no:cacheprovider . # Windows: .venv\Scripts\python -m pytest -p no:cacheprovider .
106
+ ```
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.4
2
+ Name: ocr-software
3
+ Version: 0.1.2
4
+ Summary: Client and command line for the ocr-software.com OCR API: text, word boxes, layout, searchable PDF.
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://ocr-software.com/integrations/python/
7
+ Project-URL: Repository, https://github.com/ocr-software/ocr-software-python
8
+ Keywords: ocr,pdf,searchable-pdf,hocr,markdown,document-ai
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Topic :: Scientific/Engineering :: Image Recognition
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Provides-Extra: test
15
+ Requires-Dist: pytest==9.0.2; extra == "test"
16
+ Dynamic: license-file
17
+
18
+ # ocr-software
19
+
20
+ Python client and command line for the [ocr-software.com](https://ocr-software.com/) OCR API. MIT, standard library only, Python 3.9 or newer.
21
+
22
+ ## Install
23
+
24
+ ```sh
25
+ pipx install ocr-software # command line only, in its own environment
26
+ pip install ocr-software # client library, inside a venv
27
+ uvx ocr-software --version # run once without installing (uv)
28
+ ```
29
+
30
+ First call (list the pipelines, then read one file):
31
+
32
+ ```sh
33
+ ocr-software --list-pipelines
34
+ ocr-software scan.pdf --pipeline scan-ctc@2 -o scan.txt
35
+ ```
36
+
37
+ ## Settings
38
+
39
+ Environment variables; the command line flag wins.
40
+
41
+ | Variable | Meaning |
42
+ |---|---|
43
+ | `OCR_SOFTWARE_API_KEY` | API key (starts with `ocr_`). `OCR_KEY` is read too, it is the name used in the API docs; if both are set, `OCR_SOFTWARE_API_KEY` wins. |
44
+ | `OCR_SOFTWARE_BASE_URL` | Default `https://api.ocr-software.com`; own server e.g. `http://127.0.0.1:8080` (also `--base-url`). `OCR_BASE_URL` is read too. |
45
+ | `OCR_SOFTWARE_ALLOWED_DIR` | Optional: read documents only from below this folder (symlinks and `..` do not escape). |
46
+
47
+ The key is never printed and never part of an error message. Without `--pipeline` the default is used: the fastest and cheapest available pipeline (no previews). It is named on stderr (`-q` silences that line for bulk runs; `-o NUL` or `-o /dev/null` discards the result); `--list-pipelines` marks it. Warnings of the service (`dpi_ignored`, `job_budget`, `free_limit`, `balance_too_low`) are printed as `warning CODE: text` lines on stderr by `--dry-run` and before a job that uses `--dpi` or `--job-budget` (that job asks once with a dry run first). In Python: `client.ocr(path, client.default_pipeline())`.
48
+
49
+ ## Command line
50
+
51
+ ```sh
52
+ ocr-software --list-pipelines # available pipelines, one per line: name@version, tab, what it delivers and costs; the default one on stderr (no key needed)
53
+ ocr-software scan.pdf --pipeline NAME@VERSION --dry-run # page count and price in euros, nothing runs, nothing is billed
54
+ ocr-software scan.pdf # text to stdout, with the default pipeline (named on stderr)
55
+ ocr-software scan.pdf --pipeline NAME@VERSION # text to stdout
56
+ ocr-software scan.pdf -o scan.md --pipeline NAME@VERSION --format md
57
+ ocr-software scan.pdf -o scan.pdf.txt --pipeline NAME@VERSION
58
+ ocr-software scan.pdf -o searchable.pdf --pipeline NAME@VERSION --format pdf # searchable PDF, -o is required
59
+ ocr-software scan.pdf --pipeline NAME@VERSION --format json > scan.json
60
+ ocr-software scan.pdf --pipeline NAME@VERSION --base-url http://10.0.0.5:8080 # your own server
61
+ python -m ocr_software ... # the same command without the script on PATH
62
+ ```
63
+
64
+ Options:
65
+ - `--format text` (default) `| md | json | hocr | pdf`
66
+ - `--dpi 150|200|300` (PDF render resolution)
67
+ - `--timeout SECONDS` (wait for the job, default 3600)
68
+ - `--job-budget MILLICENTS` (cap; ignored where billing is off)
69
+ - `-q`, `--dry-run`, `--version`
70
+
71
+ Without `-o` the result goes to stdout as UTF-8 (not for pdf). With `-o` the file appears whole or not at all, an existing file is replaced, and the input file itself is refused as output.
72
+
73
+ ### Exit codes
74
+
75
+ | Code | Meaning |
76
+ |---|---|
77
+ | 0 | done, every page read |
78
+ | 1 | error from the service or the network (message on stderr with the API error code, for example 401 unauthorized); also an address that does not answer: a typo and an outage look the same, so a script may retry this |
79
+ | 2 | wrong call: unreadable or unsupported input, output not writable, no key, malformed address (not http/https) |
80
+ | 3 | the job is done but a page failed or did not run, or the job stopped early: the result is written anyway, stderr says what is missing and names the job; treat the file as incomplete |
81
+ | 4 | the job did not finish within `--timeout`; it keeps running, stderr names the job |
82
+
83
+ ## Python
84
+
85
+ ```python
86
+ from ocr_software import Client, PagesUnread
87
+ client = Client() # key and address from the environment
88
+ text = client.ocr("scan.pdf", "NAME@VERSION", "md") # bytes (text.decode("utf-8") for a string); raises PagesUnread (with .data) if a page was not read
89
+ pdf = client.ocr("scan.pdf", "NAME@VERSION", "pdf") # searchable PDF
90
+ job = client.submit("scan.pdf", "NAME@VERSION") # step by step: submit, wait, result
91
+ client.wait(job["job_id"]); client.result(job["job_id"], "json")
92
+ q = client.quote("scan.pdf", "NAME@VERSION") # dry run: page count and price, nothing runs
93
+ from ocr_software import price_sentence; price_sentence(q) # the same sentence as --dry-run, in euros
94
+ ```
95
+
96
+ **Price:** the API counts money in millicents, 100,000 millicents = 1 EUR (a page of the cheapest pipeline costs 100 = EUR 0.001). Today nothing is charged ("you pay EUR 0.00 today"); the list price is shown for information. Free use has limits: [fair usage](https://ocr-software.com/legal/fair-usage/); for more volume write to info@ocr-software.com.
97
+
98
+ **Word boxes (json):** `box = [x0, y0, x1, y1]` in pixels of the page image, origin top left; `width`, `height` and `dpi` of the page say which image. In PDF points: pixels * 72 / dpi. Per word, `verified` is true only where a second reader confirmed it (`scan-vlm-ctc@2`); `safe_ok` is always false today. Judge by `confidence` (0 to 1). Every field: [reference](https://ocr-software.com/docs/reference/).
99
+
100
+ **Retries** follow the API rules: 429, 500, 502, 503, 504 and network errors up to 5 times (waits 1, 2, 4, 8, 16 s or `Retry-After`). An upload the service cuts off (it does that beyond 2 uploads at once per key, instead of answering 429) counts like 429. If no connection can be made at all (wrong address, host down) it gives up after 2 retries and names the address: a few seconds, up to about 10 s on Windows, where a refused port itself takes 2 s per try (connect timeout 10 s). A `Retry-After` over 60 s (`locked_out`) is not waited for. A submit carries an `Idempotency-Key`, so a retry never creates a second job.
101
+
102
+ Every document is checked locally first: PDF, PNG, JPEG or TIFF by its first bytes, at most 100 MiB.
103
+
104
+ ## Paperless-ngx
105
+
106
+ Pre-consume script (sketch, not tried against a real Paperless; a non-PDF input keeps its file name but becomes a PDF):
107
+
108
+ ```sh
109
+ #!/bin/sh
110
+ ocr-software "$DOCUMENT_WORKING_PATH" --pipeline NAME@VERSION --format pdf -o "$DOCUMENT_WORKING_PATH.ocr" && mv "$DOCUMENT_WORKING_PATH.ocr" "$DOCUMENT_WORKING_PATH"
111
+ ```
112
+
113
+ with `OCR_SOFTWARE_API_KEY` and `OCR_SOFTWARE_BASE_URL` set in the environment of the Paperless container.
114
+
115
+ ## Tests
116
+
117
+ Fake local service, no network, no key:
118
+
119
+ ```sh
120
+ python -m venv .venv
121
+ .venv/bin/pip install pytest # Windows: .venv\Scripts\pip install pytest
122
+ .venv/bin/python -m pytest -p no:cacheprovider . # Windows: .venv\Scripts\python -m pytest -p no:cacheprovider .
123
+ ```
@@ -0,0 +1,106 @@
1
+ # ocr-software
2
+
3
+ Python client and command line for the [ocr-software.com](https://ocr-software.com/) OCR API. MIT, standard library only, Python 3.9 or newer.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ pipx install ocr-software # command line only, in its own environment
9
+ pip install ocr-software # client library, inside a venv
10
+ uvx ocr-software --version # run once without installing (uv)
11
+ ```
12
+
13
+ First call (list the pipelines, then read one file):
14
+
15
+ ```sh
16
+ ocr-software --list-pipelines
17
+ ocr-software scan.pdf --pipeline scan-ctc@2 -o scan.txt
18
+ ```
19
+
20
+ ## Settings
21
+
22
+ Environment variables; the command line flag wins.
23
+
24
+ | Variable | Meaning |
25
+ |---|---|
26
+ | `OCR_SOFTWARE_API_KEY` | API key (starts with `ocr_`). `OCR_KEY` is read too, it is the name used in the API docs; if both are set, `OCR_SOFTWARE_API_KEY` wins. |
27
+ | `OCR_SOFTWARE_BASE_URL` | Default `https://api.ocr-software.com`; own server e.g. `http://127.0.0.1:8080` (also `--base-url`). `OCR_BASE_URL` is read too. |
28
+ | `OCR_SOFTWARE_ALLOWED_DIR` | Optional: read documents only from below this folder (symlinks and `..` do not escape). |
29
+
30
+ The key is never printed and never part of an error message. Without `--pipeline` the default is used: the fastest and cheapest available pipeline (no previews). It is named on stderr (`-q` silences that line for bulk runs; `-o NUL` or `-o /dev/null` discards the result); `--list-pipelines` marks it. Warnings of the service (`dpi_ignored`, `job_budget`, `free_limit`, `balance_too_low`) are printed as `warning CODE: text` lines on stderr by `--dry-run` and before a job that uses `--dpi` or `--job-budget` (that job asks once with a dry run first). In Python: `client.ocr(path, client.default_pipeline())`.
31
+
32
+ ## Command line
33
+
34
+ ```sh
35
+ ocr-software --list-pipelines # available pipelines, one per line: name@version, tab, what it delivers and costs; the default one on stderr (no key needed)
36
+ ocr-software scan.pdf --pipeline NAME@VERSION --dry-run # page count and price in euros, nothing runs, nothing is billed
37
+ ocr-software scan.pdf # text to stdout, with the default pipeline (named on stderr)
38
+ ocr-software scan.pdf --pipeline NAME@VERSION # text to stdout
39
+ ocr-software scan.pdf -o scan.md --pipeline NAME@VERSION --format md
40
+ ocr-software scan.pdf -o scan.pdf.txt --pipeline NAME@VERSION
41
+ ocr-software scan.pdf -o searchable.pdf --pipeline NAME@VERSION --format pdf # searchable PDF, -o is required
42
+ ocr-software scan.pdf --pipeline NAME@VERSION --format json > scan.json
43
+ ocr-software scan.pdf --pipeline NAME@VERSION --base-url http://10.0.0.5:8080 # your own server
44
+ python -m ocr_software ... # the same command without the script on PATH
45
+ ```
46
+
47
+ Options:
48
+ - `--format text` (default) `| md | json | hocr | pdf`
49
+ - `--dpi 150|200|300` (PDF render resolution)
50
+ - `--timeout SECONDS` (wait for the job, default 3600)
51
+ - `--job-budget MILLICENTS` (cap; ignored where billing is off)
52
+ - `-q`, `--dry-run`, `--version`
53
+
54
+ Without `-o` the result goes to stdout as UTF-8 (not for pdf). With `-o` the file appears whole or not at all, an existing file is replaced, and the input file itself is refused as output.
55
+
56
+ ### Exit codes
57
+
58
+ | Code | Meaning |
59
+ |---|---|
60
+ | 0 | done, every page read |
61
+ | 1 | error from the service or the network (message on stderr with the API error code, for example 401 unauthorized); also an address that does not answer: a typo and an outage look the same, so a script may retry this |
62
+ | 2 | wrong call: unreadable or unsupported input, output not writable, no key, malformed address (not http/https) |
63
+ | 3 | the job is done but a page failed or did not run, or the job stopped early: the result is written anyway, stderr says what is missing and names the job; treat the file as incomplete |
64
+ | 4 | the job did not finish within `--timeout`; it keeps running, stderr names the job |
65
+
66
+ ## Python
67
+
68
+ ```python
69
+ from ocr_software import Client, PagesUnread
70
+ client = Client() # key and address from the environment
71
+ text = client.ocr("scan.pdf", "NAME@VERSION", "md") # bytes (text.decode("utf-8") for a string); raises PagesUnread (with .data) if a page was not read
72
+ pdf = client.ocr("scan.pdf", "NAME@VERSION", "pdf") # searchable PDF
73
+ job = client.submit("scan.pdf", "NAME@VERSION") # step by step: submit, wait, result
74
+ client.wait(job["job_id"]); client.result(job["job_id"], "json")
75
+ q = client.quote("scan.pdf", "NAME@VERSION") # dry run: page count and price, nothing runs
76
+ from ocr_software import price_sentence; price_sentence(q) # the same sentence as --dry-run, in euros
77
+ ```
78
+
79
+ **Price:** the API counts money in millicents, 100,000 millicents = 1 EUR (a page of the cheapest pipeline costs 100 = EUR 0.001). Today nothing is charged ("you pay EUR 0.00 today"); the list price is shown for information. Free use has limits: [fair usage](https://ocr-software.com/legal/fair-usage/); for more volume write to info@ocr-software.com.
80
+
81
+ **Word boxes (json):** `box = [x0, y0, x1, y1]` in pixels of the page image, origin top left; `width`, `height` and `dpi` of the page say which image. In PDF points: pixels * 72 / dpi. Per word, `verified` is true only where a second reader confirmed it (`scan-vlm-ctc@2`); `safe_ok` is always false today. Judge by `confidence` (0 to 1). Every field: [reference](https://ocr-software.com/docs/reference/).
82
+
83
+ **Retries** follow the API rules: 429, 500, 502, 503, 504 and network errors up to 5 times (waits 1, 2, 4, 8, 16 s or `Retry-After`). An upload the service cuts off (it does that beyond 2 uploads at once per key, instead of answering 429) counts like 429. If no connection can be made at all (wrong address, host down) it gives up after 2 retries and names the address: a few seconds, up to about 10 s on Windows, where a refused port itself takes 2 s per try (connect timeout 10 s). A `Retry-After` over 60 s (`locked_out`) is not waited for. A submit carries an `Idempotency-Key`, so a retry never creates a second job.
84
+
85
+ Every document is checked locally first: PDF, PNG, JPEG or TIFF by its first bytes, at most 100 MiB.
86
+
87
+ ## Paperless-ngx
88
+
89
+ Pre-consume script (sketch, not tried against a real Paperless; a non-PDF input keeps its file name but becomes a PDF):
90
+
91
+ ```sh
92
+ #!/bin/sh
93
+ ocr-software "$DOCUMENT_WORKING_PATH" --pipeline NAME@VERSION --format pdf -o "$DOCUMENT_WORKING_PATH.ocr" && mv "$DOCUMENT_WORKING_PATH.ocr" "$DOCUMENT_WORKING_PATH"
94
+ ```
95
+
96
+ with `OCR_SOFTWARE_API_KEY` and `OCR_SOFTWARE_BASE_URL` set in the environment of the Paperless container.
97
+
98
+ ## Tests
99
+
100
+ Fake local service, no network, no key:
101
+
102
+ ```sh
103
+ python -m venv .venv
104
+ .venv/bin/pip install pytest # Windows: .venv\Scripts\pip install pytest
105
+ .venv/bin/python -m pytest -p no:cacheprovider . # Windows: .venv\Scripts\python -m pytest -p no:cacheprovider .
106
+ ```
@@ -2,5 +2,5 @@
2
2
  from .advice import default_pipeline, price_sentence
3
3
  from .client import Client, OcrError, PagesUnread, split_pages, unread_message
4
4
 
5
- __version__ = "0.1.0"
5
+ __version__ = "0.1.2"
6
6
  __all__ = ["Client", "OcrError", "PagesUnread", "split_pages", "unread_message", "default_pipeline", "price_sentence", "__version__"]
@@ -43,6 +43,12 @@ def price_sentence(q, show_list_price=True):
43
43
  return text
44
44
 
45
45
 
46
+ def warning_lines(q):
47
+ """One line per warning of a dry run (dpi_ignored, job_budget, free_limit, balance, balance_too_low), for stderr:
48
+ price_sentence says what it costs, these say why fewer pages run or why an option does nothing."""
49
+ return ["warning %s: %s" % (w.get("code", "?"), w.get("message", "")) for w in q.get("warnings") or []]
50
+
51
+
46
52
  def cost(p):
47
53
  """Price per page in millicents, comparable between pipelines: the catalog price, else factor * 100 (the catalog hides the
48
54
  price while billing is off, the factor stays)."""
@@ -11,7 +11,7 @@ import os
11
11
  import sys
12
12
 
13
13
  from . import __version__
14
- from .advice import default_pipeline, price_sentence, summary
14
+ from .advice import default_pipeline, price_sentence, summary, warning_lines
15
15
  from .client import DEFAULT_WAIT_TIMEOUT_S, FORMATS, Client, OcrError, PagesUnread
16
16
 
17
17
  EXIT_ERROR, EXIT_USAGE, EXIT_UNREAD, EXIT_TIMEOUT = 1, 2, 3, 4
@@ -26,6 +26,7 @@ def _parser():
26
26
  p.add_argument("--format", default="text", choices=FORMATS, help="default text")
27
27
  p.add_argument("--dpi", type=int, choices=(150, 200, 300), help="render resolution of a PDF (default 150)")
28
28
  p.add_argument("--job-budget", type=int, help="cap in millicents (100,000 = 1 EUR; one page of the cheapest pipeline = 100). Where billing is off it is ignored")
29
+ p.add_argument("-q", "--quiet", action="store_true", help="do not name the default pipeline on stderr (for bulk runs); errors and warnings stay")
29
30
  p.add_argument("--dry-run", action="store_true", help="show the page count and the price in euros, run nothing, cost nothing")
30
31
  p.add_argument("--timeout", type=int, default=DEFAULT_WAIT_TIMEOUT_S, help="seconds to wait for the job (default %(default)s)")
31
32
  p.add_argument("--base-url", help="address of the service (default OCR_SOFTWARE_BASE_URL, else the hosted API)")
@@ -54,7 +55,19 @@ def _same_file(a, b):
54
55
  return False
55
56
 
56
57
 
58
+ def _is_devnull(path):
59
+ # "-o NUL" on Windows, "-o /dev/null" everywhere (also typed on Windows from Git Bash, where no folder \dev exists)
60
+ return bool(path) and os.path.normcase(path) in (os.path.normcase(os.devnull), os.path.normcase("/dev/null"))
61
+
62
+
63
+ def _print_warnings(answer):
64
+ for line in warning_lines(answer): # an older service sends none: nothing to print
65
+ print("ocr-software: " + line, file=sys.stderr)
66
+
67
+
57
68
  def _emit(data, output):
69
+ if _is_devnull(output):
70
+ return # discard: a .part file next to the null device cannot exist
58
71
  if output:
59
72
  _write(output, data)
60
73
  else:
@@ -83,21 +96,24 @@ def main(argv=None):
83
96
  parser.error("IN is required (--list-pipelines shows the pipelines)")
84
97
  if args.format == "pdf" and not args.output:
85
98
  parser.error("--format pdf needs -o OUT (a PDF is not written to the terminal)")
86
- if args.output and _same_file(args.input, args.output):
99
+ if args.output and not _is_devnull(args.output) and _same_file(args.input, args.output):
87
100
  parser.error("OUT is the input file; it would be overwritten")
88
- if args.output and not os.path.isdir(os.path.dirname(os.path.abspath(args.output))):
101
+ if args.output and not _is_devnull(args.output) and not os.path.isdir(os.path.dirname(os.path.abspath(args.output))):
89
102
  parser.error("folder of %s does not exist; nothing was sent" % args.output) # before the job, not after it
90
103
  pipeline = args.pipeline
91
104
  if not pipeline: # the default is named, never silent (ziel.md: fastest and cheapest available; stdout stays the result)
92
105
  pipeline = client.default_pipeline()
93
- print("ocr-software: no --pipeline given, using %s (the fastest and cheapest available one; --list-pipelines shows the others)" % pipeline,
94
- file=sys.stderr)
106
+ if not args.quiet:
107
+ print("ocr-software: no --pipeline given, using %s (the fastest and cheapest available one; --list-pipelines shows the others)" % pipeline,
108
+ file=sys.stderr)
95
109
  if args.dry_run:
96
110
  q = client.quote(args.input, pipeline, args.dpi)
97
111
  q.setdefault("pipeline", pipeline)
98
112
  print(price_sentence(q))
113
+ _print_warnings(q)
99
114
  return 0
100
- data = client.ocr(args.input, pipeline, args.format, timeout=args.timeout, dpi=args.dpi, job_budget=args.job_budget)
115
+ data = client.ocr(args.input, pipeline, args.format, timeout=args.timeout, dpi=args.dpi, job_budget=args.job_budget,
116
+ on_accepted=_print_warnings) # the service names the warnings in its answer: no extra dry run
101
117
  _emit(data, args.output)
102
118
  return 0
103
119
  except PagesUnread as e:
@@ -1,6 +1,6 @@
1
1
  """Client for the ocr-software.com API (standard library only, Python 3.9 or newer).
2
2
 
3
- Settings: OCR_SOFTWARE_API_KEY (or OCR_KEY, the name used in the API docs), OCR_SOFTWARE_BASE_URL (or OCR_BASE_URL),
3
+ Settings (the OCR_SOFTWARE_ name wins if both are set): OCR_SOFTWARE_API_KEY (or OCR_KEY, the name used in the API docs), OCR_SOFTWARE_BASE_URL (or OCR_BASE_URL),
4
4
  OCR_SOFTWARE_ALLOWED_DIR (optional). The key is never printed, never in an error message, never in a log.
5
5
  Spec of every path, field and error code: <base>/openapi.json.
6
6
  """
@@ -233,7 +233,7 @@ class Client:
233
233
  head = dict(headers or {})
234
234
  if key_needed:
235
235
  if not self._key:
236
- raise OcrError("API key missing: set OCR_SOFTWARE_API_KEY (the key starts with ocr_)", "no_key")
236
+ raise OcrError("API key missing: set OCR_SOFTWARE_API_KEY or OCR_KEY (the key starts with ocr_)", "no_key")
237
237
  if not KEY_CHARS.fullmatch(self._key): # fullmatch: "$" would let a trailing newline pass # a bad character would make http.client echo the key
238
238
  raise OcrError("API key contains characters that are not allowed in a header", "no_key")
239
239
  head["Authorization"] = "Bearer " + self._key
@@ -345,13 +345,17 @@ class Client:
345
345
  data, _ = self._call("GET", "/v1/jobs/%s/result" % job_id, query={"format": format})
346
346
  return data
347
347
 
348
- def ocr(self, path, pipeline, format="text", timeout=DEFAULT_WAIT_TIMEOUT_S, **options):
349
- """Submit, wait, fetch: the result bytes. options go to submit (dpi, client_ref, job_budget, formats, features).
350
- Raises PagesUnread (with .data, the result anyway) if a page failed or was not run: never silent."""
348
+ def ocr(self, path, pipeline, format="text", timeout=DEFAULT_WAIT_TIMEOUT_S, on_accepted=None, **options):
349
+ """Submit, wait, fetch: the result as bytes, also for text, md, json and hocr (use .decode("utf-8") for a string). options go to submit (dpi, client_ref, job_budget, formats, features).
350
+ Raises PagesUnread (with .data, the result anyway) if a page failed or was not run: never silent.
351
+ on_accepted(answer) is called with the answer to the submit; its "warnings" (absent from an older service) say why fewer pages run."""
351
352
  if format not in FORMATS:
352
353
  raise OcrError("format must be one of %s" % ", ".join(FORMATS), "bad_input")
353
354
  source = read_document(path, self.allowed_dir) # read once: PDF output needs the same bytes again
354
- job_id = self._submit(source, pipeline, **options).get("job_id")
355
+ accepted = self._submit(source, pipeline, **options)
356
+ if on_accepted:
357
+ on_accepted(accepted)
358
+ job_id = accepted.get("job_id")
355
359
  if not job_id:
356
360
  raise OcrError("the answer of %s has no job_id: check OCR_SOFTWARE_BASE_URL" % self.base_url, "bad_response")
357
361
  done = self.wait(job_id, timeout)
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.4
2
+ Name: ocr-software
3
+ Version: 0.1.2
4
+ Summary: Client and command line for the ocr-software.com OCR API: text, word boxes, layout, searchable PDF.
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://ocr-software.com/integrations/python/
7
+ Project-URL: Repository, https://github.com/ocr-software/ocr-software-python
8
+ Keywords: ocr,pdf,searchable-pdf,hocr,markdown,document-ai
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Topic :: Scientific/Engineering :: Image Recognition
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Provides-Extra: test
15
+ Requires-Dist: pytest==9.0.2; extra == "test"
16
+ Dynamic: license-file
17
+
18
+ # ocr-software
19
+
20
+ Python client and command line for the [ocr-software.com](https://ocr-software.com/) OCR API. MIT, standard library only, Python 3.9 or newer.
21
+
22
+ ## Install
23
+
24
+ ```sh
25
+ pipx install ocr-software # command line only, in its own environment
26
+ pip install ocr-software # client library, inside a venv
27
+ uvx ocr-software --version # run once without installing (uv)
28
+ ```
29
+
30
+ First call (list the pipelines, then read one file):
31
+
32
+ ```sh
33
+ ocr-software --list-pipelines
34
+ ocr-software scan.pdf --pipeline scan-ctc@2 -o scan.txt
35
+ ```
36
+
37
+ ## Settings
38
+
39
+ Environment variables; the command line flag wins.
40
+
41
+ | Variable | Meaning |
42
+ |---|---|
43
+ | `OCR_SOFTWARE_API_KEY` | API key (starts with `ocr_`). `OCR_KEY` is read too, it is the name used in the API docs; if both are set, `OCR_SOFTWARE_API_KEY` wins. |
44
+ | `OCR_SOFTWARE_BASE_URL` | Default `https://api.ocr-software.com`; own server e.g. `http://127.0.0.1:8080` (also `--base-url`). `OCR_BASE_URL` is read too. |
45
+ | `OCR_SOFTWARE_ALLOWED_DIR` | Optional: read documents only from below this folder (symlinks and `..` do not escape). |
46
+
47
+ The key is never printed and never part of an error message. Without `--pipeline` the default is used: the fastest and cheapest available pipeline (no previews). It is named on stderr (`-q` silences that line for bulk runs; `-o NUL` or `-o /dev/null` discards the result); `--list-pipelines` marks it. Warnings of the service (`dpi_ignored`, `job_budget`, `free_limit`, `balance_too_low`) are printed as `warning CODE: text` lines on stderr by `--dry-run` and before a job that uses `--dpi` or `--job-budget` (that job asks once with a dry run first). In Python: `client.ocr(path, client.default_pipeline())`.
48
+
49
+ ## Command line
50
+
51
+ ```sh
52
+ ocr-software --list-pipelines # available pipelines, one per line: name@version, tab, what it delivers and costs; the default one on stderr (no key needed)
53
+ ocr-software scan.pdf --pipeline NAME@VERSION --dry-run # page count and price in euros, nothing runs, nothing is billed
54
+ ocr-software scan.pdf # text to stdout, with the default pipeline (named on stderr)
55
+ ocr-software scan.pdf --pipeline NAME@VERSION # text to stdout
56
+ ocr-software scan.pdf -o scan.md --pipeline NAME@VERSION --format md
57
+ ocr-software scan.pdf -o scan.pdf.txt --pipeline NAME@VERSION
58
+ ocr-software scan.pdf -o searchable.pdf --pipeline NAME@VERSION --format pdf # searchable PDF, -o is required
59
+ ocr-software scan.pdf --pipeline NAME@VERSION --format json > scan.json
60
+ ocr-software scan.pdf --pipeline NAME@VERSION --base-url http://10.0.0.5:8080 # your own server
61
+ python -m ocr_software ... # the same command without the script on PATH
62
+ ```
63
+
64
+ Options:
65
+ - `--format text` (default) `| md | json | hocr | pdf`
66
+ - `--dpi 150|200|300` (PDF render resolution)
67
+ - `--timeout SECONDS` (wait for the job, default 3600)
68
+ - `--job-budget MILLICENTS` (cap; ignored where billing is off)
69
+ - `-q`, `--dry-run`, `--version`
70
+
71
+ Without `-o` the result goes to stdout as UTF-8 (not for pdf). With `-o` the file appears whole or not at all, an existing file is replaced, and the input file itself is refused as output.
72
+
73
+ ### Exit codes
74
+
75
+ | Code | Meaning |
76
+ |---|---|
77
+ | 0 | done, every page read |
78
+ | 1 | error from the service or the network (message on stderr with the API error code, for example 401 unauthorized); also an address that does not answer: a typo and an outage look the same, so a script may retry this |
79
+ | 2 | wrong call: unreadable or unsupported input, output not writable, no key, malformed address (not http/https) |
80
+ | 3 | the job is done but a page failed or did not run, or the job stopped early: the result is written anyway, stderr says what is missing and names the job; treat the file as incomplete |
81
+ | 4 | the job did not finish within `--timeout`; it keeps running, stderr names the job |
82
+
83
+ ## Python
84
+
85
+ ```python
86
+ from ocr_software import Client, PagesUnread
87
+ client = Client() # key and address from the environment
88
+ text = client.ocr("scan.pdf", "NAME@VERSION", "md") # bytes (text.decode("utf-8") for a string); raises PagesUnread (with .data) if a page was not read
89
+ pdf = client.ocr("scan.pdf", "NAME@VERSION", "pdf") # searchable PDF
90
+ job = client.submit("scan.pdf", "NAME@VERSION") # step by step: submit, wait, result
91
+ client.wait(job["job_id"]); client.result(job["job_id"], "json")
92
+ q = client.quote("scan.pdf", "NAME@VERSION") # dry run: page count and price, nothing runs
93
+ from ocr_software import price_sentence; price_sentence(q) # the same sentence as --dry-run, in euros
94
+ ```
95
+
96
+ **Price:** the API counts money in millicents, 100,000 millicents = 1 EUR (a page of the cheapest pipeline costs 100 = EUR 0.001). Today nothing is charged ("you pay EUR 0.00 today"); the list price is shown for information. Free use has limits: [fair usage](https://ocr-software.com/legal/fair-usage/); for more volume write to info@ocr-software.com.
97
+
98
+ **Word boxes (json):** `box = [x0, y0, x1, y1]` in pixels of the page image, origin top left; `width`, `height` and `dpi` of the page say which image. In PDF points: pixels * 72 / dpi. Per word, `verified` is true only where a second reader confirmed it (`scan-vlm-ctc@2`); `safe_ok` is always false today. Judge by `confidence` (0 to 1). Every field: [reference](https://ocr-software.com/docs/reference/).
99
+
100
+ **Retries** follow the API rules: 429, 500, 502, 503, 504 and network errors up to 5 times (waits 1, 2, 4, 8, 16 s or `Retry-After`). An upload the service cuts off (it does that beyond 2 uploads at once per key, instead of answering 429) counts like 429. If no connection can be made at all (wrong address, host down) it gives up after 2 retries and names the address: a few seconds, up to about 10 s on Windows, where a refused port itself takes 2 s per try (connect timeout 10 s). A `Retry-After` over 60 s (`locked_out`) is not waited for. A submit carries an `Idempotency-Key`, so a retry never creates a second job.
101
+
102
+ Every document is checked locally first: PDF, PNG, JPEG or TIFF by its first bytes, at most 100 MiB.
103
+
104
+ ## Paperless-ngx
105
+
106
+ Pre-consume script (sketch, not tried against a real Paperless; a non-PDF input keeps its file name but becomes a PDF):
107
+
108
+ ```sh
109
+ #!/bin/sh
110
+ ocr-software "$DOCUMENT_WORKING_PATH" --pipeline NAME@VERSION --format pdf -o "$DOCUMENT_WORKING_PATH.ocr" && mv "$DOCUMENT_WORKING_PATH.ocr" "$DOCUMENT_WORKING_PATH"
111
+ ```
112
+
113
+ with `OCR_SOFTWARE_API_KEY` and `OCR_SOFTWARE_BASE_URL` set in the environment of the Paperless container.
114
+
115
+ ## Tests
116
+
117
+ Fake local service, no network, no key:
118
+
119
+ ```sh
120
+ python -m venv .venv
121
+ .venv/bin/pip install pytest # Windows: .venv\Scripts\pip install pytest
122
+ .venv/bin/python -m pytest -p no:cacheprovider . # Windows: .venv\Scripts\python -m pytest -p no:cacheprovider .
123
+ ```
@@ -4,9 +4,9 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ocr-software"
7
- version = "0.1.0"
7
+ version = "0.1.2"
8
8
  description = "Client and command line for the ocr-software.com OCR API: text, word boxes, layout, searchable PDF."
9
- readme = {file = "INSTALL.txt", content-type = "text/plain"}
9
+ readme = {file = "INSTALL.txt", content-type = "text/markdown"}
10
10
  license = "MIT"
11
11
  license-files = ["LICENSE"]
12
12
  requires-python = ">=3.9"
@@ -21,7 +21,7 @@ classifiers = [
21
21
  test = ["pytest==9.0.2"]
22
22
 
23
23
  [project.urls]
24
- Homepage = "https://ocr-software.com/"
24
+ Homepage = "https://ocr-software.com/integrations/python/"
25
25
  Repository = "https://github.com/ocr-software/ocr-software-python"
26
26
 
27
27
  [project.scripts]
@@ -221,3 +221,59 @@ def test_ctrl_c_ends_without_a_stack_trace(fake, doc, monkeypatch, capsysbinary)
221
221
  monkeypatch.setattr(cli.Client, "ocr", interrupted)
222
222
  code, out, errtext = run([doc, "--pipeline", "p@1"], capsysbinary)
223
223
  assert code == 130 and out == b"" and "interrupted" in errtext and "Traceback" not in errtext
224
+
225
+
226
+ def test_quiet_drops_the_default_pipeline_line_only(fake, doc, capsysbinary):
227
+ fake.scripted[("GET", "/v1/catalog")] = [(200, {}, {"pipelines": [{"pipeline": "p@1", "available": True, "price_per_page_millicents": 100}]})]
228
+ job_flow(fake, result=b"x")
229
+ code, _, errtext = run([doc, "-q"], capsysbinary)
230
+ assert code == 0 and errtext == ""
231
+ job_flow(fake, result=b"x")
232
+ code, _, errtext = run([doc], capsysbinary)
233
+ assert code == 0 and "no --pipeline given, using p@1" in errtext
234
+
235
+
236
+ def test_output_to_the_null_device_is_accepted_and_discards(fake, doc, capsysbinary):
237
+ job_flow(fake, result=b"gone")
238
+ code, out, errtext = run([doc, "--pipeline", "p@1", "-o", os.devnull], capsysbinary)
239
+ assert (code, out, errtext) == (0, b"", "")
240
+
241
+
242
+ @pytest.mark.parametrize("target", [os.devnull, "/dev/null"])
243
+ def test_both_null_device_spellings_discard_on_every_system(fake, doc, capsysbinary, target):
244
+ job_flow(fake, result=b"gone")
245
+ code, out, errtext = run([doc, "--pipeline", "p@1", "-o", target], capsysbinary)
246
+ assert (code, out, errtext) == (0, b"", "")
247
+
248
+
249
+ DPI_WARNING = {"code": "dpi_ignored", "message": "dpi applies to the pages of a PDF."}
250
+ DRY_WITH_WARNING = {"dry_run": True, "pages": 1, "price_per_page_millicents": 100, "price_millicents": 100, "max_charge_millicents": 0,
251
+ "pages_covered": 1, "warnings": [DPI_WARNING]}
252
+
253
+
254
+ def test_dry_run_shows_every_warning_as_its_own_stderr_line(fake, doc, capsysbinary):
255
+ fake.scripted[("POST", "/v1/jobs")] = [(200, {}, dict(DRY_WITH_WARNING, warnings=[DPI_WARNING, {"code": "job_budget", "message": "covers 1 of 2"}]))]
256
+ code, out, errtext = run([doc, "--pipeline", "a@1", "--dry-run", "--dpi", "300"], capsysbinary)
257
+ assert code == 0 and b"1 page with a@1" in out
258
+ assert errtext.splitlines() == ["ocr-software: warning dpi_ignored: dpi applies to the pages of a PDF.",
259
+ "ocr-software: warning job_budget: covers 1 of 2"]
260
+
261
+
262
+ def test_a_real_job_shows_the_warnings_of_the_answer_in_one_upload(fake, doc, capsysbinary):
263
+ job_flow(fake, result=b"text")
264
+ fake.scripted[("POST", "/v1/jobs")] = [(202, {}, {"job_id": "j1", "state": "running", "warnings": [DPI_WARNING]})]
265
+ code, out, errtext = run([doc, "--pipeline", "a@1", "--dpi", "300", "--job-budget", "1"], capsysbinary)
266
+ assert code == 0 and out == b"text" and errtext == "ocr-software: warning dpi_ignored: dpi applies to the pages of a PDF.\n"
267
+ posts = [c for c in fake.log if c["method"] == "POST" and "/result" not in c["path"]]
268
+ assert len(posts) == 1 and "dry_run" not in posts[0]["query"]
269
+
270
+
271
+ def test_an_older_service_without_warnings_breaks_nothing(fake, doc, capsysbinary):
272
+ job_flow(fake, result=b"text")
273
+ assert run([doc, "--pipeline", "a@1", "--dpi", "300"], capsysbinary) == (0, b"text", "")
274
+
275
+
276
+ def test_a_plain_real_job_stays_one_upload(fake, doc, capsysbinary):
277
+ job_flow(fake)
278
+ run([doc, "--pipeline", "a@1"], capsysbinary)
279
+ assert sum(1 for c in fake.log if c["method"] == "POST" and "/result" not in c["path"]) == 1
@@ -510,3 +510,13 @@ def test_split_pages_text_and_markdown():
510
510
  md = "## Page 1\n\nfirst\n\n## Page 2\n\n## Page 7\n\nnot a page\n\n## Page 3\n\nlast\n"
511
511
  assert split_pages(md, "md") == [(1, "first\n"), (2, "## Page 7\n\nnot a page\n"), (3, "last\n")]
512
512
  assert split_pages("no headings", "md") == [(1, "no headings")]
513
+
514
+
515
+ def test_both_names_set_the_ocr_software_name_wins_for_key_url_and_folder(monkeypatch):
516
+ for short, long_, bad, good in (("OCR_KEY", "OCR_SOFTWARE_API_KEY", "ocr_bogus", "ocr_good"),
517
+ ("OCR_BASE_URL", "OCR_SOFTWARE_BASE_URL", "http://bad:1", "http://good:1"),
518
+ ("OCR_ALLOWED_DIR", "OCR_SOFTWARE_ALLOWED_DIR", "/bad", "/good")):
519
+ monkeypatch.setenv(short, bad)
520
+ monkeypatch.setenv(long_, good)
521
+ c = Client()
522
+ assert (c._key, c.base_url, c.allowed_dir) == ("ocr_good", "http://good:1", "/good")
@@ -16,6 +16,8 @@ def test_readme_file_exists_and_no_runtime_dependencies():
16
16
  pyproject = (ROOT / "pyproject.toml").read_text(encoding="utf-8")
17
17
  assert (ROOT / "INSTALL.txt").is_file() and 'file = "INSTALL.txt"' in pyproject
18
18
  assert "dependencies = []" in pyproject # standard library only
19
+ # 13.11.2026: as text/plain PyPI showed one fixed-width block whose long lines ran off the page
20
+ assert 'content-type = "text/markdown"' in pyproject
19
21
 
20
22
 
21
23
  def test_readme_that_pypi_shows_has_no_draft_note():
@@ -1,71 +0,0 @@
1
- ocr-software: Python client and command line for the ocr-software.com OCR API (MIT, standard library only, Python 3.9 or newer).
2
-
3
- Install:
4
- pipx install ocr-software command line only, in its own environment
5
- pip install ocr-software client library, inside a venv
6
- uvx ocr-software --version run once without installing (uv)
7
- First call (list the pipelines, then read one file):
8
- ocr-software --list-pipelines
9
- ocr-software scan.pdf --pipeline scan-ctc@2 -o scan.txt
10
-
11
- Settings (environment; the command line flag wins):
12
- OCR_SOFTWARE_API_KEY API key (starts with ocr_). OCR_KEY is read too, it is the name used in the API docs.
13
- OCR_SOFTWARE_BASE_URL default https://api.ocr-software.com ; own server e.g. http://127.0.0.1:8080 (also --base-url). OCR_BASE_URL is read too.
14
- OCR_SOFTWARE_ALLOWED_DIR optional: read documents only from below this folder (symlinks and .. do not escape)
15
- The key is never printed and never part of an error message. Without --pipeline the default is used: the fastest and cheapest available pipeline (no previews). It is named on stderr; --list-pipelines marks it. In Python: client.ocr(path, client.default_pipeline()).
16
-
17
- Command line:
18
- ocr-software --list-pipelines available pipelines, one per line: name@version, tab, what it delivers and costs; the default one on stderr (no key needed)
19
- ocr-software scan.pdf --pipeline NAME@VERSION --dry-run page count and price in euros, nothing runs, nothing is billed
20
- ocr-software scan.pdf text to stdout, with the default pipeline (named on stderr)
21
- ocr-software scan.pdf --pipeline NAME@VERSION text to stdout
22
- ocr-software scan.pdf -o scan.md --pipeline NAME@VERSION --format md
23
- ocr-software scan.pdf -o scan.pdf.txt --pipeline NAME@VERSION
24
- ocr-software scan.pdf -o searchable.pdf --pipeline NAME@VERSION --format pdf searchable PDF, -o is required
25
- ocr-software scan.pdf --pipeline NAME@VERSION --format json > scan.json
26
- ocr-software scan.pdf --pipeline NAME@VERSION --base-url http://10.0.0.5:8080 your own server
27
- python -m ocr_software ... the same command without the script on PATH
28
- Options: --format text (default) | md | json | hocr | pdf ; --dpi 150|200|300 (PDF render resolution) ;
29
- --timeout SECONDS (wait for the job, default 3600) ; --job-budget MILLICENTS (cap; ignored where billing is off) ; --dry-run ; --version
30
- Without -o the result goes to stdout as UTF-8 (not for pdf). With -o the file appears whole or not at all, an existing
31
- file is replaced, and the input file itself is refused as output.
32
-
33
- Exit codes:
34
- 0 done, every page read
35
- 1 error from the service or the network (message on stderr with the API error code, for example 401 unauthorized);
36
- also an address that does not answer: a typo and an outage look the same, so a script may retry this
37
- 2 wrong call: unreadable or unsupported input, output not writable, no key, malformed address (not http/https)
38
- 3 the job is done but a page failed or did not run, or the job stopped early: the result is written anyway,
39
- stderr says what is missing and names the job; treat the file as incomplete
40
- 4 the job did not finish within --timeout; it keeps running, stderr names the job
41
-
42
- Python:
43
- from ocr_software import Client, PagesUnread
44
- client = Client() # key and address from the environment
45
- text = client.ocr("scan.pdf", "NAME@VERSION", "md") # bytes; raises PagesUnread (with .data) if a page was not read
46
- pdf = client.ocr("scan.pdf", "NAME@VERSION", "pdf") # searchable PDF
47
- job = client.submit("scan.pdf", "NAME@VERSION") # step by step: submit, wait, result
48
- client.wait(job["job_id"]); client.result(job["job_id"], "json")
49
- client.quote("scan.pdf", "NAME@VERSION") # dry run: page count and price, nothing runs
50
- from ocr_software import price_sentence; price_sentence(q) # the same sentence as --dry-run, in euros
51
- Price: the API counts money in millicents, 100,000 millicents = 1 EUR (a page of the cheapest pipeline costs 100 = EUR 0.001).
52
- Today nothing is charged ("you pay EUR 0.00 today"); the list price is shown for information. Free use has limits:
53
- https://ocr-software.com/legal/fair-usage/ ; for more volume write to info@ocr-software.com.
54
- Word boxes (json): box = [x0, y0, x1, y1] in pixels of the page image, origin top left; width, height and dpi of the page say which
55
- image. In PDF points: pixels * 72 / dpi. Per word, verified is true only where a second reader confirmed it (scan-vlm-ctc@2);
56
- safe_ok is always false today. Judge by confidence (0 to 1). Every field: https://ocr-software.com/docs/reference/
57
- Retries follow the API rules: 429, 500, 502, 503, 504 and network errors up to 5 times (waits 1, 2, 4, 8, 16 s or Retry-After). An
58
- upload the service cuts off (it does that beyond 2 uploads at once per key, instead of answering 429) counts like 429. If no connection
59
- can be made at all (wrong address, host down) it gives up after 2 retries and names the address: a few seconds, up to about 10 s on
60
- Windows, where a refused port itself takes 2 s per try (connect timeout 10 s). A Retry-After over 60 s (locked_out) is not waited for. A submit carries an Idempotency-Key, so a retry never creates a second job.
61
- Every document is checked locally first: PDF, PNG, JPEG or TIFF by its first bytes, at most 100 MiB.
62
-
63
- Paperless-ngx, pre-consume script (sketch, not tried against a real Paperless; a non-PDF input keeps its file name but becomes a PDF):
64
- #!/bin/sh
65
- ocr-software "$DOCUMENT_WORKING_PATH" --pipeline NAME@VERSION --format pdf -o "$DOCUMENT_WORKING_PATH.ocr" && mv "$DOCUMENT_WORKING_PATH.ocr" "$DOCUMENT_WORKING_PATH"
66
- with OCR_SOFTWARE_API_KEY and OCR_SOFTWARE_BASE_URL set in the environment of the Paperless container.
67
-
68
- Tests (fake local service, no network, no key):
69
- python -m venv .venv
70
- .venv/bin/pip install pytest (Windows: .venv\Scripts\pip install pytest)
71
- .venv/bin/python -m pytest -p no:cacheprovider . (Windows: .venv\Scripts\python -m pytest -p no:cacheprovider .)
@@ -1,88 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: ocr-software
3
- Version: 0.1.0
4
- Summary: Client and command line for the ocr-software.com OCR API: text, word boxes, layout, searchable PDF.
5
- License-Expression: MIT
6
- Project-URL: Homepage, https://ocr-software.com/
7
- Project-URL: Repository, https://github.com/ocr-software/ocr-software-python
8
- Keywords: ocr,pdf,searchable-pdf,hocr,markdown,document-ai
9
- Classifier: Programming Language :: Python :: 3
10
- Classifier: Topic :: Scientific/Engineering :: Image Recognition
11
- Requires-Python: >=3.9
12
- Description-Content-Type: text/plain
13
- License-File: LICENSE
14
- Provides-Extra: test
15
- Requires-Dist: pytest==9.0.2; extra == "test"
16
- Dynamic: license-file
17
-
18
- ocr-software: Python client and command line for the ocr-software.com OCR API (MIT, standard library only, Python 3.9 or newer).
19
-
20
- Install:
21
- pipx install ocr-software command line only, in its own environment
22
- pip install ocr-software client library, inside a venv
23
- uvx ocr-software --version run once without installing (uv)
24
- First call (list the pipelines, then read one file):
25
- ocr-software --list-pipelines
26
- ocr-software scan.pdf --pipeline scan-ctc@2 -o scan.txt
27
-
28
- Settings (environment; the command line flag wins):
29
- OCR_SOFTWARE_API_KEY API key (starts with ocr_). OCR_KEY is read too, it is the name used in the API docs.
30
- OCR_SOFTWARE_BASE_URL default https://api.ocr-software.com ; own server e.g. http://127.0.0.1:8080 (also --base-url). OCR_BASE_URL is read too.
31
- OCR_SOFTWARE_ALLOWED_DIR optional: read documents only from below this folder (symlinks and .. do not escape)
32
- The key is never printed and never part of an error message. Without --pipeline the default is used: the fastest and cheapest available pipeline (no previews). It is named on stderr; --list-pipelines marks it. In Python: client.ocr(path, client.default_pipeline()).
33
-
34
- Command line:
35
- ocr-software --list-pipelines available pipelines, one per line: name@version, tab, what it delivers and costs; the default one on stderr (no key needed)
36
- ocr-software scan.pdf --pipeline NAME@VERSION --dry-run page count and price in euros, nothing runs, nothing is billed
37
- ocr-software scan.pdf text to stdout, with the default pipeline (named on stderr)
38
- ocr-software scan.pdf --pipeline NAME@VERSION text to stdout
39
- ocr-software scan.pdf -o scan.md --pipeline NAME@VERSION --format md
40
- ocr-software scan.pdf -o scan.pdf.txt --pipeline NAME@VERSION
41
- ocr-software scan.pdf -o searchable.pdf --pipeline NAME@VERSION --format pdf searchable PDF, -o is required
42
- ocr-software scan.pdf --pipeline NAME@VERSION --format json > scan.json
43
- ocr-software scan.pdf --pipeline NAME@VERSION --base-url http://10.0.0.5:8080 your own server
44
- python -m ocr_software ... the same command without the script on PATH
45
- Options: --format text (default) | md | json | hocr | pdf ; --dpi 150|200|300 (PDF render resolution) ;
46
- --timeout SECONDS (wait for the job, default 3600) ; --job-budget MILLICENTS (cap; ignored where billing is off) ; --dry-run ; --version
47
- Without -o the result goes to stdout as UTF-8 (not for pdf). With -o the file appears whole or not at all, an existing
48
- file is replaced, and the input file itself is refused as output.
49
-
50
- Exit codes:
51
- 0 done, every page read
52
- 1 error from the service or the network (message on stderr with the API error code, for example 401 unauthorized);
53
- also an address that does not answer: a typo and an outage look the same, so a script may retry this
54
- 2 wrong call: unreadable or unsupported input, output not writable, no key, malformed address (not http/https)
55
- 3 the job is done but a page failed or did not run, or the job stopped early: the result is written anyway,
56
- stderr says what is missing and names the job; treat the file as incomplete
57
- 4 the job did not finish within --timeout; it keeps running, stderr names the job
58
-
59
- Python:
60
- from ocr_software import Client, PagesUnread
61
- client = Client() # key and address from the environment
62
- text = client.ocr("scan.pdf", "NAME@VERSION", "md") # bytes; raises PagesUnread (with .data) if a page was not read
63
- pdf = client.ocr("scan.pdf", "NAME@VERSION", "pdf") # searchable PDF
64
- job = client.submit("scan.pdf", "NAME@VERSION") # step by step: submit, wait, result
65
- client.wait(job["job_id"]); client.result(job["job_id"], "json")
66
- client.quote("scan.pdf", "NAME@VERSION") # dry run: page count and price, nothing runs
67
- from ocr_software import price_sentence; price_sentence(q) # the same sentence as --dry-run, in euros
68
- Price: the API counts money in millicents, 100,000 millicents = 1 EUR (a page of the cheapest pipeline costs 100 = EUR 0.001).
69
- Today nothing is charged ("you pay EUR 0.00 today"); the list price is shown for information. Free use has limits:
70
- https://ocr-software.com/legal/fair-usage/ ; for more volume write to info@ocr-software.com.
71
- Word boxes (json): box = [x0, y0, x1, y1] in pixels of the page image, origin top left; width, height and dpi of the page say which
72
- image. In PDF points: pixels * 72 / dpi. Per word, verified is true only where a second reader confirmed it (scan-vlm-ctc@2);
73
- safe_ok is always false today. Judge by confidence (0 to 1). Every field: https://ocr-software.com/docs/reference/
74
- Retries follow the API rules: 429, 500, 502, 503, 504 and network errors up to 5 times (waits 1, 2, 4, 8, 16 s or Retry-After). An
75
- upload the service cuts off (it does that beyond 2 uploads at once per key, instead of answering 429) counts like 429. If no connection
76
- can be made at all (wrong address, host down) it gives up after 2 retries and names the address: a few seconds, up to about 10 s on
77
- Windows, where a refused port itself takes 2 s per try (connect timeout 10 s). A Retry-After over 60 s (locked_out) is not waited for. A submit carries an Idempotency-Key, so a retry never creates a second job.
78
- Every document is checked locally first: PDF, PNG, JPEG or TIFF by its first bytes, at most 100 MiB.
79
-
80
- Paperless-ngx, pre-consume script (sketch, not tried against a real Paperless; a non-PDF input keeps its file name but becomes a PDF):
81
- #!/bin/sh
82
- ocr-software "$DOCUMENT_WORKING_PATH" --pipeline NAME@VERSION --format pdf -o "$DOCUMENT_WORKING_PATH.ocr" && mv "$DOCUMENT_WORKING_PATH.ocr" "$DOCUMENT_WORKING_PATH"
83
- with OCR_SOFTWARE_API_KEY and OCR_SOFTWARE_BASE_URL set in the environment of the Paperless container.
84
-
85
- Tests (fake local service, no network, no key):
86
- python -m venv .venv
87
- .venv/bin/pip install pytest (Windows: .venv\Scripts\pip install pytest)
88
- .venv/bin/python -m pytest -p no:cacheprovider . (Windows: .venv\Scripts\python -m pytest -p no:cacheprovider .)
@@ -1,71 +0,0 @@
1
- ocr-software: Python client and command line for the ocr-software.com OCR API (MIT, standard library only, Python 3.9 or newer).
2
-
3
- Install:
4
- pipx install ocr-software command line only, in its own environment
5
- pip install ocr-software client library, inside a venv
6
- uvx ocr-software --version run once without installing (uv)
7
- First call (list the pipelines, then read one file):
8
- ocr-software --list-pipelines
9
- ocr-software scan.pdf --pipeline scan-ctc@2 -o scan.txt
10
-
11
- Settings (environment; the command line flag wins):
12
- OCR_SOFTWARE_API_KEY API key (starts with ocr_). OCR_KEY is read too, it is the name used in the API docs.
13
- OCR_SOFTWARE_BASE_URL default https://api.ocr-software.com ; own server e.g. http://127.0.0.1:8080 (also --base-url). OCR_BASE_URL is read too.
14
- OCR_SOFTWARE_ALLOWED_DIR optional: read documents only from below this folder (symlinks and .. do not escape)
15
- The key is never printed and never part of an error message. Without --pipeline the default is used: the fastest and cheapest available pipeline (no previews). It is named on stderr; --list-pipelines marks it. In Python: client.ocr(path, client.default_pipeline()).
16
-
17
- Command line:
18
- ocr-software --list-pipelines available pipelines, one per line: name@version, tab, what it delivers and costs; the default one on stderr (no key needed)
19
- ocr-software scan.pdf --pipeline NAME@VERSION --dry-run page count and price in euros, nothing runs, nothing is billed
20
- ocr-software scan.pdf text to stdout, with the default pipeline (named on stderr)
21
- ocr-software scan.pdf --pipeline NAME@VERSION text to stdout
22
- ocr-software scan.pdf -o scan.md --pipeline NAME@VERSION --format md
23
- ocr-software scan.pdf -o scan.pdf.txt --pipeline NAME@VERSION
24
- ocr-software scan.pdf -o searchable.pdf --pipeline NAME@VERSION --format pdf searchable PDF, -o is required
25
- ocr-software scan.pdf --pipeline NAME@VERSION --format json > scan.json
26
- ocr-software scan.pdf --pipeline NAME@VERSION --base-url http://10.0.0.5:8080 your own server
27
- python -m ocr_software ... the same command without the script on PATH
28
- Options: --format text (default) | md | json | hocr | pdf ; --dpi 150|200|300 (PDF render resolution) ;
29
- --timeout SECONDS (wait for the job, default 3600) ; --job-budget MILLICENTS (cap; ignored where billing is off) ; --dry-run ; --version
30
- Without -o the result goes to stdout as UTF-8 (not for pdf). With -o the file appears whole or not at all, an existing
31
- file is replaced, and the input file itself is refused as output.
32
-
33
- Exit codes:
34
- 0 done, every page read
35
- 1 error from the service or the network (message on stderr with the API error code, for example 401 unauthorized);
36
- also an address that does not answer: a typo and an outage look the same, so a script may retry this
37
- 2 wrong call: unreadable or unsupported input, output not writable, no key, malformed address (not http/https)
38
- 3 the job is done but a page failed or did not run, or the job stopped early: the result is written anyway,
39
- stderr says what is missing and names the job; treat the file as incomplete
40
- 4 the job did not finish within --timeout; it keeps running, stderr names the job
41
-
42
- Python:
43
- from ocr_software import Client, PagesUnread
44
- client = Client() # key and address from the environment
45
- text = client.ocr("scan.pdf", "NAME@VERSION", "md") # bytes; raises PagesUnread (with .data) if a page was not read
46
- pdf = client.ocr("scan.pdf", "NAME@VERSION", "pdf") # searchable PDF
47
- job = client.submit("scan.pdf", "NAME@VERSION") # step by step: submit, wait, result
48
- client.wait(job["job_id"]); client.result(job["job_id"], "json")
49
- client.quote("scan.pdf", "NAME@VERSION") # dry run: page count and price, nothing runs
50
- from ocr_software import price_sentence; price_sentence(q) # the same sentence as --dry-run, in euros
51
- Price: the API counts money in millicents, 100,000 millicents = 1 EUR (a page of the cheapest pipeline costs 100 = EUR 0.001).
52
- Today nothing is charged ("you pay EUR 0.00 today"); the list price is shown for information. Free use has limits:
53
- https://ocr-software.com/legal/fair-usage/ ; for more volume write to info@ocr-software.com.
54
- Word boxes (json): box = [x0, y0, x1, y1] in pixels of the page image, origin top left; width, height and dpi of the page say which
55
- image. In PDF points: pixels * 72 / dpi. Per word, verified is true only where a second reader confirmed it (scan-vlm-ctc@2);
56
- safe_ok is always false today. Judge by confidence (0 to 1). Every field: https://ocr-software.com/docs/reference/
57
- Retries follow the API rules: 429, 500, 502, 503, 504 and network errors up to 5 times (waits 1, 2, 4, 8, 16 s or Retry-After). An
58
- upload the service cuts off (it does that beyond 2 uploads at once per key, instead of answering 429) counts like 429. If no connection
59
- can be made at all (wrong address, host down) it gives up after 2 retries and names the address: a few seconds, up to about 10 s on
60
- Windows, where a refused port itself takes 2 s per try (connect timeout 10 s). A Retry-After over 60 s (locked_out) is not waited for. A submit carries an Idempotency-Key, so a retry never creates a second job.
61
- Every document is checked locally first: PDF, PNG, JPEG or TIFF by its first bytes, at most 100 MiB.
62
-
63
- Paperless-ngx, pre-consume script (sketch, not tried against a real Paperless; a non-PDF input keeps its file name but becomes a PDF):
64
- #!/bin/sh
65
- ocr-software "$DOCUMENT_WORKING_PATH" --pipeline NAME@VERSION --format pdf -o "$DOCUMENT_WORKING_PATH.ocr" && mv "$DOCUMENT_WORKING_PATH.ocr" "$DOCUMENT_WORKING_PATH"
66
- with OCR_SOFTWARE_API_KEY and OCR_SOFTWARE_BASE_URL set in the environment of the Paperless container.
67
-
68
- Tests (fake local service, no network, no key):
69
- python -m venv .venv
70
- .venv/bin/pip install pytest (Windows: .venv\Scripts\pip install pytest)
71
- .venv/bin/python -m pytest -p no:cacheprovider . (Windows: .venv\Scripts\python -m pytest -p no:cacheprovider .)
@@ -1,88 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: ocr-software
3
- Version: 0.1.0
4
- Summary: Client and command line for the ocr-software.com OCR API: text, word boxes, layout, searchable PDF.
5
- License-Expression: MIT
6
- Project-URL: Homepage, https://ocr-software.com/
7
- Project-URL: Repository, https://github.com/ocr-software/ocr-software-python
8
- Keywords: ocr,pdf,searchable-pdf,hocr,markdown,document-ai
9
- Classifier: Programming Language :: Python :: 3
10
- Classifier: Topic :: Scientific/Engineering :: Image Recognition
11
- Requires-Python: >=3.9
12
- Description-Content-Type: text/plain
13
- License-File: LICENSE
14
- Provides-Extra: test
15
- Requires-Dist: pytest==9.0.2; extra == "test"
16
- Dynamic: license-file
17
-
18
- ocr-software: Python client and command line for the ocr-software.com OCR API (MIT, standard library only, Python 3.9 or newer).
19
-
20
- Install:
21
- pipx install ocr-software command line only, in its own environment
22
- pip install ocr-software client library, inside a venv
23
- uvx ocr-software --version run once without installing (uv)
24
- First call (list the pipelines, then read one file):
25
- ocr-software --list-pipelines
26
- ocr-software scan.pdf --pipeline scan-ctc@2 -o scan.txt
27
-
28
- Settings (environment; the command line flag wins):
29
- OCR_SOFTWARE_API_KEY API key (starts with ocr_). OCR_KEY is read too, it is the name used in the API docs.
30
- OCR_SOFTWARE_BASE_URL default https://api.ocr-software.com ; own server e.g. http://127.0.0.1:8080 (also --base-url). OCR_BASE_URL is read too.
31
- OCR_SOFTWARE_ALLOWED_DIR optional: read documents only from below this folder (symlinks and .. do not escape)
32
- The key is never printed and never part of an error message. Without --pipeline the default is used: the fastest and cheapest available pipeline (no previews). It is named on stderr; --list-pipelines marks it. In Python: client.ocr(path, client.default_pipeline()).
33
-
34
- Command line:
35
- ocr-software --list-pipelines available pipelines, one per line: name@version, tab, what it delivers and costs; the default one on stderr (no key needed)
36
- ocr-software scan.pdf --pipeline NAME@VERSION --dry-run page count and price in euros, nothing runs, nothing is billed
37
- ocr-software scan.pdf text to stdout, with the default pipeline (named on stderr)
38
- ocr-software scan.pdf --pipeline NAME@VERSION text to stdout
39
- ocr-software scan.pdf -o scan.md --pipeline NAME@VERSION --format md
40
- ocr-software scan.pdf -o scan.pdf.txt --pipeline NAME@VERSION
41
- ocr-software scan.pdf -o searchable.pdf --pipeline NAME@VERSION --format pdf searchable PDF, -o is required
42
- ocr-software scan.pdf --pipeline NAME@VERSION --format json > scan.json
43
- ocr-software scan.pdf --pipeline NAME@VERSION --base-url http://10.0.0.5:8080 your own server
44
- python -m ocr_software ... the same command without the script on PATH
45
- Options: --format text (default) | md | json | hocr | pdf ; --dpi 150|200|300 (PDF render resolution) ;
46
- --timeout SECONDS (wait for the job, default 3600) ; --job-budget MILLICENTS (cap; ignored where billing is off) ; --dry-run ; --version
47
- Without -o the result goes to stdout as UTF-8 (not for pdf). With -o the file appears whole or not at all, an existing
48
- file is replaced, and the input file itself is refused as output.
49
-
50
- Exit codes:
51
- 0 done, every page read
52
- 1 error from the service or the network (message on stderr with the API error code, for example 401 unauthorized);
53
- also an address that does not answer: a typo and an outage look the same, so a script may retry this
54
- 2 wrong call: unreadable or unsupported input, output not writable, no key, malformed address (not http/https)
55
- 3 the job is done but a page failed or did not run, or the job stopped early: the result is written anyway,
56
- stderr says what is missing and names the job; treat the file as incomplete
57
- 4 the job did not finish within --timeout; it keeps running, stderr names the job
58
-
59
- Python:
60
- from ocr_software import Client, PagesUnread
61
- client = Client() # key and address from the environment
62
- text = client.ocr("scan.pdf", "NAME@VERSION", "md") # bytes; raises PagesUnread (with .data) if a page was not read
63
- pdf = client.ocr("scan.pdf", "NAME@VERSION", "pdf") # searchable PDF
64
- job = client.submit("scan.pdf", "NAME@VERSION") # step by step: submit, wait, result
65
- client.wait(job["job_id"]); client.result(job["job_id"], "json")
66
- client.quote("scan.pdf", "NAME@VERSION") # dry run: page count and price, nothing runs
67
- from ocr_software import price_sentence; price_sentence(q) # the same sentence as --dry-run, in euros
68
- Price: the API counts money in millicents, 100,000 millicents = 1 EUR (a page of the cheapest pipeline costs 100 = EUR 0.001).
69
- Today nothing is charged ("you pay EUR 0.00 today"); the list price is shown for information. Free use has limits:
70
- https://ocr-software.com/legal/fair-usage/ ; for more volume write to info@ocr-software.com.
71
- Word boxes (json): box = [x0, y0, x1, y1] in pixels of the page image, origin top left; width, height and dpi of the page say which
72
- image. In PDF points: pixels * 72 / dpi. Per word, verified is true only where a second reader confirmed it (scan-vlm-ctc@2);
73
- safe_ok is always false today. Judge by confidence (0 to 1). Every field: https://ocr-software.com/docs/reference/
74
- Retries follow the API rules: 429, 500, 502, 503, 504 and network errors up to 5 times (waits 1, 2, 4, 8, 16 s or Retry-After). An
75
- upload the service cuts off (it does that beyond 2 uploads at once per key, instead of answering 429) counts like 429. If no connection
76
- can be made at all (wrong address, host down) it gives up after 2 retries and names the address: a few seconds, up to about 10 s on
77
- Windows, where a refused port itself takes 2 s per try (connect timeout 10 s). A Retry-After over 60 s (locked_out) is not waited for. A submit carries an Idempotency-Key, so a retry never creates a second job.
78
- Every document is checked locally first: PDF, PNG, JPEG or TIFF by its first bytes, at most 100 MiB.
79
-
80
- Paperless-ngx, pre-consume script (sketch, not tried against a real Paperless; a non-PDF input keeps its file name but becomes a PDF):
81
- #!/bin/sh
82
- ocr-software "$DOCUMENT_WORKING_PATH" --pipeline NAME@VERSION --format pdf -o "$DOCUMENT_WORKING_PATH.ocr" && mv "$DOCUMENT_WORKING_PATH.ocr" "$DOCUMENT_WORKING_PATH"
83
- with OCR_SOFTWARE_API_KEY and OCR_SOFTWARE_BASE_URL set in the environment of the Paperless container.
84
-
85
- Tests (fake local service, no network, no key):
86
- python -m venv .venv
87
- .venv/bin/pip install pytest (Windows: .venv\Scripts\pip install pytest)
88
- .venv/bin/python -m pytest -p no:cacheprovider . (Windows: .venv\Scripts\python -m pytest -p no:cacheprovider .)
File without changes
File without changes