# How the Episode and Speaker Profile System Powers Podcast Generation in Open Notebook

> Discover how Open Notebook uses Episode and Speaker Profile systems for podcast generation. Configure LLM settings, voice models, and speaker personalities to synthesize audio efficiently.

- Repository: [Luis Novo/open-notebook](https://github.com/lfnovo/open-notebook)
- Tags: deep-dive
- Published: 2026-06-22

---

**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`](https://github.com/lfnovo/open-notebook/blob/main/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.

```python

# 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`](https://github.com/lfnovo/open-notebook/blob/main/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:

```python
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:

```python
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:

```python
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`](https://github.com/lfnovo/open-notebook/blob/main/api/podcast_service.py)):

```python
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:

```python
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.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/podcasts/models.py) provide reusable, database-backed configurations for podcast structure and voice synthesis.
- **Model resolution** occurs through `_resolve_model_config` and profile-specific async methods, converting stored IDs into concrete `(provider, model, config)` tuples required by the `podcast_creator` library.
- **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 `PodcastEpisode` records, 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`](https://github.com/lfnovo/open-notebook/blob/main/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`](https://github.com/lfnovo/open-notebook/blob/main/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`](https://github.com/lfnovo/open-notebook/blob/main/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.