shortsai / extra_features.py
VISHAL18for4's picture
Upload 2 files
e1ebe79 verified
Raw
History Blame Contribute Delete
5.57 kB
"""
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}