opendockit 0.1.0__py3-none-any.whl
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.
- opendockit/__init__.py +69 -0
- opendockit/__main__.py +9 -0
- opendockit/cli.py +135 -0
- opendockit/converter.py +562 -0
- opendockit/describe.py +371 -0
- opendockit/detect.py +550 -0
- opendockit/exceptions.py +23 -0
- opendockit/extractors/__init__.py +38 -0
- opendockit/extractors/_equation.py +479 -0
- opendockit/extractors/_shared.py +1684 -0
- opendockit/extractors/docx.py +1687 -0
- opendockit/extractors/html.py +2585 -0
- opendockit/extractors/hwp.py +1063 -0
- opendockit/extractors/hwpx.py +870 -0
- opendockit/extractors/image.py +39 -0
- opendockit/extractors/pdf.py +9091 -0
- opendockit/extractors/pptx.py +940 -0
- opendockit/extractors/spreadsheet.py +4117 -0
- opendockit/extractors/text.py +1620 -0
- opendockit/image.py +340 -0
- opendockit/isolated.py +212 -0
- opendockit/markdown.py +535 -0
- opendockit/models.py +348 -0
- opendockit/py.typed +0 -0
- opendockit-0.1.0.dist-info/METADATA +205 -0
- opendockit-0.1.0.dist-info/RECORD +29 -0
- opendockit-0.1.0.dist-info/WHEEL +4 -0
- opendockit-0.1.0.dist-info/entry_points.txt +2 -0
- opendockit-0.1.0.dist-info/licenses/LICENSE +21 -0
opendockit/__init__.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Convert an uploaded document into a Markdown string ready for LLM input.
|
|
2
|
+
|
|
3
|
+
from opendockit import OpenAIImageDescriber, aconvert
|
|
4
|
+
result = await aconvert(data, "report.hwp", describer=OpenAIImageDescriber())
|
|
5
|
+
result.text # Markdown to feed to the LLM (images are filled in with vision-model descriptions and OCR)
|
|
6
|
+
result.sections # per page / slide / sheet (for chunking and source attribution)
|
|
7
|
+
result.images # the extracted images and their descriptions
|
|
8
|
+
result.char_count # number of characters (picture descriptions included); the basis for upload limits
|
|
9
|
+
result.exceeded # whether the conversion stopped because the text went over the character limit (500,000 by default)
|
|
10
|
+
result.warnings # limit exceeded, scanned pages, extension mismatch, and so on
|
|
11
|
+
|
|
12
|
+
On a server, convert in a worker process with `IsolatedConverter` (so one file's runaway memory or time does not spread to
|
|
13
|
+
the server). With `convert` (synchronous) alone, the images are collected in `result.images` and the body keeps only placeholder text.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
17
|
+
|
|
18
|
+
from opendockit.converter import aconvert, convert, convert_file, render
|
|
19
|
+
from opendockit.describe import ImageDescriber, OpenAIImageDescriber, describe_images
|
|
20
|
+
from opendockit.detect import detect_format
|
|
21
|
+
from opendockit.exceptions import (
|
|
22
|
+
ConversionError,
|
|
23
|
+
CorruptedDocumentError,
|
|
24
|
+
DocumentTooLargeError,
|
|
25
|
+
EncryptedDocumentError,
|
|
26
|
+
UnsupportedFormatError,
|
|
27
|
+
)
|
|
28
|
+
from opendockit.isolated import TOO_COMPLEX_MESSAGE, IsolatedConverter
|
|
29
|
+
from opendockit.models import (
|
|
30
|
+
ConversionResult,
|
|
31
|
+
ConvertOptions,
|
|
32
|
+
DocumentFormat,
|
|
33
|
+
DocumentImage,
|
|
34
|
+
ImageKind,
|
|
35
|
+
Section,
|
|
36
|
+
SectionKind,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
__version__ = version("opendockit")
|
|
42
|
+
except PackageNotFoundError: # running from a source tree that was never installed
|
|
43
|
+
__version__ = "0.0.0"
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"ConversionError",
|
|
47
|
+
"ConversionResult",
|
|
48
|
+
"ConvertOptions",
|
|
49
|
+
"CorruptedDocumentError",
|
|
50
|
+
"DocumentFormat",
|
|
51
|
+
"DocumentImage",
|
|
52
|
+
"DocumentTooLargeError",
|
|
53
|
+
"EncryptedDocumentError",
|
|
54
|
+
"ImageDescriber",
|
|
55
|
+
"ImageKind",
|
|
56
|
+
"IsolatedConverter",
|
|
57
|
+
"OpenAIImageDescriber",
|
|
58
|
+
"Section",
|
|
59
|
+
"SectionKind",
|
|
60
|
+
"TOO_COMPLEX_MESSAGE",
|
|
61
|
+
"UnsupportedFormatError",
|
|
62
|
+
"__version__",
|
|
63
|
+
"aconvert",
|
|
64
|
+
"convert",
|
|
65
|
+
"convert_file",
|
|
66
|
+
"describe_images",
|
|
67
|
+
"detect_format",
|
|
68
|
+
"render",
|
|
69
|
+
]
|
opendockit/__main__.py
ADDED
opendockit/cli.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Command-line entry point for trying a conversion.
|
|
2
|
+
|
|
3
|
+
opendockit <file> [--count | --json] [--describe [--model NAME]]
|
|
4
|
+
|
|
5
|
+
`--describe` has an OpenAI vision model describe the images (this costs money). The key is read from the `OPENAI_API_KEY`
|
|
6
|
+
environment variable, and `--describe` needs the optional extra: `pip install "opendockit[openai]"`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import asyncio
|
|
11
|
+
from dataclasses import asdict
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
from opendockit import (
|
|
18
|
+
ConversionError,
|
|
19
|
+
ConversionResult,
|
|
20
|
+
OpenAIImageDescriber,
|
|
21
|
+
convert_file,
|
|
22
|
+
describe_images,
|
|
23
|
+
)
|
|
24
|
+
from opendockit.describe import DEFAULT_MODEL
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def main() -> int:
|
|
28
|
+
_write_output_as_utf8()
|
|
29
|
+
parser = argparse.ArgumentParser(description="Convert a document to Markdown ready for LLM input.")
|
|
30
|
+
parser.add_argument("path")
|
|
31
|
+
# Given both, only --count was honored and --json was silently ignored
|
|
32
|
+
# (Pinned by: test_cli.py::TestArguments::test_count_and_json_cannot_be_used_together).
|
|
33
|
+
output = parser.add_mutually_exclusive_group()
|
|
34
|
+
output.add_argument("--json", action="store_true", help="print JSON that includes the sections, metadata, warnings and image list")
|
|
35
|
+
output.add_argument("--count", action="store_true",
|
|
36
|
+
help="print only the character count instead of the text ('exceeded' when over the limit). "
|
|
37
|
+
"Add --describe to also count the text inside pictures")
|
|
38
|
+
parser.add_argument("--describe", action="store_true",
|
|
39
|
+
help="describe the images with an OpenAI vision model and fill the descriptions into the text")
|
|
40
|
+
parser.add_argument("--model", default=DEFAULT_MODEL, help=f"model to use for --describe (default {DEFAULT_MODEL})")
|
|
41
|
+
args = parser.parse_args()
|
|
42
|
+
|
|
43
|
+
path = Path(args.path)
|
|
44
|
+
describer = OpenAIImageDescriber(model_name=args.model) if args.describe else None
|
|
45
|
+
try:
|
|
46
|
+
# convert_file checks the size from the file metadata before reading. A file over the limit is never read whole.
|
|
47
|
+
result = convert_file(path)
|
|
48
|
+
if describer is not None and result.images:
|
|
49
|
+
result = asyncio.run(_describe(result, describer))
|
|
50
|
+
except ConversionError as exc:
|
|
51
|
+
print(f"[{exc.code}] {exc}", file=sys.stderr)
|
|
52
|
+
return 1
|
|
53
|
+
except _DescriberUnavailable as exc:
|
|
54
|
+
print(f"Cannot use --describe: {exc}", file=sys.stderr)
|
|
55
|
+
return 1
|
|
56
|
+
except FileNotFoundError:
|
|
57
|
+
print(f"File not found: {path}", file=sys.stderr)
|
|
58
|
+
return 1
|
|
59
|
+
except OSError:
|
|
60
|
+
print(f"Could not read the file (it may be a directory, or permission may be missing): {path}", file=sys.stderr)
|
|
61
|
+
return 1
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
_print_result(result, args)
|
|
65
|
+
except BrokenPipeError:
|
|
66
|
+
# The reader closed first, as with `| head`. Finish without dumping a traceback. Point standard output at /dev/null so
|
|
67
|
+
# the error does not fire again at exit while flushing the remaining output buffer
|
|
68
|
+
# (the SIGPIPE advice in the Python docs, Pinned by: test_cli.py::TestArguments::test_closed_pipe_ends_quietly).
|
|
69
|
+
os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno())
|
|
70
|
+
return 1
|
|
71
|
+
return 0
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _write_output_as_utf8() -> None:
|
|
75
|
+
"""Write standard output and error as UTF-8. Under `PYTHONIOENCODING=ascii` or a Windows redirect (cp949), Korean text ended in
|
|
76
|
+
a UnicodeEncodeError traceback (Pinned by: test_cli.py::TestArguments::test_output_is_written_as_utf8_even_when_stdout_is_not).
|
|
77
|
+
A stream that cannot be reconfigured is left as it is."""
|
|
78
|
+
for stream in (sys.stdout, sys.stderr):
|
|
79
|
+
reconfigure = getattr(stream, "reconfigure", None)
|
|
80
|
+
if reconfigure is None:
|
|
81
|
+
continue
|
|
82
|
+
try:
|
|
83
|
+
reconfigure(encoding="utf-8", errors="replace")
|
|
84
|
+
except (ValueError, OSError): # a closed stream, or one that cannot be reconfigured
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _print_result(result: ConversionResult, args: argparse.Namespace) -> None:
|
|
89
|
+
if args.count:
|
|
90
|
+
print(f"{result.char_count}{' exceeded' if result.exceeded else ''}")
|
|
91
|
+
if result.images and not args.describe:
|
|
92
|
+
print(f"[warning] The text in {len(result.images)} picture(s) was not counted. Add --describe to include it.", file=sys.stderr)
|
|
93
|
+
for warning in result.warnings:
|
|
94
|
+
print(f"[warning] {warning}", file=sys.stderr)
|
|
95
|
+
elif args.json:
|
|
96
|
+
payload = asdict(result)
|
|
97
|
+
for key in ("base_warnings", "options", "fallbacks", "counted", "marker_key"):
|
|
98
|
+
payload.pop(key)
|
|
99
|
+
for section in payload["sections"]:
|
|
100
|
+
section.pop("template")
|
|
101
|
+
for image in payload["images"]:
|
|
102
|
+
# Picture bytes are written as their size only. So are the tiles of a very tall picture (`tiles`) — they came out as
|
|
103
|
+
# a repr and the output went over 1MB
|
|
104
|
+
# (Pinned by: test_cli.py::test_json_output_summarizes_picture_tiles_like_picture_data).
|
|
105
|
+
image["data"] = f"<{len(image['data'])} bytes>"
|
|
106
|
+
image["tiles"] = [f"<{len(tile)} bytes>" for tile in image["tiles"]]
|
|
107
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2, default=str))
|
|
108
|
+
else:
|
|
109
|
+
print(result.text)
|
|
110
|
+
for warning in result.warnings:
|
|
111
|
+
print(f"[warning] {warning}", file=sys.stderr)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class _DescriberUnavailable(Exception):
|
|
115
|
+
"""The vision model could not be set up (missing extra, missing API key)."""
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
async def _describe(result: ConversionResult, describer: OpenAIImageDescriber) -> ConversionResult:
|
|
119
|
+
"""Close the HTTP client the describer created in this loop before the loop ends.
|
|
120
|
+
|
|
121
|
+
The model is built first: without the `openai` extra or an API key every image would otherwise just come back as
|
|
122
|
+
"could not be described" with exit code 0 (Pinned by: test_cli.py::TestDescribeSetup).
|
|
123
|
+
"""
|
|
124
|
+
try:
|
|
125
|
+
try:
|
|
126
|
+
describer.check()
|
|
127
|
+
except Exception as exc:
|
|
128
|
+
raise _DescriberUnavailable(str(exc)) from exc
|
|
129
|
+
return await describe_images(result, describer)
|
|
130
|
+
finally:
|
|
131
|
+
await describer.aclose()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
if __name__ == "__main__":
|
|
135
|
+
sys.exit(main())
|