meltify 0.2.1__tar.gz → 0.2.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.
- {meltify-0.2.1 → meltify-0.2.2}/PKG-INFO +9 -4
- {meltify-0.2.1 → meltify-0.2.2}/README.md +5 -2
- {meltify-0.2.1 → meltify-0.2.2}/pyproject.toml +3 -2
- {meltify-0.2.1 → meltify-0.2.2}/pyproject.toml.orig +3 -2
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/commands/doctor.py +2 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/commands/read.py +1 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/__init__.py +3 -0
- meltify-0.2.2/src/meltify/converters/parquet.py +103 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/run.py +3 -0
- meltify-0.2.2/src/meltify/converters/webarchive.py +160 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/data/defaults.toml +2 -0
- {meltify-0.2.1 → meltify-0.2.2}/LICENSE +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/__init__.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/__main__.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/cli.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/commands/__init__.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/commands/brief.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/commands/check.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/commands/hidden.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/commands/media.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/commands/ocr.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/commands/submit.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/config.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/archive.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/blips.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/doc.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/embeds.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/epub.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/fallback.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/hwp.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/iwa.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/iwork.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/kakao.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/limits.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/mail.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/mbox.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/metafile.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/msg.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/notebook.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/odf.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/office.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/ooxml.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/ooxml_charts.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/pdf.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/ppt.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/quicklook.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/raster.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/render.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/rtf.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/sheet.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/slack.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/sqlite.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/subtitle.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/svg.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/tables.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/text.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/unlock.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/web.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/wordperfect.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/converters/xmlsafe.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/data/__init__.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/data/brief_en.md +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/data/brief_ko.md +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/engines/__init__.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/engines/asr.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/engines/consensus.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/engines/ocr.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/evidence.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/fetch.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/ffmpeg.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/files.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/forensics/__init__.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/forensics/pdf.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/imaging.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/lang.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/llm.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/needs.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/output.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/passwords.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/recognize.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/safe.py +0 -0
- {meltify-0.2.1 → meltify-0.2.2}/src/meltify/tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meltify
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Melt files of any format into prompt-ready text where every line cites its source
|
|
5
5
|
Keywords: llm,prompt,ocr,pdf,transcription,claude-code,agent-skills
|
|
6
6
|
Author: jeon-jihyeon
|
|
@@ -17,7 +17,7 @@ Requires-Dist: ocrmac>=1.0.1 ; sys_platform == 'darwin'
|
|
|
17
17
|
Requires-Dist: trafilatura>=2
|
|
18
18
|
Requires-Dist: python-hwpx>=1
|
|
19
19
|
Requires-Dist: striprtf>=0.0.29
|
|
20
|
-
Requires-Dist: meltify[office,media,asr-mlx,archive,crypto] ; extra == 'all'
|
|
20
|
+
Requires-Dist: meltify[office,media,asr-mlx,archive,parquet,crypto] ; extra == 'all'
|
|
21
21
|
Requires-Dist: libarchive-c>=5 ; extra == 'archive'
|
|
22
22
|
Requires-Dist: mlx-whisper>=0.4 ; platform_machine == 'arm64' and sys_platform == 'darwin' and extra == 'asr-mlx'
|
|
23
23
|
Requires-Dist: msoffcrypto-tool>=6 ; extra == 'crypto'
|
|
@@ -32,6 +32,7 @@ Requires-Dist: python-calamine>=0.4 ; extra == 'office'
|
|
|
32
32
|
Requires-Dist: legacy-doc>=0.2 ; extra == 'office'
|
|
33
33
|
Requires-Dist: olefile>=0.47 ; extra == 'office'
|
|
34
34
|
Requires-Dist: metafile-render>=0.3 ; extra == 'office'
|
|
35
|
+
Requires-Dist: pyarrow>=17 ; extra == 'parquet'
|
|
35
36
|
Requires-Dist: playwright>=1.50 ; extra == 'render'
|
|
36
37
|
Requires-Python: >=3.11.9
|
|
37
38
|
Project-URL: Repository, https://github.com/jeon-jihyeon/meltify
|
|
@@ -43,6 +44,7 @@ Provides-Extra: iwork
|
|
|
43
44
|
Provides-Extra: media
|
|
44
45
|
Provides-Extra: ocr-paddle
|
|
45
46
|
Provides-Extra: office
|
|
47
|
+
Provides-Extra: parquet
|
|
46
48
|
Provides-Extra: render
|
|
47
49
|
Description-Content-Type: text/markdown
|
|
48
50
|
|
|
@@ -99,8 +101,8 @@ Each command has a skill of the same name, such as `meltify-ocr`, that tells the
|
|
|
99
101
|
|
|
100
102
|
| Group | Formats | Needs |
|
|
101
103
|
|---|---|---|
|
|
102
|
-
| Documents | PDF, Word `.docx` `.docm` `.dotx` `.dotm` `.doc` `.dot`, HWP and HWPX, RTF, ODT, Pages, WordPerfect, EPUB, XPS, FictionBook, MOBI, HTML
|
|
103
|
-
| Spreadsheets | Excel `.xlsx` `.xlsm` `.xltx` `.xltm` `.xlsb` `.xls` `.xlt`, Hancom `.cell`, ODS, Numbers, CSV and TSV | `office` for `.xlsb`, `.xls` and `.xlt`, `iwork` for Numbers |
|
|
104
|
+
| Documents | PDF, Word `.docx` `.docm` `.dotx` `.dotm` `.doc` `.dot`, HWP and HWPX, RTF, ODT, Pages, WordPerfect, EPUB, XPS, FictionBook, MOBI, HTML and Safari `.webarchive`, Markdown and plain text | `office` for Word and EPUB |
|
|
105
|
+
| Spreadsheets | Excel `.xlsx` `.xlsm` `.xltx` `.xltm` `.xlsb` `.xls` `.xlt`, Hancom `.cell`, ODS, Numbers, Parquet, CSV and TSV | `office` for `.xlsb`, `.xls` and `.xlt`, `iwork` for Numbers, `parquet` for Parquet |
|
|
104
106
|
| Slides | PowerPoint `.pptx` `.pptm` `.potx` `.potm` `.ppsx` `.ppsm` `.ppt` `.pps` `.pot`, Hancom `.show`, ODP, Keynote | `office` for PowerPoint |
|
|
105
107
|
| Drawings | ODG, SVG and `.svgz`, EMF and WMF with `.emz` and `.wmz`, comic book `.cbz` | |
|
|
106
108
|
| Mail and chat | `.eml`, `.msg`, mbox, KakaoTalk exports, Slack export zips | `office` for `.msg` |
|
|
@@ -186,6 +188,7 @@ Optional parts install on request with `meltify doctor --install NAME`:
|
|
|
186
188
|
| `office` | Word, PowerPoint, Outlook `.msg` and EPUB, legacy `.doc`, `.ppt`, `.xls` and `.xlsb`, faster `.xlsx` reading through calamine, and drawing EMF pictures without LibreOffice |
|
|
187
189
|
| `archive` | 7z and rar through the system libarchive, plus a pinned 7-Zip download for encrypted ones |
|
|
188
190
|
| `iwork` | Numbers tables. Pages and Keynote need nothing extra |
|
|
191
|
+
| `parquet` | Parquet files, the first 200 rows and per-column min, max and null counts |
|
|
189
192
|
| `crypto` | encrypted Office files, AES zips, and password-protected HWP and iWork files |
|
|
190
193
|
| `render` | `read --render` for JavaScript-heavy pages, using your Chrome or a downloaded Chromium |
|
|
191
194
|
| `media` | video URLs through yt-dlp |
|
|
@@ -208,6 +211,7 @@ See [examples/meltify.toml](examples/meltify.toml) for a sample. A few keys wort
|
|
|
208
211
|
|
|
209
212
|
- `lang` is the language OCR expects (`ko` by default)
|
|
210
213
|
- `asr.lang` is the spoken language for speech engines. It's `auto` by default, separate from `lang`, so Whisper detects each recording's language instead of translating it
|
|
214
|
+
- `read.parquet_rows` sets how many Parquet rows melt per file
|
|
211
215
|
- `render.quicklook = false` keeps Quick Look and Spotlight out of every render
|
|
212
216
|
|
|
213
217
|
## Limits
|
|
@@ -219,6 +223,7 @@ See [examples/meltify.toml](examples/meltify.toml) for a sample. A few keys wort
|
|
|
219
223
|
- Rendered rows, EMF bitmaps and Quick Look previews are only as good as the OCR that reads them, and Quick Look exists only on macOS
|
|
220
224
|
- Pages and Keynote charts are listed in `needs` instead of read, and a Pages or Keynote file with no text records falls back to its embedded preview image
|
|
221
225
|
- DRM-protected HWP files, password-protected WordPerfect files, and Hancom `.cell` and `.show` files from before 2014 and Hanshow `.hpt` files, which are binary, aren't read. Save the Hancom ones again as `.xlsx` or `.pptx`
|
|
226
|
+
- Parquet files show their first 200 rows, and the rest are counted in `needs`
|
|
222
227
|
- Encrypted 7z and rar need 7-Zip, which `meltify doctor --install archive` downloads. Plain ones open with the system libarchive, a separate package on Linux, with 7-Zip, or with `bsdtar` on macOS
|
|
223
228
|
- The plugin launcher is a POSIX shell script and `doctor --install` only downloads macOS and Linux builds, so Windows isn't supported
|
|
224
229
|
|
|
@@ -51,8 +51,8 @@ Each command has a skill of the same name, such as `meltify-ocr`, that tells the
|
|
|
51
51
|
|
|
52
52
|
| Group | Formats | Needs |
|
|
53
53
|
|---|---|---|
|
|
54
|
-
| Documents | PDF, Word `.docx` `.docm` `.dotx` `.dotm` `.doc` `.dot`, HWP and HWPX, RTF, ODT, Pages, WordPerfect, EPUB, XPS, FictionBook, MOBI, HTML
|
|
55
|
-
| Spreadsheets | Excel `.xlsx` `.xlsm` `.xltx` `.xltm` `.xlsb` `.xls` `.xlt`, Hancom `.cell`, ODS, Numbers, CSV and TSV | `office` for `.xlsb`, `.xls` and `.xlt`, `iwork` for Numbers |
|
|
54
|
+
| Documents | PDF, Word `.docx` `.docm` `.dotx` `.dotm` `.doc` `.dot`, HWP and HWPX, RTF, ODT, Pages, WordPerfect, EPUB, XPS, FictionBook, MOBI, HTML and Safari `.webarchive`, Markdown and plain text | `office` for Word and EPUB |
|
|
55
|
+
| Spreadsheets | Excel `.xlsx` `.xlsm` `.xltx` `.xltm` `.xlsb` `.xls` `.xlt`, Hancom `.cell`, ODS, Numbers, Parquet, CSV and TSV | `office` for `.xlsb`, `.xls` and `.xlt`, `iwork` for Numbers, `parquet` for Parquet |
|
|
56
56
|
| Slides | PowerPoint `.pptx` `.pptm` `.potx` `.potm` `.ppsx` `.ppsm` `.ppt` `.pps` `.pot`, Hancom `.show`, ODP, Keynote | `office` for PowerPoint |
|
|
57
57
|
| Drawings | ODG, SVG and `.svgz`, EMF and WMF with `.emz` and `.wmz`, comic book `.cbz` | |
|
|
58
58
|
| Mail and chat | `.eml`, `.msg`, mbox, KakaoTalk exports, Slack export zips | `office` for `.msg` |
|
|
@@ -138,6 +138,7 @@ Optional parts install on request with `meltify doctor --install NAME`:
|
|
|
138
138
|
| `office` | Word, PowerPoint, Outlook `.msg` and EPUB, legacy `.doc`, `.ppt`, `.xls` and `.xlsb`, faster `.xlsx` reading through calamine, and drawing EMF pictures without LibreOffice |
|
|
139
139
|
| `archive` | 7z and rar through the system libarchive, plus a pinned 7-Zip download for encrypted ones |
|
|
140
140
|
| `iwork` | Numbers tables. Pages and Keynote need nothing extra |
|
|
141
|
+
| `parquet` | Parquet files, the first 200 rows and per-column min, max and null counts |
|
|
141
142
|
| `crypto` | encrypted Office files, AES zips, and password-protected HWP and iWork files |
|
|
142
143
|
| `render` | `read --render` for JavaScript-heavy pages, using your Chrome or a downloaded Chromium |
|
|
143
144
|
| `media` | video URLs through yt-dlp |
|
|
@@ -160,6 +161,7 @@ See [examples/meltify.toml](examples/meltify.toml) for a sample. A few keys wort
|
|
|
160
161
|
|
|
161
162
|
- `lang` is the language OCR expects (`ko` by default)
|
|
162
163
|
- `asr.lang` is the spoken language for speech engines. It's `auto` by default, separate from `lang`, so Whisper detects each recording's language instead of translating it
|
|
164
|
+
- `read.parquet_rows` sets how many Parquet rows melt per file
|
|
163
165
|
- `render.quicklook = false` keeps Quick Look and Spotlight out of every render
|
|
164
166
|
|
|
165
167
|
## Limits
|
|
@@ -171,6 +173,7 @@ See [examples/meltify.toml](examples/meltify.toml) for a sample. A few keys wort
|
|
|
171
173
|
- Rendered rows, EMF bitmaps and Quick Look previews are only as good as the OCR that reads them, and Quick Look exists only on macOS
|
|
172
174
|
- Pages and Keynote charts are listed in `needs` instead of read, and a Pages or Keynote file with no text records falls back to its embedded preview image
|
|
173
175
|
- DRM-protected HWP files, password-protected WordPerfect files, and Hancom `.cell` and `.show` files from before 2014 and Hanshow `.hpt` files, which are binary, aren't read. Save the Hancom ones again as `.xlsx` or `.pptx`
|
|
176
|
+
- Parquet files show their first 200 rows, and the rest are counted in `needs`
|
|
174
177
|
- Encrypted 7z and rar need 7-Zip, which `meltify doctor --install archive` downloads. Plain ones open with the system libarchive, a separate package on Linux, with 7-Zip, or with `bsdtar` on macOS
|
|
175
178
|
- The plugin launcher is a POSIX shell script and `doctor --install` only downloads macOS and Linux builds, so Windows isn't supported
|
|
176
179
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "meltify"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.2"
|
|
4
4
|
description = "Melt files of any format into prompt-ready text where every line cites its source"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11.9"
|
|
@@ -47,6 +47,7 @@ crypto = [
|
|
|
47
47
|
]
|
|
48
48
|
archive = ["libarchive-c>=5"]
|
|
49
49
|
iwork = ["numbers-parser>=4"]
|
|
50
|
+
parquet = ["pyarrow>=17"]
|
|
50
51
|
render = ["playwright>=1.50"]
|
|
51
52
|
media = ["yt-dlp>=2026.8.19"]
|
|
52
53
|
asr-mlx = ["mlx-whisper>=0.4; sys_platform == 'darwin' and platform_machine == 'arm64'"]
|
|
@@ -54,7 +55,7 @@ ocr-paddle = [
|
|
|
54
55
|
"paddleocr>=3.2",
|
|
55
56
|
"paddlepaddle>=3.2",
|
|
56
57
|
]
|
|
57
|
-
all = ["meltify[office,media,asr-mlx,archive,crypto]"]
|
|
58
|
+
all = ["meltify[office,media,asr-mlx,archive,parquet,crypto]"]
|
|
58
59
|
|
|
59
60
|
[project.urls]
|
|
60
61
|
Repository = "https://github.com/jeon-jihyeon/meltify"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "meltify"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.2"
|
|
4
4
|
description = "Melt files of any format into prompt-ready text where every line cites its source"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11.9"
|
|
@@ -35,11 +35,12 @@ crypto = ["msoffcrypto-tool>=6", "pyzipper>=0.4", "cryptography>=42"]
|
|
|
35
35
|
archive = ["libarchive-c>=5"]
|
|
36
36
|
# Only Numbers needs it, since Pages and Keynote are read natively
|
|
37
37
|
iwork = ["numbers-parser>=4"]
|
|
38
|
+
parquet = ["pyarrow>=17"]
|
|
38
39
|
render = ["playwright>=1.50"]
|
|
39
40
|
media = ["yt-dlp>=2026.8.19"]
|
|
40
41
|
asr-mlx = ["mlx-whisper>=0.4; sys_platform == 'darwin' and platform_machine == 'arm64'"]
|
|
41
42
|
ocr-paddle = ["paddleocr>=3.2", "paddlepaddle>=3.2"]
|
|
42
|
-
all = ["meltify[office,media,asr-mlx,archive,crypto]"]
|
|
43
|
+
all = ["meltify[office,media,asr-mlx,archive,parquet,crypto]"]
|
|
43
44
|
|
|
44
45
|
[project.urls]
|
|
45
46
|
Repository = "https://github.com/jeon-jihyeon/meltify"
|
|
@@ -47,6 +47,7 @@ MODULES = [
|
|
|
47
47
|
("pyzipper", "read aes zip", "meltify doctor --install crypto", False),
|
|
48
48
|
("cryptography", "read encrypted hwp iwork", "meltify doctor --install crypto", False),
|
|
49
49
|
("numbers_parser", "read numbers", "meltify doctor --install iwork", False),
|
|
50
|
+
("pyarrow", "read parquet", "meltify doctor --install parquet", False),
|
|
50
51
|
("playwright", "read --render", "meltify doctor --install render", False),
|
|
51
52
|
("yt_dlp", "media urls", "meltify doctor --install media", False),
|
|
52
53
|
("paddleocr", "ocr engine paddle", "meltify doctor --install ocr-paddle", False),
|
|
@@ -78,6 +79,7 @@ EXTRAS = [
|
|
|
78
79
|
"office",
|
|
79
80
|
"archive",
|
|
80
81
|
"iwork",
|
|
82
|
+
"parquet",
|
|
81
83
|
"crypto",
|
|
82
84
|
"render",
|
|
83
85
|
"media",
|
|
@@ -628,6 +628,7 @@ def _run(args: argparse.Namespace, settings: dict[str, Any], password: str | Non
|
|
|
628
628
|
password=given_password(args, env.warnings.append, password),
|
|
629
629
|
fallback=bool(conf.get("fallback", True)),
|
|
630
630
|
shallow=args.shallow,
|
|
631
|
+
parquet_rows=int(conf.get("parquet_rows", 200)),
|
|
631
632
|
quicklook=looks,
|
|
632
633
|
temps=temps,
|
|
633
634
|
)
|
|
@@ -346,6 +346,7 @@ register("media", f"{_HERE}:audio", *AUDIO)
|
|
|
346
346
|
register("media", f"{_HERE}:video", *VIDEO)
|
|
347
347
|
register("office", f"{_HERE}.office:convert", *OFFICE)
|
|
348
348
|
register("web", f"{_HERE}.web:convert", ".html", ".htm", ".xhtml")
|
|
349
|
+
register("webarchive", f"{_HERE}.webarchive:convert", ".webarchive")
|
|
349
350
|
register("epub", f"{_HERE}.epub:convert", ".epub")
|
|
350
351
|
register(
|
|
351
352
|
"archive",
|
|
@@ -372,6 +373,7 @@ register("wordperfect", f"{_HERE}.wordperfect:convert", ".wpd", ".wp", ".wp5", "
|
|
|
372
373
|
register("notebook", f"{_HERE}.notebook:convert", ".ipynb")
|
|
373
374
|
register("mbox", f"{_HERE}.mbox:convert", ".mbox")
|
|
374
375
|
register("sqlite", f"{_HERE}.sqlite:convert", ".db", ".sqlite", ".sqlite3")
|
|
376
|
+
register("parquet", f"{_HERE}.parquet:convert", ".parquet")
|
|
375
377
|
# Registered after TEXT, so these take over .srt and .vtt from the plain text entry
|
|
376
378
|
register("subtitle", f"{_HERE}.subtitle:convert", ".srt", ".vtt", ".ass")
|
|
377
379
|
register("text", f"{_HERE}.text:convert_card", ".vcf", ".ics")
|
|
@@ -383,6 +385,7 @@ register_sniff("slack", f"{_HERE}.slack:convert", f"{_HERE}.slack:sniff", {".zip
|
|
|
383
385
|
register_sniff("pdf", f"{_HERE}.pdf:convert", _magic(b"%PDF-"))
|
|
384
386
|
register_sniff("sqlite", f"{_HERE}.sqlite:convert", _magic(b"SQLite format 3\x00"))
|
|
385
387
|
register_sniff("rtf", f"{_HERE}.rtf:convert", _magic(b"{\\rtf"))
|
|
388
|
+
register_sniff("parquet", f"{_HERE}.parquet:convert", _magic(b"PAR1"))
|
|
386
389
|
register_sniff("wordperfect", f"{_HERE}.wordperfect:convert", _wordperfect)
|
|
387
390
|
# Document zips before the archive sniff, so an unnamed one isn't unpacked as a plain zip
|
|
388
391
|
for kind, target, families in (
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Parquet tables: the schema, a per-column profile, then the first rows cited by row number
|
|
2
|
+
|
|
3
|
+
A cite like `a.parquet:7` points at the seventh data row, counted from 1
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import replace
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from meltify.converters import Block, Converted, run
|
|
12
|
+
from meltify.converters.limits import MAX_MEMBER_BYTES, human_bytes
|
|
13
|
+
from meltify.converters.tables import sample_cell
|
|
14
|
+
from meltify.evidence import Src
|
|
15
|
+
from meltify.needs import not_read
|
|
16
|
+
from meltify.safe import MissingTool
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _groups(meta, limit: int) -> tuple[list[int], str | None]:
|
|
20
|
+
"""Leading row groups that hold the first `limit` rows, and why they stop short if they do
|
|
21
|
+
|
|
22
|
+
Arrow unpacks a whole page at a time, and a few KB of zstd can declare one value of
|
|
23
|
+
hundreds of MB, so the sizes the footer declares are checked before anything is read
|
|
24
|
+
"""
|
|
25
|
+
groups: list[int] = []
|
|
26
|
+
rows = size = 0
|
|
27
|
+
for i in range(meta.num_row_groups):
|
|
28
|
+
if rows >= limit:
|
|
29
|
+
break
|
|
30
|
+
group = meta.row_group(i)
|
|
31
|
+
columns = sum(group.column(j).total_uncompressed_size for j in range(group.num_columns))
|
|
32
|
+
size += max(group.total_byte_size, columns)
|
|
33
|
+
if size > MAX_MEMBER_BYTES:
|
|
34
|
+
past = human_bytes(MAX_MEMBER_BYTES)
|
|
35
|
+
return groups, f"row group {i + 1} not read, it unpacks past {past}"
|
|
36
|
+
groups.append(i)
|
|
37
|
+
rows += group.num_rows
|
|
38
|
+
return groups, None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _head(f, limit: int, groups: list[int]) -> list[tuple]:
|
|
42
|
+
rows: list[tuple] = []
|
|
43
|
+
# Batches end at row group boundaries, so one batch may hold fewer rows than asked
|
|
44
|
+
for batch in f.iter_batches(batch_size=limit, row_groups=groups):
|
|
45
|
+
# Tuples, not dicts, so two columns with the same name both survive
|
|
46
|
+
rows += list(zip(*(c.to_pylist() for c in batch.columns), strict=True))
|
|
47
|
+
if len(rows) >= limit:
|
|
48
|
+
break
|
|
49
|
+
return rows[:limit]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _profile(meta) -> str:
|
|
53
|
+
"""Min, max and nulls per column from the footer statistics, without reading any data
|
|
54
|
+
|
|
55
|
+
A row group that wrote no statistics leaves that column's cells blank, since the rest
|
|
56
|
+
can't speak for it
|
|
57
|
+
"""
|
|
58
|
+
lines = ["| column | min | max | nulls |", "|---|---|---|---|"]
|
|
59
|
+
for j in range(meta.num_columns):
|
|
60
|
+
stats = [meta.row_group(i).column(j).statistics for i in range(meta.num_row_groups)]
|
|
61
|
+
low = high = nulls = ""
|
|
62
|
+
if stats and all(s is not None and s.has_min_max for s in stats):
|
|
63
|
+
low, high = min(s.min for s in stats), max(s.max for s in stats)
|
|
64
|
+
if stats and all(s is not None and s.has_null_count for s in stats):
|
|
65
|
+
nulls = f"{sum(s.null_count for s in stats):,}"
|
|
66
|
+
name = meta.schema.column(j).path
|
|
67
|
+
cells = " | ".join(map(sample_cell, (name, low, high)))
|
|
68
|
+
lines.append(f"| {cells} | {nulls} |")
|
|
69
|
+
return "\n".join(lines)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def convert(path: Path, src: Src) -> Converted:
|
|
73
|
+
try:
|
|
74
|
+
import pyarrow.parquet as pq
|
|
75
|
+
except ImportError as e:
|
|
76
|
+
raise MissingTool("pyarrow", "meltify doctor --install parquet") from e
|
|
77
|
+
|
|
78
|
+
out = Converted("parquet")
|
|
79
|
+
with pq.ParquetFile(path) as f:
|
|
80
|
+
total = f.metadata.num_rows
|
|
81
|
+
fields = list(f.schema_arrow)
|
|
82
|
+
schema = "\n".join(f"{field.name}: {field.type}" for field in fields)
|
|
83
|
+
out.blocks.append(Block(src, f"{total:,} rows\n{schema}"))
|
|
84
|
+
if f.metadata.num_row_groups:
|
|
85
|
+
out.blocks.append(Block(src, _profile(f.metadata)))
|
|
86
|
+
limit = run.current().parquet_rows or total
|
|
87
|
+
groups, cut = _groups(f.metadata, limit)
|
|
88
|
+
rows = _head(f, limit, groups) if groups else []
|
|
89
|
+
|
|
90
|
+
if rows:
|
|
91
|
+
lines = [
|
|
92
|
+
"| row | " + " | ".join(sample_cell(field.name) for field in fields) + " |",
|
|
93
|
+
"|---" * (len(fields) + 1) + "|",
|
|
94
|
+
]
|
|
95
|
+
for n, row in enumerate(rows, start=1):
|
|
96
|
+
lines.append(f"| {n} | " + " | ".join(sample_cell(v) for v in row) + " |")
|
|
97
|
+
out.blocks.append(Block(replace(src, line=1), "\n".join(lines)))
|
|
98
|
+
if (rest := total - len(rows)) > 0:
|
|
99
|
+
shown = f"first {len(rows):,} shown, read.parquet_rows sets how many"
|
|
100
|
+
out.needs.append(not_read(rest, "more row", shown))
|
|
101
|
+
if cut:
|
|
102
|
+
out.needs.append(cut)
|
|
103
|
+
return out
|
|
@@ -71,6 +71,9 @@ class RunContext:
|
|
|
71
71
|
fallback: bool = True # read.fallback
|
|
72
72
|
# read --shallow, which starts no renderer or subprocess that only feeds OCR
|
|
73
73
|
shallow: bool = False
|
|
74
|
+
# read.parquet_rows, 0 for every row. A data file can hold billions of rows, so only the
|
|
75
|
+
# head is melted and the rest counted
|
|
76
|
+
parquet_rows: int = 200
|
|
74
77
|
quicklook: bool = True # render.quicklook
|
|
75
78
|
# State the run's workers share, which two runs with the same settings still don't
|
|
76
79
|
budget: Budget = field(default_factory=Budget, compare=False)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Safari web archives: the saved page through the web extractor, its images through OCR
|
|
2
|
+
|
|
3
|
+
A webarchive is a property list holding the page bytes, every subresource the page
|
|
4
|
+
loaded, and one nested archive per frame
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import mimetypes
|
|
11
|
+
import plistlib
|
|
12
|
+
import posixpath
|
|
13
|
+
import tempfile
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from urllib.parse import urldefrag, urljoin, urlparse
|
|
16
|
+
|
|
17
|
+
from meltify.converters import Child, Converted
|
|
18
|
+
from meltify.converters.embeds import Embeds
|
|
19
|
+
from meltify.evidence import Src
|
|
20
|
+
from meltify.needs import not_read
|
|
21
|
+
|
|
22
|
+
HTML = {"text/html", "application/xhtml+xml"}
|
|
23
|
+
# Distinct frames melted per archive, the rest listed in needs
|
|
24
|
+
MAX_FRAMES = 100
|
|
25
|
+
# Frame bytes per byte of the archive. Frames are slices of the archive, so unless they
|
|
26
|
+
# share bytes they can't add up to more than it
|
|
27
|
+
FRAME_RATIO = 2
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _name(resource: dict, fallback: str) -> str:
|
|
31
|
+
"""A file name that keeps a suffix, so the registry can melt the resource as a child"""
|
|
32
|
+
name = posixpath.basename(urlparse(resource.get("WebResourceURL") or "").path) or fallback
|
|
33
|
+
if not Path(name).suffix:
|
|
34
|
+
name += mimetypes.guess_extension(resource.get("WebResourceMIMEType") or "") or ""
|
|
35
|
+
return name
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _image(resource: object) -> bytes | None:
|
|
39
|
+
# Stylesheets, scripts and fonts carry no text worth citing
|
|
40
|
+
if not isinstance(resource, dict):
|
|
41
|
+
return None
|
|
42
|
+
data = resource.get("WebResourceData")
|
|
43
|
+
image = str(resource.get("WebResourceMIMEType")).startswith("image/")
|
|
44
|
+
return data if image and isinstance(data, bytes) else None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _utf8(data: bytes, encoding: str | None) -> bytes:
|
|
48
|
+
# Safari records the encoding the page was served with, which may disagree with its meta
|
|
49
|
+
# tag, and a UTF-8 byte order mark outranks the meta tag in the HTML parser
|
|
50
|
+
if not encoding:
|
|
51
|
+
return data
|
|
52
|
+
try:
|
|
53
|
+
text = data.decode(encoding, errors="replace")
|
|
54
|
+
except LookupError:
|
|
55
|
+
return data
|
|
56
|
+
return b"\xef\xbb\xbf" + text.removeprefix("\ufeff").encode("utf-8")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _page(resource: dict, src: Src, images: dict[str, tuple[str, bytes]], used: set[str]):
|
|
60
|
+
"""The page, with the pictures it references loaded from the archive"""
|
|
61
|
+
from meltify.converters.web import convert_page
|
|
62
|
+
|
|
63
|
+
base = resource.get("WebResourceURL") or ""
|
|
64
|
+
|
|
65
|
+
def load(ref: str, embeds: Embeds) -> tuple[str, bytes] | str:
|
|
66
|
+
url = urldefrag(urljoin(base, ref)).url
|
|
67
|
+
if url not in images:
|
|
68
|
+
return "remote image"
|
|
69
|
+
used.add(url)
|
|
70
|
+
return images[url]
|
|
71
|
+
|
|
72
|
+
data = _utf8(resource["WebResourceData"], resource.get("WebResourceTextEncodingName"))
|
|
73
|
+
with tempfile.TemporaryDirectory(prefix="meltify-webarchive-") as tmp:
|
|
74
|
+
page = Path(tmp) / "page.html"
|
|
75
|
+
page.write_bytes(data)
|
|
76
|
+
# A saved page keeps navigation and footers, as a saved .html does
|
|
77
|
+
out = convert_page(page, src, whole=True, load=load)
|
|
78
|
+
out.kind = "webarchive"
|
|
79
|
+
return out
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def convert(path: Path, src: Src) -> Converted:
|
|
83
|
+
raw = path.read_bytes()
|
|
84
|
+
try:
|
|
85
|
+
archive = plistlib.loads(raw)
|
|
86
|
+
except ValueError as e:
|
|
87
|
+
return Converted("webarchive", needs=[f"unreadable webarchive: {e}"[:200]])
|
|
88
|
+
main = archive.get("WebMainResource") if isinstance(archive, dict) else None
|
|
89
|
+
if not isinstance(main, dict) or not isinstance(main.get("WebResourceData"), bytes):
|
|
90
|
+
return Converted("webarchive", needs=["webarchive has no main resource"])
|
|
91
|
+
|
|
92
|
+
embeds = Embeds()
|
|
93
|
+
images: dict[str, tuple[str, bytes]] = {}
|
|
94
|
+
for i, res in enumerate(archive.get("WebSubresources") or [], start=1):
|
|
95
|
+
data = _image(res)
|
|
96
|
+
if data is None:
|
|
97
|
+
continue
|
|
98
|
+
# Every picture is already in memory with the plist, so only the size limit applies
|
|
99
|
+
# here, and the page's references share each one under the picture total
|
|
100
|
+
if why := embeds.refusal(len(data)):
|
|
101
|
+
embeds.skipped[why] += 1
|
|
102
|
+
continue
|
|
103
|
+
images[urldefrag(res.get("WebResourceURL") or f"#{i}").url] = (
|
|
104
|
+
_name(res, f"image{i}"),
|
|
105
|
+
data,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
used: set[str] = set()
|
|
109
|
+
if (main.get("WebResourceMIMEType") or "text/html") in HTML:
|
|
110
|
+
out = _page(main, src, images, used)
|
|
111
|
+
else:
|
|
112
|
+
# Safari also saves a bare image or PDF this way, so it melts like the file itself
|
|
113
|
+
out = Converted("webarchive")
|
|
114
|
+
out.children.append(Child(_name(main, "main"), src, data=main["WebResourceData"]))
|
|
115
|
+
# Pictures only a stylesheet or script loads, cited by their name in the archive
|
|
116
|
+
for url, (name, data) in images.items():
|
|
117
|
+
if url not in used:
|
|
118
|
+
# An SVG melts as a child under its own name, so it hangs off the archive itself
|
|
119
|
+
at = src if Path(name).suffix.lower() == ".svg" else src.inside(name)
|
|
120
|
+
embeds.picture(at, data, name)
|
|
121
|
+
embeds.into(out)
|
|
122
|
+
|
|
123
|
+
out.needs += _frames(archive.get("WebSubframeArchives") or [], src, len(raw), out)
|
|
124
|
+
return out
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _frames(frames: list, src: Src, size: int, out: Converted) -> list[str]:
|
|
128
|
+
"""Each frame is a whole webarchive of its own, melted again as a child
|
|
129
|
+
|
|
130
|
+
A binary plist can reference one frame object many times, and dumping each reference
|
|
131
|
+
again would multiply the archive at every nesting level. Identical frames go out once,
|
|
132
|
+
and the frames together never get more bytes than the archive itself holds
|
|
133
|
+
"""
|
|
134
|
+
seen: set[int] = set()
|
|
135
|
+
dumped: set[bytes] = set()
|
|
136
|
+
left = FRAME_RATIO * size
|
|
137
|
+
many = large = 0
|
|
138
|
+
for i, frame in enumerate(frames, start=1):
|
|
139
|
+
if id(frame) in seen:
|
|
140
|
+
continue
|
|
141
|
+
seen.add(id(frame))
|
|
142
|
+
if len(dumped) >= MAX_FRAMES:
|
|
143
|
+
many += 1
|
|
144
|
+
continue
|
|
145
|
+
data = plistlib.dumps(frame, fmt=plistlib.FMT_BINARY)
|
|
146
|
+
digest = hashlib.sha256(data).digest()
|
|
147
|
+
if digest in dumped:
|
|
148
|
+
continue
|
|
149
|
+
if len(data) > left:
|
|
150
|
+
large += 1
|
|
151
|
+
continue
|
|
152
|
+
left -= len(data)
|
|
153
|
+
dumped.add(digest)
|
|
154
|
+
out.children.append(Child(f"frame{i}.webarchive", src, data=data))
|
|
155
|
+
needs = []
|
|
156
|
+
if many:
|
|
157
|
+
needs.append(not_read(many, "frame", f"over {MAX_FRAMES} frames"))
|
|
158
|
+
if large:
|
|
159
|
+
needs.append(not_read(large, "frame", "more frame data than the archive holds"))
|
|
160
|
+
return needs
|
|
@@ -74,6 +74,8 @@ max_bytes = 20000000
|
|
|
74
74
|
# Binary files no converter reads get one more try: LibreOffice for the formats it imports,
|
|
75
75
|
# Pillow, ffprobe, then on macOS Quick Look and Spotlight. Off keeps them as an unsupported need
|
|
76
76
|
fallback = true
|
|
77
|
+
# Parquet rows melted per file, 0 for all. The rest stays counted as a need
|
|
78
|
+
parquet_rows = 200
|
|
77
79
|
|
|
78
80
|
[render]
|
|
79
81
|
# macOS only. Quick Look previews draw what LibreOffice can't or isn't installed for, like
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|