<?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: Lim Geng</title>
    <description>The latest articles on DEV Community by Lim Geng (@limbuilder).</description>
    <link>https://dev.to/limbuilder</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%2F4101326%2F7bf0537c-497c-4118-978e-e5525ce6a060.jpg</url>
      <title>DEV Community: Lim Geng</title>
      <link>https://dev.to/limbuilder</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/limbuilder"/>
    <language>en</language>
    <item>
      <title>How I Built an AI Video Text Remover with Next.js and Video Inpainting</title>
      <dc:creator>Lim Geng</dc:creator>
      <pubDate>Sun, 20 Sep 2026 02:02:08 +0000</pubDate>
      <link>https://dev.to/limbuilder/how-i-built-an-ai-video-text-remover-with-nextjs-and-video-inpainting-dde</link>
      <guid>https://dev.to/limbuilder/how-i-built-an-ai-video-text-remover-with-nextjs-and-video-inpainting-dde</guid>
      <description>&lt;p&gt;Removing text from an image is relatively straightforward.&lt;/p&gt;

&lt;p&gt;Removing text from a &lt;strong&gt;video&lt;/strong&gt; is a very different problem.&lt;/p&gt;

&lt;p&gt;A video is not just a collection of independent images. Every frame has to remain visually consistent with the frames before and after it. If the reconstructed background changes slightly from frame to frame, the result starts to flicker immediately.&lt;/p&gt;

&lt;p&gt;Over the past few weeks, I have been building &lt;strong&gt;Video Text Remover&lt;/strong&gt;, a small web tool for removing subtitles, captions, watermarks, timestamps, and other burned-in text from videos.&lt;/p&gt;

&lt;p&gt;You can try it here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://videotextremover.org" rel="noopener noreferrer"&gt;https://videotextremover.org&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this post, I want to share how I approached the problem, the architecture behind the tool, and a few things I learned about video inpainting along the way.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: Text Is Part of the Pixels
&lt;/h2&gt;

&lt;p&gt;There are two very different types of subtitles in video.&lt;/p&gt;

&lt;p&gt;The first type is a separate subtitle track, such as an &lt;code&gt;.srt&lt;/code&gt; file or an embedded subtitle stream.&lt;/p&gt;

&lt;p&gt;Those are easy to remove.&lt;/p&gt;

&lt;p&gt;You can simply disable or strip the subtitle track.&lt;/p&gt;

&lt;p&gt;The second type is &lt;strong&gt;burned-in text&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;captions rendered directly into the video&lt;/li&gt;
&lt;li&gt;TikTok-style subtitles&lt;/li&gt;
&lt;li&gt;timestamps&lt;/li&gt;
&lt;li&gt;channel logos&lt;/li&gt;
&lt;li&gt;watermarks&lt;/li&gt;
&lt;li&gt;usernames&lt;/li&gt;
&lt;li&gt;presentation text&lt;/li&gt;
&lt;li&gt;labels added during editing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this case, there is no separate subtitle layer.&lt;/p&gt;

&lt;p&gt;The text is literally part of the image.&lt;/p&gt;

&lt;p&gt;Removing it means answering a much harder question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What should the pixels behind the text look like?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That turns the problem into an &lt;strong&gt;inpainting&lt;/strong&gt; problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  Image Inpainting Is Not Enough
&lt;/h2&gt;

&lt;p&gt;For a single image, an inpainting model receives something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Original image
      +
Mask indicating the area to remove
      ↓
Inpainting model
      ↓
Reconstructed image
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the object being removed is small, modern models can often reconstruct the missing background surprisingly well.&lt;/p&gt;

&lt;p&gt;Video adds another dimension: &lt;strong&gt;time&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Imagine a subtitle covering part of someone's shirt.&lt;/p&gt;

&lt;p&gt;Frame 1 might reconstruct the missing area as dark blue.&lt;/p&gt;

&lt;p&gt;Frame 2 might generate a slightly different texture.&lt;/p&gt;

&lt;p&gt;Frame 3 might introduce another variation.&lt;/p&gt;

&lt;p&gt;Each frame may look acceptable individually.&lt;/p&gt;

&lt;p&gt;But when played together, those tiny differences create obvious flickering.&lt;/p&gt;

&lt;p&gt;So a useful video text removal system needs more than spatial consistency.&lt;/p&gt;

