Seedance 2.5 is live — 30-second cinematic video with native audio & real-person references
日均 AI 视频生成成本与 30 天 API 预算
2026/09/03

日均 AI 视频生成成本与 30 天 API 预算

计算 30 天竖屏 AI 视频预算:含重试、免费额度上限、安全的 API 轮询、存储与人工审核,一份可直接照做的成本指南。

在这个测试配置中,30 天内单日一视频的生成费用为 $2.52。每次尝试是一个 5 秒、480p、9:16 的短片,使用 seedance-2.0-mini。单次烟雾测试耗费 84 个 reAPI 额度,即 $0.084,在 107.2 秒内完成,并返回一个视频 URL。[6]

这是一个简单的回答,但还不是生产预算。完成的 API 任务不一定是你会发布的片段。如果审核拒绝了一半的完成尝试,生成预算会翻倍到每月 $5.04。存储、日程安排、编辑和人工审核不在这个数字之内。

本指南计算准确数字,并构建一个单工作进程示例,该示例在无法确定不确定的 POST 是否应重复提交时停止以进行手动核对。

TL;DR

  • 一个测试过的 5 秒、480p、9:16 Seedance 2.0 Mini 任务消耗 84 额度 = $0.084。30 个相同的完成尝试费用 $2.52
  • 按接受视频的完成尝试次数预算,而不仅仅是计划发布数。在每个接受的片段 1.5 次尝试时,30 天的生成预算是 $3.78;两次尝试时为 $5.04。
  • 符合条件的新账户可能会收到促销测试额度,但公开定价页面不保证固定金额或每日重置。[3]将其视为原型跑道,而非经常性容量。
  • 立即保存任务 ID。每个被接受的重复 POST 创建一个单独的任务和额度预留;每个完成的任务结算其自己的使用情况。Idempotency-Key 记录跟踪值但不对视频请求去重。[4]
  • 大约每 5 秒用 GET 轮询一次任务,将返回的文件归档到你控制的存储中,并在手动审核步骤之后再发布。[5]
  • 107.2 秒的结果是一次烟雾测试,不是关于平均速度、正常运行时间或视觉质量的声明。

人们实际在问的日视频问题

你是否需要一个真实的 REST API,能每天制作一个或两个短垂直片段,返回公网 HTTPS URL,并适应自动发布流程?对于 4 到 5 秒的 9:16 原型,成本只是问题的一半。恢复能力从原型成为日程时就变得重要:工作流需要持久状态和精心制定的重试策略,然后才需要聪明的 prompt。

一次 107 秒的运行,结论有限

测试使用了我们找到的最便宜的当前 reAPI 配置,该配置接受文字 prompt 并明确支持 9:16 和 4 到 5 秒时长:Seedance 2.0 Mini,480p。[1]该模型文档允许 480p 或 720p、9:16 输出和 4 到 15 秒的时长。[2]

测试字段记录值
日期2026 年 9 月 3 日
任务 IDtask_01a06547163473ecb7dab4478ee1abaa
模型seedance-2.0-mini
请求480p、9:16、5 秒
音频关闭
水印关闭
提交数1
最终状态已完成
消耗时间107.2 秒
计费使用84 额度 / $0.084
返回输出1 个视频 URL

这次准确的烟雾测试的存档输出:5 秒、480p、9:16、音频关闭和水印关闭。验证了技术完成度和计费;创意接受度未评分。

当前 480p 费率是每计费秒 $0.016625。[1]reAPI 将 1 额度计为 $0.001 并将任务向上取整到下一个整数额度。[2]对于测试请求:

ceil($0.016625 × 5 秒 × 1,000 额度每美元)
= ceil(83.125 额度)
= 84 额度
= $0.084

被接受的 POST 为其自己的任务保留额度。当该任务达到终止状态时,预留结算:将 statususage.credits 一起读取,而不是将准入保持作为最终账单。Seedance 2.0 Mini 在其文档计费政策下退还失败生成。[2][5]

这一个结果确认了请求路径、完成状态、返回的 URL 和费用。它无法确定典型渲染时间、成功率、接受率或视觉质量评分。

下载烟雾测试记录。它包含请求设置、任务 ID、消耗时间、终止状态和已结算额度。临时任务 URL 被省略;记录指向永久 reAPI CDN 存储中的同一视频。

