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 a SpeakerProfile by name.
  • outline_llm and transcript_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 containing name, voice_id, backstory, personality, and optional voice_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 global voice_model or 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_id for 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]
  1. The client submits a request to PodcastService.submit_generation_job in api/podcast_service.py.
  2. The service validates profile names and queues the generate_podcast command.
  3. The command loads both profiles and resolves every model ID via the registry.
  4. Resolved configurations are passed to podcast_creator via configure.
  5. The library generates the outline, transcript, and audio.
  6. The PodcastEpisode record is persisted with results.
  7. The client polls PodcastService.get_job_status to 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_command validates, resolves, and injects these profiles into the podcast_creator library via configure.
  • Every generation creates a PodcastEpisode record 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →