{ "cells": [ { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-06-28T07:40:53.279666Z", "iopub.status.busy": "2026-06-28T07:40:53.278927Z", "iopub.status.idle": "2026-06-28T07:41:45.477803Z", "shell.execute_reply": "2026-06-28T07:41:45.477123Z", "shell.execute_reply.started": "2026-06-28T07:40:53.279624Z" }, "trusted": true }, "outputs": [], "source": [ "#Multi GPU Usage code (with fade in/out on final video)\n", "# === DEBUG: List all mounted Kaggle datasets ===\n", "import os as _dbg_os\n", "print(\"=== Mounted Kaggle Datasets ===\")\n", "for _dbg_d in sorted(_dbg_os.listdir(\"/kaggle/input/\")):\n", " _dbg_p = _dbg_os.path.join(\"/kaggle/input/\", _dbg_d)\n", " if _dbg_os.path.isdir(_dbg_p):\n", " _dbg_files = _dbg_os.listdir(_dbg_p)\n", " print(f\" /kaggle/input/{_dbg_d}/ ({len(_dbg_files)} files)\")\n", " for _dbg_f in sorted(_dbg_files):\n", " print(f\" {_dbg_f}\")\n", "print(\"=\" * 40)\n", "# === END DEBUG ===\n", "import subprocess, os, sys, re, threading, time, json\n", "\n", "def load_crop_resize_from_config(config_path):\n", " \"\"\"\n", " Reads crop_config.json (as produced by preview_cropper.py) and converts:\n", " - computed_crop_region -> decoder -crop (top x bottom x left x right)\n", " - cover_zoom_transform -> decoder -resize (zoomed WxH) PLUS the\n", " centered offset needed to crop that\n", " zoomed frame back down to the ORIGINAL\n", " (final) width x height.\n", " Returns None if no config_path given, or if file doesn't exist.\n", " \"\"\"\n", " if not config_path or not os.path.exists(config_path):\n", " return None\n", "\n", " with open(config_path, \"r\") as f:\n", " cfg = json.load(f)\n", "\n", " meta = cfg[\"video_metadata\"]\n", " src_w, src_h = meta[\"width\"], meta[\"height\"]\n", "\n", " region = cfg[\"computed_crop_region\"]\n", " x1, y1, x2, y2 = region[\"x1\"], region[\"y1\"], region[\"x2\"], region[\"y2\"]\n", "\n", " crop_top = y1\n", " crop_bottom = src_h - y2\n", " crop_left = x1\n", " crop_right = src_w - x2\n", "\n", " zoom = cfg[\"cover_zoom_transform\"]\n", " resize_w = zoom[\"zoomed_width\"]\n", " resize_h = zoom[\"zoomed_height\"]\n", " center_offset_x = zoom.get(\"center_offset_x\", 0)\n", " center_offset_y = zoom.get(\"center_offset_y\", 0)\n", "\n", " print(\"🧩 Loaded crop/resize from config.json:\")\n", " print(f\" Source resolution: {src_w}x{src_h}\")\n", " print(f\" Crop region: x1={x1} y1={y1} x2={x2} y2={y2}\")\n", " print(f\" -> decoder crop (top:bottom:left:right) = {crop_top}x{crop_bottom}x{crop_left}x{crop_right}\")\n", " print(f\" -> decoder resize (zoomed/cover) target = {resize_w}x{resize_h}\")\n", " print(f\" -> final center-crop back to {src_w}x{src_h} at offset ({center_offset_x}, {center_offset_y})\")\n", "\n", " return {\n", " \"crop_top\": crop_top,\n", " \"crop_bottom\": crop_bottom,\n", " \"crop_left\": crop_left,\n", " \"crop_right\": crop_right,\n", " \"resize_w\": resize_w,\n", " \"resize_h\": resize_h,\n", " \"center_offset_x\": center_offset_x,\n", " \"center_offset_y\": center_offset_y,\n", " \"final_width\": src_w,\n", " \"final_height\": src_h,\n", " \"filename\": cfg.get(\"filename\"),\n", " \"task_id\": cfg.get(\"task_id\"),\n", " }\n", "\n", "# Map ffprobe codec_name -> matching CUVID decoder\n", "CUVID_MAP = {\n", " \"h264\": \"h264_cuvid\",\n", " \"hevc\": \"hevc_cuvid\",\n", " \"vp9\": \"vp9_cuvid\",\n", " \"vp8\": \"vp8_cuvid\",\n", " \"mpeg2video\": \"mpeg2_cuvid\",\n", " \"mpeg4\": \"mpeg4_cuvid\",\n", " \"vc1\": \"vc1_cuvid\",\n", " \"av1\": \"av1_cuvid\",\n", "}\n", "\n", "# Decoders that support the cuvid -crop / -resize options.\n", "CROP_RESIZE_CAPABLE_DECODERS = {\"h264_cuvid\", \"hevc_cuvid\", \"vp9_cuvid\"}\n", "\n", "def detect_codec(path):\n", " out = subprocess.run(\n", " [\"ffprobe\", \"-v\", \"error\", \"-select_streams\", \"v:0\",\n", " \"-show_entries\", \"stream=codec_name\", \"-of\", \"csv=p=0\", path],\n", " stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True\n", " )\n", " codec = out.stdout.strip()\n", " if not codec:\n", " raise RuntimeError(f\"Could not detect codec. ffprobe stderr: {out.stderr}\")\n", " return codec\n", "\n", "def get_duration_seconds(path):\n", " out = subprocess.run(\n", " [\"ffprobe\", \"-v\", \"error\", \"-show_entries\", \"format=duration\",\n", " \"-of\", \"csv=p=0\", path],\n", " stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True\n", " )\n", " dur_str = out.stdout.strip()\n", " if not dur_str:\n", " raise RuntimeError(f\"Could not detect duration. ffprobe stderr: {out.stderr}\")\n", " return float(dur_str)\n", "\n", "def check_gpus():\n", " out = subprocess.run([\"nvidia-smi\", \"-L\"], stdout=subprocess.PIPE, text=True)\n", " print(out.stdout.strip())\n", " n = out.stdout.strip().count(\"GPU \")\n", " if n < 2:\n", " print(\"⚠️ Less than 2 GPUs detected β€” both jobs (if split) will run concurrently on GPU0.\")\n", " return n\n", "\n", "PROGRESS_RE = re.compile(\n", " r\"frame=\\s*(\\d+).*fps=\\s*([\\d.]+).*time=(\\S+).*?(?:dup=(\\d+))?.*?(?:drop=(\\d+))?.*speed=\\s*([\\d.]+)x\"\n", ")\n", "\n", "shared_state = {\"GPU0\": \"starting...\", \"GPU1\": \"starting...\"}\n", "state_lock = threading.Lock()\n", "\n", "def render_combined_line():\n", " line = f\"\\r[GPU0] {shared_state['GPU0']} || [GPU1] {shared_state['GPU1']}\"\n", " sys.stdout.write(line.ljust(180))\n", " sys.stdout.flush()\n", "\n", "def stream_progress(proc, tag, log_lines):\n", " \"\"\"Read ffmpeg stderr line/chunk-wise (not char-by-char) to avoid pipe backpressure\n", " that can stall the encoder itself while waiting on Python to drain stderr.\"\"\"\n", " buf = \"\"\n", " while True:\n", " chunk = proc.stderr.read(4096)\n", " if not chunk:\n", " break\n", " buf += chunk\n", " while True:\n", " idx_r = buf.find(\"\\r\")\n", " idx_n = buf.find(\"\\n\")\n", " idx = min([i for i in (idx_r, idx_n) if i != -1], default=-1)\n", " if idx == -1:\n", " break\n", " line = buf[:idx].strip()\n", " buf = buf[idx+1:]\n", " if line:\n", " log_lines.append(line)\n", " m = PROGRESS_RE.search(line)\n", " if m:\n", " frame, fps, t, dup, drop, speed = m.groups()\n", " dup = dup or \"0\"\n", " drop = drop or \"0\"\n", " flag = \" ⚠️DROP!\" if int(drop) > 0 else \"\"\n", " with state_lock:\n", " shared_state[tag] = f\"frame={frame} fps={fps} time={t} dup={dup} drop={drop}{flag} speed={speed}x\"\n", " render_combined_line()\n", " if buf.strip():\n", " log_lines.append(buf.strip())\n", "\n", "def run_with_progress(cmd, tag):\n", " print(f\"\\n▢️ Starting {tag}: {' '.join(cmd)}\\n\")\n", " p = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True, bufsize=1)\n", " log_lines = []\n", " t = threading.Thread(target=stream_progress, args=(p, tag, log_lines))\n", " t.start()\n", " return p, t, log_lines\n", "\n", "# Preset is fixed to p3 (better quality) for all encode jobs, regardless of GPU count.\n", "PRESET = \"p3\"\n", "\n", "# Bitrate ceiling to prevent -cq from ballooning file size on complex/high-motion scenes.\n", "MAXRATE = \"30M\"\n", "BUFSIZE = \"60M\"\n", "\n", "# If video is longer than this, split into 2 parts and process in parallel.\n", "# If video is this length or shorter, process as a single job (no split).\n", "SPLIT_THRESHOLD_SEC = 25 * 60 # 25 minutes\n", "\n", "# ---------------- Fade in/out config (applied to the FINAL video only) ----------------\n", "FADE_DURATION_SEC = 1.5 # length of each fade, in seconds. Bump to 2.0 for a more noticeable fade.\n", "\n", "# ---------------- Intro / Outro config ----------------\n", "# If BOTH files exist, they are stitched onto the final video using a fast\n", "# stream-copy concat (-c copy) β€” no re-encode, near-zero extra time.\n", "# In that case, fade-in/out is SKIPPED (intro/outro replaces the need for it).\n", "# If either file is missing, we fall back to fade-in/out as before.\n", "INTRO_PATH = \"/kaggle/input/datasets/joymant/intro-outro/intro.mp4\"\n", "OUTRO_PATH = \"/kaggle/input/datasets/joymant/intro-outro/outro.mp4\"\n", "# NOTE: MAIN_ONLY_PATH and INTRO_OUTRO_LIST_PATH are defined further below,\n", "# right after TEMP_DIR is created.\n", "\n", "\n", "def has_intro_outro():\n", " found = os.path.exists(INTRO_PATH) and os.path.exists(OUTRO_PATH)\n", " if found:\n", " print(f\"🎬 Intro + Outro detected:\\n intro: {INTRO_PATH}\\n outro: {OUTRO_PATH}\")\n", " print(\" -> Will stitch via fast stream-copy concat (-c copy). Fade-in/out will be SKIPPED.\")\n", " else:\n", " print(\"🎬 Intro/Outro not found (one or both missing) -> falling back to fade-in/out.\")\n", " return found\n", "\n", "\n", "def stitch_intro_outro(main_video_path, final_output_path):\n", " \"\"\"\n", " Fast, copy-only concat: intro.mp4 + main_video_path + outro.mp4 -> final_output_path.\n", " No re-encoding happens here, so this adds negligible time regardless of video length.\n", " NOTE: for stream-copy concat to work cleanly, intro/outro should already match the\n", " main video's codec/resolution/fps/pixel format. If they don't, ffmpeg's concat demuxer\n", " may still produce a playable file, but mismatches won't be auto-corrected since we're\n", " deliberately avoiding re-encode here for speed.\n", " \"\"\"\n", " print(\"πŸ”— Stitching intro + main + outro (stream copy, no re-encode)...\")\n", " with open(INTRO_OUTRO_LIST_PATH, \"w\") as f:\n", " f.write(f\"file '{INTRO_PATH}'\\nfile '{main_video_path}'\\nfile '{OUTRO_PATH}'\")\n", "\n", " concat_cmd = [\"ffmpeg\", \"-y\", \"-f\", \"concat\", \"-safe\", \"0\", \"-i\", INTRO_OUTRO_LIST_PATH,\n", " \"-c\", \"copy\", final_output_path]\n", " res = subprocess.run(concat_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n", " if res.returncode != 0:\n", " print(res.stdout)\n", " raise RuntimeError(\"intro/outro stitch failed β€” check that intro/outro codec/resolution/fps \"\n", " \"match the main video (stream copy requires matching parameters).\")\n", "\n", " for f in (MAIN_ONLY_PATH, INTRO_OUTRO_LIST_PATH):\n", " if os.path.exists(f):\n", " os.remove(f)\n", " print(\"βœ… Intro/outro stitched successfully (fast copy, no quality loss, no re-encode time).\")\n", "\n", "# Kaggle directories:\n", "# /kaggle/working -> downloaded by `kaggle kernels output` (KEEP ONLY FINAL FILE HERE)\n", "# /kaggle/temp -> NOT downloaded by kernels output, safe for intermediates (part1.mp4, part2.mp4, list.txt)\n", "WORKING_DIR = \"/kaggle/working\"\n", "TEMP_DIR = \"/kaggle/temp\"\n", "\n", "os.makedirs(WORKING_DIR, exist_ok=True)\n", "os.makedirs(TEMP_DIR, exist_ok=True)\n", "\n", "FINAL_OUTPUT_PATH = os.path.join(WORKING_DIR, \"final_output_4k.mp4\")\n", "PART1_PATH = os.path.join(TEMP_DIR, \"part1.mp4\")\n", "PART2_PATH = os.path.join(TEMP_DIR, \"part2.mp4\")\n", "LIST_TXT_PATH = os.path.join(TEMP_DIR, \"list.txt\")\n", "MAIN_ONLY_PATH = os.path.join(TEMP_DIR, \"main_only.mp4\")\n", "INTRO_OUTRO_LIST_PATH = os.path.join(TEMP_DIR, \"intro_outro_list.txt\")\n", "\n", "# ---------------- Logo overlay config ----------------\n", "LOGO_LEFT_PATH = \"/kaggle/input/datasets/joymant/logo-datasets/logo1.png\"\n", "LOGO_RIGHT_PATH = \"/kaggle/input/datasets/joymant/logo-datasets/logo2.png\"\n", "\n", "# Reference geometry measured on a 1920x1080 (1080p) canvas:\n", "REF_WIDTH = 1920\n", "REF_LOGO_SIZE = 137 # logo box is 137x137 at 1080p\n", "REF_MARGIN_LEFT = 22 # left logo: distance from left edge\n", "REF_MARGIN_TOP = 18 # both logos: distance from top edge\n", "REF_MARGIN_RIGHT = 22 # right logo: distance from right edge (mirrors left margin)\n", "\n", "\n", "def get_video_resolution(path):\n", " out = subprocess.run(\n", " [\"ffprobe\", \"-v\", \"error\", \"-select_streams\", \"v:0\",\n", " \"-show_entries\", \"stream=width,height\", \"-of\", \"csv=p=0:s=x\", path],\n", " stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True\n", " )\n", " res = out.stdout.strip()\n", " if not res or \"x\" not in res:\n", " raise RuntimeError(f\"Could not detect resolution. ffprobe stderr: {out.stderr}\")\n", " w, h = res.split(\"x\")\n", " return int(w), int(h)\n", "\n", "\n", "def compute_logo_geometry(target_w, target_h):\n", " \"\"\"\n", " Scales the reference 1080p logo geometry to any output resolution,\n", " keeping it proportional (based on width ratio vs the 1920px reference).\n", " Returns dict with logo size + x/y positions for both logos, all even numbers\n", " (required by some encoders/filters for clean scaling).\n", " \"\"\"\n", " scale = target_w / REF_WIDTH\n", "\n", " def even(n):\n", " n = int(round(n))\n", " return n - (n % 2)\n", "\n", " logo_size = even(REF_LOGO_SIZE * scale)\n", " margin_left = even(REF_MARGIN_LEFT * scale)\n", " margin_top = even(REF_MARGIN_TOP * scale)\n", " margin_right = even(REF_MARGIN_RIGHT * scale)\n", "\n", " left_x = margin_left\n", " left_y = margin_top\n", " right_x = target_w - margin_right - logo_size\n", " right_y = margin_top\n", "\n", " print(f\"πŸ–ΌοΈ Logo geometry for {target_w}x{target_h} (scale={scale:.3f}):\")\n", " print(f\" logo size: {logo_size}x{logo_size}\")\n", " print(f\" left logo position : x={left_x} y={left_y}\")\n", " print(f\" right logo position: x={right_x} y={right_y}\")\n", "\n", " return {\n", " \"size\": logo_size,\n", " \"left_x\": left_x, \"left_y\": left_y,\n", " \"right_x\": right_x, \"right_y\": right_y,\n", " }\n", "\n", "\n", "def build_logo_filter_complex(geo, base_video_label=\"0:v\",\n", " fade_in=False, fade_out_start=None,\n", " fade_dur=FADE_DURATION_SEC,\n", " final_crop=None):\n", " \"\"\"\n", " Builds the -filter_complex string + extra ffmpeg input args needed to\n", " overlay both logos on a CUDA-decoded video, optionally apply fade-in/out,\n", " then re-upload to CUDA for NVENC.\n", "\n", " fade_in : True -> add a fade-in starting at t=0\n", " fade_out_start : float seconds (relative to THIS job's own clip, not the\n", " original full video) -> add a fade-out starting there.\n", " None -> no fade-out on this job.\n", " fade_dur : duration of each fade in seconds.\n", " final_crop : optional dict {\"w\", \"h\", \"x\", \"y\"}. When the decoder's\n", " -resize produced a \"cover zoom\" frame that's LARGER than\n", " the true final canvas (e.g. 1920x1252 zoomed vs the real\n", " 1920x1080 target), this crops it back down to w x h at\n", " the centered offset (x, y) BEFORE logos are overlaid, so\n", " logo placement and final resolution both land on the\n", " real target canvas instead of the intermediate zoom size.\n", "\n", " Returns (extra_input_args, filter_complex_str, output_video_label)\n", " \"\"\"\n", " size = geo[\"size\"]\n", " extra_inputs = [\"-i\", LOGO_LEFT_PATH, \"-i\", LOGO_RIGHT_PATH]\n", "\n", " fade_str = \"\"\n", " if fade_in:\n", " fade_str += f\"fade=t=in:st=0:d={fade_dur},\"\n", " if fade_out_start is not None:\n", " fade_str += f\"fade=t=out:st={max(0, fade_out_start)}:d={fade_dur},\"\n", "\n", " crop_part = \"\"\n", " if final_crop:\n", " crop_part = f\",crop={final_crop['w']}:{final_crop['h']}:{final_crop['x']}:{final_crop['y']}\"\n", "\n", " filter_complex = (\n", " f\"[{base_video_label}]hwdownload,format=nv12{crop_part}[mainsw];\"\n", " f\"[1:v]scale={size}:{size}[logoL];\"\n", " f\"[2:v]scale={size}:{size}[logoR];\"\n", " f\"[mainsw][logoL]overlay=x={geo['left_x']}:y={geo['left_y']}[tmp1];\"\n", " f\"[tmp1][logoR]overlay=x={geo['right_x']}:y={geo['right_y']}[tmp2];\"\n", " f\"[tmp2]{fade_str}hwupload_cuda[vout]\"\n", " )\n", " return extra_inputs, filter_complex, \"[vout]\"\n", "\n", "\n", "def parallel_dual_gpu_safe(config_path=None, input_video_override=None):\n", " overall_start = time.time()\n", " input_video = \"/kaggle/input/20260629-172948-b2177c/output_20260629_172948_b2177c.mkv\"\n", "\n", " n_gpus = check_gpus()\n", "\n", " # ---- Decide intro/outro vs fade-in/out up front ----\n", " use_intro_outro = has_intro_outro()\n", " use_fade = not use_intro_outro\n", "\n", " # ---- Optional crop/resize from config.json (decoder-level GPU crop+resize) ----\n", " crop_resize = load_crop_resize_from_config(config_path)\n", " crop_resize_args = []\n", " final_crop = None # set below if the decoder's cover-zoom overshoots the real canvas\n", " if crop_resize:\n", " ct, cb, cl, cr = (crop_resize[\"crop_top\"], crop_resize[\"crop_bottom\"],\n", " crop_resize[\"crop_left\"], crop_resize[\"crop_right\"])\n", " if ct or cb or cl or cr:\n", " crop_resize_args += [\"-crop\", f\"{ct}x{cb}x{cl}x{cr}\"]\n", " if crop_resize[\"resize_w\"] and crop_resize[\"resize_h\"]:\n", " rw = crop_resize[\"resize_w\"] - (crop_resize[\"resize_w\"] % 2)\n", " rh = crop_resize[\"resize_h\"] - (crop_resize[\"resize_h\"] % 2)\n", " if (rw, rh) != (crop_resize[\"resize_w\"], crop_resize[\"resize_h\"]):\n", " print(f\"⚠️ Resize target {crop_resize['resize_w']}x{crop_resize['resize_h']} has odd dimension(s) \"\n", " f\"(NVENC requires even) β€” rounding down to {rw}x{rh}\")\n", " crop_resize_args += [\"-resize\", f\"{rw}x{rh}\"]\n", "\n", " fw, fh = crop_resize[\"final_width\"], crop_resize[\"final_height\"]\n", " if (rw, rh) != (fw, fh):\n", " final_crop = {\n", " \"w\": fw, \"h\": fh,\n", " \"x\": crop_resize[\"center_offset_x\"],\n", " \"y\": crop_resize[\"center_offset_y\"],\n", " }\n", " print(f\"βœ‚οΈ Decoder resize ({rw}x{rh}) overshoots the final canvas ({fw}x{fh}) β€” \"\n", " f\"will center-crop back to {fw}x{fh} at offset \"\n", " f\"({final_crop['x']}, {final_crop['y']}) before overlaying logos.\")\n", "\n", " if input_video_override:\n", " input_video = input_video_override\n", " print(f\"πŸ“ Using EXPLICIT input override (ignoring config's filename): {input_video}\")\n", " elif crop_resize.get(\"filename\"):\n", " candidate = os.path.join(os.path.dirname(config_path), crop_resize[\"filename\"])\n", " if os.path.exists(candidate):\n", " input_video = candidate\n", " print(f\"πŸ“ Using input from config: {input_video}\")\n", " elif input_video_override:\n", " input_video = input_video_override\n", " print(f\"πŸ“ Using EXPLICIT input override: {input_video}\")\n", "\n", " codec = detect_codec(input_video)\n", " decoder = CUVID_MAP.get(codec)\n", " if decoder is None:\n", " raise RuntimeError(f\"No CUVID decoder mapping for detected codec '{codec}'. \"\n", " f\"Known: {list(CUVID_MAP)}\")\n", " print(f\"🎯 Detected input codec: {codec} -> using decoder: {decoder}\")\n", "\n", " if crop_resize_args and decoder not in CROP_RESIZE_CAPABLE_DECODERS:\n", " print(f\"⚠️ -crop/-resize decoder options are only supported for {sorted(CROP_RESIZE_CAPABLE_DECODERS)}. \"\n", " f\"Detected decoder '{decoder}' β€” crop/resize will be SKIPPED.\")\n", " crop_resize_args = []\n", " final_crop = None\n", "\n", " duration = get_duration_seconds(input_video)\n", " print(f\"πŸ•’ Video duration: {duration/60:.2f} min\")\n", "\n", " # ---- Determine actual FINAL output resolution for logo scaling ----\n", " # This must be the true final canvas (post center-crop-back), not the\n", " # intermediate \"cover zoom\" resize size, otherwise logos get placed\n", " # relative to a frame that no longer exists in the output.\n", " if crop_resize and crop_resize.get(\"final_width\") and crop_resize.get(\"final_height\"):\n", " target_w, target_h = crop_resize[\"final_width\"], crop_resize[\"final_height\"]\n", " else:\n", " target_w, target_h = get_video_resolution(input_video)\n", " logo_geo = compute_logo_geometry(target_w, target_h)\n", "\n", " # ---- Case A: short video β€” single job, no split ----\n", " if duration <= SPLIT_THRESHOLD_SEC:\n", " print(f\"πŸ“¦ Video ≀ {SPLIT_THRESHOLD_SEC/60:.0f} min β€” processing as a SINGLE job (no split) on GPU0\")\n", " encode_target = MAIN_ONLY_PATH if use_intro_outro else FINAL_OUTPUT_PATH\n", " if use_fade:\n", " print(f\"🎬 Applying fade-in at t=0 and fade-out at t={max(0, duration - FADE_DURATION_SEC):.2f}s \"\n", " f\"(duration={FADE_DURATION_SEC}s each)\")\n", " extra_inputs, filter_complex, vlabel = build_logo_filter_complex(\n", " logo_geo, \"0:v\",\n", " fade_in=use_fade,\n", " fade_out_start=(duration - FADE_DURATION_SEC) if use_fade else None,\n", " final_crop=final_crop,\n", " )\n", " cmd = [\n", " \"ffmpeg\", \"-y\",\n", " \"-hwaccel\", \"cuda\", \"-hwaccel_device\", \"0\", \"-hwaccel_output_format\", \"cuda\",\n", " \"-c:v\", decoder,\n", " ] + crop_resize_args + [\n", " \"-i\", input_video,\n", " ] + extra_inputs + [\n", " \"-filter_complex\", filter_complex,\n", " \"-map\", vlabel, \"-map\", \"0:a?\",\n", " \"-c:v\", \"hevc_nvenc\", \"-gpu\", \"0\", \"-preset\", PRESET, \"-cq\", \"18\",\n", " \"-maxrate\", MAXRATE, \"-bufsize\", BUFSIZE,\n", " \"-c:a\", \"copy\",\n", " encode_target\n", " ]\n", " start_time = time.time()\n", " p, t, log = run_with_progress(cmd, \"GPU0\")\n", " p.wait(); t.join()\n", " print()\n", " if p.returncode != 0:\n", " print(\"\\n\".join(log[-15:]))\n", " raise RuntimeError(\"single job failed\")\n", " elapsed = time.time() - start_time\n", " print(f\"\\n⏱️ Job finished in {elapsed/60:.2f} min ({elapsed:.1f} sec)\")\n", "\n", " if use_intro_outro:\n", " stitch_intro_outro(MAIN_ONLY_PATH, FINAL_OUTPUT_PATH)\n", "\n", " print(f\"πŸ“¦ Final video saved at: {FINAL_OUTPUT_PATH}\")\n", " total_elapsed = time.time() - overall_start\n", " print(f\"🏁 TOTAL TIME TAKEN: {total_elapsed/60:.2f} min ({total_elapsed:.1f} sec)\")\n", " return\n", "\n", " # ---- Case B: long video (> 25 min) β€” split into 2 parts at the exact midpoint ----\n", " split_seconds = duration / 2\n", " split_point = time.strftime(\"%H:%M:%S\", time.gmtime(split_seconds))\n", " print(f\"βœ‚οΈ Video > {SPLIT_THRESHOLD_SEC/60:.0f} min β€” splitting at exact midpoint: {split_point} \"\n", " f\"(frame-accurate, GPU re-encode)\")\n", "\n", " # part2's own (independent) duration, needed to place its fade-out correctly\n", " part2_duration = duration - split_seconds\n", " if use_fade:\n", " print(f\"🎬 Fade-in (t=0) will be applied to part1 only. \"\n", " f\"Fade-out (t={max(0, part2_duration - FADE_DURATION_SEC):.2f}s within part2) will be applied to part2 only. \"\n", " f\"No fade at the mid-video join.\")\n", " else:\n", " print(\"🎬 Intro/outro mode active for this video β€” no fade will be applied to part1/part2.\")\n", "\n", " gpu0_id = \"0\"\n", " gpu1_id = \"1\" if n_gpus >= 2 else \"0\"\n", " tag2 = \"GPU1\" if n_gpus >= 2 else \"GPU0\"\n", "\n", " # part1: fade-in only (start of final video) when in fade mode. No fade-out β€” mid-video cut.\n", " extra_inputs1, filter_complex1, vlabel1 = build_logo_filter_complex(\n", " logo_geo, \"0:v\",\n", " fade_in=use_fade,\n", " fade_out_start=None,\n", " final_crop=final_crop,\n", " )\n", " split1 = [\n", " \"ffmpeg\", \"-y\",\n", " \"-hwaccel\", \"cuda\", \"-hwaccel_device\", gpu0_id, \"-hwaccel_output_format\", \"cuda\",\n", " \"-c:v\", decoder,\n", " ] + crop_resize_args + [\n", " \"-i\", input_video,\n", " \"-t\", split_point,\n", " ] + extra_inputs1 + [\n", " \"-filter_complex\", filter_complex1,\n", " \"-map\", vlabel1, \"-map\", \"0:a?\",\n", " \"-c:v\", \"hevc_nvenc\", \"-gpu\", gpu0_id, \"-preset\", PRESET, \"-cq\", \"18\",\n", " \"-maxrate\", MAXRATE, \"-bufsize\", BUFSIZE,\n", " \"-c:a\", \"copy\",\n", " PART1_PATH\n", " ]\n", "\n", " # part2: fade-out only (end of final video) when in fade mode, timed against part2's OWN duration.\n", " # No fade-in β€” this is a mid-video cut continuing from part1.\n", " extra_inputs2, filter_complex2, vlabel2 = build_logo_filter_complex(\n", " logo_geo, \"0:v\",\n", " fade_in=False,\n", " fade_out_start=(part2_duration - FADE_DURATION_SEC) if use_fade else None,\n", " final_crop=final_crop,\n", " )\n", " split2 = [\n", " \"ffmpeg\", \"-y\",\n", " \"-hwaccel\", \"cuda\", \"-hwaccel_device\", gpu1_id, \"-hwaccel_output_format\", \"cuda\",\n", " \"-c:v\", decoder,\n", " ] + crop_resize_args + [\n", " \"-ss\", split_point, \"-i\", input_video,\n", " ] + extra_inputs2 + [\n", " \"-filter_complex\", filter_complex2,\n", " \"-map\", vlabel2, \"-map\", \"0:a?\",\n", " \"-c:v\", \"hevc_nvenc\", \"-gpu\", gpu1_id, \"-preset\", PRESET, \"-cq\", \"18\",\n", " \"-maxrate\", MAXRATE, \"-bufsize\", BUFSIZE,\n", " \"-c:a\", \"copy\",\n", " PART2_PATH\n", " ]\n", "\n", " start_time = time.time()\n", " p1, t1, log1 = run_with_progress(split1, \"GPU0\")\n", " p2, t2, log2 = run_with_progress(split2, tag2)\n", "\n", " p1.wait(); t1.join()\n", " p2.wait(); t2.join()\n", " print() # move past the live-updating line\n", "\n", " if p1.returncode != 0:\n", " print(\"\\n\".join(log1[-15:])); raise RuntimeError(\"part1 split failed\")\n", " if p2.returncode != 0:\n", " print(\"\\n\".join(log2[-15:])); raise RuntimeError(\"part2 split failed\")\n", "\n", " elapsed = time.time() - start_time\n", " print(f\"\\n⏱️ Both GPU jobs finished in {elapsed/60:.2f} min ({elapsed:.1f} sec)\")\n", "\n", " # ---- Concatenate (stream copy, identical params so no quality loss) ----\n", " print(\"πŸ”— Merging part1 + part2...\")\n", " with open(LIST_TXT_PATH, \"w\") as f:\n", " f.write(f\"file '{PART1_PATH}'\\nfile '{PART2_PATH}'\")\n", "\n", " merge_target = MAIN_ONLY_PATH if use_intro_outro else FINAL_OUTPUT_PATH\n", " concat_cmd = [\"ffmpeg\", \"-y\", \"-f\", \"concat\", \"-safe\", \"0\", \"-i\", LIST_TXT_PATH,\n", " \"-c\", \"copy\", merge_target]\n", " res = subprocess.run(concat_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n", " if res.returncode != 0:\n", " print(res.stdout)\n", " raise RuntimeError(\"concat failed\")\n", "\n", " # Clean up intermediates (they live in /kaggle/temp anyway, so this is just tidiness)\n", " for f in (PART1_PATH, PART2_PATH, LIST_TXT_PATH):\n", " if os.path.exists(f):\n", " os.remove(f)\n", "\n", " if use_intro_outro:\n", " stitch_intro_outro(MAIN_ONLY_PATH, FINAL_OUTPUT_PATH)\n", "\n", " print(\"βœ… Done! Frame-accurate split, full GPU pipeline, merged\" +\n", " (\", intro/outro stitched.\" if use_intro_outro else \", fade-in/out applied.\"))\n", " print(f\"πŸ“¦ Final video saved at: {FINAL_OUTPUT_PATH}\")\n", " total_elapsed = time.time() - overall_start\n", " print(f\"🏁 TOTAL TIME TAKEN: {total_elapsed/60:.2f} min ({total_elapsed:.1f} sec)\")\n", "\n", "if __name__ == \"__main__\":\n", " CONFIG_PATH = \"/kaggle/input/datasets/joymant/20260629-172948-b2177c/crop_config.json\"\n", " INPUT_VIDEO_OVERRIDE = \"/kaggle/input/datasets/joymant/20260629-172948-b2177c/output_20260629_172948_b2177c.mkv\"\n", " parallel_dual_gpu_safe(config_path=CONFIG_PATH, input_video_override=INPUT_VIDEO_OVERRIDE)" ] } ], "metadata": { "kaggle": { "accelerator": "none", "dataSources": [], "dockerImageVersionId": 28755, "isGpuEnabled": false, "isInternetEnabled": false, "language": "python", "sourceType": "notebook" }, "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.13" } }, "nbformat": 4, "nbformat_minor": 4 }