&lt;p&gt;It also needs &lt;strong&gt;temporal consistency&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Initial Architecture
&lt;/h2&gt;

&lt;p&gt;I wanted the product architecture to stay relatively simple.&lt;/p&gt;

&lt;p&gt;The current system roughly looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser
  ↓
Next.js Application
  ↓
Upload Video
  ↓
Cloud Object Storage
  ↓
Create Processing Job
  ↓
Video Inpainting Service
  ↓
Poll Job Status
  ↓
Processed Video
  ↓
Download Result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The frontend and application layer are built with &lt;strong&gt;Next.js&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Large video files are uploaded to object storage instead of being passed through the application server itself.&lt;/p&gt;

&lt;p&gt;This is important because sending large videos through serverless application endpoints creates several problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;request size limits&lt;/li&gt;
&lt;li&gt;function timeouts&lt;/li&gt;
&lt;li&gt;unnecessary bandwidth usage&lt;/li&gt;
&lt;li&gt;increased memory usage&lt;/li&gt;
&lt;li&gt;higher infrastructure cost&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead, the application generates an upload target and lets the browser upload the file directly.&lt;/p&gt;

&lt;p&gt;The processing service then works from the stored video.&lt;/p&gt;




&lt;h2&gt;
  
  
  Manual Selection vs Automatic Detection
&lt;/h2&gt;

&lt;p&gt;One of the first product decisions I had to make was how users should tell the system what to remove.&lt;/p&gt;

&lt;p&gt;There are two approaches.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Automatic text detection
&lt;/h3&gt;

&lt;p&gt;The ideal experience is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Upload video
↓
Detect text automatically
↓
Remove detected text
↓
Download
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is obviously the easiest experience for users.&lt;/p&gt;

&lt;p&gt;But automatic detection introduces another difficult problem.&lt;/p&gt;

&lt;p&gt;The system has to determine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what is actually text&lt;/li&gt;
&lt;li&gt;which text should be removed&lt;/li&gt;
&lt;li&gt;where it appears&lt;/li&gt;
&lt;li&gt;whether it moves&lt;/li&gt;
&lt;li&gt;whether the bounding box changes&lt;/li&gt;
&lt;li&gt;whether text appears only during part of the video&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;OCR models can detect text quite well, but video creates edge cases.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Scoreboard      → probably text
Street sign     → text, but maybe part of the scene
Subtitle        → probably remove
T-shirt logo    → maybe not
Phone screen    → depends on user intent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Detection and intent are not the same thing.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Manual area selection
&lt;/h3&gt;

&lt;p&gt;The second approach is much simpler.&lt;/p&gt;

&lt;p&gt;The user draws a rectangle around the area that should be removed.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────────┐
│                              │
│          VIDEO               │
│                              │
│   ┌──────────────────────┐   │
│   │     subtitle area    │   │
│   └──────────────────────┘   │
│                              │
└──────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This removes a lot of ambiguity.&lt;/p&gt;

&lt;p&gt;The user already knows what they want removed.&lt;/p&gt;

&lt;p&gt;The system only needs to focus on reconstructing the selected area.&lt;/p&gt;

&lt;p&gt;For many real-world cases, especially subtitles, the text stays inside roughly the same region for most of the video.&lt;/p&gt;

&lt;p&gt;That makes manual selection surprisingly effective.&lt;/p&gt;

&lt;p&gt;For this reason, I decided to support both workflows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;automatic detection for convenience&lt;/li&gt;
&lt;li&gt;manual selection for control&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Representing the Selected Region
&lt;/h2&gt;

&lt;p&gt;The browser displays the video at a different size than its actual resolution.&lt;/p&gt;

&lt;p&gt;For example, the uploaded video might be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1920 × 1080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;while the preview shown in the browser might be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;960 × 540
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the user draws a selection box on the preview, the coordinates need to be converted back to the original video coordinate system.&lt;/p&gt;

&lt;p&gt;A simple version looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scaleX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;originalWidth&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;previewWidth&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;scaleY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;originalHeight&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;previewHeight&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;actualX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;selectedX&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;scaleX&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;actualY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;selectedY&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;scaleY&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;actualWidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;selectedWidth&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;scaleX&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;actualHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;selectedHeight&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;scaleY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is another approach that I prefer even more: storing coordinates as normalized values.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Region&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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;Where every value is between &lt;code&gt;0&lt;/code&gt; and &lt;code&gt;1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A selection might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"x"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"y"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.76&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.74&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.14&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes the mask independent of the preview resolution.&lt;/p&gt;

