How EpisodeProfile and SpeakerProfile Models Structure Podcast Generation in Open Notebook
Open Notebook orchestrates podcast creation through two Pydantic-based SurrealDB models—EpisodeProfile and SpeakerProfile—that decouple runtime configuration from concrete providers via a model registry, while enforcing strict validation on segment counts (3-20) and speaker configurations (1-4).
The lfnovo/open-notebook repository implements a sophisticated podcast generation system where configuration management is handled by EpisodeProfile and SpeakerProfile classes extending ObjectModel. These models define the schema for AI-generated podcasts, storing generation parameters in the episode_profile and speaker_profile tables while supporting both legacy migration paths and modern model registry indirection.
EpisodeProfile Model Architecture
The EpisodeProfile class in open_notebook/podcasts/models.py encapsulates all parameters needed to generate a podcast episode outline and transcript. It inherits from the project's SurrealDB-aware ObjectModel base and persists to the episode_profile table.
Core Configuration Fields
Every episode profile requires a unique name (string) and optional description for UI display. The speaker_config field stores the string name of the associated SpeakerProfile that defines voice characteristics. According to the source code at lines 50-52, these fields establish the basic identity and voice linkage for the episode.
Critical runtime parameters include:
language: BCP-47 locale code (e.g.,en-US,pt-BR) that guides LLM prompts and TTS generationdefault_briefing: Jinja-style template string fed to LLMs as contextnum_segments: Integer defaulting to 5, validated to remain between 3 and 20 segments
Model Registry Integration
Rather than storing raw API credentials, modern episode profiles use the global model registry via RecordID references. Lines 68-74 in models.py define outline_llm and transcript_llm as optional strings containing record IDs pointing to Model entries in the registry. This architecture decouples the profile from concrete provider implementations, allowing credentials to rotate without updating episode configurations.
Legacy fields including outline_provider/outline_model and transcript_provider/transcript_model remain in the schema (lines 55-66) but are ignored at runtime, facilitating seamless migration from older 15-field designs.
Validation and Persistence
The model enforces data integrity through Pydantic validators. A @field_validator constrains num_segments to the 3-20 range, preventing generation of impractically short or long podcasts. The _prepare_save_data() method converts supplied outline_llm and transcript_llm values to proper RecordID objects via ensure_record_id before persistence to SurrealDB.
SpeakerProfile Model Architecture
The SpeakerProfile class manages voice synthesis configurations and speaker definitions, inheriting from ObjectModel and storing records in the speaker_profile table.
Voice Configuration and Speaker Definitions
Each profile requires a unique name and optional description. The modern configuration centers on voice_model, an optional string containing a RecordID referencing a Model entry for TTS services like ElevenLabs or OpenAI (lines 50-52).
The speakers field contains a list of 1-4 dictionaries, each defining:
name: Speaker identifiervoice_id: Voice identifier for the TTS providerbackstory: Character context for the AIpersonality: Speaking style descriptors- Optional
voice_model: Per-speaker override of the profile-level voice model
Legacy tts_provider and tts_model fields persist in the schema (lines 44-48) for migration purposes but are not used in current runtime logic.
Validation Rules
A validate_speakers() class method ensures each profile contains between 1 and 4 speakers, raising validation errors for empty or oversized speaker lists. Each speaker dictionary must contain the required keys (name, voice_id, backstory, personality), enforced before database persistence.
Runtime Configuration Resolution
Both profiles implement lazy-loading resolution methods that bridge the gap between stored RecordID references and concrete provider configurations.
Resolving LLM Configurations
EpisodeProfile exposes resolve_outline_config() and resolve_transcript_config() methods that load the referenced Model records and call the internal _resolve_model_config helper. These return a tuple of (provider, model_name, config_dict) consumable by the LLM client. If the referenced model is missing, the methods raise runtime errors before generation begins.
Resolving TTS Configurations
SpeakerProfile.resolve_tts_config() mirrors this pattern for voice synthesis, returning the provider configuration tuple for the TTS engine. The resolution respects per-speaker voice_model overrides when present in individual speaker dictionaries, falling back to the profile-level voice_model otherwise.
Integration with the Generation Pipeline
The podcast generation workflow in api/podcast_service.py and the LangGraph orchestration in open_notebook/graphs/podcast.py consume these models through a standardized flow:
- Loading: The service layer calls
EpisodeProfile.get_by_name()andSpeakerProfile.get_by_name()to retrieve configurations by their unique identifiers - Resolution: The workflow invokes
resolve_outline_config()andresolve_transcript_config()on the episode profile, andresolve_tts_config()on the speaker profile - Execution: Resolved (provider, model, config) tuples are injected into the generation pipeline, ensuring the correct LLM and TTS credentials are used for outline creation, transcript generation, and audio synthesis
# Creating an episode profile programmatically
from open_notebook.podcasts.models import EpisodeProfile
profile = await EpisodeProfile(
name="TechTalk-Jan-2024",
description="Weekly tech news roundup",
speaker_config="StandardVoice",
outline_llm="model:openai:gpt-4o-mini",
transcript_llm="model:openai:gpt-4o",
language="en-US",
default_briefing="You are a concise tech journalist...",
num_segments=6,
).save()
# Resolving models for a generation run
from open_notebook.podcasts.models import EpisodeProfile, SpeakerProfile
ep = await EpisodeProfile.get_by_name("TechTalk-Jan-2024")
sp = await SpeakerProfile.get_by_name(ep.speaker_config)
outline_provider, outline_model, outline_cfg = await ep.resolve_outline_config()
transcript_provider, transcript_model, transcript_cfg = await ep.resolve_transcript_config()
tts_provider, tts_model, tts_cfg = await sp.resolve_tts_config()
Summary
- EpisodeProfile and SpeakerProfile in
open_notebook/podcasts/models.pyserve as the central configuration schema for podcast generation, extending SurrealDB-awareObjectModel. - Model registry indirection allows profiles to store only
RecordIDreferences (outline_llm,transcript_llm,voice_model), decoupling configurations from concrete provider credentials. - Strict validation enforces 3-20 segments per episode and 1-4 speakers per profile, with required speaker attributes validated via
validate_speakers(). - Legacy compatibility is maintained through deprecated provider/model fields that remain in the schema but are ignored at runtime.
- Runtime resolution via
resolve_outline_config(),resolve_transcript_config(), andresolve_tts_config()converts registry references to executable (provider, model, config) tuples.
Frequently Asked Questions
What is the difference between legacy provider fields and the new model registry fields in Open Notebook?
Legacy fields such as outline_provider, outline_model, transcript_provider, and transcript_model stored concrete provider strings directly in the profile records. The modern implementation uses outline_llm, transcript_llm, and voice_model fields that store RecordID references to the global Model registry. This indirection allows credentials to be updated centrally without modifying individual podcast profiles, and the legacy fields are retained only for database migration purposes.
How does Open Notebook validate podcast segment and speaker counts?
The EpisodeProfile model validates num_segments through a Pydantic @field_validator that constrains values to the range 3-20. Similarly, SpeakerProfile enforces a 1-4 speaker limit through its validate_speakers() method, which checks the length of the speakers list and ensures each speaker dictionary contains required keys (name, voice_id, backstory, personality). These validations run before data reaches the SurrealDB persistence layer.
How are concrete provider credentials resolved at runtime?
When the generation workflow begins, the system calls resolve_outline_config() and resolve_transcript_config() on the EpisodeProfile, and resolve_tts_config() on the SpeakerProfile. These methods load the referenced Model records from the registry and invoke _resolve_model_config to extract the current provider name, model identifier, and configuration dictionary. This resolution happens lazily at runtime, ensuring the latest credentials are always used.
Can individual speakers override the profile-level voice model?
Yes. While SpeakerProfile defines a default voice_model at the profile level, individual entries in the speakers list may contain their own voice_model field. When resolve_tts_config() processes a specific speaker, it checks for this per-speaker override before falling back to the profile-level configuration, enabling flexible voice assignments within a single podcast episode.
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 →