How Episode Profiles and Speaker Profiles Work in Open Notebook Podcast Generation
Episode profiles and speaker profiles in Open Notebook are reusable configuration objects that store LLM models, voice settings, and personality metadata, which the system resolves into concrete provider configurations before passing them to the podcast-creator library for audio generation.
Open Notebook is an open-source knowledge management platform available at lfnovo/open-notebook. Its podcast generation pipeline relies on two distinct configuration layers—episode profiles and speaker profiles—to separate content settings from voice characteristics. These profiles are stored in SurrealDB and validated at runtime to ensure that every podcast episode uses the correct language models and text-to-speech voices.
Episode and Speaker Profiles: The Core Configuration Objects
The system defines two primary Pydantic models in open_notebook/podcasts/models.py that inherit from ObjectModel, providing automatic persistence and validation.
EpisodeProfile (lines 31–82) captures high-level episode settings:
name– unique identifier for the profile.speaker_config– references aSpeakerProfileby name.outline_llmandtranscript_llm– model registry IDs for outline and transcript generation.default_briefing,num_segments,language– content and structure parameters.
SpeakerProfile (lines 26–70) defines one-to-four speakers and their voice settings:
name– unique profile identifier.speakers– list of dictionaries containingname,voice_id,backstory,personality, and optionalvoice_model.voice_model– default TTS model for all speakers in the profile.
Validation Rules and Persistence in SurrealDB
Both models rely on ObjectModel for SurrealDB storage and implement custom validation hooks.
Speaker List Validation
The speakers list in SpeakerProfile is validated to contain 1–4 entries (see validate_speakers at lines 58–68). Each speaker must include name, voice_id, backstory, and personality. Missing fields raise ValueError before persistence.
Record ID Preparation
The _prepare_save_data hook converts user-supplied IDs to proper RecordID objects, ensuring that foreign keys are stored correctly in the graph database.
Resolving Model Registry IDs to Concrete Configurations
Profiles store model IDs (e.g., "model:openai:gpt-4o") that must be resolved into provider-model-config tuples at runtime. The BasePodcastProfile class provides the core resolution logic.
Model Registry Lookup
The _resolve_model_config method (lines 19–30) queries the ModelRegistry to load the Model record, fetches its credential via Esperanto, and returns a tuple of (provider, model_name, config_dict).
Profile-Specific Resolution Methods
Concrete resolution methods delegate to the base implementation:
EpisodeProfile.resolve_outline_config()– returns the provider tuple for the outline LLM.EpisodeProfile.resolve_transcript_config()– returns the provider tuple for the transcript LLM.SpeakerProfile.resolve_tts_config()– resolves the globalvoice_modelor per-speaker overrides.
Loading and Validating Profiles in the Generation Command
The generate_podcast_command in commands/podcast_commands.py orchestrates profile resolution at execution time.
Fetching Profiles by Name
The command loads both profiles using EpisodeProfile.get_by_name and SpeakerProfile.get_by_name.
Aborting on Missing Models
After loading, the command validates that every outline_llm, transcript_llm, and voice_model exists in the registry. If any ID is missing, the process aborts before heavy processing begins.
Finally, the command calls the resolution methods to ensure every provider key and credential is loaded.
Configuring the Podcast-Creator Library
After resolution, the command injects the processed data into the third-party podcast_creator library using its configure utility:
configure("speakers_config", {"profiles": speaker_profiles_dict})
configure("episode_config", {"profiles": episode_profiles_dict})
This step makes the full set of profiles—including resolved TTS voices and LLM endpoints—available to the library, allowing it to select the correct voice for each speaker and the correct model for each generation stage.
Creating the Podcast Episode Record
Before invoking create_podcast, the system creates a PodcastEpisode record (defined in models.py). This record stores:
- The chosen episode profile as a full dictionary dump.
- The selected speaker profile as a full dictionary dump.
- The
command_idfor asynchronous status tracking.
Storing the complete profile snapshots ensures that each episode can be reproduced or audited later, even if the original profiles are modified.
End-to-End Workflow from API Request to Audio File
The complete flow from HTTP request to finished audio involves several coordinated steps:
flowchart TD
A[Client → POST /podcast] --> B[PodcastService.submit_generation_job]
B --> C[Validate EpisodeProfile & SpeakerProfile]
C --> D[surreal-commands.submit_command('generate_podcast')]
D --> E[generate_podcast_command]
E --> F[Load EpisodeProfile & SpeakerProfile]
F --> G[Resolve Model IDs → provider/model/config]
G --> H[Configure podcast-creator (profiles)]
H --> I[create_podcast(content, briefing, …)]
I --> J[Store PodcastEpisode (audio, transcript, outline)]
J --> K[Return job_id to client]
K --> L[Client polls /podcast/status → PodcastService.get_job_status]
- The client submits a request to
PodcastService.submit_generation_jobinapi/podcast_service.py. - The service validates profile names and queues the
generate_podcastcommand. - The command loads both profiles and resolves every model ID via the registry.
- Resolved configurations are passed to
podcast_creatorviaconfigure. - The library generates the outline, transcript, and audio.
- The
PodcastEpisoderecord is persisted with results. - The client polls
PodcastService.get_job_statusto retrieve the final audio URL.
Practical Code Examples
Define an Episode Profile
Create an episode profile that references a speaker profile and specifies LLM models:
from open_notebook.podcasts.models import EpisodeProfile
await EpisodeProfile(
name="TechNewsWeekly",
description="Weekly tech roundup",
speaker_config="TechSpeakers",
outline_llm="model:openai:gpt-4o",
transcript_llm="model:openai:gpt-4o-mini",
language="en-US",
default_briefing="Summarize the latest tech headlines.",
num_segments=5,
).save()
Define a Speaker Profile with Two Speakers
Configure a multi-speaker profile with distinct voices and personalities:
from open_notebook.podcasts.models import SpeakerProfile
await SpeakerProfile(
name="TechSpeakers",
speakers=[
{
"name": "Host",
"voice_id": "en_us_female_1",
"backstory": "Tech-savvy journalist",
"personality": "cheerful, informative",
"voice_model": "model:elevenlabs:eleven_multilingual_v2"
},
{
"name": "Analyst",
"voice_id": "en_us_male_1",
"backstory": "Data-driven analyst",
"personality": "analytical, calm"
},
],
).save()
Submit a Podcast Generation Request via API
Queue a generation job using the REST endpoint:
import httpx
payload = {
"episode_profile": "TechNewsWeekly",
"speaker_profile": "TechSpeakers",
"episode_name": "2024-06-22 Tech Update",
"content": "Latest announcements from Apple, Google, and Microsoft..."
}
resp = httpx.post("http://localhost:5055/podcasts/generate", json=payload)
print(resp.json()) # => {"job_id": "...", "status": "queued", ...}
Check Job Status
Poll the status endpoint to retrieve the generated audio URL:
job_id = "command:open_notebook:12345"
status = httpx.get(f"http://localhost:5055/podcasts/status/{job_id}").json()
print(status) # {"job_id": "...", "status": "completed", "result": {...}}
Summary
- Episode profiles store LLM endpoints, briefing text, and episode structure in
open_notebook/podcasts/models.py. - Speaker profiles define 1–4 speakers with voice IDs, backstories, and optional per-speaker TTS models, validated before persistence.
- Model resolution happens centrally via
_resolve_model_config, converting registry IDs into concrete provider configurations. - The
generate_podcast_commandvalidates, resolves, and injects these profiles into thepodcast_creatorlibrary viaconfigure. - Every generation creates a
PodcastEpisoderecord storing full profile snapshots for reproducibility.
Frequently Asked Questions
How many speakers can a single speaker profile define?
A speaker profile must define between 1 and 4 speakers. The validate_speakers method in SpeakerProfile enforces this limit and requires each speaker to have name, voice_id, backstory, and personality fields.
Can different speakers use different TTS models within the same profile?
Yes. While the voice_model field at the profile level sets a default, individual speakers can override it by specifying their own voice_model key in their speaker dictionary. The resolution logic in _resolve_model_config handles each override separately.
What happens if a referenced model ID does not exist in the registry?
The generation command aborts early. After loading the profiles via get_by_name, the code validates that every outline_llm, transcript_llm, and voice_model exists in the ModelRegistry. If any ID is missing, the command raises an error before attempting to configure the podcast-creator library.
Where is the generated audio stored after the podcast is created?
The audio URL is stored in the audio_url field of the PodcastEpisode record, which is persisted to SurrealDB upon completion. The client receives a job_id and can poll PodcastService.get_job_status to retrieve the final record including the audio URL.
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 →