价格会变化。在将这些数字变成客户报价或硬支出限制之前,请查看实时 Seedance 2.0 Mini 模型页

30 天预算从完成尝试开始

最低每月计算很直接:

30 天 × 84 额度 = 2,520 额度 = $2.52

这假设每天一个完成尝试变成一个被接受的视频。一个更有用的规划公式是:

每月生成成本
= 天数 × 每天视频数 × 每个被接受视频的完成尝试数 × 每次尝试的成本

对于测试费率下每天一个被接受的片段:

每个被接受视频的完成尝试每月尝试30 天生成预算
1.030$2.52
1.545$3.78
2.060$5.04
3.090$7.56

"1.5 次尝试"是每月平均值,不是半个请求:15 个片段可能第一次通过,15 个需要一个替代。计算因不良运动、构图或与概要不匹配而被拒的完成生成。

分辨率和时长也改变了起始单位成本。以下数据适用当前文档化费率和计费公式;只有五秒 480p 行被作为本文章的烟雾测试运行。

配置每次尝试的额度每次尝试的成本30 次尝试
480p、4 秒67$0.067$2.01
480p、5 秒84$0.084$2.52
720p、4 秒144$0.144$4.32
720p、5 秒180$0.180$5.40

这些是生成成本,不是总内容成本。它们排除了 prompt 撰写、源资产、编辑、字幕、音乐许可、对象存储、自动化托管、平台 API 费用和批准每个片段花费的时间。

为什么免费额度不是可持续容量

免费额度可以证明身份验证、请求字段、轮询和下载有效。没有已知数量和续订规则,它们无法支持经常性承诺。

符合条件的新账户可能会收到促销测试额度;可用性和数量在注册或仪表板中显示。公开定价页面不保证固定金额或每日重置。[3]这使得该授予不适合作为可靠的每月容量。相同的规则适用于消费者应用限额:没有文档化的 API 访问权限、限制和重置行为,UI 额度不是 API 预算。

使用免费余额测试一条狭窄的路径。然后在付费费率下预算稳定状态工作流,并将任何免费额度视为临时的减少。这使得项目在启动后而不是仅在演示日期间才可行。

在自动化第一个请求之前

对于初学者友好的设置,准备这些部分:

  • reAPI 账户和服务器端 API 密钥。永远不要将密钥放在浏览器代码、公开存储库或可下载的工作流中。
  • 一个描述一个短镜头的 prompt。5 秒不足以容纳多场景故事。
  • 一个调度器,如 cron、GitHub Actions、n8n 或队列工作进程。
  • 一个持久的地方来保存日常作业密钥和 reAPI 任务 ID。
  • 你控制的已完成文件的存储。
  • 一个将在发布前审核片段的人。

下面的示例使用 UTC 日期。如果内容日历遵循本地时区,在该时区中一致地计算作业日期。否则,靠近午夜的作业可能会收到错误的日期的密钥。

用 cURL 提交一个 9:16 视频

首先在你的 shell 中设置 API 密钥:

export REAPI_API_KEY='replace-with-your-server-side-key'

提交一个任务并在做任何其他事情之前保存响应:

curl --fail-with-body --silent --show-error \
  --request POST 'https://reapi.ai/api/v1/videos/generations' \
  --header "Authorization: Bearer $REAPI_API_KEY" \
  --header 'Content-Type: application/json' \
  --data '{
    "model": "seedance-2.0-mini",
    "prompt": "A handmade ceramic cup beside an open notebook, soft morning window light, one slow push-in shot, natural motion, no text",
    "resolution": "480p",
    "aspect_ratio": "9:16",
    "duration": 5,
    "generate_audio": false,
    "watermark": false
  }' \
  --output submit.json

打开 submit.json 并复制其 id。如果你有 jq,这会提取它:

TASK_ID="$(jq -r '.id' submit.json)"
test -n "$TASK_ID" && test "$TASK_ID" != 'null' || exit 1
printf '%s\n' "$TASK_ID" > task-id.txt

轮询使用 GET,不消耗额度:[5]

curl --fail-with-body --silent --show-error \
  --header "Authorization: Bearer $REAPI_API_KEY" \
  "https://reapi.ai/api/v1/tasks/$TASK_ID" \
  --output task.json
