File size: 19,618 Bytes
0ac7396
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
#!/usr/bin/env bash
# Build the FastH3 Omni MLX benchmark model directory from PUBLIC Hugging Face
# sources. Resumable, idempotent, self-verifying. Run `./prepare_model_dir.sh --help`.
set -euo pipefail

KIT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
HELPERS="$KIT_DIR/prepare_helpers.py"

# ---- Pinned source revisions (HF `main` commit SHAs fetched 2026-10-20) ------
REV_OMNI="53e0d26a1691aa473b6262d8196a8feb2c297df0"     # FastVideo/FastVideo-FastH3-Omni-8-Step-V1
REV_QAD="f01e6a23ac6066aa9eb622cf94b6259442e70ec2"      # FastVideo/FastH3-Omni-NVFP4-QAD
REV_DEC="bd0c248f2a0f57ca643b3df2ac1f375fa55101fb"      # FastVideo/FastH3-NVFP4-VAE-Decoder
REV_LVAE="4925ea978147b9de492d7a5fd4f9c6d1854ca1cc"     # lightx2v/LightVAE
REPO_OMNI="FastVideo/FastVideo-FastH3-Omni-8-Step-V1"
REPO_QAD="FastVideo/FastH3-Omni-NVFP4-QAD"
REPO_DEC="FastVideo/FastH3-NVFP4-VAE-Decoder"
REPO_LVAE="lightx2v/LightVAE"
ENCODER_SOURCE_FIELD="hf://FastVideo/FastH3-Omni-NVFP4-QAD/text_encoder"

# Reference SHA-256 of our build (from the validated model dir). Downloaded/copied
# files must match exactly (failure). dit/* and root/text_encoder/{model.safetensors,
# mlx_h3_nvfp4_encoder.json} are converter outputs: a mismatch is only a WARNING
# (MLX version / hardware may change the quantized bytes).
read -r -d '' EXPECTED_SHA256 <<'EXPECTED_EOF' || true
59b99642b95ea21630e311198ddbfffbfe05aadba0c2f5d884cbdf4efcc90f44 LICENSE
c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4 LICENSE-Qwen3-VL
6f63c0101bdd0c7188ba540277ddd619952e2874437f9702bcdfa644b355dbc4 NOTICE
fcc29480ea8247d75b5bdb86805c03044083af614244600d13467814acd4eb9b dit/int6/mlx_h3_dit.json
9ea28f8944686dbfecd684664ea5574b90994e3f232c4552b5c15dfc1faeb446 dit/int6/mlx_h3_dit.safetensors
804780f7133477067bd6bbfbc02dc8b3cf9feeb400f97c08f5b1d5f6cbab3840 root/audio_scheduler/scheduler_config.json
9a3c645ff892b376c6f5f4c8685964cd75474731af594ff058492a0000caabb6 root/audio_vae/config.json
52c59e67ba8de5477c81bfbced0327aabf500f1bfdeefd5ee754529241cb26cb root/audio_vae/diffusion_pytorch_model.safetensors
68577f23f0c866e329aa8a578b2455e4ca9a26a3ca1d15ddd6d0e99091007e18 root/fastvideo_inference.json
5c72a170d2a4a1a3bc5adad2e689ae28138a9700e5b8c96c0266331e86c0acce root/processor/chat_template.json
599bab54075088774b1733fde865d5bd747cbcc7a547c5bc12610e874e26f5e3 root/processor/merges.txt
27225450ac9c6529872ee1924fcb0962ff5634834f817040f444118116f4e516 root/processor/preprocessor_config.json
a5d85b6dcc535e6b93115a9ef287e6132fdbf30270da6218194ba742261173c7 root/processor/tokenizer.json
a07e942ac874baa13758de8d1fbdb186683cc03416b5589e1b6671c6b3057c68 root/processor/tokenizer_config.json
7768af27c1fafa9cc9011c1dc20067e03f8915e03b63504550e11d5066986d13 root/processor/video_preprocessor_config.json
ca10d7e9fb3ed18575dd1e277a2579c16d108e32f27439684afa0e10b1440910 root/processor/vocab.json
8fa6c3aa70dc9e691e1a6df899fd1b6f75f70481a27cee6e18a303817075c304 root/scheduler/scheduler_config.json
29d4a1e1db0e112612bf1e7c457a42ce9fe13fd0c69a0c3a64c1d291325bd9eb root/text_encoder/config.json
1d50c336c0e34645699039d78e7532fe2e7c21b2ed71f98d4252b682d53e075e root/text_encoder/mlx_h3_nvfp4_encoder.json
f91405a223c95ed013267a6274dbee40d832a28cf89184ab86f84214b1882809 root/text_encoder/model.safetensors
599bab54075088774b1733fde865d5bd747cbcc7a547c5bc12610e874e26f5e3 root/tokenizer/merges.txt
a5d85b6dcc535e6b93115a9ef287e6132fdbf30270da6218194ba742261173c7 root/tokenizer/tokenizer.json
a07e942ac874baa13758de8d1fbdb186683cc03416b5589e1b6671c6b3057c68 root/tokenizer/tokenizer_config.json
ca10d7e9fb3ed18575dd1e277a2579c16d108e32f27439684afa0e10b1440910 root/tokenizer/vocab.json
658c1f6278262db3072d9a6f4832c7c49280f91c94a8455414525cd6648de677 root/transformer_ref/config.json
ffa6d5e7a95dc57ae0494a8c0caf588e2c555252c159104d310ffcc450e1c914 root/vae/config.json
c0d0f1beb88f0579d2a9fb249850419dbf3baa05d86ce37d3135c8c8873b4752 vae/fasth3-fast-8L-decoder-nvfp4.safetensors
84a92170f1cf6737f0509f8350cf642832afe3c0c0b1766bf4a0e6e820b62729 vae/fasth3-quality-36L-decoder-nvfp4.safetensors
4b0e5af102da19324c64df40abbd95008d8acb0b9cf76e4704d74c8ac20cefac vae/lightvae-lite-h3-encoder.safetensors
EXPECTED_EOF

