设计实践

首帧图片用 URL 传给 Runway 时,规则比想象的严格

首帧图片用 URL 传给 Runway 时,规则比想象的严格:必须 HTTPS、主机名不能是 IP、不跟随重定向、服务器必须支持 HEAD、Content-Type 必须是图片类型(application/octet-stream 不行)、Content-Length 不超过 16MB。本篇写一个提交前的检查脚本,在真实的公开网址上测试,避免任务排队之后才失

BINBHO · 图文教程

首帧图片用 URL 传给 Runway 时,规则比想象的严格:必须 HTTPS、主机名不能是 IP、不跟随重定向、服务器必须支持 HEAD、Content-Type 必须是图片类型(application/octet-stream 不行)、Content-Length 不超过 16MB。本篇写一个提交前的检查脚本,在真实的公开网址上测试,避免任务排队之后才失败。

先看清成品

  • 测试: pytest 1 项通过,覆盖不需要联网的几条规则。
  • 检查真实网址(box 上实测): python.org 的 logo、httpbin 的 jpeg、维基共享资源的原图、raw.githubusercontent.com 的 png 和 webp 都可以使用。不能使用的:
    • github.com/…/raw/… 和 Special:FilePath 返回 302 重定向。
    • httpbin 的 /bytes 返回 application/octet-stream。
    • http:// 地址不是 HTTPS。
  • 意外发现: 用错 User-Agent 时,维基共享资源返回 403(见调试示例)。

没有在 box 上跑的部分: 这个脚本只检查 URL,不调用 Runway,不需要 Key。注意 Runway 抓取图片时使用自己的 UA(以 “RunwayML API/” 开头),所以检查通过也不能保证 Runway 一定能取到图片,只能说明符合文档要求。

合规提醒:RUNWAYML_API_SECRET 只放在服务器端或环境变量里。首帧图片和参考素材必须是你有权使用的:自己拍的、获得授权的,或者许可允许的。不要用别人的肖像、商标或受版权保护的作品去生成视频,不要冒充真人。生成的视频发布时要按《人工智能生成合成内容标识办法》加上显式的“AI 生成”标识,并遵守 Runway 的使用条款和内容政策。费用按 credits 计算,以 Runway 开发者文档的 Pricing 页为准,本文不列价格。

做完之后你会得到什么?

成果 说明
check_input_url.py 按文档逐条检查:长度、HTTPS、是否 IP → HEAD(不跟随重定向)→ 状态码 → Content-Type → Content-Length
test_url.py 不联网的规则

需要准备哪些环境?

Python 3.9 以上,pip install requests pytest。用 CONTACT 环境变量填写你自己的联系邮箱,它会写进 UA。

文件结构

47-runway-C-src/
├── check_input_url.py
└── test_url.py

第 1 步:代码

check_input_url.py:

"""提交前检查图片 URL 是否符合 Runway 的输入要求(docs.dev.runwayml.com/assets/inputs),省得任务排队后才失败。
文档规则:
  1. 必须是 HTTPS;主机名必须是域名,不能是 IP;URL 不超过 2048 字符
  2. 服务器必须支持 HEAD,并返回正确的 Content-Type 和 Content-Length
  3. 不跟随重定向:返回 3xx 就算失败
  4. Content-Type 必须和素材类型匹配(图片:image/jpeg、image/jpg、image/png、image/webp),application/octet-stream 等通用类型不接受;
     不看 URL 里的扩展名
  5. 通过 URL 提供的图片不超过 16MB
(Runway 抓取时的 User-Agent 以 “RunwayML API/” 开头;如果你的服务器有防爬或 WAF,需要放行。本脚本用自己的 UA,结果只能作为参考。)
用法:CONTACT=你的邮箱 python check_input_url.py URL [URL ...]"""
import ipaddress, os, sys
from urllib.parse import urlparse
import requests

IMAGE_TYPES = {"image/jpeg", "image/jpg", "image/png", "image/webp"}
LIMIT = 16 * 1024 * 1024
# 说明身份的 UA;联系方式用 CONTACT 环境变量填你自己的邮箱。
# 不要留 you@example.com 这类占位符:实测 upload.wikimedia.org 对它返回 403“Please honor our robot policy”
CONTACT = os.environ.get("CONTACT")
UA = {"User-Agent": "binbho-input-check/1.0" + (f" (pre-flight check for Runway inputs; contact: {CONTACT})" if CONTACT else "")}

