Пакетная обработка на Python
Скрипт на Python, который делает отдельный ролик из каждого видео в папке — с проверкой бюджета, безопасным перезапуском и параллельным монтажом.
На этой странице
У вас папка с десятками записей — уроки, интервью, обзоры — и из каждой нужен короткий ролик. Скрипт ниже делает это пакетом: считает общий бюджет заранее, запускает несколько монтажей параллельно, скачивает результаты и помнит, что уже сделано, поэтому его можно спокойно прервать и запустить снова.
Что понадобится: Python 3.9+, pip install requests, токен с набором «Монтаж» в SLICER_TOKEN. Советуем выпустить для
пакетной обработки отдельный токен с лимитом расходов — он не даст скрипту потратить больше
задуманного, даже если в нём ошибка.
Скрипт
# python batch_reels.py ./lessons ./reels
import hashlib
import json
import os
import sys
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
import requests
API = "https://api.slicer.media/v1"
SETTINGS = {"templateId": "clean", "targetDurationSec": 60, "direction": {"mode": "talking", "pace": "balanced"}}
PARALLEL = 3
source_dir, output_dir = Path(sys.argv[1]), Path(sys.argv[2])
output_dir.mkdir(parents=True, exist_ok=True)
state_file = output_dir / "state.json"
state = json.loads(state_file.read_text()) if state_file.exists() else {}
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['SLICER_TOKEN']}"
class ApiError(Exception):
def __init__(self, status, error):
super().__init__(f"{status} {error['code']}: {error['message']}")
self.code = error["code"]
def api(method, path, json_body=None, key=None):
headers = {"Idempotency-Key": key} if key else {}
for attempt in range(1, 6):
response = session.request(method, API + path, json=json_body, headers=headers, timeout=120)
body = response.json()
if body["ok"]:
return body["data"]
if response.status_code in (429, 500, 502, 503, 504) and attempt < 5:
time.sleep(float(response.headers.get("Retry-After") or 2 ** attempt))
continue
raise ApiError(response.status_code, body["error"])
def key_for(path, step):
"""Один и тот же ключ при перезапуске скрипта: повтор не создаст второй монтаж (ключ живёт 24 ч)."""
stat = path.stat()
return hashlib.sha256(f"{path.resolve()}:{stat.st_size}:{stat.st_mtime_ns}:{step}".encode()).hexdigest()[:64]
def save_state():
state_file.write_text(json.dumps(state, ensure_ascii=False, indent=2))
def upload(path):
created = api("POST", "/uploads", {"fileName": path.name, "size": path.stat().st_size})
with path.open("rb") as file:
body = requests.put(created["uploadUrl"], data=file, timeout=3600).json()
if not body["ok"]:
raise RuntimeError(body["error"]["message"])
item = body["data"]
while item["status"] in ("uploading", "processing"):
time.sleep(3)
item = api("GET", f"/uploads/{item['id']}")
if item["status"] != "ready":
raise RuntimeError(item["error"] or item["status"])
return item["id"]
def process(path):
entry = state.setdefault(path.name, {})
if entry.get("done"):
return f"{path.name}: уже готов"
if "project" not in entry:
upload_id = upload(path)
request = {"uploadIds": [upload_id], "title": path.stem, "settings": SETTINGS}
quote = api("POST", "/projects", request)
project = api("POST", "/projects", {**request, "confirmCost": quote["cost"]}, key_for(path, "montage"))
entry["project"] = project["id"]
save_state()
project_id = entry["project"]
while True:
project = api("GET", f"/projects/{project_id}")
if project["status"] not in ("queued", "processing"):
break
time.sleep(5)
if project["status"] != "succeeded":
return f"{path.name}: не получился — {(project['error'] or {}).get('message', project['status'])}"
target = output_dir / f"{path.stem}.{project['result']['container']}"
with session.get(project["result"]["url"], stream=True, timeout=600) as video:
video.raise_for_status()
with target.open("wb") as out:
for chunk in video.iter_content(1 << 20):
out.write(chunk)
entry["done"] = True
save_state()
return f"{path.name}: готово → {target.name}"
# 1. Список файлов и бюджет
extensions = {f".{ext}" for ext in api("GET", "/styles")["materialLimits"]["videoExtensions"]}
files = sorted(p for p in source_dir.iterdir() if p.suffix.lower() in extensions)
todo = [p for p in files if not state.get(p.name, {}).get("project")]
price = api("POST", "/estimate", {"kind": "montage"})
me = api("GET", "/me")
budget = price["cost"] * len(todo)
limit_left = price["spendLimitLeft"]
print(f"Видео: {len(files)}, новых монтажей: {len(todo)}, бюджет: ~{budget} токенов, доступно: {me['wallet']['available']}")
if budget > me["wallet"]["available"] or (limit_left is not None and budget > limit_left):
sys.exit("Не хватает токенов или лимита расходов — уменьшите пакет или пополните баланс")
if input("Запустить? [y/N] ").strip().lower() != "y":
sys.exit(0)
# 2. Параллельная обработка
with ThreadPoolExecutor(max_workers=PARALLEL) as pool:
futures = {pool.submit(process, path): path for path in files}
for future in as_completed(futures):
try:
print(future.result())
except Exception as error: # один неудачный файл не останавливает остальные
print(f"{futures[future].name}: ошибка — {error}")Как это работает
- Бюджет заранее.
POST /v1/estimateназывает цену одного монтажа; скрипт умножает её на число новых файлов и сравнивает с балансом и остатком лимита токена. Первый бесплатный ролик в оценке тоже учитывается: если он ещё доступен, первая цена будет нулевой. - Подтверждение цены. Каждый монтаж запускается с
confirmCost, равным только что названной цене. Если цена изменится посреди пакета, монтаж не запустится (409 cost_changed), а файл попадёт в список ошибок. - Безопасный перезапуск.
state.jsonхранит id проектов и готовые файлы.Idempotency-Keyзапуска монтажа выводится из пути, размера и времени изменения файла: если скрипт упал между запуском монтажа и записью состояния, повтор в течение суток вернёт уже созданный проект, а не запустит второй. Загрузка при перезапуске делается заново — она бесплатна. - Параллельность. Три файла одновременно укладываются в лимиты с запасом: опрос раз в 5 секунд — это 36 запросов в минуту на три проекта.