招待して報酬

招待報酬の仕組み

招待リンクを共有します。友だちがリンクから登録してチャージすると、その後のチャージごとに表示された報酬を受け取れます。

Runwayでフレームレートを上げた動画を検証する方法

ローカル動画の準備とアップロード、enhance_frame_rate送信、タスク確認、出力保存からFPS・破綻・音声同期の検証までを通した手順です。

目次
Runwayでフレームレートを上げた動画を検証する方法

手元に完成済みのローカル動画があっても、Runwayでフレームレートを上げた出力はまだ存在しないかもしれません。検収の前に、ファイルを準備してアップロードし、targetFramerateを送信し、タスク完了を待って結果を保存する必要があります。この記事では、そのREST操作を最初から最後まで示し、その後にFPS、動きの破綻、編集点、音声同期を確認します。

Runwayは2026年9月17日、Runway Devにenhance_frame_rateを追加しました。動画アップスケール用エンドポイントを使い、24、25、30、48、50、60、120、23_98(23.98 fps)、29_97(29.97 fps)、59_94(59.94 fps)を指定できます。入力は最長300秒で、公開時の説明では2秒ごとに1 creditです。

「滑らかに見える」だけで合格にしないでください。まず納品先が求める正確なフレームレートを選び、メタデータ、問題が出やすい動き、編集点、冒頭・中盤・終盤の音声同期を確認してから、実際のタイムラインと配信先で試します。

まず納品フレームレートを決め、仕様が不明なら処理を止める

送信前に、編集タイムライン、放送仕様、広告プラットフォーム、またはクライアント指定から正確な値を選びます。29_97と30、59_94と60は似ていますが、長尺、放送、混在素材で取り違えると、再トランスコードや再処理が必要になります。

目標値判断の基準納品前に確認すること
23_98 / 24タイムラインやクライアントが映画系のフレームレートを明示している要求が整数24ではなく正確に23.98なのか
25 / 5025/50 fpsの制作系統、または地域別の納品仕様タイムライン、字幕、音声、他素材も同じ基準か
29_97 / 30下流システムがどちらかを明示している一方を他方で勝手に代用しないこと
59_94 / 60動きの多い映像、または高フレームレートを明示した配信先滑らかになっても失われた細部が復元されたとは限らないこと
48 / 120特定のタイムライン、スローモーション工程、高フレームレート納品下流工程が必要とする場合だけ使い、高いほど良いと考えないこと

依頼が「もっと滑らかにして」だけなら、最終納品仕様を確認します。確認しないと、技術的には正しい60 fpsファイルを作れても、59.94 fpsのタイムラインには適合しないことがあります。

元映像の基準値を残し、問題の発生場所を切り分ける

処理前に、元映像のフレームレート、長さ、コーデック、音声トラックを記録します。基準値がないと、トラック欠落、長さの変化、末尾の静止フレームが元映像、Runway出力、後段の変換のどこで生じたか判断しにくくなります。

  1. 元ファイル名、長さ、解像度、コーデック、元のフレームレート。
  2. 元動画が固定フレームレート(CFR)か可変フレームレート(VFR)か。
  3. 音声トラック数、サンプルレート、チャンネル数、おおよその長さ。
  4. 目標フレームレートと、その要求元。
  5. 高速パン、手、細線、遮蔽境界、フラッシュ、トランジション、字幕、UIオーバーレイなど、問題が出やすい3〜5個のタイムコード。
  6. 冒頭、中盤、終盤に少なくとも一つずつ、合計3個以上の音声同期基準点。

この基準値があれば、長さの変化、トラック欠落、納品仕様との不一致を見逃したまま「滑らかに見える」で検収を終えるのを防げます。

まずローカル動画がこの処理に入れられるか確認する

APIを呼ぶ前に、形式、長さ、ファイルサイズを確認します。enhance_frame_rateの入力は最大300秒で、ephemeral uploadは512バイト以上200 MB以下です。MP4/H.264、H.265、AV1など公式対応のコンテナとコーデックを優先し、長尺は自然なカット位置で分割します。

