morph — convert anything to anything, from the CLI
Project description
morph
Convert anything to anything, from the CLI.
One command. No format-specific tools to remember. No cloud upload. No API keys.
morph report.docx report.pdf
morph data.csv data.xlsx --table-style TableStyleMedium2 --header-bg 2E7D32
morph clip.mp4 clip.gif --fps 10 --width 480
morph batch '*.mp4' mp3 --workers 4
Why morph exists
ffmpeg converts media. pandoc converts documents. ImageMagick converts images.
Each is excellent at its one job and painful to use next to the others — different
flag dialects, different mental models, different install stories. Nobody had wired
them into one coherent tool with one grammar and a genuinely good CLI/TUI.
morph doesn't reinvent codecs or file formats — it orchestrates the best tool for each job (ffmpeg, pandoc, Pillow, or plain Python) behind a single, predictable interface, and handles the annoying parts for you: detecting what's missing, telling you exactly what it'll install and asking first, and routing through intermediate formats automatically when there's no direct converter.
Features
- One verb.
morph <input> <output>— notmorph convert. morph is the verb. - Smart multi-hop routing. No direct
csv → yamlconverter? morph findscsv → json → yamlon 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! - Full control, contextually.
morph data.csv data.xlsx --helpshows exactly the flags relevant to that pair — table styles, freeze panes, passwords — and nothing from unrelated converters. - Batch conversion.
morph batch '*.mp4' mp3converts many files in parallel with a live Rich progress table, smart output strategies, and a clean summary. - Conversion history. Every conversion is logged to
~/.morph_history.jsonl.morph historyshows a searchable, filterable table of past jobs. - Cross-platform dependency management. Missing
ffmpegorpandoc? morph detects your package manager (brew/apt/dnf/pacman/winget/choco/…), shows you the exact install command, and never runs it without asking first. - A real interactive TUI, fully keyboard-driven — type a path (with autocomplete dropdown for filesystem suggestions), arrow through formats (options update live), and watch a real progress bar while it converts. No mouse needed.
- Live execution feedback. ffmpeg hops show a real progress bar driven by
ffmpeg's own
-progressstream. Everything else shows a spinner labeled with which tool is working (via pandoc,via pillow, ...). - Pluggable by design. Drop a file in
morph/converters/, callregister(), and it's live — no central registry to edit. - Local-first. Nothing leaves your machine. No accounts, no upload, no API keys.
- 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. - Global Configuration (
~/.morphrc). Generate a fully-commented YAML configuration file containing every single flag for every converter usingmorph config. CLI arguments seamlessly override these base defaults, giving you maximum control.
Installation
git clone https://github.com/hariharen9/morph.git
cd morph
pip install -e .
That's it for data, image, and archive conversions — pure Python, zero external
binaries. Documents (docx/pdf/html/…) need pandoc; audio/video need
ffmpeg. You don't need to install these up front — morph detects what a given
conversion needs and prompts you the first time you hit it.
Check what's available any time:
morph deps
Quickstart
# Data
morph data.csv data.xlsx # styled workbook: table, banding, autofit
morph data.csv data.json
morph data.csv data.yaml # routed through json automatically
# Documents (pandoc)
morph notes.md notes.docx
morph report.docx report.pdf
morph book.epub book.html
# Images (Pillow + cairosvg)
morph photo.png photo.webp --quality 80 --resize 1200x
morph icon.png icon.ico # multi-resolution ICO
morph diagram.svg diagram.png
# Fonts (fontTools)
morph font.ttf font.woff2
morph font.woff font.otf
# Remote Network Files
morph https://example.com/demo.mp4 demo.gif # streams to a temp file, converts, and cleans up
morph https://example.com/article.html doc.pdf --js # scrapes via crawl4ai if no format is detected
# Ebooks (Calibre)
morph book.epub book.mobi
morph book.epub book.azw3
# Audio / video (ffmpeg)
morph song.wav song.mp3 --bitrate 192k
morph clip.mov clip.mp4 --resolution 1280x720 --fps 30
morph clip.mp4 clip.mp3 # extract the audio track
morph clip.mp4 clip.gif --fps 10 --width 480
# Archives
morph project.zip project.tar.gz
# No args — interactive TUI
morph
Batch conversion
Convert many files at once with a live progress display:
# All MP4s → MP3, 4 parallel workers
morph batch '*.mp4' mp3 --workers 4
# Whole directory, recursive, into a separate output folder
morph batch ./raw/ mp3 --recursive --out-dir ./converted/
# Mirror the input directory structure
morph batch ./raw/ mp3 --recursive --out-dir ./converted/ --mirror
# Custom output name template
morph batch '*.mp4' mp3 --rename '{stem}_audio'
# → myvideo_audio.mp3
# Skip files whose output already exists
morph batch '*.mp4' mp3 --skip-existing
# Only re-convert if input is newer than output (like make)
morph batch '*.mp4' mp3 --newer-only
# Preview what would happen — no conversion
morph batch '*.mp4' mp3 --dry-run
# Multiple patterns at once
morph batch '*.flac' '*.wav' mp3
# Pass converter flags through
morph batch '*.mp4' mp3 --bitrate 192k
During conversion, morph shows a live table:
╭─ morph batch — 8 files → mp3 ─────────────────────────────────────╮
│ Status File Time Size │
│ ✓ done concert_01.flac 0:00:04 8.3MB → 4.1MB │
│ ✓ done concert_02.flac 0:00:03 7.9MB → 3.8MB │
│ ▶ converting… concert_03.flac 0:00:01 │
│ · waiting concert_04.wav │
│ │
│ Overall ████████░░░░░░░░ 2/8 [0:00:07] │
╰──────────────────────────────────────────────────────────────────────╯
Conversion history
Every conversion (single and batch) is automatically logged:
morph history # last 20 conversions
morph history -n 50 # last 50
morph history --failed # only failures
morph history --fmt mp4 # filter by format
morph history --clear # wipe history
morph history --json # raw JSONL output (for scripting)
┌──────────┬──────┬──────┬────────────────┬────────┬────────────┐
│ When │ From │ To │ File │ Mode │ Status │
├──────────┼──────┼──────┼────────────────┼────────┼────────────┤
│ 2m ago │ md │ docx │ README.md │ single │ ✓ 1.8s │
│ 1h ago │ mp4 │ mp3 │ intro.mp4 │ batch │ ✓ 4.2s │
│ yesterday│ csv │ xlsx │ data.csv │ single │ ✓ 0.1s │
└──────────┴──────┴──────┴────────────────┴────────┴────────────┘
History is stored at ~/.morph_history.jsonl — one JSON object per line,
easy to parse with jq or Python.
Every conversion is self-documenting
$ morph data.csv data.xlsx --help
╭─ morph — conversion route ─╮
│ csv → xlsx │
╰─────────────────────────────╯
Options for this conversion (csv → xlsx):
-d, --delimiter Field delimiter. Auto-detected if omitted.
--table-style Excel table style name. (default: TableStyleMedium9)
--header-bg Header background hex colour (no #). (default: 1F3864)
--freeze-cols Left-most columns to freeze.
--password Password-protect the output sheet.
... (more)
The exact same file pointed at report.pdf shows a completely different,
shorter flag set — because pandoc's PDF pipeline genuinely only has one knob
worth exposing.
See what's reachable
# Tree view grouped by family — great for discovery
$ morph formats
morph — 45 formats across 8 families
* document (11 formats, via pandoc, 82 direct routes)
├── md → docx epub html latex odt pdf rst rtf txt
├── docx → epub html latex md odt pdf rst rtf txt
└── ...
* audio (6 formats, via ffmpeg, 30 direct routes)
├── mp3 → aac flac m4a ogg wav
└── ...
# Per-format table — see everything reachable from one format
$ morph formats mp4
┌────────┬──────────────────┬──────┐
│ Target │ Route │ Hops │
├────────┼──────────────────┼──────┤
│ mp3 │ mp4 → mp3 │ 1 │
│ gif │ mp4 → gif │ 1 │
│ webm │ mp4 → webm │ 1 │
│ ... │ ... │ ... │
└────────┴──────────────────┴──────┘
Supported formats
| Domain | Formats | Backend |
|---|---|---|
| Data | csv, xlsx, json, yaml, parquet, feather, ods, xml, html, sqlite | native (pandas / openpyxl) |
| Documents | md, html, docx, odt, rtf, epub, latex, rst, txt, ipynb, pptx, adoc, org, opml, man, pdf* | pandoc |
| Images | png, jpg/jpeg, webp, bmp, gif, tiff, ico, heic, avif, icns, pdf, svg (input only) | Pillow / cairosvg |
| Fonts | ttf, otf, woff, woff2 | fontTools |
| Ebooks | epub, mobi, azw3 | Calibre (ebook-convert) |
| Audio | mp3, wav, flac, ogg, aac, m4a, opus, wma | ffmpeg |
| Video | mp4, mkv, mov, webm, avi, flv, wmv, mpeg (+ → audio, + → gif/webp, + → srt/vtt) | ffmpeg |
| OCR | png, jpg, webp, pdf, etc. → txt | tesseract |
| Archives | zip, tar, tar.gz, tar.bz2, tar.xz | stdlib |
*pdf is output-only — morph doesn't read PDFs back into structured content, since that's a fundamentally lossier operation than every other conversion here.
Architecture
morph/
├── cli.py # dispatch + all subcommands (run, batch, formats, history, deps)
├── registry.py # the graph: register(src, dst) edges + BFS routing
├── batch.py # parallel batch engine: ThreadPoolExecutor + Rich Live display
├── history.py # JSONL log at ~/.morph_history.jsonl
├── deps.py # cross-platform dep detection & guarded installer
├── progress.py # Rich progress bars for the CLI
├── ffmpeg_utils.py # ffmpeg -progress pipe parser for real % tracking
├── tui.py # Textual keyboard-driven TUI (same engine as CLI)
└── converters/
├── __init__.py # auto-discovers every file below — nothing to register by hand
├── csv_to_xlsx.py # self-contained: engine + OptionSpecs + register() in one file
├── xlsx_to_csv.py
├── csv_json.py
├── json_yaml.py
├── documents.py # one generic pandoc engine, many pairs
├── images.py # one generic Pillow engine, many pairs
├── svg.py # cairosvg for SVG → raster/PDF
├── audio.py # ffmpeg with real progress tracking
├── video.py # ffmpeg, includes → gif + audio extraction
├── archives.py
├── fonts.py # fontTools: ttf/otf/woff/woff2
└── ebooks.py # Calibre ebook-convert
Every converter is a function (input_path, output_path, **options) -> ConversionResult,
registered once with @register(src, dst, backend=..., options=[...]). The CLI
never hardcodes a flag — it asks the registry which OptionSpecs apply to the
route it just resolved and builds the parser from that, which is also what makes
per-pair --help possible.
Adding a new conversion
Drop a file in morph/converters/:
# morph/converters/my_converter.py
from pathlib import Path
from ..registry import ConversionResult, OptionSpec, register
OPTIONS = [
OptionSpec("quality", ("--quality",), "Output quality (1-100).", default=85, type=int),
]
@register("foo", "bar", backend="sometool", family="image", options=OPTIONS)
def foo_to_bar(input_path: Path, output_path: Path, *, quality=85, **_) -> ConversionResult:
# ... do the conversion ...
return ConversionResult(output=output_path)
That's the whole integration. No imports to add elsewhere, no CLI wiring —
converters/__init__.py picks it up automatically, morph x.foo y.bar works
immediately, and it slots into the routing graph so anything that could reach
.foo can now reach .bar too.
CLI reference
morph <input> <output> [OPTIONS] # convert a single file
morph <input> <output> --help # show flags for THIS conversion only
morph batch <patterns...> <format> # batch convert (see morph batch --help)
morph formats # tree of all formats by family
morph formats <fmt> # everything reachable from <fmt>
morph history # recent conversion log
morph deps # check / install external tools
morph # launch the interactive TUI
Roadmap
- Per-pair codec tuning for audio/video (currently relies on ffmpeg defaults)
- HEIC/AVIF image support (pillow-heif / pillow-avif)
-
~/.morphrcconfig file for persistent flag defaults - Automated test suite
Contributing
Issues and PRs welcome. If you're adding a converter, see "Adding a new conversion" above — that's the entire contract. Please include a quick sanity check (a real input → real output, not just "it imports") in your PR description.
License
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file morphconv-1.0.1.tar.gz.
File metadata
- Download URL: morphconv-1.0.1.tar.gz
- Upload date:
- Size: 67.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
10a35d40db3dfc4a5cd2bd4b17f3228ee24918ae69cc1c5d8cc76db9578f11fe
|
|
| MD5 |
341db5564bd46bc1495b694711c29f7d
|
|
| BLAKE2b-256 |
3859138671b54e45530b49f1cdbdd7a491f3b3d2fbf4c12c555cbba411f3e901
|
File details
Details for the file morphconv-1.0.1-py3-none-any.whl.
File metadata
- Download URL: morphconv-1.0.1-py3-none-any.whl
- Upload date:
- Size: 70.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
87552cfb4141f0b2f655ce457e696a323b0d901c5e648357e2c72c8900f9bf6d
|
|
| MD5 |
d2bfff4b32496f95b2919623293576e4
|
|
| BLAKE2b-256 |
3b981ca471718d796e5e3688ad7bceefe79f120558d705a38a5292c1c331ae49
|