Eine vollständige Bauanleitung für einen automatisierten Podcast, vom Google-Schlüssel über einen kleinen Docker-Dienst auf der NAS bis zum Eintrag bei Apple Podcasts und Spotify.
Es gibt eine Sorte Wochenende, die jeder kennt, der sich beruflich oder aus Neugier mit künstlicher Intelligenz beschäftigt: Samstagmorgen, Kaffee, und das schlechte Gewissen, weil sich seit Montag wieder fünf Podcasts, ein Dutzend YouTube-Videos und dreißig Blogbeiträge angesammelt haben. Niemand hört das alles. Die meisten hören gar nichts davon und haben trotzdem das Gefühl, etwas zu verpassen.
Kommt dir bekannt vor? Dann bist du hier richtig. Denn genau gegen dieses Gefühl bauen wir in dieser Anleitung etwas: eine Maschine, die dir die Woche vorliest. Und zwar nicht als Spielzeug, sondern als richtigen Podcast, den du in jeder App abonnieren kannst.
Um diese Anleitung verständlicher zu machen, begleiten dich drei Personen: Die typischen Büro-Charaktere: Die kompetente IT-Kollegin, der selbsternannte Experte und der ehrliche Anfänger. Diese drei Perspektiven helfen dir, typische Stolperfallen zu erkennen.
Tanja ist die IT-Expertin. Sie weiß, wie es funktioniert, erklärt geduldig und strukturiert – und lässt sich von schlechten Ratschlägen nicht aus der Ruhe bringen. Wenn du eine Frage hast, hat Tanja die Antwort.
Bernd ist der selbsternannte „Experte“, der alles besser weiß – und meistens falsch liegt. Seine Abkürzungen und sein Halbwissen führen regelmäßig zu Problemen. Er steht für alle gefährlichen Mythen und schlechten Praktiken, die du vermeiden solltest.
Ulf ist der Lernende, genau wie du. Er stellt die Fragen, die dir im Kopf herumschwirren, und braucht manchmal einen Vergleich aus dem Alltag, um IT zu verstehen. Wenn Ulf etwas nicht versteht, ist das völlig in Ordnung, dafür ist Tanja da.
„Und… Action!“
Ulf: „Achtzehn Minuten Podcast, und keiner hat ins Mikro gesprochen? Das ist doch wie ein Fußballspiel ohne Spieler.“
Bernd: „Unsinn. Das ist ein Plugin. Einmal installieren, fertig. Ich mach das in der Mittagspause.“
Tanja: „Es gibt kein Plugin, das dir einen Artikel in ein Gespräch verwandelt, zwei Stimmen spricht, die Tonspur montiert und die Folge bei Apple anmeldet. Das sind mehrere Bausteine. Und am Ende sitzt trotzdem ein Mensch, der auf einen Knopf drückt.“
Ulf: „Welchen Knopf?“
Tanja: „Die Freigabe. Dazu kommen wir noch.“
Warum sich jemand einen Podcast von einer Maschine vorlesen lässt
Der KI Wochenrückblick auf FOUNDIC.org ist aus genau diesem Gefühl vom Samstagmorgen entstanden. Stell dir einen fleißigen Praktikanten vor, der die ganze Woche Zettel sammelt, sie sortiert und dir am Samstag eine saubere Zusammenfassung auf den Tisch legt. Genau das macht hier ein Automatisierungssystem: Es sammelt die Woche ein, ordnet sie und schreibt einen Rückblick. Wie dieser Teil funktioniert, steht in der Anleitung „n8n Podcast YouTube und Webseiten transkribieren und zusammenfassen“. Dieser Artikel setzt dort an, wo jene Anleitung aufhört: bei einem fertigen Text, der jeden Samstag per Mail kommt.
Aus diesem Text machst du hier einen Podcast: ein Gespräch zweier Stimmen, rund achtzehn Minuten lang, gesprochen von synthetischen Stimmen, montiert auf der eigenen NAS, veröffentlicht über die eigene Website und abonnierbar in Apple Podcasts, Spotify und jeder anderen Podcast-App. Jeden Samstag um neun, ohne dass jemand einen Knopf drückt, bis auf einen: die Freigabe.
Ulf: „Und was kostet der Spaß? Profi-Sprecher sind doch teuer wie ein Stürmer aus der Premier League.“
Tanja: „Die Stimmen kosten nichts. Bezahlt wird nur das Sprachmodell, und das sind ein paar Cent pro Woche.“
Die Stimmen kosten im laufenden Betrieb tatsächlich nichts, weil die Sprachausgabe im monatlichen Freikontingent eines großen Anbieters bleibt. Bezahlt wird nur das Sprachmodell, das den Artikel in ein Gespräch verwandelt: ein Aufruf pro Woche, einige Cent.
Warum das mehr ist als eine Spielerei? Weil es ein ziemlich sauberes Beispiel dafür ist, was in den letzten zwei Jahren passiert ist. Vor fünf Jahren hätte ein Podcast mit Schnitt, Pegelangleichung, Transkript, Kapitelbild und Verzeichniseinträgen ein kleines Redaktionsteam oder ein kostenpflichtiges Hosting-Abo gebraucht. Heute liegen die Bausteine einzeln im Regal, teils kostenlos, und du kannst sie auf eigener Hardware zusammenstecken wie Werkzeuge an einer Werkbank. Das ist die Demokratisierung von Enterprise-Technik, über die viel geredet wird. Hier kannst du ihr beim Arbeiten zusehen.
Bernd: „Demokratisierung heißt: Jeder kann’s, also kann nichts schiefgehen.“
Tanja: „Demokratisierung heißt: Jeder darf es versuchen. Schiefgehen kann trotzdem eine Menge.“
Und Tanja hat recht. Du kannst dabei sehr viel falsch machen. Deshalb erzählt dieser Artikel nicht nur, wie es geht, sondern auch, woran es beim ersten Mal gescheitert ist. Fast jede Regel in dieser Anleitung ist aus einem Fehler entstanden. Keine Panik also, wenn bei dir etwas hakt: Die meisten Stolperfallen sind hier schon markiert.
Was am Ende dasteht
Schauen wir uns zuerst das Ziel an, bevor wir losrennen. Hier ist das Ergebnis in Zahlen, gemessen an einem echten Lauf vom 19. September 2026:
| Messgröße | Wert |
|---|---|
| Laufzeit des gesamten Samstagslaufs (Text schreiben, vertonen, veröffentlichen) | 8 Minuten 42 Sekunden |
| davon Vertonung | 279 Sekunden |
| Gesprächsbeiträge | 108 |
| gesprochene Zeichen | 17.470 |
| Länge der Folge | 18:45 Minuten |
| Dateigröße | 8,95 MB, Mono, 24 kHz |
| Lautheit | −16 LUFS, Spitze −1,5 dBFS (der übliche Wert für Podcasts) |
| laufende Kosten | 0 € für die Stimmen, einige Cent pro Woche für das Sprachmodell |
Ulf: „Und wo hört man das dann? Im Blog, oder bei Spotify?“
Tanja: „Überall gleichzeitig. Und alle drei Stellen zeigen auf dieselbe Datei.“
Die Folge erscheint an drei Stellen gleichzeitig, und alle drei zeigen auf dieselbe Datei. Denk an ein einziges Plakat, auf das drei Wegweiser zeigen, statt drei Kopien des Plakats:
- als Abspieler im Blogbeitrag, oben und noch einmal über einem aufklappbaren Transkript,
- in einem eigenen Podcast-Feed (eine maschinenlesbare Liste aller Folgen, die Podcast-Apps regelmäßig abrufen),
- in den Verzeichnissen von Apple Podcasts, Spotify, Amazon Music, Podcast Index und podcast.de.
Auf einen Blick
Getestet mit
Ehrlich gesagt: Software hält nicht still. Klickwege und Feldnamen ändern sich von Version zu Version. Heißt ein Feld bei dir anders, liegt das meist an der Version, nicht an dir. Stand der Anlage dieses Artikels am 27. September 2026:
| Baustein | Version |
|---|---|
| NAS | Synology DS1621xs+ mit DSM 7.4.1-90080 |
| Container Manager | 24.0.2-1706 |
| n8n | 2.1.5, selbst gehostet im Docker-Container, mit PostgreSQL 16 |
| WordPress | 7.1.2 |
| Code Snippets | 3.10.2 |
| Polylang | 3.8.9 (optional, der Feed läuft auch ohne) |
| WP Fastest Cache | 1.5.2 (optional, nur falls du ein Cache-Plugin nutzt) |
| Python im Vertonungsdienst | 3.12.14 (python:3.12.14-slim-trixie) |
| ffmpeg im Vertonungsdienst | 7.1.5 aus Debian 13 |
| Sprachausgabe | Google Cloud Text-to-Speech, Stimmen Chirp 3 HD |
| Sprachmodell | Anthropic claude-sonnet-4-5-20250929 |
Voraussetzungen: Was schon laufen muss
Bernd: „Voraussetzungen überspringe ich immer. Das steht ja eh nur drin, damit der Text länger wird.“
Tanja: „Dann suchst du nachher drei Stunden einen Fehler, der hier in einem Satz erklärt ist.“
Bernd liegt falsch: Dieser Abschnitt ist das Fundament. Der Artikel baut auf drei vorhandenen Dingen auf. Wer sie noch nicht hat, findet die Anleitungen dazu auf FOUNDIC.org.
1. Eine laufende n8n-Installation mit PostgreSQL auf einer NAS oder einem Mini-PC.
n8n (sprich: „n-eight-n“, eine Software, mit der man Arbeitsabläufe aus Bausteinen zusammenklickt, ungefähr wie Dominosteine, die sich gegenseitig anstoßen) läuft hier in einem Docker-Container (eine abgeschottete Softwarekiste, die ihre eigenen Abhängigkeiten mitbringt, so wie ein Werkzeugkoffer, in dem alles drin ist, was man für einen Auftrag braucht) auf einer Synology DiskStation. Wie das geht, beschreibt „n8n selber hosten – n8n Installation auf Synology NAS“.
2. Ein Artikel pro Woche, die Schnittstelle.
Stell dir die Schnittstelle wie die Übergabe beim Staffellauf vor: Der Stab muss genau so übergeben werden, wie es abgesprochen ist, sonst fällt er auf den Boden. Der Podcast-Strang dieser Anleitung braucht genau eine Eingabe, und die ist fest vereinbart:
Pflichteingabe für den Podcast-Strang
- ein n8n-Knoten mit dem Namen "Zusammenfassung bauen"
- genau EIN Datensatz pro Wochenlauf
- darin das Feld wp_content: der fertige Artikel als HTML, mindestens 500 Zeichen
Nicht nötig: Titel, Datum, Bild – den Titel bildet der Strang selbst aus dem Datum
Ulf: „Und wenn ich gar keinen A21 habe? Bin ich dann raus?“
Tanja: „Nein. Dann nimmst du Weg B und baust dir den Knoten selbst.“
Dafür gibt es nämlich zwei Wege:
- Weg A, der bestehende A21-Workflow. Beim KI Wochenrückblick schreibt ein Workflow (ein in n8n gebauter Arbeitsablauf) namens A21 jeden Samstag aus dem Material der Woche den Rückblick und endet genau mit diesem Knoten. Der Aufbau steht in der oben genannten Transkriptions-Anleitung, Phase 7. Den Podcast-Strang hängst du dort hinten an. Nur für Weg A gelten die A21-Details in Phase 7 (Knoten
Wochenmaterial holen, der zusätzliche A20-Samstagslauf). - Weg B, ein eigener Textgenerator. Du legst in Schritt 6.0 einen Code-Knoten
Zusammenfassung bauenan, der genau diese Schnittstelle erfüllt. Dort setzt du deinen Artikel ein oder ersetzt den Knoten durch deinen eigenen Generator. Der KnotenEinstellungenbricht mit einer klaren Meldung ab, wenn kein Artikel ankommt. Du merkst also sofort, wenn der Stab fehlt, statt am Ende eine leere Folge zu bekommen.

So sieht die Kette aus, an die der Podcast später angehängt wird: vom Zeitplan über das Einsammeln des Wochenmaterials bis zum fertigen Rückblick.
3. Eine WordPress-Website, auf der du Administrator bist, mit dem Plugin Code Snippets (erlaubt, kleine PHP-Programmstücke einzubauen, ohne das Theme anzufassen).
Bernd: „Für Podcasts nimmt man natürlich ein Podcast-Plugin. Je mehr Plugins, desto professioneller.“
Tanja: „Je mehr Plugins, desto mehr Baustellen.“
Ein Podcast-Plugin brauchst du nicht, und das ist Absicht. Der Feed ist eine überschaubare Funktion, und jedes zusätzliche Plugin will gepflegt, aktualisiert und bei Fehlern verdächtigt werden. Mehr Plugins bedeuten also nicht mehr Professionalität, sondern mehr Verdächtige, wenn etwas kaputtgeht.
Dazu kommen:
- ein Google-Konto und eine Kreditkarte (für die Identitätsprüfung bei Google Cloud, belastet wird sie im Rahmen dieser Anleitung nicht),
- ein Anthropic-Zugang für das Sprachmodell Claude, das den Artikel in ein Gespräch umschreibt (ein Aufruf pro Woche, Kosten im Cent-Bereich),
- für die Verzeichnisse: ein Apple Account mit Zwei-Faktor-Anmeldung und ein Spotify-Konto,
- rund zwei Nachmittage Zeit.
Die Alternativen, die geprüft und verworfen wurden
Ulf: „Geht das nicht einfacher? Ich will doch nur, dass es läuft.“
Berechtigte Frage. Man muss das nicht so bauen. Drei Abkürzungen liegen nahe, und jede hat einen Preis.
Ein Hosting-Anbieter wie Podigee, LetsCast oder Buzzsprout. Die erledigen Feed, Statistik und Verzeichniseinträge per Knopfdruck, aber nur für Feeds, die sie erzeugen. Die eigene Domain wäre dann nicht mehr der Ursprung des Podcasts. Das ist, als würdest du dein eigenes Stadion bauen und dann jedes Heimspiel beim Nachbarverein austragen. Für ein Projekt, bei dem es gerade um digitale Selbstbestimmung geht, ist das der falsche Tausch.
Die Sprachausgabe selbst auf der NAS rechnen. Geprüft und verworfen, und zwar nicht wegen der Geschwindigkeit, sondern wegen des Klangs. Der einzige realistische Kandidat mit deutschen Stimmen und freier Lizenz ist Piper. Er klingt sauber, aber erkennbar nach Bahnhofsansage, nicht nach Erzählstimme, und zwanzig Minuten davon am Stück sind anstrengend. Die Modelle, die besser klingen (Coqui XTTS-v2, F5-TTS), verbieten in ihrer Lizenz die kommerzielle Nutzung oder brauchen eine Grafikkarte, die eine NAS nicht hat.
Ein anderer Cloud-Anbieter. Der Vergleich im August 2026 ergab drei ernsthafte Kandidaten: Google Chirp 3 HD (30 deutsche Stimmen, eine Million Zeichen pro Monat frei), Microsoft Azure Neural (17 deutsche Stimmen, eine halbe Million frei) und ElevenLabs (rund 52 Dollar im Jahr, dafür der ganze Text in einem einzigen Aufruf). Ein wöchentlicher Rückblick braucht rund 87.000 Zeichen im Monat, also weniger als ein Zehntel des Google-Kontingents. Innerhalb von Google gibt es zudem noch „Gemini-TTS“, „Studio“ sowie Neural2, WaveNet und Standard. Warum es Chirp 3 HD geworden ist, steht in Phase 2.
Der Bauplan aus der Vogelperspektive
Bernd: „Pläne sind was für Leute, die nicht improvisieren können.“
Tanja: „Ohne Plan weißt du nicht mal, welcher Knoten gerade kaputt ist.“
Bevor wir Knöpfe drücken, schauen wir uns das Ganze einmal von oben an, wie ein Trainer die Aufstellung an der Taktiktafel (die vollständige Knotenliste steht in Phase 6):
Samstag 09:00
A21 schreibt den Rückblick (vorhanden) – oder dein eigener Textgenerator
→ Einstellungen Adressen, Wochentitel, Prüfung auf Platzhalter
→ Woche prüfen gibt es diese Woche schon? Dann Abbruch statt Doppelung
→ Sprechfassung erzeugen Claude macht aus dem Artikel ein Gespräch zweier Stimmen
→ Beitrag anlegen WordPress-Entwurf mit dem Artikeltext
→ Podcast: Dialog holen Gespräch prüfen, Abmoderation anhängen, Dateiname bilden
→ Podcast: vertonen eigener Docker-Dienst "vertonung" auf der NAS:
Google spricht jeden Beitrag einzeln, ffmpeg montiert
→ Podcast: MP3 hochladen Datei in die WordPress-Mediathek
→ Podcast: Player bauen Abspieler + Transkript in den Beitrag, vier Zusatzfelder
→ Podcast: Player nachtragen Beitrag aktualisieren
Mensch prüft und klickt "Veröffentlichen"
→ Feed /feed/podcast/ zeigt die neue Folge
→ Apple, Spotify & Co. holen sie sich von dort
Drei Entscheidungen prägen diesen Aufbau, und es lohnt sich, sie vorher zu kennen:
Die Sprachausgabe läuft in einem eigenen kleinen Dienst, nicht in n8n. n8n ist gut darin, Dinge anzustoßen und weiterzureichen, so wie ein Disponent im Büro, der Aufträge verteilt, aber nicht selbst an der Maschine steht. Tonbearbeitung mit ffmpeg (einem Kommandozeilenwerkzeug, das praktisch jedes Audio- und Videoformat bearbeiten kann) gehört nicht hinein. Der Dienst vertonung bekommt das fertige Gespräch als Liste von Beiträgen und gibt eine fertige MP3 zurück. Mehr muss n8n über Audio nicht wissen.
Ulf: „Wenn das eh automatisch läuft, warum muss dann noch einer auf ‚Veröffentlichen‘ klicken?“
Tanja: „Weil ein Fehler sonst sofort bei allen Abonnenten landet. Die Freigabe ist dein Torwart.“
Der Mensch bleibt im Spiel. Der Workflow legt den Beitrag als Entwurf an. Erst wenn jemand ihn liest und auf „Veröffentlichen“ klickt, erscheint die Folge im Feed. Ein automatisierter Podcast, der ungeprüft an Tausende Abonnenten geht, ist keine gute Idee, und das hat nichts mit Misstrauen gegenüber der Technik zu tun.
Jede Tatsache steht an genau einer Stelle. Die Adresse der MP3, ihre Größe und Dauer schreibt der Workflow in eigene Zusatzfelder am Beitrag, und der Feed liest genau diese Felder. Er fischt sie nicht aus dem Beitragstext. Warum so streng? Kennst du das Büro mit zwei Telefonlisten, eine im Flur, eine im Intranet? Nach drei Monaten stimmt keine mehr mit der anderen überein. Genau so ist es hier: Zwei Stellen für dieselbe Information laufen auseinander, immer. Das ist die teuerste Lehre des ganzen Projekts.
Phase 1: Google eine Stimme abkaufen, ohne zu bezahlen
Montagmorgen, Besprechungsraum. Auf dem Whiteboard steht in Tanjas Schrift nur ein Wort: „Stimme“.
Ulf: „Und wer spricht jetzt unseren Podcast? Ich hätte da eine Stimme. Stadionsprecher-Niveau.“
Tanja: „Deine Stimme hat Urlaub, Krankheit und samstags um neun ein Spiel. Wir nehmen eine, die nie absagt: Google.“
Bernd: „Google? Dann zahlen wir uns dumm und dämlich. Die wollen doch sofort meine Kreditkarte.“
Tanja: „Die Karte wollen sie sehen, ja. Belastet wird sie im Rahmen dieser Anleitung nicht. Schauen wir uns an, warum.“
Die Sprachausgabe kommt von Google Cloud Text-to-Speech. Stell dir das wie ein Tonstudio vor, das irgendwo in einem Rechenzentrum steht und rund um die Uhr Aufträge annimmt. Du schickst Text hin, du bekommst Audio zurück. Damit das Studio weiß, wer da klopft, brauchst du einen Ausweis: den API-Schlüssel. Eine API ist eine Programmierschnittstelle, also der Lieferanteneingang, über den ein Programm einen Dienst anspricht. Der Schlüssel ist eine lange Zeichenkette, die beweist, dass der Aufruf von dir kommt.
Für diesen Ausweis brauchst du drei Dinge: ein Projekt in der Google Cloud Console (den Ordner, in dem alles landet), ein Rechnungskonto (Google will wissen, wer du bist) und eine Einschränkung, damit der Schlüssel nur das darf, was er soll. Keine Panik, das wirkt komplizierter, als es ist. Es sind fünf kurze Schritte.
Schritt 1.1: Ein Projekt anlegen
Ein Projekt ist in der Google Cloud so etwas wie ein eigener Aktenordner mit Namensschild. Alles, was du in dieser Phase anlegst, Schlüssel, Budget und freigeschaltete Dienste, kommt in diesen Ordner. Deshalb lohnt sich ein Name, den du auch in drei Monaten noch verstehst.
Klickfolge: console.cloud.google.com öffnen → mit dem Google-Konto anmelden → oben links neben dem Google-Cloud-Logo auf die Projektauswahl → Neues Projekt → bei Projektname einen sprechenden Namen eintragen, etwa podcast-tts → Übergeordnete Ressource: „Keine Organisation“ lassen → Erstellen.
Bernd: „Der Vorschlag im Namensfeld ist doch gut. Einfach übernehmen, spart Zeit.“
Tanja: „Das spart dir zehn Sekunden heute und eine halbe Stunde Rätselraten im Winter. Namen vergibst du selbst.“
Genau das zeigt auch das erste Bild: Die Maske schlägt einen nichtssagenden Namen vor. Überschreib ihn.
Erwartetes Ergebnis: Nach wenigen Sekunden erscheint eine Benachrichtigung, oben in der Projektauswahl steht jetzt dein Projektname, und das Dashboard des Projekts öffnet sich.

Die Maske schlägt einen Namen wie „My Project 44904″ vor. Ersetze ihn. In drei Monaten weißt du sonst nicht mehr, wofür das Projekt war.

Schritt 1.2: Die Sprach-API einschalten und das Rechnungskonto verknüpfen
Das Projekt ist angelegt, aber noch leer. Jetzt schaltest du das Tonstudio für diesen Ordner frei. Google hat Hunderte Dienste im Angebot, und jeder muss pro Projekt einzeln aktiviert werden. Das ist lästig, aber sinnvoll: Was nicht eingeschaltet ist, kann auch nicht versehentlich Geld kosten.
Klickfolge: Im Suchfeld oben Cloud Text-to-Speech API eingeben → das Produkt öffnen → Aktivieren.
Google verlangt jetzt ein Rechnungskonto. Das ist der Moment, in dem viele abbrechen, und er ist harmloser, als er aussieht.
Ulf: „Moment. Kreditkarte eingeben für was Kostenloses? Das ist doch wie das Probeabo beim Pay-TV. Nach vier Wochen zahlst du die ganze Saison.“
Tanja: „Guter Instinkt, falscher Fall. Während des Testzeitraums belastet Google die Karte nicht automatisch. Und für den Rest bauen wir gleich noch einen Wecker ein.“
Neue berechtigte Kunden erhalten 300 Dollar Startguthaben für 90 Tage (Stand September 2026). Für die Registrierung verlangt Google eine Zahlungsmethode. Laut Google-Anmeldeseite dient sie dazu, die Identität zu bestätigen und Betrug einzudämmen. Während des unveränderten Testkontos entstehen daraus keine automatischen Belastungen. Der Ablauf hat zwei Schritte:
Klickfolge: Im Dialog Kostenlos testen → Schritt 1 von 2: Land auswählen, Nutzungsbedingungen bestätigen → Zustimmen und fortfahren → Schritt 2 von 2: Kontaktdaten und Zahlungsmethode eintragen → Testzeitraum starten → im Pop-up die Identität bestätigen.

Schritt 2 des Testzeitraums. Die persönlichen Daten sind hier geschwärzt.

Danach zurück zur Produktseite der Text-to-Speech API und Aktivieren klicken. Ja, schon wieder. Der erste Klick hat nur den Umweg über das Rechnungskonto ausgelöst, eingeschaltet ist noch nichts.

Erwartetes Ergebnis: Die Seite API/Dienstdetails zeigt bei Cloud Text-to-Speech API den Status Aktiviert.

Was das kostet: Rechnen wir das einmal ehrlich durch. Die Chirp-3-HD-Stimmen kosten laut Preisliste 30 Dollar je Million Zeichen. Die erste Million jedes Monats ist frei, und zwar dauerhaft, nicht nur im Testzeitraum. Ein Rückblick hat rund 17.000 bis 20.000 Zeichen, vier bis fünf Folgen im Monat also rund 87.000 Zeichen. Das ist weniger als ein Zehntel des Freikontingents. Du nutzt also ungefähr so viel davon wie ein Kreisligist vom Olympiastadion.
Schritt 1.3: Einen Schlüssel anlegen, der nur sprechen darf
Bernd: „Ich mach immer einen Schlüssel für alles. Einer für alles, das ist effizient.“
Tanja: „Ein Generalschlüssel ist effizient, bis er im falschen Briefkasten landet. Dann steht jede Tür offen.“
Tanja hat recht, und das ist der Kern dieses Schritts. Ein API-Schlüssel ist wie ein Büroschlüssel. Der Hausmeister hat einen für alle Räume. Der Praktikant bekommt einen für die Teeküche. Unser Schlüssel ist der Praktikant: Er darf genau einen Dienst ansprechen, nämlich die Sprachausgabe. Gerät er irgendwann in falsche Hände, kann damit niemand deine anderen Google-Dienste benutzen.
Klickfolge: Linkes Menü APIs und Dienste → Anmeldedaten → oben Anmeldedaten erstellen → API-Schlüssel.

Im Seitenbereich API-Schlüssel erstellen:
- Name:
podcast-tts - API-Einschränkungen: Schlüssel einschränken wählen → in der Liste Cloud Text-to-Speech API anhaken → sonst nichts.
- Anwendungseinschränkungen: „Keine“ lassen (der Dienst ruft von deiner NAS aus an, deren Adresse sich ändern kann).
- Erstellen.
Ulf: „Und warum dann bei den Anwendungseinschränkungen gar nichts? Das ist doch offen wie unsere Abwehr am Samstag.“
Tanja: „Weil die Adresse deiner NAS wechseln kann. Sperrst du den Schlüssel auf eine Adresse, spricht der Podcast irgendwann nicht mehr. Die Absicherung übernimmt hier die API-Einschränkung.“

Nur ein Haken: Text-to-Speech. Ein Schlüssel, der alles darf, ist im Fall eines Lecks ein offenes Scheunentor.
Erwartetes Ergebnis: Ein Fenster zeigt den Schlüssel, eine Zeichenkette von 39 Zeichen, die mit AIza beginnt. Kopiere ihn in deinen Passwortmanager. In der Liste der Anmeldedaten steht der Schlüssel danach mit dem Vermerk „Cloud Text-to-Speech API“ als Einschränkung.

