📘

NDL古典籍OCR-Liteを用いたアノテーション付きIIIFマニフェストファイルとTEI/XMLファイルの作成

に公開

お知らせ

本記事で紹介する流れをわかりやすくした記事を作成しました。以下も参考にしてください。

https://zenn.dev/nakamura196/articles/bd58ba02f9e721

概要

NDL古典籍OCR-Liteを用いたアノテーション付きIIIFマニフェストファイルとTEI/XMLファイルの作成を行うツールを試作したので紹介します。

アノテーション付きIIIFマニフェストファイルの作成

まず、NDL古典籍OCR-Liteを用いて、IIIFマニフェストファイルを入力として、アノテーション付きIIIFマニフェストファイルを出力するGradioアプリを作成しました。Hugging FaceのSpaceを用いて公開しています。

https://nakamura196-ndlkotenocr-lite-iiif.hf.space/

出力結果として、以下のようなアノテーション付きIIIFマニフェストファイルが得られます。

{
    "@context": "http://iiif.io/api/presentation/3/context.json",
    "id": "https://dl.ndl.go.jp/api/iiif/3437686/manifest.json",
    "type": "Manifest",
    "label": {
      "none": [
        "校異源氏物語. 巻一"
      ]
    },
    "items": [
      {
        "id": "https://dl.ndl.go.jp/api/iiif/3437686/canvas/1",
        "type": "Canvas",
        "width": 6890,
        "height": 4706,
        "label": {
          "none": [
            "1"
          ]
        },
        "items": [
          {
            "id": "https://dl.ndl.go.jp/api/iiif/3437686/canvas/1/page",
            "type": "AnnotationPage",
            "items": [
              {
                "id": "https://dl.ndl.go.jp/api/iiif/3437686/canvas/1/page/imageanno",
                "type": "Annotation",
                "motivation": "sc:painting",
                "target": "https://dl.ndl.go.jp/api/iiif/3437686/canvas/1",
                "body": {
                  "id": "https://dl.ndl.go.jp/api/iiif/3437686/R0000001/full/full/0/default.jpg",
                  "type": "Image",
                  "format": "image/jpeg",
                  "width": 6890,
                  "height": 4706,
                  "service": [
                    {
                      "id": "https://dl.ndl.go.jp/api/iiif/3437686/R0000001",
                      "type": "ImageService2",
                      "profile": "level2"
                    }
                  ]
                }
              }
            ]
          }
        ],
        "annotations": [
          {
            "id": "https://dl.ndl.go.jp/api/iiif/3437686/canvas/1/annos",
            "type": "AnnotationPage",
            "items": [
              {
                "id": "https://dl.ndl.go.jp/api/iiif/3437686/canvas/1/annos/0",
                "type": "Annotation",
                "motivation": "commenting",
                "target": "https://dl.ndl.go.jp/api/iiif/3437686/canvas/1#xywh=5270,275,114,935",
                "body": {
                  "type": "TextualBody",
                  "value": "一・〇・・・・・・一一一一・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・"
                }
              },
              {
                "id": "https://dl.ndl.go.jp/api/iiif/3437686/canvas/1/annos/1",
                "type": "Annotation",
                "motivation": "commenting",
                "target": "https://dl.ndl.go.jp/api/iiif/3437686/canvas/1#xywh=5293,2009,218,424",
                "body": {
                  "type": "TextualBody",
                  "value": "○〇"
                }
              },
              {
                "id": "https://dl.ndl.go.jp/api/iiif/3437686/canvas/1/annos/2",
                "type": "Annotation",
                "motivation": "commenting",
                "target": "https://dl.ndl.go.jp/api/iiif/3437686/canvas/1#xywh=5092,3272,63,80",
                "body": {
                  "type": "TextualBody",
                  "value": "一一"
                }
              },
              {
                "id": "https://dl.ndl.go.jp/api/iiif/3437686/canvas/1/annos/3",
                "type": "Annotation",
                "motivation": "commenting",
                "target": "https://dl.ndl.go.jp/api/iiif/3437686/canvas/1#xywh=4375,304,103,1475",
                "body": {
                  "type": "TextualBody",
                  "value": "ス〇〇〇六〇〇〇一〇〇〇〇〇〇〇一一一〇〇〇一一一一〇〇〇〇〇〇〇〇〇〇一一・〇〇・・・・・・・の〇〇・・・・一・・・"
                }
              },
              {
                "id": "https://dl.ndl.go.jp/api/iiif/3437686/canvas/1/annos/4",
                "type": "Annotation",
                "motivation": "commenting",
                "target": "https://dl.ndl.go.jp/api/iiif/3437686/canvas/1#xywh=4375,2853,45,522",
                "body": {
                  "type": "TextualBody",
                  "value": "□琉球□□□□□□□□□□□□□□□□□"
                }
              },
              {
                "id": "https://dl.ndl.go.jp/api/iiif/3437686/canvas/1/annos/5",
                "type": "Annotation",
                "motivation": "commenting",
                "target": "https://dl.ndl.go.jp/api/iiif/3437686/canvas/1#xywh=4283,2756,63,252",
                "body": {
                  "type": "TextualBody",
                  "value": "〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇一〇〇一〇〇〇"
                }
              },
              {
                "id": "https://dl.ndl.go.jp/api/iiif/3437686/canvas/1/annos/6",
                "type": "Annotation",
                "motivation": "commenting",
                "target": "https://dl.ndl.go.jp/api/iiif/3437686/canvas/1#xywh=694,499,310,2991",
                "body": {
                  "type": "TextualBody",
                  "value": "同校異源氏物巻一"
                }
              }
            ]
          }
        ]
      },
      {
        "id": "https://dl.ndl.go.jp/api/iiif/3437686/canvas/2",
        "type": "Canvas",
        "width": 6890,
        "height": 4706,
        "label": {
          "none": [
            "2"
          ]
        },
        "items": [
          {
            "id": "https://dl.ndl.go.jp/api/iiif/3437686/canvas/2/page",
            "type": "AnnotationPage",
            "items": [
              {
                "id": "https://dl.ndl.go.jp/api/iiif/3437686/canvas/2/page/imageanno",
                "type": "Annotation",
                "motivation": "sc:painting",
                "target": "https://dl.ndl.go.jp/api/iiif/3437686/canvas/2",
                "body": {
                  "id": "https://dl.ndl.go.jp/api/iiif/3437686/R0000002/full/full/0/default.jpg",
                  "type": "Image",
                  "format": "image/jpeg",
                  "width": 6890,
                  "height": 4706,
                  "service": [
                    {
                      "id": "https://dl.ndl.go.jp/api/iiif/3437686/R0000002",
                      "type": "ImageService2",
                      "profile": "level2"
                    }
                  ]
                }
              }
            ]
          }
        ],
        "annotations": [
          {
            "id": "https://dl.ndl.go.jp/api/iiif/3437686/canvas/2/annos",
            "type": "AnnotationPage",
            "items": []
          }
        ]
      }
    ]
  }

