morphconv 1.0.1__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.
- morphconv-1.0.1/LICENSE +21 -0
- morphconv-1.0.1/PKG-INFO +396 -0
- morphconv-1.0.1/README.md +357 -0
- morphconv-1.0.1/morph/__init__.py +0 -0
- morphconv-1.0.1/morph/batch.py +492 -0
- morphconv-1.0.1/morph/cli.py +703 -0
- morphconv-1.0.1/morph/converters/__init__.py +19 -0
- morphconv-1.0.1/morph/converters/archives.py +85 -0
- morphconv-1.0.1/morph/converters/audio.py +58 -0
- morphconv-1.0.1/morph/converters/csv_json.py +28 -0
- morphconv-1.0.1/morph/converters/csv_to_xlsx.py +290 -0
- morphconv-1.0.1/morph/converters/documents.py +252 -0
- morphconv-1.0.1/morph/converters/ebooks.py +36 -0
- morphconv-1.0.1/morph/converters/fonts.py +72 -0
- morphconv-1.0.1/morph/converters/images.py +119 -0
- morphconv-1.0.1/morph/converters/json_yaml.py +26 -0
- morphconv-1.0.1/morph/converters/models_3d.py +149 -0
- morphconv-1.0.1/morph/converters/ocr.py +49 -0
- morphconv-1.0.1/morph/converters/pandas_extra.py +127 -0
- morphconv-1.0.1/morph/converters/svg.py +63 -0
- morphconv-1.0.1/morph/converters/video.py +153 -0
- morphconv-1.0.1/morph/converters/vtracer_converter.py +84 -0
- morphconv-1.0.1/morph/converters/web.py +92 -0
- morphconv-1.0.1/morph/converters/xlsx_to_csv.py +272 -0
- morphconv-1.0.1/morph/deps.py +228 -0
- morphconv-1.0.1/morph/ffmpeg_utils.py +79 -0
- morphconv-1.0.1/morph/history.py +136 -0
- morphconv-1.0.1/morph/progress.py +78 -0
- morphconv-1.0.1/morph/registry.py +236 -0
- morphconv-1.0.1/morph/tui.py +792 -0
- morphconv-1.0.1/morphconv.egg-info/PKG-INFO +396 -0
- morphconv-1.0.1/morphconv.egg-info/SOURCES.txt +40 -0
- morphconv-1.0.1/morphconv.egg-info/dependency_links.txt +1 -0
- morphconv-1.0.1/morphconv.egg-info/entry_points.txt +2 -0
- morphconv-1.0.1/morphconv.egg-info/requires.txt +30 -0
- morphconv-1.0.1/morphconv.egg-info/top_level.txt +1 -0
- morphconv-1.0.1/pyproject.toml +51 -0
- morphconv-1.0.1/setup.cfg +4 -0
- morphconv-1.0.1/tests/test_cli.py +66 -0
- morphconv-1.0.1/tests/test_converters.py +197 -0
- morphconv-1.0.1/tests/test_registry.py +47 -0
- morphconv-1.0.1/tests/test_web.py +46 -0
morphconv-1.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 morph contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
morphconv-1.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: morphconv
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: morph — convert anything to anything, from the CLI
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: pandas>=2.2.0
|
|
10
|
+
Requires-Dist: openpyxl>=3.1.2
|
|
11
|
+
Requires-Dist: xlsxwriter>=3.2.0
|
|
12
|
+
Requires-Dist: xlrd>=2.0.1
|
|
13
|
+
Requires-Dist: pyarrow>=15.0.0
|
|
14
|
+
Requires-Dist: odfpy>=1.4.1
|
|
15
|
+
Requires-Dist: lxml>=5.1.0
|
|
16
|
+
Requires-Dist: SQLAlchemy>=2.0.0
|
|
17
|
+
Requires-Dist: rich>=13.7.0
|
|
18
|
+
Requires-Dist: typer>=0.12.0
|
|
19
|
+
Requires-Dist: chardet>=5.2.0
|
|
20
|
+
Requires-Dist: python-dateutil>=2.9.0
|
|
21
|
+
Requires-Dist: click>=8.1.7
|
|
22
|
+
Requires-Dist: pyyaml>=6.0.1
|
|
23
|
+
Requires-Dist: textual>=0.60.0
|
|
24
|
+
Requires-Dist: Pillow>=10.0.0
|
|
25
|
+
Requires-Dist: pillow-heif>=0.15.0
|
|
26
|
+
Requires-Dist: pillow-avif-plugin>=1.4.3
|
|
27
|
+
Requires-Dist: cairosvg>=2.7.1
|
|
28
|
+
Requires-Dist: fonttools[woff]>=4.50.0
|
|
29
|
+
Requires-Dist: pytesseract>=0.3.10
|
|
30
|
+
Requires-Dist: trafilatura>=1.8.0
|
|
31
|
+
Requires-Dist: crawl4ai>=0.4.0
|
|
32
|
+
Requires-Dist: playwright>=1.40.0
|
|
33
|
+
Requires-Dist: vtracer>=0.6.0
|
|
34
|
+
Requires-Dist: bpy>=5.0.0
|
|
35
|
+
Provides-Extra: dev
|
|
36
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
37
|
+
Requires-Dist: pytest-mock>=3.12.0; extra == "dev"
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
<div align="center">
|
|
41
|
+
|
|
42
|
+
# morph
|
|
43
|
+
|
|
44
|
+
**Convert anything to anything, from the CLI.**
|
|
45
|
+
|
|
46
|
+
One command. No format-specific tools to remember. No cloud upload. No API keys.
|
|
47
|
+
|
|
48
|
+
[](LICENSE)
|
|
49
|
+
[](https://www.python.org/downloads/)
|
|
50
|
+
[](https://typer.tiangolo.com/)
|
|
51
|
+
[](https://github.com/Textualize/rich)
|
|
52
|
+
[](https://textual.textualize.io/)
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
morph report.docx report.pdf
|
|
56
|
+
morph data.csv data.xlsx --table-style TableStyleMedium2 --header-bg 2E7D32
|
|
57
|
+
morph clip.mp4 clip.gif --fps 10 --width 480
|
|
58
|
+
morph batch '*.mp4' mp3 --workers 4
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Why morph exists
|
|
66
|
+
|
|
67
|
+
`ffmpeg` converts media. `pandoc` converts documents. `ImageMagick` converts images.
|
|
68
|
+
Each is excellent at its one job and painful to use next to the others — different
|
|
69
|
+
flag dialects, different mental models, different install stories. Nobody had wired
|
|
70
|
+
them into **one coherent tool with one grammar and a genuinely good CLI/TUI**.
|
|
71
|
+
|
|
72
|
+
morph doesn't reinvent codecs or file formats — it orchestrates the best tool for
|
|
73
|
+
each job (ffmpeg, pandoc, Pillow, or plain Python) behind a single, predictable
|
|
74
|
+
interface, and handles the annoying parts for you: detecting what's missing,
|
|
75
|
+
telling you exactly what it'll install and asking first, and routing through
|
|
76
|
+
intermediate formats automatically when there's no direct converter.
|
|
77
|
+
|
|
78
|
+
## Features
|
|
79
|
+
|
|
80
|
+
- **One verb.** `morph <input> <output>` — not `morph convert`. morph *is* the verb.
|
|
81
|
+
- **Smart multi-hop routing.** No direct `csv → yaml` converter? morph finds
|
|
82
|
+
`csv → json → yaml` on its own. By utilizing a Breadth-First Search (BFS) directed graph, morph instantly unlocks thousands of implicit conversions. Currently supporting **66 formats and over 1,000+ multi-hop routes** across data, documents, images, audio, and video!
|
|
83
|
+
- **Full control, contextually.** `morph data.csv data.xlsx --help` shows exactly
|
|
84
|
+
the flags relevant to *that* pair — table styles, freeze panes, passwords —
|
|
85
|
+
and nothing from unrelated converters.
|
|
86
|
+
- **Batch conversion.** `morph batch '*.mp4' mp3` converts many files in parallel
|
|
87
|
+
with a live Rich progress table, smart output strategies, and a clean summary.
|
|
88
|
+
- **Conversion history.** Every conversion is logged to `~/.morph_history.jsonl`.
|
|
89
|
+
`morph history` shows a searchable, filterable table of past jobs.
|
|
90
|
+
- **Cross-platform dependency management.** Missing `ffmpeg` or `pandoc`? morph
|
|
91
|
+
detects your package manager (brew/apt/dnf/pacman/winget/choco/…), shows you the
|
|
92
|
+
*exact* install command, and never runs it without asking first.
|
|
93
|
+
- **A real interactive TUI**, fully keyboard-driven — type a path (with autocomplete
|
|
94
|
+
dropdown for filesystem suggestions), arrow through formats (options update live),
|
|
95
|
+
and watch a real progress bar while it converts. No mouse needed.
|
|
96
|
+
- **Live execution feedback.** ffmpeg hops show a real progress bar driven by
|
|
97
|
+
ffmpeg's own `-progress` stream. Everything else shows a spinner labeled with
|
|
98
|
+
which tool is working (`via pandoc`, `via pillow`, ...).
|
|
99
|
+
- **Pluggable by design.** Drop a file in `morph/converters/`, call `register()`,
|
|
100
|
+
and it's live — no central registry to edit.
|
|
101
|
+
- **Local-first.** Nothing leaves your machine. No accounts, no upload, no API keys.
|
|
102
|
+
- **Transparent Remote Downloads.** Pass any `http://` link ending in a file extension (e.g., `morph https://domain.com/data.csv out.json`) and morph will stream it to a local temp file, render a live progress bar, convert it, and clean it up automatically.
|
|
103
|
+
- **Global Configuration (`~/.morphrc`).** Generate a fully-commented YAML configuration file containing every single flag for every converter using `morph config`. CLI arguments seamlessly override these base defaults, giving you maximum control.
|
|
104
|
+
|
|
105
|
+
## Installation
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
git clone https://github.com/hariharen9/morph.git
|
|
109
|
+
cd morph
|
|
110
|
+
pip install -e .
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
That's it for **data, image, and archive** conversions — pure Python, zero external
|
|
114
|
+
binaries. **Documents** (docx/pdf/html/…) need `pandoc`; **audio/video** need
|
|
115
|
+
`ffmpeg`. You don't need to install these up front — morph detects what a given
|
|
116
|
+
conversion needs and prompts you the first time you hit it.
|
|
117
|
+
|
|
118
|
+
Check what's available any time:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
morph deps
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Quickstart
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
# Data
|
|
128
|
+
morph data.csv data.xlsx # styled workbook: table, banding, autofit
|
|
129
|
+
morph data.csv data.json
|
|
130
|
+
morph data.csv data.yaml # routed through json automatically
|
|
131
|
+
|
|
132
|
+
# Documents (pandoc)
|
|
133
|
+
morph notes.md notes.docx
|
|
134
|
+
morph report.docx report.pdf
|
|
135
|
+
morph book.epub book.html
|
|
136
|
+
|
|
137
|
+
# Images (Pillow + cairosvg)
|
|
138
|
+
morph photo.png photo.webp --quality 80 --resize 1200x
|
|
139
|
+
morph icon.png icon.ico # multi-resolution ICO
|
|
140
|
+
morph diagram.svg diagram.png
|
|
141
|
+
|
|
142
|
+
# Fonts (fontTools)
|
|
143
|
+
morph font.ttf font.woff2
|
|
144
|
+
morph font.woff font.otf
|
|
145
|
+
|
|
146
|
+
# Remote Network Files
|
|
147
|
+
morph https://example.com/demo.mp4 demo.gif # streams to a temp file, converts, and cleans up
|
|
148
|
+
morph https://example.com/article.html doc.pdf --js # scrapes via crawl4ai if no format is detected
|
|
149
|
+
|
|
150
|
+
# Ebooks (Calibre)
|
|
151
|
+
morph book.epub book.mobi
|
|
152
|
+
morph book.epub book.azw3
|
|
153
|
+
|
|
154
|
+
# Audio / video (ffmpeg)
|
|
155
|
+
morph song.wav song.mp3 --bitrate 192k
|
|
156
|
+
morph clip.mov clip.mp4 --resolution 1280x720 --fps 30
|
|
157
|
+
morph clip.mp4 clip.mp3 # extract the audio track
|
|
158
|
+
morph clip.mp4 clip.gif --fps 10 --width 480
|
|
159
|
+
|
|
160
|
+
# Archives
|
|
161
|
+
morph project.zip project.tar.gz
|
|
162
|
+
|
|
163
|
+
# No args — interactive TUI
|
|
164
|
+
morph
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Batch conversion
|
|
168
|
+
|
|
169
|
+
Convert many files at once with a live progress display:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
# All MP4s → MP3, 4 parallel workers
|
|
173
|
+
morph batch '*.mp4' mp3 --workers 4
|
|
174
|
+
|
|
175
|
+
# Whole directory, recursive, into a separate output folder
|
|
176
|
+
morph batch ./raw/ mp3 --recursive --out-dir ./converted/
|
|
177
|
+
|
|
178
|
+
# Mirror the input directory structure
|
|
179
|
+
morph batch ./raw/ mp3 --recursive --out-dir ./converted/ --mirror
|
|
180
|
+
|
|
181
|
+
# Custom output name template
|
|
182
|
+
morph batch '*.mp4' mp3 --rename '{stem}_audio'
|
|
183
|
+
# → myvideo_audio.mp3
|
|
184
|
+
|
|
185
|
+
# Skip files whose output already exists
|
|
186
|
+
morph batch '*.mp4' mp3 --skip-existing
|
|
187
|
+
|
|
188
|
+
# Only re-convert if input is newer than output (like make)
|
|
189
|
+
morph batch '*.mp4' mp3 --newer-only
|
|
190
|
+
|
|
191
|
+
# Preview what would happen — no conversion
|
|
192
|
+
morph batch '*.mp4' mp3 --dry-run
|
|
193
|
+
|
|
194
|
+
# Multiple patterns at once
|
|
195
|
+
morph batch '*.flac' '*.wav' mp3
|
|
196
|
+
|
|
197
|
+
# Pass converter flags through
|
|
198
|
+
morph batch '*.mp4' mp3 --bitrate 192k
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
During conversion, morph shows a live table:
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
╭─ morph batch — 8 files → mp3 ─────────────────────────────────────╮
|
|
205
|
+
│ Status File Time Size │
|
|
206
|
+
│ ✓ done concert_01.flac 0:00:04 8.3MB → 4.1MB │
|
|
207
|
+
│ ✓ done concert_02.flac 0:00:03 7.9MB → 3.8MB │
|
|
208
|
+
│ ▶ converting… concert_03.flac 0:00:01 │
|
|
209
|
+
│ · waiting concert_04.wav │
|
|
210
|
+
│ │
|
|
211
|
+
│ Overall ████████░░░░░░░░ 2/8 [0:00:07] │
|
|
212
|
+
╰──────────────────────────────────────────────────────────────────────╯
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Conversion history
|
|
216
|
+
|
|
217
|
+
Every conversion (single and batch) is automatically logged:
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
morph history # last 20 conversions
|
|
221
|
+
morph history -n 50 # last 50
|
|
222
|
+
morph history --failed # only failures
|
|
223
|
+
morph history --fmt mp4 # filter by format
|
|
224
|
+
morph history --clear # wipe history
|
|
225
|
+
morph history --json # raw JSONL output (for scripting)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
```
|
|
229
|
+
┌──────────┬──────┬──────┬────────────────┬────────┬────────────┐
|
|
230
|
+
│ When │ From │ To │ File │ Mode │ Status │
|
|
231
|
+
├──────────┼──────┼──────┼────────────────┼────────┼────────────┤
|
|
232
|
+
│ 2m ago │ md │ docx │ README.md │ single │ ✓ 1.8s │
|
|
233
|
+
│ 1h ago │ mp4 │ mp3 │ intro.mp4 │ batch │ ✓ 4.2s │
|
|
234
|
+
│ yesterday│ csv │ xlsx │ data.csv │ single │ ✓ 0.1s │
|
|
235
|
+
└──────────┴──────┴──────┴────────────────┴────────┴────────────┘
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
History is stored at `~/.morph_history.jsonl` — one JSON object per line,
|
|
239
|
+
easy to parse with `jq` or Python.
|
|
240
|
+
|
|
241
|
+
### Every conversion is self-documenting
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
$ morph data.csv data.xlsx --help
|
|
245
|
+
```
|
|
246
|
+
```
|
|
247
|
+
╭─ morph — conversion route ─╮
|
|
248
|
+
│ csv → xlsx │
|
|
249
|
+
╰─────────────────────────────╯
|
|
250
|
+
|
|
251
|
+
Options for this conversion (csv → xlsx):
|
|
252
|
+
-d, --delimiter Field delimiter. Auto-detected if omitted.
|
|
253
|
+
--table-style Excel table style name. (default: TableStyleMedium9)
|
|
254
|
+
--header-bg Header background hex colour (no #). (default: 1F3864)
|
|
255
|
+
--freeze-cols Left-most columns to freeze.
|
|
256
|
+
--password Password-protect the output sheet.
|
|
257
|
+
... (more)
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
The exact same file pointed at `report.pdf` shows a completely different,
|
|
261
|
+
shorter flag set — because pandoc's PDF pipeline genuinely only has one knob
|
|
262
|
+
worth exposing.
|
|
263
|
+
|
|
264
|
+
### See what's reachable
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
# Tree view grouped by family — great for discovery
|
|
268
|
+
$ morph formats
|
|
269
|
+
morph — 45 formats across 8 families
|
|
270
|
+
|
|
271
|
+
* document (11 formats, via pandoc, 82 direct routes)
|
|
272
|
+
├── md → docx epub html latex odt pdf rst rtf txt
|
|
273
|
+
├── docx → epub html latex md odt pdf rst rtf txt
|
|
274
|
+
└── ...
|
|
275
|
+
|
|
276
|
+
* audio (6 formats, via ffmpeg, 30 direct routes)
|
|
277
|
+
├── mp3 → aac flac m4a ogg wav
|
|
278
|
+
└── ...
|
|
279
|
+
|
|
280
|
+
# Per-format table — see everything reachable from one format
|
|
281
|
+
$ morph formats mp4
|
|
282
|
+
┌────────┬──────────────────┬──────┐
|
|
283
|
+
│ Target │ Route │ Hops │
|
|
284
|
+
├────────┼──────────────────┼──────┤
|
|
285
|
+
│ mp3 │ mp4 → mp3 │ 1 │
|
|
286
|
+
│ gif │ mp4 → gif │ 1 │
|
|
287
|
+
│ webm │ mp4 → webm │ 1 │
|
|
288
|
+
│ ... │ ... │ ... │
|
|
289
|
+
└────────┴──────────────────┴──────┘
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Supported formats
|
|
293
|
+
|
|
294
|
+
| Domain | Formats | Backend |
|
|
295
|
+
|---|---|---|
|
|
296
|
+
| **Data** | csv, xlsx, json, yaml, parquet, feather, ods, xml, html, sqlite | native (pandas / openpyxl) |
|
|
297
|
+
| **Documents** | md, html, docx, odt, rtf, epub, latex, rst, txt, ipynb, pptx, adoc, org, opml, man, pdf* | pandoc |
|
|
298
|
+
| **Images** | png, jpg/jpeg, webp, bmp, gif, tiff, ico, heic, avif, icns, pdf, svg (input only) | Pillow / cairosvg |
|
|
299
|
+
| **Fonts** | ttf, otf, woff, woff2 | fontTools |
|
|
300
|
+
| **Ebooks** | epub, mobi, azw3 | Calibre (`ebook-convert`) |
|
|
301
|
+
| **Audio** | mp3, wav, flac, ogg, aac, m4a, opus, wma | ffmpeg |
|
|
302
|
+
| **Video** | mp4, mkv, mov, webm, avi, flv, wmv, mpeg (+ → audio, + → gif/webp, + → srt/vtt) | ffmpeg |
|
|
303
|
+
| **OCR** | png, jpg, webp, pdf, etc. → txt | tesseract |
|
|
304
|
+
| **Archives** | zip, tar, tar.gz, tar.bz2, tar.xz | stdlib |
|
|
305
|
+
|
|
306
|
+
<sub>*pdf is output-only — morph doesn't read PDFs back into structured content,
|
|
307
|
+
since that's a fundamentally lossier operation than every other conversion here.</sub>
|
|
308
|
+
|
|
309
|
+
## Architecture
|
|
310
|
+
|
|
311
|
+
```
|
|
312
|
+
morph/
|
|
313
|
+
├── cli.py # dispatch + all subcommands (run, batch, formats, history, deps)
|
|
314
|
+
├── registry.py # the graph: register(src, dst) edges + BFS routing
|
|
315
|
+
├── batch.py # parallel batch engine: ThreadPoolExecutor + Rich Live display
|
|
316
|
+
├── history.py # JSONL log at ~/.morph_history.jsonl
|
|
317
|
+
├── deps.py # cross-platform dep detection & guarded installer
|
|
318
|
+
├── progress.py # Rich progress bars for the CLI
|
|
319
|
+
├── ffmpeg_utils.py # ffmpeg -progress pipe parser for real % tracking
|
|
320
|
+
├── tui.py # Textual keyboard-driven TUI (same engine as CLI)
|
|
321
|
+
└── converters/
|
|
322
|
+
├── __init__.py # auto-discovers every file below — nothing to register by hand
|
|
323
|
+
├── csv_to_xlsx.py # self-contained: engine + OptionSpecs + register() in one file
|
|
324
|
+
├── xlsx_to_csv.py
|
|
325
|
+
├── csv_json.py
|
|
326
|
+
├── json_yaml.py
|
|
327
|
+
├── documents.py # one generic pandoc engine, many pairs
|
|
328
|
+
├── images.py # one generic Pillow engine, many pairs
|
|
329
|
+
├── svg.py # cairosvg for SVG → raster/PDF
|
|
330
|
+
├── audio.py # ffmpeg with real progress tracking
|
|
331
|
+
├── video.py # ffmpeg, includes → gif + audio extraction
|
|
332
|
+
├── archives.py
|
|
333
|
+
├── fonts.py # fontTools: ttf/otf/woff/woff2
|
|
334
|
+
└── ebooks.py # Calibre ebook-convert
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Every converter is a function `(input_path, output_path, **options) -> ConversionResult`,
|
|
338
|
+
registered once with `@register(src, dst, backend=..., options=[...])`. The CLI
|
|
339
|
+
never hardcodes a flag — it asks the registry which `OptionSpec`s apply to the
|
|
340
|
+
route it just resolved and builds the parser from that, which is also what makes
|
|
341
|
+
per-pair `--help` possible.
|
|
342
|
+
|
|
343
|
+
### Adding a new conversion
|
|
344
|
+
|
|
345
|
+
Drop a file in `morph/converters/`:
|
|
346
|
+
|
|
347
|
+
```python
|
|
348
|
+
# morph/converters/my_converter.py
|
|
349
|
+
from pathlib import Path
|
|
350
|
+
from ..registry import ConversionResult, OptionSpec, register
|
|
351
|
+
|
|
352
|
+
OPTIONS = [
|
|
353
|
+
OptionSpec("quality", ("--quality",), "Output quality (1-100).", default=85, type=int),
|
|
354
|
+
]
|
|
355
|
+
|
|
356
|
+
@register("foo", "bar", backend="sometool", family="image", options=OPTIONS)
|
|
357
|
+
def foo_to_bar(input_path: Path, output_path: Path, *, quality=85, **_) -> ConversionResult:
|
|
358
|
+
# ... do the conversion ...
|
|
359
|
+
return ConversionResult(output=output_path)
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
That's the whole integration. No imports to add elsewhere, no CLI wiring —
|
|
363
|
+
`converters/__init__.py` picks it up automatically, `morph x.foo y.bar` works
|
|
364
|
+
immediately, and it slots into the routing graph so anything that could reach
|
|
365
|
+
`.foo` can now reach `.bar` too.
|
|
366
|
+
|
|
367
|
+
## CLI reference
|
|
368
|
+
|
|
369
|
+
```
|
|
370
|
+
morph <input> <output> [OPTIONS] # convert a single file
|
|
371
|
+
morph <input> <output> --help # show flags for THIS conversion only
|
|
372
|
+
morph batch <patterns...> <format> # batch convert (see morph batch --help)
|
|
373
|
+
morph formats # tree of all formats by family
|
|
374
|
+
morph formats <fmt> # everything reachable from <fmt>
|
|
375
|
+
morph history # recent conversion log
|
|
376
|
+
morph deps # check / install external tools
|
|
377
|
+
morph # launch the interactive TUI
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
## Roadmap
|
|
381
|
+
|
|
382
|
+
- [ ] Per-pair codec tuning for audio/video (currently relies on ffmpeg defaults)
|
|
383
|
+
- [ ] HEIC/AVIF image support (pillow-heif / pillow-avif)
|
|
384
|
+
- [ ] `~/.morphrc` config file for persistent flag defaults
|
|
385
|
+
- [ ] Automated test suite
|
|
386
|
+
|
|
387
|
+
## Contributing
|
|
388
|
+
|
|
389
|
+
Issues and PRs welcome. If you're adding a converter, see
|
|
390
|
+
["Adding a new conversion"](#adding-a-new-conversion) above — that's the entire
|
|
391
|
+
contract. Please include a quick sanity check (a real input → real output,
|
|
392
|
+
not just "it imports") in your PR description.
|
|
393
|
+
|
|
394
|
+
## License
|
|
395
|
+
|
|
396
|
+
[MIT](LICENSE)
|