首帧图片用 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 能不能用,提交前花一秒钟检查一下,比排队几分钟后看到任务失败划算得多。