Spaces:
Runtime error
Runtime error
| 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") | |
| # CSS Styling | |
| st.markdown(""" | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600;700&display=swap'); | |
| .stApp { | |
| background-color: #ffffff; | |
| font-family: 'Poppins', sans-serif; | |
| color: #1a1a1a; | |
| } | |
| /* Hide Streamlit's default elements */ | |
| [data-testid="stToolbar"], [data-testid="stDecoration"], [data-testid="stStatusWidget"] { | |
| display: none; | |
| } | |
| /* Header */ | |
| .header { | |
| background: #ffffff; | |
| padding: 1rem 2rem; | |
| box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| position: sticky; | |
| top: 0; | |
| z-index: 100; | |
| } | |
| .logo img { | |
| height: 60px; | |
| width: auto; | |
| } | |
| .navbar { | |
| list-style: none; | |
| display: flex; | |
| gap: 1.5rem; | |
| margin: 0; | |
| } | |
| .navbar li a { | |
| text-decoration: none; | |
| font-size: 28px; | |
| font-weight: bold; | |
| color: #060404; | |
| position: relative; | |
| padding: 10px 15px; | |
| transition: text-shadow 0.3s ease-in-out; | |
| text-shadow: 5px 5px 12px rgba(0, 0, 0, 0.5); | |
| } | |
| .navbar li a:hover { | |
| color: #ff6f61; | |
| } | |
| /* Hero Section */ | |
| .hero { | |
| background: linear-gradient(to right, #2b5876, #4e4376); | |
| background-size: cover; | |
| color: #ffffff; | |
| padding: 2rem 2rem; | |
| border-radius: 1rem; | |
| text-align: center; | |
| margin: 2rem 0; | |
| max-height: 200px; | |
| } | |
| .hero h1 { | |
| font-size: 2.5rem; | |
| font-weight: 700; | |
| margin-bottom: 0.5rem; | |
| } | |
| .hero p { | |
| font-size: 1.2rem; | |
| font-weight: 300; | |
| } | |
| /* Feature Section */ | |
| .feature-box { | |
| display: flex; | |
| justify-content: center; | |
| gap: 1.5rem; | |
| margin: 3rem 0; | |
| flex-wrap: wrap; | |
| } | |
| .feature { | |
| background: #f8f9fa; | |
| padding: 1.5rem; | |
| border-radius: 1rem; | |
| box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); | |
| width: 200px; | |
| text-align: center; | |
| transition: transform 0.3s ease, box-shadow 0.3s ease; | |
| } | |
| .feature:hover { | |
| transform: translateY(-8px) scale(1.03); | |
| box-shadow: 0 12px 24px rgba(0, 0, 0, 0.25); | |
| transition: all 0.3s ease; | |
| border: 1px solid rgba(0, 0, 0, 0.1); | |
| background-color: #fff; | |
| filter: brightness(1.05); | |
| z-index: 10; | |
| } | |
| .feature i { | |
| font-size: 1.5rem; | |
| color: #2196f3; | |
| margin-bottom: 0.5rem; | |
| } | |
| /* Plans Section */ | |
| .plans { | |
| padding: 3rem 2rem; | |
| background: #f1f4f8; | |
| border-radius: 1rem; | |
| } | |
| .plan-box { | |
| display: flex; | |
| justify-content: center; | |
| gap: 1.5rem; | |
| flex-wrap: wrap; | |
| } | |
| .plan { | |
| background: #ffffff; | |
| padding: 2rem; | |
| border-radius: 1rem; | |
| width: 250px; | |
| text-align: center; | |
| box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); | |
| transition: transform 0.3s ease, box-shadow 0.3s ease; | |
| border-top: 4px solid #28a745; | |
| height: 290px; | |
| padding-top: 10px; | |
| } | |
| .plan:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 6px 15px rgba(0, 0, 0, 0.15); | |
| } | |
| .plan h3 { | |
| font-size: 1.5rem; | |
| margin-bottom: 0.5rem; | |
| } | |
| .plan.free { border-top: 4px solid #28a745; } | |
| .plan.premium { border-top: 4px solid #ff6f61; } | |
| .plan.business { border-top: 4px solid #2196f3; } | |
| /* Buttons */ | |
| .stButton>button { | |
| background: linear-gradient(135deg, #ff6f61, #ff8a65) !important; | |
| color: #ffffff !important; | |
| font-weight: 600 !important; | |
| padding: 0.75rem 1.5rem !important; | |
| border-radius: 0.5rem !important; | |
| border: none !important; | |
| transition: transform 0.2s ease, box-shadow 0.2s ease !important; | |
| } | |
| .stButton>button:hover { | |
| transform: translateY(-2px) !important; | |
| box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2) !important; | |
| } | |
| /* File Uploader */ | |
| .uploadedFile { | |
| border: 2px dashed #2196f3; | |
| border-radius: 1rem; | |
| padding: 2rem; | |
| background: #f8f9fa; | |
| margin: 2rem 0; | |
| } | |
| /* Progress Bar */ | |
| .stProgress > div > div { | |
| background: linear-gradient(90deg, #2196f3, #4fc3f7) !important; | |
| } | |
| /* Text Area */ | |
| .stTextArea textarea { | |
| border-radius: 0.5rem; | |
| border: 1px solid #e0e0e0; | |
| padding: 1rem; | |
| font-family: 'Poppins', sans-serif; | |
| } | |
| /* Video player styling */ | |
| video { | |
| display: block; | |
| width: 350px !important; | |
| height: 500px !important; | |
| object-fit: contain; | |
| margin: 0 auto; | |
| border: 3px solid #2196f3; | |
| border-radius: 8px; | |
| } | |
| /* Footer */ | |
| footer { | |
| background: #1a1a1a; | |
| color: #ffffff; | |
| padding: 3rem 2rem; | |
| margin-top: 3rem; | |
| border-radius: 1rem 1rem 0 0; | |
| } | |
| .footer-container { | |
| display: flex; | |
| justify-content: space-around; | |
| gap: 2rem; | |
| flex-wrap: wrap; | |
| } | |
| .footer-section h4 { | |
| font-size: 1.8rem; | |
| margin-bottom: 1rem; | |
| } | |
| .footer-section ul { | |
| list-style: none; | |
| padding: 0; | |
| } | |
| .footer-section ul li a { | |
| color: #bbbbbb; | |
| text-decoration: none; | |
| font-size: 1.6rem; | |
| transition: color 0.3s ease; | |
| } | |
| .footer-section ul li a:hover { | |
| color: #ff6f61; | |
| } | |
| .footer-bottom { | |
| margin-top: 2rem; | |
| font-size: 0.9rem; | |
| } | |
| /* Responsive Design */ | |
| @media (max-width: 768px) { | |
| .header { | |
| flex-direction: column; | |
| gap: 1rem; | |
| } | |
| .navbar { | |
| flex-direction: column; | |
| gap: 0.5rem; | |
| } | |
| .hero h1 { | |
| font-size: 1.8rem; | |
| } | |
| .hero p { | |
| font-size: 1rem; | |
| } | |
| .feature, .plan { | |
| width: 100%; | |
| max-width: 300px; | |
| } | |
| } | |
| </style> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"> | |
| """, unsafe_allow_html=True) | |
| # Function Definitions | |
| 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}" | |
| 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) | |
| 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) | |
| chunks = [audio[start:start + chunk_samples] for start in range(0, len(audio), chunk_samples)] | |
| return chunks | |
| 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.text(f"Processing chunk {i+1}/{total_chunks}...") | |
| try: | |
| memory = psutil.virtual_memory() | |
| st.write(f"Memory usage: {memory.percent}% (Chunk {i+1}/{total_chunks})") | |
| 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.text("Processing complete!") | |
| 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] | |
| st.write(f"Number of tokens in input: {num_tokens}") | |
| 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: | |
| st.write(f"Transcript exceeds {max_input_length} tokens. Processing in chunks...") | |
| 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 Function | |
| def main(): | |
| st.markdown(""" | |
| <div class="header"> | |
| <div class="logo"> | |
| <img src="https://i.postimg.cc/wvFfzx5h/VIDEpp.png"> | |
| </div> | |
| <ul class="navbar"> | |
| <li><a href="#home">Home</a></li> | |
| <li><a href="#upload">Upload Video</a></li> | |
| <li><a href="#about">About Us</a></li> | |
| <li><a href="#contact">Contact Us</a></li> | |
| </ul> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown(""" | |
| <div id="home" class="hero"> | |
| <h2>VidEp – Revolutionizing Video Subtitle Editing with AI</h2> | |
| <p>Upload, transcribe, edit subtitles, and summarize videos effortlessly.</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Initialize session state | |
| if 'app_state' not in st.session_state: | |
| st.session_state['app_state'] = 'upload' | |
| if 'video_path' not in st.session_state: | |
| st.session_state['video_path'] = None | |
| if 'primary_transcript' not in st.session_state: | |
| st.session_state['primary_transcript'] = None | |
| if 'english_transcript' not in st.session_state: | |
| st.session_state['english_transcript'] = None | |
| if 'english_summary' not in st.session_state: | |
| st.session_state['english_summary'] = None | |
| if 'language' not in st.session_state: | |
| st.session_state['language'] = None | |
| if 'language_code' not in st.session_state: | |
| st.session_state['language_code'] = None | |
| if 'translate_to_english' not in st.session_state: | |
| st.session_state['translate_to_english'] = False | |
| if 'summarizer_type' not in st.session_state: | |
| st.session_state['summarizer_type'] = None | |
| if 'summary_generated' not in st.session_state: | |
| st.session_state['summary_generated'] = False | |
| if 'current_time' not in st.session_state: | |
| st.session_state['current_time'] = 0 | |
| if 'edited_video_path' not in st.session_state: | |
| st.session_state['edited_video_path'] = None | |
| if 'search_query' not in st.session_state: | |
| st.session_state['search_query'] = "" | |
| if 'show_timeframe' not in st.session_state: | |
| st.session_state['show_timeframe'] = True | |
| if st.session_state['app_state'] == 'upload': | |
| st.markdown("<div id='upload'></div>", unsafe_allow_html=True) | |
| st.markdown("<h3 style='text-align: center; color: black;'>Upload Your Video</h3>", unsafe_allow_html=True) | |
| with st.form(key="upload_form"): | |
| uploaded_file = st.file_uploader("Choose a video file", type=["mp4"], label_visibility="collapsed") | |
| if st.form_submit_button("Upload") and uploaded_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.write(f"Uploaded file: {uploaded_file.name}") | |
| st.rerun() | |
| if st.session_state['app_state'] == 'processing': | |
| with st.form(key="processing_form"): | |
| language = st.selectbox("Select language", ["English", "Urdu"], key="language_select") | |
| 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("Duration per chunk (seconds):", min_value=1.0, step=0.1, value=10.0) | |
| if language_code == "ur": | |
| translate_to_english = st.checkbox("Generate English translation", key="translate_checkbox") | |
| st.session_state['translate_to_english'] = translate_to_english | |
| else: | |
| st.session_state['translate_to_english'] = False | |
| if st.form_submit_button("Process"): | |
| with st.spinner("Processing video..."): | |
| start_time = time.time() | |
| try: | |
| st.write("Extracting audio...") | |
| 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 | |
| st.write(f"Audio duration: {audio_duration:.2f} seconds") | |
| if audio_duration < 5: | |
| st.error("Audio too short (< 5s). Upload a longer video.") | |
| return | |
| summarizer_type = 'bart' if audio_duration <= 300 else 'led' | |
| st.write(f"Using summarizer: {summarizer_type}") | |
| st.session_state['summarizer_type'] = summarizer_type | |
| st.write("Loading models...") | |
| processor, model, sum_tokenizer, sum_model, device = load_model(language_code, summarizer_type) | |
| st.write("Splitting audio into chunks...") | |
| chunks = split_audio_into_chunks(audio, sr, chunk_duration) | |
| st.write(f"Number of chunks: {len(chunks)}") | |
| st.write("Transcribing audio...") | |
| 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.write("Translating to English...") | |
| 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.write("Processing completed successfully!") | |
| 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) | |
| if st.session_state['app_state'] == 'results': | |
| st.markdown('<div style="display: flex; justify-content: center;">', unsafe_allow_html=True) | |
| st.video(st.session_state['video_path'], start_time=st.session_state['current_time']) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| st.session_state['show_timeframe'] = st.checkbox("Show timeframe in transcript", value=st.session_state['show_timeframe']) | |
| st.markdown("### Search Subtitles") | |
| # Callback to handle search query updates | |
| def update_search_query(): | |
| st.session_state['search_query'] = st.session_state.get('search_input', '').lower().strip() | |
| # Text input with on_change callback | |
| st.text_input("Search subtitles...", value=st.session_state['search_query'], key="search_input", on_change=update_search_query) | |
| # Primary Transcript | |
| st.markdown(f"### {st.session_state['language']} Transcript") | |
| primary_matches = 0 | |
| for text, start, end in st.session_state['primary_transcript']: | |
| display_text = text.lower() # Case-insensitive comparison | |
| if not st.session_state['search_query'] or st.session_state['search_query'] in display_text: | |
| primary_matches += 1 | |
| label = f"[{format_time(start)} - {format_time(end)}] {text}" if st.session_state['show_timeframe'] else 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 primary transcript for the search query.") | |
| # English Transcript | |
| if st.session_state['english_transcript']: | |
| st.markdown("### English Translation") | |
| english_matches = 0 | |
| for text, start, end in st.session_state['english_transcript']: | |
| display_text = text.lower() # Case-insensitive comparison | |
| if not st.session_state['search_query'] or st.session_state['search_query'] in display_text: | |
| english_matches += 1 | |
| label = f"[{format_time(start)} - {format_time(end)}] {text}" if st.session_state['show_timeframe'] else text | |
| if st.button(label, key=f"english_{start}"): | |
| st.session_state['current_time'] = start | |
| st.rerun() | |
| if english_matches == 0 and st.session_state['search_query']: | |
| st.info("No matches found in English transcript for the search query.") | |
| # Summary Generation | |
| if (st.session_state['language_code'] == 'en' or st.session_state['translate_to_english']) and not st.session_state['summary_generated']: | |
| if st.button("Generate Summary"): | |
| with st.spinner("Generating summary..."): | |
| try: | |
| _, _, sum_tokenizer, sum_model, device = load_model(st.session_state['language_code'], st.session_state['summarizer_type']) | |
| full_text = " ".join([text for text, _, _ 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 | |
| except Exception as e: | |
| st.error(f"Summary generation failed: {str(e)}") | |
| if st.session_state['english_summary'] and st.session_state['summary_generated']: | |
| st.markdown("### Summary") | |
| st.write(st.session_state['english_summary']) | |
| # Download Subtitles | |
| st.markdown("### Download Subtitles") | |
| include_timeframe = st.checkbox("Include timeframe in subtitles", value=True) | |
| transcript_to_download = st.session_state['primary_transcript'] or st.session_state['english_transcript'] | |
| if transcript_to_download: | |
| srt_content = generate_srt(transcript_to_download, include_timeframe) | |
| st.download_button(label="Download Subtitles (SRT)", data=srt_content, file_name="subtitles.srt", mime="text/plain") | |
| # Edit Subtitles | |
| st.markdown("### Edit Subtitles") | |
| transcript_to_edit = st.session_state['primary_transcript'] or st.session_state['english_transcript'] | |
| if transcript_to_edit and st.button("Delete Subtitles"): | |
| st.session_state['app_state'] = 'editing' | |
| st.rerun() | |
| if st.session_state['app_state'] == 'editing': | |
| st.markdown("### Delete Subtitles") | |
| transcript_to_edit = st.session_state['primary_transcript'] or st.session_state['english_transcript'] | |
| for i, (text, start, end) in enumerate(transcript_to_edit): | |
| st.write(f"{i}: [{format_time(start)} - {format_time(end)}] {text}") | |
| indices_input = st.text_input("Enter the indices of subtitles to delete (comma-separated, e.g., 0,1,3):") | |
| if st.button("Confirm Deletion"): | |
| 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 subtitles are deleted. No video to generate.") | |
| else: | |
| 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)}") | |
| if st.button("Cancel Deletion"): | |
| st.session_state['app_state'] = 'results' | |
| st.rerun() | |
| if st.session_state['app_state'] == 'results' and st.session_state['edited_video_path']: | |
| st.markdown("### Edited Video") | |
| st.markdown('<div style="display: flex; justify-content: center;">', unsafe_allow_html=True) | |
| st.video(st.session_state['edited_video_path']) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| 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") | |
| if st.session_state.get('video_path') and st.button("Reset"): | |
| 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(""" | |
| <div style='text-align: center;'> | |
| <h2 style='color: black'>Why VidEp Stands Out</h2> | |
| </div> | |
| <div class="feature-box"> | |
| <div class="feature"><i class="fas fa-cloud-upload-alt"></i><br>Cloud Upload</div> | |
| <div class="feature"><i class="fas fa-search"></i><br>Smart Search</div> | |
| <div class="feature"><i class="fas fa-edit"></i><br>Easy Editing</div> | |
| <div class="feature"><i class="fas fa-file-alt"></i><br>AI Summary</div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown(""" | |
| <div id="about" class="about-section" style="padding: 3rem 2rem; background: #f8f9fa; border-radius: 1rem; margin: 2rem 0;"> | |
| <h2 style="text-align: center; color: black; margin-bottom: 2rem;">About VidEp</h2> | |
| <div style="display: flex; align-items: center; gap: 2rem; flex-wrap: wrap;"> | |
| <div style="flex: 1; min-width: 300px;"> | |
| <img src="https://i.postimg.cc/g0z3WVgT/about.jpg" style="width: 100%; height: auto; border-radius: 1rem;" alt="About VidEp"> | |
| </div> | |
| <div style="flex: 2; min-width: 300px;"> | |
| <h3 style="color:grey;">Our Mission</h3> | |
| <p>VidEp aims to revolutionize how creators and professionals work with video content by providing state-of-the-art AI-powered tools for transcription, translation, and summarization.</p> | |
| <h3 style="color:grey;">What We Do</h3> | |
| <p>Our platform combines the latest advancements in speech recognition and natural language processing to automatically transcribe videos in multiple languages, generate accurate translations, and create concise summaries of content.</p> | |
| <h3 style="color:grey;">Why Choose Us</h3> | |
| <ul> | |
| <li>Advanced AI models for superior accuracy</li> | |
| <li>Multi-language support including English and Urdu</li> | |
| <li>Easy-to-use interface for editing and managing subtitles</li> | |
| <li>Smart search functionality to quickly find content</li> | |
| <li>Seamless video editing based on transcripts</li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown(""" | |
| <div id="contact" class="contact-section" style="padding: 3rem 2rem; background: #f1f4f8; border-radius: 1rem; margin: 2rem 0;"> | |
| <h2 style="text-align: center; color: black; margin-bottom: 2rem;">Contact Us</h2> | |
| <div style="max-width: 600px; margin: 0 auto;"> | |
| <div style="margin-bottom: 1rem;"> | |
| <label for="email" style="display: block; margin-bottom: 0.5rem; font-weight: 500;">Email</label> | |
| <input type="email" id="email" placeholder="Your email address" style="width: 100%; padding: 0.75rem; border-radius: 0.5rem; border: 1px solid #e0e0e0;"> | |
| </div> | |
| <div style="margin-bottom: 1rem;"> | |
| <label for="message" style="display: block; margin-bottom: 0.5rem; font-weight: 500;">Message</label> | |
| <textarea id="message" rows="5" placeholder="Your message" style="width: 100%; padding: 0.75rem; border-radius: 0.5rem; border: 1px solid #e0e0e0;"></textarea> | |
| </div> | |
| <button onclick="alert('Message sent successfully!')" style="background: linear-gradient(135deg, #ff6f61, #ff8a65); color: white; font-weight: 600; padding: 0.75rem 1.5rem; border-radius: 0.5rem; border: none; cursor: pointer; width: 100%;">Send Message</button> | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown(""" | |
| <div class="plans"> | |
| <h2 style="text-align: center; margin-bottom: 2rem; color: black;">Choose Your Plan</h2> | |
| <div class="plan-box"> | |
| <div class="plan free" style="background: linear-gradient(135deg, #299f45, #185726); padding-bottom: 0px"> | |
| <h3 style="color: white;">Free</h3> | |
| <p><strong>$0</strong> / month</p> | |
| <p>Basic video transcription</p> | |
| <p>English only</p> | |
| <p>Max 5 minutes video</p> | |
| <p>No summarization</p> | |
| </div> | |
| <div class="plan premium" style="background-color:#a32b2d"> | |
| <h3 style="color: white;">Premium</h3> | |
| <p><strong>$19</strong> / month</p> | |
| <p>Advanced transcription</p> | |
| <p>Multiple languages</p> | |
| <p>Max 30 minutes video</p> | |
| <p>AI summarization</p> | |
| </div> | |
| <div class="plan business" style="background-color:#396ca3"> | |
| <h3 style="color: white;">Business</h3> | |
| <p><strong>$49</strong> / month</p> | |
| <p>Enterprise-grade transcription</p> | |
| <p>All languages</p> | |
| <p>Unlimited video length</p> | |
| </div> | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown(""" | |
| <footer> | |
| <div class="footer-container"> | |
| <div class="footer-section"> | |
| <h4 style="margin-left:20px">Company Info</h4> | |
| <ul> | |
| <li><a href="#about-us">About Us</a></li> | |
| <li><a href="#privacy">Privacy Policy</a></li> | |
| <li><a href="#terms">Terms</a></li> | |
| </ul> | |
| </div> | |
| <div class="footer-section"> | |
| <h4 style="margin-left:20px">Links</h4> | |
| <ul> | |
| <li><a href="#home">Home</a></li> | |
| <li><a href="#upload">Upload</a></li> | |
| <li><a href="#about">About</a></li> | |
| <li><a href="#contact">Contact</a></li> | |
| </ul> | |
| </div> | |
| <div class="footer-section"> | |
| <h4 style="margin-left:20px">Legal</h4> | |
| <ul> | |
| <li><a href="#">Terms of Service</a></li> | |
| <li><a href="#">Privacy Policy</a></li> | |
| <li><a href="#">Cookie Policy</a></li> | |
| </ul> | |
| </div> | |
| </div> | |
| <div class="footer-bottom" style="justify-content: center; text-align: center; border-top: 1px solid white; padding-top:20px; padding-bottom: 10px;"> | |
| <p style="font-size: 20px">© 2025 VidEp. All rights reserved.</p> | |
| </div> | |
| </footer> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| const navLinks = document.querySelectorAll('.navbar a'); | |
| navLinks.forEach(link => { | |
| link.addEventListener('click', function(e) { | |
| e.preventDefault(); | |
| const targetId = this.getAttribute('href'); | |
| const targetElement = document.querySelector(targetId); | |
| if (targetElement) { | |
| targetElement.scrollIntoView({behavior: 'smooth'}); | |
| } | |
| }); | |
| }); | |
| }); | |
| </script> | |
| """, unsafe_allow_html=True) | |
| if __name__ == "__main__": | |
| main() |