commit
7648caca7b
18 changed files with 1854 additions and 57 deletions
|
|
@ -7,4 +7,6 @@ data/
|
|||
.env
|
||||
sqlite-db/
|
||||
temp/
|
||||
google-credentials.json
|
||||
google-credentials.json
|
||||
docker-compose*
|
||||
.docker_data
|
||||
13
.env.example
13
.env.example
|
|
@ -14,9 +14,15 @@ OPENAI_API_KEY=
|
|||
# EXAMPLE - anthropic/claude-3-5-sonnet-20240620
|
||||
ANTHROPIC_API_KEY=
|
||||
|
||||
|
||||
# GEMINI
|
||||
# USE MODEL NAMES AS "gemini/<modelname>"
|
||||
# EXAMPLE - gemini/gemini-1.5-pro-002
|
||||
GEMINI_API_KEY=
|
||||
|
||||
# VERTEXAI
|
||||
# USE MODEL NAMES AS "vertexai/<modelname>"
|
||||
# EXAMPLE - vertexai/gemini-1.5-pro-001
|
||||
# EXAMPLE - vertexai/gemini-1.5-pro-002
|
||||
VERTEX_PROJECT=my-google-cloud-project-name
|
||||
GOOGLE_APPLICATION_CREDENTIALS=./google-credentials.json
|
||||
|
||||
|
|
@ -31,6 +37,11 @@ OLLAMA_API_BASE="http://10.20.30.20:11434"
|
|||
OPENROUTER_BASE_URL="https://openrouter.ai/api/v1"
|
||||
OPENROUTER_API_KEY=
|
||||
|
||||
# ELEVENLABS
|
||||
# Used only by the podcast feature
|
||||
ELEVENLABS_API_KEY=
|
||||
|
||||
|
||||
# USE THIS IF YOU WANT TO DEBUG THE APP ON LANGSMITH
|
||||
# LANGCHAIN_TRACING_V2=true
|
||||
# LANGCHAIN_ENDPOINT="https://api.smith.langchain.com"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ FROM python:3.11.7-slim-bullseye
|
|||
# Install system dependencies required for building certain Python packages
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
curl wget libmagic-dev \
|
||||
curl wget libmagic-dev ffmpeg \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set the working directory in the container to /app
|
||||
|
|
@ -23,6 +23,8 @@ WORKDIR /app
|
|||
EXPOSE 8502
|
||||
|
||||
RUN mkdir -p /app/sqlite-db
|
||||
RUN mkdir -p /data
|
||||
RUN mkdir -p /surrealdb-data
|
||||
|
||||
CMD ["poetry", "run", "streamlit", "run", "app_home.py"]
|
||||
|
||||
|
|
|
|||
33
README.md
33
README.md
|
|
@ -10,17 +10,7 @@ Open Notebook empowers you to manage your research, generate AI-assisted notes,
|
|||
|
||||
Go to the [Setup Guide](docs/SETUP.md) to learn how to set up the tool in details.
|
||||
|
||||
But, the gist of it is:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/lfnovo/open_notebook.git
|
||||
cd open_notebook
|
||||
cp .env.sample .env
|
||||
poetry install
|
||||
poetry run streamlit run app_home.py
|
||||
```
|
||||
|
||||
or with Docker/Portainer:
|
||||
To setup with Docker/Portainer:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
|
@ -52,7 +42,6 @@ volumes:
|
|||
```
|
||||
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Go to the [Usage](docs/USAGE.md) page to learn how to use all features.
|
||||
|
|
@ -68,10 +57,16 @@ Go to the [Usage](docs/USAGE.md) page to learn how to use all features.
|
|||
- **Recursive Summarization**: Tackle large content by recursively summarizing it.
|
||||
- **Integrated Search Engines**: Built-in full-text and vector search for faster information retrieval.
|
||||
- **Fine-Grained Context Management**: Choose exactly what to share with the AI to maintain control.
|
||||
- **Cost Estimation**: Estimate costs for large context processing to keep budget control in check.
|
||||
- **Podcast Generator**: Automatically convert your notes into a podcast format.
|
||||
|
||||
## 🚀 New Features
|
||||
|
||||
### v0.0.4 - Podcasts 🎙️
|
||||
|
||||
You can now build amazing custom podcasts based on your own data. Customize your speakers, episode structure, cadence, voices, etc.
|
||||
|
||||
Head to the [Podcasts](docs/PODCASTS.md) page for more info
|
||||
|
||||
### v0.0.3 - Transformations ✨
|
||||
|
||||
We just release a much more powerful way to create more value from your sources.
|
||||
|
|
@ -114,19 +109,18 @@ Locate anything across your research with ease using full-text and vector-based
|
|||
Jinja based prompts that are easy to customize to your own preferences.
|
||||
|
||||
|
||||
## 🌟 Coming Soon
|
||||
## 🌟 Roadmap
|
||||
|
||||
- **Podcast Generator**: Automatically convert your notes into a podcast format.
|
||||
- **Enhanced Citations**: Improved layout and finer control for citations.
|
||||
- **Better Embeddings & Summarization**: Smarter ways to distill information.
|
||||
- **Multiple Chat Sessions**: Juggle different discussions within the same notebook.
|
||||
- **Live Front-End Updates**: Real-time UI updates for a smoother experience.
|
||||
- **Async Processing**: Faster UI through asynchronous content processing.
|
||||
- **Improved Error Handling**: Making everything more robust.
|
||||
- **Cross-Notebook Sources and Notes**: Reuse research notes across projects.
|
||||
- **Bookmark Integration**: Integrate with your favorite bookmarking app.
|
||||
- **Multi-model support**: Open AI, Anthropic, Vertex AI, Open Router, Ollama, etc. ✅ 0.0.2
|
||||
- **Insight Generation**: New tools for creating insights - [transformations](docs/TRANSFORMATIONS.md) ✅ 0.0.3
|
||||
- **Podcast Generator**: Automatically convert your notes into a podcast format. ✅ 0.0.4
|
||||
|
||||
|
||||
## 💻 Tech Stack
|
||||
|
|
@ -134,6 +128,7 @@ Jinja based prompts that are easy to customize to your own preferences.
|
|||
- **Streamlit**: For the front-end (Looking to move out of Streamlit. Contributors welcome!).
|
||||
- **SurrealDB**: Fast, scalable database solution.
|
||||
- **Langchain/Langgraph**: The backbone for LLM interactions.
|
||||
- **Podcastfy**: For generating podcasts from your notes.
|
||||
|
||||
|
||||
## 🙌 Help Wanted
|
||||
|
|
@ -151,3 +146,9 @@ Open Notebook is MIT licensed. See the [LICENSE](LICENSE) file for details.
|
|||
---
|
||||
|
||||
Your contributions, feature requests, and bug reports are always welcome. Let's build a research tool that respects our privacy and makes learning truly open for everyone. ✨
|
||||
|
||||
---
|
||||
|
||||
This project uses the following third-party libraries:
|
||||
|
||||
- [Podcastfy](https://github.com/souzatharsis/podcastfy) - Licensed under the Apache License 2.0
|
||||
|
|
@ -24,4 +24,4 @@ except InvalidDatabaseSchema as e:
|
|||
st.rerun()
|
||||
except Exception as e:
|
||||
st.error(e)
|
||||
st.stop()
|
||||
st.stop()
|
||||
|
|
|
|||
|
|
@ -76,5 +76,7 @@ DEFINE EVENT IF NOT EXISTS source_delete ON TABLE source WHEN ($after == NONE) T
|
|||
delete source_insight where source == $before.id;
|
||||
};
|
||||
|
||||
DEFINE TABLE IF NOT EXISTS podcast_config SCHEMALESS;
|
||||
|
||||
UPDATE open_notebook:database_info SET
|
||||
version= "0.0.2";
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ services:
|
|||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- ./surreal-data:/mydata
|
||||
- ./.docker_data/surreal-data:/mydata
|
||||
user: "${UID}:${GID}"
|
||||
command: start --log trace --user root --pass root rocksdb:mydatabase.db
|
||||
pull_policy: always
|
||||
|
|
@ -17,6 +17,8 @@ services:
|
|||
ports:
|
||||
- "8080:8502"
|
||||
volumes:
|
||||
- ./.docker_data/data:/app/data
|
||||
- ./docker.env:/app/.env
|
||||
- ./google-credentials.json:/app/google-credentials.json
|
||||
depends_on:
|
||||
- surrealdb
|
||||
|
|
|
|||
25
docs/PODCASTS.md
Normal file
25
docs/PODCASTS.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# Poscasts
|
||||
|
||||
**For the audio learners**
|
||||
|
||||
The podcast feature made Google Notebook LM famous and for a good reason. It can make your learning process 10x more enjoyable and efficient by processing a lot of insights in a way that is very easy to consume.
|
||||
|
||||
### Define Templates
|
||||
|
||||
Setup a template for your podcast, define the role of the speakers, the format of the podcast, and the length of each episode.
|
||||
|
||||

