<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: MartinDelophy</title>
    <description>The latest articles on DEV Community by MartinDelophy (@martindelophy).</description>
    <link>https://dev.to/martindelophy</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4029567%2Fc8d453a4-d3ea-4275-b7ce-fbece2440c21.png</url>
      <title>DEV Community: MartinDelophy</title>
      <link>https://dev.to/martindelophy</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/martindelophy"/>
    <language>en</language>
    <item>
      <title>I Built a Local-First AI Video Editor That Runs in the Browser</title>
      <dc:creator>MartinDelophy</dc:creator>
      <pubDate>Thu, 16 Jul 2026 10:11:39 +0000</pubDate>
      <link>https://dev.to/martindelophy/i-built-a-local-first-ai-video-editor-that-runs-in-the-browser-56eb</link>
      <guid>https://dev.to/martindelophy/i-built-a-local-first-ai-video-editor-that-runs-in-the-browser-56eb</guid>
      <description>&lt;p&gt;What if an AI video editor did not need to upload your media to a server before it could do anything useful?&lt;/p&gt;

&lt;p&gt;That question became &lt;strong&gt;Timeline Studio&lt;/strong&gt;, an open-source, local-first video editor built with React. It generates voiceovers, transcribes audio, analyzes subjects, creates talking portraits, and exports a finished video — with the core AI workflows running directly in the browser.&lt;/p&gt;

&lt;p&gt;You can &lt;a href="https://video-editor.ai-creator.top/" rel="noopener noreferrer"&gt;try the live demo&lt;/a&gt;, browse the &lt;a href="https://github.com/MartinDelophy/ai-video-editor" rel="noopener noreferrer"&gt;source code on GitHub&lt;/a&gt;, or watch the &lt;a href="https://youtu.be/mUXduGpBmwE" rel="noopener noreferrer"&gt;demo video on YouTube&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg8yvsav37hv955vfd8dj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg8yvsav37hv955vfd8dj.png" alt="Timeline Studio editor" width="800" height="569"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This post is not a claim that browsers have replaced desktop editors. They have not. It is a practical account of what worked, what became unexpectedly difficult, and what I learned while moving a surprisingly large AI media pipeline to the client.&lt;/p&gt;

&lt;h2&gt;
  
  
  The product idea
&lt;/h2&gt;

&lt;p&gt;The first version started as a simple image-to-voiceover tool. A user could upload an image, enter a script, generate speech, preview the result, and export a video.&lt;/p&gt;

&lt;p&gt;But a useful editor needs more than a happy-path generation button. Users need to move clips, overlap narration, edit captions, keep audio and text synchronized, add music, recover from mistakes, and understand what the exporter will actually produce.&lt;/p&gt;

&lt;p&gt;That pushed the project toward a CapCut-like desktop workspace with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;image, video, sticker, caption, source-audio, voiceover, and music tracks;&lt;/li&gt;
&lt;li&gt;clip splitting, resizing, moving, duplicating, locking, hiding, and deletion;&lt;/li&gt;
&lt;li&gt;automatic lanes for overlapping voiceovers and captions;&lt;/li&gt;
&lt;li&gt;waveform-aware captions and a zoomable timeline;&lt;/li&gt;
&lt;li&gt;browser-side MP4/WebM export;&lt;/li&gt;
&lt;li&gt;installable PWA behavior and persistent model caching.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important architectural decision was to make the timeline the source of truth. AI features do not produce isolated downloads; they create or update editor assets and timeline clips.&lt;/p&gt;

&lt;h2&gt;
  
  
  A browser AI stack with different runtimes
&lt;/h2&gt;

