宣伝用に作った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)を活用して作成し、記事中の手順は実際に投稿して確認しています。