|
||||
|
||||
### Pick your context
|
||||
|
||||
Pick the context for your podcast. You can use your own notes and assets.
|
||||
|
||||

|
||||
|
||||
### Generate your podcast
|
||||
|
||||

|
||||
|
||||
### Manage, Listen to and Download your episodes
|
||||
|
||||

|
||||
|
|
@ -49,6 +49,14 @@ Or by turning any LLM message into a Note.
|
|||
|
||||

|
||||
|
||||
## Generate your podcasts
|
||||
|
||||
Once you have your content ready, start creating beautiful podcast episodes from it.
|
||||
|
||||

|
||||
|
||||
See more at the [Podcasts](PODCASTS.md) section.
|
||||
|
||||
## Searching
|
||||
|
||||
The search page gives you a glance of all the notes you have made and the sources you have added. You can query the database both by keyword as well as using the vector search.
|
||||
|
|
|
|||
BIN
docs/assets/podcast.png
Normal file
BIN
docs/assets/podcast.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
docs/assets/podcast_listen.png
Normal file
BIN
docs/assets/podcast_listen.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
BIN
docs/assets/podcast_template.png
Normal file
BIN
docs/assets/podcast_template.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 288 KiB |
|
|
@ -40,7 +40,7 @@ class ObjectModel(BaseModel):
|
|||
except Exception as e:
|
||||
logger.error(f"Error fetching all {cls.table_name}: {str(e)}")
|
||||
logger.exception(e)
|
||||
raise DatabaseOperationError(f"Failed to fetch all {cls.table_name}")
|
||||
raise DatabaseOperationError(e)
|
||||
|
||||
@classmethod
|
||||
def get(cls: Type[T], id: str) -> Optional[T]:
|
||||
|
|
@ -152,7 +152,7 @@ class Notebook(ObjectModel):
|
|||
except Exception as e:
|
||||
logger.error(f"Error fetching sources for notebook {self.id}: {str(e)}")
|
||||
logger.exception(e)
|
||||
raise DatabaseOperationError("Failed to fetch sources for notebook")
|
||||
raise DatabaseOperationError(e)
|
||||
|
||||
@property
|
||||
def notes(self) -> List["Note"]:
|
||||
|
|
@ -171,7 +171,7 @@ class Notebook(ObjectModel):
|
|||
except Exception as e:
|
||||
logger.error(f"Error fetching notes for notebook {self.id}: {str(e)}")
|
||||
logger.exception(e)
|
||||
raise DatabaseOperationError("Failed to fetch notes for notebook")
|
||||
raise DatabaseOperationError(e)
|
||||
|
||||
|
||||
class Asset(BaseModel):
|
||||
|
|
|
|||
361
open_notebook/plugins/podcasts.py
Normal file
361
open_notebook/plugins/podcasts.py
Normal file
|
|
@ -0,0 +1,361 @@
|
|||
from typing import ClassVar, List, Literal
|
||||
|
||||
from loguru import logger
|
||||
from podcastfy.client import generate_podcast
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
from open_notebook.domain 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: str
|
||||
person2_role: str
|
||||
conversation_style: List[str]
|
||||
engagement_technique: List[str]
|
||||
dialogue_structure: List[str]
|
||||
user_instructions: str
|
||||
wordcount: int = Field(gt=500, lt=10000)
|
||||
creativity: float = Field(ge=0, le=1)
|
||||
provider: Literal["openai", "elevenlabs", "edge"] = Field(default="openai")
|
||||
voice1: str
|
||||
voice2: str
|
||||
model: str
|
||||
|
||||
def generate_episode(self, episode_name, text, instructions=None):
|
||||
self.user_instructions = (
|
||||
instructions if instructions else self.user_instructions
|
||||
)
|
||||
conversation_config = {
|
||||
"word_count": self.wordcount,
|
||||
"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": {
|
||||
# "temp_audio_dir": "./data/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",
|
||||
},
|
||||
}
|
||||
|
||||
logger.error(conversation_config)
|
||||
# conversation_config = {}
|
||||
logger.debug(
|
||||
f"Generating episode {episode_name} with config {conversation_config}"
|
||||
)
|
||||
|
||||
audio_file = generate_podcast(
|
||||
conversation_config=conversation_config, text=text, tts_model=self.provider
|
||||
)
|
||||
logger.warning(audio_file)
|
||||
episode = PodcastEpisode(
|
||||
name=episode_name,
|
||||
template=self.name,
|
||||
instructions=instructions,
|
||||
text=str(text),
|
||||
audio_file=audio_file,
|
||||
)
|
||||
episode.save()
|
||||
|
||||
@field_validator("wordcount")
|
||||
def validate_wordcount(cls, value):
|
||||
if not 500 <= value <= 6000:
|
||||
raise ValueError("Wordcount must be between 500 and 10000")
|
||||
return value
|
||||
|
||||
@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",
|
||||
"Reflective",
|
||||
"Narrative",
|
||||
"Satirical",
|
||||
"Educational",
|
||||
"Conversational",
|
||||
"Critical",
|
||||
"Empathetic",
|
||||
"Philosophical",
|
||||
"Speculative",
|
||||
"Motivational",
|
||||
"Fun",
|
||||
"Technical",
|
||||
"Light-hearted",
|
||||
"Serious",
|
||||
"Investigative",
|
||||
"Debunking",
|
||||
"Collaborative",
|
||||
"Didactic",
|
||||
"Thought-provoking",
|
||||
"Controversial",
|
||||
"Skeptical",
|
||||
"Optimistic",
|
||||
"Pessimistic",
|
||||
"Objective",
|
||||
"Subjective",
|
||||
"Sarcastic",
|
||||
"Emotional",
|
||||
"Exploratory",
|
||||
"Friendly",
|
||||
"Fast-paced",
|
||||
"Slow-paced",
|
||||
"Introspective",
|
||||
"Open-ended",
|
||||
"Affirmative",
|
||||
"Dissenting",
|
||||
]
|
||||
|
||||
# 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",
|
||||
"Rebuttal",
|
||||
"Expert Interviews",
|
||||
"Panel Discussion",
|
||||
"Case Studies",
|
||||
"Myth Busting",
|
||||
"Debunking Misconceptions",
|
||||
"Audience Questions",
|
||||
"Q&A Session",
|
||||
"Listener Feedback",
|
||||
"Rapid-fire Questions",
|
||||
"Summary of Key Points",
|
||||
"Recap",
|
||||
"Key Takeaways",
|
||||
"Actionable Tips",
|
||||
"Call to Action",
|
||||
"Future Outlook",
|
||||
"Teaser for Next Episode",
|
||||
"Closing Remarks",
|
||||
"Thank You and Credits",
|
||||
"Outtakes or Bloopers",
|
||||
"Sponsor Messages",
|
||||
"Social Media Shout-outs",
|
||||
"Resource Recommendations",
|
||||
"Feedback Request",
|
||||
"Lightning Round",
|
||||
"Behind-the-Scenes Insights",
|
||||
"Ethical Considerations",
|
||||
"Fact-checking Segment",
|
||||
"Trending Topics",
|
||||
"Closing Inspirational Quote",
|
||||
"Final Reflections",
|
||||
"Debrief",
|
||||
"Farewell Messages",
|
||||
"Next Episode Preview",
|
||||
"Live Reactions",
|
||||
"Call-in Segment",
|
||||
"Acknowledgements",
|
||||
"Transition Segments",
|
||||
"Break Segments",
|
||||
]
|
||||
|
||||
# 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",
|
||||
"Influencer",
|
||||
"Observer",
|
||||
"Listener",
|
||||
"Facilitator",
|
||||
"Innovator",
|
||||
"Debater",
|
||||
"Educator",
|
||||
"Motivator",
|
||||
"Narrator",
|
||||
"Explorer",
|
||||
"Opponent",
|
||||
"Proponent",
|
||||
"Philosopher",
|
||||
"Engineer",
|
||||
"Doctor",
|
||||
"Psychologist",
|
||||
"Economist",
|
||||
"Politician",
|
||||
"Scientist",
|
||||
"Entrepreneur",
|
||||
"Artist",
|
||||
"Author",
|
||||
"Journalist",
|
||||
"Activist",
|
||||
"Challenger",
|
||||
"Supporter",
|
||||
"Mentor",
|
||||
"Mentee",
|
||||
"Panelist",
|
||||
"Audience Representative",
|
||||
"Case Study Presenter",
|
||||
"Data Analyst",
|
||||
"Ethicist",
|
||||
"Cultural Critic",
|
||||
"Technologist",
|
||||
"Environmentalist",
|
||||
"Legal Expert",
|
||||
"Healthcare Professional",
|
||||
"Financial Advisor",
|
||||
"Policy Maker",
|
||||
"Sociologist",
|
||||
"Anthropologist",
|
||||
"Myth Buster",
|
||||
"Trend Analyst",
|
||||
"Futurist",
|
||||
"Negotiator",
|
||||
"Community Leader",
|
||||
"Voice of Reason",
|
||||
"Conflict Resolver",
|
||||
"Emotional Support",
|
||||
"Pragmatist",
|
||||
"Idealist",
|
||||
"Realist",
|
||||
"Satirist",
|
||||
"Story Analyst",
|
||||
"Language Expert",
|
||||
"Historical Witness",
|
||||
"Survivor",
|
||||
"Inspirational Figure",
|
||||
"Cultural Ambassador",
|
||||
"Digital Nomad",
|
||||
"Remote Correspondent",
|
||||
"Field Reporter",
|
||||
"Data Scientist",
|
||||
"Gamer",
|
||||
"Musician",
|
||||
"Filmmaker",
|
||||
]
|
||||
|
||||
# Engagement Techniques
|
||||
engagement_techniques = [
|
||||
"Rhetorical Questions",
|
||||
"Anecdotes",
|
||||
"Analogies",
|
||||
"Humor",
|
||||
"Metaphors",
|
||||
"Storytelling",
|
||||
"Quizzes",
|
||||
"Polls",
|
||||
"Contests/Giveaways",
|
||||
"Guest Appearances",
|
||||
"Sound Effects",
|
||||
"Music Interludes",
|
||||
"Shout-outs",
|
||||
"Interactive Challenges",
|
||||
"Personal Testimonials",
|
||||
"Quotes",
|
||||
"Jokes",
|
||||
"Surprise Elements",
|
||||
"Emotional Appeals",
|
||||
"Provocative Statements",
|
||||
"Irony",
|
||||
"Sarcasm",
|
||||
"Alliteration",
|
||||
"Repetition",
|
||||
"Foreshadowing",
|
||||
"Cliffhangers",
|
||||
"Audience Participation",
|
||||
"Sensory Descriptions",
|
||||
"Visual Aids (if applicable)",
|
||||
"Callbacks to Earlier Points",
|
||||
"Pop Culture References",
|
||||
"Hyperbole",
|
||||
"Parables",
|
||||
"Thought Experiments",
|
||||
"Puzzles and Riddles",
|
||||
"Role-playing",
|
||||
"Mock Scenarios",
|
||||
"Debates",
|
||||
"Sound Bites",
|
||||
"Catchphrases",
|
||||
"Voice Modulation",
|
||||
"Interactive Games",
|
||||
"Live Demos",
|
||||
"Behind-the-Scenes Insights",
|
||||
"Vivid Imagery",
|
||||
"Statistics and Facts",
|
||||
"Open-ended Questions",
|
||||
"Challenges to Assumptions",
|
||||
"Evoking Curiosity",
|
||||
"Memes (if visual components are included)",
|
||||
"Surveys",
|
||||
"Testimonials",
|
||||
"Provocations",
|
||||
]
|
||||
192
pages/5_🎙️_Podcasts.py
Normal file
192
pages/5_🎙️_Podcasts.py
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
import streamlit as st
|
||||
from streamlit_tags import st_tags
|
||||
|
||||
from open_notebook.plugins.podcasts import (
|
||||
PodcastConfig,
|
||||
PodcastEpisode,
|
||||
conversation_styles,
|
||||
dialogue_structures,
|
||||
engagement_techniques,
|
||||
participant_roles,
|
||||
)
|
||||
|
||||
episodes_tab, templates_tab = st.tabs(["Episodes", "Templates"])
|
||||
|
||||
with episodes_tab:
|
||||
episodes = PodcastEpisode.get_all()
|
||||
for episode in episodes:
|
||||
with st.container(border=True):
|
||||
episode_name = episode.name if episode.name else "No Name"
|
||||
st.markdown(f"**{episode.template} - {episode_name}**")
|
||||
# st.caption(naturaltime(episode.created))
|
||||
st.write(f"Instructions: {episode.instructions}")
|
||||
try:
|
||||
st.audio(episode.audio_file, format="audio/mpeg", loop=True)
|
||||
except Exception as e:
|
||||
st.write("No audio file found")
|
||||
st.error(e)
|
||||
with st.expander("Source Content"):
|
||||
st.code(episode.text)
|
||||
if st.button("Delete Episode", key=f"btn_delete{episode.id}"):
|
||||
episode.delete()
|
||||
st.rerun()
|
||||
if len(episodes) == 0:
|
||||
st.write("No episodes yet")
|
||||
with templates_tab:
|
||||
st.subheader("Podcast Templates")
|
||||
st.markdown("")
|
||||
with st.expander("**Create new Template**"):
|
||||
pd_cfg = {}
|
||||
pd_cfg["name"] = st.text_input("Template Name")
|
||||
pd_cfg["podcast_name"] = st.text_input("Podcast Name")
|
||||
pd_cfg["podcast_tagline"] = st.text_input("Podcast Tagline")
|
||||
pd_cfg["output_language"] = st.text_input("Language", value="English")
|
||||
pd_cfg["user_instructions"] = st.text_input(
|
||||
"User Instructions",
|
||||
help="Any additional intructions to pass to the LLM that will generate the transcript",
|
||||
)
|
||||
pd_cfg["person1_role"] = st.text_input("Person 1 role")
|
||||
st.caption(f"Suggestions:{', '.join(participant_roles)}")
|
||||
pd_cfg["person2_role"] = st.text_input("Person 2 role")
|
||||
pd_cfg["conversation_style"] = st_tags(
|
||||
["a"], conversation_styles, "Conversation Style"
|
||||
)
|
||||
st.caption(f"Suggestions:{', '.join(conversation_styles)}")
|
||||
pd_cfg["engagement_technique"] = st_tags(
|
||||
[], engagement_techniques, "Engagement Techniques"
|
||||
)
|
||||
st.caption(f"Suggestions:{', '.join(engagement_techniques)}")
|
||||
pd_cfg["dialogue_structure"] = st_tags(
|
||||
[], dialogue_structures, "Dialogue Structure"
|
||||
)
|
||||
st.caption(f"Suggestions:{', '.join(dialogue_structures)}")
|
||||
pd_cfg["wordcount"] = st.slider(
|
||||
"Word Count", min_value=400, max_value=6000, step=50
|
||||
)
|
||||
pd_cfg["creativity"] = st.slider(
|
||||
"Creativity", min_value=0.0, max_value=1.0, step=0.05
|
||||
)
|
||||
pd_cfg["provider"] = st.selectbox("Provider", ["openai", "elevenlabs", "edge"])
|
||||
pd_cfg["voice1"] = st.text_input(
|
||||
"Voice 1", help="You can use Elevenlabs voice ID"
|
||||
)
|
||||
pd_cfg["voice2"] = st.text_input(
|
||||
"Voice 2", help="You can use Elevenlabs voice ID"
|
||||
)
|
||||
pd_cfg["model"] = st.text_input("Model")
|
||||
if st.button("Save"):
|
||||
pd = PodcastConfig(**pd_cfg)
|
||||
pd_cfg = {}
|
||||
pd.save()
|
||||
st.rerun()
|
||||
|
||||
for pd_config in PodcastConfig.get_all():
|
||||
with st.expander(pd_config.name):
|
||||
pd_config.name = st.text_input(
|
||||
"Template Name", value=pd_config.name, key=f"name_{pd_config.id}"
|
||||
)
|
||||
pd_config.podcast_name = st.text_input(
|
||||
"Podcast Name",
|
||||
value=pd_config.podcast_name,
|
||||
key=f"podcast_name_{pd_config.id}",
|
||||
)
|
||||
pd_config.podcast_tagline = st.text_input(
|
||||
"Podcast Tagline",
|
||||
value=pd_config.podcast_tagline,
|
||||
key=f"podcast_tagline_{pd_config.id}",
|
||||
)
|
||||
pd_config.user_instructions = st.text_input(
|
||||
"User Instructions",
|
||||
value=pd_config.user_instructions,
|
||||
help="Any additional intructions to pass to the LLM that will generate the transcript",
|
||||
key=f"user_instructions_{pd_config.id}",
|
||||
)
|
||||
|
||||
pd_config.output_language = st.text_input(
|
||||
"Language",
|
||||
value=pd_config.output_language,
|
||||
key=f"output_language_{pd_config.id}",
|
||||
)
|
||||
pd_config.person1_role = st.text_input(
|
||||
"Person 1 role",
|
||||
value=pd_config.person1_role,
|
||||
key=f"person1_role_{pd_config.id}",
|
||||
)
|
||||
st.caption(f"Suggestions:{', '.join(participant_roles)}")
|
||||
pd_config.person2_role = st.text_input(
|
||||
"Person 2 role",
|
||||
value=pd_config.person2_role,
|
||||
key=f"person2_role_{pd_config.id}",
|
||||
)
|
||||
pd_config.conversation_style = st_tags(
|
||||
pd_config.conversation_style,
|
||||
conversation_styles,
|
||||
"Conversation Style",
|
||||
key=f"conversation_style_{pd_config.id}",
|
||||
)
|
||||
st.caption(f"Suggestions:{', '.join(conversation_styles)}")
|
||||
pd_config.engagement_technique = st_tags(
|
||||
pd_config.engagement_technique,
|
||||
engagement_techniques,
|
||||
"Engagement Techniques",
|
||||
key=f"engagement_technique_{pd_config.id}",
|
||||
)
|
||||
st.caption(f"Suggestions:{', '.join(engagement_techniques)}")
|
||||
pd_config.dialogue_structure = st_tags(
|
||||
pd_config.dialogue_structure,
|
||||
dialogue_structures,
|
||||
"Dialogue Structure",
|
||||
key=f"dialogue_structure_{pd_config.id}",
|
||||
)
|
||||
st.caption(f"Suggestions:{', '.join(dialogue_structures)}")
|
||||
pd_config.wordcount = st.slider(
|
||||
"Word Count",
|
||||
min_value=400,
|
||||
max_value=6000,
|
||||
step=50,
|
||||
value=pd_config.wordcount,
|
||||
key=f"wordcount_{pd_config.id}",
|
||||
)
|
||||
pd_config.creativity = st.slider(
|
||||
"Creativity",
|
||||
min_value=0.0,
|
||||
max_value=1.0,
|
||||
step=0.05,
|
||||
value=pd_config.creativity,
|
||||
key=f"creativity_{pd_config.id}",
|
||||
)
|
||||
pd_config.provider = st.selectbox(
|
||||
"Provider",
|
||||
["openai", "elevenlabs", "edge"],
|
||||
index=["openai", "elevenlabs", "edge"].index(pd_config.provider),
|
||||
key=f"provider_{pd_config.id}",
|
||||
)
|
||||
pd_config.voice1 = st.text_input(
|
||||
"Voice 1",
|
||||
value=pd_config.voice1,
|
||||
key=f"voice1_{pd_config.id}",
|
||||
help="You can use Elevenlabs voice ID",
|
||||
)
|
||||
pd_config.voice2 = st.text_input(
|
||||
"Voice 2",
|
||||
value=pd_config.voice2,
|
||||
key=f"voice2_{pd_config.id}",
|
||||
help="You can use Elevenlabs voice ID",
|
||||
)
|
||||
pd_config.model = st.text_input(
|
||||
"Model", value=pd_config.model, key=f"model_{pd_config.id}"
|
||||
)
|
||||
|
||||
if st.button("Save Config", key=f"btn_save{pd_config.id}"):
|
||||
pd_config.save()
|
||||
st.rerun()
|
||||
|
||||
if st.button("Duplicate Config", key=f"btn_duplicate{pd_config.id}"):
|
||||
pd_config.name = f"{pd_config.name} - Copy"
|
||||
pd_config.id = None
|
||||
pd_config.save()
|
||||
st.rerun()
|
||||
|
||||
if st.button("Delete Config", key=f"btn_delete{pd_config.id}"):
|
||||
pd_config.delete()
|
||||
st.rerun()
|
||||
1180
poetry.lock
generated
1180
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "open-notebook"
|
||||
version = "0.0.3"
|
||||
version = "0.0.4"
|
||||
description = "An open source implementation of a research assistant, inspired by Google Notebook LM"
|
||||
authors = ["Luis Novo <lfnovo@gmail.com>"]
|
||||
license = "MIT"
|
||||
|
|
@ -39,6 +39,7 @@ langchain-anthropic = "^0.2.3"
|
|||
langchain-ollama = "^0.2.0"
|
||||
langchain-google-vertexai = "^2.0.5"
|
||||
sdblpy = "^0.3.0"
|
||||
podcastfy = "^0.2.8"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
ipykernel = "^6.29.5"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from langchain_core.runnables import RunnableConfig
|
|||
|
||||
from open_notebook.domain import Note, Source
|
||||
from open_notebook.graphs.chat import graph as chat_graph
|
||||
from open_notebook.plugins.podcasts import PodcastConfig
|
||||
from open_notebook.utils import token_count
|
||||
from stream_app.note import make_note_from_chat
|
||||
|
||||
|
|
@ -58,26 +59,55 @@ def execute_chat(txt_input, session_id):
|
|||
def chat_sidebar(session_id):
|
||||
context = build_context(session_id=session_id)
|
||||
tokens = token_count(str(context) + str(st.session_state[session_id]["messages"]))
|
||||
with st.container(border=True):
|
||||
request = st.chat_input("Enter your question")
|
||||
# removing for now since it's not multi-model capable right now
|
||||
st.caption(f"Total tokens: {tokens}")
|
||||
if request:
|
||||
response = execute_chat(txt_input=request, session_id=session_id)
|
||||
st.session_state[session_id]["messages"] = response["messages"]
|
||||
|
||||
for msg in st.session_state[session_id]["messages"][::-1]:
|
||||
if msg.type not in ["human", "ai"]:
|
||||
continue
|
||||
if not msg.content:
|
||||
continue
|
||||
|
||||
with st.chat_message(name=msg.type):
|
||||
st.write(msg.content)
|
||||
if msg.type == "ai":
|
||||
if st.button("💾 New Note", key=f"render_save_{msg.id}"):
|
||||
make_note_from_chat(
|
||||
content=msg.content,
|
||||
notebook_id=st.session_state[session_id]["notebook"].id,
|
||||
chat_tab, podcast_tab = st.tabs(["Chat", "Podcast"])
|
||||
with st.expander(f"Context ({tokens} tokens), {len(str(context))} chars"):
|
||||
st.json(context)
|
||||
with podcast_tab:
|
||||
with st.container(border=True):
|
||||
podcast_configs = PodcastConfig.get_all()
|
||||
podcast_config_names = [pd.name for pd in podcast_configs]
|
||||
if len(podcast_configs) == 0:
|
||||
st.warning("No podcast configurations found")
|
||||
else:
|
||||
template = st.selectbox("Pick a template", podcast_config_names)
|
||||
selected_template = next(
|
||||
filter(lambda x: x.name == template, podcast_configs)
|
||||
)
|
||||
episode_name = st.text_input("Episode Name")
|
||||
instructions = st.text_area(
|
||||
"Instructions", value=selected_template.user_instructions
|
||||
)
|
||||
if st.button("Generate"):
|
||||
with st.spinner("Go grab a coffee, almost here..."):
|
||||
selected_template.generate_episode(
|
||||
episode_name=episode_name,
|
||||
text=context,
|
||||
instructions=instructions,
|
||||
)
|
||||
st.rerun()
|
||||
st.success("Episode generated successfully")
|
||||
st.page_link("pages/5_🎙️_Podcasts.py", label="Go to Podcasts")
|
||||
st.divider()
|
||||
with chat_tab:
|
||||
with st.container(border=True):
|
||||
request = st.chat_input("Enter your question")
|
||||
# removing for now since it's not multi-model capable right now
|
||||
st.caption(f"Total tokens: {tokens}")
|
||||
if request:
|
||||
response = execute_chat(txt_input=request, session_id=session_id)
|
||||
st.session_state[session_id]["messages"] = response["messages"]
|
||||
|
||||
for msg in st.session_state[session_id]["messages"][::-1]:
|
||||
if msg.type not in ["human", "ai"]:
|
||||
continue
|
||||
if not msg.content:
|
||||
continue
|
||||
|
||||
with st.chat_message(name=msg.type):
|
||||
st.write(msg.content)
|
||||
if msg.type == "ai":
|
||||
if st.button("💾 New Note", key=f"render_save_{msg.id}"):
|
||||
make_note_from_chat(
|
||||
content=msg.content,
|
||||
notebook_id=st.session_state[session_id]["notebook"].id,
|
||||
)
|
||||
st.rerun()
|
||||
|
|
|
|||
Loading…
Reference in a new issue