動画がobject storageにある場合は、HTTPS URLをvideoUriへ直接指定できます。URLはIPではなくドメインを使い、HEADに対応し、正しいContent-TypeとContent-Lengthを返し、リダイレクトに依存しない必要があります。URL入力の動画上限は32 MBです。通常のローカルマスターならephemeral uploadの方がこれらのホスティング条件を避けやすくなります。

購入済みcreditsがあることも確認します。公開時の説明は2秒あたり1 creditですが、端数の丸めは説明されていません。送信時のestimatedCostと完了後のcostを作業記録として保存してください。

このスクリプトでアップロード、送信、待機、ダウンロードを行う

次のREST例は重要な処理をSDKの待機メソッドに隠しません。API Keyを非表示で読み取り、ローカルの長さとサイズを検証し、ephemeral uploadを作成して動画を転送し、タスクを送信し、5秒ごとに状態を確認して成功した出力を保存します。

Python依存関係を入れ、ffprobeが使えることを確認します。

python3 -m pip install requests

次をrunway_fps.pyとして保存します。

from __future__ import annotations

import getpass, json, os, random, subprocess, sys, time
from pathlib import Path
import requests

API = "https://api.dev.runwayml.com"
FPS = {"24", "25", "30", "48", "50", "60", "120", "23_98", "29_97", "59_94"}
RETRYABLE = {429, 502, 503, 504}


def api(session, method, path, body=None):
    for attempt in range(6):
        response = session.request(method, API + path, json=body, timeout=60)
        if response.status_code < 400:
            return response
        if response.status_code in RETRYABLE and attempt < 5:
            time.sleep((2**attempt) * (1 + random.random() * 0.5))
            continue
        raise RuntimeError(f"HTTP {response.status_code}: {response.text}")
    raise RuntimeError("RETRY_LIMIT_REACHED")


def main():
    if len(sys.argv) not in {3, 4}:
        raise SystemExit("python runway_fps.py INPUT_VIDEO TARGET_FPS [OUTPUT_VIDEO]")

    source = Path(sys.argv[1])
    target = sys.argv[2]
    output = Path(sys.argv[3]) if len(sys.argv) == 4 else Path(f"runway-{target}fps.mp4")

    if target not in FPS:
        raise SystemExit(f"UNSUPPORTED_TARGET_FRAMERATE: {target}")
    if not source.is_file():
        raise SystemExit(f"INPUT_NOT_FOUND: {source}")
    if not 512 <= source.stat().st_size <= 200 * 1024 * 1024:
        raise SystemExit(f"INVALID_UPLOAD_SIZE_BYTES: {source.stat().st_size}")

    duration = float(subprocess.run(
        ["ffprobe", "-v", "error", "-show_entries", "format=duration",
         "-of", "default=noprint_wrappers=1:nokey=1", str(source)],
        check=True, capture_output=True, text=True,
    ).stdout.strip())
    if not 0 < duration <= 300:
        raise SystemExit(f"INVALID_DURATION_SECONDS: {duration}")

    key = os.getenv("RUNWAYML_API_SECRET") or getpass.getpass("RUNWAYML_API_SECRET: ")
    session = requests.Session()
    session.headers.update({
        "Authorization": f"Bearer {key}",
        "X-Runway-Version": "2024-11-06",
        "Content-Type": "application/json",
    })

    upload_init = api(session, "POST", "/v1/uploads", {
        "filename": source.name,
        "type": "ephemeral",
    }).json()
    with source.open("rb") as handle:
        upload = requests.post(
            upload_init["uploadUrl"],
            data=upload_init["fields"],
            files={"file": (source.name, handle)},
            timeout=300,
        )
    if upload.status_code >= 400:
        raise RuntimeError(
            f"UPLOAD_FAILED_REQUEST_NEW_UPLOAD: HTTP {upload.status_code}: {upload.text}"
        )

    created = api(session, "POST", "/v1/video_upscale", {
        "model": "enhance_frame_rate",
        "videoUri": upload_init["runwayUri"],
        "targetFramerate": target,
    }).json()
    task_id = created["id"]
    print(json.dumps({"id": task_id, "estimatedCost": created.get("estimatedCost")}, indent=2))

    while True:
        task = api(session, "GET", f"/v1/tasks/{task_id}").json()
        status = task["status"]
        if status in {"PENDING", "THROTTLED", "RUNNING"}:
            time.sleep(5)
            continue
        if status == "SUCCEEDED":
            urls = task.get("output") or []
            if not urls:
                raise RuntimeError("SUCCEEDED_WITHOUT_OUTPUT")
            with requests.get(urls[0], stream=True, timeout=300) as download:
                download.raise_for_status()
                with output.open("wb") as saved:
                    for chunk in download.iter_content(1024 * 1024):
                        if chunk:
                            saved.write(chunk)
            break
        if status == "FAILED":
            raise RuntimeError(json.dumps({
                "status": status,
                "failure": task.get("failure"),
                "failureCode": task.get("failureCode"),
                "cost": task.get("cost"),
            }, ensure_ascii=False))
        if status == "CANCELLED":
            raise RuntimeError(json.dumps({"status": status, "cost": task.get("cost")}))
        raise RuntimeError(f"UNKNOWN_TASK_STATUS: {status}")

    subprocess.run([
        "ffprobe", "-v", "error", "-show_entries",
        "stream=codec_name,width,height,r_frame_rate,avg_frame_rate,time_base,duration:format=duration",
        "-of", "json", str(output),
    ], check=True)
    print(output.resolve())


