Spaces:
Paused
Paused
Download runner.py from abc1181/perplexica: direct link, hf CLI and curl.
- Browser
- Download file 6.17 kB
-
https://huggingface.co/spaces/abc1181/perplexica/resolve/main/runner.py
- Command line
-
hf download hf://spaces/abc1181/perplexica/runner.py
-
curl -L -o runner.py https://huggingface.co/spaces/abc1181/perplexica/resolve/main/runner.py
6.17 kB
| """Motion Factory daily cron — drives the OpenReels API inside this container. | |
| POST /api/v1/jobs {topic, providers{...}} -> poll GET /api/v1/jobs/:id | |
| -> copy final mp4 to /data/motion/renders + index.jsonl. | |
| Env: RUN_HOUR_UTC (14), RUNNER_ENABLED (1), CHANNEL_NICHE, | |
| OMNIROUTE_* (topic generation + pipeline LLM), PEXELS_API_KEY. | |
| """ | |
| import json | |
| import os | |
| import shutil | |
| import time | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| import requests | |
| API = "http://127.0.0.1:7860" | |
| RUN_HOUR_UTC = int(os.environ.get("RUN_HOUR_UTC", "14")) | |
| DATA_ROOT = Path("/data/motion") if os.path.isdir("/data") else Path.home() / ".motion" | |
| RENDERS = DATA_ROOT / "renders" | |
| TOPICS_FILE = DATA_ROOT / "topics.json" | |
| INDEX_FILE = RENDERS / "index.jsonl" | |
| POLL_INTERVAL = 30 | |
| POLL_MAX = 60 * 50 # 50 min ceiling | |
| def log(m): | |
| print(f"[runner {datetime.now(timezone.utc):%H:%M:%S}] {m}", flush=True) | |
| def load_topics(): | |
| try: | |
| return json.loads(TOPICS_FILE.read_text()) | |
| except Exception: | |
| return {"topics": [], "used": []} | |
| def save_topics(d): | |
| TOPICS_FILE.parent.mkdir(parents=True, exist_ok=True) | |
| TOPICS_FILE.write_text(json.dumps(d, indent=2)) | |
| def llm_topic(niche): | |
| key, base = os.environ.get("OMNIROUTE_API_KEY"), os.environ.get("OMNIROUTE_BASE_URL", "").rstrip("/") | |
| model = os.environ.get("OMNIROUTE_MODEL", "") | |
| if not (key and base): | |
| return None | |
| try: | |
| r = requests.post( | |
| f"{base}/v1/chat/completions", | |
| headers={"Authorization": f"Bearer {key}"}, | |
| json={"model": model, "max_tokens": 60, "messages": [{"role": "user", "content": | |
| f"Give me ONE specific, viral-worthy topic for a faceless YouTube Short " | |
| f"about {niche}. Reply with ONLY the topic text."}]}, | |
| timeout=90) | |
| r.raise_for_status() | |
| return (r.json()["choices"][0]["message"]["content"].strip().strip('"') or None)[:120] | |
| except Exception as e: | |
| log(f"llm topic failed: {e}") | |
| return None | |
| def next_topic(): | |
| d = load_topics() | |
| while d["topics"]: | |
| t = d["topics"].pop(0).strip() | |
| if t and t not in d["used"]: | |
| d["used"].append(t) | |
| save_topics(d) | |
| return t | |
| niche = os.environ.get("CHANNEL_NICHE", "surprising science and technology facts") | |
| save_topics(d) | |
| return llm_topic(niche) | |
| def create_job(topic): | |
| body = { | |
| "topic": topic, | |
| "noVideo": True, | |
| "providers": { | |
| "llm": "openai-compatible", | |
| "llmModel": os.environ.get("OMNIROUTE_MODEL", ""), | |
| "llmBaseUrl": os.environ.get("OMNIROUTE_BASE_URL", "").rstrip("/") + "/v1", | |
| "tts": "kokoro", | |
| "stock": "pexels", | |
| "music": "bundled", | |
| "searchProvider": "none", | |
| }, | |
| } | |
| for attempt in range(3): | |
| try: | |
| r = requests.post(f"{API}/api/v1/jobs", json=body, timeout=60) | |
| if r.status_code == 200: | |
| jid = r.json().get("jobId") or r.json().get("id") | |
| log(f"job created: {jid}") | |
| return jid | |
| log(f"create attempt {attempt+1} -> {r.status_code} {r.text[:200]}") | |
| except Exception as e: | |
| log(f"create attempt {attempt+1} error: {e}") | |
| time.sleep(20) | |
| return None | |
| def poll_job(jid): | |
| waited = 0 | |
| while waited < POLL_MAX: | |
| time.sleep(POLL_INTERVAL) | |
| waited += POLL_INTERVAL | |
| try: | |
| r = requests.get(f"{API}/api/v1/jobs/{jid}", timeout=30) | |
| r.raise_for_status() | |
| meta = r.json() | |
| except Exception as e: | |
| log(f"poll error: {e}") | |
| continue | |
| status = meta.get("status") | |
| stages = meta.get("stages", {}) | |
| cur = " ".join(f"{k}:{v.get('status','?')}" for k, v in stages.items() if v.get("status") not in ("pending",)) | |
| log(f"job {jid}: {status} | {cur[:160]}") | |
| if status in ("completed", "failed", "cancelled"): | |
| return meta | |
| log("poll timeout") | |
| return None | |
| def archive(meta, topic): | |
| vp = meta.get("videoPath") | |
| if not vp: | |
| log(f"no videoPath; error={str(meta.get('error'))[:200]}") | |
| return | |
| src = Path(vp) | |
| if not src.exists(): | |
| cand = list(Path("/app/jobs").rglob("*.mp4")) | |
| src = max(cand, key=lambda p: p.stat().st_size) if cand else None | |
| if not src: | |
| log("mp4 not found on disk") | |
| return | |
| RENDERS.mkdir(parents=True, exist_ok=True) | |
| stamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S") | |
| slug = "".join(c if c.isalnum() else "-" for c in topic.lower())[:40].strip("-") | |
| dst = RENDERS / f"{stamp}_{slug}.mp4" | |
| shutil.copy2(src, dst) | |
| with INDEX_FILE.open("a") as f: | |
| f.write(json.dumps({"date": stamp, "topic": topic, "file": dst.name, | |
| "size_mb": round(dst.stat().st_size / 1e6, 2)}) + "\n") | |
| log(f"ARCHIVED {dst.name}") | |
| def run_once(): | |
| topic = next_topic() | |
| if not topic: | |
| log("no topic; skipping") | |
| return | |
| log(f"creating job for: {topic!r}") | |
| jid = create_job(topic) | |
| if not jid: | |
| return | |
| meta = poll_job(jid) | |
| if meta and meta.get("status") == "completed": | |
| archive(meta, topic) | |
| def seconds_until_next_run(): | |
| now = datetime.now(timezone.utc) | |
| t = now.replace(hour=RUN_HOUR_UTC, minute=0, second=0, microsecond=0) | |
| from datetime import timedelta | |
| if t <= now: | |
| t += timedelta(days=1) | |
| return (t - now).total_seconds() | |
| def main(): | |
| if os.environ.get("RUNNER_ENABLED", "1") != "1": | |
| log("disabled; idling") | |
| while True: | |
| time.sleep(3600) | |
| for _ in range(120): | |
| try: | |
| requests.get(f"{API}/api/v1/health", timeout=5) | |
| break | |
| except Exception: | |
| time.sleep(5) | |
| log(f"online; daily run at {RUN_HOUR_UTC}:00 UTC") | |
| while True: | |
| w = seconds_until_next_run() | |
| log(f"next run in {w/3600:.1f}h") | |
| time.sleep(max(w, 10)) | |
| try: | |
| run_once() | |
| except Exception as e: | |
| log(f"cycle crashed: {e}") | |
| if __name__ == "__main__": | |
| main() | |