<?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: yubin hong</title>
    <description>The latest articles on DEV Community by yubin hong (@zero_to_one0to1).</description>
    <link>https://dev.to/zero_to_one0to1</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%2F4014265%2F2cfd9d07-bff6-475e-bafc-3b3366834c62.jpg</url>
      <title>DEV Community: yubin hong</title>
      <link>https://dev.to/zero_to_one0to1</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zero_to_one0to1"/>
    <language>en</language>
    <item>
      <title>CapCut's AI Captions Went Pro-Only — So I Built a Free Alternative</title>
      <dc:creator>yubin hong</dc:creator>
      <pubDate>Fri, 17 Jul 2026 11:54:24 +0000</pubDate>
      <link>https://dev.to/zero_to_one0to1/capcuts-ai-captions-went-pro-only-so-i-built-a-free-alternative-46n5</link>
      <guid>https://dev.to/zero_to_one0to1/capcuts-ai-captions-went-pro-only-so-i-built-a-free-alternative-46n5</guid>
      <description>&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%2F5kne0p5g3cx6p3t4tx8g.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%2F5kne0p5g3cx6p3t4tx8g.png" alt=" " width="799" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hey, I run a small YouTube channel.&lt;/p&gt;

&lt;p&gt;Not a huge subscriber count, but I edit almost every day. I add captions to basically every video, and for a long time I relied on CapCut's AI auto-caption feature. It transcribes what you say automatically, which saves a ton of time compared to typing everything out line by line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then the problem started
&lt;/h2&gt;

&lt;p&gt;At some point, CapCut locked the AI caption feature behind a Pro subscription.&lt;/p&gt;

&lt;p&gt;For small creators like me, monetization isn't always stable yet, and another monthly subscription just for captions adds up fast. I kept thinking, "do I really need to pay every month just to add subtitles?" and figured a lot of other small creators were probably in the same boat.&lt;/p&gt;

&lt;p&gt;So I decided to just build it myself.&lt;/p&gt;

&lt;h2&gt;
  
  
  I built a free AI caption site
&lt;/h2&gt;

&lt;p&gt;After a few weeks of working on it in my spare time, I finished a site where you upload a video and AI automatically transcribes the speech into captions.&lt;/p&gt;

&lt;p&gt;Here's what it does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Upload a video and AI analyzes the audio to generate captions automatically&lt;/li&gt;
&lt;li&gt;Download the captions as text or burn them directly into the video&lt;/li&gt;
&lt;li&gt;Free to use for everyone, no subscription or Pro tier required&lt;/li&gt;
&lt;li&gt;Runs entirely in the browser, no heavy software to install like CapCut&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To be clear, I didn't invent some groundbreaking new technology. I mostly took existing speech recognition AI and shaped it into something small creators can use without worrying about cost. But if you've been stuck on the same caption problem I was, it should be genuinely useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why give it away for free
&lt;/h2&gt;

&lt;p&gt;No grand mission here. I needed this feature myself and didn't want to pay for it. I'm putting it out there hoping other small YouTubers and creators just starting out can handle captions without another subscription eating into their budget.&lt;/p&gt;

