mirror of
https://github.com/tcsenpai/youlama.git
synced 2025-06-07 03:35:41 +00:00
modernized webui + added po_token support + added rephrasing of original transcript
This commit is contained in:
parent
70ae2fb11a
commit
7c99592fe0
377
src/main.py
377
src/main.py
@ -17,55 +17,142 @@ load_dotenv()
|
|||||||
st.set_page_config(
|
st.set_page_config(
|
||||||
page_title="YouTube Summarizer by TCSenpai",
|
page_title="YouTube Summarizer by TCSenpai",
|
||||||
page_icon="src/assets/subtitles.png",
|
page_icon="src/assets/subtitles.png",
|
||||||
|
layout="wide", # This ensures full width
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add this after set_page_config
|
# Add custom CSS with a modern, clean design
|
||||||
st.markdown(
|
st.markdown(
|
||||||
"""
|
"""
|
||||||
<style>
|
<style>
|
||||||
/* Custom styles for the main page */
|
/* Base theme */
|
||||||
|
:root {
|
||||||
|
--primary-color: #7289da;
|
||||||
|
--bg-color: #1a1b1e;
|
||||||
|
--card-bg: #2c2d30;
|
||||||
|
--text-color: #e0e0e0;
|
||||||
|
--border-color: #404246;
|
||||||
|
--hover-color: #3a3b3e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main container */
|
||||||
.stApp {
|
.stApp {
|
||||||
max-width: 1200px;
|
background-color: var(--bg-color);
|
||||||
margin: 0 auto;
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-header-btn {
|
/* Fix container width */
|
||||||
padding: 5px 10px;
|
.stApp > header {
|
||||||
border-radius: 5px;
|
background-color: var(--bg-color);
|
||||||
border: 1px solid #4CAF50;
|
}
|
||||||
background-color: transparent;
|
|
||||||
color: #4CAF50;
|
.stApp > div:nth-child(2) {
|
||||||
cursor: pointer;
|
padding-left: 5rem !important;
|
||||||
|
padding-right: 5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Headers */
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
color: white !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
margin-bottom: 1rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input fields */
|
||||||
|
.stTextInput input, .stSelectbox select {
|
||||||
|
background-color: var(--bg-color) !important;
|
||||||
|
color: var(--text-color) !important;
|
||||||
|
border: 1px solid var(--border-color) !important;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-size: 16px;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
.toggle-header-btn:hover {
|
|
||||||
background-color: #4CAF50;
|
/* Buttons */
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Improved input styling */
|
|
||||||
.stTextInput input {
|
|
||||||
border-radius: 5px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
padding: 8px 12px;
|
|
||||||
}
|
|
||||||
.stTextInput input:focus {
|
|
||||||
border-color: #4CAF50;
|
|
||||||
box-shadow: 0 0 0 1px #4CAF50;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Button styling */
|
|
||||||
.stButton button {
|
.stButton button {
|
||||||
border-radius: 5px;
|
background: linear-gradient(45deg, var(--primary-color), #8ea1e1) !important;
|
||||||
padding: 4px 25px;
|
color: white !important;
|
||||||
transition: all 0.3s;
|
border: none !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
padding: 12px 24px !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
transition: all 0.3s !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stButton button:hover {
|
.stButton button:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
|
box-shadow: 0 4px 12px rgba(114,137,218,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings cards */
|
||||||
|
.settings-card {
|
||||||
|
background-color: var(--card-bg);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove default container styling */
|
||||||
|
.element-container {
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Clean up expander */
|
||||||
|
.streamlit-expanderHeader {
|
||||||
|
background-color: var(--card-bg) !important;
|
||||||
|
color: var(--text-color) !important;
|
||||||
|
border: 1px solid var(--border-color) !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
padding: 1rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.streamlit-expanderContent {
|
||||||
|
border: none !important;
|
||||||
|
padding: 1rem 0 0 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status messages */
|
||||||
|
.stSuccess, .stInfo, .stWarning, .stError {
|
||||||
|
background-color: var(--card-bg) !important;
|
||||||
|
color: var(--text-color) !important;
|
||||||
|
border: 1px solid var(--border-color) !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
padding: 1rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide Streamlit branding */
|
||||||
|
#MainMenu {visibility: hidden;}
|
||||||
|
footer {visibility: hidden;}
|
||||||
|
|
||||||
|
/* Improve spacing */
|
||||||
|
[data-testid="column"] {
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 1rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Video URL input container */
|
||||||
|
.video-input-container {
|
||||||
|
background-color: var(--card-bg);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Summary results container */
|
||||||
|
.results-container {
|
||||||
|
background-color: var(--card-bg);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
margin-top: 2rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
""",
|
""",
|
||||||
unsafe_allow_html=True,
|
unsafe_allow_html=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -229,62 +316,28 @@ def summarize_video(
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
# Remove the existing title
|
# Settings section
|
||||||
# st.title("YouTube Video Summarizer")
|
st.write("## AI Video Summarizer")
|
||||||
|
|
||||||
# Add input for custom Ollama URL
|
# Ollama Settings - single card
|
||||||
default_ollama_url = os.getenv("OLLAMA_URL")
|
with st.container():
|
||||||
ollama_url = st.text_input(
|
st.subheader("🎯 Ollama Settings")
|
||||||
"Ollama URL (optional)",
|
default_ollama_url = os.getenv("OLLAMA_URL")
|
||||||
value=default_ollama_url,
|
ollama_url = st.text_input(
|
||||||
placeholder="Enter custom Ollama URL",
|
"Ollama URL",
|
||||||
)
|
value=default_ollama_url,
|
||||||
|
placeholder="Enter Ollama URL",
|
||||||
if not ollama_url:
|
|
||||||
ollama_url = default_ollama_url
|
|
||||||
|
|
||||||
# Fetch available models using the specified Ollama URL
|
|
||||||
available_models = get_ollama_models(ollama_url)
|
|
||||||
default_model = os.getenv("OLLAMA_MODEL")
|
|
||||||
|
|
||||||
if not default_model in available_models:
|
|
||||||
available_models.append(default_model)
|
|
||||||
|
|
||||||
# Sets whisper options
|
|
||||||
default_whisper_url = os.getenv("WHISPER_URL")
|
|
||||||
whisper_url = st.text_input(
|
|
||||||
"Whisper URL (optional)",
|
|
||||||
value=default_whisper_url,
|
|
||||||
placeholder="Enter custom Whisper URL",
|
|
||||||
)
|
|
||||||
if not whisper_url:
|
|
||||||
whisper_url = default_whisper_url
|
|
||||||
whisper_model = os.getenv("WHISPER_MODEL")
|
|
||||||
if not whisper_model:
|
|
||||||
whisper_model = "Systran/faster-whisper-large-v3"
|
|
||||||
st.caption(f"Whisper model: {whisper_model}")
|
|
||||||
|
|
||||||
# Create model selection dropdown
|
|
||||||
# selected_model = st.selectbox(
|
|
||||||
# "Select Ollama Model",
|
|
||||||
# options=available_models,
|
|
||||||
# index=(
|
|
||||||
# available_models.index(default_model)
|
|
||||||
# if default_model in available_models
|
|
||||||
# else 0
|
|
||||||
# ),
|
|
||||||
# )
|
|
||||||
|
|
||||||
# Use columns for URL and model inputs
|
|
||||||
col1, col2 = st.columns([2, 1])
|
|
||||||
with col1:
|
|
||||||
video_url = st.text_input(
|
|
||||||
"Enter the YouTube video URL:",
|
|
||||||
placeholder="https://www.youtube.com/watch?v=...",
|
|
||||||
)
|
)
|
||||||
with col2:
|
if not ollama_url:
|
||||||
|
ollama_url = default_ollama_url
|
||||||
|
|
||||||
|
available_models = get_ollama_models(ollama_url)
|
||||||
|
default_model = os.getenv("OLLAMA_MODEL")
|
||||||
|
if default_model not in available_models:
|
||||||
|
available_models.append(default_model)
|
||||||
|
|
||||||
selected_model = st.selectbox(
|
selected_model = st.selectbox(
|
||||||
"Select Ollama Model",
|
"Model",
|
||||||
options=available_models,
|
options=available_models,
|
||||||
index=(
|
index=(
|
||||||
available_models.index(default_model)
|
available_models.index(default_model)
|
||||||
@ -293,89 +346,107 @@ def main():
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Group Ollama and Whisper settings
|
# Video URL input section
|
||||||
with st.expander("Advanced Settings"):
|
with st.container():
|
||||||
col1, col2 = st.columns(2)
|
url_col, button_col = st.columns([4, 1])
|
||||||
with col1:
|
|
||||||
ollama_url = st.text_input(
|
with url_col:
|
||||||
"Ollama URL",
|
video_url = st.text_input(
|
||||||
value=default_ollama_url,
|
"🎥 Video URL",
|
||||||
placeholder="Enter custom Ollama URL",
|
placeholder="https://www.youtube.com/watch?v=...",
|
||||||
)
|
|
||||||
with col2:
|
|
||||||
whisper_url = st.text_input(
|
|
||||||
"Whisper URL",
|
|
||||||
value=default_whisper_url,
|
|
||||||
placeholder="Enter custom Whisper URL",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
col1, col2 = st.columns(2)
|
with button_col:
|
||||||
with col1:
|
summarize_button = st.button("🚀 Summarize", use_container_width=True)
|
||||||
|
|
||||||
|
# Advanced settings in collapsible sections
|
||||||
|
with st.expander("⚙️ Advanced Settings"):
|
||||||
|
# Whisper Settings
|
||||||
|
st.subheader("🎤 Whisper Settings")
|
||||||
|
default_whisper_url = os.getenv("WHISPER_URL")
|
||||||
|
whisper_url = st.text_input(
|
||||||
|
"Whisper URL",
|
||||||
|
value=default_whisper_url,
|
||||||
|
placeholder="Enter Whisper URL",
|
||||||
|
)
|
||||||
|
if not whisper_url:
|
||||||
|
whisper_url = default_whisper_url
|
||||||
|
|
||||||
|
whisper_model = os.getenv("WHISPER_MODEL")
|
||||||
|
if not whisper_model:
|
||||||
|
whisper_model = "Systran/faster-whisper-large-v3"
|
||||||
|
st.caption(f"Current model: {whisper_model}")
|
||||||
|
|
||||||
|
st.markdown("<br>", unsafe_allow_html=True) # Add some spacing
|
||||||
|
|
||||||
|
# Whisper Options
|
||||||
|
adv_col1, adv_col2 = st.columns(2)
|
||||||
|
with adv_col1:
|
||||||
force_whisper = st.checkbox("Force Whisper", value=False)
|
force_whisper = st.checkbox("Force Whisper", value=False)
|
||||||
with col2:
|
with adv_col2:
|
||||||
fallback_to_whisper = st.checkbox("Fallback to Whisper", value=True)
|
fallback_to_whisper = st.checkbox("Fallback to Whisper", value=True)
|
||||||
|
|
||||||
# Support any video that has a valid YouTube ID
|
if summarize_button and video_url:
|
||||||
if not "https://www.youtube.com/watch?v=" or "https://youtu.be/" in video_url:
|
summary = summarize_video(
|
||||||
if "watch?v=" in video_url:
|
video_url,
|
||||||
st.warning(
|
selected_model,
|
||||||
"This is not a YouTube URL. Might be a privacy-fronted embed. Trying to extract the YouTube ID..."
|
ollama_url,
|
||||||
)
|
fallback_to_whisper=fallback_to_whisper,
|
||||||
video_id = video_url.split("watch?v=")[-1]
|
force_whisper=force_whisper,
|
||||||
video_url = f"https://www.youtube.com/watch?v={video_id}"
|
)
|
||||||
else:
|
|
||||||
st.error("Please enter a valid YouTube video URL.")
|
|
||||||
return
|
|
||||||
# Support short urls as well
|
|
||||||
if "https://youtu.be/" in video_url:
|
|
||||||
video_id = video_url.split("youtu.be/")[-1]
|
|
||||||
video_url = f"https://www.youtube.com/watch?v={video_id}"
|
|
||||||
|
|
||||||
if st.button("Summarize"):
|
# Video Information
|
||||||
if video_url:
|
st.subheader("📺 Video Information")
|
||||||
summary = summarize_video(
|
info_col1, info_col2 = st.columns(2)
|
||||||
video_url,
|
with info_col1:
|
||||||
selected_model,
|
|
||||||
ollama_url,
|
|
||||||
fallback_to_whisper=fallback_to_whisper,
|
|
||||||
force_whisper=force_whisper,
|
|
||||||
)
|
|
||||||
st.subheader("Video Information:")
|
|
||||||
st.write(f"**Title:** {summary['title']}")
|
st.write(f"**Title:** {summary['title']}")
|
||||||
|
with info_col2:
|
||||||
st.write(f"**Channel:** {summary['channel']}")
|
st.write(f"**Channel:** {summary['channel']}")
|
||||||
|
|
||||||
st.subheader("Summary:")
|
# Transcript Section
|
||||||
st.write(summary["summary"])
|
with st.expander("📝 Original Transcript", expanded=False):
|
||||||
|
col1, col2 = st.columns([3, 1])
|
||||||
|
with col1:
|
||||||
|
st.text_area(
|
||||||
|
"Raw Transcript",
|
||||||
|
summary["transcript"],
|
||||||
|
height=200,
|
||||||
|
disabled=True,
|
||||||
|
)
|
||||||
|
with col2:
|
||||||
|
if st.button("🔄 Rephrase"):
|
||||||
|
with st.spinner("Rephrasing transcript..."):
|
||||||
|
ollama_client = OllamaClient(ollama_url, selected_model)
|
||||||
|
prompt = f"Rephrase the following transcript to make it more readable and well-formatted, keeping the main content intact:\n\n{summary['transcript']}"
|
||||||
|
rephrased = ollama_client.generate(prompt)
|
||||||
|
st.markdown(rephrased)
|
||||||
|
|
||||||
st.subheader("Original Transcript:")
|
if st.button("📋 Share"):
|
||||||
st.text_area(
|
try:
|
||||||
"Full Transcript", summary["transcript"], height=300, disabled=True
|
content = f"""Video Title: {summary['title']}
|
||||||
)
|
|
||||||
|
|
||||||
# Share button moved here, after the transcript
|
|
||||||
if st.button("Share Transcript"):
|
|
||||||
try:
|
|
||||||
content = f"""Video Title: {summary['title']}
|
|
||||||
Channel: {summary['channel']}
|
Channel: {summary['channel']}
|
||||||
URL: {video_url}
|
URL: {video_url}
|
||||||
|
|
||||||
--- Transcript ---
|
--- Transcript ---
|
||||||
|
|
||||||
{summary['transcript']}"""
|
{summary['transcript']}"""
|
||||||
|
paste_url = create_paste(
|
||||||
paste_url = create_paste(f"Transcript: {summary['title']}", content)
|
f"Transcript: {summary['title']}", content
|
||||||
st.success(
|
|
||||||
f"Transcript shared successfully! [View here]({paste_url})"
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
if "PASTEBIN_API_KEY" not in os.environ:
|
|
||||||
st.warning(
|
|
||||||
"PASTEBIN_API_KEY not found in environment variables"
|
|
||||||
)
|
)
|
||||||
else:
|
st.success(
|
||||||
st.error(f"Error sharing transcript: {str(e)}")
|
f"Transcript shared successfully! [View here]({paste_url})"
|
||||||
else:
|
)
|
||||||
st.error("Please enter a valid YouTube video URL.")
|
except Exception as e:
|
||||||
|
if "PASTEBIN_API_KEY" not in os.environ:
|
||||||
|
st.warning(
|
||||||
|
"PASTEBIN_API_KEY not found in environment variables"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
st.error(f"Error sharing transcript: {str(e)}")
|
||||||
|
|
||||||
|
# Summary Section
|
||||||
|
st.subheader("📊 AI Summary")
|
||||||
|
st.markdown(summary["summary"])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
@ -5,32 +5,54 @@ from pytubefix.cli import on_progress
|
|||||||
https://www.youtube.com/watch?v=vwTDiLH6mqg
|
https://www.youtube.com/watch?v=vwTDiLH6mqg
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def download_audio(url):
|
def download_audio(url):
|
||||||
yt = YouTube(url, on_progress_callback=on_progress)
|
try:
|
||||||
audio, video = itags(yt, "1080p") # specify the resolution
|
# Create YouTube object with bot detection bypass
|
||||||
yt.streams.get_by_itag(audio).download("downloads","output.m4a") # downloads audio
|
yt = YouTube(
|
||||||
|
url,
|
||||||
|
on_progress_callback=on_progress,
|
||||||
|
use_oauth=True,
|
||||||
|
allow_oauth_cache=True,
|
||||||
|
use_po_token=True, # Add this to bypass bot detection
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get audio stream
|
||||||
|
audio_stream = yt.streams.filter(only_audio=True).order_by("abr").desc().first()
|
||||||
|
if not audio_stream:
|
||||||
|
raise Exception("No audio stream found")
|
||||||
|
|
||||||
|
# Download audio
|
||||||
|
audio_stream.download("downloads", "output.m4a")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error in download_audio: {str(e)}")
|
||||||
|
raise Exception(f"Download failed: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
def itags(yt: YouTube, resolution="1080p"):
|
def itags(yt: YouTube, resolution="1080p"):
|
||||||
max_audio = 0
|
|
||||||
audio_value = 0
|
|
||||||
for audio_stream in yt.streams.filter(only_audio=True):
|
|
||||||
abr = int(audio_stream.abr.replace("kbps", ""))
|
|
||||||
if abr > max_audio:
|
|
||||||
max_audio = abr
|
|
||||||
audio_value = audio_stream.itag
|
|
||||||
streams = yt.streams
|
|
||||||
try:
|
try:
|
||||||
video_tag = streams.filter(res=resolution, fps=60)[0].itag
|
# Get best audio stream
|
||||||
print("60 FPS")
|
audio_stream = yt.streams.filter(only_audio=True).order_by("abr").desc().first()
|
||||||
except IndexError:
|
audio_value = audio_stream.itag if audio_stream else None
|
||||||
video_tag = streams.filter(res=resolution, fps=30)
|
|
||||||
if video_tag:
|
|
||||||
video_tag = video_tag[0].itag
|
|
||||||
print("30 FPS")
|
|
||||||
else:
|
|
||||||
video_tag = streams.filter(res=resolution, fps=24)[0].itag
|
|
||||||
print("24 FPS")
|
|
||||||
return audio_value, video_tag
|
|
||||||
|
|
||||||
|
# Get video stream
|
||||||
|
video_stream = None
|
||||||
|
for fps in [60, 30, 24]:
|
||||||
|
try:
|
||||||
|
video_stream = yt.streams.filter(res=resolution, fps=fps).first()
|
||||||
|
if video_stream:
|
||||||
|
print(f"Found {fps} FPS stream")
|
||||||
|
break
|
||||||
|
except IndexError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not video_stream:
|
||||||
|
raise Exception(f"No video stream found for resolution {resolution}")
|
||||||
|
|
||||||
|
return audio_value, video_stream.itag
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error in itags: {str(e)}")
|
||||||
|
raise Exception(f"Stream selection failed: {str(e)}")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user