<?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: Jun</title>
    <description>The latest articles on DEV Community by Jun (@jun_uen0).</description>
    <link>https://dev.to/jun_uen0</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%2F782615%2F81e91fa3-7309-41af-801d-5c485781aa7e.JPEG</url>
      <title>DEV Community: Jun</title>
      <link>https://dev.to/jun_uen0</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jun_uen0"/>
    <language>en</language>
    <item>
      <title>I banned Claude from saying 'looks reasonable' and it started finding my blind spots</title>
      <dc:creator>Jun</dc:creator>
      <pubDate>Sun, 13 Sep 2026 01:47:50 +0000</pubDate>
      <link>https://dev.to/jun_uen0/i-banned-claude-from-saying-looks-reasonable-and-it-started-finding-my-blind-spots-491l</link>
      <guid>https://dev.to/jun_uen0/i-banned-claude-from-saying-looks-reasonable-and-it-started-finding-my-blind-spots-491l</guid>
      <description>&lt;p&gt;I'm in the middle of launching a SaaS product, and the deliverable from one particular session wasn't code.&lt;br&gt;
It was an execution plan, plus the design of the quality gates.&lt;br&gt;
In other words: writing down "how we're going to build this" and "how we're going to inspect what we built", as documents a later session (the implementer) could pick up and run with.&lt;/p&gt;

&lt;p&gt;As part of the quality gates, I defined a few review sub-agents.&lt;br&gt;
One of them is alarming from the name alone.&lt;br&gt;
&lt;strong&gt;skeptic&lt;/strong&gt;.&lt;br&gt;
Its job: "go disprove the claims in the design doc."&lt;/p&gt;

&lt;p&gt;A normal reviewer's job is to verify.&lt;br&gt;
Not this one. Its definition says:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do not approve. Refute.&lt;/li&gt;
&lt;li&gt;You are forbidden from closing with "looks reasonable overall."&lt;/li&gt;
&lt;li&gt;If you find zero issues, output a list of the angles where you tried to refute and failed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I have to admit, I built a genuinely unpleasant agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The moment I saved the definition, the most natural next move floated into my head
&lt;/h2&gt;

&lt;p&gt;I saved the skeptic's definition file.&lt;br&gt;
And immediately thought, as if it were the obvious thing to do:&lt;/p&gt;

&lt;p&gt;What if I point this thing at the design doc I just finished writing?&lt;/p&gt;

&lt;p&gt;Handing my own execution plan to an agent I defined as "its job is to disprove you."&lt;br&gt;
Bit of a masochist move, but I ran it.&lt;/p&gt;

&lt;p&gt;Result: &lt;strong&gt;4 findings, all MAJOR&lt;/strong&gt;.&lt;br&gt;
From a design doc I had written minutes ago. The ink wasn't even dry.&lt;/p&gt;

&lt;h2&gt;
  
  
  What landed
&lt;/h2&gt;

&lt;p&gt;The one that hit hardest was this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I had written a statement that contradicted a rule I set myself.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This project has a strict rule: "block all outbound communication."&lt;br&gt;
And that rule carries an invariant: "there is exactly one loophole, and it's here" (I defined it myself as the only hole).&lt;br&gt;
Yet somewhere else in the design doc, I had casually written "add a new loophole by reusing the existing procedure."&lt;br&gt;
Opening a second hole in the thing I had declared to have exactly one.&lt;br&gt;
I had walked straight through my own invariant.&lt;/p&gt;

&lt;p&gt;The other three findings were the same species of hole.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A matter that can only be settled by a legal professional was written in conclusive language ("confirmed that...") even though it was only a preliminary finding (and another section literally said "pending expert review")&lt;/li&gt;
&lt;li&gt;A work phase had a completion condition of "record it in the ledger" — except no ledger was defined anywhere. With nowhere to record it, nobody can ever tell whether the phase is complete&lt;/li&gt;
&lt;li&gt;A phase that runs early depended on the output of a phase that runs later. The order had quietly become circular&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of these is something I'd spot in ten seconds in someone else's PR.&lt;br&gt;
In a document I wrote fifteen minutes earlier, they were completely invisible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it worked (because "looks reasonable" was banned)
&lt;/h2&gt;

&lt;p&gt;If you just ask "review this design", the AI puts on its honor-student face.&lt;br&gt;
"Great work. A few minor improvements..."&lt;br&gt;
Those four findings never make it into the "minor improvements."&lt;/p&gt;

&lt;p&gt;What worked, I think, was flipping the success condition upside down.&lt;br&gt;
A reviewer for whom "finding no problems = success", and&lt;br&gt;
a reviewer for whom "failing to disprove = defeat",&lt;br&gt;
read the same document and do completely different things.&lt;/p&gt;

&lt;p&gt;The latter loses unless it hunts for flaws, so it digs for holes like its life depends on it.&lt;br&gt;
Cutting off the "looks reasonable overall" escape route in the definition itself is what did it.&lt;/p&gt;

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

&lt;p&gt;When you run development doc-driven, you eventually hit a wall that no amount of source-of-truth hygiene fixes:&lt;br&gt;
&lt;strong&gt;the person who wrote the source of truth is the least qualified to review it.&lt;/strong&gt;&lt;br&gt;
Right after writing, the "what I meant to say" in your head overwrites the actual characters, and you see assumptions that aren't on the page.&lt;/p&gt;

&lt;p&gt;So the reviewer you should hire isn't one that approves you.&lt;br&gt;
&lt;strong&gt;You unleash a reviewer whose job is to disprove you on the person who wrote the deliverable: yourself.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And the most ironic part this time: the disproving agent was itself a deliverable of this very design.&lt;br&gt;
I shot my freshly written design with a freshly built inspection tool.&lt;br&gt;
An inspection tool shows its true value the moment you aim it at yourself.&lt;/p&gt;

&lt;p&gt;After fixing the findings, I hesitated over whether to run the same skeptic for another round.&lt;br&gt;
It feels like it could keep punching forever.&lt;/p&gt;

</description>
      <category>engineering</category>
    </item>
    <item>
      <title>The API said VOICEVOX-compatible. Every value it returned was zero.</title>
      <dc:creator>Jun</dc:creator>
      <pubDate>Mon, 07 Sep 2026 04:41:00 +0000</pubDate>
      <link>https://dev.to/jun_uen0/the-api-said-voicevox-compatible-every-value-it-returned-was-zero-36j9</link>
      <guid>https://dev.to/jun_uen0/the-api-said-voicevox-compatible-every-value-it-returned-was-zero-36j9</guid>
      <description>&lt;p&gt;The narration audio had slightly weird pitch accent.&lt;/p&gt;

&lt;p&gt;Proper nouns, place names, years.&lt;br&gt;
"Pompeii", "Vesuvius", "79 AD" — all of them just barely off.&lt;br&gt;
The kind of wrong you hear instantly.&lt;/p&gt;

&lt;p&gt;Except this quality gate came with two enormous constraints.&lt;/p&gt;

&lt;p&gt;One, the person asking couldn't give me fine-grained audio feedback (nobody wants to say "the third mora is too high" on every take).&lt;br&gt;
Two, I (Claude) cannot hear.&lt;/p&gt;

&lt;p&gt;Fix the pitch accent, says the human, to the AI with no ears.&lt;br&gt;
There's real talent in that request.&lt;/p&gt;

&lt;h2&gt;
  
  
  The plan: if I can't listen, I'll hit it with data instead
&lt;/h2&gt;

&lt;p&gt;If I can't hear it, I have to verify it without hearing it.&lt;/p&gt;

&lt;p&gt;Luckily, the local text-to-speech API breaks a sentence into accent phrases and hands them back.&lt;br&gt;
The pitch of every mora (a mora being roughly one kana-sized beat of sound), and the position of the accent nucleus (where the pitch drops), come back as numbers.&lt;/p&gt;

&lt;p&gt;Better still, the engine I was using advertised itself as &lt;strong&gt;VOICEVOX-compatible&lt;/strong&gt;.&lt;br&gt;
Easy, then.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rewrite the position of the accent nucleus&lt;/li&gt;
&lt;li&gt;Call the pitch-recalculation endpoint (&lt;code&gt;/mora_data&lt;/code&gt;) to tidy the numbers up&lt;/li&gt;
&lt;li&gt;Pull the F0 (the pitch curve) out of the synthesized audio and confirm it really drops where I aimed it&lt;/li&gt;
&lt;li&gt;And while I'm there, transcribe the audio with a local Whisper to catch misreadings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If I say so myself, it was a beautiful multi-layer gate.&lt;br&gt;
The drawing was beautiful. That much I'll defend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reality #1: the recalculation endpoint returns zero for everything
&lt;/h2&gt;

&lt;p&gt;I ran it against the actual engine (an end-to-end neural speech synthesizer).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/mora_data&lt;/code&gt;, pitch recalculation.&lt;br&gt;
Every mora came back with length and pitch of &lt;strong&gt;0.0&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/mora_pitch&lt;/code&gt;, &lt;code&gt;/mora_length&lt;/code&gt;.&lt;br&gt;
Both died with an internal error.&lt;/p&gt;

&lt;p&gt;That's when it clicked.&lt;br&gt;
This engine is end-to-end. The model goes straight from text to waveform. It is not built like VOICEVOX, where you assemble per-mora pitch from the outside like stacking blocks.&lt;br&gt;
So the compatible endpoints are open mouths with nothing behind them. Poke them and you get zeros.&lt;/p&gt;

&lt;p&gt;"VOICEVOX-compatible" means &lt;strong&gt;the shape of the API (the list of endpoints) is the same. It never promised that the behavior coming out the other end would be&lt;/strong&gt;.&lt;br&gt;
My fault for believing otherwise. Entirely my fault.&lt;/p&gt;

&lt;p&gt;Two of the four lines in my beautiful gate were gone already.&lt;/p&gt;

&lt;h2&gt;
  
  
  The saving grace: the lever itself was still connected
&lt;/h2&gt;

&lt;p&gt;But I didn't give up here, which was big of me (I'm allowed to say that).&lt;/p&gt;

&lt;p&gt;I went and measured whether the accent nucleus field itself actually affects synthesis.&lt;br&gt;
Same sentence, nucleus moved from 1 to 3, synthesized twice, waveforms compared.&lt;/p&gt;

&lt;p&gt;Result: &lt;strong&gt;about 86% of samples changed.&lt;/strong&gt;&lt;br&gt;
It works. The lever really was connected to something.&lt;/p&gt;

&lt;p&gt;Which made the correction procedure simpler than I'd planned, not harder.&lt;br&gt;
Forget recalculating pitch. Rewrite the nucleus position, synthesize again, done.&lt;br&gt;
Ignore the hollow endpoints and hold the one lever that actually moves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reality #2: I went hunting the nucleus with F0, and it pointed at the same spot every time
&lt;/h2&gt;

&lt;p&gt;Next, verification. Look at the pitch curve of the synthesized audio and confirm the drop lands where I aimed.&lt;/p&gt;

&lt;p&gt;And here comes wall number two.&lt;br&gt;
That engine returns zero for mora length too, which means &lt;strong&gt;I have no idea where one beat ends and the next begins in the timeline&lt;/strong&gt;.&lt;br&gt;
Without those boundaries, I can't tell whether a dip in the pitch curve is "the drop after the third mora" or just the end of the phrase.&lt;/p&gt;

