跳转到主要内容

Documentation Index

Fetch the complete documentation index at: https://docs.vmeg.ai/llms.txt

Use this file to discover all available pages before exploring further.

素材通过 预签名 URL 直传 CDN/对象存储,再调用 VMEG API 完成注册。文件字节 不经过 API 网关。 本流程中所有相关 POST 均须携带 X-Idempotency-Key

选择上传方式

方式适用场景
单文件(预签名 URL)较小文件,一次 PUT
分片(Multipart)大文件、可断点续传
请求与响应字段:获取预签名 URL完成单文件上传,以及 API 参考Assets - Materials → upload 下的其他接口。

单文件上传

1

获取预签名 URL

POST /openapi/v1/assets/material/upload/gen-upload-url,传入 fileHash(MD5)与 fileName
2

PUT 到 CDN

若响应含 data.material,表示文件已去重,无需 PUT;否则将字节上传到 data.uploadUrl
3

完成上传

POST /openapi/v1/assets/material/upload/complete,使用 gen-upload-url 返回的 s3UrimaterialIdfileHashfileName

示例

import crypto from "node:crypto";
import fs from "node:fs";

const API_BASE = "https://api.vmeg.ai";
const API_KEY = process.env.VMEG_API_KEY;
const FILE_PATH = "./demo.mp4";

function md5File(path) {
  const hash = crypto.createHash("md5");
  hash.update(fs.readFileSync(path));
  return hash.digest("hex");
}

function idemKey(step) {
  return `upload-${step}-${crypto.randomUUID()}`;
}

async function apiPost(path, body) {
  const res = await fetch(`${API_BASE}${path}`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      "Content-Type": "application/json",
      "X-Idempotency-Key": idemKey(path),
    },
    body: JSON.stringify(body),
  });
  const json = await res.json();
  if (json.code !== 200) throw new Error(json.message || "API error");
  return json.data;
}

async function uploadSingleFile() {
  const fileName = FILE_PATH.split("/").pop();
  const fileHash = md5File(FILE_PATH);
  const fileBytes = fs.readFileSync(FILE_PATH);

  const gen = await apiPost("/openapi/v1/assets/material/upload/gen-upload-url", {
    fileHash,
    fileName,
  });

  if (gen.material?.materialId) {
    console.log("Instant upload (dedup):", gen.material.materialId);
    return gen.material.materialId;
  }

  const putRes = await fetch(gen.uploadUrl, { method: "PUT", body: fileBytes });
  if (!putRes.ok) throw new Error(`CDN PUT failed: ${putRes.status}`);

  const material = await apiPost("/openapi/v1/assets/material/upload/complete", {
    s3Uri: gen.s3Uri,
    materialId: gen.materialId,
    fileHash,
    fileName,
  });

  console.log("materialId:", material.materialId);
  return material.materialId;
}

uploadSingleFile().catch(console.error);
Java 示例使用 Jackson(ObjectMapper)处理 JSON。请在项目中添加 com.fasterxml.jackson.core:jackson-databind,或改用你熟悉的 JSON 库解析响应。

分片上传

大文件流程:
  1. POST .../multipart/initiate
  2. POST .../multipart/presign-parts — presigned URL per part
  3. PUT each part to CDN and record each part ETag
  4. POST .../multipart/complete — finalize and get materialId
  5. On failure: POST .../multipart/abort
分片大小请使用 initiate 响应中的 recommendedPartSize(S3 单片通常至少 5 MB,最后一片除外)。

示例

import crypto from "node:crypto";
import fs from "node:fs";

const API_BASE = "https://api.vmeg.ai";
const API_KEY = process.env.VMEG_API_KEY;
const FILE_PATH = "./large.mp4";

function md5File(path) {
  const hash = crypto.createHash("md5");
  hash.update(fs.readFileSync(path));
  return hash.digest("hex");
}

function idemKey(step) {
  return `multipart-${step}-${crypto.randomUUID()}`;
}

async function apiPost(path, body) {
  const res = await fetch(`${API_BASE}${path}`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      "Content-Type": "application/json",
      "X-Idempotency-Key": idemKey(path),
    },
    body: JSON.stringify(body),
  });
  const json = await res.json();
  if (json.code !== 200) throw new Error(json.message || "API error");
  return json.data;
}

function readPart(buffer, offset, size) {
  return buffer.subarray(offset, Math.min(offset + size, buffer.length));
}

async function uploadMultipart() {
  const fileName = FILE_PATH.split("/").pop();
  const fileHash = md5File(FILE_PATH);
  const fileBytes = fs.readFileSync(FILE_PATH);

  const init = await apiPost("/openapi/v1/assets/material/upload/multipart/initiate", {
    fileHash,
    fileName,
  });

  const partSize = init.recommendedPartSize || 8 * 1024 * 1024;
  const partNumbers = [];
  for (let offset = 0, n = 1; offset < fileBytes.length; offset += partSize, n++) {
    partNumbers.push(n);
  }

  const presign = await apiPost(
    "/openapi/v1/assets/material/upload/multipart/presign-parts",
    {
      materialId: init.materialId,
      uploadId: init.uploadId,
      fileHash,
      fileName,
      partNumbers,
    }
  );

  const parts = [];
  for (const { partNumber, url } of presign.urls) {
    const start = (partNumber - 1) * partSize;
    const chunk = readPart(fileBytes, start, partSize);
    const putRes = await fetch(url, { method: "PUT", body: chunk });
    if (!putRes.ok) throw new Error(`Part ${partNumber} PUT failed: ${putRes.status}`);
    const eTag = (putRes.headers.get("etag") || "").replace(/"/g, "");
    parts.push({ partNumber, eTag });
  }

  const material = await apiPost(
    "/openapi/v1/assets/material/upload/multipart/complete",
    {
      materialId: init.materialId,
      uploadId: init.uploadId,
      fileHash,
      fileName,
      parts,
    }
  );

  console.log("materialId:", material.materialId);
  return material.materialId;
}

uploadMultipart().catch(console.error);

上传之后

音视频翻译source.materialId 中使用 materialId。管理资产见 素材