&lt;p&gt;If you've ever hesitated before paying for an editing subscription, give it a try.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Questions or feedback are welcome in the comments. If you try it out, let me know what could be better — I'm actively improving it.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title># Engineering Log #03 — The honest asterisk got cashed in (product F1 0.887), and the model finally became a product</title>
      <dc:creator>yubin hong</dc:creator>
      <pubDate>Mon, 06 Jul 2026 01:42:04 +0000</pubDate>
      <link>https://dev.to/zero_to_one0to1/-engineering-log-03-the-honest-asterisk-got-cashed-in-product-f1-0887-and-the-model-finally-5hgk</link>
      <guid>https://dev.to/zero_to_one0to1/-engineering-log-03-the-honest-asterisk-got-cashed-in-product-f1-0887-and-the-model-finally-5hgk</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A build-in-public engineering log. Every number below is pulled straight from the test reports — &lt;strong&gt;measured values&lt;/strong&gt;, not rounded to look pretty, not invented. A success you cherry-picked isn't a success.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Log #02 ended on an asterisk. We had a video action classifier at &lt;strong&gt;94% accuracy&lt;/strong&gt; and an end-to-end highlight pipeline at &lt;strong&gt;recall 0.86&lt;/strong&gt;, but the newly-swapped "pixel finder" was measured under a generous assumption — perfect tracking, GT-interpolated boxes. Its &lt;em&gt;product&lt;/em&gt; operating point, measured honestly against real kinematics, was only &lt;strong&gt;R 0.52 / P 0.57&lt;/strong&gt;. We wrote, in plain text, that we'd have to wire it into the real pipeline with actual tracking lock and re-confirm with &lt;code&gt;product_eval&lt;/code&gt; before claiming anything.&lt;/p&gt;

&lt;p&gt;This log is that re-confirmation — plus the bigger thing that happened while we weren't looking: the model stopped being a model and became a product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where we are now:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Product pipeline (real tracking, end-to-end): &lt;strong&gt;F1 0.887&lt;/strong&gt; — recall &lt;strong&gt;0.868&lt;/strong&gt;, precision &lt;strong&gt;0.908&lt;/strong&gt; on the holdout config.&lt;/li&gt;
&lt;li&gt;The generous upper bound from Log #02 didn't collapse when we made it honest. It held.&lt;/li&gt;
&lt;li&gt;Shipped an actual product surface: a web app (upload → lock a player → auto-edit → done) and a cinematic slow-motion render pipeline.&lt;/li&gt;
&lt;li&gt;Deployment reality check: we pivoted the whole serving path from &lt;strong&gt;CPU to GPU&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Part 1 — Cashing the asterisk: the product number, measured honestly
&lt;/h2&gt;

&lt;p&gt;In Log #02 we were careful not to celebrate the spotter's headline recall, because it was measured with GT-interpolated boxes — i.e. assuming the tracker never loses the player. That's the spotter's home-field advantage. The real question was always: &lt;em&gt;what happens when the boxes come from the actual tracker, running end-to-end, on the real product config?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We ran it. Here are the receipts, straight from the eval reports, all on the same &lt;strong&gt;45-video / 68-critical&lt;/strong&gt; holdout set:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Config&lt;/th&gt;
&lt;th&gt;Recall&lt;/th&gt;
&lt;th&gt;Precision&lt;/th&gt;
&lt;th&gt;F1&lt;/th&gt;
&lt;th&gt;Fired&lt;/th&gt;
&lt;th&gt;TP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;config.yaml&lt;/code&gt; (product baseline)&lt;/td&gt;
&lt;td&gt;0.853&lt;/td&gt;
&lt;td&gt;0.906&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.879&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;58&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;config_gpu_a&lt;/code&gt; (GPU parity)&lt;/td&gt;
&lt;td&gt;0.853&lt;/td&gt;
&lt;td&gt;0.906&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.879&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;58&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;config_spotter_v2&lt;/code&gt; (holdout)&lt;/td&gt;
&lt;td&gt;0.868&lt;/td&gt;
&lt;td&gt;0.908&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.887&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;65&lt;/td&gt;
&lt;td&gt;59&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;config_gpu_b&lt;/code&gt; (precision-leaning variant)&lt;/td&gt;
&lt;td&gt;0.809&lt;/td&gt;
&lt;td&gt;0.917&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.859&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;55&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The number to sit with: the Log #02 product operating point was &lt;strong&gt;R 0.52 / P 0.57&lt;/strong&gt;. The re-confirmed, real-tracking, end-to-end product point is &lt;strong&gt;R 0.87 / P 0.91 / F1 0.887&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We want to be precise about why this isn't cheating. The 0.52/0.57 in Log #02 was the &lt;em&gt;kinematic-only&lt;/em&gt; operating point — the old finder, before the pixel spotter was integrated with tracking lock. The honest fear was that the spotter's 0.86-ish recall was an artifact of perfect boxes and would fall apart on real tracks. It didn't. When we integrated the spotter into the real pipeline with the actual tracker, the end-to-end product number landed at &lt;strong&gt;F1 0.879–0.887&lt;/strong&gt; depending on config. The generous assumption and the honest measurement converged instead of diverging. That's the good outcome, and it's the one we were least sure we'd get.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two things we're nailing down honestly:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. GPU parity is exact, not approximate.&lt;/strong&gt; &lt;code&gt;config.yaml&lt;/code&gt; on Apple Silicon (mps) and &lt;code&gt;config_gpu_a&lt;/code&gt; on CUDA produce the &lt;em&gt;identical&lt;/em&gt; R 0.853 / P 0.906 / F1 0.879 — same 64 fired, same 58 TP. That matters: it means moving to GPU is a throughput decision, not a quality one. The model doesn't behave differently on the box we ship on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;gpu_b&lt;/code&gt; is a knob, not an upgrade.&lt;/strong&gt; The precision-leaning variant trades recall (0.853 → 0.809) for a hair more precision (0.906 → 0.917). Same 68 criticals, fewer firings. It's there so we can pick a false-positive tolerance per deployment, not because it's "better." For a highlight product, missing a moment is worse than an occasional extra clip, so the recall-leaning baseline is the default.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2 — The model became a product
&lt;/h2&gt;

