diff --git a/app.py b/app.py index 2d2b809..6a212d7 100644 --- a/app.py +++ b/app.py @@ -182,6 +182,12 @@ class BufferHandler(logging.Handler): logger = logging.getLogger("shorts") logger.setLevel(logging.DEBUG) logger.propagate = False +for _old in list(logger.handlers): + logger.removeHandler(_old) + try: + _old.close() + except Exception: + pass _fmt = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s") for _h in ( RotatingFileHandler(LOG_FILE, maxBytes=5_000_000, backupCount=5, encoding="utf-8"), @@ -1806,7 +1812,17 @@ def auth_unlink(): if __name__ == "__main__": + import socket import uvicorn + probe = socket.socket() + try: + probe.bind(("127.0.0.1", 8000)) + except OSError: + print("Shorts Autopilot is already running at http://localhost:8000") + print("Stop it first with: pkill -f app.py") + raise SystemExit(1) + finally: + probe.close() if not shutil.which("ffmpeg"): logger.warning("ffmpeg not found on PATH, video stitching will fail") uvicorn.run(app, host="127.0.0.1", port=8000) diff --git a/static/index.html b/static/index.html index 149211b..d2a8db5 100644 --- a/static/index.html +++ b/static/index.html @@ -123,13 +123,13 @@ const FIELDS=[ ["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"], +["trending_days","Search window (days)","select:3|7|14|30|60|90","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"], -["source_language","Source language","text","Two-letter code, e.g. en. Skips source videos in other languages. Blank for any","p"], -["trending_count","Videos to scan","number","Max 50","p"], -["videos_per_day","Videos per day","number","0 runs continuously","p"], -["minutes_between_videos","Minutes between videos","number","0 for no gap","p"], +["region_code","Region","select:US=United States|GB=United Kingdom|CA=Canada|AU=Australia|NZ=New Zealand|IE=Ireland|IN=India|PH=Philippines|ZA=South Africa|DE=Germany|FR=France|ES=Spain|MX=Mexico|BR=Brazil|JP=Japan","","p"], +["source_language","Source language","select:en=English|es=Spanish|fr=French|de=German|pt=Portuguese|it=Italian|=Any language","Skips source videos in other languages","p"], +["trending_count","Videos to scan","select:10|25|50","","p"], +["videos_per_day","Videos per day","select:0=Unlimited|1|2|3|4|5|6","YouTube API quota allows about 6 uploads a day","p"], +["minutes_between_videos","Minutes between videos","select:0=No gap|30|60|120|180|240|360","","p"], ["","Folders (shared by all profiles)","header","",""], ["storage_dir","Storage folder","wide","Full path on a large disk, e.g. /Volumes/iDrive/shorts-autopilot. Jobs are built in output/ and moved to published/","g"], ["models_dir","Models folder","wide","Where AI models download to (several GB). Blank puts them in the storage folder","g"], @@ -139,13 +139,13 @@ const FIELDS=[ ["auto_install","Install missing pieces automatically","checkbox","Downloads models and installs packages when a run needs them","g"], ["","Video engine (shared by all profiles)","header","",""], ["video_engine","Video engine","select:local=Local (free: AI images, narration, motion)|veo=Veo (paid Google API)","Local runs entirely on this machine at no cost. Veo makes true AI video but needs billing","g"], -["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","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"], +["sd_model","Image model","select:stabilityai/sdxl-turbo=Fast (SDXL Turbo)|stabilityai/stable-diffusion-xl-base-1.0=Best quality (SDXL Base, slower)","Picking a model fills in its recommended steps, guidance, and size. First use downloads about 7 GB","g"], +["sd_steps","Image steps","select:4|6|8|20|25|30","4 to 8 for Fast, 20 to 30 for Best quality","g"], +["sd_guidance","Image guidance","select:2|3|5|7|8","2 for Fast, 7 for Best quality","g"], +["sd_width","Image width","select:640|704|768|832","Larger is slower","g"], +["sd_height","Image height","select:1152|1216|1344|1472","Keep close to a 9:16 ratio with the width","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_voice","Narration voice","select:af_heart=Heart (US female, warm)|af_bella=Bella (US female)|af_nicole=Nicole (US female, soft)|af_sarah=Sarah (US female)|af_sky=Sky (US female)|am_michael=Michael (US male)|am_adam=Adam (US male)|bf_emma=Emma (UK female)|bf_isabella=Isabella (UK female)|bm_george=George (UK male)|bm_lewis=Lewis (UK male)","Kokoro voices. The system voice ignores this","g"], ["tts_rate","Narration speed","number","Only used by the system voice. Words per minute","g"], ["piper_model","Piper voice file","wide","Blank uses the first voice found in the voices folder under Models","g"], ["gemini_api_key","Gemini API key (Veo)","password","Only needed for the Veo engine. Billing must be enabled","g"], @@ -154,11 +154,11 @@ const FIELDS=[ ["ollama_url","Ollama URL","text","","g"], ["ollama_model","Ollama model","model:text","Writes scripts and tags. Models not installed yet are downloaded automatically","g"], ["vision_model","Ollama vision model","model:vision","Checks generated scenes on made-for-kids profiles. moondream suits 16 GB machines","g"], -["ollama_timeout","Ollama timeout (seconds)","number","Max wait per Ollama request before retrying","g"], +["ollama_timeout","Ollama timeout (seconds)","select:120|300|600|900","Max wait per Ollama request before retrying","g"], ["aspect_ratio","Aspect ratio","select:9:16|16:9","","g"], -["target_seconds","Target length (seconds)","number","Split into scenes of the length below","g"], -["clip_seconds","Seconds per scene","number","8 gives 7 scenes in a 60 second Short","g"], -["error_cooldown_minutes","Wait after error (minutes)","number","","g"] +["target_seconds","Target length (seconds)","select:30|45|60|90|120|180","Split into scenes of the length below","g"], +["clip_seconds","Seconds per scene","select:5|6|8|10|12","8 gives 7 scenes in a 60 second Short","g"], +["error_cooldown_minutes","Wait after error (minutes)","select:1|5|10|15|30|60","","g"] ]; const $=s=>document.querySelector(s); const esc=s=>String(s??"").replace(/[&<>"]/g,c=>({"&":"&","<":"<",">":">",'"':"""}[c])); @@ -175,7 +175,7 @@ for(const [k,label,type,hint,scope] of FIELDS){ if(type==="header"){const h=document.createElement("h3");h.textContent=label;g.appendChild(h);continue} const s=scope==="g"?data.global:data.profile; let input; -if(type.startsWith("select:")){input=``} +if(type.startsWith("select:")){const list=type.slice(7).split("|").map(opt);const cur=String(s[k]??"");if(!list.some(([v])=>v===cur))list.unshift([cur,cur+" (current)"]);input=``} else if(type==="checkbox"){input=``} else if(type.startsWith("model:")){ const kind=type.slice(6); @@ -206,6 +206,11 @@ const kids=$("#f-made_for_kids"); if(kids){ for(const id of ["#f-kids_manual_review","#f-kids_llm_review"]){const el=$(id);if(!el)continue;el.disabled=!kids.checked;el.closest("div").style.opacity=kids.checked?1:.5} kids.onchange=syncFields} +const sdm=$("#f-sd_model"); +if(sdm&&!sdm.dataset.wired){sdm.dataset.wired="1";sdm.addEventListener("change",()=>{ +const best=sdm.value.includes("stable-diffusion-xl-base"); +const set=(id,v)=>{const el=$(id);if(!el)return;if(![...el.options].some(o=>o.value===v)){const o=document.createElement("option");o.value=v;o.textContent=v;el.appendChild(o)}el.value=v}; +set("#f-sd_steps",best?"25":"4");set("#f-sd_guidance",best?"7":"2");set("#f-sd_width",best?"768":"704");set("#f-sd_height",best?"1344":"1216")})} const eng=$("#f-video_engine"); if(eng){ const local=eng.value==="local"; @@ -214,7 +219,8 @@ for(const id of ["#f-gemini_api_key","#f-veo_model"]){const el=$(id);if(!el)cont eng.onchange=syncFields}} async function saveSettings(){ const body={global:{},profile:{}}; -for(const [k,,type,,scope] of FIELDS){if(type==="header")continue;const el=$("#f-"+k);body[scope==="g"?"global":"profile"][k]=type==="checkbox"?el.checked:type==="number"?Number(el.value):el.value} +const NUMERIC=new Set(["sd_steps","sd_guidance","sd_width","sd_height","trending_days","trending_count","videos_per_day","minutes_between_videos","target_seconds","clip_seconds","ollama_timeout","error_cooldown_minutes","tts_rate"]); +for(const [k,,type,,scope] of FIELDS){if(type==="header")continue;const el=$("#f-"+k);body[scope==="g"?"global":"profile"][k]=type==="checkbox"?el.checked:(type==="number"||NUMERIC.has(k))?Number(el.value):el.value} try{await api("/api/settings",{method:"POST",body:JSON.stringify(body)});await loadSettings();$("#saved").textContent="Saved "+new Date().toLocaleTimeString();refresh()}catch(e){$("#saved").textContent="Error: "+e.message}} async function activate(id){try{await api("/api/profiles/activate",{method:"POST",body:JSON.stringify({id})})}catch(e){alert(e.message)}await loadSettings();refresh()} async function refresh(){