Offex commited on
Commit
752cedf
Β·
verified Β·
1 Parent(s): fbfb3b5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +492 -112
app.py CHANGED
@@ -7,57 +7,159 @@ from faster_whisper import WhisperModel
7
  from indic_transliteration import sanscript
8
  from indic_transliteration.sanscript import transliterate
9
 
10
- # ===============================
11
- # Whisper Model (lazy load)
12
- # ===============================
13
- model = None
14
 
15
  def load_model():
16
- global model
17
- if model is None:
18
- model = WhisperModel("base", device="cpu", compute_type="int8")
19
- return model
20
-
21
- # ===============================
22
- # FFmpeg path
23
- # ===============================
24
  def get_ffmpeg():
25
  return shutil.which("ffmpeg") or "/usr/bin/ffmpeg"
26
 
27
- # ===============================
28
- # SAFE: Download video only (NO postprocessing)
29
- # ===============================
30
- def download_video_only(url):
31
- video_path = "downloaded_video.mp4"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
- if os.path.exists(video_path):
34
- os.remove(video_path)
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  ydl_opts = {
37
- "format": "best",
38
- "outtmpl": video_path,
39
- "quiet": True,
40
  "nocheckcertificate": True,
 
 
 
 
 
 
 
 
 
 
 
41
  }
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  with yt_dlp.YoutubeDL(ydl_opts) as ydl:
44
  ydl.download([url])
45
 
46
- return video_path
 
 
 
47
 
48
- # ===============================
49
- # SAFE: Extract audio manually (NO ffprobe)
50
- # ===============================
51
- def extract_audio_safe(video_path):
52
  audio_path = "extracted_audio.wav"
53
-
54
  if os.path.exists(audio_path):
55
  os.remove(audio_path)
56
 