&lt;p&gt;There is no single model or runtime that handles every part of this application well. Timeline Studio uses several specialized paths:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Model/runtime&lt;/th&gt;
&lt;th&gt;Execution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Chinese and multilingual TTS&lt;/td&gt;
&lt;td&gt;Piper/VITS ONNX&lt;/td&gt;
&lt;td&gt;WASM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;English TTS&lt;/td&gt;
&lt;td&gt;Kokoro 82M v1.0 q8&lt;/td&gt;
&lt;td&gt;WASM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Automatic captions&lt;/td&gt;
&lt;td&gt;Whisper small q8&lt;/td&gt;
&lt;td&gt;WASM worker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Object detection&lt;/td&gt;
&lt;td&gt;YOLOS tiny q8&lt;/td&gt;
&lt;td&gt;WASM worker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Portrait matting&lt;/td&gt;
&lt;td&gt;MODNet q8&lt;/td&gt;
&lt;td&gt;WASM worker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audio-to-face motion&lt;/td&gt;
&lt;td&gt;JoyVASA ONNX&lt;/td&gt;
&lt;td&gt;Browser worker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Neural portrait rendering&lt;/td&gt;
&lt;td&gt;LivePortrait ONNX&lt;/td&gt;
&lt;td&gt;WebGPU&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Models are lazy-loaded. Opening the editor should not immediately download every neural network it might eventually use.&lt;/p&gt;

&lt;p&gt;For example, the English speech path imports Kokoro only when the selected voice requires it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;KokoroTTS&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;kokoro-js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;KokoroTTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;onnx-community/Kokoro-82M-v1.0-ONNX&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;dtype&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;q8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;device&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wasm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;progress_callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reportProgress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;audio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;voice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;speed&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Piper handles the multilingual voice catalog, while Chinese text goes through a dedicated pinyin-aware path. A key product rule is that a failed voice must fail visibly. Silently falling back to a different speaker would make the UI appear successful while producing the wrong result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workers are not optional
&lt;/h2&gt;

&lt;p&gt;Model inference, audio decoding, frame analysis, and export are all capable of freezing the main thread. Moving the work into Web Workers was necessary for progress reporting and editor responsiveness.&lt;/p&gt;

&lt;p&gt;The vision worker keeps model instances alive after their first load and serializes analysis requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;detectorPromise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;backgroundRemoverPromise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;analysisQueue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getDetector&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;detectorPromise&lt;/span&gt; &lt;span class="o"&gt;??=&lt;/span&gt; &lt;span class="nf"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object-detection&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Xenova/yolos-tiny&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;device&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wasm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;dtype&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;q8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;detectorPromise&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keeping a session warm makes repeated actions much faster. Serializing selected workloads also avoids memory spikes and execution-provider conflicts — especially important when several large ONNX graphs compete for browser resources.&lt;/p&gt;

&lt;h2&gt;
  
  
  Video vision has to be temporal
&lt;/h2&gt;

&lt;p&gt;Running subject detection on the first frame of a video is easy. It is also wrong as soon as the subject moves.&lt;/p&gt;

&lt;p&gt;Timeline Studio pre-analyzes a video at adaptive intervals and stores timestamped YOLOS geometry and MODNet masks. Preview, smart crop, caption avoidance, background removal, and export all resolve the analysis record for the current source time.&lt;/p&gt;

&lt;p&gt;This creates a reusable temporal track:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;video time ──► 0.0s ───── 0.8s ───── 1.6s ───── 2.4s
subject box    A           B           C           D
matte mask     A'          B'          C'          D'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Longer videos use wider sample intervals to bound inference time and memory. It is a compromise, but it is much more honest than freezing a first-frame result across the entire clip.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rewriting LivePortrait for WebGPU
&lt;/h2&gt;

&lt;p&gt;The talking-avatar pipeline was the hardest part of the project.&lt;/p&gt;

&lt;p&gt;JoyVASA converts voice audio into motion. LivePortrait then applies that motion to a source portrait through appearance extraction, motion extraction, lip retargeting, stitching, warping, and SPADE generation.&lt;/p&gt;

&lt;p&gt;The available combined generator contained rank-5 operations and two 5D &lt;code&gt;GridSample&lt;/code&gt; nodes. Its native Linux plugin could not run in a browser, and the graph was not directly compatible with ONNX Runtime Web's WebGPU execution provider.&lt;/p&gt;

