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 = """
🎙️ Universal Transcript Tool
Speech to text — any platform, any language
✦ Made by Deepu ✦
✔ YouTube ✔ TikTok ✔ Facebook ✔ Twitter / X ⚠ Instagram (needs cookies) ✔ File Upload
""" COOKIES_HELP = """
🔐 When do you need cookies?
Instagram always · Private YouTube · Age-restricted content

How to get cookies (2 steps):
1. Install Cookie Editor extension in Chrome / Firefox
2. Open the site → click extension → Export → Netscape format → paste here
""" # ═══════════════════════════════════════════════ # BUILD UI # ═══════════════════════════════════════════════ with gr.Blocks(css=CSS, theme=gr.themes.Base()) as demo: gr.HTML(HEADER_HTML) gr.HTML('
') 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('
') # ── 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('
') # ── 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()