Und jetzt die wichtigste Hausregel dieser Phase: Der Schlüssel gehört ab jetzt an genau einen Ort, in eine Datei auf der NAS, die in Phase 3 entsteht. Nicht in n8n, nicht in ein Dokument, nicht in eine Chatnachricht. Jede Kopie ist ein weiterer Ersatzschlüssel unter der Fußmatte.
Schritt 1.4: Der Test, bevor irgendetwas gebaut wird
Bernd: „Testen können wir später. Erst mal alles aufbauen, dann sehen wir ja, ob es läuft.“
Tanja: „Und wenn es nicht läuft, suchst du den Fehler in zwanzig Bausteinen statt in einem. Wir prüfen den Schlüssel jetzt, solange er allein ist.“
Das ist Werkstatt-Logik: Bevor du eine neue Bohrmaschine an ein Gerüst schraubst, drückst du einmal auf den Knopf. Dieser Test braucht zwei Befehle und keine zwei Minuten.
Du brauchst dafür einen Rechner mit Terminal (Mac, Linux oder die Kommandozeile der NAS per SSH). Ersetze im Befehl DEIN_SCHLUESSEL durch deinen echten Schlüssel. Der erste Befehl fragt Google, welche deutschen Stimmen es gibt, und zählt die Chirp-3-HD-Stimmen:
curl -s "https://texttospeech.googleapis.com/v1/voices?languageCode=de-DE&key=DEIN_SCHLUESSEL" \
| grep -c 'Chirp3-HD'
Erwartetes Ergebnis: eine Zahl, im September 2026 30, also 30 deutsche Chirp-3-HD-Stimmen, 14 weibliche und 16 männliche. Kommt stattdessen eine Fehlermeldung mit API_KEY_INVALID oder PERMISSION_DENIED, stimmt der Schlüssel nicht oder die API ist im falschen Projekt aktiviert. Der zweite Fall ist tückisch: Wer mehrere Projekte hat, schaltet die API gern im falschen Ordner frei. Dann prüf oben in der Projektauswahl, wo du gerade bist.
Eine Zahl ist schön, aber du willst ja etwas hören. Jetzt ein Satz zum Hören:
curl -s -X POST "https://texttospeech.googleapis.com/v1/text:synthesize?key=DEIN_SCHLUESSEL" \
-H "Content-Type: application/json" \
-d '{"input":{"text":"Hallo, das ist die erste Probe für den Wochenrückblick."},
"voice":{"languageCode":"de-DE","name":"de-DE-Chirp3-HD-Leda"},
"audioConfig":{"audioEncoding":"MP3","sampleRateHertz":24000}}' \
| python3 -c "import sys,json,base64; open('probe.mp3','wb').write(base64.b64decode(json.load(sys.stdin)['audioContent']))"
Erwartetes Ergebnis: eine Datei probe.mp3 im aktuellen Ordner, etwa vier Sekunden lang, gesprochen von einer ruhigen Frauenstimme.
Ulf: „Die klingt ja wie eine Moderatorin. Darf ich die behalten?“
Tanja: „Merk sie dir. In Phase 2 wirst du ihr wieder begegnen.“
Schritt 1.5: Einen Wecker stellen
Ein Freikontingent ist nur so lange frei, wie niemand einen Fehler macht. Ein Workflow, der sich in einer Schleife verfängt, kann in einer Nacht Hunderttausende Zeichen verbrauchen. Stell dir einen Kopierer vor, bei dem jemand die Taste festgeklebt hat: Morgens ist das Papier alle, und niemand war es. Deshalb eine Budgetwarnung.
Klickfolge: Linkes Menü Abrechnung → Budgets und Benachrichtigungen → Budget erstellen:
- Name: frei wählbar, zum Beispiel
Podcast - Zeitraum: monatlich, Projekte: alle, Dienste: alle
- Art: Nur Benachrichtigungen
- Betrag: fester Betrag,
10 EUR - Schwellenwerte: 50 %, 90 % und 100 % auf Tatsächliche Ausgaben
- Beim Punkt zu Gutschriften und Einsparungen den Haken bei Andere Einsparungen entfernen. Sonst verrechnet Google das Testguthaben, und die Warnung schlägt während des Testzeitraums nie an.
- Fertigstellen.
Der vorletzte Punkt ist der, den fast alle übersehen. Solange das Startguthaben läuft, rechnet Google jede Ausgabe gegen das Guthaben. Auf dem Papier gibst du dann nichts aus, und dein Wecker schläft friedlich weiter, während die Schleife rattert. Ohne den Haken zählt, was tatsächlich verbraucht wird.

Das fertige Budget in der Bearbeitungsansicht: Zielbetrag 10 EUR, drei Schwellenwerte bei 5, 9 und 10 EUR, E-Mail an die Abrechnungsadministratoren.
Bernd: „Zehn Euro Budget, perfekt. Dann schaltet Google bei zehn Euro ab, und wir sind sicher.“
Tanja: „Nein. Das Budget löst Warnungen aus, bei zehn Euro zum Beispiel eine Mail. Die Nutzung stoppt es aber nicht automatisch.“
Und das ist wirklich wichtig zu wissen: Ein Budget ist ein Wecker, keine Bremse. Das Budget löst Warnungen aus, stoppt die Nutzung aber nicht automatisch. Bernds Annahme ist einer der teuersten Irrtümer in der Cloud-Welt. Die eigentliche Bremse baut Phase 3 in den Dienst selbst ein.
Phase 2: Zwei Stimmen finden, die nicht klingen wie eine
Ulf: „Dreißig Stimmen? Das ist ja ein ganzer Kader. Wie viele stellen wir auf?“
Tanja: „Zwei. Und die müssen sich so klar unterscheiden wie Heim- und Auswärtstrikot.“
Bernd: „Ach, da hört man kurz rein und nimmt die zwei, die gut klingen. Fünf Minuten.“
Dreißig Stimmen, zwei werden gebraucht. Das klingt nach einer Geschmacksfrage, und es ist auch eine, aber eine, bei der man sich systematisch irren kann. Bernds Fünf-Minuten-Plan ist genau der Weg, auf dem das passiert. Der erste Versuch ging genau so schief, wie du in Schritt 2.1 gleich sehen wirst.
Warum Chirp 3 HD und nicht Gemini
Bevor wir Stimmen auswählen, eine Grundsatzfrage. Google bietet dieselben Stimmen über zwei Wege an, so wie ein Restaurant, das dasselbe Gericht am Tisch und zum Mitnehmen verkauft. Weg eins heißt Chirp 3 HD und läuft über die klassische Text-to-Speech-API. Weg zwei heißt Gemini-TTS und läuft über das Gemini-Modell.
Ulf: „Und Gemini ist das Neuere, oder? Neuer ist immer besser. Wie beim Transfer.“
Tanja: „Auch beim Transfer sitzt der teure Neue manchmal nur auf der Bank. Hören wir erst, dann entscheiden wir.“
Gemini klingt verlockend, weil es ein ganzes Gespräch mit zwei Sprechern in einem einzigen Aufruf erzeugen kann. Im direkten Hörvergleich war der Unterschied allerdings „marginal“, und Chirp 3 HD sprach lange Zahlen sauberer.
Den Ausschlag gaben die Fallen auf dem Weg zu Gemini, sechs an der Zahl. Hier ist radikale Ehrlichkeit angebracht, denn diese Liste ist schlicht nervig:
- Die Gemini-Sprachmodelle trugen im September 2026 alle noch den Zusatz
previewim Namen. - Sobald ein Projekt mit einem Rechnungskonto verknüpft ist, fällt es aus dem kostenlosen „Free Tier“ und landet in „Tier 1″, und dort verlangt Google eine Vorauszahlung von mindestens 5 Dollar. Das Testguthaben zählt dafür nicht. Die Fehlermeldung lautet
RESOURCE_EXHAUSTED … Your prepayment credits are depletedund klingt, als sei ein Kontingent erschöpft. Tatsächlich ist das ganze Konto gesperrt, auch für reine Textanfragen. - Der API-Schlüssel aus Schritt 1.3 lässt sich gar nicht auf die Gemini API beschränken. Der Eintrag ist ausgegraut, weil sie eine Bindung an ein Dienstkonto verlangt.
- Die Test-Oberfläche in AI Studio bricht mit
HTTP 403ab. - Die Sprachoberfläche in der Cloud Console nimmt nur 200 Zeichen und kennt keine deutsche Stimme.
Die Fehlermeldung aus dem zweiten Punkt verdient einen zweiten Blick. Sie führt dich auf eine falsche Fährte: Du denkst, der Tank sei leer, dabei ist die Zapfsäule abgeschlossen. Wer so eine Meldung sieht, sucht erst mal stundenlang nach einem verbrauchten Kontingent, das es gar nicht gibt.

Die Gemini API lässt sich für den Schlüssel nicht auswählen.


Die eingebaute Sprachoberfläche: 200 Zeichen, drei Stimmen, keine davon deutsch.
Chirp 3 HD ist deshalb die bessere Wahl, aber kein Heiligtum. Es hat zwei Eigenheiten, die man kennen muss.
Erstens SSML. Das ist eine Auszeichnungssprache, mit der man Pausen, Betonung und Tonhöhe steuert, so etwas wie Regieanweisungen im Drehbuch. Chirp 3 HD unterstützt SSML inzwischen nur als Vorschaufunktion (Preview) und nur bei normalen, nicht gestreamten Aufrufen. Laut Google-Dokumentation gehören dazu unter anderem <break>, <phoneme>, <prosody> und <voice>. Diese Anleitung baut die Produktion bewusst nicht darauf auf: Sie schickt normalen Text, und Pausen und Schnitt kommen aus der eigenen Montage in Phase 3. So hängt der wöchentliche Betrieb nicht an einer Funktion, die noch nicht allgemein freigegeben ist.
Zweitens ist Chirp 3 HD nicht wiederholbar. Derselbe Satz dreimal gesprochen ergab Dateien, deren Länge um 15 Prozent schwankte. Wie ein Stürmer, der denselben Elfmeter dreimal schießt und dreimal anders anläuft. Die Länge einer Folge lässt sich also nicht vorausberechnen, sie muss an der fertigen Datei gemessen werden.
Bernd: „Dann schätzen wir die Länge einfach über die Zeichenzahl. Passt schon ungefähr.“
Tanja: „Bei 15 Prozent Schwankung passt da gar nichts ungefähr. Gemessen wird am Ende, an der fertigen Datei.“
Schritt 2.1: Alle Stimmen in einer Datei anhören
Jetzt wird gecastet. Und hier kommt der Fehler, aus dem die Regel dieses Schritts entstanden ist.
Der erste Casting-Versuch lief über die Beschreibungen im Katalog: fünf Stimmpaare, fünf getrennte Dateien. Das Ergebnis war ernüchternd. Vier der fünf Männerstimmen lagen, nachgemessen, alle zwischen 103 und 107 Hertz Grundtonhöhe. Die Grundtonhöhe ist, grob gesagt, wie tief oder hoch eine Stimme im Durchschnitt klingt. Vier Stimmen in einem so schmalen Bereich klingen fast gleich, und das Anhören in fünf getrennten Dateien war ein Gedächtnistest, kein Vergleich.
Ulf: „Ist doch wie beim Probetraining. Jeden Spieler an einem anderen Tag anschauen, und am Ende weißt du nicht mehr, wer der Schnelle war.“
Tanja: „Genau. Deshalb laufen jetzt alle gleichzeitig auf den Platz.“
Besser: Alle Stimmen sprechen denselben Satz, und alles landet mit einer Sekunde Pause in einer Datei. So hört man die Unterschiede direkt hintereinander. Das Skript dafür braucht Python 3 und ffmpeg auf deinem Rechner (auf dem Mac: brew install ffmpeg). ffmpeg ist das Werkzeug, das die einzelnen Proben am Ende zu einem Band zusammenklebt.
Datei stimmenprobe.py:
#!/usr/bin/env python3
"""Stimmenprobe: alle deutschen Chirp-3-HD-Stimmen sprechen denselben Satz,
danach landen alle Proben mit kurzer Pause in EINER Datei.
Aufruf: GOOGLE_TTS_SCHLUESSEL=... python3 stimmenprobe.py
Ergebnis: Ordner proben/ mit einer MP3 je Stimme und stimmenband.mp3
"""
import base64, json, os, subprocess, urllib.request
SCHLUESSEL = os.environ['GOOGLE_TTS_SCHLUESSEL']
SATZ = ('Diese Woche hat ein kleines Modell die großen geschlagen, '
'und zwar ausgerechnet bei einer Aufgabe mit 2.048 Zahlen.')
BASIS = 'https://texttospeech.googleapis.com/v1/'
def hole(pfad, daten=None):
anfrage = urllib.request.Request(
BASIS + pfad + ('&' if '?' in pfad else '?') + 'key=' + SCHLUESSEL,
data=json.dumps(daten).encode() if daten else None,
headers={'Content-Type': 'application/json'})
with urllib.request.urlopen(anfrage, timeout=60) as antwort:
return json.loads(antwort.read())
stimmen = sorted(s['name'] for s in hole('voices?languageCode=de-DE')['voices']
if 'Chirp3-HD' in s['name'])
print(len(stimmen), 'Chirp-3-HD-Stimmen gefunden')
os.makedirs('proben', exist_ok=True)
liste = []
for name in stimmen:
ziel = os.path.join('proben', name + '.mp3')
antwort = hole('text:synthesize', {
'input': {'text': name.split('-')[-1] + '. ' + SATZ},
'voice': {'languageCode': 'de-DE', 'name': name},
'audioConfig': {'audioEncoding': 'MP3', 'sampleRateHertz': 24000}})
with open(ziel, 'wb') as fh:
fh.write(base64.b64decode(antwort['audioContent']))
liste.append(ziel)
print(' ', name)
# Eine Sekunde Stille zwischen den Proben, alles in eine Datei.
subprocess.run(['ffmpeg', '-v', 'error', '-y', '-f', 'lavfi', '-t', '1',
'-i', 'anullsrc=r=24000:cl=mono', 'proben/_pause.mp3'], check=True)
with open('proben/_liste.txt', 'w') as fh:
for p in liste:
fh.write("file '%s'\nfile '_pause.mp3'\n" % os.path.basename(p))
subprocess.run(['ffmpeg', '-v', 'error', '-y', '-f', 'concat', '-safe', '0',
'-i', 'proben/_liste.txt', '-ar', '24000', '-ac', '1',
'stimmenband.mp3'], check=True)
print('fertig: stimmenband.mp3')
Das Skript holt sich zuerst die Liste aller deutschen Chirp-3-HD-Stimmen, lässt jede davon den Testsatz sprechen, legt die Einzelproben im Ordner proben/ ab und fügt sie danach zu einer einzigen Datei zusammen. Den Schlüssel übergibst du beim Aufruf, er steht also nicht im Skript selbst.
Aufruf:
GOOGLE_TTS_SCHLUESSEL=DEIN_SCHLUESSEL python3 stimmenprobe.py
Erwartetes Ergebnis:
30 Chirp-3-HD-Stimmen gefunden
de-DE-Chirp3-HD-Achernar
de-DE-Chirp3-HD-Achird
...
fertig: stimmenband.mp3
stimmenband.mp3 ist rund drei bis vier Minuten lang. Jede Probe beginnt mit dem Namen der Stimme, dann folgt der Testsatz. Er enthält absichtlich eine Zahl, weil Zahlen die häufigste Stolperstelle beim Vorlesen sind. Der Aufruf kostet rund 3.600 Zeichen aus dem Freikontingent. Das ist so wenig, dass du ihn ruhig mehrfach laufen lassen kannst, wenn du beim ersten Hören abgelenkt warst.
Schritt 2.2: Ein Paar wählen, das sich unterscheidet
Jetzt Kopfhörer auf und hinhören. Beim Anhören auf zwei Dinge achten:
Erstens müssen sich die beiden Stimmen deutlich voneinander abheben, sonst verschwimmt das Gespräch. Der Hörer weiß dann nicht mehr, wer gerade redet, und ein Dialog ohne erkennbare Rollen ist nur ein Monolog mit Schluckauf.
Zweitens müssen sie zwanzig Minuten lang erträglich sein, was bei sehr hellen oder sehr markanten Stimmen nicht selbstverständlich ist. Eine Stimme, die in der Probe spannend klingt, kann nach einer Viertelstunde nerven wie ein Kollege, der jeden Satz mit derselben Pointe beendet.
Bernd: „Ich nehm einfach die zwei tiefsten Männerstimmen. Tief klingt seriös.“
Tanja: „Dann hast du wieder zwei Stimmen bei 105 Hertz, und niemand hört den Unterschied. Genau diesen Fehler haben wir schon hinter uns.“
Gewählt wurden für den KI Wochenrückblick:
| Rolle | Stimme | Grundtonhöhe | Klangfarbe |
|---|---|---|---|
| Sprecher | de-DE-Chirp3-HD-Alnilam | 104 Hz | eher dunkel |
| Sprecherin | de-DE-Chirp3-HD-Leda | 206 Hz | etwas heller |
Der Abstand von rund einer Oktave sorgt dafür, dass man auch beim Nebenbei-Hören weiß, wer spricht. Eine Oktave bedeutet: Die eine Stimme schwingt ungefähr doppelt so schnell wie die andere, 104 zu 206 Hertz. Das ist der Unterschied zwischen Heim- und Auswärtstrikot, den Ulf vorhin gefordert hat. Selbst beim Abwaschen oder im Auto erkennst du sofort, wer gerade am Ball ist.
Phase 3: Ein kleines Tonstudio in einer Kiste
Montagmorgen, Kaffeeküche. Ulf hält sein Handy hoch, auf dem gerade die Google-Probe aus Phase 1 läuft.
Ulf: „Die Stimme redet. Fertig ist der Podcast, oder?“
Tanja: „Die Stimme redet einen Satz. Für achtzehn Minuten Gespräch brauchen wir jemanden, der schneidet, Pausen setzt und die Lautstärke angleicht.“
Bernd: „Dafür gibt es doch sicher einen n8n-Knoten. Zwei Klicks, und gut ist.“
Tanja: „Gibt es nicht. Und selbst wenn: Tonbearbeitung gehört nicht in n8n. Wir bauen uns ein eigenes kleines Tonstudio.“
Bernd liegt hier doppelt daneben. Einen fertigen Knoten, der aus hundert Einzelstimmen eine sauber montierte Folge macht, bringt n8n nicht mit, und Tonbearbeitung mit ffmpeg gehört ohnehin nicht hinein (das steht schon im Bauplan oben). Jetzt entsteht deshalb das Herzstück: ein Dienst namens vertonung, der auf der NAS neben n8n läuft. Stell ihn dir vor wie einen Tontechniker in einem abgeschlossenen Kämmerchen. n8n schiebt ihm durch die Durchreiche das fertige Gespräch als Liste von Beiträgen, er gibt eine fertige MP3 zurück. Dazwischen passiert, was sonst ein Tontechniker macht.
Was der Dienst tut, in einfachen Worten
Ulf: „Und was macht der Typ in seinem Kämmerchen den ganzen Tag?“
Tanja: „Vier Dinge, in fester Reihenfolge. Wie ein Trainer vor dem Spiel: erst Kader prüfen, dann aufstellen, dann spielen, dann Spielbericht.“
Schauen wir uns die vier Schritte mal an:
- Prüfen, bevor es Geld kostet. Der Dienst zählt Zeichen und Beiträge und lehnt den ganzen Auftrag ab, wenn er zu groß ist: mehr als 40.000 Zeichen, mehr als 400 Beiträge oder ein einzelner Beitrag über 4.500 Byte. Ein normaler Rückblick hat rund 20.000 Zeichen und 110 bis 150 Beiträge. Der Deckel liegt also beim Doppelten und bremst nur den Ausreißer, nicht den Betrieb. Das ist die eigentliche Kostenbremse, die das Google-Budget nicht ist. Erinnerst du dich an Phase 1? Das Budget war ein Wecker, keine Bremse. Hier sitzt jetzt die Bremse, und zwar vor der Tür, bevor Google auch nur ein Zeichen zu sehen bekommt.
- Jeden Beitrag einzeln sprechen lassen. Weil diese Anleitung kein SSML verwendet (siehe oben), gibt es für jeden Beitrag einen eigenen Aufruf bei Google, mit der Stimme des jeweiligen Sprechers. Bis zu drei Versuche pro Beitrag, außer bei Fehlern, die sich durch Wiederholen nicht bessern (falscher Schlüssel, unzulässiger Text). Das ist wie beim Elfmeter: Wenn der Ball nur knapp am Pfosten vorbeigeht, darfst du es noch mal versuchen. Wenn du aber im falschen Stadion stehst, hilft auch der dritte Anlauf nicht.
- Montieren. Das übernimmt ffmpeg, und hier steckt die eigentliche Arbeit:
- Pegelangleichung je Sprecher. Jeder Beitrag wird zu 60 Prozent an den mittleren Pegel seiner eigenen Stimme angeglichen. Die Sprünge innerhalb einer Stimme verschwinden, der Unterschied zwischen den beiden Stimmen bleibt stehen, denn auch zwei Menschen am selben Tisch sind nicht gleich laut. Eine erste Fassung hatte beide Stimmen auf denselben Wert gezogen, und prompt klangen sie einander ähnlicher.
- Pausen nach Inhalt. Nach einer Frage 240 Millisekunden, weil die Antwort prompt kommt. Vor einem kurzen Einwurf 220. Nach einem langen Beitrag 620, damit der Gedanke sich setzt. Sonst 380. Dazu ein kleines, aus der Beitragsnummer berechnetes Zittern von plus minus 50 Millisekunden, damit es nicht nach Metronom klingt, derselbe Text aber immer dieselbe Montage ergibt.
- Ein leises Rauschbett. Zwischen zwei synthetischen Beiträgen herrscht sonst digitale Stille, absolute Null, und das Ohr hört sie als Loch. Unter die ganze Spur kommt deshalb ein leises braunes Rauschen. Seine Lautstärke wird am eigenen Grundrauschen der Stimmen gemessen und drei Dezibel darunter gesetzt. Ein fester Wert, wie er zuerst eingestellt war, lag bei manchen Stimmen bis zu zwölf Dezibel darüber und war als Grummeln zu hören.
- 25 Millisekunden Ein- und Ausblende an jeder Naht, damit nichts knackt.
- Lautheit auf −16 LUFS (LUFS: ein Maß für die empfundene Lautheit; −16 ist der übliche Zielwert für Podcasts), Spitzen höchstens −1,5 dBFS.
- Beschriften. Titel, Sendungsname, Urheber, ein Titelbild und ein Kommentar mit dem Weg zurück zur Website und einem Hinweis, dass der Podcast mit künstlicher Intelligenz entsteht, landen als ID3-Angaben (die Etiketten einer MP3-Datei, die ein Abspieler anzeigt) in der Datei. Schlägt das fehl, bleibt die MP3 unbeschriftet, aber sie entsteht. Eine Folge ohne Etikett ist eine Folge. Eine Folge, die wegen eines Etiketts nicht erscheint, ist keine.
Bernd: „Rauschen reinmischen? Ich zahle doch nicht für Hi-Fi, damit wir absichtlich Rauschen draufpacken.“
Tanja: „Ohne Rauschen hörst du zwischen den Sätzen ein Loch. Mit dem Rauschbett klingt es nach Raum statt nach Funkloch.“
Tanja hat recht, und das ist einer der schönsten Kniffe im ganzen Projekt. Echte Aufnahmen haben immer ein wenig Raumklang. Synthetische Stimmen dagegen hören zwischen zwei Beiträgen komplett auf zu existieren, und genau dieses absolute Nichts empfindet dein Ohr als Fehler. Das Rauschen liegt drei Dezibel unter dem Grundrauschen der Stimmen, du hörst es also nicht als Rauschen, sondern nur als fehlendes Loch. Und die Pausenregeln? Die sind im Grunde Gesprächspsychologie in Millisekunden: Auf eine Frage antwortet man schnell, nach einem langen Gedanken atmet man durch.
Zum Schluss noch ein Wort zur Sicherheit, denn der Tontechniker sitzt ja wirklich im abgeschlossenen Kämmerchen. Der Dienst hat keinen Port nach außen (er ist nur für andere Container im selben Docker-Netz erreichbar, nicht aus dem Heimnetz und nicht aus dem Internet) und keinen Zugriff auf die Docker-Steuerung. Er kann genau eine Sache: Text in Ton verwandeln.
Ulf: „Also so eine Art Torwart, der nur im Strafraum spielen darf.“
Tanja: „Genau. Und der auch keinen Schlüssel für die Kabine hat.“
Schritt 3.1: Den Netznamen herausfinden
Damit Tontechniker und n8n sich finden, müssen sie im selben Gebäude arbeiten. Übersetzt: Der Dienst muss im selben Docker-Netz hängen wie n8n, sonst finden sich die beiden nicht. Und hier lauert die erste kleine Falle, denn der naheliegende Name ist oft der falsche.
Bernd: „Das Netz heißt natürlich n8n-network. Steht ja so in der compose-Datei.“
Tanja: „Steht da, ja. Aber Docker hängt noch etwas davor. Wir schauen nach, statt zu raten.“
Bernds Vermutung klingt logisch und ist trotzdem falsch. Du findest den echten Namen auf einem von zwei Wegen.
Klickfolge: DSM → Container Manager → linke Leiste Netzwerk → das Netz aufklappen, in dem der Container n8n-app hängt.
Oder per SSH auf der NAS:
sudo docker network ls
Erwartetes Ergebnis: Auf der DiskStation dieses Artikels heißt das Netz n8n_n8n-network. Bei einem Compose-Projekt setzt Docker den Projektnamen vor den Netznamen aus der compose-Datei. Das ist wie bei Vereinsmannschaften: Es gibt viele „Zweite Mannschaften“, aber erst mit dem Vereinsnamen davor weiß jeder, wer gemeint ist. Notiere dir deinen Namen, du brauchst ihn gleich in der letzten Zeile der compose.yaml.
Schritt 3.2: Den Ordner und die fünf Dateien anlegen
Ulf: „Fünf Dateien? Ich dachte, das ist ein kleiner Dienst.“
Tanja: „Ist er auch. Aber jede Datei hat genau einen Job. Wie in einer Werkstatt: Schlüssel, Bauplan, Betriebsanleitung, Werkzeug, Chef.“
Keine Panik, das wirkt komplizierter, als es ist. Du legst zuerst einen Ordner an und füllst ihn dann Datei für Datei.
Klickfolge: DSM → File Station → Freigabe docker → Erstellen → Ordner erstellen → vertonung.
In diesen Ordner kommen fünf Dateien: schluessel.env, Dockerfile, compose.yaml, montage.py und app.py. Am einfachsten kopierst du sie aus diesem Artikel in einen reinen Texteditor (auf dem Mac TextEdit im Modus „Reiner Text“, auf Windows Notepad), speicherst sie unter genau diesen Namen und lädst sie über File Station → Hochladen hoch. Warum ein reiner Texteditor? Weil Word und Co. heimlich Formatierungen, typografische Anführungszeichen und anderen Ballast einschmuggeln, mit dem Docker und Python nichts anfangen können. Wichtig: Die Datei Dockerfile hat keine Endung, auch kein .txt.
Bernd: „Ob da .txt hinten dran hängt, ist doch Wurst. Der Inhalt ist ja derselbe.“
Nein, ist es nicht. Docker sucht beim Bauen eine Datei, die exakt Dockerfile heißt. Eine Dockerfile.txt ist für Docker eine beliebige Textdatei, die es schlicht nicht beachtet, und der Bau scheitert. Vor allem Windows versteckt Dateiendungen gern, also lieber zweimal hinschauen.

