Invite & Earn

How invite rewards work

Share your invite link. When a friend registers through it and tops up, you receive the displayed reward on their subsequent top-ups.

Runway Frame-Rate Enhancement: How to Verify the Final Video

Start with local-video preparation, ephemeral upload, enhance_frame_rate submission, task polling, and output saving, then verify FPS, artifacts, cuts, and audio sync.

Contents
Runway Frame-Rate Enhancement: How to Verify the Final Video

You may have a finished local video but no Runway frame-rate output yet. Before acceptance testing, you need to prepare and upload the file, submit targetFramerate, wait for the task, and save the result. This guide covers that complete REST path and then shows how to verify target FPS, motion artifacts, edit points, and audio sync before delivery.

Runway added enhance_frame_rate to Runway Dev on September 17, 2026. It uses the video upscale endpoint and accepts 24, 25, 30, 48, 50, 60, 120, 23_98 (23.98 fps), 29_97 (29.97 fps), and 59_94 (59.94 fps); each input is limited to 300 seconds, and the launch note states billing at 1 credit per 2 seconds.

Do not approve the result only because it looks smoother. Choose the exact downstream cadence first, then check metadata, high-risk motion, edit points, and sync at the beginning, middle, and end before testing in the real timeline and delivery platform.

Choose the delivery cadence first; pause if no exact specification exists

Choose the target from the editing timeline, broadcaster, ad platform, or client specification before you submit anything. 29_97 and 30, or 59_94 and 60, may look almost identical, but substituting one for the other can force a new transcode or a complete rerun in long-form, broadcast, and mixed-source work.

TargetTypical decision basisConfirm before delivery
23_98 / 24The timeline or client explicitly requires a film-style cadenceWhether the requirement is precisely 23.98 rather than integer 24
25 / 50A 25/50 fps production chain or regional delivery specificationTimeline, captions, audio, and other footage use the same cadence
29_97 / 30The downstream system explicitly names one of themDo not silently substitute one for the other
59_94 / 60High-motion content or a platform that explicitly asks for high frame rateSmoother playback does not mean lost detail has been restored
48 / 120A specific timeline, slow-motion workflow, or high-frame-rate deliveryUse only when the downstream workflow requires it; higher is not automatically better

When the brief only says “make it smoother,” ask for the final delivery specification. Otherwise, you can produce a technically valid 60 fps file that still does not belong on a 59.94 fps timeline.

Save the source baseline so you can trace any failure

Record the source frame rate, duration, codec, and audio tracks before processing. Without that baseline, a missing track, changed duration, or frozen tail is much harder to attribute to the source, the Runway output, or a later transcode.

  1. Source filename, duration, dimensions, codec, and original frame rate.
  2. Whether the source is constant frame rate (CFR) or variable frame rate (VFR).
  3. Audio-track count, sample rate, channel count, and approximate duration.
  4. The target frame rate and where the requirement came from.
  5. Three to five high-risk timecodes: fast pans, hands, thin lines, occlusion edges, flashes, transitions, captions, or UI overlays.
  6. At least three audio-sync anchors near the beginning, middle, and end.

This baseline prevents a review from collapsing into “it looks smoother” while a changed duration, missing track, or delivery mismatch goes unnoticed.

Confirm that the local video can enter this workflow

Check format, duration, and size before calling the API. A single enhance_frame_rate input can be no longer than 300 seconds; an ephemeral upload must be between 512 bytes and 200 MB. Prefer a supported container and codec such as MP4 with H.264, H.265, or AV1, and split longer programs at natural cuts before processing each part.

When the video already lives in object storage, you may put its HTTPS URL directly in videoUri. The URL must use a domain rather than an IP address, support HEAD, return valid Content-Type and Content-Length headers, and not depend on redirects; the video URL limit is 32 MB. For an ordinary local master, an ephemeral upload avoids those hosting requirements.

Also confirm that the account has purchased credits. The launch note states 1 credit per 2 seconds, but it does not specify rounding for partial units, so retain estimatedCost from submission and final cost from the task record.

Use this script to upload, submit, wait, and download

This REST example keeps every critical step visible instead of hiding the flow inside an SDK wait helper. It securely prompts for the API key, validates local duration and size, creates an ephemeral upload, transfers the file, submits the frame-rate task, polls every five seconds, and downloads the successful output.

Install the Python dependency and make sure ffprobe is available:

python3 -m pip install requests