&lt;p&gt;I rewrote the 5D sampling as batched 4D &lt;code&gt;GridSample&lt;/code&gt; operations plus linear interpolation along depth. The rewritten graph grew from 277 to 316 nodes, but it could execute in the browser. Comparing the full 512×512 output against the original graph produced:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;maximum absolute error: &lt;code&gt;7.8976e-6&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;mean absolute error: &lt;code&gt;1.3853e-7&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;99.9th-percentile error: &lt;code&gt;1.7881e-6&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those numbers mattered. “It looks close” is not a sufficient validation strategy for replacing a core neural-network operator.&lt;/p&gt;

&lt;p&gt;The browser pipeline now offers a mixed-FP16 256px preview tier and a mixed-FP16 512px quality tier. It renders sparse neural keyframes, interpolates them for encoding, and keeps the reusable appearance feature on the GPU between frames.&lt;/p&gt;

&lt;p&gt;It is still not real-time. On the hardware used for validation, 512px neural frames remain expensive. The UI therefore reports real progress rather than pretending a slow operation is instant.&lt;/p&gt;

&lt;p&gt;The large avatar bundle also does not live in the Git repository. It is split into 50 MB parts and served from an immutable, revision-pinned &lt;a href="https://huggingface.co/haixin/timeline-studio-onnx-models/tree/a201b681c8f96672b5c3f624e32d4dc932f150af" rel="noopener noreferrer"&gt;Hugging Face model repository&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automatic captions need more than transcription
&lt;/h2&gt;

&lt;p&gt;Whisper returns text and timestamps, but subtitle quality inside an editor also depends on alignment.&lt;/p&gt;

&lt;p&gt;The current caption pipeline uses Whisper small q8 in a WASM worker. Tiny models were faster, but Chinese tests produced unstable hallucinations. WebGPU was also less stable for this workload, so captions deliberately stay on WASM until that path is revalidated.&lt;/p&gt;

&lt;p&gt;After transcription, segment boundaries are nudged toward nearby source-audio energy. This makes subtitle bars line up with the waveform instead of blindly trusting coarse chunk timestamps.&lt;/p&gt;

&lt;p&gt;Chinese cleanup is conservative and context-aware. Broad rewriting would make subtitles read more smoothly while risking changes to what the speaker actually said — a bad trade in an automatic transcription path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the timeline feel like an editor
&lt;/h2&gt;

&lt;p&gt;AI demos often focus on inference and underestimate interaction design. In practice, much of the work went into ordinary editor behavior:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;inserting a generated voiceover at the playhead rather than replacing the previous result;&lt;/li&gt;
&lt;li&gt;moving linked captions with their voiceover;&lt;/li&gt;
&lt;li&gt;creating extra lanes when clips overlap and compacting them afterward;&lt;/li&gt;
&lt;li&gt;keeping clip geometry time-accurate at every zoom level;&lt;/li&gt;
&lt;li&gt;using transparent hit targets for very short clips instead of visually stretching them;&lt;/li&gt;
&lt;li&gt;making Delete and Backspace clip-scoped without firing while the user edits a text field;&lt;/li&gt;
&lt;li&gt;keeping preview and export subtitle layout WYSIWYG.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Timeline zoom is based on visible duration rather than an arbitrary CSS scale. At the widest level it can show a long project; at the closest level it reaches frame-oriented ruler ticks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getTimelineTrackWidthPercent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;zoom&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;visibleDuration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getTimelineVisibleDuration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;zoom&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;visibleDuration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps a five-second clip representing five seconds no matter how far the user zooms in or out.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F035nhyof64ewb35ul2c5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F035nhyof64ewb35ul2c5.png" alt="Voiceover and captions aligned on the timeline" width="800" height="569"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Caching models without hiding storage costs
&lt;/h2&gt;

&lt;p&gt;Repeatedly downloading hundreds of megabytes makes a browser AI application unusable. Timeline Studio registers a service worker that uses cache-first behavior for model assets and network-first behavior for the app shell.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;cacheFirst&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MODEL_CACHE_NAME&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Model URLs are revision-pinned so a cached graph and its runtime assumptions do not silently drift apart. The app can also clear the model cache when browser storage becomes tight.&lt;/p&gt;

