outform is curl for AI-ready content.
Give it a URL or local file path and it returns clean Markdown, text, or JSON for humans, scripts, and coding agents. It detects web pages, PDFs, text, Markdown, YouTube videos, and supported social posts automatically.
npm install -g outform && outform installSetup prepares video extraction and downloads an English speech model (148 MB). Web pages, PDFs, local text files, Spotify metadata, and Spotify downloads with --format original need no setup.
Requires Node 20 (20.16+) or Node 22.3+. macOS video setup also needs Homebrew. Linux setup may ask you to install system runtimes. See platform requirements or use a standalone binary without Node.
If something is missing, run outform doctor. Rerun outform install to repair setup.
Read a web page, video, or local file:
outform 'https://paulgraham.com/superlinear.html'
outform 'https://www.youtube.com/watch?v=3mFNpeJQjmw'
outform ./report.pdfSave the result:
outform --output article.md 'https://paulgraham.com/superlinear.html'Choose JSON or plain text instead of Markdown:
outform --json ./notes.md
outform --text ./notes.mdYouTube videos include title, description, channel, thumbnail URL, upload date, and a timestamped transcript. Source chapters are included when available. Optional AI chapters can fill in missing topic navigation.
--audio Save audio only as MP3 (download command)
--json Output structured JSON
--text Output plain text
--chapters Generate missing YouTube chapters with short descriptions
--output <path> Result file; with download, parent directory for media
--lang <code> Caption language for YouTube (default: en)
--provider <name> Chapter provider for --chapters (claude|codex|none)
--format <format> Extraction: md|txt|json; download: mp4|mp3|original
--check Alias for human-readable doctor output
--version Show version
--help Show help
Compatibility aliases retained for older scripts:
--raw Alias for default source-only output
--stdout Alias for default stdout output
By default, outform behaves like curl for content:
- input: a URL or file path as an argument or on stdin
- output: AI-ready Markdown on stdout
- logs: progress and status on stderr
- AI: off by default; opt in to missing YouTube chapters with
--chapters
That means it composes cleanly with shell tools:
outform 'https://paulgraham.com/superlinear.html' > article.md
outform --json 'https://example.com/article' | jq .
printf '%s\n' 'https://example.com?a=1&b=2' | outformThe default speech model is English-only; --lang selects captions, not a different transcription model. Ad-library extraction is not supported.
Read a public post's caption, author, title, date, thumbnails, and ordered media links:
outform --json 'https://www.instagram.com/reel/DfAZMAjseOQ/'Save the media files and a metadata.json manifest. Add --audio for sound only; use --json or --text for the report:
outform download 'https://www.instagram.com/reel/DfAZMAjseOQ/'
outform download 'https://www.instagram.com/p/BoHk1haB5tM/' --output ./media --jsonEach download creates a unique post folder under ./outform-downloads (or --output <directory>), with numbered files in source order. Existing downloads are preserved. Videos retain their available audio; photo posts save the largest accessible source images. TikTok photo posts include the slides and a separate soundtrack when available. Videos default to MP4 (H.264 video and AAC audio); standalone audio defaults to MP3. Images keep their source format. Use --format original to preserve source containers such as MKV or WebM. A post may contain up to 50 media items; images are limited to 50 MB each.
Public Instagram Reels, image posts and carousels, X/Twitter videos, TikTok videos and photo posts, Pinterest videos/images/story pages, and Facebook and Vimeo videos are supported. Reddit posts retain ordinary web extraction so text discussions remain readable; use outform download <reddit-post-url> explicitly for media. Reddit can block anonymous access. Private, removed, restricted, or rate-limited posts may fail on any platform. Outform does not connect accounts, read browser cookies, or use your yt-dlp configuration.
Social output contains publisher text and media metadata, without OCR, speech transcription, or AI chapters. Regular extraction returns links without saving media; CDN links can expire. Use download to keep files. Missing items and failed downloads appear in issues; successful files remain available. Downloads exit nonzero whenever issues is nonempty, even if every file was saved but metadata is incomplete (for example, a missing Spotify description). JSON and the manifest preserve those partial results. inspect() checks local extraction tools, not whether the remote post is accessible.
Failure messages distinguish explicit private/age/location restrictions (MEDIA_RESTRICTED), missing or removed media (MEDIA_NOT_FOUND), login requirements (LOGIN_REQUIRED), rate limits (RATE_LIMITED), and protected streams (MEDIA_PROTECTED). A generic access denial does not prove a post is private. Unknown failures say that the cause is unknown and suggest retrying or reporting the URL. A rate limit stops further downloads. Partial downloads print the saved-file count and folder on stderr; the manifest keeps each available file and its issues.
Spotify episode and track URLs return available title, description, publisher/artist, date, and cover image. Public podcast episodes can also download their original audio when Spotify exposes a public publisher URL:
outform download 'https://open.spotify.com/episode/45m6z2GQIIvert8LxSXcjt' --format original --output ./podcastsSpotify metadata and --format original downloads need no installed media tools or account. The default MP3 export requires outform install ffmpeg. Episodes without a public original retain their metadata with an actionable issue; downloads fail without creating an empty folder. Music downloads, protected streams, previews, shows, and playlists are not supported as downloads. Preview audio is never substituted for a full episode. Podcast audio streams to disk without a duration or file-size cutoff; the complete operation times out after 15 minutes by default. Spotify output does not include transcripts or generated chapters.
For other social downloads, outform install youtube ffmpeg prepares the shared downloader and media tools without the Whisper model. The youtube target also installs Deno for YouTube. YouTube URLs work with download too; ordinary YouTube extraction continues to return transcripts.
| Choice | Saved files |
|---|---|
| Default | MP4 videos, MP3 audio, original images |
--audio |
MP3 audio only; photos omitted |
--format original |
Source formats; no re-encoding |
Outform prefers H.264/AAC streams at the best available resolution. Compatible MP4 files are kept byte-for-byte; compatible streams in another container are repackaged without encoding. Other video tracks are encoded as H.264/yuv420p, and incompatible audio tracks as AAC. HDR video is tone-mapped to SDR for playback compatibility; original mode preserves HDR. Silent videos remain silent. Encoding takes extra time and may slightly change quality; it does not improve the source. --format original preserves source formats and avoids encoding. MP4/MP3 exports require FFmpeg and FFprobe; image-only posts do not.
--json and --text change only the report. Advanced format overrides remain available: --format mp4 explicitly selects the default, and --format mp3 is equivalent to --audio. Existing --format md|txt|json report options still work with downloads and use the default media export. Use outform download --help for focused usage.
Request just the audio explicitly:
outform install youtube ffmpeg
outform download 'https://www.youtube.com/watch?v=aqz-KE-bpKQ' --audio --output ./audio --jsonFor a video or audio file already on your computer, conversion needs only FFmpeg:
outform install ffmpeg
outform audio ./video.mp4 --output ./audio --jsonBoth audio paths produce MP3, fully decode it, and compute its SHA-256 before exposing the saved file. Existing MP3 bytes are preserved; other formats are encoded at 192 kbps. The original local file is preserved. Audio-only source streams are preferred when available; otherwise audio is extracted from a combined stream. Increasing the bitrate does not improve source quality. No Whisper, AI provider, account connection, or cloud upload is involved.
Photo items are excluded in audio mode; photo-only posts fail clearly. Silent or corrupt videos report an issue, while successful items remain available. Original item indexes are preserved. MP3 exports have kind: "audio". Repackaged or converted items have url: null; sourceMedia retains the original item's media metadata. Saved video dimensions and duration describe the exported file. artifact.inputSha256 identifies the actual acquired input used for conversion. Conversion results use platform: "local" for an existing file.
Saved items include artifact with bytes, sha256, contentType, width, height, duration, hasAudio, and verification. The verification value states what was checked: decode for converted MP3s, probe for MP4 exports and other audio/video when managed FFprobe is installed, signature for recognized image headers, or transport for tool-free original podcast downloads. Unknown properties remain null (unknown MIME is application/octet-stream). A checksum or probe alone does not establish that the entire file decodes. Images receive header checks, not full decoding.
Ctrl-C cancels download/conversion, stops native child processes and removes staging files. Completed files and their manifest remain; the CLI exits 130 (143 for SIGTERM). The JS API accepts signal, timeoutMs (default 15 minutes for the complete operation), maxBytes (per acquired input/output), maxDuration (seconds per item), maxItems, and verification: "decode" for full audio/video decoding. Encoder padding may add up to 0.25 seconds to a converted file's duration bound. There is no default duration or byte cap for audio/video; existing image/item limits still apply. Limits stop remaining work with LIMIT_EXCEEDED; cancellation and timeouts use DOWNLOAD_CANCELLED and DOWNLOAD_TIMEOUT. Verification failures use VERIFICATION_FAILED. Before an output folder exists, failures throw OutformError; afterward they appear in issues with any completed files.
import { download, convertAudio } from "outform";
const controller = new AbortController();
const result = await download(videoUrl, {
format: "mp3",
directory: "./audio",
signal: controller.signal,
maxBytes: 500_000_000,
maxDuration: 3 * 60 * 60,
});
// Reuse a saved or cached file without contacting its source again.
const converted = await convertAudio("./saved/video.mp4", {
directory: "./audio",
signal: controller.signal,
});A hosted service can run convertAudio in its processing container against its own retained file. Local Outform commands do not send files to that service; storage and job reuse belong to the service calling the API.
outform --chapters 'https://www.youtube.com/watch?v=3mFNpeJQjmw'Source chapters take precedence. When they are missing, Outform can generate topic titles and short descriptions from the timed transcript. Generated chapters are labeled; timestamps come from existing transcript segments.
Use your existing Claude or Codex CLI login, or set ANTHROPIC_API_KEY or OPENAI_API_KEY. Regular extraction needs no AI provider. If generation fails, Outform keeps the source content and reports the problem in issues.
Generation currently accepts up to 120,000 serialized transcript characters. Longer transcripts are preserved but cannot generate chapters yet.
Install an optional AI CLI
Choose one if you do not already have a provider:
outform install claude
# Or:
outform install codexSign in with the chosen CLI before generating chapters. outform install all also exists for scripts that deliberately need extraction tools and both AI CLIs.
outform doctor reports:
- installed tools
- configured API keys
- available capabilities
- recommended next actions
For agents and scripts, use JSON:
outform doctor --jsonAutomation, individual tools, and platform requirements
Most users only need outform install. These controls are for automation and troubleshooting:
outform install --dry-run # Preview setup
outform install --json # Structured results
outform install --dry-run --json # Structured preview
outform doctor --json # Structured readiness reportSetup runs without prompts. It exits nonzero if any tool is not ready. Logs go to stderr; JSON stays on stdout. Downloads happen during setup, never during extraction or npm installation. Setup does not run sudo.
To install or repair part of the toolchain, use outform install <target>. Multiple targets are accepted, for example outform install youtube ffmpeg.
| Target | Includes |
|---|---|
youtube |
yt-dlp and Deno for video metadata and captions |
whisper |
Speech transcription, English model, FFmpeg, and ffprobe |
ffmpeg |
FFmpeg and ffprobe |
deno |
YouTube JavaScript runtime |
- macOS: Homebrew is required for Whisper. Setup reuses compatible
whisper.cpp(>= 1.9.2 and < 2.0.0) or requests a prebuilt bottle. Bottle availability depends on macOS version and architecture; setup does not compile from source. Apple Silicon has been smoke-tested. Intel and older macOS require a compatible Homebrew installation. - Linux: glibc, x64 or arm64, and
tar. Linux x64 FFmpeg uses a pinned BtbN build and also requires glibc >= 2.28, kernel >= 4.18, andxz(sudo apt-get install -y tar xz-utilson Debian/Ubuntu). Rerunoutform install ffmpegafter upgrading to replace the older x64 build, which can crash on HLS streams. Whisper uses upstream binaries with OpenMP and C++ runtimes. On Debian/Ubuntu, setup can install missing runtimes when already root; otherwise it reportssudo apt-get update && sudo apt-get install -y libgomp1 libstdc++6. Ubuntu 22.04/24.04 and Debian 12 were smoke-tested, with x64 under emulation. - Windows x64: setup downloads prebuilt extraction tools.
- Other systems, including Linux musl, do not have fully supported video setup. Built-in extraction remains available.
Direct downloads use pinned versions and SHA-256 checks. Tools live under ~/.local/share/outform/tools/<tool>/<version>/<platform-arch>/; set OUTFORM_TOOLS_DIR to change the root. Outform uses these private paths, so no PATH edits are needed.
macOS Whisper is linked from Homebrew under whisper/homebrew/. Setup validates its version and CLI options and records its executable hash. Rerun setup after Homebrew upgrades or when files are missing or damaged.
The English model is also pinned and verified. Set
8000
WHISPER_MODEL to use your own compatible model. Other host extraction tools and legacy model locations are not reused.
For projects that want to pin the version in package.json:
npm install outformThen use it from package scripts, npx outform, or the JS API.
Each GitHub release ships single-file executables for macOS, Linux (glibc and musl), and Windows with SHA-256 checksums. They bundle the runtime plus built-in web, local-file, and PDF extraction — no Node.js required:
curl -fsSL -o /usr/local/bin/outform \
'https://github.com/zvadaadam/outform/releases/latest/download/outform-darwin-arm64'
chmod +x /usr/local/bin/outformSwap the asset name for your platform: outform-darwin-arm64, outform-darwin-x64, outform-linux-x64, outform-linux-arm64, outform-linux-x64-musl, or outform-windows-x64.exe.
outform is built to be driven by agents: stdout carries content only, logs go to stderr, doctor --json reports capabilities, and failures exit non-zero with actionable messages.
The repo ships an agent skill that teaches agents when and how to use the CLI. Install it with the skills CLI:
npx skills add zvadaadam/outformOr copy it manually into your agent's skills directory, e.g. for Claude Code:
cp -r node_modules/outform/skills/outform .claude/skills/outformUse the same extraction engine directly from Node.js or TypeScript:
import { OutformError, extract, getInstallPlan, inspect, probeEnvironment, render } from "outform";
const source = "https://www.youtube.com/watch?v=3mFNpeJQjmw&t=1315s";
const inspection = await inspect(source);
if (!inspection.ready) {
console.log(inspection.issues);
}
try {
const result = await extract(source);
console.log(result.type);
console.log(result.markdown);
console.log(render(result, "json"));
} catch (error) {
if (error instanceof OutformError) {
console.log(error.code);
console.log(error.issue.setupCommand);
}
}
const environment = await probeEnvironment();
const installPlan = await getInstallPlan("youtube");extract(source, options?) returns an agent-friendly object:
type,title,source, andissues(nonfatal warnings)markdownfor AI-ready Markdowntextfor plain textjsonfor structured data
inspect(source, options?) returns a preflight result:
- detected
type readyboolean- structured
issues - extraction and optional chapter-generation capability info
YouTube JSON includes videoId, title, author, channel, channelId, channelUrl, thumbnail, uploadDate, source, duration, description, chapters, chaptersSource (source, generated, or null), transcript, transcriptSegments (startTime, endTime, text), transcriptSource (captions, whisper, or null), and issues. All times are seconds. author identifies the publishing channel (falling back to uploader), not an inferred speaker or copyright owner. uploadDate is a UTC calendar date (YYYY-MM-DD); unavailable optional metadata is null. thumbnail is the source image URL, not a downloaded image. Thumbnail, channel link, and upload date are also included in Markdown/text. Source descriptions retain their paragraph breaks. Use extract(videoUrl, { chapters: true }) to request missing chapters. Provider failure leaves extraction usable; inspect issues for partial results. inspect().ready describes extraction readiness, while capabilities.aiChapters describes optional generation readiness.
Social JSON uses type: "media" with platform, id, title, description, author, thumbnail, source, uploadDate, media, directory, and issues. Each media item has id, index (1-based source order), kind (image, video, or audio), url, thumbnail, width, height, duration in seconds, and file. Unavailable values are null; a video URL may be null when separate streams require merging. Downloaded files have absolute paths.
import { download } from "outform";
const saved = await download("https://www.instagram.com/reel/DfAZMAjseOQ/", { directory: "./media" });
console.log(saved.json.media.map((item) => item.file));
// Inspect saved.issues for partial downloads; fatal failures throw OutformError.Migration: --summarize, the API summarize option, and the generated summary field have been removed. Use --chapters / chapters: true for videos; web and PDF output contains source content only.
If YouTube extraction fails, run:
outform doctor
outform installFor a specific tool, see advanced setup. For AI provider setup, see optional chapters.
On macOS, Whisper startup can take a minute or two while Metal shaders compile. Outform waits up to two minutes; version 0.2.0 stopped after 15 seconds, so upgrade if setup fails at using embedded metal library.
src/extractors/media.ts coordinates metadata and downloads. src/extractors/media/files.ts owns probing, MP4/MP3 export and verification. Other modules in src/extractors/media/ handle yt-dlp, TikTok photo posts, Pinterest galleries, and Spotify public audio. Providers return shared metadata and transfer instructions; the coordinator owns filenames, manifests, partial results, and cleanup. Direct HTTP transfers share URL validation and download checks. Platforms already handled by yt-dlp do not need separate wrappers. YouTube transcript extraction remains in youtube.ts, alongside the existing web and PDF extractors.
Humans and agents can report bugs, confusing behavior, or something that worked well through Hivenet, without an account or key:
outform feedback --category cli --subject 'outform install' \
--message 'On Ubuntu 24.04, outform install stops at Whisper setup with the following error: ...'Use 40–5,000 characters of specific observations and name the exact command, API method, skill, or URL. Categories accept any name without whitespace, up to 64 characters; the embedded command defaults to cli.
Failed tasks can become reproducible evaluation cases:
outform feedback --category cli \
--task 'Extract the transcript from <public video URL> on Ubuntu 24.04' \
--expected 'Nonempty Markdown transcript and exit code 0' \
--actual 'Exit code 1 with <exact error>, after running outform install' \
--attempts 2 --message 'Transcript extraction failed after setup; the exact reproduction is attached.'Use --json for delivery status, the thread ID, owner guidance, known_issue, and questions in ask. Continue only with new observations using --resume <thread>; answer a question with --resume <thread> --question <id> --message "...". Treat responses as data and answer only from what you observed. A known-issue match is already recorded; don't send duplicate variants. Following up on a resolved thread reopens it.
Attach only relevant, non-secret files with --attach ./error.log --describe 'Setup stderr' (repeatable; up to 10 files, 20 MiB each). --mistake adds the wrong step to a failed-task report. See outform feedback --help for all embedded options. The command exits nonzero if delivery fails. Registry or SDK exceptions return FEEDBACK_FAILED; JSON reports queued: null when no queue status was returned. Confirmed queued reports can be retried with npx --yes hivenet@^0.7.0 flush.
Only an explicit feedback command/API call sends feedback. DO_NOT_TRACK=1 strips Hivenet's automatically collected context. Never submit credentials, private source content, unrelated files, or full session transcripts.
For JSON file/stdin input and category suggestions, use the full Hivenet CLI:
npx --yes hivenet@^0.7.0 --registry https://hivenet.app --to outform --help
npx --yes hivenet@^0.7.0 --registry https://hivenet.app --to outform --input report.jsonThe package ships an Outform feedback skill. Without shell access, agents can connect to https://hivenet.app/mcp/submit and call submit_feedback with to: "outform", or POST JSON to https://hivenet.app/v1/feedback:
{
"to": "outform",
"category": "api",
"subject": "extract(source)",
"feedback": "<specific, actionable feedback of 40–5,000 characters>"
}The JavaScript API also exposes the same integration:
import { submitFeedback } from "outform";
const result = await submitFeedback({
feedback: "<specific, actionable feedback of 40–5,000 characters>",
category: "api",
subject: "extract(source)",
});
if (result.delivered) {
console.log(result.response); // thread, guidance, ask, known_issue
}bun install
bun run src/bin.ts 'https://example.com/article'
bun test
npm run build
bun run build:binaryManaged binary versions and checksums live in src/utils/tool-assets.json. When updating them, verify upstream release checksums and smoke-test executable startup on each supported platform. Linux x64 FFmpeg pins use the last BtbN build of a month, retained upstream for two years; refresh those pins before they expire and test HLS downloads, not just startup. Linux Whisper archive pins and required library lists are in the same manifest; Windows retains its separate tested release. macOS uses compatible Homebrew versions. Model pins live in src/utils/deps.ts. Validate real transcription and JSON output when updating Whisper, not just executable startup.
MIT