WIP - UI, Audio, firecrawl, long-form - V0.5

This commit is contained in:
ajaysi
2024-06-20 22:48:52 +05:30
parent 899abad1ba
commit 074ddf6210
12 changed files with 206 additions and 131 deletions

View File

@@ -20,20 +20,20 @@ If you have 💻 Laptop + 🛜 Internet + 10 minutes, you will be generating blo
---
### [Getting started for Developers](https://github.com/AJaySi/AI-Writer/wiki/Alwrity--%E2%80%90-Get-started)
`
```
Step1: git clone https://github.com/AJaySi/AI-Writer.git
Step2: pip install -r requirements.txt
Step3: streamlit run alwrity.py
Step4: Visit Alwrity UI in Browser & Start generation AI personalized content.
`
```
---
### Updating to latest Code: (Existing users)
`
```
1). Git pull
2). streamlit run alwrity.py
3). pip install -r requirements.txt
`
```
---
**Still stuck, [Open issue here](https://github.com/AJaySi/AI-Writer/issues) & Someone will bail you out.
@@ -92,6 +92,7 @@ Use the [main_config](https://github.com/AJaySi/AI-Writer/blob/main/main_config)
- [Gemini API](https://gemini.google.com/app): Google powered LLM for natural language processing tasks.
- [Ollama](https://ollama.com/) : Local, Privacy focused, LLM provider for research and content generation capabilities.
- [CrewAI](https://www.crewai.com/): Collaborative AI agents framework.
- [firecrawl](https://www.firecrawl.dev/): Turn websites into LLM-ready data
---
## Features

View File

@@ -33,7 +33,7 @@ def check_api_keys():
missing_keys.append((key, description))
if missing_keys:
st.warning("🚨 Some API keys are missing! Please provide them below: 🚨")
st.error("🚨 Some API keys are missing! Please provide them below: 🚨")
new_keys = []
for key, description in missing_keys:
@@ -321,45 +321,43 @@ def main():
if api_keys_valid and llm_environs_valid:
# Clear previous messages and display the sidebar configuration
sidebar_configuration()
else:
st.error("Error loading Environment variables.")
# Define the tabs
tab1, tab2, tab3, tab4, tab5 = st.tabs(
# Define the tabs
tab1, tab2, tab3, tab4, tab5 = st.tabs(
["AI Writers", "Content Planning", "Agents Content Teams", "Alwrity Brain", "Ask Alwrity"])
with tab1:
write_blog()
with tab1:
write_blog()
with tab2:
content_planning_tools()
with tab2:
content_planning_tools()
with tab3:
ai_agents_team()
with tab3:
ai_agents_team()
with tab4:
alwrity_brain()
with tab4:
alwrity_brain()
with tab5:
st.info("Chatbot")
st.markdown("Create a collection by uploading files (PDF, MD, CSV, etc), or crawl a data source (Websites, more sources coming soon.")
st.markdown("One can ask/chat, summarize and do semantic search over the uploaded data")
#alwrity_chat_docqa()
with tab5:
st.info("Chatbot")
st.markdown("Create a collection by uploading files (PDF, MD, CSV, etc), or crawl a data source (Websites, more sources coming soon.")
st.markdown("One can ask/chat, summarize and do semantic search over the uploaded data")
#alwrity_chat_docqa()
# Sidebar for prompt modification
st.sidebar.title("📝 Modify Prompts")
prompts = read_prompts()
# Sidebar for prompt modification
st.sidebar.title("📝 Modify Prompts")
prompts = read_prompts()
if prompts:
edited_prompts = []
for i, prompt in enumerate(prompts):
edited_prompt = st.sidebar.text_area(f"Prompt {i+1}", prompt)
edited_prompts.append(edited_prompt)
if prompts:
edited_prompts = []
for i, prompt in enumerate(prompts):
edited_prompt = st.sidebar.text_area(f"Prompt {i+1}", prompt)
edited_prompts.append(edited_prompt)
if st.sidebar.button("Save Prompts"):
write_prompts(edited_prompts)
st.sidebar.success("Prompts saved successfully!")
else:
st.sidebar.warning("No prompts found in the file.")
if st.sidebar.button("Save Prompts"):
write_prompts(edited_prompts)
st.sidebar.success("Prompts saved successfully!")
else:
st.sidebar.warning("No prompts found in the file.")
# Functions for the main options

View File

@@ -1,21 +1,27 @@
import sys
import os
import asyncio
from textwrap import dedent
from pathlib import Path
from datetime import datetime
import streamlit as st
from gtts import gTTS
import base64
from dotenv import load_dotenv
# Load environment variables
load_dotenv(Path('../../.env'))
# Logger setup
from loguru import logger
logger.remove()
logger.add(sys.stdout,
colorize=True,
format="<level>{level}</level>|<green>{file}:{line}:{function}</green>| {message}"
)
colorize=True,
format="<level>{level}</level>|<green>{file}:{line}:{function}</green>| {message}")
from ..ai_web_researcher.gpt_online_researcher import do_google_serp_search,\
do_tavily_ai_search, do_metaphor_ai_research, do_google_pytrends_analysis
# Import other necessary modules
from ..ai_web_researcher.gpt_online_researcher import (
do_google_serp_search, do_tavily_ai_search,
do_metaphor_ai_research, do_google_pytrends_analysis)
from .blog_from_google_serp import write_blog_google_serp, blog_with_research
from ..ai_web_researcher.you_web_reseacher import get_rag_results, search_ydc_index
from ..blog_metadata.get_blog_metadata import blog_metadata
@@ -23,6 +29,21 @@ from ..blog_postprocessing.save_blog_to_file import save_blog_to_file
from ..gpt_providers.text_to_image_generation.main_generate_image_from_prompt import generate_image
# Function to convert text to speech and save as an audio file
def text_to_speech(text, lang='en'):
tts = gTTS(text=text, lang=lang)
tts.save("output.mp3")
return "output.mp3"
# Function to get audio file as a downloadable link
def get_audio_file(audio_file):
with open(audio_file, "rb") as file:
data = file.read()
b64_data = base64.b64encode(data).decode()
return f'<a href="data:audio/mp3;base64,{b64_data}" download="output.mp3">Download audio file</a>'
def write_blog_from_keywords(search_keywords, url=None):
"""
This function will take a blog Topic to first generate sections for it
@@ -45,8 +66,8 @@ def write_blog_from_keywords(search_keywords, url=None):
status.update(label=f"🛀 Starting Tavily AI research: {search_keywords}")
tavily_search_result, t_titles, t_answer = do_tavily_ai_search(search_keywords)
status.update(label=f"🙆 Finished Google Search & Tavily AI Search on: {search_keywords}",
state="complete", expanded=False)
status.update(label=f"🙆 Finished Google Search & Tavily AI Search on: {search_keywords}",
state="complete", expanded=False)
except Exception as err:
st.error(f"Failed in web research: {err}")
@@ -66,21 +87,21 @@ def write_blog_from_keywords(search_keywords, url=None):
# logger.info/check the final blog content.
logger.info("######### Draft1: Finished Blog from Google web search: ###########")
with st.status("Started Writing blog from Tavily Web search..", expanded=True) as status:
# Do Tavily AI research to augument the above blog.
# Do Tavily AI research to augment the above blog.
try:
#example_blog_titles.append(t_titles)
# example_blog_titles.append(t_titles)
if blog_markdown_str and tavily_search_result:
logger.info(f"\n\n######### Blog content after Tavily AI research: ######### \n\n")
blog_markdown_str = write_blog_google_serp(search_keywords, tavily_search_result)
status.update(label="Finished Writing Blog From Tavily Results:{blog_markdown_str}", expanded=True)
status.update(label=f"Finished Writing Blog From Tavily Results:{blog_markdown_str}", expanded=True)
except Exception as err:
logger.error(f"Failed to do Tavily AI research: {err}")
status.update(label="🙎 Generating - Title, Meta Description, Tags, Categories for the content.", expanded=True)
try:
blog_title, blog_meta_desc, blog_tags, blog_categories = blog_metadata(blog_markdown_str)
blog_title, blog_meta_desc, blog_tags, blog_categories = asyncio.run(blog_metadata(blog_markdown_str))
except Exception as err:
st.error(f"Failed to get blog metadata: {err}")
@@ -94,38 +115,21 @@ def write_blog_from_keywords(search_keywords, url=None):
except Exception as err:
st.warning(f"Failed in Image generation: {err}")
saved_blog_to_file = save_blog_to_file(blog_markdown_str, blog_title, blog_meta_desc,
blog_tags, blog_categories, generated_image_filepath)
saved_blog_to_file = save_blog_to_file(blog_markdown_str, blog_title, blog_meta_desc,
blog_tags, blog_categories, generated_image_filepath)
status.update(label=f"Saved the content in this file: {saved_blog_to_file}")
logger.info(f"\n\n --------- Finished writing Blog for : {search_keywords} -------------- \n")
# Render the result on streamlit UI
st.image(generated_image_filepath)
st.markdown(f"{blog_markdown_str}")
status.update(label=f"Finished, Review & Use your Original Content Below: {saved_blog_to_file}", state="complete")
# Display options below the content
col1, col2, col3, col4, col5 = st.columns(5)
if col1.button('Copy'):
pyperclip.copy(blog_markdown_str)
st.success("Text copied to clipboard!")
if col2.button('Rephrase'):
rephrased_text = rephrase_text(blog_markdown_str)
st.markdown(rephrased_text)
if col3.button('Change Tone'):
tone = st.selectbox("Select Tone", ["Formal", "Casual", "Professional"])
if st.button("Apply Tone"):
toned_text = change_tone(blog_markdown_str, tone)
st.markdown(toned_text)
if col4.button('Make Shorter'):
shorter_text = make_shorter(blog_markdown_str)
st.markdown(shorter_text)
if col5.button('Translate'):
language = st.selectbox("Select Language", ["Spanish", "French", "German"])
if st.button("Translate"):
translated_text = translate_text(blog_markdown_str, language)
st.markdown(translated_text)
# Render the result on streamlit UI
if generated_image_filepath:
st.image(generated_image_filepath)
st.markdown(f"{blog_markdown_str}")
status.update(label=f"Finished, Review & Use your Original Content Below: {saved_blog_to_file}",
state="complete")
# Passing the text and language to the engine, here we have marked slow=False. Which tells
# the module that the converted audio should have a high speed
tts = gTTS(text=blog_markdown_str, lang='en', slow=False)
# Saving the converted audio in a mp3 file
tts.save("delete_me.mp3")
st.audio("delete_me.mp3")

View File

@@ -124,15 +124,15 @@ def long_form_generator(content_keywords):
# Configure generative AI
load_dotenv(Path('../.env'))
generation_config = {
"temperature": 0.6,
"temperature": 0.7,
"top_p": 1,
"max_output_tokens": 8096,
}
genai.configure(api_key=os.getenv('GEMINI_API_KEY'))
# Initialize the generative model
#model = genai.GenerativeModel('gemini-pro', generation_config=generation_config)
model_pro = genai.GenerativeModel('gemini-1.5-flash', generation_config=generation_config)
model = genai.GenerativeModel('gemini-1.5-flash', generation_config=generation_config)
model_pro = genai.GenerativeModel('gemini-pro', generation_config=generation_config)
# Do SERP web research for given keywords to generate title and outline.
web_research_result, g_titles = do_google_serp_search(content_keywords)
@@ -203,14 +203,14 @@ def long_form_generator(content_keywords):
logger.info(f"Writing in progress... Current draft length: {len(draft)} characters")
status.update(label=f"Writing in progress... Current draft length: {len(draft)} characters")
search_terms = f"""
I will provide you with blog outline, your task is to read the outline & return 8 google search keywords.
I will provide you with content outline below, your task is to read the outline & return 8 google search keywords.
Your response will be used to do web research for writing on the given outline.
Do not explain your response, provide 8 google search sentences encompassing the given content outline.
Provide the search term results as comma separated values.\n\n
Important: Provide the search term results as comma separated values.\n\n
Content Outline:\n
'{content_outline}'
"""
search_words = generate_with_retry(model_pro, search_terms).text
search_words = generate_with_retry(model, search_terms).text
status.update(label=f"Search terms from written draft: {search_words}")
while 'IAMDONE' not in continuation:
@@ -218,6 +218,7 @@ def long_form_generator(content_keywords):
str_list = re.split(r',\s*', search_words)
# Strip quotes from each element
str_list = [s.strip('\'"') for s in str_list]
for search_term in str_list:
web_research_result, m_titles, t_titles = do_tavily_ai_search(search_term, max_results=5)
try:

View File

@@ -17,7 +17,7 @@ logger.add(sys.stdout,
)
from ..ai_web_researcher.firecrawl_web_crawler import scrape_url
from ..blog_metadata.get_blog_metadata import blog_metadata
from ..blog_metadata.get_blog_metadata import blog_metadata, run_async
from ..blog_postprocessing.save_blog_to_file import save_blog_to_file
from ..gpt_providers.text_to_image_generation.main_generate_image_from_prompt import generate_image
from ..gpt_providers.text_generation.main_text_generation import llm_text_gen
@@ -31,7 +31,11 @@ def blog_from_url(weburl):
# Use to store the blog in a string, to save in a *.md file.
blog_markdown_str = None
tavily_search_result = None
example_blog_titles = []
# Initializing the variables
blog_title = None
blog_meta_desc = None
blog_tags = None
blog_categories = None
logger.info(f"Researching and Writing Blog on: {weburl}")
with st.status("Started Writing..", expanded=True) as status:
@@ -39,12 +43,12 @@ def blog_from_url(weburl):
status.update(label=f"Researching and Writing Blog on: {weburl}")
try:
scraped_text = scrape_url(weburl)
logger.info(scraped_text)
#logger.info(scraped_text)
except Exception as err:
st.error(f"Failed to scrape web page from url-{weburl} - Error: {err}")
logger.error(f"Failed in web research: {err}")
st.stop()
status.update(label="Successfully Scraped/Fetched url: {weburl}", expanded=False, state="complete")
status.update(label=f"Successfully Scraped/Fetched url: {weburl}", expanded=False, state="complete")
with st.status(f"Started Writing blog from {weburl}..", expanded=True) as status:
# Do Tavily AI research to augument the above blog.
@@ -58,7 +62,7 @@ def blog_from_url(weburl):
try:
status.update(label="🙎 Generating - Title, Meta Description, Tags, Categories for the content.")
blog_title, blog_meta_desc, blog_tags, blog_categories = blog_metadata(blog_markdown_str)
blog_title, blog_meta_desc, blog_tags, blog_categories = run_async(blog_metadata(blog_markdown_str))
except Exception as err:
st.error(f"Failed to get blog metadata: {err}")
@@ -71,8 +75,11 @@ def blog_from_url(weburl):
saved_blog_to_file = save_blog_to_file(blog_markdown_str, blog_title, blog_meta_desc,
blog_tags, blog_categories, generated_image_filepath)
status.update(label=f"Saved the content in this file: {saved_blog_to_file}")
logger.info(f"\n\n --------- Finished writing Blog for : {weburl} -------------- \n")
st.image(generated_image_filepath)
if generated_image_filepath:
st.image(generated_image_filepath)
st.markdown(f"{blog_markdown_str}")
status.update(label=f"Finished, Review & Use your Original Content Below: {saved_blog_to_file}", state="complete")

View File

@@ -2,7 +2,7 @@ import sys
import streamlit as st
from loguru import logger
import random
import time
import asyncio
logger.remove()
logger.add(sys.stdout,
@@ -12,7 +12,7 @@ logger.add(sys.stdout,
from ..gpt_providers.text_generation.main_text_generation import llm_text_gen
def blog_metadata(blog_article):
async def blog_metadata(blog_article):
""" Common function to get blog metadata """
logger.info(f"Generating Content MetaData\n")
@@ -20,22 +20,22 @@ def blog_metadata(blog_article):
total_steps = 4
# Step 1: Generate blog title
time.sleep(random.uniform(1, 3))
await asyncio.sleep(random.uniform(1, 3))
blog_title = generate_blog_title(blog_article)
progress_bar.progress(1 / total_steps)
# Step 2: Generate blog meta description
time.sleep(random.uniform(1, 3))
await asyncio.sleep(random.uniform(1, 3))
blog_meta_desc = generate_blog_description(blog_article)
progress_bar.progress(2 / total_steps)
# Step 3: Generate blog tags
time.sleep(random.uniform(1, 3))
await asyncio.sleep(random.uniform(1, 3))
blog_tags = get_blog_tags(blog_article)
progress_bar.progress(3 / total_steps)
# Step 4: Generate blog categories
time.sleep(random.uniform(1, 3))
await asyncio.sleep(random.uniform(1, 3))
blog_categories = get_blog_categories(blog_article)
progress_bar.progress(4 / total_steps)
@@ -117,3 +117,10 @@ def get_blog_tags(blog_article):
logger.error(f"Failed to get response from LLM: {err}")
raise err
# Helper function to run the asyncio event loop within Streamlit
def run_async(coro):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
result = loop.run_until_complete(coro)
loop.close()
return result

View File

@@ -2,6 +2,8 @@ import os
import re
import sys
import streamlit as st
from streamlit_mic_recorder import speech_to_text
import tempfile
from pathlib import Path
import configparser
@@ -10,7 +12,6 @@ import uuid
from PIL import Image
from PyPDF2 import PdfReader
from docx import Document
from loguru import logger
logger.remove()
logger.add(sys.stdout,
@@ -19,7 +20,6 @@ logger.add(sys.stdout,
)
from rich import print
from lib.ai_web_researcher.gpt_online_researcher import gpt_web_researcher
from lib.ai_web_researcher.metaphor_basic_neural_web_search import metaphor_find_similar
from lib.ai_writers.keywords_to_blog_streamlit import write_blog_from_keywords
@@ -41,11 +41,40 @@ from lib.gpt_providers.text_to_image_generation.main_generate_image_from_prompt
from lib.content_planning_calender.content_planning_agents_alwrity_crew import ai_agents_planner
def record_voice(language="en"):
# https://github.com/B4PT0R/streamlit-mic-recorder?tab=readme-ov-file#example
state = st.session_state
if "text_received" not in state:
state.text_received = []
text = speech_to_text(
start_prompt="🎙Record🔊",
stop_prompt="🔇Stop Recording🚨",
language=language,
use_container_width=True,
just_once=False,
)
if text:
state.text_received.append(text)
result = ""
for text in state.text_received:
result += text
state.text_received = []
return result if result else None
def is_youtube_link(text):
if text is not None:
youtube_regex = re.compile(r'(https?://)?(www\.)?(youtube|youtu|youtube-nocookie)\.(com|be)/(watch\?v=|embed/|v/|.+\?v=)?([^&=%\?]{11})')
return youtube_regex.match(text)
def is_web_link(text):
if text is not None:
web_regex = re.compile(r'(https?://)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)')
@@ -87,10 +116,11 @@ def process_input(input_text, uploaded_file):
return "video_file"
return None
def blog_from_keyword():
""" Input blog keywords, research and write a factual blog."""
st.title("Blog Content Writer")
col1, col2 = st.columns([2, 1.5])
col1, col2, col3 = st.columns([2, 1.5, 0.5])
with col1:
user_input = st.text_area('**👇Enter Keywords/Title/YouTube Link/Web URLs**',
help='Provide keywords, titles, YouTube links, or web URLs to generate content.',
@@ -104,6 +134,10 @@ def blog_from_keyword():
uploaded_file = st.file_uploader("**👇Attach files (Audio, Video, Image, Document)**",
type=["txt", "pdf", "docx", "jpg", "jpeg", "png", "mp3", "wav", "mp4", "mkv", "avi"],
help='Attach files such as audio, video, images, or documents.')
with col3:
user_input = record_voice()
if user_input:
st.info(user_input)
temp_file_path = None
if uploaded_file is not None:

View File

@@ -1,6 +1,14 @@
import os
import sys
import json
from pathlib import Path
from loguru import logger
logger.remove()
logger.add(sys.stdout,
colorize=True,
format="<level>{level}</level>|<green>{file}:{line}:{function}</green>| {message}"
)
def read_return_config_section(config_section):
""" read_return_config_section

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 270 KiB

View File

@@ -1,13 +1,27 @@
writing_guidelines: |
As an expert content writer and web researcher, write highly detailed long form, {content_type} content on {content_keywords}.
Follow these writing guidelines:
1. You must Write in {content_language} language.
2. Ensure your content appeals to the target audience of {target_audience}.
3. Maintain a consistent tone of {content_tone} throughout.
4. Use simple {content_language} words to appeal to all readers.
5. Format your content using {output_format}.
6. Avoid words like: Unleash, ultimate, uncover, discover, elevate, revolutionizing, unveiling, harnessing, dive, delve into, embrace.
Remember, your main goal is to write as much as you can. If you get through the story too fast, that is bad. Expand, never summarize.
Writing Guidelines:
As an expert {content_type} content writer and web researcher on {content_keywords}, follow these writing guidelines:
Language: Write in the {content_language} language.
Audience: Ensure your content appeals to the target audience of {target_audience}.
Tone: Maintain a consistent tone of {content_tone} throughout.
Simplicity: Use simple {content_language} words to appeal to all readers.
Formatting: Format your content using {output_format}.
Word Choice: Avoid words like: unleash, ultimate, uncover, discover, elevate, revolutionizing, unveiling, harnessing, dive, delve into, embrace.
Immerse yourself fully in the topic you're exploring. Use vivid descriptions to captivate your readers and bring your subject to life.
Develop your ideas thoroughly—let their nuances, challenges, and intricacies unfold naturally.
Follow the structure of your outline, but don't feel constrained by it. Allow your blog post to evolve as you write.
Incorporate rich imagery, sensory details, and evocative language to make your content engaging and relatable.
Introduce elements subtly that can grow into deeper discussions, related topics, or additional insights later in the post.
Keep your readers intrigued by not resolving everything too quickly.
Plant the seeds of subtopics or potential shifts in perspective that can be expanded upon in future posts.
Remember, your main goal is to provide valuable, in-depth content. If you rush through your topic, it will leave readers wanting more.
Expand your ideas, never summarize. Write as much as you can, ensuring that your content is thorough and comprehensive.
content_title: |
@@ -51,7 +65,7 @@ starting_prompt: |
First, silently review the content outline and title. Consider how to begin writing your content. Take your time.
Start by writing the very beginning of the outline. You are not expected to finish the entire content now.
Your writing should be detailed, only scratching the surface of the first bullet of your outline.
Try to write AT MINIMUM 1000 WORDS and MAXIMUM 2000 WORDS.
Try to write AT MINIMUM 500 WORDS.
"""{{writing_guidelines}}"""
@@ -60,30 +74,29 @@ starting_prompt: |
continuation_prompt: |
As an expert {content_language} content writer and web researcher specializing in SEO-optimized content, continue writing the content for the given title and outline.
The Title of the content is:
"""{{content_title}}"""
Title of the Content:
{{content_title}}
The content Outline is:
"""{{content_outline}}"""
Content Outline:
{{content_outline}}
Relevant web research results to use:
"""{{web_research_result}}"""
Relevant Web Research Results to Use:
{{web_research_result}}
You've begun to immerse yourself in this world, and the words are flowing.
Here's what you've written so far:
"""{{content_text}}"""
You've begun to immerse yourself in this subject, and the words are flowing. Here's what you've written so far:
{{content_text}}
=====
===========
First, silently review the content outline and what you've written so far.
Take your time to understand the flow and context.
Identify the next section of your outline to write about.
It is important to continue from where you left off.
First, silently review the content outline and what you've written so far. Take your time.
Identify what the single next part of your outline you should write.
Important to Continue from where you left off.
Your task is to continue writing from where you left off and cover the next part of the outline.
You are not expected to finish the entire content now.
Your writing should be detailed enough to thoroughly explore the next part of your outline.
Aim to write at least 500 words. However, only once the entire content is completely finished, write IAMDONE.
Remember, do not write the whole outline sections right now.
Your task is to continue where you left off and write the next part of the outline.
You are not expected to finish the whole content now. Your writing should be
detailed enough that you are only scratching the surface of the next part of
your outline. Try to write AT MINIMUM 1000 WORDS. However, only once the whole outline content
is COMPLETELY finished, write IAMDONE. Remember, do NOT write a whole outline sections right now.
"""{{writing_guidelines}}"""
{writing_guidelines}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 5.8 MiB

View File

@@ -33,3 +33,5 @@ streamlit
yfinance
pandas_ta
firecrawl-py
gTTS
streamlit-mic-recorder