diff --git a/app.py b/app.py index 0f97669..3a90208 100644 --- a/app.py +++ b/app.py @@ -66,8 +66,8 @@ GLOBAL_DEFAULTS = { "sd_model": "stabilityai/sdxl-turbo", "sd_steps": 4, "sd_guidance": 2, - "sd_width": 768, - "sd_height": 1344, + "sd_width": 704, + "sd_height": 1216, "tts_engine": "auto", "tts_voice": "af_heart", "tts_rate": 170, @@ -91,6 +91,7 @@ PROFILE_DEFAULTS = { "upload_category_id": "24", "privacy_status": "private", "trending_query": "", + "trending_queries": "", "trending_days": 7, "category_id": "", "region_code": "US", @@ -644,12 +645,19 @@ def filter_language(videos, lang, strict=False): return kept -def fetch_trending(s): +def profile_queries(s): + extra = [q.strip() for q in re.split(r"[,\n]", s.get("trending_queries", "")) if q.strip()] + main = s["trending_query"].strip() + queries = ([main] if main else []) + extra + return queries + + +def fetch_trending(s, query=None): yt = youtube_client(s["profile_id"]) count = max(1, min(int(s["trending_count"]), 50)) region = s["region_code"] or "US" lang = s["source_language"].strip().lower() - query = s["trending_query"].strip() + query = (query if query is not None else s["trending_query"]).strip() if query: after = (datetime.now(timezone.utc) - timedelta(days=max(1, int(s["trending_days"])))).strftime("%Y-%m-%dT%H:%M:%SZ") params = { @@ -991,10 +999,7 @@ Return {{"violations": []}} if no rule is broken.""" def review_mode(s): - mode = s["kids_llm_review"] if s["kids_llm_review"] in REVIEW_MODES else "advisory" - if mode == "advisory" and not s["kids_manual_review"]: - return "block" - return mode + return s["kids_llm_review"] if s["kids_llm_review"] in REVIEW_MODES else "advisory" def produce_script(s, src): @@ -1002,8 +1007,6 @@ def produce_script(s, src): feedback = [] attempts = 4 if kids else 1 mode = review_mode(s) - if kids and mode != s["kids_llm_review"]: - logger.info("LLM review switched to block mode because manual review is off") for attempt in range(1, attempts + 1): check() set_stage("writing script") @@ -1081,8 +1084,12 @@ def kids_vision_check(s, clip_path): try: try: data = ollama_call(s, KIDS_VISION_PROMPT, temperature=0.1, model=model, images=images, num_predict=256) - safe = truthy(data.get("safe")) + desc = str(data.get("description", "")) reason = str(data.get("reason", "")) + echoed = "it is unsafe if" in reason.lower() or reason.strip().lower() in ("why", "short explanation") + if echoed or desc.strip().lower() in ("what you see", "description"): + raise ValueError("model echoed the prompt") + safe = truthy(data.get("safe")) except (requests.exceptions.HTTPError, ValueError): text = ollama_call(s, VISION_TEXT_PROMPT, temperature=0.1, model=model, images=images, num_predict=128, as_json=False) low = text.lower() @@ -1371,14 +1378,23 @@ def run_job(s): ", manual review hold" if s["kids_manual_review"] else "", ) set_stage("fetching trending videos") - trending = fetch_trending(s) state = load_state() - candidates = [v for v in sorted(trending, key=lambda v: -v["views"]) if v["id"] not in state["processed"]] - if kids: - safe = [v for v in candidates if not KIDS_BANNED.search(v["title"])] - if len(safe) < len(candidates): - logger.info("Skipped %d source videos with themes unsuitable for kids", len(candidates) - len(safe)) - candidates = safe + queries = profile_queries(s) or [""] + candidates = [] + for q in queries: + check() + trending = fetch_trending(s, q) + found = [v for v in sorted(trending, key=lambda v: -v["views"]) if v["id"] not in state["processed"]] + if kids: + safe = [v for v in found if not KIDS_BANNED.search(v["title"])] + if len(safe) < len(found): + logger.info("Skipped %d source videos with themes unsuitable for kids", len(found) - len(safe)) + found = safe + if found: + candidates = found + break + if len(queries) > 1: + logger.info("Nothing new for '%s', trying the next query", q or "trending chart") if not candidates: logger.warning("No usable trending videos, checking again in 30 minutes") set_stage("waiting for new trending videos") diff --git a/static/index.html b/static/index.html index 6ff5a62..9849828 100644 --- a/static/index.html +++ b/static/index.html @@ -117,11 +117,12 @@ const FIELDS=[ ["channel_niche","Channel niche","wide","Steers every script, e.g. 60 second animated movies for kids ages 3 to 7","p"], ["made_for_kids","Made for kids","checkbox","Flags uploads as made for kids and enforces child-safe rules, word filters, LLM compliance review, and frame vision checks","p"], ["kids_manual_review","Hold kids videos for manual review","checkbox","Forces made-for-kids uploads to private so you can watch them before publishing","p"], -["kids_llm_review","LLM script review","select:advisory=Advisory (show notes, you review)|block=Block (reject flagged scripts)|off=Off","Advisory only applies while manual review is on, otherwise it blocks. Word filters and vision checks always block","p"], +["kids_llm_review","LLM script review","select:advisory=Advisory (show notes, you review)|block=Block (reject flagged scripts)|off=Off","Advisory logs notes without blocking. Word filters and vision checks always block","p"], ["upload_category_id","Upload category","select:"+CATS,"","p"], ["privacy_status","Upload privacy","select:private|unlisted|public","","p"], ["ai_disclosure","Disclose AI generated content","checkbox","YouTube only requires this for realistic synthetic content, not cartoons. Leave on to be transparent with parents","p"], ["trending_query","Trending search query","text","Blank uses the general trending chart. e.g. kids animated story","p"], +["trending_queries","Backup search queries","wide","Comma separated. Tried in order when the main query has nothing new left","p"], ["trending_days","Search window (days)","number","Only used with a search query","p"], ["category_id","Source category","select:=All|"+CATS,"Filters source videos","p"], ["region_code","Region","text","Two-letter country code, e.g. US","p"], @@ -141,8 +142,8 @@ const FIELDS=[ ["sd_model","Image model","text","Hugging Face model id, e.g. stabilityai/sdxl-turbo or stabilityai/stable-diffusion-xl-base-1.0","g"], ["sd_steps","Image steps","number","4 for turbo models, 20 to 30 for standard models","g"], ["sd_guidance","Image guidance","number","2 for turbo models, 7 for standard models. Below 1.1 the anatomy filter stops working","g"], -["sd_width","Image width","number","768 avoids duplicated subjects. Larger is slower","g"], -["sd_height","Image height","number","1344 is the correct vertical ratio for SDXL","g"], +["sd_width","Image width","number","704 is a good speed and quality balance. Larger is much slower","g"], +["sd_height","Image height","number","1216 keeps the vertical ratio SDXL expects","g"], ["tts_engine","Narration engine","select:auto=Auto (best available)|kokoro=Kokoro (most natural)|piper=Piper|say=macOS say|espeak=espeak-ng","Missing engines are installed automatically on the next run","g"], ["tts_voice","Narration voice","text","Kokoro: af_heart, af_bella, af_nicole, am_michael, bf_emma. Ignored by the system voice","g"], ["tts_rate","Narration speed","number","Only used by the system voice. Words per minute","g"],