Spaces:
Running
Running
| """ | |
| 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 | |
| # ===================================================== | |
| # CONFIG | |
| # ===================================================== | |
| TEMP_SILENCE_FILE = "silence_detect.txt" | |
| OUTPUT_FILE = "jumpcut_output.mp4" | |
| # ===================================================== | |
| # SILENCE DETECTION | |
| # ===================================================== | |
| 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 | |
| # ===================================================== | |
| # PARSE SILENCE TIMESTAMPS | |
| # ===================================================== | |
| 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 | |
| # ===================================================== | |
| # BUILD FILTER (JUMP CUT LOGIC) | |
| # ===================================================== | |
| 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) | |
| # ===================================================== | |
| # CORE ENGINE | |
| # ===================================================== | |
| def smart_jumpcut(video_path): | |
| """ | |
| Main jump cut engine | |
| """ | |
| print("[JUMPCUT] Analyzing video...") | |
| # Step 1: detect silence | |
| log = detect_silence(video_path) | |
| silences = parse_silence(log) | |
| print(f"[JUMPCUT] Detected silences: {len(silences)}") | |
| # Step 2: get duration | |
| 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()) | |
| # Step 3: build filter | |
| filter_complex = build_filter(silences, duration) | |
| if not filter_complex: | |
| print("[JUMPCUT] No silences found, returning original") | |
| return video_path | |
| # Step 4: render output | |
| 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 | |
| # ===================================================== | |
| # SIMPLE FAST MODE (FALLBACK) | |
| # ===================================================== | |
| 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 | |
| # ===================================================== | |
| # PUBLIC API | |
| # ===================================================== | |
| 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) |