&lt;p&gt;Log #02 was entirely about the model. This log is where the model grows a body. Two big pieces shipped.&lt;/p&gt;

&lt;h3&gt;
  
  
  The render pipeline: from "we found the moment" to "here's the clip"
&lt;/h3&gt;

&lt;p&gt;Detecting an impact timestamp &lt;code&gt;T&lt;/code&gt; is worthless to a parent with a phone unless it comes out the other end as something watchable. So we built the output layer — an orchestrator that turns &lt;em&gt;(video + target trajectory + impact T)&lt;/em&gt; into a vertical slow-motion highlight mp4. The flow is deliberately ordered (stabilize, &lt;em&gt;then&lt;/em&gt; retime):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Decode only the window around &lt;code&gt;T&lt;/code&gt; (&lt;code&gt;[pre, post]&lt;/code&gt;) at native fps — never the whole video.&lt;/li&gt;
&lt;li&gt;Resample the target trajectory to real frame times and smooth it with a Kalman/RTS pass.&lt;/li&gt;
&lt;li&gt;Generate stabilized frames with a moving-crop virtual camera.&lt;/li&gt;
&lt;li&gt;Integrate a speed ramp and invert it to get per-output-frame &lt;code&gt;(index, α)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Interpolate and stream straight to the writer — no buffering the whole clip in memory.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One design decision worth calling out because it's counterintuitive: &lt;strong&gt;V1 does not crop or zoom in.&lt;/strong&gt; On low-resolution real-world footage, a lock-on crop just destroys image quality — the parent already framed the subject big enough. Vertical reframe and lock-on zoom are deferred to V2. We'd rather ship the un-degraded frame now than a shaky zoomed one.&lt;/p&gt;

&lt;p&gt;The slow-motion itself uses a &lt;strong&gt;lead-anchored&lt;/strong&gt; ramp: kinematic impact &lt;code&gt;T&lt;/code&gt; tends to get detected &lt;em&gt;late&lt;/em&gt; (at landing / follow-through), so we pull the slow-motion center ahead of &lt;code&gt;T&lt;/code&gt; to cover the &lt;em&gt;decisive&lt;/em&gt; action — the wind-up and contact — instead of slow-mo'ing the part after it's already over.&lt;/p&gt;

