# How Episode Profiles and Speaker Profiles Work in Open Notebook Podcast Generation

> Learn how episode profiles and speaker profiles in Open Notebook podcast generation store LLM models and voice settings for cohesive audio creation. Understand configuration to output.

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

---

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

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

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

```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()

```

### Define a Speaker Profile with Two Speakers

Configure a multi-speaker profile with distinct voices and 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 Podcast Generation Request via API

Queue a generation job using the REST endpoint:

```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 Job Status

Poll the status endpoint to retrieve the generated audio URL:

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