開発者向け··5 分で読めます

Bigjpg API 入門:Python で画像をまとめて拡大する

そのまま動くスクリプト一式。画像のアップロード、拡大タスクの作成、結果のポーリング、ダウンロードまで、500 枚の処理を途中で止めないための並列数とエラー処理つき。

Bigjpg API 入門:Python で画像をまとめて拡大する

数枚なら Web 画面で十分です。500 枚のフォルダを前にしたら API のほうが早い。エンドポイントは 4 つ、ブラウザは不要、スクリプトは何度でも回し直せます。以下は私たちが同僚にそのまま渡すバージョンです。アップロード、作成、ポーリング、ダウンロード、そして実務で効いてくる失敗時の処理まで入っています。

始める前に

  • API は有料アカウント向けの機能です。ログインして有効なプランがあれば、キーは API リファレンス に表示されます。
  • Python 3 と requests だけ。他は何もいりません。
  • 拡大はワーカー上で非同期に走ります。スクリプトはタスクを作り、あとはポーリングするだけ。500 枚の処理時間は画像の合計ではなく、自分が並んでいるキューの一番遅い処理で決まります。

スクリプト

import json
import os
import time
import requests

API = "https://bigjpg.com"
KEY = "YOUR_API_KEY"
STYLE = "art"          # "art" はイラスト、"photo" は写真
NOISE = "1"            # -1 なし、0 弱、1 中、2 強、3 最強
X2 = "2"               # 1 = 2x、2 = 4x、3 = 8x、4 = 16x
POLL_SECONDS = 8
MAX_RETRY = 1

HEADERS = {"X-API-KEY": KEY}


def upload(path):
    """Get a presigned URL, PUT the file, return its public URL."""
    name = path.split("/")[-1]
    r = requests.post(
        API + "/api/img_upload",
        headers=HEADERS,
        data={"filename": name, "filetype": "image/jpeg"},
        timeout=60,
    )
    info = r.json()
    if info.get("status") != "ok":
        raise RuntimeError("upload failed: " + str(info))

    with open(path, "rb") as image:
        r = requests.put(
            info["url"],
            data=image,
            headers={"Content-Type": "image/jpeg"},
            timeout=300,
        )
    if r.status_code != 200:
        raise RuntimeError("upload failed with HTTP %s" % r.status_code)
    return info["fileurl"]


def create(image_url):
    """Create an enlargement task, return its task id."""
    payload = {
        "style": STYLE,
        "noise": NOISE,
        "x2": X2,
        "input": image_url,
    }
    r = requests.post(
        API + "/api/task/",
        headers=HEADERS,
        data=json.dumps(payload),
        timeout=60,
    )
    data = r.json()
    if "tid" not in data:
        raise RuntimeError("task failed: " + str(data))
    return data["tid"]


def poll(tids):
    """Query up to 100 task ids at once. Returns task id -> result dict."""
    r = requests.get(API + "/api/task/" + ",".join(tids), timeout=60)
    return r.json()


def download(url, out):
    """Download a finished image, checking the transfer really succeeded."""
    r = requests.get(url, timeout=300)
    if r.status_code != 200:
        raise RuntimeError("download failed with HTTP %s" % r.status_code)
    with open(out, "wb") as f:
        f.write(r.content)
    return out


def main(paths):
    pending = {}
    attempts = {}
    for path in paths:
        try:
            tid = create(upload(path))
        except Exception as e:
            print("skipped", path, e)
            continue
        pending[tid] = path
        attempts[tid] = 0
        print("queued", path, tid)

    while pending:
        for tid, info in poll(list(pending)[:100]).items():
            if info.get("status") == "success":
                out = os.path.splitext(pending[tid])[0] + "_bigjpg.jpg"
                try:
                    download(info["url"], out)
                except Exception as e:
                    print("download failed", pending[tid], e)
                    pending.pop(tid)
                    continue
                print("done", pending.pop(tid), "->", out)

            elif info.get("status") == "error":
                # 一度だけリトライし、それでもだめならこのファイルは捨てる。
                attempts[tid] += 1
                if attempts[tid] <= MAX_RETRY:
                    requests.post(API + "/api/task/" + tid, headers=HEADERS, timeout=60)
                    print("retrying", pending[tid])
                else:
                    print("giving up on", pending.pop(tid))

        if pending:
            time.sleep(POLL_SECONDS)


if __name__ == "__main__":
    import sys
    main(sys.argv[1:])

各エンドポイントの役割

呼び出し役割補足
POST /api/img_upload署名付きのアップロード URL と公開画像 URL を返す宣言した Content-Type のまま PUT で送り、返ってきた fileurl をタスクの input に使う
POST /api/task/拡大タスクを作成するtid、処理時間の目安 minute、remaining_api_calls を返す
GET /api/task/{ids}1 件または複数件の状態を取得するid はカンマ区切り、1 リクエストあたり最大 100 件
POST /api/task/{ids}失敗したタスクを再実行するerror 状態のタスクだけがキューに戻る

並列数とマナー

  • ポーリングは思っているより間隔を空ける。拡大は一瞬では終わりません。5〜10 秒に一度で十分で、100 件をまとめて聞けばリクエスト数も抑えられます。
  • アップロードは絞る。1 台あたり同時 2〜4 本が妥当な上限です。もっと速くしたいならスレッドを増やすのではなく、インスタンスを増やしてください。
  • バックオフつきで再試行する。ネットワークエラーはタスクの失敗ではありません。待ち時間を伸ばしながら HTTP 呼び出しを 3 回までやり直し、それでもだめなら壊れたファイルとして扱います。
  • remaining_api_calls を見る。タスク作成のたびに返ってきます。低めのしきい値で止めれば、エラーの山を見てから上限に気づく羽目になりません。
  • 対応表を残す。tid、入力ファイル名、設定、出力ファイル名を 1 つのファイルに書いておく。夜通し回す処理では、そのログだけが何が終わったかの証拠になります。

実際に踏むエラー

  • valid_api_key_required — ヘッダーがないか、キーが不明です。ヘッダー名と、キーのプランが期限切れでないかを確認してください。
  • requires_vip — 無料アカウントか期限切れ。API は有効な有料プラン向けです。
  • month_limit — 月の割り当てを使い切りました。残量は各タスクが返すので、その前に自分で止められます。
  • param_error — style は art か photo、noise は -1〜3、x2 は 1〜4 です。
  • input_url_error — input には外部から取得できる http か https の画像 URL が必要です。署名付きの非公開 URL は失効します。先にアップロードして、返ってきた公開 URL を渡してください。

バッチのコスト

2x・4x・8x は 1 枚 1 クレジット、16x は 2 クレジット。500 枚を 4x で回せば 500 クレジット、同じ 500 枚を 16x にすると 1000 です。写真の多くはどのみち 4x が適正なので、安いほうが仕上がりも良いという結果になりがちです。理由は 倍率ガイド にまとめました。

パラメータとエラー状態を含む完全なリファレンスは API ページ にあります。このスクリプトと実際の挙動が食い違ったら、あのページが最新版です。

← 記事一覧