{"id":"ee6fee26-9fa4-4a1b-aa21-b316b4107231","slug":"aimd","kind":"mcp","name":"aimd","summary":"Context preparation tool for LLM workflows - Transcribe audio/video and convert documents to markdown.","owner":"shuuul","repoUrl":"https://github.com/shuuul/aimd","license":"Apache-2.0","licenseTier":"permissive","trust":"community","stars":3,"match":null,"fewStars":true,"format":"mcp","createdAt":"2025-12-21T16:27:44.000Z","addedAt":"2026-10-21T16:30:56.837Z","logoUrl":"https://avatars.githubusercontent.com/u/6903405?u=a41453d4fcf0b281ab17b8ec84c2cb6edd85d19f&v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["design-media","files-documents","memory-knowledge"],"tags":[],"runtime":"binary","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-25T19:33:19.402Z","sample":false,"url":"https://conduits.dev/tools/aimd","language":"Rust","transports":["stdio"],"install":{},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"<div align=\"center\">\n  <img src=\"https://raw.githubusercontent.com/shuuul/aimd/main/assets/aimd-banner-sm.png\" alt=\"aimd\">\n\n  <a href=\"https://github.com/shuuul/aimd/releases\">\n    <img src=\"https://img.shields.io/github/v/release/shuuul/aimd\" alt=\"Latest release\">\n  </a>\n  <a href=\"https://github.com/shuuul/aimd/blob/main/LICENSE\">\n    <img src=\"https://img.shields.io/badge/license-Apache--2.0-blue\" alt=\"Apache License 2.0\">\n  </a>\n  <img src=\"https://img.shields.io/badge/Rust-native-orange?logo=rust\" alt=\"Native Rust\">\n  <img src=\"https://img.shields.io/badge/MLX-Metal%20%7C%20CUDA-black?logo=apple\" alt=\"MLX on Metal and CUDA\">\n  <img src=\"https://img.shields.io/badge/platform-macOS%20arm64%20%7C%20Linux%20NVIDIA-lightgrey\" alt=\"Platforms\">\n\n  <p><b>URLs, audio/video and documents in. LLM-ready Markdown out.<br>Local ASR and OCR on Apple Silicon, in native Rust on MLX.</b></p>\n</div>\n\n# aimd\n\n`aimd` takes one input, works out what kind it is, extracts or transcribes the\ncontent, and writes Markdown. Results also carry a chunked text context for\ndownstream AI workflows. It is a single native Rust binary: speech\nrecognition and OCR run locally through [MLX](https://github.com/ml-explore/mlx)\n(Metal on macOS, CUDA on Linux). There is no Python runtime, no PyTorch and\nno Transformers.\n\n## Highlights\n\n- **One command** for URLs, audio/video files, EPUB and other Pandoc\n  documents, text-layer PDFs, scanned PDFs and images, Markdown, and text.\n- **URL transcripts** through yt-dlp (YouTube, Bilibili, podcasts, and other\n  yt-dlp sites). Subtitles are used first; without them, aimd downloads the\n  audio and transcribes it locally.\n- **Local ASR** with [Qwen3-ASR](#models) (1.7B or 0.6B), with automatic\n  language detection and context biasing from page metadata.\n- **Local OCR** for images and scanned PDFs: [Unlimited-OCR-2](#models)\n  (default), Unlimited-OCR v1, or the fast [PP-OCRv6](#models) line detector\n  and recognizer.\n- **Document conversion** through Pandoc. EPUB keeps spine order and extracts\n  chapters and images. Text-layer PDFs are converted by\n  [pdf-inspector](https://github.com/firecrawl/pdf-inspector) without OCR.\n- **Source anchors** (opt-in `--anchors`): OCR blocks carry their page and\n  position, and transcripts are timed paragraphs (forced alignment with\n  Qwen3-ForcedAligner). Export JSON segments, SRT, or WebVTT.\n- **Remote backends**: optional OpenAI-compatible ASR/OCR servers.\n- **Two interfaces**: CLI (`aimd`) and MCP stdio server (`aimd-mcp`).\n\n## Why native Rust + MLX\n\nEach supported model is reimplemented in Rust on MLX's C++ API and tuned for\nthat model. Every speedup is checked against the Python MLX reference\nimplementations (mlx-audio, mlx-vlm, mlx4ocr). The output must stay\nidentical: transcripts token for token, OCR Markdown byte for byte, with a\nlabeled accuracy corpus as the gate.\n\n**Fast cold start.** One binary starts in milliseconds; there is no\ninterpreter to boot or framework to import. Safetensors weights are\nmemory-mapped, and quantized checkpoints go straight into MLX's\n`quantized_matmul` with no dequantize pass.\n\n**Kernels tuned per model.**\n\n- Fused MoE expert and router kernels for Unlimited-OCR.\n- A custom Metal attention kernel for single-token decode steps.\n- Compiled elementwise kernels for SwiGLU, GELU, rotary embeddings and the\n  conv bias + activation + residual epilogues in PP-OCRv6. They are\n  shape-agnostic, so a new page size never triggers a recompile.\n\n**GPU never waits on the host.**\n\n- Decode loops submit the next step before reading the current token, and\n  Metal command buffers are sized so the KV cache updates in place.\n- PDF pages render in the background while the previous page is in OCR.\n- CPU work (log-mel features, Pillow-exact image resizing, PP-OCRv6 crops)\n  runs on every core.\n\nApple M2 Pro, same machine and inputs as the Python reference\n([full numbers](docs/performance.md)):\n\n| | Python reference | aimd |\n|---|---|---|\n| Qwen3-ASR-1.7B 4-bit, model load | mlx-audio 10.0 s | 0.4 s (3.8 s with a cold disk cache) |\n| Unlimited-OCR 4-bit, model load | mlx-vlm 3.3 s | 0.6 s |\n| Unlimited-OCR 4-bit, full page (4x5 crops) | mlx-vlm 4.5-4.7 s | 3.8 s |\n| Unlimited-OCR 4-bit, decode (same page) | mlx-vlm 170-177 tok/s | 254 tok/s |\n| PP-OCRv6 medium, warm page | mlx4ocr (compiled) 0.22-0.49 s | 0.20-0.45 s |\n| Qwen3-ForcedAligner, 300 s clip | mlx-audio 4.7 / 5.6 s (en / zh) | 4.5 / 5.3 s |\n\n## Install\n\n```bash\nbrew install shuuul/tap/aimd\naimd --help\n```\n\nThe formula lives in [shuuul/homebrew-tap](https://github.com/shuuul/homebrew-tap).\nBottles are built for macOS on Apple Silicon (arm64). On Linux (x86_64 or\naarch64 with an NVIDIA GPU) the formula builds from source against the tap's\n`mlx-cuda`, MLX's CUDA backend. The NVIDIA driver, CUDA 13, cuDNN 9, and NCCL 2\ncome from the system and must be on the dynamic loader path. Arch's\n`/opt/cuda` is found automatically; elsewhere set `CUDA_HOME` if the toolkit is\nnot in `/usr/local/cuda`.\n\nHomebrew also installs the runtime tools aimd uses: MLX (`mlx` on macOS,\n`mlx-cuda` on Linux), `ffmpeg`, `mupdf`\n(`mutool`, for rendering scanned PDF pages), `pandoc`, and `yt-dlp`. Sites\nchange often, so keep yt-dlp current with `brew upgrade yt-dlp`. aimd suggests\nthis when yt-dlp's error output looks like an outdated extractor.\n\nModel weights are not bundled. The first local ASR or OCR run downloads the\nselected checkpoint from Hugging Face into the standard HF cache\n(`HF_HUB_CACHE`, else `HF_HOME/hub`, else `~/.cache/huggingface/hub`).\nCheckpoints already in that cache are reused.\n\n## Quick start\n\n```bash\n# Input type is detected automatically\naimd audio.mp3\naimd \"https://www.youtube.com/watch?v=...\"\naimd document.epub\naimd notes.txt\naimd scan.pdf\naimd page.png\n\n# Common options\naimd audio.mp3 --output transcript.md\naimd audio.wav --language en\naimd scan.pdf --task ocr --start 0 --end 2\naimd \"https://www.youtube.com/watch?v=...\" --cookies-from-browser chrome\naimd \"https://www.youtube.com/watch?v=...\" --raw-transcript\n```\n\nWithout `--output`, the file name comes from the result title:\n`<title>_transcript.md` in the current directory for transcripts, and\n`<title>_converted.md` or `<title>_ocr.md` next to the input for conversions\nand OCR.\n\n## CLI usage\n\n```text\naimd [OPTIONS] INPUT_SOURCE\n```\n\n| Option | Description |\n|--------|-------------|\n| `--task TEXT` | Explicit task: `transcript`, `convert`, or `ocr`. Defaults to auto-routing. |\n| `-o, --output PATH` | Output file. Generated from the title when omitted. |\n| `-f, --format TEXT` | `markdown` (default), `json` (the MCP response with segments), `srt` or `vtt` (transcripts only). See [Source anchors](#source-anchors). |\n| `--anchors` | Add OCR position comments and `[hh:mm:ss]` transcript paragraphs to the Markdown (runs forced alignment). Off by default. |\n| `-m, --model TEXT` | ASR or OCR model (see [Models](#models)). |\n| `--precision TEXT` | `4bit`, `6bit`, or `8bit` (dash variants such as `4-bit` are accepted). |\n| `-l, --language TEXT` | ASR language (`zh`, `en`, `ja`, `yue`, ...). Omit it to auto-detect. OCR ignores it. |\n| `--context TEXT` | ASR biasing text: proper nouns, names, domain vocabulary. |\n| `--no-context` | Do not build ASR context from URL page metadata. |\n| `--start INTEGER`, `--end INTEGER` | 0-based inclusive page range for OCR of PDFs. |\n| `-s, --save-original PATH` | Save the downloaded audio/video of a URL to a file or directory. |\n| `-c, --cookies PATH` | Netscape-format cookies file for URL extraction. |\n| `--cookies-from-browser TEXT` | Browser cookie source for URL extraction, e.g. `chrome` or `chrome:default`. |\n| `--raw-transcript` | Keep original SRT/VTT subtitle formatting instead of plain text. |\n| `--temp-dir PATH` | Directory for temporary downloads, transcoding, and extraction (env `AIMD_TEMP_DIR`). |\n| `--log-level TEXT` | `DEBUG`, `INFO` (default), `WARNING`, `ERROR`, `CRITICAL`. Logs go to stderr. |\n| `--asr-base-url`, `--asr-model`, `--asr-api-key` | Remote ASR backend (env `AIMD_ASR_*`). |\n| `--ocr-base-url`, `--ocr-model`, `--ocr-api-key` | Remote OCR backend (env `AIMD_OCR_*`). |\n\nThe CLI also reads `KEY=VALUE` pairs from a `.env` file in the current\ndirectory. Variables already set in the environment take precedence.\n\n### Audio and video files\n\n```bash\naimd interview.m4a\naimd lecture.mp4\naimd audio.wav --language en\naimd audio.wav --model qwen3-asr-0.6b --precision 8bit\naimd interview.mp3 --context \"Vocabulary: Qwen, MLX, LoRA.\"\n```\n\nffmpeg decodes audio and extracts it from video. Inputs longer than 10\nminutes are transcribed in segments. Without `--language`, Qwen3-ASR detects\nthe language itself (per segment), which also handles mixed-language audio.\nPass `--language` only to force one: `zh`, `en`, `ja`, `ko`, `de`, `es`,\n`fr`, `it`, `pt`, `ru`, `ar`, `hi`, `th`, `vi`, `id`, `tr`, `nl`, `pl`,\n`sv`, `da`, `fi`, `ms`, `yue`, `cs`, `fil`, `fa`, `el`, `hu`, `mk`, `ro`, or\nthe full language name. If a 4-bit run gets stuck in a repetition loop, aimd\nretries that segment once with the matching 8-bit checkpoint.\n\n### URLs\n\n```bash\naimd \"https://www.youtube.com/watch?v=...\"\naimd \"https://www.bilibili.com/video/BV...\"\naimd \"https://www.xiaoyuzhoufm.com/episode/...\"\n```\n\naimd uses subtitles when the page has them and simplifies them to plain text\nunless you pass `--raw-transcript`. Otherwise it downloads the audio and\ntranscribes it. In that case the page metadata (title, author, description,\ntags, chapters; at most 2000 characters) becomes ASR context so that names\nand terms are recognized correctly. `--no-context` turns this off. `--context`\nadds your own text, and works for local files too.\n\nFor restricted or signed-in content:\n\n```bash\naimd \"https://www.youtube.com/watch?v=...\" --cookies cookies.txt\naimd \"https://www.bilibili.com/video/BV...\" --cookies-from-browser \"chrome:default\"\n```\n\nWithout a cookie option, aimd tries the available browser cookie sources\nbefore falling back to unauthenticated access. An explicit cookie option that\nyt-dlp rejects fails immediately. If neither subtitles nor audio\ntranscription produce text, the request fails; aimd does not return\nmetadata-only Markdown as a transcript.\n\n### ASR context: names and terms from descriptions\n\nQwen3-ASR takes free-form context text as a hint for spelling names, brands,\nand jargon. It is not an instruction prompt: give it the words you expect to\nhear, not commands. Context does not change which language is detected or\nwhat gets transcribed; it only nudges ambiguous words toward the spellings\nyou supply.\n\n**YouTube videos and podcast pages.** For URLs, aimd builds the context from\nthe page metadata automatically whenever it has to transcribe audio:\n\n```text\nTitle: <title>\nAuthor: <uploader or channel>\nDescription: <first 800 characters of the description>\nTags: <up to 20 tags>\nChapters: <chapter titles>\n```\n\nThe whole block is capped at 2000 characters. The description is usually\nthe most useful part: video descriptions and podcast show notes list guest\nnames, products, papers, and links that the speaker then says out loud. Your\nown `--context` goes in front of the metadata, so put the terms that matter\nmost there:\n\n```bash\n# YouTube talk: metadata context is automatic; add the names it may miss\naimd \"https://www.youtube.com/watch?v=...\" \\\n  --context \"Speakers: Andrej Karpathy. Terms: nanoGPT, RLHF, tokenizer.\"\n\n# Podcast episode page\naimd \"https://www.xiaoyuzhoufm.com/episode/...\" \\\n  --context \"嘉宾：张三、李四。话题：MLX、Apple Silicon、端侧推理。\"\n\n# Turn metadata off when the description is unrelated (sponsor copy, link lists)\naimd \"https://www.youtube.com/watch?v=...\" --no-context --context \"Qwen, MLX\"\n```\n\nWhen the page already has subtitles, aimd uses them and does not run ASR,\nso context has no effect there.\n\n**Downloaded audio and local recordings** get no metadata, so pass the show\nnotes yourself. Keep it short and term-dense: a list of names and terms\nworks better than the full notes.\n\n```bash\naimd episode.mp3 --context \"Guests: Jane Doe (Acme). Terms: LoRA, KV cache, Metal.\"\naimd episode.mp3 --context \"$(head -c 1500 shownotes.txt)\"\n```\n\nTips:\n\n- Lead with proper nouns and domain terms; drop links, timestamps, and\n  sponsor copy.\n- Write names in the script the speaker uses (for example `张三` in a\n  Chinese podcast, not a romanization).\n- Long-audio segments all receive the same context.\n- Never paste a previous transcript as context; ASR errors in it get\n  reinforced.\n\n### Documents\n\n```bash\naimd document.epub\naimd report.docx\naimd slides.pptx\naimd paper.pdf\naimd notes.md\n```\n\nPandoc converts every format it reads (docx, pptx, xlsx, odt, epub, html,\nrtf, rst, LaTeX, and more). `.txt` and `.md` pass through unchanged. Legacy\nbinary `.doc`, `.ppt`, and `.xls` files are not supported because Pandoc has\nno reader for them. MOBI/AZW3 are not supported either.\n\nEPUB, DOCX, and ODT conversions write a directory next to the source, and\n`--output` is ignored for them:\n\n```text\ndocument_name/\n├── document_name.md\n├── chapters/\n└── images/\n```\n\n### OCR for scanned PDFs and images\n\n```bash\naimd page.png\naimd scan.pdf                      # OCR when the PDF has no usable text layer\naimd scan.pdf --task ocr           # force OCR\naimd scan.pdf --precision 8bit     # Unlimited-OCR-2 8-bit checkpoint\naimd scan.pdf --model unlimited-ocr  # Unlimited-OCR v1, 5-page requests\naimd photo.jpg --model pp-ocrv6    # PP-OCRv6: fast plain text lines\naimd scan.pdf --start 0 --end 2    # 0-based inclusive page range\n```\n\nImages always go to OCR. For PDFs, pdf-inspector checks the text layer. PDFs\nwith clean text are converted as documents. Scanned PDFs, and PDFs whose\nfonts cannot be decoded, go to OCR. aimd renders those pages with `mutool`\nand runs OCR on them. The default Unlimited-OCR-2 reads PDFs 10 pages per\nrequest (gundam tiles per page; progress and cancellation between\nrequests) and single images in gundam mode. Unlimited-OCR v1 reads PDFs 5\npages per request (one 1024 base view per page) and single images in gundam mode\n(`document parsing.` prompt), with a sliding-window no-repeat n-gram guard\n(n=35, window 128) and at most 8192 tokens per page. PP-OCRv6 runs one page\nat a time.\n\nWhich OCR model to use:\n\n- `unlimited-ocr-2` (default): full document parsing to Markdown, including\n  tables, formulas and paragraphs or tables that continue across pages. A\n  block-sparse (DASH) decoder parses up to 10 pages in one request.\n- `unlimited-ocr`: Unlimited-OCR v1, the same Markdown in 5-page requests.\n- `pp-ocrv6`: PP-OCRv6 medium text detection + recognition. Well under a\n  second per page, multilingual, but it outputs plain text lines in reading\n  order with no tables or formatting. Good for photos, screenshots, and\n  signs. JPEG decoding differs slightly from PIL, so scores and boxes can\n  move a little against Python PaddleOCR; PNG and rendered PDF pages match.\n\nOCR is multilingual and ignores `--language`.\n\n### Source anchors\n\nWith `--anchors` (MCP `anchors: true`) the Markdown records where each piece\ncame from, so an LLM (or you) can cite the page region or the moment in the\naudio. The default output has no anchors; `--format json/srt/vtt` compute the\npositions and timestamps either way.\n\nOCR output puts an HTML comment before each block. Markdown viewers hide\nit; `chunk_list` keeps it. `page` is 1-based and `bbox` is\n`x0,y0,x1,y1`, normalized to the page (0..1):\n\n```markdown\n## Page 2\n\n<!-- aimd:loc page=2 bbox=0.081,0.062,0.912,0.104 kind=title -->\n# Quarterly Report\n\n<!-- aimd:loc page=2 bbox=0.081,0.130,0.912,0.402 kind=text -->\nRevenue grew 12% year over year...\n```\n\nUnlimited-OCR and Unlimited-OCR-2 anchor each layout block. PP-OCRv6 and remote\nOCR anchor the whole page. PP-OCRv6 line boxes appear in the JSON `segments`.\n\nTranscripts are split into sentences and grouped into paragraphs (a new one\nafter a 2 s pause or about 300 characters), each with its start time:\n\n```markdown\n[00:00:00] Welcome back to the show. Today we're talking about MLX.\n\n[00:00:14] So the first question is how the unified memory works...\n```\n\nLocal ASR runs Qwen3-ForcedAligner-0.6B (8-bit, downloaded on first use)\non segments just under 5 minutes for word-level times. It covers Chinese,\nCantonese, English, French, German, Italian, Korean, Portuguese, Russian, and\nSpanish. Japanese, other languages and remote ASR get segment-level times, and\nURL subtitles keep their cue times.\n\n```bash\naimd interview.m4a -f srt          # interview_transcript.srt\naimd \"https://youtu.be/...\" -f vtt\naimd scan.pdf -f json              # Markdown + segments with page/bbox\naimd audio.mp3 --anchors           # [hh:mm:ss] paragraphs in the Markdown\n```\n\n`--format json` writes the `process_input` response (see\n[MCP server](#mcp-server)) with a `segments` list: `text`, plus `page`,\n`bbox`, `quad` (line corners), `kind` for OCR, or `start`/`end` seconds for\ntranscripts. SRT/VTT cues are one sentence each, split at commas when longer\nthan 7 s or 84 characters (42 for CJK).\n\n## Models\n\naimd downloads these checkpoints from Hugging Face on first use:\n\n| Model | Task | `--model` | Checkpoints | Upstream |\n|-------|------|-----------|-------------|----------|\n| Qwen3-ASR 1.7B | Transcription (default) | `qwen3-asr-1.7b` | [4bit](https://huggingface.co/mlx-community/Qwen3-ASR-1.7B-4bit), [6bit](https://huggingface.co/mlx-community/Qwen3-ASR-1.7B-6bit), [8bit](https://huggingface.co/mlx-community/Qwen3-ASR-1.7B-8bit) | [Qwen/Qwen3-ASR-1.7B](https://huggingface.co/Qwen/Qwen3-ASR-1.7B) |\n| Qwen3-ASR 0.6B | Transcription | `qwen3-asr-0.6b` | [4bit](https://huggingface.co/mlx-community/Qwen3-ASR-0.6B-4bit), [6bit](https://huggingface.co/mlx-community/Qwen3-ASR-0.6B-6bit), [8bit](https://huggingface.co/mlx-community/Qwen3-ASR-0.6B-8bit) | [Qwen/Qwen3-ASR-0.6B](https://huggingface.co/Qwen/Qwen3-ASR-0.6B) |\n| Qwen3-ForcedAligner 0.6B | Transcript timestamps (`--anchors`, SRT/VTT/JSON) | automatic | [8bit](https://huggingface.co/mlx-community/Qwen3-ForcedAligner-0.6B-8bit) | [Qwen/Qwen3-ForcedAligner-0.6B](https://huggingface.co/Qwen/Qwen3-ForcedAligner-0.6B) |\n| Unlimited-OCR | OCR | `unlimited-ocr` | [4bit](https://huggingface.co/mlx-community/Unlimited-OCR-4bit), [6bit](https://huggingface.co/mlx-community/Unlimited-OCR-6bit), [8bit](https://huggingface.co/mlx-community/Unlimited-OCR-8bit) | [baidu/Unlimited-OCR](https://huggingface.co/baidu/Unlimited-OCR) |\n| Unlimited-OCR-2 | OCR (default, multi-page) | `unlimited-ocr-2` | [4bit](https://huggingface.co/mlx-community/Unlimited-OCR-2-4bit), [6bit](https://huggingface.co/mlx-community/Unlimited-OCR-2-6bit), [8bit](https://huggingface.co/mlx-community/Unlimited-OCR-2-8bit) | [baidu/Unlimited-OCR-2](https://huggingface.co/baidu/Unlimited-OCR-2) |\n| PP-OCRv6 medium | OCR (plain text lines) | `pp-ocrv6` | [det](https://huggingface.co/PaddlePaddle/PP-OCRv6_medium_det_safetensors) + [rec](https://huggingface.co/PaddlePaddle/PP-OCRv6_medium_rec_safetensors) | PaddlePaddle |\n\n### Precision\n\nThe model (`--model`) and the precision (`--precision`) are separate options.\n\n| `--model` | Precisions | Default |\n|-----------|------------|---------|\n| `qwen3-asr-1.7b`, `qwen3-asr-0.6b` | `4bit`, `6bit`, `8bit` | `4bit` |\n| `unlimited-ocr`, `unlimited-ocr-2` | `4bit`, `6bit`, `8bit` | `4bit` |\n| `pp-ocrv6` | none (ignored with a warning) | - |\n\n```bash\naimd audio.wav --model qwen3-asr-1.7b --precision 4bit\naimd scan.pdf --model unlimited-ocr --precision 8bit\n```\n\nExplicit `mlx-community/Qwen3-ASR-*` and `mlx-community/Unlimited-OCR{,-2}-*`\ncheckpoint IDs are also accepted. If a checkpoint ID already contains a\nprecision, a different `--precision` is an error. Other models are rejected.\nbf16 (dense) Qwen3-ASR and Unlimited-OCR (1 and 2) checkpoints are not supported by the\nnative engine yet.\n\nLocal inference needs the MLX GPU device (Metal on macOS, CUDA on Linux). With\n`AIMD_MLX_DEVICE=cpu`, or on a machine without a usable GPU, local ASR/OCR\nfails early. Use a remote\nbackend in that case.\n\n## Remote ASR and OCR\n\nRemote inference is opt-in. It sends audio or page images to an\nOpenAI-compatible server (`/v1/audio/transcriptions` for ASR, chat\ncompletions for OCR), and no local model is loaded for that task. If you give\na bare server URL, aimd appends `/v1`. The server decides the weight\nprecision, so `--precision` is ignored with a warning.\n\n```bash\nAIMD_ASR_BASE_URL=http://192.168.100.114:8000 \\\nAIMD_ASR_MODEL=Qwen3-ASR-1.7B \\\n  aimd audio.wav\n\nAIMD_OCR_BASE_URL=http://192.168.100.114:10000 \\\nAIMD_OCR_MODEL=Unlimited-OCR \\\n  aimd scan.png\n```\n\n| Variable | CLI option | Default |\n|----------|------------|---------|\n| `AIMD_ASR_BASE_URL` | `--asr-base-url` | unset (local ASR) |\n| `AIMD_ASR_MODEL` | `--asr-model` | `Qwen3-ASR-1.7B` |\n| `AIMD_ASR_API_KEY` | `--asr-api-key` | `not-needed` |\n| `AIMD_OCR_BASE_URL` | `--ocr-base-url` | unset (local OCR) |\n| `AIMD_OCR_MODEL` | `--ocr-model` | `Unlimited-OCR` |\n| `AIMD_OCR_API_KEY` | `--ocr-api-key` | `not-needed` |\n\nExplicit options override the environment. MCP tool parameters use the same\nnames in snake_case (`asr_base_url`, ...).\n\nThe OCR instruction follows the served model name. A name starting with\n`Unlimited-OCR-2` (after any `org/` prefix; case, `_` and spaces ignored) gets\n`<PAGE><image>` + `Multi page parsing.` and its block output is converted to\nMarkdown; any other name gets the v1 `<image>` + `document parsing.`.\n\n## MCP server\n\n```bash\naimd-mcp\n```\n\nA stdio MCP server with two tools:\n\n- `healthz`\n- `process_input`: the same flow and fields as the CLI\n  (`input_source`, `task_type`, `model`, `precision`, `language`, `context`,\n  `metadata_context`, `start`, `end`, `output_file`, `save_original`,\n  `cookies`, `cookies_from_browser`, `raw_transcript`, and the remote backend\n  fields). `anchors` (default `false`) matches `--anchors`, and\n  `include_segments` (default `false`) adds the `segments` list described\n  in [Source anchors](#source-anchors), with or without anchors.\n\nExample client configuration:\n\n```json\n{\n  \"mcpServers\": {\n    \"aimd\": { \"command\": \"aimd-mcp\" }\n  }\n}\n```\n\nSet `AIMD_TEMP_DIR` in the server environment to control where temporary\nfiles go.\n\n## Configuration\n\n| Variable | Purpose |\n|----------|---------|\n| `AIMD_TEMP_DIR` | Temporary downloads, transcoding, and extraction (CLI: `--temp-dir`). |\n| `AIMD_ASR_*`, `AIMD_OCR_*` | Remote backends (see above). |\n| `AIMD_MLX_DEVICE` | `gpu` or `cpu`. Local inference needs the GPU; `cpu` is for tests. |\n| `AIMD_YT_DLP` | Path to the `yt-dlp` executable (default: `yt-dlp` on `PATH`). |\n| `HF_HUB_CACHE`, `HF_HOME` | Hugging Face cache location for model weights. |\n| `HF_TOKEN` | Hugging Face token for checkpoint downloads. |\n| `HF_ENDPOINT` | Alternative Hugging Face endpoint. |\n| `HF_HUB_OFFLINE` | Use only cached checkpoints and never download. |\n| `YT_DLP_CONFIG_HOME`, `YT_DLP_CACHE_DIR` | Read by yt-dlp itself. |\n\n## Building from source\n\nThis needs Rust (stable, edition 2024) and Homebrew `mlx`. To use the\nbinaries, you also need the runtime tools.\n\n```bash\nbrew install mlx ffmpeg mupdf pandoc yt-dlp\ngit clone https://github.com/shuuul/aimd.git\ncd aimd\ncargo build --release -p aimd-cli -p aimd-mcp\n./target/release/aimd --help\n```\n\n`crates/aimd-mlx/build.rs` finds MLX through `brew --prefix mlx`. Set\n`MLX_PREFIX` to use a different MLX install. See [AGENTS.md](AGENTS.md) for\nthe crate layout, test tiers, and release process, and\n[docs/architecture.md](docs/architecture.md) for the processing flow.\n\nRelease notes are on the GitHub Releases page.\n\n## License\n\nApache-2.0. See [LICENSE](https://github.com/shuuul/aimd/blob/main/LICENSE).\n"}