| """ |
| jumpcut.py |
| --------------------------------------- |
| Smart Jump Cut Engine (V8) |
| |
| Purpose: |
| - Remove silence and filler pauses |
| - Improve pacing for short-form video |
| - Optimize retention curve |
| - Create TikTok/Reels-style fast cuts |
| |
| Works fully on CPU (FFmpeg-based). |
| No GPU required. |
| """ |
|
|
| import subprocess |
| import os |
|
|
|
|
| |
| |
| |
|
|
| TEMP_SILENCE_FILE = "silence_detect.txt" |
| OUTPUT_FILE = "jumpcut_output.mp4" |
|
|
|
|
| |
| |
| |
|
|
| def detect_silence(video_path): |
| """ |
| Uses ffmpeg silencedetect to find pauses. |
| """ |
|
|
| cmd = [ |
| "ffmpeg", |
| "-i", video_path, |
| "-af", "silencedetect=noise=-30dB:d=0.4", |
| "-f", "null", |
| "-" |
| ] |
|
|
| result = subprocess.run(cmd, stderr=subprocess.PIPE, text=True) |
|
|
| return result.stderr |
|
|
|
|
| |
| |
| |
|
|
| def parse_silence(log): |
| """ |
| Extract silence start/end timestamps |
| """ |
|
|
| silences = [] |
|
|
| start = None |
|
|
| for line in log.split("\n"): |
|
|
| if "silence_start" in line: |
| try: |
| start = float(line.split("silence_start:")[1].strip()) |
| except: |
| continue |
|
|
| if "silence_end" in line and start is not None: |
| try: |
| end = float(line.split("silence_end:")[1].split("|")[0].strip()) |
| silences.append((start, end)) |
| start = None |
| except: |
| continue |
|
|
| return silences |
|
|
|
|
| |
| |
| |
|
|
| def build_filter(silences, duration): |
| """ |
| Converts silence ranges into ffmpeg trim filter |
| """ |
|
|
| if not silences: |
| return None |
|
|
| segments = [] |
| last_end = 0 |
|
|
| for start, end in silences: |
|
|
| if start > last_end: |
| segments.append((last_end, start)) |
|
|
| last_end = end |
|
|
| if last_end < duration: |
| segments.append((last_end, duration)) |
|
|
| filters = [] |
|
|
| for i, (start, end) in enumerate(segments): |
| filters.append( |
| f"[0:v]trim=start={start}:end={end},setpts=PTS-STARTPTS[v{i}];" |
| f"[0:a]atrim=start={start}:end={end},asetpts=PTS-STARTPTS[a{i}]" |
| ) |
|
|
| video_concat = "".join([f"[v{i}]" for i in range(len(segments))]) |
| audio_concat = "".join([f"[a{i}]" for i in range(len(segments))]) |
|
|
| filters.append( |
| f"{video_concat}{audio_concat}concat=n={len(segments)}:v=1:a=1[outv][outa]" |
| ) |
|
|
| return ";".join(filters) |
|
|
|
|
| |
| |
| |
|
|
| def smart_jumpcut(video_path): |
| """ |
| Main jump cut engine |
| """ |
|
|
| print("[JUMPCUT] Analyzing video...") |
|
|
| |
| log = detect_silence(video_path) |
|
|
| silences = parse_silence(log) |
|
|
| print(f"[JUMPCUT] Detected silences: {len(silences)}") |
|
|
| |
| probe_cmd = [ |
| "ffprobe", |
| "-v", "error", |
| "-show_entries", |
| "format=duration", |
| "-of", |
| "default=noprint_wrappers=1:nokey=1", |
| video_path |
| ] |
|
|
| duration = float(subprocess.check_output(probe_cmd).decode().strip()) |
|
|
| |
| filter_complex = build_filter(silences, duration) |
|
|
| if not filter_complex: |
| print("[JUMPCUT] No silences found, returning original") |
| return video_path |
|
|
| |
| output_path = OUTPUT_FILE |
|
|
| cmd = [ |
| "ffmpeg", "-y", |
| "-i", video_path, |
| "-filter_complex", filter_complex, |
| "-map", "[outv]", |
| "-map", "[outa]", |
| "-c:v", "libx264", |
| "-preset", "ultrafast", |
| "-c:a", "aac", |
| output_path |
| ] |
|
|
| print("[JUMPCUT] Rendering optimized video...") |
|
|
| subprocess.run(cmd, check=True) |
|
|
| print("[JUMPCUT] Done:", output_path) |
|
|
| return output_path |
|
|
|
|
| |
| |
| |
|
|
| def fast_jumpcut(video_path): |
| """ |
| Lightweight fallback: |
| removes only large pauses quickly |
| """ |
|
|
| output = "fast_jumpcut.mp4" |
|
|
| cmd = [ |
| "ffmpeg", "-y", |
| "-i", video_path, |
| "-af", "silenceremove=start_periods=1:start_threshold=-30dB:stop_periods=-1", |
| "-c:v", "libx264", |
| "-preset", "ultrafast", |
| "-c:a", "aac", |
| output |
| ] |
|
|
| subprocess.run(cmd, check=True) |
|
|
| return output |
|
|
|
|
| |
| |
| |
|
|
| def smart_jumpcut_engine(video_path, mode="smart"): |
| """ |
| Entry point used by main.py |
| """ |
|
|
| if mode == "fast": |
| return fast_jumpcut(video_path) |
|
|
| return smart_jumpcut(video_path) |