from typing import ClassVar, List, Optional from loguru import logger from podcastfy.client import generate_podcast from pydantic import Field, field_validator, model_validator from open_notebook.config import DATA_FOLDER from open_notebook.domain.notebook import ObjectModel class PodcastEpisode(ObjectModel): table_name: ClassVar[str] = "podcast_episode" name: str template: str instructions: str text: str audio_file: str class PodcastConfig(ObjectModel): table_name: ClassVar[str] = "podcast_config" name: str podcast_name: str podcast_tagline: str output_language: str = Field(default="English") person1_role: List[str] person2_role: List[str] conversation_style: List[str] engagement_technique: List[str] dialogue_structure: List[str] transcript_model: Optional[str] = None transcript_model_provider: Optional[str] = None user_instructions: Optional[str] = None ending_message: Optional[str] = None creativity: float = Field(ge=0, le=1) provider: str = Field(default="openai") voice1: str voice2: str model: str # Backwards compatibility @field_validator("person1_role", "person2_role", mode="before") @classmethod def split_string_to_list(cls, value): if isinstance(value, str): return [item.strip() for item in value.split(",")] return value @model_validator(mode="after") def validate_voices(self) -> "PodcastConfig": if not self.voice1 or not self.voice2: raise ValueError("Both voice1 and voice2 must be provided") return self async def generate_episode( self, episode_name: str, text: str, instructions: str = "", longform: bool = False, chunks: int = 8, min_chunk_size=600, ): self.user_instructions = ( instructions if instructions else self.user_instructions ) conversation_config = { "max_num_chunks": chunks, "min_chunk_size": min_chunk_size, "conversation_style": self.conversation_style, "roles_person1": self.person1_role, "roles_person2": self.person2_role, "dialogue_structure": self.dialogue_structure, "podcast_name": self.podcast_name, "podcast_tagline": self.podcast_tagline, "output_language": self.output_language, "user_instructions": self.user_instructions, "engagement_techniques": self.engagement_technique, "creativity": self.creativity, "text_to_speech": { "output_directories": { "transcripts": f"{DATA_FOLDER}/podcasts/transcripts", "audio": f"{DATA_FOLDER}/podcasts/audio", }, "temp_audio_dir": f"{DATA_FOLDER}/podcasts/audio/tmp", "ending_message": "Thank you for listening to this episode. Don't forget to subscribe to our podcast for more interesting conversations.", "default_tts_model": self.provider, self.provider: { "default_voices": { "question": self.voice1, "answer": self.voice2, }, "model": self.model, }, "audio_format": "mp3", }, } api_key_label = None llm_model_name = None tts_model = None if self.transcript_model_provider: if self.transcript_model_provider == "openai": api_key_label = "OPENAI_API_KEY" llm_model_name = self.transcript_model elif self.transcript_model_provider == "anthropic": api_key_label = "ANTHROPIC_API_KEY" llm_model_name = self.transcript_model elif self.transcript_model_provider == "gemini": api_key_label = "GOOGLE_API_KEY" llm_model_name = self.transcript_model if self.provider == "google": tts_model = "gemini" elif self.provider == "openai": tts_model = "openai" elif self.provider == "anthropic": tts_model = "anthropic" elif self.provider == "vertexai": tts_model = "geminimulti" elif self.provider == "elevenlabs": tts_model = "elevenlabs" logger.info( f"Generating episode {episode_name} with config {conversation_config} and using model {llm_model_name}, tts model {tts_model}" ) try: audio_file = generate_podcast( conversation_config=conversation_config, text=text, tts_model=tts_model, llm_model_name=llm_model_name, api_key_label=api_key_label, longform=longform, ) episode = PodcastEpisode( name=episode_name, template=self.name, instructions=instructions, text=str(text), audio_file=audio_file, ) await episode.save() except Exception as e: logger.error(f"Failed to generate episode {episode_name}: {e}") raise @field_validator( "name", "podcast_name", "podcast_tagline", "output_language", "model" ) @classmethod def validate_required_strings(cls, value: str, field) -> str: if value is None or value.strip() == "": raise ValueError(f"{field.field_name} cannot be None or empty string") return value.strip() @field_validator("creativity") def validate_creativity(cls, value): if not 0 <= value <= 1: raise ValueError("Creativity must be between 0 and 1") return value conversation_styles = [ "Analytical", "Argumentative", "Informative", "Humorous", "Casual", "Formal", "Inspirational", "Debate-style", "Interview-style", "Storytelling", "Satirical", "Educational", "Philosophical", "Speculative", "Motivational", "Fun", "Technical", "Light-hearted", "Serious", "Investigative", "Debunking", "Didactic", "Thought-provoking", "Controversial", "Sarcastic", "Emotional", "Exploratory", "Fast-paced", "Slow-paced", "Introspective", ] # Dialogue Structures dialogue_structures = [ "Topic Introduction", "Opening Monologue", "Guest Introduction", "Icebreakers", "Historical Context", "Defining Terms", "Problem Statement", "Overview of the Issue", "Deep Dive into Subtopics", "Pro Arguments", "Con Arguments", "Cross-examination", "Expert Interviews", "Case Studies", "Myth Busting", "Q&A Session", "Rapid-fire Questions", "Summary of Key Points", "Recap", "Key Takeaways", "Actionable Tips", "Call to Action", "Future Outlook", "Closing Remarks", "Resource Recommendations", "Trending Topics", "Closing Inspirational Quote", "Final Reflections", ] # Podcast Participant Roles participant_roles = [ "Main Summarizer", "Questioner/Clarifier", "Optimist", "Skeptic", "Specialist", "Thesis Presenter", "Counterargument Provider", "Professor", "Student", "Moderator", "Host", "Co-host", "Expert Guest", "Novice", "Devil's Advocate", "Analyst", "Storyteller", "Fact-checker", "Comedian", "Interviewer", "Interviewee", "Historian", "Visionary", "Strategist", "Critic", "Enthusiast", "Mediator", "Commentator", "Researcher", "Reporter", "Advocate", "Debater", "Explorer", ] # Engagement Techniques engagement_techniques = [ "Rhetorical Questions", "Anecdotes", "Analogies", "Humor", "Metaphors", "Storytelling", "Quizzes", "Personal Testimonials", "Quotes", "Jokes", "Emotional Appeals", "Provocative Statements", "Sarcasm", "Pop Culture References", "Thought Experiments", "Puzzles and Riddles", "Role-playing", "Debates", "Catchphrases", "Statistics and Facts", "Open-ended Questions", "Challenges to Assumptions", "Evoking Curiosity", ]