Fix podcasts generating in English + failing with invalid JSON

Two root causes, both fixed in the bind-mounted podcast prompt templates
(prompts/podcast/*.jinja):

1. Language: the episode profile's `language` field IS passed to both
   podcast_creator templates as {{ language }}, but the stock templates never
   reference it — so podcasts came out English (driven only by the English
   stock briefings/speakers), then read aloud by the German-locked Chatterbox
   TTS = "English with a German accent". Added a CRITICAL LANGUAGE REQUIREMENT
   block keyed on {{ language }} to both templates; now `language: "de"`
   actually forces German. Verified end-to-end: a full run produced a 44-line
   all-German transcript + audio.

2. Invalid json output failures: qwen3.5:27b is a thinking model; on long
   segments the <think> block ate the response-token budget and truncated the
   JSON. Prepended /no_think to both templates (~3x faster, valid JSON).

Templates are bind-mounted read-only via docker-compose (directory mount, so
edits survive a container restart without inode-staleness). Bundled briefings
and speaker backstories were also translated to German to reduce drift.

Known limitation documented: feeding an entire book (~70k tokens) as podcast
content makes each of the 6 LLM calls take ~3.5 min and is unreliable; use a
shorter source or summary. Confirmed working with concise content.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-07-11 02:30:05 +02:00
commit 054f867a8a
6 changed files with 304 additions and 18 deletions

View file

@ -379,21 +379,36 @@ Das Episode- oder Sprecherprofil verweist auf ein Modell ohne konfiguriertes Cre
`PUT /api/speaker-profiles/{id}` (`voice_model`) auf registrierte lokale/OpenRouter-Modelle `PUT /api/speaker-profiles/{id}` (`voice_model`) auf registrierte lokale/OpenRouter-Modelle
umstellen — siehe `scripts/setup_models.sh` für die IDs der Standardmodelle. umstellen — siehe `scripts/setup_models.sh` für die IDs der Standardmodelle.
### Podcast wird auf Englisch statt Deutsch erzeugt ### Podcast wird auf Englisch statt Deutsch erzeugt (oder auf Englisch mit deutschem Akzent vorgelesen)
Das verwendete Episode-Profil hat kein `language`-Feld gesetzt — ohne Angabe generiert das Das ist behoben: Die Podcast-Prompt-Vorlagen (`prompts/podcast/outline.jinja` und
Sprachmodell standardmäßig auf Englisch, unabhängig von der Sprache der Quelle. Prüfen und `transcript.jinja` in diesem Projekt) wurden um eine explizite Sprachanweisung ergänzt, die das
korrigieren: `language`-Feld des Episode-Profils auswertet — sie werden per Bind-Mount aus `docker-compose.yml`
in den Container eingehängt. Setzt du am Episode-Profil `language: "de"`, kommt der Podcast auf
Deutsch. (Vorher hatte das `language`-Feld keine Wirkung, weil die mitgelieferten Vorlagen es
ignorierten — daher englischer Text, der von der deutsch-fixierten TTS mit Akzent vorgelesen wurde.)
Falls doch wieder Englisch erscheint, prüfe das `language`-Feld:
```bash ```bash
curl -s http://127.0.0.1:5055/api/episode-profiles | python3 -c \ curl -s http://127.0.0.1:5055/api/episode-profiles | python3 -c \
"import json,sys; [print(p['name'], p['language']) for p in json.load(sys.stdin)]" "import json,sys; [print(p['name'], p['language']) for p in json.load(sys.stdin)]"
``` ```
Fehlt `de`, per `PUT /api/episode-profiles/{id}` (vollständiger Body nötig, siehe oben) Fehlt `de`/`de-DE`, per `PUT /api/episode-profiles/{id}` (vollständiger Body) ergänzen. Zusätzlich
`"language": "de"` ergänzen. Die drei mitgelieferten Profile (`business_analysis`, sind Briefing und Sprecherbeschreibungen der mitgelieferten Profile auf Deutsch übersetzt (hilft
`solo_expert`, `tech_discussion`) sind bereits korrigiert; bei neu angelegten eigenen Profilen gegen Sprach-Drift, wenn der Quellinhalt englisch ist). **Wichtig:** Änderst du die
selbst daran denken. Vorlagen-Dateien selbst, ist danach ein Container-Neustart nötig (`docker compose up -d
--force-recreate open_notebook`), damit der Bind-Mount die neue Version übernimmt.
### Podcast-Erzeugung schlägt mit „Invalid json output" fehl
Das Textmodell hat ungültiges JSON geliefert — passierte bei `qwen3.5:27b` (einem „denkenden"
Modell), wenn der gesamte Notebook-Inhalt (bei einem ganzen Buch ~70.000 Tokens) in jeden
Transkript-Abschnitt gepackt wird: Der Denk-Block frisst dann das Antwort-Budget auf und das JSON
wird abgeschnitten. Behoben durch `/no_think` als erste Zeile beider Podcast-Vorlagen (schaltet
den Denkmodus ab, ~3× schneller, volles Budget fürs JSON). Falls es dennoch auftritt: kürzeren
Quellinhalt verwenden (nicht das ganze Buch) oder die Generierung erneut starten.
### Weitere technische Details ### Weitere technische Details

View file

@ -41,13 +41,38 @@ A separate local `llama.cpp` server exists on this machine (managed via `~/llama
**Chat model context window:** the Ollama credential used for `qwen3.5:27b` (`credential:di8b31l16zikyyb6isyi`) has `num_ctx=98304` set explicitly. Without this, esperanto's `OllamaLanguageModel` silently defaults `num_ctx` to **8192** regardless of the model's real capability or `OLLAMA_CONTEXT_LENGTH` — any chat context (e.g. "full content" mode on a real document) beyond that gets truncated by Ollama, and because `qwen3.5:27b` is a thinking model, the truncated prompt frequently produces an empty final answer (the response is silently swallowed by `clean_thinking_content`) instead of a visible error — looks exactly like "chat gives no answer" from the UI. `98304` is the largest value that still keeps the whole model on GPU 1 (`ollama ps` shows `100% GPU`); `131072` spills ~91% onto CPU and makes a single response take 5-6 minutes instead of ~1-2. Even at 98304, a full-book "Volltext" chat (~70k+ tokens of context) takes ~2-3 minutes — that's inherent to processing that much context on one consumer GPU, not a bug. For large documents, the default short/RAG context mode (only relevant chunks, not the whole document) stays fast; reserve "Volltext" for shorter sources. If `qwen3.5:27b` ever gets swapped for a different Ollama chat model, re-check this credential's `num_ctx` still makes sense for its VRAM footprint. **Chat model context window:** the Ollama credential used for `qwen3.5:27b` (`credential:di8b31l16zikyyb6isyi`) has `num_ctx=98304` set explicitly. Without this, esperanto's `OllamaLanguageModel` silently defaults `num_ctx` to **8192** regardless of the model's real capability or `OLLAMA_CONTEXT_LENGTH` — any chat context (e.g. "full content" mode on a real document) beyond that gets truncated by Ollama, and because `qwen3.5:27b` is a thinking model, the truncated prompt frequently produces an empty final answer (the response is silently swallowed by `clean_thinking_content`) instead of a visible error — looks exactly like "chat gives no answer" from the UI. `98304` is the largest value that still keeps the whole model on GPU 1 (`ollama ps` shows `100% GPU`); `131072` spills ~91% onto CPU and makes a single response take 5-6 minutes instead of ~1-2. Even at 98304, a full-book "Volltext" chat (~70k+ tokens of context) takes ~2-3 minutes — that's inherent to processing that much context on one consumer GPU, not a bug. For large documents, the default short/RAG context mode (only relevant chunks, not the whole document) stays fast; reserve "Volltext" for shorter sources. If `qwen3.5:27b` ever gets swapped for a different Ollama chat model, re-check this credential's `num_ctx` still makes sense for its VRAM footprint.
**Podcast language:** the three bundled episode profiles (`business_analysis`, `solo_expert`, **Podcast language + reliability — fixed via patched prompt templates (`prompts/podcast/`,
`tech_discussion`) ship with `language: null`, which the underlying `podcast_creator` library bind-mounted).** Two separate problems, one fix location:
treats as "no language specified" and defaults to English for both outline and transcript
generation — regardless of what language the source content or the briefing text is in. All 1. *Language.* The episode profile's `language` field (`"de"` → resolved to `"German"`) **is**
three are set to `language: "de"` here. If you create a new episode profile, set `language` passed into both `podcast_creator` prompt templates as a `{{ language }}` variable
explicitly (`PUT /api/episode-profiles/{id}`, full body) — it doesn't inherit from the notebook (`nodes.py`, both `generate_outline_node` and `generate_transcript_node`), but the stock
or the source. templates shipped in the image never reference it — so setting `language` had no effect and
podcasts came out in English (the stock briefings and speaker backstories are English, which
is the only language signal the model then sees). English text read aloud by the German-locked
Chatterbox TTS is what produced the "English with a bad German accent" symptom.
2. *Reliability.* `qwen3.5:27b` is a thinking model. Podcast content is the **entire notebook**
(`Notebook.get_context()` → every source's full text + all insights; for a whole book that's
~70k+ tokens), fed into *each* transcript-segment call. On long segments the `<think>` block
eats the 5000-token response budget and the JSON gets truncated → `Invalid json output`
whole job fails. (Confirmed: `/no_think` cut a segment from 98s to 34s and left valid JSON.)
Fix — both handled in the two templates under `prompts/podcast/`, bind-mounted read-only over
the image's copies via `docker-compose.yml`:
- Prepended `/no_think` as the first line of both templates (Qwen honors it anywhere; harmless
text for non-Qwen models) — disables thinking, freeing the token budget and ~3x speedup.
- Added a `{% if language %}CRITICAL LANGUAGE REQUIREMENT … in {{ language }}{% endif %}` block
near the end of both templates, so the already-plumbed `language` field now actually forces the
output language.
The bundled briefings and speaker backstory/personality were **also** translated to German (belt
and suspenders; reduces English drift from the source content), but the template change is what
makes `language` authoritative. **Editing these templates requires a container restart** to take
effect — they're bind-mounted, and the mount is the directory `prompts/podcast/` (not individual
files, which go stale on inode-replacing edits). If you switch off `qwen3.5:27b`, the `/no_think`
line becomes inert but harmless.
If you create your own episode profile, just set `language` — the template handles the rest.
### Text-to-speech / speech-to-text ### Text-to-speech / speech-to-text

View file

@ -204,6 +204,11 @@ Kurzreferenz — Details jeweils in [`CLAUDE.md`](CLAUDE.md):
leere Chat-Antworten bei größerem Kontext, ohne sichtbaren Fehler. leere Chat-Antworten bei größerem Kontext, ohne sichtbaren Fehler.
- **Neue Host-Ports brauchen eine `ufw`-Regel**, sonst kann der Container sie nicht erreichen - **Neue Host-Ports brauchen eine `ufw`-Regel**, sonst kann der Container sie nicht erreichen
(Timeout, keine Fehlermeldung in Open Notebook selbst). (Timeout, keine Fehlermeldung in Open Notebook selbst).
- **Podcasts werden ohne explizites `language`-Feld auf Englisch erzeugt**, unabhängig von der - **Podcast-Sprache** wird über das `language`-Feld des Episode-Profils gesteuert, aber nur
Sprache der Quelle. Die drei mitgelieferten Episode-Profile sind bereits auf `"de"` gesetzt; dank der gepatchten Prompt-Vorlagen unter `prompts/podcast/` (per Bind-Mount in
bei selbst angelegten Profilen selbst daran denken. `docker-compose.yml` eingehängt) — die Original-Vorlagen im Image ignorieren `language`, was
englische Podcasts (von der deutsch-fixierten TTS mit Akzent vorgelesen) verursachte. Die
Vorlagen enthalten zusätzlich `/no_think`, damit `qwen3.5:27b` bei langem Quellinhalt nicht
durch überlange Reasoning-Blöcke ungültiges JSON liefert. Änderst du die Vorlagen, danach
`docker compose up -d --force-recreate open_notebook`. Details:
[`BEDIENUNGSANLEITUNG.md`](BEDIENUNGSANLEITUNG.md#10-troubleshooting) und [`CLAUDE.md`](CLAUDE.md).

View file

@ -34,3 +34,11 @@ services:
- "host.docker.internal:host-gateway" - "host.docker.internal:host-gateway"
volumes: volumes:
- ./notebook_data:/app/data - ./notebook_data:/app/data
# Podcast-Prompt-Vorlagen mit expliziter Sprachanweisung ({{ language }})
# und abgeschaltetem Thinking-Modus (/no_think), damit Podcasts in der
# eingestellten Sprache erzeugt werden (statt Englisch) und das JSON bei
# langen Segmenten nicht durch riesige Reasoning-Blöcke abgeschnitten wird.
# Verzeichnis-Mount (nicht Einzeldateien), damit Edits ohne Inode-Probleme
# nach einem Container-Neustart greifen. Der Ordner im Image enthält nur
# genau diese zwei Vorlagen.
- ./prompts/podcast:/app/prompts/podcast:ro

View file

@ -0,0 +1,91 @@
/no_think
You are an AI assistant specialized in creating podcast outlines. Your task is to create a detailed outline for a podcast episode based on a provided briefing. The outline you create will be used to generate the podcast transcript.
Here is the briefing for the podcast episode:
<briefing>
{{ briefing }}
</briefing>
The user has provided content to be used as the context for this podcast episode:
<context>
{% if context is string %}
{{ context }}
{% else %}
{% for item in context %}
<content_piece>
{{ item }}
</content_piece>
{% endfor %}
{% endif %}
</context>
The podcast will feature the following speakers:
<speakers>
{% for speaker in speakers %}
- **{{ speaker.name }}**: {{ speaker.backstory }}
Personality: {{ speaker.personality }}
{% endfor %}
</speakers>
Please create an outline based on this briefing. Your outline should consist of {{ num_segments }} main segments for the podcast episode, along with a description of each segment. Follow these guidelines:
1. Read the briefing carefully and identify the main topics and themes.
2. Create {{ num_segments }} distinct segments that cover the entire scope of the briefing.
3. For each segment, provide a clear and concise name that reflects its content.
4. Write a detailed description for each segment, explaining what will be discussed and provide suggestions of topics according to the context given. The writer will use your suggestion to design the dialogs.
5. Consider the speaker personalities and backstories when planning segments - match content to speaker expertise.
6. Ensure that the segments flow logically from one to the next.
7. This is a whole podcast so no need to reintroduce speakers or topics on each segment. Segments are just markers for us to know to change the topics, nothing else.
8. Include an introduction segment at the beginning and a conclusion or wrap-up segment at the end.
Format your outline using the following structure:
```json
{
"segments": [
{
"name": "[Segment Name]",
"description": "[Description of the segment content]",
"size": "short"
},
{
"name": "[Segment Name]",
"description": "[Description of the segment content]",
"size": "medium"
},
{
"name": "[Segment Name]",
"description": "[Description of the segment content]",
"size": "long"
},
...
]
}
```
Formatting instructions:
{{ format_instructions}}
Additional tips:
- Make sure the segment names are catchy and informative.
- In the descriptions, include key points or questions that will be addressed in each segment.
- Consider the target audience mentioned in the briefing when crafting your outline.
- If the briefing mentions a guest, include segments for introducing the guest and featuring their expertise.
- The size of the segment should be short, medium or long. Think about the content of the segment and how important it is to the episode.
{% if language %}
CRITICAL LANGUAGE REQUIREMENT:
- Write the ENTIRE outline — every segment name and every segment description — exclusively in {{ language }}.
- Do NOT use English or any other language, not even for technical terms or proper concepts where a {{ language }} wording exists.
- These instructions are written in English, but your output MUST be in {{ language }}.
{% endif %}
IMPORTANT OUTPUT FORMAT:
- If you use extended thinking with <think> tags, put ALL your reasoning inside <think></think> tags
- Put the final JSON output OUTSIDE and AFTER any <think> tags
- Do NOT wrap the JSON in ```json code blocks - return the raw JSON object only
- Example correct format:
<think>Let me analyze the briefing...</think>
{"segments": [...]}
Please provide your outline now, following the format and guidelines provided above.

View file

@ -0,0 +1,142 @@
/no_think
You are an AI assistant specialized in creating podcast transcripts.
Your task is to generate a transcript for a specific segment of a podcast episode based on a provided briefing and outline.
The transcript will be used to generate podcast audio. Follow these instructions carefully:
First, review the briefing for the podcast episode:
<briefing>
{{ briefing }}
</briefing>
The user has provided content to be used as the context for this podcast episode:
<context>
{% if context is string %}
{{ context }}
{% else %}
{% for item in context %}
<content_piece>
{{ item }}
</content_piece>
{% endfor %}
{% endif %}
</context>
The podcast features the following speakers:
<speakers>
{% for speaker in speakers %}
- **{{ speaker.name }}**: {{ speaker.backstory }}
Personality: {{ speaker.personality }}
{% endfor %}
</speakers>
Next, examine the outline produced by our director:
<outline>
{{ outline }}
</outline>
{% if transcript %}
Here is the current transcript so far:
<transcript>
{{ transcript }}
</transcript>
{% endif %}
{% if is_final %}
{% if speakers|length == 1 %}
This is the final segment of the podcast. Make sure to wrap up the presentation and provide a conclusion.
{% else %}
This is the final segment of the podcast. Make sure to wrap up the conversation and provide a conclusion.
{% endif %}
{% endif %}
You will focus on creating the dialogue for the following segment ONLY:
<segment>
{{ segment }}
</segment>
{% if speakers|length == 1 %}
IMPORTANT: This is a SOLO podcast with only ONE speaker ({{ speaker_names[0] }}). Do NOT invent or add any other speakers.
All dialogue entries must use "{{ speaker_names[0] }}" as the speaker name.
Follow these format requirements strictly:
- Use ONLY the speaker name "{{ speaker_names[0] }}" for all dialogue entries.
- Do NOT create or invent any additional speakers.
- Stick to the segment, do not go further than what's requested. Other agents will do the rest of the podcast.
- The transcript must have at least {{ turns }} dialogue segments from the speaker.
- The speaker should present the content in an engaging, educational manner.
{% else %}
Follow these format requirements strictly:
- Use the actual speaker names ({{ speaker_names|join(', ') }}) to denote speakers.
- Choose which speaker should speak based on their personality, backstory, and the content being discussed.
- Stick to the segment, do not go further than what's requested. Other agents will do the rest of the podcast.
- The transcript must have at least {{ turns }} turns of messages between the speakers.
- Each speaker should contribute meaningfully based on their expertise and personality.
{% endif %}
```json
{
"transcript": [
{
"speaker": "[Actual Speaker Name]",
"dialogue": "[Speaker's dialogue based on their personality and expertise]"
},
...
]
}
```
Formatting instructions:
{{ format_instructions}}
{% if speakers|length == 1 %}
Guidelines for creating the transcript:
- Ensure the presentation flows naturally and covers all points in the outline.
- Ensure you return the root "transcript" key in your response.
- Make the content sound engaging and educational.
- Include relevant details from the briefing.
- Break up the content into digestible segments with natural transitions.
- Use appropriate transitions between topics.
- Match the speaker's dialogue to their personality and expertise.
- This is a whole podcast so no need to reintroduce the speaker or topics on each segment. Segments are just markers for us to know to change the topics, nothing else.
- CRITICAL: There is only ONE speaker. Use ONLY: {{ speaker_names[0] }}. Do NOT invent additional speakers.
{% else %}
Guidelines for creating the transcript:
- Ensure the conversation flows naturally and covers all points in the outline.
- Ensure you return the root "transcript" key in your response.
- Make the dialogue sound conversational and engaging.
- Include relevant details from the briefing.
- Avoid long monologues; keep exchanges between speakers balanced.
- Use appropriate transitions between topics.
- Match each speaker's dialogue to their personality and expertise.
- Choose speakers strategically based on who would naturally contribute to each topic.
- This is a whole podcast so no need to reintroduce speakers or topics on each segment. Segments are just markers for us to know to change the topics, nothing else.
- IMPORTANT: Only use the provided speaker names: {{ speaker_names|join(', ') }}
{% endif %}
{% if language %}
CRITICAL LANGUAGE REQUIREMENT:
- Write ALL dialogue lines exclusively in {{ language }}.
- Do NOT use English or any other language, not even for technical terms or proper concepts where a {{ language }} wording exists.
- These instructions are written in English, but every spoken line you produce MUST be in {{ language }}.
{% endif %}
IMPORTANT OUTPUT FORMAT:
- If you use extended thinking with <think> tags, put ALL your reasoning inside <think></think> tags
- Put the final JSON output OUTSIDE and AFTER any <think> tags
- Do NOT wrap the JSON in ```json code blocks - return the raw JSON object only
- Example correct format:
<think>Let me plan the dialogue...</think>
{"transcript": [...]}
When you're ready, provide the transcript.
{% if speakers|length == 1 %}
Remember, you are creating a realistic solo podcast presentation based on the given information.
Make it informative, engaging, and natural-sounding while adhering to the format requirements.
There is only ONE speaker - do not add any other speakers.
{% else %}
Remember, you are creating a realistic podcast conversation based on the given information.
Make it informative, engaging, and natural-sounding while adhering to the format requirements.
{% endif %}