&lt;p&gt;On top of single clips sits a &lt;strong&gt;reel compiler&lt;/strong&gt;: rank highlights by criticality, greedily pick the best ones that fit a 15/30/60s budget, then re-order them &lt;em&gt;chronologically&lt;/em&gt; so the reel still tells the game's story, with short crossfades at the seams. (Both render and reel are pure cv2/numpy, FFmpeg-free, silent MVP — audio is Phase 2.)&lt;/p&gt;

&lt;h3&gt;
  
  
  The web app
&lt;/h3&gt;

&lt;p&gt;There's now an actual front end: a bilingual (KO/EN) React/TS app with the whole flow a non-technical user needs — upload, lock the player you care about, watch the AI edit, get the finished vertical highlight. Screens for landing/upload, target lock, processing, gallery, and done. The pitch is one sentence: &lt;em&gt;upload your kid's game, get the highlights, don't touch an editor.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3 — Deployment reality: CPU → GPU
&lt;/h2&gt;

&lt;p&gt;Log #02's deployment plan was Oracle Cloud's Always Free tier — 24/7 on ARM CPU, torch-CPU, no GPU. The goal was a real server that stays up when the laptop sleeps, at zero cost. Two things made us pivot.&lt;/p&gt;

&lt;p&gt;First, &lt;strong&gt;the free tier shrank&lt;/strong&gt;. Oracle cut Always Free A1 from 4 OCPU / 24 GB to &lt;strong&gt;2 OCPU / 12 GB&lt;/strong&gt; (2026-06). Less headroom for exactly the workload we run.&lt;/p&gt;

&lt;p&gt;Second, and decisively, &lt;strong&gt;CPU inference is too slow for the pipeline we actually shipped&lt;/strong&gt;. The production path is RT-DETR detection &lt;em&gt;plus&lt;/em&gt; r2plus1d_18 dense scoring across the whole video — on CPU, a single job takes minutes. That's survivable for an async "we'll notify you" MVP, but it caps daily throughput hard. So we built the GPU serving path: &lt;code&gt;Dockerfile.cuda&lt;/code&gt;, a one-command &lt;code&gt;deploy_gpu.sh&lt;/code&gt; (rsync → remote provision of Docker + nvidia-container-toolkit + firewall + build + start), and a T4 benchmark step. The pixel spotter (&lt;code&gt;video_scorer.pth&lt;/code&gt;) bakes into the image as the deploy artifact, and — per Part 1 — GPU output is bit-for-bit the same quality as local, so this is purely about serving more jobs per day.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 4 — Two dead ends we're keeping dead (receipts included)
&lt;/h2&gt;

&lt;p&gt;Honesty means reporting the things that &lt;em&gt;didn't&lt;/em&gt; work and staying disciplined about not reviving them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RF-DETR fine-tuning stays shelved.&lt;/strong&gt; Back in Log #01 territory we fine-tuned RF-DETR to mAP 0.716 and it moved end-to-end recall &lt;em&gt;backwards&lt;/em&gt;. We re-confirmed on the real domain: no improvement. We're keeping baseline RT-DETR. Swapping to rfdetr remains a config flag away, but the measured verdict is that it doesn't earn its complexity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The pose-TCN action classifier is now legacy.&lt;/strong&gt; The pixel spotter is the confirmed production finder-and-judge (F1 0.887). The old pose-TCN path is effectively disabled (&lt;code&gt;crit_tcn_floor: 1.0&lt;/code&gt;) because it showed no discriminative power on the real domain. It's still hot-swappable if we ever want it back, but nothing in the default path depends on it.&lt;/p&gt;

&lt;p&gt;Neither of these is a failure we're hiding — they're two forks we explored, measured, and closed. The 0.887 didn't come from adding more model. It came from finding the &lt;em&gt;right&lt;/em&gt; one and refusing to bolt the wrong ones back on.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where we stand
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Product number, honest:&lt;/strong&gt; F1 0.887 (R 0.868 / P 0.908) end-to-end with real tracking. The Log #02 asterisk resolved in our favor — the generous upper bound survived contact with reality.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU parity:&lt;/strong&gt; identical quality on mps and CUDA; the CPU→GPU move is throughput, not accuracy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's a product now:&lt;/strong&gt; upload → lock → auto-edit → vertical slow-mo reel, behind a real web app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discipline held:&lt;/strong&gt; RF-DETR and pose-TCN both measured, both closed, neither revived.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The remaining star, unchanged
&lt;/h2&gt;

