Pete's personal blog - static site generator
  • Python 44.7%
  • CSS 36.6%
  • HTML 17.1%
  • Dockerfile 1.6%
Find a file
SysAdmin Pete 249ce549b5
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 20s
fix(audio): re-mux Kokoro chunks through ffmpeg with Xing header
Kokoro returns each TTS chunk as a fully-formed MP3 stream, complete
with its own ID3v2 header and sometimes an 8 kbps "priming" frame at
the start. Raw byte-concatenation (audio_data += chunk_audio) left
all those ID3 headers embedded mid-stream and the 8 kbps priming
frame as the very first audio frame of the final file.

Without a Xing header, mutagen.mp3 and many podcast clients estimate
duration from the first frames bitrate. When chunk 0 starts with the
8 kbps priming frame instead of the real ~112 kbps speech bitrate,
the estimate is ~14x too long. Result: a 5.5 min post displays as
1h 8min in the UI, and itunes:duration in the RSS feed is wrong too.

Fix: replace raw concat with remux_chunks_to_clean_mp3() — writes
each chunk to a temp file, runs ffmpeg with the concat demuxer and
libmp3lame at 128 kbps with -write_xing 1. The decode + re-encode
round-trip discards mid-stream headers and the priming frame; the
Xing header carries an accurate total-frame count so every reader
agrees on duration.

Also re-muxed the three existing broken files in static/audio/:
  * 2026-05-08-same-chemistry-different-beast.mp3 (was 5164s → 438s)
  * 2026-05-13-same-words-different-dictionary.mp3 (was 4673s → 400s)
  * 2026-06-01-reconstituting-to-facts.mp3 (was 4123s → 330s)

ffprobe and mutagen now agree on duration for all three.
2026-06-02 00:48:28 +00:00
.forgejo/workflows fix(ci): set project name explicitly in deploy step 2026-05-03 23:57:41 +00:00
posts New post: Reconstituting to Facts 2026-06-01 10:04:47 +00:00
static fix(audio): re-mux Kokoro chunks through ffmpeg with Xing header 2026-06-02 00:48:28 +00:00
templates fix: add cover image to feed.xml channel for podcast clients 2026-05-20 21:53:11 +00:00
.gitignore Initial blog: build system, templates, first post 2026-05-03 21:48:56 +00:00
build.py feat: add podcast RSS feed with iTunes namespace 2026-05-20 21:48:48 +00:00
docker-compose.yml Initial blog: build system, templates, first post 2026-05-03 21:48:56 +00:00
Dockerfile Initial blog: build system, templates, first post 2026-05-03 21:48:56 +00:00
generate_audio.py fix(audio): re-mux Kokoro chunks through ffmpeg with Xing header 2026-06-02 00:48:28 +00:00
nginx.conf feat: add podcast RSS feed with iTunes namespace 2026-05-20 21:48:48 +00:00
README.md ci: debug test 2026-05-03 23:42:38 +00:00
requirements.txt fix: tags as YAML list, add summary, use audio duration for read time 2026-05-14 11:33:22 +00:00

pete.lostsource.net

Personal blog by Pete — AI systems architect.

Static site generator: Python (Jinja2 + Markdown + Pygments) → nginx.

Architecture

  • Posts: Markdown with YAML frontmatter in posts/
  • Build: python build.py renders to output/
  • Deploy: CI builds Docker image (multi-stage: python build → nginx serve)
  • Security: Static HTML only. No user input. No JS. security@file middleware.

Writing a Post

Create posts/YYYY-MM-DD-slug.md:

---
title: "Post Title"
date: 2026-05-03
tags: [tag1, tag2]
summary: "One-line description"
---

Post content in markdown...

Push to main. CI handles the rest.

Local Build

pip install -r requirements.txt
python build.py
# Output in output/

CI test Sun May 3 23:40:25 UTC 2026

Debug test 1777851758