if __name__ == "__main__":
    main()

input.mp4を60 fpsへ変換し、output-60fps.mp4として保存する例です。

python3 runway_fps.py input.mp4 60 output-60fps.mp4

RUNWAYML_API_SECRET:と表示された時点でキーを入力します。入力は画面に出ず、shell履歴にも残らず、スクリプトもファイルへ保存しません。同名の環境変数が安全に設定済みなら、その値を読み取ります。

スクリプトが呼ぶ3段階のAPIを理解する

3段階すべてが成功して初めて生成完了です。フィールド名は大文字小文字を区別し、REST JSONではvideoUriとtargetFramerateを使います。

段階リクエスト必須内容成功の目印
アップロード初期化POST https://api.dev.runwayml.com/v1/uploadsfilename、type: "ephemeral"uploadUrl、fields、runwayUriが返る
フレームレート処理送信POST https://api.dev.runwayml.com/v1/video_upscalemodel: "enhance_frame_rate"、videoUri、targetFramerateタスクidとestimatedCostが返る
タスク取得GET https://api.dev.runwayml.com/v1/tasks/{id}パス内のタスクIDstatus: "SUCCEEDED"かつoutputが空でない

初期化後はuploadUrlへmultipart POSTを送り、返されたfieldsをすべてそのまま含め、動画をfileフィールドとして添付します。この転送が成功してからrunwayUriを処理要求に使えます。URIの有効時間は24時間です。

成功したらすぐに出力をダウンロードして保存する

PENDING、THROTTLED、RUNNINGの間は待機します。Runwayは同じタスクについて5秒より短い間隔で更新を期待しないよう説明しています。output[0]を読むのはSUCCEEDEDのときだけです。FAILEDとCANCELLEDは失敗の終端状態です。

出力URLは通常24〜48時間で期限切れになるため、すぐに自分の永続ストレージへ保存し、一時URLを最終納品リンクとして渡さないでください。期限切れなら新しい有料生成を始める前に、同じタスクを再取得して新しいURLを得ます。ダウンロード成功はAPIタスク完了を示すだけで、下記の検収は引き続き必要です。

エラーの種類に応じて処理し、すべてを盲目的に再試行しない

uploadUrlへのmultipart POSTが失敗したら、同じpresigned uploadを再利用せず、/v1/uploadsを再度呼びます。400、401、404、405は入力、キー、リソース、メソッドを修正してください。例の自動再試行対象は429、502、503、504だけで、指数バックオフとjitterを使います。

タスクがFAILEDならfailure、failureCode、costを保存します。SAFETY.*は再試行せず、ASSET.INVALIDは素材を修正してから再送し、INTERNAL.BAD_OUTPUT.*は入力問題を確認してから判断します。INPUT_PREPROCESSING.INTERNAL、INTERNAL、コードなし、THIRD_PARTY.UNAVAILABLEは待ってから再試行できます。同じ要求を無限に繰り返さないでください。

