Spaces:
Running
Running
| 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() | |