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("""
Upload
β€Ί
2
Processing
β€Ί
3
Results
""", 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("""
Upload
β€Ί
Processing
β€Ί
3
Results
""", 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"""
Segments
{n}
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"""
{st.session_state['language']} Transcript
{len(st.session_state['primary_transcript'])} segments
""", 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("""
English Translation
""", 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
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
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("""
Get in touch
Have a question or feedback? We'd love to hear from you.
""", unsafe_allow_html=True) # ── FOOTER ─────────────────────────────────────────────────────────────────── st.markdown(""" """, unsafe_allow_html=True) if __name__ == "__main__": main()