&lt;p&gt;Local-first does not mean “free.” It exchanges server cost and upload latency for model downloads, device storage, memory pressure, and hardware variability. Those costs need to be visible in progress UI and error messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Export is another rendering engine
&lt;/h2&gt;

&lt;p&gt;The preview canvas and exported video must agree about crop geometry, captions, stickers, filters, transitions, and current timeline time.&lt;/p&gt;

&lt;p&gt;Export composes the visual canvas and audio tracks in the browser, records a supported native format, and uses FFmpeg WASM only when MP4 transcoding is needed. If MP4 conversion fails, the editor preserves the successfully rendered WebM instead of discarding the user's work.&lt;/p&gt;

&lt;p&gt;This fallback hierarchy turned out to be important because browser codec support varies considerably.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnowgifovutatdm63qrzx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnowgifovutatdm63qrzx.png" alt="Browser export progress" width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;The largest lesson was that browser AI is a systems problem, not just a model-loading problem.&lt;/p&gt;

&lt;p&gt;You have to design model selection, execution providers, workers, caching, memory lifetime, timeline semantics, rendering, codecs, and failure states as one product. A fast model with poor orchestration still creates a slow editor. An accurate model without temporal data can create visually incorrect output. A successful inference that is not inserted cleanly into the timeline is still a poor editing experience.&lt;/p&gt;

&lt;p&gt;The browser is already capable of much more local media intelligence than many applications use today. But the best results come from acknowledging its constraints: lazy-load aggressively, cache carefully, keep heavy work off the main thread, validate graph rewrites numerically, and never hide a degraded result behind a “success” message.&lt;/p&gt;

&lt;p&gt;Timeline Studio is still evolving, and contributions are welcome. If you want to experiment with local-first creative tooling, check out the &lt;a href="https://github.com/MartinDelophy/ai-video-editor" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;, try the &lt;a href="https://video-editor.ai-creator.top/" rel="noopener noreferrer"&gt;live editor&lt;/a&gt;, and let me know which part of the browser AI stack you would like to see explored next.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>react</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Built a Local-First AI Video Editor That Runs in the Browser</title>
      <dc:creator>MartinDelophy</dc:creator>
      <pubDate>Wed, 15 Jul 2026 04:52:13 +0000</pubDate>
      <link>https://dev.to/martindelophy/i-built-a-local-first-ai-video-editor-that-runs-in-the-browser-3844</link>
      <guid>https://dev.to/martindelophy/i-built-a-local-first-ai-video-editor-that-runs-in-the-browser-3844</guid>
      <description>&lt;p&gt;What if an AI video editor did not need to upload your media to a server before it could do anything useful?&lt;/p&gt;

&lt;p&gt;That question became &lt;strong&gt;Timeline Studio&lt;/strong&gt;, an open-source, local-first video editor built with React. It generates voiceovers, transcribes audio, analyzes subjects, creates talking portraits, and exports a finished video — with the core AI workflows running directly in the browser.&lt;/p&gt;

&lt;p&gt;You can &lt;a href="https://video-editor.ai-creator.top/" rel="noopener noreferrer"&gt;try the live demo&lt;/a&gt;, browse the &lt;a href="https://github.com/MartinDelophy/ai-video-editor" rel="noopener noreferrer"&gt;source code on GitHub&lt;/a&gt;, or watch the &lt;a href="https://youtu.be/mUXduGpBmwE" rel="noopener noreferrer"&gt;demo video on YouTube&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg8yvsav37hv955vfd8dj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg8yvsav37hv955vfd8dj.png" alt="Timeline Studio editor" width="800" height="569"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This post is not a claim that browsers have replaced desktop editors. They have not. It is a practical account of what worked, what became unexpectedly difficult, and what I learned while moving a surprisingly large AI media pipeline to the client.&lt;/p&gt;

