1 #!/usr/bin/env bash
2 # =============================================================================
3 # onde/crates/onde-wasm/build.sh
4 #
5 # One-command wasm-pack build for the onde WASM Whisper engine.
6 #
7 # Prerequisites
8 # -------------
9 # • Rust with the wasm32-unknown-unknown target:
10 # rustup target add wasm32-unknown-unknown
11 #
12 # • wasm-pack:
13 # cargo install wasm-pack
14 #
15 # • (Optional) wasm-opt for smaller binaries — installed automatically by
16 # wasm-pack if missing, but a pre-installed version is faster.
17 #
18 # Usage
19 # -----
20 # # Standard release build
21 # ./build.sh
22 #
23 # # Debug build (faster compile, larger .wasm, source-mapped)
24 # BUILD_TYPE=debug ./build.sh
25 #
26 # # Override the output directory (default: onde/crates/onde-wasm/pkg)
27 # OUT_DIR=/path/to/output ./build.sh
28 #
29 # Output
30 # ------
31 # pkg/
32 # onde_wasm.js — ES module glue
33 # onde_wasm_bg.wasm — compiled WASM binary
34 # onde_wasm.d.ts — TypeScript declarations for WhisperDecoder
35 # onde_wasm_bg.wasm.d.ts
36 # package.json
37 #
38 # Model assets (download separately — see section below)
39 # ------------------------------------------------------
40 # The WhisperDecoder constructor requires four byte arrays that are NOT
41 # bundled into the .wasm binary. The JS consumer must fetch them from
42 # HuggingFace and pass them as Uint8Arrays.
43 #
44 # Recommended model: whisper-tiny.en (non-quantized, ~75 MB total)
45 #
46 # WEIGHTS https://huggingface.co/openai/whisper-tiny.en/resolve/main/model.safetensors
47 # TOKENIZER https://huggingface.co/openai/whisper-tiny.en/resolve/main/tokenizer.json
48 # CONFIG https://huggingface.co/openai/whisper-tiny.en/resolve/main/config.json
49 # MEL https://huggingface.co/spaces/lmz/candle-whisper/resolve/main/mel_filters.safetensors
50 #
51 # Recommended model: whisper-tiny (multilingual, ~75 MB total)
52 #
53 # WEIGHTS https://huggingface.co/openai/whisper-tiny/resolve/main/model.safetensors
54 # TOKENIZER https://huggingface.co/openai/whisper-tiny/resolve/main/tokenizer.json
55 # CONFIG https://huggingface.co/openai/whisper-tiny/resolve/main/config.json
56 # MEL https://huggingface.co/spaces/lmz/candle-whisper/resolve/main/mel_filters.safetensors
57 #
58 # Quantized model: whisper-tiny.en Q8_0 (~42 MB, faster load)
59 #
60 # WEIGHTS https://huggingface.co/lmz/candle-whisper/resolve/main/model-tiny-en-q80.gguf
61 # TOKENIZER https://huggingface.co/lmz/candle-whisper/resolve/main/tokenizer-tiny-en.json
62 # CONFIG https://huggingface.co/lmz/candle-whisper/resolve/main/config-tiny-en.json
63 # MEL https://huggingface.co/spaces/lmz/candle-whisper/resolve/main/mel_filters.safetensors
64 #
65 # Run the download helper:
66 # ./build.sh --download-assets
67 #
68 # Vite integration (karokowe-connected-devices)
69 # ---------------------------------------------
70 # 1. Copy pkg/ into your project, e.g.:
71 # cp -r pkg/ frontend/karokowe-connected-devices/src/wasm/onde/
72 #
73 # 2. Add to vite.config.ts:
74 # import { defineConfig } from "vite";
75 # export default defineConfig({
76 # optimizeDeps: { exclude: ["onde-wasm"] },
77 # server: {
78 # headers: {
79 # "Cross-Origin-Opener-Policy": "same-origin",
80 # "Cross-Origin-Embedder-Policy": "require-corp",
81 # },
82 # },
83 # });
84 #
85 # NOTE: The COOP/COEP headers are NOT required by this pure-Rust WASM
86 # build (no SharedArrayBuffer / pthreads are used — inference runs
87 # synchronously on the JS thread). They are shown here for completeness
88 # if you later add a Web Worker wrapper.
89 #
90 # 3. In your Worker or module:
91 # import init, { WhisperDecoder } from "./onde_wasm.js";
92 # await init();
93 # const decoder = new WhisperDecoder(
94 # weightsBytes, tokenizerBytes, melFiltersBytes, configBytes,
95 # false, // quantized
96 # false, // is_multilingual
97 # true, // timestamps
98 # null, // task
99 # null, // language
100 # );
101 # const json = decoder.decode(wavBytes);
102 # const result = JSON.parse(json);
103 # // result.text — full transcript
104 # // result.segments[] — [{ start, end, text }]
105 #
106 # =============================================================================
107
108 set -euo pipefail
109
110 # ── Script location ───────────────────────────────────────────────────────────
111 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
112
113 # ── Parameters ─────────────────────────────────────────────────────────────────
114 BUILD_TYPE="${BUILD_TYPE:-release}" # "release" or "debug"
115 OUT_DIR="${OUT_DIR:-${SCRIPT_DIR}/pkg}"
116
117 # ── Colours ───────────────────────────────────────────────────────────────────
118 if [ -t 1 ]; then
119 RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
120 CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m'
121 else
122 RED=''; GREEN=''; YELLOW=''; CYAN=''; BOLD=''; RESET=''
123 fi
124
125 log_info() { echo -e "${CYAN}[onde-wasm]${RESET} $*"; }
126 log_ok() { echo -e "${GREEN}[onde-wasm]${RESET} $*"; }
127 log_warn() { echo -e "${YELLOW}[onde-wasm]${RESET} $*"; }
128 log_error() { echo -e "${RED}[onde-wasm] ERROR:${RESET} $*" >&2; }
129 log_section() { echo -e "\n${BOLD}── $* ─────────────────────────────────${RESET}"; }
130
131 # ── --download-assets helper ───────────────────────────────────────────────────
132 download_assets() {
133 log_section "Downloading model assets"
134
135 ASSETS_DIR="${SCRIPT_DIR}/assets"
136 mkdir -p "${ASSETS_DIR}"
137
138 declare -A URLS=(
139 ["model.safetensors"]="https://huggingface.co/openai/whisper-tiny.en/resolve/main/model.safetensors"
140 ["tokenizer.json"]="https://huggingface.co/openai/whisper-tiny.en/resolve/main/tokenizer.json"
141 ["config.json"]="https://huggingface.co/openai/whisper-tiny.en/resolve/main/config.json"
142 ["mel_filters.safetensors"]="https://huggingface.co/spaces/lmz/candle-whisper/resolve/main/mel_filters.safetensors"
143 )
144
145 for fname in "${!URLS[@]}"; do
146 dest="${ASSETS_DIR}/${fname}"
147 if [ -f "${dest}" ]; then
148 log_info " ${fname} — already present, skipping"
149 continue
150 fi
151 log_info " Downloading ${fname}…"
152 curl -fL --progress-bar -o "${dest}" "${URLS[$fname]}"
153 log_ok " ${fname} saved ($(du -sh "${dest}" | cut -f1))"
154 done
155
156 log_ok "All assets in ${ASSETS_DIR}/"
157 echo
158 echo " Pass these to WhisperDecoder in JS:"
159 echo " weights: ${ASSETS_DIR}/model.safetensors"
160 echo " tokenizer: ${ASSETS_DIR}/tokenizer.json"
161 echo " config: ${ASSETS_DIR}/config.json"
162 echo " mel_filters: ${ASSETS_DIR}/mel_filters.safetensors"
163 exit 0
164 }
165
166 if [[ "${1:-}" == "--download-assets" ]]; then
167 download_assets
168 fi
169
170 # ── Preflight ──────────────────────────────────────────────────────────────────
171 log_section "Preflight"
172
173 if ! command -v wasm-pack &>/dev/null; then
174 log_error "wasm-pack not found."
175 echo
176 echo " Install with:"
177 echo " cargo install wasm-pack"
178 echo
179 exit 1
180 fi
181
182 WASM_PACK_VERSION=$(wasm-pack --version)
183 log_info "wasm-pack: ${WASM_PACK_VERSION}"
184
185 if ! rustup target list --installed | grep -q "wasm32-unknown-unknown"; then
186 log_warn "wasm32-unknown-unknown target not installed — installing now…"
187 rustup target add wasm32-unknown-unknown
188 fi
189
190 log_info "Build type : ${BUILD_TYPE}"
191 log_info "Output dir : ${OUT_DIR}"
192
193 # ── Build ──────────────────────────────────────────────────────────────────────
194 log_section "Build"
195
196 WASM_PACK_PROFILE_FLAG="--release"
197 if [[ "${BUILD_TYPE}" == "debug" ]]; then
198 WASM_PACK_PROFILE_FLAG="--dev"
199 fi
200
201 wasm-pack build \
202 --target web \
203 --out-dir "${OUT_DIR}" \
204 ${WASM_PACK_PROFILE_FLAG} \
205 "${SCRIPT_DIR}"
206
207 log_ok "wasm-pack build complete."
208
209 # ── Post-process: fix package.json name ───────────────────────────────────────
210 # wasm-pack emits the crate name as the package name; rename to onde-wasm
211 # so the Vite import path is predictable.
212 PKG_JSON="${OUT_DIR}/package.json"
213 if [ -f "${PKG_JSON}" ]; then
214 # Use sed for portable in-place replacement (BSD and GNU sed both support this).
215 sed -i.bak 's/"name": "onde_wasm"/"name": "onde-wasm"/' "${PKG_JSON}"
216 rm -f "${PKG_JSON}.bak"
217 log_info "package.json name → onde-wasm"
218 fi
219
220 # ── Summary ───────────────────────────────────────────────────────────────────
221 log_section "Output"
222
223 echo
224 log_ok "onde-wasm build succeeded!"
225 echo
226 echo " Artefacts:"
227 ls -lh "${OUT_DIR}/"*.{js,wasm,ts} 2>/dev/null \
228 | grep -v '^total' \
229 | awk '{printf " %-40s %s\n", $9, $5}' \
230 || true
231 echo
232 echo " Next steps:"
233 echo
234 echo " 1. Download model assets (first time only):"
235 echo " ${BASH_SOURCE[0]} --download-assets"
236 echo
237 echo " 2. Copy pkg/ into your Vite project:"
238 echo " cp -r ${OUT_DIR}/. frontend/karokowe-connected-devices/src/wasm/onde/"
239 echo
240 echo " 3. Import in a Web Worker:"
241 echo " import init, { WhisperDecoder } from \"./onde_wasm.js\";"
242 echo " await init();"
243 echo " const decoder = new WhisperDecoder("
244 echo " weightsBytes, tokenizerBytes, melFiltersBytes, configBytes,"
245 echo " false, false, true, null, null"
246 echo " );"
247 echo " const result = JSON.parse(decoder.decode(wavBytes));"
248 echo