#!/usr/bin/env python3 """T01 v2 · слой текста через OpenRouter (protocol.md § «Слой текста»): 5 моделей × P1–P10, 1 попытка + ≤ 1 повтор. Запуск: python3 run_text.py → text/<модель>/P.json|.md, text/runs.jsonl. Уже готовые (status ok) пропускает. """ import base64, json, subprocess, tempfile, time from concurrent.futures import ThreadPoolExecutor from pathlib import Path HERE = Path(__file__).resolve().parent ROOT = HERE.parent WRAP = str(Path("обёртка OpenRouter редакции").expanduser()) MODELS = {"gpt54mini": "openai/gpt-5.4-mini", "gemini31pro": "google/gemini-3.1-pro-preview", "sonnet5": "anthropic/claude-sonnet-5", "deepseekv4": "deepseek/deepseek-v4-flash-vision-exp", "qwen38max": "qwen/qwen3.8-max-0902"} TAIL = "\nГлавное фото не генерируй — дай только название и описание." OUT = HERE / "text" def items(): for f in ("inputs.json", "inputs-c2.json"): for it in json.loads((ROOT / f).read_text(encoding="utf-8"))["items"]: n = it["id"].split("-")[1] img = (ROOT / it["image"]).resolve() prompt = (ROOT / "runs" / f"prompt-{it['id']}.txt").read_text(encoding="utf-8").rstrip() yield n, img, prompt def small(img): t = Path(tempfile.mkdtemp()) / "in.jpg" subprocess.run(["sips", "-Z", "1024", "-s", "format", "jpeg", str(img), "--out", str(t)], check=True, capture_output=True) return "data:image/jpeg;base64," + base64.b64encode(t.read_bytes()).decode() def call(short, model, n, url, prompt): d = OUT / short d.mkdir(parents=True, exist_ok=True) body = {"model": model, "max_tokens": 2000, "messages": [{"role": "user", "content": [ {"type": "text", "text": prompt + TAIL}, {"type": "image_url", "image_url": {"url": url}}]}]} rec = {"model": model, "input": n} for attempt in (1, 2): t0 = time.time() p = subprocess.run([WRAP, "chat", json.dumps(body, ensure_ascii=False)], capture_output=True, text=True, timeout=300) rec.update(attempt=attempt, sec=round(time.time() - t0, 1)) try: r = json.loads(p.stdout) except json.JSONDecodeError: r = {"error": (p.stdout or p.stderr)[:300]} txt = None if not r.get("error") and r.get("choices"): txt = r["choices"][0]["message"].get("content") u = r.get("usage") or {} rec.update(cost=u.get("cost"), prompt_tokens=u.get("prompt_tokens"), completion_tokens=u.get("completion_tokens"), finish=(r.get("choices") or [{}])[0].get("finish_reason")) if txt: (d / f"{n}.json").write_text(json.dumps(r, ensure_ascii=False, indent=1), encoding="utf-8") (d / f"{n}.md").write_text(txt.strip() + "\n", encoding="utf-8") rec["status"] = "ok" return rec rec.update(status="fail", error=str(r.get("error"))[:300]) (d / f"{n}-fail-{attempt}.json").write_text(json.dumps(r, ensure_ascii=False)[:5000], encoding="utf-8") return rec def main(): OUT.mkdir(exist_ok=True) log = OUT / "runs.jsonl" done = set() if log.exists(): for line in log.read_text().splitlines(): j = json.loads(line) if j["status"] == "ok": done.add((j["model"], j["input"])) jobs = [] for n, img, prompt in items(): url = small(img) for short, model in MODELS.items(): if (model, n) not in done: jobs.append((short, model, n, url, prompt)) with ThreadPoolExecutor(5) as ex, log.open("a") as fh: for rec in ex.map(lambda a: call(*a), jobs): fh.write(json.dumps(rec, ensure_ascii=False) + "\n"); fh.flush() print(rec["model"], rec["input"], rec["status"], rec.get("cost"), rec.get("finish"), flush=True) if __name__ == "__main__": main()