TEI/XMLファイルの作成

上記で得られたアノテーション付きIIIFマニフェストファイルを入力として、TEI/XMLファイルを作成するライブラリを作成しました。

https://www.npmjs.com/package/@nakamura196/iiif-to-tei

以下のような構成から使用することができます。

package.json
{
  "name": "convert",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@nakamura196/iiif-to-tei": "^1.0.1",
    "glob": "^11.0.2"
  }
}

data/inputフォルダにマニフェストファイルを格納し、以下を実行すると、data/outputフォルダにTEI/XMLファイルが出力されます。

index.js
// import { someFunction } from 'iiif-to-tei';
// または
// import { IIIFToTEIConverter } from '../src/index';
// const iiifToTei = require('@repo/iiif-to-tei');
const { IIIFToTEIConverter } = require('@nakamura196/iiif-to-tei');
const fs = require('fs');
const path = require('path');
const glob = require('glob');

const input_dir = "./data/input";
const output_dir = "./data/output";

// 出力ディレクトリが存在しない場合は作成
if (!fs.existsSync(output_dir)) {
    fs.mkdirSync(output_dir, { recursive: true });
    console.log(`出力ディレクトリを作成しました: ${output_dir}`);
}

// 入力ディレクトリ内のすべてのJSONファイルを取得
const jsonFiles = glob.sync(path.join(input_dir, "*.json"));

