Spaces:
Sleeping
Sleeping
| import librosa | |
| import cv2 | |
| import numpy as np | |
| import whisper | |
| import json | |
| from pathlib import Path | |
| SILENCE_THRESHOLD = 0.01 | |
| SILENCE_MIN_DURATION = 1.5 # seconds | |
| SCENE_THRESHOLD = 30.0 # frame diff threshold | |
| MIN_GAP_SECONDS = 60 # min gap between candidates | |
| def detect_silence(audio_path): | |
| y, sr = librosa.load(audio_path, sr=None, mono=True) | |
| frame_length = int(sr * 0.1) | |
| hop_length = frame_length // 2 | |
| rms = librosa.feature.rms(y=y, frame_length=frame_length, hop_length=hop_length)[0] | |
| times = librosa.frames_to_time(np.arange(len(rms)), sr=sr, hop_length=hop_length) | |
| candidates = [] | |
| in_silence = False | |
| silence_start = 0 | |
| for t, r in zip(times, rms): | |
| if r < SILENCE_THRESHOLD and not in_silence: | |
| in_silence = True | |
| silence_start = t | |
| elif r >= SILENCE_THRESHOLD and in_silence: | |
| duration = t - silence_start | |
| if duration >= SILENCE_MIN_DURATION: | |
| candidates.append({"timestamp": round(silence_start + duration / 2, 2), | |
| "type": "silence", "score": round(float(duration), 3)}) | |
| in_silence = False | |
| return candidates | |
| def detect_scene_changes(video_path): | |
| cap = cv2.VideoCapture(video_path) | |
| fps = cap.get(cv2.CAP_PROP_FPS) | |
| candidates = [] | |
| prev_frame = None | |
| frame_idx = 0 | |
| while True: | |
| ret, frame = cap.read() | |
| if not ret: | |
| break | |
| gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) | |
| if prev_frame is not None: | |
| diff = np.mean(np.abs(gray.astype(float) - prev_frame.astype(float))) | |
| if diff > SCENE_THRESHOLD: | |
| t = round(frame_idx / fps, 2) | |
| candidates.append({"timestamp": t, "type": "scene_change", "score": round(float(diff), 3)}) | |
| prev_frame = gray | |
| frame_idx += 1 | |
| cap.release() | |
| return candidates | |
| def detect_transcript_boundaries(audio_path): | |
| model = whisper.load_model("base") | |
| result = model.transcribe(audio_path, word_timestamps=True) | |
| candidates = [] | |
| segments = result.get("segments", []) | |
| for i in range(1, len(segments)): | |
| gap = segments[i]["start"] - segments[i-1]["end"] | |
| if gap > 1.0: | |
| t = round((segments[i-1]["end"] + segments[i]["start"]) / 2, 2) | |
| candidates.append({"timestamp": t, "type": "transcript_boundary", "score": round(gap, 3)}) | |
| return candidates | |
| def merge_candidates(all_candidates, total_duration, min_gap=MIN_GAP_SECONDS): | |
| all_candidates.sort(key=lambda x: x["timestamp"]) | |
| # Remove candidates in first 20% and last 10% | |
| start_cut = total_duration * 0.20 | |
| end_cut = total_duration * 0.90 | |
| filtered = [c for c in all_candidates if start_cut <= c["timestamp"] <= end_cut] | |
| merged = [] | |
| last_t = -min_gap | |
| for c in filtered: | |
| if c["timestamp"] - last_t >= min_gap: | |
| merged.append(c) | |
| last_t = c["timestamp"] | |
| return merged | |
| def run(video_path, output_path="candidates.json"): | |
| print(f"[Component 1] Processing: {video_path}") | |
| cap = cv2.VideoCapture(video_path) | |
| fps = cap.get(cv2.CAP_PROP_FPS) | |
| frame_count = cap.get(cv2.CAP_PROP_FRAME_COUNT) | |
| total_duration = frame_count / fps | |
| cap.release() | |
| silence = detect_silence(video_path) | |
| print(f" Silence candidates: {len(silence)}") | |
| scene = detect_scene_changes(video_path) | |
| print(f" Scene change candidates: {len(scene)}") | |
| transcript = detect_transcript_boundaries(video_path) | |
| print(f" Transcript boundary candidates: {len(transcript)}") | |
| all_c = silence + scene + transcript | |
| merged = merge_candidates(all_c, total_duration) | |
| print(f" Final merged candidates: {len(merged)}") | |
| with open(output_path, "w") as f: | |
| json.dump({"total_duration": total_duration, "candidates": merged}, f, indent=2) | |
| print(f" Saved to {output_path}") | |
| return merged | |
| if __name__ == "__main__": | |
| import sys | |
| video_path = sys.argv[1] if len(sys.argv) > 1 else "test_video.mp4" | |
| run(video_path) | |