feat: Blog TTS — automatisch blogposts omzetten naar audio via Gitea workflow #72
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
🎙️ Blog Text-to-Speech Pipeline
Samenvatting
Voeg een automatische Text-to-Speech pipeline toe aan Cozy-Den, zodat elke nieuwe blogpost automatisch wordt omgezet naar een
.mp3audiobestand. Het audiobestand wordt gehost op de eigen infrastructuur (nginx/Proxmox) en ingebonden in de Astro blogpost pagina via een<audio>player.🎯 Doelstelling
.mp3hebben.mp3) in de git repo🏗️ Architectuur
📋 Taken
1. Python TTS script (
scripts/tts_generate.py).mdbestand als input argumentslugentitlete lezen<slug>.mp32. Gitea Actions workflow (
.gitea/workflows/tts.yml)pushnaarmainmet wijzigingen insrc/content/blog/**.md.mdbestanden zijn toegevoegd in de laatste commit (niet gewijzigd, alleen nieuw).mp3bestaat op de audio server.mp3hebbentts_generate.pyaan voor nieuwe posts.mp3via SCP/rsync naar Proxmox audio directory3. Nginx configuratie (Proxmox)
/var/www/audio/audio.hiddenden.cafe/<slug>.mp3te serveren4. Astro integratie
audioveld toe aan het blog frontmatter schema (optioneel, bijv.audio: true/false)<audio>player te tonen als het mp3 bestand bestaatslugvan de post als bestandsnaam om de audio URL te construeren5. Secrets & configuratie
MISTRAL_API_KEYtoe als Gitea Actions secret🔒 Loop preventie
De workflow triggert uitsluitend op
.mdbestanden:De gegenereerde
.mp3bestanden worden nooit naar de repo gecommit — ze gaan direct naar de audio server via SCP. Hierdoor kan een mp3 upload nooit een nieuwe workflow trigger veroorzaken.💰 Kosten inschatting
Een gemiddelde blogpost is
4.000 karakters. Bij Voxtral TTS ($16 per miljoen karakters) kost één blogpost omzetten naar audio **$0,064** — minder dan een cent. Voor tientallen posts per jaar blijft dit volledig verwaarloosbaar.🔧 Tech stack
audio.hiddenden.cafe)📝 Notities
--forceflag aan het script toe te voegen om handmatig een specifieke post opnieuw te genereren🔍 TTS Provider vergelijking — Azure & OpenRouter onderzoek
Aanvullend onderzoek naar Azure AI Foundry en OpenRouter als mogelijke TTS providers.
Azure AI Foundry — ✅ Kan TTS, maar overkill
Azure Speech in Foundry biedt meer dan 600 stemmen in 150+ talen, inclusief HD neural voices en uitstekende Nederlandse stemmen. Pricing is vergelijkbaar (~$15-16/M chars) en er is een gratis F0 tier.
Nadeel: Azure vereist dat je het volledige Azure ecosysteem instapt — storage, functions, identity management, allemaal apart gefactureerd. Voor een simpel blog-to-audio script is dat veel infrastructurele overhead zonder toegevoegde waarde.
OpenRouter — ❌ Niet geschikt voor TTS
OpenRouter ondersteunt audio output via modellen zoals GPT-audio, maar alleen via het chat completions formaat — audio als onderdeel van een conversatie response, niet als dedicated TTS endpoint. Het is omslachtig en duurder dan een provider rechtstreeks aanroepen. OpenRouter is een LLM router, niet een TTS platform.
Provider vergelijking voor deze use case
💡 Aanbeveling: provider-agnostisch script
Het TTS script moet de provider configureerbaar maken via een environment variable, zodat je makkelijk kunt wisselen zonder de code aan te passen:
Aanbevolen startpunt: Google Cloud TTS — de gratis tier van 1 miljoen karakters per maand is voor een persoonlijke blog meer dan genoeg, en de Nederlandse stemmen zijn van uitstekende kwaliteit. Later kan altijd geswitcht worden naar Voxtral of OpenAI indien gewenst.
✅ Implementatie —
scripts/tts_generate.pybijgewerktDe volgende fixes zijn doorgevoerd via Claude Code op basis van twee gevonden problemen: env vars werden niet geladen en lange blogposts produceerden slechte audio.
.envloader (load_dotenv, regel 22–33).envtwee niveaus boven het script (= project root)KEY=VALUE, skipt comments en lege regels, stript quotessetdefault— shell env vars worden nooit overschrevenChunking (
split_into_chunks, regel 79–103)\n\n), dan op zinnen ((?<=[.!?])\s+) als een alinea te lang isMP3 merge (
merge_audio_chunks, regel 106–120)\xff\xfb\x90\x00+ 413 null bytes (geldig 128kbps/44100Hz frame)Provider functies
_*_synthesize(chunk, api_key) -> byteshelpertts_*functie chunkt, logt (Chunk X/Y (N chars)...) en voegt samenPROVIDERSdict en CLI interface zijn ongewijzigdOpenstaande taken
.gitea/workflows/tts.yml)audio.hiddenden.cafe<audio>player integreren in blogpost layout (src/pages/blog/[...slug].astro)