&lt;p&gt;Later, it can be converted into pixels for any video size.&lt;/p&gt;




&lt;h2&gt;
  
  
  Video Processing Should Be Asynchronous
&lt;/h2&gt;

&lt;p&gt;Video processing can easily take minutes.&lt;/p&gt;

&lt;p&gt;That means a normal synchronous API request is not appropriate.&lt;/p&gt;

&lt;p&gt;Instead of doing this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /remove-text
↓
wait...
wait...
wait...
↓
return processed video
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I use a job-style workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /jobs
↓
jobId
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the frontend checks the status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /jobs/:jobId
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response may look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"processing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"progress"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;62&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And eventually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"completed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resultUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This architecture has several advantages.&lt;/p&gt;

&lt;p&gt;If the browser tab refreshes, the processing job can continue.&lt;/p&gt;

&lt;p&gt;If processing fails, the backend can retry.&lt;/p&gt;

&lt;p&gt;If the external processing service is temporarily unavailable, jobs can be queued instead of immediately failing.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Real Challenge: Temporal Consistency
&lt;/h2&gt;

&lt;p&gt;The hardest part is not detecting the text.&lt;/p&gt;

&lt;p&gt;It is reconstructing the background convincingly over time.&lt;/p&gt;

&lt;p&gt;Consider a camera moving horizontally.&lt;/p&gt;

&lt;p&gt;A subtitle covers part of the ground.&lt;/p&gt;

&lt;p&gt;The missing region might contain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Frame 1: grass
Frame 2: grass + shadow
Frame 3: edge of a road
Frame 4: road
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simply running an image inpainting model independently on each frame can produce unstable results.&lt;/p&gt;

&lt;p&gt;Video inpainting systems typically try to use information from nearby frames.&lt;/p&gt;

&lt;p&gt;A simplified idea is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Previous frames
      ↓
Current masked frame
      ↑
Future frames
      ↓
Temporal reconstruction
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the background behind the text is visible in nearby frames, the model can use that information to reconstruct the missing area more consistently.&lt;/p&gt;

&lt;p&gt;This is why video-specific models tend to produce better results than simply applying an image model frame by frame.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Some Videos Work Much Better Than Others
&lt;/h2&gt;

&lt;p&gt;One thing that became obvious very quickly is that the difficulty varies enormously depending on the scene.&lt;/p&gt;

&lt;h3&gt;
  
  
  Easier cases
&lt;/h3&gt;

&lt;p&gt;Text over:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;static backgrounds&lt;/li&gt;
&lt;li&gt;walls&lt;/li&gt;
&lt;li&gt;sky&lt;/li&gt;
&lt;li&gt;blurred backgrounds&lt;/li&gt;
&lt;li&gt;simple textures&lt;/li&gt;
&lt;li&gt;areas with little motion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;usually works quite well.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Static interview
+
subtitle at the bottom
+
mostly blurred background

→ relatively easy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Harder cases
&lt;/h3&gt;

&lt;p&gt;Things become much more difficult when the text overlaps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;faces&lt;/li&gt;
&lt;li&gt;hands&lt;/li&gt;
&lt;li&gt;complex clothing&lt;/li&gt;
&lt;li&gt;fast-moving objects&lt;/li&gt;
&lt;li&gt;detailed textures&lt;/li&gt;
&lt;li&gt;camera cuts&lt;/li&gt;
&lt;li&gt;animations&lt;/li&gt;
&lt;li&gt;particles&lt;/li&gt;
&lt;li&gt;rapidly changing scenes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Imagine text covering someone's fingers while they are moving.&lt;/p&gt;

&lt;p&gt;The model has to reconstruct not only the appearance of the fingers, but also their motion across multiple frames.&lt;/p&gt;

&lt;p&gt;That is a much harder problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  Masks Matter More Than I Expected
&lt;/h2&gt;

&lt;p&gt;Another lesson was that the mask itself has a large impact on output quality.&lt;/p&gt;

&lt;p&gt;A mask that is too small leaves fragments of text behind.&lt;/p&gt;

&lt;p&gt;A mask that is too large forces the model to reconstruct unnecessary parts of the image.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bad:

┌─────────────────────────────┐
│                             │
│         remove me           │
│            ───              │
│           mask              │
└─────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only covering the center of the letters can leave visible edges.&lt;/p&gt;

