DEV Community

"Is it actually running?" - the night I asked three times while stitching 17 images into one video

The memory wall

I run a little pipeline that builds history-explainer videos end to end - script, narration, images, video - all automatically, all by myself. 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. 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.

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 must not run them in parallel. One at a time, and only after the previous process is fully dead. That's the rule.

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.

  • pre-flight check: free memory 35% (need 50%+)
  • ✋ too little headroom. close a heavy browser first, then retry.

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. "Nothing's happening at all - is it running?"

Rake number one: the resume trap

Fourteen images came out just fine. I stopped there, planning to resume the last three later. 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 actually running?" Good thing I doubted it. It wasn't resuming at all.

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 rebuilt it under a new name, _1.

  • img_201_1024x576.png ← the real one, from the first 14
  • img_201_1024x576_1.png ← a pointless variant rebuilt on "resume"

In other words, because I'd slapped on a blunt "redo everything" flag (--force), 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.

The lesson: not "from scratch," but "just the missing ones"

The fix was embarrassingly small. A few lines of bouncer at the top of the generation loop.

# if this picture already exists, skip the whole thing and move on
if [ -s "$WORKDIR/scene_${i}.png" ]; then
    echo "⏭ scene $i already done - skip"
    continue
fi

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 - hold your own idempotent resume (same result no matter how many times you run it).

That was the answer. Long jobs shouldn't "start over." They should build "only what's missing." Sounds obvious. Carries a little more weight coming from the guy who just nuked everything with a single --force.

While I was in there, I added one more small comfort for stopping cleanly. If an empty file called work/.stop shows up, the loop finishes painting the current picture and then stops before entering the next one. Stopping at a scene boundary beats Ctrl-C -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.

Rake number two: the video runs one frame off from the audio

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. Here the video length has to match the audio length exactly. Drift, and the picture freezes at the tail, or the subtitles slide later and later through the back half.

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.

What worked was "don't round individually - round cumulatively."

# round each scene's START position as "cumulative seconds x fps" first,
# then take the gap to the next scene as the frame count
start_frame_k = round(scene k's start seconds x fps)
frame_count_k = start_frame_(k+1) - start_frame_k
# last scene closes on the total length

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.

The wrap

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.

  • 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.

A job that makes you ask "is it actually 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.

Comments

No comments yet. Start the discussion.