man markdowner 2.1

markdowner(1)

MARKDOWNER(1)                    User Commands                   MARKDOWNER(1)

NAME

       markdowner  -  convert  documents, mail, images, and audio to Markdown;
       vault, search, ask

SYNOPSIS

       markdowner [-v|-D] [-o FILE]  [-L  LANG]  [--archive  KIND]  [--exclude
       KIND] FILE
       markdowner  vault  [-v|-D]  [-j  N] [-o DIR] [-L LANG] [--archive KIND]
       [--exclude KIND] [-E] INDIR [INDIR...]
       markdowner zettel [-v|-D] [--rebuild] VAULT [VAULT...]
       markdowner logseq [-v|-D] VAULT [VAULT...]
       markdowner index [-v|-D] [--no-rag] [--rebuild] [-j N] VAULT [VAULT...]
       markdowner search [-v|-D] [OPTION]... VAULT [VAULT...] [QUERY...]
       markdowner ask [-v|-D] [--dry-run]  [--deep[=0|1|2|3|4|5]]  [--no-deep]
       [OPTION]... VAULT [VAULT...] [QUESTION...]
       markdowner models [-v|-D] [status|update]
       markdowner metrics [-v|-D] VAULT [VAULT...]
       markdowner status [-v|-D] VAULT [VAULT...]
       markdowner webui [-v|-D] [--no-browser] [-p PORT]