Der Ordner docker › vertonung auf der DiskStation dieses Artikels, noch in der ersten Fassung mit vier Dateien. Mit dem Dockerfile aus dieser Anleitung sind es fünf.
Datei 1: schluessel.env. Das ist der Tresor mit genau einem Fach. Genau eine Zeile, dein Schlüssel aus Phase 1:
GOOGLE_TTS_SCHLUESSEL=DEIN_SCHLUESSEL
Keine Anführungszeichen, keine Leerzeichen, und die Datei sollte mit einem normalen Zeilenende enden, nicht mit einem Windows-Zeilenende. Diese Datei gehört in keine Sicherung, die die NAS verlässt, und in kein Dokument. Erinnere dich an Phase 1: Der Schlüssel gehört an genau einen Ort, und das ist dieser.
Datei 2: Dockerfile. Der Bauplan für das Abbild des Dienstes. Denk an eine Fertighaus-Anleitung: Sie beschreibt einmal genau, was in die Kiste kommt. Er legt die Python-Fassung genau fest und installiert ffmpeg einmal beim Bauen. Danach startet der Container in Sekunden, auch ohne Internetverbindung, und eine spätere Paketversion kann sein Verhalten nicht unbemerkt ändern. Anpassen musst du hier nichts.
# Bauplan fuer das Abbild des Vertonungsdienstes.
# Die Python-Fassung ist genau festgelegt (3.12.14 auf Debian 13 "trixie").
# ffmpeg wird EINMAL beim Bauen installiert, nicht bei jedem Start:
# Der Container startet danach in Sekunden und auch ohne Internet.
FROM python:3.12.14-slim-trixie
RUN apt-get update \
&& apt-get install -y --no-install-recommends ffmpeg \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# app.py und montage.py werden nicht ins Abbild kopiert, sondern in
# compose.yaml eingehaengt. So wirkt eine Aenderung nach einem Neustart,
# ohne dass neu gebaut werden muss.
CMD ["python", "/app/app.py"]
Datei 3: compose.yaml. Wenn das Dockerfile der Bauplan ist, dann ist die compose-Datei die Betriebsanleitung. Sie sagt dem Container Manager, wie der Container aus diesem Abbild gestartet wird. Anpassen musst du drei Stellen, alle mit DEIN oder /volume1 markiert:
- die beiden Pfade, falls dein Docker-Ordner nicht auf
/volume1liegt (auf der DiskStation dieses Artikels ist es/volume1), SENDUNG,URHEBERundWEBSITE: damit wird die MP3 beschriftet,- ganz unten
DEIN_N8N_NETZ: der Netzname aus Schritt 3.1.
services:
vertonung:
# Das Abbild wird aus dem Dockerfile im selben Ordner gebaut:
# Python 3.12.14 und ffmpeg, beides beim Bauen festgeschrieben.
build: .
image: vertonung:1
container_name: vertonung
restart: unless-stopped
working_dir: /app
volumes:
# Pfade anpassen, falls dein Docker-Ordner nicht auf /volume1 liegt.
- /volume1/docker/vertonung/app.py:/app/app.py:ro
# montage.py wird NICHT kopiert, sondern eingehaengt: es soll genau eine
# Fassung der Montage geben.
- /volume1/docker/vertonung/montage.py:/app/montage.py:ro
# Der Google-Schluessel steht NICHT in dieser Datei. Er liegt daneben in
# "schluessel.env" mit genau einer Zeile: GOOGLE_TTS_SCHLUESSEL=...
env_file:
- schluessel.env
environment:
PORT: "8080"
SPRACHE: "de-DE"
STIMME_M: "de-DE-Chirp3-HD-Alnilam"
STIMME_W: "de-DE-Chirp3-HD-Leda"
# Beschriftung der MP3 - ANPASSEN:
SENDUNG: "DEIN_SENDUNGSNAME"
URHEBER: "DEIN_NAME_ODER_DEINE_MARKE"
WEBSITE: "https://DEINE_DOMAIN/"
# Harte Grenzen. Ein normaler Rueckblick hat rund 20.000 Zeichen und
# 110 bis 150 Beitraege - der Deckel liegt beim Doppelten und bremst nur
# den Ausreisser, nicht den Betrieb.
MAX_ZEICHEN: "40000"
MAX_BEITRAEGE: "400"
MAX_JE_BEITRAG: "4500"
ZEITLIMIT_SEKUNDEN: "900"
# Kein "ports:" - der Dienst ist absichtlich NUR im Docker-Netz erreichbar,
# nicht aus dem Heimnetz und nicht von aussen.
networks:
- ki
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request;urllib.request.urlopen('http://127.0.0.1:8080/gesund',timeout=10)"]
interval: 60s
timeout: 15s
retries: 3
start_period: 30s
# Sicherheitsschrauben: keine neuen Rechte, kein Docker-Socket.
security_opt:
- no-new-privileges:true
# Genug Luft, um rund 150 Tonstuecke einzeln nachzubearbeiten.
mem_limit: 2g
pids_limit: 256
networks:
ki:
# ANPASSEN: das Netz, in dem n8n haengt (Schritt 3.1).
external: true
name: DEIN_N8N_NETZ
Datei 4: montage.py. Die Tonmontage, also das Werkzeug, das Pausen, Pegel und Rauschbett aus der Liste oben umsetzt. Sie wird unverändert übernommen.
# -*- coding: utf-8 -*-
"""Montage der Sprachstuecke zu einer Podcast-Spur.
Behebt die drei Maengel, die am 14.09.2026 im Vergleich mit einem fremden
KI-Podcast gemessen wurden:
1. digitale Stille an den Nahtstellen -> leises Grundrauschen unter der Spur
2. starre Pausen von 350 ms -> Laenge nach Textinhalt gestaffelt
3. Pegelspruenge zwischen den Beitraegen -> teilweise Angleichung je Beitrag
Die drei Schritte lassen sich einzeln abschalten (rauschbett, angleichen,
loudnorm). Das ist kein Zierrat: am 14.09.2026 stellte sich heraus, dass
dieselbe Montage, die den Abstand zu einem fremden Podcast geschlossen hat,
zugleich die Stimmen einander aehnlicher macht - und ohne Schalter laesst sich
nicht feststellen, welcher Schritt dafuer verantwortlich ist.
Die Angleichung wirkt je Sprecher getrennt: sie glaettet Spruenge innerhalb
einer Stimme, laesst den Pegelunterschied zwischen den beiden Stimmen aber
stehen, weil auch zwei Menschen an einem Tisch nicht gleich laut sind.
Aufruf als Skript montiert ~/tts-test/chirp nach ~/mnt/Downloads/probe-chirp3hd-v2.mp3.
Als Baustein: from montage import montiere; montiere(teileordner, zieldatei)
"""
import json, os, re, subprocess, sys
BASIS = os.path.expanduser('~/tts-test')
SR = 24000
ANGLEICH = 0.60 # nur teilweise angleichen - volle Angleichung nimmt dem
# Gespraech die natuerliche Dynamik (Zielgroesse LRA ~4 LU)
BODEN_DB = None # Grundrauschen. None = automatisch am eigenen Rauschboden der
# Sprachdateien ausrichten. Ein fester Wert (z.B. -62.5) war bis
# zum 14.09.2026 eingestellt und lag damit bis zu 12 dB UEBER dem
# Boden mancher Stimmen - hoerbar als Grummeln.
BODEN_ABSTAND = 3.0 # so viel unter dem gemessenen Boden der Stimmen
BLENDE = 0.025 # 25 ms Ein- und Ausblende an jeder Naht
def sh(*a):
return subprocess.run(a, capture_output=True, text=True)
def dauer(p):
return float(sh('ffprobe', '-v', 'error', '-show_entries', 'format=duration',
'-of', 'default=nw=1:nk=1', p).stdout.strip())
def rauschboden(p):
"""Der eigene Rauschboden einer Sprachdatei: das 5. Perzentil der
20-ms-Fenster, ohne die digitale Stille am Anfang und Ende."""
r = subprocess.run(['ffmpeg', '-v', 'error', '-i', p, '-ac', '1', '-ar', str(SR),
'-f', 's16le', '-acodec', 'pcm_s16le', '-'], capture_output=True)
import array, math
roh = array.array('h'); roh.frombytes(r.stdout)
W = int(0.02 * SR); n = len(roh) // W
werte = []
for i in range(n):
s2 = sum(float(v) * v for v in roh[i*W:(i+1)*W]) / W
if s2 > 0:
werte.append(10 * math.log10(s2 / (32768.0 ** 2)))
if not werte: return -70.0
werte.sort()
return werte[max(0, int(len(werte) * 0.05))]
def mittelpegel(p):
e = sh('ffmpeg', '-i', p, '-af', 'volumedetect', '-f', 'null', '-').stderr
return float(re.search(r'mean_volume: (-?[\d.]+) dB', e).group(1))
def lade_turns(probe=None):
return json.load(open(probe or os.path.join(BASIS, 'probe.json')))['turns']
def pause_nach(turns, i):
"""Pause zwischen Beitrag i und i+1, in Sekunden.
Die Laenge folgt dem Text, nicht dem Zufall: das Zittern wird aus dem
Beitragsindex abgeleitet, damit derselbe Text immer dieselbe Montage ergibt.
"""
vor, nach = turns[i]['t'], turns[i + 1]['t']
if vor.rstrip().endswith('?'): grund = 0.240 # Antwort kommt prompt
elif len(nach) < 30: grund = 0.220 # kurzer Einwurf
elif len(vor) > 200: grund = 0.620 # Gedanke setzt sich
else: grund = 0.380
zitter = ((i * 37) % 101 - 50) / 1000.0 # +/- 50 ms, reproduzierbar
return round(max(0.15, grund + zitter), 3)
def montiere(teileordner, zieldatei, arbeitsordner=None, turns=None, leise=False,
rauschbett=True, angleichen=True, loudnorm=True, titelbild=None,
tags=None):
turns = turns or lade_turns()
arbeit = arbeitsordner or os.path.join(teileordner, '_montage')
os.makedirs(arbeit, exist_ok=True)
def sag(*a):
if not leise: print(*a)
# Zwei Fallen stecken in diesen zwei Zeilen, und beide schnappen erst beim
# 100. Beitrag zu. Bis zum 18.09.2026 stand hier `teil\d\d\.mp3` und ein
# gewoehnliches sorted():
# 1. Der Ausdruck passte nur auf ZWEI Ziffern. `teil100.mp3` fiel durch,
# und von 149 Tonstuecken kamen 100 an - die Zusicherung darunter hat
# das gemeldet, sonst waere eine um ein Drittel gekuerzte Folge
# herausgekommen, der man nichts angesehen haette.
# 2. sorted() auf Zeichenketten haette auch mit drei Ziffern falsch
# geordnet: 'teil100' steht alphabetisch VOR 'teil99'. Die Folge waere
# vollstaendig gewesen und trotzdem durcheinander.
# Deshalb jetzt: beliebig viele Ziffern, und nach ZAHL sortiert, nicht nach
# Zeichen. Das vertraegt sich mit alten Ordnern (teil00.mp3) genauso.
muster = re.compile(r'teil(\d+)\.mp3')
treffer = [(int(m.group(1)), m.group(0))
for m in (muster.fullmatch(f) for f in os.listdir(teileordner)) if m]
teile = [f for _, f in sorted(treffer)]
assert len(teile) == len(turns), f'{len(teile)} Teile, {len(turns)} Beitraege'
pfade = [os.path.join(teileordner, f) for f in teile]
pegel = [mittelpegel(q) for q in pfade]
# Zielpegel JE SPRECHER: der Unterschied zwischen den Stimmen bleibt stehen,
# geglaettet werden nur die Spruenge innerhalb einer Stimme.
ziel = {}
for s in {t['s'] for t in turns}:
eigene = [pegel[k] for k, t in enumerate(turns) if t['s'] == s]
ziel[s] = sorted(eigene)[len(eigene) // 2]
angepasst = []
for k, q in enumerate(pfade):
z = os.path.join(arbeit, 'a%02d.wav' % k)
v = (ziel[turns[k]['s']] - pegel[k]) * ANGLEICH if angleichen else 0.0
d = dauer(q)
filt = (f'volume={v:.2f}dB,afade=t=in:st=0:d={BLENDE},'
f'afade=t=out:st={max(0.0, d - BLENDE):.3f}:d={BLENDE}')
sh('ffmpeg', '-v', 'error', '-i', q, '-af', filt, '-ar', str(SR), '-ac', '1',
'-c:a', 'pcm_s16le', z, '-y')
angepasst.append(z)
nachher = [mittelpegel(z) for z in angepasst]
sag(f' Pegelspanne {max(pegel)-min(pegel):.1f} dB -> {max(nachher)-min(nachher):.1f} dB'
+ ('' if angleichen else ' (Angleichung aus)'))
liste, pausen = os.path.join(arbeit, 'liste.txt'), []
with open(liste, 'w') as fh:
for k, z in enumerate(angepasst):
fh.write("file '%s'\n" % z)
if k < len(angepasst) - 1:
p = pause_nach(turns, k); pausen.append(p)
sp = os.path.join(arbeit, 'p%02d.wav' % k)
sh('ffmpeg', '-v', 'error', '-t', str(p), '-f', 'lavfi',
'-i', f'anullsrc=r={SR}:cl=mono', '-c:a', 'pcm_s16le', sp, '-y')
fh.write("file '%s'\n" % sp)
sag(f' Pausen {len(pausen)}: {min(pausen)*1000:.0f}-{max(pausen)*1000:.0f} ms')
rede = os.path.join(arbeit, 'rede.wav')
sh('ffmpeg', '-v', 'error', '-f', 'concat', '-safe', '0', '-i', liste,
'-c:a', 'pcm_s16le', '-ar', str(SR), '-ac', '1', rede, '-y')
quelle = rede
if rauschbett:
gesamt = dauer(rede)
if BODEN_DB is None:
boeden = sorted(rauschboden(q) for q in pfade)
gemessen = boeden[len(boeden) // 2]
# loudnorm hebt das Ergebnis spaeter um rund 4 dB an
boden_db = gemessen - BODEN_ABSTAND - (4.0 if loudnorm else 0.0)
sag(f' Rauschboden der Stimmen {gemessen:.1f} dBFS -> Bett auf {boden_db:.1f} dB')
else:
boden_db = BODEN_DB
bett = os.path.join(arbeit, 'bett.wav')
sh('ffmpeg', '-v', 'error', '-t', f'{gesamt+0.5:.2f}', '-f', 'lavfi',
'-i', f'anoisesrc=color=brown:r={SR}:a=0.30',
'-af', 'lowpass=f=2500,highpass=f=40', '-ac', '1', '-c:a', 'pcm_s16le', bett, '-y')
bett2 = os.path.join(arbeit, 'bett2.wav')
sh('ffmpeg', '-v', 'error', '-i', bett,
'-af', f'volume={boden_db - mittelpegel(bett):.2f}dB',
'-c:a', 'pcm_s16le', bett2, '-y')
gemischt = os.path.join(arbeit, 'gemischt.wav')
sh('ffmpeg', '-v', 'error', '-i', rede, '-i', bett2,
'-filter_complex', '[0:a][1:a]amix=inputs=2:duration=first:normalize=0[a]',
'-map', '[a]', '-ar', str(SR), '-ac', '1', '-c:a', 'pcm_s16le', gemischt, '-y')
quelle = gemischt
else:
sag(' (Rauschbett aus)')
af = ['-af', 'loudnorm=I=-16:TP=-1.5:LRA=11'] if loudnorm else []
if not loudnorm: sag(' (loudnorm aus)')
r = sh('ffmpeg', '-v', 'error', '-i', quelle, *af,
'-ar', str(SR), '-ac', '1', '-c:a', 'libmp3lame', '-q:a', '4', zieldatei, '-y')
if r.returncode != 0:
raise RuntimeError('ffmpeg: ' + r.stderr[-400:])
if titelbild or tags:
beschriften(zieldatei, titelbild, tags, sag)
sag(f' -> {zieldatei} {os.path.getsize(zieldatei)} Byte, {dauer(zieldatei):.2f} s')
return zieldatei
def beschriften(mp3, bild=None, tags=None, sag=print):
"""Die fertige MP3 beschriften: Titelbild und ID3-Felder.
Warum das Bild: Apple Podcasts zeigt Folgenbilder aus dem Feed in der
eigenen App kaum noch an; Apps wie Overcast nehmen stattdessen das Bild
aus der Datei. Ein Bild an beiden Stellen deckt zwei Leserschaften ab.
Warum die Textfelder: Eine MP3 ohne Beschriftung heisst im Abspieler wie
ihre Datei und fuehrt nirgendwohin. Am 26.09.2026 nachgesehen - die
veroeffentlichte Folge trug genau einen Eintrag, `encoder`. Wer die Datei
weitergibt, hatte nichts in der Hand, was zur Website zurueckfuehrt.
Warum ein zweiter ffmpeg-Aufruf und nicht ein einziger: Die Tonspur wird
hier nur KOPIERT (-c:a copy). Alles in einem Durchgang haette bedeutet,
den Ton neu zu kodieren - und damit die sorgfaeltig eingestellte
Lautheit ein zweites Mal durch den Kodierer zu schicken.
Und die wichtigste Regel steht am Ende: Schlaegt das fehl, bleibt die MP3
wie sie war. Eine Folge ohne Beschriftung ist eine Folge. Eine Folge, die
wegen einer Beschriftung nicht erscheint, ist keine.
"""
hat_bild = bool(bild and os.path.exists(bild) and os.path.getsize(bild) > 0)
tags = {k: str(v) for k, v in (tags or {}).items() if v not in (None, '')}
if not hat_bild and not tags:
sag(' (nichts zu beschriften)')
return False
ziel = mp3 + '.beschriftet.mp3'
# Ein JPEG wird UNVERAENDERT uebernommen (-c:v copy). Am 26.09.2026 gemessen,
# was der Unterschied ausmacht: Mit '-c:v mjpeg' rechnete ffmpeg das
# Titelbild von 489 kB auf 116 kB herunter - das Bild wurde also ohne Not
# ein zweites Mal komprimiert. Bei einer Folge von mehreren Megabyte sind
# die paar hundert Kilobyte den Unterschied nicht wert.
# Ein PNG wird weiterhin umgewandelt: Es waere sonst um ein Vielfaches
# groesser als dasselbe Motiv als JPEG.
befehl = ['ffmpeg', '-v', 'error', '-i', mp3]
if hat_bild:
with open(bild, 'rb') as fh:
ist_jpeg = fh.read(3) == b'\xff\xd8\xff'
befehl += ['-i', bild, '-map', '0:a', '-map', '1:v',
'-c:v', 'copy' if ist_jpeg else 'mjpeg',
'-metadata:s:v', 'title=Album cover',
'-metadata:s:v', 'comment=Cover (front)',
'-disposition:v', 'attached_pic']
else:
befehl += ['-map', '0:a']
befehl += ['-c:a', 'copy', '-id3v2_version', '3']
for name, wert in tags.items():
befehl += ['-metadata', '%s=%s' % (name, wert)]
befehl += [ziel, '-y']
r = sh(*befehl)
if r.returncode != 0 or not os.path.exists(ziel) or os.path.getsize(ziel) == 0:
if os.path.exists(ziel):
os.remove(ziel)
# Zweiter Anlauf OHNE Bild, wenn es Textfelder gibt. Am 26.09.2026
# beim Blindtest aufgefallen: Ein unbrauchbares Bild kostete sonst
# auch Titel und Rueckweg - und die haetten nichts dafuer gekonnt.
if hat_bild and tags:
sag(' Titelbild unbrauchbar, beschrifte nur mit Text')
return beschriften(mp3, None, tags, sag)
sag(' nicht beschriftet: ' + (r.stderr[-200:] or 'ffmpeg ohne Meldung'))
return False
os.replace(ziel, mp3)
sag(' beschriftet: %s%s'
% ('Titelbild %d Byte' % os.path.getsize(bild) if hat_bild else 'ohne Bild',
(', Felder: ' + ', '.join(sorted(tags))) if tags else ''))
return True
if __name__ == '__main__':
montiere(os.path.join(BASIS, 'chirp'),
os.path.expanduser('~/mnt/Downloads/probe-chirp3hd-v2.mp3'))
Datei 5: app.py. Der eigentliche Dienst, sozusagen der Chef am Empfang. Er nimmt Aufträge an, prüft sie, lässt Google sprechen und ruft die Montage auf. Anpassen musst du hier nichts: Sendungsname, Urheber und Website liest der Dienst aus compose.yaml. Stehen dort noch die Platzhalter, schreibt er beim Start eine Warnung ins Protokoll.
Ulf: „Warum nicht alles in eine Datei? Weniger Dateien, weniger Stress.“
Tanja: „Weil es genau eine Fassung der Montage geben soll. Kopien sind der Anfang vom Chaos.“
Warum zwei Dateien statt einer? Weil es genau eine Fassung der Montage geben soll. Die Montage steckt in montage.py, und app.py bindet sie nur ein. Dass das keine Theorie ist, zeigt eine Erfahrung aus diesem Projekt: In einem anderen Teil desselben Projekts stand eine Hilfsfunktion in vier Kopien in vier Workflows, repariert wurde nur eine. Wochenlang liefen drei kaputte weiter, ohne dass es jemand merkte. Seitdem wird hier eingebunden statt kopiert. Das ist dasselbe Prinzip wie im Bauplan oben: Jede Tatsache, und jedes Stück Logik, steht an genau einer Stelle.
Schritt 3.3: Den Container starten
Jetzt wird es spannend. Aus fünf Textdateien wird ein laufender Dienst.
Bernd: „Ich hätte das ja einfach über die Registry gezogen. Warum selbst bauen?“
Tanja: „Weil es dieses Abbild nirgends fertig gibt. Unser Dockerfile ist der Bauplan, und der Container Manager baut danach.“
Klickfolge: Container Manager → linke Leiste Projekt → Erstellen → Projektname: vertonung → Pfad: den Ordner docker/vertonung wählen → Quelle: „Vorhandene docker-compose.yml verwenden“ (der Assistent erkennt compose.yaml) → Weiter → Fertig.
Lass dich nicht davon irritieren, dass der Assistent von docker-compose.yml spricht, obwohl deine Datei compose.yaml heißt. Beides meint dasselbe, und der Assistent findet die Datei.
Erwartetes Ergebnis: Ein Terminalfenster mit dem Titel „Erstellen – vertonung“ zeigt, wie das Abbild gebaut wird: erst das Python-Grundabbild, dann die ffmpeg-Pakete, dann fünf Bauschritte. Beim ersten Mal dauert das einige Minuten. Zeit für einen Kaffee. Es endet sinngemäß mit:
Successfully built …
Successfully tagged vertonung:1
Container vertonung Created
Container vertonung Starting
Container vertonung Started
Exit Code: 0
Dazwischen steht auf vielen DiskStations diese Meldung:
Your kernel does not support PIDs limit capabilities or the cgroup is not mounted. PIDs limit discarded.
Bernd: „Da! Kernel does not support. Kaputt. Ich hab doch gesagt, Synology taugt nichts.“
Durchatmen. Das ist kein Fehler. Die Zeile pids_limit: 256 wird auf diesem Kernel stillschweigend ignoriert. Sie begrenzt, wie viele Prozesse der Container starten darf, und der Kernel der DiskStation kennt diese Begrenzung einfach nicht. Sie bleibt trotzdem in der Datei, sie kostet nichts und wirkt auf einem System, das sie kann. Du solltest nur wissen, dass sie hier kein Schutz ist. Ehrlich gesagt ist das eine der nervigen Stellen: Eine Meldung, die nach Alarm klingt und doch nur ein Achselzucken verdient.

Der Bau mit dem Dockerfile, hier an einem Testprojekt vertonung-test auf derselben DiskStation: fünf Bauschritte, dann „Successfully built“, der Container startet, Exit Code 0.
Nach dem Bau startet der Container in wenigen Sekunden. In den ersten 30 Sekunden zeigt die Projektliste noch „starting“, weil die Gesundheitsprüfung erst einmal durchlaufen muss. Das ist normal. Wie beim Aufwärmen vor dem Anpfiff: Der Spieler steht schon auf dem Platz, aber der Schiedsrichter hat noch nicht gepfiffen.

Kurz nach dem Start ist das Projekt grün.
Jetzt schauen wir dem Tontechniker einmal über die Schulter und lesen sein Protokoll.
Klickfolge zum Nachsehen: Container Manager → Container → vertonung → Protokoll.
Erwartetes Ergebnis: eine Zeile
Vertonung hoert auf Port 8080, Schluessel gesetzt
Steht dort Schluessel FEHLT, wurde schluessel.env nicht gelesen. Der Dienst läuft dann zwar, würde aber jeden Auftrag ablehnen.

So sieht es aus, wenn der Schlüssel fehlt: Der Dienst läuft und beantwortet die Gesundheitsprüfung, würde aber jeden Auftrag ablehnen.
Ulf: „Und dann? Einfach auf Neustart drücken, oder?“
Tanja: „Genau das hilft eben nicht. Neustart heißt: gleicher Container, gleicher alter Stand.“
Hier liegt ein echter Klassiker. Die Ursache ist fast immer dieselbe: Die Datei wurde nach dem ersten Start angelegt oder geändert. Docker liest env_file nur beim Erzeugen des Containers, nicht bei einem Neustart. Stell dir vor, der Container bekommt beim Einzug einmal einen Zettel mit dem Schlüssel in die Hand gedrückt. Ein Neustart ist wie kurz aus dem Raum gehen und wiederkommen: Er hat immer noch den alten Zettel. Erst ein neuer Einzug bringt einen neuen Zettel. Lösung: Container Manager → Projekt → vertonung auswählen → Beenden, danach Erstellen. Erst dieser Neuaufbau erzeugt den Container neu und liest die Datei; ein bloßer Neustart des Containers genügt nicht.
Eine kosmetische Eigenheit: Nach einem Neuaufbau zeigt die DSM-Oberfläche den Container manchmal unter einem Namen wie 49815ad03e31_vertonung. Das ist ein Zwischenname, den Docker kurz vergibt und DSM in der Anzeige behält. Docker selbst kennt ihn nicht. Nichts reparieren, der Dienst ist im Netz weiter unter vertonung erreichbar. Wer hier anfängt, Container umzubenennen oder zu löschen, repariert etwas, das nicht kaputt ist.
Schritt 3.4: Die Gesundheitsprüfung aus n8n heraus
Ulf: „Ich tipp einfach die Adresse in den Browser und schau, ob er antwortet.“
Tanja: „Geht nicht. Der Dienst hat keinen Port nach außen, dein Browser kommt da gar nicht hin.“
Genau so haben wir es in der Einleitung zu dieser Phase gewollt. Der Dienst ist nur aus dem Docker-Netz erreichbar, also prüfen wir ihn von dort, wo er später auch angesprochen wird: aus n8n. Das hat einen schönen Nebeneffekt. Wenn der Test klappt, weißt du nicht nur, dass der Dienst lebt, sondern auch, dass n8n ihn unter seinem Namen findet.
Klickfolge: In n8n einen neuen Workflow „Vertonung testen“ anlegen → Add first step → Trigger manually → + → HTTP Request → Method: GET → URL: http://vertonung:8080/gesund → Execute step.
Beachte die Adresse: Statt einer IP-Adresse steht dort einfach vertonung. Innerhalb eines Docker-Netzes sind Containernamen gleichzeitig Adressen, so wie im Büro der Zuruf „Tanja!“ reicht, ohne Zimmernummer.
Erwartetes Ergebnis:
{
"ok": true,
"ffmpeg": "ffmpeg version 7.1.5-…",
"schluessel_gesetzt": true,
"stimmen": { "m": "de-DE-Chirp3-HD-Alnilam", "w": "de-DE-Chirp3-HD-Leda" },
"grenzen": { "zeichen": 40000, "beitraege": 400, "je_beitrag": 4500 }
}

Der Knoten „Gesund“: links die Adresse, rechts die Antwort mit ok: true, schluessel_gesetzt: true, den beiden Stimmen und den Grenzen.
Du siehst hier auf einen Blick alles, was zählt: ok, ob ffmpeg da ist, ob der Schlüssel gesetzt ist, welche beiden Stimmen hinterlegt sind und welche Grenzen die Kostenbremse zieht.
Bernd: „Bei mir kommt nur ein Login-Fenster. Ich geh ja immer direkt über die IP und Port 5678, das ist schneller.“
Und genau das ist eine Falle, die einen ganzen Abend kosten kann: Öffne n8n über die Adresse, unter der du es sonst auch benutzt, also deine Domain. Wer n8n zwischendurch über die IP-Adresse der NAS und Port 5678 aufruft, landet in einer anderen, oft abgelaufenen Sitzung und sucht den Fehler an der falschen Stelle. Bernds Abkürzung ist also keine. Sie führt in eine Parallelwelt, in der nichts so aussieht wie erwartet.
Schritt 3.5: Die erste echte Probe
Ulf: „Und jetzt? Redet der Kasten endlich?“
Tanja: „Jetzt redet er. Das ist der Anpfiff.“
Jetzt spricht Google zum ersten Mal über den Dienst. Bisher haben wir nur geprüft, ob der Tontechniker im Kämmerchen sitzt. Jetzt geben wir ihm den ersten echten Auftrag: ein Mini-Gespräch mit vier Beiträgen. Einen zweiten HTTP-Request-Knoten anhängen:
- Method:
POST - URL:
http://vertonung:8080/vertonen - Send Body: an → Body Content Type:
JSON→ Specify Body:Using JSON→
{"turns": [
{"s": "w", "t": "Willkommen zur ersten Probe. Kannst du mich hören?"},
{"s": "m", "t": "Laut und deutlich. Und ich klinge sogar ein bisschen anders als du."},
{"s": "w", "t": "Das ist der Sinn der Sache. Wie lang wird das hier?"},
{"s": "m", "t": "Etwa zwanzig Sekunden. Dann ist die Probe vorbei."}
]}
- Options → Add option → Response → Response Format:
File→ Put Output in Field:data - Options → Timeout:
900000
Zwei dieser Optionen verdienen einen Satz. Response Format File sagt n8n: Was zurückkommt, ist keine Textantwort, sondern eine Datei, bitte als Datei ablegen. Und der Timeout von 900000 Millisekunden, also 15 Minuten, gibt dem Dienst genug Luft für eine echte Folge später. Die Probe hier ist in Sekunden fertig, aber eine ganze Folge mit über hundert einzelnen Google-Aufrufen braucht deutlich länger, und ein zu knapper Timeout würde n8n mitten in der Montage auflegen lassen.
Execute step.
Erwartetes Ergebnis: Nach etwa acht Sekunden liegt im Ausgabefenster unter Binary eine Datei data vom Typ audio/mpeg, rund 150 KB groß, etwa 20 Sekunden lang. Über View kannst du sie direkt im Browser anhören. Zwei Stimmen, natürliche Pausen, kein Knacken.
Ulf: „Wow. Das klingt ja wie echt.“
Bernd: „Hätte ich auch hingekriegt.“
Wer nachmessen will, statt nur zu staunen, lädt die Datei herunter und fragt ffprobe:
ffprobe -v error -show_entries stream=sample_rate,channels -show_entries format=duration probe.mp3
Erwartetes Ergebnis: sample_rate=24000, channels=1, Dauer um 20 Sekunden. Nicht irritieren lassen, wenn der Browser 48.000 Hertz anzeigt: Das ist die Abtastrate, auf die der Browser beim Abspielen hochrechnet, nicht die der Datei. Die Datei selbst ist mono mit 24.000 Hertz, und für zwei Sprechstimmen ist das völlig ausreichend.
Damit ist der Dienst abgenommen. Er hört, Google nimmt den Schlüssel an, ffmpeg montiert, und n8n erreicht ihn unter seinem Namen. Vier Prüfungen, vier Haken. Den Testworkflow kannst du archivieren.
Phase 4: WordPress bekommt einen Podcast-Feed
Montagmorgen, Besprechungsraum. Auf dem Whiteboard steht noch „Phase 3 erledigt“, daneben hat jemand einen kleinen Pokal gemalt.
Bernd: „So, jetzt kommt der teure Teil. Für einen richtigen Podcast brauchen wir einen Hosting-Anbieter mit Abo. Sonst nimmt uns Apple doch gar nicht.“
Tanja: „Brauchen wir nicht. Ein Podcast ist im Kern eine einzige Textdatei im Internet. Die baut uns WordPress.“
Ulf: „Eine Textdatei? Ich dachte, da steckt irgendwas Geheimes von Apple dahinter.“
Keine Panik, hier steckt nichts Geheimes dahinter. Ein Podcast ist, technisch betrachtet, erstaunlich unspektakulär: eine RSS-Datei (ein standardisiertes XML-Format, das Neuigkeiten als Liste von Einträgen beschreibt) mit ein paar zusätzlichen Angaben für Apple, und in jedem Eintrag ein Verweis auf eine MP3-Datei. Stell dir den Spielplan am Schwarzen Brett des Vereinsheims vor: Wer wissen will, ob ein neues Spiel angesetzt ist, geht regelmäßig hin und schaut nach. Genau so arbeiten Apple Podcasts, Spotify und alle anderen Apps. Sie rufen diese Datei regelmäßig ab und schauen, ob etwas Neues darin steht. Mehr ist es nicht.
Bernds Abo ist also nicht nötig. Ein Hosting-Anbieter macht auch nichts anderes, als so eine Datei bereitzustellen. Das kann deine Website selbst.
WordPress kann solche Feeds von Haus aus erzeugen, nur eben keinen Podcast-Feed. Den bauen wir mit einem einzigen Code Snippet, also einem kleinen Stück Programmcode, das du in WordPress einhängst, ohne an den Dateien des Themes herumzuschrauben.
Schritt 4.1: Code Snippets installieren
Damit du Code sauber einhängen kannst, brauchst du zuerst das passende Werkzeug. Das Plugin Code Snippets ist dein Werkzeugkasten: Es verwaltet solche Codestücke, lässt sie einzeln an- und abschalten und fängt Fehler ab, bevor sie deine Seite lahmlegen.
Klickfolge: WordPress-Admin → Plugins → Installieren → Suchfeld Code Snippets → beim Plugin „Code Snippets“ von Code Snippets Pro auf Jetzt installieren → Aktivieren.
Erwartetes Ergebnis: In der linken Leiste erscheint der Menüpunkt Snippets.
Schritt 4.2: Das Sendungsbild hochladen
Bernd: „Titelbild ist einfach. Wir nehmen unser Logo und blasen es auf 3000 Pixel auf. Je größer, desto besser.“
Tanja: „Größer heißt hier nur unschärfer. Und entscheidend ist ohnehin, wie das Bild ganz klein aussieht.“
Jeder Podcast braucht ein Titelbild, das Trikot deiner Sendung sozusagen. Apple verlangt: quadratisch, zwischen 1400 und 3000 Pixel Kantenlänge (bevorzugt 3000), JPEG oder PNG, RGB-Farbraum, ohne Transparenz. Für diesen Aufbau halten wir die Bilder zusätzlich unter 512 KB. Das ist keine aktuelle Apple-Pflicht, hält aber Feed und Abrufe klein und hat sich im praktischen Test bewährt.
Der eigentliche Prüfstein ist aber nicht die Größe, sondern die kleinste Darstellung: In der Folgenliste einer Podcast-App ist das Bild nur rund 55 Pixel groß. Was dort nicht mehr lesbar ist, ist überflüssig.
Ulf: „55 Pixel? Das ist ja kleiner als die Vereinswappen in der Tabelle auf dem Handy.“
Tanja: „Genau der Vergleich. Und trotzdem erkennst du jeden Verein am Wappen, weil die Form stark genug ist.“
So ist es auch hier. Beim KI Wochenrückblick bleibt in dieser Größe das große orangefarbene „KI“ lesbar, der Ring erkennbar, der Schriftzug wird zur Textur. Das ist gewollt. Ein Titelbild, das erst in voller Größe funktioniert, funktioniert nie.
Noch ein Detail, und hier liegt Bernd daneben: Nicht jede erlaubte Größe ist sinnvoll. Das FOUNDIC-Logo ist nur 827 Pixel breit. Bei einem 3000er Bild hätte es hochgerechnet werden müssen und wäre das unschärfste Element geworden. Aufblasen erzeugt keine neuen Details, es verschmiert nur die vorhandenen. Deshalb ist das Sendungsbild 2048 × 2048 Pixel groß und 227 KB schwer. Das liegt sauber im erlaubten Bereich und nutzt, was das Logo hergibt.
Jetzt kommt das Bild auf deine Website. Die Adresse, die WordPress dafür vergibt, brauchst du gleich im Snippet.
Klickfolge: Medien → Datei hinzufügen → Bild hochladen → in der Mediathek anklicken → Datei-URL kopieren.

Der Upload-Dialog der Mediathek. Die maximale Dateigröße (hier 128 MB) ist später auch für die MP3-Dateien wichtig. Eine Folge hat rund 9 MB.
Schritt 4.3: Das Feed-Snippet anlegen
Ulf: „Und jetzt? Kopier ich einfach irgendeinen Code aus dem Internet rein?“
Tanja: „Du kopierst genau diesen Code. Aber vorher schauen wir uns an, was er tut. Sonst weißt du nachher nicht, wo du suchen musst, wenn etwas hakt.“
Schauen wir uns das mal an. Der Code tut vier Dinge:
- Er meldet vier Zusatzfelder am Beitrag an:
podcast_url(Adresse der MP3),podcast_bytes(Dateigröße, Pflicht für Apple),podcast_dauer(z. B.18:45) undpodcast_skript(das Gespräch im Klartext). Stell dir das wie zusätzliche Fächer an jedem Beitrag vor. Der Workflow schreibt diese Felder später, der Feed liest genau sie. Die Angabeshow_in_restist dabei zwingend. Ohne sie nimmt WordPress die Felder über die Schnittstelle stillschweigend nicht an und antwortet trotzdem mit „200 OK“. - Er richtet die Adresse
/feed/podcast/ein und sorgt dafür, dass sie auch auf eine HEAD-Anfrage (eine Anfrage, die nur die Kopfzeilen einer Seite abruft, nicht den Inhalt) den richtigen Inhaltstyp meldet. Dazu unten mehr, das war die gemeinste Falle des ganzen Projekts. - Er baut den Feed aus allen veröffentlichten Beiträgen, die eine Tondatei haben. Entwürfe tauchen nicht auf.
- Unter
/feed/podcast/?transkript=<Beitragsnummer>liefert er das Transkript einer Folge als reinen Text. Apple und Spotify werten diese Angabe aus.
Bernd: „200 OK heißt doch, es hat geklappt. Dieses show_in_rest ist bestimmt nur Zierde.“
Tanja: „200 OK heißt nur, dass WordPress dir höflich zugehört hat. Ob es die Felder auch gespeichert hat, sagt das nicht.“
Das ist einer der nervigsten Fehler überhaupt, weil er keine Fehlermeldung erzeugt. Ohne show_in_rest bekommst du ein freundliches „200 OK“, und die Felder bleiben trotzdem leer. Wie ein Pförtner, der jedes Paket annimmt und es dann still in den Müll wirft. Also: Die Angabe ist Pflicht, Bernd irrt sich hier.
Jetzt legst du das Snippet an. Achte auf den letzten Klick, dort lauert die erste Versuchung.
Klickfolge: Snippets → Neu hinzufügen → Titel Podcast-Feed → Typ PHP → den Code unten in das Codefeld einfügen → unter dem Codefeld Überall ausführen wählen → Änderungen speichern (noch nicht „Speichern und aktivieren“).
Warum erst speichern und noch nicht aktivieren? Weil du vorher noch deine eigenen Angaben eintragen musst. Vor dem Speichern passt du einen einzigen Block an: „DEINE ANGABEN“ im Abschnitt „3. Der Feed selbst“. Alles, was du dort ersetzen musst, beginnt mit DEIN_ oder DEINE_:
| Platzhalter | was hinein gehört |
|---|---|
DEIN_SENDUNGSNAME | der Name deiner Sendung |
https://DEINE_DOMAIN/ | die Adresse deiner Website, mit Schrägstrich am Ende |
DEIN_WEBSITENAME | wie deine Website im Linktext heißen soll, etwa meine-seite.de |
DEIN_NAME_ODER_DEINE_MARKE | der Name, den die Apps als Urheber zeigen |
DEIN_NAME | der Inhaber der Sendung |
DEINE_OEFFENTLICHE_PODCAST_EMAIL | eine Adresse, die Post empfangen kann. Apple und Spotify schicken dorthin den Bestätigungscode. Sie steht in jedem Feedabruf öffentlich lesbar, deshalb am besten eine eigene Adresse nur für den Podcast. |
…/DEIN_SENDUNGSBILD.jpg | die Datei-URL deines Sendungsbilds aus Schritt 4.2 |
DEIN_UNTERTITEL | ein Untertitel in wenigen Wörtern |
DEINE_BESCHREIBUNG | zwei, drei Sätze über deine Sendung |
DEINE_PODCAST_GUID | die Kennung deines Feeds (Rechnung gleich unten) |
DEIN_KUERZEL-folge- | ein kurzer Vorsatz für die Kennung jeder Folge, etwa meinpodcast-folge-. Nie mehr ändern, sobald die erste Folge veröffentlicht ist. |
$sprache, $kategorie und $unterkategorie sind schon sinnvoll vorbelegt. Die Kategorie muss aus Apples fester Liste stammen. Eigene Fantasiekategorien kennt Apple nicht.
Ulf: „Und wenn ich einen Platzhalter übersehe? Dann steht im Feed DEIN_NAME, und alle lachen.“
Tanja: „Dafür hat der Code eine Notbremse eingebaut.“
Die eingebaute Sicherung: Solange im Block noch ein Platzhalter steht, liefert /feed/podcast/ absichtlich den Fehler 503 mit dem Satz „Podcast-Feed noch nicht eingerichtet“. So kann kein halb ausgefüllter Feed mit fremden Angaben bei Apple landen. Ein absichtlicher Fehler ist hier also dein Freund: lieber ein ehrliches „Noch nicht fertig“ als ein Feed mit falschen Angaben.
Das Plugin Polylang brauchst du nicht. Ist es installiert, nimmt der Feed nur die Beiträge in der Sprache aus $sprache, sonst wird die Zeile übersprungen.
Bleibt eine Angabe, die du nicht einfach hinschreiben kannst: die Podcast-Kennung (podcast:guid). Denk an die Spielerpassnummer beim Verband. Wechselt ein Spieler den Verein, bleibt er über diese Nummer derselbe Spieler. Genauso funktioniert die Kennung. Sie ist keine erfundene Zahl, sondern wird aus der Feed-Adresse errechnet. Zieht der Feed eines Tages um, erkennen Verzeichnisse daran, dass es dieselbe Sendung ist. Berechnen mit (Adresse ohne https:// und ohne Schrägstrich am Ende):
python3 -c "import uuid; print(uuid.uuid5(uuid.UUID('ead4c236-bf58-58c6-a2c6-a6b28d128cb6'), 'www.deine-domain.de/feed/podcast'))"
Erwartetes Ergebnis: eine Kennung im Format xxxxxxxx-xxxx-5xxx-xxxx-xxxxxxxxxxxx. Für www.foundic.org/feed/podcast ergibt die Rechnung 864185ed-72c3-5b21-8e82-c33d961f525f, die Kennung des KI Wochenrückblicks. So kannst du die Rechnung selbst prüfen: Setz testweise diese Adresse ein, und es muss genau diese Kennung herauskommen. Deine eigene Kennung trägst du im Snippet bei DEINE_PODCAST_GUID ein.
Der vollständige Code:
<?php
/**
* Podcast-Feed fuer WordPress, ohne Podcast-Plugin
* ------------------------------------------------
* Gehoert in Code Snippets, Ausfuehrung: "Ueberall".
*
* ANPASSEN: nur den Block "DEINE ANGABEN" in Abschnitt 3. Solange dort noch
* ein Platzhalter (DEIN_..., DEINE_...) steht, liefert der Feed absichtlich
* einen Fehler statt einer Sendung mit falschen Angaben.
*
* NACH DEM AKTIVIEREN EINMAL: Einstellungen -> Permalinks -> "Aenderungen
* speichern" anklicken (ohne etwas zu aendern). Ohne das kennt WordPress die
* neue Adresse /feed/podcast/ nicht und liefert 404. Das ist die haeufigste
* Ursache, wenn ein selbstgebauter Feed "nicht geht".
*
* WICHTIG, WP Fastest Cache: Die Adresse /feed/podcast/ muss in den Ausnahmen
* stehen. Ein zwischengespeicherter Podcast-Feed liefert wochenlang dieselbe
* Folge an ALLE Apps gleichzeitig, und niemand merkt es, weil im Browser alles
* richtig aussieht.
*
* Hinweis zur Bauweise: Dieser Code verlaesst den PHP-Modus an keiner Stelle.
* Die uebliche Vorlagen-Schreibweise mit "?>" und rohem XML dazwischen ist in
* einer Datei voellig richtig, in einem Snippet aber unnoetig heikel - Code
* Snippets fuehrt den Text mit eval() aus. Alles wird deshalb mit echo
* ausgegeben.
*/
/* -------------------------------------------------------------------------
* 1. Vier Zusatzfelder am Beitrag
*
* Der Feed koennte die Tondatei auch aus dem Beitragstext herausfischen. Das
* waere aber eine zweite Stelle, an der dieselbe Tatsache steht - und die
* laufen immer auseinander. Deshalb schreibt A21 die Werte ausdruecklich
* an den Beitrag, und der Feed liest genau sie.
*
* show_in_rest ist zwingend: Ohne das nimmt WordPress die Felder ueber die
* Schnittstelle NICHT an - und zwar wortlos, mit Statuscode 200. Am 19.09.2026
* nachgesehen: ohne dieses Snippet war keiner der Schluessel registriert.
*
* ---------------------------------------------------------------------- */
add_action( 'init', function () {
$felder = array(
'podcast_url' => 'string', // vollstaendige Adresse der MP3
'podcast_bytes' => 'integer', // Dateigroesse, Pflicht im <enclosure>
'podcast_dauer' => 'string', // "15:56" - WordPress kennt sie nach dem Upload selbst
// Die gesprochene Fassung im Klartext, je Beitrag eine Zeile mit
// "Sprecherin: " bzw. "Sprecher: " davor. Ohne dieses Feld laege sie nur
// in den n8n-Ausfuehrungsdaten - und die raeumt n8n nach 336 Stunden
// weg. Ein Zwischenspeicher, der aufraeumt, ist kein Archiv.
'podcast_skript' => 'string',
);
foreach ( $felder as $name => $typ ) {
register_post_meta( 'post', $name, array(
'type' => $typ,
'single' => true,
'show_in_rest' => true,
'auth_callback' => function () { return current_user_can( 'edit_posts' ); },
) );
}
} );
/* -------------------------------------------------------------------------
* 1b. Eine eigene Bildgroesse fuer Folgenbilder
*
* Apple verlangt fuer Folgenbilder dieselben Masse wie fuer das Sendungsbild:
* Quadrat zwischen 1400 und 3000 Pixeln, RGB, JPEG oder PNG. Zusaetzlich soll
* die Datei UNTER 512 kB bleiben (eigene Vorgabe dieses Aufbaus, keine
* Apple-Pflicht). Genau daran scheitern die Beitragsbilder in ihrer
* Originalfassung: Sie sind quadratisch, aber nur 1024 Pixel gross und liegen
* je nach Motiv zwischen 360 und 532 kB.
*
* Deshalb diese Zwischengroesse. WordPress rechnet sie beim Hochladen aus dem
* Original herunter - und ein Bild, das es herunterrechnet, wird dabei auch
* wieder frisch als JPEG gespeichert und damit deutlich kleiner.
*
* WICHTIG, und der Grund, Beitragsbilder mindestens 1408 Pixel gross zu erzeugen:
* WordPress rechnet NIE hoch. Aus einem 1024er Original entsteht kein 1400er
* Bild, sondern gar keines - die Groesse wird stillschweigend uebersprungen.
* Der Feed faellt dann auf das Original zurueck (siehe folgenbild() unten).
* ---------------------------------------------------------------------- */
add_action( 'after_setup_theme', function () {
add_image_size( 'podcast-folge', 1400, 1400, true );
} );
/**
* Das Bild einer Folge: bevorzugt die Podcast-Groesse, sonst das Original.
*
* Gibt eine leere Zeichenkette zurueck, wenn der Beitrag kein Bild hat. Dann
* bleibt das <itunes:image> im <item> weg, und die Apps zeigen das Sendungs-
* bild - besser ein Bild weniger als eine Adresse, die ins Leere zeigt.
*/
function podcast_folgenbild( $beitrag_id ) {
$bild_id = get_post_thumbnail_id( $beitrag_id );
if ( ! $bild_id ) {
return '';
}
$gross = wp_get_attachment_image_src( $bild_id, 'podcast-folge' );
// wp_get_attachment_image_src liefert auch dann etwas, wenn die Groesse
// gar nicht erzeugt wurde - dann aber das Original. Deshalb wird hier die
// gemeldete Breite geprueft und nicht bloss, ob ueberhaupt etwas kam.
if ( $gross && isset( $gross[1] ) && $gross[1] >= 1400 ) {
return $gross[0];
}
$voll = wp_get_attachment_image_src( $bild_id, 'full' );
return $voll ? $voll[0] : '';
}
/* -------------------------------------------------------------------------
* 2. Die Adresse /feed/podcast/
* ---------------------------------------------------------------------- */
add_action( 'init', function () {
add_feed( 'podcast', 'podcast_feed_ausgeben' );
} );
/* ---------------------------------------------------------------------
* Inhaltstyp auch bei HEAD-Anfragen richtig stellen
*
* WordPress beendet HEAD-Anfragen in template-loader.php, BEVOR die
* Feed-Funktion ueberhaupt laeuft - das header() weiter unten kommt dann
* nie zum Zug. Gesendet wird stattdessen, was WP::send_headers() gesetzt
* hat, und feed_content_type() liefert fuer einen selbst angemeldeten
* Feed-Namen den Notnagel "application/octet-stream".
*
* Gemessen am 26.09.2026: GET meldete application/rss+xml, HEAD dagegen
* application/octet-stream. Ein Abrufer, der erst HEAD fragt, haelt den
* Feed damit fuer eine Binaerdatei.
* ------------------------------------------------------------------ */
add_filter( 'feed_content_type', function ( $typ, $feed ) {
return ( 'podcast' === $feed ) ? 'application/rss+xml' : $typ;
}, 10, 2 );
/* -------------------------------------------------------------------------
* 3. Der Feed selbst
* ---------------------------------------------------------------------- */
function podcast_feed_ausgeben() {
// Dieselbe Adresse liefert mit ?transkript=<Nummer> das Transkript als
// reinen Text. Bewusst kein eigener Feed: Ein zweiter add_feed()-Aufruf
// verlangte erneut ein Speichern der Permalinks, und diese Adresse faellt
// ausserdem schon unter die Cache-Ausnahme "feed/podcast".
$wunsch = isset( $_GET['transkript'] ) ? absint( $_GET['transkript'] ) : 0;
if ( $wunsch ) {
$beitrag = get_post( $wunsch );
$skript = $beitrag ? get_post_meta( $wunsch, 'podcast_skript', true ) : '';
if ( ! $beitrag || 'publish' !== $beitrag->post_status || ! $skript ) {
status_header( 404 );
header( 'Content-Type: text/plain; charset=UTF-8', true );
echo "Kein Transkript zu dieser Folge.\n";
return;
}
header( 'Content-Type: text/plain; charset=UTF-8', true );
echo $beitrag->post_title . "\n\n" . $skript . "\n";
return;
}
/* === DEINE ANGABEN - nur diesen Block anpassen ===================== */
$sendung = 'DEIN_SENDUNGSNAME'; // z. B. KI Wochenrückblick
$website = 'https://DEINE_DOMAIN/'; // Startseite, mit / am Ende
$website_name = 'DEIN_WEBSITENAME'; // z. B. meine-seite.de
$urheber = 'DEIN_NAME_ODER_DEINE_MARKE'; // erscheint in den Apps
$inhaber = 'DEIN_NAME'; // nur fuer Apple/Spotify
// Oeffentlich lesbar! Eigene Adresse nur fuer den Podcast, die Post empfangen kann:
$email = 'DEINE_OEFFENTLICHE_PODCAST_EMAIL';
$titelbild = 'https://DEINE_DOMAIN/wp-content/uploads/DEIN_SENDUNGSBILD.jpg';
$kurz = 'DEIN_UNTERTITEL'; // wenige Woerter
$beschreibung = 'DEINE_BESCHREIBUNG'; // zwei, drei Saetze
$podcast_guid = 'DEINE_PODCAST_GUID'; // Rechnung siehe Artikel
$guid_vorsatz = 'DEIN_KUERZEL-folge-'; // nie mehr aendern!
$sprache = 'de';
$kategorie = 'News'; // aus Apples Liste
$unterkategorie = 'Tech News';
/* ==================================================================== */
// Sicherung: Ein Platzhalter darf nie in einem echten Feed landen.
foreach ( array( $sendung, $website, $website_name, $urheber, $inhaber, $email, $titelbild, $kurz, $beschreibung, $podcast_guid, $guid_vorsatz ) as $wert ) {
if ( false !== strpos( $wert, 'DEIN_' ) || false !== strpos( $wert, 'DEINE_' ) ) {
status_header( 503 );
header( 'Content-Type: text/plain; charset=UTF-8', true );
echo "Podcast-Feed noch nicht eingerichtet: Im Snippet stehen noch Platzhalter.\n";
return;
}
}
$feedadresse = home_url( '/feed/podcast/' );
/* ---------------------------------------------------------------------
* Der anklickbare Weg zurueck und der KI-Hinweis
*
* Beides steht bewusst im <description>-CDATA und nicht nur als <link>:
* Podcast-Apps zeigen die Beschreibung an und machen Verweise darin
* anklickbar; der <link> allein landet je nach App irgendwo im
* Kleingedruckten oder gar nicht.
*
* Erlaubt ist in Apple Podcasts nur eine Handvoll HTML - <p>, <a>, <ul>,
* <ol>, <li>. Mehr wird hier auch nicht gebraucht.
*
* Zum KI-Hinweis: Die Stimmen sind synthetisch, und Artikel 50 der
* KI-Verordnung verlangt seit dem 02.08.2026 Transparenz bei kuenstlich erzeugten Inhalten. Ob die
* Ausnahme fuer redaktionell geprueftes Material greift, ist Auslegung -
* eine Zeile kostet nichts, ein Streit darueber schon.
* ------------------------------------------------------------------ */
$heimat = '<p><a href="' . esc_url( $website ) . '">Alle Folgen und der ganze Rückblick zum Nachlesen: ' . esc_html( $website_name ) . '</a></p>';
$ki_hinweis = '<p>Dieser Podcast wird mit künstlicher Intelligenz erstellt: Die Texte entstehen automatisiert und werden redaktionell freigegeben, die beiden Stimmen sind synthetisch.</p>';
$beschreibung_html = '<p>' . $beschreibung . '</p>' . $heimat . $ki_hinweis;
// Nur veroeffentlichte Beitraege, die wirklich eine Tondatei haben.
// 'draft' taucht hier bewusst nicht auf: Jeder Beitrag wird von Hand
// freigegeben, und bis dahin hat auch niemand die Folge zu hoeren.
$abfrage = array(
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => 200,
'orderby' => 'date',
'order' => 'DESC',
'meta_query' => array( array(
'key' => 'podcast_url',
'compare' => 'EXISTS',
) ),
);
// Nur mit dem Plugin Polylang: nur die Beitraege in der Sprache der
// Sendung. Ohne Polylang wird diese Zeile einfach uebersprungen.
if ( function_exists( 'pll_languages_list' ) ) {
$abfrage['lang'] = $sprache;
}
$folgen = new WP_Query( $abfrage );
header( 'Content-Type: application/rss+xml; charset=UTF-8', true );
echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
echo '<rss version="2.0"' . "\n";
echo ' xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"' . "\n";
echo ' xmlns:podcast="https://podcastindex.org/namespace/1.0"' . "\n";
echo ' xmlns:atom="http://www.w3.org/2005/Atom">' . "\n";
echo "<channel>\n";
echo "\t<title>" . esc_html( $sendung ) . "</title>\n";
echo "\t<link>" . esc_url( $website ) . "</link>\n";
echo "\t<language>" . esc_html( $sprache ) . "</language>\n";
echo "\t<description><![CDATA[" . $beschreibung_html . "]]></description>\n";
echo "\t<itunes:subtitle>" . esc_html( $kurz ) . "</itunes:subtitle>\n";
echo "\t<itunes:summary><![CDATA[" . $beschreibung_html . "]]></itunes:summary>\n";
echo "\t<itunes:author>" . esc_html( $urheber ) . "</itunes:author>\n";
echo "\t<itunes:type>episodic</itunes:type>\n";
echo "\t<itunes:explicit>false</itunes:explicit>\n";
echo "\t<itunes:owner>\n";
echo "\t\t<itunes:name>" . esc_html( $inhaber ) . "</itunes:name>\n";
echo "\t\t<itunes:email>" . esc_html( $email ) . "</itunes:email>\n";
echo "\t</itunes:owner>\n";
echo "\t<itunes:image href=\"" . esc_url( $titelbild ) . "\"/>\n";
echo "\t<itunes:category text=\"" . esc_attr( $kategorie ) . "\">\n";
echo "\t\t<itunes:category text=\"" . esc_attr( $unterkategorie ) . "\"/>\n";
echo "\t</itunes:category>\n";
echo "\t<atom:link href=\"" . esc_url( $feedadresse ) . "\" rel=\"self\" type=\"application/rss+xml\"/>\n";
// Beides ist freiwillig, aber nicht beliebig:
// Die Kennung ist ein UUIDv5 ueber die Zeichenkette "www.deine-domain.de/feed/podcast"
// (Adresse ohne Protokoll und ohne Schraegstrich am Ende) mit dem Namensraum
// ead4c236-bf58-58c6-a2c6-a6b28d128cb6. Sie ist also aus der Adresse errechnet
// und nicht ausgedacht - dieselbe Adresse ergibt immer dieselbe Kennung.
// Wozu: Zieht der Feed eines Tages um, erkennen Verzeichnisse an dieser Kennung,
// dass es dieselbe Sendung ist, statt eine zweite anzulegen.
echo "\t<copyright>" . esc_html( $website_name ) . "</copyright>\n";
echo "\t<podcast:guid>" . esc_html( $podcast_guid ) . "</podcast:guid>\n";
while ( $folgen->have_posts() ) {
$folgen->the_post();
$id = get_the_ID();
$url = get_post_meta( $id, 'podcast_url', true );
$bytes = (int) get_post_meta( $id, 'podcast_bytes', true );
$dauer = get_post_meta( $id, 'podcast_dauer', true );
// Ohne Adresse oder ohne Laenge kein Eintrag. Apple lehnt einen
// <enclosure> ohne length ab, und eine Folge, die in der Liste steht
// und sich nicht abspielen laesst, ist schlimmer als keine Folge.
if ( ! $url || $bytes < 1 ) {
continue;
}
$text = wp_strip_all_tags( get_the_excerpt() );
// Jede Folge bekommt ihren eigenen anklickbaren Weg zurueck - auf den
// Rueckblick, zu dem sie gehoert, nicht nur auf die Startseite.
$text_html = '<p>' . esc_html( $text ) . '</p>'
. '<p><a href="' . esc_url( get_permalink() ) . '">Diesen Rückblick nachlesen auf ' . esc_html( $website_name ) . '</a></p>'
. $ki_hinweis;
echo "\t<item>\n";
echo "\t\t<title>" . esc_html( get_the_title() ) . "</title>\n";
echo "\t\t<link>" . esc_url( get_permalink() ) . "</link>\n";
echo "\t\t<pubDate>" . esc_html( get_post_time( 'r', true ) ) . "</pubDate>\n";
// Die guid ist bewusst NICHT die Adresse: Podcast-Apps erkennen Folgen
// daran. Aendert sich eine Adresse, erschiene die Folge sonst ueberall
// ein zweites Mal. Die Beitragsnummer aendert sich nie.
echo "\t\t<guid isPermaLink=\"false\">" . esc_html( $guid_vorsatz ) . (int) $id . "</guid>\n";
echo "\t\t<description><![CDATA[" . $text_html . "]]></description>\n";
echo "\t\t<itunes:summary><![CDATA[" . $text_html . "]]></itunes:summary>\n";
echo "\t\t<enclosure url=\"" . esc_url( $url ) . "\" length=\"" . $bytes . "\" type=\"audio/mpeg\"/>\n";
if ( $dauer ) {
echo "\t\t<itunes:duration>" . esc_html( $dauer ) . "</itunes:duration>\n";
}
// Das Bild dieser Folge - dasselbe, das ueber dem Rueckblick steht.
// Apple Podcasts zeigt Folgenbilder in der App kaum noch an, andere
// Apps sehr wohl; und das Sendungsbild bleibt die Rueckfallebene.
$folgenbild = podcast_folgenbild( $id );
if ( $folgenbild ) {
echo "\t\t<itunes:image href=\"" . esc_url( $folgenbild ) . "\"/>\n";
}
// Transkript, falls vorhanden. Apple und Spotify werten das aus; die
// Adresse liefert denselben Feed-Endpunkt als reinen Text zurueck.
if ( get_post_meta( $id, 'podcast_skript', true ) ) {
$tr = add_query_arg( 'transkript', $id, home_url( '/feed/podcast/' ) );
echo "\t\t<podcast:transcript url=\"" . esc_url( $tr ) . "\" type=\"text/plain\" language=\"" . esc_attr( $sprache ) . "\"/>\n";
}
echo "\t\t<itunes:episodeType>full</itunes:episodeType>\n";
echo "\t\t<itunes:explicit>false</itunes:explicit>\n";
echo "\t</item>\n";
}
wp_reset_postdata();
echo "</channel>\n";
echo "</rss>\n";
}
Bernd: „Das hab ich doch alles ausgefüllt. Ich seh doch, was da steht.“
Tanja: „Du siehst, was du sehen willst. Die Maschine sieht alles.“
Die Suchprüfung vor dem Speichern: Wer den Code in einer Datei vorbereitet (etwa als podcast-feed.php), prüft im Terminal, ob wirklich nichts vergessen wurde:
grep -n -E "DEIN_|DEINE_|FOUNDIC|foundic|michael@|864185ed" podcast-feed.php
Erwartetes Ergebnis: gar keine Ausgabe. Jede Zeile, die hier erscheint, enthält noch einen Platzhalter oder eine Angabe aus diesem Artikel. Ein übersehener Rest fällt beim Durchlesen leicht durch, bei der Suche nicht. Dieselbe Prüfung lohnt sich für compose.yaml und für den n8n-Knoten Einstellungen aus Phase 6.
Eine Bemerkung zur Bauweise, falls du schon einmal PHP gesehen hast und dich wunderst: Der Code verlässt den PHP-Modus nie und gibt das XML ausschließlich mit echo aus. In einer normalen Vorlagendatei wäre die übliche Schreibweise mit ?> und rohem XML dazwischen völlig in Ordnung. Code Snippets führt den Text aber intern mit eval() aus, und dort ist jeder Moduswechsel ein unnötiges Risiko. Die etwas umständlichere Schreibweise ist also kein Versehen, sondern Absicht.

Das Snippet im Editor von Code Snippets, vor dem Speichern. Das Bild zeigt die Fassung auf FOUNDIC.org mit deren Angaben; in deiner Fassung stehen an diesen Stellen deine Werte aus dem Block „DEINE ANGABEN“.
Jetzt, wo alle Angaben stimmen, darf das Snippet scharf geschaltet werden.
Jetzt aktivieren: Nach dem Speichern in der Liste Snippets → Alle Snippets den Schalter beim Snippet „Podcast-Feed“ auf aktiv stellen.
Erwartetes Ergebnis: Der Schalter steht auf aktiv. Kommt stattdessen eine Fehlermeldung mit Zeilennummer, ist beim Anpassen ein Anführungszeichen verloren gegangen. Das ist der häufigste Anfängerfehler an dieser Stelle, und er ist harmlos: Code Snippets aktiviert fehlerhaften Code nicht, die Seite bleibt also erreichbar. Geh zur genannten Zeile, such das fehlende Anführungszeichen und speichere erneut.

Nach dem Speichern meldet Code Snippets „Snippet updated“.
Schritt 4.4: Die Permalinks einmal speichern
Bernd: „Permalinks? Da hab ich nichts geändert. Warum sollte ich da was speichern?“
Tanja: „Genau deshalb. Du änderst nichts, du lässt WordPress nur seine Wegweiser neu aufstellen.“
Das ist der Schritt, den jeder vergisst, und der Grund für die meisten „Mein Feed geht nicht“-Fragen in Foren. WordPress kennt neue Adressen wie /feed/podcast/ erst, wenn es seine Adressregeln neu schreibt. Stell dir das Schilderverzeichnis im Bürogebäude vor: Ein neues Büro ist bezogen, aber solange niemand das Schild im Foyer aktualisiert, findet es keiner. Bernds Einwand ist also falsch. Gerade weil du nichts änderst, braucht es den Klick, denn erst das Speichern schreibt die Regeln neu.
Klickfolge: Einstellungen → Permalinks → ohne etwas zu ändern ganz unten Änderungen speichern.

Die Permalink-Seite. Ein Klick auf „Änderungen speichern“ genügt, geändert wird nichts.
Schritt 4.5: Den Feed vom Cache ausnehmen
Ulf: „Was ist eigentlich ein Cache?“
Tanja: „Ein Zwischenspeicher. Wie eine Aufzeichnung vom letzten Spiel, die jemand immer wieder abspielt, statt live zu schalten.“
Bernd: „Brauchen wir nicht anfassen. Bei mir im Browser sieht der Feed richtig aus.“
Und genau das ist die Falle. Wenn deine Website ein Cache-Plugin verwendet (es speichert fertige Seiten zwischen, damit sie schneller ausgeliefert werden), musst du den Feed ausnehmen. Ein zwischengespeicherter Podcast-Feed liefert wochenlang dieselbe Folge an alle Apps gleichzeitig, und niemand merkt es, weil im Browser des Betreibers alles richtig aussieht. Bernds Browser ist also kein Beweis, im Gegenteil: Er ist genau der Ort, an dem der Fehler unsichtbar bleibt.
Klickfolge für WP Fastest Cache: WP Fastest Cache → Reiter Ausschließen → Bereich Seiten ausschließen → neue Regel anlegen: „REQUEST_URI“ beginnt mit feed/podcast (ohne Schrägstrich am Anfang) → speichern.
Andere Cache-Plugins haben eine vergleichbare Ausnahmeliste. Such dort nach einem Bereich, in dem du Seiten oder Adressen ausschließen kannst.

Unter „Seiten ausschließen“ steht die Regel „Start With: feed/podcast“.
Schritt 4.6: Den leeren Feed prüfen
Zeit für den ersten Anpfiff. Noch hat der Feed keine Folge, aber er muss schon gültig antworten. Wie beim Aufwärmen vor dem Spiel: Die Mannschaft steht noch ohne Ball auf dem Platz, aber jeder muss schon an seiner Position sein. Im Terminal:
curl -s -o /dev/null -w "%{http_code} %{content_type}\n" https://www.deine-domain.de/feed/podcast/
curl -sI https://www.deine-domain.de/feed/podcast/ | grep -i "^content-type"
Erwartetes Ergebnis:
200 application/rss+xml; charset=UTF-8
content-type: application/rss+xml; charset=UTF-8
Die erste Zeile prüft den normalen Abruf, die zweite die HEAD-Anfrage. Beide müssen application/rss+xml melden.
Bernd: „Zwei Abfragen für dieselbe Adresse? Eine reicht. Wenn die stimmt, stimmt alles.“
Tanja: „Genau so haben wir zwei Nachmittage verloren.“
Warum beide? Beim ersten Einreichen bei Apple brach Podcasts Connect zweimal mit der nichtssagenden Meldung „Es ist ein Fehler aufgetreten“ ab. Die Ursache: Der normale Abruf lieferte korrekt application/rss+xml, die HEAD-Anfrage dagegen application/octet-stream, also „irgendeine Binärdatei“. WordPress beantwortet HEAD-Anfragen, bevor die Feed-Funktion überhaupt läuft, und kennt für einen selbst angemeldeten Feed keinen passenden Typ. Wer zuerst HEAD fragt, wie Apple, hält den Feed deshalb für eine Binärdatei.
Ulf: „Also wie ein Scout, der nur auf die Rückennummer schaut und den Spieler gar nicht spielen sieht?“
Tanja: „Ziemlich genau. Und wenn auf dem Trikot das Falsche steht, fährt der Scout wieder nach Hause.“
Der kleine Filter feed_content_type im Snippet behebt das. Man sieht den Fehler aber nur, wenn man beide Anfragearten misst. Bernd liegt also falsch: Eine einzelne Abfrage hätte hier alles grün gezeigt, während Apple trotzdem abgelehnt hätte.
Und für den Cache: Rufe den Feed einmal mit einem Zufallsanhang ab und vergleiche den Inhaltstyp. Der Zufallsanhang macht die Adresse für den Cache neu, er kann also keine gespeicherte Fassung ausliefern:
curl -s -o /dev/null -w "%{content_type}\n" "https://www.deine-domain.de/feed/podcast/?x=$RANDOM"
Liefert der Abruf ohne Anhang text/xml, der mit Anhang aber application/rss+xml, kommt die erste Antwort aus dem Cache. Dann greift die Ausnahme aus Schritt 4.5 noch nicht. Prüfe in dem Fall die Regel in deinem Cache-Plugin noch einmal.
Phase 5: Aus einem Artikel wird ein Gespräch
Bernd: „Wozu der Aufwand? Wir lassen den Artikel einfach vorlesen. Text rein, Ton raus, fertig.“
Ulf: „Klingt doch gut. Weniger Arbeit.“
Tanja: „Probier mal, zwanzig Minuten lang einer Stimme zuzuhören, die einen Zeitungsartikel vorliest.“
Bernds Abkürzung klingt verlockend, führt aber ins Leere. Man könnte den fertigen Rückblick einfach vorlesen lassen. Das Ergebnis wäre ein Hörbuch, kein Podcast, und nach drei Minuten würde man abschalten. Ein Gespräch zweier Stimmen trägt zwanzig Minuten, ein vorgelesener Text nicht. Denk an eine Fußballübertragung: Ein Kommentator allein, der nur Spielernamen aufzählt, ist ermüdend. Zwei, die sich gegenseitig ins Wort fallen, nachfragen und einordnen, halten dich wach. Also bekommt ein Sprachmodell den Auftrag, den Artikel in ein Gespräch zu verwandeln.
Die Geschichte vom Gespräch, das keines war
Jetzt wird es ehrlich, denn der erste Versuch ging gründlich daneben. Und gerade daraus lernst du am meisten.
Die erste Fassung dieses Auftrags war vernünftig formuliert: Der Artikel bleibt inhaltlich unverändert, jeder Beitrag 40 bis 120 Wörter, Füllbeiträge wie „Spannend!“ verboten, rund 3.500 Wörter, nicht kürzen. Das Modell hat genau das geliefert. Und es war unbrauchbar.
Nachgemessen: 90 Prozent des Gesprächs bestanden aus Wortfolgen, die wörtlich im Artikel standen. Nur 3 von 55 Beiträgen endeten mit einem Fragezeichen, nur 4 bezogen sich überhaupt auf das, was die andere Stimme gerade gesagt hatte. Es waren zwei Menschen, die abwechselnd Absätze aus derselben Zeitung vorlasen.
Bernd: „Dann ist das Modell halt dumm. Nehmt ein besseres.“
Tanja: „Das Modell hat exakt gemacht, was wir bestellt haben. Der Fehler stand in unserem Auftrag.“
Das Modell hatte nichts falsch gemacht. Der Auftrag hatte genau das bestellt: „inhaltlich unverändert“ heißt für ein Sprachmodell „abschreiben“, „40 bis 120 Wörter“ verbietet jeden kurzen Einwurf, und „nicht kürzen“ verbietet das Verdichten, das ein Gespräch ausmacht. Ein Prompt ist ein Vertrag, und Verträge werden wörtlich genommen. Wer im Spielervertrag „spielt ausschließlich als Innenverteidiger“ festschreibt, darf sich nicht wundern, wenn der Mann nie ein Tor schießt.
Die neue Fassung dreht jede dieser Regeln um. Das Ergebnis im nächsten Lauf: 92 Beiträge, Median 26 Wörter, 39 Beiträge unter 25 Wörtern, 13 echte Rückfragen, und der wörtliche Anteil sank von 90 auf 49 Prozent. Kein perfektes Gespräch, aber eines, dem man zuhört.
Der Auftrag an das Modell
Ulf: „Und was steht jetzt in dem neuen Vertrag?“
Tanja: „Lies selbst. Er ist absichtlich in normaler Sprache geschrieben.“
In lesbarer Form:
Du machst aus dem Wochenrückblick, den du gleich bekommst, ein Gespräch für einen Podcast mit zwei Stimmen. Die Sprecherin heißt im Ergebnis "w", der Sprecher "m". Beide bleiben namenlos.
Du schreibst den Artikel nicht um - du ERZÄHLST ihn. Kein Satz wörtlich, nie acht aufeinanderfolgende Wörter aus dem Artikel.
LÄNGE: kürzer als der Artikel, 2.000 bis 2.500 Wörter. Ein Gespräch verdichtet.
BEITRÄGE:
- 5 bis 90 Wörter je Beitrag. Mindestens ein Drittel der Beiträge unter 25 Wörtern. Zwei lange Beiträge nie hintereinander. Die kurzen Beiträge über das ganze Gespräch verteilen, nicht nur an den Anfang.
- Jeder Beitrag hat eine eigene Aussage. Verboten sind Füllbeiträge wie "Spannend!", "Absolut." oder "Genau.".
- In jedem Themenblock mindestens zwei Rückfragen, Einwände oder Präzisierungen - ausdrücklich auch im letzten Block.
- Widerspruch nur dort, wo der Artikel wirklich zwei Sichtweisen hergibt.
- Sprich die Hörer zwei- bis viermal direkt mit "du" an.
ANFANG: Der erste Beitrag gehört "w" und beginnt wörtlich mit: "Dieser Rückblick wurde mit künstlicher Intelligenz erstellt und mit synthetischen Stimmen gesprochen."
ENDE: Schreib keinen Abschied und keinen Hinweis auf die Website - das hängt der Workflow selbst an. Ein Ausblick auf die nächste Woche ist erlaubt.
SCHREIBWEISE - das Ergebnis wird vorgelesen:
- Echte Umlaute und ß, nie ae, oe, ue oder ss als Ersatz.
- Keine Tagesdaten. Nur Monat und Jahr oder gar keine Zeitangabe.
- Keine Aufzählungszeichen, keine Überschriften, keine Links, kein HTML, keine Emojis.
AUSGABE: nur JSON, nichts davor und nichts danach:
{"dialog":[{"s":"w","t":"..."},{"s":"m","t":"..."}]}
Ein paar Regeln verdienen eine Erklärung, denn keine davon steht zufällig da:
- „nie acht aufeinanderfolgende Wörter“ ist messbar. Man kann nach jedem Lauf nachzählen, ob sie eingehalten wurde. Eine Regel wie „formuliere frei“ kann man nicht nachzählen. Wie beim Abseits: Eine klare Linie lässt sich prüfen, ein Bauchgefühl nicht.
- Der erste Satz ist ein Transparenzhinweis. Die Stimmen sind synthetisch, und die europäische KI-Verordnung verlangt in Artikel 50 Transparenz bei künstlich erzeugten Inhalten. Ob die Ausnahme für redaktionell geprüftes Material greift, ist Auslegungssache. Eine Zeile kostet nichts, ein Streit darüber schon.
- Echte Umlaute sind keine Stilfrage. Ein Modell, das „kuenstlicher“ schreibt, bekommt eine Stimme, die „ku-enstlicher“ sagt. Die Sprachausgabe liest, was dasteht, Buchstabe für Buchstabe.
- Keine Tagesdaten: Maschinelle Transkripte der Quell-Podcasts verkürzen Jahreszahlen gern („August 26″ statt „August 2026″), und ein Modell macht daraus beim Nacherzählen zuverlässig „am 26. August“. Monat und Jahr genügen.
- Kein Abschied: Die Abmoderation hängt der Workflow in Phase 6 fest an, damit sie jede Woche gleich klingt.
- Namenlose Stimmen: Die beiden heißen in der Ausgabe nur
wundm. Synthetischen Stimmen erfundene Menschennamen zu geben, würde eine Person vortäuschen, die es nicht gibt.
Phase 6: Der Podcast-Strang in n8n
Montagmorgen, Besprechungsraum. Auf dem Beamer ein leeres n8n-Fenster.
Ulf: „Und jetzt? Klicken wir einfach so lange Kästchen hin, bis ein Podcast rauskommt?“
Tanja: „Fast. Wir bauen eine Kette aus Knoten. Jeder Knoten macht genau einen Handgriff und reicht das Ergebnis weiter. Wie ein Spielzug über zwölf Stationen.“
Bernd: „Zwölf? Einer reicht doch. Artikel rein, Podcast raus. Der Rest ist Beschäftigungstherapie für die IT.“
Tanja: „Dann merkst du einen Fehler erst, wenn die Rechnung von Google kommt. Die Prüfknoten sind genau dafür da.“
Tanja hat recht, und das ist der Kern dieser Phase. Jetzt entsteht die Kette von Knoten, die aus dem Artikel eine veröffentlichungsreife Folge macht. Es sind elf neue Knoten nach Zusammenfassung bauen, zusammen mit diesem Eingangsknoten also zwölf. Vier davon prüfen und bereiten vor, damit ein Fehler früh und laut auffällt statt teuer und leise. Denk an eine Passkontrolle am Flughafen: Lieber einmal am Eingang kurz stoppen, als mit falschem Ticket im falschen Flieger zu sitzen.
Hier der ganze Ablauf auf einen Blick, von oben nach unten:
Zusammenfassung bauen (Weg A: A21 / Weg B: Platzhalter)
→ Einstellungen deine Adressen, Wochentitel, Plausibilitätsprüfung
→ Vorhandenen Beitrag suchen gibt es diese Woche schon in WordPress?
→ Woche prüfen wenn ja: Abbruch, kein zweiter Entwurf
→ Sprechfassung vorbereiten Auftrag an Claude zusammenstellen
→ Sprechfassung erzeugen Claude macht das Gespräch
→ Beitrag anlegen WordPress-Entwurf
→ Podcast: Dialog holen Gespräch prüfen, Abmoderation, Dateiname
→ Podcast: vertonen Dienst "vertonung" auf der NAS
→ Podcast: MP3 hochladen Datei in die Mediathek
→ Podcast: Player bauen Abspieler, Transkript, vier Zusatzfelder
→ Podcast: Player nachtragen Beitrag aktualisieren
So sieht das in der Praxis aus, wenn alles durchläuft:

Ein erfolgreicher Lauf von A21 auf der NAS dieses Artikels: oben der Rückblick, unten Beitrag, Titelbild und der Podcast-Strang bis „Podcast: Player nachtragen“, alle Knoten grün. Die vier Prüf- und Vorbereitungsknoten dieser Anleitung sind dort noch nicht eingebaut.
Schritt 6.0: So legst du die Knoten in n8n an
Ulf: „Muss ich mir für jeden Knoten eine neue Klickerei merken?“
Tanja: „Nein. Es sind immer dieselben paar Handgriffe. Einmal lernen, zwölfmal anwenden. Wie Einwurf, Ecke, Freistoß: Die Regeln bleiben gleich, nur die Spieler wechseln.“
Genau so ist es. Alle Knoten dieser Phase werden mit denselben wenigen Handgriffen angelegt. Einmal hier erklärt, gelten sie für jeden folgenden Schritt. Nimm dir für diesen Abschnitt ruhig etwas Zeit, denn ab Schritt 6.2 setzt die Anleitung genau diese Handgriffe voraus. Die Feldnamen sind die der englischen n8n-Oberfläche in Version 2.1.5.
Den Workflow öffnen oder anlegen. Du hast zwei Startpunkte, je nachdem, ob du schon einen Artikel-Workflow hast oder nicht:
- Weg A: Workflow A21 öffnen. Der Strang beginnt am vorhandenen Knoten
Zusammenfassung bauen. - Weg B: Overview → oben rechts Create workflow → oben links auf den Namen „My workflow“ klicken und einen eigenen Namen eintippen. In der leeren Fläche Add first step… → Trigger manually. Der neue Knoten heißt „When clicking ‘Execute workflow’“; benenne ihn in
Von Hand startenum (siehe unten). Dahinter kommt als erster Knoten ein Code-KnotenZusammenfassung bauenmit diesem Inhalt:
// Zusammenfassung bauen (Platzhalter fuer Weg B)
// Dieser Knoten liefert den fertigen Artikel. Weg A: Diesen Knoten loeschen und
// die Kette an deinen vorhandenen Knoten "Zusammenfassung bauen" haengen.
// Weg B: Hier deinen eigenen Text einsetzen oder den Knoten durch deinen
// Textgenerator ersetzen. Pflicht ist genau EIN Datensatz mit dem Feld
// wp_content (der Artikel als HTML, mindestens 500 Zeichen).
return [{ json: {
wp_content: '<p>HIER STEHT DEIN ARTIKEL ALS HTML.</p>',
}}];
Dort setzt du deinen eigenen Artikel ein, oder du ersetzt den Knoten durch deinen Textgenerator. Wichtig ist nur, dass er so heißt und das Feld wp_content liefert. Der Rest der Kette ist es egal, woher der Text kommt. Hauptsache, er liegt am verabredeten Platz.
Einen Knoten anhängen. Am rechten Rand des letzten Knotens erscheint ein kleines +. Darauf klicken → im Suchfeld den Knotentyp eintippen:
- für einen Code-Knoten
Code→ Code → Code in JavaScript, - für einen HTTP-Knoten
HTTP Request→ HTTP Request.
Der neue Knoten ist automatisch mit dem vorigen verbunden. Bequem, oder? Mit einer Ausnahme. Weg A: Hat Zusammenfassung bauen schon eine Verbindung zur Mail, hängst du den ersten neuen Knoten (Einstellungen) über + oben rechts in der Leiste an und ziehst dann mit der Maus eine Linie vom rechten Punkt von Zusammenfassung bauen zum linken Punkt von Einstellungen. Der Ausgang hat danach zwei Linien, eine zur Mail und eine zum Podcast. Ein Ausgang darf also mehrere Abnehmer haben, so wie ein Mittelfeldspieler den Ball mal nach links, mal nach rechts spielen kann. Nur dass n8n beide gleichzeitig bedient.
Einen Knoten umbenennen. Knoten per Doppelklick öffnen → oben links auf den Namen klicken → neuen Namen eintippen → Enter.
Bernd: „Namen sind Schall und Rauch. Ich nenne meine Knoten ‚Knoten 1‘ bis ‚Knoten 12‘. Übersichtlicher geht’s nicht.“
Tanja: „Dann findet kein einziger Code-Knoten seine Kollegen. Die rufen sich nämlich beim Namen.“
Und das ist wörtlich gemeint. Die Namen müssen buchstabengenau stimmen, mit Groß- und Kleinschreibung, Doppelpunkt und Leerzeichen, also etwa Podcast: Dialog holen. Die Code-Knoten und Ausdrücke greifen mit $('Name') auf andere Knoten zu; ein Tippfehler im Namen führt zu der Meldung „Referenced node doesn’t exist“. Stell dir eine Poststelle vor, die nur exakt adressierte Briefe zustellt. Ein fehlender Doppelpunkt, und der Brief geht zurück.
Code einfügen. Im Code-Knoten stehen Mode Run Once for All Items und Language JavaScript schon richtig. Daran musst du nichts ändern. Im Feld JavaScript steht ein Beispielcode: hineinklicken, Cmd+A (Windows: Strg+A), Entf, dann den Code aus diesem Artikel einfügen. Wichtig ist das vollständige Löschen vorher. Bleiben Reste des Beispiels stehen, mischt sich fremder Code unter deinen.
Feste Werte und Ausdrücke. In den Tabellen dieser Phase gibt es zwei Arten von Werten:
- feste Werte wie
POSToderaudio/mpeg: einfach eintragen. - Ausdrücke, erkennbar an den doppelten geschweiften Klammern
{{ … }}: Mit der Maus über das Feld fahren, über dem Feld erscheint der Umschalter Fixed | Expression → Expression wählen → Wert einfügen. Unter dem Feld zeigt n8n dann das ausgerechnete Ergebnis, sobald die Knoten davor einmal gelaufen sind. Steht dort[undefined]oder eine Fehlermeldung, stimmt ein Knotenname nicht.
Ulf: „Und was ist der Unterschied? Text ist doch Text.“
Tanja: „Ein fester Wert ist wie ein Aufkleber: Da steht, was draufsteht. Ein Ausdruck ist eine kleine Rechenaufgabe, die n8n erst im Moment des Laufs löst.“
Genau darum ist der Umschalter so wichtig. Fügst du einen Ausdruck versehentlich als festen Wert ein, schickt n8n die geschweiften Klammern wörtlich weiter, statt sie auszurechnen. Die Vorschau unter dem Feld ist deine Kontrolle: Siehst du dort ein echtes Ergebnis, ist alles richtig.
Schalter und Unterfelder. Viele Felder erscheinen erst, wenn ein Schalter an ist. Nicht wundern also, wenn du ein Feld aus der Tabelle zunächst nicht findest. Send Query Parameters, Send Headers und Send Body sind Schiebeschalter; danach erscheinen Specify … : Using Fields Below und darunter Add Parameter für jede Zeile mit Name und Value. Unter Options öffnet Add option eine Liste, aus der du Timeout oder Response auswählst.
Der Reiter Settings. Oben im geöffneten Knoten gibt es neben Parameters den Reiter Settings. Den übersieht man leicht, weil n8n immer auf Parameters öffnet. Dort stehen Always Output Data, Retry On Fail, Max. Tries und Wait Between Tries (ms). Steht bei einem Knoten nichts zu Settings, bleibt dort alles ausgeschaltet.
Zugänge. Die WordPress-Knoten verwenden Authentication Generic Credential Type → Generic Auth Type Basic Auth → Basic Auth: dein WordPress-Zugang aus Schritt 6.1. Der Claude-Knoten verwendet Authentication Predefined Credential Type → Credential Type Anthropic → Anthropic: dein Anthropic-Zugang. Die Zugänge selbst legst du einmal an, gleich in Schritt 6.1. In den Knoten wählst du sie dann nur noch aus einer Liste aus.
Speichern. Nach jedem Knoten Cmd+S (Windows: Strg+S) oder oben rechts Save. Klingt banal, rettet aber Nerven, wenn der Browser mal abstürzt.
Einzeln ausprobieren, mit Vorsicht. Jeder geöffnete Knoten hat oben rechts Execute step. Bis einschließlich Sprechfassung vorbereiten kannst du damit gefahrlos prüfen, ob ein Knoten das erwartete Ergebnis liefert.
Bernd: „Ich drück einfach überall auf Execute step. Probieren geht über Studieren.“
Tanja: „Bis zum vierten Knoten gern. Danach probierst du mit echtem Geld und füllst nebenbei die Mediathek mit Müll.“
So ist es. Ab Sprechfassung erzeugen kostet jeder Klick etwas oder legt etwas an: einen Aufruf bei Claude, einen WordPress-Entwurf, eine Vertonung bei Google, eine MP3 in der Mediathek. Den ganzen Strang lässt du deshalb erst in Schritt 7.3 einmal komplett laufen.
Ein Hinweis zur Knotenversion: Getestet wurde der Strang mit HTTP-Request-Knoten der Version 4.2. Ein neu angelegter HTTP-Request-Knoten ist in n8n 2.1.5 Version 4.3 (steht ganz unten im Reiter Settings). Die Felder, die diese Anleitung verwendet, heißen dort gleich und stehen an derselben Stelle. Keine Panik also, wenn bei dir eine andere Nummer steht.
Die fertige Kette hat nach Zusammenfassung bauen elf Knoten in einer Reihe. Jeder Knoten hat genau eine Linie zum nächsten. Diese Tabelle ist deine Mannschaftsaufstellung: Name, Position, und in welchem Schritt der Spieler aufs Feld kommt.
| Nr. | Name (genau so) | Knotentyp | Schritt |
|---|---|---|---|
| 1 | Einstellungen | Code in JavaScript | 6.2 |
| 2 | Vorhandenen Beitrag suchen | HTTP Request | 6.3 |
| 3 | Woche prüfen | Code in JavaScript | 6.4 |
| 4 | Sprechfassung vorbereiten | Code in JavaScript | 6.5 |
| 5 | Sprechfassung erzeugen | HTTP Request | 6.5 |
| 6 | Beitrag anlegen | HTTP Request | 6.6 |
| 7 | Podcast: Dialog holen | Code in JavaScript | 6.7 |
| 8 | Podcast: vertonen | HTTP Request | 6.8 |
| 9 | Podcast: MP3 hochladen | HTTP Request | 6.9 |
| 10 | Podcast: Player bauen | Code in JavaScript | 6.10 |
| 11 | Podcast: Player nachtragen | HTTP Request | 6.11 |
Die Kette ist in dieser Form am 27. September 2026 auf der NAS dieses Artikels vollständig durchgelaufen: vom Artikel bis zum fertigen Entwurf mit einer 14:46 Minuten langen Folge.
Schritt 6.1: n8n den Zugang zu WordPress und Anthropic geben
Ulf: „Kann n8n nicht einfach mit meinem normalen WordPress-Passwort rein?“
Bernd: „Klar. Hab ich immer so gemacht. Ein Passwort für alles, dann vergisst man keins.“
Tanja: „Und wenn es irgendwo abhandenkommt, stehen alle Türen offen. Für Maschinen gibt es ein eigenes Passwort.“
Bernds Methode ist bequem und gefährlich. Besser geht es so: n8n spricht WordPress über dessen REST-Schnittstelle an (eine Programmierschnittstelle, über die sich Beiträge und Medien anlegen und ändern lassen). Stell sie dir als Lieferanteneingang vor: Nicht die Haustür für Menschen, sondern die Rampe, an der Programme Beiträge abliefern und abholen. Dafür braucht es ein Anwendungspasswort: ein eigenes Passwort nur für diesen Zweck, das sich jederzeit einzeln widerrufen lässt, ohne das eigene Anmeldepasswort zu ändern. Wie ein Zweitschlüssel für den Hausmeister, den du ihm jederzeit wieder abnehmen kannst.
Klickfolge in WordPress: Benutzer → Profil → ganz unten Anwendungspasswörter → Name n8n → Neues Anwendungspasswort hinzufügen.
Erwartetes Ergebnis: ein Passwort aus sechs Viererblöcken, etwa abcd efgh ijkl mnop qrst uvwx. Es wird nur einmal angezeigt. Kopiere es also sofort, bevor du die Seite verlässt.

Die Liste zeigt nur Namen und Datum, das Passwort selbst ist nach dem Anlegen nicht mehr einsehbar. Hier heißt der Eintrag „cowork-api“.
Klickfolge in n8n: links Credentials → Add Credential → Basic Auth → User: dein WordPress-Benutzername → Password: das Anwendungspasswort → Name zum Beispiel WordPress → Save.
Erwartetes Ergebnis: Der Zugang steht in der Liste. Ob er stimmt, zeigt sich beim ersten Aufruf: Vorhandenen Beitrag suchen antwortet dann ohne Fehler, ein falsches Passwort meldet 401. Genau so ist der Zugang auf der NAS dieses Artikels eingerichtet und getestet. n8n kennt auch einen eigenen Zugangstyp „WordPress API“; mit dem HTTP-Request-Knoten ist er hier nicht erprobt. Wer auf Nummer sicher gehen will, bleibt bei Basic Auth.
Dasselbe für Anthropic: Credentials → Add Credential → Anthropic → deinen API-Schlüssel aus der Anthropic-Konsole eintragen → Save.
Und noch eine Zahl: die Nummer der Kategorie, in die der Rückblick soll. WordPress zeigt dir diese Nummer nirgends groß an, du musst sie ein bisschen suchen. Klickfolge: Beiträge → Kategorien → mit der Maus über den Kategorienamen fahren. In der Statuszeile des Browsers steht eine Adresse mit tag_ID=8. Die Zahl ist die Kategorienummer. Schreib sie dir auf, du brauchst sie im nächsten Schritt.
Schritt 6.2: „Einstellungen“
Ulf: „Wo trag ich jetzt meine Website ein? In jeden Knoten einzeln?“
Tanja: „Nur an einer einzigen Stelle. Das ist der Knoten Einstellungen. Alle anderen schauen dort nach.“
Das ist wie das Schwarze Brett im Büro: Die Durchwahl steht einmal dort, und alle lesen sie ab. Zieht die Abteilung um, änderst du einen Zettel statt zwölf. Knotentyp Code in JavaScript, Name Einstellungen, direkt hinter Zusammenfassung bauen. Mode und Language bleiben wie vorgegeben (Run Once for All Items, JavaScript). Er ist der einzige Knoten, in dem du etwas eintragen musst: deine Website-Adresse und die Kategorienummer. Alle anderen Knoten lesen ihre Adressen von hier.
// Einstellungen - der EINZIGE Knoten, in dem du etwas eintragen musst.
// Alle anderen Knoten lesen ihre Adressen von hier.
const E = {
website: 'https://DEINE_DOMAIN', // deine WordPress-Adresse, ohne / am Ende
kategorie: 'DEINE_KATEGORIENUMMER', // Zahl aus Schritt 6.1, z. B. 8
vertonung: 'http://vertonung:8080', // der Dienst aus Phase 3
sendung: 'KI Wochenrückblick', // Anfang jedes Folgentitels
modell: 'claude-sonnet-4-5-20250929',
};
// 1. Sind noch Platzhalter uebrig? Dann lieber sofort laut abbrechen.
const offen = Object.keys(E).filter(k => String(E[k]).includes('DEIN'));
if (offen.length) {
throw new Error('Bitte zuerst im Knoten Einstellungen ausfuellen - ' + offen.join(', '));
}
const kategorie = parseInt(E.kategorie, 10);
if (!(kategorie > 0)) throw new Error('Die Kategorienummer ist keine Zahl');
// 2. Kommt ueberhaupt ein Artikel an?
const artikel = String(($('Zusammenfassung bauen').first().json || {}).wp_content || '');
if (artikel.length < 500) {
throw new Error('Zusammenfassung bauen liefert keinen Artikel im Feld wp_content');
}
// 3. Der Wochentitel. Samstag bis Freitag der vergangenen Woche, zum Beispiel
// "KI Wochenrückblick 12.-18.09.26" oder ueber den Monatswechsel
// "KI Wochenrückblick 29.08.-4.09.26". Er ist zugleich der Wochen-Schluessel,
// an dem der Workflow eine schon angelegte Woche wiedererkennt.
const a = $now.minus({ days: 7 });
const e = $now.minus({ days: 1 });
const zeitraum = (a.month === e.month ? a.toFormat('d.') : a.toFormat('d.MM.'))
+ '-' + e.toFormat('d.MM.yy');
return [{ json: {
website: E.website.replace(/\/+$/, ''),
kategorie,
vertonung: E.vertonung.replace(/\/+$/, ''),
modell: E.modell,
titel: E.sendung + ' ' + zeitraum,
}}];
Drei Dinge passieren darin:
- Platzhalter fallen sofort auf. Steht irgendwo noch
DEIN…, bricht der Knoten mit der Meldung „Bitte zuerst im Knoten Einstellungen ausfuellen“ ab, bevor irgendetwas Geld kostet. - Ein fehlender Artikel fällt sofort auf. Liefert
Zusammenfassung bauenweniger als 500 Zeichen im Feldwp_content, bricht der Knoten ab. - Der Wochentitel entsteht hier. Er nennt den Zeitraum der vergangenen sieben Tage, Samstag bis Freitag. Am 19. September 2026 ergibt er
KI Wochenrückblick 12.-18.09.26, über einen MonatswechselKI Wochenrückblick 29.08.-4.09.26, über den JahreswechselKI Wochenrückblick 26.12.-1.01.27. Derselbe Titel steht später in der Mediathek, in der MP3 und in jeder Podcast-App. Und er ist der Wochen-Schlüssel: Jede Woche hat genau einen, und an ihm erkennt der nächste Knoten, ob diese Woche schon angelegt ist.
Bernd: „Abbrechen mit Fehlermeldung? Das sieht doch unprofessionell aus. Lass ihn einfach weiterlaufen.“
Tanja: „Ein roter Knoten am Anfang kostet nichts. Ein grüner Lauf mit ‚DEINE_DOMAIN‘ in der Adresse kostet Claude, Google und deine Zeit.“
Früh und laut scheitern ist hier ausdrücklich gewollt. Ein Schiedsrichter, der das Abseits sofort pfeift, erspart allen das Tor, das hinterher doch nicht zählt.
Erwartetes Ergebnis: ein Datensatz mit website, kategorie, vertonung, modell und titel.
Schritt 6.3: „Vorhandenen Beitrag suchen“
Jetzt fragt der Workflow bei WordPress nach, bevor er irgendetwas anlegt. Ein HTTP-Request-Knoten, der WordPress fragt, ob es einen Beitrag mit diesem Titel schon gibt, egal ob Entwurf oder veröffentlicht. Das ist der Blick in den Posteingang, bevor du eine Mail ein zweites Mal verschickst.
Knotentyp HTTP Request, angehängt an Einstellungen. Die Felder von oben nach unten:
| Feld | Einstellung |
|---|---|
| Name | Vorhandenen Beitrag suchen |
| Method | GET |
| URL | Expression: {{ $('Einstellungen').first().json.website }}/wp-json/wp/v2/posts |
| Authentication | Generic Credential Type → Generic Auth Type Basic Auth → Basic Auth: dein WordPress-Zugang |
| Send Query Parameters | an → Specify Query Parameters Using Fields Below → fünfmal Add Parameter, Werte siehe nächste Tabelle |
| Send Headers | aus |
| Send Body | aus |
| Options | keine |
| Reiter Settings → Always Output Data | an (alles andere aus) |
Die fünf Query Parameters:
| Name | Value |
|---|---|
search | Expression: {{ $('Einstellungen').first().json.titel }} |
status | draft,pending,future,private,publish |
context | edit |
per_page | 20 |
_fields | id,title,status |
Kurz zur Einordnung: search sucht nach dem Wochentitel, status sorgt dafür, dass auch Entwürfe, geplante und private Beiträge mitgezählt werden, und _fields hält die Antwort schlank auf id,title,status.
Ulf: „Und wenn es nichts findet? Dann ist doch alles gut, oder?“
Tanja: „Eigentlich ja. Aber genau da steckt die Falle.“
Die Falle dieses Knotens: Findet WordPress nichts, antwortet es mit einer leeren Liste, und n8n macht daraus null Datensätze. Ohne den Schalter Always Output Data bliebe der Lauf hier stehen, grün und ohne Fehlermeldung. Das ist die gemeinste Art von Fehler: Alles sieht gut aus, und trotzdem passiert nichts. Wie ein Staffelläufer, der den Stab nicht übergibt, weil er leer ist. Mit dem Schalter gibt der Knoten einen leeren Datensatz weiter, und es geht weiter. Den Schalter findest du im Reiter Settings, nicht bei den Parametern.
Erwartetes Ergebnis: in einer neuen Woche ein leerer Datensatz, in einer schon angelegten Woche ein Datensatz mit id, title und status.
Schritt 6.4: „Woche prüfen“
Der vorige Knoten hat nur gefragt. Dieser hier entscheidet. Knotentyp Code in JavaScript, Name Woche prüfen, angehängt an Vorhandenen Beitrag suchen. Mode und Language bleiben wie vorgegeben. Den Beispielcode ersetzen durch:
// Woche pruefen
// Gibt es diesen Wochenrueckblick schon als Beitrag - egal ob Entwurf oder
// veroeffentlicht? Dann bricht der Lauf hier ab, BEVOR etwas Geld kostet und
// bevor ein zweiter Entwurf und eine zweite MP3 entstehen.
const titel = $('Einstellungen').first().json.titel;
const treffer = $input.all()
.map(i => i.json)
.filter(p => {
if (!p || !p.id || !p.title) return false;
const t = String(p.title.raw || p.title.rendered || '');
// Genau dieser Titel, oder dieser Titel mit einer Schlagzeile dahinter
// ("... 12.-18.09.26: Die Woche der KI-Agenten").
return t === titel || t.startsWith(titel + ':');
});
if (treffer.length) {
const p = treffer[0];
// Bewusst ohne Doppelpunkt - n8n schneidet Meldungen am letzten Doppelpunkt ab.
throw new Error('Diese Woche gibt es schon als Beitrag ' + p.id
+ ' (Status ' + p.status + '). Kein zweiter Entwurf. Weiter geht es mit Retry, siehe Wiederanlauf');
}
return [{ json: { titel, frei: true } }];
Bernd: „Doppelte Entwürfe? Passiert mir nie. Ich starte einfach nochmal, wenn was hängt.“
Tanja: „Genau so entstehen sie.“
Das ist die Sicherung gegen Doppelentwürfe. Wer einen Lauf, der mittendrin abgebrochen ist, einfach noch einmal ganz startet, bekommt ohne diesen Knoten einen zweiten Entwurf und eine zweite MP3 in der Mediathek. Mit ihm hält der Lauf hier an, bevor Claude oder Google etwas kosten, und die Meldung nennt die Nummer des vorhandenen Beitrags. Bernds Neustart-Taktik ist also genau der Fall, gegen den dieser Knoten schützt. Wie es dann weitergeht, steht in Schritt 6.12.
Die Suche vergleicht den Titel genau: Er muss gleich sein oder mit dem Wochentitel und einem Doppelpunkt beginnen, wie bei „KI Wochenrückblick 12.-18.09.26: Die Woche der KI-Agenten“. search allein würde auch „KI Wochenrückblick 12.-18.09.26 (Kopie)“ finden. WordPress sucht nämlich großzügig, ähnlich wie eine Suchmaschine. Der Code ist der strenge Türsteher, der danach genau auf den Namen schaut.
Erwartetes Ergebnis: in einer neuen Woche ein Datensatz mit titel und frei: true.
Schritt 6.5: „Sprechfassung vorbereiten“ und „Sprechfassung erzeugen“
Ulf: „Jetzt kommt die KI, oder? Endlich der spannende Teil.“
Tanja: „Jetzt kommt die KI. Aber erst packen wir ihr den Auftrag ordentlich ein.“
Zwei Knoten. Der erste schreibt den Auftrag, der zweite schickt ihn ab. Zuerst Code in JavaScript, Name Sprechfassung vorbereiten, angehängt an Woche prüfen. Der Auftrag an Claude aus Phase 5 steht darin als normaler Text. Das ist lesbarer und weniger fehleranfällig als eine einzige lange Zeile mit Hunderten von Escape-Zeichen:
// Sprechfassung vorbereiten
// Baut den Auftrag an Claude. Der Auftragstext steht hier als normaler Text,
// nicht als lange Zeile mit Escape-Zeichen - so bleibt er les- und aenderbar.
const AUFTRAG = `Du machst aus dem Wochenrückblick, den du gleich bekommst, ein Gespräch für einen Podcast mit zwei Stimmen. Die Sprecherin heißt im Ergebnis "w", der Sprecher "m". Beide bleiben namenlos.
Du schreibst den Artikel nicht um - du ERZÄHLST ihn. Kein Satz wörtlich, nie acht aufeinanderfolgende Wörter aus dem Artikel.
LÄNGE: kürzer als der Artikel, 2.000 bis 2.500 Wörter. Ein Gespräch verdichtet.
BEITRÄGE:
- 5 bis 90 Wörter je Beitrag. Mindestens ein Drittel der Beiträge unter 25 Wörtern. Zwei lange Beiträge nie hintereinander. Die kurzen Beiträge über das ganze Gespräch verteilen, nicht nur an den Anfang.
- Jeder Beitrag hat eine eigene Aussage. Verboten sind Füllbeiträge wie "Spannend!", "Absolut." oder "Genau.".
- In jedem Themenblock mindestens zwei Rückfragen, Einwände oder Präzisierungen - ausdrücklich auch im letzten Block.
- Widerspruch nur dort, wo der Artikel wirklich zwei Sichtweisen hergibt.
- Sprich die Hörer zwei- bis viermal direkt mit "du" an.
ANFANG: Der erste Beitrag gehört "w" und beginnt wörtlich mit: "Dieser Rückblick wurde mit künstlicher Intelligenz erstellt und mit synthetischen Stimmen gesprochen."
ENDE: Schreib keinen Abschied und keinen Hinweis auf die Website - das hängt der Workflow selbst an. Ein Ausblick auf die nächste Woche ist erlaubt.
SCHREIBWEISE - das Ergebnis wird vorgelesen:
- Echte Umlaute und ß, nie ae, oe, ue oder ss als Ersatz.
- Keine Tagesdaten. Nur Monat und Jahr oder gar keine Zeitangabe.
- Keine Aufzählungszeichen, keine Überschriften, keine Links, kein HTML, keine Emojis.
AUSGABE: nur JSON, nichts davor und nichts danach:
{"dialog":[{"s":"w","t":"..."},{"s":"m","t":"..."}]}`;
const artikel = String($('Zusammenfassung bauen').first().json.wp_content)
.replace(/<[^>]+>/g, ' ')
.replace(/\s+/g, ' ')
.trim();
return [{ json: { anfrage: {
model: $('Einstellungen').first().json.modell,
max_tokens: 16000,
temperature: 0.7,
system: AUFTRAG,
messages: [{ role: 'user', content: artikel }],
}}}];
Dahinter der HTTP-Request-Knoten, der Claude aufruft:
Knotentyp HTTP Request, angehängt an Sprechfassung vorbereiten:
| Feld | Einstellung |
|---|---|
| Name | Sprechfassung erzeugen |
| Method | POST |
| URL | https://api.anthropic.com/v1/messages (fester Wert) |
| Authentication | Predefined Credential Type → Credential Type Anthropic → Anthropic: dein Anthropic-Zugang |
| Send Query Parameters | aus |
| Send Headers | an → Specify Headers Using Fields Below → Add Parameter: Name anthropic-version, Value 2023-06-01 |
| Send Body | an → Body Content Type JSON → Specify Body Using JSON → JSON: Expression {{ JSON.stringify($json.anfrage) }} |
| Options | Add option → Timeout → 600000 |
| Reiter Settings | Retry On Fail an → Max. Tries 4 → Wait Between Tries (ms) 5000 (der höchste Wert, den das Feld zulässt) |
Das Modell claude-sonnet-4-5-20250929 steht im Knoten Einstellungen. Es ist dasselbe, das in A21 auch den Artikel schreibt. Wer ein neueres verwendet, trägt dessen Namen dort ein. Hier zahlt sich das Schwarze Brett aus Schritt 6.2 aus: eine Zeile ändern, fertig.
Bernd: „Modelle laufen ewig. Einmal eingetragen, nie wieder anfassen.“
Tanja: „Modelle haben ein Ablaufdatum wie Joghurt. Das steht bei Anthropic sogar öffentlich.“
Und zwar so: Anthropic sichert dieses Modell nur bis mindestens 29. September 2026 zu. Prüfe deshalb vor dem Aufbau in Anthropics Übersicht „Model deprecations“, ob es noch aktiv ist. Falls nicht, trage im Knoten Einstellungen das aktuelle Sonnet-Modell ein.
Sonderzeichen im Artikel: Sprechfassung vorbereiten entfernt die HTML-Tags, wandelt aber HTML-Entitäten wie &, oder " nicht zurück. Das ist eine ehrliche Schwachstelle. Enthält dein Artikel viele Sonderzeichen, Tabellen oder Codeblöcke, prüfe die bereinigte Texteingabe in der Ausgabe von Sprechfassung vorbereiten.
Warum die großzügigen Wiederholungen im Reiter Settings? Dahinter steckt eine echte Panne aus dem Test. Am zweiten Samstag mit Podcast hatte die NAS von etwa drei bis zehn Uhr morgens keine Internetverbindung. Der Lauf um neun scheiterte nach 15 Sekunden mit EAI_AGAIN (der Rechner konnte den Namen api.anthropic.com nicht auflösen), und ein Versuch mit zehn Sekunden Abstand half nichts. Mehrere Versuche mit größerem Abstand überbrücken kurze Aussetzer. Das ist wie beim Telefonieren mit schlechtem Netz: Nicht einmal wählen und aufgeben, sondern ein paar Mal mit Pause probieren. Gegen einen stundenlangen Ausfall hilft nur das Nachholen von Hand, siehe Schritt 6.12.

Die frühe, schlanke Fassung von A21: Wochenmaterial holen, aufbereiten, Rückblick schreiben, Sprechfassung erzeugen, zusammenstellen, mailen. Heute steht die Sprechfassung hinter dem fertigen Artikel.
Erwartetes Ergebnis: eine Antwort mit "type": "message", im Feld content[0].text ein JSON-Text, der mit {"dialog":[{"s":"w","t":"Dieser Rückblick wurde mit künstlicher Intelligenz erstellt … beginnt. Rund 100 bis 150 Beiträge.
Schritt 6.6: „Beitrag anlegen“
Claude hat geliefert, jetzt bekommt WordPress seinen Beitrag. Knotentyp HTTP Request, angehängt an Sprechfassung erzeugen:
| Feld | Einstellung |
|---|---|
| Name | Beitrag anlegen |
| Method | POST |
| URL | Expression: {{ $('Einstellungen').first().json.website }}/wp-json/wp/v2/posts |
| Authentication | Generic Credential Type → Generic Auth Type Basic Auth → Basic Auth: dein WordPress-Zugang |
| Send Query Parameters | aus |
| Send Headers | aus |
| Send Body | an → Body Content Type JSON → Specify Body Using JSON → JSON: Expression, Inhalt siehe unten |
| Options | keine |
| Reiter Settings | alles aus |
Der Inhalt des Feldes JSON, als Expression, in einer Zeile:
{{ JSON.stringify({ title: $('Einstellungen').first().json.titel, content: $('Zusammenfassung bauen').first().json.wp_content, status: 'draft', categories: [$('Einstellungen').first().json.kategorie] }) }}
Bernd: „Warum nicht gleich veröffentlichen? Entwürfe sind was für Zauderer.“
Tanja: „Weil noch keine MP3 dranhängt. Und weil ein Mensch drüberschauen soll, bevor die Welt es hört.“
Entscheidend ist status: 'draft': Der Beitrag entsteht als Entwurf. Bernd liegt also falsch. Nichts geht ungeprüft online.
Und die Reihenfolge ist Absicht. Der Beitrag wird erst angelegt, nachdem Claude das Gespräch geliefert hat. Scheitert Claude, gibt es also auch keinen verwaisten Entwurf. Erst das Material, dann der Aktenordner, nicht umgekehrt.
Erwartetes Ergebnis: eine Antwort mit "id": 4960 (deine Nummer ist eine andere), "status": "draft" und einem Feld content.raw mit dem Artikeltext. Diese Antwort brauchen die nächsten Knoten.
Schritt 6.7: „Podcast: Dialog holen“
Montagmorgen, Kaffeeküche. Ulf starrt auf seinen Bildschirm, auf dem ein langer Textblock aus Claude klebt.
Ulf: „Das Modell hat doch schon das ganze Gespräch geschrieben. Warum brauchen wir da noch einen Knoten dazwischen?“
Tanja: „Weil ein Text aus einem Sprachmodell wie ein Pass aus dem Mittelfeld ist. Meistens kommt er an. Aber du prüfst trotzdem, bevor du schießt.“
Bernd: „Prüfen, prüfen. Das Ding ist schlau, das liefert immer sauberes JSON. Einfach direkt an Google weiterschicken.“
Genau das ist der Irrtum, den dieser Knoten abfängt. Ein Sprachmodell liefert nicht garantiert sauberes JSON. Mal fehlt eine Klammer, mal ist das Gespräch halb so lang wie gedacht. Schickst du so etwas ungeprüft weiter, merkst du den Fehler erst, wenn die Folge schon online ist. Deshalb sitzt hier ein Prüfer, der wie ein Wareneingang im Lager arbeitet: auspacken, nachzählen, Etikett drauf.
So legst du ihn an: Knotentyp Code in JavaScript, Name Podcast: Dialog holen, angehängt an Beitrag anlegen. Mode und Language bleiben wie vorgegeben, da musst du nichts umstellen. Der Knoten liest das Gespräch, prüft es, hängt die Abmoderation an und bildet den Dateinamen. Diesen Code fügst du ein:
// Podcast: Dialog holen
// Liest die Sprechfassung, prueft sie und baut daraus drei Dinge:
// turns - die Beitraege fuer den Vertonungsdienst
// skript - dieselben Beitraege als Klartext (fuer das Zusatzfeld)
// dateiname - der Name der MP3 in der Mediathek
// Die beiden Stimmen heissen an genau EINER Stelle. Transkript, Skript und
// Wochenmail sollen dieselben Woerter benutzen.
const NAMEN = { w: 'Sprecherin', m: 'Sprecher' };
// Die Abmoderation steht fest im Code, nicht im Prompt: Ein Satz, der jede
// Woche gleich klingen soll, darf nicht davon abhaengen, ob das Modell ihn
// diesmal mitschreibt.
const ABMODERATION = [
{ s: 'w', t: 'Das war die KI-Woche. Den ganzen Rückblick zum Nachlesen findest du auf unserer Website.' },
{ s: 'm', t: 'Nächsten Samstag sind wir wieder da. Bis dahin.' },
];
// 1. Antwort des Modells lesen. Es liefert {"dialog":[{"s":"w","t":"..."}, ...]}
const roh = String((($('Sprechfassung erzeugen').first().json.content || [])[0] || {}).text || '');
const text = roh.replace(/^```(?:json)?/i, '').replace(/```$/, '').trim();
let dialog;
try {
dialog = JSON.parse(text.slice(text.indexOf('{'), text.lastIndexOf('}') + 1)).dialog;
} catch (e) {
// Bewusst ohne Doppelpunkt in der Meldung - n8n schneidet Fehlermeldungen
// am letzten Doppelpunkt ab.
throw new Error('Die Sprechfassung ist kein lesbares JSON');
}
// 2. Pruefen. Kaputt ist nicht leer - kaputt muss laut werden.
if (!Array.isArray(dialog)) throw new Error('Die Sprechfassung enthaelt kein Feld dialog');
const turns = dialog
.filter(b => b && (b.s === 'w' || b.s === 'm') && String(b.t || '').trim())
.map(b => ({ s: b.s, t: String(b.t).replace(/\s+/g, ' ').trim() }));
if (turns.length < 20) throw new Error('Nur ' + turns.length + ' brauchbare Beitraege, erwartet sind mindestens 20');
// 3. Abmoderation anhaengen - aber nur einmal.
if (!turns[turns.length - 1].t.includes('Nächsten Samstag sind wir wieder da')) {
turns.push(...ABMODERATION);
}
// 4. Klartext fuer das Transkript.
const skript = turns.map(b => NAMEN[b.s] + ': ' + b.t).join('\n');
// 5. Dateiname aus Datum und Titel: nur Kleinbuchstaben, Ziffern, Bindestrich.
const beitrag = $('Beitrag anlegen').first().json;
const titel = String((beitrag.title && beitrag.title.raw) || 'KI Wochenrückblick');
const kurz = titel.toLowerCase()
.replace(/ä/g, 'ae').replace(/ö/g, 'oe').replace(/ü/g, 'ue').replace(/ß/g, 'ss')
.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 80);
const heute = new Date().toISOString().slice(0, 10);
return [{ json: {
turns,
skript,
titel,
beitrag_id: beitrag.id,
seite: beitrag.link,
dateiname: heute + '_' + kurz + '.mp3',
beitraege: turns.length,
zeichen: turns.reduce((n, b) => n + b.t.length, 0),
}}];
Schauen wir uns das mal an. Drei Entscheidungen stecken darin, und jede hat einen guten Grund:
- Kaputt ist nicht leer. Liefert das Modell kein lesbares JSON oder weniger als 20 Beiträge, bricht der Knoten mit einer klaren Meldung ab. Eine halbe Folge, die niemandem auffällt, ist schlimmer als ein roter Knoten am Samstagmorgen. Rot nervt, aber rot ist ehrlich. Die Fehlermeldungen enthalten übrigens absichtlich keinen Doppelpunkt: n8n schneidet Meldungen am letzten Doppelpunkt ab, und dann steht in der Fehlermail nur noch die Hälfte. Klingt nach Kleinkram, spart dir aber im Ernstfall das Rätselraten.
- Die Abmoderation steht im Code, nicht im Prompt. Ein Satz, der jede Woche gleich klingen soll, darf nicht davon abhängen, ob das Modell ihn diesmal mitschreibt oder umformuliert. Denk an die Stadionhymne vor dem Anpfiff: Die kommt vom Band, nicht vom Stadionsprecher aus dem Gedächtnis. Die Sperre prüft vorher, ob die Abmoderation schon da ist, damit ein zweiter Lauf sie nicht verdoppelt.
- Die Namen der Stimmen stehen an genau einer Stelle. In einer frühen Fassung hießen sie im Transkript „Moderator“ und „Moderatorin“, in der Wochenmail aber „Sprecher“ und „Sprecherin“. Die Angleichung betraf danach 578 Stellen in den bis dahin erschienenen Folgen. Eine Stelle für die Namen heißt: eine Stelle zum Ändern, nicht 578.
Ulf: „578? Wer hat das denn alles von Hand geändert?“
Tanja: „Niemand, der das noch einmal machen will. Darum steht es jetzt ganz oben im Code.“
Erwartetes Ergebnis: ein Datensatz mit turns (Liste der Beiträge), skript (Klartext), titel, beitrag_id, seite, dateiname (etwa 2026-09-19_ki-wochenrueckblick-12-18-09-26.mp3), beitraege (etwa 110) und zeichen (etwa 17.000).
Schritt 6.8: „Podcast: vertonen“
Jetzt wird es laut. Dieser Knoten schickt das geprüfte Gespräch an deinen Vertonungsdienst aus Phase 3, und der lässt Google die Stimmen sprechen.
Bernd: „Und wenn das schiefgeht, stellen wir einfach drei Wiederholungen ein. Doppelt hält besser, dreifach noch besser.“
Klingt vernünftig, ist hier aber genau falsch. Jeder Versuch lässt Google alle Beiträge neu sprechen. Drei automatische Wiederholungen heißen im schlimmsten Fall: dreimal dieselbe teure Arbeit, ohne dass jemand nachgesehen hat, warum es beim ersten Mal nicht geklappt hat. Deshalb bleibt die Wiederholung hier bewusst aus. Scheitert der Aufruf, soll ein Mensch nachsehen, statt dass der Workflow dreimal dieselbe teure Arbeit anstößt.
Es ist ein HTTP-Request-Knoten, der den Dienst aus Phase 3 anspricht. Knotentyp HTTP Request, angehängt an Podcast: Dialog holen. Die Einstellungen im Einzelnen:
| Feld | Einstellung |
|---|---|
| Name | Podcast: vertonen |
| Method | POST |
| URL | Expression: {{ $('Einstellungen').first().json.vertonung }}/vertonen |
| Authentication | None |
| Send Query Parameters | aus |
| Send Headers | aus |
| Send Body | an → Body Content Type JSON → Specify Body Using JSON → JSON: Expression {{ JSON.stringify({ turns: $json.turns, titel: $json.titel, seite: $json.seite }) }} |
| Options | Add option → Response → Response Format File → Put Output in Field data; dann noch einmal Add option → Timeout → 900000 (15 Minuten) |
| Reiter Settings | Retry On Fail aus |
Achte besonders auf die letzte Zeile: Im Reiter Settings bleibt Retry On Fail aus. Und keine Panik wegen des langen Timeouts. Die Vertonung braucht einfach ihre Zeit, und ohne großzügiges Zeitlimit würde n8n aufgeben, während Google noch spricht.
Erwartetes Ergebnis: nach vier bis sechs Minuten eine Binärdatei data, audio/mpeg, 8 bis 10 MB. Beim Lauf vom 19. September: 108 Beiträge, 279 Sekunden, 8.952.428 Byte, 18:45 Minuten.
Schritt 6.9: „Podcast: MP3 hochladen“
Die fertige MP3 liegt jetzt in n8n, aber noch nicht dort, wo Hörer sie finden. Die Datei geht deshalb in die WordPress-Mediathek. Stell dir die Mediathek als Materiallager vor: Was nicht eingebucht ist, kann auch kein Beitrag ausgeben.
Knotentyp HTTP Request, angehängt an Podcast: vertonen:
| Feld | Einstellung |
|---|---|
| Name | Podcast: MP3 hochladen |
| Method | POST |
| URL | Expression: {{ $('Einstellungen').first().json.website }}/wp-json/wp/v2/media |
| Authentication | Generic Credential Type → Generic Auth Type Basic Auth → Basic Auth: dein WordPress-Zugang |
| Send Query Parameters | aus |
| Send Headers | an → Specify Headers Using Fields Below → zweimal Add Parameter, Werte siehe nächste Tabelle |
| Send Body | an → Body Content Type n8n Binary File → Input Data Field Name data |
| Options | keine |
| Reiter Settings | Retry On Fail aus |
Dazu kommen die beiden Kopfzeilen (Header Parameters). Sie sagen WordPress, wie die Datei heißen soll und dass es sich um Audio handelt:
| Name | Value |
|---|---|
Content-Disposition | Expression: {{ 'attachment; filename="' + $('Podcast: Dialog holen').first().json.dateiname + '"' }} |
Content-Type | audio/mpeg |
Ulf: „Send Body? Die Datei ist doch schon da, die hat der Knoten davor gerade gebaut.“
Tanja: „Da ist sie. Aber nur in n8n. Ohne den Schalter bleibt sie auf der Ersatzbank sitzen.“
Die Falle dieses Knotens: Wer den Schalter Send Body vergisst, weil die Datei ja „schon da“ ist, bekommt von WordPress die Antwort 400 – Keine Daten bereitgestellt. Ohne diesen Schalter schickt n8n die Datei schlicht nicht mit. Das ist einer der häufigsten Anfängerfehler an dieser Stelle, also lieber zweimal hinschauen.
Erwartetes Ergebnis: eine Antwort mit id (die Mediennummer), source_url (die Adresse der MP3, etwa https://www.deine-domain.de/wp-content/uploads/2026/09/2026-09-19_ki-wochenrueckblick-12-18-09-26.mp3) und media_details mit filesize und length_formatted. Die Dauer ermittelt WordPress beim Hochladen selbst, du musst sie nirgends ausrechnen.
Schritt 6.10: „Podcast: Player bauen“
Die MP3 ist im Lager, der Artikel liegt als Entwurf bereit. Jetzt fehlt der Monteur, der beides zusammenschraubt.
Knotentyp Code in JavaScript, Name Podcast: Player bauen, angehängt an Podcast: MP3 hochladen. Mode und Language bleiben wie vorgegeben. Der Knoten setzt den Beitrag zusammen, und zwar in dieser Reihenfolge:
- oben ein Abspieler mit einem Satz, der sagt, was man hört,
- dann der Artikel,
- dann unter „Zum Hören und Mitlesen“ der Abspieler ein zweites Mal mit der Dauer,
- darunter ein aufklappbares Transkript.
Außerdem füllt er die vier Zusatzfelder, aus denen später der Feed liest. Diesen Code fügst du ein:
// Podcast: Player bauen
// Setzt Abspieler, Hinweiszeile und aufklappbares Transkript in den Beitrag
// und fuellt die vier Zusatzfelder, aus denen der Feed liest.
const d = $('Podcast: Dialog holen').first().json;
const b = $('Beitrag anlegen').first().json;
const m = $json; // Antwort von "Podcast: MP3 hochladen"
if (!m.id || !m.source_url) throw new Error('Der Upload hat keine Mediennummer geliefert');
const NAMEN = { w: 'Sprecherin', m: 'Sprecher' };
const esc = s => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
const dauer = (m.media_details && m.media_details.length_formatted) || '';
const bytes = (m.media_details && m.media_details.filesize) || 0;
const abspieler = unterschrift =>
'<!-- wp:audio {"id":' + m.id + '} -->\n'
+ '<figure class="wp-block-audio"><audio controls src="' + m.source_url + '"></audio>'
+ '<figcaption class="wp-element-caption">' + esc(unterschrift) + '</figcaption></figure>\n'
+ '<!-- /wp:audio -->';
const transkript = d.turns.map(t =>
'<!-- wp:paragraph -->\n<p><strong>' + NAMEN[t.s] + ':</strong> ' + esc(t.t) + '</p>\n<!-- /wp:paragraph -->'
).join('\n\n');
// Der Inhalt wird IMMER aus dem urspruenglichen Artikeltext von "Beitrag anlegen"
// neu zusammengesetzt. Ein zweiter Versuch (Retry) ergibt deshalb denselben
// Beitrag und keinen doppelten Abspieler.
const alt = String((b.content && b.content.raw) || '');
// Sperre: Steht die Datei schon im Ausgangstext, stimmt etwas mit der Kette nicht.
if (alt.includes(m.source_url)) throw new Error('Der Abspieler steht schon im Beitrag');
const inhalt = [
abspieler('Diese Folge anhören: ' + d.titel + ' — dieselben Themen, frei erzählt statt vorgelesen.'),
alt,
'<!-- wp:heading -->\n<h2 class="wp-block-heading">Zum Hören und Mitlesen</h2>\n<!-- /wp:heading -->',
abspieler(d.titel + (dauer ? ' — ' + dauer + ' Minuten' : '')),
'<!-- wp:details -->\n<details class="wp-block-details"><summary>Transkript der Folge</summary>'
+ transkript + '</details>\n<!-- /wp:details -->',
].join('\n\n');
return [{ json: {
beitrag_id: b.id,
body: {
content: inhalt,
meta: {
podcast_url: m.source_url,
podcast_bytes: Number(bytes),
podcast_dauer: dauer,
podcast_skript: d.skript,
},
},
}}];
Bernd: „Der Satz unter dem Player ist doch Deko. Den liest eh keiner.“
Doch, und er hat eine Aufgabe. Der Satz unter dem oberen Abspieler („dieselben Themen, frei erzählt statt vorgelesen“) ist eine kleine, aber wichtige Erwartungssteuerung. Wer den Podcast neben dem Text laufen lässt, soll nicht nach der Stelle suchen, an der die Stimme gerade vorliest. Sie liest nicht vor. Ohne den Hinweis scrollt der Hörer verwirrt durch den Artikel und hält den Podcast für kaputt.
Und das Transkript? Das Transkript im Beitrag ist nicht nur Service für Hörgeschädigte und Suchmaschinen. Es ist auch das Archiv des Gesprächs: n8n löscht die Daten vergangener Läufe nach 336 Stunden, also nach zwei Wochen. Ohne das Zusatzfeld podcast_skript wäre der gesprochene Text nach zwei Wochen verloren. n8n ist also eher Zwischenablage als Aktenschrank. Was bleiben soll, gehört nach WordPress.
Schritt 6.11: „Podcast: Player nachtragen“
Letzter Knoten, letzter Pass vor dem Tor. Er schreibt alles in den Beitrag.
Knotentyp HTTP Request, angehängt an Podcast: Player bauen:
| Feld | Einstellung |
|---|---|
| Name | Podcast: Player nachtragen |
| Method | POST |
| URL | Expression: {{ $('Einstellungen').first().json.website }}/wp-json/wp/v2/posts/{{ $json.beitrag_id }} |
| Authentication | Generic Credential Type → Generic Auth Type Basic Auth → Basic Auth: dein WordPress-Zugang |
| Send Query Parameters | aus |
| Send Headers | aus |
| Send Body | an → Body Content Type JSON → Specify Body Using JSON → JSON: Expression {{ JSON.stringify($json.body) }} |
| Options | keine |
| Reiter Settings | Retry On Fail an → Max. Tries 3 (Wait Between Tries bleibt auf dem vorgegebenen Wert) |
Ulf: „Moment. Bei der Vertonung durfte nicht wiederholt werden, hier plötzlich dreimal? Was denn jetzt?“
Tanja: „Kommt drauf an, was eine Wiederholung anrichtet. Hier richtet sie nichts an.“
Genau das ist der Unterschied. Hier darf wiederholt werden: Der Knoten ändert einen vorhandenen Beitrag, und Podcast: Player bauen setzt den Inhalt jedes Mal aus dem ursprünglichen Artikeltext neu zusammen. Ein zweiter Versuch schreibt denselben Beitrag noch einmal, er erzeugt keinen doppelten Abspieler. Das ist, als würdest du ein Formular zweimal mit denselben Angaben ausfüllen: Am Ende steht das Gleiche drin. Bei der Vertonung dagegen kostet jeder Versuch echte Arbeit bei Google.
Erwartetes Ergebnis: eine Antwort, in der unter meta die vier Felder gefüllt sind: podcast_url, podcast_bytes (etwa 8952428), podcast_dauer (etwa 18:45) und podcast_skript. Stehen dort leere Werte, obwohl der Knoten mit 200 geantwortet hat, ist das Feed-Snippet aus Phase 4 nicht aktiv, denn ohne show_in_rest verwirft WordPress die Felder wortlos. Das ist tückisch: grüner Haken, leere Felder, keine Fehlermeldung. Wenn du das siehst, geh zurück zu Phase 4.
Schritt 6.12: Wiederanlauf, wenn ein Lauf mittendrin abbricht
Samstag, kurz nach acht. Die Fehlermail ist da, ein Knoten ist rot.
Bernd: „Kein Problem. Workflow einfach nochmal starten, von vorne. Hat bei meinem Drucker auch immer geholfen.“
Tanja: „Und danach hast du zwei Entwürfe und zwei MP3s. Beim Drucker waren es wenigstens nur doppelte Seiten.“
Bernds Reflex ist verständlich, aber hier falsch. Der Strang legt Dinge in WordPress an, die ein zweiter, vollständiger Lauf nicht wiederfindet: einen Entwurf und eine MP3 in der Mediathek. Wer von vorne startet, baut also neben den halbfertigen Beitrag einen zweiten. Deshalb gilt eine einfache Regel: Ab Beitrag anlegen wird nicht neu gestartet, sondern fortgesetzt.
Fortsetzen heißt in n8n „Retry“. Im Fußball wäre das kein Wiederholungsspiel, sondern die Fortsetzung eines abgebrochenen Spiels beim alten Spielstand. Klickfolge: Workflow öffnen → oben Executions → den fehlgeschlagenen Lauf (rot) anklicken → oben rechts Retry → Retry with currently saved workflow.
n8n startet dann ab dem Knoten, der gescheitert ist, und nimmt für alle Knoten davor die Ergebnisse aus dem ersten Versuch: dieselbe Beitragsnummer, dieselbe Mediennummer, dasselbe Gespräch. Das ist an genau diesem Strang ausprobiert worden: Ein Testlauf scheiterte absichtlich erst am letzten Knoten, Podcast: Player nachtragen. Nach Retry lief nur dieser Knoten noch einmal, 2,7 Sekunden lang, und schrieb Abspieler, Transkript und alle vier Zusatzfelder in denselben Entwurf. In WordPress gab es danach einen Entwurf und eine MP3, nicht zwei.

Das Retry-Menü einer fehlgeschlagenen Ausführung. Beide Einträge setzen ausdrücklich „from node with error“ fort.

Der Wiederholungslauf: „Succeeded in 2.735s“, „Retry of execution #8502″, unten die Antwort von WordPress für denselben Beitrag 5031.
Jetzt noch etwas, das dich sonst unnötig erschreckt. Eine Eigenheit von n8n 2.1.5: Nach dem Klick auf Retry kann rechts unten die rote Meldung „Problem with retry – Converting circular structure to JSON“ erscheinen, obwohl der Wiederholungslauf erfolgreich war. Ja, das ist verwirrend, und nein, du hast nichts falsch gemacht. Maßgeblich ist der Eintrag in der Liste der Ausführungen: grün und „Retry of execution …“.
Ulf: „Und was ist der Unterschied zwischen ‚currently saved‘ und dem anderen Eintrag?“
„Currently saved“ heißt: Hast du den Fehler inzwischen im Workflow behoben und gespeichert, läuft der Wiederholungsversuch mit der reparierten Fassung. „Original workflow“ nimmt die Fassung vom Zeitpunkt des Laufs. Meistens willst du also „currently saved“, denn du hast ja gerade etwas repariert.
Wo ist der Lauf stehen geblieben, und was ist zu tun?
Die folgende Tabelle ist dein Spickzettel für den Samstagmorgen. Such die Zeile mit dem roten Knoten und tu, was dort steht:
| gescheitert bei | was schon existiert | was zu tun ist |
|---|---|---|
Einstellungen bis Sprechfassung erzeugen | nichts in WordPress | Fehler beheben, Workflow einfach neu starten. Kostet höchstens einen weiteren Aufruf bei Claude. |
Beitrag anlegen | in aller Regel nichts. Nur wenn die Antwort unterwegs verloren ging, kann der Entwurf trotzdem entstanden sein. | Unter Beiträge → Entwürfe nachsehen. Kein Entwurf: Retry oder neu starten. Entwurf da: kein Retry. Ein Retry führt Beitrag anlegen noch einmal aus und legt einen zweiten Entwurf an, denn Woche prüfen läuft dabei nicht mit. Stattdessen den unvollständigen Entwurf in den Papierkorb legen und den Workflow neu starten. |
Podcast: Dialog holen | der Entwurf | Meldung lesen. Meist hat Claude kein brauchbares Gespräch geliefert. Dann den Entwurf in den Papierkorb legen und neu starten, denn das Gespräch entsteht vor dem Entwurf. |
Podcast: vertonen | der Entwurf | Dienst prüfen (Schritt 3.4), dann Retry. Google spricht dabei alle Beiträge noch einmal, rund 17.000 Zeichen aus dem Freikontingent. |
Podcast: MP3 hochladen | der Entwurf, vielleicht schon die MP3 | In der Mediathek nach dem Dateinamen suchen (Datum und Titel, etwa 2026-09-19_ki-wochenrueckblick-…). Dann Retry. Lag die Datei schon dort, gibt es sie danach zweimal. Die, auf die der Beitrag nicht zeigt, ist eine Waise und kann weg (siehe unten). |
Podcast: Player bauen oder Podcast: Player nachtragen | Entwurf und MP3 | Retry. Es entsteht nichts Neues, der Beitrag wird nur fertig beschrieben. |
Eine Zeile verdient besondere Aufmerksamkeit, weil sie der Faustregel „ab Beitrag anlegen fortsetzen“ scheinbar widerspricht: Scheitert der Lauf bei Beitrag anlegen selbst, schau zuerst unter Beiträge → Entwürfe nach. Ist dort ein Entwurf entstanden, obwohl die Antwort unterwegs verloren ging, machst du kein Retry. Ein Retry würde Beitrag anlegen noch einmal ausführen und einen zweiten Entwurf anlegen, denn Woche prüfen läuft dabei nicht mit. Stattdessen legst du den unvollständigen Entwurf in den Papierkorb und startest den Workflow neu.
Was ein kompletter Neustart stattdessen tut: Er kommt nur bis Woche prüfen und bricht dort nach gut zwei Sekunden mit der Meldung „Diese Woche gibt es schon als Beitrag 5031 (Status draft). Kein zweiter Entwurf. Weiter geht es mit Retry, siehe Wiederanlauf“ ab. Das ist Absicht. Ohne diese Sicherung entstünden bei jedem Neustart ein weiterer Entwurf und eine weitere MP3. Bernds Drucker-Methode läuft also ins Leere, und zwar gewollt.

Der zweite Start derselben Woche im Test: Claude, Google und WordPress werden gar nicht erst angesprochen.
Wenn Retry nicht mehr geht (etwa weil der Lauf älter als 14 Tage ist und n8n die Daten gelöscht hat): Entwurf in den Papierkorb legen, die zugehörige MP3 in der Mediathek löschen, dann neu starten. Weil der Entwurf dann im Papierkorb liegt und nicht mehr als Entwurf zählt, lässt Woche prüfen den Lauf durch.
Und falls schon jemand vor dir Bernd gespielt hat? So räumst du auf.
Doppelte Entwürfe erkennen: Beiträge → Filter Entwürfe → im Suchfeld den Wochentitel, etwa KI Wochenrückblick 12.-18.09.26. Mehr als ein Treffer heißt: Hier ist schon einmal neu gestartet worden.
Waisen in der Mediathek erkennen: Medien → Ansicht Liste → Filter Audio. Gibt es denselben Dateinamen zweimal, hat WordPress an den zweiten ein -1 angehängt. Welche der beiden die richtige ist, zeigt der Beitrag: Die Adresse im Abspieler (und im Feld podcast_url) nennt sie. Die andere ist die Waise, also eine Datei, auf die kein Beitrag zeigt.
Bernd: „Dann lösch ich am besten gleich alles, was nach Podcast aussieht. Sauber ist sauber.“
Bitte nicht. Aufräumen ja, Kahlschlag nein. Was du keinesfalls löschen solltest:
- einen veröffentlichten Beitrag mit Folge. Seine Nummer steckt in der Folgenkennung (
guid) im Feed. Verschwindet er, verschwindet die Folge aus allen Apps. - die MP3 einer veröffentlichten Folge. Die Apps haben ihre Adresse gespeichert. Und die WordPress-Mediathek hat keinen Papierkorb: gelöscht ist gelöscht.
- den fehlgeschlagenen Lauf unter Executions, solange du ihn noch fortsetzen willst. Ohne ihn gibt es kein Retry.
Phase 7: Samstag, neun Uhr, der erste echte Lauf
Freitagnachmittag. Bernd hat den Kalender auf dem großen Bildschirm geöffnet und schiebt Termine hin und her, als wäre es ein Transfermarkt.
Bernd: „Wieso Samstag um neun? Ich stell das auf ‚alle sieben Tage‘ und fertig. Sieben ist sieben.“
Tanja: „Sieben Tage ab wann? Ab dem Moment, in dem der Lauf startet? Dann hängt deine Woche an der Startminute.“
Ulf: „Ist doch wie Spieltag. Samstag ist Bundesliga, da weiß jeder, wann Anpfiff ist.“
Tanja: „Genau das bauen wir jetzt. Ein fester Anpfiff und eine Woche, die immer gleich lang ist.“
Bis hierhin hast du jeden Lauf selbst angestoßen. Jetzt übernimmt die Uhr. Und damit die Uhr nichts Dummes tut, stellen wir drei Dinge sauber ein: den Takt, das Zeitfenster und die Zeitzone.
Schritt 7.1: Den Takt auf Samstag stellen
Ein Wochenrückblick, der samstags erscheint, sollte die Woche von Samstag bis Freitag abdecken, und zwar in vollen Kalendertagen. Warum so pingelig? Wer „die letzten sieben Tage ab jetzt“ abfragt, bekommt je nach Startminute mal einen Beitrag vom Samstagmorgen dazu und mal nicht. Das ist wie eine Tabelle, bei der mal das Frühspiel mitzählt und mal nicht. Keiner weiß mehr, was eigentlich drinsteht.
Klickfolge in A21 (Weg A): den Zeitplan-Knoten öffnen → Trigger Interval Weeks → Weeks Between Triggers 1 → Trigger on Weekdays Saturday → Trigger at Hour 9am → Trigger at Minute 0. Den Knoten umbenennen in Samstag 09:00.
Weg B: In deinem Workflow den Knoten Von Hand starten durch einen Zeitplan-Knoten (Schedule Trigger) mit denselben Einstellungen ersetzen. Der Rest dieses Schritts betrifft nur A21.
Jetzt das Zeitfenster. Im Knoten Wochenmaterial holen ersetzt du die Zeile mit now() - interval '7 days' durch:
WHERE published_date >= date_trunc('day', now()) - interval '7 days'
AND published_date < date_trunc('day', now())
Was passiert da? date_trunc('day', now()) schneidet die Uhrzeit ab und landet auf Mitternacht des heutigen Tages. Von dort sieben Tage zurück ist der Anfang, Mitternacht heute ist das Ende. Egal ob der Lauf um 09:00 oder um 09:07 startet, die Woche ist immer dieselbe.
Bernd: „Material ist Material. Das Sammeln läuft doch sowieso jeden Morgen.“
Tanja: „Um vier Uhr, ja. Alles, was danach bis Freitagnacht reinkommt, fehlt dann aber noch.“
Das Material muss am Samstagmorgen auch vollständig sein. Deshalb bekommt der Sammel-Workflow A20 zusätzlich zum täglichen Lauf um 04:00 einen zweiten Zeitplan-Knoten samstags um 07:00, zwei Stunden vor dem Rückblick. Beide Zeitplan-Knoten hängen an denselben ersten Knoten. Stell dir zwei Wecker vor, die dieselbe Kaffeemaschine anwerfen.
Und die Zeitzone: n8n rechnet ab Werk in America/New_York. Ein Zeitplan-Knoten zeigt dann brav „9am“ an, meint aber neun Uhr in New York. Das ist eine dieser Einstellungen, die niemand sieht, bis der Podcast plötzlich am Nachmittag erscheint. Klickfolge je Workflow: oben rechts … → Settings → Timezone Europe/Berlin → Save. Dauerhaft löst man es an der Instanz mit GENERIC_TIMEZONE=Europe/Berlin in der Docker-Umgebung von n8n.
Schritt 7.2: Veröffentlichen ist nicht Speichern
Bernd: „Hab gespeichert, hab getestet, läuft. Wochenende.“
Tanja: „Und welche Fassung läuft morgen um neun?“
Bernd: „Na, die gespeicherte. Logisch.“
Leider nicht. Das ist die Falle, in die jeder genau einmal tritt. In n8n führt ein Klick auf Execute workflow die Fassung aus, die gerade im Editor steht. Ein planmäßiger Lauf führt dagegen die Fassung aus, die zuletzt veröffentlicht wurde. Wer nur speichert und von Hand testet, hat nie geprüft, was samstags um neun wirklich läuft.
Denk an eine Mannschaftsaufstellung: Was du im Training auf die Taktiktafel malst, ist die Editor-Fassung. Was beim Schiedsrichter auf dem Spielberichtsbogen steht, ist die veröffentlichte. Gespielt wird am Samstag nur, was auf dem Bogen steht.
Klickfolge: in A21 und A20 oben rechts Publish.
Erwartetes Ergebnis: Die Schaltfläche Publish ist ausgegraut, daneben steht „Saved“. Der Workflow ist aktiv.
Umgekehrt gilt auch: Veröffentlichen schaltet einen Workflow ein. Wer einen Workflow bearbeitet, der eigentlich ruhen soll, muss ihn nach dem Veröffentlichen wieder deaktivieren. Sonst weckt der Klick einen Spieler auf, der eigentlich auf der Bank sitzen sollte.
Schritt 7.3: Einmal von Hand durchlaufen lassen
Ulf: „Warum nicht einfach bis Samstag warten? Dann sehen wir ja, ob es klappt.“
Tanja: „Weil du im Pokalfinale keine neue Taktik ausprobierst. Generalprobe jetzt, dann ist Samstag Routine.“
Bevor der Samstag kommt, ein vollständiger Lauf von Hand: A21 (Weg A) beziehungsweise deinen eigenen Workflow (Weg B) öffnen → Execute workflow. Das kostet einen Aufruf bei Anthropic im Cent-Bereich und rund 17.000 Zeichen aus dem Google-Freikontingent. Also kaum etwas, und dafür weißt du danach wirklich Bescheid.
Dann heißt es warten. Hol dir einen Kaffee, der Lauf braucht seine Zeit.
Erwartetes Ergebnis nach acht bis neun Minuten:
- alle Knoten grün, im Strang von
EinstellungenbisPodcast: Player nachtragenjeweils ein Datensatz, - in WordPress unter Beiträge ein neuer Entwurf „KI Wochenrückblick …“,
- im Entwurf oben ein Abspieler, dann der Artikel, dann „Zum Hören und Mitlesen“ mit dem zweiten Abspieler und darunter „Transkript der Folge“ zum Aufklappen.
Und gleich noch ein zweiter Klick auf Execute workflow. Klingt nach Absicht zum Chaos, ist aber ein Test. Der zweite Lauf muss nach wenigen Sekunden bei Woche prüfen mit „Diese Woche gibt es schon als Beitrag …“ abbrechen. Dann weißt du, dass die Sicherung gegen Doppelentwürfe greift.

Der veröffentlichte Beitrag: unter dem Titel der Abspieler mit der Zeile „Diese Folge anhören“.

Das aufgeklappte Transkript im Beitrag: jeder Beitrag ein Absatz, davor fett „Sprecherin:“ oder „Sprecher:“.
Schritt 7.4: Hören, lesen, freigeben
Jetzt kommt der einzige Handgriff, der bleibt: den Entwurf lesen, die Folge anhören (zumindest anspielen) und auf Veröffentlichen klicken.
Bernd: „Den Klick kann man doch auch automatisieren. Dann muss ich gar nichts mehr machen.“
Tanja: „Könnte man. Dann veröffentlichst du aber auch die Woche, in der das Modell Unsinn schreibt, ungelesen unter deinem Namen.“
Die Freigabe ist also kein lästiger Rest, sondern der Punkt, an dem ein Mensch die Verantwortung übernimmt. Mit dem Klick ändert sich zweierlei. Der Beitrag steht auf der Website, und der Feed enthält die Folge. Als Veröffentlichungszeit erscheint in den Podcast-Apps der Zeitpunkt der Freigabe, nicht der des Laufs.
Vertrauen ist gut, nachmessen ist besser. Die erste Zeile zählt die Folgen im Feed, die zweite zeigt dir den Anhang mit der MP3.
Prüfen im Terminal:
curl -s https://www.deine-domain.de/feed/podcast/ | grep -c "<item>"
curl -s https://www.deine-domain.de/feed/podcast/ | grep -o '<enclosure[^>]*>'
Erwartetes Ergebnis: 1 und eine Zeile der Form
<enclosure url="https://www.deine-domain.de/wp-content/uploads/2026/09/2026-09-19_ki-wochenrueckblick-12-18-09-26.mp3" length="8952428" type="audio/mpeg"/>
Und dann die Probe, die Podcast-Apps beim Vorspulen machen: Lässt sich die Datei in Stücken abrufen? Eine App, die zu Minute zwölf springt, will nicht die ganze Datei, sondern nur das passende Stück. Die Adresse aus dem enclosure setzt du in den folgenden Befehl ein.
curl -s -o /dev/null -w "%{http_code}\n" -r 0-1023 "ADRESSE_AUS_DEM_ENCLOSURE"
Erwartetes Ergebnis: 206 („Partial Content“). Bei 200 liefert der Server immer die ganze Datei, dann springt das Vorspulen in manchen Apps an den Anfang zurück.

Der Feed im Browser: oben die Angaben zur Sendung, darunter die Folgen, jede mit ihrer enclosure.
Phase 8: Hinaus in die Verzeichnisse
Ulf: „Und jetzt? Findet mich jetzt jeder auf Spotify?“
Bernd: „Klar. Ist ja im Internet. Die Suchmaschinen finden das schon.“
Tanja: „Nein. Ein Feed ist wie ein Verein ohne Verbandsanmeldung. Er existiert, aber in keiner Tabelle.“
Der Feed steht, die erste Folge ist drin. Jetzt muss die Welt davon erfahren. Bernds Hoffnung, dass sich das von allein erledigt, trägt nicht: Einen Dienst, der einen selbst gehosteten Feed automatisch überall einträgt, gibt es nicht. Das ist der Preis der Unabhängigkeit, und er ist kleiner, als er aussieht: Fünf Anmeldungen genügen, weil viele Apps ihren Katalog von Apple oder vom Podcast Index beziehen. Overcast etwa sagt ausdrücklich, dass ein Podcast ein bis zwei Tage nach dem Apple-Eintrag dort auftaucht.
Bevor du loslegst, eine Regel, die du dir auf einen Zettel an den Monitor kleben solltest. Zwei Dinge dürfen sich ab jetzt nie mehr ändern: die Feed-Adresse (jedes Verzeichnis merkt sie sich) und die guid jeder Folge (die Kennung, an der Apps eine Folge wiedererkennen; im Snippet ist das dein Vorsatz $guid_vorsatz plus die Beitragsnummer). Ändert sich eine davon, erscheinen Folgen doppelt oder Abonnenten gehen verloren. Die Feed-Adresse ist deine Postanschrift, die guid die Rückennummer jeder Folge. Zieh um oder tausch Nummern, und niemand findet dich mehr.
Schritt 8.1: Apple Podcasts zuerst, und mit Geduld
Apple zuerst, weil andere Apps den Katalog von dort übernehmen. Die Anmeldung ist kostenlos, hat aber Hürden, die in keiner Anleitung in dieser Reihenfolge stehen. Ehrlich gesagt ist das der nervigste Teil der ganzen Phase. Hier ist der Weg, der tatsächlich funktioniert hat:
- Podcasts Connect hat keine Registrierung. Auf
podcastsconnect.apple.comgibt es nur „Anmelden“. Du brauchst zuerst einen Apple Account mit Zwei-Faktor-Anmeldung, angelegt unteraccount.apple.com. - „Apple Account aktivieren“, erste Ursache: Das Konto braucht eine hinterlegte Zahlungsmethode. Es wird nichts belastet, sie dient als Echtheitsnachweis. Klickfolge:
account.apple.com→ Zahlung und Versand → Zahlungsmethode hinzufügen. - „Apple Account aktivieren“, zweite Ursache: Ein Konto, das nur über die Website entstanden ist, hat den Bedingungen für Mediendienste nie zugestimmt. Lösung: einmal auf
podcasts.apple.comanmelden und die Bedingungen annehmen. Danach erscheint in Podcasts Connect „Apple Podcasts Connect beitreten“. Dieselbe Fehlermeldung hatte also zwei Ursachen. Nach der ersten Reparatur muss man erneut prüfen, nicht annehmen. - In Podcasts Connect eine neue Sendung anlegen und dabei die Variante mit einem vorhandenen RSS-Feed wählen → Feed-Adresse
https://www.deine-domain.de/feed/podcast/eintragen. (Apple benennt die Schaltflächen gelegentlich um. Maßgeblich ist der Weg: neue Sendung, per RSS-Feed.) - Apple übernimmt Titel, Bild, Beschreibung und Kategorie aus dem Feed. Ergänzen: Aktualisierungshäufigkeit wöchentlich, Inhaltsrechte „enthält keine Inhalte von Drittanbietern“ (das ist eine Rechtserklärung, die du selbst beurteilen musst), dann Veröffentlichen.
Bernd: „Fehlermeldung? Dann ist Apple überlastet. Einfach morgen noch mal.“
Tanja: „Oder dein Server antwortet auf HEAD falsch. Das kannst du in einer Minute messen, statt einen Tag zu warten.“
Bricht das Einreichen mit „Es ist ein Fehler aufgetreten“ ab, ist das mit großer Wahrscheinlichkeit die HEAD-Falle aus Schritt 4.6. Nicht warten, messen.
Erwartetes Ergebnis: Die Sendung steht in Podcasts Connect mit dem Status Verfügbar, jede Folge einzeln ebenfalls. Unter https://podcasts.apple.com/de/podcast/<name>/id<nummer> ist sie abrufbar.

Die Sendung in Podcasts Connect: Status „Verfügbar“, Feed-Adresse, Titelbild und Kategorie „Nachrichten, Neues aus der Technik“.
Abrufbar ist nicht auffindbar. Der KI Wochenrückblick war über seine Adresse sofort in Deutschland, Österreich, der Schweiz und den USA abrufbar, die Suche im deutschen Store fand ihn aber noch Tage später nicht. Verzeichniseintrag und Suchindex sind bei Apple zwei getrennte Dinge, der Index folgt mit Verzögerung. Das ist wie ein neuer Spieler, der schon im Kader steht, aber noch nicht im Stadionheft. Beides einzeln prüfen.
Schritt 8.2: Spotify
Ulf: „Spotify nimmt doch bestimmt das von Apple, oder?“
Tanja: „Nee, die spielen in einer eigenen Liga.“
Spotify übernimmt nichts von Apple und braucht eine eigene Einreichung. Die gute Nachricht: Sie geht schneller.
Ablauf: podcasters.spotify.com (Spotify for Creators) → anmelden → einen Podcast hinzufügen und dabei die Option für einen vorhandenen RSS-Feed wählen (nicht „neu bei Spotify erstellen“) → Feed-Adresse eintragen → Spotify schickt einen Bestätigungscode an die Adresse aus <itunes:email> → Code eingeben → Land, Sprache, Kategorie angeben → beim Hostinganbieter „Sonstiges“ → Einreichen.
Achtung: Diese Angaben lassen sich nach dem Einreichen nicht mehr ändern. Also kurz durchatmen und lieber zweimal lesen, bevor du klickst.
Erwartetes Ergebnis: eine Bestätigungsseite mit dem künftigen Link https://open.spotify.com/show/…. Nach einigen Stunden ist die Sendung dort abrufbar.

Die Sendung in Spotify for Creators mit der neuesten Folge.
Schritt 8.3: Die übrigen drei
Der Rest ist Fleißarbeit, aber schnell erledigt. Drei weitere Verzeichnisse, jeweils Feed eintragen und bestätigen:
| Verzeichnis | Weg | Was passiert |
|---|---|---|
| Amazon Music / Audible | „Amazon Music for Podcasters“ → Feed eintragen → Bestätigungsmail an die Inhaberadresse klicken (verfällt nach 24 Stunden) | Status zunächst „Ausstehend“, nach der Verarbeitung „Aktiv“ |
| Podcast Index | podcastindex.org → Feed einreichen | „Feed added successfully“, nach 15 bis 20 Minuten durchsuchbar. Aus diesem offenen Verzeichnis schöpfen viele neuere Apps. |
| podcast.de | Feed eintragen | Die Folgen erscheinen tröpfchenweise, als Autor zeigt podcast.de den Inhabernamen aus <itunes:owner> |
YouTube Music (über YouTube Studio) und Deezer sind möglich, aber zweite Reihe.
Phase 9: Feinschliff, der den Unterschied macht
Bernd: „Läuft doch. Wozu noch polieren?“
Tanja: „Weil ‚läuft‘ und ‚klingt nach richtigem Podcast‘ zwei verschiedene Tabellenplätze sind.“
Die folgenden Schritte sind optional. Der Podcast läuft auch ohne sie. Aber sie sind der Unterschied zwischen „funktioniert“ und „fühlt sich an wie ein richtiger Podcast“.
Ein Bild für jede Folge
Der Feed gibt jeder Folge das Beitragsbild des Rückblicks als eigenes Folgenbild mit, sofern es eines gibt. Apple verlangt dafür dieselben Vorgaben wie für das Sendungsbild: quadratisch, mindestens 1400 Pixel. Die 512 KB aus Phase 4 gelten auch hier als eigene Vorgabe dieses Aufbaus, nicht als Apple-Pflicht. Das Snippet legt dafür eine eigene Bildgröße podcast-folge mit 1400 × 1400 Pixeln an, die WordPress beim Hochladen automatisch herunterrechnet.
Ulf: „Und wenn mein Bild kleiner ist? Dann zieht WordPress das halt groß.“
Klingt logisch, ist aber falsch. Der Haken: WordPress rechnet nie hoch. Aus einem Beitragsbild mit 1024 Pixeln entsteht keine 1400er Fassung, sondern gar keine. Wer die Beitragsbilder wie beim KI Wochenrückblick mit einem Bildmodell erzeugt (die Anleitung zum KI-Newsroom zeigt das mit Flux), sollte sie deshalb in 1408 × 1408 Pixeln anfordern: das kleinste Vielfache von 16 über 1400.
Ohne Beitragsbild zeigen die Apps das Sendungsbild. Das ist keine Lücke, sondern die vorgesehene Rückfallebene. Wie das Vereinswappen auf dem Trikot, wenn kein Sponsor draufsteht.
Das Etikett in der Datei
Eine MP3 kann mehr als Ton. Sie trägt ein Etikett mit sich herum, so wie ein Aktenordner einen Rückenschild hat. app.py beschriftet jede MP3 mit Titel, Sendungsname und einem Kommentar mit dem Weg zurück zur Website. Übergibt man dem Dienst zusätzlich bild_url, landet auch ein Titelbild in der Datei. Apps wie Overcast zeigen das Bild aus der Datei statt aus dem Feed.
Eine Kuriosität dabei: ffmpeg schreibt einen „Kommentar“ nicht in das ID3-Feld, das Abspieler als Kommentar anzeigen, sondern in ein benutzerdefiniertes Feld, das viele gar nicht lesen. Das ist, als würdest du einen Zettel in die falsche Ablage legen: vorhanden, aber niemand schaut dort nach. Der Rückweg steht deshalb zusätzlich im Feld für den Untertitel (TIT3), das ffmpeg tatsächlich als solches schreibt. Und das Titelbild wird mit -c:v copy unverändert übernommen. Ohne diesen Schalter rechnete ffmpeg ein Bild von 489 KB auf 116 KB herunter, also ein zweites Mal komprimiert, ganz ohne Not.
Für ältere Folgen gibt es den Endpunkt POST /beschriften: Er nimmt eine fertige MP3 samt Bildadresse und Titel entgegen und beschriftet sie neu, ohne neu zu sprechen. Der Ton wird nur kopiert, es entstehen keine Kosten bei Google.
Die Abmoderation und die direkte Ansprache
Jede gute Sendung hat einen festen Abpfiff. Die zwei festen Schlusssätze aus Podcast: Dialog holen geben der Sendung einen Rahmen, den das Modell nicht jede Woche neu erfinden muss. Ein Modell, das den Abschied selbst schreibt, verabschiedet sich jede Woche anders, manchmal gar nicht, manchmal zweimal.
Bernd: „Dann schreib ich ins Prompt einfach ‚mindestens fünfmal du sagen‘. Das Modell macht, was man ihm sagt.“
Tanja: „Die Erfahrung sagt etwas anderes. Quotenregeln in Prompts tragen schlecht.“
Die Anweisung, die Hörer zwei- bis viermal mit „du“ anzusprechen, ist dagegen ein Versuch mit offenem Ausgang. Quotenregeln in Prompts tragen erfahrungsgemäß schlecht: Von der Vorgabe „mindestens zwei Rückfragen je Themenblock“ blieben in der Praxis 7 Prozent Beiträge mit Fragezeichen übrig. Bernds Vertrauen in die Zahl im Prompt ist also nicht gedeckt. Ob die direkte Ansprache besser trägt, muss man an den veröffentlichten Transkripten nachzählen.
Wartung: Was nach dem Bau noch anfällt
Bernd: „Und ab jetzt läuft das von allein. Nie wieder anfassen.“
Tanja: „Ein Auto fährt auch von allein, bis der Ölstand fehlt.“
Ein automatisierter Podcast ist kein Selbstläufer, sondern ein Selbstläufer mit Wartungsplan. Der Aufwand ist klein, aber er ist nicht null. Hier ist der Plan, sortiert nach Häufigkeit.
Jeden Samstag, nach neun Uhr (zehn Minuten):
- In n8n unter Executions nachsehen, ob A20 (07:00) und A21 (09:00) grün durchgelaufen sind. Achtung: „Grün“ heißt nur, dass nichts abgestürzt ist. Ein Sammellauf ohne Internet meldet ebenfalls Erfolg, holt aber null Quellen. Ein kurzer Blick in die Mail zeigt, ob der Rückblick normal lang ist.
- Den Entwurf lesen, die Folge anspielen, freigeben.
- Nach der Freigabe einmal den Feed abrufen (Schritt 7.4) und prüfen, ob die neue Folge drinsteht.
Wenn der Samstagslauf ausfällt: Ist er abgebrochen, bevor ein Entwurf entstand, erst A20 von Hand starten, damit das Material der letzten Stunden da ist, dann A21. Gibt es schon einen Entwurf oder eine MP3, gilt Schritt 6.12. Die Zeitfenster rechnen mit ganzen Kalendertagen und liefern deshalb auch am Samstagmittag noch dieselbe Woche. Keine Panik also, wenn du erst mittags merkst, dass etwas hängt.
Monatlich:
- Im Google-Konto unter Abrechnung einen Blick auf den Verbrauch werfen. Rund 87.000 Zeichen im Monat sind der Normalfall, eine Million ist frei.
- Den Speicherplatz im Auge behalten: Die Mediathek wächst um rund 480 MB im Jahr.
Wenn der Testzeitraum bei Google endet: Das Freikontingent von einer Million Zeichen im Monat bleibt, es gehört nicht zum Testzeitraum. Prüfe trotzdem, ob die Budgetwarnung aus Schritt 1.5 nach dem Umstieg auf ein reguläres Konto noch richtig eingestellt ist.
Nach Updates: Das Abbild vertonung:1 bleibt so, wie es gebaut wurde, auch nach einem Neustart der NAS. Ein Neustart frischt also nichts auf. Wer ffmpeg und die Systempakete alle paar Monate auffrischen will, baut es neu: Projekt vertonung Beenden → unter Image das Abbild vertonung:1 löschen → Projekt wieder Erstellen. Fehlt das Abbild, baut der Container Manager es aus dem Dockerfile neu. Danach die Gesundheitsprüfung aus Schritt 3.4 wiederholen. Eine neue Python-Fassung kommt nur, wenn du die Zeile FROM im Dockerfile änderst.
Wenn eine Folge nachträglich geändert werden muss: Eine neu hochgeladene MP3 bekommt in WordPress eine neue Adresse und eine neue Mediennummer. Im Beitrag müssen dann beide Abspieler auf die neue Datei zeigen, einschließlich der Nummer im Blockkommentar <!-- wp:audio {"id":…} -->, und das Feld podcast_url muss die neue Adresse bekommen. Die guid der Folge bleibt, wie sie ist, die hängt an der Beitragsnummer. Die alte Datei in der Mediathek wird danach nicht mehr gebraucht. Achtung: Die WordPress-Mediathek hat keinen Papierkorb, gelöscht ist gelöscht.
Troubleshooting: Was schiefgehen kann und schon schiefgegangen ist
Ulf: „Sind das alles ausgedachte Fehler, damit es schlau aussieht?“
Tanja: „Nein. Jeder davon ist beim Bau wirklich passiert.“
Jede Zeile dieser Tabelle ist beim Bau des KI Wochenrückblicks tatsächlich passiert. Such dein Symptom in der linken Spalte, lies die Ursache, wende die Lösung an.
| Symptom | Ursache | Lösung |
|---|---|---|
Protokoll des Containers zeigt Schluessel FEHLT | schluessel.env erst nach dem ersten Start angelegt oder geändert; Docker liest sie nur beim Erzeugen des Containers | Projekt Beenden, dann Erstellen (Neuaufbau), nicht nur neu starten |
| Container-Start meldet „PIDs limit discarded“ | Kernel der DiskStation kennt die Begrenzung nicht | Kein Fehler. Die Zeile wirkt hier nur nicht. |
n8n erreicht http://vertonung:8080 nicht | Dienst hängt in einem anderen Docker-Netz als n8n | Netznamen prüfen (Schritt 3.1), unten in compose.yaml eintragen, Projekt neu erstellen |
DSM zeigt den Container als <zeichenfolge>_vertonung | Zwischenname, den DSM in der Anzeige behält | Nichts tun, der Dienst ist unter vertonung erreichbar |
/vertonen antwortet 400 mit einer Begründung | Auftrag über einer der Grenzen (Zeichen, Beiträge, Länge je Beitrag) | Die Begründung lesen. Meist ist das Gespräch ungewöhnlich lang geraten, dann den Lauf von Hand wiederholen. |
Dienst antwortete 502 „100 Teile, 149 Beitraege“ | Alte Fassung mit zweistelligen Dateinamen; das 100. Tonstück hieß teil100.mp3 und passte nicht ins Muster, außerdem sortierte „teil100″ alphabetisch vor „teil99″ | In der Fassung dieses Artikels behoben: vierstellige Namen, Sortierung nach Zahl |
MP3-Upload antwortet 400 – Keine Daten bereitgestellt | Schalter Send Body im Upload-Knoten aus | Einschalten, Body Content Type „n8n Binary File“, Feld data |
Zusatzfelder bleiben leer, obwohl WordPress 200 meldet | Felder ohne show_in_rest registriert oder Snippet nicht aktiv | Snippet aktivieren, dann den Lauf wiederholen |
/feed/podcast/ liefert 404 | WordPress kennt die neue Adresse noch nicht | Einstellungen → Permalinks → Änderungen speichern |
| Apps zeigen wochenlang dieselbe Folge | Cache-Plugin liefert einen alten Stand des Feeds aus | Ausnahme feed/podcast eintragen (Schritt 4.5), Vergleich mit Zufallsanhang |
| Apple: „Es ist ein Fehler aufgetreten“ beim Einreichen | HEAD-Anfrage liefert application/octet-stream | Filter feed_content_type im Snippet (ist enthalten); mit curl -sI prüfen |
| „Apple Account aktivieren“ trotz Anmeldung | keine Zahlungsmethode hinterlegt oder Mediendienst-Bedingungen nie angenommen | beides nacheinander beheben, nach jeder Reparatur erneut prüfen |
| Sendung bei Apple per Link abrufbar, aber nicht über die Suche | Suchindex folgt mit Tagen Verzögerung | abwarten; getrennt prüfen |
Gemini-TTS meldet RESOURCE_EXHAUSTED … prepayment credits are depleted | Projekt mit Rechnungskonto fällt in Tier 1, verlangt 5 Dollar Vorauszahlung; Testguthaben zählt nicht | Chirp 3 HD über die Text-to-Speech-API verwenden, wie in dieser Anleitung |
| Browser zeigt 48.000 Hz, die Datei soll 24.000 Hz haben | Der Browser rechnet beim Abspielen hoch | mit ffprobe an der heruntergeladenen Datei messen |
Samstagslauf bricht nach 15 Sekunden mit EAI_AGAIN ab | Internet- oder DNS-Ausfall an der NAS | A20, dann A21 von Hand nachholen; Wiederholungen am Claude-Knoten einstellen |
Woche prüfen meldet „Diese Woche gibt es schon als Beitrag …“ | derselbe Wochenlauf wurde ein zweites Mal gestartet | Kein Fehler, sondern die Sicherung. Den ersten Lauf mit Retry fortsetzen (Schritt 6.12) |
| Nach Retry rote Meldung „Problem with retry – Converting circular structure to JSON“ | Anzeigefehler in n8n 2.1.5 | In der Liste der Ausführungen nachsehen: Steht dort „Succeeded“ und „Retry of execution …“, ist alles gut |
| Zeitplan läuft sechs Stunden zu spät | n8n rechnet ab Werk in America/New_York | Zeitzone je Workflow auf Europe/Berlin, dauerhaft GENERIC_TIMEZONE |
| Änderung am Workflow wirkt im Samstagslauf nicht | nur gespeichert, nicht veröffentlicht | Publish klicken |
| Gespräch klingt wie zwei Menschen, die abwechselnd vorlesen | Prompt verlangt „inhaltlich unverändert“ und lange Beiträge | Prompt aus Phase 5 verwenden: erzählen statt umschreiben, kurze Beiträge, Rückfragen |
| Stimme spricht „ku-enstlich“ | Modell schreibt Umlaute als ae/oe/ue | Regel „echte Umlaute“ im Prompt |
| Falsches Tagesdatum im Gespräch („am 26. August“) | maschinelle Transkripte kürzen Jahreszahlen, das Modell macht einen Tag daraus | keine Tagesdaten im Prompt erlauben |
| Nach einem Neuaufbau fehlen Texte vergangener Läufe | n8n löscht Ausführungsdaten nach 336 Stunden | Skript im Zusatzfeld podcast_skript sichern (ist eingebaut) |
| Folgenbild bleibt beim Sendungsbild hängen | Beitragsbild kleiner als 1400 Pixel; WordPress rechnet nicht hoch | Beitragsbilder mit 1408 × 1408 Pixeln erzeugen |
| Mediathek zeigt plötzlich Bilder „(kein Titel)“ | WordPress packt das in die MP3 eingebettete Titelbild beim Upload als eigenen Anhang aus | harmlos, bei Bedarf in der Mediathek entfernen |
Was am Ende dasteht und was offen bleibt
Rechnen wir zusammen. Eine NAS, die ohnehin läuft. Ein Container mit rund 330 Zeilen Python und 266 Zeilen Tonmontage. Ein WordPress-Snippet von gut 300 Zeilen. Zwölf Knoten in n8n, den Eingangsknoten Zusammenfassung bauen mitgezählt. Fünf Verzeichniseinträge. Laufende Kosten: null Euro für die Stimmen, einige Cent pro Woche für das Sprachmodell. Heraus kommt jeden Samstag eine Folge von achtzehn bis zwanzig Minuten, mit zwei unterscheidbaren Stimmen, natürlichen Pausen, sauberer Lautheit, einem Transkript, einem eigenen Feed und einem Eintrag bei Apple, Spotify und Amazon.
Vor wenigen Jahren wäre das ein Projekt für ein kleines Team gewesen. Heute ist es ein Projekt für zwei Nachmittage und etwas Geduld mit Fehlermeldungen.
Das eigentlich Interessante ist aber nicht die Technik. Es ist die Frage, die sich beim Hören der ersten Folgen aufdrängt: Was genau hört man da eigentlich? Die Fakten stammen aus Fachpresse, Podcasts und Blogs, die Menschen geschrieben und gesprochen haben. Die Auswahl und Einordnung hat ein Sprachmodell getroffen, geprüft und freigegeben von einem Menschen. Das Gespräch ist inszeniert, die Stimmen sind synthetisch, die Pausen berechnet, sogar das leise Rauschen darunter ist künstlich, damit es natürlicher klingt.
Der Podcast sagt das alles offen, im ersten Satz jeder Folge und in jeder Beschreibung. Und trotzdem bleibt ein merkwürdiges Gefühl, wenn zwei Stimmen, die es nicht gibt, sich über eine Woche unterhalten, die sie nicht erlebt haben, und man dabei ertappt, dass man ihnen gern zuhört.
Vielleicht ist das die eigentliche Frage, die solche Projekte stellen. Nicht, ob Maschinen Podcasts machen können. Das können sie offensichtlich. Sondern: Wenn Einordnung zum Nebenprodukt eines Workflows wird, der samstags um neun Uhr startet, wessen Urteil hören wir dann eigentlich, und wie lange bleibt der eine Klick auf „Veröffentlichen“ mehr als eine Formalität?
Samstag, kurz nach neun. Im Büro riecht es nach Kaffee, auf Tanjas Bildschirm liegt der frische Entwurf.
Bernd: „Und? Kann ich jetzt einfach auf Veröffentlichen drücken?“
Tanja: „Kannst du. Nachdem du ihn gelesen hast.“
Ulf: „Ich hör schon mal rein. Die zwei klingen echt wie Kommentatoren. Nur ohne Abseitsdiskussion.“
Tanja: „Dann hör genau hin. Das Urteil, das da drinsteckt, gibst du mit deinem Klick frei.“
FOUNDIC.org ist werbefrei und ohne Bezahlschranke. Wenn dir diese Anleitung geholfen hat:
Lade uns auf einen Kaffee ein
