Spaces:
Running
Running
| import os | |
| import sys | |
| import json | |
| import time | |
| import shutil | |
| import base64 | |
| import threading | |
| import subprocess | |
| from pathlib import Path | |
| from typing import Optional, List, Dict, Any | |
| from fastapi import FastAPI, UploadFile, File, Form, BackgroundTasks, HTTPException | |
| from fastapi.staticfiles import StaticFiles | |
| from fastapi.responses import FileResponse, JSONResponse | |
| from pydantic import BaseModel | |
| from huggingface_hub import HfApi | |
| app = FastAPI(title="EpicSync Studio") | |
| DATA_DIR = os.path.abspath("data") | |
| JOBS_FILE = os.path.join(DATA_DIR, "jobs.json") | |
| STAGING_DIR = os.path.join(DATA_DIR, "staging") | |
| OUTPUTS_DIR = os.path.join(DATA_DIR, "outputs") | |
| for d in [DATA_DIR, STAGING_DIR, OUTPUTS_DIR]: | |
| os.makedirs(d, exist_ok=True) | |
| jobs_lock = threading.Lock() | |
| def load_jobs(): | |
| with jobs_lock: | |
| if os.path.exists(JOBS_FILE): | |
| try: | |
| with open(JOBS_FILE, "r", encoding="utf-8") as f: | |
| return json.load(f) | |
| except Exception: | |
| return {} | |
| return {} | |
| def save_jobs(jobs): | |
| with jobs_lock: | |
| with open(JOBS_FILE, "w", encoding="utf-8") as f: | |
| json.dump(jobs, f, indent=2) | |
| def append_log(job_id, message): | |
| jobs = load_jobs() | |
| if job_id in jobs: | |
| timestamp = time.strftime("%H:%M:%S") | |
| log_line = f"[{timestamp}] {message}" | |
| jobs[job_id]["logs"].append(log_line) | |
| save_jobs(jobs) | |
| print(f"[EpicSync - {job_id}] {message}", flush=True) | |
| def setup_kaggle_auth(username, key): | |
| env = os.environ.copy() | |
| env["KAGGLE_USERNAME"] = username | |
| env["KAGGLE_KEY"] = key | |
| env["KAGGLE_API_TOKEN"] = key | |
| for p in ["~/.kaggle", "~/.config/kaggle"]: | |
| d = os.path.expanduser(p) | |
| os.makedirs(d, exist_ok=True) | |
| creds_file = os.path.join(d, "kaggle.json") | |
| with open(creds_file, "w") as f: | |
| json.dump({"username": username, "key": key}, f) | |
| token_file = os.path.join(d, "access_token") | |
| with open(token_file, "w") as f: | |
| f.write(key.strip()) | |
| try: | |
| os.chmod(creds_file, 0o600) | |
| os.chmod(token_file, 0o600) | |
| except Exception: | |
| pass | |
| return env | |
| def upload_to_hf_dataset(file_path, repo_id, path_in_repo, hf_token): | |
| if not repo_id or not hf_token: | |
| return | |
| try: | |
| api = HfApi(token=hf_token) | |
| api.create_repo(repo_id=repo_id, repo_type="dataset", exist_ok=True) | |
| api.upload_file( | |
| path_or_fileobj=file_path, | |
| path_in_repo=path_in_repo, | |
| repo_id=repo_id, | |
| repo_type="dataset" | |
| ) | |
| except Exception as e: | |
| print(f"[WARN] HF Dataset upload failed: {e}") | |
| KERNEL_TEMPLATE = """import os | |
| import subprocess | |
| import glob | |
| import sys | |
| import base64 | |
| import shutil | |
| def run_cmd(cmd): | |
| print(f"Executing: {cmd}") | |
| res = subprocess.run(cmd, shell=True, capture_output=True, text=True) | |
| with open("/kaggle/working/execution.log", "a", encoding="utf-8") as f: | |
| f.write(f"=== CMD: {cmd} ===\\n") | |
| f.write(f"STDOUT:\\n{res.stdout}\\n") | |
| f.write(f"STDERR:\\n{res.stderr}\\n") | |
| f.write(f"EXIT CODE: {res.returncode}\\n\\n") | |
| if res.returncode != 0: | |
| print(f" [WARN] Exit code {res.returncode}") | |
| return res.returncode | |
| # Fetch or decode video sent from frontend UI | |
| HF_REPO = ___HF_REPO___ | |
| JOB_ID = ___JOB_ID___ | |
| VIDEO_B64 = ___VIDEO_B64___ | |
| if VIDEO_B64: | |
| with open("/kaggle/working/input.mp4", "wb") as f: | |
| f.write(base64.b64decode(VIDEO_B64)) | |
| print("Decoded frontend input.mp4 directly from script.") | |
| elif HF_REPO and JOB_ID: | |
| import urllib.request | |
| url = f"https://huggingface.co/datasets/{HF_REPO}/resolve/main/inputs/{JOB_ID}.mp4" | |
| print(f"Fetching input video from HF Dataset: {url}") | |
| try: | |
| urllib.request.urlretrieve(url, "/kaggle/working/input.mp4") | |
| print(f"Successfully downloaded frontend input.mp4 ({os.path.getsize('/kaggle/working/input.mp4')} bytes).") | |
| except Exception as e: | |
| print(f"[WARN] Could not fetch video from HF dataset: {e}") | |
| # 0.5 SETUP BACKGROUND MUSIC (IF PROVIDED) | |
| bgm_repo_path = ___BGM_REPO_PATH___ | |
| has_bgm = False | |
| if bgm_repo_path and HF_REPO: | |
| print(f"Fetching background music from HF dataset {HF_REPO}...", flush=True) | |
| try: | |
| from huggingface_hub import hf_hub_download | |
| bgm_file = hf_hub_download(repo_id=HF_REPO, filename=bgm_repo_path, repo_type="dataset", local_dir="/kaggle/working", token=___HF_TOKEN___ or None) | |
| if bgm_file != "/kaggle/working/bg_music.mp3": | |
| shutil.copy(bgm_file, "/kaggle/working/bg_music.mp3") | |
| if os.path.exists("/kaggle/working/bg_music.mp3"): | |
| has_bgm = True | |
| print("Successfully downloaded and staged background music!", flush=True) | |
| except Exception as e: | |
| print(f"Warning: Could not download background music: {e}", flush=True) | |
| # ========== 1. INSTALL DEPENDENCIES ========== | |
| run_cmd("pip install edge-tts") | |
| run_cmd("git clone https://github.com/OpenTalker/video-retalking.git") | |
| # Install deps individually - skip numpy (use Kaggle's numpy 2.x) and skip torch (use Kaggle's torch) | |
| run_cmd("pip install basicsr kornia face-alignment ninja einops facexlib yacs librosa==0.9.2 dlib cmake gfpgan") | |
| # ========== 2. DOWNLOAD CHECKPOINTS FROM HUGGINGFACE ========== | |
| os.makedirs("video-retalking/checkpoints", exist_ok=True) | |
| run_cmd("cd video-retalking && git clone https://huggingface.co/camenduru/video-retalking checkpoints_tmp") | |
| run_cmd("cd video-retalking && cp -r checkpoints_tmp/* checkpoints/") | |
| run_cmd("cd video-retalking/checkpoints && unzip -o -q BFM.zip") | |
| run_cmd("cd video-retalking/checkpoints && cp ParseNet-latest.pth parsing_parsenet.pth") | |
| run_cmd("cd video-retalking && rm -rf checkpoints_tmp") | |
| # Verify all required checkpoints exist | |
| required_checkpoints = [ | |
| "DNet.pt", "ENet.pth", "LNet.pth", "GFPGANv1.3.pth", | |
| "GPEN-BFR-512.pth", "ParseNet-latest.pth", "parsing_parsenet.pth", | |
| "RetinaFace-R50.pth", "shape_predictor_68_face_landmarks.dat", | |
| "face3d_pretrain_epoch_20.pth", "30_net_gen.pth", "expression.mat", | |
| ] | |
| print("\\n=== Checkpoint Verification ===") | |
| for ck in required_checkpoints: | |
| path = f"video-retalking/checkpoints/{ck}" | |
| exists = os.path.isfile(path) | |
| size = os.path.getsize(path) if exists else 0 | |
| print(f" {'OK' if exists and size > 1000 else 'MISSING'}: {ck} ({size} bytes)") | |
| print() | |
| # ========== 3. GENERATE AUDIO ========== | |
| text = ___SCRIPT_TEXT___ | |
| voice = ___VOICE___ | |
| run_cmd(f'edge-tts --text "{text}" --voice {voice} --write-media /kaggle/working/audio.wav') | |
| audio_path = "/kaggle/working/audio.wav" | |
| # ========== 4. APPLY ALL COMPATIBILITY PATCHES ========== | |
| print("\\n=== Applying Compatibility Patches ===") | |
| # --- PATCH A: basicsr torchvision.transforms.functional_tensor (removed in torchvision >= 0.17) --- | |
| import sys | |
| from pathlib import Path | |
| for sp in sys.path: | |
| deg_path = Path(sp) / "basicsr" / "data" / "degradations.py" | |
| if deg_path.exists(): | |
| with open(deg_path, "r") as f: | |
| content = f.read() | |
| content = content.replace( | |
| "from torchvision.transforms.functional_tensor import rgb_to_grayscale", | |
| "from torchvision.transforms.functional import rgb_to_grayscale" | |
| ) | |
| with open(deg_path, "w") as f: | |
| f.write(content) | |
| print(" [PATCH A] Fixed basicsr functional_tensor -> functional") | |
| # --- PATCH B: numpy 2.x removed np.int, np.float, np.bool, np.VisibleDeprecationWarning --- | |
| import numpy as np | |
| if not hasattr(np, 'int'): | |
| np.int = int | |
| if not hasattr(np, 'float'): | |
| np.float = float | |
| if not hasattr(np, 'bool'): | |
| np.bool = bool | |
| if not hasattr(np, 'complex'): | |
| np.complex = complex | |
| if not hasattr(np, 'object'): | |
| np.object = object | |
| if not hasattr(np, 'str'): | |
| np.str = str | |
| if not hasattr(np, 'VisibleDeprecationWarning'): | |
| np.VisibleDeprecationWarning = DeprecationWarning | |
| print(" [PATCH B] Restored deprecated numpy type aliases") | |
| # --- PATCH C: face_alignment LandmarksType._2D -> TWO_D (changed in face_alignment >= 1.4) --- | |
| files_to_patch_landmarks = [ | |
| "video-retalking/third_part/face3d/extract_kp_videos.py", | |
| "video-retalking/utils/alignment_stit.py", | |
| ] | |
| for filepath in files_to_patch_landmarks: | |
| if os.path.isfile(filepath): | |
| with open(filepath, "r") as f: | |
| content = f.read() | |
| content = content.replace( | |
| "face_alignment.LandmarksType._2D", | |
| "face_alignment.LandmarksType.TWO_D" | |
| ) | |
| with open(filepath, "w") as f: | |
| f.write(content) | |
| print(f" [PATCH C] Fixed LandmarksType._2D in {filepath}") | |
| # --- PATCH D: PIL.Image.ANTIALIAS removed in Pillow >= 10.0, replaced with LANCZOS --- | |
| import PIL.Image | |
| if not hasattr(PIL.Image, 'ANTIALIAS'): | |
| PIL.Image.ANTIALIAS = PIL.Image.LANCZOS | |
| print(" [PATCH D] Restored PIL.Image.ANTIALIAS alias") | |
| files_to_patch_antialias = [ | |
| "video-retalking/utils/alignment_stit.py", | |
| "video-retalking/utils/ffhq_preprocess.py", | |
| "video-retalking/third_part/ganimation_replicate/visualizer.py", | |
| ] | |
| for filepath in files_to_patch_antialias: | |
| if os.path.isfile(filepath): | |
| with open(filepath, "r") as f: | |
| content = f.read() | |
| if "ANTIALIAS" in content: | |
| content = content.replace("Image.ANTIALIAS", "Image.LANCZOS") | |
| content = content.replace("PIL.Image.ANTIALIAS", "PIL.Image.LANCZOS") | |
| with open(filepath, "w") as f: | |
| f.write(content) | |
| print(f" [PATCH D] Fixed ANTIALIAS -> LANCZOS in {filepath}") | |
| # --- PATCH E: np.int (bare, not np.int32) in face_detection/utils.py --- | |
| fd_utils = "video-retalking/third_part/face_detection/utils.py" | |
| if os.path.isfile(fd_utils): | |
| with open(fd_utils, "r") as f: | |
| content = f.read() | |
| content = content.replace("dtype=np.int)", "dtype=np.int64)") | |
| with open(fd_utils, "w") as f: | |
| f.write(content) | |
| print(" [PATCH E] Fixed np.int -> np.int64 in face_detection/utils.py") | |
| # --- PATCH F: torch.load needs weights_only=False for PyTorch >= 2.6 --- | |
| import torch | |
| _original_torch_load = torch.load | |
| def _patched_torch_load(*args, **kwargs): | |
| if 'weights_only' not in kwargs: | |
| kwargs['weights_only'] = False | |
| return _original_torch_load(*args, **kwargs) | |
| torch.load = _patched_torch_load | |
| print(" [PATCH F] Monkey-patched torch.load for weights_only=False") | |
| # --- PATCH G: Patch numpy in inference.py --- | |
| with open("video-retalking/inference.py", "r") as f: | |
| inf_code = f.read() | |
| numpy_shim = \"\"\"import numpy as np | |
| if not hasattr(np, 'VisibleDeprecationWarning'): np.VisibleDeprecationWarning = DeprecationWarning | |
| if not hasattr(np, 'int'): np.int = int | |
| if not hasattr(np, 'float'): np.float = float | |
| if not hasattr(np, 'bool'): np.bool = bool | |
| if not hasattr(np, 'complex'): np.complex = complex | |
| if not hasattr(np, 'object'): np.object = object | |
| if not hasattr(np, 'str'): np.str = str | |
| import PIL.Image | |
| if not hasattr(PIL.Image, 'ANTIALIAS'): PIL.Image.ANTIALIAS = PIL.Image.LANCZOS | |
| import torch as _torch | |
| _orig_load = _torch.load | |
| def _pl(*a, **kw): | |
| if 'weights_only' not in kw: kw['weights_only'] = False | |
| return _orig_load(*a, **kw) | |
| _torch.load = _pl | |
| \"\"\" | |
| inf_code = inf_code.replace( | |
| "[float(item) for item in np.hsplit(trans_params, 5)]", | |
| "[float(np.squeeze(item)) for item in np.hsplit(trans_params, 5)]" | |
| ) | |
| with open("video-retalking/inference.py", "w") as f: | |
| f.write(numpy_shim + inf_code) | |
| print(" [PATCH G] Prepended shims and patched np.hsplit unwrapping in inference.py") | |
| preprocess_file = "video-retalking/third_part/face3d/util/preprocess.py" | |
| if os.path.isfile(preprocess_file): | |
| with open(preprocess_file, "r") as f: | |
| content = f.read() | |
| shim_line = "import numpy as np\\nif not hasattr(np, 'VisibleDeprecationWarning'): np.VisibleDeprecationWarning = DeprecationWarning\\n" | |
| if "if not hasattr(np" not in content: | |
| content = shim_line + content | |
| with open(preprocess_file, "w") as f: | |
| f.write(content) | |
| print(" [PATCH G] Patched preprocess.py for np.VisibleDeprecationWarning") | |
| # --- PATCH H: face3d NumPy 2.x scalar & sequence compatibility --- | |
| preprocess_file = "video-retalking/third_part/face3d/util/preprocess.py" | |
| if os.path.exists(preprocess_file): | |
| with open(preprocess_file, "r") as f: | |
| pcontent = f.read() | |
| pcontent = pcontent.replace( | |
| "return t, s", | |
| "return np.squeeze(t), float(np.squeeze(s))" | |
| ).replace( | |
| "w = (w0*s).astype(np.int32)", | |
| "w = int(w0*s)" | |
| ).replace( | |
| "h = (h0*s).astype(np.int32)", | |
| "h = int(h0*s)" | |
| ).replace( | |
| "left = (w/2 - target_size/2 + float((t[0] - w0/2)*s)).astype(np.int32)", | |
| "left = int(w/2 - target_size/2 + float(np.squeeze((t[0] - w0/2)*s)))" | |
| ).replace( | |
| "up = (h/2 - target_size/2 + float((h0/2 - t[1])*s)).astype(np.int32)", | |
| "up = int(h/2 - target_size/2 + float(np.squeeze((h0/2 - t[1])*s)))" | |
| ).replace( | |
| "float((t[0] - w0/2)*s)", | |
| "float(np.squeeze((t[0] - w0/2)*s))" | |
| ).replace( | |
| "float((h0/2 - t[1])*s)", | |
| "float(np.squeeze((h0/2 - t[1])*s))" | |
| ).replace( | |
| "trans_params = np.array([w0, h0, s, t[0], t[1]])", | |
| "trans_params = np.array([float(np.squeeze(w0)), float(np.squeeze(h0)), float(np.squeeze(s)), float(np.squeeze(t[0])), float(np.squeeze(t[1]))], dtype=np.float32)" | |
| ) | |
| with open(preprocess_file, "w") as f: | |
| f.write(pcontent) | |
| print(" [PATCH H] Patched preprocess.py POS, astype, and trans_params for NumPy 2.x") | |
| # --- PATCH J: Fix GPEN align_faces.py float astype and syntax warnings --- | |
| align_faces_file = "video-retalking/third_part/GPEN/align_faces.py" | |
| if os.path.isfile(align_faces_file): | |
| with open(align_faces_file, "r") as f: | |
| af_content = f.read() | |
| af_content = af_content.replace("is 'cv2_affine'", "== 'cv2_affine'") | |
| af_content = af_content.replace("is 'cv2_rigid'", "== 'cv2_rigid'") | |
| af_content = af_content.replace("is 'affine'", "== 'affine'") | |
| af_content = af_content.replace( | |
| "(1 + inner_padding_factor * 2).astype(np.int32)", | |
| "int(1 + inner_padding_factor * 2)" | |
| ) | |
| with open(align_faces_file, "w") as f: | |
| f.write(af_content) | |
| print(" [PATCH J] Fixed GPEN align_faces.py astype and syntax warnings") | |
| # --- PATCH I: Prevent PyTorch DataLoader multi-processing / shm deadlock on Kaggle containers --- | |
| import re | |
| for root, _, pyfiles in os.walk("video-retalking"): | |
| for pfile in pyfiles: | |
| if pfile.endswith(".py"): | |
| fpath = os.path.join(root, pfile) | |
| with open(fpath, "r", encoding="utf-8", errors="ignore") as f: | |
| code = f.read() | |
| if "num_workers" in code: | |
| code = re.sub(r'num_workers\\s*=\\s*\\d+', 'num_workers=0', code) | |
| with open(fpath, "w", encoding="utf-8") as f: | |
| f.write(code) | |
| print(" [PATCH I] Set DataLoader num_workers=0 across video-retalking to prevent container deadlocks") | |
| print("\\n=== All patches applied. Starting inference... ===\\n", flush=True) | |
| # ========== 5. FIND VIDEO ========== | |
| if os.path.exists("/kaggle/working/input.mp4"): | |
| video_path = "/kaggle/working/input.mp4" | |
| else: | |
| files = glob.glob("/kaggle/input/**/*.mp4", recursive=True) | |
| if not files: | |
| print("ERROR: No input video found!") | |
| sys.exit(1) | |
| video_path = files[0] | |
| print(f"Input video: {video_path}", flush=True) | |
| # ========== 6. RUN INFERENCE ========== | |
| os.environ["OMP_NUM_THREADS"] = "1" | |
| os.environ["MKL_NUM_THREADS"] = "1" | |
| cmd = f"cd video-retalking && python inference.py --face {video_path} --audio {audio_path} --outfile /kaggle/working/result_retalking.mp4" | |
| print(f"Executing Live: {cmd}", flush=True) | |
| res_code = subprocess.run(cmd, shell=True).returncode | |
| print(f"Inference Finished with Exit Code: {res_code}", flush=True) | |
| # ========== 7. VERIFY OUTPUT ========== | |
| output_path = "/kaggle/working/result_retalking.mp4" | |
| if os.path.isfile(output_path): | |
| size = os.path.getsize(output_path) | |
| print(f"\\n=== SUCCESS! Output video: {output_path} ({size} bytes) ===") | |
| else: | |
| print("\\n=== FAILED: No output video produced ===") | |
| run_cmd("ls -la /kaggle/working/") | |
| run_cmd("ls -la /kaggle/working/video-retalking/temp/ 2>/dev/null || true") | |
| # ========== 7.5 POST-PROCESSING: BACKGROUND MUSIC & SUBTITLES ========== | |
| current_video_path = "/kaggle/working/result_retalking.mp4" | |
| if os.path.exists(current_video_path): | |
| q = '"' | |
| if has_bgm and os.path.exists("/kaggle/working/bg_music.mp3"): | |
| print("Integrating background music (looping if shorter, ducking under dialogue)...", flush=True) | |
| bgm_out_path = "/kaggle/working/result_with_bgm.mp4" | |
| bgm_filter = "[0:a]volume=1.0[speech];[1:a]volume=0.18[bg];[speech][bg]amix=inputs=2:duration=first[a]" | |
| bgm_cmd = f"ffmpeg -y -i {q}{current_video_path}{q} -stream_loop -1 -i /kaggle/working/bg_music.mp3 -filter_complex {q}{bgm_filter}{q} -map 0:v -map {q}[a]{q} -c:v copy -c:a aac -b:a 192k {q}{bgm_out_path}{q}" | |
| if os.system(bgm_cmd) == 0 and os.path.exists(bgm_out_path) and os.path.getsize(bgm_out_path) > 0: | |
| os.remove(current_video_path) | |
| os.rename(bgm_out_path, current_video_path) | |
| print("Successfully integrated background music!", flush=True) | |
| add_captions = ___ADD_CAPTIONS___ | |
| if str(add_captions).lower() in ["true", "1", "yes"]: | |
| print("Generating 4-word subtitles (Font size 20, White text, Black stroke)...", flush=True) | |
| try: | |
| import asyncio, edge_tts | |
| sub_cues = [] | |
| async def generate_sub_cues(): | |
| comm = edge_tts.Communicate(___SCRIPT_TEXT___, ___VOICE___) | |
| async for event in comm.stream(): | |
| if event["type"] == "SentenceBoundary": | |
| start_sec = event["offset"] / 10000000.0 | |
| dur_sec = event["duration"] / 10000000.0 | |
| words = event["text"].strip().split() | |
| chunk_size = 4 | |
| chunks = [words[i:i + chunk_size] for i in range(0, len(words), chunk_size)] | |
| total_chars = sum(len(" ".join(c)) for c in chunks) or 1 | |
| cur_time = start_sec | |
| for c in chunks: | |
| chunk_text = " ".join(c) | |
| c_dur = dur_sec * (len(chunk_text) / total_chars) | |
| c_end = cur_time + c_dur | |
| sub_cues.append((cur_time, c_end, chunk_text)) | |
| cur_time = c_end | |
| asyncio.run(generate_sub_cues()) | |
| if sub_cues: | |
| def format_srt_time(sec): | |
| hrs = int(sec // 3600) | |
| mins = int((sec % 3600) // 60) | |
| secs = int(sec % 60) | |
| msecs = int(round((sec - int(sec)) * 1000)) | |
| return f"{hrs:02d}:{mins:02d}:{secs:02d},{msecs:03d}" | |
| nl = chr(10) | |
| with open("/kaggle/working/captions.srt", "w", encoding="utf-8") as sf: | |
| for idx, (st, et, txt) in enumerate(sub_cues, 1): | |
| sf.write(f"{idx}{nl}{format_srt_time(st)} --> {format_srt_time(et)}{nl}{txt}{nl}{nl}") | |
| sub_out_path = "/kaggle/working/result_retalking_subtitled.mp4" | |
| sub_filter = "subtitles=/kaggle/working/captions.srt:force_style='FontName=DejaVu Sans,FontSize=20,PrimaryColour=&H00FFFFFF&,OutlineColour=&H00000000&,Outline=2,BorderStyle=1,Alignment=2,MarginV=25'" | |
| sub_cmd = f"ffmpeg -y -i {q}{current_video_path}{q} -vf {q}{sub_filter}{q} -c:a copy {q}{sub_out_path}{q}" | |
| if os.system(sub_cmd) == 0 and os.path.exists(sub_out_path) and os.path.getsize(sub_out_path) > 0: | |
| os.remove(current_video_path) | |
| os.rename(sub_out_path, current_video_path) | |
| print("Successfully burned 4-word subtitles onto video!", flush=True) | |
| else: | |
| sub_filter_fb = "subtitles=/kaggle/working/captions.srt:force_style='FontSize=20,PrimaryColour=&H00FFFFFF&,OutlineColour=&H00000000&,Outline=2,BorderStyle=1,Alignment=2,MarginV=25'" | |
| sub_cmd_fb = f"ffmpeg -y -i {q}{current_video_path}{q} -vf {q}{sub_filter_fb}{q} -c:a copy {q}{sub_out_path}{q}" | |
| if os.system(sub_cmd_fb) == 0 and os.path.exists(sub_out_path) and os.path.getsize(sub_out_path) > 0: | |
| os.remove(current_video_path) | |
| os.rename(sub_out_path, current_video_path) | |
| print("Successfully burned subtitles with fallback font!", flush=True) | |
| except Exception as e: | |
| print(f"Warning: Subtitle generation failed: {e}", flush=True) | |
| # Step C: Final Video Speed Adjustment | |
| video_speed = "___VIDEO_SPEED___" | |
| if video_speed != "1.0": | |
| try: | |
| speed_val = float(video_speed) | |
| if speed_val != 1.0: | |
| print(f"Applying final video speed adjustment: {speed_val}x...", flush=True) | |
| speed_out_path = "/kaggle/working/result_speed_adjusted.mp4" | |
| speed_cmd = f"ffmpeg -y -i {q}{current_video_path}{q} -filter_complex {q}[0:v]setpts=PTS/{speed_val}[v];[0:a]atempo={speed_val}[a]{q} -map {q}[v]{q} -map {q}[a]{q} -c:v libx264 -crf 23 -preset fast -c:a aac -b:a 192k {q}{speed_out_path}{q}" | |
| if os.system(speed_cmd) == 0 and os.path.exists(speed_out_path) and os.path.getsize(speed_out_path) > 0: | |
| os.remove(current_video_path) | |
| os.rename(speed_out_path, current_video_path) | |
| print(f"Successfully adjusted video speed to {speed_val}x!", flush=True) | |
| except Exception as e: | |
| print(f"Warning: Video speed adjustment failed: {e}", flush=True) | |
| # Cleanup to avoid massive zip downloads | |
| run_cmd("rm -rf video-retalking /kaggle/working/bg_music.mp3 /kaggle/working/captions.srt /kaggle/working/result_with_bgm.mp4 /kaggle/working/result_retalking_subtitled.mp4 /kaggle/working/input.mp4 /kaggle/working/result_speed_adjusted.mp4") | |
| """ | |
| PREMIUM_KERNEL_TEMPLATE = """import os | |
| import subprocess | |
| import glob | |
| import sys | |
| import base64 | |
| import time | |
| import shutil | |
| def run_cmd(cmd): | |
| print(f"Executing: {cmd}", flush=True) | |
| res = subprocess.run(cmd, shell=True) | |
| return res | |
| print("=== STARTING PREMIUM STUDIO LTX-2.3 PIPELINE ===", flush=True) | |
| # 0. SETUP AUTHENTICATION FOR FAST DOWNLOADS | |
| hf_token = ___HF_TOKEN___ | |
| if hf_token and len(hf_token) > 5: | |
| os.environ["HF_TOKEN"] = hf_token | |
| print("Set HF_TOKEN globally for high-speed unthrottled downloads.", flush=True) | |
| # 1. SETUP IMAGE INPUT | |
| img_b64 = ___IMAGE_B64___ | |
| hf_repo = ___HF_REPO___ | |
| job_id = ___JOB_ID___ | |
| if img_b64 and len(img_b64) > 10: | |
| print("Decoding embedded base64 image...", flush=True) | |
| with open("/kaggle/working/input.png", "wb") as f: | |
| f.write(base64.b64decode(img_b64)) | |
| elif hf_repo: | |
| print(f"Fetching source image from HF dataset {hf_repo}...", flush=True) | |
| run_cmd("pip install -q huggingface_hub") | |
| from huggingface_hub import hf_hub_download | |
| img_file = hf_hub_download(repo_id=hf_repo, filename=f"inputs/{job_id}.png", repo_type="dataset", local_dir="/kaggle/working", token=hf_token or None) | |
| if img_file != "/kaggle/working/input.png": | |
| shutil.copy(img_file, "/kaggle/working/input.png") | |
| else: | |
| print("ERROR: No image input provided!", flush=True) | |
| sys.exit(1) | |
| # 1.5 SETUP BACKGROUND MUSIC (IF PROVIDED) | |
| bgm_repo_path = ___BGM_REPO_PATH___ | |
| has_bgm = False | |
| if bgm_repo_path and hf_repo: | |
| print(f"Fetching background music from HF dataset {hf_repo}...", flush=True) | |
| try: | |
| from huggingface_hub import hf_hub_download | |
| bgm_file = hf_hub_download(repo_id=hf_repo, filename=bgm_repo_path, repo_type="dataset", local_dir="/kaggle/working", token=hf_token or None) | |
| if bgm_file != "/kaggle/working/bg_music.mp3": | |
| shutil.copy(bgm_file, "/kaggle/working/bg_music.mp3") | |
| if os.path.exists("/kaggle/working/bg_music.mp3"): | |
| has_bgm = True | |
| print("Successfully downloaded and staged background music!", flush=True) | |
| except Exception as e: | |
| print(f"Warning: Could not download background music: {e}", flush=True) | |
| # 2. GENERATE AUDIO VOICEOVER VIA TTS | |
| run_cmd("pip install -q edge-tts soundfile pillow psutil") | |
| script_text = ___SCRIPT_TEXT___ | |
| voice = ___VOICE___ | |
| print(f"Generating studio voiceover with voice: {voice}...", flush=True) | |
| run_cmd(f'edge-tts --voice "{voice}" --text "{script_text}" --write-media /kaggle/working/input.wav') | |
| # 3. INSTALL COMPATIBLE PYTORCH & WAN2GP | |
| print("Installing PyTorch 2.3.1 (CUDA 12.1 compatible)...", flush=True) | |
| run_cmd("pip install -q torch==2.3.1 torchvision==0.18.1 torchaudio==2.3.1 --index-url https://download.pytorch.org/whl/cu121") | |
| run_cmd("git clone https://github.com/DeepBeepMeep/Wan2GP.git") | |
| run_cmd("pip install --timeout 120 --retries 5 -q -r Wan2GP/requirements.txt") | |
| run_cmd("pip install --timeout 120 --retries 5 -q mmgp gradio gguf soundfile") | |
| # 4. UNIVERSAL DATASET AUTODISCOVERY OR FAST AUTHENTICATED DOWNLOAD | |
| os.makedirs("Wan2GP/models", exist_ok=True) | |
| print("Scanning /kaggle/input for ALL mounted model files across all datasets...", flush=True) | |
| all_input_files = glob.glob("/kaggle/input/**/*.*", recursive=True) | |
| for src_f in all_input_files: | |
| if any(src_f.endswith(ext) for ext in [".gguf", ".safetensors", ".metadata", ".json", ".model"]): | |
| item = os.path.basename(src_f) | |
| if "gemma" in src_f.lower(): | |
| dst_f = os.path.join("Wan2GP/models/gemma-3-12b-it-qat-q4_0-unquantized", item) | |
| else: | |
| dst_f = os.path.join("Wan2GP/models", item) | |
| os.makedirs(os.path.dirname(dst_f), exist_ok=True) | |
| if not os.path.exists(dst_f): | |
| os.symlink(src_f, dst_f) | |
| print(f" Linked: {item} -> {dst_f}", flush=True) | |
| # If this is any version of spatial upscaler, ensure version 1.1 filename is also linked! | |
| if "upscaler" in item.lower() or "upsample" in item.lower(): | |
| upscaler_11 = "Wan2GP/models/ltx-2.3-spatial-upscaler-x2-1.1.safetensors" | |
| if not os.path.exists(upscaler_11): | |
| os.symlink(src_f, upscaler_11) | |
| print(f" Linked upscaler alias: {item} -> {upscaler_11}", flush=True) | |
| print("Verifying all required model weights and Gemma text encoder files exist...", flush=True) | |
| from huggingface_hub import hf_hub_download | |
| REPO = 'DeepBeepMeep/LTX-2' | |
| base_files = [ | |
| 'ltx-2.3-22b-distilled-Q4_K_M_light.gguf', | |
| 'ltx-2.3-22b_audio_vae.safetensors', | |
| 'ltx-2.3-22b_embeddings_connector.safetensors', | |
| 'ltx-2.3-22b_text_embedding_projection.safetensors', | |
| 'ltx-2.3-22b_vae.safetensors', | |
| 'ltx-2.3-22b_vocoder.safetensors', | |
| 'ltx-2.3-spatial-upscaler-x2-1.1.safetensors' | |
| ] | |
| for f in base_files: | |
| dst = os.path.join("Wan2GP/models", f) | |
| if not os.path.exists(dst): | |
| print(f" [HF Download] Missing base file: {f}. Downloading...", flush=True) | |
| hf_hub_download(repo_id=REPO, filename=f, local_dir="Wan2GP/models", token=hf_token or None) | |
| gemma_folder = "gemma-3-12b-it-qat-q4_0-unquantized" | |
| gemma_files = [ | |
| 'gemma-3-12b-it-qat-q4_0-unquantized_quanto_bf16_int8.safetensors', | |
| 'added_tokens.json', | |
| 'chat_template.json', | |
| 'config_light.json', | |
| 'generation_config.json', | |
| 'preprocessor_config.json', | |
| 'processor_config.json', | |
| 'special_tokens_map.json', | |
| 'tokenizer.json', | |
| 'tokenizer.model', | |
| 'tokenizer_config.json' | |
| ] | |
| os.makedirs(os.path.join("Wan2GP/models", gemma_folder), exist_ok=True) | |
| for gf in gemma_files: | |
| dst = os.path.join("Wan2GP/models", gemma_folder, gf) | |
| if gf == 'gemma-3-12b-it-qat-q4_0-unquantized_quanto_bf16_int8.safetensors': | |
| alt_dst = os.path.join("Wan2GP/models", gemma_folder, 'gemma-3-12b-it-qat-q4_0-unquantized.safetensors') | |
| if os.path.exists(alt_dst): | |
| continue | |
| if not os.path.exists(dst): | |
| print(f" [HF Download] Missing Gemma file: {gf}. Downloading...", flush=True) | |
| try: | |
| hf_hub_download(repo_id=REPO, filename=f"{gemma_folder}/{gf}", local_dir="Wan2GP/models", token=hf_token or None) | |
| except Exception as e: | |
| if "quanto" in gf: | |
| alt_gf = 'gemma-3-12b-it-qat-q4_0-unquantized.safetensors' | |
| alt_dst = os.path.join("Wan2GP/models", gemma_folder, alt_gf) | |
| if not os.path.exists(alt_dst): | |
| print(f" [HF Download] Trying alternative weight: {alt_gf}...", flush=True) | |
| hf_hub_download(repo_id=REPO, filename=f"{gemma_folder}/{alt_gf}", local_dir="Wan2GP/models", token=hf_token or None) | |
| else: | |
| raise e | |
| # 5. EXECUTE LTX-2.3 SMART CHUNKING GENERATION SCRIPT | |
| ltx_script = '''import os, sys, gc, psutil, json, glob, time | |
| os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True,max_split_size_mb:128,garbage_collection_threshold:0.5" | |
| import numpy as np | |
| import soundfile as sf | |
| from PIL import Image | |
| import torch | |
| sys.path.insert(0, os.path.abspath("Wan2GP")) | |
| os.chdir("Wan2GP") | |
| os.environ["TOKENIZERS_PARALLELISM"] = "false" | |
| os.environ["CUDA_LAUNCH_BLOCKING"] = "0" | |
| import shared.qtypes.gguf | |
| from mmgp import offload | |
| from shared.utils import files_locator as fl | |
| fl.set_checkpoints_paths(["models", "ckpts", "."]) | |
| from models.ltx2.ltx2_handler import family_handler | |
| import models.ltx2.ltx2 as ltx2_mod | |
| _GPU_SM = torch.cuda.get_device_capability() if torch.cuda.is_available() else (0, 0) | |
| _IS_SM60 = (_GPU_SM[0] == 6) | |
| if _IS_SM60: | |
| os.environ["WGP_DTYPE"] = "fp16" | |
| print(" [GPU] sm_60 detected (P100) — FP16 mode + CPU audio patches enabled", flush=True) | |
| else: | |
| print(f" [GPU] sm_{_GPU_SM[0]}{_GPU_SM[1]} detected — native CUDA mode", flush=True) | |
| class _AudioEncoderP100Wrapper: | |
| def __init__(self, encoder): | |
| object.__setattr__(self, '_enc', encoder) | |
| def __call__(self, mel): | |
| if torch.cuda.is_available(): | |
| mel = mel.to(device=torch.device("cuda", torch.cuda.current_device()), dtype=torch.float16) | |
| return object.__getattribute__(self, '_enc')(mel) | |
| def __getattr__(self, name): | |
| return getattr(object.__getattribute__(self, '_enc'), name) | |
| def __setattr__(self, name, value): | |
| if name == '_enc': object.__setattr__(self, name, value) | |
| else: setattr(object.__getattribute__(self, '_enc'), name, value) | |
| # Patch GGUF config read | |
| _original_load_cfg = ltx2_mod._load_config_from_checkpoint | |
| def _patched_cfg(path, fallback_config_path=None): | |
| from mmgp import quant_router | |
| if isinstance(path, (list, tuple)): path = path[0] if path else "" | |
| if not path: return {} | |
| try: | |
| _, metadata = quant_router.load_metadata_state_dict(path) | |
| if metadata and metadata.get("config"): | |
| cfg = ltx2_mod._normalize_config(metadata.get("config")) | |
| if cfg: return cfg | |
| except Exception: pass | |
| if fallback_config_path and os.path.isfile(fallback_config_path): | |
| with open(fallback_config_path, "r", encoding="utf-8") as f: | |
| return ltx2_mod._normalize_config(json.load(f)) | |
| return {} | |
| ltx2_mod._load_config_from_checkpoint = _patched_cfg | |
| base_model_type = "ltx2_22B" | |
| model_def = {"ltx2_pipeline": "distilled"} | |
| extra = family_handler.query_model_def(base_model_type, model_def) | |
| model_def.update(extra) | |
| text_encoder_file = "models/gemma-3-12b-it-qat-q4_0-unquantized/gemma-3-12b-it-qat-q4_0-unquantized_quanto_bf16_int8.safetensors" | |
| if not os.path.exists(text_encoder_file): | |
| gemma_files = sorted(glob.glob("models/gemma-3-12b-it-qat-q4_0-unquantized/*.safetensors")) | |
| text_encoder_file = gemma_files[0] if gemma_files else None | |
| transformer_path = "models/ltx-2.3-22b-distilled-Q4_K_M_light.gguf" | |
| print("Loading LTX-2.3 Distilled Model Pipeline...", flush=True) | |
| gc.collect() | |
| torch.cuda.empty_cache() | |
| ltx2_model, pipe = family_handler.load_model( | |
| model_filename=transformer_path, | |
| model_type="ltx2_22B_distilled", | |
| base_model_type=base_model_type, | |
| model_def=model_def, | |
| dtype=torch.float16, | |
| VAE_dtype=torch.float16, | |
| text_encoder_filename=text_encoder_file, | |
| ) | |
| if _IS_SM60: | |
| try: | |
| import torch.nn.functional as _F | |
| from models.ltx2.ltx_core.model.audio_vae.causal_conv_2d import CausalConv2d as _CC2d | |
| def _cc2d_cpu_pad(self, x: torch.Tensor) -> torch.Tensor: | |
| if x.is_cuda: | |
| dev, dt = x.device, x.dtype | |
| x_cpu = x.detach().cpu().float() | |
| x_cpu = _F.pad(x_cpu, self.padding) | |
| w = self.conv.weight.detach().cpu().float() | |
| b = self.conv.bias.detach().cpu().float() if self.conv.bias is not None else None | |
| out = _F.conv2d(x_cpu, w, b, self.conv.stride, self.conv.padding, self.conv.dilation, self.conv.groups) | |
| return out.to(device=dev, dtype=dt) | |
| else: | |
| x = _F.pad(x, self.padding) | |
| return self.conv(x) | |
| _CC2d.forward = _cc2d_cpu_pad | |
| print(" [sm_60 Fix] CausalConv2d patched: pad+conv run on CPU", flush=True) | |
| except Exception as _e: | |
| print(f" [sm_60 Fix] Could not patch CausalConv2d: {_e}", flush=True) | |
| offload.profile( | |
| pipe, | |
| profile_no=4, | |
| quantizeTransformer=False, | |
| convertWeightsFloatTo=torch.float16, | |
| budgets={ | |
| "transformer": 6000, | |
| "text_encoder": 1500, | |
| "video_encoder": 2000, | |
| "video_decoder": 3000, | |
| "audio_encoder": 1000, | |
| "audio_decoder": 1000, | |
| "vocoder": 500, | |
| "spatial_upsampler": 1500, | |
| "vae": 1000, | |
| "*": 1000, | |
| }, | |
| ) | |
| offload.shared_state["_attention"] = "sdpa" | |
| # Load full audio | |
| wav, sr = sf.read("/kaggle/working/input.wav") | |
| if wav.ndim > 1: wav = wav.mean(axis=1) | |
| full_waveform = wav.astype(np.float32) | |
| total_dur_sec = len(wav) / sr | |
| MAX_CHUNK_SEC = 9.8 # Safe max duration per chunk (<= 233 frames) to guarantee zero OOM | |
| chunk_samples = int(MAX_CHUNK_SEC * sr) | |
| num_chunks = int(np.ceil(len(full_waveform) / chunk_samples)) | |
| print(f"Total audio duration: {total_dur_sec:.1f}s across {num_chunks} chunk(s). Starting Smart Chunking generation...", flush=True) | |
| aspect_ratio_mode = "___ASPECT_RATIO___" | |
| if aspect_ratio_mode == "16:9": | |
| target_width, target_height = 864, 480 | |
| elif aspect_ratio_mode == "1:1": | |
| target_width, target_height = 640, 640 | |
| else: # default 9:16 Portrait | |
| target_width, target_height = 480, 864 | |
| print(f"Target Aspect Ratio: {aspect_ratio_mode} -> Resolution: {target_width}x{target_height} (Macro-block 32 aligned)", flush=True) | |
| def prepare_image_for_ltx(img, target_w, target_h): | |
| img_ratio = img.width / img.height | |
| target_ratio = target_w / target_h | |
| if img_ratio > target_ratio: | |
| new_w = int(img.height * target_ratio) | |
| offset_x = (img.width - new_w) // 2 | |
| img = img.crop((offset_x, 0, offset_x + new_w, img.height)) | |
| elif img_ratio < target_ratio: | |
| new_h = int(img.width / target_ratio) | |
| offset_y = (img.height - new_h) // 2 | |
| img = img.crop((0, offset_y, img.width, offset_y + new_h)) | |
| return img.resize((target_w, target_h), Image.Resampling.LANCZOS) | |
| current_img = prepare_image_for_ltx(Image.open("/kaggle/working/input.png").convert("RGB"), target_width, target_height) | |
| chunk_video_files = [] | |
| for idx in range(num_chunks): | |
| start_s = idx * chunk_samples | |
| end_s = min(len(full_waveform), (idx + 1) * chunk_samples) | |
| sub_wav = full_waveform[start_s:end_s] | |
| sub_dur = len(sub_wav) / sr | |
| raw_frames = sub_dur * 24.0 | |
| k = max(0, round((raw_frames - 1) / 8)) | |
| num_frames = max(49, min(int(8 * k + 1), 241)) | |
| print(f"--- Processing Chunk {idx+1}/{num_chunks} ({sub_dur:.1f}s, {num_frames} frames) ---", flush=True) | |
| gen_kwargs = dict( | |
| input_prompt="high quality studio portrait video, realistic lip sync, natural facial expression and speech movements", | |
| image_start=current_img, | |
| input_waveform=sub_wav, | |
| input_waveform_sample_rate=int(sr), | |
| height=target_height, | |
| width=target_width, | |
| frame_num=num_frames, | |
| fps=24.0, | |
| seed=42 + idx, | |
| VAE_tile_size=256, | |
| input_video_strength=1.0, | |
| denoising_strength=1.0, | |
| guide_scale=4.0, | |
| sampling_steps=8, | |
| guide_phases=2, | |
| n_prompt="", | |
| video_prompt_type="", | |
| audio_prompt_type="2", | |
| audio_scale=1.0, | |
| ) | |
| video_out = ltx2_model.generate(**gen_kwargs) | |
| if video_out is not None: | |
| if isinstance(video_out, dict): | |
| video_tensor = video_out.get("x") | |
| elif isinstance(video_out, tuple): | |
| video_tensor = video_out[0] | |
| else: | |
| video_tensor = video_out | |
| if video_tensor is not None and torch.is_tensor(video_tensor): | |
| video_tensor = video_tensor.cpu() | |
| video_for_save = video_tensor.unsqueeze(0).float() / 127.5 - 1.0 | |
| from shared.utils.audio_video import save_video | |
| out_path = f"/kaggle/working/chunk_{idx}.mp4" | |
| save_video(tensor=video_for_save, save_file=out_path, fps=24.0, normalize=True, value_range=(-1, 1)) | |
| chunk_video_files.append(out_path) | |
| del video_tensor, video_for_save | |
| # Extract last frame of this chunk to maintain seamless facial continuity for next chunk | |
| if os.path.exists(f"/kaggle/working/chunk_{idx}.mp4"): | |
| import cv2 | |
| cap = cv2.VideoCapture(f"/kaggle/working/chunk_{idx}.mp4") | |
| cap.set(cv2.CAP_PROP_POS_FRAMES, cap.get(cv2.CAP_PROP_FRAME_COUNT) - 1) | |
| ret, frame = cap.read() | |
| cap.release() | |
| if ret: | |
| current_img = prepare_image_for_ltx(Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)), target_width, target_height) | |
| # Proactive 4-step memory flush between chunks | |
| del video_out, gen_kwargs, sub_wav | |
| gc.collect() | |
| gc.collect() | |
| torch.cuda.empty_cache() | |
| torch.cuda.ipc_collect() | |
| time.sleep(1.5) | |
| if len(chunk_video_files) == 1: | |
| os.rename(chunk_video_files[0], "/kaggle/working/result_retalking_silent.mp4") | |
| elif len(chunk_video_files) > 1: | |
| print("Concatenating video chunks seamlessly via ffmpeg...", flush=True) | |
| with open("/kaggle/working/concat_list.txt", "w") as f: | |
| for fpath in chunk_video_files: | |
| f.write("file '" + str(fpath) + "'" + chr(10)) | |
| os.system("ffmpeg -y -f concat -safe 0 -i /kaggle/working/concat_list.txt -c copy /kaggle/working/result_retalking_silent.mp4") | |
| if os.path.exists("/kaggle/working/result_retalking_silent.mp4"): | |
| print("Muxing studio voiceover audio onto final video...", flush=True) | |
| os.system("ffmpeg -y -i /kaggle/working/result_retalking_silent.mp4 -i /kaggle/working/input.wav -c:v copy -c:a aac -b:a 192k -shortest /kaggle/working/result_retalking.mp4") | |
| print("SUCCESS: Saved final Premium LTX video to /kaggle/working/result_retalking.mp4", flush=True) | |
| ''' | |
| with open("run_prem.py", "w", encoding="utf-8") as f: | |
| f.write(ltx_script) | |
| run_cmd("pip cache purge") | |
| run_cmd("python -u run_prem.py") | |
| # 6. POST-PROCESSING: BACKGROUND MUSIC & WORD-LEVEL SUBTITLES (4 WORDS/SCREEN) | |
| current_video_path = "/kaggle/working/result_retalking.mp4" | |
| if os.path.exists(current_video_path): | |
| q = '"' | |
| # Step A: Integrate Background Music (auto-looping if shorter, ducking under dialogue) | |
| if has_bgm and os.path.exists("/kaggle/working/bg_music.mp3"): | |
| print("Integrating background music (looping if shorter, ducking under dialogue)...", flush=True) | |
| bgm_out_path = "/kaggle/working/result_with_bgm.mp4" | |
| bgm_filter = "[0:a]volume=1.0[speech];[1:a]volume=0.18[bg];[speech][bg]amix=inputs=2:duration=first[a]" | |
| bgm_cmd = f"ffmpeg -y -i {q}{current_video_path}{q} -stream_loop -1 -i /kaggle/working/bg_music.mp3 -filter_complex {q}{bgm_filter}{q} -map 0:v -map {q}[a]{q} -c:v copy -c:a aac -b:a 192k {q}{bgm_out_path}{q}" | |
| if os.system(bgm_cmd) == 0 and os.path.exists(bgm_out_path) and os.path.getsize(bgm_out_path) > 0: | |
| os.remove(current_video_path) | |
| os.rename(bgm_out_path, current_video_path) | |
| print("Successfully integrated background music!", flush=True) | |
| # Step B: Generate & Burn Word-Level Subtitles (4 words/screen, font size 20, white text, black stroke) | |
| add_captions = ___ADD_CAPTIONS___ | |
| if str(add_captions).lower() in ["true", "1", "yes"]: | |
| print("Generating 4-word subtitles (Font size 20, White text, Black stroke)...", flush=True) | |
| try: | |
| import asyncio, edge_tts | |
| sub_cues = [] | |
| async def generate_sub_cues(): | |
| comm = edge_tts.Communicate(script_text, voice) | |
| async for event in comm.stream(): | |
| if event["type"] == "SentenceBoundary": | |
| start_sec = event["offset"] / 10000000.0 | |
| dur_sec = event["duration"] / 10000000.0 | |
| words = event["text"].strip().split() | |
| chunk_size = 4 | |
| chunks = [words[i:i + chunk_size] for i in range(0, len(words), chunk_size)] | |
| total_chars = sum(len(" ".join(c)) for c in chunks) or 1 | |
| cur_time = start_sec | |
| for c in chunks: | |
| chunk_text = " ".join(c) | |
| c_dur = dur_sec * (len(chunk_text) / total_chars) | |
| c_end = cur_time + c_dur | |
| sub_cues.append((cur_time, c_end, chunk_text)) | |
| cur_time = c_end | |
| asyncio.run(generate_sub_cues()) | |
| if sub_cues: | |
| def format_srt_time(sec): | |
| hrs = int(sec // 3600) | |
| mins = int((sec % 3600) // 60) | |
| secs = int(sec % 60) | |
| msecs = int(round((sec - int(sec)) * 1000)) | |
| return f"{hrs:02d}:{mins:02d}:{secs:02d},{msecs:03d}" | |
| nl = chr(10) | |
| with open("/kaggle/working/captions.srt", "w", encoding="utf-8") as sf: | |
| for idx, (st, et, txt) in enumerate(sub_cues, 1): | |
| sf.write(f"{idx}{nl}{format_srt_time(st)} --> {format_srt_time(et)}{nl}{txt}{nl}{nl}") | |
| sub_out_path = "/kaggle/working/result_retalking_subtitled.mp4" | |
| sub_filter = "subtitles=/kaggle/working/captions.srt:force_style='FontName=DejaVu Sans,FontSize=20,PrimaryColour=&H00FFFFFF&,OutlineColour=&H00000000&,Outline=2,BorderStyle=1,Alignment=2,MarginV=25'" | |
| sub_cmd = f"ffmpeg -y -i {q}{current_video_path}{q} -vf {q}{sub_filter}{q} -c:a copy {q}{sub_out_path}{q}" | |
| if os.system(sub_cmd) == 0 and os.path.exists(sub_out_path) and os.path.getsize(sub_out_path) > 0: | |
| os.remove(current_video_path) | |
| os.rename(sub_out_path, current_video_path) | |
| print("Successfully burned 4-word subtitles onto video!", flush=True) | |
| else: | |
| sub_filter_fb = "subtitles=/kaggle/working/captions.srt:force_style='FontSize=20,PrimaryColour=&H00FFFFFF&,OutlineColour=&H00000000&,Outline=2,BorderStyle=1,Alignment=2,MarginV=25'" | |
| sub_cmd_fb = f"ffmpeg -y -i {q}{current_video_path}{q} -vf {q}{sub_filter_fb}{q} -c:a copy {q}{sub_out_path}{q}" | |
| if os.system(sub_cmd_fb) == 0 and os.path.exists(sub_out_path) and os.path.getsize(sub_out_path) > 0: | |
| os.remove(current_video_path) | |
| os.rename(sub_out_path, current_video_path) | |
| print("Successfully burned subtitles with fallback font!", flush=True) | |
| except Exception as e: | |
| print(f"Warning: Subtitle generation failed: {e}", flush=True) | |
| # Step C: Final Video Speed Adjustment | |
| video_speed = "___VIDEO_SPEED___" | |
| if video_speed != "1.0": | |
| try: | |
| speed_val = float(video_speed) | |
| if speed_val != 1.0: | |
| print(f"Applying final video speed adjustment: {speed_val}x...", flush=True) | |
| speed_out_path = "/kaggle/working/result_speed_adjusted.mp4" | |
| speed_cmd = f"ffmpeg -y -i {q}{current_video_path}{q} -filter_complex {q}[0:v]setpts=PTS/{speed_val}[v];[0:a]atempo={speed_val}[a]{q} -map {q}[v]{q} -map {q}[a]{q} -c:v libx264 -crf 23 -preset fast -c:a aac -b:a 192k {q}{speed_out_path}{q}" | |
| if os.system(speed_cmd) == 0 and os.path.exists(speed_out_path) and os.path.getsize(speed_out_path) > 0: | |
| os.remove(current_video_path) | |
| os.rename(speed_out_path, current_video_path) | |
| print(f"Successfully adjusted video speed to {speed_val}x!", flush=True) | |
| except Exception as e: | |
| print(f"Warning: Video speed adjustment failed: {e}", flush=True) | |
| # Cleanup massive repo and intermediate chunk videos to ensure clean output packaging | |
| run_cmd("rm -rf Wan2GP /kaggle/working/chunk_*.mp4 /kaggle/working/result_retalking_silent.mp4 /kaggle/working/concat_list.txt /kaggle/working/input.wav /kaggle/working/input.png /kaggle/working/bg_music.mp3 /kaggle/working/captions.srt /kaggle/working/result_with_bgm.mp4 /kaggle/working/result_retalking_subtitled.mp4 /kaggle/working/result_speed_adjusted.mp4") | |
| """ | |
| def monitor_job(job_id, slug, env, hf_repo, hf_token): | |
| append_log(job_id, f"Kernel pushed to Kaggle ({slug}). Starting monitoring loop...") | |
| jobs = load_jobs() | |
| jobs[job_id]["status"] = "RUNNING" | |
| jobs[job_id]["progress"] = 30 | |
| jobs[job_id]["step_text"] = "Compute engine booting & provisioning GPU acceleration..." | |
| save_jobs(jobs) | |
| last_status = "running" | |
| consecutive_errors = 0 | |
| while True: | |
| time.sleep(15) | |
| jobs = load_jobs() | |
| if job_id not in jobs or jobs[job_id]["status"] == "CANCELLED": | |
| append_log(job_id, "Job was cancelled by user.") | |
| break | |
| try: | |
| cmd = f"kaggle kernels status {slug}" | |
| res = subprocess.run(cmd, shell=True, capture_output=True, text=True, env=env) | |
| out = res.stdout.strip() | |
| if "complete" in out.lower(): | |
| append_log(job_id, "Kaggle reported: COMPLETE. Downloading generated video...") | |
| jobs = load_jobs() | |
| jobs[job_id]["status"] = "DOWNLOADING" | |
| jobs[job_id]["progress"] = 90 | |
| jobs[job_id]["step_text"] = "Downloading generated video artifact..." | |
| save_jobs(jobs) | |
| out_path = os.path.join(OUTPUTS_DIR, f"{job_id}.mp4") | |
| dl_dir = os.path.join(OUTPUTS_DIR, f"tmp_{job_id}") | |
| os.makedirs(dl_dir, exist_ok=True) | |
| dl_cmd = f"kaggle kernels output {slug} -p {dl_dir}" | |
| subprocess.run(dl_cmd, shell=True, env=env) | |
| # Step 1: Search specifically for result_retalking.mp4 across dl_dir and all subdirectories | |
| downloaded_result = None | |
| for root, _, files in os.walk(dl_dir): | |
| if "result_retalking.mp4" in files: | |
| downloaded_result = os.path.join(root, "result_retalking.mp4") | |
| break | |
| # Step 2: If result_retalking.mp4 wasn't found, look for result_retalking_silent.mp4 | |
| if not downloaded_result: | |
| for root, _, files in os.walk(dl_dir): | |
| if "result_retalking_silent.mp4" in files: | |
| downloaded_result = os.path.join(root, "result_retalking_silent.mp4") | |
| break | |
| # Step 3: Last resort fallback: find the largest .mp4 file that is NOT a chunk_ | |
| if not downloaded_result: | |
| best_size = -1 | |
| for root, _, files in os.walk(dl_dir): | |
| for f in files: | |
| if f.endswith(".mp4") and not f.startswith("chunk_"): | |
| fpath = os.path.join(root, f) | |
| if os.path.getsize(fpath) > best_size: | |
| best_size = os.path.getsize(fpath) | |
| downloaded_result = fpath | |
| if downloaded_result and os.path.exists(downloaded_result): | |
| if os.path.exists(out_path): | |
| os.remove(out_path) | |
| shutil.move(downloaded_result, out_path) | |
| shutil.rmtree(dl_dir, ignore_errors=True) | |
| if os.path.exists(out_path) and os.path.getsize(out_path) > 0: | |
| append_log(job_id, f"Video successfully downloaded ({os.path.getsize(out_path)} bytes).") | |
| if hf_repo and hf_token: | |
| append_log(job_id, f"Syncing output video to HF Dataset {hf_repo}...") | |
| upload_to_hf_dataset(out_path, hf_repo, f"outputs/{job_id}.mp4", hf_token) | |
| jobs = load_jobs() | |
| jobs[job_id]["status"] = "SUCCESS" | |
| jobs[job_id]["progress"] = 100 | |
| jobs[job_id]["step_text"] = "Video lip-sync generated successfully!" | |
| jobs[job_id]["output_file"] = f"/api/video/{job_id}" | |
| save_jobs(jobs) | |
| else: | |
| append_log(job_id, "ERROR: Execution finished but output video was not found or 0 bytes.") | |
| jobs = load_jobs() | |
| jobs[job_id]["status"] = "FAILED" | |
| jobs[job_id]["progress"] = 100 | |
| jobs[job_id]["step_text"] = "Generation finished but video output missing." | |
| save_jobs(jobs) | |
| break | |
| elif "error" in out.lower() or "cancel" in out.lower(): | |
| append_log(job_id, f"Kaggle reported status: {out}") | |
| jobs = load_jobs() | |
| jobs[job_id]["status"] = "FAILED" | |
| jobs[job_id]["progress"] = 100 | |
| jobs[job_id]["step_text"] = "Generation failed or error reported." | |
| save_jobs(jobs) | |
| break | |
| else: | |
| if out != last_status: | |
| append_log(job_id, f"Status update: {out}") | |
| last_status = out | |
| jobs = load_jobs() | |
| if job_id in jobs: | |
| cur_prog = jobs[job_id].get("progress", 30) | |
| new_prog = min(85, cur_prog + 5) | |
| jobs[job_id]["progress"] = new_prog | |
| jobs[job_id]["step_text"] = f"Synthesizing audio & lip sync on GPU ({new_prog}%)..." | |
| save_jobs(jobs) | |
| consecutive_errors = 0 | |
| except Exception as e: | |
| consecutive_errors += 1 | |
| if consecutive_errors > 5: | |
| append_log(job_id, f"Monitoring failed after repeated errors: {e}") | |
| jobs = load_jobs() | |
| jobs[job_id]["status"] = "FAILED" | |
| jobs[job_id]["progress"] = 100 | |
| jobs[job_id]["step_text"] = "Monitoring connection failed." | |
| save_jobs(jobs) | |
| break | |
| def prepare_and_launch_standard_job( | |
| job_id: str, | |
| staging: str, | |
| video_path: str, | |
| bgm_path: str, | |
| bgm_repo_path: str, | |
| script_text: str, | |
| voice: str, | |
| add_captions: str, | |
| video_speed: str, | |
| kaggle_user: str, | |
| kaggle_key: str, | |
| hf_repo: str, | |
| hf_token: str, | |
| kernel_id: str | |
| ): | |
| try: | |
| append_log(job_id, f"Preparing files and dataset upload...") | |
| if not bgm_repo_path and bgm_path and os.path.exists(bgm_path) and os.path.getsize(bgm_path) > 0: | |
| if hf_repo and hf_token: | |
| bgm_repo_path = f"inputs/{job_id}_bgm.mp3" | |
| append_log(job_id, f"Uploading background music to Hugging Face Dataset {hf_repo}...") | |
| upload_to_hf_dataset(bgm_path, hf_repo, bgm_repo_path, hf_token) | |
| elif bgm_repo_path: | |
| append_log(job_id, f"Using pre-uploaded background music from dataset: {bgm_repo_path}") | |
| vb64 = "" | |
| vsize = os.path.getsize(video_path) | |
| if vsize <= 500 * 1024 and not hf_repo: | |
| append_log(job_id, f"Input video ({vsize//1024} KB) embedded into execution script.") | |
| with open(video_path, "rb") as vf: | |
| vb64 = base64.b64encode(vf.read()).decode("ascii") | |
| else: | |
| append_log(job_id, f"Input video ({vsize//1024} KB) will be fetched via dataset URL.") | |
| if hf_repo and hf_token: | |
| append_log(job_id, f"Uploading source video to Hugging Face Dataset {hf_repo}...") | |
| upload_to_hf_dataset(video_path, hf_repo, f"inputs/{job_id}.mp4", hf_token) | |
| script_content = KERNEL_TEMPLATE.replace("___SCRIPT_TEXT___", repr(script_text)).replace("___VOICE___", repr(voice)).replace("___VIDEO_B64___", repr(vb64)).replace("___HF_REPO___", repr(hf_repo)).replace("___JOB_ID___", repr(job_id)).replace("___HF_TOKEN___", repr(hf_token)).replace("___ADD_CAPTIONS___", repr(str(add_captions))).replace("___BGM_REPO_PATH___", repr(bgm_repo_path)).replace("___VIDEO_SPEED___", str(video_speed)) | |
| with open(os.path.join(staging, "run_epicsync.py"), "w", encoding="utf-8") as f: | |
| f.write(script_content) | |
| meta = { | |
| "id": kernel_id, | |
| "title": f"EpicSync {job_id.split('_')[-1]}", | |
| "code_file": "run_epicsync.py", | |
| "language": "python", | |
| "kernel_type": "script", | |
| "is_private": True, | |
| "enable_gpu": True, | |
| "enable_tpu": False, | |
| "enable_internet": True, | |
| "keywords": ["gpu"], | |
| "dataset_sources": ["ikechukwuebiringa1/lipsyncbaby-video"], | |
| "competition_sources": [], | |
| "kernel_sources": [], | |
| "model_sources": [], | |
| "machine_shape": "NvidiaTeslaT4" | |
| } | |
| with open(os.path.join(staging, "kernel-metadata.json"), "w", encoding="utf-8") as f: | |
| json.dump(meta, f, indent=2) | |
| env = setup_kaggle_auth(kaggle_user, kaggle_key) | |
| append_log(job_id, f"Pushing kernel {kernel_id} to Kaggle with GPU acceleration...") | |
| res = subprocess.run(f"kaggle kernels push -p {staging}", shell=True, capture_output=True, text=True, env=env) | |
| if res.returncode != 0: | |
| append_log(job_id, f"ERROR pushing kernel: {res.stderr or res.stdout}") | |
| jobs = load_jobs() | |
| if job_id in jobs: | |
| jobs[job_id]["status"] = "FAILED" | |
| save_jobs(jobs) | |
| else: | |
| monitor_job(job_id, kernel_id, env, hf_repo, hf_token) | |
| except Exception as e: | |
| append_log(job_id, f"ERROR in background launch: {str(e)}") | |
| jobs = load_jobs() | |
| if job_id in jobs: | |
| jobs[job_id]["status"] = "FAILED" | |
| save_jobs(jobs) | |
| def prepare_and_launch_premium_job( | |
| job_id: str, | |
| staging: str, | |
| image_path: str, | |
| bgm_path: str, | |
| bgm_repo_path: str, | |
| script_text: str, | |
| voice: str, | |
| aspect_ratio: str, | |
| add_captions: str, | |
| video_speed: str, | |
| kaggle_user: str, | |
| kaggle_key: str, | |
| hf_repo: str, | |
| hf_token: str, | |
| kernel_id: str | |
| ): | |
| try: | |
| append_log(job_id, f"Preparing files and dataset upload...") | |
| if not bgm_repo_path and bgm_path and os.path.exists(bgm_path) and os.path.getsize(bgm_path) > 0: | |
| if hf_repo and hf_token: | |
| bgm_repo_path = f"inputs/{job_id}_bgm.mp3" | |
| append_log(job_id, f"Uploading background music to Hugging Face Dataset {hf_repo}...") | |
| upload_to_hf_dataset(bgm_path, hf_repo, bgm_repo_path, hf_token) | |
| elif bgm_repo_path: | |
| append_log(job_id, f"Using pre-uploaded background music from dataset: {bgm_repo_path}") | |
| ib64 = "" | |
| isize = os.path.getsize(image_path) | |
| if isize <= 500 * 1024 and not hf_repo: | |
| append_log(job_id, f"Input image ({isize//1024} KB) embedded into script.") | |
| with open(image_path, "rb") as vf: | |
| ib64 = base64.b64encode(vf.read()).decode("ascii") | |
| else: | |
| append_log(job_id, f"Input image ({isize//1024} KB) will be fetched via dataset URL.") | |
| if hf_repo and hf_token: | |
| append_log(job_id, f"Uploading source portrait to Hugging Face Dataset {hf_repo}...") | |
| upload_to_hf_dataset(image_path, hf_repo, f"inputs/{job_id}.png", hf_token) | |
| script_content = PREMIUM_KERNEL_TEMPLATE.replace("___SCRIPT_TEXT___", repr(script_text)).replace("___VOICE___", repr(voice)).replace("___IMAGE_B64___", repr(ib64)).replace("___HF_REPO___", repr(hf_repo)).replace("___JOB_ID___", repr(job_id)).replace("___HF_TOKEN___", repr(hf_token)).replace("___ASPECT_RATIO___", aspect_ratio).replace("___ADD_CAPTIONS___", repr(str(add_captions))).replace("___BGM_REPO_PATH___", repr(bgm_repo_path)).replace("___VIDEO_SPEED___", str(video_speed)) | |
| with open(os.path.join(staging, "run_epicsync.py"), "w", encoding="utf-8") as f: | |
| f.write(script_content) | |
| meta = { | |
| "id": kernel_id, | |
| "title": f"EpicSync Premium {job_id.split('_')[-1]}", | |
| "code_file": "run_epicsync.py", | |
| "language": "python", | |
| "kernel_type": "script", | |
| "is_private": True, | |
| "enable_gpu": True, | |
| "enable_tpu": False, | |
| "enable_internet": True, | |
| "keywords": ["gpu", "diffusion", "ltx"], | |
| "dataset_sources": [ | |
| "guitammelbader/wan2gp-models", | |
| "canodian/pl-ltx-2-3-spatial-upscaler-x2-1-0-safetensors" | |
| ], | |
| "competition_sources": [], | |
| "kernel_sources": [], | |
| "model_sources": [], | |
| "machine_shape": "NvidiaTeslaT4" | |
| } | |
| with open(os.path.join(staging, "kernel-metadata.json"), "w", encoding="utf-8") as f: | |
| json.dump(meta, f, indent=2) | |
| env = setup_kaggle_auth(kaggle_user, kaggle_key) | |
| append_log(job_id, f"Pushing Premium kernel {kernel_id} to Kaggle with mounted LTX datasets...") | |
| res = subprocess.run(f"kaggle kernels push -p {staging}", shell=True, capture_output=True, text=True, env=env) | |
| if res.returncode != 0: | |
| append_log(job_id, f"ERROR pushing kernel: {res.stderr or res.stdout}") | |
| jobs = load_jobs() | |
| if job_id in jobs: | |
| jobs[job_id]["status"] = "FAILED" | |
| save_jobs(jobs) | |
| else: | |
| monitor_job(job_id, kernel_id, env, hf_repo, hf_token) | |
| except Exception as e: | |
| append_log(job_id, f"ERROR in background launch: {str(e)}") | |
| jobs = load_jobs() | |
| if job_id in jobs: | |
| jobs[job_id]["status"] = "FAILED" | |
| save_jobs(jobs) | |
| async def create_job( | |
| background_tasks: BackgroundTasks, | |
| script_text: str = Form(...), | |
| voice: str = Form("en-US-AnaNeural"), | |
| add_captions: Optional[str] = Form("false"), | |
| video_speed: Optional[str] = Form("1.0"), | |
| bgm_select: Optional[str] = Form(""), | |
| kaggle_user: str = Form("ikechukwuebiringa1"), | |
| kaggle_key: str = Form("KGAT_fc473ab2c166567756eac24217d1fbd2"), | |
| hf_repo: str = Form("Airpyk98/EpicSync-Dataset"), | |
| hf_token: str = Form(""), | |
| video: UploadFile = File(...), | |
| bg_music: Optional[UploadFile] = File(None) | |
| ): | |
| if not kaggle_key or "0f12d3a4" in kaggle_key: | |
| kaggle_key = "KGAT_fc473ab2c166567756eac24217d1fbd2" | |
| if not hf_repo or hf_repo.strip() == "": | |
| hf_repo = "Airpyk98/EpicSync-Dataset" | |
| if not hf_token or hf_token.strip() == "": | |
| hf_token = base64.b64decode("aGZfRkp2UHlJT09nblJOc1NSeldBdmtQb2lqYnBPcW1weHZiZg==").decode("ascii") | |
| job_id = f"epicsync_{int(time.time())}" | |
| kernel_id = f"{kaggle_user}/epicsync-standard-runner" | |
| staging = os.path.join(STAGING_DIR, job_id) | |
| os.makedirs(staging, exist_ok=True) | |
| video_path = os.path.join(staging, "input.mp4") | |
| with open(video_path, "wb") as f: | |
| f.write(await video.read()) | |
| bgm_path = "" | |
| bgm_repo_path = "" | |
| if bgm_select and bgm_select.strip() != "": | |
| bgm_repo_path = bgm_select.strip() | |
| elif bg_music and bg_music.filename: | |
| bgm_path = os.path.join(staging, "bg_music.mp3") | |
| with open(bgm_path, "wb") as f: | |
| f.write(await bg_music.read()) | |
| jobs = load_jobs() | |
| jobs[job_id] = { | |
| "id": job_id, | |
| "title": f"EpicSync Job {time.strftime('%H:%M:%S')}", | |
| "status": "STAGING", | |
| "progress": 15, | |
| "step_text": "Packaging input video & pushing to compute engine...", | |
| "script": script_text, | |
| "voice": voice, | |
| "slug": kernel_id, | |
| "created_at": time.time(), | |
| "logs": [f"[{time.strftime('%H:%M:%S')}] Job initialized."] | |
| } | |
| save_jobs(jobs) | |
| background_tasks.add_task( | |
| prepare_and_launch_standard_job, | |
| job_id, staging, video_path, bgm_path, bgm_repo_path, script_text, voice, str(add_captions), str(video_speed), | |
| kaggle_user, kaggle_key, hf_repo, hf_token, kernel_id | |
| ) | |
| return {"job_id": job_id, "status": "STAGING"} | |
| async def create_premium_job( | |
| background_tasks: BackgroundTasks, | |
| script_text: str = Form(...), | |
| voice: str = Form("en-US-AnaNeural"), | |
| aspect_ratio: str = Form("9:16"), | |
| add_captions: Optional[str] = Form("false"), | |
| video_speed: Optional[str] = Form("1.0"), | |
| bgm_select: Optional[str] = Form(""), | |
| kaggle_user: str = Form("ikechukwuebiringa1"), | |
| kaggle_key: str = Form("KGAT_fc473ab2c166567756eac24217d1fbd2"), | |
| hf_repo: str = Form("Airpyk98/EpicSync-Dataset"), | |
| hf_token: str = Form(""), | |
| image: UploadFile = File(...), | |
| bg_music: Optional[UploadFile] = File(None) | |
| ): | |
| if not kaggle_key or "0f12d3a4" in kaggle_key: | |
| kaggle_key = "KGAT_fc473ab2c166567756eac24217d1fbd2" | |
| if not hf_repo or hf_repo.strip() == "": | |
| hf_repo = "Airpyk98/EpicSync-Dataset" | |
| if not hf_token or hf_token.strip() == "": | |
| hf_token = base64.b64decode("aGZfRkp2UHlJT09nblJOc1NSeldBdmtQb2lqYnBPcW1weHZiZg==").decode("ascii") | |
| job_id = f"epicsync_premium_{int(time.time())}" | |
| kernel_id = f"{kaggle_user}/epicsync-premium-runner" | |
| staging = os.path.join(STAGING_DIR, job_id) | |
| os.makedirs(staging, exist_ok=True) | |
| image_path = os.path.join(staging, "input.png") | |
| with open(image_path, "wb") as f: | |
| f.write(await image.read()) | |
| bgm_path = "" | |
| bgm_repo_path = "" | |
| if bgm_select and bgm_select.strip() != "": | |
| bgm_repo_path = bgm_select.strip() | |
| elif bg_music and bg_music.filename: | |
| bgm_path = os.path.join(staging, "bg_music.mp3") | |
| with open(bgm_path, "wb") as f: | |
| f.write(await bg_music.read()) | |
| jobs = load_jobs() | |
| jobs[job_id] = { | |
| "id": job_id, | |
| "title": f"✨ Premium LTX-2.3 Job {time.strftime('%H:%M:%S')}", | |
| "status": "STAGING", | |
| "progress": 15, | |
| "step_text": f"Packaging {aspect_ratio} portrait image & provisioning LTX-2.3 3D compute engine...", | |
| "script": script_text, | |
| "voice": voice, | |
| "aspect_ratio": aspect_ratio, | |
| "slug": kernel_id, | |
| "mode": "premium", | |
| "created_at": time.time(), | |
| "logs": [f"[{time.strftime('%H:%M:%S')}] Premium LTX-2.3 Job initialized with {aspect_ratio} aspect ratio."] | |
| } | |
| save_jobs(jobs) | |
| background_tasks.add_task( | |
| prepare_and_launch_premium_job, | |
| job_id, staging, image_path, bgm_path, bgm_repo_path, script_text, voice, aspect_ratio, str(add_captions), str(video_speed), | |
| kaggle_user, kaggle_key, hf_repo, hf_token, kernel_id | |
| ) | |
| return {"job_id": job_id, "status": "STAGING"} | |
| def get_jobs(): | |
| return load_jobs() | |
| def cancel_job(job_id: str, kaggle_user: str = Form("ikechukwuebiringa1"), kaggle_key: str = Form("KGAT_fc473ab2c166567756eac24217d1fbd2")): | |
| jobs = load_jobs() | |
| if job_id not in jobs: | |
| raise HTTPException(status_code=404, detail="Job not found") | |
| slug = jobs[job_id].get("slug") | |
| if slug: | |
| env = setup_kaggle_auth(kaggle_user, kaggle_key) | |
| subprocess.run(f"kaggle kernels cancel {slug}", shell=True, env=env) | |
| jobs[job_id]["status"] = "CANCELLED" | |
| jobs[job_id]["progress"] = 0 | |
| jobs[job_id]["step_text"] = "Task cancelled by user." | |
| append_log(job_id, "Job explicitly cancelled by user.") | |
| save_jobs(jobs) | |
| return {"status": "CANCELLED"} | |
| def clear_logs(): | |
| jobs = load_jobs() | |
| # Keep successful runs or clear all logs per user preference | |
| jobs = {k: v for k, v in jobs.items() if v.get("status") == "RUNNING"} | |
| save_jobs(jobs) | |
| return {"status": "CLEARED"} | |
| def get_video(job_id: str): | |
| path = os.path.join(OUTPUTS_DIR, f"{job_id}.mp4") | |
| if os.path.exists(path): | |
| return FileResponse(path, media_type="video/mp4") | |
| raise HTTPException(status_code=404, detail="Video file not found") | |
| def list_bgm_files(hf_repo: str = "Airpyk98/EpicSync-Dataset", hf_token: str = ""): | |
| if not hf_repo or hf_repo.strip() == "": | |
| hf_repo = "Airpyk98/EpicSync-Dataset" | |
| if not hf_token or hf_token.strip() == "": | |
| hf_token = base64.b64decode("aGZfRkp2UHlJT09nblJOc1NSeldBdmtQb2lqYnBPcW1weHZiZg==").decode("ascii") | |
| try: | |
| from huggingface_hub import HfApi | |
| api = HfApi(token=hf_token) | |
| files = api.list_repo_files(repo_id=hf_repo, repo_type="dataset") | |
| bgm_files = [f for f in files if (f.startswith("bgm/") or f.startswith("inputs/")) and f.lower().endswith((".mp3", ".wav", ".m4a", ".aac", ".ogg"))] | |
| return {"status": "success", "files": sorted(bgm_files)} | |
| except Exception as e: | |
| return {"status": "error", "message": str(e), "files": []} | |
| async def upload_bgm_file( | |
| file: UploadFile = File(...), | |
| custom_name: str = Form(...), | |
| hf_repo: str = Form("Airpyk98/EpicSync-Dataset"), | |
| hf_token: str = Form("") | |
| ): | |
| if not hf_repo or hf_repo.strip() == "": | |
| hf_repo = "Airpyk98/EpicSync-Dataset" | |
| if not hf_token or hf_token.strip() == "": | |
| hf_token = base64.b64decode("aGZfRkp2UHlJT09nblJOc1NSeldBdmtQb2lqYnBPcW1weHZiZg==").decode("ascii") | |
| clean_name = re.sub(r'[^a-zA-Z0-9_-]', '_', custom_name.strip()) | |
| if not clean_name: | |
| clean_name = f"bgm_{int(time.time())}" | |
| ext = os.path.splitext(file.filename)[1] or ".mp3" | |
| repo_path = f"bgm/{clean_name}{ext}" | |
| temp_path = os.path.join(STAGING_DIR, f"temp_{clean_name}{ext}") | |
| with open(temp_path, "wb") as f: | |
| f.write(await file.read()) | |
| try: | |
| upload_to_hf_dataset(temp_path, hf_repo, repo_path, hf_token) | |
| if os.path.exists(temp_path): | |
| os.remove(temp_path) | |
| return {"status": "success", "repo_path": repo_path, "message": f"Successfully saved {clean_name} to dataset!"} | |
| except Exception as e: | |
| if os.path.exists(temp_path): | |
| os.remove(temp_path) | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| app.mount("/", StaticFiles(directory="static", html=True), name="static") | |