手順1:ffprobeで目標値に達していることを確認する

画面を見る前に、ffprobeで平均レート、タイムベース、実フレーム数、長さ、音声ストリームを確認します。Finder、エクスプローラー、プレイヤーの単一FPS表示だけでは合否を判断できません。

ffprobe -v error -select_streams v:0 \
  -show_entries stream=codec_name,width,height,r_frame_rate,avg_frame_rate,time_base,duration \
  -of json output.mp4
ffprobe -v error -select_streams v:0 -count_frames \
  -show_entries stream=nb_read_frames,avg_frame_rate,r_frame_rate,duration \
  -of json output.mp4
ffprobe -v error \
  -show_entries format=duration:stream=index,codec_type,codec_name,sample_rate,channels,duration \
  -of json output.mp4

確認項目は次のとおりです。

  • avg_frame_rateが目標値、または同等の有理数表現になっている。
  • r_frame_rateとavg_frame_rateが理由なく大きく食い違っていない。大差がある場合はVFRとして詳しく調べる。
  • ほぼCFRのファイルなら、nb_read_framesがおおむね「長さ×目標fps」に近い。
  • 出力の長さが元動画と一致し、末尾フレームの欠落や不要な静止フレーム追加がない。
  • 解像度、コーデック、音声ストリームが納品仕様に合う。
  • 音声ストリームの長さが映像ストリームと不自然に異なっていない。

29.97と59.94では、ツールが30000/1001や60000/1001のような分数で表示することがあります。分数表示だけを失敗と判断してはいけません。

手順2:破綻しやすいカットから先に確認する

高速な動き、遮蔽境界、細かい文字、編集点から先に確認します。補間の破綻が見えやすいため、次の場面を100%表示でコマ送りまたは低速再生してください。

  • 高速パン、追従撮影、高速で動く物体。
  • 手、指、髪、眼鏡のフレーム、唇。
  • フェンス、ブラインド、格子、小さな文字、細いUI線。
  • 前景物が背景の輪郭を横切る、または隠れていた背景を見せる場面。
  • 水、煙、粒子、葉、高周波の細かな質感。
  • フラッシュ、ハードカット、ディゾルブ、ショット切り替え前後のフレーム。

漠然とした「鮮明さ」ではなく、再現可能な欠陥を探します。二重輪郭、残像、曲がった輪郭、1フレームだけ消える物体、脈動する質感、変形した手足、カット位置で混ざるフレーム、静止字幕の揺れなどです。

問題を見つけたら、正確なタイムコード、目標レート、元動画の該当部分、出力の該当部分を記録します。これにより、元動画にすでに存在した欠陥、新たに生じた問題、再生デコーダーの差を切り分けやすくなります。

手順3:冒頭・中盤・終盤で音声同期を確認する

冒頭が合っていても全体の同期は保証されません。固定オフセットと時間とともに増えるずれを区別するため、冒頭・中盤・終盤を次の順で確認します。

  1. 冒頭付近で、手拍子、破裂音、衝撃、着地、視覚的なカットなど明確な基準点を探す。
  2. 中盤と終盤でも同じ確認を行う。
  3. 3地点でほぼ同じずれなら、固定遅延の可能性が高い。
  4. 後半ほど誤差が大きくなるなら、長さ、タイムベース、フレームレート解釈の問題を疑う。
  5. リップシンク素材は一つの音節だけでなく、連続した発話の冒頭、中盤、終盤を確認する。

リリースノートには音声の扱いが説明されていません。音声トラックが常に変更なく保持される、または自動的に同期されると仮定せず、実際に納品するファイルで判断してください。

手順4:実際のタイムラインと配信先でもう一度試す

