# How EpisodeProfile and SpeakerProfile Models Structure Podcast Generation in Open Notebook

> Discover how EpisodeProfile and SpeakerProfile models structure podcast generation in Open Notebook. Learn about Pydantic models, SurrealDB, and strict validation for efficient podcast creation.

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

---

**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`](https://github.com/lfnovo/open-notebook/blob/main/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 generation
- `default_briefing`: Jinja-style template string fed to LLMs as context
- `num_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`](https://github.com/lfnovo/open-notebook/blob/main/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 identifier
- `voice_id`: Voice identifier for the TTS provider
- `backstory`: Character context for the AI
- `personality`: 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`](https://github.com/lfnovo/open-notebook/blob/main/api/podcast_service.py) and the LangGraph orchestration in [`open_notebook/graphs/podcast.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/graphs/podcast.py) consume these models through a standardized flow:

1. **Loading**: The service layer calls `EpisodeProfile.get_by_name()` and `SpeakerProfile.get_by_name()` to retrieve configurations by their unique identifiers
2. **Resolution**: The workflow invokes `resolve_outline_config()` and `resolve_transcript_config()` on the episode profile, and `resolve_tts_config()` on the speaker profile
3. **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

```python

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

```

```python

# 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.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/podcasts/models.py) serve as the central configuration schema for podcast generation, extending SurrealDB-aware `ObjectModel`.
- **Model registry indirection** allows profiles to store only `RecordID` references (`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()`, and `resolve_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.