&lt;p&gt;A slightly larger region often works better:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────┐
│                             │
│      ┌───────────────┐      │
│      │   remove me   │      │
│      └───────────────┘      │
│                             │
└─────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But making the region dramatically larger than necessary also increases the chance of visual artifacts.&lt;/p&gt;

&lt;p&gt;The best mask usually has a small amount of padding around the text.&lt;/p&gt;




&lt;h2&gt;
  
  
  Handling Multiple Text Areas
&lt;/h2&gt;

&lt;p&gt;Some videos contain more than one thing to remove.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────┐
│ USERNAME                    │
│                             │
│                             │
│                             │
│        subtitles            │
└─────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Supporting only one rectangle becomes restrictive very quickly.&lt;/p&gt;

&lt;p&gt;So I added support for multiple selection regions.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;regions&lt;/span&gt; &lt;span class="o"&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;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.03&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.08&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.78&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.12&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;These regions can later be converted into one combined mask.&lt;/p&gt;

&lt;p&gt;This turned out to be useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;logos + subtitles&lt;/li&gt;
&lt;li&gt;username + watermark&lt;/li&gt;
&lt;li&gt;timestamp + caption&lt;/li&gt;
&lt;li&gt;several static overlays&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Cost Is a Product Problem Too
&lt;/h2&gt;

&lt;p&gt;Video AI processing is significantly more expensive than many image AI tasks.&lt;/p&gt;

&lt;p&gt;A single image might require one inference.&lt;/p&gt;

&lt;p&gt;A video may contain thousands of frames.&lt;/p&gt;

&lt;p&gt;For example, a 30-second video at 30 FPS contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;30 × 30 = 900 frames
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A five-minute video contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5 × 60 × 30 = 9,000 frames
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Obviously, video models do not necessarily process every frame independently, but the scale difference explains why video inference can become expensive.&lt;/p&gt;

&lt;p&gt;This affects the product design.&lt;/p&gt;

&lt;p&gt;You need to think about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;maximum video duration&lt;/li&gt;
&lt;li&gt;maximum file size&lt;/li&gt;
&lt;li&gt;processing resolution&lt;/li&gt;
&lt;li&gt;job concurrency&lt;/li&gt;
&lt;li&gt;storage lifecycle&lt;/li&gt;
&lt;li&gt;retries&lt;/li&gt;
&lt;li&gt;failed jobs&lt;/li&gt;
&lt;li&gt;abuse prevention&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These infrastructure decisions matter almost as much as the AI model itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  Temporary Storage Is Important
&lt;/h2&gt;

&lt;p&gt;Uploaded videos can be large.&lt;/p&gt;

&lt;p&gt;Keeping every original and processed file forever would quickly become expensive.&lt;/p&gt;

&lt;p&gt;A better lifecycle is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Upload
↓
Process
↓
User downloads result
↓
Temporary retention period
↓
Automatic deletion
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps storage usage predictable.&lt;/p&gt;

&lt;p&gt;It also reduces the amount of user data retained by the service.&lt;/p&gt;




&lt;h2&gt;
  
  
  Error Handling Matters More with AI APIs
&lt;/h2&gt;

&lt;p&gt;Traditional APIs are often relatively deterministic.&lt;/p&gt;

&lt;p&gt;AI inference APIs are different.&lt;/p&gt;

&lt;p&gt;A request may fail because of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;temporary provider errors&lt;/li&gt;
&lt;li&gt;model startup time&lt;/li&gt;
&lt;li&gt;GPU availability&lt;/li&gt;
&lt;li&gt;timeout&lt;/li&gt;
&lt;li&gt;unsupported codec&lt;/li&gt;
&lt;li&gt;malformed video&lt;/li&gt;
&lt;li&gt;unexpectedly large files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the processing layer needs to distinguish between retryable and non-retryable errors.&lt;/p&gt;

&lt;p&gt;Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isTemporaryError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;markJobAsFailed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&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;Retry logic should also have limits.&lt;/p&gt;

&lt;p&gt;Otherwise a broken video may be processed forever.&lt;/p&gt;

&lt;p&gt;A more realistic pattern is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Attempt 1
↓
Failed

Wait

Attempt 2
↓
Failed

Wait longer

Attempt 3
↓
Failed

Mark job as failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  UX Is Part of the Technical Problem
&lt;/h2&gt;

