O3O DocBuilder và chuyển đổi
Trích văn bản, siêu dữ liệu và ảnh thu nhỏ
Ba endpoint đồng bộ lấy nội dung ra khỏi tài liệu: POST /v1/extract/text cho văn bản thuần theo từng trang, POST /v1/extract/meta cho siêu dữ liệu, POST /v1/extract/thumbnail cho ảnh một trang.
Trong trang này
Trích xuất luôn chạy đồng bộ, không tạo job và dùng được ở cả hai gói. Cả ba endpoint nhận mọi định dạng nguồn trong ma trận định dạng, gửi bằng multipart (trường file) hoặc JSON (trường url). Văn bản và ảnh thu nhỏ đi qua PDF trung gian rồi dùng pdftotext hoặc pdftoppm; siêu dữ liệu được đọc qua UNO mà không dựng trang.
Trích văn bản#
/v1/extract/textTrích văn bản thuần, theo từng trang.
Thân multipart/form-data
filefilebắt buộcTệp nguồn.fromstringtuỳ chọnĐịnh dạng nguồn khi tên tệp không có đuôi rõ ràng.passwordstringtuỳ chọnMật khẩu mở tệp nguồn.max_charsinteger 1–5.000.000tuỳ chọnMặc định:1000000Cắt văn bản trả về ở số ký tự này.
Thân application/json
urlURL, ≤ 2048bắt buộcURL tải tệp nguồn, chịu quy tắc chống SSRF.fromstringtuỳ chọnĐịnh dạng nguồn khi tên tệp không có đuôi rõ ràng.passwordstringtuỳ chọnMật khẩu mở tệp nguồn.max_charsinteger 1–5.000.000tuỳ chọnMặc định:1000000Cắt văn bản trả về ở số ký tự này.
curl -sS http://localhost:8080/v1/extract/text \
-H "Authorization: Bearer O3O_DEMO_KEY" \
-F "file=@bao-gia.docx"import { readFile } from "node:fs/promises";
const BASE_URL = "http://localhost:8080";
const HEADERS = { Authorization: "Bearer O3O_DEMO_KEY" };
const form = new FormData();
form.append("file", new Blob([await readFile("bao-gia.docx")]), "bao-gia.docx");
const res = await fetch(`${BASE_URL}/v1/extract/text`, {
method: "POST",
headers: HEADERS,
body: form,
signal: AbortSignal.timeout(90_000),
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
console.log(await res.json());import requests
BASE_URL = "http://localhost:8080"
HEADERS = {"Authorization": "Bearer O3O_DEMO_KEY"}
with open("bao-gia.docx", "rb") as f:
r = requests.post(
f"{BASE_URL}/v1/extract/text",
headers=HEADERS,
files={"file": ("bao-gia.docx", f)},
timeout=90,
)
if not r.ok:
raise RuntimeError(f"{r.status_code}: {r.text}")
print(r.json())<?php
$ch = curl_init("http://localhost:8080/v1/extract/text");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["Authorization: Bearer O3O_DEMO_KEY"],
CURLOPT_POSTFIELDS => [
'file' => new CURLFile('bao-gia.docx'),
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 90,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($body === false || $status >= 400) {
throw new RuntimeException("HTTP $status: " . ($body === false ? curl_error($ch) : $body));
}
print_r(json_decode($body, true));using System.Net.Http.Headers;
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(90) };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "O3O_DEMO_KEY");
using var form = new MultipartFormDataContent();
form.Add(new ByteArrayContent(await File.ReadAllBytesAsync("bao-gia.docx")), "file", "bao-gia.docx");
using var res = await http.PostAsync("http://localhost:8080/v1/extract/text", form);
if (!res.IsSuccessStatusCode)
throw new HttpRequestException($"{(int)res.StatusCode}: {await res.Content.ReadAsStringAsync()}");
Console.WriteLine(await res.Content.ReadAsStringAsync());{
"text": "BÁO GIÁ BẢN QUYỀN\nSố: BG-2026-0917 · Ngày lập: 21/09/2026\n1. Thông tin khách hàng\nKính gửi: Công ty TNHH Thương mại và Dịch vụ An Phát\n\nPhụ lục: Cách đếm kết nối\nMột người mở ba tài liệu ở chế độ sửa được tính là ba kết nối.",
"pages": [
{
"page": 1,
"text": "BÁO GIÁ BẢN QUYỀN\nSố: BG-2026-0917 · Ngày lập: 21/09/2026\n1. Thông tin khách hàng\nKính gửi: Công ty TNHH Thương mại và Dịch vụ An Phát"
},
{
"page": 2,
"text": "Phụ lục: Cách đếm kết nối\nMột người mở ba tài liệu ở chế độ sửa được tính là ba kết nối."
}
],
"page_count": 2,
"chars": 226,
"truncated": false
}textlà toàn bộ văn bản, các trang nối bằng hai dấu xuống dòng, bị cắt ởmax_chars.charslà số ký tự của văn bản ĐẦY ĐỦ trước khi cắt;truncatedcho biết đã cắt hay chưa.pagescho văn bản từng trang, tiện đánh chỉ mục tìm kiếm theo trang.
Lỗi thường gặp: 400 bad_request khi thiếu cả file lẫn url hoặc max_chars ngoài khoảng cho phép, 415 unsupported_format, 422 corrupt_source, 422 password_required. Bảng đầy đủ ở cuối trang.
Siêu dữ liệu#
/v1/extract/metaTrích siêu dữ liệu và số liệu thống kê của tài liệu.
Thân multipart/form-data
filefilebắt buộcTệp nguồn.fromstringtuỳ chọnĐịnh dạng nguồn khi tên tệp không có đuôi rõ ràng.passwordstringtuỳ chọnMật khẩu mở tệp nguồn.
Thân application/json
urlURL, ≤ 2048bắt buộcURL tải tệp nguồn, chịu quy tắc chống SSRF.fromstringtuỳ chọnĐịnh dạng nguồn khi tên tệp không có đuôi rõ ràng.passwordstringtuỳ chọnMật khẩu mở tệp nguồn.
curl -sS http://localhost:8080/v1/extract/meta \
-H "Authorization: Bearer O3O_DEMO_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/files/bang-luong.xlsx"}'const BASE_URL = "http://localhost:8080";
const HEADERS = { Authorization: "Bearer O3O_DEMO_KEY" };
const payload = {"url": "https://example.com/files/bang-luong.xlsx"};
const res = await fetch(`${BASE_URL}/v1/extract/meta`, {
method: "POST",
headers: { ...HEADERS, "Content-Type": "application/json" },
body: JSON.stringify(payload),
signal: AbortSignal.timeout(90_000),
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
console.log(await res.json());import requests
BASE_URL = "http://localhost:8080"
HEADERS = {"Authorization": "Bearer O3O_DEMO_KEY"}
payload = {"url": "https://example.com/files/bang-luong.xlsx"}
r = requests.post(f"{BASE_URL}/v1/extract/meta", headers=HEADERS, json=payload, timeout=90)
if not r.ok:
raise RuntimeError(f"{r.status_code}: {r.text}")
print(r.json())<?php
$payload = json_encode(['url' => 'https://example.com/files/bang-luong.xlsx'], JSON_UNESCAPED_UNICODE);
$ch = curl_init("http://localhost:8080/v1/extract/meta");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["Authorization: Bearer O3O_DEMO_KEY", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $payload,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 90,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($body === false || $status >= 400) {
throw new RuntimeException("HTTP $status: " . ($body === false ? curl_error($ch) : $body));
}
print_r(json_decode($body, true));using System.Net.Http.Headers;
using System.Text;
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(90) };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "O3O_DEMO_KEY");
var json = """
{
"url": "https://example.com/files/bang-luong.xlsx"
}
""";
using var res = await http.PostAsync("http://localhost:8080/v1/extract/meta",
new StringContent(json, Encoding.UTF8, "application/json"));
if (!res.IsSuccessStatusCode)
throw new HttpRequestException($"{(int)res.StatusCode}: {await res.Content.ReadAsStringAsync()}");
Console.WriteLine(await res.Content.ReadAsStringAsync());sheets liệt kê tên trang tính; các số liệu của văn bản là null.{
"family": "sheet",
"format": "xlsx",
"size": 9876,
"title": "Bảng lương tháng 9 năm 2026",
"subject": null,
"author": "Phòng Hành chính Nhân sự",
"description": null,
"keywords": [],
"created": "2026-09-21T10:00:00Z",
"modified": "2026-09-21T10:00:00Z",
"generator": "LibreOffice/7.4.7.2$Linux_X86_64",
"pages": null,
"words": null,
"chars": null,
"paragraphs": null,
"sheets": [
"Bảng lương",
"Theo phòng ban"
],
"slides": null
}Trường của kết quả
familytext | sheet | slidebắt buộcHọ tài liệu.formatstringbắt buộcĐịnh dạng nguồn đã nhận ra.sizeintegerbắt buộcKích thước tệp nguồn, byte.title, subject, author, descriptionstring | nulltuỳ chọnThuộc tính tài liệu.keywordsarraytuỳ chọnTừ khoá.created, modifieddate-time | nulltuỳ chọnThời điểm tạo và sửa.generatorstring | nulltuỳ chọnPhần mềm đã tạo tệp.pagesinteger | nulltuỳ chọnSố trang (họtext),nullvới họ khác.words, chars, paragraphsinteger | nulltuỳ chọnSố liệu thống kê của tài liệu.sheetsarraytuỳ chọnTên các trang tính (họsheet), mảng rỗng với họ khác.slidesinteger | nulltuỳ chọnSố trang chiếu (họslide).
Lỗi thường gặp: 401 unauthorized, 413 file_too_large, 415 unsupported_format, 422 corrupt_source, 422 password_required; với nguồn URL thêm 422 url_not_allowed và 422 download_failed.
Ảnh thu nhỏ#
/v1/extract/thumbnailẢnh PNG hoặc JPG của một trang, trả thẳng ảnh.
Thân multipart/form-data
filefilebắt buộcTệp nguồn.fromstringtuỳ chọnĐịnh dạng nguồn khi tên tệp không có đuôi rõ ràng.passwordstringtuỳ chọnMật khẩu mở tệp nguồn.pageinteger ≥ 1tuỳ chọnMặc định:1Trang cần dựng ảnh.widthinteger 16–2000tuỳ chọnMặc định:320Chiều rộng ảnh, điểm ảnh.formatpng | jpgtuỳ chọnMặc định:pngĐịnh dạng ảnh.
Thân application/json
urlURL, ≤ 2048bắt buộcURL tải tệp nguồn, chịu quy tắc chống SSRF.fromstringtuỳ chọnĐịnh dạng nguồn khi tên tệp không có đuôi rõ ràng.passwordstringtuỳ chọnMật khẩu mở tệp nguồn.pageinteger ≥ 1tuỳ chọnMặc định:1Trang cần dựng ảnh.widthinteger 16–2000tuỳ chọnMặc định:320Chiều rộng ảnh, điểm ảnh.formatpng | jpgtuỳ chọnMặc định:pngĐịnh dạng ảnh.
curl -sS http://localhost:8080/v1/extract/thumbnail \
-H "Authorization: Bearer O3O_DEMO_KEY" \
-F "file=@gioi-thieu.pptx" \
-F "page=1" \
-F "width=320" \
-F "format=png" \
-D - \
-o trang-bia.png -w "HTTP %{http_code}\n"import { readFile, writeFile } from "node:fs/promises";
const BASE_URL = "http://localhost:8080";
const HEADERS = { Authorization: "Bearer O3O_DEMO_KEY" };
const form = new FormData();
form.append("file", new Blob([await readFile("gioi-thieu.pptx")]), "gioi-thieu.pptx");
form.append("page", "1");
form.append("width", "320");
form.append("format", "png");
const res = await fetch(`${BASE_URL}/v1/extract/thumbnail`, {
method: "POST",
headers: HEADERS,
body: form,
signal: AbortSignal.timeout(90_000),
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
await writeFile("trang-bia.png", Buffer.from(await res.arrayBuffer()));
console.log("X-O3O-Page-Count:", res.headers.get("X-O3O-Page-Count"));import requests
BASE_URL = "http://localhost:8080"
HEADERS = {"Authorization": "Bearer O3O_DEMO_KEY"}
with open("gioi-thieu.pptx", "rb") as f:
r = requests.post(
f"{BASE_URL}/v1/extract/thumbnail",
headers=HEADERS,
files={"file": ("gioi-thieu.pptx", f)},
data={
"page": "1",
"width": "320",
"format": "png",
},
timeout=90,
)
if not r.ok:
raise RuntimeError(f"{r.status_code}: {r.text}")
with open("trang-bia.png", "wb") as fh:
fh.write(r.content)
print("X-O3O-Page-Count:", r.headers.get("X-O3O-Page-Count"))<?php
$headers = [];
$ch = curl_init("http://localhost:8080/v1/extract/thumbnail");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["Authorization: Bearer O3O_DEMO_KEY"],
CURLOPT_POSTFIELDS => [
'file' => new CURLFile('gioi-thieu.pptx'),
'page' => '1',
'width' => '320',
'format' => 'png',
],
CURLOPT_HEADERFUNCTION => function ($ch, $line) use (&$headers) {
$parts = explode(":", $line, 2);
if (count($parts) === 2) {
$headers[strtolower(trim($parts[0]))] = trim($parts[1]);
}
return strlen($line);
},
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 90,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($body === false || $status >= 400) {
throw new RuntimeException("HTTP $status: " . ($body === false ? curl_error($ch) : $body));
}
file_put_contents("trang-bia.png", $body);
echo "X-O3O-Page-Count: " . ($headers["x-o3o-page-count"] ?? "") . "\n";using System.Net.Http.Headers;
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(90) };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "O3O_DEMO_KEY");
using var form = new MultipartFormDataContent();
form.Add(new ByteArrayContent(await File.ReadAllBytesAsync("gioi-thieu.pptx")), "file", "gioi-thieu.pptx");
form.Add(new StringContent("1"), "page");
form.Add(new StringContent("320"), "width");
form.Add(new StringContent("png"), "format");
using var res = await http.PostAsync("http://localhost:8080/v1/extract/thumbnail", form);
if (!res.IsSuccessStatusCode)
throw new HttpRequestException($"{(int)res.StatusCode}: {await res.Content.ReadAsStringAsync()}");
await File.WriteAllBytesAsync("trang-bia.png", await res.Content.ReadAsByteArrayAsync());
Console.WriteLine("X-O3O-Page-Count: " + string.Join(",", res.Headers.GetValues("X-O3O-Page-Count")));X-O3O-Page-Count cho biết tổng số trang, tiện dựng nút chuyển trang khi hiển thị ảnh thu nhỏ.{
"X-O3O-Request-Id": "req_0123456789abcdef",
"X-O3O-Page-Count": "3",
"X-RateLimit-Limit": "10",
"X-RateLimit-Remaining": "9",
"X-RateLimit-Reset": "1789984860"
}Mã lỗi thường gặp#
| HTTP | Mã | Khi nào |
|---|---|---|
| 400 | bad_request | Thiếu cả file lẫn url, hoặc page vượt số trang. |
| 401 | unauthorized | Thiếu hoặc sai khoá. detail.reason là missing, invalid hoặc no_keys_configured. |
| 413 | file_too_large | Tệp vào hoặc tệp ra vượt max_file_mb. detail.limit_mb. |
| 415 | unsupported_format | Không nhận ra định dạng nguồn, hoặc cặp nguồn và đích không được hỗ trợ. |
| 422 | corrupt_source | LibreOffice không mở được tệp nguồn. |
| 422 | password_required | Tệp có mật khẩu mà không truyền mật khẩu, hoặc mật khẩu sai. |
| 422 | url_not_allowed | URL vi phạm quy tắc chống SSRF. |
| 422 | download_failed | Không tải được URL nguồn. |
| 429 | rate_limited | Vượt hạn mức. detail.window là minute hoặc day; kèm Retry-After. |
| 503 | pool_unavailable | Không còn worker nào sống. |
| 504 | timeout | Chế độ đồng bộ quá sync_timeout_seconds, hoặc job quá job_timeout_seconds. Worker bị dừng và dựng lại. |
{
"error": {
"code": "bad_request",
"message": "Tham số không hợp lệ.",
"detail": {
"errors": [
{
"path": "/page",
"message": "Trang 9 vượt số trang của tài liệu (3)."
}
]
},
"request_id": "req_0123456789abcdef"
}
}Sắp có#
Trích cấu trúc tài liệu (/v1/extract/structure), trích ảnh nhúng (/v1/extract/media) và so sánh hai tài liệu (/v1/compare).