import gradio as gr import subprocess import os import random import shutil import tempfile import json import time import threading import uuid import re from pathlib import Path from datetime import datetime from queue import Queue # ========== Global Job Storage ========== jobs = {} class UltimateVideoEvader: def __init__(self): self.ffmpeg_path = self._find_ffmpeg() self.ffprobe_path = self.ffmpeg_path.replace("ffmpeg", "ffprobe") self.output_base_dir = tempfile.mkdtemp(prefix="vfe_jobs_") print(f"[*] Job output directory: {self.output_base_dir}") def _find_ffmpeg(self): for cmd in ["ffmpeg", "/usr/bin/ffmpeg"]: if shutil.which(cmd): return cmd return "ffmpeg" def _get_resolution(self, input_file): try: cmd = [ self.ffprobe_path, "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height,duration", "-of", "json", input_file ] result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode == 0: data = json.loads(result.stdout) if "streams" in data and data["streams"]: w = int(data["streams"][0]["width"]) h = int(data["streams"][0]["height"]) duration = float(data["streams"][0].get("duration", 0)) return (w, h, duration) except: pass return (1920, 1080, 0) def _random_float(self, a, b): return round(random.uniform(a, b), 4) def build_brutal_command(self, input_file, output_file): cmd = [self.ffmpeg_path, "-i", input_file] cmd.extend(["-map_metadata", "-1"]) scale = self._random_float(0.6, 1.4) w, h, duration = self._get_resolution(input_file) new_w = int(w * scale) new_h = int(h * scale) new_w = new_w if new_w % 2 == 0 else new_w + 1 new_h = new_h if new_h % 2 == 0 else new_h + 1 fps = self._random_float(15, 60) speed = self._random_float(0.8, 1.3) tempo = self._random_float(0.8, 1.3) * speed tempo = min(tempo, 2.0) pitch = self._random_float(-1.0, 1.0) noise_db = self._random_float(1, 12) bright = self._random_float(-0.1, 0.1) contrast = self._random_float(0.85, 1.15) saturation = self._random_float(0.7, 1.3) hue = self._random_float(0, 360) crop_percent = self._random_float(0.90, 0.98) crop_w = int(new_w * crop_percent) crop_h = int(new_h * crop_percent) crop_w = crop_w if crop_w % 2 == 0 else crop_w + 1 crop_h = crop_h if crop_h % 2 == 0 else crop_h + 1 pad_left = (new_w - crop_w) // 2 pad_top = (new_h - crop_h) // 2 video_filters = [] video_filters.append(f"scale={new_w}:{new_h}") video_filters.append(f"setpts={1/speed}*PTS") video_filters.append(f"noise=alls={noise_db}:allf=t+u") video_filters.append(f"eq=brightness={bright}:contrast={contrast}:saturation={saturation}") video_filters.append(f"hue=H={hue}") video_filters.append(f"crop={crop_w}:{crop_h}:{pad_left}:{pad_top},pad={new_w}:{new_h}:{pad_left}:{pad_top}") offset = random.randint(0, 120) if offset > 0: video_filters.append(f"trim=start_frame={offset}") if random.choice([True, False]): video_filters.append("reverse") if random.choice([True, False]): blur = self._random_float(0.5, 2.0) video_filters.append(f"gblur=sigma={blur}") filter_chain = ",".join(video_filters) cmd.extend(["-vf", filter_chain]) cmd.extend(["-r", str(round(fps, 2))]) audio_filters = [] if 0.5 <= tempo <= 2.0: audio_filters.append(f"atempo={tempo}") pitch_factor = 2 ** (pitch / 12) audio_filters.append(f"rubberband=pitch={pitch_factor}") if audio_filters: cmd.extend(["-af", ",".join(audio_filters)]) codec = random.choice(["libx264", "libx265", "libvpx-vp9"]) cmd.extend(["-c:v", codec]) preset = random.choice(["ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow"]) cmd.extend(["-preset", preset]) crf = random.randint(18, 28) cmd.extend(["-crf", str(crf)]) audio_codec = random.choice(["aac", "libmp3lame"]) cmd.extend(["-c:a", audio_codec, "-b:a", "128k"]) container = random.choice(["mp4", "mkv", "mov"]) base = os.path.splitext(output_file)[0] output_file = f"{base}.{container}" cmd.extend(["-y", output_file]) return cmd, output_file def process_video_background(self, job_id, input_path): log_queue = jobs[job_id]["log_queue"] def log(msg): log_queue.put(f"[{datetime.now().strftime('%H:%M:%S')}] {msg}") try: log("🚀 Job started") jobs[job_id]["status"] = "processing" jobs[job_id]["progress"] = 10 job_dir = os.path.join(self.output_base_dir, job_id) os.makedirs(job_dir, exist_ok=True) output_base = os.path.join(job_dir, "evaded_video") log("📦 Building FFmpeg command...") cmd, final_output = self.build_brutal_command(input_path, output_base) jobs[job_id]["progress"] = 20 input_copy = os.path.join(job_dir, "input_" + os.path.basename(input_path)) shutil.copy2(input_path, input_copy) log(f"📁 Input file: {os.path.basename(input_path)}") _, _, duration = self._get_resolution(input_path) if duration > 0: log(f"⏱️ Input duration: {duration:.2f} seconds") jobs[job_id]["progress"] = 25 log("⚙️ Starting FFmpeg processing...") process = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1, universal_newlines=True ) speed_pattern = re.compile(r'speed=\s*([\d.]+)x') fps_pattern = re.compile(r'fps=\s*([\d.]+)') time_pattern = re.compile(r'time=(\d{2}):(\d{2}):(\d{2})\.(\d{2})') start_time = time.time() last_log_time = 0 while True: line = process.stderr.readline() if not line: break line = line.strip() if 'frame=' in line or 'time=' in line: time_match = time_pattern.search(line) if time_match: h, m, s, cs = time_match.groups() current_time = int(h) * 3600 + int(m) * 60 + int(s) + int(cs) / 100 speed_match = speed_pattern.search(line) speed_val = speed_match.group(1) if speed_match else "?" fps_match = fps_pattern.search(line) fps_val = fps_match.group(1) if fps_match else "?" if duration > 0 and time_match and speed_match: if float(speed_val) > 0: eta = (duration - current_time) / float(speed_val) eta_min = int(eta // 60) eta_sec = int(eta % 60) eta_str = f"{eta_min}m {eta_sec}s" else: eta_str = "calculating..." percent = min(100, int((current_time / duration) * 100)) if duration > 0 else 0 jobs[job_id]["progress"] = min(25 + int(percent * 0.7), 95) if time.time() - last_log_time > 3: log(f"⏳ Progress: {percent}% | Time: {current_time:.1f}s/{duration:.1f}s | Speed: {speed_val}x | FPS: {fps_val} | ETA: {eta_str}") last_log_time = time.time() process.wait() if process.returncode != 0: jobs[job_id]["status"] = "failed" jobs[job_id]["error"] = "FFmpeg error" jobs[job_id]["progress"] = 0 log("❌ FFmpeg failed with error") return if os.path.exists(final_output) and os.path.getsize(final_output) > 0: jobs[job_id]["status"] = "completed" jobs[job_id]["output_file"] = final_output jobs[job_id]["progress"] = 100 jobs[job_id]["timestamp"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") file_size = round(os.path.getsize(final_output) / (1024 * 1024), 2) log(f"✅ Processing complete! Output size: {file_size} MB") log(f"📥 Download ready: {os.path.basename(final_output)}") else: jobs[job_id]["status"] = "failed" jobs[job_id]["error"] = "Output file not created" jobs[job_id]["progress"] = 0 log("❌ Output file not created") except Exception as e: jobs[job_id]["status"] = "failed" jobs[job_id]["error"] = str(e) jobs[job_id]["progress"] = 0 log(f"❌ Error: {str(e)}") log("🏁 Job finished") log_queue.put("__END__") def start_job(self, input_path): if not input_path or not os.path.exists(input_path): return "❌ कोई फ़ाइल नहीं", None job_id = str(uuid.uuid4())[:8] log_queue = Queue() jobs[job_id] = { "status": "queued", "input_file": input_path, "output_file": None, "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "progress": 0, "error": None, "job_id": job_id, "log_queue": log_queue } thread = threading.Thread( target=self.process_video_background, args=(job_id, input_path) ) thread.daemon = True thread.start() return f"✅ Job started! ID: {job_id}", job_id def get_job_status(self, job_id): if job_id not in jobs: return "❌ Job not found", None, None job = jobs[job_id] status = job["status"] progress = job["progress"] error = job.get("error", "") status_text = f"Status: {status}\nProgress: {progress}%" if error: status_text += f"\nError: {error}" output_file = job.get("output_file") return status_text, output_file, status def get_live_logs(self, job_id): if job_id not in jobs: return "No job found" queue = jobs[job_id].get("log_queue") if not queue: return "Log queue not initialized" logs = [] while not queue.empty(): msg = queue.get() if msg == "__END__": break logs.append(msg) return "\n".join(logs) if logs else "Waiting for logs..." def get_all_jobs(self): job_list = [] for job_id, job in jobs.items(): job_list.append({ "job_id": job_id, "status": job["status"], "timestamp": job.get("timestamp", "Unknown"), "output_file": job.get("output_file", ""), "input_file": os.path.basename(job.get("input_file", "unknown")), "progress": job.get("progress", 0), "has_output": job.get("output_file") is not None and os.path.exists(job.get("output_file", "")) }) job_list.sort(key=lambda x: x["timestamp"], reverse=True) return job_list def get_download_link(self, job_id): if job_id not in jobs: return None, "❌ Job not found" job = jobs[job_id] if job.get("status") != "completed": return None, f"⏳ Job is {job.get('status')}" if not job.get("output_file") or not os.path.exists(job.get("output_file")): return None, "❌ File missing" file_path = job["output_file"] file_name = os.path.basename(file_path) # Return file and HTML link return file_path, f"✅ Ready: {file_name}" # ========== Gradio Interface ========== def create_interface(): evader = UltimateVideoEvader() with gr.Blocks(title="Video Fingerprint Evader") as demo: gr.Markdown(""" # 🚀 Video Fingerprint Evader – Live Logs + Background Processing """) job_id_state = gr.State("") with gr.Tab("📤 Upload & Process"): with gr.Row(): with gr.Column(scale=1): input_video = gr.File( label="📤 वीडियो अपलोड करें", file_types=[".mp4", ".mkv", ".avi", ".mov", ".webm", ".flv"], type="filepath" ) process_btn = gr.Button("🔥 Process in Background", variant="primary") job_status = gr.Textbox(label="📊 Job Status", lines=3, interactive=False) gr.Markdown("---") gr.Markdown("### 📥 Download Current Job") download_btn = gr.Button("📥 Download Video", variant="secondary") download_file = gr.File(label="📥 Download", visible=False) download_link_html = gr.HTML(label="", visible=False) download_status = gr.Textbox(label="Status", lines=2, interactive=False) with gr.Column(scale=2): log_display = gr.Textbox( label="📋 Live Logs", lines=25, interactive=False, autoscroll=True ) refresh_logs_btn = gr.Button("🔄 Refresh Logs", variant="secondary") status_display = gr.Textbox(label="📊 Quick Status", lines=2, interactive=False) with gr.Tab("📜 Job History"): gr.Markdown("### सभी Jobs की History") with gr.Row(): with gr.Column(scale=2): history_refresh_btn = gr.Button("🔄 Refresh History") job_history = gr.Dataframe( headers=["Job ID", "Input File", "Status", "Timestamp", "Progress"], datatype=["str", "str", "str", "str", "str"], label="Job History", interactive=False ) with gr.Column(scale=1): gr.Markdown("### 📥 Download by Job ID") download_job_id = gr.Textbox(label="Enter Job ID", placeholder="e.g., 5b7c3970") download_by_id_btn = gr.Button("📥 Download Video", variant="primary") download_by_id_file = gr.File(label="Download", visible=False) download_by_id_link = gr.HTML(label="", visible=False) download_by_id_status = gr.Textbox(label="Status", lines=2, interactive=False) # ===== Processing Functions ===== def start_process(file_path): if not file_path: return "❌ कृपया पहले वीडियो अपलोड करें।", "", "No job", "", "" status, job_id = evader.start_job(file_path) return status, job_id, f"Job {job_id} running...", "", job_id def refresh_logs(job_id): if not job_id or job_id not in jobs: return "❌ No job running or invalid ID", "No job" logs = evader.get_live_logs(job_id) status = jobs.get(job_id, {}).get("status", "unknown") progress = jobs.get(job_id, {}).get("progress", 0) return logs, f"Status: {status} | Progress: {progress}%" def download_current(job_id): if not job_id or job_id not in jobs: return None, "", False, "❌ No job" file_path, status = evader.get_download_link(job_id) if file_path and os.path.exists(file_path): file_name = os.path.basename(file_path) # Create HTML download link html_link = f'📥 Click here to download: {file_name}' return file_path, html_link, True, status return None, "", False, status def download_by_id(job_id): if not job_id: return None, "", False, "❌ Please enter Job ID" file_path, status = evader.get_download_link(job_id) if file_path and os.path.exists(file_path): file_name = os.path.basename(file_path) html_link = f'📥 Click here to download: {file_name}' return file_path, html_link, True, status return None, "", False, status def refresh_history(): job_list = evader.get_all_jobs() if not job_list: return gr.update(value=[], visible=True) rows = [] for j in job_list: status_display = j["status"] if j["status"] == "completed": status_display = "✅ completed" elif j["status"] == "processing": status_display = "⏳ processing" elif j["status"] == "failed": status_display = "❌ failed" rows.append([ j["job_id"], j["input_file"], status_display, j["timestamp"], f"{j['progress']}%" ]) return gr.update(value=rows, visible=True) # ===== Event Handlers ===== process_btn.click( start_process, inputs=[input_video], outputs=[job_status, job_id_state, status_display, log_display, job_id_state] ).then( refresh_logs, inputs=[job_id_state], outputs=[log_display, status_display] ) refresh_logs_btn.click( refresh_logs, inputs=[job_id_state], outputs=[log_display, status_display] ) download_btn.click( download_current, inputs=[job_id_state], outputs=[download_file, download_link_html, download_file, download_status] ) history_refresh_btn.click( refresh_history, inputs=[], outputs=[job_history] ) download_by_id_btn.click( download_by_id, inputs=[download_job_id], outputs=[download_by_id_file, download_by_id_link, download_by_id_file, download_by_id_status] ) # ===== Auto-refresh ===== timer = gr.Timer(value=5, active=False) def toggle_timer(job_id): if job_id and job_id in jobs: return gr.update(active=True) return gr.update(active=False) process_btn.click( toggle_timer, inputs=[job_id_state], outputs=[timer] ) timer.tick( refresh_logs, inputs=[job_id_state], outputs=[log_display, status_display] ) return demo if __name__ == "__main__": demo = create_interface() demo.launch(debug=True)