// 各JSONファイルを処理
jsonFiles.forEach(jsonFile => {
    const jsonData = JSON.parse(fs.readFileSync(jsonFile, 'utf8'));
    // const xmlOutput = iiifToTei(jsonData);

        // コンバーターを初期化
        const converter = new IIIFToTEIConverter({
            includeImages: true,
            includeFacsimile: true
          });

          const teiXml = converter.convert(jsonData);
    
    // 出力ファイル名を生成(.jsonを.xmlに置換)
    const outputFile = path.join(output_dir, path.basename(jsonFile, '.json') + '.xml');
    
    // XMLファイルを保存
    fs.writeFileSync(outputFile, teiXml);
    console.log(`変換完了: ${path.basename(jsonFile)}${path.basename(outputFile)}`);
});

出力されるTEI/XMLファイルの例は以下です。

<?xml version="1.0" encoding="UTF-8"?>
<?xml-model href="http://www.tei-c.org/release/xml/tei/custom/schema/relaxng/tei_all.rng" type="application/xml" schematypens="http://relaxng.org/ns/structure/1.0"?>
<?xml-model href="http://www.tei-c.org/release/xml/tei/custom/schema/relaxng/tei_all.rng" type="application/xml" schematypens="http://purl.oclc.org/dsdl/schematron"?>
<TEI xmlns="http://www.tei-c.org/ns/1.0">
  <teiHeader>
    <fileDesc>
      <titleStmt>
        <title>校異源氏物語. 巻一</title>
      </titleStmt>
      <publicationStmt>
        <p>Converted from IIIF Manifest</p>
      </publicationStmt>
      <sourceDesc>
        <msDesc>
          <msIdentifier>
            <idno>https://dl.ndl.go.jp/api/iiif/3437686/manifest.json</idno>
          </msIdentifier>
        </msDesc>
      </sourceDesc>
    </fileDesc>
  </teiHeader>
  <text>
    <body>
      <div n="1">
        <ab type="line" corresp="#zone-0-0">一・〇・・・・・・一一一一・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・</ab>
        <ab type="line" corresp="#zone-0-1">○〇</ab>
        <ab type="line" corresp="#zone-0-2">一一</ab>
        <ab type="line" corresp="#zone-0-3">ス〇〇〇六〇〇〇一〇〇〇〇〇〇〇一一一〇〇〇一一一一〇〇〇〇〇〇〇〇〇〇一一・〇〇・・・・・・・の〇〇・・・・一・・・</ab>
        <ab type="line" corresp="#zone-0-4">□琉球□□□□□□□□□□□□□□□□□</ab>
        <ab type="line" corresp="#zone-0-5">〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇一〇〇一〇〇〇</ab>
        <ab type="line" corresp="#zone-0-6">同校異源氏物巻一</ab>
      </div>
      <div n="2"/>
    </body>
  </text>
  <facsimile sameAs="https://dl.ndl.go.jp/api/iiif/3437686/manifest.json">
    <surface sameAs="https://dl.ndl.go.jp/api/iiif/3437686/canvas/1" ulx="0" uly="0" lrx="6890" lry="4706">
      <graphic url="https://dl.ndl.go.jp/api/iiif/3437686/R0000001/full/full/0/default.jpg"/>
      <zone xml:id="zone-0-0" ulx="5270" uly="275" lrx="5384" lry="1210"/>
      <zone xml:id="zone-0-1" ulx="5293" uly="2009" lrx="5511" lry="2433"/>
      <zone xml:id="zone-0-2" ulx="5092" uly="3272" lrx="5155" lry="3352"/>
      <zone xml:id="zone-0-3" ulx="4375" uly="304" lrx="4478" lry="1779"/>
      <zone xml:id="zone-0-4" ulx="4375" uly="2853" lrx="4420" lry="3375"/>
      <zone xml:id="zone-0-5" ulx="4283" uly="2756" lrx="4346" lry="3008"/>
      <zone xml:id="zone-0-6" ulx="694" uly="499" lrx="1004" lry="3490"/>
    </surface>
    <surface sameAs="https://dl.ndl.go.jp/api/iiif/3437686/canvas/2" ulx="0" uly="0" lrx="6890" lry="4706">
      <graphic url="https://dl.ndl.go.jp/api/iiif/3437686/R0000002/full/full/0/default.jpg"/>
    </surface>
  </facsimile>