&lt;p&gt;The domain caveat from Log #02 still stands and we're not going to let it fade: these numbers are on SVW-derived research footage. The real test is still a commercial rebuild on genuine user uploads. If F1 0.887 survives that domain shift, we have a product. If it drops, you'll see the drop here first — same as every other number in this log.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you're building in the same space and hitting the "the model works but is it a product" wall, I'd love to compare notes in the comments — especially on the CPU-vs-GPU serving math for video pipelines.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>programming</category>
    </item>
    <item>
      <title>Engineering Log #02: What our sports CV model actually scores right now (94% classifier, 0.86 highlight recall)</title>
      <dc:creator>yubin hong</dc:creator>
      <pubDate>Sun, 05 Jul 2026 01:06:30 +0000</pubDate>
      <link>https://dev.to/zero_to_one0to1/engineering-log-02-what-our-sports-cv-model-actually-scores-right-now-94-classifier-086-43fg</link>
      <guid>https://dev.to/zero_to_one0to1/engineering-log-02-what-our-sports-cv-model-actually-scores-right-now-94-classifier-086-43fg</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Build-in-public engineering log. Every number below is a real measurement pulled straight from our test reports — nothing rounded up to look nicer, nothing invented. If you only ever hear the wins, the wins mean nothing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In Log #01 we told the messy story of getting highlight recall from &lt;strong&gt;0.56 → 0.86&lt;/strong&gt;. Since then we built the piece that sits &lt;em&gt;underneath&lt;/em&gt; that pipeline: a video action classifier that decides &lt;em&gt;what&lt;/em&gt; is happening in a clip. This log is the honest scoreboard for both — because we were curious ourselves what the model actually scores today.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR current state:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Action classifier: &lt;strong&gt;94.0% test accuracy&lt;/strong&gt;, macro-F1 &lt;strong&gt;0.94&lt;/strong&gt;, across 4 classes.&lt;/li&gt;
&lt;li&gt;Highlight pipeline (end to end): recall &lt;strong&gt;0.86&lt;/strong&gt;, up from 0.56.&lt;/li&gt;
&lt;li&gt;Training data grew &lt;strong&gt;475 → 762 clips&lt;/strong&gt; between v1 and v2.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Part 1 — The action classifier: 94% today
&lt;/h2&gt;

&lt;p&gt;The classifier answers a narrow but load-bearing question: given a short clip, is this a &lt;strong&gt;baseball swing&lt;/strong&gt;, a &lt;strong&gt;basketball layup&lt;/strong&gt;, a &lt;strong&gt;soccer shot&lt;/strong&gt;, or &lt;strong&gt;nothing&lt;/strong&gt; (background play)? Get this wrong and every downstream highlight decision inherits the mistake.&lt;/p&gt;

&lt;p&gt;The model is nothing exotic — an &lt;strong&gt;R(2+1)D-18&lt;/strong&gt; backbone, 16 frames sampled per clip at 112×112. Deliberately small: it has to run cheaply on real uploaded footage, not win a leaderboard.&lt;/p&gt;

