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

手元に完成済みのローカル動画があっても、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 / 50 | 25/50 fpsの制作系統、または地域別の納品仕様 | タイムライン、字幕、音声、他素材も同じ基準か |
29_97 / 30 | 下流システムがどちらかを明示している | 一方を他方で勝手に代用しないこと |
59_94 / 60 | 動きの多い映像、または高フレームレートを明示した配信先 | 滑らかになっても失われた細部が復元されたとは限らないこと |
48 / 120 | 特定のタイムライン、スローモーション工程、高フレームレート納品 | 下流工程が必要とする場合だけ使い、高いほど良いと考えないこと |
依頼が「もっと滑らかにして」だけなら、最終納品仕様を確認します。確認しないと、技術的には正しい60 fpsファイルを作れても、59.94 fpsのタイムラインには適合しないことがあります。
元映像の基準値を残し、問題の発生場所を切り分ける
処理前に、元映像のフレームレート、長さ、コーデック、音声トラックを記録します。基準値がないと、トラック欠落、長さの変化、末尾の静止フレームが元映像、Runway出力、後段の変換のどこで生じたか判断しにくくなります。
- 元ファイル名、長さ、解像度、コーデック、元のフレームレート。
- 元動画が固定フレームレート(CFR)か可変フレームレート(VFR)か。
- 音声トラック数、サンプルレート、チャンネル数、おおよその長さ。
- 目標フレームレートと、その要求元。
- 高速パン、手、細線、遮蔽境界、フラッシュ、トランジション、字幕、UIオーバーレイなど、問題が出やすい3〜5個のタイムコード。
- 冒頭、中盤、終盤に少なくとも一つずつ、合計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/uploads | filename、type: "ephemeral" | uploadUrl、fields、runwayUriが返る |
| フレームレート処理送信 | POST https://api.dev.runwayml.com/v1/video_upscale | model: "enhance_frame_rate"、videoUri、targetFramerate | タスクidとestimatedCostが返る |
| タスク取得 | GET https://api.dev.runwayml.com/v1/tasks/{id} | パス内のタスクID | status: "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:冒頭・中盤・終盤で音声同期を確認する
冒頭が合っていても全体の同期は保証されません。固定オフセットと時間とともに増えるずれを区別するため、冒頭・中盤・終盤を次の順で確認します。
- 冒頭付近で、手拍子、破裂音、衝撃、着地、視覚的なカットなど明確な基準点を探す。
- 中盤と終盤でも同じ確認を行う。
- 3地点でほぼ同じずれなら、固定遅延の可能性が高い。
- 後半ほど誤差が大きくなるなら、長さ、タイムベース、フレームレート解釈の問題を疑う。
- リップシンク素材は一つの音節だけでなく、連続した発話の冒頭、中盤、終盤を確認する。
リリースノートには音声の扱いが説明されていません。音声トラックが常に変更なく保持される、または自動的に同期されると仮定せず、実際に納品するファイルで判断してください。
手順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段階で元動画から納品まで完了する
手戻りを減らす順序は、仕様と基準、アップロードと送信、待機と保存、そして技術・目視・実環境の検証です。
- 下流仕様から正確な
targetFramerateを選ぶ。 ffprobeで元動画のレート、長さ、音声、リスクの高いタイムコードを保存し、300秒以内と確認する。- ローカルファイルは
POST /v1/uploadsを呼び、uploadUrl、fields、runwayUriを保存する。 uploadUrlへmultipartフォームを送り、失敗したら新しいアップロードを申請する。model、videoUri、targetFramerateでPOST /v1/video_upscaleを呼ぶ。- タスク
idとestimatedCostを保存する。 - 終端状態まで5秒ごとに
GET /v1/tasks/{id}を呼ぶ。 SUCCEEDEDならoutputをダウンロードして永続保存し、FAILEDまたはCANCELLEDは種類別に処理する。ffprobeとコマ送りで実レート、長さ、破綻、編集点、音声同期を確認する。- 納品前に実際のタイムラインと配信先で試す。
公式資料:Models、Inputs、Uploads、Video upscale API Reference、Task API Reference、Outputs、HTTP errors、Task failures、API Changelog。インターフェースのフィールドと手順は2026年9月26日に確認しました。