Save the following as 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()

For example, convert input.mp4 to 60 fps and save it as output-60fps.mp4:

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

Enter the key only when the terminal displays RUNWAYML_API_SECRET:. The prompt does not echo the value or put it in shell history, and the script does not write it to disk. If the environment variable is already set securely, the script reads it instead.

Understand the three API stages in the script

Generation is complete only after all three stages succeed. Field names are case-sensitive: REST JSON must use videoUri and targetFramerate.

StageRequestRequired contentSuccess signal
Initialize uploadPOST https://api.dev.runwayml.com/v1/uploadsfilename, type: "ephemeral"uploadUrl, fields, and runwayUri are returned
Submit enhancementPOST https://api.dev.runwayml.com/v1/video_upscalemodel: "enhance_frame_rate", videoUri, targetFramerateTask id and estimatedCost are returned
Retrieve taskGET https://api.dev.runwayml.com/v1/tasks/{id}Task ID in the pathstatus: "SUCCEEDED" and a non-empty output array

After upload initialization, send a multipart POST to uploadUrl, preserving every returned entry in fields and attaching the video under the field name file. Only after that transfer succeeds is runwayUri ready for the enhancement request. The URI remains valid for 24 hours.

Download and persist the result immediately after success

Continue waiting while the task is PENDING, THROTTLED, or RUNNING; Runway says clients should not expect more than one update within five seconds for the same task. Read output[0] only after SUCCEEDED. Treat FAILED and CANCELLED as terminal failures.

Output URLs normally expire within 24–48 hours, so download the asset promptly into your own persistent storage and do not expose the temporary URL as the final delivery link. If a URL has expired, retrieve the same task again for a fresh URL instead of immediately paying for another generation. A successful download proves only that the API job completed; the file still needs the acceptance checks below.

Handle failures by type instead of retrying everything

If the multipart POST to uploadUrl fails, do not reuse that presigned upload. Call /v1/uploads again and restart the upload. For API responses 400, 401, 404, or 405, correct the input, key, resource, or method first. The sample retries only 429, 502, 503, and 504 with exponential backoff and jitter.

When a task is FAILED, retain failure, failureCode, and cost: do not retry SAFETY.*; repair the media before resubmitting ASSET.INVALID; investigate input problems before retrying INTERNAL.BAD_OUTPUT.*; and wait before retrying INPUT_PREPROCESSING.INTERNAL, INTERNAL, a missing failure code, or THIRD_PARTY.UNAVAILABLE. Never loop the same request indefinitely.

Step 1: Use ffprobe to confirm the file actually meets the target

Use ffprobe to verify average rate, time base, actual frame count, duration, and audio streams before judging the picture. A single FPS label in Finder, File Explorer, or a player is not an acceptance test.

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

Check the following:

  • avg_frame_rate matches the target or an equivalent rational representation.
  • r_frame_rate and avg_frame_rate are not in unexplained conflict; a large difference may require a VFR investigation.
  • For a near-CFR file, nb_read_frames is reasonably close to duration multiplied by target fps.
  • Output duration matches the source, with no missing ending frames or added frozen tail.
  • Dimensions, codec, and audio streams meet the delivery specification.
  • Audio-stream duration is not unexpectedly different from video-stream duration.

For 29.97 and 59.94, tools commonly report fractions such as 30000/1001 and 60000/1001. A fractional display is not a failure.

Step 2: Inspect the shots most likely to fail

Start with fast motion, occlusion edges, fine text, and edit points because these areas expose interpolation failures quickly. Inspect the following at 100% scale, frame by frame or at reduced playback speed:

  • fast pans, tracking shots, and fast-moving objects;
  • hands, fingers, hair, eyeglass frames, and lips;
  • fences, blinds, grids, small text, and thin UI lines;
  • foreground objects crossing or revealing background edges;
  • water, smoke, particles, leaves, and high-frequency textures;
  • flashes, hard cuts, dissolves, and the frames surrounding a shot change.

Look for reproducible defects rather than a vague sense of sharpness: double contours, ghosting, bent edges, objects disappearing for a frame, pulsing texture, deformed limbs, blended frames at a cut, or jitter in static text.

When you find a problem, record the exact timecode, target rate, source excerpt, and output excerpt. That makes it possible to separate a defect already present in the source from a new issue or a playback-decoder difference.

Step 3: Check sync at the beginning, middle, and end

