From 054f867a8af4a744839469571cdfc25a308bf7e6 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Sat, 11 Jul 2026 02:30:05 +0200 Subject: [PATCH] Fix podcasts generating in English + failing with invalid JSON MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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 --- BEDIENUNGSANLEITUNG.md | 31 +++++-- CLAUDE.md | 39 +++++++-- README.md | 11 ++- docker-compose.yml | 8 ++ prompts/podcast/outline.jinja | 91 ++++++++++++++++++++ prompts/podcast/transcript.jinja | 142 +++++++++++++++++++++++++++++++ 6 files changed, 304 insertions(+), 18 deletions(-) create mode 100644 prompts/podcast/outline.jinja create mode 100644 prompts/podcast/transcript.jinja diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index f9d4bf7..b887ba0 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -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 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 -Sprachmodell standardmäßig auf Englisch, unabhängig von der Sprache der Quelle. Prüfen und -korrigieren: +Das ist behoben: Die Podcast-Prompt-Vorlagen (`prompts/podcast/outline.jinja` und +`transcript.jinja` in diesem Projekt) wurden um eine explizite Sprachanweisung ergänzt, die das +`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 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)]" ``` -Fehlt `de`, per `PUT /api/episode-profiles/{id}` (vollständiger Body nötig, siehe oben) -`"language": "de"` ergänzen. Die drei mitgelieferten Profile (`business_analysis`, -`solo_expert`, `tech_discussion`) sind bereits korrigiert; bei neu angelegten eigenen Profilen -selbst daran denken. +Fehlt `de`/`de-DE`, per `PUT /api/episode-profiles/{id}` (vollständiger Body) ergänzen. Zusätzlich +sind Briefing und Sprecherbeschreibungen der mitgelieferten Profile auf Deutsch übersetzt (hilft +gegen Sprach-Drift, wenn der Quellinhalt englisch ist). **Wichtig:** Änderst du die +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 diff --git a/CLAUDE.md b/CLAUDE.md index 59f075c..53af565 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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. -**Podcast language:** the three bundled episode profiles (`business_analysis`, `solo_expert`, -`tech_discussion`) ship with `language: null`, which the underlying `podcast_creator` library -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 -three are set to `language: "de"` here. If you create a new episode profile, set `language` -explicitly (`PUT /api/episode-profiles/{id}`, full body) — it doesn't inherit from the notebook -or the source. +**Podcast language + reliability — fixed via patched prompt templates (`prompts/podcast/`, +bind-mounted).** Two separate problems, one fix location: + +1. *Language.* The episode profile's `language` field (`"de"` → resolved to `"German"`) **is** + passed into both `podcast_creator` prompt templates as a `{{ language }}` variable + (`nodes.py`, both `generate_outline_node` and `generate_transcript_node`), but the stock + 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 `` 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 diff --git a/README.md b/README.md index 789f885..5f61c96 100644 --- a/README.md +++ b/README.md @@ -204,6 +204,11 @@ Kurzreferenz — Details jeweils in [`CLAUDE.md`](CLAUDE.md): leere Chat-Antworten bei größerem Kontext, ohne sichtbaren Fehler. - **Neue Host-Ports brauchen eine `ufw`-Regel**, sonst kann der Container sie nicht erreichen (Timeout, keine Fehlermeldung in Open Notebook selbst). -- **Podcasts werden ohne explizites `language`-Feld auf Englisch erzeugt**, unabhängig von der - Sprache der Quelle. Die drei mitgelieferten Episode-Profile sind bereits auf `"de"` gesetzt; - bei selbst angelegten Profilen selbst daran denken. +- **Podcast-Sprache** wird über das `language`-Feld des Episode-Profils gesteuert, aber nur + dank der gepatchten Prompt-Vorlagen unter `prompts/podcast/` (per Bind-Mount in + `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). diff --git a/docker-compose.yml b/docker-compose.yml index d830346..97ea4a9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,3 +34,11 @@ services: - "host.docker.internal:host-gateway" volumes: - ./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 diff --git a/prompts/podcast/outline.jinja b/prompts/podcast/outline.jinja new file mode 100644 index 0000000..c0ba2f8 --- /dev/null +++ b/prompts/podcast/outline.jinja @@ -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 }} + + +The user has provided content to be used as the context for this podcast episode: + +{% if context is string %} +{{ context }} +{% else %} +{% for item in context %} + +{{ item }} + +{% endfor %} +{% endif %} + + +The podcast will feature the following speakers: + +{% for speaker in speakers %} +- **{{ speaker.name }}**: {{ speaker.backstory }} + Personality: {{ speaker.personality }} +{% endfor %} + + +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 tags, put ALL your reasoning inside tags +- Put the final JSON output OUTSIDE and AFTER any tags +- Do NOT wrap the JSON in ```json code blocks - return the raw JSON object only +- Example correct format: + Let me analyze the briefing... + {"segments": [...]} + +Please provide your outline now, following the format and guidelines provided above. diff --git a/prompts/podcast/transcript.jinja b/prompts/podcast/transcript.jinja new file mode 100644 index 0000000..3b9b75f --- /dev/null +++ b/prompts/podcast/transcript.jinja @@ -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 }} + + +The user has provided content to be used as the context for this podcast episode: + +{% if context is string %} +{{ context }} +{% else %} +{% for item in context %} + +{{ item }} + +{% endfor %} +{% endif %} + + +The podcast features the following speakers: + +{% for speaker in speakers %} +- **{{ speaker.name }}**: {{ speaker.backstory }} + Personality: {{ speaker.personality }} +{% endfor %} + + +Next, examine the outline produced by our director: + +{{ outline }} + + +{% if transcript %} +Here is the current transcript so far: + +{{ 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 }} + + +{% 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 tags, put ALL your reasoning inside tags +- Put the final JSON output OUTSIDE and AFTER any tags +- Do NOT wrap the JSON in ```json code blocks - return the raw JSON object only +- Example correct format: + Let me plan the dialogue... + {"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 %}