Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import subprocess | |
| import os | |
| import re | |
| import uuid | |
| import threading | |
| import time | |
| import shutil | |
| import hashlib | |
| from datetime import datetime, timedelta | |
| # --------------------------------------------------------------------------- | |
| # π SECRET ACCESS KEY GATE | |
| # --------------------------------------------------------------------------- | |
| SECRET_KEY_HASH = "1c107e39306b96cb5e21d4ef3e968eb51397f516313c6a9b522add0193b995e1" | |
| ACCESS_KEY = os.environ.get("APP_ACCESS_KEY", "") | |
| def is_authorized(): | |
| if not ACCESS_KEY or SECRET_KEY_HASH == "PUT_YOUR_SHA256_HASH_HERE": | |
| return False | |
| return hashlib.sha256(ACCESS_KEY.encode()).hexdigest() == SECRET_KEY_HASH | |
| # Directory for storing jobs and results | |
| JOBS_DIR = "jobs" | |
| os.makedirs(JOBS_DIR, exist_ok=True) | |
| # Dictionary to keep track of job status | |
| jobs_db = {} | |
| # Cache: GPU (nvenc) available hai ya nahi | |
| _GPU_AVAILABLE = None | |
| def gpu_available(): | |
| """Check karta hai ki h264_nvenc encoder actually kaam karta hai is machine par.""" | |
| global _GPU_AVAILABLE | |
| if _GPU_AVAILABLE is not None: | |
| return _GPU_AVAILABLE | |
| try: | |
| test = subprocess.run( | |
| ['ffmpeg', '-f', 'lavfi', '-i', 'color=black:s=64x64:d=0.1', | |
| '-c:v', 'h264_nvenc', '-f', 'null', '-'], | |
| capture_output=True, text=True, timeout=20 | |
| ) | |
| _GPU_AVAILABLE = (test.returncode == 0) | |
| except Exception: | |
| _GPU_AVAILABLE = False | |
| return _GPU_AVAILABLE | |
| def cleanup_old_jobs(): | |
| """Removes jobs and files older than 7 hours.""" | |
| while True: | |
| now = datetime.now() | |
| to_delete = [] | |
| for job_code, info in jobs_db.items(): | |
| if now - info["timestamp"] > timedelta(hours=7): | |
| to_delete.append(job_code) | |
| for job_code in to_delete: | |
| info = jobs_db.pop(job_code) | |
| if info["output"] and os.path.exists(info["output"]): | |
| try: | |
| os.remove(info["output"]) | |
| except: | |
| pass | |
| input_file = os.path.join(JOBS_DIR, f"input_{job_code}.mp4") | |
| if os.path.exists(input_file): | |
| try: | |
| os.remove(input_file) | |
| except: | |
| pass | |
| time.sleep(3600) | |
| threading.Thread(target=cleanup_old_jobs, daemon=True).start() | |
| def get_duration(path): | |
| """Get total duration (seconds) of input video. Tries multiple methods.""" | |
| duration = 0.0 | |
| try: | |
| result = subprocess.run( | |
| ['ffprobe', '-v', 'error', '-show_entries', 'format=duration', | |
| '-of', 'default=noprint_wrapper=1:nokey=1', path], | |
| capture_output=True, text=True, timeout=30 | |
| ) | |
| val = result.stdout.strip() | |
| if val and val != 'N/A': | |
| duration = float(val) | |
| except Exception: | |
| duration = 0.0 | |
| if duration <= 0: | |
| try: | |
| result = subprocess.run( | |
| ['ffprobe', '-v', 'error', '-select_streams', 'v:0', | |
| '-show_entries', 'stream=duration', | |
| '-of', 'default=noprint_wrapper=1:nokey=1', path], | |
| capture_output=True, text=True, timeout=30 | |
| ) | |
| val = result.stdout.strip() | |
| if val and val != 'N/A': | |
| duration = float(val) | |
| except Exception: | |
| pass | |
| if duration <= 0: | |
| try: | |
| result = subprocess.run( | |
| ['ffprobe', '-v', 'error', '-count_packets', | |
| '-show_entries', 'stream=duration', | |
| '-of', 'default=noprint_wrapper=1:nokey=1', path], | |
| capture_output=True, text=True, timeout=60 | |
| ) | |
| for line in result.stdout.strip().split('\n'): | |
| line = line.strip() | |
| if line and line != 'N/A': | |
| try: | |
| val = float(line) | |
| if val > duration: | |
| duration = val | |
| except ValueError: | |
| pass | |
| except Exception: | |
| pass | |
| if duration <= 0: | |
| try: | |
| result = subprocess.run( | |
| ['ffmpeg', '-i', path], | |
| capture_output=True, text=True, timeout=30 | |
| ) | |
| match = re.search(r'Duration:\s*(\d+):(\d+):(\d+\.\d+)', result.stderr) | |
| if match: | |
| h, m, s = match.groups() | |
| duration = int(h) * 3600 + int(m) * 60 + float(s) | |
| except Exception: | |
| pass | |
| if duration <= 0: | |
| try: | |
| result = subprocess.run( | |
| ['ffmpeg', '-sseof', '-1', '-i', path, '-f', 'null', '-'], | |
| capture_output=True, text=True, timeout=60 | |
| ) | |
| matches = re.findall(r'time=(\d\d:\d\d:\d\d\.\d+)', result.stderr) | |
| if matches: | |
| last_time = matches[-1] | |
| h, m, s = last_time.split(":") | |
| duration = int(h) * 3600 + int(m) * 60 + float(s) | |
| except Exception: | |
| pass | |
| return duration | |
| def get_embedded_title(path): | |
| """Try to read an embedded 'title' tag from the video's metadata.""" | |
| try: | |
| result = subprocess.run( | |
| ['ffprobe', '-v', 'error', '-show_entries', 'format_tags=title', | |
| '-of', 'default=noprint_wrapper=1:nokey=1', path], | |
| capture_output=True, text=True | |
| ) | |
| title = result.stdout.strip() | |
| if title: | |
| return title | |
| result = subprocess.run( | |
| ['ffprobe', '-v', 'error', '-show_entries', 'stream_tags=title', | |
| '-of', 'default=noprint_wrapper=1:nokey=1', path], | |
| capture_output=True, text=True | |
| ) | |
| title = result.stdout.strip().split("\n")[0].strip() | |
| return title if title else None | |
| except: | |
| return None | |
| def time_str_to_seconds(t): | |
| """Convert ffmpeg HH:MM:SS.xx time string to seconds.""" | |
| try: | |
| h, m, s = t.split(":") | |
| return int(h) * 3600 + int(m) * 60 + float(s) | |
| except: | |
| return 0.0 | |
| def format_eta(seconds): | |
| if seconds <= 0 or seconds != seconds: | |
| return "--:--" | |
| seconds = int(seconds) | |
| m, s = divmod(seconds, 60) | |
| h, m = divmod(m, 60) | |
| if h: | |
| return f"{h:02d}:{m:02d}:{s:02d}" | |
| return f"{m:02d}:{s:02d}" | |
| STATS_RE = re.compile( | |
| r'frame=\s*(\d+).*?fps=\s*([\d.]+).*?time=(\d\d:\d\d:\d\d\.\d+).*?speed=\s*([\d.]+)x' | |
| ) | |
| def clean_base_name(name): | |
| """Cleans an original filename into a safe base string.""" | |
| base, _ext = os.path.splitext(name) | |
| cleaned = re.sub(r'[^a-zA-Z0-9]+', '_', base).strip('_') | |
| return cleaned if cleaned else "video" | |
| # --------------------------------------------------------------------------- | |
| # ποΈ EQUALIZER PRESETS & FUNCTIONS | |
| # --------------------------------------------------------------------------- | |
| EQ_PRESETS = { | |
| "Custom": {"60Hz": 0, "230Hz": 0, "910Hz": 0, "3.6kHz": 0, "14kHz": 0}, | |
| "Normal": {"60Hz": 3, "230Hz": 0, "910Hz": 0, "3.6kHz": 0, "14kHz": 3}, | |
| "Classical": {"60Hz": 0, "230Hz": 0, "910Hz": 0, "3.6kHz": 5, "14kHz": 7}, | |
| "Dance": {"60Hz": 9, "230Hz": 7, "910Hz": 2, "3.6kHz": 0, "14kHz": 1}, | |
| "Flat": {"60Hz": 0, "230Hz": 0, "910Hz": 0, "3.6kHz": 0, "14kHz": 0}, | |
| "Bass Boost": {"60Hz": 12, "230Hz": 8, "910Hz": 0, "3.6kHz": -2, "14kHz": -4}, | |
| "Vocal": {"60Hz": -3, "230Hz": 0, "910Hz": 5, "3.6kHz": 7, "14kHz": 3}, | |
| "Rock": {"60Hz": 8, "230Hz": 4, "910Hz": -2, "3.6kHz": 4, "14kHz": 8}, | |
| } | |
| def build_eq_filter(hz60, hz230, hz910, khz3_6, khz14): | |
| """Build FFmpeg superequalizer/firequalizer filter string from 5-band EQ values.""" | |
| eq_filter = ( | |
| f"firequalizer=gain_entry=" | |
| f"'entry(60,{hz60});entry(230,{hz230});entry(910,{hz910});" | |
| f"entry(3600,{khz3_6});entry(14000,{khz14})'" | |
| ) | |
| return eq_filter | |
| def _video_encode_args(variant, crf_or_cq): | |
| """GPU (nvenc) ya CPU (libx264) ke liye encoder args deta hai.""" | |
| if variant == "gpu": | |
| return ['-c:v', 'h264_nvenc', '-preset', 'p1', '-tune', 'll', '-cq', str(crf_or_cq)] | |
| return ['-vcodec', 'libx264', '-preset', 'ultrafast', '-crf', str(crf_or_cq)] | |
| def eq_preview(input_video, hz60, hz230, hz910, khz3_6, khz14): | |
| """Generate a 10-second preview clip with the EQ settings applied.""" | |
| if input_video is None: | |
| return None, "β οΈ Pehle video upload karo!" | |
| eq_filter = build_eq_filter(hz60, hz230, hz910, khz3_6, khz14) | |
| preview_id = str(uuid.uuid4())[:6] | |
| preview_path = os.path.join(JOBS_DIR, f"eq_preview_{preview_id}.mp4") | |
| total_dur = get_duration(input_video) | |
| start_time = min(5.0, max(0, total_dur - 15)) if total_dur > 15 else 0 | |
| variants = ["gpu", "cpu"] if gpu_available() else ["cpu"] | |
| last_err = "" | |
| for variant in variants: | |
| command = [ | |
| 'ffmpeg', '-y', | |
| '-ss', str(start_time), | |
| '-i', input_video, | |
| '-t', '10', | |
| '-af', eq_filter, | |
| ] + _video_encode_args(variant, 28) + [ | |
| '-acodec', 'aac', '-b:a', '128k', '-ar', '44100', | |
| '-pix_fmt', 'yuv420p', | |
| preview_path | |
| ] | |
| try: | |
| result = subprocess.run(command, capture_output=True, text=True, timeout=30) | |
| if result.returncode == 0 and os.path.exists(preview_path): | |
| tag = "GPU" if variant == "gpu" else "CPU" | |
| return preview_path, f"β Preview ready ({tag})! EQ: 60Hz={hz60}dB, 230Hz={hz230}dB, 910Hz={hz910}dB, 3.6kHz={khz3_6}dB, 14kHz={khz14}dB" | |
| last_err = result.stderr[-200:] | |
| except subprocess.TimeoutExpired: | |
| last_err = "Preview timeout β video too heavy for quick preview." | |
| except Exception as e: | |
| last_err = str(e) | |
| return None, f"β Preview failed: {last_err}" | |
| def apply_preset(preset_name): | |
| """Returns slider values for a given preset.""" | |
| preset = EQ_PRESETS.get(preset_name, EQ_PRESETS["Flat"]) | |
| return preset["60Hz"], preset["230Hz"], preset["910Hz"], preset["3.6kHz"], preset["14kHz"] | |
| # --------------------------------------------------------------------------- | |
| # MAIN PROCESSING | |
| # --------------------------------------------------------------------------- | |
| def _build_main_command(variant, input_path, mode, output_filename, obf_opts): | |
| mirror = obf_opts.get("mirror", False) | |
| zoom = obf_opts.get("zoom", False) | |
| color = obf_opts.get("color", False) | |
| noise = obf_opts.get("noise", False) | |
| speed = obf_opts.get("speed", False) | |
| v_actions = [] | |
| if mirror: | |
| v_actions.append("hflip") | |
| if zoom: | |
| v_actions.append("crop=iw*0.96:ih*0.96,scale=iw:ih") | |
| if color: | |
| v_actions.append("eq=saturation=1.05:contrast=1.02:brightness=0.01") | |
| if noise: | |
| v_actions.append("noise=alls=3:allf=t") | |
| if speed: | |
| v_actions.append("setpts=PTS/1.006") | |
| filter_prep = "" | |
| main_v0 = "[0:v]" | |
| main_v1 = "[1:v]" | |
| if v_actions: | |
| v_filter_chain = ",".join(v_actions) | |
| filter_prep += f"[0:v]{v_filter_chain}[vobf0];" | |
| main_v0 = "[vobf0]" | |
| if mode == "Full Screen": | |
| filter_prep += f"[1:v]{v_filter_chain}[vobf1];" | |
| main_v1 = "[vobf1]" | |
| if mode == "Full Screen": | |
| fc = filter_prep + ( | |
| f"{main_v0}scale=iw:ih[v2];{main_v1}crop=in_w/2:in_h/2,boxblur=1:1,scale=iw*2:ih*2[v1];" | |
| f"[v2][v1]overlay=1:enable='gte(mod(t,5),3)':x=0:y=0;" | |
| f"[0:a]atempo=1,bass=frequency=200:gain=-90,volume=+20dB,aecho=1:0.6:2:0.4," | |
| f"bass=g=3:f=110:w=20,bass=g=10:f=500:w=20,bass=g=3:f=300:w=30,bass=g=10:f=110:w=20,bass=g=20:f=110:w=40," | |
| f"firequalizer=gain_entry='entry(0,-23);entry(250,-11.5);entry(6000,0);entry(12000,8);entry(16000,16)'," | |
| f"compand=attacks=7:decays=1:points=-90/-90 -70/-60 -15/-15 0/-10:soft-knee=1:volume=-70:gain=3," | |
| f"pan=stereo| FL < FL + 0.5*FC + 0.6*BL + 0.6*SL | FR < FR + 2*FC + 1*BR + 2*SR,highpass=f=300,lowpass=f=700,volume=6[a1];" | |
| f"amovie=Bg2.mp4:loop=9999,volume=1[a2];[a1][a2]amix=duration=shortest" | |
| ) | |
| if speed: | |
| fc = fc.replace("atempo=1", "atempo=1.006") | |
| return [ | |
| 'ffmpeg', '-y', | |
| '-i', input_path, | |
| '-ss', '4', '-i', input_path, | |
| '-filter_complex', fc, | |
| ] + _video_encode_args(variant, 30) + [ | |
| '-pix_fmt', 'yuv420p', '-r', '30', '-g', '60', '-b:v', '1550k', '-shortest', | |
| '-acodec', 'aac', '-b:a', '128k', '-ar', '44100', '-threads', '0', | |
| output_filename | |
| ] | |
| else: | |
| fc = filter_prep + ( | |
| f"{main_v0}scale=410:280,setsar=1:1[vbox];" | |
| f"movie=ibg.mp4:loop=999,setpts=N/(FRAME_RATE*TB)[bg];" | |
| f"[bg][vbox]overlay=shortest=1:x=18:y=102[vmain2];" | |
| f"[0:a]atempo=1,bass=frequency=200:gain=-90,volume=+20dB,aecho=1:0.6:2:0.4," | |
| f"bass=g=3:f=110:w=20,bass=g=10:f=500:w=20,bass=g=3:f=300:w=30,bass=g=10:f=110:w=20,bass=g=20:f=110:w=40," | |
| f"firequalizer=gain_entry='entry(0,-23);entry(250,-11.5);entry(6000,0);entry(12000,8);entry(16000,16)'," | |
| f"compand=attacks=7:decays=1:points=-90/-90 -70/-60 -15/-15 0/-10:soft-knee=1:volume=-70:gain=3," | |
| f"pan=stereo| FL < FL + 0.5*FC + 0.6*BL + 0.6*SL | FR < FR + 2*FC + 1*BR + 2*SR,highpass=f=300,lowpass=f=700,volume=6[a1];" | |
| f"amovie=Bg2.mp4:loop=9999,volume=1[a2];[a1][a2]amix=duration=shortest[amain]" | |
| ) | |
| if speed: | |
| fc = fc.replace("atempo=1", "atempo=1.006") | |
| return [ | |
| 'ffmpeg', '-y', | |
| '-i', input_path, | |
| '-i', 'blocker.mp4', | |
| '-i', 'Myvideo.mp4', | |
| '-filter_complex', fc, | |
| '-map', '[vmain2]', '-map', '[amain]', | |
| ] + _video_encode_args(variant, 26) + [ | |
| '-pix_fmt', 'yuv420p', '-r', '30', '-g', '60', '-b:v', '2000k', '-shortest', | |
| '-acodec', 'aac', '-b:a', '128k', '-ar', '44100', '-threads', '0', | |
| output_filename | |
| ] | |
| def _run_and_track(command, job_code, total_duration, start_time): | |
| process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1) | |
| max_time_seen = 0.0 | |
| for line in process.stdout: | |
| jobs_db[job_code]["logs"] = (jobs_db[job_code]["logs"] + line)[-2000:] | |
| match = STATS_RE.search(line) | |
| if match: | |
| frame, fps, cur_time_str, speed = match.groups() | |
| cur_seconds = time_str_to_seconds(cur_time_str) | |
| fps_val = float(fps) | |
| speed_val = float(speed) | |
| if cur_seconds > max_time_seen: | |
| max_time_seen = cur_seconds | |
| progress = 0.0 | |
| eta = 0.0 | |
| if total_duration > 0: | |
| progress = min(99.9, (cur_seconds / total_duration) * 100) | |
| remaining = max(0.0, total_duration - cur_seconds) | |
| if speed_val > 0: | |
| eta = remaining / speed_val | |
| jobs_db[job_code].update({ | |
| "fps": fps_val, | |
| "speed": speed_val, | |
| "cur_time": cur_seconds, | |
| "progress": progress, | |
| "eta": eta, | |
| "elapsed": time.time() - start_time, | |
| }) | |
| if total_duration <= 0 and max_time_seen > 0: | |
| jobs_db[job_code]["duration"] = max_time_seen | |
| process.wait() | |
| return process.returncode == 0, max_time_seen | |
| def run_ffmpeg(job_code, input_path, mode, clean_base, obf_opts): | |
| output_filename = os.path.join(JOBS_DIR, f"{clean_base}_{job_code}.mp4") | |
| total_duration = get_duration(input_path) | |
| jobs_db[job_code]["duration"] = total_duration | |
| start_time = time.time() | |
| max_time_seen = 0.0 | |
| ok = False | |
| variants = ["gpu", "cpu"] if gpu_available() else ["cpu"] | |
| for variant in variants: | |
| command = _build_main_command(variant, input_path, mode, output_filename, obf_opts) | |
| ok, seen = _run_and_track(command, job_code, total_duration, start_time) | |
| max_time_seen = max(max_time_seen, seen) | |
| if ok: | |
| break | |
| jobs_db[job_code]["progress"] = 0.0 | |
| if ok: | |
| jobs_db[job_code]["status"] = "Completed" | |
| jobs_db[job_code]["output"] = output_filename | |
| jobs_db[job_code]["progress"] = 100.0 | |
| jobs_db[job_code]["eta"] = 0.0 | |
| if jobs_db[job_code]["duration"] <= 0 and max_time_seen > 0: | |
| jobs_db[job_code]["duration"] = max_time_seen | |
| else: | |
| jobs_db[job_code]["status"] = "Failed" | |
| jobs_db[job_code]["total_time_taken"] = time.time() - start_time | |
| if os.path.exists(input_path): | |
| os.remove(input_path) | |
| VALID_VIDEO_EXT = {".mp4", ".mov", ".mkv", ".webm", ".avi", ".m4v", ".3gp", ".flv"} | |
| def start_job(input_video, mode, selected_obf_features): | |
| if input_video is None: | |
| return "β οΈ Please upload a video.", None | |
| ext = os.path.splitext(input_video)[1].lower() | |
| if ext not in VALID_VIDEO_EXT: | |
| return f"β οΈ '{ext}' supported nahi hai. Sirf video files upload karo.", None | |
| job_code = str(uuid.uuid4())[:8].upper() | |
| original_name = os.path.basename(input_video) | |
| temp_input = os.path.join(JOBS_DIR, f"input_{job_code}.mp4") | |
| shutil.copy(input_video, temp_input) | |
| embedded_title = get_embedded_title(temp_input) | |
| display_name = embedded_title if embedded_title else original_name | |
| jobs_db[job_code] = { | |
| "status": "Processing", | |
| "output": None, | |
| "logs": "Job started...\n", | |
| "timestamp": datetime.now(), | |
| "mode": mode, | |
| "filename": display_name, | |
| "duration": 0.0, | |
| "cur_time": 0.0, | |
| "fps": 0.0, | |
| "speed": 0.0, | |
| "progress": 0.0, | |
| "eta": 0.0, | |
| "elapsed": 0.0, | |
| "total_time_taken": 0.0, | |
| } | |
| obf_opts = { | |
| "mirror": "Mirror Flip" in selected_obf_features, | |
| "zoom": "Zoom/Crop Slightly" in selected_obf_features, | |
| "color": "Color Shift" in selected_obf_features, | |
| "noise": "Subtle Noise/Grain" in selected_obf_features, | |
| "speed": "Speed Micro-variation" in selected_obf_features | |
| } | |
| thread = threading.Thread(target=run_ffmpeg, args=(job_code, temp_input, mode, clean_base_name(display_name), obf_opts)) | |
| thread.start() | |
| return f"π Job started! Your Job Code is: {job_code}\nCopy this code to check status later.", job_code | |
| def build_meter_html(info): | |
| if not info: | |
| return "<div class='meter-box meter-idle'>No active job yet.</div>" | |
| status = info["status"] | |
| progress = info.get("progress", 0.0) | |
| fps = info.get("fps", 0.0) | |
| speed = info.get("speed", 0.0) | |
| cur_time = info.get("cur_time", 0.0) | |
| total_duration = info.get("duration", 0.0) | |
| eta = info.get("eta", 0.0) | |
| elapsed = info.get("elapsed", info.get("total_time_taken", 0.0)) | |
| status_color = "#00ff9d" if status == "Completed" else ("#ff4d4d" if status == "Failed" else "#00c3ff") | |
| return f""" | |
| <div class="meter-box"> | |
| <div class="meter-status" style="color:{status_color};">β {status}</div> | |
| <div class="progress-track"> | |
| <div class="progress-fill" style="width:{progress:.1f}%;"></div> | |
| </div> | |
| <div class="progress-pct">{progress:.1f}%</div> | |
| <div class="stats-grid"> | |
| <div class="stat-card"> | |
| <div class="stat-label">FPS</div> | |
| <div class="stat-value">{fps:.1f}</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-label">Speed</div> | |
| <div class="stat-value">{speed:.2f}x</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-label">Processed</div> | |
| <div class="stat-value">{format_eta(cur_time)}</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-label">Total</div> | |
| <div class="stat-value">{format_eta(total_duration)}</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-label">ETA</div> | |
| <div class="stat-value">{format_eta(eta)}</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-label">Elapsed</div> | |
| <div class="stat-value">{format_eta(elapsed)}</div> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| def live_meter_update(job_code): | |
| if not job_code: | |
| return build_meter_html(None) | |
| info = jobs_db.get(job_code.strip().upper()) | |
| return build_meter_html(info) | |
| def check_status(job_code): | |
| job_code = job_code.strip().upper() | |
| if job_code not in jobs_db: | |
| return "Job Code not found.", None, "", build_meter_html(None) | |
| info = jobs_db[job_code] | |
| status_msg = f"Status: {info['status']}\nFile: {info.get('filename', '-')}\nMode: {info['mode']}\nTime: {info['timestamp'].strftime('%Y-%m-%d %H:%M:%S')}" | |
| return status_msg, info["output"], info["logs"], build_meter_html(info) | |
| def get_history(): | |
| if not jobs_db: | |
| return "No history available." | |
| history = "| Job Code | File Name | Status | Mode | Time |\n| --- | --- | --- | --- | --- |\n" | |
| for code, info in sorted(jobs_db.items(), key=lambda x: x[1]['timestamp'], reverse=True): | |
| history += f"| {code} | {info.get('filename', '-')} | {info['status']} | {info['mode']} | {info['timestamp'].strftime('%H:%M:%S')} |\n" | |
| return history | |
| # --------------------------------------------------------------------------- | |
| # PREMIUM UI / CSS | |
| # --------------------------------------------------------------------------- | |
| CUSTOM_CSS = """ | |
| @import url('https://fonts.googleapis.com/css2?family=Poppins:wght=400;600;800&display=swap'); | |
| * { font-family: 'Poppins', sans-serif; } | |
| body, .gradio-container { | |
| background: linear-gradient(160deg, #ffffff 0%, #f3f5ff 45%, #eef0ff 100%) !important; | |
| } | |
| #rgb-title { | |
| text-align: center; | |
| font-size: 2.4rem; | |
| font-weight: 800; | |
| letter-spacing: 1px; | |
| margin: 6px 0 2px 0; | |
| background: linear-gradient(90deg, #ff0055, #ff9900, #d4c400, #22c55e, #00b8d9, #0066ff, #a855f7, #ff0055); | |
| background-size: 400% 400%; | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| animation: rgbFlow 4s linear infinite; | |
| } | |
| @keyframes rgbFlow { | |
| 0% { background-position: 0% 50%; } | |
| 50% { background-position: 100% 50%; } | |
| 100% { background-position: 0% 50%; } | |
| } | |
| #subtitle { | |
| text-align: center; | |
| color: #4b5066 !important; | |
| margin-bottom: 18px; | |
| font-size: 0.95rem; | |
| } | |
| .gr-panel, .block, .form { | |
| background: #ffffff !important; | |
| border: 1px solid #e3e6f5 !important; | |
| border-radius: 18px !important; | |
| box-shadow: 0 4px 24px rgba(100,110,220,0.08) !important; | |
| } | |
| button.primary, .gr-button-primary { | |
| background: linear-gradient(90deg, #ff0055, #7000ff) !important; | |
| border: none !important; | |
| color: white !important; | |
| font-weight: 700 !important; | |
| box-shadow: 0 6px 20px rgba(112,0,255,0.28); | |
| transition: transform 0.15s ease; | |
| } | |
| button.primary:hover { transform: translateY(-2px); } | |
| .meter-box { | |
| background: #ffffff; | |
| border: 1px solid #e3e6f5; | |
| border-radius: 18px; | |
| padding: 18px 20px; | |
| box-shadow: 0 6px 28px rgba(100,110,220,0.10); | |
| } | |
| .meter-idle { | |
| text-align: center; | |
| color: #8a90ab; | |
| font-size: 0.9rem; | |
| padding: 30px; | |
| } | |
| .meter-status { | |
| font-weight: 800; | |
| font-size: 1rem; | |
| margin-bottom: 10px; | |
| letter-spacing: 0.5px; | |
| } | |
| .progress-track { | |
| width: 100%; | |
| height: 14px; | |
| border-radius: 10px; | |
| background: #eef0fb; | |
| overflow: hidden; | |
| margin-bottom: 6px; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| border-radius: 10px; | |
| background: linear-gradient(90deg, #00b8d9, #7000ff, #ff0055); | |
| background-size: 300% 100%; | |
| animation: rgbFlow 3s linear infinite; | |
| transition: width 0.4s ease; | |
| } | |
| .progress-pct { | |
| text-align: right; | |
| font-size: 0.8rem; | |
| color: #6b7190; | |
| margin-bottom: 14px; | |
| } | |
| .stats-grid { | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: 10px; | |
| } | |
| .stat-card { | |
| background: #f6f7fd; | |
| border: 1px solid #e3e6f5; | |
| border-radius: 12px; | |
| padding: 10px 6px; | |
| text-align: center; | |
| } | |
| .stat-label { | |
| font-size: 0.72rem; | |
| color: #6b7190; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| margin-bottom: 4px; | |
| font-weight: 700; | |
| } | |
| .stat-value { | |
| font-size: 1.15rem; | |
| font-weight: 800; | |
| color: #0077aa; | |
| } | |
| .stat-card:nth-child(2n) .stat-value { | |
| color: #d6003d; | |
| } | |
| #made-by { | |
| text-align: center; | |
| margin-top: 22px; | |
| font-size: 1rem; | |
| font-weight: 800; | |
| background: linear-gradient(90deg, #ff0055, #ff9900, #d4c400, #22c55e, #00b8d9, #0066ff, #a855f7, #ff0055); | |
| background-size: 400% 400%; | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| animation: rgbFlow 4s linear infinite; | |
| } | |
| .eq-section { | |
| background: linear-gradient(135deg, #1a1c2e 0%, #2d1f4e 100%); | |
| border-radius: 18px; | |
| padding: 20px; | |
| margin: 10px 0; | |
| } | |
| .eq-preset-btn { | |
| border-radius: 20px !important; | |
| font-weight: 700 !important; | |
| font-size: 0.85rem !important; | |
| } | |
| .gradio-container * { | |
| color: #1a1c2e; | |
| } | |
| .gradio-container .tab-nav button, | |
| .gradio-container button { | |
| color: #1a1c2e !important; | |
| } | |
| button.primary, button.primary * { | |
| color: #ffffff !important; | |
| } | |
| .gradio-container label span, | |
| .gradio-container .label-wrap span { | |
| color: #2b2e45 !important; | |
| font-weight: 600 !important; | |
| } | |
| .gradio-container table, | |
| .gradio-container th, | |
| .gradio-container td { | |
| color: #1a1c2e !important; | |
| border-color: #e3e6f5 !important; | |
| } | |
| .gradio-container thead th { | |
| background: #eef0fb !important; | |
| } | |
| .gradio-container tbody tr:nth-child(odd) { | |
| background: #f8f9fe !important; | |
| } | |
| .gradio-container input, | |
| .gradio-container textarea { | |
| color: #111 !important; | |
| background: #ffffff !important; | |
| } | |
| """ | |
| LOCKED_CSS = """ | |
| body, .gradio-container { | |
| background: linear-gradient(160deg, #ffffff 0%, #f3f5ff 50%, #eef0ff 100%) !important; | |
| } | |
| #lock-screen { | |
| text-align: center; | |
| margin-top: 15vh; | |
| padding: 40px 20px; | |
| } | |
| #lock-title { | |
| font-size: 2rem; | |
| font-weight: 800; | |
| color: #d6003d; | |
| } | |
| #lock-sub { | |
| color: #4b5066; | |
| margin-top: 12px; | |
| font-size: 1rem; | |
| } | |
| """ | |
| if is_authorized(): | |
| with gr.Blocks(title="Deepu Video Suite") as demo: | |
| gr.HTML("<div id='rgb-title'>β‘ MOBILE SERIAL CODING β PRO SUITE β‘</div>") | |
| gr.HTML("<div id='subtitle'>Upload video to process in background. Use Job Code to retrieve it later.</div>") | |
| with gr.Tabs(): | |
| with gr.Tab("π New Task"): | |
| with gr.Row(): | |
| with gr.Column(): | |
| input_vid = gr.File( | |
| label="Upload Video (Files chooser, not Gallery)", | |
| file_types=None, | |
| type="filepath" | |
| ) | |
| mode_select = gr.Radio(["Full Screen", "Half Screen"], label="Processing Mode", value="Half Screen") | |
| opt_obfuscation = gr.CheckboxGroup( | |
| choices=[ | |
| "Mirror Flip", | |
| "Zoom/Crop Slightly", | |
| "Color Shift", | |
| "Subtle Noise/Grain", | |
| "Speed Micro-variation" | |
| ], | |
| label="π οΈ Fingerprint Obfuscation Settings", | |
| value=[] | |
| ) | |
| start_btn = gr.Button("π Start Background Process", variant="primary") | |
| job_output = gr.Markdown("Job status will appear here.") | |
| job_code_display = gr.Textbox(label="Your Job Code", interactive=False) | |
| with gr.Column(): | |
| gr.Markdown("### π Live Speed Meter") | |
| live_meter = gr.HTML(build_meter_html(None)) | |
| meter_timer = gr.Timer(1.0, active=True) | |
| start_btn.click( | |
| fn=start_job, | |
| inputs=[input_vid, mode_select, opt_obfuscation], | |
| outputs=[job_output, job_code_display] | |
| ) | |
| meter_timer.tick(fn=live_meter_update, inputs=[job_code_display], outputs=[live_meter]) | |
| # ----- ποΈ EQUALIZER TAB ----- | |
| with gr.Tab("ποΈ Equalizer"): | |
| gr.Markdown("### ποΈ Custom Audio Equalizer") | |
| gr.Markdown("Upload video β Adjust EQ sliders β Preview 10sec clip β Apply to full video") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| eq_video_input = gr.File( | |
| label="Upload Video for EQ", | |
| file_types=None, | |
| type="filepath" | |
| ) | |
| gr.Markdown("**Presets:**") | |
| with gr.Row(): | |
| preset_custom = gr.Button("Custom", size="sm") | |
| preset_normal = gr.Button("Normal", size="sm") | |
| preset_classical = gr.Button("Classical", size="sm") | |
| preset_dance = gr.Button("Dance", size="sm") | |
| with gr.Row(): | |
| preset_flat = gr.Button("Flat", size="sm") | |
| preset_bass = gr.Button("Bass Boost", size="sm") | |
| preset_vocal = gr.Button("Vocal", size="sm") | |
| preset_rock = gr.Button("Rock", size="sm") | |
| gr.Markdown("**5-Band EQ (dB):**") | |
| eq_60 = gr.Slider(-15, 15, value=0, step=1, label="60 Hz (Sub Bass)") | |
| eq_230 = gr.Slider(-15, 15, value=0, step=1, label="230 Hz (Bass)") | |
| eq_910 = gr.Slider(-15, 15, value=0, step=1, label="910 Hz (Mid)") | |
| eq_3600 = gr.Slider(-15, 15, value=0, step=1, label="3.6 kHz (Upper Mid)") | |
| eq_14000 = gr.Slider(-15, 15, value=0, step=1, label="14 kHz (Treble)") | |
| with gr.Row(): | |
| preview_btn = gr.Button("ποΈ Preview (10 sec)", variant="primary") | |
| apply_full_btn = gr.Button("β Apply to Full Video", variant="primary") | |
| with gr.Column(scale=1): | |
| gr.Markdown("**Preview Result:**") | |
| eq_preview_video = gr.Video(label="EQ Preview (10 sec clip)") | |
| eq_status = gr.Markdown("Adjust sliders and click Preview to hear the result.") | |
| eq_full_output = gr.Video(label="Full EQ Video (after Apply)") | |
| eq_full_status = gr.Markdown("") | |
| eq_sliders = [eq_60, eq_230, eq_910, eq_3600, eq_14000] | |
| preset_custom.click(fn=lambda: apply_preset("Custom"), outputs=eq_sliders) | |
| preset_normal.click(fn=lambda: apply_preset("Normal"), outputs=eq_sliders) | |
| preset_classical.click(fn=lambda: apply_preset("Classical"), outputs=eq_sliders) | |
| preset_dance.click(fn=lambda: apply_preset("Dance"), outputs=eq_sliders) | |
| preset_flat.click(fn=lambda: apply_preset("Flat"), outputs=eq_sliders) | |
| preset_bass.click(fn=lambda: apply_preset("Bass Boost"), outputs=eq_sliders) | |
| preset_vocal.click(fn=lambda: apply_preset("Vocal"), outputs=eq_sliders) | |
| preset_rock.click(fn=lambda: apply_preset("Rock"), outputs=eq_sliders) | |
| preview_btn.click( | |
| fn=eq_preview, | |
| inputs=[eq_video_input, eq_60, eq_230, eq_910, eq_3600, eq_14000], | |
| outputs=[eq_preview_video, eq_status] | |
| ) | |
| def eq_apply_full(input_video, hz60, hz230, hz910, khz3_6, khz14): | |
| if input_video is None: | |
| return None, "β οΈ Pehle video upload karo!" | |
| eq_filter = build_eq_filter(hz60, hz230, hz910, khz3_6, khz14) | |
| output_id = str(uuid.uuid4())[:6] | |
| output_path = os.path.join(JOBS_DIR, f"eq_full_{output_id}.mp4") | |
| variants = ["gpu", "cpu"] if gpu_available() else ["cpu"] | |
| last_err = "" | |
| for variant in variants: | |
| command = [ | |
| 'ffmpeg', '-y', | |
| '-i', input_video, | |
| '-af', eq_filter, | |
| ] + _video_encode_args(variant, 28) + [ | |
| '-acodec', 'aac', '-b:a', '128k', '-ar', '44100', | |
| '-pix_fmt', 'yuv420p', | |
| output_path | |
| ] | |
| try: | |
| result = subprocess.run(command, capture_output=True, text=True, timeout=300) | |
| if result.returncode == 0 and os.path.exists(output_path): | |
| tag = "GPU" if variant == "gpu" else "CPU" | |
| return output_path, f"β Full video with EQ applied ({tag})! Download ready." | |
| last_err = result.stderr[-200:] | |
| except subprocess.TimeoutExpired: | |
| last_err = "Timeout β video bahut bada hai." | |
| except Exception as e: | |
| last_err = str(e) | |
| return None, f"β Failed: {last_err}" | |
| apply_full_btn.click( | |
| fn=eq_apply_full, | |
| inputs=[eq_video_input, eq_60, eq_230, eq_910, eq_3600, eq_14000], | |
| outputs=[eq_full_output, eq_full_status] | |
| ) | |
| with gr.Tab("π Retrieve Job"): | |
| with gr.Row(): | |
| with gr.Column(): | |
| input_code = gr.Textbox(label="Enter Job Code") | |
| check_btn = gr.Button("π Check Status", variant="primary") | |
| status_display = gr.Markdown("Enter code to see details.") | |
| output_vid = gr.Video(label="Download Processed Video") | |
| with gr.Column(): | |
| gr.Markdown("### π Live Speed Meter") | |
| retrieve_meter = gr.HTML(build_meter_html(None)) | |
| logs_display = gr.Textbox(label="Job Logs", lines=10, interactive=False) | |
| check_btn.click(fn=check_status, inputs=input_code, outputs=[status_display, output_vid, logs_display, retrieve_meter]) | |
| with gr.Tab("π History"): | |
| history_btn = gr.Button("π Refresh History", variant="primary") | |
| history_table = gr.Markdown("Click refresh to see recent jobs.") | |
| gr.Markdown("*Note: History is automatically cleared every 7 hours.*") | |
| history_btn.click(fn=get_history, outputs=history_table) | |
| gr.HTML("<div id='made-by'>β¨ Made with β€οΈ by DEEPU β¨</div>") | |
| # FIX: Is main authorized wale section mein share=True missing tha, ab sahi kar diya hai | |
| demo.launch(theme=gr.themes.Soft(), share=True, css=CUSTOM_CSS) | |
| else: | |
| with gr.Blocks() as demo: | |
| gr.HTML( | |
| "<div id='lock-screen'>" | |
| "<div id='lock-title'>π Access Denied</div>" | |
| "<div id='lock-sub'>Ye app ek valid Access Key ke bina nahi chalega.<br>" | |
| "Space Settings me sahi <b>APP_ACCESS_KEY</b> secret set karo.</div>" | |
| "</div>" | |
| ) | |
| demo.launch(theme=gr.themes.Soft(), share=True, css=CUSTOM_CSS) | |