0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GIFをMP4に変えて、Threads API で動画として投稿する(Python・標準ライブラリだけ)

0
Posted at

宣伝用に作ったGIFアニメ(ダッシュボード風の画面が動くもの)を、Threads に投稿したくなりました。

ところが Threads API の投稿の種類は TEXT・IMAGE・VIDEO・CAROUSEL で、GIF をそのまま動くまま載せる方法は見当たりませんでした。そこで、GIF を MP4 に変えてから VIDEO として投稿しました。この記事はその手順のメモです。

確認した環境:Python 3.12(標準ライブラリだけ)、ffmpeg 7.1、Threads API v1.0(2026年10月時点)。

1. GIF を MP4 に変える

ffmpeg -y -stream_loop 1 -i studio_anim_1200x675.gif \
  -movflags +faststart -pix_fmt yuv420p \
  -vf "scale=1200:676:flags=lanczos,fps=30" \
  -c:v libx264 -crf 20 studio_anim.mp4

ポイントは3つです。

  • -pix_fmt yuv420p:GIF から作ると色の形式が変わることがあり、そのままだと再生できない環境があります。多くの環境で再生できる yuv420p にそろえます。
  • 縦横を偶数にする:yuv420p の H.264 では、幅と高さが偶数である必要があります。元の GIF が 1200×675 だったので、scale=1200:676 で偶数にしました。
  • -stream_loop 1:GIF は短いので、2回分つなげて少し長い動画にしました(約16秒)。-movflags +faststart は、ネットで再生を始めやすくするための指定です。

できた MP4 は約350KB でした。

2. 動画を公開URLに置く

Threads API は、ファイルを直接アップロードするのではなく、公開されたURLから動画を取りに来ます。そこで、自分のサイト(GitHub Pages)に置いてから投稿しました。

curl -sI https://example.com/sns/studio/fudosan.mp4 | grep -iE "^HTTP|content-type"
# HTTP/2 200
# content-type: video/mp4

content-type: video/mp4 で返ってくることを確かめてから次に進みます。push 直後はまだ 404 のことがあるので、200 になるまで待ちました。

3. Threads API で投稿する

流れは、画像のときと同じく 「入れ物(コンテナ)を作る → 準備ができるまで待つ → 公開する」 の3段階です。違うのは media_type=VIDEO と video_url を使うところです。

import json, os, ssl, sys, time, urllib.parse, urllib.request

API = "https://graph.threads.net/v1.0"
TOKEN = open(os.path.expanduser("~/.config/xxx/threads_token")).read().strip()
CTX = ssl.create_default_context(cafile="/etc/ssl/cert.pem")

def call(method, path, **params):
    params["access_token"] = TOKEN
    data = urllib.parse.urlencode(params).encode()
    url = f"{API}/{path}"
    req = (urllib.request.Request(url + "?" + data.decode()) if method == "GET"
           else urllib.request.Request(url, data=data, method="POST"))
    try:
        with urllib.request.urlopen(req, timeout=60, context=CTX) as r:
            return json.load(r)
    except urllib.error.HTTPError as e:
        # エラー文にトークンが混ざっても画面に出さない
        sys.exit(f"API error {e.code}: {e.read().decode(errors='replace').replace(TOKEN, '***')[:500]}")

def wait_ready(cid):
    for _ in range(60):  # 3秒 × 60回 = 最大3分
        st = call("GET", cid, fields="status").get("status")
        if st == "FINISHED":
            return
        if st in ("ERROR", "EXPIRED"):
            sys.exit(f"container {cid} {st}")
        time.sleep(3)

def post_video(text, video_url):
    c = call("POST", "me/threads", media_type="VIDEO", video_url=video_url, text=text)["id"]
    wait_ready(c)          # 動画はサーバー側の処理に時間がかかる
    mid = call("POST", "me/threads_publish", creation_id=c)["id"]
    return call("GET", mid, fields="permalink")["permalink"]

画像のときは数秒で FINISHED になりましたが、動画は処理に時間がかかることがあるので、待つ回数を多めにしています。準備ができる前に threads_publish を呼ぶとエラーになるので、待つ処理は省かないほうが安全です。

4. リンクは返信に置く

本文にリンクを入れると宣伝っぽく見えやすいので、本文は話題だけにして、リンクは自分の返信に置くようにしました。返信は reply_to_id を付けるだけです。

def reply(parent_id, text):
    c = call("POST", "me/threads", media_type="TEXT", text=text, reply_to_id=parent_id)["id"]
    wait_ready(c)
    return call("POST", "me/threads_publish", creation_id=c)["id"]

つまずいたところ

  • 一時的なエラー:画像の投稿で「メディアを取得できません」というエラー(コード 2207052)が一度出ました。URLは正しく開けていて、数分後に同じ内容で再実行したら成功しました。すぐにあきらめず、少し待って再実行すると通ることがあります。
  • トークンの扱い:エラー文にURLごとトークンが含まれることがあるので、表示する前に *** に置き換えています。

まとめ

  • GIF は yuv420p・偶数サイズの MP4 に変える
  • 公開URLに置いて、content-type: video/mp4 を確かめる
  • media_type=VIDEO でコンテナを作り、FINISHED まで待ってから公開する
  • リンクは返信に置くと、本文が読みやすくなる

API の仕様は変わることがあるので、使う前に Meta の公式ドキュメントも確かめてください。


※この記事は、AI(Claude)を活用して作成し、記事中の手順は実際に投稿して確認しています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?