Editing / app.py
factblink514's picture
Update app.py
f6d58df verified
Raw
History Blame Contribute Delete
36 kB
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)