""" Asset Manager — Fetches background clips from Pexels API. Downloads landscape-oriented clips relevant to the script topic. Falls back to local files if Pexels fails. """ import os import random import glob import requests import time import re PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY", "") PEXELS_VIDEO_URL = "https://api.pexels.com/videos/search" PEXELS_PER_PAGE = min(int(os.environ.get("PEXELS_PER_PAGE", "80")), 80) # Anti-repetition _used_ids = set() # Self-improvement/psychology keywords for varied backgrounds FALLBACK_SEARCHES = [ "dark room", "alone", "silhouette", "shadow face", "thinking", "stress", "therapy", "city night", "argument", "conversation", "sad man", "sad woman", "anxiety", "depression", "lonely", "mystery", "dramatic light", "relationship", "breakup", "office stress", "student studying dark", "focused work desk", "city night cinematic", "person thinking alone", "gym discipline training", "office work focus", "library study", "walking alone city", "mindset success", "psychology human behavior", ] TOPIC_QUERY_MAP = { "gaslighting": ["argument", "confused woman", "relationship conflict", "stress"], "manipulation": ["conversation", "argument", "shadow face", "relationship conflict"], "dark psychology": ["dark room", "shadow face", "mystery", "thinking"], "narciss": ["argument", "relationship conflict", "angry man", "couple arguing"], "silent treatment": ["lonely", "sad woman", "alone", "relationship"], "guilt": ["stress", "sad man", "anxiety", "alone"], "love bombing": ["couple", "relationship", "romantic", "dating"], "toxic": ["argument", "relationship conflict", "stress", "breakup"], "emotional": ["stress", "crying", "sad woman", "anxiety"], "victim": ["sad man", "alone", "stress", "depression"], "triangulation": ["jealousy", "couple", "relationship", "argument"], "push-pull": ["relationship", "breakup", "alone", "couple"], "confusion": ["confused", "thinking", "stress", "anxiety"], "over-explaining": ["conversation", "office talk", "stress", "argument"], "intermittent": ["phone waiting", "alone", "relationship", "sad woman"], "fake nice": ["smiling", "office conversation", "handshake", "people talking"], "power": ["business meeting", "confidence", "office", "leadership"], "walking away": ["walking away", "alone walking", "city night", "breakup"], } def build_search_queries(topic, count): normalized = re.sub(r"[-_:]", " ", topic.lower()) normalized = re.sub(r"[^a-z0-9 ]+", " ", normalized) queries = [] for key, mapped_queries in TOPIC_QUERY_MAP.items(): if key in normalized: queries.extend(mapped_queries) words = [word for word in normalized.split() if len(word) > 3] if words: queries.append(" ".join(words[:2])) queries.extend(random.sample(FALLBACK_SEARCHES, min(len(FALLBACK_SEARCHES), count * 3))) deduped = [] for query in queries: if query and query not in deduped: deduped.append(query) return deduped class AssetManager: def __init__(self, assets_dir="assets"): self.assets_dir = assets_dir self.backgrounds_dir = os.path.join(assets_dir, "backgrounds") os.makedirs(self.backgrounds_dir, exist_ok=True) self._search_index = 0 def get_random_background_video(self, topic="self improvement psychology"): """Gets a landscape background clip from Pexels or local files.""" # Try Pexels first clip = self._fetch_from_pexels(topic) if clip: return clip # Fallback: try generic search fallback_kw = FALLBACK_SEARCHES[self._search_index % len(FALLBACK_SEARCHES)] self._search_index += 1 clip = self._fetch_from_pexels(fallback_kw) if clip: return clip # Fallback: local files print("⚠️ Pexels failed, trying local files...", flush=True) videos = glob.glob(os.path.join(self.backgrounds_dir, "*.mp4")) if not videos: videos = glob.glob(os.path.join(self.assets_dir, "*.mp4")) if videos: return random.choice(videos) print("❌ No background videos found!", flush=True) return None def get_background_videos(self, topic="self improvement psychology", count=6): """Gets multiple landscape background clips for one video.""" clips = [] queries = build_search_queries(topic, count) for query in queries: if len(clips) >= count: break clip = self._fetch_from_pexels(query) if clip and clip not in clips: clips.append(clip) if len(clips) < count: local_videos = glob.glob(os.path.join(self.backgrounds_dir, "*.mp4")) if not local_videos: local_videos = glob.glob(os.path.join(self.assets_dir, "*.mp4")) random.shuffle(local_videos) for clip in local_videos: if len(clips) >= count: break if clip not in clips: clips.append(clip) print(f"🎞️ Background clips selected: {len(clips)}/{count}", flush=True) return clips def _fetch_from_pexels(self, query): """Search and download a landscape clip from Pexels.""" if not PEXELS_API_KEY: print("⚠️ PEXELS_API_KEY not set!", flush=True) return None try: headers = {"Authorization": PEXELS_API_KEY} params = { "query": query, "orientation": "landscape", "per_page": PEXELS_PER_PAGE, "page": random.randint(1, 25), # Random page = different clips each time "size": "medium", } print(f"🔍 Pexels: searching '{query}' ({PEXELS_PER_PAGE} clips/page)...", flush=True) resp = requests.get(PEXELS_VIDEO_URL, headers=headers, params=params, timeout=15) if resp.status_code != 200: print(f"⚠️ Pexels API error: {resp.status_code}", flush=True) return None videos = resp.json().get("videos", []) if not videos: print(f"⚠️ No results for '{query}'", flush=True) return None # Filter out already-used clips available = [v for v in videos if v["id"] not in _used_ids] if not available: _used_ids.clear() # Reset if all used available = videos # Pick random clip video = random.choice(available) _used_ids.add(video["id"]) # Find best quality file (prefer HD, landscape) download_url = None for vf in video.get("video_files", []): w = vf.get("width", 0) h = vf.get("height", 0) if w >= 1920 and w >= h: download_url = vf["link"] break # Fallback: any file if not download_url and video.get("video_files"): download_url = video["video_files"][0]["link"] if not download_url: return None # Download filename = f"pexels_{video['id']}_{int(time.time())}.mp4" local_path = os.path.join(self.backgrounds_dir, filename) print(f"⬇️ Downloading: {filename}...", flush=True) vid_resp = requests.get(download_url, timeout=60) if vid_resp.status_code == 200: with open(local_path, "wb") as f: f.write(vid_resp.content) size_mb = len(vid_resp.content) / (1024 * 1024) print(f"✅ Downloaded: {filename} ({size_mb:.1f} MB)", flush=True) return local_path except Exception as e: print(f"⚠️ Pexels error: {e}", flush=True) return None if __name__ == "__main__": am = AssetManager() bg = am.get_random_background_video("self improvement psychology") print(f"Background: {bg}")