Spaces:
Sleeping
Sleeping
| """ | |
| extra_features.py β new, independent features that don't touch the core | |
| video-rendering pipeline in app.py. | |
| Why a separate file: app.py is already ~2700 lines and every edit to it risks | |
| the templates that are already working in production. New, unrelated features | |
| go here instead, and app.py just imports and wires them up. This is step one | |
| of splitting the codebase into connected files instead of one giant one β | |
| see the bottom of this file for how the next pieces (audio.py, captions.py, | |
| templates/) should be pulled out the same way, without touching working code | |
| in the same pass. | |
| Functions here take the shared helpers they need (call_openrouter, clean_json, | |
| etc.) as arguments instead of importing app.py directly β this avoids a | |
| circular import (app.py imports this file), and makes each function testable | |
| on its own. | |
| """ | |
| import json | |
| import subprocess | |
| import asyncio | |
| import re | |
| from pathlib import Path | |
| from typing import Callable, Awaitable, List, Dict | |
| # βββ FEATURE 1: CONTENT IDEA BRAINSTORMER ββββββββββββββββββββββββββ | |
| # Mirrors Crayo's "Brainstorm Content Ideas" tool. Pure LLM call, reuses your | |
| # existing NVIDIAβGroqβOpenRouter chain β no new API keys or dependencies. | |
| IDEA_TYPE_PROMPTS = { | |
| "story_video": "a Reddit/AITA-style first-person story video", | |
| "chatgpt_video": "a video where an AI chatbot answers/reacts to a question on screen", | |
| "split_screen": "a split-screen video (gameplay or satisfying footage on one side, story/facts narration on the other)", | |
| } | |
| async def brainstorm_content_ideas( | |
| call_openrouter: Callable[..., Awaitable[str]], | |
| clean_json_array: Callable[[str], str], | |
| idea_type: str, | |
| topics: List[str], | |
| count: int = 6, | |
| ) -> List[Dict[str, str]]: | |
| kind = IDEA_TYPE_PROMPTS.get(idea_type, IDEA_TYPE_PROMPTS["story_video"]) | |
| topic_line = ", ".join(topics) if topics else "anything currently trending" | |
| prompt = f"""Generate {count} concrete video ideas for {kind}. | |
| Topic areas to draw from: {topic_line} | |
| Each idea must be specific enough to script immediately β not a generic category. | |
| Bad: "a story about betrayal" | |
| Good: "A wedding planner discovers the groom is her ex from 10 years ago β during the rehearsal dinner" | |
| For each idea return: | |
| - title: a punchy, specific working title (under 12 words) | |
| - hook: the exact opening line/moment that would stop someone scrolling | |
| - angle: one sentence on why this specific idea would perform well (what emotion/curiosity it taps) | |
| Return ONLY a JSON array: [{{"title":"...","hook":"...","angle":"..."}}]""" | |
| for attempt in range(3): | |
| try: | |
| sys_msg = "Return ONLY a JSON array, no markdown, no extra text." if attempt == 0 else \ | |
| "Your previous reply was not a valid JSON array. Return ONLY the array." | |
| raw = clean_json_array(await call_openrouter(prompt, sys_msg, skip_nvidia=(attempt >= 1))) | |
| parsed = json.loads(raw) | |
| if isinstance(parsed, list) and parsed: | |
| return parsed[:count] | |
| except Exception: | |
| if attempt == 2: | |
| raise Exception("Idea generation failed β try again") | |
| return [] | |
| # βββ FEATURE 2: STANDALONE "DOWNLOAD SOCIAL VIDEO" TOOL ββββββββββββ | |
| # Mirrors Crayo's "Download Social Videos" tool β paste a YouTube/TikTok link, | |
| # get a plain MP4 back at a chosen quality, no editing. Reuses the same | |
| # multi-client yt-dlp resilience approach already used by Clip Maker. | |
| QUALITY_FORMAT_MAP = { | |
| "1080p": "bestvideo[height<=1080][ext=mp4]+bestaudio[ext=m4a]/best[height<=1080]", | |
| "720p": "bestvideo[height<=720][ext=mp4]+bestaudio[ext=m4a]/best[height<=720]", | |
| "480p": "bestvideo[height<=480][ext=mp4]+bestaudio[ext=m4a]/best[height<=480]", | |
| "audio_only": "bestaudio[ext=m4a]/bestaudio", | |
| } | |
| async def download_social_video( | |
| url: str, | |
| quality: str, | |
| dest_path: Path, | |
| proxy: str = "", | |
| cookies_path: Path = None, | |
| ) -> dict: | |
| """Downloads a YouTube/TikTok URL to dest_path at the requested quality. | |
| Returns {"ok": bool, "detail": str}. Reuses the same player-client | |
| fallback chain as Clip Maker since the same YouTube-side blocking issues | |
| apply here too.""" | |
| fmt = QUALITY_FORMAT_MAP.get(quality, QUALITY_FORMAT_MAP["1080p"]) | |
| is_youtube = "youtube.com" in url or "youtu.be" in url | |
| client_chain = ["android", "ios", "android_vr", "tv", "web_safari"] if is_youtube else [None] | |
| last_stderr = "" | |
| for client in client_chain: | |
| cmd = ["yt-dlp", "-f", fmt, "--merge-output-format", "mp4", "--no-playlist", | |
| "--retries", "5", "--fragment-retries", "5", "--retry-sleep", "3", | |
| "--socket-timeout", "30"] | |
| if client: | |
| cmd += ["--extractor-args", f"youtube:player_client={client}"] | |
| if proxy: | |
| cmd += ["--proxy", proxy] | |
| if cookies_path and cookies_path.exists(): | |
| cmd += ["--cookies", str(cookies_path)] | |
| cmd += ["-o", str(dest_path), url] | |
| result = await asyncio.to_thread(subprocess.run, cmd, capture_output=True, text=True) | |
| if result.returncode == 0 and dest_path.exists() and dest_path.stat().st_size > 10_000: | |
| return {"ok": True, "detail": f"downloaded (client={client or 'default'})"} | |
| last_stderr = result.stderr or "" | |
| lines = [l for l in last_stderr.strip().splitlines() if l.strip()] | |
| detail = " | ".join(lines[-3:]) if lines else "unknown yt-dlp error" | |
| return {"ok": False, "detail": detail} | |