&lt;p&gt;Here's the current test report, verbatim from &lt;code&gt;video_scorer_testreport.json&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;th&gt;Precision&lt;/th&gt;
&lt;th&gt;Recall&lt;/th&gt;
&lt;th&gt;F1&lt;/th&gt;
&lt;th&gt;Support&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;baseball_swing&lt;/td&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;td&gt;0.971&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.986&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;soccer_shooting&lt;/td&gt;
&lt;td&gt;0.933&lt;/td&gt;
&lt;td&gt;0.933&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.933&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;0.938&lt;/td&gt;
&lt;td&gt;0.918&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.928&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;49&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;basketball_layup&lt;/td&gt;
&lt;td&gt;0.850&lt;/td&gt;
&lt;td&gt;0.944&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.895&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Overall accuracy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.940&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;117&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Macro-F1 lands at &lt;strong&gt;0.94&lt;/strong&gt;. Baseball swing is nearly solved (precision 1.000 — it never fires falsely). Basketball layup is our weakest link at F1 0.895, and the confusion matrix says exactly why.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reading the confusion matrix honestly
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 pred: bball  base   none  soccer
baseball_swing  [   34      0      1      0  ]
basketball_layup[    0     17      1      0  ]
none            [    0      3     45      1  ]
soccer_shooting [    0      0      1     14  ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Almost every error is a confusion with &lt;strong&gt;&lt;code&gt;none&lt;/code&gt;&lt;/strong&gt;, not a cross-sport mix-up. The model rarely calls a layup a swing; it occasionally can't decide whether a play is a highlight &lt;em&gt;at all&lt;/em&gt;. Concretely: 3 background clips get misread as layups (that's what drags layup precision to 0.85), and a scatter of real actions leak into &lt;code&gt;none&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's the good kind of error profile. The sports are cleanly separable; the remaining work is sharpening the &lt;strong&gt;action vs. no-action boundary&lt;/strong&gt;, which is a data problem, not an architecture problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2 — More data, honestly labeled
&lt;/h2&gt;

&lt;p&gt;The single biggest lever between v1 and v2 wasn't a clever loss function. It was clips.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;v1&lt;/th&gt;
&lt;th&gt;v2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total clips&lt;/td&gt;
&lt;td&gt;475&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;762&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;none&lt;/code&gt; (train)&lt;/td&gt;
&lt;td&gt;119&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;335&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We roughly &lt;strong&gt;tripled the negatives&lt;/strong&gt; (&lt;code&gt;none&lt;/code&gt;). That's not glamorous, but the confusion matrix above is exactly why it mattered: our errors live on the action/no-action boundary, and you only teach that boundary by showing the model far more of what a &lt;em&gt;non&lt;/em&gt;-highlight looks like.&lt;/p&gt;

&lt;p&gt;One caveat we keep visible in our own metadata: the current set is a &lt;strong&gt;research prototype&lt;/strong&gt;. The note in &lt;code&gt;dataset_meta.json&lt;/code&gt; literally reads &lt;em&gt;"commercial model = rebuild on user / commercially-usable video."&lt;/em&gt; The 94% is real, but it's 94% on prototype-domain data — we're not going to quietly let that number imply more than it earned.&lt;/p&gt;

&lt;h3&gt;
  
  
  The augmentation bet (this one paid off)
&lt;/h3&gt;