&lt;h2&gt;
  
  
  The product idea
&lt;/h2&gt;

&lt;p&gt;The first version started as a simple image-to-voiceover tool. A user could upload an image, enter a script, generate speech, preview the result, and export a video.&lt;/p&gt;

&lt;p&gt;But a useful editor needs more than a happy-path generation button. Users need to move clips, overlap narration, edit captions, keep audio and text synchronized, add music, recover from mistakes, and understand what the exporter will actually produce.&lt;/p&gt;

&lt;p&gt;That pushed the project toward a CapCut-like desktop workspace with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;image, video, sticker, caption, source-audio, voiceover, and music tracks;&lt;/li&gt;
&lt;li&gt;clip splitting, resizing, moving, duplicating, locking, hiding, and deletion;&lt;/li&gt;
&lt;li&gt;automatic lanes for overlapping voiceovers and captions;&lt;/li&gt;
&lt;li&gt;waveform-aware captions and a zoomable timeline;&lt;/li&gt;
&lt;li&gt;browser-side MP4/WebM export;&lt;/li&gt;
&lt;li&gt;installable PWA behavior and persistent model caching.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important architectural decision was to make the timeline the source of truth. AI features do not produce isolated downloads; they create or update editor assets and timeline clips.&lt;/p&gt;

&lt;h2&gt;
  
  
  A browser AI stack with different runtimes
&lt;/h2&gt;

&lt;p&gt;There is no single model or runtime that handles every part of this application well. Timeline Studio uses several specialized paths:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Model/runtime&lt;/th&gt;
&lt;th&gt;Execution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Chinese and multilingual TTS&lt;/td&gt;
&lt;td&gt;Piper/VITS ONNX&lt;/td&gt;
&lt;td&gt;WASM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;English TTS&lt;/td&gt;
&lt;td&gt;Kokoro 82M v1.0 q8&lt;/td&gt;
&lt;td&gt;WASM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Automatic captions&lt;/td&gt;
&lt;td&gt;Whisper small q8&lt;/td&gt;
&lt;td&gt;WASM worker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Object detection&lt;/td&gt;
&lt;td&gt;YOLOS tiny q8&lt;/td&gt;
&lt;td&gt;WASM worker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Portrait matting&lt;/td&gt;
&lt;td&gt;MODNet q8&lt;/td&gt;
&lt;td&gt;WASM worker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audio-to-face motion&lt;/td&gt;
&lt;td&gt;JoyVASA ONNX&lt;/td&gt;
&lt;td&gt;Browser worker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Neural portrait rendering&lt;/td&gt;
&lt;td&gt;LivePortrait ONNX&lt;/td&gt;
&lt;td&gt;WebGPU&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Models are lazy-loaded. Opening the editor should not immediately download every neural network it might eventually use.&lt;/p&gt;

&lt;p&gt;For example, the English speech path imports Kokoro only when the selected voice requires it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;KokoroTTS&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;kokoro-js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;KokoroTTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;onnx-community/Kokoro-82M-v1.0-ONNX&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;dtype&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;q8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;device&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wasm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;progress_callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reportProgress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;audio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;voice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;speed&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Piper handles the multilingual voice catalog, while Chinese text goes through a dedicated pinyin-aware path. A key product rule is that a failed voice must fail visibly. Silently falling back to a different speaker would make the UI appear successful while producing the wrong result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workers are not optional
&lt;/h2&gt;

&lt;p&gt;Model inference, audio decoding, frame analysis, and export are all capable of freezing the main thread. Moving the work into Web Workers was necessary for progress reporting and editor responsiveness.&lt;/p&gt;

&lt;p&gt;The vision worker keeps model instances alive after their first load and serializes analysis requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;detectorPromise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;backgroundRemoverPromise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;analysisQueue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getDetector&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;detectorPromise&lt;/span&gt; &lt;span class="o"&gt;??=&lt;/span&gt; &lt;span class="nf"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object-detection&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Xenova/yolos-tiny&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;device&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wasm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;dtype&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;q8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;detectorPromise&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keeping a session warm makes repeated actions much faster. Serializing selected workloads also avoids memory spikes and execution-provider conflicts — especially important when several large ONNX graphs compete for browser resources.&lt;/p&gt;

