Iron Noodle logoIron Noodle
Video line · retro & standing gates

What held.
What didn't.

Two days on the video line: a narration defect that had already shipped, a hologram spot rebuilt three times, and a cast that wouldn't stay the same person. Every gate on this page exists because something got past its absence.

The governing rule: a gate that cannot fail the build is not a gate. Advisory warnings get skipped under time pressure — that is exactly how a video shipped with two different voices in it and a passing QC sheet.

4defects caught by gates
7caused by my own gaps
3rebuilds of one spot
1wrong answer that moved a decision

What we did right

Keep doing these — each one caught something real
Provenance

Measured the artifact, not the notes

The README asserted one narrator. The file disagreed. A pitch fingerprint on identical text matched macOS system speech to within 0.3 Hz — that settled it in one measurement.

Documentation cannot detect its own defect.

Deterministic > statistical

Found a signal that can't be argued with

Pitch statistics overlapped enough to debate. Container format did not: the API returns 24 kHz mono, the bad clips were 48 kHz stereo. That became the load-bearing check.

Prefer a check with no judgement call in it.

Gates that bite

Build stops instead of substituting

The replacement TTS step has no fallback path. If the provider is unreachable the build halts rather than quietly reaching for a local voice.

The original bug was a silent fallback nobody could see.

Proof

Tested the gate against known-bad input

Ran the new checks against the defective audio to confirm they actually stop the build. A gate never fired in anger is a guess.

Re-run, don't reuse

Re-ran QC instead of copying last verdict

That is the only reason a 3.2 s dry CTA tail was found — the audio beds were shorter than the new runtime and the previous session's passing result would have hidden it.

Ship discipline

Byte-verified every deploy

md5 of the local deliverable against the fetched remote, every time. Also caught edge propagation returning an HTML fallback for ~30 s on fresh paths.

Scope honesty

Corrected overstatements on the record

Claimed all seven clips were the wrong voice when only one was proven. Corrected it in chat and in the commit message rather than letting the stronger claim stand.

Protecting verified work

Killed a blanket re-run after one image

Spotted a generate-all script overwriting already-QC'd plates and stopped it before it reached them. The output directory was gitignored — there was no restore path.

What we did wrong

Mine unless stated — each became a gate
Root failure

Answered from the README

Reported "nothing regressed" on a spot that had genuinely lost its voice, because I trusted the project's own build notes instead of probing the file.

→ G6. A defect report is answered by measuring the artifact.

Identity

Applied "sheet is the law" to the mascot only

The bot held up across 14 shots because it was passed as a model sheet image. The two women were passed as prose — and rendered as different people, in one case a different art style.

→ G1. Prose never carries identity.

Refactor loss

Dropped a check during a rewrite

Restructuring the voice gate for multi-speaker quietly removed the content check. A clip then spoke its own stage direction aloud and passed origin and pitch.

→ G3. Origin and pitch cannot see wrong words.

Didn't transfer

Fixed a bug in one script, not its sibling

Non-deterministic generators silently replace already-rendered assets. I added --only= to the voice script, then hit the identical bug in the stills script beside it an hour later.

→ G2. Fixing a class of bug means grepping for the pattern.

Wrong answer

Said a capability wasn't provisioned

Told the Architect ElevenLabs wasn't available. It was — I checked env vars and grepped memory but never listed the secrets directory. That wrong answer shaped a decision.

→ Credentials. List the directory before reporting absence.

Wrong tool for the job

Built photoreal humans beside a cartoon mascot

Two full cuts rejected on sight. A generated "real" face lands in the uncanny valley and collides with stylised IP.

→ G0. Cartoon is the default when a mascot is in frame.

Measured the wrong thing

Used pitch spread as a provenance proxy

It failed 6 of 14 correct clips, because the reads were deliberately weary and deadpan. The test was measuring performance and calling it origin.

→ G3. Flat delivery legitimately scores narrow.

Wiring ≠ working

Nearly signed off on a node count

Counting audio elements in the HTML proves the effects are wired, not that they are audible. Only a level measurement on the rendered file proves that.

→ G5. Verify in the finished file.

Self-inflicted

Two avoidable tooling wounds

A ps aux | grep waiter matched its own command line and deadlocked forever. A text-index code splice matched a comment and cut a constants block out of a working file.

→ Tooling notes.

Silent no-op

Trusted a helper that quietly did nothing

~/bin/save exits at "Nothing to commit" before pushing or archiving. On already-committed work, Tier 3 silently does neither.

→ Verify the side effect, not the exit code.

The line

Run these in order · every job
A gate that cannot fail the build is not a gate.
G0

Brief

  • Scope: internal or commercial? It changes what is defensible.
  • Style: mascot in frame → house cartoon, not photoreal.
  • Name every protected element and exclude it in every prompt.
  • Raise IP concerns once, take the answer, log the decision.
G1

Identity — sheet is the law, for the whole cast

  • Model sheet per recurring character: 3 views, expressions, signature pose.
  • Publish the sheets; pass them on every plate, for every character present.
  • Prose describes staging only. Wardrobe enumerated per shot.
G2

Generators

  • Every generator takes --only=. Never blanket re-run to add one asset.
  • If the output dir is gitignored, a blanket run is destructive.
G3

Voice — three independent gates

  • Origin: container format matches what the API returns. Deterministic; load-bearing.
  • Content: transcribe every clip; must start on the line, no direction tells.
  • Cast: group by role, not voice — one voice can play two characters.
  • No fallback path. Prompt as DIRECTION (do not speak this) / LINE:.
  • Emotion via tags; low stability lets a read swing. Never floor pitch spread.
G4

Composition

  • Re-run lint / validate / inspect every build. Never carry a verdict forward.
  • Beds cover the full runtime. Reused plates get distinct filenames. One root composition.
  • Persistent overlays sit on a contrast chip.
G5

Mix

  • Measure the rendered file: level at the effect vs the adjacent silent beat.
  • alimiter needs level=disabled or it re-normalises to 0 dB.
G6

Visual QC

  • Contact sheet, then open individual frames. Look, don't infer.
  • Identity across shots · wardrobe · mascot on-model · on-screen text vs VO · legibility over art.
  • WCAG checkers compare declared CSS colours and cannot see pixels behind text.
G7

Deploy

  • md5 local vs fetched remote. Identical or it did not ship.
  • Expect ~30 s edge lag on fresh paths; retry with a cache-buster before calling failure.
G8

Record

  • Docket → recap → session log → dropbox → D1. Receipts first: md5s, output, measured numbers.

Traps worth remembering

Cost real time this week
TrapTellFix
Silent local-voice fallbackContainer format differs from the API's native outputNo fallback path; build stops
Clip narrates its own directionClip runs much longer than its line (9.25 s for 5.61 s)Labelled DIRECTION/LINE + content gate
Free vs billed API key429 partway through a multi-clip runBilled key lives in the secrets dir — check it first
Blanket generator re-runAssets change under an already-rendered cut--only= on every generator
Two root compositionsDuplicated audio on playbackVariants outside root
Self-matching process waiterWaits forever, no outputNever grep for a string your own command contains
Piping a long job through tailNo progress until exitTrack by file mtimes
Save helper no-op"Nothing to commit" then silencePush and archive manually