# Approximate sizes in GB.
SRC_GB=95; ENC_GB=16; OUT_FIXED_GB=7   # decoders+encoder(vae)+misc
fmt_gb() { case "$1" in int6) echo 17;; int8) echo 20;; bf16) echo 38;; *) echo 0;; esac; }

# ---- Options -----------------------------------------------------------------
FASTVIDEO=""; MODEL_DIR="$PWD/fasth3-omni-mlx-model"; SOURCES=""; FORMATS="int6"
PYTHON="python3"; SKIP_DOWNLOAD=0; KEEP_SOURCES=0; LOCK_DIR=""; DRY_RUN=0
RESERVE_GB=5   # free space that must remain after each big stage

usage() {
  cat <<EOF
Usage: $0 --fastvideo <FastVideo checkout> [options]
  --fastvideo DIR     FastVideo checkout (branch aryan/omniref-mlx-speed). Required.
  --model-dir DIR     output model directory (default: $MODEL_DIR)
  --sources DIR       download directory (default: <model-dir>/../fasth3-sources); may be deleted afterwards
  --formats "int6"    DiT formats, space/comma separated: int6 (default), int8, bf16
  --python PATH       python of the FastVideo venv (needs mlx, fastvideo); default python3
  --skip-download     use existing --sources (layout: omni-bf16/ nvfp4-qad/ vae-decoders/ lightvae/)
  --keep-sources      do not delete downloaded sources after a successful verified build
  --reserve-gb N      free GB that must remain after each big stage (default $RESERVE_GB)
  --lock-dir DIR      opt-in: serialize memory-heavy stages via mkdir DIR/gpu.lock
  --dry-run           print the hf/conversion commands without running them
Safe to interrupt (Ctrl-C) and re-run: finished stages are verified and skipped.
EOF
}

