| 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 |
|
|
| |
| 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 |
|
|
| |
| 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)) |
|
|
| |
| 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) |
|
|
| |
| 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:]}" |
|
|
| |
| if BGM_FILE: |
| bgm_loop = os.path.join(work_dir, "bgm_loop.mp3") |
| |
| 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) |
|
|
| |
| 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'<span style="background:#16a34a;color:white;padding:4px 12px;border-radius:14px;font-weight:bold;">β
Done</span>' |
| elif status == "error": |
| return f'<span style="background:#dc2626;color:white;padding:4px 12px;border-radius:14px;font-weight:bold;">β Error</span>' |
| elif status == "queued": |
| return f'<span style="background:#6b7280;color:white;padding:4px 12px;border-radius:14px;font-weight:bold;">β³ Queued</span>' |
| else: |
| return f'<span style="background:#2563eb;color:white;padding:4px 12px;border-radius:14px;font-weight:bold;">βοΈ {pct}%</span>' |
|
|
|
|
| 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 = ' <span style="color:#818cf8;">π΅ Audio changed</span>' 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'<div style="color:#9ca3af;font-family:monospace;margin-top:4px;">' |
| f'β‘ {t.get("progress",0)}% | {elapsed}s/{total_dur}s | Speed: {speed}x | FPS: {fps} | ETA: {eta_txt}' |
| f'</div>') |
| elif t.get("status") == "done": |
| size = t.get("size_mb", "?") |
| detail = f'<div style="color:#9ca3af;">Size: {size} MB</div>' |
| elif t.get("status") == "error": |
| detail = f'<div style="color:#f87171;">{t.get("message","")}</div>' |
|
|
| return f""" |
| <div style="background:#1a1a2e;border-radius:10px;padding:14px;margin-bottom:10px;"> |
| <div style="display:flex;align-items:center;gap:10px;"> |
| {badge} |
| <span style="color:white;font-family:monospace;font-size:16px;">{name}</span> |
| </div> |
| <div style="color:#6b7280;font-size:12px;margin-top:4px;">{when}</div> |
| {dna_tag} |
| {detail} |
| </div> |
| """ |
|
|
|
|
| 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) |
|
|
|
|
| |
| 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() |