Spaces:
Running
Running
Download app.py from abdullah090809/FYP: direct link, hf CLI and curl.
- Browser
- Download file 72.4 kB
-
https://huggingface.co/spaces/abdullah090809/FYP/resolve/main/app.py
- Command line
-
hf download hf://spaces/abdullah090809/FYP/app.py
-
curl -L -o app.py https://huggingface.co/spaces/abdullah090809/FYP/resolve/main/app.py
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) | |
| 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} Β· ' | |
| 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 & 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 & 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 & 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 & 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() |