while [[ $# -gt 0 ]]; do
  case "$1" in
    --fastvideo) FASTVIDEO="$2"; shift 2;;
    --model-dir) MODEL_DIR="$2"; shift 2;;
    --sources) SOURCES="$2"; shift 2;;
    --formats) FORMATS="$2"; shift 2;;
    --python) PYTHON="$2"; shift 2;;
    --skip-download) SKIP_DOWNLOAD=1; shift;;
    --keep-sources) KEEP_SOURCES=1; shift;;
    --reserve-gb) RESERVE_GB="$2"; shift 2;;
    --lock-dir) LOCK_DIR="$2"; shift 2;;
    --dry-run) DRY_RUN=1; shift;;
    -h|--help) usage; exit 0;;
    *) echo "unknown option: $1" >&2; usage >&2; exit 2;;
  esac
done
[[ -n "$FASTVIDEO" ]] || { echo "error: --fastvideo is required" >&2; usage >&2; exit 2; }
FORMATS="${FORMATS//,/ }"
for f in $FORMATS; do [[ "$(fmt_gb "$f")" != 0 ]] || { echo "error: unknown format '$f' (int6|int8|bf16)" >&2; exit 2; }; done
abspath() { case "$1" in /*) echo "${1%/}";; *) echo "$PWD/${1%/}";; esac; }
MODEL_DIR="$(abspath "$MODEL_DIR")"; FASTVIDEO="$(abspath "$FASTVIDEO")"
[[ -n "$SOURCES" ]] || SOURCES="$(dirname "$MODEL_DIR")/fasth3-sources"
SOURCES="$(abspath "$SOURCES")"

S_OMNI="$SOURCES/omni-bf16"; S_QAD="$SOURCES/nvfp4-qad"; S_DEC="$SOURCES/vae-decoders"; S_LVAE="$SOURCES/lightvae"

log()  { printf '[%s] %s\n' "$(date +%H:%M:%S)" "$*"; }
warn() { printf '[%s] WARNING: %s\n' "$(date +%H:%M:%S)" "$*" >&2; WARNINGS+=("$*"); }
die()  { printf '[%s] ERROR: %s\n' "$(date +%H:%M:%S)" "$*" >&2; exit 1; }
WARNINGS=(); declare -a TIMINGS=()
run()  { if [[ $DRY_RUN -eq 1 ]]; then echo "  (dry-run) $*"; else "$@"; fi; }
helper() { "$PYTHON" -I "$HELPERS" "$@"; }
timed() {  # timed <label> <command...>; records elapsed time, preserves exit status
  local label="$1" t0=$SECONDS rc=0; shift
  "$@" || rc=$?
  TIMINGS+=("$label: $((SECONDS - t0))s"); return $rc
}

# ---- Lock (opt-in) -------------------------------------------------------------
LOCK_HELD=""
lock_release() { if [[ -n "$LOCK_HELD" ]]; then rm -rf "$LOCK_HELD"; LOCK_HELD=""; fi; }
lock_acquire() {
  [[ -n "$LOCK_DIR" && $DRY_RUN -eq 0 ]] || return 0
  mkdir -p "$LOCK_DIR"
  until mkdir "$LOCK_DIR/gpu.lock" 2>/dev/null; do
    log "waiting for $LOCK_DIR/gpu.lock (owner pid $(cat "$LOCK_DIR/gpu.lock/owner" 2>/dev/null || echo ?)); retry in 30 s"
    sleep 30
  done
  LOCK_HELD="$LOCK_DIR/gpu.lock"; echo $$ > "$LOCK_HELD/owner"; log "acquired $LOCK_HELD"
}
trap 'lock_release' EXIT
trap 'lock_release; exit 130' INT TERM

# ---- Preflight -------------------------------------------------------------------
free_gb() { df -Pk "$1" | awk 'NR==2 {printf "%d", $4/1048576}'; }
existing_parent() { local p="$1"; while [[ ! -d "$p" ]]; do p="$(dirname "$p")"; done; echo "$p"; }

need_space() {  # need_space <dir> <GB needed> <stage label>
  local dir; dir="$(existing_parent "$1")"; local free; free="$(free_gb "$dir")"
  if (( free < $2 + RESERVE_GB )); then
    local msg="not enough disk for $3: need ~$2 GB + $RESERVE_GB GB reserve, only $free GB free on $dir"
    if [[ $DRY_RUN -eq 1 ]]; then warn "$msg"; else die "$msg (partial work is kept; free space and re-run)"; fi
  fi
}

preflight() {
  [[ "$(uname -s)" == Darwin && "$(uname -m)" == arm64 ]] || warn "not Apple Silicon macOS; MLX conversion will not run"
  [[ -f "$FASTVIDEO/scripts/checkpoint_conversion/convert_minimax_h3_mlx.py" ]] \
    || die "$FASTVIDEO is not a FastVideo checkout with convert_minimax_h3_mlx.py (need branch aryan/omniref-mlx-speed)"
  command -v "$PYTHON" >/dev/null || die "python not found: $PYTHON"
  if [[ $DRY_RUN -eq 0 ]]; then
    PYTHONPATH="$FASTVIDEO" "$PYTHON" -I -c 'import sys; sys.path.insert(0, sys.argv[1]); import mlx.core, fastvideo.mlx_runtime.minimax_h3_conditioner' "$FASTVIDEO" \
      || die "$PYTHON cannot import mlx and fastvideo; pass --python of the FastVideo venv"
  fi
  if [[ $SKIP_DOWNLOAD -eq 0 ]]; then
    command -v hf >/dev/null || die "'hf' CLI not found; run: $PYTHON -m pip install -U huggingface_hub"
  fi
  local mem_gb; mem_gb=$(( $(sysctl -n hw.memsize 2>/dev/null || echo 0) / 1073741824 ))
  log "total RAM: ${mem_gb} GB"
  (( mem_gb >= 24 )) || warn "RAM below 24 GB; int6/int8 conversion (peak ~21 GB) may fail"
  local f out_gb=$((OUT_FIXED_GB + ENC_GB)) fmts=""
  for f in $FORMATS; do
    if [[ $f == bf16 ]] && (( mem_gb < 64 )); then
      warn "skipping bf16: conversion needs ~40 GB free RAM and this Mac has ${mem_gb} GB (<64)"; continue
    fi
    out_gb=$((out_gb + $(fmt_gb "$f"))); fmts+="$f "
  done
  FORMATS="${fmts% }"
  local src_gb=0; [[ $SKIP_DOWNLOAD -eq 0 ]] && src_gb=$SRC_GB
  if [[ "$(df -Pk "$(existing_parent "$SOURCES")" | awk 'NR==2{print $1}')" == "$(df -Pk "$(existing_parent "$MODEL_DIR")" | awk 'NR==2{print $1}')" ]]; then
    log "sources and outputs share a disk: need ~$((src_gb + out_gb)) GB total"
    need_space "$MODEL_DIR" $((src_gb + out_gb)) "whole build"
  else
    need_space "$MODEL_DIR" "$out_gb" "outputs"
    [[ $src_gb -eq 0 ]] || need_space "$SOURCES" "$src_gb" "downloads"
  fi
  log "formats to build: ${FORMATS:-none}"
}

# ---- Stage: download ---------------------------------------------------------------
sources_ok() {  # all required source files present and well-formed (quiet)
  helper shards-check "$S_OMNI/transformer_ref" diffusion_pytorch_model >/dev/null 2>&1 || return 1
  local f
  for f in transformer_ref/config.json scheduler/scheduler_config.json audio_scheduler/scheduler_config.json fastvideo_inference.json; do
    [[ -s "$S_OMNI/$f" ]] || return 1; done
  helper shards-check "$S_QAD/text_encoder" model >/dev/null 2>&1 || return 1
  for f in text_encoder/config.json text_encoder/model.safetensors.index.json audio_vae/config.json audio_vae/diffusion_pytorch_model.safetensors \
           processor/tokenizer.json tokenizer/tokenizer.json; do [[ -s "$S_QAD/$f" ]] || return 1; done
  helper st-check "$S_QAD/audio_vae/diffusion_pytorch_model.safetensors" "$S_DEC/quality-36L/decoder-nvfp4.safetensors" \
    "$S_DEC/fast-8L/decoder-nvfp4.safetensors" "$S_LVAE/lightvae-lite-h3-encoder.safetensors" >/dev/null 2>&1 || return 1
}

hf_get() {  # hf_get <repo> <rev> <dir> <include...>
  local repo="$1" rev="$2" dir="$3"; shift 3
  local inc=(); local p; for p in "$@"; do inc+=(--include "$p"); done
  mkdir -p "$dir"
  log "hf download $repo @ ${rev:0:10}"
  run hf download "$repo" --revision "$rev" "${inc[@]}" --local-dir "$dir"
}

stage_download() {
  if [[ $SKIP_DOWNLOAD -eq 1 ]]; then
    log "download: skipped (--skip-download); checking $SOURCES"
    [[ $DRY_RUN -eq 1 ]] || sources_ok || die "sources under $SOURCES are incomplete (need omni-bf16/ nvfp4-qad/ vae-decoders/ lightvae/)"
    return
  fi
  if [[ $DRY_RUN -eq 0 ]] && sources_ok; then log "download: all sources present and valid, skipping"; return; fi
  need_space "$SOURCES" "$SRC_GB" "downloads"
  hf_get "$REPO_OMNI" "$REV_OMNI" "$S_OMNI" 'transformer_ref/*' 'scheduler/*' 'audio_scheduler/*' fastvideo_inference.json
  hf_get "$REPO_QAD" "$REV_QAD" "$S_QAD" 'text_encoder/*' 'processor/*' 'tokenizer/*' 'audio_vae/*'
  hf_get "$REPO_DEC" "$REV_DEC" "$S_DEC" quality-36L/decoder-nvfp4.safetensors fast-8L/decoder-nvfp4.safetensors
  hf_get "$REPO_LVAE" "$REV_LVAE" "$S_LVAE" lightvae-lite-h3-encoder.safetensors
  [[ $DRY_RUN -eq 1 ]] || sources_ok || die "downloads finished but source validation failed; re-run to resume"
}

# ---- Stage: assemble root/, vae/, licenses ---------------------------------------------
copy_file() {  # atomic, skip when identical
  local src="$1" dst="$2"
  [[ -f "$src" ]] || die "missing source file: $src"
  if [[ -f "$dst" ]] && cmp -s "$src" "$dst"; then return 0; fi
  mkdir -p "$(dirname "$dst")"
  local tmp="$dst.partial.$$"; cp "$src" "$tmp" && mv -f "$tmp" "$dst"
}
copy_dir() {  # copy_dir <src dir> <dst dir>: regular files only (skip AppleDouble / .cache)
  local src="$1" dst="$2" f
  [[ -d "$src" ]] || die "missing source dir: $src"
  while IFS= read -r f; do copy_file "$src/$f" "$dst/$f"; done \
    < <(cd "$src" && find -L . -type f ! -name '._*' ! -name '.DS_Store' ! -path './.cache/*' ! -name '*.metadata' | sed 's#^\./##' | sort)
}

stage_assemble() {
  [[ $DRY_RUN -eq 0 ]] || { log "assemble: would copy root/ configs, vae/ weights, licenses and derive root/vae/config.json"; return 0; }
  local r="$MODEL_DIR/root"
  log "assembling root/, vae/, licenses"
  copy_file "$S_OMNI/fastvideo_inference.json" "$r/fastvideo_inference.json"
  copy_dir  "$S_OMNI/scheduler" "$r/scheduler"
  copy_dir  "$S_OMNI/audio_scheduler" "$r/audio_scheduler"
  copy_file "$S_OMNI/transformer_ref/config.json" "$r/transformer_ref/config.json"
  copy_dir  "$S_QAD/audio_vae" "$r/audio_vae"
  copy_dir  "$S_QAD/processor" "$r/processor"
  copy_dir  "$S_QAD/tokenizer" "$r/tokenizer"
  copy_file "$S_DEC/quality-36L/decoder-nvfp4.safetensors" "$MODEL_DIR/vae/fasth3-quality-36L-decoder-nvfp4.safetensors"
  copy_file "$S_DEC/fast-8L/decoder-nvfp4.safetensors" "$MODEL_DIR/vae/fasth3-fast-8L-decoder-nvfp4.safetensors"
  copy_file "$S_LVAE/lightvae-lite-h3-encoder.safetensors" "$MODEL_DIR/vae/lightvae-lite-h3-encoder.safetensors"
  helper vae-config "$S_LVAE/lightvae-lite-h3-encoder.safetensors" "$r/vae/config.json"
  local l; for l in LICENSE LICENSE-Qwen3-VL NOTICE; do copy_file "$KIT_DIR/licenses/$l" "$MODEL_DIR/$l"; done
}

# ---- Stage: DiT conversion (sequential) -------------------------------------------------
convert_one() {  # convert_one <fmt>
  local fmt="$1" out="$MODEL_DIR/dit/$1" stage="$MODEL_DIR/dit/.partial-$1"
  if [[ $DRY_RUN -eq 1 ]]; then
    echo "  (dry-run) (cd $FASTVIDEO && PYTHONPATH=$FASTVIDEO $PYTHON scripts/checkpoint_conversion/convert_minimax_h3_mlx.py --model-root $S_OMNI/transformer_ref --out $stage --formats $fmt --include-vsa)  # then mv to $out"
    return 0
  fi
  if [[ -s "$out/mlx_h3_dit.json" ]] && helper st-check "$out/mlx_h3_dit.safetensors" >/dev/null 2>&1; then
    log "dit/$fmt: already converted and well-formed, skipping"; return 0
  fi
  need_space "$MODEL_DIR" "$(fmt_gb "$fmt")" "dit/$fmt conversion"
  rm -rf "$out" "$stage"   # incomplete or corrupt earlier attempt
  lock_acquire
  log "converting dit/$fmt (this takes a while)"
  (cd "$FASTVIDEO" && run env PYTHONPATH="$FASTVIDEO" "$PYTHON" scripts/checkpoint_conversion/convert_minimax_h3_mlx.py \
      --model-root "$S_OMNI/transformer_ref" --out "$stage" --formats "$fmt" --include-vsa)
  if [[ $DRY_RUN -eq 0 ]]; then
    helper st-check "$stage/$fmt/mlx_h3_dit.safetensors" || die "dit/$fmt conversion produced an invalid file"
    mkdir -p "$MODEL_DIR/dit"; mv "$stage/$fmt" "$out"; rm -rf "$stage"
  fi
  lock_release
}
stage_dit() { local f; for f in $FORMATS; do timed "dit/$f" convert_one "$f"; done; }

# ---- Stage: NVFP4 encoder cache -----------------------------------------------------------
stage_encoder() {
  local out="$MODEL_DIR/root/text_encoder"
  if [[ $DRY_RUN -eq 1 ]]; then
    echo "  (dry-run) $PYTHON -c 'export_mlx_h3_nvfp4_encoder($S_QAD/text_encoder, $out)'; then set source_dir=$ENCODER_SOURCE_FIELD"
    return 0
  fi
  if [[ -s "$out/model.safetensors" && -s "$out/mlx_h3_nvfp4_encoder.json" ]] \
     && helper st-check "$out/model.safetensors" >/dev/null 2>&1; then
    log "text_encoder cache: present and well-formed, skipping export"
  else
    need_space "$MODEL_DIR" "$ENC_GB" "encoder export"
    rm -rf "$out" "$MODEL_DIR"/root/.text_encoder.partial-*   # export refuses a non-empty target
    lock_acquire
    log "exporting NVFP4 encoder cache (format v2)"
    run env PYTHONPATH="$FASTVIDEO" "$PYTHON" -c '
import sys
from fastvideo.mlx_runtime.minimax_h3_conditioner import export_mlx_h3_nvfp4_encoder
export_mlx_h3_nvfp4_encoder(sys.argv[1], sys.argv[2])' "$S_QAD/text_encoder" "$out"
    lock_release
  fi
  # Make the manifest independent of where sources lived (same edit as our reference build).
  [[ $DRY_RUN -eq 1 ]] || helper rewrite-source "$out/mlx_h3_nvfp4_encoder.json" "$ENCODER_SOURCE_FIELD"
}

# ---- Stage: verify ----------------------------------------------------------------------------
stage_verify() {
  [[ $DRY_RUN -eq 0 ]] || { log "verify: skipped in dry-run"; return 0; }
  local exp; exp="$(mktemp)"; printf '%s\n' "$EXPECTED_SHA256" > "$exp"
  log "verifying SHA-256 against the reference build"
  local rc=0; helper verify "$MODEL_DIR" "$exp" $FORMATS || rc=$?
  rm -f "$exp"; return $rc
}

# ---- Main -------------------------------------------------------------------------------------------
log "model dir: $MODEL_DIR | sources: $SOURCES | python: $PYTHON | formats: $FORMATS"
preflight
timed download stage_download
timed assemble stage_assemble
stage_dit
timed encoder stage_encoder
VERIFY_RC=0; timed verify stage_verify || VERIFY_RC=$?

if [[ $VERIFY_RC -eq 0 && $SKIP_DOWNLOAD -eq 0 && $KEEP_SOURCES -eq 0 && $DRY_RUN -eq 0 ]]; then
  log "removing downloaded sources $SOURCES (use --keep-sources to retain)"; rm -rf "$SOURCES"
fi

echo; echo "================ SUMMARY ================"
printf 'timings:\n'; printf '  %s\n' ${TIMINGS[@]+"${TIMINGS[@]}"}
echo "model dir: $MODEL_DIR"
(( ${#WARNINGS[@]} == 0 )) || { echo "warnings:"; printf '  - %s\n' "${WARNINGS[@]}"; }
if [[ $DRY_RUN -eq 1 ]]; then echo "dry-run: nothing was downloaded or converted"; exit 0; fi
if [[ $VERIFY_RC -ne 0 ]]; then
  echo "RESULT: VERIFICATION FAILED. Downloaded-file hash mismatches mean a corrupt/changed download:"
  echo "delete the named files under $SOURCES and $MODEL_DIR and re-run."; exit 1
fi
echo "RESULT: OK"
FIRST_FMT="${FORMATS%% *}"
cat <<EOF

Next, from $KIT_DIR (use the FastVideo venv python):
  $PYTHON run_matrix.py --self-test --data-root ~/omniref-handoff
  $PYTHON run_matrix.py --smoke --model-dir "$MODEL_DIR" --fastvideo-root "$FASTVIDEO" --data-root ~/omniref-handoff
  $PYTHON run_matrix.py --model-dir "$MODEL_DIR" --fastvideo-root "$FASTVIDEO" --data-root ~/omniref-handoff --out ./results-mystudio
(DiT format used by default: ${FIRST_FMT:-int6}; see run_matrix.py --help for selecting int8/bf16.)
EOF