likhit 0.1.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.
- likhit-0.1.0/PKG-INFO +104 -0
- likhit-0.1.0/README.md +85 -0
- likhit-0.1.0/pyproject.toml +31 -0
- likhit-0.1.0/src/likhit/__init__.py +6 -0
- likhit-0.1.0/src/likhit/cli.py +84 -0
- likhit-0.1.0/src/likhit/core.py +126 -0
- likhit-0.1.0/src/likhit/errors.py +13 -0
- likhit-0.1.0/src/likhit/extractors/__init__.py +5 -0
- likhit-0.1.0/src/likhit/extractors/base.py +45 -0
- likhit-0.1.0/src/likhit/extractors/font_based.py +261 -0
- likhit-0.1.0/src/likhit/extractors/font_classifier.py +50 -0
- likhit-0.1.0/src/likhit/extractors/kalimati.py +501 -0
- likhit-0.1.0/src/likhit/extractors/legacy_maps.py +68 -0
- likhit-0.1.0/src/likhit/extractors/tables.py +291 -0
- likhit-0.1.0/src/likhit/handlers/__init__.py +6 -0
- likhit-0.1.0/src/likhit/handlers/base.py +22 -0
- likhit-0.1.0/src/likhit/handlers/ciaa_press_release.py +363 -0
- likhit-0.1.0/src/likhit/handlers/content_blocks.py +143 -0
- likhit-0.1.0/src/likhit/handlers/kanun_patrika.py +201 -0
- likhit-0.1.0/src/likhit/markdown_assembly.py +94 -0
- likhit-0.1.0/src/likhit/markitdown_integration.py +94 -0
- likhit-0.1.0/src/likhit/models/__init__.py +25 -0
- likhit-0.1.0/src/likhit/models/repair_types.py +19 -0
- likhit-0.1.0/src/likhit/models/types.py +132 -0
- likhit-0.1.0/src/likhit/nepali_pdf_repair.py +324 -0
- likhit-0.1.0/src/likhit/renderers/__init__.py +5 -0
- likhit-0.1.0/src/likhit/renderers/base.py +15 -0
- likhit-0.1.0/src/likhit/renderers/markdown.py +649 -0
- likhit-0.1.0/src/likhit/utils/__init__.py +0 -0
- likhit-0.1.0/src/likhit/version.py +3 -0
likhit-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: likhit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Nepali PDF-to-Markdown converter powered by MarkItDown
|
|
5
|
+
Author: notashwinii
|
|
6
|
+
Author-email: ashwinii.subedi@gmail.com
|
|
7
|
+
Requires-Python: >=3.12,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
12
|
+
Requires-Dist: fonttools (>=4.47.0,<5.0.0)
|
|
13
|
+
Requires-Dist: markitdown[pdf] (==0.1.5)
|
|
14
|
+
Requires-Dist: npttf2utf (>=0.3.7,<1.0.0)
|
|
15
|
+
Requires-Dist: pymupdf (==1.27.2)
|
|
16
|
+
Requires-Dist: pyyaml (>=6.0,<7.0)
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# likhit
|
|
20
|
+
|
|
21
|
+
`likhit` is a public PDF-to-Markdown tool for Nepali government documents.
|
|
22
|
+
|
|
23
|
+
The default path is powered by [MarkItDown](https://github.com/microsoft/markitdown), with `likhit` intercepting born-digital Nepali PDFs that need Nepal-specific repair before Markdown is emitted. That repair layer handles Kalimati broken-CMap fixes, Devanagari reordering and spacing normalization, and legacy Nepali font remapping where applicable.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
### With Poetry
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
poetry install
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Run project commands with `poetry run`:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
poetry run pytest
|
|
37
|
+
poetry run ruff check .
|
|
38
|
+
poetry run black --check .
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Recommended Usage
|
|
42
|
+
|
|
43
|
+
Convert a single PDF to editable Markdown:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
poetry run likhit convert path/to/document.pdf --out path/to/document.md
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Convert multiple PDFs at once:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
poetry run likhit convert path/to/a.pdf path/to/b.pdf --out-dir path/to/output-dir
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
If `--out` or `--out-dir` is omitted, `likhit` writes Markdown files in the current directory using the input filename stem.
|
|
56
|
+
|
|
57
|
+
## Usage
|
|
58
|
+
|
|
59
|
+
`convert` is the public path.
|
|
60
|
+
|
|
61
|
+
- Input scope: born-digital PDFs only
|
|
62
|
+
- Output: generic editable Markdown
|
|
63
|
+
- Engine: MarkItDown by default
|
|
64
|
+
- `likhit` value-add: Nepali PDF repair before Markdown output when needed
|
|
65
|
+
- Recognized document layouts such as Kanun Patrika and CIAA-style PDFs are auto-detected internally so `likhit` can preserve better text order and structure without a `--type` flag
|
|
66
|
+
- No OCR support is included in this branch
|
|
67
|
+
|
|
68
|
+
## Architecture
|
|
69
|
+
|
|
70
|
+
The new default pipeline is:
|
|
71
|
+
|
|
72
|
+
1. `likhit convert` opens the PDF and checks whether it matches a known structure-aware document type.
|
|
73
|
+
2. If the PDF matches a known layout such as Kanun Patrika or a CIAA-style document, `likhit` reuses its existing structure-aware extraction logic internally.
|
|
74
|
+
3. Otherwise, MarkItDown handles the default conversion path.
|
|
75
|
+
4. When the PDF needs Nepali repair, `likhit` repairs the text first:
|
|
76
|
+
- Kalimati broken-CMap repair
|
|
77
|
+
- Devanagari reordering
|
|
78
|
+
- Devanagari spacing normalization
|
|
79
|
+
- Legacy-font remapping through `npttf2utf`
|
|
80
|
+
5. `likhit` assembles repaired text blocks into Markdown.
|
|
81
|
+
|
|
82
|
+
This keeps the public product story simple: `likhit` is the tool users call, while MarkItDown is embedded infrastructure.
|
|
83
|
+
|
|
84
|
+
## Current Scope
|
|
85
|
+
|
|
86
|
+
- Supported default input: PDF only
|
|
87
|
+
- Supported default output: Markdown only
|
|
88
|
+
- Supported default document class: born-digital PDFs
|
|
89
|
+
- Unsupported in this branch: OCR, scanned/image-only PDFs, `.doc`, `.docx`, and image inputs
|
|
90
|
+
|
|
91
|
+
## Project Layout
|
|
92
|
+
|
|
93
|
+
- `src/likhit/core.py`: public `convert` and `convert_many` entry points
|
|
94
|
+
- `src/likhit/markitdown_integration.py`: MarkItDown instance setup and custom PDF converter
|
|
95
|
+
- `src/likhit/nepali_pdf_repair.py`: reusable Nepal-specific PDF repair layer
|
|
96
|
+
- `src/likhit/markdown_assembly.py`: generic Markdown assembly for the default conversion path
|
|
97
|
+
- `src/likhit/extractors/`, `src/likhit/handlers/`, `src/likhit/renderers/`: internal Nepali PDF repair and legacy extraction internals
|
|
98
|
+
- `tests/`: conversion, extraction, and CLI coverage
|
|
99
|
+
|
|
100
|
+
## References
|
|
101
|
+
|
|
102
|
+
- MarkItDown: https://github.com/microsoft/markitdown
|
|
103
|
+
- MarkItDown sample plugin: https://github.com/microsoft/markitdown/tree/main/packages/markitdown-sample-plugin
|
|
104
|
+
|
likhit-0.1.0/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# likhit
|
|
2
|
+
|
|
3
|
+
`likhit` is a public PDF-to-Markdown tool for Nepali government documents.
|
|
4
|
+
|
|
5
|
+
The default path is powered by [MarkItDown](https://github.com/microsoft/markitdown), with `likhit` intercepting born-digital Nepali PDFs that need Nepal-specific repair before Markdown is emitted. That repair layer handles Kalimati broken-CMap fixes, Devanagari reordering and spacing normalization, and legacy Nepali font remapping where applicable.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
### With Poetry
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
poetry install
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Run project commands with `poetry run`:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
poetry run pytest
|
|
19
|
+
poetry run ruff check .
|
|
20
|
+
poetry run black --check .
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Recommended Usage
|
|
24
|
+
|
|
25
|
+
Convert a single PDF to editable Markdown:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
poetry run likhit convert path/to/document.pdf --out path/to/document.md
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Convert multiple PDFs at once:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
poetry run likhit convert path/to/a.pdf path/to/b.pdf --out-dir path/to/output-dir
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
If `--out` or `--out-dir` is omitted, `likhit` writes Markdown files in the current directory using the input filename stem.
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
`convert` is the public path.
|
|
42
|
+
|
|
43
|
+
- Input scope: born-digital PDFs only
|
|
44
|
+
- Output: generic editable Markdown
|
|
45
|
+
- Engine: MarkItDown by default
|
|
46
|
+
- `likhit` value-add: Nepali PDF repair before Markdown output when needed
|
|
47
|
+
- Recognized document layouts such as Kanun Patrika and CIAA-style PDFs are auto-detected internally so `likhit` can preserve better text order and structure without a `--type` flag
|
|
48
|
+
- No OCR support is included in this branch
|
|
49
|
+
|
|
50
|
+
## Architecture
|
|
51
|
+
|
|
52
|
+
The new default pipeline is:
|
|
53
|
+
|
|
54
|
+
1. `likhit convert` opens the PDF and checks whether it matches a known structure-aware document type.
|
|
55
|
+
2. If the PDF matches a known layout such as Kanun Patrika or a CIAA-style document, `likhit` reuses its existing structure-aware extraction logic internally.
|
|
56
|
+
3. Otherwise, MarkItDown handles the default conversion path.
|
|
57
|
+
4. When the PDF needs Nepali repair, `likhit` repairs the text first:
|
|
58
|
+
- Kalimati broken-CMap repair
|
|
59
|
+
- Devanagari reordering
|
|
60
|
+
- Devanagari spacing normalization
|
|
61
|
+
- Legacy-font remapping through `npttf2utf`
|
|
62
|
+
5. `likhit` assembles repaired text blocks into Markdown.
|
|
63
|
+
|
|
64
|
+
This keeps the public product story simple: `likhit` is the tool users call, while MarkItDown is embedded infrastructure.
|
|
65
|
+
|
|
66
|
+
## Current Scope
|
|
67
|
+
|
|
68
|
+
- Supported default input: PDF only
|
|
69
|
+
- Supported default output: Markdown only
|
|
70
|
+
- Supported default document class: born-digital PDFs
|
|
71
|
+
- Unsupported in this branch: OCR, scanned/image-only PDFs, `.doc`, `.docx`, and image inputs
|
|
72
|
+
|
|
73
|
+
## Project Layout
|
|
74
|
+
|
|
75
|
+
- `src/likhit/core.py`: public `convert` and `convert_many` entry points
|
|
76
|
+
- `src/likhit/markitdown_integration.py`: MarkItDown instance setup and custom PDF converter
|
|
77
|
+
- `src/likhit/nepali_pdf_repair.py`: reusable Nepal-specific PDF repair layer
|
|
78
|
+
- `src/likhit/markdown_assembly.py`: generic Markdown assembly for the default conversion path
|
|
79
|
+
- `src/likhit/extractors/`, `src/likhit/handlers/`, `src/likhit/renderers/`: internal Nepali PDF repair and legacy extraction internals
|
|
80
|
+
- `tests/`: conversion, extraction, and CLI coverage
|
|
81
|
+
|
|
82
|
+
## References
|
|
83
|
+
|
|
84
|
+
- MarkItDown: https://github.com/microsoft/markitdown
|
|
85
|
+
- MarkItDown sample plugin: https://github.com/microsoft/markitdown/tree/main/packages/markitdown-sample-plugin
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "likhit"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Nepali PDF-to-Markdown converter powered by MarkItDown"
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "notashwinii", email = "ashwinii.subedi@gmail.com" }
|
|
7
|
+
]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.12,<4.0"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"fonttools>=4.47.0,<5.0.0",
|
|
12
|
+
"npttf2utf>=0.3.7,<1.0.0",
|
|
13
|
+
"pymupdf==1.27.2",
|
|
14
|
+
"pyyaml>=6.0,<7.0",
|
|
15
|
+
"markitdown[pdf]==0.1.5"
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
likhit = "likhit.cli:main"
|
|
20
|
+
|
|
21
|
+
[tool.poetry]
|
|
22
|
+
packages = [{ include = "likhit", from = "src" }]
|
|
23
|
+
|
|
24
|
+
[tool.poetry.group.dev.dependencies]
|
|
25
|
+
pytest = "^8.0.0"
|
|
26
|
+
ruff = "^0.11.0"
|
|
27
|
+
black = "^25.0.0"
|
|
28
|
+
|
|
29
|
+
[build-system]
|
|
30
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
31
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""CLI entry point for conversion."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from likhit.core import convert_many, derive_convert_output_name
|
|
10
|
+
from likhit.errors import LikhitError, ValidationError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
14
|
+
parser = argparse.ArgumentParser(
|
|
15
|
+
prog="likhit",
|
|
16
|
+
description="Convert born-digital PDFs to editable Markdown.",
|
|
17
|
+
)
|
|
18
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
19
|
+
|
|
20
|
+
convert_parser = subparsers.add_parser(
|
|
21
|
+
"convert",
|
|
22
|
+
help="Convert PDFs to editable Markdown",
|
|
23
|
+
)
|
|
24
|
+
convert_parser.add_argument("inputs", nargs="+", help="One or more input PDF files")
|
|
25
|
+
convert_parser.add_argument("--out", help="Output path for a single input file")
|
|
26
|
+
convert_parser.add_argument(
|
|
27
|
+
"--out-dir",
|
|
28
|
+
help="Directory for generated Markdown files; defaults to current directory",
|
|
29
|
+
)
|
|
30
|
+
return parser
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _write_convert_outputs(args: argparse.Namespace) -> int:
|
|
34
|
+
if len(args.inputs) > 1 and args.out:
|
|
35
|
+
raise ValidationError("--out can only be used with a single input file")
|
|
36
|
+
|
|
37
|
+
for input_path in args.inputs:
|
|
38
|
+
path = Path(input_path)
|
|
39
|
+
if path.suffix.lower() != ".pdf":
|
|
40
|
+
raise ValidationError(
|
|
41
|
+
"Unsupported input format for convert. Only born-digital PDF files are supported."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
output_dir = Path(args.out_dir or ".")
|
|
45
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
results = convert_many(args.inputs)
|
|
47
|
+
|
|
48
|
+
existing_names: set[str] = {
|
|
49
|
+
path.name for path in output_dir.iterdir() if path.is_file()
|
|
50
|
+
}
|
|
51
|
+
for source_path, markdown in results:
|
|
52
|
+
if args.out:
|
|
53
|
+
destination = Path(args.out)
|
|
54
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
if destination.exists():
|
|
56
|
+
raise ValidationError(f"Output file already exists: {destination}")
|
|
57
|
+
else:
|
|
58
|
+
destination = output_dir / derive_convert_output_name(
|
|
59
|
+
source_path,
|
|
60
|
+
existing_names,
|
|
61
|
+
)
|
|
62
|
+
destination.write_text(markdown, encoding="utf-8")
|
|
63
|
+
return 0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def main(argv: list[str] | None = None) -> int:
|
|
67
|
+
parser = build_parser()
|
|
68
|
+
args = parser.parse_args(argv)
|
|
69
|
+
if args.command is None:
|
|
70
|
+
parser.print_help()
|
|
71
|
+
return 1
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
if args.command == "convert":
|
|
75
|
+
return _write_convert_outputs(args)
|
|
76
|
+
parser.print_help()
|
|
77
|
+
return 1
|
|
78
|
+
except LikhitError as exc:
|
|
79
|
+
print(str(exc), file=sys.stderr)
|
|
80
|
+
return 1
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Public conversion entry points."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
from likhit.errors import ExtractionError, ValidationError
|
|
9
|
+
from likhit.handlers import CIAAPressReleaseHandler, KanunPatrikaHandler
|
|
10
|
+
from likhit.markitdown_integration import convert_pdf_to_markdown
|
|
11
|
+
from likhit.models import DocumentType, ExtractionResult
|
|
12
|
+
from likhit.renderers import MarkdownRenderer
|
|
13
|
+
from likhit.extractors.font_based import FontBasedStrategy
|
|
14
|
+
|
|
15
|
+
_KANUN_MARKERS = (
|
|
16
|
+
"नेपालकानूनपत्रिका",
|
|
17
|
+
"नेपालकानुनपत्रिका",
|
|
18
|
+
"निर्णय नं",
|
|
19
|
+
"ने.का.प.",
|
|
20
|
+
)
|
|
21
|
+
_CIAA_MARKERS = (
|
|
22
|
+
"अख्तियार दुरुपयोग अनुसन्धान आयोग",
|
|
23
|
+
"प्रेस विज्ञ",
|
|
24
|
+
"प्रेस मिज्ञ",
|
|
25
|
+
"विषय:",
|
|
26
|
+
"मिषय:",
|
|
27
|
+
"आयोगकोनिर्णय",
|
|
28
|
+
"अनुसन्धानबाट पुष्टि भएको",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _metadata_from_options(
|
|
33
|
+
title: str | None,
|
|
34
|
+
publication_date: str | None,
|
|
35
|
+
source_url: str | None,
|
|
36
|
+
) -> dict[str, str | None]:
|
|
37
|
+
return {
|
|
38
|
+
"title": title,
|
|
39
|
+
"publication_date": publication_date,
|
|
40
|
+
"source_url": source_url,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _resolve_handler(
|
|
45
|
+
doc_type: DocumentType,
|
|
46
|
+
) -> CIAAPressReleaseHandler | KanunPatrikaHandler:
|
|
47
|
+
if doc_type is DocumentType.CIAA_PRESS_RELEASE:
|
|
48
|
+
return CIAAPressReleaseHandler()
|
|
49
|
+
if doc_type is DocumentType.KANUN_PATRIKA:
|
|
50
|
+
return KanunPatrikaHandler()
|
|
51
|
+
raise ValidationError(f"Unsupported document type: {doc_type.value}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _normalize_detection_text(text: str) -> str:
|
|
55
|
+
return re.sub(r"\s+", "", text).casefold()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _detect_document_type(raw_text: str) -> DocumentType | None:
|
|
59
|
+
normalized = _normalize_detection_text(raw_text)
|
|
60
|
+
|
|
61
|
+
if any(
|
|
62
|
+
_normalize_detection_text(marker) in normalized for marker in _KANUN_MARKERS
|
|
63
|
+
):
|
|
64
|
+
return DocumentType.KANUN_PATRIKA
|
|
65
|
+
if any(_normalize_detection_text(marker) in normalized for marker in _CIAA_MARKERS):
|
|
66
|
+
return DocumentType.CIAA_PRESS_RELEASE
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _render_markdown_without_frontmatter(result: ExtractionResult) -> str:
|
|
71
|
+
markdown = MarkdownRenderer().render(result).lstrip()
|
|
72
|
+
if not markdown.startswith("---\n"):
|
|
73
|
+
return markdown.strip()
|
|
74
|
+
|
|
75
|
+
parts = markdown.split("\n---\n", 1)
|
|
76
|
+
if len(parts) != 2:
|
|
77
|
+
return markdown.strip()
|
|
78
|
+
return parts[1].strip()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _convert_with_detected_structure(file_path: str) -> str | None:
|
|
82
|
+
strategy = FontBasedStrategy()
|
|
83
|
+
try:
|
|
84
|
+
raw_document = strategy.extract_text(file_path)
|
|
85
|
+
except ExtractionError as exc:
|
|
86
|
+
if str(exc) == "No text content found in document":
|
|
87
|
+
return None
|
|
88
|
+
raise
|
|
89
|
+
doc_type = _detect_document_type(raw_document.raw_text)
|
|
90
|
+
if doc_type is None:
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
handler = _resolve_handler(doc_type)
|
|
94
|
+
result = handler.build_result(
|
|
95
|
+
raw_document,
|
|
96
|
+
_metadata_from_options(None, None, None),
|
|
97
|
+
)
|
|
98
|
+
return _render_markdown_without_frontmatter(result)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def convert(file_path: str) -> str:
|
|
102
|
+
path = Path(file_path)
|
|
103
|
+
if path.suffix.lower() != ".pdf":
|
|
104
|
+
raise ValidationError(
|
|
105
|
+
"Unsupported input format for convert. Only born-digital PDF files are supported."
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
structured_markdown = _convert_with_detected_structure(file_path)
|
|
109
|
+
if structured_markdown is not None:
|
|
110
|
+
return structured_markdown
|
|
111
|
+
return convert_pdf_to_markdown(file_path)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def derive_convert_output_name(source_path: str, existing: set[str]) -> str:
|
|
115
|
+
base_name = Path(source_path).stem or "document"
|
|
116
|
+
candidate = f"{base_name}.md"
|
|
117
|
+
counter = 2
|
|
118
|
+
while candidate in existing:
|
|
119
|
+
candidate = f"{base_name}-{counter}.md"
|
|
120
|
+
counter += 1
|
|
121
|
+
existing.add(candidate)
|
|
122
|
+
return candidate
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def convert_many(file_paths: list[str]) -> list[tuple[str, str]]:
|
|
126
|
+
return [(file_path, convert(file_path)) for file_path in file_paths]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Project-specific exceptions."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class LikhitError(Exception):
|
|
5
|
+
"""Base exception for extraction failures."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ValidationError(LikhitError):
|
|
9
|
+
"""Raised when user input or extracted content is invalid."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ExtractionError(LikhitError):
|
|
13
|
+
"""Raised when document extraction fails."""
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Shared extraction abstractions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
from likhit.models import Table
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(slots=True)
|
|
12
|
+
class TextFragment:
|
|
13
|
+
"""A normalized text block plus its page/layout metadata."""
|
|
14
|
+
|
|
15
|
+
text: str
|
|
16
|
+
page_number: int
|
|
17
|
+
x0: float
|
|
18
|
+
y0: float
|
|
19
|
+
x1: float
|
|
20
|
+
y1: float
|
|
21
|
+
block_number: int = 0
|
|
22
|
+
line_number: int = 0
|
|
23
|
+
gap_before: float | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(slots=True)
|
|
27
|
+
class RawDocument:
|
|
28
|
+
"""Unstructured text extracted from a document."""
|
|
29
|
+
|
|
30
|
+
paragraphs: list[str]
|
|
31
|
+
raw_text: str
|
|
32
|
+
fragments: list[TextFragment]
|
|
33
|
+
tables: list[Table] = field(default_factory=list)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ExtractionStrategy(ABC):
|
|
37
|
+
"""Base interface for extraction strategies."""
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def extract_text(self, file_path: str, pages: str | None = None) -> RawDocument:
|
|
41
|
+
"""Extract unstructured text from a document."""
|
|
42
|
+
|
|
43
|
+
@abstractmethod
|
|
44
|
+
def extract_tables(self, file_path: str) -> list[Table]:
|
|
45
|
+
"""Extract structured tables from a document."""
|