import gradio as gr import subprocess import os import shutil import json import threading import random import string import time import multiprocessing CPU_THREADS = str(multiprocessing.cpu_count()) BASE_DIR = os.path.join(os.getcwd(), "job_data") JOBS_FILE = os.path.join(BASE_DIR, "jobs.json") os.makedirs(BASE_DIR, exist_ok=True) _lock = threading.Lock() HISTORY_TTL_SECONDS = 5 * 60 * 60 # 5 ghante # ---------- BGM file path (server par permanently) ---------- BGM_FILE = None for ext in [".mp4", ".mp3", ".m4a", ".wav"]: potential = os.path.join(os.getcwd(), f"bgm{ext}") if os.path.exists(potential): BGM_FILE = potential break # ---------- Storage helpers ---------- def _load_jobs(): if not os.path.exists(JOBS_FILE): return {} try: with open(JOBS_FILE, "r") as f: return json.load(f) except Exception: return {} def _save_jobs(jobs): with open(JOBS_FILE, "w") as f: json.dump(jobs, f, indent=2) def _add_task(code, task): with _lock: jobs = _load_jobs() jobs.setdefault(code, []).append(task) _save_jobs(jobs) def _update_task(code, task_id, **kwargs): with _lock: jobs = _load_jobs() for t in jobs.get(code, []): if t["id"] == task_id: t.update(kwargs) _save_jobs(jobs) def _get_task(code, task_id): jobs = _load_jobs() for t in jobs.get(code, []): if t["id"] == task_id: return t return None def _cleanup_old_tasks(): """5 ghante se purani tasks hata do (data + entry dono).""" with _lock: jobs = _load_jobs() now = time.time() changed = False for code in list(jobs.keys()): kept = [] for t in jobs[code]: if now - t.get("created_ts", now) < HISTORY_TTL_SECONDS: kept.append(t) else: changed = True task_dir = os.path.join(BASE_DIR, code, t["id"]) shutil.rmtree(task_dir, ignore_errors=True) if kept: jobs[code] = kept else: del jobs[code] changed = True if changed: _save_jobs(jobs) def random_code(): return "".join(random.choices(string.ascii_uppercase + string.digits, k=6)) # ---------- Video helpers ---------- RES_MAP = { "480p (sabse fast)": (854, 480), "720p (balanced)": (1280, 720), "1080p (best quality, slow)": (1920, 1080), } def get_duration(path): try: out = subprocess.run( ["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", path], capture_output=True, text=True ) return max(0.1, float(out.stdout.strip())) except Exception: return 0.1 def build_audio_filter(): """Halka audio variation — pitch/EQ/echo. Sirf sound texture ke liye, fingerprint-evasion tool nahi hai.""" pitch_shift = random.uniform(0.98, 1.02) eq_freq = random.choice([200, 500, 1000, 3000]) eq_gain = random.uniform(-1.5, 1.5) filters = [ f"asetrate=44100*{pitch_shift},aresample=44100", f"equalizer=f={eq_freq}:t=q:w=1:g={eq_gain:.2f}", f"aecho=0.5:0.3:6:0.1", ] return ",".join(filters) def run_ffmpeg_with_progress(cmd, code, task_id, total_duration, base_pct, weight_pct, fps_hint=""): cmd = [cmd[0], "-y", "-progress", "pipe:1", "-nostats"] + cmd[2:] proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1) error_lines = [] last_write = 0 out_time_sec = 0.0 speed_val = 0.0 fps_val = 0.0 for line in proc.stdout: line = line.strip() if "=" in line: key, _, val = line.partition("=") if key == "out_time_ms": try: out_time_sec = int(val) / 1_000_000 except ValueError: pass elif key == "speed": try: speed_val = float(val.replace("x", "").strip()) except ValueError: speed_val = 0.0 elif key == "fps": try: fps_val = float(val) except ValueError: pass else: error_lines.append(line) now = time.time() if now - last_write > 0.6: local_pct = min(99, (out_time_sec / total_duration) * 100) if total_duration > 0 else 0 overall_pct = round(base_pct + (weight_pct * local_pct / 100), 1) remaining = max(0, total_duration - out_time_sec) eta = round(remaining / speed_val, 1) if speed_val > 0 else None _update_task(code, task_id, progress=overall_pct, speed=round(speed_val, 2), fps=round(fps_val, 1), eta_seconds=eta, elapsed=round(out_time_sec, 1), total_dur=round(total_duration, 1), status="processing") last_write = now proc.wait() return proc.returncode == 0, "\n".join(error_lines[-15:]) def do_merge(video1, video2, resize_mode, resolution_choice, change_audio_dna, bgm_volume, work_dir, output_path, code, task_id): try: d1 = get_duration(video1) d2 = get_duration(video2) total = d1 + d2 if resize_mode == "Fast (same resolution/codec required)" and not change_audio_dna and not BGM_FILE: list_file = os.path.join(work_dir, "list.txt") with open(list_file, "w") as f: f.write(f"file '{video1}'\n") f.write(f"file '{video2}'\n") cmd = ["ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", list_file, "-c", "copy", output_path] ok, err = run_ffmpeg_with_progress(cmd, code, task_id, total, 0, 100) if ok: return True, "Merge ho gaya (fast copy mode)" norm1 = os.path.join(work_dir, "norm1.mp4") norm2 = os.path.join(work_dir, "norm2.mp4") target_w, target_h = RES_MAP.get(resolution_choice, (1280, 720)) w1 = (d1 / total * 90) if total > 0 else 45 w2 = 90 - w1 for src, dst, base, weight, dur in [ (video1, norm1, 0, w1, d1), (video2, norm2, w1, w2, d2), ]: audio_filter_str = "aresample=44100" if change_audio_dna: audio_filter_str = build_audio_filter() cmd = [ "ffmpeg", "-y", "-threads", CPU_THREADS, "-i", src, "-vf", f"scale={target_w}:{target_h}:force_original_aspect_ratio=decrease:flags=fast_bilinear," f"pad={target_w}:{target_h}:(ow-iw)/2:(oh-ih)/2,setsar=1", "-af", audio_filter_str, "-r", "30", "-c:v", "libx264", "-preset", "ultrafast", "-crf", "26", "-c:a", "aac", "-b:a", "128k", "-ar", "44100", "-ac", "2", dst ] ok, err = run_ffmpeg_with_progress(cmd, code, task_id, dur, base, weight) if not ok: return False, f"Normalize error: {err}" _update_task(code, task_id, progress=95) # Merge normalized videos first list_file = os.path.join(work_dir, "list2.txt") with open(list_file, "w") as f: f.write(f"file '{norm1}'\n") f.write(f"file '{norm2}'\n") merged_video = os.path.join(work_dir, "merged_temp.mp4") cmd = ["ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", list_file, "-c", "copy", merged_video] result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: return False, f"Merge error: {result.stderr[-400:]}" # Add BGM if server par BGM file exist karta hai if BGM_FILE: bgm_loop = os.path.join(work_dir, "bgm_loop.mp3") # Loop BGM to match video duration loop_cmd = [ "ffmpeg", "-y", "-i", BGM_FILE, "-filter_complex", f"aloop=loop=-1:size=2e9,atrim=0:{total}", "-c:a", "mp3", "-b:a", "128k", bgm_loop ] subprocess.run(loop_cmd, capture_output=True, text=True) # Mix BGM with video audio vol = max(0, min(1, float(bgm_volume or 0.5))) final_cmd = [ "ffmpeg", "-y", "-i", merged_video, "-i", bgm_loop, "-filter_complex", f"[0:a]volume=1.0[a0];[1:a]volume={vol}[a1];[a0][a1]amix=inputs=2:duration=longest", "-c:v", "copy", "-c:a", "aac", "-b:a", "192k", output_path ] subprocess.run(final_cmd, capture_output=True, text=True) else: shutil.copy(merged_video, output_path) dna_note = " + audio thoda change kiya" if change_audio_dna else "" bgm_note = " + BGM add kiya" if BGM_FILE else "" return True, f"Merge ho gaya!{dna_note}{bgm_note}" except Exception as e: return False, f"Error: {str(e)}" def run_job_background(code, task_id, v1_saved, v2_saved, resize_mode, resolution_choice, change_audio_dna, bgm_volume, out_name): task_dir = os.path.join(BASE_DIR, code, task_id) output_path = os.path.join(task_dir, out_name) try: ok, msg = do_merge(v1_saved, v2_saved, resize_mode, resolution_choice, change_audio_dna, bgm_volume, task_dir, output_path, code, task_id) if ok and os.path.exists(output_path): size_mb = round(os.path.getsize(output_path) / (1024 * 1024), 2) _update_task(code, task_id, status="done", message=msg, output=output_path, progress=100, size_mb=size_mb, finished_ts=time.time()) else: _update_task(code, task_id, status="error", message=msg, finished_ts=time.time()) except Exception as e: _update_task(code, task_id, status="error", message=f"Error: {str(e)}", finished_ts=time.time()) def submit_job(video1, video2, resize_mode, resolution_choice, change_audio_dna, bgm_volume, custom_code): _cleanup_old_tasks() if video1 is None or video2 is None: return "❌ Dono videos upload karo pehle!", "" code = (custom_code or "").strip().upper() code = "".join(c for c in code if c.isalnum())[:20] if not code: code = random_code() task_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) task_dir = os.path.join(BASE_DIR, code, task_id) os.makedirs(task_dir, exist_ok=True) v1_saved = os.path.join(task_dir, "input1" + os.path.splitext(video1)[1]) v2_saved = os.path.join(task_dir, "input2" + os.path.splitext(video2)[1]) shutil.copy(video1, v1_saved) shutil.copy(video2, v2_saved) out_name = f"output_{task_id}.mp4" display_name = os.path.basename(video1) task = { "id": task_id, "status": "queued", "message": "Queue me hai...", "progress": 0, "speed": 0, "fps": 0, "eta_seconds": None, "elapsed": 0, "total_dur": 0, "size_mb": None, "display_name": display_name, "audio_dna": change_audio_dna, "created_ts": time.time(), "created_at": time.strftime("%H:%M:%S"), "output": None, } _add_task(code, task) t = threading.Thread( target=run_job_background, args=(code, task_id, v1_saved, v2_saved, resize_mode, resolution_choice, change_audio_dna, bgm_volume, out_name), daemon=True ) t.start() return (f"✅ Queue mein add hua! Task ID: **{task_id}**\n\n" f"Job Code: **{code}** — isi code se dobara bhi submit kar sakta hai, " f"aur isi code se status/history bhi check hoga."), code def progress_badge_html(pct, status): pct = max(0, min(100, pct or 0)) if status == "done": return f'✅ Done' elif status == "error": return f'❌ Error' elif status == "queued": return f'⏳ Queued' else: return f'⚙️ {pct}%' def render_task_card(t): badge = progress_badge_html(t.get("progress", 0), t.get("status")) name = t.get("display_name", "video.mp4") when = t.get("created_at", "") dna_tag = ' 🎵 Audio changed' if t.get("audio_dna") else "" detail = "" if t.get("status") == "processing": speed = t.get("speed", 0) fps = t.get("fps", 0) elapsed = t.get("elapsed", 0) total_dur = t.get("total_dur", 0) eta = t.get("eta_seconds") eta_txt = f"{int(eta)}s" if eta is not None else "..." detail = (f'
' f'⚡ {t.get("progress",0)}% | {elapsed}s/{total_dur}s | Speed: {speed}x | FPS: {fps} | ETA: {eta_txt}' f'
') elif t.get("status") == "done": size = t.get("size_mb", "?") detail = f'
Size: {size} MB
' elif t.get("status") == "error": detail = f'
{t.get("message","")}
' return f"""
{badge} {name}
{when}
{dna_tag} {detail}
""" def check_status(code): if not code or not code.strip(): return "⚠️ Code daalo pehle", "", None code = "".join(c for c in code.strip().upper() if c.isalnum()) jobs = _load_jobs() tasks = jobs.get(code, []) if not tasks: return f"❌ Code '{code}' ke liye koi task nahi mila.", "", None tasks_sorted = sorted(tasks, key=lambda t: t.get("created_ts", 0), reverse=True) queued = sum(1 for t in tasks if t.get("status") == "queued") processing = sum(1 for t in tasks if t.get("status") == "processing") done = sum(1 for t in tasks if t.get("status") == "done") error = sum(1 for t in tasks if t.get("status") == "error") summary = f"📊 **{code}** — Queue: {queued} waiting | {processing} processing | {done} done | {error} error" cards_html = "".join(render_task_card(t) for t in tasks_sorted) latest_done = next((t for t in tasks_sorted if t.get("status") == "done" and t.get("output")), None) video_out = latest_done["output"] if latest_done else None return summary, cards_html, video_out def get_history(): _cleanup_old_tasks() jobs = _load_jobs() if not jobs: return "Abhi tak koi job nahi hai. (History 5 ghante baad auto-delete ho jati hai)" rows = ["| Code | Waiting | Processing | Done | Error |", "|------|---------|-----------|------|-------|"] for code, tasks in jobs.items(): q = sum(1 for t in tasks if t.get("status") == "queued") p = sum(1 for t in tasks if t.get("status") == "processing") d = sum(1 for t in tasks if t.get("status") == "done") e = sum(1 for t in tasks if t.get("status") == "error") rows.append(f"| {code} | {q} | {p} | {d} | {e} |") rows.append("\n_History 5 ghante baad automatically delete ho jati hai._") return "\n".join(rows) # ---------- UI ---------- with gr.Blocks(title="Video Merger - 2 Videos Jodo") as demo: gr.Markdown("# 🎬 Video Merger\nDo videos upload karo, background me merge hoga — tab band karo chahe!") with gr.Tab("🔗 Queue Mein Add Karo"): with gr.Row(): video1_input = gr.Video(label="Pehla Video") video2_input = gr.Video(label="Dusra Video") bgm_volume = gr.Slider( minimum=0.0, maximum=1.0, value=0.5, step=0.05, label="BGM Volume (0.0 = mute, 1.0 = full) — server par bgm.mp4/bgm.mp3 hona chahiye" ) if BGM_FILE: gr.Markdown(f"✅ Server par BGM file mil gaya: `{os.path.basename(BGM_FILE)}` — auto-apply hoga!") else: gr.Markdown("⚠️ Server par koi BGM file nahi mili. `bgm.mp4` ya `bgm.mp3` upload karo app ke saath.") resize_mode = gr.Radio( choices=["Fast (same resolution/codec required)", "Safe (auto resize + re-encode, thoda slow)"], value="Fast (same resolution/codec required)", label="Mode" ) resolution_choice = gr.Radio( choices=list(RES_MAP.keys()), value="480p (sabse fast)", label="Resolution (sirf Safe mode ke liye)" ) change_audio_dna = gr.Checkbox( label="🎵 Audio thoda change karo (pitch/EQ/echo — halka variation)", value=False ) custom_code_input = gr.Textbox( label="Job Code (naya banao ya purana daalo — usi code pe bar-bar submit kar sakta hai)", placeholder="jaise: DEEPU01" ) submit_btn = gr.Button("➕ Queue Mein Add Karo", variant="primary") submit_status = gr.Markdown() code_box = gr.Textbox(label="Tera Job Code (save kar le, dobara isi se submit/status check kar sakta hai)", interactive=False) submit_btn.click( fn=submit_job, inputs=[video1_input, video2_input, resize_mode, resolution_choice, change_audio_dna, bgm_volume, custom_code_input], outputs=[submit_status, code_box] ) with gr.Tab("📋 Task History & Downloads"): gr.Markdown("Apna Job Code daalo — live digital meter, sab tasks ki history aur download yahin milega.\n\n_(History 5 ghante baad auto-delete ho jati hai)_") code_input = gr.Textbox(label="Job Code") check_btn = gr.Button("🔄 Refresh History", variant="primary") status_output = gr.Markdown() cards_output = gr.HTML() result_video = gr.Video(label="Latest Ready Video") check_btn.click(fn=check_status, inputs=[code_input], outputs=[status_output, cards_output, result_video]) timer = gr.Timer(2.0, active=True) timer.tick(fn=check_status, inputs=[code_input], outputs=[status_output, cards_output, result_video]) with gr.Tab("📜 Sabki History"): gr.Markdown("Sabhi codes ki summary (5 ghante ke andar wali)") history_output = gr.Markdown() refresh_all_btn = gr.Button("🔄 Refresh") refresh_all_btn.click(fn=get_history, inputs=[], outputs=[history_output]) demo.load(fn=get_history, inputs=[], outputs=[history_output]) if __name__ == "__main__": demo.launch()