How outline.jinja and transcript.jinja Drive the Podcast Generation Flow in Open Notebook
The outline.jinja and transcript.jinja templates power a two-stage LLM pipeline inside Open Notebook where the outline template structures the episode and the transcript template generates the conversational dialogue for each segment.
Open Notebook automates multi-speaker podcast creation through a queued command pipeline that turns source content into structured audio. The podcast generation flow is orchestrated by the generate_podcast command in commands/podcast_commands.py, which uses the podcast_creator library to render Jinja prompts at distinct stages. Understanding how these templates interact reveals how raw content becomes a scripted, voice-ready episode.
From API Request to Background Job
The flow starts when a client POSTs to /podcasts/generate. The PodcastService.submit_generation_job method, implemented in api/podcast_service.py, validates the request and enqueues a Surreal-Commands background job. The queued generate_podcast_command then executes the core generation logic.
The FastAPI router in api/routers/podcasts.py exposes this endpoint, but the heavy lifting happens asynchronously inside the command layer to avoid blocking the API.
A typical client request looks like this:
POST /api/podcasts/generate
{
"episode_profile": "my_episode_profile",
"speaker_profile": "my_speaker_profile",
"episode_name": "AI in Medicine",
"notebook_id": "nb123",
"briefing_suffix": "Focus on ethical concerns."
}
Profile Loading and Briefing Construction
Before any template is rendered, the command resolves configuration profiles and builds a detailed briefing.
# 1️⃣ Load profiles & resolve model configs
episode_profile = await EpisodeProfile.get_by_name(input_data.episode_profile)
speaker_profile = await SpeakerProfile.get_by_name(episode_profile.speaker_config)
# 2️⃣ Build briefing
briefing = episode_profile.default_briefing
if input_data.briefing_suffix:
briefing += f"\n\nAdditional instructions: {input_data.briefing_suffix}"
# 3️⃣ Call podcast-creator (internally uses the Jinja templates)
result = await create_podcast(
content=input_data.content,
briefing=briefing,
episode_name=episode_dir_name,
output_dir=str(output_dir),
speaker_config=speaker_profile.name,
episode_profile=episode_profile.name,
)
The briefing string combines the episode_profile.default_briefing with an optional briefing_suffix supplied by the user. This briefing, along with the raw source content, is later passed into both Jinja templates to maintain consistency across the episode.
How outline.jinja Structures the Episode
The first template-driven phase generates the episode skeleton. Inside podcast_creator, the outline phase renders prompts/podcast/outline.jinja with three variables:
briefing– the episode-level brief.context– the source material (string or list of strings).speakers– a list of speaker profiles containing name, backstory, and personality.
The template produces a JSON object with a segments array. Each segment entry contains a name, description, and size hint (short, medium, or long). The podcast_creator library parses this rendered JSON and stores it as episode.outline (see lines 56–62 of commands/podcast_commands.py).
A typical outline stored in the database looks like this:
{
"segments": [
{"name":"Intro", "description":"Introduce the topic", "size":"short"},
{"name":"Ethics", "description":"Discuss ethical implications", "size":"medium"},
{"name":"Future", "description":"Look ahead", "size":"long"}
]
}
This JSON acts as the creative brief for the transcript generation stage that follows.
How transcript.jinja Generates Dialogue
For every segment defined in the outline, podcast_creator renders prompts/podcast/transcript.jinja. This second template receives:
- The same
briefingandcontext. speakers(or a single speaker for solo podcasts).- The
outlinegenerated in the previous step. - The current
segmentdefinition.
The template returns a JSON object with a transcript list, where each element maps a speaker name to its dialogue. The command layer stores this result in episode.transcript (see lines 56–62 of commands/podcast_commands.py).
The transcript structure saved to the episode record follows this format:
{
"transcript": [
{"speaker":"Dr. Ada", "dialogue":"Welcome..."},
{"speaker":"Prof. Turing", "dialogue":"From an ethical standpoint..."}
]
}
Because the template is invoked once per segment, the final assembled transcript concatenates multiple segment outputs into a continuous conversational script ready for text-to-speech rendering.
Persisting the Final Episode
After create_podcast finishes processing all segments, the generate_podcast command persists a PodcastEpisode record. The database object attaches:
outline– the raw JSON produced byoutline.jinja.transcript– the raw JSON produced bytranscript.jinja.audio_file_path– the final audio file generated by the TTS step.
The simplified database view looks like this:
{
"id": "episode-abc123",
"name": "AI in Medicine",
"briefing": "...",
"outline": {
"segments": [
{"name":"Intro", "description":"Introduce the topic", "size":"short"},
{"name":"Ethics", "description":"Discuss ethical implications", "size":"medium"},
{"name":"Future", "description":"Look ahead", "size":"long"}
]
},
"transcript": {
"transcript": [
{"speaker":"Dr. Ada", "dialogue":"Welcome..."},
{"speaker":"Prof. Turing", "dialogue":"From an ethical standpoint..."}
]
},
"audio_file": "/data/podcasts/episodes/xyz/audio.mp3"
}
Summary
- The podcast generation flow in Open Notebook uses a queued command architecture to transform source material into spoken audio.
- The
generate_podcast_commandincommands/podcast_commands.pyloads profiles, builds a briefing, and delegates template rendering to thepodcast_creatorlibrary. outline.jinjareceivesbriefing,context, andspeakers, and emits a JSON episode structure with asegmentsarray.transcript.jinjareceives the outline, current segment, and speaker data, and emits a JSON dialogue script for that segment.- The command stores both JSON artifacts plus the final
audio_file_pathin thePodcastEpisoderecord.
Frequently Asked Questions
What triggers the podcast generation flow in Open Notebook?
A client POST request to /podcasts/generate triggers PodcastService.submit_generation_job, which enqueues a Surreal-Commands background job. The queued generate_podcast_command then executes the outline and transcript template pipeline asynchronously.
What variables does outline.jinja receive during rendering?
The outline.jinja template receives briefing (the episode brief), context (source material), and speakers (a list of profile objects with name, backstory, and personality). It must render a JSON object containing a segments array with name, description, and size fields.
How does transcript.jinja know what to write for each segment?
The transcript.jinja template is rendered once per segment. It receives the full outline, the current segment definition, the original briefing and context, and the speakers list. This ensures each segment's dialogue stays aligned with the overall episode structure and source material.
Where are the generated outline and transcript stored?
The generate_podcast command saves the rendered JSON from both templates directly into the PodcastEpisode database record as outline and transcript. It also attaches the audio_file_path produced by the downstream TTS engine.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →