How the Episode and Speaker Profile System Powers Podcast Generation in Open Notebook
Open Notebook separates podcast generation into two reusable configuration objects—EpisodeProfile and SpeakerProfile—that define LLM settings, voice models, and speaker personalities, then resolves these into concrete provider configurations before synthesizing audio.
The lfnovo/open-notebook repository implements a modular podcast generation pipeline where the episode and speaker profile system decouples content structure from voice synthesis parameters. This architecture allows researchers and content creators to mix and match episode outlines with different speaker combinations while maintaining consistent voice characteristics and model configurations stored in SurrealDB.
Understanding the Core Profile Models
The system centers on two Pydantic-based models defined in open_notebook/podcasts/models.py: EpisodeProfile for podcast-level settings and SpeakerProfile for voice and personality definitions.
EpisodeProfile: Podcast Structure and LLM Configuration
The EpisodeProfile class (lines 31–82) encapsulates the high-level parameters required to generate a podcast episode. Key fields include name for identification, speaker_config referencing a SpeakerProfile, and LLM configuration fields (outline_llm, transcript_llm) that specify which models handle content outlining versus transcript generation.
Additional critical fields include default_briefing for system prompts, num_segments to control episode length, and language for localization. The model inherits from ObjectModel, which provides automatic SurrealDB persistence through the _prepare_save_data hook that converts string IDs to proper RecordID objects before saving.
SpeakerProfile: Voice and Personality Definitions
The SpeakerProfile class (lines 26–70) defines one to four speakers through its speakers list, with each entry containing name, voice_id, backstory, personality, and optional voice_model overrides. The global voice_model field sets a default TTS (text-to-speech) provider for all speakers in the profile.
Validation occurs in validate_speakers (lines 58–68), enforcing that the speaker count remains between 1 and 4 entries while ensuring required personality fields are present. This allows per-speaker voice model overrides, enabling multi-vendor TTS configurations within a single podcast.
Model Resolution and Provider Configuration
Before generation begins, the system must transform stored model IDs into concrete provider-configuration tuples. The _resolve_model_config helper function (lines 19–30) loads the referenced Model record, retrieves credentials via Esperanto, and returns a tuple of (provider, model_name, config_dict).
Both profile classes expose async resolution methods. EpisodeProfile provides resolve_outline_config() and resolve_transcript_config() for LLM settings, while SpeakerProfile offers resolve_tts_config() for voice synthesis. This resolution step is critical because the downstream podcast_creator library requires validated provider configurations rather than raw database IDs.
# Resolution happens before any audio generation
outline_provider, outline_model, outline_cfg = await episode_profile.resolve_outline_config()
tts_provider, tts_model, tts_cfg = await speaker_profile.resolve_tts_config()
The Podcast Generation Pipeline
The actual generation orchestration occurs in commands/podcast_commands.py within the generate_podcast_command function (starting at line 84). This command integrates both profiles through a systematic validation and configuration process.
Loading and Validating Profiles
The command first retrieves profiles by name using EpisodeProfile.get_by_name and SpeakerProfile.get_by_name. It validates that all referenced model IDs exist and are accessible, aborting immediately if any required LLM or TTS configuration is missing. This validation ensures that credential errors surface before expensive audio generation begins.
Configuring the Podcast Creator Library
After resolving all model configurations—including per-speaker TTS overrides—the command injects the complete profile dictionaries into the third-party podcast_creator library:
configure("speakers_config", {"profiles": speaker_profiles_dict})
configure("episode_config", {"profiles": episode_profiles_dict})
This configuration step makes the full set of episode and speaker profiles available to the library, allowing it to select the correct TTS voice for each speaker and the appropriate LLM for outline and transcript generation.
Creating the Podcast Episode Record
Before invoking create_podcast, the command instantiates a PodcastEpisode record (lines 16–30) that stores full dumps of the chosen episode and speaker profiles, along with the command ID for asynchronous status tracking. This persists the complete generation context for later retrieval and debugging.
End-to-End Workflow Example
The following examples demonstrate defining profiles and submitting generation requests using the Open Notebook API.
First, define an episode profile that references a speaker configuration:
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()
Next, create a speaker profile with distinct 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 generation request via the REST API (handled by PodcastService.submit_generation_job in api/podcast_service.py):
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 the asynchronous job status using the returned command ID:
job_id = "command:open_notebook:12345"
status = httpx.get(f"http://localhost:5055/podcasts/status/{job_id}").json()
Summary
- EpisodeProfile and SpeakerProfile in
open_notebook/podcasts/models.pyprovide reusable, database-backed configurations for podcast structure and voice synthesis. - Model resolution occurs through
_resolve_model_configand profile-specific async methods, converting stored IDs into concrete(provider, model, config)tuples required by thepodcast_creatorlibrary. - Validation happens centrally in
generate_podcast_command, ensuring all LLM and TTS credentials are verified before audio generation begins. - Flexibility is achieved through per-speaker voice model overrides and the ability to pair any episode profile with any speaker profile.
- Persistence of generation context occurs via
PodcastEpisoderecords, enabling status tracking and reproducibility.
Frequently Asked Questions
How many speakers can a single SpeakerProfile support?
A SpeakerProfile supports between one and four speakers, enforced by the validate_speakers method in open_notebook/podcasts/models.py. Each speaker requires a name, voice_id, backstory, and personality field, with optional voice_model overrides for individual speakers.
Can different speakers use different TTS providers in the same podcast?
Yes. While the SpeakerProfile defines a global voice_model default, individual speakers can override this by specifying their own voice_model field in their speaker dictionary. The resolution system in generate_podcast_command processes these overrides separately, allowing multi-vendor configurations such as ElevenLabs for the host and Azure TTS for guests.
Where does the episode and speaker profile system resolve model credentials?
Resolution occurs in the _resolve_model_config helper function within open_notebook/podcasts/models.py. This function loads the referenced Model record from SurrealDB, retrieves the associated credentials via Esperanto, and returns the provider name, model identifier, and configuration dictionary required by the synthesis library.
How does the API handle asynchronous podcast generation?
The PodcastService.submit_generation_job method in api/podcast_service.py queues the generation request via surreal-commands, returning a command_id immediately. Clients poll PodcastService.get_job_status to retrieve the final audio URL, transcript, and outline once the generate_podcast_command completes and persists the result to a PodcastEpisode record.
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 →