57
  subprocess.run(
58
  [
59
- get_ffmpeg(),
60
- "-y",
61
  "-i", video_path,
62
  "-vn",
63
  "-ac", "1",
@@ -67,137 +169,415 @@ def extract_audio_safe(video_path):
67
  stdout=subprocess.DEVNULL,
68
  stderr=subprocess.DEVNULL
69
  )
70
-
71
  return audio_path
72
 
73
- # ===============================
74
- # Hindi script normalizer
75
- # ===============================
76
  def normalize_script(text, lang):
77
  if lang == "hi":
78
  try:
79
  return transliterate(text, sanscript.ARABIC, sanscript.DEVANAGARI)
80
- except:
81
  return text
82
  return text
83
 
84
- # ===============================
85
- # Transcription logic (STABLE)
86
- # ===============================
87
- def transcribe(url, file, lang_choice):
88
  try:
89
- # -------- FILE MODE --------
90
  if file:
91
  ext = os.path.splitext(file)[1].lower()
92
- if ext in [".mp3", ".wav", ".m4a"]:
93
- audio = file
94
- else:
95
- audio = extract_audio_safe(file)
96
 
97
- # -------- URL MODE --------
98
- elif url:
99
- video = download_video_only(url)
100
- audio = extract_audio_safe(video)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
 
102
  else:
103
  return "⚠️ Please paste a URL or upload a file."
104
 
105
- # Safety check
106
- if not os.path.exists(audio) or os.path.getsize(audio) < 10000:
107
- return "❌ Audio extraction failed. Please try again."
108
 
109
- model = load_model()
 
110
  language = None if lang_choice == "Auto Detect" else lang_choice
111
 
112
- segments, info = model.transcribe(
113
  audio,
114
  beam_size=1,
115
  vad_filter=True,
116
  language=language
117
  )
118
 
119
- raw_text = " ".join(s.text for s in segments)
120
  final_text = normalize_script(raw_text, info.language)
121
 
122
- return f"🌍 Detected Language: {info.language}\n\n{final_text.strip()}"
123
 
124
  except Exception as e:
125
- if "instagram" in str(e).lower():
126
- return "❌ Instagram URL is blocked on Hugging Face. Please upload the video file instead."
 
 
 
 
 
 
127
  return f"❌ Error: {str(e)}"
128
 
129
- # ===============================
130
- # MODERN UI
131
- # ===============================
132
- css = """
133
- body {
134
- background: radial-gradient(circle at top, #0f2027, #203a43, #2c5364);
135
- }
136
- .glass {
137
- background: rgba(255,255,255,0.08);
138
- backdrop-filter: blur(18px);
139
- border-radius: 18px;
140
- padding: 25px;
141
- box-shadow: 0 20px 40px rgba(0,0,0,0.4);
142
- }
143
- .gr-button-primary {
144
- background: linear-gradient(135deg,#00c6ff,#0072ff);
145
- border: none;
146
- color: white;
147
- font-weight: 600;
148
- }
149
- .gr-input, .gr-textarea {
150
- background: rgba(255,255,255,0.12) !important;
151
- color: white !important;
152
- }
153
- h1, h2, label, .markdown-text {
154
- color: #ffffff !important;
155
- }
156
- footer {display:none;}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  """
158
 
159
- with gr.Blocks(css=css, theme=gr.themes.Base()) as demo:
160
- with gr.Column(elem_classes="glass"):
161
- gr.Markdown("## πŸš€ Universal Transcript Tool (STABLE)")
162
- gr.Markdown(
163
- "βœ” YouTube βœ” TikTok βœ” Facebook βœ” Twitter/X\n\n"
164
- "⚠️ Instagram URL blocked on Hugging Face β†’ **Upload video instead**\n\n"
165
- "**No random ffprobe errors. Ever.**"
166
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
 
168
- with gr.Tabs():
 
 
 
 
 
 
 
169
  with gr.TabItem("πŸ”— Paste Link"):
170
- url = gr.Textbox(label="Video URL")
 
 
 
 
171
  btn_url = gr.Button("🎧 Transcribe Link", variant="primary")
172
 
 
173
  with gr.TabItem("πŸ“‚ Upload File"):
174
- file = gr.File(
175
  label="Upload Video / Audio",
176
- file_types=[".mp4", ".mkv", ".mov", ".webm", ".avi", ".mp3", ".wav"]
 
177
  )
178
  btn_file = gr.Button("πŸ“‚ Transcribe File", variant="primary")
179
 
 
 
 
180
  lang = gr.Dropdown(
181
  label="🌍 Transcript Language",
182
  choices=[
183
- "Auto Detect",
184
- "hi",
185
- "ur",
186
- "en",
187
- "ar",
188
- "fr",
189
- "de",
190
- "es",
191
- "ru",
192
- "ja",
193
- "zh"
194
  ],
195
  value="Auto Detect"
196
  )
197
 
198
- output = gr.Code(label="Transcript Output", lines=14)
 
 
 
 
 
 
 
199
 
200
- btn_url.click(transcribe, [url, gr.State(None), lang], output)
201
- btn_file.click(transcribe, [gr.State(None), file, lang], output)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
 
203
- demo.launch()
 
7
  from indic_transliteration import sanscript
8
  from indic_transliteration.sanscript import transliterate
9
 
10
+ # ═══════════════════════════════════════════════
11
+ # WHISPER MODEL (lazy load)
12
+ # ═══════════════════════════════════════════════
13
+ _model = None
14
 
15
  def load_model():
16
+ global _model
17
+ if _model is None:
18
+ _model = WhisperModel("base", device="cpu", compute_type="int8")
19
+ return _model
20
+
21
+ # ═══════════════════════════════════════════════
22
+ # UTILS
23
+ # ═══════════════════════════════════════════════
24
  def get_ffmpeg():
25
  return shutil.which("ffmpeg") or "/usr/bin/ffmpeg"
26
 
27
+ def cleanup_old_files():
28
+ for f in ["downloaded_video.mp4", "downloaded_video.m4a",
29
+ "downloaded_video.webm", "downloaded_video.mp3",
30
+ "downloaded_video.wav", "downloaded_video.ogg",
31
+ "extracted_audio.wav", "temp_cookies.txt"]:
32
+ if os.path.exists(f):
33
+ try:
34
+ os.remove(f)
35
+ except Exception:
36
+ pass
37
+
38
+ def find_downloaded_file():
39
+ """Find whatever yt-dlp actually downloaded."""
40
+ for ext in ["mp4", "m4a", "webm", "mp3", "wav", "ogg", "mkv", "flv"]:
41
+ path = f"downloaded_video.{ext}"
42
+ if os.path.exists(path):
43
+ return path
44
+ return None
45
+
46
+ # ═══════════════════════════════════════════════
47
+ # COOKIES HELPER
48
+ # ═══════════════════════════════════════════════
49
+ def save_cookies(cookies_str):
50
+ """
51
+ Accept cookies in two formats:
52
+ 1) Netscape format (starts with '# Netscape HTTP Cookie File')
53
+ 2) key=value; key2=value2 (browser copy-paste format)
54
+ Returns path to cookie file, or None.
55
+ """
56
+ if not cookies_str or not cookies_str.strip():
57
+ return None
58
+
59
+ cookies_file = "temp_cookies.txt"
60
+ raw = cookies_str.strip()
61
+
62
+ if raw.startswith("# Netscape"):
63
+ with open(cookies_file, "w") as f:
64
+ f.write(raw)
65
+ else:
66
+ # Convert simple "key=value; key2=value2" into Netscape format
67
+ lines = ["# Netscape HTTP Cookie File"]
68
+ for pair in raw.split(";"):
69
+ pair = pair.strip()
70
+ if "=" in pair:
71
+ k, v = pair.split("=", 1)
72
+ lines.append(f".instagram.com\tTRUE\t/\tFALSE\t9999999999\t{k.strip()}\t{v.strip()}")
73
+ with open(cookies_file, "w") as f:
74
+ f.write("\n".join(lines))
75
 
76
+ return cookies_file
 
77
 
78
+ # ═══════════════════════════════════════════════
79
+ # DOWNLOAD (YouTube / Instagram / Others)
80
+ # ═══════════════════════════════════════════════
81
+ def download_video(url, cookies_str=""):
82
+ cleanup_old_files()
83
+
84
+ is_youtube = any(x in url for x in ["youtube.com", "youtu.be"])
85
+ is_instagram = "instagram.com" in url
86
+ is_tiktok = "tiktok.com" in url
87
+
88
+ cookies_file = save_cookies(cookies_str)
89
+
90
+ # ── Base options ──
91
  ydl_opts = {
92
+ "format" : "bestaudio/best",
93
+ "outtmpl" : "downloaded_video.%(ext)s",
94
+ "quiet" : True,
95
  "nocheckcertificate": True,
96
+ "retries" : 5,
97
+ "fragment_retries" : 5,
98
+ "ignoreerrors" : False,
99
+ "http_headers" : {
100
+ "User-Agent": (
101
+ "Mozilla/5.0 (Linux; Android 12; Pixel 6) "
102
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
103
+ "Chrome/112.0.0.0 Mobile Safari/537.36"
104
+ ),
105
+ "Accept-Language": "en-US,en;q=0.9",
106
+ },
107
  }
108
 
109
+ if cookies_file:
110
+ ydl_opts["cookiefile"] = cookies_file
111
+
112
+ # ── YouTube-specific bypass (android player avoids bot detection) ──
113
+ if is_youtube:
114
+ ydl_opts["extractor_args"] = {
115
+ "youtube": {
116
+ "player_client": ["android", "ios", "web"],
117
+ "player_skip" : ["webpage"],
118
+ }
119
+ }
120
+ ydl_opts["http_headers"]["User-Agent"] = (
121
+ "com.google.android.youtube/17.36.4 "
122
+ "(Linux; U; Android 12; GB) gzip"
123
+ )
124
+
125
+ # ── Instagram-specific headers ──
126
+ if is_instagram:
127
+ ydl_opts["http_headers"] = {
128
+ "User-Agent" : "Instagram 219.0.0.12.117 Android (31/12; 420dpi; 1080x2400; samsung; SM-G991B; o1s; exynos2100; en_US; 346877738)",
129
+ "Accept" : "*/*",
130
+ "Accept-Language" : "en-US",
131
+ "X-IG-App-ID" : "936619743392459",
132
+ }
133
+ if not cookies_file:
134
+ raise ValueError(
135
+ "instagram_no_cookies"
136
+ )
137
+
138
+ # ── TikTok ──
139
+ if is_tiktok:
140
+ ydl_opts["http_headers"]["User-Agent"] = (
141
+ "TikTok 26.2.0 rv:262018 (iPhone; iOS 14.4.2; en_US) Cronet"
142
+ )
143
+
144
  with yt_dlp.YoutubeDL(ydl_opts) as ydl:
145
  ydl.download([url])
146
 
147
+ path = find_downloaded_file()
148
+ if not path:
149
+ raise FileNotFoundError("Download completed but file not found.")
150
+ return path
151
 
152
+ # ═══════════════════════════════════════════════
153
+ # AUDIO EXTRACTION
154
+ # ═══════════════════════════════════════════════
155
+ def extract_audio(video_path):
156
  audio_path = "extracted_audio.wav"
 
157
  if os.path.exists(audio_path):
158
  os.remove(audio_path)
159
 
160
  subprocess.run(
161
  [
162
+ get_ffmpeg(), "-y",
 
163
  "-i", video_path,
164
  "-vn",
165
  "-ac", "1",
 
169
  stdout=subprocess.DEVNULL,
170
  stderr=subprocess.DEVNULL
171
  )
 
172
  return audio_path
173
 
174
+ # ═══════════════════════════════════════════════
175
+ # SCRIPT NORMALIZER
176
+ # ═══════════════════════════════════════════════
177
  def normalize_script(text, lang):
178
  if lang == "hi":
179
  try:
180
  return transliterate(text, sanscript.ARABIC, sanscript.DEVANAGARI)
181
+ except Exception:
182
  return text
183
  return text
184
 
185
+ # ═══════════════════════════════════════════════
186
+ # MAIN TRANSCRIBE
187
+ # ═══════════════════════════════════════════════
188
+ def transcribe(url, file, lang_choice, cookies_str=""):
189
  try:
190
+ # ── FILE MODE ──
191
  if file:
192
  ext = os.path.splitext(file)[1].lower()
193
+ audio = file if ext in [".mp3", ".wav", ".m4a"] else extract_audio(file)
 
 
 
194
 
195
+ # ── URL MODE ──
196
+ elif url and url.strip():
197
+ try:
198
+ video = download_video(url.strip(), cookies_str)
199
+ except ValueError as ve:
200
+ if "instagram_no_cookies" in str(ve):
201
+ return (
202
+ "❌ Instagram requires login cookies to download.\n\n"
203
+ "πŸ“‹ How to get cookies:\n"
204
+ "1. Open Instagram in Chrome/Firefox\n"
205
+ "2. Install 'Cookie Editor' browser extension\n"
206
+ "3. Click Export β†’ Copy (Netscape format)\n"
207
+ "4. Paste in the πŸ” Cookies box below and try again.\n\n"
208
+ "Or simply download the video and upload the file directly."
209
+ )
210
+ raise
211
+ except Exception as e:
212
+ err = str(e).lower()
213
+ if "sign in" in err or "login" in err or "private" in err:
214
+ return (
215
+ "❌ This content requires login.\n\n"
216
+ "Paste your browser cookies in the πŸ” Cookies box and retry,\n"
217
+ "or download the video and upload it directly."
218
+ )
219
+ if "http error 429" in err or "too many" in err:
220
+ return "❌ Rate limited by the platform. Wait a few minutes and try again."
221
+ if "youtube" in err and ("bot" in err or "sign in" in err):
222
+ return (
223
+ "❌ YouTube bot detection triggered.\n\n"
224
+ "Paste your YouTube cookies in the πŸ” Cookies box and retry."
225
+ )
226
+ raise
227
+
228
+ audio = extract_audio(video)
229
 
230
  else:
231
  return "⚠️ Please paste a URL or upload a file."
232
 
233
+ # ── Safety check ──
234
+ if not os.path.exists(audio) or os.path.getsize(audio) < 5000:
235
+ return "❌ Audio extraction failed. File may be too short or corrupted."
236
 
237
+ # ── Transcribe ──
238
+ m = load_model()
239
  language = None if lang_choice == "Auto Detect" else lang_choice
240
 
241
+ segments, info = m.transcribe(
242
  audio,
243
  beam_size=1,
244
  vad_filter=True,
245
  language=language
246
  )
247
 
248
+ raw_text = " ".join(s.text for s in segments).strip()
249
  final_text = normalize_script(raw_text, info.language)
250
 
251
+ return f"🌍 Detected Language: {info.language.upper()}\n\n{final_text}"
252
 
253
  except Exception as e:
254
+ err = str(e).lower()
255
+ if "instagram" in err:
256
+ return (
257
+ "❌ Instagram download failed.\n"
258
+ "Upload the video file directly, or paste cookies and retry."
259
+ )
260
+ if "ffmpeg" in err:
261
+ return "❌ FFmpeg error during audio extraction."
262
  return f"❌ Error: {str(e)}"
263
 
264
+ # ═══════════════════════════════════════════════
265
+ # PREMIUM UI
266
+ # ═══════════════════════════════════════════════
267
+
268
+ CSS = """
269
+ /* ── Imports ── */
270
+ @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@700;900&family=Sora:wght@300;400;600;700&display=swap');
271
+
272
+ /* ── Base ── */
273
+ body, .gradio-container {
274
+ background: radial-gradient(ellipse at 20% 0%, #0d1b2a 0%, #0a0f1e 50%, #020408 100%) !important;
275
+ font-family: 'Sora', sans-serif !important;
276
+ }
277
+ footer { display: none !important; }
278
+ .gap { gap: 16px; }
279
+
280
+ /* ── RGB Animations ── */
281
+ @keyframes rgb-glow {
282
+ 0% { color:#ff0080; text-shadow:0 0 14px #ff0080,0 0 28px #ff0080; }
283
+ 16% { color:#ff8c00; text-shadow:0 0 14px #ff8c00,0 0 28px #ff8c00; }
284
+ 33% { color:#ffe600; text-shadow:0 0 14px #ffe600,0 0 28px #ffe600; }
285
+ 50% { color:#00ff88; text-shadow:0 0 14px #00ff88,0 0 28px #00ff88; }
286
+ 66% { color:#00c8ff; text-shadow:0 0 14px #00c8ff,0 0 28px #00c8ff; }
287
+ 83% { color:#a855f7; text-shadow:0 0 14px #a855f7,0 0 28px #a855f7; }
288
+ 100% { color:#ff0080; text-shadow:0 0 14px #ff0080,0 0 28px #ff0080; }
289
+ }
290
+ @keyframes rgb-border {
291
+ 0% { border-color:#ff0080; box-shadow:0 0 12px #ff0080,inset 0 0 8px rgba(255,0,128,.08); }
292
+ 16% { border-color:#ff8c00; box-shadow:0 0 12px #ff8c00,inset 0 0 8px rgba(255,140,0,.08); }
293
+ 33% { border-color:#ffe600; box-shadow:0 0 12px #ffe600,inset 0 0 8px rgba(255,230,0,.08); }
294
+ 50% { border-color:#00ff88; box-shadow:0 0 12px #00ff88,inset 0 0 8px rgba(0,255,136,.08); }
295
+ 66% { border-color:#00c8ff; box-shadow:0 0 12px #00c8ff,inset 0 0 8px rgba(0,200,255,.08); }
296
+ 83% { border-color:#a855f7; box-shadow:0 0 12px #a855f7,inset 0 0 8px rgba(168,85,247,.08); }
297
+ 100% { border-color:#ff0080; box-shadow:0 0 12px #ff0080,inset 0 0 8px rgba(255,0,128,.08); }
298
+ }
299
+ @keyframes gradient-bg {
300
+ 0% { background-position:0% 50%; }
301
+ 50% { background-position:100% 50%; }
302
+ 100% { background-position:0% 50%; }
303
+ }
304
+ @keyframes scan {
305
+ 0% { background-position:0 0; }
306
+ 100% { background-position:0 100px; }
307
+ }
308
+ @keyframes pulse-badge {
309
+ 0%,100% { transform:scale(1); box-shadow:0 0 18px rgba(0,200,255,.4); }
310
+ 50% { transform:scale(1.03);box-shadow:0 0 36px rgba(168,85,247,.6); }
311
+ }
312
+
313
+ /* ── Header ── */
314
+ .hero-wrap {
315
+ text-align: center;
316
+ padding: 32px 20px 20px;
317
+ position: relative;
318
+ }
319
+ .hero-title {
320
+ font-family: 'Orbitron', sans-serif;
321
+ font-size: clamp(1.6em, 4vw, 2.6em);
322
+ font-weight: 900;
323
+ letter-spacing: 2px;
324
+ animation: rgb-glow 3s linear infinite;
325
+ margin: 0 0 8px;
326
+ }
327
+ .hero-sub {
328
+ font-family: 'Sora', sans-serif;
329
+ font-size: .95em;
330
+ color: #94a3b8;
331
+ margin: 0 0 18px;
332
+ letter-spacing: .5px;
333
+ }
334
+ .made-badge {
335
+ display: inline-block;
336
+ font-family: 'Orbitron', sans-serif;
337
+ font-size: .72em;
338
+ font-weight: 700;
339
+ letter-spacing: 4px;
340
+ padding: 7px 26px;
341
+ border: 2px solid #00c8ff;
342
+ border-radius: 50px;
343
+ animation: rgb-glow 3s linear infinite, rgb-border 3s linear infinite, pulse-badge 3s ease-in-out infinite;
344
+ text-transform: uppercase;
345
+ background: rgba(0,0,0,.15);
346
+ }
347
+
348
+ /* ── Platform badges ── */
349
+ .platforms {
350
+ display: flex;
351
+ justify-content: center;
352
+ gap: 10px;
353
+ flex-wrap: wrap;
354
+ margin: 14px 0 0;
355
+ }
356
+ .plat-tag {
357
+ font-size: .72em;
358
+ padding: 4px 14px;
359
+ border-radius: 50px;
360
+ border: 1px solid rgba(255,255,255,.15);
361
+ color: #cbd5e1;
362
+ background: rgba(255,255,255,.05);
363
+ letter-spacing: .5px;
364
+ }
365
+ .plat-ok { border-color: rgba(0,255,136,.35); color:#00ff88; }
366
+ .plat-warn{ border-color: rgba(255,190,0,.35); color:#fbbf24; }
367
+
368
+ /* ── Glass card ── */
369
+ .glass-card {
370
+ background: rgba(255,255,255,.04) !important;
371
+ backdrop-filter: blur(24px) !important;
372
+ border: 1px solid rgba(255,255,255,.08) !important;
373
+ border-radius: 20px !important;
374
+ box-shadow: 0 24px 60px rgba(0,0,0,.5) !important;
375
+ }
376
+
377
+ /* ── Divider ── */
378
+ .rgb-line {
379
+ height: 2px;
380
+ margin: 28px 0;
381
+ background: linear-gradient(90deg,transparent,#00c8ff,#a855f7,#ff0080,transparent);
382
+ background-size: 200% 200%;
383
+ animation: gradient-bg 4s ease infinite;
384
+ border-radius: 2px;
385
+ }
386
+
387
+ /* ── Tabs ── */
388
+ .tabs .tab-nav button {
389
+ font-family: 'Sora', sans-serif !important;
390
+ font-weight: 600 !important;
391
+ font-size: .9em !important;
392
+ color: #64748b !important;
393
+ border-radius: 10px 10px 0 0 !important;
394
+ padding: 10px 20px !important;
395
+ transition: all .25s !important;
396
+ }
397
+ .tabs .tab-nav button.selected {
398
+ color: #00c8ff !important;
399
+ border-bottom: 2px solid #00c8ff !important;
400
+ background: rgba(0,200,255,.07) !important;
401
+ }
402
+
403
+ /* ── Inputs ── */
404
+ input, textarea, .gr-input, .gr-textarea, .codemirror-wrapper {
405
+ background: rgba(255,255,255,.06) !important;
406
+ border: 1px solid rgba(255,255,255,.1) !important;
407
+ color: #e2e8f0 !important;
408
+ border-radius: 12px !important;
409
+ font-family: 'Sora', sans-serif !important;
410
+ }
411
+ input:focus, textarea:focus {
412
+ border-color: #00c8ff !important;
413
+ box-shadow: 0 0 12px rgba(0,200,255,.2) !important;
414
+ outline: none !important;
415
+ }
416
+ label, .label-wrap span {
417
+ color: #94a3b8 !important;
418
+ font-family: 'Sora', sans-serif !important;
419
+ font-size: .85em !important;
420
+ letter-spacing: .3px !important;
421
+ }
422
+
423
+ /* ── Primary button ── */
424
+ button.primary, .gr-button-primary {
425
+ background: linear-gradient(135deg,#0ea5e9,#2563eb) !important;
426
+ border: none !important;
427
+ color: #fff !important;
428
+ font-family: 'Sora', sans-serif !important;
429
+ font-weight: 700 !important;
430
+ border-radius: 12px !important;
431
+ padding: 12px 20px !important;
432
+ font-size: 1em !important;
433
+ box-shadow: 0 4px 18px rgba(14,165,233,.35) !important;
434
+ transition: all .3s ease !important;
435
+ }
436
+ button.primary:hover {
437
+ background: linear-gradient(135deg,#38bdf8,#1d4ed8) !important;
438
+ box-shadow: 0 6px 28px rgba(14,165,233,.55) !important;
439
+ transform: translateY(-1px) !important;
440
+ }
441
+
442
+ /* ── Output code block ── */
443
+ .codemirror-wrapper {
444
+ background: rgba(0,0,0,.4) !important;
445
+ border: 1px solid rgba(0,200,255,.2) !important;
446
+ }
447
+ .CodeMirror {
448
+ background: transparent !important;
449
+ color: #a5f3fc !important;
450
+ font-family: 'Sora', sans-serif !important;
451
+ }
452
+
453
+ /* ── Accordion (cookies) ── */
454
+ .accordion-header button {
455
+ background: rgba(255,255,255,.04) !important;
456
+ border: 1px solid rgba(255,200,0,.2) !important;
457
+ color: #fbbf24 !important;
458
+ border-radius: 12px !important;
459
+ font-family: 'Sora', sans-serif !important;
460
+ font-weight: 600 !important;
461
+ }
462
+
463
+ /* ── Warning note ── */
464
+ .note-warn {
465
+ background: rgba(251,191,36,.07);
466
+ border: 1px solid rgba(251,191,36,.25);
467
+ border-radius: 12px;
468
+ padding: 12px 18px;
469
+ color: #fbbf24;
470
+ font-size: .82em;
471
+ line-height: 1.6;
472
+ margin-top: 6px;
473
+ }
474
+
475
+ /* ── Dropdown ── */
476
+ .gr-dropdown select, select {
477
+ background: rgba(255,255,255,.06) !important;
478
+ color: #e2e8f0 !important;
479
+ border: 1px solid rgba(255,255,255,.1) !important;
480
+ border-radius: 10px !important;
481
+ }
482
+
483
+ /* ── Scrollbar ── */
484
+ ::-webkit-scrollbar { width:6px; }
485
+ ::-webkit-scrollbar-track { background:transparent; }
486
+ ::-webkit-scrollbar-thumb { background:#1e40af; border-radius:4px; }
487
  """
488
 
489
+ HEADER_HTML = """
490
+ <div class="hero-wrap">
491
+ <div class="hero-title">πŸŽ™οΈ Universal Transcript Tool</div>
492
+ <div class="hero-sub">Speech to text β€” any platform, any language</div>
493
+ <div class="made-badge">✦ Made by Deepu ✦</div>
494
+ <div class="platforms">
495
+ <span class="plat-tag plat-ok">βœ” YouTube</span>
496
+ <span class="plat-tag plat-ok">βœ” TikTok</span>
497
+ <span class="plat-tag plat-ok">βœ” Facebook</span>
498
+ <span class="plat-tag plat-ok">βœ” Twitter / X</span>
499
+ <span class="plat-tag plat-warn">⚠ Instagram (needs cookies)</span>
500
+ <span class="plat-tag plat-ok">βœ” File Upload</span>
501
+ </div>
502
+ </div>
503
+ """
504
+
505
+ COOKIES_HELP = """
506
+ <div class="note-warn">
507
+ <strong>πŸ” When do you need cookies?</strong><br>
508
+ Instagram always Β· Private YouTube Β· Age-restricted content<br><br>
509
+ <strong>How to get cookies (2 steps):</strong><br>
510
+ 1. Install <em>Cookie Editor</em> extension in Chrome / Firefox<br>
511
+ 2. Open the site β†’ click extension β†’ <strong>Export β†’ Netscape format</strong> β†’ paste here
512
+ </div>
513
+ """
514
+
515
+ # ═══════════════════════════════════════════════
516
+ # BUILD UI
517
+ # ═══════════════════════════════════════════════
518
+ with gr.Blocks(css=CSS, theme=gr.themes.Base()) as demo:
519
 
520
+ gr.HTML(HEADER_HTML)
521
+ gr.HTML('<div class="rgb-line"></div>')
522
+
523
+ with gr.Column(elem_classes="glass-card", scale=1):
524
+
525
+ with gr.Tabs(elem_classes="tabs"):
526
+
527
+ # ── Tab 1: URL ──
528
  with gr.TabItem("πŸ”— Paste Link"):
529
+ url_input = gr.Textbox(
530
+ label="Video URL",
531
+ placeholder="https://youtube.com/watch?v=... or TikTok / Facebook / Twitter link",
532
+ lines=1
533
+ )
534
  btn_url = gr.Button("🎧 Transcribe Link", variant="primary")
535
 
536
+ # ── Tab 2: Upload ──
537
  with gr.TabItem("πŸ“‚ Upload File"):
538
+ file_input = gr.File(
539
  label="Upload Video / Audio",
540
+ file_types=[".mp4", ".mkv", ".mov", ".webm", ".avi",
541
+ ".mp3", ".wav", ".m4a"]
542
  )
543
  btn_file = gr.Button("πŸ“‚ Transcribe File", variant="primary")
544
 
545
+ gr.HTML('<div class="rgb-line"></div>')
546
+
547
+ # ── Language ──
548
  lang = gr.Dropdown(
549
  label="🌍 Transcript Language",
550
  choices=[
551
+ "Auto Detect", "hi", "ur", "en", "ar",
552
+ "fr", "de", "es", "ru", "ja", "zh"
 
 
 
 
 
 
 
 
 
553
  ],
554
  value="Auto Detect"
555
  )
556
 
557
+ # ── Cookies (Accordion) ──
558
+ with gr.Accordion("πŸ” Cookies β€” Instagram / Private Content (click to expand)", open=False):
559
+ gr.HTML(COOKIES_HELP)
560
+ cookies_input = gr.Textbox(
561
+ label="Paste Netscape Cookies Here",
562
+ placeholder="# Netscape HTTP Cookie File\n.instagram.com TRUE / FALSE 9999999999 sessionid XXXXX",
563
+ lines=6
564
+ )
565
 
566
+ gr.HTML('<div class="rgb-line"></div>')
567
+
568
+ # ── Output ──
569
+ output = gr.Code(label="πŸ“„ Transcript Output", lines=14, language=None)
570
+
571
+ # ── Events ──
572
+ btn_url.click(
573
+ fn=transcribe,
574
+ inputs=[url_input, gr.State(None), lang, cookies_input],
575
+ outputs=output
576
+ )
577
+ btn_file.click(
578
+ fn=transcribe,
579
+ inputs=[gr.State(None), file_input, lang, cookies_input],
580
+ outputs=output
581
+ )
582
 
583
+ demo.launch()