Commit Β·
2323e40
1
Parent(s): 5680194
Feature: YouTube heatmap extraction for viral segment detection
Browse files
server.py
CHANGED
|
@@ -405,7 +405,6 @@ def download_video(youtube_url: str, max_retries: int = 3) -> Path:
|
|
| 405 |
raise RuntimeError(f"Download failed after {max_retries} attempts:\n{last_err}")
|
| 406 |
|
| 407 |
|
| 408 |
-
# ββ Step 2: Video info ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 409 |
def get_video_info(video_path: Path):
|
| 410 |
"""Return (duration, width, height) via ffprobe JSON."""
|
| 411 |
r = subprocess.run(
|
|
@@ -424,6 +423,83 @@ def get_video_info(video_path: Path):
|
|
| 424 |
return duration, width, height
|
| 425 |
|
| 426 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
# ββ Step 3: Audio energy analysis ββββββββββββββββββββββββββββββββββββββββββββ
|
| 428 |
def extract_audio_energy(video_path: Path, duration: float):
|
| 429 |
"""
|
|
@@ -921,6 +997,9 @@ def process():
|
|
| 921 |
try:
|
| 922 |
# 1. Download
|
| 923 |
video_path = download_video(youtube_url)
|
|
|
|
|
|
|
|
|
|
| 924 |
|
| 925 |
# 2. Info
|
| 926 |
duration, width, height = get_video_info(video_path)
|
|
|
|
| 405 |
raise RuntimeError(f"Download failed after {max_retries} attempts:\n{last_err}")
|
| 406 |
|
| 407 |
|
|
|
|
| 408 |
def get_video_info(video_path: Path):
|
| 409 |
"""Return (duration, width, height) via ffprobe JSON."""
|
| 410 |
r = subprocess.run(
|
|
|
|
| 423 |
return duration, width, height
|
| 424 |
|
| 425 |
|
| 426 |
+
def get_youtube_heatmap(video_path: Path, url: str):
|
| 427 |
+
"""
|
| 428 |
+
Fetch YouTube's 'Most Replayed' heatmap data using yt-dlp.
|
| 429 |
+
Returns: [{start_time: float, end_time: float, score: float}] or []
|
| 430 |
+
"""
|
| 431 |
+
print("[>>] Fetching YouTube heatmap...", flush=True)
|
| 432 |
+
|
| 433 |
+
# We use the same hardened bypass settings as download_video
|
| 434 |
+
info_json_path = video_path.with_suffix(".info.json")
|
| 435 |
+
|
| 436 |
+
cmd = [
|
| 437 |
+
"yt-dlp",
|
| 438 |
+
"--force-ipv4",
|
| 439 |
+
"--ignore-config",
|
| 440 |
+
"--no-cache-dir",
|
| 441 |
+
"--user-agent", _USER_AGENT,
|
| 442 |
+
"--extractor-args", "youtube:player_client=web,tv,ios;player_skip=web_embedded_check",
|
| 443 |
+
"--remote-components", "ejs:github",
|
| 444 |
+
"--no-check-certificates",
|
| 445 |
+
"--geo-bypass",
|
| 446 |
+
"--add-header", "Accept-Language:en-US,en;q=0.9",
|
| 447 |
+
"--add-header", "Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
| 448 |
+
"--write-info-json",
|
| 449 |
+
"--skip-download",
|
| 450 |
+
"-o", str(video_path.with_suffix("")), # This will result in filename.info.json
|
| 451 |
+
url,
|
| 452 |
+
]
|
| 453 |
+
|
| 454 |
+
# Add cookies if available
|
| 455 |
+
cookies_path = _find_cookies_file()
|
| 456 |
+
if cookies_path:
|
| 457 |
+
cmd[1:1] = ["--cookies", cookies_path]
|
| 458 |
+
|
| 459 |
+
# Add Proxy if defined
|
| 460 |
+
if YTDLP_PROXY:
|
| 461 |
+
cmd[1:1] = ["--proxy", YTDLP_PROXY]
|
| 462 |
+
|
| 463 |
+
try:
|
| 464 |
+
# Run yt-dlp to get JSON
|
| 465 |
+
subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
| 466 |
+
|
| 467 |
+
if not info_json_path.exists():
|
| 468 |
+
print("[!] Heatmap info JSON not found.", flush=True)
|
| 469 |
+
return []
|
| 470 |
+
|
| 471 |
+
with open(info_json_path, "r", encoding="utf-8") as f:
|
| 472 |
+
data = json.load(f)
|
| 473 |
+
|
| 474 |
+
heatmap = data.get("heatmap")
|
| 475 |
+
if not heatmap:
|
| 476 |
+
print("[!] No heatmap data found in YouTube metadata.", flush=True)
|
| 477 |
+
return []
|
| 478 |
+
|
| 479 |
+
# Normalize scores to 0.0 - 1.0
|
| 480 |
+
max_val = max((item.get("value", 0) for item in heatmap), default=1.0)
|
| 481 |
+
if max_val == 0: max_val = 1.0
|
| 482 |
+
|
| 483 |
+
normalized = []
|
| 484 |
+
for item in heatmap:
|
| 485 |
+
normalized.append({
|
| 486 |
+
"start_time": float(item["start_time"]),
|
| 487 |
+
"end_time": float(item["end_time"]),
|
| 488 |
+
"score": round(float(item["value"]) / max_val, 4)
|
| 489 |
+
})
|
| 490 |
+
|
| 491 |
+
print(f"[OK] Extracted {len(normalized)} heatmap segments.", flush=True)
|
| 492 |
+
|
| 493 |
+
# Cleanup
|
| 494 |
+
try: info_json_path.unlink()
|
| 495 |
+
except: pass
|
| 496 |
+
|
| 497 |
+
return normalized
|
| 498 |
+
except Exception as e:
|
| 499 |
+
print(f"[!] Heatmap extraction error: {e}", flush=True)
|
| 500 |
+
return []
|
| 501 |
+
|
| 502 |
+
|
| 503 |
# ββ Step 3: Audio energy analysis ββββββββββββββββββββββββββββββββββββββββββββ
|
| 504 |
def extract_audio_energy(video_path: Path, duration: float):
|
| 505 |
"""
|
|
|
|
| 997 |
try:
|
| 998 |
# 1. Download
|
| 999 |
video_path = download_video(youtube_url)
|
| 1000 |
+
|
| 1001 |
+
# 1.1 Heatmap (New Viral Signalling)
|
| 1002 |
+
heatmap_data = get_youtube_heatmap(video_path, youtube_url)
|
| 1003 |
|
| 1004 |
# 2. Info
|
| 1005 |
duration, width, height = get_video_info(video_path)
|