关于 xai-grok-imagine-video
xai-grok-imagine-video 是使用 xAI 直接 API 生成、编辑和延展视频的制作技能,适用于需要控制请求、成本与素材留存的媒体工作流。它强调在付费请求前记录意图,并核对异步结果和产物。
主要功能
- 01文本生成视频
- 02图像或参考图生成视频
- 03编辑与延展视频
- 04估算请求费用
- 05异步结果核对与素材留存
适用场景
- 01按创意简报生成视频片段
- 02基于已有图像制作动态内容
- 03修改或延长现有视频
- 04追踪付费生成请求及其产物
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ calesthio/ generative-media-skills --skill xai-grok-imagine-video安装后在 Claude Code 中输入/xai-grok-imagine-video确认已加载。
附带文件EVAL.md
详细介绍
xAI Grok Imagine 视频
将 xAI 直连 API 用作受控的媒体制作服务:冻结创意简报和源素材权利,生成定价后的请求,在付费 POST 前持久化请求意图,核对异步结果,然后保存成品及其证据。
以下所有易变信息和价格均已于 2026-07-10 根据 xAI 一手来源核实。
选择正确的制作渠道
直连接口为 https://api.x.ai。在服务端使用 Authorization: Bearer $XAI_API_KEY 进行身份验证。API 密钥归 xAI Console 团队所有,使用预付费额度或按发票结算的 API 计费。
请勿混用以下彼此独立的平台:
grok.com和移动应用上的 Grok 使用消费者套餐、每周额度、消费者数据控制和消费者条款。那里宣传的“Video 1.5 Fast”不是直连 API 的模型标识。- X 上的 Grok 受 X 的条款和隐私规则约束。X Premium 或 SuperGrok 订阅不等于 API 额度。
- Grok Imagine 图像模型使用
/v1/images/*;语音和语音合成使用其他接口。它们的参数和价格不能套用到视频。 - 合作伙伴平台和网关(包括 xAI 营销中提及的服务)可能会封装 xAI 模型,但也会增加自己的架构、审核、保留政策、定价和区域限制。
- Azure、Google Cloud 和其他托管服务提供单独的身份验证、配额、功能支持、计费和数据治理合同。本技能介绍的是 xAI 的直连 API。
公共 API 使用 api.x.ai。xAI 宣传了多个推理集群和企业数据驻留选项,其状态页面则显示了美国和欧盟的区域 API 服务。不要仅根据页面标签选择区域主机名或承诺数据驻留。只能使用 xAI Console 或其订单表单/DPA 为团队提供的接口、区域和合同控制。
模型与模式决策表
每个请求只能启用一种模式。
| 目标 | 端点和 REST 字段 | 有效模型 | 输出控制 |
|---|---|---|---|
| 文本转视频 | POST /v1/videos/generations;prompt | grok-imagine-video | duration 为 1–15 s;支持的宽高比;480p/720p |
| 图像转视频 | generations;prompt + image | 任一模型 | 1–15 s;支持的宽高比;基础版:480p/720p;1.5:480p/720p/1080p |
| 参考图转视频 | generations;prompt + reference_images | 仅 grok-imagine-video | 1–10 s;支持的宽高比;480p/720p;1–7 张参考图 |
| 编辑视频 | POST /v1/videos/edits;prompt + video | 仅 grok-imagine-video | 无法自定义时长、宽高比或分辨率;输入 ≤8.7 s;输出沿用时长和宽高比,分辨率最高为 720p |
| 延长视频 | POST /v1/videos/extensions;prompt + video | 仅 grok-imagine-video | 输入为 2–15 s;延长 2–10 s,默认为 6;输出沿用宽高比和分辨率,最高为 720p |
grok-imagine-video-1.5 已正式发布,仅支持图像转视频。它不支持文本转视频或参考图转视频,其模型页面仅声明支持图像输入。当前别名为 grok-imagine-video-1.5-preview 和 grok-imagine-video-1.5-2026-05-30;除非已记录的可复现性决策要求使用带日期的别名,否则优先使用正式版 slug。不要将面向消费者的“1.5 Fast”标签误认为 API 标识符。
允许的生成宽高比为 1:1、16:9、9:16、4:3、3:4、3:2 和 2:3;默认值为 16:9。图像转视频时,如果省略该参数,则沿用输入图像的宽高比。提供 aspect_ratio 会覆盖这一设置,并对图像进行拉伸;如果不能接受失真,请在提交前谨慎裁剪或填充。默认分辨率为 480p。1080p 选项仅适用于 1.5 图像转视频。
参考图用于引导人物、产品、服装或风格,但不会成为首帧。REST 结构:
{
"model": "grok-imagine-video",
"prompt": "The cyclist from <IMAGE_1> rides past the cafe from <IMAGE_2>.",
"reference_images": [
{"file_id": "file_authorized_subject"},
{"url": "https://authorized.example/cafe.jpg"}
],
"duration": 8,
"aspect_ratio": "16:9",
"resolution": "720p"
}
只能使用 image、reference_images 或 video,不可混用。image + reference_images 明确无效。图像可以是 PNG/JPEG/WebP;通过 Files 引用的视频可以是 MP4;每个定位符都可以是 HTTPS URL、Base64 数据 URI 或私有 file_id。当前通用 Files 指南指出文件大小为 48 MB,而上传参考文档指出为 50 MB。请将可接受的最大值视为文档冲突:将文件控制在 48 MB 以下,或在上传前确认确切端点的限制。Files 之外的公开媒体/Base64 限制未说明。
HTTPS 定位符授权 xAI 获取相应资源,并会将 URL 暴露给请求处理和日志记录;查询字符串可能包含类似 bearer token 的机密信息。重复使用时,优先采用设置了合理 TTL 的私有 file_id。切勿记录 Base64 或签名 URL;未经 DNS/IP/重定向 SSRF 防护以及字节数/时间限制,不要让自己的应用代理或获取任意用户 URL。
xAI 对该功能发布及 1.5 的公告称,原生生成音频包含与动作同步的音效、环境音和对白。这是提供方声明。当前视频 REST 架构未说明音频启用/禁用字段、单独的音频提示词、语言控制、采样率、声道、编解码器或响度。帧率同样未作说明。在主提示词中适当描述所需音效/对白,然后检查实际媒体流;在结果生成前,切勿保证一定有音频或保证其保真度。
为整个媒体转换定价
当前直接 API 的美元费率:
| 模型 | 媒体输入 | 480p 输出 | 720p 输出 | 1080p 输出 |
|---|---|---|---|---|
grok-imagine-video | 图像 $0.002/张;视频 $0.01/源视频秒 | $0.05/输出秒 | $0.07/输出秒 | 不支持 |
grok-imagine-video-1.5 | 图像 $0.01 | $0.08/输出秒 | $0.14/输出秒 | $0.25/输出秒 |
文本输入不收取列出的媒体费用。批准前请先计算:
- T2V:
时长 × 输出费率。 - I2V:一张图像的费用加上
时长 × 输出费率。 - 参考:
参考图像数量 × $0.002加上时长 × 输出费率。 - 编辑:
源视频时长 × $0.01的视频输入费用,加上输出时长 × 继承/上限分辨率对应的输出费率。输出时长等于源视频时长。 - 延长:
源视频时长 × $0.01加上延长时长 × 继承/上限分辨率对应的输出费率。xAI 将延长定价标为促销价,且可能变动;批准前请立即重新核实。
示例:基础模型生成 6 秒 720p T2V 的费用为 $0.42;1.5 模型生成 12 秒 1080p I2V 的费用为 $0.01 + $3.00 = $3.01;基础模型生成 8 秒 720p、包含三张图像的参考请求费用为 $0.006 + $0.56 = $0.566。每个变体都应作为可能单独计费的请求报价。媒体请求批处理没有批量折扣。
完成后,usage.cost_in_usd_ticks 是每个请求的实际费用;10,000,000,000 个 ticks 等于 1 美元。请保存该整数,并与已批准的报价进行比较。价格估算并不代表可以未经告知就超额收费。
直接描述动作,而非修饰名词
xAI 发布的是自然语言示例,并非正式的摄影机指令语法。不要自行编造方括号指令、负面提示词字段、种子、CFG、FPS 或时间码保证。
围绕以下五个要点组织生成提示词:
- 确定开场构图,以及持续不变的身份/材质特征。
- 让主体完成一个具有因果逻辑的动作过程,并表现出可见的重量感和接触。
- 用常见的电影摄影术语指定一种主要摄影机运动:缓慢推进、向右摇摄、向后移动、向上倾斜、固定镜头、手持跟拍。
- 只安排少数几个节拍,用“首先”“然后”描述,并给出可观察的结尾。
- 补充光线/风格;如有必要,再加一条简短的声音或对白提示。
这是一种制作启发式方法,不是参数约定。一个有用的 8 秒简报可以这样写:“黎明时分的广角镜头:一只蓝色陶瓷杯放在被雨水打湿、颜色变深的咖啡馆桌上。一只手伸入画面,拿起杯子,蒸汽向镜头卷来;摄影机缓慢推进一次。最后,杯子在窗边被稳稳端着。安静的室内环境音,伴随一声轻柔的陶瓷碰响。”请检查动作的因果逻辑、遮挡、手部、文字、反射和音画同步,不要只相信描述性形容词。
针对不同模式的指导:
- **1.5 或基础 I2V:**图像是起始帧。描述变化——什么开始运动、摄影机如何移动,以及最终状态。避免重新描述与图像构图相冲突的内容。若宽高比不兼容,请在 API 外部裁剪/补边,而不是拉伸图像。
- **参考转视频:**为按顺序排列的每张参考图指定一个用途,并在提示词中使用
<IMAGE_1>、<IMAGE_2>等标记,如 xAI 的示例所示。说明哪个身份穿戴哪件产品,以及哪张参考图提供环境/风格。少用互不冲突的参考图,比使用七张联系松散的参考图更容易审核。 - **编辑:**要求做最小幅度的更改,并说明必须保持不变的内容:“只将雨伞改为哑光黄色;保留人脸、时长、摄影机路径、雨水、标牌和音频。”xAI 宣称其保留效果出色,但在将整个编辑结果与源内容进行比较之前,这仍只是提供商的说法。
- **延长:**从最后可见的动作、摄影机速度、光线和声音底色继续。只描述接下来的动作。返回的文件包含原始内容和延长部分;请检查衔接处、重复帧、节奏、身份一致性和音频过渡。
在传输前划定付费边界
创建一份档案,记录精确的端点和请求正文、模型或带日期的别名、价格页面的时间戳、报价公式、源素材时长/分辨率、变体数量、提示词、宽高比/分辨率、源素材哈希值和权利信息、目标区域/团队、保留/公开分享选项,以及审核/披露审查人。
默认使用本地试运行,无需密钥、不发起网络请求,也不消耗独占创建账本。将提示词、文件 ID、Base64 和完整签名 URL 替换为哈希值。以规范形式绑定精确的端点/请求正文摘要、模型/模式、有序源素材哈希值、本地作业密钥、输入/输出精确主机策略、目标位置、新鲜的价格凭据、ZDR 要求、权利/审核/QA 凭据、美元报价,以及单次创建次数。要求提供匹配的摘要和有限的正美元上限。在 POST 之前,以独占创建语义写入意图记录。
视频端点未说明幂等键。因此,绝不要自动重试创建 POST。如果发生超时、连接重置或 5xx,且未捕获到 request_id,则记录 create-outcome-unknown;在批准替代请求前,先核对 xAI Console 账单、费用记录,并联系支持团队。已知的 4xx 拒绝可以修正后以新的尝试重新提交。经过身份验证的状态 GET 和最终媒体下载可以采用有界退避。
文档说明 service_unavailable 可以稍后重试,但这并不意味着付费创建操作具有幂等性。其他终止代码包括 invalid_argument、permission_denied 和 failed_precondition。常见 HTTP 错误包括 400、401、403、404、415、422 和 429。不要修改被拦截的提示词以规避审核。
核实期间,第一方速率限制渲染结果存在冲突:根据渲染端/集群的不同,当前限制/模型页面显示的视频速率分别为 1 RPS、10 RPS 和 70 RPM。因此,公开文档无法确定精确的实际运行限制。以团队专属的 xAI Console 限制为准,并保守地控制创建速率。此冲突并不能成为按显示的最大值并发创建的理由。
示例:可运行的受保护 REST 客户端
将此 Python 3.11+ 示例保存为技能目录之外的 xai_video_guard.py。它支持全部五种直接模式,分别报价媒体输入和输出费用,默认采用空运行,需要精确的摘要和费用上限,在创建前保存意图,不会重试创建操作,可恢复已知请求 ID,记录 ZDR 标头,验证审核结果,并捕获 xAI 的实际费用计量值。它通过精确主机名和固定公共 DNS 的 TLS 连接下载,不携带 bearer,也不跟随重定向;随后会以持久方式暂存文件、探测、完整解码,并支持崩溃后继续发布 MP4。需要 Python 3.11+、ffprobe 和 ffmpeg。
每个媒体定位符都必须通过 --source-local 提供一个有序本地镜像。程序会计算每个镜像的哈希并完整解码;对于编辑/扩展操作,它会重新探测时长/分辨率,并拒绝与档案不符的镜像。单个源使用其文件 SHA-256;多个参考素材则使用档案处理流程打印的规范有序列表摘要。HTTPS 定位符还必须指定精确的 --input-host;私有的 file_... ID 不需要。
#!/usr/bin/env python3
import argparse
import hashlib
import http.client
import ipaddress
import json
import os
import pathlib
import random
import socket
import ssl
import subprocess
import sys
import tempfile
import threading
import time
import urllib.error
import urllib.parse
import urllib.request
from datetime import datetime, timezone
from decimal import Decimal, InvalidOperation
API = "https://api.x.ai/v1"
OUT_RATE = {
"grok-imagine-video": {"480p": Decimal("0.05"), "720p": Decimal("0.07")},
"grok-imagine-video-1.5": {"480p": Decimal("0.08"), "720p": Decimal("0.14"), "1080p": Decimal("0.25")},
}
IMAGE_RATE = {"grok-imagine-video": Decimal("0.002"), "grok-imagine-video-1.5": Decimal("0.01")}
VIDEO_INPUT_RATE = Decimal("0.01")
ASPECTS = {"1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3"}
MAX_JSON = 256 * 1024
MAX_VIDEO = 512 * 1024 * 1024
MAX_TOOL_OUTPUT = 512 * 1024
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl): return None
NO_REDIRECT = urllib.request.build_opener(NoRedirect)
class APIProblem(Exception):
def __init__(self, kind, status=None, body_sha256=None):
super().__init__(kind); self.kind, self.status, self.body_sha256 = kind, status, body_sha256
def canonical(value): return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
def sha256_bytes(value): return hashlib.sha256(value).hexdigest()
def utc_now(): return datetime.now(timezone.utc).isoformat()
def fsync_directory(path):
if os.name != "nt":
fd = os.open(path, os.O_RDONLY)
try: os.fsync(fd)
finally: os.close(fd)
def save_record(path, record):
target = pathlib.Path(path); target.parent.mkdir(parents=True, exist_ok=True)
fd, name = tempfile.mkstemp(prefix=".xai-video-", suffix=".json", dir=target.parent)
try:
os.fchmod(fd, 0o600)
with os.fdopen(fd, "wb") as out:
out.write(canonical(record) + b"\n"); out.flush(); os.fsync(out.fileno())
os.replace(name, target); fsync_directory(target.parent)
finally:
if os.path.exists(name): os.unlink(name)
def exclusive_record(path, record):
target = pathlib.Path(path); target.parent.mkdir(parents=True, exist_ok=True)
fd = os.open(target, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
with os.fdopen(fd, "wb") as out:
out.write(canonical(record) + b"\n"); out.flush(); os.fsync(out.fileno())
fsync_directory(target.parent)
def read_record(path):
raw = pathlib.Path(path).read_bytes()
if len(raw) > MAX_JSON: raise SystemExit("Ledger exceeds JSON cap")
value = json.loads(raw)
if not isinstance(value, dict): raise SystemExit("Ledger is not an object")
return value
def bounded_process(argv, timeout, cap=MAX_TOOL_OUTPUT):
process = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
chunks, total, overflow, lock = [], [0], threading.Event(), threading.Lock()
def drain(stream):
while True:
data = stream.read(65536)
if not data: return
with lock:
total[0] += len(data)
if total[0] > cap: overflow.set(); process.kill(); return
chunks.append(data)
threads = [threading.Thread(target=drain, args=(stream,), daemon=True) for stream in (process.stdout, process.stderr)]
for thread in threads: thread.start()
try: code = process.wait(timeout=timeout)
except subprocess.TimeoutExpired:
process.kill(); process.wait(); raise RuntimeError(f"tool timeout: {pathlib.Path(argv[0]).name}")
for thread in threads: thread.join(timeout=2)
output = b"".join(chunks)
if overflow.is_set(): raise RuntimeError(f"tool output cap exceeded: {pathlib.Path(argv[0]).name}")
if code: raise RuntimeError(f"tool failed: {pathlib.Path(argv[0]).name}; diagnostics_sha256={sha256_bytes(output)}")
return output
def exact_hosts(raw, label):
hosts = []
for item in raw.split(","):
host = item.strip().rstrip(".").lower()
if not host: continue
if "*" in host or "/" in host or ":" in host: raise SystemExit(f"{label} accepts exact DNS hostnames only")
try: ipaddress.ip_address(host)
except ValueError: pass
else: raise SystemExit(f"{label} rejects IP literals")
hosts.append(host)
if not hosts: raise SystemExit(f"{label} requires at least one reviewed exact host")
return tuple(sorted(set(hosts)))
def public_addresses(host):
addresses = []
for item in socket.getaddrinfo(host, 443, type=socket.SOCK_STREAM):
address = ipaddress.ip_address(item[4][0])
if not address.is_global: raise SystemExit("Media host resolves to a non-public address")
addresses.append(str(address))
if not addresses: raise SystemExit("Media host did not resolve")
return sorted(set(addresses))
def locator(value, allowed_hosts):
if value.startswith("file_"):
if len(value) > 256 or any(character not in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-" for character in value):
raise SystemExit("Invalid xAI file_id")
return {"file_id": value}
parsed = urllib.parse.urlsplit(value)
if parsed.scheme != "https" or not parsed.hostname or parsed.username or parsed.password or parsed.fragment or parsed.port not in (None, 443):
raise SystemExit("Media locator must be an xAI file_id or public HTTPS URL")
host = parsed.hostname.rstrip(".").lower()
try:
ipaddress.ip_address(host)
except ValueError: pass
else: raise SystemExit("IP-literal media URLs are forbidden")
if host not in allowed_hosts: raise SystemExit("Media URL host is outside the approved exact-host policy")
return {"url": value}
def redact(value):
if "url" in value:
p = urllib.parse.urlsplit(value["url"])
return {"url_host": p.hostname, "url_sha256": sha256_bytes(value["url"].encode("utf-8"))}
if "file_id" in value: return {"file_id_sha256": sha256_bytes(value["file_id"].encode("utf-8"))}
return {"locator_sha256": sha256_bytes(canonical(value))}
def request_json(method, url, api_key, payload=None, retries=0):
body = None if payload is None else canonical(payload)
for attempt in range(retries + 1):
req = urllib.request.Request(url, data=body, method=method, headers={
"Authorization": f"Bearer {api_key}", "Content-Type": "application/json", "Accept": "application/json"})
try:
with NO_REDIRECT.open(req, timeout=45) as response:
raw = response.read(MAX_JSON + 1)
zdr = response.headers.get("x-zero-data-retention")
if len(raw) > MAX_JSON: raise APIProblem("response-too-large")
try: data = json.loads(raw)
except json.JSONDecodeError: raise APIProblem("invalid-json", response.status, sha256_bytes(raw))
if not isinstance(data, dict): raise APIProblem("non-object-json", response.status, sha256_bytes(raw))
return data, None if zdr is None else zdr.lower()
except urllib.error.HTTPError as exc:
raw = exc.read(MAX_JSON + 1)
problem = APIProblem("http-error", exc.code, sha256_bytes(raw))
if 400 <= exc.code < 500 or attempt == retries:
raise problem
except APIProblem: raise
except (urllib.error.URLError, TimeoutError, OSError) as exc:
if attempt == retries: raise APIProblem("transport-" + type(exc).__name__)
time.sleep(min(2 ** attempt, 8) * (1 + random.random() * 0.25))
def source_evidence(args):
required = 0 if args.mode == "text" else (len(args.reference) if args.mode == "reference" else 1)
if len(args.source_local) != required: raise SystemExit(f"Provide exactly {required} ordered --source-local mirrors")
evidence = []
for path_text in args.source_local:
path = pathlib.Path(path_text).resolve()
if not path.is_file() or path.stat().st_size <= 0 or path.stat().st_size >= 48 * 1024 * 1024:
raise SystemExit("Source mirror must exist and remain below the conservative 48 MiB Files limit")
digest = hashlib.sha256()
with path.open("rb") as stream:
header = stream.read(32); stream.seek(0)
for chunk in iter(lambda: stream.read(1024 * 1024), b""): digest.update(chunk)
kind = "video" if args.mode in {"edit", "extend"} else "image"
if kind == "image" and not (header.startswith(b"\xff\xd8\xff") or header.startswith(b"\x89PNG\r\n\x1a\n") or (header.startswith(b"RIFF") and header[8:12] == b"WEBP")):
raise SystemExit("Image mirror must be JPEG, PNG, or WebP by signature")
if kind == "video" and (len(header) < 12 or header[4:8] != b"ftyp"):
raise SystemExit("Video mirror must be MP4 by signature")
raw = bounded_process(["ffprobe", "-v", "error", "-show_streams", "-show_format", "-of", "json", str(path)], 30, MAX_JSON)
metadata = json.loads(raw); streams = metadata.get("streams", [])
visual = next((item for item in streams if item.get("codec_type") == "video"), None)
if not visual: raise SystemExit("Source mirror has no visual stream")
bounded_process(["ffmpeg", "-nostdin", "-v", "error", "-i", str(path), "-f", "null", "-"], 300)
item = {"path": str(path), "sha256": digest.hexdigest(), "bytes": path.stat().st_size,
"kind": kind, "width": visual.get("width"), "height": visual.get("height")}
if kind == "video":
try: duration = Decimal(str(metadata.get("format", {}).get("duration")))
except InvalidOperation: raise SystemExit("Source video duration is invalid")
if not duration.is_finite() or duration <= 0: raise SystemExit("Source duration must be finite and positive")
short_edge = min(int(visual.get("width")), int(visual.get("height")))
resolution = "480p" if short_edge <= 480 else ("720p" if short_edge <= 720 else "1080p")
item.update(duration=str(duration), codec=visual.get("codec_name"), resolution=resolution)
evidence.append(item)
if evidence and args.source_sha256:
expected = evidence[0]["sha256"] if len(evidence) == 1 else sha256_bytes(canonical([item["sha256"] for item in evidence]))
if args.source_sha256.lower() != expected: raise SystemExit("source-sha256 does not match the validated local mirror evidence")
if evidence and args.mode in {"edit", "extend"}:
actual_duration = Decimal(evidence[0]["duration"])
if args.source_duration is not None and abs(args.source_duration - actual_duration) > Decimal("0.05"):
raise SystemExit("Declared source duration differs from ffprobe evidence")
if args.source_resolution is not None and args.source_resolution != evidence[0]["resolution"]:
raise SystemExit("Declared source resolution differs from ffprobe evidence")
args.source_duration, args.source_resolution = actual_duration, evidence[0]["resolution"]
return evidence
def quote(args):
if args.mode == "edit":
output_seconds = Decimal(str(args.source_duration))
output_resolution = "720p" if args.source_resolution in {"720p", "1080p"} else "480p"
return output_seconds * (VIDEO_INPUT_RATE + OUT_RATE[args.model][output_resolution])
if args.mode == "extend":
output_resolution = "720p" if args.source_resolution in {"720p", "1080p"} else "480p"
return Decimal(str(args.source_duration)) * VIDEO_INPUT_RATE + Decimal(args.duration) * OUT_RATE[args.model][output_resolution]
result = Decimal(args.duration) * OUT_RATE[args.model][args.resolution]
if args.mode == "image":
result += IMAGE_RATE[args.model]
elif args.mode == "reference":
result += IMAGE_RATE[args.model] * len(args.reference)
return result
def validate(args):
if not args.prompt.strip(): raise SystemExit("A non-empty prompt is required")
if args.model == "grok-imagine-video-1.5" and args.mode != "image":
raise SystemExit("grok-imagine-video-1.5 is image-to-video only")
if args.mode in {"text", "image", "reference"}:
if args.duration is None or not 1 <= args.duration <= 15: raise SystemExit("Generation duration must be 1–15 seconds")
if args.mode == "reference" and args.duration > 10: raise SystemExit("Reference-to-video maximum is 10 seconds")
if args.resolution not in OUT_RATE[args.model]: raise SystemExit("Resolution unsupported by selected model")
if args.aspect_ratio not in ASPECTS: raise SystemExit("Unsupported aspect ratio")
if args.mode == "image" and not args.image: raise SystemExit("--image is required")
if args.mode == "reference" and not 1 <= len(args.reference) <= 7: raise SystemExit("Provide 1–7 references")
if args.mode in {"edit", "extend"}:
if not args.video or args.source_duration is None or args.source_resolution is None:
raise SystemExit("Video mode requires --video, --source-duration, --source-resolution, and a local mirror")
if args.model != "grok-imagine-video": raise SystemExit("Editing and extension require grok-imagine-video")
if not args.source_duration.is_finite(): raise SystemExit("Source duration must be finite")
if args.mode == "edit" and not 0 < args.source_duration <= 8.7:
raise SystemExit("Edit input must be at most 8.7 seconds")
if args.mode == "extend":
if not 2 <= args.source_duration <= 15: raise SystemExit("Extension input must be 2–15 seconds")
if args.duration is None or not 2 <= args.duration <= 10: raise SystemExit("Extension duration must be 2–10 seconds")
if args.mode != "text" and not args.source_sha256:
raise SystemExit("Media mode requires --source-sha256 provenance evidence")
if args.source_sha256 and (len(args.source_sha256) != 64 or any(c not in "0123456789abcdefABCDEF" for c in args.source_sha256)):
raise SystemExit("--source-sha256 must be exactly 64 hexadecimal characters")
def build(args, input_hosts):
endpoint = "/videos/generations"
body = {"model": args.model, "prompt": args.prompt}
if args.mode in {"text", "image", "reference"}:
body.update(duration=args.duration, aspect_ratio=args.aspect_ratio, resolution=args.resolution)
if args.mode == "image": body["image"] = locator(args.image, input_hosts)
if args.mode == "reference": body["reference_images"] = [locator(x, input_hosts) for x in args.reference]
if args.mode == "edit": endpoint = "/videos/edits"; body["video"] = locator(args.video, input_hosts)
if args.mode == "extend":
endpoint = "/videos/extensions"; body["video"] = locator(args.video, input_hosts); body["duration"] = args.duration
body.pop("aspect_ratio", None); body.pop("resolution", None)
return endpoint, body
def redacted_body(body):
clean = dict(body)
for key in ("image", "video"):
if key in clean: clean[key] = redact(clean[key])
if "reference_images" in clean: clean["reference_images"] = [redact(x) for x in clean["reference_images"]]
return clean
class PinnedHTTPSConnection(http.client.HTTPSConnection):
def __init__(self, host, address, timeout):
super().__init__(host, 443, timeout=timeout, context=ssl.create_default_context()); self.address = address
def connect(self):
raw = socket.create_connection((self.address, 443), self.timeout)
try: self.sock = self._context.wrap_socket(raw, server_hostname=self.host)
except Exception: raw.close(); raise
def output_target(url, allowed_hosts):
parsed = urllib.parse.urlsplit(url)
if parsed.scheme != "https" or not parsed.hostname or parsed.username or parsed.password or parsed.fragment or parsed.port not in (None, 443):
raise RuntimeError("Output URL violates HTTPS authority policy")
host = parsed.hostname.rstrip(".").lower()
try: ipaddress.ip_address(host)
except ValueError: pass
else: raise RuntimeError("Output URL rejects IP literals")
if host not in allowed_hosts: raise RuntimeError("Output host is outside the approved exact-host policy")
addresses = public_addresses(host); target = parsed.path or "/"
if parsed.query: target += "?" + parsed.query
return host, target, addresses
def verify_mp4(path):
path = pathlib.Path(path); size = path.stat().st_size
if size <= 0 or size > MAX_VIDEO: raise RuntimeError("Artifact size is outside policy")
digest = hashlib.sha256()
with path.open("rb") as stream:
if stream.read(12)[4:8] != b"ftyp": raise RuntimeError("Artifact lacks MP4 ftyp signature")
stream.seek(0)
for chunk in iter(lambda: stream.read(1024 * 1024), b""): digest.update(chunk)
raw = bounded_process(["ffprobe", "-v", "error", "-show_streams", "-show_format", "-of", "json", str(path)], 30, MAX_JSON)
metadata = json.loads(raw); streams = metadata.get("streams", [])
video = next((item for item in streams if item.get("codec_type") == "video"), None)
if not video: raise RuntimeError("Artifact has no video stream")
bounded_process(["ffmpeg", "-nostdin", "-v", "error", "-i", str(path), "-f", "null", "-"], 300)
return {"bytes": size, "sha256": digest.hexdigest(), "qa": {"container": metadata.get("format", {}).get("format_name"),
"video_codec": video.get("codec_name"), "width": video.get("width"), "height": video.get("height"),
"fps": video.get("avg_frame_rate"), "duration": metadata.get("format", {}).get("duration"),
"audio_present": any(item.get("codec_type") == "audio" for item in streams), "full_decode": "passed"}}
def download_mp4(url, output, allowed_hosts, ledger_path, record):
target = pathlib.Path(output); target.parent.mkdir(parents=True, exist_ok=True)
stage = target.with_name("." + target.name + ".xai-stage"); expected = record.get("artifact_staged") or record.get("artifact")
if target.exists():
if not isinstance(expected, dict): raise RuntimeError("Existing artifact has no durable expected evidence")
artifact = verify_mp4(target)
if (artifact["sha256"], artifact["bytes"]) != (expected.get("sha256"), expected.get("bytes")): raise RuntimeError("Existing artifact mismatch")
return artifact
if stage.exists() and isinstance(expected, dict):
artifact = verify_mp4(stage)
if (artifact["sha256"], artifact["bytes"]) != (expected.get("sha256"), expected.get("bytes")): raise RuntimeError("Staged artifact mismatch")
os.replace(stage, target); fsync_directory(target.parent); return artifact
host, path_query, addresses = output_target(url, allowed_hosts); url_hash = sha256_bytes(url.encode("utf-8"))
if record.get("output_url_sha256") not in (None, url_hash): raise RuntimeError("Output URL changed during incomplete transfer")
record.update(state="downloading", output_url_sha256=url_hash, output_host=host, download_started_at=record.get("download_started_at") or utc_now())
save_record(ledger_path, record); stage.unlink(missing_ok=True)
connection = PinnedHTTPSConnection(host, addresses[0], 120)
try:
connection.request("GET", path_query, headers={"Host": host, "Accept": "video/mp4", "User-Agent": "xai-video-guard/2"})
response = connection.getresponse()
if response.status != 200: raise RuntimeError(f"Output returned HTTP {response.status}")
media_type = response.getheader("Content-Type", "").split(";", 1)[0].lower()
if media_type not in {"video/mp4", "application/octet-stream"}: raise RuntimeError("Unexpected output media type")
announced = response.getheader("Content-Length")
if announced is not None and (not announced.isdigit() or int(announced) > MAX_VIDEO): raise RuntimeError("Invalid/excessive Content-Length")
fd = os.open(stage, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600); total = 0
try:
with os.fdopen(fd, "wb") as dst:
while True:
chunk = response.read(1024 * 1024)
if not chunk: break
total += len(chunk)
if total > MAX_VIDEO: raise RuntimeError("Output exceeded byte cap")
dst.write(chunk)
dst.flush(); os.fsync(dst.fileno())
except BaseException: stage.unlink(missing_ok=True); raise
finally: connection.close()
artifact = verify_mp4(stage); record.update(state="artifact-staged", artifact_staged=artifact, artifact_validated_at=utc_now())
save_record(ledger_path, record); os.replace(stage, target); fsync_directory(target.parent); return artifact
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--mode", choices=["text", "image", "reference", "edit", "extend"], required=True)
parser.add_argument("--model", choices=list(OUT_RATE), required=True)
parser.add_argument("--prompt", required=True); parser.add_argument("--duration", type=int)
parser.add_argument("--resolution", choices=["480p", "720p", "1080p"], default="480p")
parser.add_argument("--aspect-ratio", default="16:9")
parser.add_argument("--image"); parser.add_argument("--reference", action="append", default=[]); parser.add_argument("--video")
parser.add_argument("--source-duration", type=Decimal); parser.add_argument("--source-resolution", choices=["480p", "720p", "1080p"])
parser.add_argument("--source-sha256", help="SHA-256 of source file, or a dossier hash covering ordered references")
parser.add_argument("--source-local", action="append", default=[], help="Ordered local mirror; repeat for references")
parser.add_argument("--job-key", required=True)
parser.add_argument("--input-host", default="", help="Comma-separated reviewed exact source URL hosts; omit for file_id-only inputs")
parser.add_argument("--output-host", required=True, help="Comma-separated reviewed exact result hosts")
parser.add_argument("--pricing-checked-at", required=True); parser.add_argument("--pricing-evidence-sha256", required=True)
parser.add_argument("--rights-sha256", required=True); parser.add_argument("--moderation-sha256", required=True); parser.add_argument("--qa-plan-sha256", required=True)
parser.add_argument("--ledger", required=True); parser.add_argument("--output", default="xai-video.mp4")
parser.add_argument("--execute", action="store_true"); parser.add_argument("--approval-sha256"); parser.add_argument("--max-cost-usd")
parser.add_argument("--require-zdr", action="store_true")
args = parser.parse_args()
if not args.job_key or any(not (character.isalnum() or character in "-_") for character in args.job_key): raise SystemExit("Invalid job-key")
validate(args)
input_hosts = () if not args.input_host.strip() else exact_hosts(args.input_host, "input-host")
output_hosts = exact_hosts(args.output_host, "output-host")
endpoint, body = build(args, set(input_hosts)); sources = source_evidence(args); amount = quote(args)
if not amount.is_finite() or amount <= 0: raise SystemExit("Computed cost must be finite and positive")
for label, value in (("pricing", args.pricing_evidence_sha256), ("rights", args.rights_sha256), ("moderation", args.moderation_sha256), ("QA", args.qa_plan_sha256)):
if len(value) != 64 or any(character not in "0123456789abcdefABCDEF" for character in value): raise SystemExit(f"{label} evidence must be SHA-256")
safe_body = redacted_body(body); safe_body["prompt"] = "sha256:" + sha256_bytes(args.prompt.encode("utf-8"))
safe_sources = [{key: value for key, value in item.items() if key != "path"} | {"path_sha256": sha256_bytes(item["path"].encode("utf-8"))} for item in sources]
output_policy = {"exact_hosts": list(output_hosts), "https": True, "redirects": "reject", "public_dns": True,
"reject_ip_literals": True, "dns_pinning": "connect-IP-with-TLS-SNI-and-host-certificate",
"max_bytes": MAX_VIDEO, "container": "mp4", "full_decode": True}
request_hash = sha256_bytes(canonical(body))
envelope = {"schema_version": 1, "job_key": args.job_key, "api_origin": API, "endpoint": endpoint,
"mode": args.mode, "model": args.model, "request_sha256": request_hash, "redacted_request": safe_body,
"sources": safe_sources, "input_exact_hosts": list(input_hosts), "output": str(pathlib.Path(args.output).resolve()),
"output_policy": output_policy, "quoted_usd": format(amount, "f"), "create_count": 1,
"pricing": {"checked_at": args.pricing_checked_at, "evidence_sha256": args.pricing_evidence_sha256.lower()},
"governance": {"rights_sha256": args.rights_sha256.lower(), "moderation_sha256": args.moderation_sha256.lower(),
"qa_plan_sha256": args.qa_plan_sha256.lower(), "require_zdr": args.require_zdr}}
approval = sha256_bytes(canonical(envelope))
print(json.dumps({"approval_sha256": approval, "plan": envelope}, indent=2, ensure_ascii=False))
print(f"DRY RUN — exact maximum USD {format(amount, 'f')}; one create; no network request sent", file=sys.stderr)
if not args.execute: return
if args.approval_sha256 != approval: raise SystemExit("Exact approval digest mismatch")
try: maximum = Decimal(args.max_cost_usd or "")
except InvalidOperation: raise SystemExit("max-cost-usd must be decimal")
if not maximum.is_finite() or maximum <= 0 or maximum < amount: raise SystemExit("max-cost-usd must be finite, positive, and at least the quote")
try: checked_at = datetime.fromisoformat(args.pricing_checked_at.replace("Z", "+00:00"))
except ValueError: raise SystemExit("pricing-checked-at must be ISO-8601")
if checked_at.tzinfo is None: raise SystemExit("pricing-checked-at must include timezone")
age = datetime.now(timezone.utc) - checked_at.astimezone(timezone.utc)
if age.total_seconds() < -300 or age.total_seconds() > 86400: raise SystemExit("Pricing check must be no more than 24 hours old")
for item in body.values():
locators = item if isinstance(item, list) else [item]
for locator_item in locators:
if isinstance(locator_item, dict) and "url" in locator_item: public_addresses(urllib.parse.urlsplit(locator_item["url"]).hostname)
if args.require_zdr and os.environ.get("XAI_ZDR_CONFIRMED") != "1":
raise SystemExit("Verify ZDR in xAI Console, then set XAI_ZDR_CONFIRMED=1 for this governed run")
api_key = os.environ.get("XAI_API_KEY")
if not api_key: raise SystemExit("Set XAI_API_KEY in the server-side environment")
ledger_path = pathlib.Path(args.ledger)
if ledger_path.exists():
record = read_record(ledger_path)
if record.get("request_sha256") != request_hash or record.get("approval_sha256") != approval: raise SystemExit("Ledger belongs to a different approved request")
request_id = record.get("request_id")
if not request_id: raise SystemExit(f"Ledger state {record.get('state')!r} has no request ID; never replay")
zdr = record.get("create_zdr_header")
else:
record = {"schema_version": 1, "state": "intent-recorded", "recorded_at": utc_now(), "job_key": args.job_key,
"request_sha256": request_hash, "approval_sha256": approval, "endpoint": endpoint, "mode": args.mode,
"model": args.model, "redacted_request": safe_body, "sources": safe_sources, "output_policy": output_policy,
"quoted_usd": format(amount, "f"), "pricing": envelope["pricing"], "governance": envelope["governance"]}
try: exclusive_record(ledger_path, record)
except FileExistsError: raise SystemExit("Another process acquired this job ledger")
record.update(state="create-started", create_started_at=utc_now()); save_record(ledger_path, record)
try: created, zdr = request_json("POST", API + endpoint, api_key, body, retries=0)
except APIProblem as exc:
ambiguous = exc.status is None or exc.status >= 500 or exc.status in {408, 409}
record.update(state="create-outcome-unknown" if ambiguous else "create-rejected-known", error_kind=exc.kind,
http_status=exc.status, error_body_sha256=exc.body_sha256, replay_allowed=False, create_outcome_at=utc_now())
save_record(ledger_path, record)
raise SystemExit("Create stopped; ambiguous jobs must be reconciled and no job key is replayable")
request_id = created.get("request_id")
if not isinstance(request_id, str) or not request_id or len(request_id) > 128:
record.update(state="create-outcome-unknown", error_kind="unusable-success-request-id",
response_sha256=sha256_bytes(canonical(created)), replay_allowed=False, create_outcome_at=utc_now())
save_record(ledger_path, record); raise SystemExit("No usable request_id returned; reconcile before replacement")
record.update(state="request-pending", request_id=request_id, create_zdr_header=zdr, submitted_at=utc_now()); save_record(ledger_path, record)
if args.require_zdr and zdr != "true":
record["zdr_mismatch"] = True; save_record(ledger_path, record)
raise SystemExit("ZDR header was not true; stop handling and escalate immediately")
deadline = time.monotonic() + 20 * 60
while time.monotonic() < deadline:
try: result, poll_zdr = request_json("GET", API + "/videos/" + urllib.parse.quote(request_id, safe=""), api_key, retries=3)
except APIProblem as exc:
record.update(poll_error_kind=exc.kind, poll_http_status=exc.status, poll_error_body_sha256=exc.body_sha256); save_record(ledger_path, record)
raise SystemExit("Polling failed; resume the known request ID later")
status = result.get("status"); record.update(last_status=status, poll_zdr_header=poll_zdr, progress=result.get("progress"), last_polled_at=utc_now()); save_record(ledger_path, record)
if args.require_zdr and poll_zdr != "true":
record["zdr_mismatch"] = True; save_record(ledger_path, record)
raise SystemExit("A poll response did not confirm ZDR; stop handling and escalate")
if status == "done":
video = result.get("video") or {}
if video.get("respect_moderation") is not True:
record.update(state="moderation-blocked", moderation_value=video.get("respect_moderation")); save_record(ledger_path, record)
raise SystemExit("Output did not pass moderation; do not publish or bypass")
url = video.get("url")
if not url: raise RuntimeError("Done response has no video URL")
artifact = download_mp4(url, args.output, set(output_hosts), ledger_path, record)
ticks = (result.get("usage") or {}).get("cost_in_usd_ticks")
if not isinstance(ticks, int) or ticks < 0: reconciliation = {"status": "missing-or-invalid-stop-further-creates"}
else:
actual = Decimal(ticks) / Decimal(10_000_000_000)
reconciliation = {"status": "matched" if actual == amount else "variance-stop-further-creates",
"actual_usd": format(actual, "f"), "variance_usd": format(actual - amount, "f")}
if actual > maximum: reconciliation["status"] = "ceiling-exceeded-stop-further-creates"
record.update(state="artifact-secured", artifact=dict(artifact, path=str(pathlib.Path(args.output).resolve())),
output_duration=video.get("duration"), cost_in_usd_ticks=ticks, cost_reconciliation=reconciliation,
result_model=result.get("model"), human_review="pending", stored_at=utc_now())
save_record(ledger_path, record); print(json.dumps({"artifact": record["artifact"], "cost_reconciliation": reconciliation}, indent=2)); return
if status in {"failed", "expired"}:
record.update(state="request-" + status, error_sha256=sha256_bytes(str(result.get("error", "")).encode("utf-8"))); save_record(ledger_path, record)
raise SystemExit(f"Video request {status}")
if status != "pending": raise RuntimeError(f"Unknown status {status!r}")
time.sleep(5)
record["state"] = "local-poll-timeout"; save_record(ledger_path, record)
raise SystemExit("Local polling ended; resume GET by request_id, never create a replacement")
if __name__ == "__main__":
main()
空运行示例:一个 6 秒的 720p T2V,报价为 USD 0.42,不使用密钥,也不发起网络请求。将每个证据占位符替换为受保护的 64 位十六进制摘要,并使用当前的时区感知价格核对时间:
python xai_video_guard.py --mode text --model grok-imagine-video --duration 6 --resolution 720p --aspect-ratio 16:9 --job-key cup-v1 --output-host vidgen.x.ai --pricing-checked-at 2026-07-10T12:00:00Z --pricing-evidence-sha256 '<64-hex>' --rights-sha256 '<64-hex>' --moderation-sha256 '<64-hex>' --qa-plan-sha256 '<64-hex>' --prompt "Wide dawn shot of a blue ceramic cup on a rain-dark cafe table. A hand lifts it while the camera makes one slow push-in. End beside the window. Quiet room tone and one soft ceramic clink." --ledger attempt-cup.json
完成受保护的负载/当前价格审核后,使用完全相同且未更改的参数再次运行,并添加 --execute --approval-sha256 '<printed-digest>' --max-cost-usd 0.42;仅在服务端 shell 中导出 XAI_API_KEY。切勿将真实密钥粘贴到文件、共享命令历史记录、浏览器代码、日志或聊天中。
示例 1.5 I2V 空运行:12 秒的 1080p 输出费用为 USD 3.010。源素材必须已获得使用授权,并记录其 SHA-256:
python xai_video_guard.py --mode image --model grok-imagine-video-1.5 --duration 12 --resolution 1080p --aspect-ratio 16:9 --image file_authorized_waterfall --source-local ./authorized-waterfall.png --source-sha256 REPLACE_WITH_LOCAL_FILE_SHA256 --job-key waterfall-v1 --output-host vidgen.x.ai --pricing-checked-at 2026-07-10T12:00:00Z --pricing-evidence-sha256 '<64-hex>' --rights-sha256 '<64-hex>' --moderation-sha256 '<64-hex>' --qa-plan-sha256 '<64-hex>' --prompt "The waterfall surges from the starting frame; mist rolls toward the lens as the camera slowly dollies back. End on the full cliff. Deep rushing water and distant birds." --ledger attempt-waterfall.json
本示例特意采用下载方式,而不请求 storage_options。只有在确定其隐私生命周期后,才添加服务端持久化功能。
将异步状态视为证据
创建操作会返回 request_id。每隔几秒轮询一次 GET /v1/videos/{request_id};xAI 示例使用 5 秒间隔。精确状态为 pending、done、expired 和 failed。本地 SDK 超时并不意味着提供方操作失败:保留请求 ID 并继续轮询。
状态为 done 时,验证 video.respect_moderation 为 true,记录 model、progress、duration 和整数费用计量值,然后尽快下载临时的 vidgen.x.ai URL。普通 URL 未公布确切有效期。批量图像/视频结果 URL 属于另一种情况,会在 1 小时后过期。
运行 ffprobe 并完整解码文件。记录容器、视频编解码器、尺寸、时长、帧率、每个音频流的编解码器/声道数/采样率、字节数和 SHA-256。审核开场帧保真度、时间上的身份一致性、接触物理效果、摄像机连续性、编辑约束或扩展衔接、音画同步、语音准确性、不需要的文字/徽标、审核结果以及公开披露准备情况。
有意选择存储、Files 和 Batch
对于需要反复使用且受治理的输入,优先使用私有 Files file_id,而非公开源 URL。xAI Files 支持 PNG/JPEG/WebP 和 MP4,用于 Imagine 参考素材;文件按团队范围管理。上传的文件会一直保留,直到删除,除非设置 expires_after,其范围为 3,600 到 2,592,000 秒(1 小时到 30 天)。当生产记录不再需要这些文件时,请将其删除。
Files 存储单独计费:当前定价页面显示为 $0.025/GiB/day;Files 下载费用为 $0.20/GiB。如果获批的工作流包含 file_id 或持久化 storage_options,请将这项随时间和用量变化的费用纳入考量。受保护的示例通过使用普通的临时结果并在本地进行受治理的下载,避免了这些可变存储费用。
storage_options 可以将 Imagine 输出私密地持久化:
{"filename":"approved-cut.mp4","expires_after":86400,"public_url":false}
省略 expires_after 会使文件永久保留,直到被删除。public_url: true 会创建无需身份验证的分享链接;如果未设置过期时间,链接可能会永久有效。优先使用私有存储。如果必须公开交付,请设置最短的 1 小时至 30 天过期时间,记录受众和过期时间,并在交付后撤销链接。公开 URL 的有效期永远不会超过其文件。不要在普通日志中记录签名 URL 或不可猜测的 URL。
目前没有明确说明 ZDR 与持久化 Files 或 storage_options 的兼容性。持久化存储在概念上不同于临时推理。对于严格的 ZDR 工作,在 xAI 确认合同约定的行为前,请避免持久化到 Files;应保留自己的受治理加密副本。
Batch 支持 /v1/videos/generations、/edits 和 /extensions,包括混合 JSONL 任务。它按标准媒体费率收费,不提供视频折扣;大多数批次会在 24 小时内完成。仅将其用于经过审核的大批量工作,并设置总费用上限、稳定的 custom_id 和逐项权利记录。返回的媒体 URL 会在 1 小时后过期。Batch 不能用来规避实时创建请求结果不确定时的幂等性问题。
适度使用、明确披露并保护个人
xAI 将视频标记为须接受内容政策审核。respect_moderation: true 是发布所必需的条件,但并不足够。不要规避提供方的安全措施,也不要移除来源信息元数据或水印;AUP 明确禁止规避安全措施和剥离嵌入的来源信息。
请确保对所有提示词、源图像和视频、品牌、地点、音乐、声音及表演均拥有相应权利。对于可识别的个人,请记录其知情授权,涵盖肖像和声音、合成处理、编辑范围、受众、地区、时长和商业背景。AUP 禁止欺骗性冒充、未经同意使用他人肖像制作色情或亲密内容、诽谤或造成虚假印象的描绘、侵犯隐私权或公开权、欺诈,以及对儿童的性剥削。网上找到的名人图像不代表获得了同意。
根据现行 Enterprise Terms,在 xAI 与客户之间,客户保留输入权利并拥有输出,但须遵守适用法律,且不包括 xAI 技术。此项权利分配不会赋予客户对第三方个人、品牌、表演或受版权保护源素材的权利;输出也可能并非独一无二。Terms 禁止将输出描述为由人类生成,也禁止使用输出训练客户或其提供方的 ML/AI 模型。
如果观众可能受到误导,请清楚披露实质性的 AI 生成或修改,并按适用司法辖区和平台要求标注政治、广告、生物识别、儿童安全和合成媒体内容。保留原始媒体、哈希值、提示词、模型别名、xAI 请求和费用凭证、编辑记录及披露决定。切勿声称 xAI 审核能够证明内容真实、获得认可、已取得版权许可或符合法律要求。
规定 API 数据契约,而非消费者数据契约
对于直接使用 API 的客户,适用 xAI 的 Enterprise Terms、DPA 和 AUP;公开的消费者 Privacy Policy 明确说明,其不涵盖为企业客户处理的 xAI API 数据。该政策还说明,X 上的 Grok 受 X 政策约束。
xAI 的 API 安全常见问题说明,未经明确许可,不会使用 API 输入/输出进行训练;通常会保留 30 天用于滥用审计,之后删除。Enterprise Terms 规定,不会使用用户内容训练基础/AI 系统,并规定最迟在交互结束后 30 天内删除,但所列法律、安全、安保、审核和调查例外情况除外。
ZDR 仅面向企业,适用于整个团队。请在 xAI Console 中确认,并记录创建请求和轮询请求响应中的 x-zero-data-retention: true 响应头。仍会进行审核。常见问题称,数据绝不会持久化,交付后不会留存记录;当前 Enterprise Terms 则更准确地允许保留临时处理产物,直至交付或完成后一小时(以较早者为准),之后不会留存持久日志/备份。应以法律条款和订单表为准,记录这一措辞差异,不要宣称“零处理”,也不要规避自身的审计/保留义务。
当前 Enterprise Terms 要求客户防止在启用 ZDR 的 API 之外主动提交个人数据。任何个人数据只能通过签约的 ZDR 团队,并依照适用的 DPA、合法依据、告知、数据最小化、传输评估、访问控制和删除计划进行传输。PHI 还需要 BAA 和 ZDR。欧盟数据驻留是企业选项,不能仅凭 api.x.ai 推断为默认配置。请通过合同确认集群、子处理方、跨境传输机制和故障转移行为。
将声明归入正确类别
- 有据可查的事实: 当前 xAI 第一方来源中的请求字段、限制、价格、状态、政策文本或观测到的响应属性。
- 服务商声明: xAI 对质量、物理效果、指令遵循、保留、音频、延迟或基准排名的说法。
- 生产启发式做法: 源自电影制作和风险控制的提示词结构、参考素材精简、镜头简化、审核策略或连续性实践。
- 未知事项: 当前架构和合同未明确的任何内容——FPS、临时 URL 的确切有效期、音频规格/控件、种子、负面提示词、Base64/公开输入大小、ZDR 与 Files 的确切交互方式,或未配置企业选项时的数据驻留情况。
对于重要冲突,应以较新的端点专属参考文档、xAI Console 和签署的合同为准;否则,请暂停并咨询 xAI 支持。2026-07-10 已知的冲突包括:
- 公开的速率限制/模型说明在 1 RPS、10 RPS 和 70 RPM 之间存在差异;
- Files 指南称上限为 48 MB,而上传参考文档称为 50 MB;
- ZDR 常见问题称交付后不会留存任何内容,而 Enterprise Terms 明确规定了以交付或一小时为界(以较早者为准)的临时处理期限;
- xAI 的发布/新闻页面介绍了原生音频,但 REST 参考文档没有提供音频控件或技术规格;
- 默认 Imagine 输出 URL 仅被描述为“临时”,而一小时的有效期仅在 Batch 结果文档中有明确说明。
一手资料索引
- 视频指南和 REST 参考:https://docs.x.ai/developers/model-capabilities/video/generation、https://docs.x.ai/developers/rest-api-reference/inference/videos
- I2V、参考素材、编辑、扩展:https://docs.x.ai/developers/model-capabilities/video/image-to-video、https://docs.x.ai/developers/model-capabilities/video/reference-to-video、https://docs.x.ai/developers/model-capabilities/video/editing、https://docs.x.ai/developers/model-capabilities/video/extension
- 模型页面和定价:https://docs.x.ai/developers/models/grok-imagine-video、https://docs.x.ai/developers/models/grok-imagine-video-1.5、https://docs.x.ai/developers/pricing
- 模型查询、费用、限制、错误、版本发布:https://docs.x.ai/developers/rest-api-reference/inference/models、https://docs.x.ai/developers/cost-tracking、https://docs.x.ai/developers/rate-limits、https://docs.x.ai/developers/debugging、https://docs.x.ai/developers/release-notes
- 文件和 Batch:https://docs.x.ai/developers/model-capabilities/imagine/files、https://docs.x.ai/developers/model-capabilities/imagine/files/inputs、https://docs.x.ai/developers/model-capabilities/imagine/files/outputs、https://docs.x.ai/developers/files/managing-files、https://docs.x.ai/developers/files/public-urls、https://docs.x.ai/developers/advanced-api-usage/batch-api
- API 安全和法律条款:https://docs.x.ai/developers/faq/security、https://x.ai/legal/terms-of-service-enterprise、https://x.ai/legal/data-processing-addendum、https://x.ai/legal/acceptable-use-policy
- 消费者服务与 X 的边界:https://docs.x.ai/grok/overview、https://x.ai/legal/terms-of-service、https://x.ai/legal/privacy-policy
- 原生音频相关的提供方声明和 1.5 版本发布:https://x.ai/news/grok-imagine-api、https://x.ai/news/grok-imagine-video-1-5
- 服务直接运行状态:https://status.x.ai/
在当前 xAI 文档和研究索引中,未找到 Grok Imagine Video 专门的一手技术模型卡或安全卡。除非 xAI 发布具体的一手资料,否则应将其架构、训练语料、人口群体性能、水印实现、详细安全评估和基准测试的泛化能力视为未知。