A clean beginning does not prove that the whole file is in sync; check the beginning, middle, and end to distinguish a fixed offset from progressive drift. Use the following sequence:

  1. Find a clear anchor near the beginning, such as a clap, plosive consonant, impact, landing, or visual cut.
  2. Repeat the check near the middle and end.
  3. Similar offsets at all three locations suggest a fixed delay.
  4. An error that grows over time points toward duration, time-base, or frame-rate interpretation problems.
  5. For lip-sync material, inspect the beginning, middle, and end of continuous speech rather than one syllable.

The release note does not describe how audio is handled, so do not assume that the audio track is always preserved unchanged or automatically synchronized. Judge the delivered file.

Step 4: Test again in the real timeline and delivery platform

Always test the file in the actual editing timeline and final platform, because an NLE or second transcode can reinterpret cadence, change speed, or lose an audio track. Perform at least these two checks:

  • Put the file on the intended editing timeline and confirm that the NLE does not reinterpret it, change its speed, or lose an audio track.
  • Test it in the final platform or playback environment and confirm that a second transcode has not changed cadence, captions, or sync.

When the platform transcodes again, preserve both the Runway output and the platform version. Inspect them separately before attributing a problem to the upstream file.

Use this table to pass, rework, or retain a source segment

Pass the file only when every critical item meets the delivery requirement. If one shot fails, rework that segment or retain the source shot before rerunning the entire program.

CheckPass conditionAction on failure
Target frame rateExact delivery cadence; 29.97/59.94 are not mislabeled as 30/60Correct the target or timeline interpretation
Duration and frame countDuration matches source; CFR frame count is near the expected valueInvestigate VFR, truncation, frozen tail, and time base
Dimensions and codecConform to editor or channel requirementsRewrap or transcode to the delivery specification
Fast motionNo unacceptable double images, bending, or disappearing objectsMark timecodes; try another target or retain the source segment
Cuts and flashesNo blended, repeated, or flashing frames around editsSplit at a natural cut, reprocess, and review the seam
Text and UIGlyphs, thin lines, and static overlays remain stableReapply graphics in post instead of processing them with the picture
Audio syncNo visible offset or drift at beginning, middle, and endCompare stream durations/time bases, then realign or transcode
File integrityFull decode succeeds; ending and audio tracks are intactRedownload, rewrap, or rerun the task

Do not move from 60 to 120 fps in these cases

Stay at the lowest frame rate that satisfies the delivery specification when 120 fps adds only file size and downstream processing. Do not increase the rate in the following cases:

  • the downstream specification only asks for 24, 25, 29.97, or 30 fps;
  • the source already contains heavy ghosting, compression blocks, or motion blur;
  • captions, UI, or fine lines become less stable;
  • an audio-sync problem is still unexplained;
  • the target platform will force a lower-frame-rate transcode;
  • the higher-rate version brings no visible benefit but adds storage, decoding, or downstream transcode load.

Frame rate is a delivery parameter, not a standalone quality score. The pass criterion is “matches the required cadence without unacceptable new defects,” not “has the largest number.”

Complete the delivery in ten steps

The lowest-rework order is specification and baseline, upload and submission, waiting and storage, then mechanical, visual, and real-environment verification.

  1. Choose the exact targetFramerate from the downstream specification.
  2. Record source rate, duration, audio, and risk timecodes with ffprobe, and confirm the file is no longer than 300 seconds.
  3. For a local file, call POST /v1/uploads and retain uploadUrl, fields, and runwayUri.
  4. Send the multipart form to uploadUrl; request a new upload instead of reusing a failed one.
  5. Call POST /v1/video_upscale with model, videoUri, and targetFramerate.
  6. Store the returned task id and estimatedCost.
  7. Call GET /v1/tasks/{id} every five seconds until the task reaches a terminal status.
  8. On SUCCEEDED, download and persist output; handle FAILED or CANCELLED by error type.
  9. Verify actual cadence, duration, artifacts, cuts, and audio sync with ffprobe and frame-by-frame review.
  10. Test the file in the real timeline and target platform before handoff.

Official references: Models, Inputs, Uploads, Video upscale API Reference, Task API Reference, Outputs, HTTP errors, Task failures, and the API Changelog. Interface fields and steps were verified on September 26, 2026.

Ready to optimize your LLM workflow?

Join thousands of developers building faster, smarter, and more cost-effective AI applications with BetterToken.

Get Started for Free