&lt;h2&gt;
  
  
  Video vision has to be temporal
&lt;/h2&gt;

&lt;p&gt;Running subject detection on the first frame of a video is easy. It is also wrong as soon as the subject moves.&lt;/p&gt;

&lt;p&gt;Timeline Studio pre-analyzes a video at adaptive intervals and stores timestamped YOLOS geometry and MODNet masks. Preview, smart crop, caption avoidance, background removal, and export all resolve the analysis record for the current source time.&lt;/p&gt;

&lt;p&gt;This creates a reusable temporal track:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;video time ──► 0.0s ───── 0.8s ───── 1.6s ───── 2.4s
subject box    A           B           C           D
matte mask     A'          B'          C'          D'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Longer videos use wider sample intervals to bound inference time and memory. It is a compromise, but it is much more honest than freezing a first-frame result across the entire clip.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rewriting LivePortrait for WebGPU
&lt;/h2&gt;

&lt;p&gt;The talking-avatar pipeline was the hardest part of the project.&lt;/p&gt;

&lt;p&gt;JoyVASA converts voice audio into motion. LivePortrait then applies that motion to a source portrait through appearance extraction, motion extraction, lip retargeting, stitching, warping, and SPADE generation.&lt;/p&gt;

&lt;p&gt;The available combined generator contained rank-5 operations and two 5D &lt;code&gt;GridSample&lt;/code&gt; nodes. Its native Linux plugin could not run in a browser, and the graph was not directly compatible with ONNX Runtime Web's WebGPU execution provider.&lt;/p&gt;

&lt;p&gt;I rewrote the 5D sampling as batched 4D &lt;code&gt;GridSample&lt;/code&gt; operations plus linear interpolation along depth. The rewritten graph grew from 277 to 316 nodes, but it could execute in the browser. Comparing the full 512×512 output against the original graph produced:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;maximum absolute error: &lt;code&gt;7.8976e-6&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;mean absolute error: &lt;code&gt;1.3853e-7&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;99.9th-percentile error: &lt;code&gt;1.7881e-6&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those numbers mattered. “It looks close” is not a sufficient validation strategy for replacing a core neural-network operator.&lt;/p&gt;

&lt;p&gt;The browser pipeline now offers a mixed-FP16 256px preview tier and a mixed-FP16 512px quality tier. It renders sparse neural keyframes, interpolates them for encoding, and keeps the reusable appearance feature on the GPU between frames.&lt;/p&gt;

&lt;p&gt;It is still not real-time. On the hardware used for validation, 512px neural frames remain expensive. The UI therefore reports real progress rather than pretending a slow operation is instant.&lt;/p&gt;

&lt;p&gt;The large avatar bundle also does not live in the Git repository. It is split into 50 MB parts and served from an immutable, revision-pinned &lt;a href="https://huggingface.co/haixin/timeline-studio-onnx-models/tree/a201b681c8f96672b5c3f624e32d4dc932f150af" rel="noopener noreferrer"&gt;Hugging Face model repository&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automatic captions need more than transcription
&lt;/h2&gt;

&lt;p&gt;Whisper returns text and timestamps, but subtitle quality inside an editor also depends on alignment.&lt;/p&gt;

&lt;p&gt;The current caption pipeline uses Whisper small q8 in a WASM worker. Tiny models were faster, but Chinese tests produced unstable hallucinations. WebGPU was also less stable for this workload, so captions deliberately stay on WASM until that path is revalidated.&lt;/p&gt;

&lt;p&gt;After transcription, segment boundaries are nudged toward nearby source-audio energy. This makes subtitle bars line up with the waveform instead of blindly trusting coarse chunk timestamps.&lt;/p&gt;

&lt;p&gt;Chinese cleanup is conservative and context-aware. Broad rewriting would make subtitles read more smoothly while risking changes to what the speaker actually said — a bad trade in an automatic transcription path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the timeline feel like an editor
&lt;/h2&gt;

