
I Tried Out Textract with Japanese Language Support in the Tokyo Region
This page has been translated by machine translation. View original
Introduction
On November 2, 2026, it was announced that Amazon Textract has added support for more languages, including Japanese, and is now available in the Tokyo Region.
To try out the Japanese support, this article presents the results of processing a Japanese PDF with embedded text in the Tokyo Region. In addition, I transcribed page images of the same article with Textract and Bedrock's Sonnet 5.5, and compared the results and costs.
What the update includes
APIs available in the Tokyo Region
According to the announcement, the following six APIs are available in Tokyo and Zurich.
- DetectDocumentText and AnalyzeDocument
- StartDocumentTextDetection and GetDocumentTextDetection
- StartDocumentAnalysis and GetDocumentAnalysis
AnalyzeExpense, AnalyzeID, AnalyzeLending, and Custom Queries (adapters) are not available in these two Regions.
Scope of Japanese support
According to the announcement, the updated model supports more than 40 languages including Japanese, and applies to DetectDocumentText and AnalyzeDocument. No changes are needed to the API request and response formats, and it is available in all Regions where Textract is available. Multilingual table extraction is said to be more robust for tables with merged cells, skewed or rotated tables, and partially missing or broken tables.
According to the Developer Guide, the supported languages for each API are as follows.
- Supports all languages
- StartDocumentTextDetection and GetDocumentTextDetection
- Table, Signatures, and Layout in AnalyzeDocument and StartDocumentAnalysis
- Supports English, Spanish, Italian, Portuguese, French, and German
- AnalyzeDocument and StartDocumentAnalysis with Forms specified
- GetDocumentAnalysis
Japanese is not listed among the supported languages for Forms. According to the Developer Guide, handwriting recognition is available only for English.
Overview of the tests
There were two kinds of input, both processed in the Tokyo Region (ap-northeast-1). The input, the APIs used, and what was checked in each test are as follows.
| Input | APIs used | What was checked | |
|---|---|---|---|
| Test 1 | PDF (12 pages, with embedded text) | Textract asynchronous APIs (text detection, Tables, Layout) | Extraction of Japanese characters, tables, and headings |
| Test 2 | Page images (JPEG, split into 6) | Textract synchronous API (text detection) and Bedrock's Sonnet 5.5 | Comparison of transcription accuracy and cost |
Materials
The PDF is a file of the following DevelopersIO article saved as a PDF in the Chrome browser: A4, 12 pages, about 2.4MB, with embedded text.
Each page has a header with the print date and time and a footer with the URL and page number. For the comparison ground truth, I used the embedded text with these removed. The author section, link cards, and related articles are included in the comparison.
I also prepared a page image (JPEG) of the same article. It is passed to Textract and Bedrock's Sonnet 5.5 as an image without a text layer. How it was made and the results are described in "Test 2".
Test 1: PDF (Textract)
Calling the APIs
The synchronous APIs accept PDFs of only one page. The asynchronous APIs accept up to 3,000 pages and 500MB, so I processed the 12-page PDF with the asynchronous APIs. I placed the PDF in an S3 bucket in the Tokyo Region and started the following two jobs.
aws textract start-document-text-detection --region ap-northeast-1 --document-location 'S3Object={Bucket=<バケット名>,Name=devio-lwf.pdf}'
aws textract start-document-analysis --region ap-northeast-1 --feature-types TABLES LAYOUT --document-location 'S3Object={Bucket=<バケット名>,Name=devio-lwf.pdf}'
I checked the job status every 6 seconds, and by the check 26 to 27 seconds after starting, both had reached SUCCEEDED.
You retrieve the results by specifying the JobId returned by the start command. Use get-document-text-detection for text detection, and get-document-analysis for Tables and Layout. A single retrieval returns up to 1,000 blocks, and from the second call onward you pass the NextToken from the response to --next-token. The second and later retrievals look like this.
aws textract get-document-text-detection --region ap-northeast-1 --job-id <JobId> --next-token <NextToken>
For this PDF, the first retrieval returned pages 1 to 7 for text detection and pages 1 to 6 for Tables and Layout. I followed the NextToken to retrieve the rest up to page 12.
Text detection
I output the LINEs (36 lines) of page 7 with the following command. Three of those lines are shown below.
jq -r '.Blocks[] | select(.BlockType=="LINE" and .Page==7) | [.Confidence, .Text] | @tsv' <取得結果のJSON>
99.33175659179688 過去24時間のピーク時の vCPU 消費数は、 CloudWatch の EndpointActiveEnvironments の1
98.97364044189453 分ごとの最大値に、 1環境あたりの vCPU 数 (2) をかけて算出しました。 東京リージョンの値
99.96810913085938 には、 dev と stg の環境も含まれます。
LINEs are returned per visual line, so sentences are split midway.
For comparison with the original text, I compared per-character occurrence counts between the PDF's embedded text (excluding the printed headers and footers) and the LINE output. I normalized with NFKC and removed whitespace, and did not look at ordering. The LINE output also includes characters inside figures and screenshots.
Of 5,785 characters, 5,720 (98.88%) were included in the output. The 65 characters not included were 53 inside tables, 10 in running text, and 2 in the related articles section. All 57 arrows in this PDF were inside the tables on pages 8 to 9. The breakdown is as follows.
| Original text | Textract output | Count | Output line (excerpt) |
|---|---|---|---|
| → | Only 5 of 57 are included in the output | 52 | — |
| Wave dash | Replaced with ~ | 5 | approx. 0.4~1.2 |
| Hiragana "he" | Replaced with the katakana "he" | 3 | "request to the Region" (with the katakana "he") |
| "Arc" kanji (as in "parentheses") | Replaced with the lookalike "lonely" kanji | 1 | "inside parentheses" (with the wrong kanji) |
| Hyphen (inside a table), @ (related articles section), and a CJK Radicals Supplement variant of the "long" character | The hyphen and @ are not in the output. The radical variant is replaced with the regular "long" kanji | 4 (hyphen 1, @ 2, radical variant 1) | — |
| AI | Al | 1 (not included in the 65 characters) | Al search bot |
The confidence of LINEs, across the 193 lines containing Japanese (hiragana, katakana, and kanji), averaged 98.52 with a minimum of 81.41. Five lines were below 90. Of all 654 lines, the 24 lines with confidence below 80 were all lines without Japanese (URLs, symbols, and alphanumeric fragments).
The confidence of the lines with misreads was 81.41 for the line with the "arc" to "lonely" kanji swap, 92.7 for the "Al" line, and 98.3 to 99.7 for the three lines with the katakana "he". If you check only the lines with confidence below 90, the only misread you find is the "arc" to "lonely" kanji swap.
Tables and layout
The body of the original article has 8 tables, and 7 of them were extracted with matching numbers of rows and columns. The one spanning pages (18 rows × 6 columns) was split into two TABLEs of 4 rows and 15 rows. The first row of the second one is a header row repeated in print, so for the comparison I joined the two and excluded this row. Besides the body tables, areas such as the author section, link cards, related articles, and the footer at the end of the article were also returned as TABLEs.
The cell comparison covered the 8 body tables, 195 cells in total, after NFKC normalization and whitespace removal. 146 cells (74.9%) matched exactly, and ignoring arrows, 194 (99.5%) matched. In the table spanning pages, of the 54 cells containing arrows, the arrow was dropped in 49 cells and remained in 5. The only cell that still did not match when ignoring arrows was one cell where 0.004→- became 0.004.
| Cell in the original table | Textract cell |
|---|---|
| 0.461→0.118 | 0.461 0.118 |
| 14→15 | 14 15 |
| 0.004→- | 0.004 |
| Miss count (ECS→Web Functions) | Miss count (ECS Web Functions) |
For layout, 12 of the 13 headings (h2 and h3) in the original article were extracted as LAYOUT_SECTION_HEADER. The one not extracted was "Introduction" in the original article. There were 16 LAYOUT_SECTION_HEADERs overall, and the other 4 were the article title, the title of a link card, "Related articles", and "classmethod" in the footer at the end of the article. The table of contents was not treated as a heading.
Test 2: Page images (Textract and Bedrock's Sonnet)
Here I turned the same article into an image in the browser, passed the same image to Textract and Amazon Bedrock's Claude Sonnet 5.5, and compared the transcription results and costs. Using an image without a text layer lets me give both the same input.
Creating the image
In Chrome DevTools, I opened the command menu (Ctrl+Shift+P, or Cmd+Shift+P on Mac) and captured the whole article page with "Capture full size screenshot". The result was a PNG of 1,268×10,817 pixels, about 4.0MB. I trimmed the ads at the end of the article, the related articles, and the site footer, and made a JPEG (1,268×8,978 pixels, 1.74MB).
Besides the article body, the image shows the menu at the top, the event list and ads on the right, and an architecture diagram in the body. The graph's position was not rendered and is blank.
Limits and splitting
The per-image limits for Textract and Bedrock are as follows.
| Textract (synchronous) | Bedrock (Converse) | |
|---|---|---|
| Size per image | 10MB | 3.75MB |
| Pixels (height and width) | 10,000 or less | 8,000 or less |
| PNG (4.0MB, height 10,817) | Height exceeds | Size and height exceed |
| JPEG (1.74MB, height 8,978) | Within range | Height exceeds |
The Textract limits are from the Developer Guide, and the Bedrock limits are from the API reference. The JPEG is within Textract's range, but its height exceeds Bedrock's limit, so to use the same conditions for both, I split it into 6 images of about 1,500 pixels each.
The splitting was done in Python. It looks for rows with no characters in the body column and cuts there. The ad column on the right is not used to decide the cuts.
Split script (split_image.py)
It uses Pillow and NumPy. Run it with python3 split_image.py page.jpg split.
"""縦長の画像を、本文の列に文字がない行で、約1,500ピクセルごとに分割する。
使い方: python3 split_image.py <入力画像> <出力ディレクトリ>
"""
import os
import sys
import numpy as np
from PIL import Image
src, out_dir = sys.argv[1], sys.argv[2]
os.makedirs(out_dir, exist_ok=True)
TARGET = 1500 # Target height per tile (pixels)
WINDOW = 250 # Range around the target position in which to look for a cut
BODY_W = 980 # Right edge of the body column. The ad column on the right is not used for detection
im = Image.open(src).convert("RGB")
w, h = im.size
gray = np.asarray(im.convert("L")).astype(float)
ink = (gray[:, :BODY_W] < 200).sum(axis=1) # Number of dark pixels per row
cuts = [0]
while h - cuts[-1] > TARGET + WINDOW:
center = cuts[-1] + TARGET
# Cut at the row with the fewest dark pixels that is closest to the target position
best = min(range(center - WINDOW, center + WINDOW),
key=lambda y: (ink[y], abs(y - center)))
cuts.append(best)
cuts.append(h)
for i, (top, bottom) in enumerate(zip(cuts, cuts[1:]), 1):
path = os.path.join(out_dir, f"tile-{i:02d}.png")
im.crop((0, top, w, bottom)).save(path)
print(f"{path}: y={top}-{bottom} height={bottom - top}")
The splitting results are as follows.
| Tile | Vertical position (pixels) | Height |
|---|---|---|
| 1 | 0–1,500 | 1,500 |
| 2 | 1,500–3,000 | 1,500 |
| 3 | 3,000–4,501 | 1,501 |
| 4 | 4,501–5,993 | 1,492 |
| 5 | 5,993–7,493 | 1,500 |
| 6 | 7,493–8,978 | 1,485 |
Processing with Textract
I processed the 6 tiles with the synchronous detect-document-text. With AWS CLI 2.39.3 (cli_binary_format not set), specifying --document Bytes=fileb://<image> failed with UnsupportedDocumentException (Request has unsupported document format), so I passed JSON with the image embedded as base64.
Script to convert an image to JSON (make_document_json.py)
Save the following as make_document_json.py and run it with python3 make_document_json.py <image> <output JSON>.
import base64
import json
import sys
src, dst = sys.argv[1], sys.argv[2]
data = open(src, "rb").read()
json.dump({"Bytes": base64.b64encode(data).decode("ascii")}, open(dst, "w"))
for i in 01 02 03 04 05 06; do
python3 make_document_json.py split/tile-$i.png doc-$i.json
aws textract detect-document-text --region ap-northeast-1 \
--document file://doc-$i.json > textract-$i.json
done
All 6 succeeded, each returning in 1.3 to 1.6 seconds. The LINEs returned were 71, 111, 69, 50, 92, and 66 from the first tile onward, 459 lines in total.
The unsplit JPEG (height 8,978) is also within Textract's limits, so I processed it the same way. The call succeeded, but it returned only 79 LINEs and 326 characters, mainly large text such as headings. Most of the body was not detected.
Processing with Bedrock's Sonnet
On Bedrock, I called the Global inference profile global.anthropic.claude-sonnet-5-5 from the Tokyo Region with the Converse API. I passed the 6 tiles one at a time and had it transcribe them with the following prompt.
この画像に含まれる文字をすべて、見えるとおりに文字起こししてください。説明や要約は付けず、文字起こしだけを出力してください。
With Sonnet 5.5, specifying temperature results in a ValidationException (temperature is deprecated for this model), so I did not specify it. The response may include a reasoningContent block showing the reasoning, so I concatenated only the text blocks.
Bedrock invocation script (bedrock_transcribe.py)
It uses boto3. Running python3 bedrock_transcribe.py global.anthropic.claude-sonnet-5-5 split . sonnet produces sonnet-01.txt through sonnet-06.txt.
"""分割した画像を Bedrock の Converse API で文字起こしし、トークン数を記録する。
使い方: python3 bedrock_transcribe.py <モデルID> <タイル画像のディレクトリ> <出力ディレクトリ> <出力ファイルの接頭辞>
"""
import json
import os
import sys
import time
import boto3
model_id, tile_dir, out_dir, prefix = sys.argv[1:5]
os.makedirs(out_dir, exist_ok=True)
client = boto3.client("bedrock-runtime", region_name="ap-northeast-1")
prompt = "この画像に含まれる文字をすべて、見えるとおりに文字起こししてください。説明や要約は付けず、文字起こしだけを出力してください。"
total_in = total_out = 0
for i in range(1, 7):
with open(os.path.join(tile_dir, f"tile-{i:02d}.png"), "rb") as f:
image = f.read()
started = time.time()
res = client.converse(
modelId=model_id,
messages=[{"role": "user", "content": [
{"image": {"format": "png", "source": {"bytes": image}}},
{"text": prompt},
]}],
inferenceConfig={"maxTokens": 8192},
)
usage = res["usage"]
total_in += usage["inputTokens"]
total_out += usage["outputTokens"]
# The response may contain a reasoning block (reasoningContent), so concatenate only the text blocks
blocks = res["output"]["message"]["content"]
text = "".join(b["text"] for b in blocks if "text" in b)
with open(os.path.join(out_dir, f"{prefix}-{i:02d}.txt"), "w", encoding="utf-8") as f:
f.write(text)
with open(os.path.join(out_dir, f"{prefix}-{i:02d}.meta.json"), "w") as f:
json.dump({"usage": usage, "stopReason": res["stopReason"]}, f)
print(f"tile-{i:02d} in={usage['inputTokens']} out={usage['outputTokens']} "
f"blocks={[list(b)[0] for b in blocks]} {time.time() - started:.1f}s")
print("TOTAL in", total_in, "out", total_out)
The input token count was 2,548 for all 6 tiles. This roughly matches width × height ÷ 750 (the rule of thumb for image tokens in Anthropic's documentation).
| Tile | Input tokens | Output tokens | Time taken |
|---|---|---|---|
| 1 | 2,548 | 2,121 | 14.3 s |
| 2 | 2,548 | 1,128 | 9.3 s |
| 3 | 2,548 | 852 | 5.6 s |
| 4 | 2,548 | 884 | 6.3 s |
| 5 | 2,548 | 1,203 | 8.4 s |
| 6 | 2,548 | 1,074 | 7.7 s |
| Total | 15,288 | 7,262 |
The first tile has more output tokens because a reasoning block was included. The transcription of the first tile was 1,391 characters.
Comparison with the ground truth
The ground truth is the body text extracted from the original article's HTML. It is 5,004 characters, excluding the contents of collapsed sections, standalone link lines, and table cell separators. As with the PDF, the comparison used per-character occurrence counts. Because the output also contains characters from ads and the like, occurrence counts alone look better than reality. So I also checked whether 6 consecutive characters within each ground-truth line appear as-is in the output.
Comparison script (compare_ocr.py)
Run it by listing names and file patterns, like python3 compare_ocr.py ground-truth.txt "Textract=textract-0*.json" "Sonnet=sonnet-0*.txt". .json files are read as Textract output.
"""OCR・文字起こしの出力を、元記事の本文(正解データ)と比べる。
使い方: python3 compare_ocr.py <正解データ> "名前=ファイルのパターン" ...
例: python3 compare_ocr.py ground-truth.txt "Textract=textract-0*.json" "Sonnet=sonnet-0*.txt"
.json は Textract の出力(LINE の Text を連結)、それ以外はテキストとして読む。
"""
import glob
import json
import re
import sys
import unicodedata
from collections import Counter
def norm(s):
# NFKC で正規化し、空白を除く
return re.sub(r"\s+", "", unicodedata.normalize("NFKC", s))
def ngrams(s, n):
return [s[i:i + n] for i in range(len(s) - n + 1)]
def read_output(pattern):
texts = []
for path in sorted(glob.glob(pattern)):
if path.endswith(".json"):
blocks = json.load(open(path, encoding="utf-8"))["Blocks"]
texts.append("".join(b["Text"] for b in blocks if b["BlockType"] == "LINE"))
else:
texts.append(open(path, encoding="utf-8").read())
return norm("".join(texts))
# 正解データ: 折りたたみ(details)の中身は画面に表示されないので除く(34〜54行目)。
# 単独のリンク行(画像ではカード表示になる)と、表のセル区切りの | も除く。
raw = open(sys.argv[1], encoding="utf-8").read().splitlines()
raw = raw[:33] + ["buildspec の抜粋"] + raw[54:]
lines = [re.sub(r"\s*\|\s*$", "", l) for l in raw if not re.match(r"^\s*https?://\S+\s*$", l)]
lines = [norm(l) for l in lines if norm(l)]
truth = "".join(lines)
print("正解データ", len(truth), "文字")
for arg in sys.argv[2:]:
name, pattern = arg.split("=", 1)
out = read_output(pattern)
# 1. 文字ごとの出現数(PDF の比較と同じ方法。順序は見ない)
missing = Counter(truth) - Counter(out)
n = sum(missing.values())
print(f"\n{name}: 出力 {len(out)} 文字")
print(f" 正解のうち含まれた文字 {len(truth) - n}/{len(truth)} = {(len(truth) - n) / len(truth) * 100:.2f}%")
print(" 含まれなかった文字", missing.most_common(8))
# 2. 正解の行の中の連続 n 文字が、出力にそのまま現れるか(1文字の誤りで周辺の n 文字が不一致になる)
for k in (4, 6, 8):
total = sum(len(ngrams(l, k)) for l in lines)
hit = sum(g in out for l in lines for g in ngrams(l, k))
print(f" 行内の連続{k}文字の一致 {hit}/{total} = {hit / total * 100:.2f}%")
| Textract | Sonnet 5.5 | |
|---|---|---|
| Characters in output | 7,056 | 7,279 |
| Characters included out of the 5,004 ground-truth characters | 4,991 (99.74%) | 5,004 (100%) |
| Match of 6 consecutive characters within a line | 96.15% | 98.82% |
The output has more characters than the ground truth because characters in ads, menus, and figures are also included in the output.
The 13 characters missing from Textract (6 tiles) and the misread characters are as follows. They are the same kinds of errors found in the PDF test.
- Arrow "→": 5 of 54 are missing from the output
- Wave dash: all 5 replaced with "~"
- Hiragana "he": replaced with the katakana "he" (2 places)
- "Arc" kanji: replaced with the lookalike "lonely" kanji (in "parentheses")
- "North" kanji: the single character in "North America" is missing from the output
- "I": the "I" in DevelopersIO was replaced with "l" (a misread that the occurrence-count comparison does not detect)
Sonnet 5.5 had no misread or missing characters in the body text. The only differences from the ground truth were places where inline code was wrapped in backticks, which is an added formatting rather than a character error.
Reading order also contributes to Textract's lower 6-consecutive-character match compared with Sonnet 5.5. Characters from the ads on the right and from tables are returned mixed in with the body lines.
The time per tile was 1.3 to 1.6 seconds for Textract and 5.6 to 14.3 seconds for Sonnet 5.5. Costs are summarized in the "Cost" section.
Scope of this comparison
- The target is images of the pages of a single article, with one run each.
- Ground truth exists only for the body text. Characters in ads and figures were not included in the evaluation.
- Textract also returns bounding boxes and table structure. This comparison looks only at the transcribed characters.
Cost
The Textract unit prices in the Tokyo Region, per page (one image counts as one page), are 0.0015 USD for text detection, 0.015 USD for Tables, and 0.004 USD for Layout.
Calculated with these unit prices, the cost of each test is as follows.
| Test | Processing | Calculation | Cost |
|---|---|---|---|
| Test 1 | PDF 12 pages: text detection, Tables, Layout | 12 × (0.0015 + 0.015 + 0.004) | About 0.25 USD (text detection 0.018, Tables 0.18, Layout 0.048) |
| Test 2 | 6 page images: Textract text detection | 6 × 0.0015 | 0.009 USD |
| Test 2 | The same 6 images: Sonnet 5.5 | Input 15,288 tokens, output 7,262 tokens | About 0.10 USD |
Sonnet 5.5 is calculated with Global unit prices (input 2.00 USD, output 10.00 USD per million tokens).
| Sonnet 5.5 | Calculation | Cost |
|---|---|---|
| Input | 15,288 × 2.00 / 1 million | About 0.031 USD |
| Output | 7,262 × 10.00 / 1 million | About 0.073 USD |
| Total | About 0.10 USD |
For the same 6 images, Sonnet 5.5 cost about 11 times as much as Textract. All figures are estimates based on published unit prices.
Summary
With Amazon Textract in the Tokyo Region, I was able to run text detection on a Japanese PDF and extract tables and layout.
If you need OCR or transcription of images but had passed on Textract because it lacked Japanese support or availability in the Tokyo Region, or if you adopted an LLM such as Sonnet for OCR but cost is a problem, give this update a try.
