| """ |
| 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) |
|
|
| |
| _used_ids = set() |
|
|
| |
| 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.""" |
|
|
| |
| clip = self._fetch_from_pexels(topic) |
| if clip: |
| return clip |
|
|
| |
| 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 |
|
|
| |
| 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), |
| "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 |
|
|
| |
| available = [v for v in videos if v["id"] not in _used_ids] |
| if not available: |
| _used_ids.clear() |
| available = videos |
|
|
| |
| video = random.choice(available) |
| _used_ids.add(video["id"]) |
|
|
| |
| 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 |
|
|
| |
| if not download_url and video.get("video_files"): |
| download_url = video["video_files"][0]["link"] |
|
|
| if not download_url: |
| return None |
|
|
| |
| 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}") |
|
|