&lt;p&gt;AI demos often focus on inference and underestimate interaction design. In practice, much of the work went into ordinary editor behavior:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;inserting a generated voiceover at the playhead rather than replacing the previous result;&lt;/li&gt;
&lt;li&gt;moving linked captions with their voiceover;&lt;/li&gt;
&lt;li&gt;creating extra lanes when clips overlap and compacting them afterward;&lt;/li&gt;
&lt;li&gt;keeping clip geometry time-accurate at every zoom level;&lt;/li&gt;
&lt;li&gt;using transparent hit targets for very short clips instead of visually stretching them;&lt;/li&gt;
&lt;li&gt;making Delete and Backspace clip-scoped without firing while the user edits a text field;&lt;/li&gt;
&lt;li&gt;keeping preview and export subtitle layout WYSIWYG.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Timeline zoom is based on visible duration rather than an arbitrary CSS scale. At the widest level it can show a long project; at the closest level it reaches frame-oriented ruler ticks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getTimelineTrackWidthPercent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;zoom&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;visibleDuration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getTimelineVisibleDuration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;zoom&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;visibleDuration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps a five-second clip representing five seconds no matter how far the user zooms in or out.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F035nhyof64ewb35ul2c5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F035nhyof64ewb35ul2c5.png" alt="Voiceover and captions aligned on the timeline" width="800" height="569"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Caching models without hiding storage costs
&lt;/h2&gt;

&lt;p&gt;Repeatedly downloading hundreds of megabytes makes a browser AI application unusable. Timeline Studio registers a service worker that uses cache-first behavior for model assets and network-first behavior for the app shell.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;cacheFirst&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MODEL_CACHE_NAME&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Model URLs are revision-pinned so a cached graph and its runtime assumptions do not silently drift apart. The app can also clear the model cache when browser storage becomes tight.&lt;/p&gt;

&lt;p&gt;Local-first does not mean “free.” It exchanges server cost and upload latency for model downloads, device storage, memory pressure, and hardware variability. Those costs need to be visible in progress UI and error messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Export is another rendering engine
&lt;/h2&gt;

&lt;p&gt;The preview canvas and exported video must agree about crop geometry, captions, stickers, filters, transitions, and current timeline time.&lt;/p&gt;

&lt;p&gt;Export composes the visual canvas and audio tracks in the browser, records a supported native format, and uses FFmpeg WASM only when MP4 transcoding is needed. If MP4 conversion fails, the editor preserves the successfully rendered WebM instead of discarding the user's work.&lt;/p&gt;

&lt;p&gt;This fallback hierarchy turned out to be important because browser codec support varies considerably.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnowgifovutatdm63qrzx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnowgifovutatdm63qrzx.png" alt="Browser export progress" width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;The largest lesson was that browser AI is a systems problem, not just a model-loading problem.&lt;/p&gt;

&lt;p&gt;You have to design model selection, execution providers, workers, caching, memory lifetime, timeline semantics, rendering, codecs, and failure states as one product. A fast model with poor orchestration still creates a slow editor. An accurate model without temporal data can create visually incorrect output. A successful inference that is not inserted cleanly into the timeline is still a poor editing experience.&lt;/p&gt;

&lt;p&gt;The browser is already capable of much more local media intelligence than many applications use today. But the best results come from acknowledging its constraints: lazy-load aggressively, cache carefully, keep heavy work off the main thread, validate graph rewrites numerically, and never hide a degraded result behind a “success” message.&lt;/p&gt;

&lt;p&gt;Timeline Studio is still evolving, and contributions are welcome. If you want to experiment with local-first creative tooling, check out the &lt;a href="https://github.com/MartinDelophy/ai-video-editor" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;, try the &lt;a href="https://video-editor.ai-creator.top/" rel="noopener noreferrer"&gt;live editor&lt;/a&gt;, and let me know which part of the browser AI stack you would like to see explored next.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>react</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