&lt;p&gt;Phone footage is ugly: motion blur, compression noise, players filmed tiny and far away, handheld shake. So we simulate all of it at train time rather than pretend clean clips generalize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;motion blur up to a 21px kernel&lt;/li&gt;
&lt;li&gt;downscale to &lt;strong&gt;0.35–0.7×&lt;/strong&gt; ("player filmed far away")&lt;/li&gt;
&lt;li&gt;JPEG quality floor of &lt;strong&gt;40&lt;/strong&gt; (phone compression artifacts)&lt;/li&gt;
&lt;li&gt;mild shift/scale/rotate for handheld shake&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each real clip spawns ~2 augmented variants. The point isn't more data for its own sake — it's data that looks like the mess the model meets in production.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3 — The pipeline recall story (the Log #01 recap, with the receipts)
&lt;/h2&gt;

&lt;p&gt;The classifier feeds a larger highlight-detection pipeline. That pipeline is the one that went &lt;strong&gt;0.56 → 0.86 recall&lt;/strong&gt;. Two lessons from that arc are worth re-stating, because they shaped how we built Part 1:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. We fixed the wrong layer first.&lt;/strong&gt; Our instinct was "make the detector see better," so we fine-tuned RF-DETR to a respectable &lt;strong&gt;0.716 mAP&lt;/strong&gt; — and end-to-end recall moved 0.60 → 0.59. &lt;em&gt;Backwards.&lt;/em&gt; When we decomposed every miss by stage, detection owned only &lt;strong&gt;15%&lt;/strong&gt; of failures; &lt;strong&gt;55%&lt;/strong&gt; died at the pose/kinematic stage. We'd polished the layer that wasn't broken.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Never trust a batched change.&lt;/strong&gt; We turned on four good features at once and the score &lt;em&gt;dropped&lt;/em&gt; 0.769 → 0.692. A one-at-a-time ablation exposed a single culprit (&lt;code&gt;video_mode&lt;/code&gt;) dragging the batch down. Three of four features were genuinely helping; one bad apple made the whole basket look rotten.&lt;/p&gt;

&lt;p&gt;Per-sport, the recall gains held across the board rather than being carried by one lucky category — basketball alone went &lt;strong&gt;0.43 → 0.82&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where we actually stand
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Classifier:&lt;/strong&gt; 94.0% accuracy, macro-F1 0.94. Sports are cleanly separated; the only soft spot is the highlight/no-highlight boundary, and we know it's a negatives-data problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pipeline:&lt;/strong&gt; 0.86 recall end-to-end, robust across sports.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Known asterisk:&lt;/strong&gt; prototype-domain data. The commercial rebuild on user footage is the next real test, and that number could move — we'll report it either way.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Rebuild the dataset on genuinely user-uploaded, commercially-usable footage and re-run this exact report. If 94% survives the domain shift, we have a product. If it doesn't, you'll see the drop here first.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Building something in the same space and hitting the action/no-action boundary too? I'd love to compare confusion matrices in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>computervision</category>
      <category>buildinpublic</category>
      <category>python</category>
    </item>
    <item>
      <title>We took highlight detection from 0.56 to 0.86 — with zero new footage and zero cloud training</title>
      <dc:creator>yubin hong</dc:creator>
      <pubDate>Sat, 04 Jul 2026 01:06:38 +0000</pubDate>
      <link>https://dev.to/zero_to_one0to1/we-took-highlight-detection-from-056-to-086-with-zero-new-footage-and-zero-cloud-training-j7g</link>
      <guid>https://dev.to/zero_to_one0to1/we-took-highlight-detection-from-056-to-086-with-zero-new-footage-and-zero-cloud-training-j7g</guid>
      <description>&lt;p&gt;SportZone turns a parent's phone video of a youth game into a highlight reel. The hard part is finding the decisive moment. On real footage we were missing nearly half of them. Here's how a week of measuring — not guessing — fixed it.&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%2Fxgjfoe4ll2i8r0ls6zdg.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%2Fxgjfoe4ll2i8r0ls6zdg.png" alt=" " width="799" height="265"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;01THE PROBLEM&lt;/em&gt;&lt;br&gt;
On real phone footage, the model went half-blind&lt;/p&gt;

&lt;p&gt;Our classifier scored 0.82 on curated YouTube clips. Confident, we ran it on genuine parent-filmed smartphone video for the first time. The number that matters — did we catch the decisive moment? — came back at 0.56. We were missing 44% of the highlights. A highlight tool that misses half the highlights isn't a tool.&lt;/p&gt;

&lt;p&gt;The curated-vs-real gap is the whole game. So the first thing we built wasn't a fix — it was a way to measure honestly on the real domain.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;02 THE WRONG TURN&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We bet on the obvious culprit. We lost.&lt;/p&gt;

&lt;p&gt;"The boxes look tight — the detector must be missing people." Reasonable. So we did the expensive thing: assembled ~9,100 commercially-licensed sports images, fine-tuned a detector on a cloud GPU, hit a healthy mAP of 0.716, and plugged it back in.&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%2F3s39q8n1ojdppgu0pm15.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%2F3s39q8n1ojdppgu0pm15.png" alt=" " width="800" height="96"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hours of data-wrangling and training bought us nothing. Frustrating — but the failure was the clue. If a better detector changes nothing, detection was never the bottleneck. We just didn't have the evidence yet.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;03 THE MEASUREMENT THAT CHANGED EVERYTHING&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We decomposed the failures instead of arguing about them&lt;/p&gt;

&lt;p&gt;We took every missed moment and tagged where in the pipeline it leaked: detection → tracking → pose → impact. One script, no opinions. The breakdown ended the debate:&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%2Fadybujwsj8c63r4j1ovq.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%2Fadybujwsj8c63r4j1ovq.png" alt=" " width="800" height="259"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Detection was 15%. The real leak — 55% — was downstream: the person was found and tracked, but the pose signal was too weak for our kinematics to register the impact. We'd been paving the wrong road. Lesson: decompose before you invest.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;04 FIXING THE REAL BOTTLENECK&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Sharper pose signal broke a ceiling we'd been stuck under&lt;/p&gt;

&lt;p&gt;Now aimed at the right target, the fixes were cheap and pure-software: upscale the pose crop (256 → 384), raise model precision (complexity 1 → 2), and stop cropping off the legs with an asymmetric crop bias. No data. No GPU. This wasn't moving an operating point — it was a genuinely cleaner signal, and it broke past an F1 ceiling of 0.54 that pure threshold-tuning had never cracked.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;05THE TRAP&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The improvement first looked like a regression&lt;/p&gt;

&lt;p&gt;We flipped on four improvements at once and recall dropped: 0.769 → 0.692. The tempting move: lower the detection threshold until the number looks good again. That would have papered over a real defect instead of finding it. So we ran a clean one-at-a-time ablation.&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%2F54co5lzp92xo05vx2w7z.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%2F54co5lzp92xo05vx2w7z.png" alt=" " width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the same lesson as the detector detour, one layer deeper: the number lying to you is more dangerous than the number that's low. Ablation is how you tell them apart.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;06THE RESULT&lt;/em&gt;&lt;br&gt;
0.56 → 0.86, and the weak sports came home&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%2Fnbtmkifmx6aqk0s7l4cd.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%2Fnbtmkifmx6aqk0s7l4cd.png" alt=" " width="800" height="736"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;● Shipping note  Precision sits at 0.51 — a few extra false highlights. For a highlight reel that's the right trade: better to over-catch than miss the goal. We tighten it after beta.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;07WHAT WE'D TATTOO ON OUR ARM&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Three lessons, paid for in wasted GPU hours&lt;/p&gt;

&lt;p&gt;i.&lt;br&gt;
Decompose before you invest. We burned a cloud fine-tune chasing a bottleneck that was 15% of the problem. A one-afternoon failure breakdown would have redirected the whole week.&lt;/p&gt;

&lt;p&gt;ii.&lt;br&gt;
Ablate one change at a time. Four fixes at once hid a regression inside a net gain. Isolation named the single culprit in one pass.&lt;/p&gt;

&lt;p&gt;iii.&lt;br&gt;
Distrust the number that recovers too easily. Lowering a threshold would have masked the video-mode bug. The convenient fix and the correct fix are rarely the same move.&lt;/p&gt;

&lt;h1&gt;
  
  
  the winning config — all software, no new data
&lt;/h1&gt;

&lt;p&gt;z_threshold: 2.0        # impact sensitivity&lt;br&gt;
video_mode: false     # the one flag that was the culprit&lt;br&gt;
crop_size: 384         # sharper pose signal&lt;br&gt;
model_complexity: 2    # pose precision&lt;br&gt;
bottom_bias: 0.15      # stop cropping off the legs&lt;br&gt;
tcn_rescue: on         # classifier rescues weak candidates&lt;/p&gt;

</description>
      <category>ai</category>
      <category>deeplearning</category>
      <category>machinelearning</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