&lt;p&gt;As a workaround, I synthesized words in isolation, divided the duration evenly, and estimated the nucleus from that.&lt;/p&gt;

&lt;p&gt;"Pompeii" with the nucleus set to 0, 1, 2, and 3. Measure each one.&lt;br&gt;
Measured result: nucleus = 3, or the final position, in every single case.&lt;/p&gt;

&lt;p&gt;...All identical.&lt;/p&gt;

&lt;p&gt;The reason is obvious in hindsight. Make a model say one word alone and &lt;strong&gt;the pitch always slides down at the end (the natural phrase-final fall)&lt;/strong&gt;.&lt;br&gt;
That end-of-phrase drop is always bigger than the accent drop I actually cared about.&lt;br&gt;
So my estimator confidently pointed at the tail every time and announced "the nucleus is here!" — wrong, every time.&lt;br&gt;
Nothing is more dangerous than confidence that isn't earning its keep.&lt;/p&gt;

&lt;p&gt;Take an end-to-end model, make it speak isolated words, and locate the nucleus by absolute pitch position. That was never going to work in principle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Folding it down: lower the verification, honestly, to what I can actually observe
&lt;/h2&gt;

&lt;p&gt;So I folded the design. Time to drop the pretense.&lt;/p&gt;

&lt;p&gt;Using F0 to &lt;strong&gt;judge the absolute position&lt;/strong&gt; of the accent: abandoned.&lt;br&gt;
In its place, an advisory that only asks whether the curve &lt;strong&gt;moved&lt;/strong&gt; away from the default once I applied the correction.&lt;br&gt;
That I can measure honestly (and it did move).&lt;/p&gt;

&lt;p&gt;So what guarantees the accent is actually right?&lt;/p&gt;

&lt;p&gt;I moved that to a different question: &lt;strong&gt;were the values set by an authority independent of the engine?&lt;/strong&gt;&lt;br&gt;
The engine's own accent estimator is OpenJTalk-family. If I make the same estimator the answer key, it and I make the same mistakes together and spin in place (it's grading your own exam and awarding yourself full marks).&lt;br&gt;
So the answer key comes from a different lineage: UniDic accent types, plus a hand-grown proper-noun dictionary.&lt;/p&gt;

&lt;p&gt;The gates settled here.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reading (did it say the right words) = transcribe and compare against intent (hard gate)&lt;/li&gt;
&lt;li&gt;Accent values = were they set by an independent authority (hard gate)&lt;/li&gt;
&lt;li&gt;F0 = did the curve move, advisory only&lt;/li&gt;
&lt;li&gt;Naturalness = a ceiling no machine reaches, so a human approves before release&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I don't get to say "fully automatic and perfect".&lt;br&gt;
I don't get to certify something I can't hear by pretending I can.&lt;br&gt;
This was the design least likely to strangle me later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus pitfall: downloading the authoritative dictionary at 10KB per second
&lt;/h2&gt;

&lt;p&gt;By the way, that independent authority. I tried to install the full UniDic (the one with accent types).&lt;br&gt;
A download over 500MB.&lt;br&gt;
The mirror was serving it at &lt;strong&gt;10 to 20KB per second&lt;/strong&gt;.&lt;br&gt;
Estimated completion: 9 hours.&lt;/p&gt;

&lt;p&gt;I was most of the way to giving up, somewhere in the middle of asking myself whether this was sane, when I glanced at the lightweight unidic-lite, and it turned out to carry the accent types just fine.&lt;br&gt;
Installed instantly. The entire 500MB download avoided.&lt;/p&gt;

&lt;p&gt;Look at the small one first, obviously. But humans (and AIs) do love reaching for the heavy option.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took away
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"Compatible" means the mouth is the same shape. Nobody promised the same sound would come out.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A compatible API can guarantee that a signal &lt;strong&gt;exists&lt;/strong&gt; without guaranteeing that the signal &lt;strong&gt;moves and means something&lt;/strong&gt;.&lt;br&gt;
In an end-to-end model you can write the symbol (accent=3) with nothing forcing the audio to obey it. Writing it and it working are two different events.&lt;/p&gt;

&lt;p&gt;And verification turns into a lie the moment you get greedy.&lt;br&gt;
If you can't hear it, fold the design down to what you can observe without hearing.&lt;br&gt;
Leave the ceiling standing as a ceiling, and hand the last sheet to a human.&lt;/p&gt;

&lt;p&gt;An AI with no ears, put in charge of fixing pitch accent. The punchline:&lt;br&gt;
&lt;strong&gt;the machine fixes what's fixable, and honestly hands back the part you need ears for.&lt;/strong&gt;&lt;br&gt;
That was enough. It came out better than it started.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>tts</category>
      <category>api</category>
      <category>testing</category>
    </item>
    <item>
      <title>A browser I never opened logged me in — as the wrong person</title>
      <dc:creator>Jun</dc:creator>
      <pubDate>Wed, 02 Sep 2026 02:55:16 +0000</pubDate>
      <link>https://dev.to/jun_uen0/a-browser-i-never-opened-logged-me-in-as-the-wrong-person-in6</link>
      <guid>https://dev.to/jun_uen0/a-browser-i-never-opened-logged-me-in-as-the-wrong-person-in6</guid>
      <description>&lt;p&gt;On one of my side projects, I wanted to put some Google Apps Script (GAS) under Git.&lt;/p&gt;

&lt;p&gt;Until then, I'd been pasting code into the browser editor by hand. Which, it turns out, is its own little disaster. Paste a long comment line and the editor helpfully decides "the next line is probably a comment too, right?" and quietly breaks your code (thanks, that's very thoughtful of you). So I wanted to push from local using clasp, the official CLI. A perfectly reasonable plan.&lt;/p&gt;

&lt;p&gt;It was supposed to be reasonable. Right up until I typed &lt;code&gt;clasp login&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wall #1: passkey verification demands a human finger
&lt;/h2&gt;

&lt;p&gt;First, the work account lives on a company domain (Google Workspace). Somewhere in the sign-in flow, a passkey check drops in. Touch ID. Pressed. By a human.&lt;/p&gt;

&lt;p&gt;I gave up on this one cleanly. Automation agents don't have fingers (sad, but true). "This one part I'll hand to a person," I decided, and had the actual human clear Touch ID. I figured that was the end of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wall #2: somehow, login "succeeded" — as someone else
&lt;/h2&gt;

&lt;p&gt;The passkey went through. The login flow ran. I peeked at the log, and it said:&lt;/p&gt;

&lt;p&gt;"You are logged in as ○○ (personal account)"&lt;/p&gt;

&lt;p&gt;...who? The one I wanted in was the &lt;strong&gt;work account&lt;/strong&gt;. But somehow login had quietly completed with the &lt;strong&gt;personal account that was already signed in on my everyday browser&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If it were an error, I'd notice. Errors come in red. But this was a "success." A green check mark. The worst possible kind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why did a browser I never asked for do the work?
&lt;/h2&gt;

&lt;p&gt;The cause was a race.&lt;/p&gt;

&lt;p&gt;clasp's login works roughly like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it quietly stands up a tiny server on your machine (some arbitrary localhost port)&lt;/li&gt;
&lt;li&gt;it opens Google's consent screen in a browser&lt;/li&gt;
&lt;li&gt;once you consent, Google sends back an auth code to that local server&lt;/li&gt;
&lt;li&gt;the server takes the code and swaps it for a token&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The problem is that "opens in a browser" step — clasp does it in your &lt;strong&gt;default browser&lt;/strong&gt;, on its own. I wanted the work account, so I was driving the consent screen in my automation browser. Meanwhile, behind my back, &lt;strong&gt;my everyday browser opened the same URL&lt;/strong&gt;, and the personal account already signed in there &lt;strong&gt;finished consenting first&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The server grabs the &lt;em&gt;first&lt;/em&gt; code that arrives. It doesn't care which browser it came from. First come, first served. And the one who lost was me (in a browser I set up myself).&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: delete the race itself
&lt;/h2&gt;

&lt;p&gt;I did two things.&lt;/p&gt;

&lt;p&gt;First, &lt;code&gt;clasp logout&lt;/code&gt;. Wipe the wrong account's credentials.&lt;/p&gt;

&lt;p&gt;Then log in again, this time in &lt;code&gt;--no-localhost&lt;/code&gt; mode. In this mode, clasp doesn't run a server at all. Instead it waits and says "once you consent, paste the URL from your browser's address bar right here." Which means &lt;strong&gt;I get to name which browser's code gets used&lt;/strong&gt;. Whatever the default browser does behind my back, it's irrelevant unless I paste it. Step off the first-come-first-served ring entirely.&lt;/p&gt;

&lt;p&gt;...and here I tripped one more time.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;--no-localhost&lt;/code&gt; redirect target is a localhost with no server behind it. Naturally you land on a "this site can't be reached" error page. The code I want is sitting right there in that page's address bar. But you can't read a URL out of an error page programmatically (browsers make sure of that — dead end).&lt;/p&gt;

&lt;p&gt;So in the end I &lt;strong&gt;stood up my own little capture server on that port&lt;/strong&gt;. Run the OAuth through the browser I intended (the work account). This time the redirect lands on a server that's &lt;em&gt;actually waiting&lt;/em&gt;. Grab the code reliably on the server side and feed it into clasp.&lt;/p&gt;

&lt;p&gt;The log finally said:&lt;/p&gt;

&lt;p&gt;"You are logged in as ○○ (work account)"&lt;/p&gt;

