Files
image2api/frontend/src/views/DocsView.vue
T
chiyi cfb7bcebc9 fix(frontend): 视频模型参考图/媒体上限按 preset 展示,文档页加定价列
模型管理与文档页改为读 /video-presets 的 max_images/max_videos/max_audios,去掉写死的图片/视频/音频上限与重复的参考资产总数;文档表新增定价列,model/类型/定价 三列不换行。
2026-08-09 21:02:18 +08:00

575 lines
33 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
// API 对接文档 — OpenAI-compatible. Lists live models and shows ready-to-run
// curl / Python(openai SDK) examples for image + video, wired to this
// deployment's base URL and the caller's model ids.
import { ref, computed, onMounted } from 'vue'
import { auth } from '../auth'
import { api } from '../api'
import Icon from '../components/Icon.vue'
import { points } from '../credits'
import { mediaCaps, presetMap } from '../videoCaps'
const base = computed(() => location.origin) // /v1 is same-origin (dev: Vite proxy)
const keyHint = computed(() => auth.user?.api_keys?.[0]?.key_preview || 'YOUR_API_KEY')
const models = ref([])
const presets = ref({}) // /video-presets: key → 预设(参考资产分类上限的真源)
onMounted(async () => {
const [r, p] = await Promise.all([api('/managed-models'), api('/video-presets')])
if (r.ok) models.value = (r.data?.data || []).filter((m) => m.enabled !== false)
if (p.ok) presets.value = presetMap(p.data?.data)
})
// 各类参考资产的上限(图片/视频/音频)由后端预设给出,不在这里写死。
function caps(m) { return mediaCaps(m, presets.value[m.id]) }
// 参考图张数:支持音视频参考的模型,max_reference_images 是各类资产合计,图片自己
// 的上限来自预设 max_images。
function refImages(m) { return caps(m)?.images || m.max_reference_images }
const imageModels = computed(() => models.value.filter((m) => m.type === 'image'))
const videoModels = computed(() => models.value.filter((m) => m.type === 'video'))
function pubName(m) {
return m?.alias || m?.id || ''
}
const sampleImage = computed(() => pubName(imageModels.value[0]) || 'firefly-image-4')
const sampleVideo = computed(() => pubName(videoModels.value[0]) || 'firefly-kling3')
const sampleSeconds = computed(() => String(videoModels.value[0]?.durations?.[0] || '8s').replace(/s$/, ''))
function modeEmpty(m) {
if (m.type === 'image') return !(m.resolutions || []).length && !m.image_to_image && !(m.ratios || []).length
return !(m.resolutions || []).length && !(m.durations || []).length && !m.max_reference_images && !caps(m) && !(m.ratios || []).length
}
// ---- 定价(同模型管理的分档展示):代理账号看代理价,某档没设代理价时回退普通价;
// 普通价为空表示该档不支持,不展示。视频价 = 分辨率价 + 时长价(按秒计价的模型给 /s 价)。
const isAgent = computed(() => auth.user?.role === 'agent')
function tierPrice(normal, agent, key) {
const n = (normal || {})[key]
if (n == null) return null
if (isAgent.value) {
const a = (agent || {})[key]
if (a != null) return Number(a)
}
return Number(n)
}
function resPrice(m, r) { return tierPrice(m.prices, m.prices_agent, r) }
function durPrice(m, d) { return tierPrice(m.duration_prices, m.duration_prices_agent, d) }
function perSecondPrice(m) { return tierPrice(m.duration_prices, m.duration_prices_agent, 'per_second') }
function priceEmpty(m) {
if ((m.resolutions || []).some((r) => resPrice(m, r) != null)) return false
if (m.type !== 'video') return true
if (perSecondPrice(m) != null) return false
return !(m.durations || []).some((d) => durPrice(m, d) != null)
}
// ---- request parameter tables ----
const imageParams = [
['model', 'string', '必填', '模型名(别名优先),见上表(图像)'],
['prompt', 'string', '必填', '文字描述'],
['size', 'string', '可选', '宽x高,如 "1024x1024"。同时决定「比例」+「分辨率档」(按长边)。具体怎么填见下方对照表;留空 = 1:1 · 2K'],
]
const editParams = [
['image', 'file', '必填', '输入图;多张参考图重复 image[] 字段(multipart 文件上传)'],
['prompt', 'string', '必填', '编辑/参考描述'],
['model', 'string', '必填', '模型名(别名优先,需支持图生图)'],
['size', 'string', '可选', '同图像:决定比例 + 分辨率档(见下方对照表)'],
]
const videoParams = [
['model', 'string', '必填', '模型名(别名优先),见上表(视频)'],
['prompt', 'string', '必填', '文字描述'],
['seconds', 'string|int', '必填', '时长秒数,如 "5" "8"(取决于模型支持)'],
['size', 'string', '可选', '如 "1280x720" / "720x1280" → 决定比例与分辨率'],
['input_reference', 'file', '可选', '参考图/首尾帧(multipart 文件上传;多张时重复 input_reference[] 字段)。runway 图生视频必填 1 张,张数上限见上方模型表'],
['reference_mode', 'string', '可选', '参考图用途:"frame"=首尾帧(最多 2 张,第 1 张为首帧、第 2 张为尾帧,只传 1 张即仅首帧),"asset"=普通参考图。留空用模型默认;仅支持单一模式的模型不可传'],
]
// ---- size → 比例 × 分辨率档 对照表(用 size 该传的值)----
// size 的长边映射档位:<1800→1K · 18003499→2K · ≥3500→4K;宽高比映射比例。
const sizeTable = [
{ ratio: '1:1 · 方', k1: '1024x1024', k2: '2048x2048', k4: '4096x4096' },
{ ratio: '5:4 · 横', k1: '1280x1024', k2: '2560x2048', k4: '3840x3072' },
{ ratio: '4:3 · 横', k1: '1024x768', k2: '2048x1536', k4: '4096x3072' },
{ ratio: '3:2 · 横', k1: '1200x800', k2: '2400x1600', k4: '3600x2400' },
{ ratio: '16:9 · 横', k1: '1280x720', k2: '2048x1152', k4: '4096x2304' },
{ ratio: '2:1 · 横', k1: '1440x720', k2: '2880x1440', k4: '4096x2048' },
{ ratio: '21:9 · 超宽', k1: '1680x720', k2: '2520x1080', k4: '5040x2160' },
{ ratio: '3:1 · 超宽', k1: '1536x512', k2: '2304x768', k4: '3840x1280' },
{ ratio: '4:1 · 超宽', k1: '1728x432', k2: '2880x720', k4: '4096x1024' },
{ ratio: '8:1 · 超宽', k1: '1728x216', k2: '2880x360', k4: '4096x512' },
{ ratio: '4:5 · 竖', k1: '1024x1280', k2: '2048x2560', k4: '3072x3840' },
{ ratio: '3:4 · 竖', k1: '768x1024', k2: '1536x2048', k4: '3072x4096' },
{ ratio: '2:3 · 竖', k1: '800x1200', k2: '1600x2400', k4: '2400x3600' },
{ ratio: '9:16 · 竖', k1: '720x1280', k2: '1152x2048', k4: '2304x4096' },
{ ratio: '1:3 · 竖', k1: '512x1536', k2: '768x2304', k4: '1280x3840' },
{ ratio: '1:4 · 竖', k1: '432x1728', k2: '720x2880', k4: '1024x4096' },
{ ratio: '1:8 · 竖', k1: '216x1728', k2: '360x2880', k4: '512x4096' },
]
// ---- 视频 size → 比例 × 分辨率(720p / 1080p)----
// 视频按「短边」判档:短边 <1080 → 720p,≥1080 → 1080p;宽高比映射比例。
const videoSizeTable = [
{ ratio: '16:9 · 横', p720: '1280x720', p1080: '1920x1080' },
{ ratio: '9:16 · 竖', p720: '720x1280', p1080: '1080x1920' },
{ ratio: '1:1 · 方', p720: '720x720', p1080: '1080x1080' },
{ ratio: '4:3 · 横', p720: '960x720', p1080: '1440x1080' },
{ ratio: '3:4 · 竖', p720: '720x960', p1080: '1080x1440' },
{ ratio: '3:2 · 横', p720: '1080x720', p1080: '1620x1080' },
{ ratio: '2:3 · 竖', p720: '720x1080', p1080: '1080x1620' },
]
// ---- examples (built in script so refs resolve correctly) ----
const examples = computed(() => [
{
title: '文生图 · curl',
code:
`curl ${base.value}/v1/images/generations \\
-H "Authorization: Bearer ${keyHint.value}" \\
-H "Content-Type: application/json" \\
-d '{
"model": "${sampleImage.value}",
"prompt": "a corgi running in a golden wheat field, cinematic",
"size": "2048x2048"
}'`,
},
{
title: '文生图 · Python (openai SDK)',
code:
`import urllib.request
from openai import OpenAI
client = OpenAI(api_key="${keyHint.value}", base_url="${base.value}/v1")
resp = client.images.generate(
model="${sampleImage.value}",
prompt="a corgi running in a golden wheat field, cinematic",
size="2048x2048", # 2K · 1:1,见下方对照表
)
# 结果是图片 URL(上游原始直链,会过期 → 尽快下载/转存)
urllib.request.urlretrieve(resp.data[0].url, "out.png")`,
},
{
title: '图生图 / 参考图 · curl (multipart)',
code:
`curl ${base.value}/v1/images/edits \\
-H "Authorization: Bearer ${keyHint.value}" \\
-F model="${sampleImage.value}" \\
-F prompt="把这张图改成赛博朋克风格" \\
-F size="2048x2048" \\
-F image=@input.png
# 多张参考图:重复 -F image=@a.png -F image=@b.png`,
},
{
title: '图生图 · Python (openai SDK)',
code:
`import urllib.request
from openai import OpenAI
client = OpenAI(api_key="${keyHint.value}", base_url="${base.value}/v1")
resp = client.images.edit(
model="${sampleImage.value}",
image=open("input.png", "rb"), # 多张:image=[open("a.png","rb"), open("b.png","rb")]
prompt="把这张图改成赛博朋克风格",
)
# 结果是图片 URL(上游原始直链,会过期 → 尽快下载/转存)
urllib.request.urlretrieve(resp.data[0].url, "out.png")`,
},
{
title: '视频 · curl(创建 → 轮询 → 下载)',
code:
`# 1) 创建任务 → 立即返回 {"id": "...", "status": "queued"}
curl ${base.value}/v1/videos \\
-H "Authorization: Bearer ${keyHint.value}" \\
-H "Content-Type: application/json" \\
-d '{
"model": "${sampleVideo.value}",
"prompt": "a paper boat sailing down a rainy street, cinematic",
"seconds": "${sampleSeconds.value}",
"size": "1280x720"
}'
# 2) 轮询状态,直到 status=completed
curl ${base.value}/v1/videos/<VIDEO_ID> \\
-H "Authorization: Bearer ${keyHint.value}"
# 3) 下载 mp4(完成后)
curl ${base.value}/v1/videos/<VIDEO_ID>/content \\
-H "Authorization: Bearer ${keyHint.value}" -o out.mp4`,
},
{
title: '视频 · Python (requests, 轮询)',
code:
`import time, requests
base = "${base.value}/v1"
h = {"Authorization": "Bearer ${keyHint.value}"}
# 1) 创建
job = requests.post(f"{base}/videos", headers=h, json={
"model": "${sampleVideo.value}",
"prompt": "a paper boat sailing down a rainy street",
"seconds": "${sampleSeconds.value}",
"size": "1280x720",
}).json()
vid = job["id"]
# 2) 轮询
while True:
s = requests.get(f"{base}/videos/{vid}", headers=h).json()
if s["status"] in ("completed", "failed"):
break
time.sleep(5)
# 3) 下载
if s["status"] == "completed":
mp4 = requests.get(f"{base}/videos/{vid}/content", headers=h).content
open("out.mp4", "wb").write(mp4)`,
},
{
title: '列出模型 · curl',
code:
`curl ${base.value}/v1/models \\
-H "Authorization: Bearer ${keyHint.value}"`,
},
])
// ---- copy + toast ----
const toastMsg = ref('')
let t = null
function toast(m) { toastMsg.value = m; clearTimeout(t); t = setTimeout(() => (toastMsg.value = ''), 1800) }
async function copy(text) {
try { await navigator.clipboard.writeText(text); toast('已复制') } catch { toast('复制失败') }
}
</script>
<template>
<div class="theme-text space-y-10">
<header>
<span class="sticker"> 开发者</span>
<h1 class="mt-3 text-4xl md:text-5xl font-bold tracking-tight">接口文档</h1>
<p class="text-white/45 mt-2">完全兼容 OpenAI 接口规范 改个 <code class="text-white/70">base_url</code> <code class="text-white/70">api_key</code> 即可直接调用图像 / 视频 / 图生图全支持</p>
</header>
<!-- quickstart -->
<section class="grid md:grid-cols-2 gap-4">
<div class="card p-6">
<h2 class="text-sm font-semibold text-white/80">基础信息</h2>
<dl class="mt-4 space-y-3 text-sm">
<div class="flex items-center justify-between gap-3">
<dt class="text-white/45">Base URL</dt><dd class="font-mono text-white/90">{{ base }}/v1</dd>
</div>
<div class="flex items-center justify-between gap-3">
<dt class="text-white/45">鉴权</dt><dd class="font-mono text-white/90">Authorization: Bearer &lt;key&gt;</dd>
</div>
<div class="flex items-center justify-between gap-3">
<dt class="text-white/45">你的 Key</dt><dd class="font-mono text-white/70">{{ keyHint }}</dd>
</div>
</dl>
<p class="text-[11px] text-white/40 mt-4">还没有 Key? <router-link to="/settings" class="text-violet-300 underline">设置 API Key</router-link> 生成</p>
</div>
<div class="card p-6">
<h2 class="text-sm font-semibold text-white/80">端点</h2>
<ul class="mt-4 space-y-2.5 text-sm font-mono">
<li class="flex items-center gap-2"><span class="badge-get">GET</span><span class="text-white/80">/v1/models</span></li>
<li class="flex items-center gap-2"><span class="badge-post">POST</span><span class="text-white/80">/v1/images/generations</span><span class="text-white/35 font-sans text-xs">文生图</span></li>
<li class="flex items-center gap-2"><span class="badge-post">POST</span><span class="text-white/80">/v1/images/edits</span><span class="text-white/35 font-sans text-xs">图生图(multipart)</span></li>
<li class="flex items-center gap-2"><span class="badge-post">POST</span><span class="text-white/80">/v1/videos</span><span class="text-white/35 font-sans text-xs">建视频任务</span></li>
<li class="flex items-center gap-2"><span class="badge-get">GET</span><span class="text-white/80">/v1/videos/{id}</span><span class="text-white/35 font-sans text-xs">查状态</span></li>
<li class="flex items-center gap-2"><span class="badge-get">GET</span><span class="text-white/80">/v1/videos/{id}/content</span><span class="text-white/35 font-sans text-xs">下载 mp4</span></li>
</ul>
</div>
</section>
<!-- models -->
<section>
<h2 class="text-lg font-semibold mb-3">可用模型</h2>
<div class="card overflow-hidden">
<table class="w-full text-sm">
<thead>
<tr class="text-left text-[11px] uppercase tracking-wider text-white/40 border-b border-white/[0.08]">
<th class="px-4 py-3 font-medium whitespace-nowrap">model</th>
<th class="px-4 py-3 font-medium whitespace-nowrap">类型</th>
<th class="px-4 py-3 font-medium whitespace-nowrap" title="视频价 = 分辨率价 + 时长价">定价 <span class="normal-case text-white/30">积分</span></th>
<th class="px-4 py-3 font-medium">能力</th>
</tr>
</thead>
<tbody>
<tr v-for="m in models" :key="m.id" class="border-b border-white/[0.04] last:border-0">
<td class="px-4 py-3 font-mono text-white/90 whitespace-nowrap">{{ pubName(m) }}</td>
<td class="px-4 py-3 text-white/60 whitespace-nowrap">{{ m.type === 'video' ? '视频' : '图像' }}</td>
<td class="px-4 py-3">
<div class="flex flex-wrap items-center gap-1 text-[11px]">
<template v-for="r in (m.resolutions || [])" :key="'pr'+r">
<span v-if="resPrice(m, r) != null" class="cap cap-price">{{ r }}<b class="ml-1 font-semibold tabular-nums">{{ points(resPrice(m, r)) }}</b></span>
</template>
<template v-if="m.type === 'video'">
<span v-if="perSecondPrice(m) != null" class="cap cap-price">/s<b class="ml-1 font-semibold tabular-nums">{{ points(perSecondPrice(m)) }}</b></span>
<template v-else v-for="d in (m.durations || [])" :key="'pd'+d">
<span v-if="durPrice(m, d) != null" class="cap cap-price">{{ d }}<b class="ml-1 font-semibold tabular-nums">+{{ points(durPrice(m, d)) }}</b></span>
</template>
</template>
<span v-if="priceEmpty(m)" class="text-white/30"></span>
</div>
</td>
<td class="px-4 py-3">
<div class="flex flex-wrap items-center gap-1 text-[11px]">
<template v-if="m.type === 'image'">
<span v-for="r in (m.resolutions || [])" :key="r" class="cap cap-k">{{ r }}</span>
</template>
<template v-if="m.type === 'video'">
<span v-for="r in (m.resolutions || [])" :key="r" class="cap cap-k">{{ r }}</span>
<span v-if="(m.durations || []).length >= 2" class="cap cap-dur">{{ m.durations[0] }}{{ m.durations[m.durations.length - 1] }}</span>
<span v-else-if="(m.durations || []).length === 1" class="cap cap-dur">{{ m.durations[0] }}</span>
</template>
<span v-if="m.reference_mode === 'frame' && m.max_reference_images > 0" class="cap cap-frame">首尾帧 {{ Math.min(m.max_reference_images, 2) }}</span>
<span v-if="m.reference_mode === 'frame' && m.max_reference_images > 2" class="cap cap-ref">参考图 {{ refImages(m) }}</span>
<span v-else-if="m.reference_mode && m.reference_mode !== 'none' && m.reference_mode !== 'frame' && m.max_reference_images > 0" class="cap cap-ref">参考图 {{ refImages(m) }}</span>
<span v-if="m.type === 'image' && m.image_to_image && (!m.reference_mode || m.reference_mode === 'none')" class="cap cap-ref">参考图 1</span>
<template v-if="caps(m)">
<span v-if="caps(m).videos" class="cap cap-media">视频 {{ caps(m).videos }}</span>
<span v-if="caps(m).audios" class="cap cap-media">音频 {{ caps(m).audios }}</span>
</template>
<span v-for="r in (m.ratios || [])" :key="'rt'+r" class="cap cap-ratio">{{ r.replace(':', '×') }}</span>
<span v-if="modeEmpty(m)" class="text-white/30"></span>
</div>
</td>
</tr>
<tr v-if="!models.length"><td colspan="4" class="px-4 py-10 text-center text-white/35">暂无可用模型</td></tr>
</tbody>
</table>
</div>
</section>
<!-- parameters -->
<section class="grid lg:grid-cols-2 gap-6">
<div>
<h2 class="text-lg font-semibold mb-3">文生图参数 <span class="text-xs font-normal text-white/40">/v1/images/generations</span></h2>
<div class="card overflow-hidden">
<table class="w-full text-sm">
<thead><tr class="text-left text-[11px] uppercase tracking-wider text-white/40 border-b border-white/[0.08]">
<th class="px-4 py-2.5 font-medium">参数</th><th class="px-4 py-2.5 font-medium">类型</th><th class="px-4 py-2.5 font-medium">必填</th><th class="px-4 py-2.5 font-medium">说明</th>
</tr></thead>
<tbody>
<tr v-for="p in imageParams" :key="p[0]" class="border-b border-white/[0.04] last:border-0">
<td class="px-4 py-2.5 font-mono text-white/85">{{ p[0] }}</td>
<td class="px-4 py-2.5 text-white/50 font-mono text-xs">{{ p[1] }}</td>
<td class="px-4 py-2.5 text-white/55">{{ p[2] }}</td>
<td class="px-4 py-2.5 text-white/60 text-xs">{{ p[3] }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div>
<h2 class="text-lg font-semibold mb-3">图生图参数 <span class="text-xs font-normal text-white/40">/v1/images/edits · multipart</span></h2>
<div class="card overflow-hidden">
<table class="w-full text-sm">
<thead><tr class="text-left text-[11px] uppercase tracking-wider text-white/40 border-b border-white/[0.08]">
<th class="px-4 py-2.5 font-medium">参数</th><th class="px-4 py-2.5 font-medium">类型</th><th class="px-4 py-2.5 font-medium">必填</th><th class="px-4 py-2.5 font-medium">说明</th>
</tr></thead>
<tbody>
<tr v-for="p in editParams" :key="p[0]" class="border-b border-white/[0.04] last:border-0">
<td class="px-4 py-2.5 font-mono text-white/85">{{ p[0] }}</td>
<td class="px-4 py-2.5 text-white/50 font-mono text-xs">{{ p[1] }}</td>
<td class="px-4 py-2.5 text-white/55">{{ p[2] }}</td>
<td class="px-4 py-2.5 text-white/60 text-xs">{{ p[3] }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="lg:col-span-2">
<h2 class="text-lg font-semibold mb-3">视频参数 <span class="text-xs font-normal text-white/40">/v1/videos · 异步</span></h2>
<div class="card overflow-hidden">
<table class="w-full text-sm">
<thead><tr class="text-left text-[11px] uppercase tracking-wider text-white/40 border-b border-white/[0.08]">
<th class="px-4 py-2.5 font-medium">参数</th><th class="px-4 py-2.5 font-medium">类型</th><th class="px-4 py-2.5 font-medium">必填</th><th class="px-4 py-2.5 font-medium">说明</th>
</tr></thead>
<tbody>
<tr v-for="p in videoParams" :key="p[0]" class="border-b border-white/[0.04] last:border-0">
<td class="px-4 py-2.5 font-mono text-white/85">{{ p[0] }}</td>
<td class="px-4 py-2.5 text-white/50 font-mono text-xs">{{ p[1] }}</td>
<td class="px-4 py-2.5 text-white/55">{{ p[2] }}</td>
<td class="px-4 py-2.5 text-white/60 text-xs">{{ p[3] }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- size 对照表(课时表)—— 解决"传错分辨率" -->
<section>
<h2 class="text-lg font-semibold mb-1">图像分辨率对照表 · <code class="text-white/70 text-sm">size</code> 该传什么</h2>
<p class="text-xs text-white/45 mb-3">
左边选比例,上面选分辨率档,交叉格里就是 <code class="text-white/70">size</code> 要传的值(直接复制)
没有 <code class="text-white/70">quality</code> 参数,图像分辨率只看 <code class="text-white/70">size</code> <strong class="text-white/70">长边</strong>
档位必须是该模型支持的(见上方可用模型的分辨率列),不支持会自动回退到该模型最低档
</p>
<div class="card overflow-hidden">
<table class="w-full text-sm">
<thead><tr class="text-left text-[11px] uppercase tracking-wider text-white/40 border-b border-white/[0.08]">
<th class="px-4 py-2.5 font-medium">比例</th>
<th class="px-4 py-2.5 font-medium">1K</th>
<th class="px-4 py-2.5 font-medium">2K</th>
<th class="px-4 py-2.5 font-medium">4K</th>
</tr></thead>
<tbody>
<tr v-for="row in sizeTable" :key="row.ratio" class="border-b border-white/[0.04] last:border-0">
<td class="px-4 py-2.5 text-white/75">{{ row.ratio }}</td>
<td class="px-4 py-2.5 font-mono text-white/85">{{ row.k1 }}</td>
<td class="px-4 py-2.5 font-mono text-white/85">{{ row.k2 }}</td>
<td class="px-4 py-2.5 font-mono text-white/85">{{ row.k4 }}</td>
</tr>
</tbody>
</table>
</div>
<p class="text-xs text-white/40 mt-2">
:想要 <strong class="text-white/70">2K 16:9 横图</strong> <code class="text-white/70">"size": "2048x1152"</code>
留空 size = 默认 <strong class="text-white/70">1:1 · 2K</strong>
</p>
<!-- 视频分辨率(720p / 1080p,按短边判) -->
<h2 class="text-lg font-semibold mb-1 mt-8">视频分辨率对照表 · <code class="text-white/70 text-sm">size</code> 该传什么</h2>
<p class="text-xs text-white/45 mb-3">
视频用 <code class="text-white/70">720p</code> / <code class="text-white/70">1080p</code> 两档,只看 <code class="text-white/70">size</code> <strong class="text-white/70">短边</strong>(短边 1080 = 1080p,否则 720p)
档位必须是该视频模型支持的( grok-video 720p),不支持会被拒
</p>
<div class="card overflow-hidden">
<table class="w-full text-sm">
<thead><tr class="text-left text-[11px] uppercase tracking-wider text-white/40 border-b border-white/[0.08]">
<th class="px-4 py-2.5 font-medium">比例</th>
<th class="px-4 py-2.5 font-medium">720p</th>
<th class="px-4 py-2.5 font-medium">1080p</th>
</tr></thead>
<tbody>
<tr v-for="row in videoSizeTable" :key="row.ratio" class="border-b border-white/[0.04] last:border-0">
<td class="px-4 py-2.5 text-white/75">{{ row.ratio }}</td>
<td class="px-4 py-2.5 font-mono text-white/85">{{ row.p720 }}</td>
<td class="px-4 py-2.5 font-mono text-white/85">{{ row.p1080 }}</td>
</tr>
</tbody>
</table>
</div>
<p class="text-xs text-white/40 mt-2">
:想要 <strong class="text-white/70">720p 16:9 横版视频</strong> <code class="text-white/70">"size": "1280x720"</code>;
竖版 9:16 <code class="text-white/70">"720x1280"</code>
</p>
</section>
<!-- examples -->
<section class="space-y-4">
<h2 class="text-lg font-semibold">调用示例</h2>
<div v-for="ex in examples" :key="ex.title" class="code-card">
<div class="code-card-bar">
<span class="code-dots" aria-hidden="true"><i class="dot dot-r"></i><i class="dot dot-y"></i><i class="dot dot-g"></i></span>
<span class="code-card-title">{{ ex.title }}</span>
<button @click="copy(ex.code)" class="code-copy">
<Icon name="copy" class="w-3.5 h-3.5" /> 复制
</button>
</div>
<pre class="code-card-body"><code>{{ ex.code }}</code></pre>
</div>
</section>
<!-- responses -->
<section>
<h2 class="text-lg font-semibold mb-3">响应 & 计费</h2>
<div class="card p-6 space-y-3 text-sm text-white/70">
<p><strong class="text-white/90">图像</strong>(generations / edits)返回 OpenAI 图片格式:<code class="text-white/85 font-mono">{{ '{ "created": ..., "data": [{ "url": "..." }] }' }}</code> —— <code class="text-white/85 font-mono">data[0].url</code> 是产物 URL,服务端不留存(<strong class="text-white/90">不返回 base64</strong>)多数模型返回上游<strong class="text-white/90">原始直链</strong>;少数上游需鉴权的( gpt-image)会返回一个本站转发链 <code class="text-white/85 font-mono">/v1/images/{id}/content</code>,由服务端带账号凭据取回<strong class="text-white/90">两种链接都会过期</strong>,<strong class="text-white/90">尽快下载或转存到你自己的存储</strong></p>
<p><strong class="text-white/90">视频</strong>(异步,Sora 风格三步):</p>
<ol class="list-decimal list-inside space-y-1 text-white/65 pl-1">
<li><code class="text-white/85 font-mono">POST /v1/videos</code> 立即返回任务对象 <code class="text-white/85 font-mono">{{ '{ "id": "...", "object": "video", "status": "queued", ... }' }}</code></li>
<li>轮询 <code class="text-white/85 font-mono">GET /v1/videos/{id}</code>,<code class="text-white/70">status</code> <code class="text-white/70">queued in_progress completed</code>( <code class="text-white/70">failed</code>)</li>
<li>完成后 <code class="text-white/85 font-mono">GET /v1/videos/{id}/content</code> 返回 <strong class="text-white/90">mp4 原始二进制</strong>( base64 URL)</li>
</ol>
<p><strong class="text-white/90">计费(预扣)</strong>:生成<strong class="text-white/90"></strong>按上表价格从你的 Key 账号预扣积分;图像或视频上游失败会自动退回 —— 失败不扣费</p>
<p><strong class="text-white/90">参数映射</strong>:<code class="text-white/70">size</code>(宽x高)同时决定<strong class="text-white/90">比例 + 分辨率档</strong>(长边:&lt;18001K · 180034992K · 35004K),<code class="text-white/70">seconds</code>视频时长<strong class="text-white/90">没有 quality 参数</strong>,分辨率只看 size档位须是该模型支持的(不支持会回退到该模型最低档);参数须落在定价表内否则 400,余额不足 402</p>
<div class="pt-2 grid sm:grid-cols-2 gap-2 text-xs">
<div class="flex items-center gap-2"><span class="badge-err">401</span> Key 无效 / 上游需重新授权</div>
<div class="flex items-center gap-2"><span class="badge-err">404</span> 未知 model / 视频任务不存在</div>
<div class="flex items-center gap-2"><span class="badge-err">400</span> 参数缺失 / 不支持或未定价</div>
<div class="flex items-center gap-2"><span class="badge-err">402</span> 积分不足</div>
<div class="flex items-center gap-2"><span class="badge-err">409</span> 视频尚未完成(content 未就绪)</div>
<div class="flex items-center gap-2"><span class="badge-err">429</span> 账号并发已满,请重试</div>
<div class="flex items-center gap-2"><span class="badge-err">503</span> 上游繁忙,请重试</div>
</div>
</div>
</section>
<transition name="fade">
<div v-if="toastMsg" class="fixed bottom-8 left-1/2 -translate-x-1/2 z-50 bg-white text-black text-sm font-medium px-5 py-2.5 rounded-full shadow-2xl">{{ toastMsg }}</div>
</transition>
</div>
</template>
<style scoped>
.badge-get, .badge-post, .badge-err {
border-radius: 4px; padding: 2px 6px; font-size: 10px; line-height: 1;
}
.badge-get { background: rgb(16 185 129 / 0.14); color: rgb(4 120 87); box-shadow: inset 0 0 0 1px rgb(16 185 129 / 0.35); }
.badge-post { background: rgb(14 165 233 / 0.14); color: rgb(3 105 161); box-shadow: inset 0 0 0 1px rgb(14 165 233 / 0.35); }
.badge-err { background: rgb(244 63 94 / 0.12); color: rgb(190 18 60); box-shadow: inset 0 0 0 1px rgb(244 63 94 / 0.3); font-family: ui-monospace, monospace; }
html.dark .badge-get { background: rgb(16 185 129 / 0.15); color: rgb(110 231 183); box-shadow: inset 0 0 0 1px rgb(52 211 153 / 0.3); }
html.dark .badge-post { background: rgb(14 165 233 / 0.15); color: rgb(125 211 252); box-shadow: inset 0 0 0 1px rgb(56 189 248 / 0.3); }
html.dark .badge-err { background: rgb(244 63 94 / 0.15); color: rgb(253 164 175); box-shadow: inset 0 0 0 1px rgb(251 113 133 / 0.3); }
.cap { display: inline-flex; align-items: center; padding: 0.18rem 0.55rem; font-size: 0.7rem; border-radius: 9999px; white-space: nowrap; }
.cap-k { background: rgb(16 185 129 / 0.12); color: rgb(4 120 87); box-shadow: inset 0 0 0 1px rgb(16 185 129 / 0.3); }
.cap-dur { background: rgb(99 102 241 / 0.12); color: rgb(55 48 163); box-shadow: inset 0 0 0 1px rgb(99 102 241 / 0.3); }
.cap-frame { background: rgb(236 72 153 / 0.12); color: rgb(159 18 57); box-shadow: inset 0 0 0 1px rgb(236 72 153 / 0.3); }
.cap-ref { background: rgb(16 185 129 / 0.12); color: rgb(4 120 87); box-shadow: inset 0 0 0 1px rgb(16 185 129 / 0.3); }
.cap-media { background: rgb(245 158 11 / 0.12); color: rgb(146 64 14); box-shadow: inset 0 0 0 1px rgb(245 158 11 / 0.3); }
.cap-price { background: rgb(14 165 233 / 0.12); color: rgb(3 105 161); box-shadow: inset 0 0 0 1px rgb(14 165 233 / 0.3); font-variant-numeric: tabular-nums; }
.cap-ratio { background: rgb(100 116 139 / 0.1); color: rgb(51 65 85); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; box-shadow: inset 0 0 0 1px rgb(100 116 139 / 0.25); }
html.dark .cap-k { background: rgb(16 185 129 / 0.18); color: rgb(110 231 183); box-shadow: inset 0 0 0 1px rgb(52 211 153 / 0.4); }
html.dark .cap-dur { background: rgb(99 102 241 / 0.18); color: rgb(165 180 252); box-shadow: inset 0 0 0 1px rgb(129 140 248 / 0.4); }
html.dark .cap-frame { background: rgb(236 72 153 / 0.18); color: rgb(244 114 182); box-shadow: inset 0 0 0 1px rgb(244 114 182 / 0.45); }
html.dark .cap-ref { background: rgb(16 185 129 / 0.18); color: rgb(110 231 183); box-shadow: inset 0 0 0 1px rgb(52 211 153 / 0.4); }
html.dark .cap-media { background: rgb(245 158 11 / 0.18); color: rgb(252 211 77); box-shadow: inset 0 0 0 1px rgb(252 211 77 / 0.45); }
html.dark .cap-price { background: rgb(14 165 233 / 0.18); color: rgb(125 211 252); box-shadow: inset 0 0 0 1px rgb(56 189 248 / 0.45); }
html.dark .cap-ratio { background: rgb(255 255 255 / 0.06); color: rgb(255 255 255 / 0.65); box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.12); }
/* 苹果风代码卡片:自带深色配色,不随页面主题切换,深/浅背景下都可读 */
.code-card {
border-radius: 14px; overflow: hidden;
background: #1d1d1f;
box-shadow: 0 1px 2px rgb(0 0 0 / 0.25), 0 8px 24px rgb(0 0 0 / 0.18), inset 0 0 0 1px rgb(255 255 255 / 0.08);
}
.code-card-bar {
display: flex; align-items: center; gap: 10px;
padding: 9px 12px;
background: linear-gradient(180deg, #333336, #2a2a2d);
border-bottom: 1px solid rgb(0 0 0 / 0.4);
}
.code-dots { display: inline-flex; gap: 6px; }
.code-dots .dot { width: 11px; height: 11px; border-radius: 9999px; display: inline-block; }
.dot-r { background: #ff5f57; box-shadow: inset 0 0 0 0.5px rgb(0 0 0 / 0.15); }
.dot-y { background: #febc2e; box-shadow: inset 0 0 0 0.5px rgb(0 0 0 / 0.15); }
.dot-g { background: #28c840; box-shadow: inset 0 0 0 0.5px rgb(0 0 0 / 0.15); }
.code-card-title {
flex: 1; min-width: 0; text-align: center;
font-size: 12px; color: rgb(255 255 255 / 0.55);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.code-copy {
display: inline-flex; align-items: center; gap: 5px;
font-size: 12px; line-height: 1;
color: rgb(255 255 255 / 0.6);
background: rgb(255 255 255 / 0.08);
border-radius: 7px; padding: 5px 9px;
transition: background 0.15s, color 0.15s;
}
.code-copy:hover { color: #fff; background: rgb(255 255 255 / 0.16); }
.code-card-body {
margin: 0; padding: 14px 16px;
font-size: 12px; line-height: 1.7;
color: #e8e8ed;
overflow: auto;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
</style>