Offex's picture
Update app.py
752cedf verified
import gradio as gr
import yt_dlp
import os
import shutil
import subprocess
from faster_whisper import WhisperModel
from indic_transliteration import sanscript
from indic_transliteration.sanscript import transliterate
# ═══════════════════════════════════════════════
# WHISPER MODEL (lazy load)
# ═══════════════════════════════════════════════
_model = None
def load_model():
global _model
if _model is None:
_model = WhisperModel("base", device="cpu", compute_type="int8")
return _model
# ═══════════════════════════════════════════════
# UTILS
# ═══════════════════════════════════════════════
def get_ffmpeg():
return shutil.which("ffmpeg") or "/usr/bin/ffmpeg"
def cleanup_old_files():
for f in ["downloaded_video.mp4", "downloaded_video.m4a",
"downloaded_video.webm", "downloaded_video.mp3",
"downloaded_video.wav", "downloaded_video.ogg",
"extracted_audio.wav", "temp_cookies.txt"]:
if os.path.exists(f):
try:
os.remove(f)
except Exception:
pass
def find_downloaded_file():
"""Find whatever yt-dlp actually downloaded."""
for ext in ["mp4", "m4a", "webm", "mp3", "wav", "ogg", "mkv", "flv"]:
path = f"downloaded_video.{ext}"
if os.path.exists(path):
return path
return None
# ═══════════════════════════════════════════════
# COOKIES HELPER
# ═══════════════════════════════════════════════
def save_cookies(cookies_str):
"""
Accept cookies in two formats:
1) Netscape format (starts with '# Netscape HTTP Cookie File')
2) key=value; key2=value2 (browser copy-paste format)
Returns path to cookie file, or None.
"""
if not cookies_str or not cookies_str.strip():
return None
cookies_file = "temp_cookies.txt"
raw = cookies_str.strip()
if raw.startswith("# Netscape"):
with open(cookies_file, "w") as f:
f.write(raw)
else:
# Convert simple "key=value; key2=value2" into Netscape format
lines = ["# Netscape HTTP Cookie File"]
for pair in raw.split(";"):
pair = pair.strip()
if "=" in pair:
k, v = pair.split("=", 1)
lines.append(f".instagram.com\tTRUE\t/\tFALSE\t9999999999\t{k.strip()}\t{v.strip()}")
with open(cookies_file, "w") as f:
f.write("\n".join(lines))
return cookies_file
# ═══════════════════════════════════════════════
# DOWNLOAD (YouTube / Instagram / Others)
# ═══════════════════════════════════════════════
def download_video(url, cookies_str=""):
cleanup_old_files()
is_youtube = any(x in url for x in ["youtube.com", "youtu.be"])
is_instagram = "instagram.com" in url
is_tiktok = "tiktok.com" in url
cookies_file = save_cookies(cookies_str)
# ── Base options ──
ydl_opts = {
"format" : "bestaudio/best",
"outtmpl" : "downloaded_video.%(ext)s",
"quiet" : True,
"nocheckcertificate": True,
"retries" : 5,
"fragment_retries" : 5,
"ignoreerrors" : False,
"http_headers" : {
"User-Agent": (
"Mozilla/5.0 (Linux; Android 12; Pixel 6) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/112.0.0.0 Mobile Safari/537.36"
),
"Accept-Language": "en-US,en;q=0.9",
},
}
if cookies_file:
ydl_opts["cookiefile"] = cookies_file
# ── YouTube-specific bypass (android player avoids bot detection) ──
if is_youtube:
ydl_opts["extractor_args"] = {
"youtube": {
"player_client": ["android", "ios", "web"],
"player_skip" : ["webpage"],
}
}
ydl_opts["http_headers"]["User-Agent"] = (
"com.google.android.youtube/17.36.4 "
"(Linux; U; Android 12; GB) gzip"
)
# ── Instagram-specific headers ──
if is_instagram:
ydl_opts["http_headers"] = {
"User-Agent" : "Instagram 219.0.0.12.117 Android (31/12; 420dpi; 1080x2400; samsung; SM-G991B; o1s; exynos2100; en_US; 346877738)",
"Accept" : "*/*",
"Accept-Language" : "en-US",
"X-IG-App-ID" : "936619743392459",
}
if not cookies_file:
raise ValueError(
"instagram_no_cookies"
)
# ── TikTok ──
if is_tiktok:
ydl_opts["http_headers"]["User-Agent"] = (
"TikTok 26.2.0 rv:262018 (iPhone; iOS 14.4.2; en_US) Cronet"
)
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.download([url])
path = find_downloaded_file()
if not path:
raise FileNotFoundError("Download completed but file not found.")
return path
# ═══════════════════════════════════════════════
# AUDIO EXTRACTION
# ═══════════════════════════════════════════════
def extract_audio(video_path):
audio_path = "extracted_audio.wav"
if os.path.exists(audio_path):
os.remove(audio_path)
subprocess.run(
[
get_ffmpeg(), "-y",
"-i", video_path,
"-vn",
"-ac", "1",
"-ar", "16000",
audio_path
],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
return audio_path
# ═══════════════════════════════════════════════
# SCRIPT NORMALIZER
# ═══════════════════════════════════════════════
def normalize_script(text, lang):
if lang == "hi":
try:
return transliterate(text, sanscript.ARABIC, sanscript.DEVANAGARI)
except Exception:
return text
return text
# ═══════════════════════════════════════════════
# MAIN TRANSCRIBE
# ═══════════════════════════════════════════════
def transcribe(url, file, lang_choice, cookies_str=""):
try:
# ── FILE MODE ──
if file:
ext = os.path.splitext(file)[1].lower()
audio = file if ext in [".mp3", ".wav", ".m4a"] else extract_audio(file)
# ── URL MODE ──
elif url and url.strip():
try:
video = download_video(url.strip(), cookies_str)
except ValueError as ve:
if "instagram_no_cookies" in str(ve):
return (
"❌ Instagram requires login cookies to download.\n\n"
"πŸ“‹ How to get cookies:\n"
"1. Open Instagram in Chrome/Firefox\n"
"2. Install 'Cookie Editor' browser extension\n"
"3. Click Export β†’ Copy (Netscape format)\n"
"4. Paste in the πŸ” Cookies box below and try again.\n\n"
"Or simply download the video and upload the file directly."
)
raise
except Exception as e:
err = str(e).lower()
if "sign in" in err or "login" in err or "private" in err:
return (
"❌ This content requires login.\n\n"
"Paste your browser cookies in the πŸ” Cookies box and retry,\n"
"or download the video and upload it directly."
)
if "http error 429" in err or "too many" in err:
return "❌ Rate limited by the platform. Wait a few minutes and try again."
if "youtube" in err and ("bot" in err or "sign in" in err):
return (
"❌ YouTube bot detection triggered.\n\n"
"Paste your YouTube cookies in the πŸ” Cookies box and retry."
)
raise
audio = extract_audio(video)
else:
return "⚠️ Please paste a URL or upload a file."
# ── Safety check ──
if not os.path.exists(audio) or os.path.getsize(audio) < 5000:
return "❌ Audio extraction failed. File may be too short or corrupted."
# ── Transcribe ──
m = load_model()
language = None if lang_choice == "Auto Detect" else lang_choice
segments, info = m.transcribe(
audio,
beam_size=1,
vad_filter=True,
language=language
)
raw_text = " ".join(s.text for s in segments).strip()
final_text = normalize_script(raw_text, info.language)
return f"🌍 Detected Language: {info.language.upper()}\n\n{final_text}"
except Exception as e:
err = str(e).lower()
if "instagram" in err:
return (
"❌ Instagram download failed.\n"
"Upload the video file directly, or paste cookies and retry."
)
if "ffmpeg" in err:
return "❌ FFmpeg error during audio extraction."
return f"❌ Error: {str(e)}"
# ═══════════════════════════════════════════════
# PREMIUM UI
# ═══════════════════════════════════════════════
CSS = """
/* ── Imports ── */
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@700;900&family=Sora:wght@300;400;600;700&display=swap');
/* ── Base ── */
body, .gradio-container {
background: radial-gradient(ellipse at 20% 0%, #0d1b2a 0%, #0a0f1e 50%, #020408 100%) !important;
font-family: 'Sora', sans-serif !important;
}
footer { display: none !important; }
.gap { gap: 16px; }
/* ── RGB Animations ── */
@keyframes rgb-glow {
0% { color:#ff0080; text-shadow:0 0 14px #ff0080,0 0 28px #ff0080; }
16% { color:#ff8c00; text-shadow:0 0 14px #ff8c00,0 0 28px #ff8c00; }
33% { color:#ffe600; text-shadow:0 0 14px #ffe600,0 0 28px #ffe600; }
50% { color:#00ff88; text-shadow:0 0 14px #00ff88,0 0 28px #00ff88; }
66% { color:#00c8ff; text-shadow:0 0 14px #00c8ff,0 0 28px #00c8ff; }
83% { color:#a855f7; text-shadow:0 0 14px #a855f7,0 0 28px #a855f7; }
100% { color:#ff0080; text-shadow:0 0 14px #ff0080,0 0 28px #ff0080; }
}
@keyframes rgb-border {
0% { border-color:#ff0080; box-shadow:0 0 12px #ff0080,inset 0 0 8px rgba(255,0,128,.08); }
16% { border-color:#ff8c00; box-shadow:0 0 12px #ff8c00,inset 0 0 8px rgba(255,140,0,.08); }
33% { border-color:#ffe600; box-shadow:0 0 12px #ffe600,inset 0 0 8px rgba(255,230,0,.08); }
50% { border-color:#00ff88; box-shadow:0 0 12px #00ff88,inset 0 0 8px rgba(0,255,136,.08); }
66% { border-color:#00c8ff; box-shadow:0 0 12px #00c8ff,inset 0 0 8px rgba(0,200,255,.08); }
83% { border-color:#a855f7; box-shadow:0 0 12px #a855f7,inset 0 0 8px rgba(168,85,247,.08); }
100% { border-color:#ff0080; box-shadow:0 0 12px #ff0080,inset 0 0 8px rgba(255,0,128,.08); }
}
@keyframes gradient-bg {
0% { background-position:0% 50%; }
50% { background-position:100% 50%; }
100% { background-position:0% 50%; }
}
@keyframes scan {
0% { background-position:0 0; }
100% { background-position:0 100px; }
}
@keyframes pulse-badge {
0%,100% { transform:scale(1); box-shadow:0 0 18px rgba(0,200,255,.4); }
50% { transform:scale(1.03);box-shadow:0 0 36px rgba(168,85,247,.6); }
}
/* ── Header ── */
.hero-wrap {
text-align: center;
padding: 32px 20px 20px;
position: relative;
}
.hero-title {
font-family: 'Orbitron', sans-serif;
font-size: clamp(1.6em, 4vw, 2.6em);
font-weight: 900;
letter-spacing: 2px;
animation: rgb-glow 3s linear infinite;
margin: 0 0 8px;
}
.hero-sub {
font-family: 'Sora', sans-serif;
font-size: .95em;
color: #94a3b8;
margin: 0 0 18px;
letter-spacing: .5px;
}
.made-badge {
display: inline-block;
font-family: 'Orbitron', sans-serif;
font-size: .72em;
font-weight: 700;
letter-spacing: 4px;
padding: 7px 26px;
border: 2px solid #00c8ff;
border-radius: 50px;
animation: rgb-glow 3s linear infinite, rgb-border 3s linear infinite, pulse-badge 3s ease-in-out infinite;
text-transform: uppercase;
background: rgba(0,0,0,.15);
}
/* ── Platform badges ── */
.platforms {
display: flex;
justify-content: center;
gap: 10px;
flex-wrap: wrap;
margin: 14px 0 0;
}
.plat-tag {
font-size: .72em;
padding: 4px 14px;
border-radius: 50px;
border: 1px solid rgba(255,255,255,.15);
color: #cbd5e1;
background: rgba(255,255,255,.05);
letter-spacing: .5px;
}
.plat-ok { border-color: rgba(0,255,136,.35); color:#00ff88; }
.plat-warn{ border-color: rgba(255,190,0,.35); color:#fbbf24; }
/* ── Glass card ── */
.glass-card {
background: rgba(255,255,255,.04) !important;
backdrop-filter: blur(24px) !important;
border: 1px solid rgba(255,255,255,.08) !important;
border-radius: 20px !important;
box-shadow: 0 24px 60px rgba(0,0,0,.5) !important;
}
/* ── Divider ── */
.rgb-line {
height: 2px;
margin: 28px 0;
background: linear-gradient(90deg,transparent,#00c8ff,#a855f7,#ff0080,transparent);
background-size: 200% 200%;
animation: gradient-bg 4s ease infinite;
border-radius: 2px;
}
/* ── Tabs ── */
.tabs .tab-nav button {
font-family: 'Sora', sans-serif !important;
font-weight: 600 !important;
font-size: .9em !important;
color: #64748b !important;
border-radius: 10px 10px 0 0 !important;
padding: 10px 20px !important;
transition: all .25s !important;
}
.tabs .tab-nav button.selected {
color: #00c8ff !important;
border-bottom: 2px solid #00c8ff !important;
background: rgba(0,200,255,.07) !important;
}
/* ── Inputs ── */
input, textarea, .gr-input, .gr-textarea, .codemirror-wrapper {
background: rgba(255,255,255,.06) !important;
border: 1px solid rgba(255,255,255,.1) !important;
color: #e2e8f0 !important;
border-radius: 12px !important;
font-family: 'Sora', sans-serif !important;
}
input:focus, textarea:focus {
border-color: #00c8ff !important;
box-shadow: 0 0 12px rgba(0,200,255,.2) !important;
outline: none !important;
}
label, .label-wrap span {
color: #94a3b8 !important;
font-family: 'Sora', sans-serif !important;
font-size: .85em !important;
letter-spacing: .3px !important;
}
/* ── Primary button ── */
button.primary, .gr-button-primary {
background: linear-gradient(135deg,#0ea5e9,#2563eb) !important;
border: none !important;
color: #fff !important;
font-family: 'Sora', sans-serif !important;
font-weight: 700 !important;
border-radius: 12px !important;
padding: 12px 20px !important;
font-size: 1em !important;
box-shadow: 0 4px 18px rgba(14,165,233,.35) !important;
transition: all .3s ease !important;
}
button.primary:hover {
background: linear-gradient(135deg,#38bdf8,#1d4ed8) !important;
box-shadow: 0 6px 28px rgba(14,165,233,.55) !important;
transform: translateY(-1px) !important;
}
/* ── Output code block ── */
.codemirror-wrapper {
background: rgba(0,0,0,.4) !important;
border: 1px solid rgba(0,200,255,.2) !important;
}
.CodeMirror {
background: transparent !important;
color: #a5f3fc !important;
font-family: 'Sora', sans-serif !important;
}
/* ── Accordion (cookies) ── */
.accordion-header button {
background: rgba(255,255,255,.04) !important;
border: 1px solid rgba(255,200,0,.2) !important;
color: #fbbf24 !important;
border-radius: 12px !important;
font-family: 'Sora', sans-serif !important;
font-weight: 600 !important;
}
/* ── Warning note ── */
.note-warn {
background: rgba(251,191,36,.07);
border: 1px solid rgba(251,191,36,.25);
border-radius: 12px;
padding: 12px 18px;
color: #fbbf24;
font-size: .82em;
line-height: 1.6;
margin-top: 6px;
}
/* ── Dropdown ── */
.gr-dropdown select, select {
background: rgba(255,255,255,.06) !important;
color: #e2e8f0 !important;
border: 1px solid rgba(255,255,255,.1) !important;
border-radius: 10px !important;
}
/* ── Scrollbar ── */
::-webkit-scrollbar { width:6px; }
::-webkit-scrollbar-track { background:transparent; }
::-webkit-scrollbar-thumb { background:#1e40af; border-radius:4px; }
"""
HEADER_HTML = """
<div class="hero-wrap">
<div class="hero-title">πŸŽ™οΈ Universal Transcript Tool</div>
<div class="hero-sub">Speech to text β€” any platform, any language</div>
<div class="made-badge">✦ Made by Deepu ✦</div>
<div class="platforms">
<span class="plat-tag plat-ok">βœ” YouTube</span>
<span class="plat-tag plat-ok">βœ” TikTok</span>
<span class="plat-tag plat-ok">βœ” Facebook</span>
<span class="plat-tag plat-ok">βœ” Twitter / X</span>
<span class="plat-tag plat-warn">⚠ Instagram (needs cookies)</span>
<span class="plat-tag plat-ok">βœ” File Upload</span>
</div>
</div>
"""
COOKIES_HELP = """
<div class="note-warn">
<strong>πŸ” When do you need cookies?</strong><br>
Instagram always Β· Private YouTube Β· Age-restricted content<br><br>
<strong>How to get cookies (2 steps):</strong><br>
1. Install <em>Cookie Editor</em> extension in Chrome / Firefox<br>
2. Open the site β†’ click extension β†’ <strong>Export β†’ Netscape format</strong> β†’ paste here
</div>
"""
# ═══════════════════════════════════════════════
# BUILD UI
# ═══════════════════════════════════════════════
with gr.Blocks(css=CSS, theme=gr.themes.Base()) as demo:
gr.HTML(HEADER_HTML)
gr.HTML('<div class="rgb-line"></div>')
with gr.Column(elem_classes="glass-card", scale=1):
with gr.Tabs(elem_classes="tabs"):
# ── Tab 1: URL ──
with gr.TabItem("πŸ”— Paste Link"):
url_input = gr.Textbox(
label="Video URL",
placeholder="https://youtube.com/watch?v=... or TikTok / Facebook / Twitter link",
lines=1
)
btn_url = gr.Button("🎧 Transcribe Link", variant="primary")
# ── Tab 2: Upload ──
with gr.TabItem("πŸ“‚ Upload File"):
file_input = gr.File(
label="Upload Video / Audio",
file_types=[".mp4", ".mkv", ".mov", ".webm", ".avi",
".mp3", ".wav", ".m4a"]
)
btn_file = gr.Button("πŸ“‚ Transcribe File", variant="primary")
gr.HTML('<div class="rgb-line"></div>')
# ── Language ──
lang = gr.Dropdown(
label="🌍 Transcript Language",
choices=[
"Auto Detect", "hi", "ur", "en", "ar",
"fr", "de", "es", "ru", "ja", "zh"
],
value="Auto Detect"
)
# ── Cookies (Accordion) ──
with gr.Accordion("πŸ” Cookies β€” Instagram / Private Content (click to expand)", open=False):
gr.HTML(COOKIES_HELP)
cookies_input = gr.Textbox(
label="Paste Netscape Cookies Here",
placeholder="# Netscape HTTP Cookie File\n.instagram.com TRUE / FALSE 9999999999 sessionid XXXXX",
lines=6
)
gr.HTML('<div class="rgb-line"></div>')
# ── Output ──
output = gr.Code(label="πŸ“„ Transcript Output", lines=14, language=None)
# ── Events ──
btn_url.click(
fn=transcribe,
inputs=[url_input, gr.State(None), lang, cookies_input],
outputs=output
)
btn_file.click(
fn=transcribe,
inputs=[gr.State(None), file_input, lang, cookies_input],
outputs=output
)
demo.launch()