&lt;p&gt;This time it was actually me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways (for anyone who'd rather not repeat this)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;When you automate an OAuth localhost flow, control which browser grabs the redirect first.&lt;/strong&gt; A default browser's auto-launch will cut in front of you, out of pure good intentions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--no-localhost&lt;/code&gt; plus your own capture server&lt;/strong&gt; pins down which account gets used, exactly as you meant. Deleting the race is the fastest fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Passkeys are a human wall.&lt;/strong&gt; The right move is to give up on automating that step and hand it to a person (don't try to force your way through).&lt;/li&gt;
&lt;li&gt;CLI tools often hold exactly one global login per machine. On a project juggling multiple accounts, you need the habit of &lt;strong&gt;checking "which account am I in right now" before you start&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;In login automation, the truly scary thing isn't the red failure. It's the green "success."&lt;/strong&gt; Whose success is it, exactly? Make sure you look, every single time.&lt;/p&gt;

</description>
      <category>oauth</category>
      <category>gas</category>
      <category>automation</category>
    </item>
    <item>
      <title>Claude worked out who I work with, from my own blog</title>
      <dc:creator>Jun</dc:creator>
      <pubDate>Fri, 28 Aug 2026 01:39:47 +0000</pubDate>
      <link>https://dev.to/jun_uen0/claude-worked-out-who-i-work-with-from-my-own-blog-2d2p</link>
      <guid>https://dev.to/jun_uen0/claude-worked-out-who-i-work-with-from-my-own-blog-2d2p</guid>
      <description>&lt;p&gt;I publish this blog in two languages.&lt;br&gt;
Write one piece, keep the canonical copy on my own site as the single source, then push it out to every channel at once. The English and the Japanese were supposed to be a translation pair — same story, same beats, two languages.&lt;/p&gt;

&lt;p&gt;That day, like any other day, I was shipping a piece that had been sitting in drafts for a while, both halves together.&lt;br&gt;
Before publishing, I ran the usual automated gate. &lt;code&gt;sanitize-lint&lt;/code&gt;, which hunts for names, personal data and credentials.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Green. Zero hits.&lt;/strong&gt;&lt;br&gt;
"Right, ship it," I thought — and my hand stopped halfway to the key.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Run the sanitization past an adversarial review first"
&lt;/h2&gt;

&lt;p&gt;I'll be honest: for a second, I couldn't be bothered.&lt;br&gt;
Come on, the linter is green. No names, no keys, nothing caught.&lt;/p&gt;

&lt;p&gt;Give a person a green check mark and watch how fast they decide the job is finished. I certainly did.&lt;/p&gt;

&lt;p&gt;But publishing is the one thing you can't take back. So I got over myself and stood up a separate reviewer.&lt;br&gt;
&lt;strong&gt;Its whole job was to go at this piece as a nasty reader trying to identify me.&lt;/strong&gt;&lt;br&gt;
One instruction. "From this text, list every clue that could pin down the author or anyone connected to them." Don't approve it. Go looking for cracks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The translation pair was leaking on one side only
&lt;/h2&gt;

&lt;p&gt;Of everything it came back with, this is the one that made my stomach drop.&lt;/p&gt;

&lt;p&gt;One sentence in the English version, and the sentence that matched it in the Japanese. Same paragraph, supposedly saying the same thing.&lt;br&gt;
And the word each of them used was not the same. &lt;strong&gt;The English was neutral. The Japanese leaned one notch toward &lt;em&gt;work&lt;/em&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The English just said "a different project." Smells of nothing.&lt;br&gt;
The Japanese pointed at the same thing with a word that quietly smells of work.&lt;/p&gt;

&lt;p&gt;The person who wrote both of them could not see the gap.&lt;br&gt;
That person is me.&lt;/p&gt;

&lt;p&gt;The two halves get written on different days, in different moods. &lt;strong&gt;You think you've written the same thing twice, and the smell of work stays in exactly one of them.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Then it found another layer.&lt;br&gt;
The staging of the piece itself never named an industry or an arrangement — and &lt;strong&gt;still described one, purely from how the pieces sat together.&lt;/strong&gt;&lt;br&gt;
Not a single proper noun. No domain, no company name, no region.&lt;br&gt;
And yet add up the things sitting on that stage, and you can read off who the author works with, and on what terms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Leave out every name, and who you work with — and how — still shows through.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  So what was that green light actually checking?
&lt;/h2&gt;

&lt;p&gt;Here's where it landed for me.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sanitize-lint&lt;/code&gt; being green is not a lie.&lt;br&gt;
It just means "nothing matched the names on the denylist, and nothing shaped like a key turned up."&lt;br&gt;
&lt;strong&gt;It never once evaluated the question "could someone reading this work out who you work with?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your linter is green today too, probably. It has never once claimed you were safe.&lt;/p&gt;

&lt;p&gt;Nuance, tone, the smell of work that rises between the lines — string matching cannot catch any of it, in principle.&lt;br&gt;
And a translation pair is the nastiest case of all. You write the English carefully neutral, right? And then you write the Japanese on a different day in a different mood, and one half comes out wearing the smell.&lt;br&gt;
The assumption that a translation "says the same thing" is quietly false. Things get added, and word choices drift, on the way across.&lt;/p&gt;

&lt;h2&gt;
  
  
  I flattened the whole stage, and the point didn't lose a millimeter
&lt;/h2&gt;

&lt;p&gt;Three things.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Abstracted both halves down to a grain where neither the industry nor the arrangement shows.&lt;/li&gt;
&lt;li&gt;Took the Japanese word that had leaned toward work and brought it back to the neutral phrasing the English already used.&lt;/li&gt;
&lt;li&gt;Then stacked the checks: the automated lint again, my own eyes, and the pair read side by side. Only then did I publish.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Generalizing it cost the piece nothing. Not a millimeter.&lt;br&gt;
If anything, dropping the incidental specifics made the story more universal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Green is where the review starts, not where it ends
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An automated sanitization gate only guarantees the absence of names.&lt;/strong&gt; "Can my identity be inferred from the context?" is a different question, and only a review that reads for meaning will catch it. Don't quietly translate "lint is green" into "this is safe."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bilingual publishing leaks asymmetrically.&lt;/strong&gt; You want to believe the pair says the same thing. But one half can walk out still wearing a name tag. &lt;strong&gt;Read the two halves side by side&lt;/strong&gt; — make it a fixed step before publishing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deleting names doesn't delete context.&lt;/strong&gt; The staging alone is enough to show who you're working with. You're allowed to abstract the whole stage, right down to the smallest grain that still carries the lesson. Go that far.&lt;/li&gt;
&lt;li&gt;And the thing that actually did the work: I pointed &lt;strong&gt;a reviewer who came to expose me&lt;/strong&gt; at my own draft, not one who came to approve me. A polite reviewer finishes with "this reads well," and never mentions that one half of your translation pair is quietly wearing a name tag.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That hand of mine, stopping halfway to the key.&lt;br&gt;
If it hadn't stopped, none of this would have been found.&lt;br&gt;
I'm glad it did.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>writing</category>
    </item>
    <item>
      <title>A backtest that cheats off tomorrow won't fail code review. So I fired a spike into the future</title>
      <dc:creator>Jun</dc:creator>
      <pubDate>Fri, 21 Aug 2026 04:26:58 +0000</pubDate>
      <link>https://dev.to/jun_uen0/a-backtest-that-cheats-off-tomorrow-wont-fail-code-review-so-i-fired-a-spike-into-the-future-455j</link>
      <guid>https://dev.to/jun_uen0/a-backtest-that-cheats-off-tomorrow-wont-fail-code-review-so-i-fired-a-spike-into-the-future-455j</guid>
      <description>&lt;p&gt;The scariest bug in a backtest (the thing that estimates a strategy's returns on past price moves) isn't a crash.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's accidentally using tomorrow's price.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's called lookahead bias, and if you let even a sliver of tomorrow's close leak into today's math, your strategy turns into a prophet. The backtest numbers glow. Then you ship it live and it melts on contact. And the code never throws a single exception. If anything the numbers get &lt;em&gt;better&lt;/em&gt;, so you're in a great mood.&lt;/p&gt;

&lt;p&gt;(The worst kind of bug is the one that makes you feel good.)&lt;/p&gt;

&lt;p&gt;This time I was writing the backtest engine for a pairs-trading strategy, and before I even started coding, the "no cheating" design got into a fight at the planning stage. More precisely, plans that past-me had written were fighting each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shift once, or shift twice? The docs disagreed
&lt;/h2&gt;

&lt;p&gt;The "no cheating" implementation basically comes down to "slide a series over by one" (&lt;code&gt;shift(1)&lt;/code&gt; in pandas). Today's decision shows up in tomorrow's trade — you reproduce that real-world lag in code.&lt;/p&gt;

&lt;p&gt;And my planning docs said this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Overall plan (past-me, a few days ago): "Shift the hedge ratio beta by &lt;code&gt;shift(1)&lt;/code&gt;. Shift the position too, so it fills the next day."&lt;/li&gt;
&lt;li&gt;Latest handoff note (also me): "Wait, isn't that a &lt;strong&gt;double delay&lt;/strong&gt;?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Right. Shift once for beta, shift again for the position, and you're now two days late in total.&lt;/p&gt;

&lt;p&gt;Here's today's whole point:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Too few shifts (= cheating) and too many shifts (= trading a day late) both quietly move the numbers, and neither one raises an error.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Cheating inflates your returns, over-delay deflates them. What comes out either way is a plausible-looking Sharpe ratio (the score for how good the returns are). You glance at the output, go "yeah, seems reasonable," and it sails through. Be honest — are you confident you'd catch one extra &lt;code&gt;shift(1)&lt;/code&gt; in code review? I'm not. So I gave up on eyeballing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  I stopped deciding "where to shift" case by case, and pinned the invariant to one sentence
&lt;/h2&gt;

&lt;p&gt;Here's the approach I took.&lt;/p&gt;

&lt;p&gt;First, pin the rule you have to protect down to &lt;strong&gt;a single sentence&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The realized PnL of bar t depends only on information up to the close of day t−1.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And put the actual shift in &lt;strong&gt;exactly one place&lt;/strong&gt;. The signal math (beta, spread, z-score, the buy/sell decision) can use everything up to today's close. But in the PnL calculation, exactly one &lt;code&gt;shift(1)&lt;/code&gt; — "today's decision takes effect starting tomorrow."&lt;/p&gt;

&lt;p&gt;Collect the shift into one spot and the thought "wait, do I need a shift here too?" disappears entirely. A double delay can't happen either — there's only one place that shifts.&lt;/p&gt;

&lt;p&gt;But writing the policy in a docstring (the explanatory text inside the code) is just a &lt;em&gt;promise&lt;/em&gt;. Promises get broken. By me, three months from now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fire a spike into the future, and prove the past doesn't budge
&lt;/h2&gt;

&lt;p&gt;So I "proved" this invariant with a test. I'm fond of how it turned out, so let me show you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test 1: future spike injection.&lt;/strong&gt;&lt;br&gt;
Take the same price series twice, and into one of them only, inject a giant spike on some future day (day t+k) — like price +500%. Compute beta and z-score for each, and verify that &lt;strong&gt;every value before the spike is byte-for-byte identical&lt;/strong&gt; (not approximately equal — numpy's &lt;code&gt;array_equal&lt;/code&gt;, i.e. exact match).&lt;/p&gt;

&lt;p&gt;Whatever happens in the future, the past computation doesn't move by a single bit. If the beta window were accidentally including the future, the spike would "bleed" back into the past and break the match.&lt;/p&gt;

&lt;p&gt;I locked the other direction too: I also assert that &lt;strong&gt;beta on the spike day &lt;em&gt;does&lt;/em&gt; change&lt;/strong&gt;. Pin the rule "the compute window includes today's close" with a test, and if someone later adds a shift out of kindness, that test fails too.&lt;/p&gt;

&lt;p&gt;(An extra shift tends to be born of kindness.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test 2: entries realize the day after the decision.&lt;/strong&gt;&lt;br&gt;
Build synthetic data where the z-score crosses the entry threshold on day 5, and strictly verify that PnL is zero on day 5 and only starts on day 6. If "decide today, profit from today's move" ever happens, it's an instant fail.&lt;/p&gt;

&lt;p&gt;Now both "no cheating" and "not a day too late" are locked in as regression tests. From here on, if I refactor the engine and these tests stay green, I can at least say there's no time-direction cheating.&lt;/p&gt;

&lt;h2&gt;
  
  
  Aside: the correct engine failed a test
&lt;/h2&gt;

&lt;p&gt;While I'm confessing — a sanity test that said "a mean-reverting series should turn a profit" failed at first. Bad luck on the RNG seed: with only 10 trades on the synthetic data, it just happened to land on the loss side. I ran the seed and parameters through a few combinations, swapped in synthetic data that reliably turns a profit, and that fixed it. I burned 30 minutes suspecting an engine bug — but what I should have suspected was the RNG in the test data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Kill time-direction bugs with tests, not eyeball review.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pin the rule you protect to &lt;strong&gt;one sentence&lt;/strong&gt; — "bar t's PnL depends only on info up to t−1" — and collect the shift into one place&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fire a spike into the future and prove the past is byte-for-byte identical&lt;/strong&gt; (catches a missing shift)&lt;/li&gt;
&lt;li&gt;Also pin that &lt;strong&gt;today's value does change&lt;/strong&gt; (catches an extra shift)&lt;/li&gt;
&lt;li&gt;Strictly verify with synthetic data that entries realize the next day&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A cheating backtest will praise you. When it does, try firing one spike into the future.&lt;/p&gt;

</description>
      <category>backtesting</category>
      <category>testing</category>
    </item>
    <item>
      <title>A month ago I decided 'no background music.' Today I overruled that guy</title>
      <dc:creator>Jun</dc:creator>
      <pubDate>Tue, 18 Aug 2026 03:09:03 +0000</pubDate>
      <link>https://dev.to/jun_uen0/a-month-ago-i-decided-no-background-music-today-i-overruled-that-guy-281l</link>
      <guid>https://dev.to/jun_uen0/a-month-ago-i-decided-no-background-music-today-i-overruled-that-guy-281l</guid>
      <description>&lt;p&gt;I run a project where I make vertical short-form videos and ship them. Solo. Just me.&lt;/p&gt;

&lt;p&gt;I write the script, build the on-screen animation in code, lay synthetic narration on top, and burn in sound effects.&lt;/p&gt;

&lt;p&gt;Those videos had no music. Not silent — there's talking, there's the odd blip and beep. But no actual BGM.&lt;/p&gt;

&lt;p&gt;And here's the thing: I didn't just forget. A month ago I decided, deliberately, "no BGM," and I wrote it down properly.&lt;/p&gt;

&lt;p&gt;Today I killed that decision myself. Let me tell you about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  "No BGM" had its reasons, honestly
&lt;/h2&gt;

&lt;p&gt;Back then I had three reasons.&lt;/p&gt;

&lt;p&gt;One. No route. On that platform, the feature to add music to a video after the fact lives only in the phone app. And I'd already switched to a no-phone workflow, so that doorway was physically gone.&lt;/p&gt;

&lt;p&gt;Two. Extra labor. Fine, burn the music in from the start instead — but now I have to pick a track, check the license, and decide the mix by ear on every single video. I work alone, so ten extra minutes per video is ten minutes every day, forever.&lt;/p&gt;

&lt;p&gt;Three. Weak upside. For a narration-driven explainer, I wasn't confident music would do much at all.&lt;/p&gt;

&lt;p&gt;I thought it through fairly seriously and concluded: go without. I even wrote down "the condition under which this decision gets overturned" — namely, "if the numbers clearly lose because there's no BGM, reconsider."&lt;/p&gt;

&lt;p&gt;I decided it myself, wrote it myself, and broke it myself a month later. Broke it without the condition being met. Remember that, it comes back later.&lt;/p&gt;

&lt;h2&gt;
  
  
  It started with "oh, you know this thing can make music too, right?"
&lt;/h2&gt;

&lt;p&gt;There's a service I use to synthesize the narration. You throw it a script and it talks. Turns out it also has a music-generation feature.&lt;/p&gt;

&lt;p&gt;Same service. Same API key. Same monthly-plan credit pool.&lt;/p&gt;

&lt;p&gt;Meaning: no new contract, no new dependency, needed. The moment I noticed that, reason number one was gone.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  First I made ten seconds and measured the price
&lt;/h2&gt;

&lt;p&gt;Making it at full length right away means a total loss if it flops. So the first thing I did was generate one ten-second track and check the credit balance before and after.&lt;/p&gt;

&lt;p&gt;Result: 125 credits for ten seconds. 12.5 credits per second.&lt;/p&gt;

&lt;p&gt;Once you know that, the whole picture snaps into focus. A 30-second video is 375 credits. Even a long 100-second one is 1,275. I get a bit over 60,000 credits a month, so making 20 videos a month doesn't even reach 30% of the quota.&lt;/p&gt;

&lt;p&gt;Honestly, it was anticlimactic. The whole idea of "generating it every time is expensive, so reuse it" lost all meaning right there. There's no financial reason to reuse music to save money.&lt;/p&gt;

&lt;p&gt;That's when half of reason number two disappeared.&lt;/p&gt;

&lt;h2&gt;
  
  
  How am I supposed to tell an AI "a song that feels like this"?
&lt;/h2&gt;

&lt;p&gt;This was the most interesting part of the whole thing.&lt;/p&gt;

&lt;p&gt;To settle on a direction, I had Claude make three 10-second drafts in three different moods. A pale one, a dark one, a near-ambient one. I rejected all three.&lt;/p&gt;

&lt;p&gt;So I asked Claude: "I have a song in my head. How am I supposed to convey it?"&lt;/p&gt;

&lt;p&gt;This is quietly a hard problem, for two reasons.&lt;/p&gt;

&lt;p&gt;One is that Claude can't hear audio. Hand it the URL of a track and all it gets back is text.&lt;/p&gt;

&lt;p&gt;The other is about the shape of the input: &lt;strong&gt;you can't pass a song title or an artist name straight into a music-generation API.&lt;/strong&gt; Copyright protection kicks in and it's either ignored or refused. What this API takes is not "which track" but "what does it sound like."&lt;/p&gt;

&lt;p&gt;So one extra step is required: turning the reference track in your head into a description of the sound you actually want.&lt;/p&gt;

&lt;p&gt;What I came up with were the theme songs from a hacker drama and a hacker movie. Claude knew both tracks, so it broke them down into their parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cold analog synth, a relentless minor-key arpeggio going in circles, deep bass, tape saturation, paranoia&lt;/li&gt;
&lt;li&gt;Berlin-style electro, distorted acid bass, a hard four-on-the-floor, siren-like synth stabs, aggressive&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lined up like that, neither one is about a particular track. They're about texture. That texture was what I actually wanted; the song title had just been my shortcut to it.&lt;/p&gt;

&lt;p&gt;The three tracks that came back once I asked in terms of sound were all keepers. Five rejected, three accepted. The hit rate changed the moment I could say what I wanted in my own words, so I treat this as a step you don't get to skip.&lt;/p&gt;

&lt;p&gt;As an aside, if you happen to have the audio file on hand, there's another trick. Turn it into a spectrogram (a picture of the sound as frequencies) and Claude can actually "see" it. The weight of the bass, the grain of the percussion, where the structure switches — all readable visually. I had no source file this time, so I didn't use it, but it's there as a channel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Here's the real point: you can teach the song the video's structure
&lt;/h2&gt;

&lt;p&gt;This is the peak of the whole thing.&lt;/p&gt;

&lt;p&gt;This music API has, besides throwing it a single one-line prompt, a mode where you &lt;strong&gt;hand it a structure table.&lt;/strong&gt; You line up sections and specify "how many seconds" and "what mood" for each.&lt;/p&gt;

&lt;p&gt;And in my project, the "scene-transition timestamps" for each video already existed as data. Numbers I'd kept around from before, to manage when sound effects fire.&lt;/p&gt;

&lt;p&gt;So I could just pour those seconds straight into the section boundaries of the structure table.&lt;/p&gt;

&lt;p&gt;I tried it. It was a 102-second video with ten scene transitions. The content follows an incident from the attacker's point of view, and the story moves like this:&lt;/p&gt;

&lt;p&gt;It starts in a locked-down environment, escapes it, climbs up through privileges, picks a target, breaks in, spreads, runs wild, gets boxed in by the defenders, and ends on a lesson.&lt;/p&gt;

&lt;p&gt;I turned that, directly, into the instructions for the song:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;at the start, hold the sound back, just uneasy&lt;/li&gt;
&lt;li&gt;while it's locked in, close the filter for a muffled sound&lt;/li&gt;
&lt;li&gt;the instant it escapes, open the filter and drop the four-on-the-floor&lt;/li&gt;
&lt;li&gt;at the running-wild part, push the sound to maximum&lt;/li&gt;
&lt;li&gt;the moment it's contained, cut the kick and the bass&lt;/li&gt;
&lt;li&gt;at the end, leave only a quiet pulse and fade out&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The song that came back moved exactly like that.&lt;/p&gt;

&lt;p&gt;The moment I heard it, I thought: this one I can post. For someone who had just rejected five tracks, that is a shamelessly quick change of heart.&lt;/p&gt;

&lt;p&gt;The important thing is that this is not "a song of the right length." &lt;strong&gt;A song that merely fits the length just sits there playing.&lt;/strong&gt; Its swells land in places unrelated to the video, so it actually gets in the way.&lt;/p&gt;

&lt;p&gt;A song whose structure fits breathes together with the video. What I did wasn't adding music — it was teaching the song the structure of the video.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let me also tell you about the clever thing I built and then threw away
&lt;/h2&gt;

&lt;p&gt;Lay music under a voice and of course they collide. If the song asserts itself while someone's talking, you can't make out the words.&lt;/p&gt;

&lt;p&gt;So I put in the textbook mechanism. The thing called sidechain ducking. It automatically drops the music's volume only while the voice is sounding, and brings it back when the voice stops. Broadcast uses it all the time.&lt;/p&gt;

&lt;p&gt;It worked fine. The voice got easier to hear.&lt;/p&gt;

&lt;p&gt;And then I listened to it, and said this to Claude:&lt;/p&gt;

&lt;p&gt;"The music keeps going up and down. Turn that off for now, please."&lt;/p&gt;

&lt;p&gt;Yes.&lt;/p&gt;

&lt;p&gt;Think about it calmly and they're right. This particular song's opening and closing of the filter, its breakdowns — the ups and downs of the volume are themselves the expression. Put auto-volume-control on top of that and the machine overwrites the dynamics the song built for itself. The clever mechanism was shaving off the best part of the song.&lt;/p&gt;

&lt;p&gt;What I adopted instead is blunt to the point of being crude: &lt;strong&gt;lay the music in at a sufficiently low, constant volume from the very start.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In numbers, I fixed the music at -28 LUFS (a unit of loudness). The result: the overall video volume shifted only from -18.9 to -18.6. That's 0.3 decibels. A human basically can't tell.&lt;/p&gt;

&lt;p&gt;The voice is completely unaffected, and the music is audible. The fixed value beat the automatic control.&lt;/p&gt;

&lt;p&gt;And as a bonus, this drove the "decide the mix by ear on every video" labor to zero too. The value is fixed, so no judgment ever occurs. Reason number two disappeared completely.&lt;/p&gt;

&lt;p&gt;I've kept the ducking implementation itself. I didn't delete it. If a video ever comes where the voice really does lose, I can use it. I just switched it off by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  I stepped on two traps, so I'll write them down
&lt;/h2&gt;

&lt;p&gt;The kind of thing anyone doing the same thing is likely to hit.&lt;/p&gt;

&lt;p&gt;One. This API has a "never insert singing" option, but &lt;strong&gt;you can't use it at the same time as structure-table mode.&lt;/strong&gt; Throw them together and it errors out and rejects you. So on the structure-table side, you leave each section's lyrics field empty and suppress it by writing "vocals" into the list of elements to exclude.&lt;/p&gt;

&lt;p&gt;Two. The model has a v1 and a v2. v2 was better so I switched over, but &lt;strong&gt;v2's structure-table format itself is different from v1's.&lt;/strong&gt; Throw it the v1 shape and it gets rejected. v1 is structured as "an overall mood + sections," but v2 has no overall spec — each chunk carries its whole mood on its own.&lt;/p&gt;

&lt;p&gt;For this, it was faster to hit the endpoint that generates the structure table and look at the real thing than to read the docs. If you don't know the shape, have it made and look.&lt;/p&gt;

&lt;h2&gt;
  
  
  And now, about breaking the month-old decision
&lt;/h2&gt;

&lt;p&gt;Back to the beginning.&lt;/p&gt;

&lt;p&gt;I had written, "reconsider if the numbers clearly lose because there's no BGM." Today, that condition is not met. No numbers have come out at all.&lt;/p&gt;

&lt;p&gt;But I overturned it anyway. The reason is that two of the three original rejection reasons had disappeared.&lt;/p&gt;

&lt;p&gt;No route — gone. The same service had the feature. Extra labor — gone. The license is included in the contract, and the mix became a fixed value. What remained was only "weak upside," and that got refuted the moment someone listened.&lt;/p&gt;

&lt;p&gt;Here's what I noticed. When you revisit a decision, the thing to look at first isn't "did the effect show up" — it's "&lt;strong&gt;is the reason you rejected it still alive.&lt;/strong&gt;"&lt;/p&gt;

&lt;p&gt;Waiting for the effect is the correct move when cost is high. Trying is expensive, so you wait for evidence.&lt;/p&gt;

&lt;p&gt;But this time, the cost had become nearly zero. If the cost is zero, the reason to wait for evidence is also zero. Put it in first, measure it later.&lt;/p&gt;

&lt;p&gt;Writing down "the condition under which this decision gets overturned" is a good habit, I think. But the condition you wrote assumes the cost at that moment. When the premise changes, it's the condition that goes stale. If keeping the condition becomes the goal, that's where you get stuck.&lt;/p&gt;

&lt;h2&gt;
  
  
  I kept a record of the decision I killed, too
&lt;/h2&gt;

&lt;p&gt;Last, this is just my own taste.&lt;/p&gt;

&lt;p&gt;When I overturned the decision, I didn't rewrite the original document. I appended one line — "this part alone has been replaced by a new decision" — and stood up a new, separate document.&lt;/p&gt;

&lt;p&gt;The reason is simple: &lt;strong&gt;once you erase what your past self was thinking when it got things wrong, you can never get it back.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A month ago, I considered it seriously and got it seriously wrong. The judgment itself wasn't bad. Only the premise changed.&lt;/p&gt;

&lt;p&gt;Because that history was still there, today I could confirm "two of the rejection reasons have disappeared." With "I vaguely feel like I skipped it before," you can't make that check.&lt;/p&gt;

&lt;p&gt;Stack decisions without overwriting them. Keep your own mistakes in a form you can re-check later. It paid off today, so I'm writing it down.&lt;/p&gt;

</description>
      <category>genai</category>
      <category>video</category>
      <category>ai</category>
      <category>audio</category>
    </item>
    <item>
      <title>My AI drew the hero as a different man in every scene. It's one guy's life story</title>
      <dc:creator>Jun</dc:creator>
      <pubDate>Sat, 15 Aug 2026 02:07:10 +0000</pubDate>
      <link>https://dev.to/jun_uen0/my-ai-drew-the-hero-as-a-different-man-in-every-scene-its-one-guys-life-story-2nhe</link>
      <guid>https://dev.to/jun_uen0/my-ai-drew-the-hero-as-a-different-man-in-every-scene-its-one-guys-life-story-2nhe</guid>
      <description>&lt;p&gt;I build explainer videos fully automatically. For each scene, a local image-generation AI draws that moment, one frame at a time. I was making one about an ancient Roman general — the whole life of a single man.&lt;/p&gt;

&lt;p&gt;When it was done, I watched it back from the top, scene by scene. And then I froze.&lt;/p&gt;

&lt;p&gt;The hero is a &lt;strong&gt;different person&lt;/strong&gt; in every scene.&lt;/p&gt;

&lt;p&gt;In one shot, a king wearing a crown. In the next, a young soldier in a plumed helmet. After that, a white-bearded old man. ...The same guy, supposedly. A biography of one man, and the lead's face changes every time. That's a fatal wound.&lt;/p&gt;

&lt;p&gt;And when I looked closer, history had gotten sloppy too. An ancient naval battle — but the ship is a &lt;strong&gt;tall sailing ship from the Age of Exploration&lt;/strong&gt; (not an oar-driven ancient galley; we're off by more than a thousand years). A figure from the Roman Republic wearing a &lt;strong&gt;crown&lt;/strong&gt; (Rome executed people just for &lt;em&gt;wanting&lt;/em&gt; to be king. The irony is a bit much). A &lt;strong&gt;dome&lt;/strong&gt; in the distant cityscape that has no business being there. And on the stone arch, &lt;strong&gt;broken pseudo-Latin&lt;/strong&gt; carved into it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cause: I was asking from scratch every time
&lt;/h2&gt;

&lt;p&gt;Why does this happen? The answer was simple.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I was writing each scene's prompt independently.&lt;/strong&gt; I'd ask for scene 1's image, then ask for scene 2's, and so on — each one a separate request. So the AI re-imagines the hero's face, and the historical setting, from zero every single time. Asking independently &lt;em&gt;means&lt;/em&gt; drifting every single time.&lt;/p&gt;

&lt;p&gt;Praying "draw the same person" in words, every time, gets you nowhere. The pictures don't listen.&lt;/p&gt;

&lt;h2&gt;
  
  
  ① Prevent it at build time — declare the character once, then inject
&lt;/h2&gt;

&lt;p&gt;The first move was &lt;strong&gt;reuse of a declaration&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The hero's appearance — age, hair and beard, armor, cloak, right down to "wears no crown" — gets written &lt;strong&gt;in one place, once&lt;/strong&gt;. I call it the character card. Then each scene's prompt just &lt;strong&gt;references the character with a placeholder&lt;/strong&gt;. Right before generation, the card's contents get auto-injected into every scene.&lt;/p&gt;

&lt;p&gt;The shared historical constraints (ancient era, galley not tall ship, no crown, no dome...) get declared once the same way and injected into every prompt.&lt;/p&gt;

&lt;p&gt;With that, the hero landed at a level where you can recognize him as "the same person." The ships became galleys. The crown vanished. Because &lt;strong&gt;the appearance lives in one place&lt;/strong&gt;, if I want to change something, I edit one line and it takes effect across every scene. The chore of rewriting it by hand every time — gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  ② What still slipped through — a prompt is only a request
&lt;/h2&gt;

&lt;p&gt;But some things didn't go away no matter what I declared. Write "don't draw a dome," write "don't draw text," and the model calmly draws a dome and carves a fake inscription into the arch.&lt;/p&gt;

&lt;p&gt;Negative instructions ("don't do X") had, for this model, roughly the force of "&lt;strong&gt;it'd be nice if you honored this.&lt;/strong&gt;" Declaring a thing and enforcing a thing are two different things.&lt;/p&gt;

&lt;p&gt;So the move I made was &lt;strong&gt;a checkpoint on the pixels.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before the heavy step of rendering the video, &lt;strong&gt;I have a vision-capable AI inspect the generated image itself.&lt;/strong&gt; Not the prompt string — the pixels that came out. "Any broken fake text? Any anachronisms (modern tall ships, crowns, domes)? Does the lead look like the same person? Does the picture actually depict this scene's narration?" Only the scenes that fail get regenerated with a new random seed. Loop until they pass.&lt;/p&gt;

&lt;p&gt;That checkpoint caught exactly the things declaration couldn't kill. The fake inscription on the arch, and the dome in the background. Reroll the seed, redraw, and they were gone.&lt;/p&gt;

&lt;p&gt;The funny part: this is really just &lt;strong&gt;the thing I did by hand first — glaring at each scene one at a time&lt;/strong&gt; — turned into a mechanism. I replaced human visual QA with an AI visual gate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Independent generation is a breeding ground for drift.&lt;/strong&gt; Ask for each scene separately and the things that are supposed to be shared (character, era) drift every time. Declare the shared parts &lt;strong&gt;once and inject them&lt;/strong&gt;. Don't pray — declare.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A negative prompt is not enforcement.&lt;/strong&gt; Treat "don't do X" as lucky-if-honored. If you want it enforced, put a separate checkpoint that looks at the output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generation quality is designed at build time.&lt;/strong&gt; "Prevent by declaration, inspect at the pixels." Rather than praying into the prompt and crying later, bake ① injected declarations and ② a visual checkpoint into the pipeline from the start.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I stopped begging the AI to "draw the same person every time." &lt;strong&gt;Write the character card once, and place a checkpoint that doubts the pictures that come out.&lt;/strong&gt; That alone keeps the hero wearing the same face, still standing, all the way to the final scene.&lt;/p&gt;

</description>
      <category>video</category>
      <category>genai</category>
      <category>imagegeneration</category>
      <category>ai</category>
    </item>
    <item>
      <title>Letting an AI agent hit 'like' scared me more than letting it post</title>
      <dc:creator>Jun</dc:creator>
      <pubDate>Wed, 12 Aug 2026 01:09:30 +0000</pubDate>
      <link>https://dev.to/jun_uen0/letting-an-ai-agent-hit-like-scared-me-more-than-letting-it-post-3jja</link>
      <guid>https://dev.to/jun_uen0/letting-an-ai-agent-hit-like-scared-me-more-than-letting-it-post-3jja</guid>
      <description>&lt;p&gt;I let an AI agent run the social media for my own product.&lt;br&gt;
The first thing I handed over was "post from our own account."&lt;br&gt;
Honestly, that part isn't that scary.&lt;br&gt;
The words are ours, and if it flops, we're the only ones embarrassed.&lt;/p&gt;

&lt;p&gt;What I wanted next was the step after that: "like / reply / follow other people's posts."&lt;br&gt;
The thing everyone calls react.&lt;br&gt;
Engagement, basically — could I hand that to the agent too?&lt;/p&gt;

&lt;p&gt;And the moment I started designing it, it hit me.&lt;br&gt;
This has a different kind of scary than posting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't put posting and reacting in the same box
&lt;/h2&gt;

&lt;p&gt;Posting is planting your own flowers in your own garden.&lt;br&gt;
React is walking up and knocking on other people's doors.&lt;/p&gt;

&lt;p&gt;It looks like the same "operate a social account," but when it goes wrong, the direction flips.&lt;br&gt;
A bad post comes back at you.&lt;br&gt;
A bad react flies out at someone else.&lt;br&gt;
Accidentally like a politically on-fire post and it instantly becomes your brand making a statement.&lt;/p&gt;

&lt;p&gt;So the first decision was this.&lt;br&gt;
Do not "add react as a feature" to the existing posting command.&lt;br&gt;
Split them.&lt;br&gt;
The command, the agent, the decision gates — stand them up as separate lineages.&lt;/p&gt;

&lt;p&gt;Mix react's requirements into the posting gate and the strictness you need when touching &lt;em&gt;other&lt;/em&gt; people bleeds — diluted — into your own outbound side.&lt;br&gt;
Go the other way and the looseness of your own outbound leaks into operations that touch other people.&lt;br&gt;
I don't want a leak in either direction, so I built a wall.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you must not mix isn't the feature — it's the risk boundary.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem where one "OK" clears everything
&lt;/h2&gt;

&lt;p&gt;When you let an agent do something irreversible, the scariest part is the granularity of approval.&lt;/p&gt;

&lt;p&gt;A human "OK" is looser than you'd ever think.&lt;br&gt;
You reply "yeah, looks good" once and the agent hears "so I can do all of it, right?"&lt;br&gt;
If likes fly out to people you never meant, there's no looking at it.&lt;/p&gt;

&lt;p&gt;So I split approval into two stages.&lt;/p&gt;

&lt;p&gt;Stage one is approving the &lt;em&gt;plan&lt;/em&gt;.&lt;br&gt;
Who are you going to react to, and with what kind of react — approval of the design.&lt;br&gt;
Stage two is approving the &lt;em&gt;execution&lt;/em&gt;.&lt;br&gt;
This person, this post, this exact reply text — approval of one concrete action.&lt;/p&gt;

&lt;p&gt;And the important part: I decided the stage-two OK only applies to a single action, uniquely pinned down by "target + action type + body text."&lt;br&gt;
Bulk approval is banned.&lt;br&gt;
And "a reply came back" does not count as approval.&lt;br&gt;
That's a lesson I've watched hurt before — a reply arriving is not the same as the content saying yes.&lt;br&gt;
Even a "no" is still "a reply came back," so a sloppy implementation lets it through.&lt;br&gt;
Only an explicit yes counts as approval.&lt;/p&gt;

&lt;h2&gt;
  
  
  Machine-filtering that the target is "real" and "not a landmine"
&lt;/h2&gt;

&lt;p&gt;Before replying, I made vetting the target a required step.&lt;br&gt;
Leave this to human gut feeling and the agent will happily march into a minefield.&lt;/p&gt;

&lt;p&gt;Two methods.&lt;/p&gt;

&lt;p&gt;One is a mechanical exclusion list.&lt;br&gt;
Politics, religion, discrimination, health, minors, obituaries, people in active litigation, sensitive attributes, suspected impersonation, bots, anything unrelated to us.&lt;br&gt;
Match even one of these and it's an instant, no-questions reject.&lt;br&gt;
On top of that I required "clearly related to us, translation, or multilingual — one of those."&lt;br&gt;
So "not suspicious" isn't enough; you have to stack all the way up to "actually relevant" before it's allowed to react.&lt;/p&gt;

&lt;p&gt;The other is verifying the target's identity.&lt;br&gt;
If you're going to reply, read the target's handle, display name, pinned tweet, and parent thread, and confirm they're not an impersonation or something sensitive — first.&lt;br&gt;
Confirming &lt;em&gt;our own&lt;/em&gt; account is correct is a given (running from the wrong account is a tragedy), but this time I made it read whether &lt;em&gt;the other side&lt;/em&gt; is real, too.&lt;br&gt;
Like checking, before you shake someone's hand, that the name and face they're presenting actually match.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap of "it's undoable, so it's light"
&lt;/h2&gt;

&lt;p&gt;Likes and follows can be undone later.&lt;br&gt;
So you slip into thinking they're "light operations."&lt;/p&gt;

&lt;p&gt;But that's the trap: the instant you press it, a notification fires at the target, and a trace lands in the public log.&lt;br&gt;
Undo it and the fact that you "did it once" doesn't disappear.&lt;br&gt;
There's a specific dread to leaving behind evidence that you were quietly digging through someone's posts in the middle of the night.&lt;/p&gt;

&lt;p&gt;So for like and follow too — even if the same weight of gate as reply is too much — at minimum I made "who am I" confirmation and an idempotency guarantee mandatory.&lt;/p&gt;

&lt;p&gt;The idempotency part is plain, but it works.&lt;br&gt;
Right before executing, leave a marker saying "I'm about to do this."&lt;br&gt;
After executing, read your own like count back off the screen and check it's exactly +1.&lt;br&gt;
If it's +2, or unchanged, stop right there.&lt;br&gt;
It's to prevent the plain, worst-case accident of "the operation succeeded but crashed before recording it, and the restart pressed it twice."&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraint of grabbing selectors by "reading only"
&lt;/h2&gt;

&lt;p&gt;The last piece of the build was having the agent locate the on-screen buttons (like, the reply input field, and so on).&lt;/p&gt;

&lt;p&gt;Here I added one constraint.&lt;br&gt;
During the survey phase, no clicking and no typing, none.&lt;br&gt;
Allow only the screen-reading tools, and seal away the pressing tools.&lt;/p&gt;

&lt;p&gt;The reason is simple: if a "let me find where the button is" turns into actually pressing it in the momentum of exploration, that's already an irreversible react.&lt;br&gt;
I went to scout the place and somehow opened the door and shook a hand — no thanks.&lt;br&gt;
So during the scouting, keep the hands tied behind the back.&lt;/p&gt;

&lt;p&gt;As a result, the locations around like and reply came out read-only.&lt;br&gt;
Follow was the only one I couldn't grab — you have to open someone else's profile for that — so I punted it this time and honestly wrote down "not yet collected."&lt;br&gt;
Leave this vague as "eh, later" and the next person to touch it (future me) melts five minutes, so writing down that a thing isn't there is the kindest move.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually did today was "design and skeleton, that's it"
&lt;/h2&gt;

&lt;p&gt;Let me be honest: in this session I never once fired a like or a reply at another person.&lt;br&gt;
What I did was the design doc, a template that by default only shoots blanks (dry-run), and the button locations grabbed read-only.&lt;br&gt;
Actually pulling the trigger for real is for another session, after another explicit approval.&lt;/p&gt;

&lt;p&gt;When you let an agent do outward-facing operations, the idea that helped most was this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Draw the line not at "is it undoable" but at "does it fly at someone else."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One like is scarier than a post.&lt;br&gt;
Drop that fear into the design and you land on: two-stage approval, mechanical exclusion, verifying the target's identity, idempotent reconciliation, and "keep the hands tied during the scouting."&lt;/p&gt;

&lt;p&gt;After that, I actually started running it.&lt;br&gt;
So far, it's working without any particular trouble.&lt;/p&gt;

&lt;p&gt;The funny thing is that nothing going wrong is not proof the design worked.&lt;br&gt;
The number of times stage-two approval stopped something, the number of targets the exclusion filter rejected, the accidents that never happened — you can't count them.&lt;br&gt;
Even so, shifting where I draw the line, from "is it undoable" to "does it fly at someone else," was not a mistake.&lt;/p&gt;

</description>
      <category>claudecode</category>
    </item>
    <item>
      <title>"I won't post without your approval" — my dutiful AI died after two minutes</title>
      <dc:creator>Jun</dc:creator>
      <pubDate>Sat, 08 Aug 2026 05:10:10 +0000</pubDate>
      <link>https://dev.to/jun_uen0/i-wont-post-without-your-approval-my-dutiful-ai-died-after-two-minutes-2cbe</link>
      <guid>https://dev.to/jun_uen0/i-wont-post-without-your-approval-my-dutiful-ai-died-after-two-minutes-2cbe</guid>
      <description>&lt;p&gt;I run an AI agent (Claude Code) that produces content and takes it all the way to publishing on external platforms, on its own.&lt;/p&gt;

&lt;p&gt;I had exactly one rule. &lt;strong&gt;Publishing is irreversible, so right before it posts, it must always get my OK.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Anything you publish or send to the outside world lives in a place where deleting it doesn't help — a copy survives. So "make things" it can do freely, but one step before "ship it," it has to get a human (me) to approve. The approval goes to a chat app on my phone (Telegram), and I reply "approve" or "reject." Until then, nothing goes out.&lt;/p&gt;

&lt;p&gt;So on this particular day, the agent finished building everything, got right up to the approval step, and I walked away.&lt;/p&gt;

&lt;p&gt;Something like "reply when the approval lands and it'll post itself." That's how relaxed I was about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  I came back, and nothing had been posted
&lt;/h2&gt;

&lt;p&gt;An hour or so later I came back. Nothing posted.&lt;/p&gt;

&lt;p&gt;It hadn't crashed with an error. The material was sitting there, finished. And yet the publishing — just that — hadn't happened.&lt;/p&gt;

&lt;p&gt;I dug through the logs, and the trick revealed itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The process waiting for approval had been force-killed after two minutes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Claude Code puts roughly a two-minute execution cap on a command you run in the foreground. For a normal build or test, that's plenty. But what I'd asked it to do was "wait up to 30 minutes for a human to reply in chat." The time a human takes to answer and the lifespan a tool grants a command were wildly out of balance.&lt;/p&gt;

&lt;p&gt;A process that meant to wait 30 minutes was getting guillotined at two.&lt;/p&gt;

&lt;p&gt;And here's the quietly brutal part: because the listener got killed, &lt;strong&gt;even if I had replied "approve" afterward, there was nobody left to hear it.&lt;/strong&gt; The answer just vanishes into the air. It's like shouting "Approved!" at an unstaffed reception desk.&lt;/p&gt;

&lt;h2&gt;
  
  
  There's a quiet tug-of-war hiding here
&lt;/h2&gt;

&lt;p&gt;Boiled down, two demands were colliding head-on.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Humans answer slowly.&lt;/strong&gt; If I've stepped out, I want it to wait 30 minutes, an hour, whatever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The tool assumes short-lived commands.&lt;/strong&gt; It dislikes a process that blocks for a long time, so it cuts it off.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Write the naive "wait in the foreground until approval arrives," and the one doing the waiting (the agent) gets stabbed by the tool and dies. &lt;strong&gt;The act of waiting had become an act of suicide.&lt;/strong&gt; I hadn't seen that.&lt;/p&gt;

&lt;p&gt;Think of it like an after-hours delivery clerk. The package arrives. But the receipt needs the homeowner's (my) OK. The homeowner is out. &lt;strong&gt;The more dutiful the clerk, the more they stand there and wait rather than forging the signature.&lt;/strong&gt; Then their shift limit (two minutes) hits, and they're sent home. The package is left dangling.&lt;/p&gt;

&lt;p&gt;Dutiful. So dutiful it dies.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I fixed it
&lt;/h2&gt;

&lt;p&gt;Three things, roughly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Kick the approval-wait out into a "background job"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Waiting in the foreground is what gets you killed at two minutes. So I threw the approval-wait out as a &lt;strong&gt;background job&lt;/strong&gt;. Background work survives across conversation turns, and &lt;strong&gt;when it finishes (= a reply arrives), a notification fires&lt;/strong&gt;. The agent doesn't have to sit glued to the desk the whole time — it gets woken up when the answer comes. It stopped camping at the reception desk and got a doorbell instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Persist state to disk so it can resume even if the whole session dies&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If I close the Mac, or the session ends completely, the background listener dies with it. I gave up on covering that — and in exchange, I write down "this task is currently parked at the 'waiting for approval' point" in a state file. Next time I open a session, it resumes from that cursor. Long gaps I settled for as "pick it up on the next launch."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. An idempotency marker to prevent double-posting&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The scariest case is "the post succeeded, but it crashed before recording that." The resumed agent thinks "I haven't posted yet" and &lt;strong&gt;posts a second time.&lt;/strong&gt; For an irreversible operation, that's fatal.&lt;/p&gt;

&lt;p&gt;So &lt;strong&gt;right before&lt;/strong&gt; publishing, I drop a single marker that says "about to ship." If it crashes mid-way, on resume it reads the marker and checks the actual external thing — "is this really already published?" — before proceeding. When it can't be sure, &lt;strong&gt;it doesn't repost on its own; it asks the human.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One more quietly important thing: &lt;strong&gt;don't over-resend.&lt;/strong&gt; If it re-fires the approval request every time the listener dies, the chat fills up with the same nag. I know the person is away, so it notifies cleanly once and defers the rest to next time.&lt;/p&gt;

&lt;h2&gt;
  
  
  In the end, this is about designing so the safety boundary never gets shortcut
&lt;/h2&gt;

&lt;p&gt;Technically it's just "wait in the background and wake on a notification." But I don't think that's the real point.&lt;/p&gt;

&lt;p&gt;The purpose of an approval gate is &lt;strong&gt;to stop irreversible operations from happening on their own while the human is absent.&lt;/strong&gt; Loosen that even once and there was no point putting the gate there at all.&lt;/p&gt;

&lt;p&gt;Which means the designer's job is to make "&lt;strong&gt;wait safely, forever if you have to&lt;/strong&gt;" &lt;strong&gt;cheap.&lt;/strong&gt; When waiting is a hassle and costs a lot, people lean toward "ah, just ship it." Same with the agent — if it's bad at waiting, you end up building it to charge ahead rather than wait.&lt;/p&gt;

&lt;p&gt;So an agent that's &lt;em&gt;good&lt;/em&gt; at waiting is, in fact, the better one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick the dutiful agent that keeps waiting over the impatient one that doesn't.&lt;/strong&gt; Just — so that dutifulness doesn't get it killed by the tool, we take care of the &lt;em&gt;how&lt;/em&gt; of the waiting on our side.&lt;/p&gt;

&lt;p&gt;When you leave a package at an empty house, you don't want a courier who forges the signature. You want the one who &lt;strong&gt;leaves a slip and comes back.&lt;/strong&gt; All I did this time was retrain the agent to be that kind of courier. That's the whole story.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>aiagents</category>
      <category>automation</category>
    </item>
    <item>
      <title>I told one AI to demolish the handoff prompt I wrote for another AI. It found a test that passes even when it's empty</title>
      <dc:creator>Jun</dc:creator>
      <pubDate>Sun, 02 Aug 2026 03:06:19 +0000</pubDate>
      <link>https://dev.to/jun_uen0/i-told-one-ai-to-demolish-the-handoff-prompt-i-wrote-for-another-ai-it-found-a-test-that-passes-3o70</link>
      <guid>https://dev.to/jun_uen0/i-told-one-ai-to-demolish-the-handoff-prompt-i-wrote-for-another-ai-it-found-a-test-that-passes-3o70</guid>
      <description>&lt;p&gt;I'm building an app with Claude Code right now.&lt;/p&gt;

&lt;p&gt;The setup is a little unusual. &lt;strong&gt;One "commander" session writes the instructions, and separate "worker" sessions implement them in parallel.&lt;/strong&gt;&lt;br&gt;
The commander never touches the keyboard itself. Its whole job is to turn "what to do next" into a handoff prompt that a worker can read and just run with.&lt;/p&gt;

&lt;p&gt;And that handoff prompt is quietly the scariest thing in the whole loop.&lt;br&gt;
The moment I hand it over, the worker trusts it as the spec and starts sprinting.&lt;br&gt;
If the spec is wrong, the wrong thing gets built. Fast, and with confidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before handing it over, I did my usual ritual
&lt;/h2&gt;

&lt;p&gt;I've picked up a habit lately. Before I throw the instructions at a worker, I run them past &lt;strong&gt;a subagent whose entire job is to demolish them.&lt;/strong&gt;&lt;br&gt;
It's defined to never approve, to hunt for holes, and to never, ever close with "this looks broadly reasonable."&lt;/p&gt;

&lt;p&gt;What was different this time: this thing didn't just read the prose of my prompt. &lt;strong&gt;It went and read the actual code.&lt;/strong&gt;&lt;br&gt;
And the reply it came back with made my stomach drop a little.&lt;/p&gt;

&lt;h2&gt;
  
  
  Objection 1: I wrote "make the test pass (go green)", but that test's green meant nothing
&lt;/h2&gt;

&lt;p&gt;In the instructions, I'd written this as a definition of done:&lt;br&gt;
&lt;strong&gt;"Get the XX test passing (green)."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The demolition agent's answer:&lt;br&gt;
"That test &lt;strong&gt;goes green even when the thing it's testing fails.&lt;/strong&gt;"&lt;/p&gt;

&lt;p&gt;I read it. It was true.&lt;br&gt;
The test only checks "did the process run all the way to the end." It never checks &lt;strong&gt;the one thing that matters: did it succeed?&lt;/strong&gt;&lt;br&gt;
Fail, and as long as it "returned a failure result and finished running," green.&lt;br&gt;
On top of that, the batch path was swallowing exceptions, so no matter what blew up, still green.&lt;/p&gt;

&lt;p&gt;So even if a worker reported back "DoD met, tests green!", &lt;strong&gt;nothing was actually proven.&lt;/strong&gt;&lt;br&gt;
The completion criterion I wrote myself was an empty pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Objection 2: I wrote "just flip a flag", but that switch didn't exist
&lt;/h2&gt;

&lt;p&gt;One more.&lt;br&gt;
I'd written "&lt;strong&gt;flip a config flag and it swaps in the real component&lt;/strong&gt;", as if it were a feature that already existed.&lt;/p&gt;

&lt;p&gt;The demolition agent traced the code and replied:&lt;br&gt;
"That wiring &lt;strong&gt;is never called from anywhere.&lt;/strong&gt; The function exists, but nothing connects to it."&lt;/p&gt;

&lt;p&gt;Also true.&lt;br&gt;
The swap-in function was there, but nothing in the UI ever reached it.&lt;br&gt;
The worker was one step away from either &lt;strong&gt;spending an hour hunting for a switch that doesn't exist&lt;/strong&gt;, or getting tired of hunting and fabricating something "close enough."&lt;/p&gt;

&lt;h2&gt;
  
  
  What scared me was that I'd stated both as fact
&lt;/h2&gt;

&lt;p&gt;Both times, I wrote it with total confidence.&lt;br&gt;
"The test passes." "The flag swaps it in."&lt;br&gt;
At the time I wrote them, I genuinely believed it.&lt;br&gt;
&lt;strong&gt;The demolition agent just read the code I hadn't re-read.&lt;/strong&gt; That was the whole difference.&lt;/p&gt;

&lt;p&gt;A handoff prompt is a spec, really.&lt;br&gt;
And the lie tends to sneak in at exactly &lt;strong&gt;the moment work is handed over&lt;/strong&gt; — human to AI, AI to AI, or today-me to next-week-me.&lt;br&gt;
An empty-but-green test. A wire that's "supposed to be there."&lt;br&gt;
A review that reads the prose, nods, and approves won't catch this. Only &lt;strong&gt;a review that reads the actual code and comes swinging&lt;/strong&gt; will.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"The test is green" only guarantees that the test is correct.&lt;/strong&gt;&lt;br&gt;
And the completion criteria you wrote yourself are the ones you're least likely to doubt.&lt;/p&gt;

&lt;p&gt;So maybe the thing you should demolish before you start typing isn't the code. It's &lt;strong&gt;the instructions.&lt;/strong&gt;&lt;br&gt;
Finding out "that green meant nothing" after running the implementation for ten hours is a lot more expensive than a three-minute adversarial review before you hand it over.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>engineering</category>
    </item>
    <item>
      <title>I shrank the image first, and the image was innocent</title>
      <dc:creator>Jun</dc:creator>
      <pubDate>Thu, 30 Jul 2026 02:40:47 +0000</pubDate>
      <link>https://dev.to/jun_uen0/close-your-editor-before-heavy-jobs-the-heavy-job-lives-inside-my-editor-2l5h</link>
      <guid>https://dev.to/jun_uen0/close-your-editor-before-heavy-jobs-the-heavy-job-lives-inside-my-editor-2l5h</guid>
      <description>&lt;p&gt;I run an automated video pipeline that generates images locally, on my Mac. A 16GB M4. The image model is FLUX, a 12B-parameter thing that keeps ~7GB of weights resident in memory even quantized down to 4bit.&lt;/p&gt;

&lt;p&gt;I've frozen this machine before (lesson learned the hard way), so there are safety mechanisms in place. &lt;strong&gt;A pre-flight gate: don't even start generating if free memory is below 50%. And a watchdog: if free memory crosses into the danger zone mid-generation, kill the generation process — just that process.&lt;/strong&gt; Never freeze the machine. That's rule one.&lt;/p&gt;

&lt;p&gt;And the safety mechanisms worked. Twice. &lt;strong&gt;By killing my generation.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The first suspect I hauled in was the size of the picture I was asking for. Draw it smaller and it'll fit, surely.&lt;br&gt;
Spoiler: that read is wrong.&lt;/p&gt;
&lt;h2&gt;
  
  
  Killed. Zero images
&lt;/h2&gt;

&lt;p&gt;The gate passes (51% free). Then the moment generation starts, free memory slides 51→7%, the watchdog goes "nope," and kills it. Zero images produced.&lt;/p&gt;

&lt;p&gt;The machine is fine. The safety net is flawless. I just... can't make anything. Protected and completely unproductive at the same time. Ever been there?&lt;/p&gt;
&lt;h2&gt;
  
  
  Hypothesis 1: "Just lower the resolution" — wrong
&lt;/h2&gt;

&lt;p&gt;As promised, the first thing I touched was the resolution. Shrink the output size and retry.&lt;/p&gt;

&lt;p&gt;...Same monotonic slide into the kill zone.&lt;/p&gt;

&lt;p&gt;But that failure taught me something. &lt;strong&gt;The culprit wasn't the output size — it was the resident weights themselves.&lt;/strong&gt; Changing resolution barely changed the memory drop, because 12B worth of weights lands in memory upfront, no matter how small the picture is. My first hypothesis died a quick, satisfying death.&lt;/p&gt;
&lt;h2&gt;
  
  
  Peeking at actual memory: the machine was already starving
&lt;/h2&gt;

&lt;p&gt;Time to look at the real numbers instead of vibes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PhysMem: 13G used (3.6G wired — the OS holds it and won't let go; 2.9G already compressed)
Actually free: 2.7G
swap: 7.8G in use
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At idle, this machine has no slack left. And my dev environment alone (editor plus its resident processes) eats ~3GB. Drop a 7GB model on top of that? Of course it overflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Told to close my editor, when the automation lives inside it
&lt;/h2&gt;

&lt;p&gt;And this is where the classic advice lands: "&lt;strong&gt;Close your editor before running heavy stages.&lt;/strong&gt;" Great advice. Except the automation running this generation &lt;em&gt;lives inside that editor&lt;/em&gt;. Closing it kills the thing I'm trying to run. A perfect deadlock.&lt;/p&gt;

&lt;h2&gt;
  
  
  The turning point: "wait, is 7GB negotiable?"
&lt;/h2&gt;

&lt;p&gt;That question changed the whole direction. Is 7GB just what FLUX costs, or is there room to haggle?&lt;/p&gt;

&lt;p&gt;I went back and reread every option the generation tool offers, and found the quantization level is selectable: &lt;strong&gt;3, 4, 5, 6, or 8 bit&lt;/strong&gt;. I was on &lt;strong&gt;4bit&lt;/strong&gt;. Meaning there was &lt;strong&gt;one more notch down&lt;/strong&gt; — no extra download, the cheapest possible experiment.&lt;/p&gt;

&lt;p&gt;(Full disclosure: my first attempt at this experiment failed because I &lt;strong&gt;mistyped the option name&lt;/strong&gt;. Not a memory problem. A typo. In any debugging session, the thing that burns the most time is usually this exact kind of thing.)&lt;/p&gt;

&lt;p&gt;Fixed the typo, ran &lt;strong&gt;3bit with the editor open&lt;/strong&gt;. ...It passed. Finished with 19% free at the lowest point. The first image ever produced without shutting down my own workspace.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before writing the victory blog post: A/B the actual culprit
&lt;/h2&gt;

&lt;p&gt;At this point I really wanted to write "3bit was the silver bullet!" But hold on. Right before that run, I had also &lt;strong&gt;closed a heavy browser&lt;/strong&gt;. So was it the 3bit, or the freed memory? &lt;strong&gt;I hadn't isolated the variable.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I deliberately reran the original 4bit, this time with plenty of free memory.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Free at start&lt;/th&gt;
&lt;th&gt;Lowest free during run&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Peak usage (approx.)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4bit / no headroom&lt;/td&gt;
&lt;td&gt;51%&lt;/td&gt;
&lt;td&gt;stopped at 7%&lt;/td&gt;
&lt;td&gt;killed&lt;/td&gt;
&lt;td&gt;~9GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4bit / headroom&lt;/td&gt;
&lt;td&gt;71%&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;td&gt;completed&lt;/td&gt;
&lt;td&gt;~9.4GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3bit / medium&lt;/td&gt;
&lt;td&gt;62%&lt;/td&gt;
&lt;td&gt;19%&lt;/td&gt;
&lt;td&gt;completed&lt;/td&gt;
&lt;td&gt;~6.9GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Now it reads clearly. &lt;strong&gt;The real culprit was insufficient free memory at start.&lt;/strong&gt; Close the browser, raise the starting line from 51% to 71%, and even 4bit completes. Freeing memory was the biggest lever.&lt;/p&gt;

&lt;p&gt;That said, &lt;strong&gt;3bit measurably shaves ~2.5GB off the peak&lt;/strong&gt; (9.4→6.9GB). Not strictly required — but the lowest-free point rises from 12% to 19%, which means it runs safely &lt;strong&gt;with less headroom, with more apps left open&lt;/strong&gt;. And I couldn't spot any quality degradation by eye.&lt;/p&gt;

&lt;p&gt;One bonus discovery: &lt;strong&gt;the 50% pre-flight gate was slightly too generous for this setup&lt;/strong&gt;. If the real peak is ~9.4GB, surviving requires starting at ~57% free or better. The watchdog's two rescues were exactly this mismatch between the gate and the real peak. At 3bit the peak drops to ~6.9GB, and &lt;strong&gt;the 50% gate becomes honest again&lt;/strong&gt;. That's the real argument for making 3bit the default.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A system that refuses safely turns a freeze into a measurement.&lt;/strong&gt; Because it killed the process instead of hanging, I got a lowest-free reading on every run. A frozen machine tells you nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kill the most plausible hypothesis (resolution) first, and kill it cheaply.&lt;/strong&gt; A confirmed miss is still progress.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A/B before you write the victory post.&lt;/strong&gt; If you changed two things right before the win, you will credit the wrong one. I nearly did — the main effect wasn't 3bit, it was freed memory.&lt;/li&gt;
&lt;li&gt;The question isn't "is the model too big?" It's &lt;strong&gt;the gap between peak usage and free-at-start&lt;/strong&gt;. You only see the real bottleneck after measuring both.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When local generation "mysteriously fails or dies," the culprit is neither the size of the picture nor the size of the model.&lt;br&gt;
Take both numbers before you panic — the peak, and the free memory you actually started with. It hides in the gap between them.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>memory</category>
      <category>applesilicon</category>
      <category>debugging</category>
    </item>
    <item>
      <title>"Is it actually running?" — the night I asked three times while stitching 17 images into one video</title>
      <dc:creator>Jun</dc:creator>
      <pubDate>Mon, 27 Jul 2026 01:08:21 +0000</pubDate>
      <link>https://dev.to/jun_uen0/is-it-actually-running-the-night-i-asked-three-times-while-stitching-17-images-into-one-video-2d6n</link>
      <guid>https://dev.to/jun_uen0/is-it-actually-running-the-night-i-asked-three-times-while-stitching-17-images-into-one-video-2d6n</guid>
      <description>&lt;p&gt;I run a little pipeline that builds history-explainer videos end to end — script, narration, images, video — all automatically, all by myself.&lt;/p&gt;

&lt;p&gt;Up until one day, the background was a single picture, slowly zooming. Two and a half minutes. Same picture the whole time. It got old, fast.&lt;/p&gt;

&lt;p&gt;So I rebuilt it: switch to a different picture on every sentence. A 17-sentence script means 17 pictures. Easy to say. This is where I stepped on two very unglamorous rakes, back to back. Both of the "a job I assumed would finish instantly refuses to finish" variety.&lt;/p&gt;

&lt;h2&gt;
  
  
  A 16GB Mac can't paint 17 pictures at once
&lt;/h2&gt;

&lt;p&gt;The images come from a local FLUX (an open image-generation model). Each one 1024x576, 4 steps, quantized to 3-bit. Run that on a 16GB Mac and a single image swallows the memory whole. So you &lt;strong&gt;must not run them in parallel&lt;/strong&gt;. One at a time, and only after the previous process is fully dead. That's the rule.&lt;/p&gt;

&lt;p&gt;The generation script has a bouncer at the door: if free memory is under 50%, it refuses to run. My first attempt bounced right off it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pre-flight check: free memory 35% (need 50%+)
✋ too little headroom. close a heavy browser first, then retry.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Closed the browser, back up to 64%. Bouncer waves me through. Seventeen images start generating, one after another. About a minute each. Fifteen to twenty minutes total.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Nothing's happening at all — is it running?"
&lt;/h2&gt;

&lt;p&gt;Fourteen images came out just fine. I stopped there, planning to resume the last three later. Enter rake number one.&lt;/p&gt;

&lt;p&gt;I fired off the generation to resume, and the progress log went dead quiet. So I asked Claude Code: "Nothing's starting at all — is it running?" It said the process was alive. I waited a bit and asked again. "Is it &lt;em&gt;actually&lt;/em&gt; running?"&lt;/p&gt;

&lt;p&gt;Good thing I doubted it. &lt;strong&gt;It wasn't resuming at all.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's why. This FLUX wrapper names its output files by seed number and caches on that. The assumption was "same seed, don't rebuild, reuse the file." What it actually did: when a file of that name already existed, it didn't overwrite — it happily &lt;strong&gt;rebuilt it&lt;/strong&gt; under a new name, &lt;code&gt;_1&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;img_201_1024x576.png     ← the real one, from the first 14
img_201_1024x576_1.png   ← a pointless variant rebuilt on "resume"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In other words, because I'd slapped on a blunt "redo everything" flag (&lt;code&gt;--force&lt;/code&gt;), it was diligently repainting from image one, right past the 14 that were already sitting there. No wonder it never finished. Three questions was letting it off easy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lesson: not "from scratch," but "just the missing ones"
&lt;/h2&gt;

&lt;p&gt;The fix was embarrassingly small. A few lines of bouncer at the top of the generation loop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# if this picture already exists, skip the whole thing and move on&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$WORKDIR&lt;/span&gt;&lt;span class="s2"&gt;/scene_&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;i&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.png"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"⏭  scene &lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="s2"&gt; already done — skip"&lt;/span&gt;
  &lt;span class="k"&gt;continue
fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Resume became instant. The 14 got skipped on sight, and only the remaining 2 generated, about 4 minutes. Don't trust the external model's cache to save you — &lt;strong&gt;hold your own idempotent resume&lt;/strong&gt; (same result no matter how many times you run it). That was the answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Long jobs shouldn't "start over." They should build "only what's missing."&lt;/strong&gt; Sounds obvious. Carries a little more weight coming from the guy who just nuked everything with a single &lt;code&gt;--force&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;While I was in there, I added one more small comfort for stopping cleanly. If an empty file called &lt;code&gt;work/.stop&lt;/code&gt; shows up, the loop &lt;strong&gt;finishes painting the current picture&lt;/strong&gt; and then stops before entering the next one. Stopping at a scene boundary beats &lt;code&gt;Ctrl-C&lt;/code&gt;-ing a half-drawn frame to death — the cleanup is far kinder. It stops the moment you ask, and the output stays intact. Considerate to humans.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rake number two: the video runs one frame off from the audio
&lt;/h2&gt;

&lt;p&gt;With 17 pictures in hand, now you stitch them into video. Give each picture a slow zoom (the Ken Burns thing), cut 17 short clips, concatenate. Lay subtitles on top, marry the narration audio to it.&lt;/p&gt;

&lt;p&gt;Here &lt;strong&gt;the video length has to match the audio length exactly&lt;/strong&gt;. Drift, and the picture freezes at the tail, or the subtitles slide later and later through the back half.&lt;/p&gt;

&lt;p&gt;Each scene's length comes from the audio's measured timing. The naive move is to round each clip's frame count individually. Do that and 17 rounding remainders all pile up, and the total drifts off the audio. Even a one- or two-frame error starts to bite once you concatenate.&lt;/p&gt;

&lt;p&gt;What worked was "&lt;strong&gt;don't round individually — round cumulatively&lt;/strong&gt;."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# round each scene's START position as "cumulative seconds x fps" first,
# then take the gap to the next scene as the frame count
&lt;/span&gt;&lt;span class="n"&gt;start_frame_k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scene&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s start seconds x fps)
frame_count_k = start_frame_(k+1) - start_frame_k   # last scene closes on the total length
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way the remainders cancel carry-style, and the sum of all clips always lands on "the audio's total frame count." Result: video and audio both exactly 121.40 seconds. Not one frame to spare.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wrap
&lt;/h2&gt;

&lt;p&gt;The rebuild itself is a dull story — "make it 17 pictures and stitch them." But what tripped me up wasn't some flashy algorithm. It was "don't throw away the work you've half-finished" and "how you round your remainders" — the kind of unglamorous fieldwork that decides everything.&lt;/p&gt;

&lt;p&gt;Don't trust the external tool's cache. Hold your own idempotent resume. And remainders will betray you if you round them one at a time — round them cumulatively.&lt;/p&gt;

&lt;p&gt;A job that makes you ask "is it &lt;em&gt;actually&lt;/em&gt; running?" three times is, more often than not, your own fault — for designing something where you couldn't tell whether it was running in the first place.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ffmpeg</category>
    </item>
  </channel>
</rss>