def check(url):
    problems = []
    u = urlparse(url)
    if len(url) > 2048: problems.append("URL 超过 2048 字符")
    if u.scheme != "https": problems.append("不是 HTTPS")
    try: ipaddress.ip_address(u.hostname or ""); problems.append("主机名是 IP 地址,必须用域名")
    except ValueError: pass
    if problems: return problems
    try: r = requests.head(url, allow_redirects=False, timeout=15, headers=UA)
    except requests.RequestException as e: return [f"HEAD 请求失败:{type(e).__name__}"]
    if 300 <= r.status_code < 400: return [f"返回 {r.status_code} 重定向到 {r.headers.get('Location')};Runway 不跟随重定向,请直接用最终地址"]
    if r.status_code == 405: return ["服务器不支持 HEAD(405)"]
    if r.status_code != 200: return [f"HEAD 返回 {r.status_code}"]
    ct = r.headers.get("Content-Type", "").split(";")[0].strip().lower()
    if ct not in IMAGE_TYPES: problems.append(f"Content-Type 是 “{ct or '无'}”,不是 jpeg/png/webp 图片类型")
    cl = r.headers.get("Content-Length")
    if cl is None: problems.append("没有 Content-Length")
    elif int(cl) > LIMIT: problems.append(f"文件 {int(cl)/1024/1024:.1f}MB,超过 URL 方式的 16MB 上限")
    return problems

def main():
    bad = 0
    for url in sys.argv[1:]:
        p = check(url); bad += bool(p)
        print(("✓ 可以使用" if not p else "✗ " + ";".join(p)) + f"\n   {url}")
    sys.exit(1 if bad else 0)

if __name__ == "__main__":
    main()

第 2 步:运行

$ python check_input_url.py \
   "https://upload.wikimedia.org/wikipedia/commons/8/85/Tour_Eiffel_Wikimedia_Commons_(cropped).jpg" \
   "https://commons.wikimedia.org/wiki/Special:FilePath/Tour_Eiffel_Wikimedia_Commons_(cropped).jpg" \
   "https://github.com/python-pillow/Pillow/raw/main/Tests/images/hopper.png" \
   "https://raw.githubusercontent.com/python-pillow/Pillow/main/Tests/images/hopper.png" \
   "https://httpbin.org/bytes/1024" "http://example.com/a.png"
✓ 可以使用
   https://upload.wikimedia.org/…/Tour_Eiffel_Wikimedia_Commons_(cropped).jpg
✗ 返回 302 重定向到 https://commons.wikimedia.org/wiki/Special:Redirect/file/…;Runway 不跟随重定向,请直接用最终地址
   https://commons.wikimedia.org/wiki/Special:FilePath/…
✗ 返回 302 重定向到 https://raw.githubusercontent.com/python-pillow/Pillow/main/Tests/images/hopper.png;…
   https://github.com/python-pillow/Pillow/raw/main/Tests/images/hopper.png
✓ 可以使用
   https://raw.githubusercontent.com/python-pillow/Pillow/main/Tests/images/hopper.png
✗ Content-Type 是 “application/octet-stream”,不是 jpeg/png/webp 图片类型
   https://httpbin.org/bytes/1024
✗ 不是 HTTPS
   http://example.com/a.png

(以上结果来自 box 上的两次运行,合并后列出。)检查点:GitHub 网页上的 “raw” 链接会重定向,要改用 raw.githubusercontent.com 的地址。

调试示例:占位邮箱被维基拒绝了

第一版的 UA 里写的联系方式是占位符 you@example.com。结果 upload.wikimedia.org 上的这张图(Runway 入门文档自己的示例图)返回 403,body 是“Please honor our robot policy”。我用 curl 逐个对比了几种 UA:

UA 结果
binbho-input-check/1.0 200
binbho-input-check/1.0 (contact: you@example.com) 403
写上真实邮箱 200
RunwayML API/1.0 200

被拦下的原因是占位邮箱本身。所以联系方式改成从 CONTACT 环境变量读取,不填就不带联系方式,不再写死一个假地址。46-felo-B 的链接检查器也做了同样的修改。

常见问题

现象 原因 处理
302 重定向 短链接、网页上的 raw 链接、CDN 跳转 用最终地址
Content-Type 不对 对象存储没有设置 MIME 类型 上传时设置 image/jpeg 等类型
检查通过,Runway 仍然取不到图片 服务器的 WAF 拦截了 Runway 的 UA 放行以 “RunwayML API/” 开头的 UA;或者改用 data URI / ephemeral upload
403 网站拦截自动访问 不要绕过;换一个你自己能控制的存储

模板规则

  • 提交前先检查输入 URL,规则来自文档。
  • 不跟随重定向,直接用最终地址。
  • UA 要说明身份,联系方式要真实,不留占位符。

项目资源

用途 地址
Runway API 入门 https://docs.dev.runwayml.com/guides/using-the-api/
API 参考(Markdown 版) https://docs.dev.runwayml.com/api.md
输入要求(图片 / 视频 / 大小限制) https://docs.dev.runwayml.com/assets/inputs/
任务失败码 https://docs.dev.runwayml.com/errors/task-failures/

一条 URL 能不能用,提交前花一秒钟检查一下,比排队几分钟后看到任务失败划算得多。