FYP / app.py
abdullah090809's picture
Made changes to the UI
232ab6c verified
Raw History Blame Contribute Delete
72.4 kB
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("""
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* ─── RESET & BASE ─────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg-base: #0d1117;
--bg-surface: #161b22;
--bg-elevated: #21262d;
--border: #30363d;
--border-light: #21262d;
--accent: #3b82f6;
--accent-light: #60a5fa;
--accent-dim: #1d4ed8;
--accent-glow: rgba(59,130,246,0.15);
--accent-glow2: rgba(59,130,246,0.08);
--text-primary: #f0f6fc;
--text-secondary:#8b949e;
--text-muted: #484f58;
--success: #3fb950;
--success-glow: rgba(63,185,80,0.15);
--warning: #d29922;
--danger: #f85149;
--danger-glow: rgba(248,81,73,0.15);
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 16px;
--radius-xl: 24px;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.4);
--shadow-md: 0 4px 12px rgba(0,0,0,0.5);
--shadow-lg: 0 8px 32px rgba(0,0,0,0.6);
--font: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
}
/* ─── STREAMLIT OVERRIDES ──────────────────────────────── */
.stApp {
background: var(--bg-base) !important;
font-family: var(--font) !important;
color: var(--text-primary) !important;
}
.stApp > header { display: none !important; }
[data-testid="stToolbar"],
[data-testid="stDecoration"],
[data-testid="stStatusWidget"],
[data-testid="collapsedControl"],
#MainMenu { display: none !important; }
[data-testid="stSidebar"] { display: none !important; }
.block-container {
max-width: 1200px !important;
padding: 0 24px 80px !important;
margin: 0 auto !important;
}
section[data-testid="stFileUploadDropzone"] {
background: var(--bg-surface) !important;
border: 2px dashed var(--border) !important;
border-radius: var(--radius-lg) !important;
transition: border-color 0.2s, background 0.2s !important;
}
section[data-testid="stFileUploadDropzone"]:hover {
border-color: var(--accent) !important;
background: var(--accent-glow2) !important;
}
.stTextInput > div > div > input,
.stNumberInput > div > div > input,
.stSelectbox > div > div {
background: var(--bg-surface) !important;
border: 1px solid var(--border) !important;
border-radius: var(--radius-md) !important;
color: var(--text-primary) !important;
font-family: var(--font) !important;
font-size: 14px !important;
padding: 10px 14px !important;
transition: border-color 0.2s, box-shadow 0.2s !important;
}
.stTextInput > div > div > input:focus,
.stNumberInput > div > div > input:focus {
border-color: var(--accent) !important;
box-shadow: 0 0 0 3px var(--accent-glow) !important;
outline: none !important;
}
.stTextInput label, .stNumberInput label,
.stSelectbox label, .stCheckbox label,
.stFileUploader label {
color: var(--text-secondary) !important;
font-size: 13px !important;
font-weight: 500 !important;
letter-spacing: 0.02em !important;
text-transform: uppercase !important;
margin-bottom: 6px !important;
}
.stCheckbox > label {
text-transform: none !important;
font-size: 14px !important;
color: var(--text-primary) !important;
}
.stCheckbox > label > span { color: var(--text-primary) !important; }
[data-testid="stCheckbox"] span { color: var(--text-primary) !important; }
/* ─── BUTTONS ──────────────────────────────────────────── */
.stButton > button {
background: var(--accent) !important;
color: #fff !important;
font-family: var(--font) !important;
font-weight: 600 !important;
font-size: 14px !important;
padding: 10px 20px !important;
border-radius: var(--radius-md) !important;
border: none !important;
letter-spacing: 0.01em !important;
cursor: pointer !important;
transition: background 0.15s, transform 0.1s, box-shadow 0.15s !important;
box-shadow: 0 1px 4px rgba(59,130,246,0.3) !important;
white-space: nowrap !important;
}
.stButton > button:hover {
background: var(--accent-light) !important;
transform: translateY(-1px) !important;
box-shadow: 0 4px 16px rgba(59,130,246,0.4) !important;
}
.stButton > button:active {
transform: translateY(0) !important;
box-shadow: none !important;
}
/* Ghost/secondary variant via class trick on wrapping div */
.btn-ghost .stButton > button {
background: transparent !important;
border: 1px solid var(--border) !important;
color: var(--text-secondary) !important;
box-shadow: none !important;
}
.btn-ghost .stButton > button:hover {
border-color: var(--accent) !important;
color: var(--accent-light) !important;
background: var(--accent-glow2) !important;
}
.btn-danger .stButton > button {
background: transparent !important;
border: 1px solid var(--danger) !important;
color: var(--danger) !important;
box-shadow: none !important;
}
.btn-danger .stButton > button:hover {
background: var(--danger-glow) !important;
transform: translateY(-1px) !important;
}
/* ─── TRANSCRIPT SUBTITLE BUTTONS ──────────────────────── */
div[data-testid="stVerticalBlock"] > div > div > .stButton > button {
background: var(--bg-surface) !important;
border: 1px solid var(--border) !important;
color: var(--text-primary) !important;
font-weight: 400 !important;
font-size: 13px !important;
text-align: left !important;
width: 100% !important;
padding: 12px 16px !important;
border-radius: var(--radius-md) !important;
box-shadow: none !important;
transition: border-color 0.15s, background 0.15s !important;
font-family: var(--font-mono) !important;
}
div[data-testid="stVerticalBlock"] > div > div > .stButton > button:hover {
background: var(--bg-elevated) !important;
border-color: var(--accent) !important;
color: var(--accent-light) !important;
transform: none !important;
}
/* ─── PROGRESS BAR ─────────────────────────────────────── */
.stProgress > div > div {
background: linear-gradient(90deg, var(--accent), var(--accent-light)) !important;
border-radius: 99px !important;
}
.stProgress > div {
background: var(--bg-elevated) !important;
border-radius: 99px !important;
}
/* ─── SPINNER ──────────────────────────────────────────── */
.stSpinner > div { border-top-color: var(--accent) !important; }
/* ─── SELECT BOX ───────────────────────────────────────── */
[data-testid="stSelectbox"] > div > div {
background: var(--bg-surface) !important;
border-color: var(--border) !important;
color: var(--text-primary) !important;
}
[data-testid="stSelectbox"] svg { fill: var(--text-secondary) !important; }
/* ─── DOWNLOAD BUTTON ──────────────────────────────────── */
.stDownloadButton > button {
background: transparent !important;
border: 1px solid var(--success) !important;
color: var(--success) !important;
font-family: var(--font) !important;
font-weight: 600 !important;
font-size: 13px !important;
padding: 9px 18px !important;
border-radius: var(--radius-md) !important;
cursor: pointer !important;
transition: background 0.15s, box-shadow 0.15s !important;
}
.stDownloadButton > button:hover {
background: var(--success-glow) !important;
box-shadow: 0 0 12px var(--success-glow) !important;
}
/* ─── ALERTS / INFO ────────────────────────────────────── */
.stAlert {
background: var(--bg-surface) !important;
border-radius: var(--radius-md) !important;
border: 1px solid var(--border) !important;
color: var(--text-primary) !important;
}
[data-testid="stInfo"] { border-left: 3px solid var(--accent) !important; }
[data-testid="stSuccess"] { border-left: 3px solid var(--success) !important; }
[data-testid="stError"] { border-left: 3px solid var(--danger) !important; }
[data-testid="stWarning"] { border-left: 3px solid var(--warning) !important; }
/* ─── VIDEO ────────────────────────────────────────────── */
video {
border-radius: var(--radius-lg) !important;
border: 1px solid var(--border) !important;
max-width: 100% !important;
}
/* ─── FORM ─────────────────────────────────────────────── */
[data-testid="stForm"] {
background: var(--bg-surface) !important;
border: 1px solid var(--border) !important;
border-radius: var(--radius-xl) !important;
padding: 32px !important;
}
/* ─── MARKDOWN HEADINGS ─────────────────────────────────── */
.stMarkdown h1, .stMarkdown h2, .stMarkdown h3 {
color: var(--text-primary) !important;
font-family: var(--font) !important;
}
/* ─── CUSTOM COMPONENTS ─────────────────────────────────── */
/* Topnav */
.videp-nav {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 0 0 0;
height: 60px;
border-bottom: 1px solid var(--border);
margin-bottom: 0;
position: sticky;
top: 0;
z-index: 200;
background: rgba(13,17,23,0.92);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
.videp-logo {
display: flex;
align-items: center;
gap: 10px;
text-decoration: none;
}
.videp-logo-icon {
width: 34px;
height: 34px;
background: linear-gradient(135deg, var(--accent), #8b5cf6);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
}
.videp-logo-text {
font-size: 17px;
font-weight: 700;
color: var(--text-primary);
letter-spacing: -0.01em;
}
.videp-logo-badge {
font-size: 10px;
font-weight: 600;
background: var(--accent-glow);
color: var(--accent-light);
border: 1px solid rgba(59,130,246,0.3);
border-radius: 4px;
padding: 2px 6px;
letter-spacing: 0.05em;
}
.videp-nav-links {
display: flex;
gap: 4px;
list-style: none;
}
.videp-nav-links li a {
color: var(--text-secondary);
text-decoration: none;
font-size: 14px;
font-weight: 500;
padding: 6px 12px;
border-radius: var(--radius-sm);
transition: color 0.15s, background 0.15s;
}
.videp-nav-links li a:hover {
color: var(--text-primary);
background: var(--bg-elevated);
}
/* Hero */
.videp-hero {
padding: 72px 0 64px;
text-align: center;
position: relative;
overflow: hidden;
}
.videp-hero-eyebrow {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-weight: 600;
color: var(--accent-light);
letter-spacing: 0.08em;
text-transform: uppercase;
background: var(--accent-glow);
border: 1px solid rgba(59,130,246,0.25);
border-radius: 99px;
padding: 5px 14px;
margin-bottom: 24px;
}
.videp-hero h1 {
font-size: clamp(32px, 5vw, 56px);
font-weight: 800;
color: var(--text-primary);
letter-spacing: -0.03em;
line-height: 1.1;
margin-bottom: 20px;
}
.videp-hero h1 .gradient-text {
background: linear-gradient(135deg, var(--accent-light), #a78bfa);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.videp-hero p {
font-size: 18px;
color: var(--text-secondary);
max-width: 520px;
margin: 0 auto 40px;
line-height: 1.6;
font-weight: 400;
}
/* Signature: animated waveform */
.videp-waveform {
display: flex;
align-items: center;
justify-content: center;
gap: 3px;
height: 40px;
margin: 0 auto 40px;
}
.videp-waveform span {
display: block;
width: 3px;
background: var(--accent);
border-radius: 99px;
animation: wave 1.4s ease-in-out infinite;
opacity: 0.7;
}
.videp-waveform span:nth-child(1) { height: 8px; animation-delay: 0.0s; }
.videp-waveform span:nth-child(2) { height: 16px; animation-delay: 0.1s; }
.videp-waveform span:nth-child(3) { height: 28px; animation-delay: 0.2s; }
.videp-waveform span:nth-child(4) { height: 20px; animation-delay: 0.3s; }
.videp-waveform span:nth-child(5) { height: 36px; animation-delay: 0.0s; }
.videp-waveform span:nth-child(6) { height: 24px; animation-delay: 0.15s; }
.videp-waveform span:nth-child(7) { height: 32px; animation-delay: 0.25s; }
.videp-waveform span:nth-child(8) { height: 18px; animation-delay: 0.05s; }
.videp-waveform span:nth-child(9) { height: 28px; animation-delay: 0.35s; }
.videp-waveform span:nth-child(10) { height: 12px; animation-delay: 0.1s; }
.videp-waveform span:nth-child(11) { height: 22px; animation-delay: 0.2s; }
.videp-waveform span:nth-child(12) { height: 34px; animation-delay: 0.0s; }
.videp-waveform span:nth-child(13) { height: 20px; animation-delay: 0.15s; }
.videp-waveform span:nth-child(14) { height: 28px; animation-delay: 0.3s; }
.videp-waveform span:nth-child(15) { height: 14px; animation-delay: 0.05s; }
@keyframes wave {
0%, 100% { transform: scaleY(1); opacity: 0.5; }
50% { transform: scaleY(1.8); opacity: 1; }
}
/* Feature pills */
.videp-pill-row {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
margin-bottom: 48px;
}
.videp-pill {
display: inline-flex;
align-items: center;
gap: 7px;
font-size: 13px;
font-weight: 500;
color: var(--text-secondary);
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: 99px;
padding: 7px 14px;
transition: color 0.15s, border-color 0.15s;
}
.videp-pill:hover { color: var(--text-primary); border-color: var(--accent); }
.videp-pill i { color: var(--accent); font-size: 12px; }
/* Section heading */
.section-heading {
font-size: 11px;
font-weight: 700;
color: var(--text-muted);
letter-spacing: 0.1em;
text-transform: uppercase;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
}
.section-heading::after {
content: '';
flex: 1;
height: 1px;
background: var(--border);
}
/* Upload zone */
.upload-zone-label {
text-align: center;
padding: 8px 0 16px;
}
.upload-zone-label p {
font-size: 14px;
color: var(--text-secondary);
margin-top: 8px;
}
.upload-zone-label .upload-icon {
font-size: 32px;
margin-bottom: 10px;
display: block;
opacity: 0.5;
}
/* Config card */
.config-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
/* Transcript area */
.transcript-section {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-xl);
padding: 24px;
margin-top: 20px;
}
.transcript-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 18px;
}
.transcript-title {
font-size: 15px;
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 8px;
}
.transcript-badge {
font-size: 11px;
font-weight: 600;
padding: 3px 8px;
border-radius: 99px;
background: var(--accent-glow);
color: var(--accent-light);
border: 1px solid rgba(59,130,246,0.2);
}
/* Search box */
.search-wrap {
position: relative;
margin-bottom: 16px;
}
.search-wrap .stTextInput > div > div > input {
padding-left: 40px !important;
}
/* Subtitle chip */
.subtitle-chip {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 14px;
border-radius: var(--radius-md);
border: 1px solid var(--border-light);
background: var(--bg-base);
margin-bottom: 6px;
cursor: pointer;
transition: border-color 0.15s, background 0.15s;
}
.subtitle-chip:hover {
border-color: var(--accent);
background: var(--accent-glow2);
}
.subtitle-timestamp {
font-family: var(--font-mono);
font-size: 11px;
color: var(--accent-light);
white-space: nowrap;
min-width: 90px;
background: var(--accent-glow);
padding: 3px 7px;
border-radius: 4px;
}
.subtitle-text {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.5;
}
/* Summary box */
.summary-card {
background: var(--bg-surface);
border: 1px solid var(--border);
border-left: 3px solid var(--accent);
border-radius: var(--radius-lg);
padding: 24px;
margin-top: 20px;
}
.summary-card p {
font-size: 15px;
color: var(--text-secondary);
line-height: 1.7;
}
/* Stats row */
.stats-row {
display: flex;
gap: 16px;
margin-bottom: 24px;
flex-wrap: wrap;
}
.stat-card {
flex: 1;
min-width: 140px;
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 18px 20px;
}
.stat-label {
font-size: 11px;
font-weight: 600;
color: var(--text-muted);
letter-spacing: 0.08em;
text-transform: uppercase;
margin-bottom: 8px;
}
.stat-value {
font-size: 24px;
font-weight: 700;
color: var(--text-primary);
font-variant-numeric: tabular-nums;
}
.stat-value.accent { color: var(--accent-light); }
/* Processing card */
.processing-card {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-xl);
padding: 32px;
}
/* Step indicator */
.step-row {
display: flex;
gap: 12px;
margin-bottom: 32px;
overflow-x: auto;
padding-bottom: 4px;
}
.step {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
font-weight: 500;
white-space: nowrap;
color: var(--text-muted);
}
.step.active { color: var(--accent-light); }
.step.done { color: var(--success); }
.step-dot {
width: 20px;
height: 20px;
border-radius: 50%;
border: 2px solid currentColor;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
flex-shrink: 0;
}
.step.active .step-dot { background: var(--accent); border-color: var(--accent); color: #fff; }
.step.done .step-dot { background: var(--success); border-color: var(--success); color: #fff; }
.step-sep { color: var(--text-muted); font-size: 12px; }
/* Features section */
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin: 48px 0;
}
.feature-card {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 24px;
transition: border-color 0.2s, transform 0.2s;
}
.feature-card:hover {
border-color: rgba(59,130,246,0.4);
transform: translateY(-2px);
}
.feature-icon {
width: 40px;
height: 40px;
background: var(--accent-glow);
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
margin-bottom: 14px;
color: var(--accent-light);
}
.feature-card h4 {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 6px;
}
.feature-card p {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.5;
}
/* Plans */
.plans-section {
margin: 48px 0;
}
.plans-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
margin-top: 32px;
}
.plan-card {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-xl);
padding: 28px;
position: relative;
transition: transform 0.2s, border-color 0.2s;
}
.plan-card:hover { transform: translateY(-3px); border-color: var(--accent); }
.plan-card.featured {
border-color: var(--accent);
background: linear-gradient(180deg, rgba(59,130,246,0.06) 0%, var(--bg-surface) 100%);
}
.plan-badge {
position: absolute;
top: -10px;
left: 50%;
transform: translateX(-50%);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
background: var(--accent);
color: #fff;
padding: 3px 10px;
border-radius: 99px;
}
.plan-name {
font-size: 14px;
font-weight: 700;
color: var(--text-secondary);
letter-spacing: 0.05em;
text-transform: uppercase;
margin-bottom: 12px;
}
.plan-price {
font-size: 38px;
font-weight: 800;
color: var(--text-primary);
letter-spacing: -0.02em;
line-height: 1;
margin-bottom: 4px;
}
.plan-price span {
font-size: 14px;
font-weight: 400;
color: var(--text-muted);
}
.plan-desc {
font-size: 13px;
color: var(--text-secondary);
margin: 16px 0;
padding-top: 16px;
border-top: 1px solid var(--border);
line-height: 1.6;
}
.plan-feature {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--text-secondary);
margin-bottom: 8px;
}
.plan-feature i { color: var(--success); font-size: 12px; flex-shrink: 0; }
/* About section */
.about-section {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-xl);
padding: 48px;
margin: 48px 0;
}
.about-grid {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 48px;
align-items: center;
}
.about-img {
width: 100%;
border-radius: var(--radius-lg);
border: 1px solid var(--border);
}
.about-heading {
font-size: 28px;
font-weight: 800;
color: var(--text-primary);
letter-spacing: -0.02em;
margin-bottom: 16px;
}
.about-text {
font-size: 15px;
color: var(--text-secondary);
line-height: 1.7;
margin-bottom: 20px;
}
.about-list { list-style: none; padding: 0; }
.about-list li {
display: flex;
align-items: center;
gap: 10px;
font-size: 14px;
color: var(--text-secondary);
margin-bottom: 10px;
}
.about-list li i { color: var(--accent-light); font-size: 13px; }
/* Contact section */
.contact-section {
margin: 48px 0;
}
.contact-card {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-xl);
padding: 40px;
max-width: 560px;
margin: 0 auto;
}
.contact-input {
width: 100%;
background: var(--bg-base);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 12px 16px;
color: var(--text-primary);
font-family: var(--font);
font-size: 14px;
margin-bottom: 14px;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
.contact-input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-glow);
}
.contact-textarea {
resize: vertical;
min-height: 120px;
}
.contact-btn {
width: 100%;
background: var(--accent);
color: #fff;
font-family: var(--font);
font-weight: 600;
font-size: 14px;
padding: 12px;
border-radius: var(--radius-md);
border: none;
cursor: pointer;
transition: background 0.15s, transform 0.1s;
}
.contact-btn:hover { background: var(--accent-light); transform: translateY(-1px); }
.contact-label {
display: block;
font-size: 12px;
font-weight: 600;
color: var(--text-muted);
letter-spacing: 0.06em;
text-transform: uppercase;
margin-bottom: 6px;
}
/* Footer */
.videp-footer {
background: var(--bg-surface);
border-top: 1px solid var(--border);
padding: 48px 0 32px;
margin-top: 80px;
}
.footer-grid {
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr;
gap: 40px;
margin-bottom: 40px;
}
.footer-brand p {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.6;
margin-top: 12px;
max-width: 260px;
}
.footer-col h5 {
font-size: 11px;
font-weight: 700;
color: var(--text-muted);
letter-spacing: 0.1em;
text-transform: uppercase;
margin-bottom: 14px;
}
.footer-col ul { list-style: none; padding: 0; }
.footer-col ul li { margin-bottom: 8px; }
.footer-col ul li a {
font-size: 13px;
color: var(--text-secondary);
text-decoration: none;
transition: color 0.15s;
}
.footer-col ul li a:hover { color: var(--text-primary); }
.footer-bottom {
border-top: 1px solid var(--border);
padding-top: 24px;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
}
.footer-bottom p {
font-size: 12px;
color: var(--text-muted);
}
/* Divider */
.divider {
height: 1px;
background: var(--border);
margin: 40px 0;
}
/* Section title */
.section-title {
font-size: 28px;
font-weight: 800;
color: var(--text-primary);
letter-spacing: -0.02em;
margin-bottom: 8px;
}
.section-subtitle {
font-size: 15px;
color: var(--text-secondary);
margin-bottom: 32px;
}
/* Delete/Edit view */
.edit-row {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px 14px;
border: 1px solid var(--border-light);
border-radius: var(--radius-md);
background: var(--bg-base);
margin-bottom: 6px;
}
.edit-index {
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-muted);
min-width: 28px;
padding-top: 2px;
}
.edit-time {
font-family: var(--font-mono);
font-size: 11px;
color: var(--accent-light);
min-width: 110px;
background: var(--accent-glow);
padding: 3px 7px;
border-radius: 4px;
white-space: nowrap;
}
.edit-text {
font-size: 13px;
color: var(--text-secondary);
flex: 1;
line-height: 1.5;
}
/* Memory/Loading chip */
.mem-chip {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--text-muted);
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: 99px;
padding: 4px 10px;
font-family: var(--font-mono);
}
/* Responsive */
@media (max-width: 768px) {
.videp-nav-links { display: none; }
.about-grid { grid-template-columns: 1fr; }
.footer-grid { grid-template-columns: 1fr 1fr; }
.config-grid { grid-template-columns: 1fr; }
.stats-row { gap: 10px; }
.videp-hero h1 { font-size: 32px; }
.videp-hero p { font-size: 15px; }
}
@media (max-width: 480px) {
.footer-grid { grid-template-columns: 1fr; }
.plans-grid { grid-template-columns: 1fr; }
.block-container { padding: 0 16px 60px !important; }
}
</style>
""", 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'<span class="mem-chip"><i class="fas fa-microchip"></i> Chunk {i+1} / {total_chunks} &nbsp;Β·&nbsp; '
f'{psutil.virtual_memory().percent:.1f}% RAM</span>',
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("""
<div class="videp-nav">
<a href="#home" class="videp-logo">
<div class="videp-logo-icon">🎬</div>
<span class="videp-logo-text">VidEp</span>
<span class="videp-logo-badge">BETA</span>
</a>
<ul class="videp-nav-links">
<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>
""", unsafe_allow_html=True)
# ── HERO ────────────────────────────────────────────────────────────────────
st.markdown('<div id="home"></div>', unsafe_allow_html=True)
st.markdown("""
<div class="videp-hero">
<div class="videp-hero-eyebrow"><i class="fas fa-bolt"></i> Powered by Whisper &amp; BART</div>
<h1>Video intelligence,<br><span class="gradient-text">zero friction</span></h1>
<p>Transcribe, translate, and summarize any video in seconds. Edit subtitles, search content, and export β€” all in one place.</p>
<div class="videp-waveform">
<span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
</div>
<div class="videp-pill-row">
<div class="videp-pill"><i class="fas fa-language"></i> English &amp; Urdu</div>
<div class="videp-pill"><i class="fas fa-search"></i> Smart Search</div>
<div class="videp-pill"><i class="fas fa-file-alt"></i> AI Summary</div>
<div class="videp-pill"><i class="fas fa-cut"></i> Video Editing</div>
<div class="videp-pill"><i class="fas fa-download"></i> SRT Export</div>
</div>
</div>
""", 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('<div id="upload"></div>', unsafe_allow_html=True)
col_l, col_c, col_r = st.columns([1, 2, 1])
with col_c:
st.markdown('<div class="section-heading">Get started</div>', unsafe_allow_html=True)
st.markdown("""
<div style="text-align:center; margin-bottom:8px;">
<div class="section-title">Upload your video</div>
<div class="section-subtitle">MP4 files supported Β· Transcription begins automatically</div>
</div>
""", unsafe_allow_html=True)
with st.form(key="upload_form"):
st.markdown("""
<div class="upload-zone-label">
<span class="upload-icon">πŸ“Ή</span>
<p>Drag and drop or click to browse</p>
</div>
""", unsafe_allow_html=True)
uploaded_file = st.file_uploader(
"Choose a video file",
type=["mp4"],
label_visibility="collapsed"
)
st.markdown("<br>", 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("""
<div style="margin-bottom:24px;">
<div class="section-title">Configure &amp; Process</div>
<div class="section-subtitle">Set transcription options, then run the pipeline.</div>
</div>
""", 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("<br>", unsafe_allow_html=True)
process_btn = st.form_submit_button("Run Transcription β†’", use_container_width=True)
if process_btn:
st.markdown('<div class="processing-card">', unsafe_allow_html=True)
# Step indicator
st.markdown("""
<div class="step-row">
<div class="step done"><div class="step-dot"><i class="fas fa-check" style="font-size:8px"></i></div>Upload</div>
<span class="step-sep">β€Ί</span>
<div class="step active"><div class="step-dot">2</div>Processing</div>
<span class="step-sep">β€Ί</span>
<div class="step"><div class="step-dot">3</div>Results</div>
</div>
""", 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'<div class="stat-card"><div class="stat-label">Duration</div><div class="stat-value accent">{int(audio_duration)}s</div></div>', unsafe_allow_html=True)
with col2:
st.markdown(f'<div class="stat-card"><div class="stat-label">Summarizer</div><div class="stat-value" style="font-size:18px">{summarizer_type.upper()}</div></div>', unsafe_allow_html=True)
with col3:
st.markdown(f'<div class="stat-card"><div class="stat-label">Device</div><div class="stat-value" style="font-size:18px">{"GPU" if torch.cuda.is_available() else "CPU"}</div></div>', 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('<div class="section-heading">Transcribing</div>', 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('<div class="section-heading">Translating to English</div>', 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('</div>', unsafe_allow_html=True)
# ── RESULTS ──────────────────────────────────────────────────────────────────
if st.session_state['app_state'] == 'results':
# Step indicator
st.markdown("""
<div class="step-row" style="margin:20px 0 28px;">
<div class="step done"><div class="step-dot"><i class="fas fa-check" style="font-size:8px"></i></div>Upload</div>
<span class="step-sep">β€Ί</span>
<div class="step done"><div class="step-dot"><i class="fas fa-check" style="font-size:8px"></i></div>Processing</div>
<span class="step-sep">β€Ί</span>
<div class="step active"><div class="step-dot">3</div>Results</div>
</div>
""", unsafe_allow_html=True)
# Two-column layout: video + controls
left_col, right_col = st.columns([1, 1], gap="large")
with left_col:
st.markdown('<div class="section-heading">Preview</div>', 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"""
<div class="stats-row" style="margin-top:16px;">
<div class="stat-card">
<div class="stat-label">Segments</div>
<div class="stat-value accent">{n}</div>
</div>
<div class="stat-card">
<div class="stat-label">Duration</div>
<div class="stat-value" style="font-size:20px">{format_time(total_dur)}</div>
</div>
<div class="stat-card">
<div class="stat-label">Language</div>
<div class="stat-value" style="font-size:16px">{st.session_state['language']}</div>
</div>
</div>
""", unsafe_allow_html=True)
# Actions
st.markdown('<div class="section-heading" style="margin-top:24px;">Actions</div>', 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("<br>", 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('<div class="btn-ghost">', unsafe_allow_html=True)
if st.button("βœ‚ Edit & Remove Segments", use_container_width=True):
st.session_state['app_state'] = 'editing'
st.rerun()
st.markdown('</div>', 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("<br>", 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('<div class="section-heading">Transcript</div>', 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"""
<div class="transcript-header" style="margin-top:16px;">
<div class="transcript-title">
<i class="fas fa-closed-captioning" style="color:var(--accent)"></i>
{st.session_state['language']} Transcript
</div>
<span class="transcript-badge">{len(st.session_state['primary_transcript'])} segments</span>
</div>
""", 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("""
<div class="transcript-header" style="margin-top:28px;">
<div class="transcript-title">
<i class="fas fa-globe" style="color:#a78bfa"></i>
English Translation
</div>
</div>
""", 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('<div class="divider"></div>', unsafe_allow_html=True)
st.markdown("""
<div class="transcript-title" style="margin-bottom:14px;">
<i class="fas fa-stars" style="color:var(--accent)"></i> AI Summary
</div>
""", unsafe_allow_html=True)
st.markdown(
f'<div class="summary-card"><p>{st.session_state["english_summary"]}</p></div>',
unsafe_allow_html=True
)
# Edited video
if st.session_state['edited_video_path']:
st.markdown('<div class="divider"></div>', unsafe_allow_html=True)
st.markdown("""
<div class="section-title" style="margin-bottom:16px;">Edited Video</div>
""", 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("""
<div style="margin-bottom:28px;">
<div class="section-title">Remove Segments</div>
<div class="section-subtitle">Enter segment indices to delete, then confirm. The video will be re-cut accordingly.</div>
</div>
""", unsafe_allow_html=True)
transcript_to_edit = st.session_state['primary_transcript'] or st.session_state['english_transcript']
# Render transcript list
st.markdown('<div class="transcript-section">', unsafe_allow_html=True)
for i, (text, start, end) in enumerate(transcript_to_edit):
st.markdown(f"""
<div class="edit-row">
<span class="edit-index">#{i}</span>
<span class="edit-time">{format_time(start)} – {format_time(end)}</span>
<span class="edit-text">{text}</span>
</div>
""", unsafe_allow_html=True)
st.markdown('</div>', unsafe_allow_html=True)
st.markdown("<br>", 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('<div class="btn-ghost">', unsafe_allow_html=True)
if st.button("Cancel", use_container_width=True):
st.session_state['app_state'] = 'results'
st.rerun()
st.markdown('</div>', unsafe_allow_html=True)
# ── RESET ────────────────────────────────────────────────────────────────────
if st.session_state.get('video_path'):
st.markdown('<div class="divider"></div>', unsafe_allow_html=True)
col_r, _ = st.columns([1, 3])
with col_r:
st.markdown('<div class="btn-danger">', 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('</div>', unsafe_allow_html=True)
# ── FEATURES SECTION ─────────────────────────────────────────────────────────
st.markdown('<div class="divider"></div>', unsafe_allow_html=True)
st.markdown("""
<div style="text-align:center; margin-bottom:32px;">
<div class="section-title">Everything you need</div>
<div class="section-subtitle">One platform for all your video transcription needs.</div>
</div>
<div class="features-grid">
<div class="feature-card">
<div class="feature-icon"><i class="fas fa-cloud-upload-alt"></i></div>
<h4>Simple Upload</h4>
<p>Drop any MP4 and start processing immediately. No account setup needed.</p>
</div>
<div class="feature-card">
<div class="feature-icon"><i class="fas fa-search"></i></div>
<h4>Smart Search</h4>
<p>Instantly find any word or phrase across your entire transcript.</p>
</div>
<div class="feature-card">
<div class="feature-icon"><i class="fas fa-cut"></i></div>
<h4>Segment Editing</h4>
<p>Remove unwanted segments and export a clean, trimmed video.</p>
</div>
<div class="feature-card">
<div class="feature-icon"><i class="fas fa-file-alt"></i></div>
<h4>AI Summary</h4>
<p>Get a concise summary of any video powered by BART and LED models.</p>
</div>
<div class="feature-card">
<div class="feature-icon"><i class="fas fa-language"></i></div>
<h4>Multi-Language</h4>
<p>Full support for English and Urdu, with automatic translation.</p>
</div>
<div class="feature-card">
<div class="feature-icon"><i class="fas fa-download"></i></div>
<h4>SRT Export</h4>
<p>Download subtitle files compatible with any video player or editor.</p>
</div>
</div>
""", unsafe_allow_html=True)
# ── PLANS ────────────────────────────────────────────────────────────────────
st.markdown('<div class="divider"></div>', unsafe_allow_html=True)
st.markdown("""
<div style="text-align:center; margin-bottom:32px;">
<div class="section-title">Simple pricing</div>
<div class="section-subtitle">Start free. Scale when you need to.</div>
</div>
<div class="plans-grid">
<div class="plan-card">
<div class="plan-name">Free</div>
<div class="plan-price">$0<span> / month</span></div>
<div class="plan-desc">Perfect for trying out the platform.</div>
<div class="plan-feature"><i class="fas fa-check-circle"></i> Basic transcription</div>
<div class="plan-feature"><i class="fas fa-check-circle"></i> English only</div>
<div class="plan-feature"><i class="fas fa-check-circle"></i> Up to 5 min video</div>
<div class="plan-feature"><i class="fas fa-check-circle"></i> SRT export</div>
</div>
<div class="plan-card featured">
<div class="plan-badge">Most popular</div>
<div class="plan-name">Premium</div>
<div class="plan-price">$19<span> / month</span></div>
<div class="plan-desc">For creators and professionals.</div>
<div class="plan-feature"><i class="fas fa-check-circle"></i> Advanced transcription</div>
<div class="plan-feature"><i class="fas fa-check-circle"></i> English + Urdu</div>
<div class="plan-feature"><i class="fas fa-check-circle"></i> Up to 30 min video</div>
<div class="plan-feature"><i class="fas fa-check-circle"></i> AI summarization</div>
</div>
<div class="plan-card">
<div class="plan-name">Business</div>
<div class="plan-price">$49<span> / month</span></div>
<div class="plan-desc">Enterprise-grade for teams.</div>
<div class="plan-feature"><i class="fas fa-check-circle"></i> Unlimited video length</div>
<div class="plan-feature"><i class="fas fa-check-circle"></i> All languages</div>
<div class="plan-feature"><i class="fas fa-check-circle"></i> Priority processing</div>
<div class="plan-feature"><i class="fas fa-check-circle"></i> API access</div>
</div>
</div>
""", unsafe_allow_html=True)
# ── ABOUT ────────────────────────────────────────────────────────────────────
st.markdown('<div id="about" class="divider"></div>', unsafe_allow_html=True)
st.markdown("""
<div class="about-section">
<div class="about-grid">
<div>
<img src="https://i.postimg.cc/g0z3WVgT/about.jpg" class="about-img" alt="About VidEp">
</div>
<div>
<div style="font-size:11px; font-weight:700; color:var(--accent-light); letter-spacing:0.1em; text-transform:uppercase; margin-bottom:12px;">About VidEp</div>
<div class="about-heading">Built for the way people actually work with video</div>
<p class="about-text">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.</p>
<p class="about-text">We handle the heavy lifting β€” you focus on the insights.</p>
<ul class="about-list">
<li><i class="fas fa-check"></i> OpenAI Whisper for superior accuracy</li>
<li><i class="fas fa-check"></i> Multi-language support: English and Urdu</li>
<li><i class="fas fa-check"></i> BART and LED for intelligent summarization</li>
<li><i class="fas fa-check"></i> Non-destructive segment editing with FFmpeg</li>
<li><i class="fas fa-check"></i> Works entirely in your browser β€” no data stored</li>
</ul>
</div>
</div>
</div>
""", unsafe_allow_html=True)
# ── CONTACT ──────────────────────────────────────────────────────────────────
st.markdown('<div id="contact" class="divider"></div>', unsafe_allow_html=True)
st.markdown("""
<div class="contact-section">
<div style="text-align:center; margin-bottom:32px;">
<div class="section-title">Get in touch</div>
<div class="section-subtitle">Have a question or feedback? We'd love to hear from you.</div>
</div>
<div class="contact-card">
<label class="contact-label">Email</label>
<input class="contact-input" type="email" placeholder="your@email.com">
<label class="contact-label">Subject</label>
<input class="contact-input" type="text" placeholder="What's this about?">
<label class="contact-label">Message</label>
<textarea class="contact-input contact-textarea" placeholder="Tell us more…"></textarea>
<button class="contact-btn" onclick="alert('Message sent!')">Send Message β†’</button>
</div>
</div>
""", unsafe_allow_html=True)
# ── FOOTER ───────────────────────────────────────────────────────────────────
st.markdown("""
<div class="videp-footer">
<div class="footer-grid">
<div class="footer-brand">
<div class="videp-logo">
<div class="videp-logo-icon">🎬</div>
<span class="videp-logo-text">VidEp</span>
</div>
<p>AI-powered video transcription, translation, and summarization β€” built for everyone.</p>
</div>
<div class="footer-col">
<h5>Product</h5>
<ul>
<li><a href="#home">Home</a></li>
<li><a href="#upload">Upload</a></li>
<li><a href="#about">About</a></li>
</ul>
</div>
<div class="footer-col">
<h5>Legal</h5>
<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 class="footer-col">
<h5>Contact</h5>
<ul>
<li><a href="#contact">Email Us</a></li>
<li><a href="#">Twitter</a></li>
<li><a href="#">GitHub</a></li>
</ul>
</div>
</div>
<div class="footer-bottom">
<p>Β© 2025 VidEp. All rights reserved.</p>
<p>Made with β™₯ using Streamlit &amp; Whisper</p>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function(e) {
const target = document.querySelector(this.getAttribute('href'));
if (target) {
e.preventDefault();
target.scrollIntoView({ behavior: 'smooth' });
}
});
});
});
</script>
""", unsafe_allow_html=True)
if __name__ == "__main__":
main()