Devlog #2
Last time: a parser pipeline that couldn’t bake anything. Now SubBake actually muxes subtitles into video — and I collected three of the dumbest bugs I’ve ever fought along the way.
The muxer was the easy part
core/muxer.py runs FFmpeg as a subprocess with -progress pipe:1 and reads out_time_us lines off stdout for live progress. Video and audio are stream-copied, so a 24-minute episode remuxes in seconds. The sharp edges were plumbing: stderr needs its own drain thread or the pipe buffer fills and FFmpeg just stops, and the container picks your subtitle codec — MKV copies the track as-is, MP4 demands mov_text, WebM demands webvtt.
One ffmpeg -i later: Stream #0:2(kor): Subtitle: subrip (default), title “Korean”. It works.
SRT ate my colors
Then I actually watched the result. The original fansub had per-speaker colors; my output was all white. Of course — my SMI parser stripped every HTML tag, including <font color="#FF80C0">, and SRT has no styling anyway. So the plan changed: SMI now converts to ASS. The parser keeps font tags, and a new core/ass_converter.py writes a proper ASS document.
Writing the font regex by hand nearly broke me
This regex took longer than the entire muxer:
_FONT_COLOR_RE = re.compile(
r'<font\s+[^>]*color\s*=\s*["\']?([^"\'>\s]+)["\']?[^>]*>',
re.IGNORECASE,
)
The job sounds trivial: find opening <font> tags, capture the color. My first draft, <font color="(.*?)">, fell apart immediately — real fansub files write color="Red" (a name, not hex), color=#FF80C0 (no quotes), and <font face="Arial" color="..."> (color isn’t the first attribute). The class [^"\'>\s]+ exists because the value must stop at a quote, a space, or the tag end — and nesting quote escapes inside a Python raw string is its own hell. I built it the way everyone builds regexes: one character class at a time in a tester, pasting real SMI lines until nothing exploded. Nobody writes this in one shot; anyone who claims otherwise is lying.
Then the colors came out wrong — red and blue swapped. ASS stores colors as &HBBGGRR&, a byte order from the 80s, so every RGB value gets flipped first. That cost me a long “why is the pink line blue” stare.
The giant subtitle
First ASS render: billboard-sized subtitles. Fontsize 36 sounds fine until you learn what PlayResX/PlayResY do — ASS declares a design resolution and the renderer scales it to the real video. I designed for 640×360 and played at 1080p: a 3× upscale, 108-pixel letters. Now I declare PlayRes 1920×1080 and the size holds at any resolution. I verified by burning test frames, because I no longer trust my own reasoning about this format.
The mystery box
Last ghost: a hollow rectangle between the pink Japanese line and the Korean translation. Every codepoint in those lines was clean, and re-rendering the same structure in isolation showed nothing. The culprit was the blank line — the SMI source separates the two languages with an empty line, my converter turned it into \N\N, and my player draws an empty subtitle line as a hollow box. Collapsing consecutive newlines fixed it, and as a bonus the output now looks exactly like the original presentation. Sometimes the bug is a blank line. That sentence is going on my gravestone.
Where SubBake stands
- Muxing works end to end: stream copy, live progress, clean cancel, per-container codecs
- SMI→ASS with per-speaker colors, verified on a real episode
- Sane subtitle size at any resolution; phantom box gone
- Still no GUI — everything runs through
python -cone-liners
Next up
The interface: a PySide6 shell with drag-and-drop, a queue table, and worker threads — the point where this becomes an app instead of modules I poke from a terminal. Qt will be perfectly calm and reasonable. (It will not be.)
Comments 0
No comments yet. Be the first!
Sign in to join the conversation.