DESCRIPTION

       markdowner  is a single CLI (C + C++17) that converts office documents,
       mail, calendars, contacts, chat exports, web/text files, images  (OCR),
       and  audio (ASR) to Markdown for a Zettelkasten vault, FTS5 search, and
       hybrid  RAG   ask   (local   BGE-M3   embeddings   by   default   after
       ./tools/fetch-embed.sh; no silent hash fallback). Optional Mistral chat
       via  libcurl  + nlohmann/json. Without MISTRAL_API_KEY, ask stays local
       (retrieve-only). OCR and ASR  are  open-source  and  local  (Tesseract,
       Python  sidecar,  faster-whisper); no OCR cloud.  A whole collection of
       one  material  can  skip  per-page  classification  with  --archive   /
       OCR_ARCHIVE  (see  ARCHIVE).  Skip engines with --exclude / OCR_EXCLUDE
       (see EXCLUDE).  Converts emit quality  frontmatter  (quality:  ok|weak,
       lang,  doc_kind, validation) and vault runs append _meta/metrics.jsonl.
       Local plugins under plugins/*.json extend convert for extra extensions.
       Graph visualization uses a local Logseq file graph (AGPL), prepared  by
       logseq  (also  run  automatically  after zettel).  Only open-source li‐
       braries are used.  Each input project directory becomes its  own  vault
       under ./vaults/ with a private sources/, notes/, and _meta/ tree (never
       share  one mega- sources/ across projects).  Commands that take a vault
       path accept one or more paths so a shell glob works (markdowner  zettel
       ./vaults/*,  markdowner  search ./vaults/* foo).  Directories that look
       like a vault (sources/ or _meta/) are  processed  sequentially  with  a
       header per vault; other glob entries (plain files, ., .., non-vault di‐
       rectories)  are  skipped  with  a  warning. Combined exit status is the
       worst of 0, 1, or 2.  For search and ask , those vault directories  are
       the  vaults;  leftover words that are not existing files or directories
       are the query/question, run against each vault (no extra quoting needed
       after ./vaults/*).  vault accepts one or more source INDIR  paths  (one
       vault per directory).

GLOBAL OPTIONS

       -h, --help
              Print help and exit.

       -V, --version
              Print version and exit.

       -v, --verbose
              Extra diagnostics on stderr (paths, OCR ensemble/HTR scores, in‐
              dex/ask detail). Same as MARKDOWNER_LOG=verbose.  Accepted glob‐
              ally  (before  the  subcommand)  and on every subcommand.  Level
              ladder: INFO = progress (convert/ok/fail/done); VERBOSE =  deci‐
              sions/scores;  DEBUG  =  skip size/mtime, text previews, sidecar
              traces.

       -D, --debug
              Full diagnostics including OCR  text  previews;  sets  OCR_SIDE‐
              CAR_DEBUG   for   Python   traceback/detail.   Same   as   MARK‐
              DOWNER_LOG=debug.  Always appends debug lines to a log file (see
              MARKDOWNER_LOG_FILE).  Vault progress events at INFO and OCR de‐
              cisions at VERBOSE are included; unchanged skip lines are  DEBUG
              (so  debug.log  is  not  a skip dump).  Accepted globally and on
              every subcommand.

       MARKDOWNER_LOG
              quiet|info|verbose|debug (or 0|1|2|3).  Default info keeps  nor‐
              mal progress lines.

       MARKDOWNER_LOG_FILE
              Append log lines to this path. With --debug / MARKDOWNER_LOG=de‐
              bug   a   file   is   always   used:  this  path  if  set;  else
              VAULT/_meta/debug.log for  any  vault-bound  subcommand  (vault,
              zettel,  logseq,  index,  search,  ask,  metrics,  status); else
              $XDG_STATE_HOME/markdowner/logs/markdowner-YYYYMMDD.log   (fall‐
              back  ~/.local/state/markdowner/logs/).   Set to 0 / off to dis‐
              able file logging.  Line format (UTC):  YYYY-MM-DDThh:mm:ss.sss‐
              ZLEVELmessage.  Stderr prints markdowner: log file → when a file
              is opened.

       MARKDOWNER_LOG_DIR
              Directory  for auto daily logs when MARKDOWNER_LOG_FILE is unset
              (default under XDG_STATE_HOME).

CONVERT MODE

       Default mode: convert one FILE to Markdown.

       -o, --output=PATH
              Output path (or directory  for  multi-message  formats  such  as
              mbox/pst).

       -L, --lang=LANG
              OCR/ASR  language  for  image  and  audio  inputs  (e.g.  nld or
              nld+eng).  Default is nld  when  Dutch  tessdata  is  installed;
              overridable  with OCR_LANG / ASR_LANG.  For ASR, Tesseract-style
              codes are mapped (nldnl).

       -A, --archive=KIND
              Force OCR engines for a whole collection: auto  (default:  clas‐
              sify  each page), print / drukwerk (RapidOCR/Tesseract, no HTR),
              newspaper / krant (same, dense print), handwriting / handschrift
              (VLM:  Qwen2.5-VL,  HTR  fallback),  historic  /  arletta   (AR‐
              letta/kraken  only),  modern  / trocr (TrOCR only), mixed (print
              then always HTR), form / formulier (printed form  +  handwritten
              fields).   Same  as OCR_ARCHIVE.  See ARCHIVE for the full table
              of kinds and which engines they force.

       -X, --exclude=KIND[,KIND...]
              Skip engines. Same kinds as --archive  (comma-separated).  Exam‐
              ple:  --exclude  historic skips ARletta/kraken so only TrOCR re‐
              mains for handwriting.  See EXCLUDE.

       -v, --verbose / -D, --debug
              Same as GLOBAL OPTIONS; debug writes a UTC log file (daily under
              XDG state unless MARKDOWNER_LOG_FILE is set).

       Supported extensions: .docx .doc .odt .pdf .xlsx .ods .pptx .odp .epub,
       .eml .mbox .msg .pst .ost .ics .vcf .vcard, .html  .htm  .md  .markdown
       .txt  .text .csv .tsv .rtf .json, .tif .tiff .jpg .jpeg .png, .wav .mp3
       .m4a .aac .ogg .flac .webm .wma .opus.

       Office Open XML / ODF / EPUB / HTML / RTF  prefer  pandoc  (subprocess)
       when  installed;  otherwise  ZIP+XML  or lightweight extractors (with a
       warning). If pandoc is installed but fails, the builtin parser  is  re‐
       fused unless MARKDOWNER_LEGACY=1 .  Disable pandoc with MARKDOWNER_PAN‐
       DOC=0.   Spreadsheets  and presentations prefer LibreOffice ( soffice →
       HTML → pandoc, not the builtin HTML parser) when installed. The ZIP ex‐
       tractors cap at 500 rows / 50 columns / 200 slides and refuse truncated
       output unless  MARKDOWNER_LEGACY=1.   Disable  LibreOffice  with  MARK‐
       DOWNER_SOFFICE=0.   Legacy  .doc  uses antiword / catdoc / LibreOffice.
       PDF uses pdftotext (Poppler); if the result is sparse and  pdftoppm  or
       pdftocairo  is  on  PATH,  pages are rendered and OCR'd (Paddle sidecar
       when available, else Tesseract).  Audio files are  transcribed  with  a
       local  faster-whisper  sidecar  (requires  ffmpeg and tools/fetch-whis‐
       per.sh).  See AUDIO (ASR) below.  Chat: WhatsApp-style  .txt  and  JSON
       messages arrays (also .json).

       Optional convert plugins: plugins/*.json with ext, cmd, and type (over‐
       ride directory with MARKDOWNER_PLUGINS).  See plugins/example.json.

QUALITY

       Markdowner does not silently degrade Office ingest or RAG embeddings.

       Office (pandoc)
              docx  /  odt  / epub / html / rtf use pandoc when it is on PATH.
              If pandoc is present but conversion fails, the  ZIP/XML  builtin
              parser  is  not  used  unless MARKDOWNER_LEGACY=1.  If pandoc is
              missing, the builtin parser runs with a warning.

       Sheets / slides (LibreOffice + pandoc)
              xlsx / ods / pptx / odp use soffice / libreoffice to HTML,  then
              pandoc to Markdown. Builtin html_to_md is only a warned fallback
              when  pandoc  is  missing  or fails on that HTML.  If soffice is
              present but conversion fails, ZIP extractors are refused  unless
              MARKDOWNER_LEGACY=1.

       Truncation
              ZIP  sheet/slide  extractors stop at 500 rows / 50 columns / 200
              slides and refuse to write a  truncated  document  unless  MARK‐
              DOWNER_LEGACY=1.

       RAG embeddings
              ASK_RAG_EMBED=auto  (the default) requires a fetched local model
              (BAAI/bge-m3 via ./tools/fetch-embed.sh).  Without  that  model,
              index/ask  fail  (exit  2);  they never silently switch to hash‐
              ing-trick vectors.  Skip embeddings with index --no-rag or MARK‐
              DOWNER_RAG_INDEX=0.  ASK_RAG_EMBED=hash is debug-only (weak  re‐
              call).

       See   ENVIRONMENT   for  MARKDOWNER_PANDOC,  MARKDOWNER_SOFFICE,  MARK‐
       DOWNER_LEGACY, and ASK_RAG_EMBED.

AUDIO (ASR)

       markdowner transcribes audio to Markdown  via  a  local  faster-whisper
       sidecar (offline after model download). Supported extensions: .wav .mp3
       .m4a .aac .ogg .flac .webm .wma .opus.

       Setup (once):

       • install ffmpeg on PATHpip install -r tools/requirements-asr.txt into third_party/ocr-venv./tools/fetch-whisper.sh  (default  model large-v3third_party/mod‐
         els/whisper-large-v3/)

       Usage:

       markdowner -L eng -o interview.md interview.mp3
       ASR_TIMESTAMPS=1 markdowner -o ts.md clip.wav
       markdowner vault ./recordings

       -L / OCR_LANG / ASR_LANG select the language (nld maps to  Whisper  nl;
       use  auto  to  detect).   Set  ASR_TIMESTAMPS=1 for lines prefixed with
       [mm:ss].  Output frontmatter uses type: asr, asr_engine,  and  optional
       asr_confidence.

       Related  environment  variables:  ASR_MODEL,  ASR_MODEL_DIR,  ASR_SIDE‐
       CAR_SERVE, ASR_SIDECAR_TIMEOUT,  MARKDOWNER_ASR_SIDECAR  (see  ENVIRON‐
       MENT).

CALENDAR (ICS)

       markdowner parses iCalendar (RFC 5545) files into one Markdown note per
       calendar  file.   YAML frontmatter uses type: ics plus events, optional
       calname/prodid/method, and words.  Each VEVENT (and  VTODO)  becomes  a
       heading  with  start/end,  location, categories, recurrence, organizer,
       attendees, and description. Folded lines and text escapes (\n, \,)  are
       handled; timezone definition blocks are skipped.

ARCHIVE

       --archive  /  -A / OCR_ARCHIVE declares the material of a whole collec‐
       tion so Markdowner can  skip  per-page  classification  and  force  the
       matching  engines from RapidOCR, Tesseract, Qwen2.5-VL, ARletta/kraken,
       and TrOCR. The web UI Archief dropdown is  the  same  flag  (stored  as
       OCR_ARCHIVE  outside  the engine-slider profile). The slider still con‐
       trols how hard OCR runs; the archive flag wins for print vs handwriting
       routing.

       Canonical values (Dutch and library aliases  in  parentheses).  Unknown
       --archive is a usage error. Unknown OCR_ARCHIVE falls back to auto with
       a   warning.   Non-auto  kinds  are  written  to  YAML  frontmatter  as
       ocr_archive.

       auto   (any, unknown, dynamic, default) Classify  each  page  (the  de‐
              fault). Print OCR first; handwriting or print-garbage uses a lo‐
              cal  VLM  (Qwen2.5-VL)  then TrOCR/ARletta as fallback. Does not
              rewrite OCR_HTR / OCR_SIDECAR_PIPELINE from the slider.

       print  (drukwerk, typed,  typeset,  printed)  RapidOCR/Tesseract  only;
              OCR_HTR=0.  Use for typed reports and books.

       newspaper
              (krant,  kranten, news, newsprint) Same as print; treat the col‐
              lection as dense newsprint (no HTR).

       handwriting
              (handschrift, htr, handwritten, cursive) Force  the  handwriting
              pipeline   (OCR_SIDECAR_PIPELINE=vlm  unless  OCR_VLM=0):  local
              Qwen2.5-VL, then TrOCR/ARletta if the VLM is off or empty.

       historic
              (historisch, arletta, kraken, archival) HTR with  ARletta/kraken
              only (early-modern / 19–20c Dutch).

       modern (nieuw, trocr, notebook, contemporary) HTR with TrOCR only (mod‐
              ern notebooks and forms of contemporary cursive).

       mixed  (gemengd,  mix,  both) Print OCR, then VLM (and HTR fallback) on
              handwriting or print-garbage.

       form   (formulier, forms) Printed form plus handwritten  fields  (print
              then VLM/HTR).

       Examples: markdowner --archive handwriting -o letter.md scan.jpg
       markdowner vault --archive print ./scans-newspapers
       OCR_ARCHIVE=historic markdowner vault ./letters

EXCLUDE

       --exclude  / -X / OCR_EXCLUDE turns engines off using the same kinds as
       --archive (comma, plus, or space separated). Archive says what the  ma‐
       terial  is; exclude says which of those engines not to run. Applied af‐
       ter archive.  The web UI checkboxes write OCR_EXCLUDE outside  the  en‐
       gine-slider profile.

       historic
              (historisch,  arletta,  kraken) Skip ARletta/kraken. Handwriting
              still uses VLM + TrOCR.

       modern (nieuw, trocr) Skip TrOCR. Handwriting still uses VLM + ARletta.

       handwriting
              (handschrift, htr) Skip VLM and HTR (print-only).

       print  / newspaper (drukwerk, krant) Skip RapidOCR/Tesseract  (VLM/HTR-
              only).

       mixed  /  form  Do not force HTR after print for that archive kind (dy‐
              namic skip remains).

       Unknown --exclude tokens are a usage error. A combination  that  leaves
       no engine (--archive historic --exclude historic) is a usage error. Un‐
       known  OCR_EXCLUDE is ignored with a warning. Non-empty lists are writ‐
       ten to YAML as ocr_exclude.

       Examples: markdowner --archive handwriting --exclude historic  -o  let‐
       ter.md scan.jpg
       OCR_EXCLUDE=arletta markdowner vault ./notebooks

OCR

       Image  conversion  uses  a dynamic quality ensemble by default (OCR_EN‐
       GINE=auto).  Per page it may run in-process Tesseract and/or the Python
       sidecar (tools/ocr_sidecar), then keep the better text by  text-quality
       score  (HTR  is  never chosen solely because the engine name is HTR/AR‐
       letta).  The sidecar classifies each page  as  print,  handwriting,  or
       mixed  from  ink/line  regularity and print-OCR quality without path or
       folder rules. For a whole  vault  of  one  material,  set  --archive  /
       OCR_ARCHIVE  (see  ARCHIVE)  instead of guessing per page.  Handwriting
       pages  use   ARletta   /   kraken   HTR   when   models   are   present
       (./tools/fetch-arletta.sh); mixed pages may merge a RapidOCR letterhead
       with  an HTR body.  When both print OCR and HTR run, the higher quality
       score wins; a strong print score keeps print text even if the page  was
       tentatively  classified  as  handwriting.  Set OCR_QUALITY=fast to skip
       the Tesseract+sidecar ensemble (single print engine; HTR  fallback  may
       still run).

       Overrides   (optional):  OCR_SIDECAR_PIPELINE=htr  forces  handwriting;
       rapidocr / paddleocr / structurev3 force those print paths.   OCR_HTR=0
       disables HTR; default is always (run HTR when print OCR did not already
       solve  the  page  — handwriting, mixed, weak print).  OCR_HTR=auto uses
       the  same   dynamic   skip.    English   TrOCR   remains   a   fallback
       (./tools/fetch-trocr.sh) when ARletta/kraken is unavailable or weak.

       YAML frontmatter includes type: ocr, ocr_engine (tesseract / rapidocr /
       arletta:super / trocr-handwritten / paddle-…), optional ocr_confidence,
       lang,  lang_detected,  doc_kind, ocr_archive, quality, validation, plus
       ingest metrics image_width, image_height,  dpi_est,  skew_deg,  bright‐
       ness, contrast.

       Environment  (common):  OCR_ENGINE (auto|paddle|tesseract), OCR_ARCHIVE
       (auto|print|newspaper|handwriting |historic|modern|mixed|form), OCR_EX‐
       CLUDE (comma-separated kinds to skip; see EXCLUDE),  OCR_QUALITY  (fast
       skips  ensemble),  MARKDOWNER_OCR_SIDECAR, OCR_SIDECAR_TIMEOUT (default
       1800 when HTR is enabled;  override  up  to  7200),  OCR_SIDECAR_SERVE,
       OCR_SIDECAR_PIPELINE (auto|rapidocr|htr|arletta|paddleocr|structurev3),
       OCR_HTR  (auto|always|0),  OCR_HTR_ENGINE  (auto|kraken|arletta|trocr),
       OCR_HTR_ARLETTA_DIR,  OCR_HTR_MODEL,  OCR_HTR_LINE_ENSEMBLE,   OCR_PRE‐
       PROCESS  (handwriting optional gentler cursive preprocess), OCR_BRIGHT‐
       NESS_AUTO (0  disables  auto  brightness  TRC  on  dark/bright  scans),
       OCR_CRITICAL (1 uses a longer sidecar timeout, 3600 s, for hard pages),
       OCR_LANG,  OCR_PSM  (Tesseract  page  segmentation 0–13; default AUTO),
       OCR_BINARIZE, OCR_MIN_LONG_EDGE (minimum  long-edge  pixels  after  up‐
       scale;   default   2400),   OCR_MAX_LONG_EDGE  (clamp;  default  4500),
       OCR_PDF_DPI (default 350), OCR_PDF_JOBS (parallel page OCR; default 2),
       OCR_SIDECAR_INIT_TIMEOUT (StructureV3 isolate  timeout;  default  120).
       Long  OCR  runs  print  progress on stderr (ocr: sidecar still running…
       every  10 s;   sidecar   backend   lines).    Vault   -j   >   1   sets
       OMP_NUM_THREADS=1 when unset to avoid OpenMP oversubscription.  Scanned
       PDFs  with  little  extractable text are rendered via pdftoppm / pdfto‐
       cairo  when  available,  then  OCR'd   (no   hard   page   cap).    See
       third_party/README.md  for  sidecar  install (Python 3.10–3.12) and HTR
       model fetch.

       Traineddata is resolved from TESSDATA_PREFIX (the tessdata  directory),
       a  build-time  default under third_party/prefix, or common system paths
       (/usr/share/tesseract-ocr/5/tessdata, …).  Install language packs  such
       as  tesseract-ocr-eng  and  tesseract-ocr-nld  when  using  the  system
       Tesseract.  Optional tesseract-ocr-osd enables orientation detection.

QUALITY AND VALIDATION

       Every convert  writes  YAML  quality  fields  when  possible:  quality:
       ok|weak,  lang  / lang_detected (NL/DE/EN/FR frequency guess), doc_kind
       (e.g. letter, newspaper, form,  handwritten),  and  validation  (remark
       list). Empty bodies fail; weak text is marked ok-weak in vault progress
       and  still  written.   Optional  local  hunspell adds spell remarks; if
       missing, remarks include spell: unavailable.  Vault  appends  one  JSON
       line  per  file to _meta/metrics.jsonl (timings and quality). Summarize
       with markdowner metrics VAULT.

MODELS SUBCOMMAND

       markdowner models [-v|-D] status reports on-disk OSS  models  (ARletta,
       TrOCR,  Whisper,  tessdata,  BGE-M3).  markdowner models [-v|-D] update
       runs the local fetch scripts  (tools/fetch-arletta.sh,  fetch-trocr.sh,
       fetch-whisper.sh, fetch-embed.sh); network is used only for downloads.

       -v, --verbose / -D, --debug
              Same global log levels.

METRICS SUBCOMMAND

       markdowner   metrics  [-v|-D]  VAULT  [VAULT...]   prints  counts  from
       _meta/metrics.jsonl (lines / ok / weak / fail).

       -v, --verbose / -D, --debug
              Same global log levels; debug log under VAULT/_meta/debug.log.

STATUS SUBCOMMAND

       markdowner status [-v|-D] VAULT [VAULT...]  prints a health summary per
       vault:  resolved  path,  source  counts,  manifest   ok/weak/fail/skip,
       zettel/logseq,  FTS  doc count, RAG (chunks.jsonl vs embeddings.f32 row
       counts, including mismatch/corrupt), last  run.log  vault/index  lines,
       and  inconsistencies  (missing  source files, empty RAG dir, unreadable
       SQLite). Multiple paths are accepted so a shell glob works:  markdowner
       status ./vaults/*.  Non-vault entries (plain files, ., ..)  are skipped
       with  a  warning.  Soft warnings (no RAG yet) exit 0; exit 1 if no path
       was a valid vault; exit 2 if any vault has hard  corruption  (RAG  mis‐
       match, unreadable FTS). Alias: vault-status.

       -v, --verbose / -D, --debug
              Same global log levels; debug log under VAULT/_meta/debug.log.

WEBUI SUBCOMMAND

       markdowner  webui  starts  the  local Web UI (tools/webui/server.py) on
       127.0.0.1:8877 (next free port in +20 if busy) and opens  that  URL  in
       the default browser.  Alias: markdowner ui.  Stop with Ctrl+C.

       The server is stdlib Python (no extra pip packages). It must be able to
       find  tools/webui/server.py  (run  from  the  source tree, or set MARK‐
       DOWNER_ROOT / MARKDOWNER_WEBUI).  Jobs spawned by the UI use this mark‐
       downer binary when MARKDOWNER_BIN  is  unset  (set  automatically  from
       /proc/self/exe).

       --no-browser, --no-open
              Start the server without opening a browser (headless / SSH).

       -p, --port=PORT
              Preferred bind port (same as MARKDOWNER_WEBUI_PORT).

       -v, --verbose / -D, --debug
              Same global log levels.

VAULT SUBCOMMAND

       markdowner  vault  [-v|-D]  INDIR  [INDIR...]   walks each INDIR recur‐
       sively, converts every supported document, mail,  or  image  file,  and
       writes  a  Zettelkasten  vault  (full  conversions  under sources/; run
       zettel afterwards for atomic notes). Source files are  never  modified.
       Re-runs  skip  unchanged  files  (size+mtime, then SHA-256 in the mani‐
       fest). Conversion uses a worker pool.  Progress is printed on stderr as
       vault: [n/N] convert|ok|skip|fail|ok-weak with per-file timing and  to‐
       tal  elapsed  time.  Also appends _meta/metrics.jsonl.  Multiple source
       directories (shell glob markdowner vault ./bron/*) build one vault  per
       INDIR.   Non-directories  are skipped. Combined exit is the worst fail‐
       ure.  With a Paddle OCR sidecar, prefer  -j  2  to  4  (heavy  models);
       higher -j is fine with OCR_ENGINE=tesseract.

       -v, --verbose / -D, --debug
              Same global log levels; debug and run logs under VAULT/_meta/de‐
              bug.log and VAULT/_meta/run.log.

       -o, --output=DIR
              Vault  directory.  Default:  ./vaults/basename(INDIR)  (trailing
              slashes on INDIR are ignored when deriving the name,  so  ./fix‐
              tures/testdata/  becomes vaults/testdata, not vaults/vault).  If
              DIR is the vaults container (final path component vaults,  or  a
              directory  that  already  holds sibling vaults), Markdowner uses
              DIR/basename(INDIR) instead,  so  each  project  keeps  its  own
              sources/,  notes/, and _meta/.  Do not use -o ./vaults expecting
              a single shared sources/ tree.

       -E, --each
              Create one vault per  immediate  subdirectory  of  INDIR  (under
              ./vaults or under -o).

       -j, --jobs=N
              Number  of  parallel workers. Default and maximum: 90% of online
              CPUs (at least 1). A higher -j is reduced with a  warning.  With
              the  OCR  sidecar  the pool may be capped lower still (typically
              2–4).

       -L, --lang=LANG
              OCR language for images found during ingest.

       -A, --archive=KIND
              Same as convert --archive : force print vs  handwriting  engines
              for every image in this vault (OCR_ARCHIVE; see ARCHIVE).

       -X, --exclude=KIND[,KIND...]
              Same as convert --exclude (see EXCLUDE).

ZETTEL SUBCOMMAND

       markdowner   zettel   [-v|-D]   [--rebuild]   VAULT  [VAULT...]   reads
       VAULT/sources, splits documents on  Markdown  headings  and  horizontal
       rules  (---), writes Folgezettel-linked notes under notes/, updates in‐
       dex.md (keyword register), writes _meta/isolation.md,  and  prepares  a
       local  Logseq  file  graph  (see  LOGSEQ).  Multiple vaults (shell glob
       markdowner zettel ./vaults/*) run sequentially; non-vaults are skipped.
       Combined exit is the worst failure.

       Default is  incremental  :  unchanged  sources  (SHA-256,  recorded  in
       _meta/zettel.jsonl)  are  skipped  and their existing notes are reused.
       Sources that gained or lost sections overwrite notes in place and  drop
       leftover  numbered  files.  Notes whose source file disappeared are re‐
       moved. The keyword register and isolation report are  always  rewritten
       from  the  current  notes.   On  a  first run after upgrading from 1.x,
       sources whose notes/*/0001.md is at least as  new  as  the  source  are
       treated as unchanged (bootstrap).

       --rebuild
              Delete  existing  notes/  ,  index.md , _meta/isolation.md , and
              _meta/zettel.jsonl then rebuild from sources/.  Use this after a
              botched incremental run, or when you  want  a  guaranteed  clean
              tree. A full rebuild can take a long time on large vaults.

       -v, --verbose / -D, --debug
              Same global log levels; debug log under VAULT/_meta/debug.log.