</TEI>

Oxygen XML Editorを使って、以下のように出力結果を確認することができます。

参考:Turborepoを用いたモノレポ開発

上述したnpmパッケージの開発にあたり、Turborepoを用いたモノレポを用いました。

ウェブアプリはNext.jsを用いて開発しました。以下から利用することができます。

https://iiif-tei-monorepo-web.vercel.app/

APIについては、Swagger UIを通じて、以下からご確認いただけます。

https://iiif-tei-monorepo-web.vercel.app/api-docs

Pythonからは以下のように使用することができます。

import requests
import json
from typing import Optional, Dict, Any
from dataclasses import dataclass

@dataclass
class ConvertOptions:
    """変換オプション"""
    include_images: bool = False
    include_facsimile: bool = False
    base_url: Optional[str] = None

class IIIFToTEIClient:
    """IIIF to TEI変換APIクライアント"""
    
    def __init__(self, api_base_url: str):
        """
        Args:
            api_base_url: APIのベースURL(例: "http://localhost:3000")
        """
        self.api_base_url = api_base_url.rstrip('/')
        self.convert_endpoint = f"{self.api_base_url}/api/convert"
    
    def convert_from_manifest(self, 
                             manifest_object: Dict[str, Any], 
                             options: Optional[ConvertOptions] = None) -> str:
        """
        IIIFマニフェストオブジェクトからTEI XMLに変換
        
        Args:
            manifest_object: IIIFマニフェストオブジェクト
            options: 変換オプション
            
        Returns:
            変換されたTEI XML文字列
        """
        payload = {
            "manifest": manifest_object
        }
        
        if options:
            payload["options"] = {
                "includeImages": options.include_images,
                "includeFacsimile": options.include_facsimile,
                "baseUrl": options.base_url
            }
        
        return self._make_request(payload)
    
    def _make_request(self, payload: Dict[str, Any]) -> str:
        """
        APIリクエストを実行
        
        Args:
            payload: リクエストペイロード
            
        Returns:
            変換されたTEI XML文字列
        """
        try:
            response = requests.post(
                self.convert_endpoint,
                json=payload,
                headers={
                    'Content-Type': 'application/json'
                },
                timeout=30
            )
            
            # HTTPエラーをチェック
            response.raise_for_status()
            
            # レスポンスをJSONとして解析
            result = response.json()
            
            # エラーレスポンスをチェック
            if not result.get('success', False):
                error_msg = result.get('error', 'Unknown error')
                details = result.get('details', '')
                raise ValueError(f"API Error: {error_msg}. Details: {details}")
            
            return result.get('teiXml', '')
            
        except requests.exceptions.RequestException as e:
            raise requests.RequestException(f"API request failed: {str(e)}")
        except json.JSONDecodeError as e:
            raise ValueError(f"Invalid JSON response: {str(e)}")
# クライアントを初期化
client = IIIFToTEIClient("https://iiif-tei-monorepo-web.vercel.app")

# 例3: マニフェストオブジェクトから変換
try:
    # 実際のマニフェストオブジェクトをここに設定
    manifest_object = {
        "@context": "http://iiif.io/api/presentation/3/context.json",
        "id": "https://dl.ndl.go.jp/api/iiif/3437686/manifest.json",
        "type": "Manifest",
        ...
    }
    
    tei_xml = client.convert_from_manifest(manifest_object)
    print("マニフェストオブジェクトからの変換成功!")

    # 結果をファイルに保存
    with open("output.xml", "w", encoding="utf-8") as f:
        f.write(tei_xml)
    
except Exception as e:
    print(f"エラー: {e}")

まとめ

NDL古典籍OCR-Liteを用いたOCRテキストからTEI/XMLファイルを作成する流れについて紹介しました。

今後は、上記のように複数のアプリを介さず、1つのアプリで完結するような仕組みも構築したいと思います。

色々と足りない点がありますが、参考になる部分がありましたら幸いです。

Discussion