実際の編集タイムラインと最終プラットフォームで必ず確認します。NLEや二次トランスコードがフレームレートを再解釈し、速度を変えたり音声トラックを失ったりすることがあるため、最低でも次の二つを試してください。

  • 実際の編集タイムラインに置き、NLEがフレームレートを別解釈したり、速度を変えたり、音声トラックを失ったりしないことを確認する。
  • 最終プラットフォームまたは再生環境で試し、二次トランスコードによってフレームレート、字幕、同期が変わっていないことを確認する。

配信先が再エンコードする場合は、Runway出力と配信先で生成された版を両方保存し、別々に確認します。上流ファイルの問題と決めつける前に比較してください。

この表で合格・再処理・元映像維持を判断する

重要項目がすべて納品条件を満たした場合だけ合格にします。1カットだけ失敗したなら、全編を再処理する前に、その区間だけ再処理するか元映像を残してください。

確認項目合格条件不合格時の対応
目標フレームレート納品仕様どおりで、29.97/59.94を30/60と誤表示していない目標値またはタイムライン解釈を修正する
長さとフレーム数元動画と長さが一致し、CFRのフレーム数がおおむね期待値に近いVFR、切り捨て、末尾静止、タイムベースを調べる
解像度とコーデック編集者または配信先の要件に適合する納品仕様に合わせてリラップまたはトランスコードする
高速な動き許容できない二重像、曲がり、物体消失がないタイムコードを記録し、別の目標値を試すか元区間を残す
カットとフラッシュ編集点の前後に混合、重複、異常な点滅フレームがない自然なカットで分割し、再処理後に継ぎ目を確認する
文字とUI文字、細線、静止オーバーレイが安定している画像と一緒に処理せず、後工程でグラフィックを重ね直す
音声同期冒頭、中盤、終盤で見えるずれやドリフトがないストリーム長とタイムベースを比較し、再同期または変換する
ファイル完全性全体をデコードでき、末尾と音声トラックが完全である再ダウンロード、リラップ、またはタスクを再実行する

60から120 fpsへ上げないほうがよい条件

120 fpsが容量と後工程の負荷だけを増やすなら、納品仕様を満たす低い値で止めます。次の条件ではフレームレートを上げないでください。

  • 下流仕様が24、25、29.97、30 fpsのいずれかしか要求していない。
  • 元動画に強い残像、圧縮ブロック、モーションブラーがある。
  • 字幕、UI、細線が不安定になる。
  • 音声同期の問題をまだ説明できていない。
  • 最終プラットフォームが低いフレームレートへ強制変換する。
  • 高フレームレート版に目に見える利点がなく、保存容量、デコード負荷、後段変換負荷だけが増える。

フレームレートは納品パラメーターであり、単独の品質点数ではありません。合格基準は「数字が最大」でなく、「要求されたフレームレートに合い、許容できない新しい欠陥がない」です。

10段階で元動画から納品まで完了する

手戻りを減らす順序は、仕様と基準、アップロードと送信、待機と保存、そして技術・目視・実環境の検証です。

  1. 下流仕様から正確なtargetFramerateを選ぶ。
  2. ffprobeで元動画のレート、長さ、音声、リスクの高いタイムコードを保存し、300秒以内と確認する。
  3. ローカルファイルはPOST /v1/uploadsを呼び、uploadUrl、fields、runwayUriを保存する。
  4. uploadUrlへmultipartフォームを送り、失敗したら新しいアップロードを申請する。
  5. model、videoUri、targetFramerateでPOST /v1/video_upscaleを呼ぶ。
  6. タスクidとestimatedCostを保存する。
  7. 終端状態まで5秒ごとにGET /v1/tasks/{id}を呼ぶ。
  8. SUCCEEDEDならoutputをダウンロードして永続保存し、FAILEDまたはCANCELLEDは種類別に処理する。
  9. ffprobeとコマ送りで実レート、長さ、破綻、編集点、音声同期を確認する。
  10. 納品前に実際のタイムラインと配信先で試す。

公式資料:Models、Inputs、Uploads、Video upscale API Reference、Task API Reference、Outputs、HTTP errors、Task failures、API Changelog。インターフェースのフィールドと手順は2026年9月26日に確認しました。

LLM ワークフローを最適化しませんか?

単一 API でモデルを接続し、キーと AI コストを管理できます。

無料で始める