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 "