import streamlit as st
import tempfile
import os
import torch
from transformers import AutoProcessor, AutoModelForSpeechSeq2Seq, AutoTokenizer, AutoModelForSeq2SeqLM
import librosa
import numpy as np
import ffmpeg
import time
import json
import psutil
st.set_page_config(
layout="wide",
page_title="VidEp β AI Video Intelligence",
page_icon="π¬",
initial_sidebar_state="collapsed"
)
# βββ DESIGN SYSTEM ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Palette:
# --bg-base: #0d1117 (near-black navy)
# --bg-surface: #161b22 (card surface)
# --bg-elevated: #21262d (elevated / hover)
# --border: #30363d (subtle borders)
# --accent: #3b82f6 (electric blue)
# --accent-light: #60a5fa
# --accent-glow: rgba(59,130,246,0.15)
# --text-primary: #f0f6fc
# --text-secondary:#8b949e
# --text-muted: #484f58
# --success: #3fb950
# --warning: #d29922
# --danger: #f85149
# Typography: Inter (Google Fonts)
# Signature: animated waveform SVG in hero
st.markdown("""
""", unsafe_allow_html=True)
# βββ UTILITIES βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def format_time(seconds):
minutes = int(seconds // 60)
secs = int(seconds % 60)
return f"{minutes}:{secs:02d}"
def seconds_to_srt_time(seconds):
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
secs = int(seconds % 60)
millis = int((seconds - int(seconds)) * 1000)
return f"{hours:02d}:{minutes:02d}:{secs:02d},{millis:03d}"
# βββ MODEL LOADING ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class TranscriptionProgress:
def __init__(self):
self.progress_bar = None
self.status_text = None
def init_progress(self):
self.progress_bar = st.progress(0.0)
self.status_text = st.empty()
def update(self, progress: float, status: str):
progress = max(0.0, min(1.0, progress))
if self.progress_bar is not None:
self.progress_bar.progress(progress)
if self.status_text is not None:
self.status_text.text(status)
@st.cache_resource
def load_model(language='en', summarizer_type='bart'):
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
if language == 'ur':
processor = AutoProcessor.from_pretrained("GogetaBlueMUI/whisper-medium-ur-fleurs-v2")
model = AutoModelForSpeechSeq2Seq.from_pretrained("GogetaBlueMUI/whisper-medium-ur-fleurs-v2").to(device)
else:
processor = AutoProcessor.from_pretrained("openai/whisper-small")
model = AutoModelForSpeechSeq2Seq.from_pretrained("openai/whisper-small").to(device)
if device.type == "cuda":
model = model.half()
if summarizer_type == 'bart':
sum_tokenizer = AutoTokenizer.from_pretrained("facebook/bart-large-cnn")
sum_model = AutoModelForSeq2SeqLM.from_pretrained("facebook/bart-large-cnn").to(device)
else:
sum_tokenizer = AutoTokenizer.from_pretrained("pszemraj/led-large-book-summary")
sum_model = AutoModelForSeq2SeqLM.from_pretrained("pszemraj/led-large-book-summary").to(device)
return processor, model, sum_tokenizer, sum_model, device
def split_audio_into_chunks(audio, sr, chunk_duration):
chunk_samples = int(chunk_duration * sr)
return [audio[start:start + chunk_samples] for start in range(0, len(audio), chunk_samples)]
def transcribe_audio(audio, sr, processor, model, device, start_time, language, task="transcribe"):
inputs = processor(audio, sampling_rate=sr, return_tensors="pt")
input_features = inputs.input_features.to(device)
if model.dtype == torch.float16:
input_features = input_features.half()
generate_kwargs = {
"task": task,
"language": "urdu" if language == "ur" else language,
"max_new_tokens": 128,
"return_timestamps": True
}
try:
with torch.no_grad():
outputs = model.generate(input_features, **generate_kwargs)
text = processor.decode(outputs[0], skip_special_tokens=True)
return [(text, start_time, start_time + len(audio) / sr)]
except Exception as e:
st.error(f"Transcription error: {str(e)}")
return [(f"Error: {str(e)}", start_time, start_time + len(audio) / sr)]
def process_chunks(chunks, sr, processor, model, device, language, chunk_duration, task="transcribe", transcript_file="temp_transcript.json"):
transcript = []
chunk_start = 0
total_chunks = len(chunks)
progress_bar = st.progress(0)
status_text = st.empty()
if os.path.exists(transcript_file):
os.remove(transcript_file)
for i, chunk in enumerate(chunks):
status_text.markdown(
f' Chunk {i+1} / {total_chunks} Β· '
f'{psutil.virtual_memory().percent:.1f}% RAM ',
unsafe_allow_html=True
)
try:
chunk_transcript = transcribe_audio(chunk, sr, processor, model, device, chunk_start, language, task)
transcript.extend(chunk_transcript)
with open(transcript_file, "w", encoding="utf-8") as f:
json.dump(transcript, f, ensure_ascii=False)
chunk_start += chunk_duration
progress_bar.progress((i + 1) / total_chunks)
except Exception as e:
st.error(f"Error processing chunk {i+1}: {str(e)}")
break
status_text.empty()
progress_bar.empty()
return transcript
def summarize_text(text, tokenizer, model, device, summarizer_type='bart'):
if summarizer_type == 'bart':
max_input_length = 1024
max_summary_length = 150
chunk_size = 512
else:
max_input_length = 16384
max_summary_length = 512
chunk_size = 8192
inputs = tokenizer(text, return_tensors="pt", truncation=False)
input_ids = inputs["input_ids"].to(device)
num_tokens = input_ids.shape[1]
if num_tokens < 50:
return "Transcript too short to summarize effectively."
try:
summaries = []
if num_tokens <= max_input_length:
truncated_inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=max_input_length).to(device)
with torch.no_grad():
summary_ids = model.generate(truncated_inputs["input_ids"], num_beams=4, max_length=max_summary_length, min_length=50, early_stopping=True, temperature=0.7)
summaries.append(tokenizer.decode(summary_ids[0], skip_special_tokens=True))
else:
tokens = input_ids[0].tolist()
for i in range(0, num_tokens, chunk_size):
chunk_tokens = tokens[i:i + chunk_size]
chunk_input_ids = torch.tensor([chunk_tokens]).to(device)
with torch.no_grad():
summary_ids = model.generate(chunk_input_ids, num_beams=4, max_length=max_summary_length // 2, min_length=25, early_stopping=True, temperature=0.7)
summaries.append(tokenizer.decode(summary_ids[0], skip_special_tokens=True))
combined_summary = " ".join(summaries)
combined_inputs = tokenizer(combined_summary, return_tensors="pt", truncation=True, max_length=max_input_length).to(device)
with torch.no_grad():
final_summary_ids = model.generate(combined_inputs["input_ids"], num_beams=4, max_length=max_summary_length, min_length=50, early_stopping=True, temperature=0.7)
summaries = [tokenizer.decode(final_summary_ids[0], skip_special_tokens=True)]
return " ".join(summaries)
except Exception as e:
st.error(f"Summarization error: {str(e)}")
return f"Error: {str(e)}"
def save_uploaded_file(uploaded_file):
try:
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as tmp_file:
tmp_file.write(uploaded_file.read())
return tmp_file.name
except Exception as e:
st.error(f"Error saving uploaded file: {str(e)}")
return None
def merge_intervals(intervals):
if not intervals:
return []
intervals.sort(key=lambda x: x[0])
merged = [intervals[0]]
for current in intervals[1:]:
previous = merged[-1]
if previous[1] >= current[0]:
merged[-1] = (previous[0], max(previous[1], current[1]))
else:
merged.append(current)
return merged
def create_edited_video(video_path, transcript, keep_indices):
try:
intervals_to_keep = [(transcript[i][1], transcript[i][2]) for i in keep_indices]
merged_intervals = merge_intervals(intervals_to_keep)
temp_files = []
for j, (start, end) in enumerate(merged_intervals):
temp_file = f"temp_{j}.mp4"
ffmpeg.input(video_path, ss=start, to=end).output(temp_file, c='copy').run(overwrite_output=True, quiet=True)
temp_files.append(temp_file)
with open("list.txt", "w") as f:
for temp_file in temp_files:
f.write(f"file '{temp_file}'\n")
edited_video_path = "edited_video.mp4"
ffmpeg.input('list.txt', format='concat', safe=0).output(edited_video_path, c='copy').run(overwrite_output=True, quiet=True)
for temp_file in temp_files:
if os.path.exists(temp_file):
os.remove(temp_file)
if os.path.exists("list.txt"):
os.remove("list.txt")
return edited_video_path
except Exception as e:
st.error(f"Error creating edited video: {str(e)}")
return None
def generate_srt(transcript, include_timeframe=True):
srt_content = ""
for text, start, end in transcript:
if include_timeframe:
start_time = seconds_to_srt_time(start)
end_time = seconds_to_srt_time(end)
srt_content += f"{start_time} --> {end_time}\n{text}\n\n"
else:
srt_content += f"{text}\n\n"
return srt_content
# βββ MAIN βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def main():
# ββ NAV βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
st.markdown("""
""", unsafe_allow_html=True)
# ββ HERO ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
st.markdown('
', unsafe_allow_html=True)
st.markdown("""
Powered by Whisper & BART
Video intelligence,zero friction
Transcribe, translate, and summarize any video in seconds. Edit subtitles, search content, and export β all in one place.
English & Urdu
Smart Search
AI Summary
Video Editing
SRT Export
""", unsafe_allow_html=True)
# ββ SESSION STATE ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
defaults = {
'app_state': 'upload',
'video_path': None,
'primary_transcript': None,
'english_transcript': None,
'english_summary': None,
'language': None,
'language_code': None,
'translate_to_english': False,
'summarizer_type': None,
'summary_generated': False,
'current_time': 0,
'edited_video_path': None,
'search_query': "",
'show_timeframe': True,
}
for k, v in defaults.items():
if k not in st.session_state:
st.session_state[k] = v
# ββ UPLOAD βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if st.session_state['app_state'] == 'upload':
st.markdown('
', unsafe_allow_html=True)
col_l, col_c, col_r = st.columns([1, 2, 1])
with col_c:
st.markdown('Get started
', unsafe_allow_html=True)
st.markdown("""
Upload your video
MP4 files supported Β· Transcription begins automatically
""", unsafe_allow_html=True)
with st.form(key="upload_form"):
st.markdown("""
πΉ
Drag and drop or click to browse
""", unsafe_allow_html=True)
uploaded_file = st.file_uploader(
"Choose a video file",
type=["mp4"],
label_visibility="collapsed"
)
st.markdown(" ", unsafe_allow_html=True)
submitted = st.form_submit_button("Upload & Continue β", use_container_width=True)
if submitted and uploaded_file:
with st.spinner("Saving fileβ¦"):
video_path = save_uploaded_file(uploaded_file)
if video_path:
st.session_state['video_path'] = video_path
st.session_state['app_state'] = 'processing'
st.rerun()
elif submitted:
st.warning("Please select a video file first.")
# ββ PROCESSING CONFIG ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if st.session_state['app_state'] == 'processing':
col_l, col_c, col_r = st.columns([1, 2, 1])
with col_c:
st.markdown("""
Configure & Process
Set transcription options, then run the pipeline.
""", unsafe_allow_html=True)
with st.form(key="processing_form"):
language = st.selectbox(
"Transcription Language",
["English", "Urdu"]
)
language_code = "en" if language == "English" else "ur"
st.session_state['language'] = language
st.session_state['language_code'] = language_code
chunk_duration = st.number_input(
"Chunk duration (seconds)",
min_value=1.0,
step=0.5,
value=10.0,
help="Shorter chunks = more granular subtitles, longer processing time."
)
if language_code == "ur":
translate_to_english = st.checkbox(
"Also generate English translation",
)
st.session_state['translate_to_english'] = translate_to_english
else:
st.session_state['translate_to_english'] = False
st.markdown(" ", unsafe_allow_html=True)
process_btn = st.form_submit_button("Run Transcription β", use_container_width=True)
if process_btn:
st.markdown('', unsafe_allow_html=True)
# Step indicator
st.markdown("""
""", unsafe_allow_html=True)
with st.spinner("Extracting audioβ¦"):
try:
audio_path = "processed_audio.wav"
ffmpeg.input(st.session_state['video_path']).output(
audio_path, ac=1, ar=16000
).run(overwrite_output=True, quiet=True)
audio, sr = librosa.load(audio_path, sr=16000)
audio = np.nan_to_num(audio, nan=0.0, posinf=0.0, neginf=0.0)
audio_duration = len(audio) / sr
if audio_duration < 5:
st.error("Audio is too short (< 5 seconds). Please upload a longer video.")
st.stop()
summarizer_type = 'bart' if audio_duration <= 300 else 'led'
st.session_state['summarizer_type'] = summarizer_type
col1, col2, col3 = st.columns(3)
with col1:
st.markdown(f'
Duration
{int(audio_duration)}s
', unsafe_allow_html=True)
with col2:
st.markdown(f'
Summarizer
{summarizer_type.upper()}
', unsafe_allow_html=True)
with col3:
st.markdown(f'
Device
{"GPU" if torch.cuda.is_available() else "CPU"}
', unsafe_allow_html=True)
st.info("Loading AI models⦠This may take a moment on first run.")
processor, model, sum_tokenizer, sum_model, device = load_model(language_code, summarizer_type)
st.markdown('
Transcribing
', unsafe_allow_html=True)
chunks = split_audio_into_chunks(audio, sr, chunk_duration)
primary_transcript = process_chunks(
chunks, sr, processor, model, device,
language_code, chunk_duration,
task="transcribe",
transcript_file="temp_primary_transcript.json"
)
english_transcript = None
if st.session_state['translate_to_english'] and language_code == "ur":
st.markdown('
Translating to English
', unsafe_allow_html=True)
processor, model, _, _, device = load_model('en', summarizer_type)
english_transcript = process_chunks(
chunks, sr, processor, model, device,
'ur', chunk_duration,
task="translate",
transcript_file="temp_english_transcript.json"
)
st.session_state.update({
'primary_transcript': primary_transcript,
'english_transcript': english_transcript,
'summary_generated': False,
'app_state': 'results'
})
st.success("Processing complete! Loading resultsβ¦")
time.sleep(0.8)
st.rerun()
except Exception as e:
st.error(f"Processing failed: {str(e)}")
finally:
if os.path.exists(audio_path):
os.remove(audio_path)
for temp_file in ["temp_primary_transcript.json", "temp_english_transcript.json"]:
if os.path.exists(temp_file):
os.remove(temp_file)
st.markdown('
', unsafe_allow_html=True)
# ββ RESULTS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if st.session_state['app_state'] == 'results':
# Step indicator
st.markdown("""
""", unsafe_allow_html=True)
# Two-column layout: video + controls
left_col, right_col = st.columns([1, 1], gap="large")
with left_col:
st.markdown('Preview
', unsafe_allow_html=True)
st.video(st.session_state['video_path'], start_time=st.session_state['current_time'])
# Stats
if st.session_state['primary_transcript']:
n = len(st.session_state['primary_transcript'])
total_dur = st.session_state['primary_transcript'][-1][2] if n > 0 else 0
st.markdown(f"""
Duration
{format_time(total_dur)}
Language
{st.session_state['language']}
""", unsafe_allow_html=True)
# Actions
st.markdown('Actions
', unsafe_allow_html=True)
inc_tf = st.checkbox("Include timestamps in export", value=True)
transcript_to_dl = st.session_state['primary_transcript'] or st.session_state['english_transcript']
if transcript_to_dl:
srt_content = generate_srt(transcript_to_dl, inc_tf)
st.download_button(
label="β¬ Download Subtitles (.srt)",
data=srt_content,
file_name="subtitles.srt",
mime="text/plain",
use_container_width=True
)
st.markdown(" ", unsafe_allow_html=True)
transcript_to_edit = st.session_state['primary_transcript'] or st.session_state['english_transcript']
if transcript_to_edit:
with st.container():
st.markdown('', unsafe_allow_html=True)
if st.button("β Edit & Remove Segments", use_container_width=True):
st.session_state['app_state'] = 'editing'
st.rerun()
st.markdown('
', unsafe_allow_html=True)
# Summary
if (st.session_state['language_code'] == 'en' or st.session_state['translate_to_english']) \
and not st.session_state['summary_generated']:
st.markdown(" ", unsafe_allow_html=True)
if st.button("β¦ Generate AI Summary", use_container_width=True):
with st.spinner("Summarizing transcriptβ¦"):
try:
_, _, sum_tokenizer, sum_model, device = load_model(
st.session_state['language_code'],
st.session_state['summarizer_type']
)
full_text = " ".join([
t for t, _, _ in
(st.session_state['english_transcript'] or st.session_state['primary_transcript'])
])
english_summary = summarize_text(
full_text, sum_tokenizer, sum_model, device,
st.session_state['summarizer_type']
)
st.session_state['english_summary'] = english_summary
st.session_state['summary_generated'] = True
st.rerun()
except Exception as e:
st.error(f"Summary generation failed: {str(e)}")
with right_col:
st.markdown('Transcript
', unsafe_allow_html=True)
st.session_state['show_timeframe'] = st.checkbox(
"Show timestamps", value=st.session_state['show_timeframe']
)
def update_search():
st.session_state['search_query'] = st.session_state.get('search_input', '').lower().strip()
st.text_input(
"Search subtitlesβ¦",
value=st.session_state['search_query'],
key="search_input",
on_change=update_search,
placeholder="Type to filter segmentsβ¦"
)
# Primary transcript
st.markdown(f"""
""", unsafe_allow_html=True)
primary_matches = 0
for text, start, end in st.session_state['primary_transcript']:
if not st.session_state['search_query'] or st.session_state['search_query'] in text.lower():
primary_matches += 1
if st.session_state['show_timeframe']:
label = f"[{format_time(start)}β{format_time(end)}] {text}"
else:
label = text
if st.button(label, key=f"primary_{start}"):
st.session_state['current_time'] = start
st.rerun()
if primary_matches == 0 and st.session_state['search_query']:
st.info("No matches found in this transcript.")
# English translation
if st.session_state['english_transcript']:
st.markdown("""
""", unsafe_allow_html=True)
for text, start, end in st.session_state['english_transcript']:
if not st.session_state['search_query'] or st.session_state['search_query'] in text.lower():
if st.session_state['show_timeframe']:
label = f"[{format_time(start)}β{format_time(end)}] {text}"
else:
label = text
if st.button(label, key=f"english_{start}"):
st.session_state['current_time'] = start
st.rerun()
# Summary card (full width)
if st.session_state['english_summary'] and st.session_state['summary_generated']:
st.markdown('
', unsafe_allow_html=True)
st.markdown("""
AI Summary
""", unsafe_allow_html=True)
st.markdown(
f'{st.session_state["english_summary"]}
',
unsafe_allow_html=True
)
# Edited video
if st.session_state['edited_video_path']:
st.markdown('
', unsafe_allow_html=True)
st.markdown("""
Edited Video
""", unsafe_allow_html=True)
col_v, col_d = st.columns([2, 1])
with col_v:
st.video(st.session_state['edited_video_path'])
with col_d:
with open(st.session_state['edited_video_path'], "rb") as file:
st.download_button(
label="β¬ Download Edited Video",
data=file,
file_name="edited_video.mp4",
mime="video/mp4",
use_container_width=True
)
# ββ EDITING ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if st.session_state['app_state'] == 'editing':
st.markdown("""
Remove Segments
Enter segment indices to delete, then confirm. The video will be re-cut accordingly.
""", unsafe_allow_html=True)
transcript_to_edit = st.session_state['primary_transcript'] or st.session_state['english_transcript']
# Render transcript list
st.markdown('', unsafe_allow_html=True)
for i, (text, start, end) in enumerate(transcript_to_edit):
st.markdown(f"""
#{i}
{format_time(start)} β {format_time(end)}
{text}
""", unsafe_allow_html=True)
st.markdown('
', unsafe_allow_html=True)
st.markdown(" ", unsafe_allow_html=True)
indices_input = st.text_input(
"Indices to delete",
placeholder="e.g. 0, 2, 5",
help="Comma-separated segment numbers. Use the # values shown above."
)
col_a, col_b, _ = st.columns([1, 1, 2])
with col_a:
if st.button("Confirm Deletion", use_container_width=True):
try:
delete_indices = [int(idx.strip()) for idx in indices_input.split(',') if idx.strip()]
delete_indices = [idx for idx in delete_indices if 0 <= idx < len(transcript_to_edit)]
keep_indices = [i for i in range(len(transcript_to_edit)) if i not in delete_indices]
if not keep_indices:
st.error("All segments would be removed. Please keep at least one.")
else:
with st.spinner("Re-cutting videoβ¦"):
edited_video_path = create_edited_video(
st.session_state['video_path'], transcript_to_edit, keep_indices
)
if edited_video_path:
st.session_state['edited_video_path'] = edited_video_path
st.session_state['app_state'] = 'results'
st.rerun()
except ValueError:
st.error("Invalid input β please enter comma-separated integers.")
except Exception as e:
st.error(f"Error during video editing: {str(e)}")
with col_b:
st.markdown('', unsafe_allow_html=True)
if st.button("Cancel", use_container_width=True):
st.session_state['app_state'] = 'results'
st.rerun()
st.markdown('
', unsafe_allow_html=True)
# ββ RESET ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if st.session_state.get('video_path'):
st.markdown('
', unsafe_allow_html=True)
col_r, _ = st.columns([1, 3])
with col_r:
st.markdown('', unsafe_allow_html=True)
if st.button("β© Start Over", use_container_width=True):
if st.session_state['video_path'] and os.path.exists(st.session_state['video_path']):
os.remove(st.session_state['video_path'])
if st.session_state['edited_video_path'] and os.path.exists(st.session_state['edited_video_path']):
os.remove(st.session_state['edited_video_path'])
st.session_state.clear()
st.rerun()
st.markdown('
', unsafe_allow_html=True)
# ββ FEATURES SECTION βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
st.markdown('
', unsafe_allow_html=True)
st.markdown("""
Everything you need
One platform for all your video transcription needs.
Simple Upload
Drop any MP4 and start processing immediately. No account setup needed.
Smart Search
Instantly find any word or phrase across your entire transcript.
Segment Editing
Remove unwanted segments and export a clean, trimmed video.
AI Summary
Get a concise summary of any video powered by BART and LED models.
Multi-Language
Full support for English and Urdu, with automatic translation.
SRT Export
Download subtitle files compatible with any video player or editor.
""", unsafe_allow_html=True)
# ββ PLANS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
st.markdown('
', unsafe_allow_html=True)
st.markdown("""
Simple pricing
Start free. Scale when you need to.
Free
$0 / month
Perfect for trying out the platform.
Basic transcription
English only
Up to 5 min video
SRT export
Most popular
Premium
$19 / month
For creators and professionals.
Advanced transcription
English + Urdu
Up to 30 min video
AI summarization
Business
$49 / month
Enterprise-grade for teams.
Unlimited video length
All languages
Priority processing
API access
""", unsafe_allow_html=True)
# ββ ABOUT ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
st.markdown('
', unsafe_allow_html=True)
st.markdown("""
About VidEp
Built for the way people actually work with video
VidEp combines state-of-the-art speech recognition from OpenAI Whisper with powerful summarization models to give creators, journalists, and researchers an effortless way to unlock the content inside their videos.
We handle the heavy lifting β you focus on the insights.
OpenAI Whisper for superior accuracy
Multi-language support: English and Urdu
BART and LED for intelligent summarization
Non-destructive segment editing with FFmpeg
Works entirely in your browser β no data stored
""", unsafe_allow_html=True)
# ββ CONTACT ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
st.markdown('
', unsafe_allow_html=True)
st.markdown("""
""", unsafe_allow_html=True)
# ββ FOOTER βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
st.markdown("""
""", unsafe_allow_html=True)
if __name__ == "__main__":
main()