LOGSEQ SUBCOMMAND

       markdowner  logseq  [-v|-D] VAULT [VAULT...]  turns VAULT into a Logseq
       file graph for offline open-source graph view:  creates  pages/,  jour‐
       nals/, assets/, and logseq/config.edn (sync/journals off; home page in‐
       dex;  sources/  and  _meta/  hidden). Notes under notes/ are exposed as
       page  symlinks  using   Logseq   namespace   filenames   (stem/0001pages/stem___0001.md).  Re-run after regenerating notes; safe and idem‐
       potent.  Install Logseq with tools/fetch-logseq.sh or your distro pack‐
       age,  then  Add  local  graph on the vault folder. See LOGSEQ.md in the
       vault.

       -v, --verbose / -D, --debug
              Same global log levels; debug log under VAULT/_meta/debug.log.

INDEX SUBCOMMAND

       markdowner index [-v|-D] [--no-rag] [--rebuild] [-j N] VAULT [VAULT...]
       builds or refreshes an on-disk  FTS5  index  at  VAULT/_meta/inv/fts.db
       and,  unless skipped, a hybrid RAG store under VAULT/_meta/rag/ (chunks
       + embeddings used by ask).  After  upgrading  Markdowner,  run  a  full
       reindex  if the store format changed (older MDDC002 shard trees are un‐
       used).  Multiple vaults (shell glob markdowner  index  ./vaults/*)  run
       sequentially; non-vaults are skipped.

       Default is incremental docs table and only inserts, updates, or deletes
       changed  rows.  RAG reuses chunks and vectors for files whose SHA still
       matches _meta/rag/file_cache.json (same  embed  model  and  chunk  set‐
       tings);  only  new  or  changed  files are embedded. An unchanged vault
       prints FTS5 up to date and ask_rag: up to date and exits quickly.  If a
       previous RAG write was interrupted  (chunks.jsonl  longer  than  embed‐
       dings.f32),  the  next  incremental  index does not abort; it resumes a
       matching embed checkpoint or rebuilds vectors without reusing the  mis‐
       matched store.

       Search  and  ask  prefer this index. After vault or zettel runs, invoke
       index explicitly (ingest does not always  fully  reindex).  Incremental
       index  after adding a few documents is the cheap path; --rebuild is the
       expensive full rewrite.

       Local embeddings default to  BAAI/bge-m3  after  ./tools/fetch-embed.sh
       (sentence-transformers;  CPU  can  take  many  hours  on large vaults).
       Without a fetched model, ASK_RAG_EMBED=auto fails (no silent hash). Use
       ./tools/fetch-embed.sh  or  index  --no-rag  (or  explicit  ASK_RAG_EM‐
       BED=hash  for debug). Local embeds checkpoint to embeddings.partial.f32
       / embed_progress.json so an interrupted run can resume (same  new-chunk
       fingerprint).  On  GPU the default encode batch soft-bumps to 64 (over‐
       ride with ASK_RAG_EMBED_BATCH).  Tune chunk size / batch / backend  via
       ASK_RAG_* (see ENVIRONMENT).

       -j, --jobs=N
              Parallel  FTS workers (default and maximum: 90% of online CPUs).
              Progress lines index: [n/N] docs  are  printed  periodically  on
              stderr.

       --no-rag
              Build  FTS  only;  skip  _meta/rag/  embeddings.  Same  as MARK‐
              DOWNER_RAG_INDEX=0.

       --rebuild
              Wipe the FTS database and _meta/rag/ store,  then  rebuild  from
              scratch.  Skips the “up to date” short-circuit and the incremen‐
              tal FTS/RAG path, and does not  resume  embed  checkpoints.   On
              large vaults this can take a long time (especially BGE-M3 embed‐
              dings).

       -v, --verbose
              Extra  FTS/RAG  progress;  also raises MARKDOWNER_LOG to verbose
              and passes -v to tools/ask_rag.

       -D, --debug
              Full diagnostics and VAULT/_meta/debug.log.  Implies verbose for
              the RAG sidecar.

SEARCH SUBCOMMAND

       markdowner search [-v|-D] VAULT [VAULT...]  [QUERY...]  searches  Mark‐
       down  under  each  vault. Multiple QUERY words are combined with AND at
       document level; matching lines are printed as path:line: text.

       After a shell glob (markdowner search ./vaults/* foo), directories that
       look like a vault are vaults; other directories, existing files, ., and
       ..  are skipped with a warning; leftover words that  are  not  existing
       paths  are  the  query. The same query runs on every vault; results are
       labeled when more than one path was given. Combined exit is  the  worst
       of 0, 1, or 2.  Multiple vaults require a QUERY.

       If  _meta/inv/fts.db  is  present,  the query uses FTS5 (no full corpus
       scan).  Otherwise it scans and prints a hint to run index.  Case-sensi‐
       tive search always uses the scan path.

       If only VAULT is given on a TTY, an interactive  search>  prompt  reads
       queries until quit, EOF, or Ctrl+C.

       -n, --max-count=N
              Maximum number of hits to print (default: 30).

       -s, --sources
              Search only sources/.

       -t, --notes
              Search only notes/.

       -c, --case-sensitive
              Match case-sensitively (default is case-insensitive).

       -v, --verbose / -D, --debug
              Same global log levels; debug log under VAULT/_meta/debug.log.

ASK SUBCOMMAND

       markdowner  ask  [-v|-D] VAULT [VAULT...]  [QUESTION...]  answers ques‐
       tions over one or more vaults. Vault vs question uses the same split as
       search (vault directories vs leftover words). The same question runs on
       each vault (markdowner ask ./vaults/* Multiple vaults require  a  QUES‐
       TION.

       Preferred path: hybrid RAG via tools/ask_rag (BM25 over chunks + embed‐
       dings,  RRF  fusion, then grounded chat).  markdowner index builds both
       the FTS5 index (_meta/inv/fts.db) and the RAG store  (_meta/rag/)  with
       local  OSS  embeddings  by default (one-time ./tools/fetch-embed.sh; no
       API key). Skip embeddings with  index  --no-rag  or  MARKDOWNER_RAG_IN‐
       DEX=0.   Embed backends (ASK_RAG_EMBED): auto (local BGE-M3 if fetched,
       else fail; no silent hash),  local  (BGE-M3  /  sentence-transformers),
       hash  (debug only; fast deterministic vectors; weaker semantic recall),
       mistral  (cloud;  needs   MISTRAL_API_KEY).    Large   corpora:   raise
       ASK_RAG_CHUNK_CHARS   (default   1800)   to   cut  chunk  count,  raise
       ASK_RAG_EMBED_BATCH (default 16 on CPU; soft 64 on GPU when unset), use
       ASK_RAG_EMBED=hash  for  a  quick  index,  or   install   GPU   PyTorch
       (./tools/ensure-torch-gpu.sh)  so  local  encodes use CUDA/ROCm. Inter‐
       rupted local embeds resume from checkpoint.  You can still run

       tools/ask_rag index VAULT

       manually. When the RAG store exists, markdowner ask  delegates  to  the
       sidecar automatically.  Without a RAG store, ask falls back to FTS hits
       from  fts.db  (or  errors if neither exists).  Default is RAG/FTS only;
       keep the older C++ lexical retrieve with MARKDOWNER_ASK=legacy.   Over‐
       ride the script path with MARKDOWNER_ASK_RAG.

       Default ask is --deep=5 (investigative loop). Use --deep=0 or --no-deep
       for  one hybrid retrieve (BM25 pool + vector pool, RRF fusion, about 12
       chunks) then one grounded chat — a nearest-neighbour sample, not a scan
       of the vault. The whole sources/ and notes/ trees are  already  in  the
       store  (recursive);  deep  does not “open extra subdirs”. It re-queries
       the same store, then (from L2) partitions only  along  axes  that  this
       archive  actually  has.  A flat scan vault without dates skips the year
       axis and still uses unread threads.   L4  adds  spelling  variants  for
       zero-hit  tokens,  hop-2 names (new names searched alone), and adjacent
       files in the same folder.  L5 pairs those names with  every  seed  term
       and  adds  extra  vector queries.  Never a full census: gaps stay gaps.
       Shop/order/newsletter chunks are downranked unless the  question  names
       them.

       Legacy  lexical  ask retrieves passages with the vault index/search and
       hand-tuned keyword heuristics (frozen; do  not  extend  synonym  tables
       there).  When both sources/ and notes/ hit the same converted document,
       legacy ask keeps the notes/ passage (dedupe) so RAG context is not dou‐
       bled.  Chat uses local Ollama when ASK_RAG_CHAT=ollama (or auto without
       a  Mistral  key),  else  Mistral Chat Completions if MISTRAL_API_KEY is
       set. Optional cloud  embed  model:  MISTRAL_EMBED_MODEL  (default  mis‐
       tral-embed).   Without  chat backend (or with --dry-run), ask stays re‐
       trieve-only and prints chunks.  Optional  MISTRAL_MODEL  /  ASK_RAG_OL‐
       LAMA_MODEL.  With only VAULT on a TTY, an interactive ask> prompt reads
       questions  until  quit,  EOF,  or Ctrl+C.  A new markdowner ask process
       starts with empty history (nothing is loaded from an earlier CLI ask or
       from the WebUI). Prior questions and answers are kept in  this  session
       only  and  sent with each call (follow-ups like “what did she say about
       that?” work). Type clear to reset this session.  Phrases search deeper,
       more sources, dieper zoek, grondiger zoek, and meer bronnen  raise  the
       deep level by one (max L5) and repeat the last content question.

       Retrieval regression (example vault):

       tools/ask_rag eval VAULT testdata/ask_rag_cases_mt_beraad.json

       -v, --verbose
              Raises log level and ask/RAG sidecar verbosity.

       -D, --debug
              Full  diagnostics  and VAULT/_meta/debug.log.  Implies --verbose
              for the ask sidecar.

       --dry-run
              Retrieve only (no chat API call).

       --deep[=0|1|2|3|4|5]
              Investigative retrieve (default 5; 0 turns it off). Not a bigger
              top-k; not a vault census. Also  --no-deep  for  level  0.   The
              store already contains every subdirectory; levels add rounds and
              axes, not extra folders.

              L1     --deep=1.  Decompose the question (synonyms + names), ex‐
                     tra  BM25  sweeps  plus  a  few  vector  queries,  dedupe
                     notes/sources/copies/page splits, then lexical follow-ups
                     on names/years found in the evidence.

              L2     --deep=2 or --deep2.  L1 plus partitions  that  exist  in
                     this  store:  unread  threads  (high-scoring chunks whose
                     thread was not kept); path prefixes if the corpus is  not
                     one  blob;  years  if  enough  paths contain 19xx/20xx. A
                     vault without those signals skips those axes (no  assumed
                     INBOX or year directories).

              L3     --deep=3  or  --deep3.   L2 plus sibling chunks of chosen
                     threads (other pages of the same document) and  a  larger
                     packed context.

              L4     --deep=4  or  --deep4.  L3 plus spelling variants for to‐
                     kens that scored 0 BM25 (e.g. teeken→teken), hop-2  names
                     searched  alone  (not only paired with the original ques‐
                     tion), and adjacent files in the same folder.

              L5     --deep=5 or --deep5.   Default.  L4  plus  pairing  hop-2
                     names  with  every seed term, extra hop-2 vector queries,
                     and more thread/neighbor expansion.

              Optional ASK_RAG_DEEP_LLM=1 adds chat synonyms  (slower).  Caps:
              ASK_RAG_DEEP_ROUNDS,  ASK_RAG_DEEP_FINAL, ASK_RAG_DEEP_MAX_CHARS
              (see ENVIRONMENT).  L2+ use the GPU embed sidecar for query vec‐
              tors; L4/L5 add more.  Do not run them beside a heavy index  em‐
              bed on a small iGPU.

       -s, --sources / -t, --notes
              Restrict to sources/ or notes/.

       -n, --max-count=N
              Legacy lexical hit count (default 40).

       -C, --context=N
              Legacy context lines (default 24).

       --max-chars =N
              Context budget (default 48000).

       --retrieve =WORDS
              Override retrieval keywords (legacy lexical only).

       Vault snippets are sent to the Mistral API only when MISTRAL_API_KEY is
       set;  use cloud ask only on approved corpora.  Load the key from ./.env
       or ~/.config/markdowner/.env (see .env.example; chmod 600; never commit
       .env).  Already-exported environment variables always  win;  Markdowner
       never prints secret values.  Override the file with MARKDOWNER_DOTENV.

MAIL EXPORTS

       Gmail Takeout
              Export mail as .mbox then markdowner vault on the Takeout folder
              (or convert the mbox directly).

       Outlook
              Save  messages  as  .eml or .msg , or place a .pst in the ingest
              tree.  Encrypted  PST/OST  is  skipped  with  an  error;  prefer
              EML/MBOX  when  possible. PST extraction is best-effort for mes‐
              sages that still carry MIME on disk.

       Each message becomes one Markdown file with YAML frontmatter (from, to,
       subject, date, message_id).  Attachments are listed but  not  extracted
       as binary files.  Thread grouping is not implemented yet.

VAULT LAYOUT

       One  input  directory  becomes  one  vault  under the vaults/ container
       (never use the container itself as vault root):
       vaults/
         <project>/
           home.md
           index.md
           sources/          full document / mail / OCR / ASR conversions
           notes/            atomic zettels
           pages/            Logseq page symlinks (→ notes/)
           journals/         Logseq journals (disabled in config)
           assets/           Logseq assets
           logseq/           config.edn (local file graph)
           LOGSEQ.md         how to open the graph
           _meta/
             manifest.jsonl
             metrics.jsonl   per-file timings / quality (ok|weak|fail)
             run.log          vault run (UTC ISO lines)
             debug.log        --debug / MARKDOWNER_LOG=debug (when no LOG_FILE)
             isolation.md
             inv/            FTS5 index (fts.db)
             rag/            hybrid RAG (chunks.jsonl, embeddings.f32, vectors.usearch, manifest.json)
       Each vault has its own sources/.   Sibling  projects  do  not  share  a
       sources tree.

ENVIRONMENT

       MARKDOWNER_PANDOC
              Set  to  0  /  off  / legacy to skip Pandoc and use built-in Of‐
              fice/HTML/RTF/EPUB extractors (with a  warning).   Default:  use
              pandoc  on  PATH  when the format is supported (docx, odt, epub,
              html, rtf).  If pandoc is installed but conversion fails,  Mark‐
              downer  refuses  the  builtin parser unless MARKDOWNER_LEGACY=1.
              LibreOffice HTML for sheets/slides is also converted with pandoc
              when available.

       MARKDOWNER_SOFFICE
              Set  to  0  /   off   /   legacy   to   skip   LibreOffice   for
              xlsx/ods/pptx/odp  and  use  ZIP+XML extractors (with a warning;
              truncated output is still refused  unless  MARKDOWNER_LEGACY=1).
              Default:  use soffice / libreoffice on PATH when present (HTML →
              pandoc).  If soffice is installed but conversion fails, ZIP  ex‐
              tractors are refused unless MARKDOWNER_LEGACY=1.

       MARKDOWNER_LEGACY
              Set  to  1  to allow builtin Office parsers after pandoc/soffice
              failure, and to accept truncated ZIP sheet/slide extractors (500
              rows / 50 columns / 200 slides).  Default:  refuse  those  fall‐
              backs  when  the  preferred tool was available, and refuse trun‐
              cated ZIP ingest.

       MARKDOWNER_WEBUI_HOST
              Bind address for markdowner webui (default 127.0.0.1).

       MARKDOWNER_WEBUI_PORT
              Preferred port for markdowner webui (default  8877;  the  server
              tries the next 19 ports if busy).

       MARKDOWNER_WEBUI
              Override path to tools/webui/server.py .

       MARKDOWNER_BIN
              Override  path  to  the markdowner binary used by the Web UI for
              jobs. Set automatically by markdowner webui when unset.

       MISTRAL_API_KEY
              API key for cloud ask chat (and optional ASK_RAG_EMBED=mistral).
              Prefer storing it in ./.env or ~/.config/markdowner/.env  (chmod
              600;  see  .env.example)  rather than exporting it in shell his‐
              tory.  If unset, ask uses local Ollama when available, else  re‐
              trieve-only (same as --dry-run without a chat backend).

       MISTRAL_MODEL
              Chat model name (default mistral-large-latest).

       MISTRAL_EMBED_MODEL
              Cloud  embed  model  when  ASK_RAG_EMBED=mistral  (default  mis‐
              tral-embed).

       MARKDOWNER_RAG_INDEX
              Set to 0 to skip RAG embeddings during index (same as --no-rag).

       MARKDOWNER_ASK
              rag (default: hybrid RAG / FTS via tools/ask_rag; no C++ lexical
              fallthrough), legacy (frozen C++ heuristics only), or auto (same
              as rag).

       MARKDOWNER_ASK_RAG
              Override path to tools/ask_rag / ask_rag.py.

       ASK_RAG_EMBED
              auto (default: local BGE-M3 if fetched,  else  fail;  no  silent
              hash),  local,  hash  (debug,  weak  recall),  mistral,  or fake
              (tests).

       ASK_RAG_CHUNK_CHARS
              Chunk size in characters when building _meta/rag/  with  chonkie
              RecursiveChunker (default 1800).  Larger values → fewer chunks →
              faster embeds on big vaults.

       ASK_RAG_CHUNK_OVERLAP
              Overlap passed to chonkie OverlapRefinery (default 200).

       ASK_RAG_EMBED_BATCH
              Texts  per  encode  batch  (default  16  on CPU).  When unset on
              CUDA/ROCm, soft-default  64  (same  model;  higher  throughput).
              Raise further if VRAM allows; OOM auto-splits the batch.

       ASK_RAG_EMBED_BATCH_GPU
              Soft GPU batch when ASK_RAG_EMBED_BATCH is unset (default 64).

       ASK_RAG_LOCAL_MODEL
              Hub id for forced local load (default BAAI/bge-m3).

       ASK_RAG_LOCAL_MODEL_DIR
              Directory  of  a  fetched  sentence-transformers  model (default
              third_party/models/embed-bge-m3).

       ASK_RAG_DEVICE
              Force embed device: cpu, cuda, or cuda:0.   Default:  cuda  when
              torch.cuda.is_available()  (NVIDIA  CUDA or AMD ROCm), else cpu.
              Install GPU wheels with ./tools/ensure-torch-gpu.sh.

       ASK_RAG_CHAT
              auto, ollama, or mistral.

       ASK_RAG_OLLAMA_MODEL
              Ollama chat model (default qwen2.5:72b).

       ASK_RAG_TOP_VEC
              Vector retrieval pool size (default 24).

       ASK_RAG_TOP_BM25
              BM25 retrieval pool size (default 24).

       ASK_RAG_TOP_FINAL
              Final fused hit count (default 12).

       ASK_RAG_MAX_CHARS
              Context budget for RAG packing (default 48000). Deep  L2+  raise
              this unless ASK_RAG_DEEP_MAX_CHARS is set.

       ASK_RAG_DEEP_LLM
              Set  to  1  to add a chat pass for extra lexical synonyms during
              --deep (default off; heuristic only).

       ASK_RAG_DEEP_ROUNDS
              Override deep follow-up  rounds  (defaults:  L1=3,  L2=4,  L3=5,
              L4=6, L5=7).

       ASK_RAG_DEEP_FINAL
              Override  how  many  diverse  hits  deep packs (defaults: L1=24,
              L2=36, L3=48, L4=56, L5=64).

       ASK_RAG_DEEP_MAX_CHARS
              Override deep  context  budget  (defaults:  L1=64000,  L2=80000,
              L3=96000, L4=112000, L5=128000).

       OCR_LANG
              Default OCR/ASR language when -L is omitted (default nld).

       ASR_LANG
              ASR  language  override  (mapped  to Whisper ISO codes; nldnl).
              Falls back to OCR_LANG.

       ASR_MODEL
              Whisper model size (default large-v3).

       ASR_MODEL_DIR
              Local faster-whisper model directory  (default  third_party/mod‐
              els/whisper-large-v3 after fetch-whisper.sh).

       ASR_TIMESTAMPS
              Set to 1 to prefix transcript lines with [mm:ss].

       ASR_SIDECAR_SERVE
              Set to 0 for oneshot ASR (default: warm JSONL daemon).

       ASR_SIDECAR_TIMEOUT
              ASR sidecar timeout in seconds (default 600).

       MARKDOWNER_ASR_SIDECAR
              Path to asr_sidecar script.

       OCR_ARCHIVE
              Collection material flag (same as --archive; see ARCHIVE).  Sets
              OCR_SIDECAR_PIPELINE, OCR_HTR, OCR_HTR_ENGINE and OCR_PREPROCESS
              to  match  RapidOCR,  Tesseract,  Qwen2.5-VL,  ARletta/kraken or
              TrOCR when not auto.

       OCR_EXCLUDE
              Skip engines (same as --exclude; see EXCLUDE).

       OCR_ENGINE
              auto (dynamic: print ensemble and/or handwriting HTR  per  page;
              final  pick  by text-quality score, not engine name), paddle, or
              tesseract.

       OCR_QUALITY
              Set to fast to skip the Tesseract+sidecar ensemble (single print
              path; HTR may still run).

       MARKDOWNER_OCR_SIDECAR
              Path to tools/ocr_sidecar or ocr_sidecar.py.

       OCR_SIDECAR_TIMEOUT
              Sidecar wall-clock timeout in seconds (default 1800 when HTR  is
              enabled; dual historic+modern HTR on CPU is slow; override up to
              7200).

       OCR_SIDECAR_SERVE
              Set to 0 or oneshot to spawn a fresh sidecar per image (default:
              keep a warm --serve JSONL daemon).

       OCR_SIDECAR_PIPELINE
              auto  (default:  classify  print  vs handwriting, pick engines),
              rapidocr, vlm (Qwen2.5-VL page transcription), htr /  arletta  /
              handwriting / trocr (force HTR), paddleocr, or structurev3.

       OCR_VLM
              auto (default: Ollama vision if a Qwen2.5-VL tag is loaded, else
              Transformers/ROCm),   ollama,  transformers,  or  0  to  disable
              (TrOCR/ARletta remain as HTR fallback).

       OCR_VLM_MODEL
              Ollama tag (default qwen2.5vl:7b) or a Transformers directory  /
              Hugging Face id.

       OCR_VLM_MAX_EDGE
              Long-edge  pixels  for  the  VLM  image (default 1536; print OCR
              still uses OCR_PDF_DPI).

       OCR_HTR
              always (default: compare print vs HTR when HTR can help  —  skip
              TrOCR  on  dense  print/newspapers;  true handwriting still runs
              HTR), auto (same dynamic routing), or 0 / off to disable.   When
              the  sidecar  already  compared  HTR (htr_attempted), Markdowner
              does not re-run a second HTR pass.  A daemon wall-clock  timeout
              does  not  start  a  oneshot  clone.  For print-only batches use
              OCR_HTR=0 (not the default — mixed archives need HTR).

       OCR_HTR_ENGINE
              auto (default: historic ARletta/kraken first; modern TrOCR  only
              if  historic  is  weak  or  the page has notebook rulings. Ruled
              notebooks prefer modern and skip historic when modern is already
              usable), ensemble / both (always run historic and  modern),  ar‐
              letta  / kraken (historic only), or trocr / modern (contemporary
              handwriting only).

       OCR_HTR_ARLETTA_DIR
              Directory with ARletta *.mlmodel files (default third_party/mod‐
              els/arletta; fetch with tools/fetch-arletta.sh).

       OCR_HTR_MODEL
              Override  TrOCR  directory  for  modern  handwriting.  Otherwise
              prefers  third_party/models/trocr-base-handwritten  on CPU (fin‐
              ishes) and trocr-large-handwritten on GPU,  then  the  other  if
              missing      (fetch     with     tools/fetch-trocr.shbase     or
              tools/fetch-trocr.sh  large).   OCR_HTR_PREFER_LARGE=1|0  forces
              the  order.   Historic  Dutch  uses  ARletta  under  OCR_HTR_AR‐
              LETTA_DIR.

       OCR_HTR_LINE_ENSEMBLE
              auto (default on CPU: also try RapidOCR/Tesseract per line  when
              TrOCR looks weak), always (default on GPU), or 0.  OCR_HTR_BEAMS
              defaults to 4 on CPU and 8 on GPU (max 8).

       OCR_PREPROCESS
              Set  to handwriting for gentler preprocess on cursive (optional;
              auto path usually enough).

       OCR_BRIGHTNESS_AUTO
              Set to 0 to disable automatic brightness TRC on dark  or  bright
              scans (default: on).

       OCR_CRITICAL
              Set  to  1  for  a  longer sidecar timeout (3600 s) on difficult
              pages.

       OCR_SIDECAR_INIT_TIMEOUT
              Seconds for isolated StructureV3 before falling through (default
              120).

       OCR_PDF_DPI / OCR_PDF_BATCH / OCR_PDF_JOBS
              PDF render DPI (default 350), pages per  render  batch  (default
              4), and parallel page OCR workers.

       MARKDOWNER_PLUGINS
              Directory  of  convert  *.json plugins (default plugins/ next to
              the binary / tree root).

       OMP_NUM_THREADS
              Prefer 1 when vault -j > 1.

       TESSDATA_PREFIX
              Directory that contains *.traineddata (the tessdata  directory).
              Used when system or bundled paths are insufficient.

EXIT STATUS

       0      Success, or all vault inputs skipped as unchanged.

       1      Usage error or missing path.

       2      Conversion  failure, no ask search hits, or ask API failure.  In
              vault mode, other files may still have been processed.

EXAMPLES

       markdowner report.pdf
       markdowner sample.eml
       markdowner sample.ics
       markdowner -L eng scan.tiff
       markdowner -o ./out-mail testdata/sample.mbox
       markdowner vault -j 2 ./scans
       markdowner vault -E ./inbox
       markdowner vault ./fixtures/testdata/
       markdowner vault -L eng ./archive
       markdowner vault --archive handwriting ./letters
       markdowner vault --archive handwriting --exclude historic ./notebooks
       markdowner --archive print -o newspaper.md scan.jpg
       markdowner zettel -v ./vaults/scans
       markdowner zettel ./vaults/*
       markdowner logseq ./vaults/scans
       markdowner logseq ./vaults/*
       APPIMAGE_EXTRACT_AND_RUN=1 ./third_party/logseq/logseq
       markdowner index -D -j 8 ./vaults/docs
       markdowner index --no-rag ./vaults/docs
       ASK_RAG_EMBED=hash markdowner index ./vaults/docs
       ASK_RAG_CHUNK_CHARS=4000 ASK_RAG_CHUNK_OVERLAP=100 markdowner index ./vaults/docs
       ASK_RAG_EMBED_BATCH=64 markdowner index ./vaults/docs
       markdowner search -v ./vaults/docs
       markdowner search ./vaults/docs common ground
       markdowner search ./vaults/* common ground
       markdowner ask ./vaults/docs "local question without API"
       cp .env.example .env && chmod 600 .env   # put MISTRAL_API_KEY=… in .env
       markdowner ask ./vaults/docs
       markdowner ask -D --dry-run ./vaults/docs "What is the NDD?"
       markdowner ask ./vaults/docs "What is the NDD?"
       markdowner ask --no-deep ./vaults/docs "short question"
       markdowner ask --deep=1 ./vaults/emails "employer timeline"
       markdowner ask --deep=2 ./vaults/docs "who is …"
       markdowner ask --deep=3 ./vaults/docs "who is …"
       markdowner ask --deep=4 ./vaults/docs "who is …"
       markdowner ask --deep=5 ./vaults/docs "who is …"
       markdowner ask ./vaults/* "What is the NDD?"
       OCR_SIDECAR_PIPELINE=rapidocr markdowner -o /tmp/o.md scan.jpg
       markdowner -o /tmp/any.md sample-scan.jpg
       ./tools/fetch-arletta.sh
       ./tools/fetch-whisper.sh
       ./tools/fetch-embed.sh
       markdowner -L eng -o interview.md interview.mp3
       ASR_TIMESTAMPS=1 markdowner -o ts.md clip.wav
       markdowner -D -o /tmp/o.md sample-scan.jpg
       MARKDOWNER_LOG=verbose MARKDOWNER_LOG_FILE=/tmp/md.log markdowner vault -j 2 ./scans
       markdowner ask --dry-run --retrieve smith ./vaults/testdata "who are the authors"
       markdowner models -v status
       markdowner models update
       markdowner metrics -v ./vaults/docs
       markdowner status ./vaults/docs
       markdowner status ./vaults/*
       markdowner webui
       markdowner webui --no-browser -p 9000
       OCR_CRITICAL=1 markdowner -o /tmp/hard.md hard-scan.tif

PERFORMANCE

       Without an FTS index, each search reads every Markdown file  under  the
       vault.  Build the index once (or after bulk ingest) for large corpora.

       RAG  embedding  of  BGE-M3 on CPU scales with chunk count (large agenda
       PDFs  →  tens/hundreds  of  thousands   of   chunks).   Prefer   larger
       ASK_RAG_CHUNK_CHARS,  GPU  via ./tools/ensure-torch-gpu.sh (NVIDIA CUDA
       or AMD ROCm; torch.cuda.is_available() should become true),  a  lighter
       local model dir, or ASK_RAG_EMBED=hash / index --no-rag when wall-clock
       matters  more  than semantic recall. Local embeds checkpoint/resume via
       embeddings.partial.f32 (same chunks fingerprint).

       Microbenchmark (indexed vs scan) on the sample vault: make bench-search

DEPENDENCIES

       Runtime open-source libraries:  tesseract,  leptonica  (OCR  fallback),
       libsqlite3  (FTS5), and libcurl (optional cloud ask). JSON for the Mis‐
       tral API uses nlohmann/json (header-only at build time).  Optional doc‐
       ument conversion: pandoc (docx/odt/epub/html/rtf, and LibreOffice  HTML
       for   sheets/slides;   GPL   subprocess)   and  soffice  /  libreoffice
       (xlsx/ods/pptx/odp → HTML).  Optional local graph viewer: Logseq (AGPL;
       see tools/fetch-logseq.sh).  Optional  OCR  sidecar  (Python  3.10–3.12
       venv):  paddleocr/paddlepaddle/paddlex and/or rapidocr-onnxruntime (see
       tools/requirements-ocr.txt).   Optional  handwriting  VLM:   Qwen2.5-VL
       (tools/fetch-vlm-ocr.sh)  via  Ollama  or  Transformers/ROCm.  Optional
       handwriting  HTR:  kraken  +  ARletta  models  (tools/fetch-arletta.sh,
       tools/requirements-ocr-htr.txt)  and/or  TrOCR  (tools/fetch-trocr.sh).
       Optional ASR (audio →  Markdown):  faster-whisper  (see  tools/require‐
       ments-asr.txt,  tools/fetch-whisper.sh)  and  ffmpeg on PATH.  Optional
       RAG embeds: sentence-transformers  +  ./tools/fetch-embed.sh  (BGE-M3),
       plus chonkie, rank_bm25, numpy, usearch, and python-dotenv (pip install
       -r tools/requirements-ask.txt into third_party/ocr-venv).  Re-index af‐
       ter  upgrading the RAG stack (manifest version2; adds vectors.usearch).
       Optional spell remarks: hunspell with language dictionaries (else vali‐
       dation notes  spell:  unavailable).   PDF  page  render  for  OCR  uses
       pdftoppm    /    pdftocairo    (Poppler)    when    on    PATH.     See
       third_party/README.md in the source tree  for  packaging  without  root
       apt.

NOTES

       Options  follow  normal Unix/getopt_long(3) rules: short options may be
       clustered; long options use --name or --name=value.  A double  dash  --
       ends  option  parsing. With GNU libc, options may appear after operands
       unless POSIXLY_CORRECT is set.

SEE ALSO

       getopt(3), pandoc(1), tesseract(1), ffmpeg(1), sqlite3(1)

       Dependency setup: third_party/README.md.

AUTHORS

       Written by Pascal Greuter.

markdowner 2.1                    August 2026                    MARKDOWNER(1)