&lt;p&gt;When a task takes several minutes, showing a spinner is not enough.&lt;/p&gt;

&lt;p&gt;Users want to know whether something is actually happening.&lt;/p&gt;

&lt;p&gt;Even if the underlying provider does not expose precise frame-level progress, showing processing stages helps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Uploading video
      ↓
Preparing video
      ↓
Removing text
      ↓
Generating final video
      ↓
Completed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This sounds like a small detail, but it makes the application feel much more reliable.&lt;/p&gt;

&lt;p&gt;Long-running AI workflows need visible state.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Would Improve Next
&lt;/h2&gt;

&lt;p&gt;There are still many things I want to improve.&lt;/p&gt;

&lt;h3&gt;
  
  
  Better automatic detection
&lt;/h3&gt;

&lt;p&gt;The ideal system would track text over time instead of simply detecting text in isolated frames.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OCR
↓
Bounding box
↓
Track across frames
↓
Determine lifetime
↓
Generate temporal mask
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This would make automatic removal much more precise.&lt;/p&gt;




&lt;h3&gt;
  
  
  Better handling of moving watermarks
&lt;/h3&gt;

&lt;p&gt;Static subtitles are relatively easy because their position rarely changes.&lt;/p&gt;

&lt;p&gt;Moving logos and animated overlays are much harder.&lt;/p&gt;

&lt;p&gt;Tracking them frame by frame would allow the mask to move with the target.&lt;/p&gt;




&lt;h3&gt;
  
  
  Scene-aware processing
&lt;/h3&gt;

&lt;p&gt;A five-minute video might contain dozens of scene changes.&lt;/p&gt;

&lt;p&gt;Instead of treating the entire video as one continuous sequence, the system could detect cuts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Video
↓
Scene detection
↓
Scene 1
Scene 2
Scene 3
...
↓
Process separately
↓
Merge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This may improve consistency and reduce unnecessary context.&lt;/p&gt;




&lt;h3&gt;
  
  
  Preview before full processing
&lt;/h3&gt;

&lt;p&gt;Another useful feature would be processing only a few seconds first.&lt;/p&gt;

&lt;p&gt;Users could verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;whether the selected region is correct&lt;/li&gt;
&lt;li&gt;whether the model handles the background well&lt;/li&gt;
&lt;li&gt;whether the result is acceptable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;before spending time and compute processing the entire video.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Biggest Lesson
&lt;/h2&gt;

&lt;p&gt;Before building this project, I thought the core problem would be:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How do I remove text from a video?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;After working on it, I realized the actual problem is closer to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How do I reconstruct missing pixels across time while keeping the result visually consistent?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Text detection is only one part of the system.&lt;/p&gt;

&lt;p&gt;The full problem includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;coordinate systems&lt;/li&gt;
&lt;li&gt;masks&lt;/li&gt;
&lt;li&gt;uploads&lt;/li&gt;
&lt;li&gt;object storage&lt;/li&gt;
&lt;li&gt;asynchronous jobs&lt;/li&gt;
&lt;li&gt;video codecs&lt;/li&gt;
&lt;li&gt;AI inference&lt;/li&gt;
&lt;li&gt;retries&lt;/li&gt;
&lt;li&gt;temporal consistency&lt;/li&gt;
&lt;li&gt;cost control&lt;/li&gt;
&lt;li&gt;UX&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is what made this project much more interesting than I initially expected.&lt;/p&gt;

&lt;p&gt;I packaged the current version into a small web tool called &lt;strong&gt;Video Text Remover&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://videotextremover.org" rel="noopener noreferrer"&gt;https://videotextremover.org&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It currently focuses on removing visible text and overlays from videos using automatic detection or manually selected regions.&lt;/p&gt;

&lt;p&gt;There is still a lot to improve, especially around moving text and difficult backgrounds, but building it has been a useful exploration of what production video AI actually looks like beyond a simple model demo.&lt;/p&gt;

&lt;p&gt;If you're also building image or video AI tools, I'd be interested to hear how you're handling long-running inference jobs, storage costs, and temporal consistency.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Tags&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;#webdev&lt;/code&gt; &lt;code&gt;#nextjs&lt;/code&gt; &lt;code&gt;#ai&lt;/code&gt; &lt;code&gt;#machinelearning&lt;/code&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>nextjs</category>
      <category>machinelearning</category>
    </item>
  </channel>
</rss>
