How podcast generation is modeled and executed: the two-tier profile system, the model-registry references, and the deliberate no-auto-retry policy.
- SpeakerProfile — voice configuration: a
voice_model(record<model>reference for TTS) plus 1–4 speakers (name, voice_id, backstory, personality). Individual speakers can override the profile'svoice_model. - EpisodeProfile — generation settings:
outline_llm/transcript_llm(record<model>references),language(BCP 47, e.g.pt-BR), segment count (3–20), briefing template. It references a SpeakerProfile by name. - PodcastEpisode — a generated episode. Links content, profiles and the async job (
commandfield → surreal-commands RecordID).
Profile fields reference Model records instead of raw provider/model strings. At generation time _resolve_model_config(model_id) loads the Model, resolves its linked credential (or falls back to provision_provider_keys()), and returns (provider, model_name, config) for podcast-creator.
The legacy string fields (tts_provider, outline_provider, …) that predated the registry were dropped by SQL migration 22 (#1107). The migration best-effort maps any still-unresolved profile to an existing model record (provider + name + type) before dropping the columns; profiles with no matching record stay unresolved — the UI already flags them as needing model selection and the user re-picks once. The old startup data migration (open_notebook/podcasts/migration.py) is gone.
PodcastEpisode stores episode_profile and speaker_profile as dicts (snapshots), not references. Editing a profile never retroactively changes past episodes — that's intentional. Corollary: deleting a profile does not cascade to episodes.
Generation runs as a generate_podcast_command job on the surreal-commands worker:
- The command resolves model configs and credentials for all profiles before invoking podcast-creator, and validates that
outline_llm,transcript_llmandvoice_modelare set. max_attempts: 1— no automatic retries. A mid-generation retry would create duplicate episode records (records are created during execution). Failed episodes are markedfailedwith an error message; retry is explicitly user-initiated viaPOST /podcasts/episodes/{id}/retry.- Status tracking:
get_job_status()/get_job_detail()query surreal-commands and return"unknown"on failure rather than raising. Listing endpoints use the batchedget_job_details_for_commands()so N episodes cost one status query, not N. - TTS failures fall back to silent audio rather than failing the episode.