Browser-Based AI: Lessons from Shipping Three Local Tools

LazySage Advanced 4h ago Updated Jul 25, 2026 128 views 9 likes 3 min read

Running AI entirely on the client side is a brutal filter for development. I set a strict rule for my side project: no files ever touch a server. If it couldn't be handled client-side, it didn't ship. This constraint led to the creation of WeConvertIt, where background removal, music stem separation, and speech-to-text all run locally.

The process revealed several critical failure points that aren't documented in standard tutorials—specifically regarding model integrity and runtime bugs.

Detecting Model Corruption via Physics Violations

My stem splitter (vocal/instrumental separation) uses Meta's HTDemucs. I initially used a third-party ONNX export and wired it up via onnxruntime-web, but the output was robotic garbage with vocals bleeding into the instrumental track.

I spent hours chasing the wrong ghosts. I suspected my FP16 conversion for smaller download sizes, but comparing FP16 and FP32 natively showed they were numerically identical to four significant figures. I checked the sample rate (44.1kHz stereo), and the logs confirmed the data reaching the model was correct.

To isolate the issue, I reimplemented the _spec() and _ispec() (STFT and inverse STFT) from the PyTorch source in JavaScript. I compared the outputs against the original PyTorch implementation using identical inputs. Every probed value matched to eight decimal places. My pipeline was perfect; the model was the problem.

I ran a full-band song through both the official PyTorch checkpoint and my ONNX model. The results were physically impossible:

  • Vocals @ 20–60Hz: Reference (PyTorch) 4.8% of mix vs. ONNX model 69.3%
  • Vocals @ 2–6kHz: Reference (PyTorch) 0.6% of mix vs. ONNX model 75.4%
  • Bass @ sub-bass: Reference (PyTorch) 0% vs. ONNX model 145.2%
  • Other @ sub-bass: Reference (PyTorch) 0% vs. ONNX model 174.7%

You cannot extract more energy from a signal than existed in the original mix. Seeing values at 145% and 174% magnitude was the smoking gun: the model file itself was defective.

The fix was to stop relying on third-party conversions and export my own ONNX from the official pretrained checkpoint. This process is notoriously finicky due to custom STFT op handling. After validating the new export, I hit a 0.987/0.999 correlation with the reference.

Pro Tip: When a model produces "bad" results, don't just rely on subjective quality. Look for a metric that is physically impossible. It's a much faster signal than "it sounds off."

The ONNX Runtime Graph Optimizer Trap

For speech-to-text, I used Whisper via transformers.js. I hit a wall during session creation with a cryptic error:

TransposeDQWeightsForMatMulNBits Missing required scale ... model.decoder.embed_tokens.weight_transposed_DequantizeLinear

At first glance, this looked like a quantization error. MatMulNBits is a 4-bit operator, but I was using a standard uint8 build. I searched the binary for MatMulNBits strings—there were none.

The culprit wasn't the model file, but the ONNX Runtime graph optimizer. It was creating the 4-bit op on the fly during session creation because the model used tied embeddings (where the encoder and decoder share the same weight matrix). The optimizer tried to optimize the transpose operation for these tied weights but failed to provide the required scale parameter.

To fix this, I had to explicitly disable the specific optimization that was triggering the bug. In my onnxruntime-web configuration, I passed the following session options:

const sessionOptions = {
  graphOptimizationLevel: 'basic', // Avoid 'extended' or 'all' to skip the buggy optimizer pass
  executionMode: 'sequential',
};

const session = await ort.InferenceSession.create('/path/to/whisper.onnx', sessionOptions);

By dropping the optimization level from all to basic, the runtime stopped attempting the faulty weight transposition, and the model loaded instantly.

Memory Ceilings and Large File Crashes

The final hurdle was the browser's memory limit. While small files worked perfectly, long audio files triggered browser crashes or "Out of Memory" (OOM) errors. This is because onnxruntime-web often allocates large contiguous buffers for tensors.

I discovered that the memory ceiling isn't just about the total RAM available, but about how the browser manages the WASM heap. For long-form audio, the STFT transforms create massive intermediate tensors that spike memory usage far beyond the final output size.

To solve this, I implemented a sliding window processing strategy:

1. Split the audio into 30-second chunks.
2. Process each chunk through the model independently.
3. Apply a small cross-fade (approx 100ms) at the boundaries to prevent audible clicking.
4. Reassemble the processed chunks in a Float32Array before final export.

This kept the peak memory usage constant regardless of the file length, turning a tool that crashed at 5 minutes into one that could handle hour-long recordings.

AI ProgrammingAI Codingmachinelearningwebdevjavascript

All Replies (3)

A
Alex17 Advanced 12h ago
Did something similar with a local PDF parser. The memory leak issues in the browser are a nightmare compared to backend.
0 Reply
N
NeonPanda Intermediate 12h ago
Tried this with a small image classifier. Optimizing the model size was the hardest part just to get it to load under 5 seconds.
0 Reply
N
NightPanda Expert 12h ago
Did you try quantization or pruning? I found that cutting the weights made a huge difference in load times.
0 Reply

Write a Reply

Markdown supported