jq '{id, status, usage, output, error}' task.json

大约 5 秒后,当状态为 processing 时再运行最后一个命令。文档化的终止状态为 completedfailed。不要仅因为第一次轮询没有视频就重新提交 POST

在 Node.js 中构建一个更安全的日常工作流

cURL 示例证明了字段。下面的脚本是一个单工作进程、最多一次守卫,不是精确一次队列。它在提交前保存意图,任务响应一到达就记录任务 ID,限制每个网络调用,本地存档,并在无法知道是否 POST 被接受时停止以进行手动核对。它使用 Node.js 20 或更高版本,没有外部包。不要针对同一个 JSON 文件运行两个副本。

将其保存为 daily-video.mjs

import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
import { join } from 'node:path';

const API_BASE = 'https://reapi.ai/api/v1';
const STORE_PATH = './daily-video-jobs.json';
const OUTPUT_DIR = './daily-video-output';
const POLL_MS = 5_000;
const POLL_LIMIT_MS = 30 * 60 * 1_000;
const POST_TIMEOUT_MS = 30_000;
const GET_TIMEOUT_MS = 20_000;
const DOWNLOAD_TIMEOUT_MS = 120_000;
const apiKey = process.env.REAPI_API_KEY;

if (!apiKey) {
  throw new Error('Set REAPI_API_KEY in the server environment.');
}

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

function retryAfterMs(value) {
  if (!value) return POLL_MS;
  const seconds = Number(value);
  if (Number.isFinite(seconds)) return Math.max(seconds * 1_000, 0);
  const date = Date.parse(value);
  return Number.isNaN(date) ? POLL_MS : Math.max(date - Date.now(), 0);
}

async function loadJobs() {
  try {
    return JSON.parse(await readFile(STORE_PATH, 'utf8'));
  } catch (error) {
    if (error.code === 'ENOENT') return {};
    throw error;
  }
}

async function saveJobs(jobs) {
  const temporaryPath = `${STORE_PATH}.tmp`;
  await writeFile(temporaryPath, `${JSON.stringify(jobs, null, 2)}\n`);
  await rename(temporaryPath, STORE_PATH);
}

async function requestJson(url, options) {
  const response = await fetch(url, options);
  const body = await response.text();
  let data;

  try {
    data = JSON.parse(body);
  } catch {
    data = { raw: body };
  }

  if (!response.ok) {
    const error = new Error(`HTTP ${response.status}: ${body.slice(0, 300)}`);
    error.status = response.status;
    error.retryAfterMs = retryAfterMs(response.headers.get('retry-after'));
    error.requestId = data?.error?.request_id ?? null;
    throw error;
  }

  return data;
}

async function archiveVideo(url, jobKey) {
  const response = await fetch(url, {
    signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS),
  });
  if (!response.ok) {
    throw new Error(`Video download failed with HTTP ${response.status}.`);
  }

  await mkdir(OUTPUT_DIR, { recursive: true });
  const safeName = jobKey.replace(/[^a-z0-9-]/gi, '_');
  const outputPath = join(OUTPUT_DIR, `${safeName}.mp4`);
  const bytes = Buffer.from(await response.arrayBuffer());
  await writeFile(outputPath, bytes);
  return outputPath;
}

