OMNIDOC / QUICKSTART
翻译第一份文件。
下面每个示例都是独立的完整流程:上传、确认文档、创建任务、等待完成、下载结果。选择一种语言,从头运行即可。
开始之前
- 在 API 控制台创建 Key,并设置环境变量
OMNIDOC_API_KEY。 - 准备一份
report.docx,放在脚本所在目录;也可修改示例中的文件路径。 - 先用 余额查询验证凭据。翻译会产生用量,确认账号积分及 Key 预算。
示例不自动重试创建请求。遇到网络超时,保留任务 ID、幂等键和请求体,先查询状态。已经创建的任务不会因为脚本退出而自动取消。
#!/usr/bin/env bash
set -euo pipefail
: "${OMNIDOC_API_KEY:?请设置 OMNIDOC_API_KEY}"
BASE='https://omnidoc.top/api/v1'
FILE='report.docx'
TARGET='en'
SIZE=$(wc -c < "$FILE" | tr -d ' ')
# 1. 申请直传;上传地址和凭据不要写入日志。
TICKET=$(curl --fail-with-body -sS --max-time 30 "$BASE/upload/presign" \
-H "X-API-Key: $OMNIDOC_API_KEY" -H 'Content-Type: application/json' \
-d "$(jq -n --arg name "$(basename "$FILE")" --argjson size "$SIZE" \
'{filename:$name,file_size:$size}')")
UPLOAD_URL=$(jq -er '.upload_url' <<< "$TICKET")
FINALIZE_TOKEN=$(jq -er '.finalize_token' <<< "$TICKET")
UPLOAD_HEADERS=()
while IFS= read -r header; do UPLOAD_HEADERS+=(-H "$header"); done \
< <(jq -r '.upload_headers | to_entries[] | "\(.key): \(.value)"' <<< "$TICKET")
# 2. 只发返回的上传头;不带 API Key。
curl --fail-with-body -sS --max-time 300 -X PUT "$UPLOAD_URL" \
"${UPLOAD_HEADERS[@]}" --upload-file "$FILE"
# 3. 确认上传,取得 document.id。
DOCUMENT=$(curl --fail-with-body -sS --max-time 30 "$BASE/upload/finalize" \
-H "X-API-Key: $OMNIDOC_API_KEY" -H 'Content-Type: application/json' \
-d "$(jq -n --arg token "$FINALIZE_TOKEN" '{finalize_token:$token}')")
DOCUMENT_ID=$(jq -er '.document.id' <<< "$DOCUMENT")
# 4. 每个新任务生成一个键;重试本次创建时保留此键和请求体。
IDEMPOTENCY_KEY=$(uuidgen)
JOB=$(curl --fail-with-body -sS --max-time 30 "$BASE/jobs" \
-H "X-API-Key: $OMNIDOC_API_KEY" -H 'Content-Type: application/json' \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d "$(jq -n --arg doc "$DOCUMENT_ID" --arg lang "$TARGET" \
'{document_id:$doc,target_lang:$lang}')")
JOB_ID=$(jq -er '.id' <<< "$JOB")
printf 'job_id: %s\n' "$JOB_ID"
# 5. 有界轮询;超时不取消任务,也不要因此重复创建。
STATUS=''
for ((attempt=0; attempt<240; attempt++)); do
JOB=$(curl --fail-with-body -sS --max-time 20 "$BASE/jobs/$JOB_ID" \
-H "X-API-Key: $OMNIDOC_API_KEY")
STATUS=$(jq -er '.status' <<< "$JOB")
if [[ "$STATUS" == 'completed' ]]; then break; fi
if [[ "$STATUS" == 'failed' ]]; then
jq -r '.error_msg // "translation failed"' <<< "$JOB" >&2; exit 1
fi
sleep 3
done
if [[ "$STATUS" != 'completed' ]]; then
printf '等待超时,请继续查询任务 %s\n' "$JOB_ID" >&2; exit 1
fi
# 6. 逐项下载到新建目录,不使用服务端文件名作为路径。
ARTIFACTS=$(curl --fail-with-body -sS --max-time 30 \
"$BASE/artifacts?job_id=$JOB_ID" -H "X-API-Key: $OMNIDOC_API_KEY")
OUTPUT=$(mktemp -d './omnidoc-output.XXXXXX')
INDEX=0
while IFS=$'\t' read -r id format; do
INDEX=$((INDEX + 1))
[[ "$format" =~ ^[a-zA-Z0-9]+$ ]] || format='bin'
curl --fail-with-body -sS --max-time 300 \
"$BASE/artifacts/$id/download" -H "X-API-Key: $OMNIDOC_API_KEY" \
-o "$OUTPUT/$INDEX.$format"
done < <(jq -r '.[] | [.id,.format] | @tsv' <<< "$ARTIFACTS")
printf '结果目录:%s\n' "$OUTPUT"
from __future__ import annotations
import os
import re
import time
import uuid
from pathlib import Path
from urllib.parse import unquote
import requests
API_KEY = os.environ["OMNIDOC_API_KEY"]
BASE = "https://omnidoc.top/api/v1"
API_HEADERS = {"X-API-Key": API_KEY}
def upload_document(file_path: Path) -> dict:
size = file_path.stat().st_size
response = requests.post(
f"{BASE}/upload/presign",
headers=API_HEADERS,
json={
"filename": file_path.name,
"file_size": size,
},
timeout=20,
)
response.raise_for_status()
ticket = response.json()
with file_path.open("rb") as stream:
put_headers = {**ticket["upload_headers"], "Content-Length": str(size)}
uploaded = requests.put(
ticket["upload_url"],
headers=put_headers,
data=stream,
timeout=300,
)
uploaded.raise_for_status()
finalized = requests.post(
f"{BASE}/upload/finalize",
headers=API_HEADERS,
json={"finalize_token": ticket["finalize_token"]},
timeout=30,
)
finalized.raise_for_status()
return finalized.json()["document"]
def create_job(document_id: str, target_lang: str, engine: str) -> dict:
response = requests.post(
f"{BASE}/jobs",
headers={**API_HEADERS, "Idempotency-Key": str(uuid.uuid4())},
json={
"document_id": document_id,
"target_lang": target_lang,
"engine": engine,
},
timeout=30,
)
response.raise_for_status()
return response.json()
def wait_for_job(job_id: str) -> dict:
deadline = time.monotonic() + 1200
while time.monotonic() < deadline:
response = requests.get(
f"{BASE}/jobs/{job_id}", headers=API_HEADERS, timeout=20
)
response.raise_for_status()
job = response.json()
if job["status"] == "completed":
return job
if job["status"] == "failed":
raise RuntimeError(job.get("error_msg") or "translation failed")
print(f"{job.get('stage', 'processing')}: {job.get('stage_progress', 0)}%")
time.sleep(3)
raise TimeoutError(f"等待超时;可继续查询任务 {job_id},不要重复创建")
def response_filename(response: requests.Response, fallback: str) -> str:
disposition = response.headers.get("Content-Disposition", "")
utf8 = re.search(r"filename\*=UTF-8''([^;]+)", disposition, re.I)
if utf8:
return Path(unquote(utf8.group(1))).name or fallback
ascii_name = re.search(r'filename="([^"]+)"', disposition, re.I)
if ascii_name:
return Path(ascii_name.group(1)).name or fallback
return fallback
def download_artifacts(job_id: str, output_dir: Path) -> list[Path]:
output_dir.mkdir(parents=True, exist_ok=True)
response = requests.get(
f"{BASE}/artifacts",
headers=API_HEADERS,
params={"job_id": job_id},
timeout=20,
)
response.raise_for_status()
results: list[Path] = []
for artifact in response.json():
download = requests.get(
f"{BASE}/artifacts/{artifact['id']}/download",
headers=API_HEADERS,
timeout=300,
)
download.raise_for_status()
raw_name = response_filename(download, "output.bin")
filename = re.sub(r'[<>:"/\\|?*\x00-\x1f]', "_", raw_name).strip(". ") or "output.bin"
filename = f"{len(results) + 1:03d}_{filename}"
destination = output_dir / filename
with destination.open("xb") as output:
output.write(download.content)
results.append(destination)
return results
def translate_document(
source: str,
*,
target_lang: str = "en",
engine: str = "xr-397b",
) -> list[Path]:
document = upload_document(Path(source))
job = create_job(document["id"], target_lang, engine)
print("job_id:", job["id"])
wait_for_job(job["id"])
return download_artifacts(job["id"], Path("omnidoc-output"))
if __name__ == "__main__":
for result in translate_document("report.docx", target_lang="en"):
print(result)import axios from 'axios';
import fs from 'node:fs';
import path from 'node:path';
import { randomUUID } from 'node:crypto';
const API_KEY = process.env.OMNIDOC_API_KEY;
if (!API_KEY) throw new Error('请设置 OMNIDOC_API_KEY');
const BASE = 'https://omnidoc.top/api/v1';
const API_HEADERS = { 'X-API-Key': API_KEY };
async function uploadDocument(filePath) {
const stat = fs.statSync(filePath);
const { data: ticket } = await axios.post(
`${BASE}/upload/presign`,
{
filename: path.basename(filePath),
file_size: stat.size,
},
{ headers: API_HEADERS, timeout: 20_000 }
);
await axios.put(ticket.upload_url, fs.createReadStream(filePath), {
headers: {
...ticket.upload_headers,
'Content-Length': stat.size,
},
maxBodyLength: Infinity,
timeout: 300_000,
});
const { data: finalized } = await axios.post(
`${BASE}/upload/finalize`,
{ finalize_token: ticket.finalize_token },
{ headers: API_HEADERS, timeout: 30_000 }
);
return finalized.document;
}
async function createJob(documentId, targetLang, engine) {
const { data } = await axios.post(
`${BASE}/jobs`,
{
document_id: documentId,
target_lang: targetLang,
engine,
},
{
headers: { ...API_HEADERS, 'Idempotency-Key': randomUUID() },
timeout: 30_000,
}
);
return data;
}
async function waitForJob(jobId) {
const deadline = Date.now() + 1_200_000;
while (Date.now() < deadline) {
const { data } = await axios.get(`${BASE}/jobs/${jobId}`, {
headers: API_HEADERS,
timeout: 20_000,
});
if (data.status === 'completed') return data;
if (data.status === 'failed') {
throw new Error(data.error_msg || 'translation failed');
}
await new Promise((resolve) => setTimeout(resolve, 3000));
}
throw new Error(`等待超时;可继续查询任务 ${jobId},不要重复创建`);
}
function responseFilename(contentDisposition, fallback) {
const utf8 = contentDisposition?.match(/filename\*=UTF-8''([^;]+)/i);
if (utf8) return path.basename(decodeURIComponent(utf8[1]));
const asciiName = contentDisposition?.match(/filename="([^"]+)"/i);
return asciiName ? path.basename(asciiName[1]) : fallback;
}
async function downloadArtifacts(jobId, outputDir) {
fs.mkdirSync(outputDir, { recursive: true });
const { data: artifacts } = await axios.get(`${BASE}/artifacts`, {
headers: API_HEADERS,
params: { job_id: jobId },
timeout: 20_000,
});
const outputs = [];
for (const artifact of artifacts) {
const response = await axios.get(
`${BASE}/artifacts/${artifact.id}/download`,
{
headers: API_HEADERS,
responseType: 'arraybuffer',
timeout: 300_000,
}
);
const filename = responseFilename(
response.headers['content-disposition'],
`${artifact.id}.${artifact.format}`
);
const safeName = filename.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_').replace(/[. ]+$/g, '') || 'output.bin';
const destination = path.join(outputDir, `${String(outputs.length + 1).padStart(3, '0')}_${safeName}`);
fs.writeFileSync(destination, response.data, { flag: 'wx' });
outputs.push(destination);
}
return outputs;
}
async function translateDocument(filePath, targetLang = 'en') {
const document = await uploadDocument(filePath);
const job = await createJob(document.id, targetLang, 'xr-397b');
console.log('job_id:', job.id);
await waitForJob(job.id);
return downloadArtifacts(job.id, 'omnidoc-output');
}
translateDocument('report.docx').then(console.log).catch((error) => { console.error(error.message); process.exitCode = 1; });cURL 示例适用于 Bash,不要直接粘贴到 PowerShell。Python 与 Node.js 示例可在 Windows 使用;下载时若目标文件已存在会报错,以避免覆盖旧结果。
运行后你会得到什么
控制台先输出 job_id。成功后,文件写入当前目录的 omnidoc-output(cURL 会创建带随机后缀的新目录)。轮询结束条件为 completed 或 failed。
遇到错误
401:检查环境变量是否设置、Key 是否有效。402或任务中的quota_exceeded:检查积分。403:检查 Key 预算、配置权限与资源归属。429:读取业务错误码,区分频率、排队与预算限制。- 直传地址过期或上传失败:重新申请上传地址。
记录 X-Request-Id 与请求路径;日志不要包含完整密钥和上传凭据。完整错误与重试参考 →