async function main() {
  const utcDate = new Date().toISOString().slice(0, 10);
  const jobKey = `daily-short:${utcDate}`;
  const jobs = await loadJobs();

  if (!jobs[jobKey]) {
    // Persist intent before POST. A crash after this point must not cause an
    // automatic second submission on the next scheduler run.
    jobs[jobKey] = {
      state: 'submitting',
      createdAt: new Date().toISOString(),
      idempotencyKey: jobKey,
    };
    await saveJobs(jobs);

    try {
      const task = await requestJson(`${API_BASE}/videos/generations`, {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${apiKey}`,
          'Content-Type': 'application/json',
          // Trace-only on reAPI. This header does not deduplicate the POST.
          'Idempotency-Key': jobKey,
        },
        signal: AbortSignal.timeout(POST_TIMEOUT_MS),
        body: JSON.stringify({
          model: 'seedance-2.0-mini',
          prompt:
            'A handmade ceramic cup beside an open notebook, soft morning ' +
            'window light, one slow push-in shot, natural motion, no text',
          resolution: '480p',
          aspect_ratio: '9:16',
          duration: 5,
          generate_audio: false,
          watermark: false,
        }),
      });

      if (!task.id) throw new Error('Submission returned no task ID.');

      jobs[jobKey] = {
        ...jobs[jobKey],
        state: 'processing',
        taskId: task.id,
        submittedAt: new Date().toISOString(),
      };
      await saveJobs(jobs);
    } catch (error) {
      const receivedHttpResponse = Number.isInteger(error.status);
      jobs[jobKey] = {
        ...jobs[jobKey],
        state: receivedHttpResponse ? 'submit_rejected' : 'submit_uncertain',
        error: error instanceof Error ? error.message : String(error),
        requestId: error.requestId ?? null,
      };
      await saveJobs(jobs);
      throw new Error(
        receivedHttpResponse
          ? `Submission for ${jobKey} was rejected. Review the HTTP error ` +
              'before creating a separately counted retry.'
          : `Submission for ${jobKey} is uncertain. Use ${jobKey} and the ` +
              'saved request ID, if present, to inspect logs or the dashboard. ' +
              'Do not submit again until a person reconciles the result.',
      );
    }
  }

  let job = jobs[jobKey];
  if (job.state === 'pending_review' || job.state === 'approved') {
    console.log(`${jobKey} already has an archived output: ${job.archivePath}`);
    return;
  }
  if (job.state === 'completed_no_output') {
    throw new Error(
      `${jobKey} completed without a video URL. Inspect ${job.taskId}; do not POST again.`,
    );
  }
  if (job.state === 'failed') {
    throw new Error(
      `${jobKey} failed. Review its settled usage before creating a new retry key.`,
    );
  }
  if (!job.taskId) {
    throw new Error(
      `${jobKey} has no task ID. Resolve ${job.state} manually; do not resubmit.`,
    );
  }

  if (!job.outputUrl) {
    const deadline = Date.now() + POLL_LIMIT_MS;
    let task;

    while (Date.now() < deadline) {
      try {
        task = await requestJson(`${API_BASE}/tasks/${job.taskId}`, {
          headers: { Authorization: `Bearer ${apiKey}` },
          signal: AbortSignal.timeout(GET_TIMEOUT_MS),
        });
      } catch (error) {
        const remainingMs = Math.max(deadline - Date.now(), 0);
        if (error.status === 429) {
          const waitMs = Math.min(error.retryAfterMs ?? POLL_MS, remainingMs);
          console.warn(`Rate limited; waiting ${waitMs} ms before another GET.`);
          if (waitMs > 0) await sleep(waitMs);
          continue;
        }
        const networkError = !Number.isInteger(error.status);
        const serverError = error.status >= 500 && error.status < 600;
        if (!networkError && !serverError) {
          throw error;
        }
        const waitMs = Math.min(POLL_MS, remainingMs);
        console.warn('Network or server error; retrying the same task GET.');
        if (waitMs > 0) await sleep(waitMs);
        continue;
      }

      if (task.status === 'completed' || task.status === 'failed') break;
      if (task.status !== 'processing') {
        throw new Error(`Unexpected task status: ${task.status}`);
      }
      await sleep(Math.min(POLL_MS, Math.max(deadline - Date.now(), 0)));
    }

    if (!task || (task.status !== 'completed' && task.status !== 'failed')) {
      jobs[jobKey] = { ...job, state: 'poll_timeout' };
      await saveJobs(jobs);
      throw new Error(
        'Polling timed out. The task may still be running; do not POST again.',
      );
    }

    if (task.status === 'failed') {
      jobs[jobKey] = {
        ...job,
        state: 'failed',
        usageCredits: task.usage?.credits ?? null,
        taskError: task.error ?? null,
      };
      await saveJobs(jobs);
      throw new Error(
        'Generation failed. Review settled usage before scheduling a retry.',
      );
    }

    const outputUrl = task.output?.video_urls?.[0];
    if (!outputUrl) {
      jobs[jobKey] = {
        ...job,
        state: 'completed_no_output',
        usageCredits: task.usage?.credits ?? null,
        completedAt: new Date().toISOString(),
      };
      await saveJobs(jobs);
      throw new Error('Completed task returned no video URL; do not POST again.');
    }

    // Save completion before downloading so storage trouble cannot trigger POST.
    jobs[jobKey] = {
      ...job,
      state: 'completed',
      outputUrl,
      usageCredits: task.usage?.credits ?? null,
      completedAt: new Date().toISOString(),
    };
    await saveJobs(jobs);
    job = jobs[jobKey];
  }

  try {
    const archivePath = await archiveVideo(job.outputUrl, jobKey);
    jobs[jobKey] = {
      ...job,
      state: 'pending_review',
      archivePath,
      archiveError: null,
    };
    await saveJobs(jobs);
    console.log(`Saved ${archivePath}. Review it before publishing.`);
  } catch (error) {
    jobs[jobKey] = {
      ...job,
      state: 'archive_failed',
      archiveError: error instanceof Error ? error.message : String(error),
    };
    await saveJobs(jobs);
    throw new Error(
      'Archiving failed. The task remains completed; rerun to retry only the download.',
    );
  }
}

await main();

运行一次:

node daily-video.mjs

JSON 文件在一台机器上是可重新启动的,但它不是多工作进程锁。在生产中,使用唯一的 job_key 插入一个数据库行,然后只让赢得原子声明的工作进程将其从 scheduled 移动到 submitting。数据库声明防止两个活跃工作进程一起提交。它不能使数据库写入和外部 API 请求原子化。

崩溃后会发生什么

安全的下一个动作取决于达到存储的最后状态:

最后持久状态工作流知道什么下一个动作
无行意图保存未完成,该代码在该保存之前永远不会 POST正常开始
submittingsubmit_uncertain,没有任务 IDPOST 可能未启动,或者可能在响应丢失前被接受停止并手动核对;永远不要自动 POST
submit_rejected非 2xx 响应到达审核错误,然后创建单独计数的重试
processingpoll_timeout,任务 ID 存在任务存在继续为该 ID 获得 GET 轮询
failed任务达到终止错误并结算其使用情况审核 usage.credits;仅在审慎决定后使用新的重试密钥
completedarchive_failed,输出 URL 存在生成完成仅重试下载
completed_no_output任务结束而没有预期的 URL检查该任务;不要再次 POST
pending_reviewapproved文件已存档不做 API 工作就退出

不确定状态是不可避免的间隙。连接可能在 reAPI 接受请求后失败,但任务 ID 到达你的进程之前。该脚本将日常作业密钥作为 Idempotency-Key 发送,以便日志可以关联尝试,但 reAPI 仅记录该标头以进行跟踪;它不去重。[4]被接受的第二个 POST 创建另一个任务和预留,第二个完成结算另一个费用。如果日志或仪表板无法解决歧义,操作员必须在跳过那天的片段和冒险重复之间做出选择。在这个示例中没有诚实的自动精确一次答案。

任务查询是不同的。重试 GET 既不创建生成也不使用额度。该示例限制每个请求,在确定的 4xx 错误时停止,在 429 时尊重 Retry-After,在 30 分钟轮询窗口内重试网络错误或 5xx 响应。5 秒间隔也避免重复读取任务端点的五秒在途缓存。[5]

在发布前审核和存储片段

API 的作业在它交付文件时结束。技术完成的片段仍然可能代表 brief 有误或在社交媒体上失败。审核每个结果的:

  • 预期的主题、动作和摄像机运动;
  • 扭曲的对象、手、脸、包装或徽标;
  • 不想要的词语或虚构的产品声称;
  • 闪烁、突然的场景变化或不安全的裁剪;
  • prompt、参考材料、音乐和肖像的权利;
  • 目标平台当前的技术和披露要求。

只有在该检查后才将片段标记为 approved。如果它失败,创建单独计数的重试,如 daily-short:2026-09-03:retry-1;永远不要从成本账本中删除第一次尝试。

在将其添加到明天的发布队列前移动 MP4。reAPI 的输出 URL 是一个交接位置,不是永久媒体库。[5]教程在本地保存;在临时部署上使用 S3、R2 或你的普通对象存储。

$2.52 数字不保证什么

每月数字是一个透明场景,不是保证。它假设 30 天内相同的价格、时长、分辨率和完成尝试数。它不建立:

  • 从一个 107.2 秒观察的正常队列或生成时间;
  • 服务正常运行时间或失败率;
  • 你的 prompt 的视觉接受率;
  • 超过请求的 480p 和 9:16 设置的确切输出尺寸;
  • 成功发布到任何社交平台;
  • 未来价格或免费额度可用性。

在仪表板中保持这些变量可见。最少,记录 job_keytask_id、请求的设置、最终状态、usage.credits、审核决定和存档对象 URL。30 天后,用你自己测量的接受率替换假设的每个视频尝试次数。

常见问题

一个 5 秒垂直 AI 视频每天的成本是多少?

测试的 Seedance 2.0 Mini 配置耗费 84 额度,或 $0.084,对于一个完成的五秒 480p 9:16 任务。在每天一个完成尝试时,30 天成本 $2.52。在预算未来月份前检查实时模型价格。

如果我每天想要两个视频怎么办?

在每个被接受视频一个完成尝试时,60 次尝试按测试费率会成本 $5.04。将其乘以你测量的每个被接受视频的尝试数。例如,1.5 次尝试每个接受会使生成预算为 $7.56。

是否有一个每天重置的免费文字转视频 API?

不要假设一个除非提供商文档化 API 密钥访问权限、确切的免费数量、合格的模型和重置日程。符合条件的 reAPI 账户可能会收到促销测试额度,可用性和数量在注册或仪表板中显示;公开定价页面不保证固定授予或每日重置。[3]

为什么按完成尝试计数而不是发布的片段?

因为 API 可以完成在创意、品牌、法律或裁剪审核中失败的技术有效视频。账单跟踪生成使用情况;你的内容日历跟踪被接受的片段。记录两个数字会显示每个可发布结果的真实成本。

失败的 Seedance 2.0 Mini 任务是否消耗额度?

模型文档说失败的生成自动退款。[2]仍然记录最后的任务状态和 usage.credits 而不是假设该值。失败的请求也可能消耗时间并可能导致单独计费的替代尝试。

我能安全地用相同的幂等性密钥重试提交吗?

不能。每个被接受的 POST 创建一个单独的任务并预留额度;每个完成的任务然后结算其自己的 usage.creditsIdempotency-Key 是一个跟踪值,不是去重。[4]坚持你自己的作业密钥和任务 ID,并在提交结果不确定时停止以进行手动调查。

我能直接从返回的视频 URL 发布吗?

先存档文件。任务输出链接不是你控制的存储的替代,直接发布会跳过审核步骤。保存视频,审核它,然后将你的存储对象传递给发布工作流。[5]

107.2 秒是正常的生成时间吗?

我们不知道。它是一次烟雾测试的消耗时间。测量正常延迟或可靠性需要许多日期的运行和一个定义的样本,包括失败和超时任务。

结论

对"日常 AI 视频成本多少?"的有用答案不仅仅是每个调用 $0.084。它是完美 30 天运行 $2.52,或工作流平均 1.5-3 个完成尝试每个被接受片段时 $3.78-$7.56。从该范围开始,测量你自己的审核通过率,并将免费额度置于经常性容量计划之外。

第一个生产里程碑同样实用:一个原子声明的日常作业密钥,一个保存的任务 ID,安全的 GET 轮询,一个存档的文件和一个人工批准。一旦那个循环有效,每天计划它是容易的部分。

参考资料

  1. reAPI。视频模型目录Seedance 2.0 Mini 模型页。2026 年 9 月 3 日检索。
  2. reAPI。Seedance 2.0 Mini API 文档。2026 年 9 月 3 日检索。
  3. reAPI。定价。2026 年 9 月 3 日检索。
  4. reAPI。API 概览。2026 年 9 月 3 日检索。
  5. reAPI。任务状态和轮询文档。2026 年 9 月 3 日检索。
  6. reAPI。第一方 Seedance 2.0 Mini 烟雾测试记录。任务 task_01a06547163473ecb7dab4478ee1abaa,2026 年 9 月 3 日完成。一个 480p、9:16、五秒请求,音频和水印禁用;84 额度、107.2 秒和一个返回的视频 URL。

延伸阅读